From 1d9597636013ef7dad7b2355fed641df9001fcea Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 4 Feb 2026 14:47:56 +0700 Subject: [PATCH] FEAT[BE] :add marketing type field to delivery and sales order DTOs, enhance validation and service logic for consistent marketing type handling --- .../marketing/dto/deliveryorder.dto.go | 5 +- .../modules/marketing/dto/salesorder.dto.go | 5 +- .../marketing/services/salesorder.service.go | 65 +++++++++++++------ .../validations/salesorder.validation.go | 3 +- 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/internal/modules/marketing/dto/deliveryorder.dto.go b/internal/modules/marketing/dto/deliveryorder.dto.go index 451856c2..bd4b2a0b 100644 --- a/internal/modules/marketing/dto/deliveryorder.dto.go +++ b/internal/modules/marketing/dto/deliveryorder.dto.go @@ -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, diff --git a/internal/modules/marketing/dto/salesorder.dto.go b/internal/modules/marketing/dto/salesorder.dto.go index 866fe268..11479036 100644 --- a/internal/modules/marketing/dto/salesorder.dto.go +++ b/internal/modules/marketing/dto/salesorder.dto.go @@ -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, diff --git a/internal/modules/marketing/services/salesorder.service.go b/internal/modules/marketing/services/salesorder.service.go index ffc53d79..58901794 100644 --- a/internal/modules/marketing/services/salesorder.service.go +++ b/internal/modules/marketing/services/salesorder.service.go @@ -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") } } diff --git a/internal/modules/marketing/validations/salesorder.validation.go b/internal/modules/marketing/validations/salesorder.validation.go index bf38417f..6d6b80b6 100644 --- a/internal/modules/marketing/validations/salesorder.validation.go +++ b/internal/modules/marketing/validations/salesorder.validation.go @@ -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"` }