From f59cdd821ab9d71d2f2224be5e0e41395db4209c Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 3 Feb 2026 13:32:37 +0700 Subject: [PATCH] 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