From f59cdd821ab9d71d2f2224be5e0e41395db4209c Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 3 Feb 2026 13:32:37 +0700 Subject: [PATCH 1/6] FEAT[BE] :add marketing type and conversion fields to marketing entities and services --- ...458_create_transfer_laying_sequence.up.sql | 2 +- ...203054206_update_marketing_tables.down.sql | 8 ++ ...60203054206_update_marketing_tables.up.sql | 9 ++ internal/entities/marketing.go | 1 + internal/entities/marketing_product.go | 19 +-- .../services/deliveryorder.service.go | 40 +++---- .../marketing/services/salesorder.service.go | 108 ++++++++---------- .../validations/salesorder.validation.go | 15 ++- internal/utils/constant.go | 40 +++++++ 9 files changed, 141 insertions(+), 101 deletions(-) create mode 100644 internal/database/migrations/20260203054206_update_marketing_tables.down.sql create mode 100644 internal/database/migrations/20260203054206_update_marketing_tables.up.sql diff --git a/internal/database/migrations/20260129083458_create_transfer_laying_sequence.up.sql b/internal/database/migrations/20260129083458_create_transfer_laying_sequence.up.sql index f5f5bdf7..1a48a512 100644 --- a/internal/database/migrations/20260129083458_create_transfer_laying_sequence.up.sql +++ b/internal/database/migrations/20260129083458_create_transfer_laying_sequence.up.sql @@ -1,5 +1,5 @@ -- Create sequence for transfer laying movement number -CREATE SEQUENCE transfer_laying_seq START +CREATE SEQUENCE IF NOT EXISTS transfer_laying_seq START WITH 1 INCREMENT BY 1 MINVALUE 1 MAXVALUE 99999 NO CYCLE; diff --git a/internal/database/migrations/20260203054206_update_marketing_tables.down.sql b/internal/database/migrations/20260203054206_update_marketing_tables.down.sql new file mode 100644 index 00000000..b498f23e --- /dev/null +++ b/internal/database/migrations/20260203054206_update_marketing_tables.down.sql @@ -0,0 +1,8 @@ +-- Remove columns from marketing_products +ALTER TABLE marketing_products +DROP COLUMN IF EXISTS week, +DROP COLUMN IF EXISTS weight_per_convertion, +DROP COLUMN IF EXISTS convertion_unit; + +-- Remove column from marketings +ALTER TABLE marketings DROP COLUMN IF EXISTS marketing_type; \ No newline at end of file diff --git a/internal/database/migrations/20260203054206_update_marketing_tables.up.sql b/internal/database/migrations/20260203054206_update_marketing_tables.up.sql new file mode 100644 index 00000000..72f7c8e7 --- /dev/null +++ b/internal/database/migrations/20260203054206_update_marketing_tables.up.sql @@ -0,0 +1,9 @@ +-- Add marketing_type to marketings table +ALTER TABLE marketings +ADD COLUMN IF NOT EXISTS marketing_type VARCHAR(50); + +-- Add convertion fields to marketing_products table +ALTER TABLE marketing_products +ADD COLUMN IF NOT EXISTS convertion_unit VARCHAR(20), +ADD COLUMN IF NOT EXISTS weight_per_convertion NUMERIC(15, 3), +ADD COLUMN IF NOT EXISTS week INTEGER; \ No newline at end of file diff --git a/internal/entities/marketing.go b/internal/entities/marketing.go index c9ff7624..c1ca293b 100644 --- a/internal/entities/marketing.go +++ b/internal/entities/marketing.go @@ -14,6 +14,7 @@ type Marketing struct { SoDate time.Time `gorm:"type:date;not null"` SalesPersonId uint `gorm:"not null"` Notes string `gorm:"type:text"` + MarketingType string `gorm:"type:varchar(50)"` CreatedBy uint `gorm:"not null"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` diff --git a/internal/entities/marketing_product.go b/internal/entities/marketing_product.go index f2294f10..ce13d3d8 100644 --- a/internal/entities/marketing_product.go +++ b/internal/entities/marketing_product.go @@ -1,14 +1,17 @@ package entities type MarketingProduct struct { - Id uint `gorm:"primaryKey;autoIncrement"` - MarketingId uint `gorm:"not null"` - ProductWarehouseId uint `gorm:"not null"` - Qty float64 `gorm:"type:numeric(15,3);not null"` - UnitPrice float64 `gorm:"type:numeric(15,3);not null"` - AvgWeight float64 `gorm:"type:numeric(15,3);not null"` - TotalWeight float64 `gorm:"type:numeric(15,3);not null"` - TotalPrice float64 `gorm:"type:numeric(15,3);not null"` + Id uint `gorm:"primaryKey;autoIncrement"` + MarketingId uint `gorm:"not null"` + ProductWarehouseId uint `gorm:"not null"` + Qty float64 `gorm:"type:numeric(15,3);not null"` + ConvertionUnit *string `gorm:"type:varchar(20)"` + WeightPerConvertion *float64 `gorm:"type:numeric(15,3)"` + Week *int `gorm:"type:integer"` + UnitPrice float64 `gorm:"type:numeric(15,3);not null"` + AvgWeight float64 `gorm:"type:numeric(15,3);not null"` + TotalWeight float64 `gorm:"type:numeric(15,3);not null"` + TotalPrice float64 `gorm:"type:numeric(15,3);not null"` Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index 6d9392a6..80045027 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -237,6 +237,12 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + marketingRepoTx := marketingRepo.NewMarketingRepository(dbTransaction) + + marketing, err := marketingRepoTx.GetByID(c.Context(), req.MarketingId, nil) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") + } allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId) if err != nil { @@ -283,23 +289,11 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery itemDeliveryDate = &parsedDate } - isPakanOrOVK := false - if foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 { - for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags { - if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) { - isPakanOrOVK = true - break - } - } - } - totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight var totalPrice float64 - if isPakanOrOVK { - + if marketing.MarketingType == string(utils.MarketingTypeTrading) { totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice } else { - totalPrice = totalWeight * requestedProduct.UnitPrice } @@ -374,6 +368,12 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction) + marketingRepoTx := marketingRepo.NewMarketingRepository(dbTransaction) + + marketing, err := marketingRepoTx.GetByID(c.Context(), id, nil) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") + } allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { @@ -421,23 +421,11 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO itemDeliveryDate = deliveryProduct.DeliveryDate } - isPakanOrOVK := false - if foundMarketingProduct.ProductWarehouse.Id != 0 && foundMarketingProduct.ProductWarehouse.Product.Id != 0 && len(foundMarketingProduct.ProductWarehouse.Product.Flags) > 0 { - for _, flag := range foundMarketingProduct.ProductWarehouse.Product.Flags { - if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) { - isPakanOrOVK = true - break - } - } - } - totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight var totalPrice float64 - if isPakanOrOVK { - + if marketing.MarketingType == string(utils.MarketingTypeTrading) { totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice } else { - totalPrice = totalWeight * requestedProduct.UnitPrice } diff --git a/internal/modules/marketing/services/salesorder.service.go b/internal/modules/marketing/services/salesorder.service.go index df75fe82..a43370d5 100644 --- a/internal/modules/marketing/services/salesorder.service.go +++ b/internal/modules/marketing/services/salesorder.service.go @@ -103,6 +103,10 @@ 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") + } + actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err @@ -115,6 +119,9 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e } for _, item := range req.MarketingProducts { + if item.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid convertion_unit. Must be one of: PETI, KG") + } if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { return nil, err } @@ -149,6 +156,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e SoDate: soDate, SalesPersonId: req.SalesPersonId, Notes: req.Notes, + MarketingType: req.MarketingType, CreatedBy: actorID, } if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil { @@ -161,10 +169,9 @@ 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, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil { + if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, marketing.MarketingType, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") } - } if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil { return err @@ -207,6 +214,10 @@ 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") + } + if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { return nil, err } @@ -234,6 +245,9 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u if len(req.MarketingProducts) > 0 { for _, item := range req.MarketingProducts { + if item.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid convertion_unit. Must be one of: PETI, KG") + } if err := m.EnsureProductWarehouseAccess(c, s.MarketingRepo.DB(), item.ProductWarehouseId); err != nil { return nil, err } @@ -281,6 +295,9 @@ 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(updateBody) > 0 { if err := marketingRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil { @@ -306,31 +323,17 @@ 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 { - // Get product untuk cek flag PAKAN atau OVK - productWarehouse, err := s.ProductWarehouseRepo.GetByID(c.Context(), rp.ProductWarehouseId, func(db *gorm.DB) *gorm.DB { - return db.Preload("Product.Flags") - }) - if err != nil { - return err - } - - // Cek apakah product punya flag PAKAN atau OVK - isPakanOrOVK := false - if productWarehouse.Product.Id != 0 && len(productWarehouse.Product.Flags) > 0 { - for _, flag := range productWarehouse.Product.Flags { - if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) { - isPakanOrOVK = true - break - } - } - } - totalWeight := rp.Qty * rp.AvgWeight var totalPrice float64 - if isPakanOrOVK { + if marketing.MarketingType == string(utils.MarketingTypeTrading) { totalPrice = rp.Qty * rp.UnitPrice } else { totalPrice = totalWeight * rp.UnitPrice @@ -340,7 +343,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product") } - if err == nil && deliveryProduct.Id != 0 { oldQty := old.Qty newQty := rp.Qty @@ -363,12 +365,15 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } updateBody := map[string]any{ - "product_warehouse_id": rp.ProductWarehouseId, - "qty": rp.Qty, - "unit_price": rp.UnitPrice, - "avg_weight": rp.AvgWeight, - "total_weight": totalWeight, - "total_price": totalPrice, + "product_warehouse_id": rp.ProductWarehouseId, + "qty": rp.Qty, + "unit_price": rp.UnitPrice, + "avg_weight": rp.AvgWeight, + "total_weight": totalWeight, + "total_price": totalPrice, + "convertion_unit": rp.ConvertionUnit, + "weight_per_convertion": rp.WeightPerConvertion, + "week": rp.Week, } if err := marketingProductRepoTx.PatchOne(c.Context(), old.Id, updateBody, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product") @@ -391,7 +396,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } } } else { - if err := s.createMarketingProductWithDelivery(c.Context(), id, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil { + if err := s.createMarketingProductWithDelivery(c.Context(), id, marketing.MarketingType, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") } } @@ -399,7 +404,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u for _, old := range oldProducts { if _, ok := reqByPW[old.ProductWarehouseId]; !ok { - deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product") @@ -682,45 +686,27 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e return updated, nil } -func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error { - - // Get product untuk cek flag PAKAN atau OVK - productWarehouse, err := s.ProductWarehouseRepo.GetByID(ctx, rp.ProductWarehouseId, func(db *gorm.DB) *gorm.DB { - return db.Preload("Product.Flags") - }) - if err != nil { - return err - } - - // Cek apakah product punya flag PAKAN atau OVK - isPakanOrOVK := false - if productWarehouse.Product.Id != 0 && len(productWarehouse.Product.Flags) > 0 { - for _, flag := range productWarehouse.Product.Flags { - if flag.Name == string(utils.FlagPakan) || flag.Name == string(utils.FlagOVK) { - isPakanOrOVK = true - break - } - } - } +func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, marketingType string, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error { totalWeight := rp.Qty * rp.AvgWeight var totalPrice float64 - if isPakanOrOVK { - // PAKAN atau OVK: qty × unit_price + if marketingType == string(utils.MarketingTypeTrading) { totalPrice = rp.Qty * rp.UnitPrice } else { - // Produk lain: total_weight × unit_price totalPrice = totalWeight * rp.UnitPrice } marketingProduct := &entity.MarketingProduct{ - MarketingId: marketingId, - ProductWarehouseId: rp.ProductWarehouseId, - Qty: rp.Qty, - UnitPrice: rp.UnitPrice, - AvgWeight: rp.AvgWeight, - TotalWeight: totalWeight, - TotalPrice: totalPrice, + MarketingId: marketingId, + ProductWarehouseId: rp.ProductWarehouseId, + Qty: rp.Qty, + UnitPrice: rp.UnitPrice, + AvgWeight: rp.AvgWeight, + TotalWeight: totalWeight, + TotalPrice: totalPrice, + ConvertionUnit: rp.ConvertionUnit, + WeightPerConvertion: rp.WeightPerConvertion, + Week: rp.Week, } if err := marketingProductRepo.CreateOne(ctx, marketingProduct, nil); err != nil { return err diff --git a/internal/modules/marketing/validations/salesorder.validation.go b/internal/modules/marketing/validations/salesorder.validation.go index b69da394..9a3cee29 100644 --- a/internal/modules/marketing/validations/salesorder.validation.go +++ b/internal/modules/marketing/validations/salesorder.validation.go @@ -5,15 +5,19 @@ 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 { - VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"` - ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"` - UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` - Qty float64 `json:"qty" validate:"required,gt=0"` - AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"` + 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"` + Week *int `json:"week" validate:"omitempty,gt=0"` + ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"` + UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` + Qty float64 `json:"qty" validate:"required,gt=0"` + AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"` } type Update struct { @@ -21,6 +25,7 @@ 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"` } diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 9abd6a30..cb8a0ba2 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -212,6 +212,30 @@ const ( KandangStatusActive KandangStatus = "ACTIVE" ) +// ------------------------------------------------------------------- +// Marketing Type +// ------------------------------------------------------------------- + +type MarketingType string + +const ( + MarketingTypeAyam MarketingType = "AYAM" + MarketingTypeTelur MarketingType = "TELUR" + MarketingTypeTrading MarketingType = "TRADING" + MarketingTypeAyamPullet MarketingType = "AYAM PULLET" +) + +// ------------------------------------------------------------------- +// Convertion Unit +// ------------------------------------------------------------------- + +type ConvertionUnit string + +const ( + ConvertionUnitPeti ConvertionUnit = "PETI" + ConvertionUnitKG ConvertionUnit = "KG" +) + // ------------------------------------------------------------------- // ProjectFlockCategory // ------------------------------------------------------------------- @@ -609,6 +633,22 @@ func IsValidPaymentParty(v string) bool { return false } +func IsValidMarketingType(v string) bool { + switch MarketingType(v) { + case MarketingTypeAyam, MarketingTypeTelur, MarketingTypeTrading, MarketingTypeAyamPullet: + return true + } + return false +} + +func IsValidConvertionUnit(v string) bool { + switch ConvertionUnit(v) { + case ConvertionUnitPeti, ConvertionUnitKG: + return true + } + return false +} + // example use // Recording helper From 90de167fcd700eacc46ca18780748a8c1c270b9f Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 4 Feb 2026 09:59:15 +0700 Subject: [PATCH 2/6] FEAT[BE] :add type filtering and validation to product warehouse services --- .../product_warehouse.controller.go | 1 + .../product_warehouse.repository.go | 7 ++-- .../services/product_warehouse.service.go | 23 ++++++++++- .../product_warehouse.validation.go | 1 + .../services/deliveryorder.service.go | 30 +++++++------- .../marketing/services/salesorder.service.go | 40 +++++++++++-------- .../validations/salesorder.validation.go | 2 +- internal/utils/constant.go | 2 +- 8 files changed, 70 insertions(+), 36 deletions(-) diff --git a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go index 47d85a65..bc6cdaed 100644 --- a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go +++ b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go @@ -32,6 +32,7 @@ func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error { Flags: c.Query("flags", ""), KandangId: uint(c.QueryInt("kandang_id", 0)), TransferContext: c.Query(utils.TransferContextKey, ""), + Type: c.Query("type", ""), } if query.Page < 1 || query.Limit < 1 { diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go index a7fe452b..e49fc421 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -168,9 +168,10 @@ func (r *ProductWarehouseRepositoryImpl) ApplyFlagsFilter(db *gorm.DB, flags []s } return db. - Joins("JOIN products ON products.id = product_warehouses.product_id"). - Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products"). - Where("flags.name IN ?", flags) + Joins("JOIN products p_flag ON p_flag.id = product_warehouses.product_id"). + Joins("JOIN flags f_flag ON f_flag.flagable_id = p_flag.id AND f_flag.flagable_type = ?", "products"). + Where("f_flag.name IN ?", flags). + Distinct() } func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error { diff --git a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go index 5bb3f692..98656de1 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -99,6 +99,12 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) offset := (params.Page - 1) * params.Limit + if params.Type != "" { + if !utils.IsValidMarketingType(params.Type) { + return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing type") + } + } + cleanFlags := utils.ParseFlags(params.Flags) productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { @@ -128,7 +134,22 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) db = db.Where("warehouse_id = ?", params.WarehouseId) } - db = s.Repository.ApplyFlagsFilter(db, cleanFlags) + if params.Type != "" { + switch params.Type { + case string(utils.MarketingTypeAyamPullet): + db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagDOC), string(utils.FlagPullet), string(utils.FlagLayer)}) + case string(utils.MarketingTypeAyam): + db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagAyamAfkir), string(utils.FlagAyamCulling), string(utils.FlagAyamMati)}) + case string(utils.MarketingTypeTelur): + db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagTelur), string(utils.FlagTelurUtuh), string(utils.FlagTelurPecah), string(utils.FlagTelurPutih), string(utils.FlagTelurRetak)}) + case string(utils.MarketingTypeTrading): + db = s.Repository.ApplyFlagsFilter(db, []string{string(utils.FlagPakan), string(utils.FlagPreStarter), string(utils.FlagStarter), string(utils.FlagFinisher), string(utils.FlagOVK), string(utils.FlagObat), string(utils.FlagVitamin), string(utils.FlagKimia), string(utils.FlagEkspedisi)}) + } + } + + if len(cleanFlags) > 0 { + db = s.Repository.ApplyFlagsFilter(db, cleanFlags) + } return db.Order("product_warehouses.id DESC") }) diff --git a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go index 61a41ad0..7e7da7a6 100644 --- a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go +++ b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go @@ -20,4 +20,5 @@ type Query struct { Flags string `query:"flags" validate:"omitempty"` KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"` TransferContext string `query:"transfer_context" validate:"omitempty,oneof=inventory_transfer"` + Type string `query:"type" validate:"omitempty,oneof=AYAM TELUR TRADING AYAM_PULLET"` } diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index 80045027..268a81eb 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -289,13 +289,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery itemDeliveryDate = &parsedDate } - totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight - var totalPrice float64 - if marketing.MarketingType == string(utils.MarketingTypeTrading) { - totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice - } else { - totalPrice = totalWeight * requestedProduct.UnitPrice - } + totalWeight, totalPrice := s.calculatePriceByMarketingType(marketing.MarketingType, requestedProduct.Qty, requestedProduct.AvgWeight, requestedProduct.UnitPrice, foundMarketingProduct.Week) deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.UnitPrice = requestedProduct.UnitPrice @@ -421,13 +415,7 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO itemDeliveryDate = deliveryProduct.DeliveryDate } - totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight - var totalPrice float64 - if marketing.MarketingType == string(utils.MarketingTypeTrading) { - totalPrice = requestedProduct.Qty * requestedProduct.UnitPrice - } else { - totalPrice = totalWeight * requestedProduct.UnitPrice - } + totalWeight, totalPrice := s.calculatePriceByMarketingType(marketing.MarketingType, requestedProduct.Qty, requestedProduct.AvgWeight, requestedProduct.UnitPrice, foundMarketingProduct.Week) deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.UnitPrice = requestedProduct.UnitPrice @@ -471,6 +459,20 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO return s.getMarketingWithDeliveries(c, id) } +func (s *deliveryOrdersService) calculatePriceByMarketingType(marketingType string, qty, avgWeight, unitPrice float64, week *int) (totalWeight, totalPrice float64) { + if marketingType == string(utils.MarketingTypeTrading) { + totalWeight = 0 + totalPrice = qty * unitPrice + } else if marketingType == string(utils.MarketingTypeAyamPullet) && week != nil && *week > 0 { + totalWeight = qty * avgWeight + totalPrice = unitPrice * float64(*week) * qty + } else { + totalWeight = qty * avgWeight + totalPrice = totalWeight * unitPrice + } + return totalWeight, totalPrice +} + func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct, requestedQty float64, actorID uint) error { if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 { return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") diff --git a/internal/modules/marketing/services/salesorder.service.go b/internal/modules/marketing/services/salesorder.service.go index a43370d5..a64caa9f 100644 --- a/internal/modules/marketing/services/salesorder.service.go +++ b/internal/modules/marketing/services/salesorder.service.go @@ -104,7 +104,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e } if !utils.IsValidMarketingType(req.MarketingType) { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing_type. Must be one of: AYAM, TELUR, TRADING, AYAM PULLET") + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing_type. Must be one of: AYAM, TELUR, TRADING, AYAM_PULLET") } actorID, err := m.ActorIDFromContext(c) @@ -119,6 +119,9 @@ 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.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid convertion_unit. Must be one of: PETI, KG") } @@ -215,7 +218,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } 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") + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid marketing_type. Must be one of: AYAM, TELUR, TRADING, AYAM_PULLET") } if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { @@ -245,6 +248,9 @@ 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.ConvertionUnit != nil && !utils.IsValidConvertionUnit(*item.ConvertionUnit) { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid convertion_unit. Must be one of: PETI, KG") } @@ -331,13 +337,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u for _, rp := range req.MarketingProducts { if old, ok := oldByPW[rp.ProductWarehouseId]; ok { - totalWeight := rp.Qty * rp.AvgWeight - var totalPrice float64 - if marketing.MarketingType == string(utils.MarketingTypeTrading) { - totalPrice = rp.Qty * rp.UnitPrice - } else { - totalPrice = totalWeight * rp.UnitPrice - } + totalWeight, totalPrice := s.calculatePriceByMarketingType(marketing.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) { @@ -688,13 +688,7 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, marketingType string, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error { - totalWeight := rp.Qty * rp.AvgWeight - var totalPrice float64 - if marketingType == string(utils.MarketingTypeTrading) { - totalPrice = rp.Qty * rp.UnitPrice - } else { - totalPrice = totalWeight * rp.UnitPrice - } + totalWeight, totalPrice := s.calculatePriceByMarketingType(marketingType, rp.Qty, rp.AvgWeight, rp.UnitPrice, rp.Week) marketingProduct := &entity.MarketingProduct{ MarketingId: marketingId, @@ -730,3 +724,17 @@ func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Cont return nil } + +func (s *salesOrdersService) calculatePriceByMarketingType(marketingType string, qty, avgWeight, unitPrice float64, week *int) (totalWeight, totalPrice float64) { + if marketingType == string(utils.MarketingTypeTrading) { + totalWeight = 0 + totalPrice = qty * unitPrice + } else if marketingType == string(utils.MarketingTypeAyamPullet) && week != nil && *week > 0 { + totalWeight = qty * avgWeight + totalPrice = unitPrice * float64(*week) * qty + } else { + totalWeight = qty * avgWeight + totalPrice = totalWeight * unitPrice + } + return totalWeight, totalPrice +} diff --git a/internal/modules/marketing/validations/salesorder.validation.go b/internal/modules/marketing/validations/salesorder.validation.go index 9a3cee29..bf38417f 100644 --- a/internal/modules/marketing/validations/salesorder.validation.go +++ b/internal/modules/marketing/validations/salesorder.validation.go @@ -17,7 +17,7 @@ type CreateMarketingProduct struct { ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"` UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` Qty float64 `json:"qty" validate:"required,gt=0"` - AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"` + AvgWeight float64 `json:"avg_weight" validate:"omitempty,gt=0"` } type Update struct { diff --git a/internal/utils/constant.go b/internal/utils/constant.go index cb8a0ba2..1de04fa3 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -222,7 +222,7 @@ const ( MarketingTypeAyam MarketingType = "AYAM" MarketingTypeTelur MarketingType = "TELUR" MarketingTypeTrading MarketingType = "TRADING" - MarketingTypeAyamPullet MarketingType = "AYAM PULLET" + MarketingTypeAyamPullet MarketingType = "AYAM_PULLET" ) // ------------------------------------------------------------------- From 474c42770b507e514631bd32e606701d34783be5 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 4 Feb 2026 11:46:32 +0700 Subject: [PATCH 3/6] FEAT[BE] :add week calculation and chickin preload to product warehouse services --- .../dto/product_warehouse.dto.go | 73 +++++++++++++++++++ .../services/product_warehouse.service.go | 3 +- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go index b8f51c52..b9c95004 100644 --- a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go +++ b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go @@ -6,6 +6,7 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" + "gitlab.com/mbugroup/lti-api.git/internal/utils" ) // === DTO Structs === @@ -22,6 +23,7 @@ type ProductWarehouseListDTO struct { Product *productDTO.ProductRelationDTO `json:"product,omitempty"` Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` ProjectFlockKandang *ProjectFlockKandangRelationDTO `json:"project_flock_kandang,omitempty"` + Week int `json:"week"` CreatedUser *UserRelationDTO `json:"created_user,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -109,6 +111,22 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT } dto.ProjectFlockKandang = pfkDTO + + // Calculate week for AYAM_PULLET/AYAM products + productFlags := make([]string, len(e.Product.Flags)) + for i, f := range e.Product.Flags { + productFlags[i] = f.Name + } + + var category string + if e.ProjectFlockKandang.ProjectFlock.Id != 0 { + category = e.ProjectFlockKandang.ProjectFlock.Category + } + + now := time.Now() + _, ageInWeeks := calculateAgeFromChickin(e.ProjectFlockKandang, &now, productFlags, category) + + dto.Week = ageInWeeks } return dto @@ -138,3 +156,58 @@ func ToProductWarehouseNestedDTO(e entity.ProductWarehouse) ProductWarehousNeste Warehouse: &warehouse, } } + +// Helper function to calculate age from chickin (same logic as closingMarketing.dto.go) +func calculateAgeFromChickin(projectFlockKandang *entity.ProjectFlockKandang, currentDate *time.Time, productFlags []string, category string) (int, int) { + if projectFlockKandang == nil || currentDate == nil || len(projectFlockKandang.Chickins) == 0 { + return 0, 0 + } + + // Return 0 for TRADING, TELUR, and AYAM flags (only AYAM_PULLET should have week) + for _, flag := range productFlags { + if flag == string(utils.FlagOVK) || + flag == string(utils.FlagPakan) || + flag == string(utils.FlagPreStarter) || + flag == string(utils.FlagStarter) || + flag == string(utils.FlagFinisher) || + flag == string(utils.FlagObat) || + flag == string(utils.FlagVitamin) || + flag == string(utils.FlagKimia) || + flag == string(utils.FlagEkspedisi) || + flag == string(utils.FlagTelur) || + flag == string(utils.FlagTelurUtuh) || + flag == string(utils.FlagTelurPecah) || + flag == string(utils.FlagTelurPutih) || + flag == string(utils.FlagTelurRetak) || + flag == string(utils.FlagAyamAfkir) || + flag == string(utils.FlagAyamCulling) || + flag == string(utils.FlagAyamMati) { + return 0, 0 + } + } + + // Find earliest chickin date + earliestChickinDate := projectFlockKandang.Chickins[0].ChickInDate + for _, chickin := range projectFlockKandang.Chickins { + if chickin.ChickInDate.Before(earliestChickinDate) { + earliestChickinDate = chickin.ChickInDate + } + } + + diff := currentDate.Sub(earliestChickinDate) + ageInDays := int(diff.Hours() / 24) + + var ageInWeeks int + if ageInDays <= 0 { + ageInWeeks = 0 + } else { + if category == string(utils.ProjectFlockCategoryLaying) { + ageInDays = ageInDays + 119 + ageInWeeks = ((ageInDays - 1) / 7) + 1 + } else { + ageInWeeks = ((ageInDays - 1) / 7) + 1 + } + } + + return ageInDays, ageInWeeks +} diff --git a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go index 98656de1..7132644e 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -46,7 +46,8 @@ func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB { Preload("Warehouse.Area"). Preload("Warehouse.Kandang"). Preload("ProjectFlockKandang"). - Preload("ProjectFlockKandang.ProjectFlock") + Preload("ProjectFlockKandang.ProjectFlock"). + Preload("ProjectFlockKandang.Chickins") } func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) { From 357b5709f50c9142c8809eab1e79e337e08eda97 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 4 Feb 2026 12:48:05 +0700 Subject: [PATCH 4/6] FEAT[BE] :add conversion fields and week tracking to marketing product DTOs and update mapping functions --- .../marketing/dto/deliveryorder.dto.go | 61 ++++++++++++------- .../modules/marketing/dto/salesorder.dto.go | 49 +++++++++------ .../services/deliveryorder.service.go | 2 + .../marketing/services/salesorder.service.go | 1 + 4 files changed, 73 insertions(+), 40 deletions(-) diff --git a/internal/modules/marketing/dto/deliveryorder.dto.go b/internal/modules/marketing/dto/deliveryorder.dto.go index 4bcbacca..451856c2 100644 --- a/internal/modules/marketing/dto/deliveryorder.dto.go +++ b/internal/modules/marketing/dto/deliveryorder.dto.go @@ -76,16 +76,20 @@ type DeliveryGroupDTO struct { } type DeliveryMarketingProductDTO struct { - Id uint `json:"id"` - MarketingId uint `json:"marketing_id"` - ProductWarehouseId uint `json:"product_warehouse_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - AvgWeight float64 `json:"avg_weight"` - TotalWeight float64 `json:"total_weight"` - TotalPrice float64 `json:"total_price"` - ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` - VehicleNumber string `json:"vehicle_number,omitempty"` + Id uint `json:"id"` + MarketingId uint `json:"marketing_id"` + ProductWarehouseId uint `json:"product_warehouse_id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + AvgWeight float64 `json:"avg_weight"` + TotalWeight float64 `json:"total_weight"` + TotalPrice float64 `json:"total_price"` + ConvertionUnit *string `json:"convertion_unit,omitempty"` + WeightPerConvertion *float64 `json:"weight_per_convertion,omitempty"` + TotalPeti *float64 `json:"total_peti,omitempty"` + Week *int `json:"week,omitempty"` + ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` + VehicleNumber string `json:"vehicle_number,omitempty"` } func ToMarketingRelationDTO(marketing *entity.Marketing) MarketingRelationDTO { @@ -97,24 +101,35 @@ func ToMarketingRelationDTO(marketing *entity.Marketing) MarketingRelationDTO { } } -func ToDeliveryMarketingProductDTO(e entity.MarketingProduct) DeliveryMarketingProductDTO { +func ToDeliveryMarketingProductDTO(e entity.MarketingProduct, marketingType string) DeliveryMarketingProductDTO { var productWarehouse *productwarehouseDTO.ProductWarehousNestedDTO if e.ProductWarehouse.Id != 0 { mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse) productWarehouse = &mapped } + // 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 + totalPeti = &calculated + } + return DeliveryMarketingProductDTO{ - Id: e.Id, - MarketingId: e.MarketingId, - ProductWarehouseId: e.ProductWarehouseId, - Qty: e.Qty, - UnitPrice: e.UnitPrice, - AvgWeight: e.AvgWeight, - TotalWeight: e.TotalWeight, - TotalPrice: e.TotalPrice, - ProductWarehouse: productWarehouse, - VehicleNumber: getVehicleNumber(e), + Id: e.Id, + MarketingId: e.MarketingId, + ProductWarehouseId: e.ProductWarehouseId, + Qty: e.Qty, + UnitPrice: e.UnitPrice, + AvgWeight: e.AvgWeight, + TotalWeight: e.TotalWeight, + TotalPrice: e.TotalPrice, + ConvertionUnit: e.ConvertionUnit, + WeightPerConvertion: e.WeightPerConvertion, + TotalPeti: totalPeti, + Week: e.Week, + ProductWarehouse: productWarehouse, + VehicleNumber: getVehicleNumber(e), } } @@ -161,7 +176,7 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M if len(marketing.Products) > 0 { salesOrderProducts = make([]DeliveryMarketingProductDTO, len(marketing.Products)) for i, product := range marketing.Products { - salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product) + salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product, marketing.MarketingType) } } @@ -201,7 +216,7 @@ func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity if len(marketing.Products) > 0 { salesOrderProducts = make([]DeliveryMarketingProductDTO, len(marketing.Products)) for i, product := range marketing.Products { - salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product) + salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product, marketing.MarketingType) } } diff --git a/internal/modules/marketing/dto/salesorder.dto.go b/internal/modules/marketing/dto/salesorder.dto.go index 86bd5f84..866fe268 100644 --- a/internal/modules/marketing/dto/salesorder.dto.go +++ b/internal/modules/marketing/dto/salesorder.dto.go @@ -10,13 +10,17 @@ import ( // === DTO Structs === type MarketingProductDTO struct { - Id uint `json:"id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - AvgWeight float64 `json:"avg_weight"` - TotalWeight float64 `json:"total_weight"` - TotalPrice float64 `json:"total_price"` - ProductWarehouse *productWarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` + Id uint `json:"id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + AvgWeight float64 `json:"avg_weight"` + TotalWeight float64 `json:"total_weight"` + TotalPrice float64 `json:"total_price"` + ConvertionUnit *string `json:"convertion_unit,omitempty"` + WeightPerConvertion *float64 `json:"weight_per_convertion,omitempty"` + TotalPeti *float64 `json:"total_peti,omitempty"` + Week *int `json:"week,omitempty"` + ProductWarehouse *productWarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` } type SalesOrdersListDTO struct { @@ -29,7 +33,7 @@ type SalesOrdersListDTO struct { // === Mapper Functions === -func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { +func ToMarketingProductDTO(e entity.MarketingProduct, marketingType string) MarketingProductDTO { var productWarehouse *productWarehouseDTO.ProductWarehousNestedDTO if e.ProductWarehouse.Id != 0 { @@ -37,21 +41,32 @@ func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { productWarehouse = &mapped } + // 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 + totalPeti = &calculated + } + return MarketingProductDTO{ - Id: e.Id, - Qty: e.Qty, - UnitPrice: e.UnitPrice, - AvgWeight: e.AvgWeight, - TotalWeight: e.TotalWeight, - TotalPrice: e.TotalPrice, - ProductWarehouse: productWarehouse, + Id: e.Id, + Qty: e.Qty, + UnitPrice: e.UnitPrice, + AvgWeight: e.AvgWeight, + TotalWeight: e.TotalWeight, + TotalPrice: e.TotalPrice, + ConvertionUnit: e.ConvertionUnit, + WeightPerConvertion: e.WeightPerConvertion, + TotalPeti: totalPeti, + Week: e.Week, + ProductWarehouse: productWarehouse, } } func ToSalesOrdersListDTO(e entity.Marketing) SalesOrdersListDTO { products := make([]MarketingProductDTO, len(e.Products)) for i, p := range e.Products { - products[i] = ToMarketingProductDTO(p) + products[i] = ToMarketingProductDTO(p, e.MarketingType) } return SalesOrdersListDTO{ @@ -68,7 +83,7 @@ func ToSalesOrdersListDTOFromMarketing(e entity.Marketing) SalesOrdersListDTO { if len(e.Products) > 0 { salesOrder = make([]MarketingProductDTO, len(e.Products)) for i, product := range e.Products { - salesOrder[i] = ToMarketingProductDTO(product) + salesOrder[i] = ToMarketingProductDTO(product, e.MarketingType) } } diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index 268a81eb..493f689f 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -65,6 +65,7 @@ func (s deliveryOrdersService) withRelations(db *gorm.DB) *gorm.DB { Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product"). + Preload("Products.ProductWarehouse.Product.Uom"). Preload("Products.ProductWarehouse.Warehouse"). Preload("Products.DeliveryProduct") } @@ -111,6 +112,7 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product"). + Preload("Products.ProductWarehouse.Product.Uom"). Preload("Products.ProductWarehouse.Warehouse"). Preload("Products.DeliveryProduct") diff --git a/internal/modules/marketing/services/salesorder.service.go b/internal/modules/marketing/services/salesorder.service.go index a64caa9f..ffc53d79 100644 --- a/internal/modules/marketing/services/salesorder.service.go +++ b/internal/modules/marketing/services/salesorder.service.go @@ -69,6 +69,7 @@ func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product.Flags"). + Preload("Products.ProductWarehouse.Product.Uom"). Preload("Products.ProductWarehouse.Warehouse") } From 1d9597636013ef7dad7b2355fed641df9001fcea Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 4 Feb 2026 14:47:56 +0700 Subject: [PATCH 5/6] 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"` } From aa1fd1c35b6485f6a1bf944d9384236e6cab3e73 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 5 Feb 2026 09:57:38 +0700 Subject: [PATCH 6/6] FEAT[BE] :update price calculation in sales order service for accurate rounding, add new conversion unit for quantity --- .../modules/marketing/services/salesorder.service.go | 11 ++++++----- internal/utils/constant.go | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/modules/marketing/services/salesorder.service.go b/internal/modules/marketing/services/salesorder.service.go index 58901794..eb2e4f5b 100644 --- a/internal/modules/marketing/services/salesorder.service.go +++ b/internal/modules/marketing/services/salesorder.service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math" "strings" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" @@ -752,13 +753,13 @@ func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Cont func (s *salesOrdersService) calculatePriceByMarketingType(marketingType string, qty, avgWeight, unitPrice float64, week *int) (totalWeight, totalPrice float64) { if marketingType == string(utils.MarketingTypeTrading) { totalWeight = 0 - totalPrice = qty * unitPrice + totalPrice = math.Round(qty*unitPrice*100) / 100 } else if marketingType == string(utils.MarketingTypeAyamPullet) && week != nil && *week > 0 { - totalWeight = qty * avgWeight - totalPrice = unitPrice * float64(*week) * qty + totalWeight = math.Round(qty*avgWeight*100) / 100 + totalPrice = math.Round(unitPrice*float64(*week)*qty*100) / 100 } else { - totalWeight = qty * avgWeight - totalPrice = totalWeight * unitPrice + totalWeight = math.Round(qty*avgWeight*100) / 100 + totalPrice = math.Round(totalWeight*unitPrice*100) / 100 } return totalWeight, totalPrice } diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 1de04fa3..27d1ec3e 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -234,6 +234,7 @@ type ConvertionUnit string const ( ConvertionUnitPeti ConvertionUnit = "PETI" ConvertionUnitKG ConvertionUnit = "KG" + ConvertionUnitQty ConvertionUnit = "QTY" ) // ------------------------------------------------------------------- @@ -643,7 +644,7 @@ func IsValidMarketingType(v string) bool { func IsValidConvertionUnit(v string) bool { switch ConvertionUnit(v) { - case ConvertionUnitPeti, ConvertionUnitKG: + case ConvertionUnitPeti, ConvertionUnitKG, ConvertionUnitQty: return true } return false