FEAT[BE] :add marketing type field to delivery and sales order DTOs, enhance validation and service logic for consistent marketing type handling

This commit is contained in:
aguhh18
2026-02-04 14:47:56 +07:00
parent 357b5709f5
commit 1d95976360
4 changed files with 53 additions and 25 deletions
@@ -2,6 +2,7 @@ package dto
import (
"fmt"
"math"
"sort"
"time"
@@ -79,6 +80,7 @@ type DeliveryMarketingProductDTO struct {
Id uint `json:"id"`
MarketingId uint `json:"marketing_id"`
ProductWarehouseId uint `json:"product_warehouse_id"`
MarketingType string `json:"marketing_type"`
Qty float64 `json:"qty"`
UnitPrice float64 `json:"unit_price"`
AvgWeight float64 `json:"avg_weight"`
@@ -111,7 +113,7 @@ func ToDeliveryMarketingProductDTO(e entity.MarketingProduct, marketingType stri
// Calculate total_peti only for TELUR marketing type
var totalPeti *float64
if marketingType == "TELUR" && e.ConvertionUnit != nil && *e.ConvertionUnit == "PETI" && e.WeightPerConvertion != nil && *e.WeightPerConvertion > 0 {
calculated := e.TotalWeight / *e.WeightPerConvertion
calculated := math.Floor(e.TotalWeight / *e.WeightPerConvertion)
totalPeti = &calculated
}
@@ -119,6 +121,7 @@ func ToDeliveryMarketingProductDTO(e entity.MarketingProduct, marketingType stri
Id: e.Id,
MarketingId: e.MarketingId,
ProductWarehouseId: e.ProductWarehouseId,
MarketingType: marketingType,
Qty: e.Qty,
UnitPrice: e.UnitPrice,
AvgWeight: e.AvgWeight,
@@ -1,6 +1,7 @@
package dto
import (
"math"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -11,6 +12,7 @@ import (
type MarketingProductDTO struct {
Id uint `json:"id"`
MarketingType string `json:"marketing_type"`
Qty float64 `json:"qty"`
UnitPrice float64 `json:"unit_price"`
AvgWeight float64 `json:"avg_weight"`
@@ -44,12 +46,13 @@ func ToMarketingProductDTO(e entity.MarketingProduct, marketingType string) Mark
// Calculate total_peti only for TELUR marketing type
var totalPeti *float64
if marketingType == "TELUR" && e.ConvertionUnit != nil && *e.ConvertionUnit == "PETI" && e.WeightPerConvertion != nil && *e.WeightPerConvertion > 0 {
calculated := e.TotalWeight / *e.WeightPerConvertion
calculated := math.Floor(e.TotalWeight / *e.WeightPerConvertion)
totalPeti = &calculated
}
return MarketingProductDTO{
Id: e.Id,
MarketingType: marketingType,
Qty: e.Qty,
UnitPrice: e.UnitPrice,
AvgWeight: e.AvgWeight,
@@ -104,8 +104,23 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
return nil, err
}
if !utils.IsValidMarketingType(req.MarketingType) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing_type. Must be one of: AYAM, TELUR, TRADING, AYAM_PULLET")
// Validasi semua product harus punya marketing_type yang sama
if len(req.MarketingProducts) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "marketing_products is required")
}
firstMarketingType := req.MarketingProducts[0].MarketingType
if !utils.IsValidMarketingType(firstMarketingType) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Tipe penjualan tidak valid")
}
for i, item := range req.MarketingProducts {
if !utils.IsValidMarketingType(item.MarketingType) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tipe penjualan tidak valid pada produk ke-%d", i+1))
}
if item.MarketingType != firstMarketingType {
return nil, fiber.NewError(fiber.StatusBadRequest, "Semua produk harus memiliki tipe penjualan yang sama")
}
}
actorID, err := m.ActorIDFromContext(c)
@@ -120,11 +135,11 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
}
for _, item := range req.MarketingProducts {
if req.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "avg_weight is required for non-TRADING marketing type")
if item.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Berat rata-rata harus diisi")
}
if item.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid convertion_unit. Must be one of: PETI, KG")
return nil, fiber.NewError(fiber.StatusBadRequest, "Unit konversi tidak valid")
}
if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil {
return nil, err
@@ -160,7 +175,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
SoDate: soDate,
SalesPersonId: req.SalesPersonId,
Notes: req.Notes,
MarketingType: req.MarketingType,
MarketingType: firstMarketingType,
CreatedBy: actorID,
}
if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil {
@@ -173,7 +188,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
if product.ProductWarehouseId != 0 {
pwIDs = append(pwIDs, product.ProductWarehouseId)
}
if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, marketing.MarketingType, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product.MarketingType, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product")
}
}
@@ -218,8 +233,21 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
return nil, err
}
if req.MarketingType != "" && !utils.IsValidMarketingType(req.MarketingType) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing_type. Must be one of: AYAM, TELUR, TRADING, AYAM_PULLET")
// Validasi semua product harus punya marketing_type yang sama
if len(req.MarketingProducts) > 0 {
firstMarketingType := req.MarketingProducts[0].MarketingType
if !utils.IsValidMarketingType(firstMarketingType) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Tipe penjualan tidak valid")
}
for i, item := range req.MarketingProducts {
if !utils.IsValidMarketingType(item.MarketingType) {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tipe penjualan tidak valid pada produk ke-%d", i+1))
}
if item.MarketingType != firstMarketingType {
return nil, fiber.NewError(fiber.StatusBadRequest, "Semua produk harus memiliki tipe penjualan yang sama")
}
}
}
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
@@ -249,11 +277,11 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
if len(req.MarketingProducts) > 0 {
for _, item := range req.MarketingProducts {
if req.MarketingType != "" && req.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "avg_weight is required for non-TRADING marketing type")
if item.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Berat rata-rata harus diisi")
}
if item.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid convertion_unit. Must be one of: PETI, KG")
return nil, fiber.NewError(fiber.StatusBadRequest, "Unit konversi tidak valid")
}
if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil {
return nil, err
@@ -302,8 +330,8 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
if req.Notes != "" {
updateBody["notes"] = req.Notes
}
if req.MarketingType != "" {
updateBody["marketing_type"] = req.MarketingType
if len(req.MarketingProducts) > 0 {
updateBody["marketing_type"] = req.MarketingProducts[0].MarketingType
}
if len(updateBody) > 0 {
@@ -330,15 +358,10 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
reqByPW[rp.ProductWarehouseId] = rp
}
marketing, err := marketingRepoTx.GetByID(c.Context(), id, nil)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing")
}
for _, rp := range req.MarketingProducts {
if old, ok := oldByPW[rp.ProductWarehouseId]; ok {
totalWeight, totalPrice := s.calculatePriceByMarketingType(marketing.MarketingType, rp.Qty, rp.AvgWeight, rp.UnitPrice, rp.Week)
totalWeight, totalPrice := s.calculatePriceByMarketingType(rp.MarketingType, rp.Qty, rp.AvgWeight, rp.UnitPrice, rp.Week)
deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -397,7 +420,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
}
}
} else {
if err := s.createMarketingProductWithDelivery(c.Context(), id, marketing.MarketingType, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
if err := s.createMarketingProductWithDelivery(c.Context(), id, rp.MarketingType, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product")
}
}
@@ -5,11 +5,11 @@ type Create struct {
SalesPersonId uint `json:"sales_person_id" validate:"required,gt=0"`
Date string `json:"date" validate:"required,datetime=2006-01-02"`
Notes string `json:"notes" validate:"omitempty,max=500"`
MarketingType string `json:"marketing_type" validate:"required,min=1,max=50"`
MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"required,min=1,dive"`
}
type CreateMarketingProduct struct {
MarketingType string `json:"marketing_type" validate:"required,min=1,max=50"`
VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"`
ConvertionUnit *string `json:"convertion_unit" validate:"omitempty,min=1,max=20"`
WeightPerConvertion *float64 `json:"weight_per_convertion" validate:"omitempty,gt=0"`
@@ -25,7 +25,6 @@ type Update struct {
SalesPersonId uint `json:"sales_person_id" validate:"omitempty,gt=0"`
Date string `json:"date" validate:"omitempty,datetime=2006-01-02"`
Notes string `json:"notes" validate:"omitempty,max=500"`
MarketingType string `json:"marketing_type" validate:"omitempty,min=1,max=50"`
MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"omitempty,min=1,dive"`
}