diff --git a/cmd/delete-adjustments/main.go b/cmd/delete-adjustments/main.go index e07a7ae3..6555749b 100644 --- a/cmd/delete-adjustments/main.go +++ b/cmd/delete-adjustments/main.go @@ -11,12 +11,10 @@ import ( "strings" "time" - commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" "gitlab.com/mbugroup/lti-api.git/internal/config" "gitlab.com/mbugroup/lti-api.git/internal/database" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" "gorm.io/gorm" ) @@ -61,13 +59,7 @@ func main() { ctx := context.Background() db := database.Connect(config.DBHost, config.DBName) - productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) - stockAllocRepo := commonRepo.NewStockAllocationRepository(db) - fifoSvc := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, nil) fifoStockV2Svc := commonSvc.NewFifoStockV2Service(db, nil) - if err := registerAdjustmentFIFO(fifoSvc); err != nil { - log.Fatalf("failed to register adjustment fifo config: %v", err) - } adjustments, err := loadAdjustments(ctx, db, ids) if err != nil { @@ -185,7 +177,7 @@ func main() { } fmt.Printf( - "PLAN adj=%d lane=STOCKABLE function=%s total=%.3f remove_qty=%.3f action=reverse_stock+delete\n", + "PLAN adj=%d lane=STOCKABLE function=%s total=%.3f remove_qty=%.3f action=reflow_to_zero+delete\n", adj.ID, route.FunctionCode, adj.TotalQty, @@ -198,16 +190,25 @@ func main() { } err = db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { - if removeQty > 0 { - if err := fifoSvc.AdjustStockableQuantity(ctx, commonSvc.StockAdjustRequest{ - StockableKey: fifo.StockableKeyAdjustmentIn, - StockableID: adj.ID, - ProductWarehouseID: adj.ProductWarehouseID, - Quantity: -removeQty, - Tx: tx, - }); err != nil { - return fmt.Errorf("reverse stockable quantity: %w", err) - } + if err := tx.WithContext(ctx). + Table("adjustment_stocks"). + Where("id = ?", adj.ID). + Updates(map[string]any{ + "total_qty": 0, + "total_used": 0, + }).Error; err != nil { + return fmt.Errorf("set stockable qty to zero: %w", err) + } + + reflowReq := commonSvc.FifoStockV2ReflowRequest{ + FlagGroupCode: route.FlagGroupCode, + ProductWarehouseID: adj.ProductWarehouseID, + AsOf: &adj.CreatedAt, + IdempotencyKey: fmt.Sprintf("delete-adjustment-stockable-%d-%d", adj.ID, time.Now().UnixNano()), + Tx: tx, + } + if _, err := fifoStockV2Svc.Reflow(ctx, reflowReq); err != nil { + return fmt.Errorf("reflow stockable to zero: %w", err) } if err := hardDeleteStockableAllocations(ctx, tx, fifo.StockableKeyAdjustmentIn.String(), adj.ID); err != nil { @@ -243,39 +244,6 @@ func main() { } } -func registerAdjustmentFIFO(fifoSvc commonSvc.FifoService) error { - if err := fifoSvc.RegisterStockable(fifo.StockableConfig{ - Key: fifo.StockableKeyAdjustmentIn, - Table: "adjustment_stocks", - Columns: fifo.StockableColumns{ - ID: "id", - ProductWarehouseID: "product_warehouse_id", - TotalQuantity: "total_qty", - TotalUsedQuantity: "total_used", - CreatedAt: "created_at", - }, - OrderBy: []string{"created_at ASC", "id ASC"}, - }); err != nil && !strings.Contains(strings.ToLower(err.Error()), "already registered") { - return err - } - - if err := fifoSvc.RegisterUsable(fifo.UsableConfig{ - Key: fifo.UsableKeyAdjustmentOut, - Table: "adjustment_stocks", - Columns: fifo.UsableColumns{ - ID: "id", - ProductWarehouseID: "product_warehouse_id", - UsageQuantity: "usage_qty", - PendingQuantity: "pending_qty", - CreatedAt: "created_at", - }, - OrderBy: []string{"created_at ASC", "id ASC"}, - }); err != nil && !strings.Contains(strings.ToLower(err.Error()), "already registered") { - return err - } - return nil -} - func modeLabel(apply bool) string { if apply { return "APPLY" diff --git a/cmd/reflow-project-flock-kandang/main.go b/cmd/reflow-project-flock-kandang/main.go index 8e797bf7..973026a9 100644 --- a/cmd/reflow-project-flock-kandang/main.go +++ b/cmd/reflow-project-flock-kandang/main.go @@ -178,14 +178,35 @@ func main() { successApply++ } + orphanPopulationRows := int64(0) + syncedPopulationQtyRows := int64(0) + syncedPopulationUsedRows := int64(0) + if rowsOrphan, rowsQty, rowsUsed, err := resyncProjectFlockPopulation(ctx, db, projectFlockKandangID); err != nil { + fmt.Printf("FAIL population_resync project_flock_kandang_id=%d error=%v\n", projectFlockKandangID, err) + failedApply++ + } else { + orphanPopulationRows = rowsOrphan + syncedPopulationQtyRows = rowsQty + syncedPopulationUsedRows = rowsUsed + fmt.Printf( + "SYNC project_flock_populations orphan_marked=%d qty_synced=%d used_synced=%d\n", + orphanPopulationRows, + syncedPopulationQtyRows, + syncedPopulationUsedRows, + ) + } + fmt.Println() fmt.Printf( - "Summary: planned=%d skipped_pw=%d failed_resolve=%d applied=%d failed_apply=%d\n", + "Summary: planned=%d skipped_pw=%d failed_resolve=%d applied=%d failed_apply=%d population_orphan=%d population_qty_synced=%d population_used_synced=%d\n", len(targets), skippedPW, failedResolve, successApply, failedApply, + orphanPopulationRows, + syncedPopulationQtyRows, + syncedPopulationUsedRows, ) if failedResolve > 0 || failedApply > 0 { os.Exit(1) @@ -379,3 +400,66 @@ func resolveFlagGroupsByProductWarehouse(ctx context.Context, db *gorm.DB, produ } return groups, nil } + +func resyncProjectFlockPopulation(ctx context.Context, db *gorm.DB, projectFlockKandangID uint) (int64, int64, int64, error) { + if projectFlockKandangID == 0 { + return 0, 0, 0, nil + } + + orphanResult := db.WithContext(ctx).Exec(` + UPDATE project_flock_populations pfp + SET deleted_at = NOW(), + updated_at = NOW() + FROM project_chickins pc + WHERE pfp.project_chickin_id = pc.id + AND pc.project_flock_kandang_id = ? + AND pc.deleted_at IS NOT NULL + AND pfp.deleted_at IS NULL + `, projectFlockKandangID) + if orphanResult.Error != nil { + return 0, 0, 0, orphanResult.Error + } + + qtyResult := db.WithContext(ctx).Exec(` + UPDATE project_flock_populations p + SET total_qty = GREATEST(COALESCE(pc.usage_qty, 0), 0), + updated_at = NOW() + FROM project_chickins pc + WHERE p.project_chickin_id = pc.id + AND pc.project_flock_kandang_id = ? + AND pc.deleted_at IS NULL + AND p.deleted_at IS NULL + `, projectFlockKandangID) + if qtyResult.Error != nil { + return 0, 0, 0, qtyResult.Error + } + + usedResult := db.WithContext(ctx).Exec(` + WITH scoped AS ( + SELECT pfp.id, pfp.total_qty + FROM project_flock_populations pfp + JOIN project_chickins pc ON pc.id = pfp.project_chickin_id + WHERE pc.project_flock_kandang_id = ? + AND pc.deleted_at IS NULL + AND pfp.deleted_at IS NULL + ), + alloc AS ( + SELECT sa.stockable_id, SUM(sa.qty) AS used_qty + FROM stock_allocations sa + WHERE sa.stockable_type = 'PROJECT_FLOCK_POPULATION' + AND sa.status = 'ACTIVE' + GROUP BY sa.stockable_id + ) + UPDATE project_flock_populations p + SET total_used_qty = LEAST(COALESCE(a.used_qty, 0), GREATEST(s.total_qty, 0)), + updated_at = NOW() + FROM scoped s + LEFT JOIN alloc a ON a.stockable_id = s.id + WHERE p.id = s.id + `, projectFlockKandangID) + if usedResult.Error != nil { + return 0, 0, 0, usedResult.Error + } + + return orphanResult.RowsAffected, qtyResult.RowsAffected, usedResult.RowsAffected, nil +} diff --git a/internal/modules/inventory/adjustments/module.go b/internal/modules/inventory/adjustments/module.go index b1522c0f..42c8332d 100644 --- a/internal/modules/inventory/adjustments/module.go +++ b/internal/modules/inventory/adjustments/module.go @@ -5,7 +5,6 @@ import ( "github.com/gofiber/fiber/v2" "gorm.io/gorm" - commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" rAdjustmentStock "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/repositories" sAdjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services" @@ -17,7 +16,6 @@ import ( rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" "gitlab.com/mbugroup/lti-api.git/internal/utils" - "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" ) type AdjustmentModule struct{} @@ -31,50 +29,14 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat userRepo := rUser.NewUserRepository(db) productRepo := rproduct.NewProductRepository(db) adjustmentStockRepo := rAdjustmentStock.NewAdjustmentStockRepository(db) - stockAllocRepo := commonRepo.NewStockAllocationRepository(db) - - fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log) fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log) - err := fifoService.RegisterStockable(fifo.StockableConfig{ - Key: fifo.StockableKeyAdjustmentIn, - Table: "adjustment_stocks", - Columns: fifo.StockableColumns{ - ID: "id", - ProductWarehouseID: "product_warehouse_id", - TotalQuantity: "total_qty", - TotalUsedQuantity: "total_used", - CreatedAt: "created_at", - }, - OrderBy: []string{"created_at ASC", "id ASC"}, - }) - if err != nil { - panic("Failed to register ADJUSTMENT_IN as Stockable: " + err.Error()) - } - - err = fifoService.RegisterUsable(fifo.UsableConfig{ - Key: fifo.UsableKeyAdjustmentOut, - Table: "adjustment_stocks", - Columns: fifo.UsableColumns{ - ID: "id", - ProductWarehouseID: "product_warehouse_id", - UsageQuantity: "usage_qty", - PendingQuantity: "pending_qty", - CreatedAt: "created_at", - }, - OrderBy: []string{"created_at ASC", "id ASC"}, - }) - if err != nil { - panic("Failed to register ADJUSTMENT_OUT as Usable: " + err.Error()) - } - adjustmentService := sAdjustment.NewAdjustmentService( productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, adjustmentStockRepo, - fifoService, fifoStockV2Service, validate, projectFlockKandangRepo, diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index 261b7b2f..8e92f036 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -39,7 +39,6 @@ type adjustmentService struct { ProductRepo productRepo.ProductRepository ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository AdjustmentStockRepository adjustmentStockRepo.AdjustmentStockRepository - FifoSvc common.FifoService FifoStockV2Svc common.FifoStockV2Service } @@ -55,7 +54,6 @@ func NewAdjustmentService( warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, adjustmentStockRepo adjustmentStockRepo.AdjustmentStockRepository, - fifoSvc common.FifoService, fifoStockV2Svc common.FifoStockV2Service, validate *validator.Validate, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, @@ -69,7 +67,6 @@ func NewAdjustmentService( ProductRepo: productRepo, ProjectFlockKandangRepo: projectFlockKandangRepo, AdjustmentStockRepository: adjustmentStockRepo, - FifoSvc: fifoSvc, FifoStockV2Svc: fifoStockV2Svc, } } diff --git a/internal/modules/inventory/transfers/module.go b/internal/modules/inventory/transfers/module.go index 8e1aae94..50498493 100644 --- a/internal/modules/inventory/transfers/module.go +++ b/internal/modules/inventory/transfers/module.go @@ -24,7 +24,6 @@ import ( rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" "gitlab.com/mbugroup/lti-api.git/internal/utils" - "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" ) type TransferModule struct{} @@ -43,7 +42,6 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate kandangRepo := rKandang.NewKandangRepository(db) nonstockRepo := rNonstock.NewNonstockRepository(db) documentRepo := commonRepo.NewDocumentRepository(db) - stockAllocRepo := commonRepo.NewStockAllocationRepository(db) expenseRepository := expenseRepo.NewExpenseRepository(db) expenseRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db) @@ -69,7 +67,6 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate validate, ) - fifoService := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log) fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log) expenseBridge := sTransfer.NewTransferExpenseBridge( db, @@ -79,39 +76,7 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate expenseServiceInstance, ) - err = fifoService.RegisterStockable(fifo.StockableConfig{ - Key: fifo.StockableKeyStockTransferIn, - Table: "stock_transfer_details", - Columns: fifo.StockableColumns{ - ID: "id", - ProductWarehouseID: "dest_product_warehouse_id", - TotalQuantity: "total_qty", - TotalUsedQuantity: "total_used", - CreatedAt: "created_at", - }, - OrderBy: []string{"created_at ASC", "id ASC"}, - }) - if err != nil { - panic(err) - } - - err = fifoService.RegisterUsable(fifo.UsableConfig{ - Key: fifo.UsableKeyStockTransferOut, - Table: "stock_transfer_details", - Columns: fifo.UsableColumns{ - ID: "id", - ProductWarehouseID: "source_product_warehouse_id", - UsageQuantity: "usage_qty", - PendingQuantity: "pending_qty", - CreatedAt: "created_at", - }, - OrderBy: []string{"created_at ASC", "id ASC"}, - }) - if err != nil { - panic(err) - } - - transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo, documentSvc, fifoService, fifoStockV2Service, expenseBridge) + transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo, documentSvc, fifoStockV2Service, expenseBridge) userService := sUser.NewUserService(userRepo, validate) TransferRoutes(router, userService, transferService) diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index 4f88e0ec..fd7749ee 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -44,12 +44,11 @@ type transferService struct { WarehouseRepo warehouseRepo.WarehouseRepository ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository DocumentSvc commonSvc.DocumentService - FifoSvc commonSvc.FifoService FifoStockV2Svc commonSvc.FifoStockV2Service ExpenseBridge TransferExpenseBridge } -func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, fifoSvc commonSvc.FifoService, fifoStockV2Svc commonSvc.FifoStockV2Service, expenseBridge TransferExpenseBridge) TransferService { +func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, documentSvc commonSvc.DocumentService, fifoStockV2Svc commonSvc.FifoStockV2Service, expenseBridge TransferExpenseBridge) TransferService { return &transferService{ Log: utils.Log, Validate: validate, @@ -63,7 +62,6 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr WarehouseRepo: warehouseRepo, ProjectFlockKandangRepo: projectFlockKandangRepo, DocumentSvc: documentSvc, - FifoSvc: fifoSvc, FifoStockV2Svc: fifoStockV2Svc, ExpenseBridge: expenseBridge, } diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 45ab0905..3a54f3ba 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -257,7 +257,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti for idx, chickin := range newChikins { desiredQty := chickinQtyMap[uint(idx)] - if err := s.ConsumeChickinStocks(c.Context(), dbTransaction, chickin, desiredQty, actorID); err != nil { + if err := s.StageChickinStocks(c.Context(), dbTransaction, chickin, desiredQty, actorID); err != nil { return err } } @@ -368,7 +368,7 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { return err } - if chickin.UsageQty > 0 { + if chickin.UsageQty > 0 || chickin.PendingUsageQty > 0 { if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); err != nil { return err } @@ -479,6 +479,20 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit } for _, chickin := range chickins { + approvedQty := chickin.UsageQty + if approvedQty <= 0 { + approvedQty = chickin.PendingUsageQty + } + if approvedQty < 0 { + approvedQty = 0 + } + + if err := s.ConsumeChickinStocks(c.Context(), dbTransaction, &chickin, approvedQty, actorID); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to finalize usage qty for chickin %d", chickin.Id)) + } + chickin.UsageQty = approvedQty + chickin.PendingUsageQty = 0 + populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(c.Context(), chickin.Id) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to check population for chickin %d", chickin.Id)) @@ -510,19 +524,13 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to create population for chickin %d", chickin.Id)) } - if err := chickinRepoTx.PatchOne(c.Context(), chickin.Id, map[string]any{ - "pending_usage_qty": 0, - }, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reset pending usage qty for chickin %d", chickin.Id)) - } - if err := s.ReplenishChickinStocks(c.Context(), dbTransaction, &chickin, sourcePW, population, actorID); err != nil { return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock for chickin %d", chickin.Id)) } } } if action == entity.ApprovalActionRejected { - chickins, err := chickinRepoTx.GetPendingByProjectFlockKandangID(c.Context(), approvableID) + chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get pending chickins for rejection %d", approvableID)) } @@ -532,6 +540,17 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit } for _, chickin := range chickins { + populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(c.Context(), chickin.Id) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to check population for chickin %d", chickin.Id)) + } + if populationExists { + continue + } + + if chickin.UsageQty <= 0 && chickin.PendingUsageQty <= 0 { + continue + } if err := s.ReleaseChickinStocks(c.Context(), dbTransaction, &chickin, actorID); err != nil { return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for rejected chickin %d: %v", chickin.Id, err)) @@ -612,6 +631,20 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB, return s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, desiredQty, 0) } +func (s *chickinService) StageChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, desiredQty float64, actorID uint) error { + if chickin == nil { + return nil + } + if tx == nil { + return errors.New("transaction is required") + } + if desiredQty < 0 { + return errors.New("desired quantity must be zero or greater") + } + + return s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, 0, desiredQty) +} + func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, targetPW *entity.ProductWarehouse, population *entity.ProjectFlockPopulation, actorID uint) error { if chickin == nil || targetPW == nil || population == nil { return nil diff --git a/internal/modules/production/transfer_layings/module.go b/internal/modules/production/transfer_layings/module.go index dfe2ad44..f7661034 100644 --- a/internal/modules/production/transfer_layings/module.go +++ b/internal/modules/production/transfer_layings/module.go @@ -2,7 +2,6 @@ package transfer_layings import ( "fmt" - "strings" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -14,7 +13,6 @@ import ( rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories" sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" "gitlab.com/mbugroup/lti-api.git/internal/utils" - "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" @@ -34,45 +32,7 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db) productWarehouseRepo := rInventory.NewProductWarehouseRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db) - - stockAllocationRepo := commonRepo.NewStockAllocationRepository(db) - fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log) - - // daftarin jadi stockable - if err := fifoService.RegisterStockable(fifo.StockableConfig{ - Key: fifo.StockableKeyTransferToLayingIn, - Table: "laying_transfer_targets", - Columns: fifo.StockableColumns{ - ID: "id", - ProductWarehouseID: "product_warehouse_id", - TotalQuantity: "total_qty", - TotalUsedQuantity: "total_used", - CreatedAt: "created_at", - }, - OrderBy: []string{"created_at ASC", "id ASC"}, - }); err != nil { - if !strings.Contains(strings.ToLower(err.Error()), "already registered") { - panic(fmt.Sprintf("failed to register transfer to laying stockable workflow: %v", err)) - } - } - - // daftarin jadi usable - if err := fifoService.RegisterUsable(fifo.UsableConfig{ - Key: fifo.UsableKeyTransferToLayingOut, - Table: "laying_transfer_sources", - Columns: fifo.UsableColumns{ - ID: "id", - ProductWarehouseID: "product_warehouse_id", - UsageQuantity: "usage_qty", - PendingQuantity: "pending_usage_qty", - CreatedAt: "created_at", - }, - OrderBy: []string{"created_at ASC", "id ASC"}, - }); err != nil { - if !strings.Contains(strings.ToLower(err.Error()), "already registered") { - panic(fmt.Sprintf("failed to register transfer to laying usable workflow: %v", err)) - } - } + fifoStockV2Service := commonSvc.NewFifoStockV2Service(db, utils.Log) approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) @@ -90,7 +50,7 @@ func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, val productWarehouseRepo, warehouseRepo, approvalService, - fifoService, + fifoStockV2Service, validate, ) userService := sUser.NewUserService(userRepo, validate) diff --git a/internal/modules/production/chickins/services/fifo_stock_v2_helper.go b/internal/modules/production/transfer_layings/services/fifo_stock_v2_helper.go similarity index 75% rename from internal/modules/production/chickins/services/fifo_stock_v2_helper.go rename to internal/modules/production/transfer_layings/services/fifo_stock_v2_helper.go index a931e43e..3df37dbf 100644 --- a/internal/modules/production/chickins/services/fifo_stock_v2_helper.go +++ b/internal/modules/production/transfer_layings/services/fifo_stock_v2_helper.go @@ -12,12 +12,12 @@ import ( ) const ( - chickinOutFunctionCode = "CHICKIN_OUT" - chickinUsableLane = "USABLE" - chickinSourceTable = "project_chickins" + transferLayingInFunctionCode = "TRANSFER_TO_LAYING_IN" + transferLayingStockableLane = "STOCKABLE" + transferLayingSourceTable = "laying_transfer_targets" ) -func reflowChickinScope( +func reflowTransferLayingScope( ctx context.Context, fifoStockV2Svc commonSvc.FifoStockV2Service, tx *gorm.DB, @@ -34,7 +34,7 @@ func reflowChickinScope( return fmt.Errorf("product warehouse id is required") } - flagGroupCode, err := resolveChickinFlagGroupByProductWarehouse(ctx, tx, productWarehouseID) + flagGroupCode, err := resolveTransferLayingFlagGroupByProductWarehouse(ctx, tx, productWarehouseID) if err != nil { return err } @@ -51,7 +51,7 @@ func reflowChickinScope( return err } -func resolveChickinFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (string, error) { +func resolveTransferLayingFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (string, error) { type row struct { FlagGroupCode string `gorm:"column:flag_group_code"` } @@ -62,9 +62,9 @@ func resolveChickinFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, Select("rr.flag_group_code"). Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE"). Where("rr.is_active = TRUE"). - Where("rr.lane = ?", chickinUsableLane). - Where("rr.function_code = ?", chickinOutFunctionCode). - Where("rr.source_table = ?", chickinSourceTable). + Where("rr.lane = ?", transferLayingStockableLane). + Where("rr.function_code = ?", transferLayingInFunctionCode). + Where("rr.source_table = ?", transferLayingSourceTable). Where(` EXISTS ( SELECT 1 diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index 15351e56..3eb94a74 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -19,7 +19,6 @@ import ( validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations" rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" - "gitlab.com/mbugroup/lti-api.git/internal/utils/fifo" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -51,7 +50,7 @@ type transferLayingService struct { WarehouseRepo rWarehouse.WarehouseRepository StockLogRepo rStockLogs.StockLogRepository ApprovalService commonSvc.ApprovalService - FifoSvc commonSvc.FifoService + FifoStockV2Svc commonSvc.FifoStockV2Service } func NewTransferLayingService( @@ -64,7 +63,7 @@ func NewTransferLayingService( productWarehouseRepo rInventory.ProductWarehouseRepository, warehouseRepo rWarehouse.WarehouseRepository, approvalService commonSvc.ApprovalService, - fifoSvc commonSvc.FifoService, + fifoStockV2Svc commonSvc.FifoStockV2Service, validate *validator.Validate, ) TransferLayingService { return &transferLayingService{ @@ -80,7 +79,7 @@ func NewTransferLayingService( WarehouseRepo: warehouseRepo, StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()), ApprovalService: approvalService, - FifoSvc: fifoSvc, + FifoStockV2Svc: fifoStockV2Svc, } } @@ -744,7 +743,6 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { repoTx := s.Repository.WithTx(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) - stockAllocationRepo := commonRepo.NewStockAllocationRepository(dbTransaction) sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction) stockLogRepoTx := rStockLogs.NewStockLogRepository(dbTransaction) @@ -771,6 +769,9 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( } if action == entity.ApprovalActionApproved { + if s.FifoStockV2Svc == nil { + return fiber.NewError(fiber.StatusInternalServerError, "FIFO v2 service is not available") + } sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), approvableID) if err != nil { @@ -792,58 +793,70 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( totalSourceRequested += source.RequestedQty } + sourceBeforeUsage := make(map[uint]float64, len(sources)) + affectedPW := make(map[uint]struct{}) + for _, source := range sources { if source.ProductWarehouseId == nil { return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Source product warehouse tidak ditemukan untuk transfer %d", approvableID)) } - sourceShare := (source.RequestedQty / totalSourceRequested) * totalTargetQty - - consumeResult, err := s.FifoSvc.Consume(c.Context(), commonSvc.StockConsumeRequest{ - UsableKey: fifo.UsableKeyTransferToLayingOut, - UsableID: source.Id, - ProductWarehouseID: *source.ProductWarehouseId, - Quantity: sourceShare, - AllowPending: false, - Tx: dbTransaction, - }) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal consume FIFO stock: %v", err)) + sourceShare := 0.0 + if totalSourceRequested > 0 { + sourceShare = (source.RequestedQty / totalSourceRequested) * totalTargetQty } + sourceBeforeUsage[source.Id] = source.UsageQty + if err := sourceRepoTx.PatchOne(c.Context(), source.Id, map[string]interface{}{ - "usage_qty": source.UsageQty + consumeResult.UsageQuantity, - "pending_usage_qty": consumeResult.PendingQuantity, + "usage_qty": sourceShare, + "pending_usage_qty": 0, }, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Gagal update source usage qty") } - targetShares := distributeProportionalWithRounding(targets, totalTargetQty, sourceShare) + affectedPW[*source.ProductWarehouseId] = struct{}{} + } - for i, target := range targets { - roundedQty := math.Round(targetShares[i]) - if roundedQty <= 0 { - continue - } - mappingAllocation := &entity.StockAllocation{ - StockableType: fifo.UsableKeyTransferToLayingOut.String(), - StockableId: source.Id, - UsableType: fifo.StockableKeyTransferToLayingIn.String(), - UsableId: target.Id, - ProductWarehouseId: *source.ProductWarehouseId, - Qty: roundedQty, - Status: entity.StockAllocationStatusActive, - } - if err := stockAllocationRepo.CreateOne(c.Context(), mappingAllocation, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Gagal create mapping allocation source→target") - } + for _, target := range targets { + if target.ProductWarehouseId == nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID)) + } + + if err := targetRepoTx.PatchOne(c.Context(), target.Id, map[string]interface{}{ + "total_qty": target.TotalQty, + }, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Gagal update target total qty") + } + affectedPW[*target.ProductWarehouseId] = struct{}{} + } + + for pwID := range affectedPW { + asOfCopy := transfer.TransferDate + if err := reflowTransferLayingScope(c.Context(), s.FifoStockV2Svc, dbTransaction, pwID, &asOfCopy); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal reflow FIFO stock transfer laying (pw=%d): %v", pwID, err)) + } + } + + for _, source := range sources { + if source.ProductWarehouseId == nil { + continue + } + refreshedSource, err := sourceRepoTx.GetByID(c.Context(), source.Id, nil) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Gagal refresh source transfer setelah reflow") + } + + usageDelta := refreshedSource.UsageQty - sourceBeforeUsage[source.Id] + if usageDelta <= 0 { + continue } stockLogDecrease := &entity.StockLog{ ProductWarehouseId: *source.ProductWarehouseId, CreatedBy: actorID, Increase: 0, - Decrease: sourceShare, + Decrease: usageDelta, LoggableType: string(utils.StockLogTypeTransferLaying), LoggableId: approvableID, Notes: fmt.Sprintf("TL #%s", transfer.TransferNumber), @@ -867,26 +880,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) ( for _, target := range targets { if target.ProductWarehouseId == nil { - return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", approvableID)) - } - - note := fmt.Sprintf("Transfer to Laying #%s", transfer.TransferNumber) - _, err := s.FifoSvc.Replenish(c.Context(), commonSvc.StockReplenishRequest{ - StockableKey: fifo.StockableKeyTransferToLayingIn, - StockableID: target.Id, - ProductWarehouseID: *target.ProductWarehouseId, - Quantity: target.TotalQty, - Note: ¬e, - Tx: dbTransaction, - }) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Gagal replenish stock ke target warehouse: %v", err)) - } - - if err := targetRepoTx.PatchOne(c.Context(), target.Id, map[string]interface{}{ - "total_qty": target.TotalQty, - }, nil); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Gagal update target total qty") + continue } stockLogIncrease := &entity.StockLog{ diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index ba5f7384..313c4b7f 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -256,35 +256,13 @@ func (s *purchaseService) GetOne(c *fiber.Ctx, id uint) (*entity.Purchase, error s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err) } if len(purchase.Items) > 0 { - itemIDs := make([]uint, 0, len(purchase.Items)) - for i := range purchase.Items { - if purchase.Items[i].Id == 0 { - continue - } - itemIDs = append(itemIDs, purchase.Items[i].Id) + lockedIDs, err := s.resolveChickinLockedItemIDs(c.Context(), s.PurchaseRepo.DB(), purchase.Items) + if err != nil { + return nil, err } - if len(itemIDs) > 0 { - var usedIDs []uint - if err := s.PurchaseRepo.DB().WithContext(c.Context()). - Model(&entity.StockAllocation{}). - Distinct("stockable_id"). - Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?", - fifo.StockableKeyPurchaseItems.String(), - itemIDs, - fifo.UsableKeyProjectChickin.String(), - []string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending}, - ). - Pluck("stockable_id", &usedIDs).Error; err != nil { - return nil, err - } - usedSet := make(map[uint]struct{}, len(usedIDs)) - for _, id := range usedIDs { - usedSet[id] = struct{}{} - } - for i := range purchase.Items { - if _, ok := usedSet[purchase.Items[i].Id]; ok { - purchase.Items[i].HasChickin = true - } + for i := range purchase.Items { + if _, ok := lockedIDs[purchase.Items[i].Id]; ok { + purchase.Items[i].HasChickin = true } } } @@ -532,48 +510,31 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid } if action == entity.ApprovalActionApproved { - itemIDs := make([]uint, 0, len(purchase.Items)) itemByID := make(map[uint]entity.PurchaseItem, len(purchase.Items)) for i := range purchase.Items { if purchase.Items[i].Id == 0 { continue } - itemIDs = append(itemIDs, purchase.Items[i].Id) itemByID[purchase.Items[i].Id] = purchase.Items[i] } - if len(itemIDs) > 0 { - var usedIDs []uint - if err := s.PurchaseRepo.DB().WithContext(ctx). - Model(&entity.StockAllocation{}). - Distinct("stockable_id"). - Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?", - fifo.StockableKeyPurchaseItems.String(), - itemIDs, - fifo.UsableKeyProjectChickin.String(), - []string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending}, - ). - Pluck("stockable_id", &usedIDs).Error; err != nil { - return nil, err - } - if len(usedIDs) > 0 { - usedSet := make(map[uint]struct{}, len(usedIDs)) - for _, id := range usedIDs { - usedSet[id] = struct{}{} + lockedIDs, err := s.resolveChickinLockedItemIDs(ctx, s.PurchaseRepo.DB(), purchase.Items) + if err != nil { + return nil, err + } + if len(lockedIDs) > 0 { + for _, payload := range req.Items { + if payload.PurchaseItemID == 0 || payload.Qty == nil { + continue } - for _, payload := range req.Items { - if payload.PurchaseItemID == 0 || payload.Qty == nil { - continue - } - if _, used := usedSet[payload.PurchaseItemID]; !used { - continue - } - item, ok := itemByID[payload.PurchaseItemID] - if !ok { - continue - } - if *payload.Qty != item.SubQty { - return nil, utils.BadRequest("Purchase sudah chickin, qty tidak bisa diubah") - } + if _, locked := lockedIDs[payload.PurchaseItemID]; !locked { + continue + } + item, ok := itemByID[payload.PurchaseItemID] + if !ok { + continue + } + if *payload.Qty != item.SubQty { + return nil, utils.BadRequest("Purchase sudah chickin, qty tidak bisa diubah") } } } @@ -827,49 +788,32 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation } } if action == entity.ApprovalActionApproved { - itemIDs := make([]uint, 0, len(purchase.Items)) itemByID := make(map[uint]entity.PurchaseItem, len(purchase.Items)) for i := range purchase.Items { if purchase.Items[i].Id == 0 { continue } - itemIDs = append(itemIDs, purchase.Items[i].Id) itemByID[purchase.Items[i].Id] = purchase.Items[i] } - if len(itemIDs) > 0 { - var usedIDs []uint - if err := s.PurchaseRepo.DB().WithContext(ctx). - Model(&entity.StockAllocation{}). - Distinct("stockable_id"). - Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?", - fifo.StockableKeyPurchaseItems.String(), - itemIDs, - fifo.UsableKeyProjectChickin.String(), - []string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending}, - ). - Pluck("stockable_id", &usedIDs).Error; err != nil { - return nil, err - } - if len(usedIDs) > 0 { - usedSet := make(map[uint]struct{}, len(usedIDs)) - for _, id := range usedIDs { - usedSet[id] = struct{}{} + lockedIDs, err := s.resolveChickinLockedItemIDs(ctx, s.PurchaseRepo.DB(), purchase.Items) + if err != nil { + return nil, err + } + if len(lockedIDs) > 0 { + for _, payload := range req.Items { + if _, used := lockedIDs[payload.PurchaseItemID]; !used { + continue } - for _, payload := range req.Items { - if _, used := usedSet[payload.PurchaseItemID]; !used { - continue - } - item, ok := itemByID[payload.PurchaseItemID] - if !ok { - continue - } - receivedQty := item.SubQty - if payload.ReceivedQty != nil { - receivedQty = *payload.ReceivedQty - } - if receivedQty != item.TotalQty { - return nil, utils.BadRequest("Purchase sudah chickin, qty penerimaan tidak bisa diubah") - } + item, ok := itemByID[payload.PurchaseItemID] + if !ok { + continue + } + receivedQty := item.SubQty + if payload.ReceivedQty != nil { + receivedQty = *payload.ReceivedQty + } + if receivedQty != item.TotalQty { + return nil, utils.BadRequest("Purchase sudah chickin, qty penerimaan tidak bisa diubah") } } } @@ -1437,28 +1381,12 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error { } transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { - itemIDs := make([]uint, 0, len(itemsToDelete)) - for _, item := range itemsToDelete { - if item.Id == 0 { - continue - } - itemIDs = append(itemIDs, item.Id) + lockedIDs, err := s.resolveChickinLockedItemIDs(ctx, tx, itemsToDelete) + if err != nil { + return err } - if len(itemIDs) > 0 { - var count int64 - if err := tx.Model(&entity.StockAllocation{}). - Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?", - fifo.StockableKeyPurchaseItems.String(), - itemIDs, - fifo.UsableKeyProjectChickin.String(), - []string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending}, - ). - Count(&count).Error; err != nil { - return err - } - if count > 0 { - return utils.BadRequest("Purchase already chickin, failed to delete purchase") - } + if len(lockedIDs) > 0 { + return utils.BadRequest("Purchase already chickin, failed to delete purchase") } if err := s.rollbackPurchaseStock(ctx, tx, itemsToDelete, note, actorID); err != nil { @@ -1957,6 +1885,67 @@ func (s *purchaseService) applyTravelDocumentURLs(ctx context.Context, purchase } } +func collectPurchaseItemIDs(items []entity.PurchaseItem) []uint { + itemIDs := make([]uint, 0, len(items)) + for i := range items { + if items[i].Id == 0 { + continue + } + itemIDs = append(itemIDs, items[i].Id) + } + return itemIDs +} + +func (s *purchaseService) resolveChickinLockedItemIDs(ctx context.Context, db *gorm.DB, items []entity.PurchaseItem) (map[uint]struct{}, error) { + itemIDs := collectPurchaseItemIDs(items) + return s.resolveChickinLockedItemIDsByItemID(ctx, db, itemIDs) +} + +func (s *purchaseService) resolveChickinLockedItemIDsByItemID(ctx context.Context, db *gorm.DB, itemIDs []uint) (map[uint]struct{}, error) { + locked := make(map[uint]struct{}) + if len(itemIDs) == 0 { + return locked, nil + } + if db == nil { + return nil, errors.New("database is required") + } + + var allocationLockedIDs []uint + if err := db.WithContext(ctx). + Model(&entity.StockAllocation{}). + Distinct("stockable_id"). + Where("stockable_type = ? AND stockable_id IN ? AND usable_type = ? AND status IN ?", + fifo.StockableKeyPurchaseItems.String(), + itemIDs, + fifo.UsableKeyProjectChickin.String(), + []string{entity.StockAllocationStatusActive, entity.StockAllocationStatusPending}, + ). + Pluck("stockable_id", &allocationLockedIDs).Error; err != nil { + return nil, err + } + for _, itemID := range allocationLockedIDs { + locked[itemID] = struct{}{} + } + + var conversionLockedIDs []uint + if err := db.WithContext(ctx). + Table("purchase_items pi"). + Distinct("pi.id"). + Joins("JOIN project_chickins pc ON pc.product_warehouse_id = pi.product_warehouse_id AND pc.deleted_at IS NULL"). + Joins("JOIN project_flock_populations pfp ON pfp.project_chickin_id = pc.id AND pfp.deleted_at IS NULL"). + Where("pi.id IN ?", itemIDs). + Where("pi.project_flock_kandang_id IS NOT NULL"). + Where("pc.project_flock_kandang_id = pi.project_flock_kandang_id"). + Pluck("pi.id", &conversionLockedIDs).Error; err != nil { + return nil, err + } + for _, itemID := range conversionLockedIDs { + locked[itemID] = struct{}{} + } + + return locked, nil +} + func collectPFKIDsFromPurchase(p *entity.Purchase) []uint { seen := make(map[uint]struct{}) ids := make([]uint, 0)