package service import ( "context" "errors" "fmt" "math" "strings" "time" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" fifoV2 "gitlab.com/mbugroup/lti-api.git/internal/common/service/fifo_stock_v2" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rShared "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "gorm.io/gorm" ) type DeliveryOrdersService interface { GetAll(ctx *fiber.Ctx, params *validation.DeliveryOrderQuery) ([]dto.MarketingListDTO, int64, error) GetOne(ctx *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error) CreateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error) UpdateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderUpdate, id uint) (*dto.MarketingDetailDTO, error) BulkApproveToStatus(ctx *fiber.Ctx, req *validation.BulkApprovalRequest, target approvalutils.ApprovalStep) ([]dto.MarketingDetailDTO, error) } type deliveryOrdersService struct { Validate *validator.Validate MarketingRepo marketingRepo.MarketingRepository MarketingProductRepo marketingRepo.MarketingProductRepository MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository StockLogRepo rShared.StockLogRepository ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository ProjectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository ApprovalSvc commonSvc.ApprovalService FifoStockV2Svc commonSvc.FifoStockV2Service } func NewDeliveryOrdersService( marketingRepo marketingRepo.MarketingRepository, marketingProductRepo marketingRepo.MarketingProductRepository, marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository, stockLogRepo rShared.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository, approvalSvc commonSvc.ApprovalService, fifoStockV2Svc commonSvc.FifoStockV2Service, validate *validator.Validate, ) DeliveryOrdersService { return &deliveryOrdersService{ Validate: validate, MarketingRepo: marketingRepo, MarketingProductRepo: marketingProductRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, StockLogRepo: stockLogRepo, ProductWarehouseRepo: productWarehouseRepo, ProjectFlockPopulationRepo: projectFlockPopulationRepo, ApprovalSvc: approvalSvc, FifoStockV2Svc: fifoStockV2Svc, } } func (s deliveryOrdersService) withRelations(db *gorm.DB) *gorm.DB { return db. Preload("CreatedUser"). Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product"). Preload("Products.ProductWarehouse.Product.Uom"). Preload("Products.ProductWarehouse.Warehouse"). Preload("Products.DeliveryProduct") } func (s deliveryOrdersService) getMarketingWithDeliveries(c *fiber.Ctx, marketingId uint) (*dto.MarketingDetailDTO, error) { if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), marketingId); err != nil { return nil, err } marketing, err := s.MarketingRepo.GetByID(c.Context(), marketingId, s.withRelations) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") } latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketingId, nil) if err != nil { } marketing.LatestApproval = latestApproval allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), marketingId) if err != nil { allDeliveryProducts = []entity.MarketingDeliveryProduct{} } responseDTO := dto.ToMarketingDetailDTO(marketing, allDeliveryProducts) return &responseDTO, nil } func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryOrderQuery) ([]dto.MarketingListDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } scope, err := m.ResolveLocationScope(c, s.MarketingRepo.DB()) if err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = db. Preload("CreatedUser"). Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product"). Preload("Products.ProductWarehouse.Product.Uom"). Preload("Products.ProductWarehouse.Warehouse"). Preload("Products.DeliveryProduct") if params.Status != "" { status := strings.TrimSpace(params.Status) latestApprovalSubQuery := s.MarketingRepo.DB(). WithContext(c.Context()). Table("approvals"). Select("DISTINCT ON (approvable_id) approvable_id, step_name, action"). Where("approvable_type = ?", utils.ApprovalWorkflowMarketing.String()). Order("approvable_id, id DESC") if strings.EqualFold(status, "DITOLAK") { db = db.Where(`EXISTS ( SELECT 1 FROM (?) AS latest_approval WHERE latest_approval.approvable_id = marketings.id AND latest_approval.action = ? )`, latestApprovalSubQuery, string(entity.ApprovalActionRejected)) } else { db = db.Where(`EXISTS ( SELECT 1 FROM (?) AS latest_approval WHERE latest_approval.approvable_id = marketings.id AND LOWER(latest_approval.step_name) = LOWER(?) AND (latest_approval.action IS NULL OR latest_approval.action <> ?) )`, latestApprovalSubQuery, status, string(entity.ApprovalActionRejected)) } } if params.Search != "" { searchPattern := "%" + params.Search + "%" db = db.Where(`( marketings.so_number ILIKE ? OR EXISTS ( SELECT 1 FROM customers c WHERE c.id = marketings.customer_id AND c.name ILIKE ? ) OR EXISTS ( SELECT 1 FROM users su WHERE su.id = marketings.sales_person_id AND su.name ILIKE ? ) OR EXISTS ( SELECT 1 FROM marketing_products mp JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id JOIN products p ON p.id = pw.product_id WHERE mp.marketing_id = marketings.id AND p.name ILIKE ? ) OR EXISTS ( SELECT 1 FROM marketing_products mp JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id JOIN warehouses w ON w.id = pw.warehouse_id WHERE mp.marketing_id = marketings.id AND w.name ILIKE ? ) )`, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern) } if len(params.ProductIDs) > 0 { db = db.Where(`EXISTS ( SELECT 1 FROM marketing_products mp JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id JOIN products p ON p.id = pw.product_id WHERE mp.marketing_id = marketings.id AND p.id IN ? )`, params.ProductIDs) } if params.CustomerId != 0 { db = db.Where("marketings.customer_id = ?", params.CustomerId) } if scope.Restrict { if len(scope.IDs) == 0 { return db.Where("1 = 0") } db = db.Where( `EXISTS ( SELECT 1 FROM marketing_products mp JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id JOIN warehouses w ON w.id = pw.warehouse_id WHERE mp.marketing_id = marketings.id AND w.location_id IN ? )`, scope.IDs, ) } if params.MarketingId != 0 { return db.Where("id = ?", params.MarketingId) } return db.Order("created_at DESC").Order("updated_at DESC") }) if err != nil { return nil, 0, err } for i := range marketings { latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketings[i].Id, func(db *gorm.DB) *gorm.DB { return db.Preload("ActionUser") }) if err != nil { continue } marketings[i].LatestApproval = latestApproval } result := make([]dto.MarketingListDTO, len(marketings)) for i, marketing := range marketings { result[i] = dto.ToMarketingListDTO(&marketing, []entity.MarketingDeliveryProduct{}) } return result, total, nil } func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error) { if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { return nil, err } marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Marketing not found") } if err != nil { return nil, err } allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), id) if err != nil { allDeliveryProducts = []entity.MarketingDeliveryProduct{} } if s.ApprovalSvc != nil { approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketing.Id, func(db *gorm.DB) *gorm.DB { return db.Preload("ActionUser") }) if err != nil { } else if len(approvals) > 0 { if marketing.LatestApproval == nil { latest := approvals[len(approvals)-1] marketing.LatestApproval = &latest } } else { marketing.LatestApproval = nil } } responseDTO := dto.ToMarketingDetailDTO(marketing, allDeliveryProducts) return &responseDTO, nil } func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), req.MarketingId); err != nil { return nil, err } if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Marketing", ID: &req.MarketingId, Exists: s.MarketingRepo.IdExists}, ); err != nil { return nil, err } actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err } approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB())) latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, nil) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") } if latestApproval == nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing has not been submitted for approval") } if latestApproval.StepNumber < uint16(utils.MarketingStepSalesOrder) { return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing must be approved to Sales Order step before creating delivery order") } if latestApproval.StepNumber >= uint16(utils.MarketingDeliveryOrder) { return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery order already exists for this marketing") } err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { 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 { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", req.MarketingId)) } return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") } for _, requestedProduct := range req.DeliveryProducts { var foundMarketingProduct *entity.MarketingProduct for i := range allMarketingProducts { if allMarketingProducts[i].Id == requestedProduct.MarketingProductId { foundMarketingProduct = &allMarketingProducts[i] break } } if foundMarketingProduct == nil { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId)) } if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses( c.Context(), dbTransaction, []uint{foundMarketingProduct.ProductWarehouseId}, ); err != nil { return err } deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", requestedProduct.MarketingProductId)) } return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") } var itemDeliveryDate *time.Time if requestedProduct.DeliveryDate != "" { parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate) if err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err)) } itemDeliveryDate = &parsedDate } totalWeight, totalPrice := s.resolveDeliveryTotals(marketing.MarketingType, requestedProduct, foundMarketingProduct) deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.WeightPerConvertion = requestedProduct.WeightPerConvertion deliveryProduct.TotalWeight = totalWeight deliveryProduct.TotalPrice = totalPrice deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber if requestedProduct.Qty > 0 { if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty, actorID); err != nil { return err } } if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") } } approvalAction := entity.ApprovalActionApproved if _, err := approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, utils.MarketingDeliveryOrder, &approvalAction, actorID, nil); err != nil { if !errors.Is(err, gorm.ErrDuplicatedKey) { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create delivery order approval") } } return nil }) if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create delivery order") } return s.getMarketingWithDeliveries(c, req.MarketingId) } func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryOrderUpdate, id uint) (*dto.MarketingDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { return nil, err } if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, ); err != nil { return nil, err } actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err } err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { 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) { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") } if len(req.DeliveryProducts) > 0 { for _, requestedProduct := range req.DeliveryProducts { var foundMarketingProduct *entity.MarketingProduct for i := range allMarketingProducts { if allMarketingProducts[i].Id == requestedProduct.MarketingProductId { foundMarketingProduct = &allMarketingProducts[i] break } } if foundMarketingProduct == nil { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId)) } if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses( c.Context(), dbTransaction, []uint{foundMarketingProduct.ProductWarehouseId}, ); err != nil { return err } deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", requestedProduct.MarketingProductId)) } return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") } oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty var itemDeliveryDate *time.Time if requestedProduct.DeliveryDate != "" { parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate) if err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err)) } itemDeliveryDate = &parsedDate } else if deliveryProduct.DeliveryDate != nil { itemDeliveryDate = deliveryProduct.DeliveryDate } totalWeight, totalPrice := s.resolveDeliveryTotals(marketing.MarketingType, requestedProduct, foundMarketingProduct) deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.WeightPerConvertion = requestedProduct.WeightPerConvertion deliveryProduct.TotalWeight = totalWeight deliveryProduct.TotalPrice = totalPrice deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber if requestedProduct.Qty != oldRequestedQty { if oldRequestedQty > 0 { if err := s.releaseDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, actorID); err != nil { return err } } if requestedProduct.Qty > 0 { if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty, actorID); err != nil { return err } } } if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") } } } return nil }) if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery order") } return s.getMarketingWithDeliveries(c, id) } func (s deliveryOrdersService) BulkApproveToStatus(c *fiber.Ctx, req *validation.BulkApprovalRequest, target approvalutils.ApprovalStep) ([]dto.MarketingDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } approvableIDs := utils.UniqueUintSlice(req.ApprovableIds) if len(approvableIDs) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") } for _, id := range approvableIDs { if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil { return nil, err } } actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err } var deliveryDate time.Time if req.RequiresDate(target) { deliveryDate, err = utils.ParseDateString(req.Date) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format") } } err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) marketingRepoTx := marketingRepo.NewMarketingRepository(tx) for _, id := range approvableIDs { if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: marketingRepoTx.IdExists}, ); err != nil { return err } marketing, err := marketingRepoTx.GetByID(c.Context(), id, s.withRelations) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing %d not found", id)) } return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") } latestApproval, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") } if latestApproval == nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d", id)) } if latestApproval.Action != nil && *latestApproval.Action == entity.ApprovalActionRejected { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d is rejected and cannot be bulk approved", id)) } currentStep := approvalutils.ApprovalStep(latestApproval.StepNumber) if currentStep >= target { currentStepName := utils.MarketingApprovalSteps[currentStep] targetStepName := utils.MarketingApprovalSteps[target] if currentStep == target { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d is already at %s step", id, targetStepName)) } return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d is already beyond %s step (current step: %s)", id, targetStepName, currentStepName)) } if len(marketing.Products) > 0 { pwIDs := make([]uint, 0, len(marketing.Products)) for _, product := range marketing.Products { if product.ProductWarehouseId != 0 { pwIDs = append(pwIDs, product.ProductWarehouseId) } } if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), tx, pwIDs); err != nil { return err } } for step := currentStep + 1; step <= target; step++ { if step == utils.MarketingDeliveryOrder { if err := s.createDeliveryFromMarketingProducts(c.Context(), tx, marketing, deliveryDate, actorID); err != nil { return err } } approvalAction := entity.ApprovalActionApproved if _, err := approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowMarketing, id, step, &approvalAction, actorID, req.Notes, ); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") } } } return nil }) if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to bulk approve marketings") } results := make([]dto.MarketingDetailDTO, 0, len(approvableIDs)) for _, id := range approvableIDs { result, err := s.getMarketingWithDeliveries(c, id) if err != nil { return nil, err } results = append(results, *result) } return results, nil } func (s deliveryOrdersService) createDeliveryFromMarketingProducts( ctx context.Context, tx *gorm.DB, marketing *entity.Marketing, deliveryDate time.Time, actorID uint, ) error { if marketing == nil { return fiber.NewError(fiber.StatusBadRequest, "Marketing not found") } if tx == nil { return fiber.NewError(fiber.StatusInternalServerError, "Transaction is required") } marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(tx) for _, marketingProduct := range marketing.Products { deliveryProduct := marketingProduct.DeliveryProduct if deliveryProduct == nil { record, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(ctx, marketingProduct.Id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", marketingProduct.Id)) } return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") } deliveryProduct = record } oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty deliveryDateCopy := deliveryDate deliveryProduct.ProductWarehouseId = marketingProduct.ProductWarehouseId deliveryProduct.UnitPrice = marketingProduct.UnitPrice deliveryProduct.AvgWeight = marketingProduct.AvgWeight deliveryProduct.WeightPerConvertion = marketingProduct.WeightPerConvertion deliveryProduct.TotalWeight = marketingProduct.TotalWeight deliveryProduct.TotalPrice = marketingProduct.TotalPrice deliveryProduct.DeliveryDate = &deliveryDateCopy requestedQty := marketingProduct.Qty if requestedQty != oldRequestedQty { if oldRequestedQty > 0 { if err := s.releaseDeliveryStock(ctx, tx, deliveryProduct, &marketingProduct, actorID); err != nil { return err } } if requestedQty > 0 { if err := s.consumeDeliveryStock(ctx, tx, deliveryProduct, &marketingProduct, requestedQty, actorID); err != nil { return err } } } if err := marketingDeliveryProductRepositoryTx.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") } } return nil } func (s *deliveryOrdersService) calculatePriceByMarketingType(marketingType string, qty, avgWeight, unitPrice float64, week *int, convertionUnit *string, _ *float64) (totalWeight, totalPrice float64) { if marketingType == string(utils.MarketingTypeTrading) { totalWeight = 0 totalPrice = math.Round(qty*unitPrice*100) / 100 } else if marketingType == string(utils.MarketingTypeAyamPullet) && week != nil && *week > 0 { totalWeight = math.Round(qty*avgWeight*100) / 100 totalPrice = math.Round(unitPrice*float64(*week)*qty*100) / 100 } else { totalWeight = math.Round(qty*avgWeight*100) / 100 if marketingType == string(utils.MarketingTypeTelur) && convertionUnit != nil { switch *convertionUnit { case string(utils.ConvertionUnitQty): totalPrice = math.Round(qty*unitPrice*100) / 100 return totalWeight, totalPrice case string(utils.ConvertionUnitPeti): totalPrice = math.Round(totalWeight*unitPrice*100) / 100 return totalWeight, totalPrice } } totalPrice = math.Round(totalWeight*unitPrice*100) / 100 } return totalWeight, totalPrice } func (s *deliveryOrdersService) resolveDeliveryTotals(marketingType string, requestedProduct validation.DeliveryProduct, marketingProduct *entity.MarketingProduct) (totalWeight, totalPrice float64) { totalWeight, totalPrice = s.calculatePriceByMarketingType( marketingType, requestedProduct.Qty, requestedProduct.AvgWeight, requestedProduct.UnitPrice, marketingProduct.Week, marketingProduct.ConvertionUnit, marketingProduct.WeightPerConvertion, ) if requestedProduct.TotalWeight != nil { totalWeight = *requestedProduct.TotalWeight } if requestedProduct.TotalPrice != nil { totalPrice = *requestedProduct.TotalPrice } 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") } if deliveryProduct == nil || deliveryProduct.Id == 0 { return fiber.NewError(fiber.StatusInternalServerError, "Delivery product not found") } if deliveryProduct.ProductWarehouseId == 0 { return fiber.NewError(fiber.StatusInternalServerError, "Delivery product warehouse not found") } if deliveryProduct.ProductWarehouseId != marketingProduct.ProductWarehouseId { return fiber.NewError(fiber.StatusBadRequest, "Delivery product warehouse mismatch with marketing product") } deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx) previousUsage := deliveryProduct.UsageQty deliveryProduct.UsageQty = requestedQty deliveryProduct.PendingQty = 0 if err := deliveryProductRepo.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") } if err := reflowMarketingScope( ctx, s.FifoStockV2Svc, tx, marketingProduct.ProductWarehouseId, resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt), ); err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err)) } refreshed, err := deliveryProductRepo.GetByID(ctx, deliveryProduct.Id, nil) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh delivery product") } deliveryProduct.UsageQty = refreshed.UsageQty deliveryProduct.PendingQty = refreshed.PendingQty deliveryProduct.CreatedAt = refreshed.CreatedAt if err := s.allocatePopulationForMarketingDelivery(ctx, tx, deliveryProduct, marketingProduct.ProductWarehouseId); err != nil { return err } allocatedDelta := deliveryProduct.UsageQty - previousUsage if actorID > 0 && allocatedDelta > 0 { decreaseLog := &entity.StockLog{ Decrease: allocatedDelta, LoggableType: string(utils.StockLogTypeMarketing), LoggableId: deliveryProduct.Id, ProductWarehouseId: deliveryProduct.ProductWarehouseId, CreatedBy: actorID, Notes: fmt.Sprintf("FIFO v2 reflow consume (%.2f)", allocatedDelta), } stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, deliveryProduct.ProductWarehouseId, 1) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") } if len(stockLogs) > 0 { latestStockLog := stockLogs[0] decreaseLog.Stock = latestStockLog.Stock - decreaseLog.Decrease } else { decreaseLog.Stock -= decreaseLog.Decrease } s.StockLogRepo.WithTx(tx).CreateOne(ctx, decreaseLog, nil) } return nil } func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct, actorID uint) error { if deliveryProduct == nil || deliveryProduct.Id == 0 { return nil } if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 { return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") } deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx) currentUsage := deliveryProduct.UsageQty currentPending := deliveryProduct.PendingQty if currentUsage <= 0 && currentPending <= 0 { return nil } affectedKandangIDs, err := s.marketingPopulationKandangIDsFromActiveAllocations(ctx, tx, deliveryProduct.Id) if err != nil { return err } deliveryProduct.UsageQty = 0 deliveryProduct.PendingQty = 0 if err := deliveryProductRepo.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset delivery product") } if err := reflowMarketingScope( ctx, s.FifoStockV2Svc, tx, marketingProduct.ProductWarehouseId, resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt), ); err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err)) } refreshed, err := deliveryProductRepo.GetByID(ctx, deliveryProduct.Id, nil) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh delivery product") } deliveryProduct.UsageQty = refreshed.UsageQty deliveryProduct.PendingQty = refreshed.PendingQty deliveryProduct.CreatedAt = refreshed.CreatedAt if err := fifoV2.ReleasePopulationConsumptionByUsable(ctx, tx, fifo.UsableKeyMarketingDelivery.String(), deliveryProduct.Id); err != nil { return err } if err := s.resyncPopulationUsageByKandangIDs(ctx, tx, affectedKandangIDs); err != nil { return err } releasedUsage := currentUsage - deliveryProduct.UsageQty if actorID > 0 && releasedUsage > 0 { increaseLog := &entity.StockLog{ Increase: releasedUsage, LoggableType: string(utils.StockLogTypeMarketing), LoggableId: deliveryProduct.Id, ProductWarehouseId: marketingProduct.ProductWarehouseId, CreatedBy: actorID, Notes: fmt.Sprintf("FIFO v2 reflow release (%.2f)", releasedUsage), } stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs") } if len(stockLogs) > 0 { latestStockLog := stockLogs[0] increaseLog.Stock = latestStockLog.Stock + increaseLog.Increase } else { increaseLog.Stock += increaseLog.Increase } s.StockLogRepo.WithTx(tx).CreateOne(ctx, increaseLog, nil) } return nil } func (s deliveryOrdersService) allocatePopulationForMarketingDelivery( ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, productWarehouseID uint, ) error { if deliveryProduct == nil || deliveryProduct.Id == 0 { return fiber.NewError(fiber.StatusBadRequest, "Delivery product tidak valid") } if tx == nil { return errors.New("transaction is required") } if deliveryProduct.UsageQty <= 0 { return nil } if productWarehouseID == 0 { return fiber.NewError(fiber.StatusBadRequest, "Product warehouse tidak ditemukan") } flagGroupCode, err := resolveMarketingFlagGroupByProductWarehouse(ctx, tx, productWarehouseID) if err != nil { return err } if !strings.EqualFold(flagGroupCode, "AYAM") { return nil } exactAllocations, err := s.findDirectPopulationAllocationsForMarketing(ctx, tx, deliveryProduct.Id) if err != nil { return err } if len(exactAllocations) > 0 { if err := fifoV2.ReleasePopulationConsumptionByUsable(ctx, tx, fifo.UsableKeyMarketingDelivery.String(), deliveryProduct.Id); err != nil { return err } if err := s.applyDirectPopulationAllocationsForMarketing(ctx, tx, productWarehouseID, deliveryProduct.Id, exactAllocations); err != nil { return err } return s.resyncPopulationUsageByKandangIDs(ctx, tx, marketingAllocationKandangIDs(exactAllocations)) } sourceGroups, err := s.findPopulationSourceGroupsForMarketing(ctx, tx, deliveryProduct.Id, productWarehouseID) if err != nil { return err } if len(sourceGroups) == 0 { return nil } if err := fifoV2.ReleasePopulationConsumptionByUsable(ctx, tx, fifo.UsableKeyMarketingDelivery.String(), deliveryProduct.Id); err != nil { return err } for _, group := range sourceGroups { populations, err := s.ProjectFlockPopulationRepo.WithTx(tx).GetByProjectFlockKandangIDAndProductWarehouseID( ctx, group.ProjectFlockKandangID, group.ProductWarehouseID, ) if err != nil { return err } if len(populations) == 0 { return fiber.NewError(fiber.StatusBadRequest, "Populasi tidak ditemukan untuk delivery") } if err := s.allocatePopulationConsumptionWithoutRelease( ctx, tx, populations, productWarehouseID, deliveryProduct.Id, group.Qty, ); err != nil { return err } } return s.resyncPopulationUsageByKandangIDs(ctx, tx, marketingSourceGroupKandangIDs(sourceGroups)) } type marketingPopulationAllocation struct { ProjectFlockPopulationID uint `gorm:"column:project_flock_population_id"` ProjectFlockKandangID uint `gorm:"column:project_flock_kandang_id"` Qty float64 `gorm:"column:qty"` } type marketingPopulationSourceGroup struct { ProjectFlockKandangID uint `gorm:"column:project_flock_kandang_id"` ProductWarehouseID uint `gorm:"column:product_warehouse_id"` Qty float64 `gorm:"column:qty"` } func (s deliveryOrdersService) findDirectPopulationAllocationsForMarketing( ctx context.Context, tx *gorm.DB, deliveryProductID uint, ) ([]marketingPopulationAllocation, error) { var rows []marketingPopulationAllocation err := tx.WithContext(ctx). Table("stock_allocations sa"). Select(` pfp.id AS project_flock_population_id, pc.project_flock_kandang_id AS project_flock_kandang_id, SUM(sa.qty) AS qty `). Joins("JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()). Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id"). Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?", fifo.UsableKeyMarketingDelivery.String(), deliveryProductID, entity.StockAllocationStatusActive, entity.StockAllocationPurposeConsume, ). Group("pfp.id, pc.project_flock_kandang_id"). Order("pfp.id ASC"). Scan(&rows).Error return rows, err } func (s deliveryOrdersService) findPopulationSourceGroupsForMarketing( ctx context.Context, tx *gorm.DB, deliveryProductID uint, productWarehouseID uint, ) ([]marketingPopulationSourceGroup, error) { groups := make(map[string]marketingPopulationSourceGroup) appendGroup := func(projectFlockKandangID uint, sourceProductWarehouseID uint, qty float64) { if projectFlockKandangID == 0 || sourceProductWarehouseID == 0 || qty <= 0 { return } key := fmt.Sprintf("%d:%d", projectFlockKandangID, sourceProductWarehouseID) current := groups[key] current.ProjectFlockKandangID = projectFlockKandangID current.ProductWarehouseID = sourceProductWarehouseID current.Qty += qty groups[key] = current } var transferRows []marketingPopulationSourceGroup if err := tx.WithContext(ctx). Table("stock_allocations sa"). Select(` source_pw.project_flock_kandang_id AS project_flock_kandang_id, std.source_product_warehouse_id AS product_warehouse_id, SUM(sa.qty) AS qty `). Joins("JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()). Joins("JOIN product_warehouses source_pw ON source_pw.id = std.source_product_warehouse_id"). Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?", fifo.UsableKeyMarketingDelivery.String(), deliveryProductID, entity.StockAllocationStatusActive, entity.StockAllocationPurposeConsume, ). Where("source_pw.project_flock_kandang_id IS NOT NULL"). Group("source_pw.project_flock_kandang_id, std.source_product_warehouse_id"). Scan(&transferRows).Error; err != nil { return nil, err } for _, row := range transferRows { appendGroup(row.ProjectFlockKandangID, row.ProductWarehouseID, row.Qty) } var purchaseRows []marketingPopulationSourceGroup if err := tx.WithContext(ctx). Table("stock_allocations sa"). Select(` pi.project_flock_kandang_id AS project_flock_kandang_id, pi.product_warehouse_id AS product_warehouse_id, SUM(sa.qty) AS qty `). Joins("JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()). Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?", fifo.UsableKeyMarketingDelivery.String(), deliveryProductID, entity.StockAllocationStatusActive, entity.StockAllocationPurposeConsume, ). Where("pi.project_flock_kandang_id IS NOT NULL"). Where("pi.product_warehouse_id IS NOT NULL"). Group("pi.project_flock_kandang_id, pi.product_warehouse_id"). Scan(&purchaseRows).Error; err != nil { return nil, err } for _, row := range purchaseRows { appendGroup(row.ProjectFlockKandangID, row.ProductWarehouseID, row.Qty) } var layingRows []marketingPopulationSourceGroup if err := tx.WithContext(ctx). Table("stock_allocations sa"). Select(` ltt.target_project_flock_kandang_id AS project_flock_kandang_id, ltt.product_warehouse_id AS product_warehouse_id, SUM(sa.qty) AS qty `). Joins("JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()). Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?", fifo.UsableKeyMarketingDelivery.String(), deliveryProductID, entity.StockAllocationStatusActive, entity.StockAllocationPurposeConsume, ). Where("ltt.product_warehouse_id IS NOT NULL"). Group("ltt.target_project_flock_kandang_id, ltt.product_warehouse_id"). Scan(&layingRows).Error; err != nil { return nil, err } for _, row := range layingRows { appendGroup(row.ProjectFlockKandangID, row.ProductWarehouseID, row.Qty) } if len(groups) == 0 { pw, err := s.ProductWarehouseRepo.WithTx(tx).GetByID(ctx, productWarehouseID, nil) if err != nil { return nil, err } if pw.ProjectFlockKandangId != nil && *pw.ProjectFlockKandangId != 0 { appendGroup(*pw.ProjectFlockKandangId, productWarehouseID, 0) } } result := make([]marketingPopulationSourceGroup, 0, len(groups)) for _, group := range groups { if group.Qty == 0 { group.Qty = s.resolveMarketingRequestedUsageQty(ctx, tx, deliveryProductID) } if group.Qty > 0 { result = append(result, group) } } return result, nil } func (s deliveryOrdersService) applyDirectPopulationAllocationsForMarketing( ctx context.Context, tx *gorm.DB, productWarehouseID uint, deliveryProductID uint, allocations []marketingPopulationAllocation, ) error { stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx) for _, allocation := range allocations { if allocation.ProjectFlockPopulationID == 0 || allocation.Qty <= 0 { continue } record := &entity.StockAllocation{ ProductWarehouseId: productWarehouseID, StockableType: fifo.StockableKeyProjectFlockPopulation.String(), StockableId: allocation.ProjectFlockPopulationID, UsableType: fifo.UsableKeyMarketingDelivery.String(), UsableId: deliveryProductID, Qty: allocation.Qty, Status: entity.StockAllocationStatusActive, AllocationPurpose: entity.StockAllocationPurposeConsume, } if err := stockAllocationRepo.CreateOne(ctx, record, nil); err != nil { return err } if err := tx.WithContext(ctx). Model(&entity.ProjectFlockPopulation{}). Where("id = ?", allocation.ProjectFlockPopulationID). Update("total_used_qty", gorm.Expr("total_used_qty + ?", allocation.Qty)).Error; err != nil { return err } } return nil } func (s deliveryOrdersService) allocatePopulationConsumptionWithoutRelease( ctx context.Context, tx *gorm.DB, populations []entity.ProjectFlockPopulation, productWarehouseID uint, deliveryProductID uint, consumeQty float64, ) error { if consumeQty <= 0 { return nil } remaining := consumeQty stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx) for _, population := range populations { available := population.TotalQty - population.TotalUsedQty if available <= 0 { continue } portion := available if remaining < portion { portion = remaining } if portion <= 0 { continue } record := &entity.StockAllocation{ ProductWarehouseId: productWarehouseID, StockableType: fifo.StockableKeyProjectFlockPopulation.String(), StockableId: population.Id, UsableType: fifo.UsableKeyMarketingDelivery.String(), UsableId: deliveryProductID, Qty: portion, Status: entity.StockAllocationStatusActive, AllocationPurpose: entity.StockAllocationPurposeConsume, } if err := stockAllocationRepo.CreateOne(ctx, record, nil); err != nil { return err } if err := tx.WithContext(ctx). Model(&entity.ProjectFlockPopulation{}). Where("id = ?", population.Id). Update("total_used_qty", gorm.Expr("total_used_qty + ?", portion)).Error; err != nil { return err } remaining -= portion if remaining <= 0.000001 { break } } if remaining > 0.000001 { return fiber.NewError(fiber.StatusBadRequest, "Populasi tidak mencukupi") } return nil } func (s deliveryOrdersService) marketingPopulationKandangIDsFromActiveAllocations( ctx context.Context, tx *gorm.DB, deliveryProductID uint, ) ([]uint, error) { var ids []uint err := tx.WithContext(ctx). Table("stock_allocations sa"). Distinct("pc.project_flock_kandang_id"). Joins("JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()). Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id"). Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?", fifo.UsableKeyMarketingDelivery.String(), deliveryProductID, entity.StockAllocationStatusActive, entity.StockAllocationPurposeConsume, ). Pluck("pc.project_flock_kandang_id", &ids).Error return ids, err } func (s deliveryOrdersService) resyncPopulationUsageByKandangIDs(ctx context.Context, tx *gorm.DB, kandangIDs []uint) error { for _, kandangID := range uniqueUintIDs(kandangIDs) { if kandangID == 0 { continue } if err := s.ProjectFlockPopulationRepo.WithTx(tx).ResyncUsageByProjectFlockKandangID(ctx, tx, kandangID); err != nil { return err } } return nil } func (s deliveryOrdersService) resolveMarketingRequestedUsageQty(ctx context.Context, tx *gorm.DB, deliveryProductID uint) float64 { var usageQty float64 if err := tx.WithContext(ctx). Table("marketing_delivery_products"). Select("usage_qty"). Where("id = ?", deliveryProductID). Scan(&usageQty).Error; err != nil { return 0 } return usageQty } func marketingAllocationKandangIDs(rows []marketingPopulationAllocation) []uint { ids := make([]uint, 0, len(rows)) for _, row := range rows { ids = append(ids, row.ProjectFlockKandangID) } return ids } func marketingSourceGroupKandangIDs(rows []marketingPopulationSourceGroup) []uint { ids := make([]uint, 0, len(rows)) for _, row := range rows { ids = append(ids, row.ProjectFlockKandangID) } return ids } func uniqueUintIDs(ids []uint) []uint { seen := make(map[uint]struct{}, len(ids)) result := make([]uint, 0, len(ids)) for _, id := range ids { if id == 0 { continue } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} result = append(result, id) } return result }