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 ( import (
"fmt" "fmt"
"math"
"sort" "sort"
"time" "time"
@@ -79,6 +80,7 @@ type DeliveryMarketingProductDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
MarketingId uint `json:"marketing_id"` MarketingId uint `json:"marketing_id"`
ProductWarehouseId uint `json:"product_warehouse_id"` ProductWarehouseId uint `json:"product_warehouse_id"`
MarketingType string `json:"marketing_type"`
Qty float64 `json:"qty"` Qty float64 `json:"qty"`
UnitPrice float64 `json:"unit_price"` UnitPrice float64 `json:"unit_price"`
AvgWeight float64 `json:"avg_weight"` AvgWeight float64 `json:"avg_weight"`
@@ -111,7 +113,7 @@ func ToDeliveryMarketingProductDTO(e entity.MarketingProduct, marketingType stri
// Calculate total_peti only for TELUR marketing type // Calculate total_peti only for TELUR marketing type
var totalPeti *float64 var totalPeti *float64
if marketingType == "TELUR" && e.ConvertionUnit != nil && *e.ConvertionUnit == "PETI" && e.WeightPerConvertion != nil && *e.WeightPerConvertion > 0 { 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 totalPeti = &calculated
} }
@@ -119,6 +121,7 @@ func ToDeliveryMarketingProductDTO(e entity.MarketingProduct, marketingType stri
Id: e.Id, Id: e.Id,
MarketingId: e.MarketingId, MarketingId: e.MarketingId,
ProductWarehouseId: e.ProductWarehouseId, ProductWarehouseId: e.ProductWarehouseId,
MarketingType: marketingType,
Qty: e.Qty, Qty: e.Qty,
UnitPrice: e.UnitPrice, UnitPrice: e.UnitPrice,
AvgWeight: e.AvgWeight, AvgWeight: e.AvgWeight,
@@ -1,6 +1,7 @@
package dto package dto
import ( import (
"math"
"time" "time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -11,6 +12,7 @@ import (
type MarketingProductDTO struct { type MarketingProductDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
MarketingType string `json:"marketing_type"`
Qty float64 `json:"qty"` Qty float64 `json:"qty"`
UnitPrice float64 `json:"unit_price"` UnitPrice float64 `json:"unit_price"`
AvgWeight float64 `json:"avg_weight"` 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 // Calculate total_peti only for TELUR marketing type
var totalPeti *float64 var totalPeti *float64
if marketingType == "TELUR" && e.ConvertionUnit != nil && *e.ConvertionUnit == "PETI" && e.WeightPerConvertion != nil && *e.WeightPerConvertion > 0 { 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 totalPeti = &calculated
} }
return MarketingProductDTO{ return MarketingProductDTO{
Id: e.Id, Id: e.Id,
MarketingType: marketingType,
Qty: e.Qty, Qty: e.Qty,
UnitPrice: e.UnitPrice, UnitPrice: e.UnitPrice,
AvgWeight: e.AvgWeight, AvgWeight: e.AvgWeight,
@@ -104,8 +104,23 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
return nil, err return nil, err
} }
if !utils.IsValidMarketingType(req.MarketingType) { // Validasi semua product harus punya marketing_type yang sama
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing_type. Must be one of: AYAM, TELUR, TRADING, AYAM_PULLET") 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) 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 { for _, item := range req.MarketingProducts {
if req.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 { if item.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "avg_weight is required for non-TRADING marketing type") return nil, fiber.NewError(fiber.StatusBadRequest, "Berat rata-rata harus diisi")
} }
if item.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) { 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 { if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil {
return nil, err return nil, err
@@ -160,7 +175,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
SoDate: soDate, SoDate: soDate,
SalesPersonId: req.SalesPersonId, SalesPersonId: req.SalesPersonId,
Notes: req.Notes, Notes: req.Notes,
MarketingType: req.MarketingType, MarketingType: firstMarketingType,
CreatedBy: actorID, CreatedBy: actorID,
} }
if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil { 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 { if product.ProductWarehouseId != 0 {
pwIDs = append(pwIDs, product.ProductWarehouseId) 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") 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 return nil, err
} }
if req.MarketingType != "" && !utils.IsValidMarketingType(req.MarketingType) { // Validasi semua product harus punya marketing_type yang sama
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing_type. Must be one of: AYAM, TELUR, TRADING, AYAM_PULLET") 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 { 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 { if len(req.MarketingProducts) > 0 {
for _, item := range req.MarketingProducts { for _, item := range req.MarketingProducts {
if req.MarketingType != "" && req.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 { if item.MarketingType != string(utils.MarketingTypeTrading) && item.AvgWeight == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "avg_weight is required for non-TRADING marketing type") return nil, fiber.NewError(fiber.StatusBadRequest, "Berat rata-rata harus diisi")
} }
if item.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) { 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 { if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil {
return nil, err return nil, err
@@ -302,8 +330,8 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
if req.Notes != "" { if req.Notes != "" {
updateBody["notes"] = req.Notes updateBody["notes"] = req.Notes
} }
if req.MarketingType != "" { if len(req.MarketingProducts) > 0 {
updateBody["marketing_type"] = req.MarketingType updateBody["marketing_type"] = req.MarketingProducts[0].MarketingType
} }
if len(updateBody) > 0 { if len(updateBody) > 0 {
@@ -330,15 +358,10 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
reqByPW[rp.ProductWarehouseId] = rp 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 { for _, rp := range req.MarketingProducts {
if old, ok := oldByPW[rp.ProductWarehouseId]; ok { 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) deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { 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 { } 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") 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"` SalesPersonId uint `json:"sales_person_id" validate:"required,gt=0"`
Date string `json:"date" validate:"required,datetime=2006-01-02"` Date string `json:"date" validate:"required,datetime=2006-01-02"`
Notes string `json:"notes" validate:"omitempty,max=500"` 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"` MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"required,min=1,dive"`
} }
type CreateMarketingProduct struct { 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"` VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"`
ConvertionUnit *string `json:"convertion_unit" validate:"omitempty,min=1,max=20"` ConvertionUnit *string `json:"convertion_unit" validate:"omitempty,min=1,max=20"`
WeightPerConvertion *float64 `json:"weight_per_convertion" validate:"omitempty,gt=0"` 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"` SalesPersonId uint `json:"sales_person_id" validate:"omitempty,gt=0"`
Date string `json:"date" validate:"omitempty,datetime=2006-01-02"` Date string `json:"date" validate:"omitempty,datetime=2006-01-02"`
Notes string `json:"notes" validate:"omitempty,max=500"` 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"` MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"omitempty,min=1,dive"`
} }