diff --git a/internal/modules/production/chickins/module.go b/internal/modules/production/chickins/module.go index 143ebad2..09514f0d 100644 --- a/internal/modules/production/chickins/module.go +++ b/internal/modules/production/chickins/module.go @@ -14,6 +14,7 @@ import ( rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" + rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" sChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services" @@ -38,6 +39,7 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * projectflockpopulationrepo := rProjectFlock.NewProjectFlockPopulationRepository(db) projectFlockRepo := rProjectFlock.NewProjectflockRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) + productRepo := rProduct.NewProductRepository(db) stockAllocationRepo := commonRepo.NewStockAllocationRepository(db) fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log) userRepo := rUser.NewUserRepository(db) @@ -88,6 +90,7 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * kandangRepo, warehouseRepo, productWarehouseRepo, + productRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index de49bb1e..eabe596c 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -12,6 +12,7 @@ import ( m "gitlab.com/mbugroup/lti-api.git/internal/middleware" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" + rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations" @@ -44,6 +45,7 @@ type chickinService struct { KandangRepo KandangRepo.KandangRepository WarehouseRepo rWarehouse.WarehouseRepository ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository + ProductRepo rProduct.ProductRepository ProjectFlockRepo rProjectFlock.ProjectflockRepository ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository @@ -52,7 +54,7 @@ type chickinService struct { StockLogRepo rStockLogs.StockLogRepository } -func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate, fifoSvc commonSvc.FifoService) ChickinService { +func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, productRepo rProduct.ProductRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate, fifoSvc commonSvc.FifoService) ChickinService { return &chickinService{ Log: utils.Log, Validate: validate, @@ -60,6 +62,7 @@ func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo Kan KandangRepo: kandangRepo, WarehouseRepo: warehouseRepo, ProductWarehouseRepo: productWarehouseRepo, + ProductRepo: productRepo, ProjectFlockRepo: projectFlockRepo, ProjectflockKandangRepo: projectflockkandangRepo, ProjectflockPopulationRepo: projectflockpopulationRepo, @@ -99,7 +102,6 @@ func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity return db.Order("created_at DESC").Order("updated_at DESC") }) if err != nil { - s.Log.Errorf("Failed to get chickins: %+v", err) return nil, 0, err } return chickins, total, nil @@ -347,7 +349,6 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found") } - s.Log.Errorf("Failed to update chickin: %+v", err) return nil, err } @@ -380,7 +381,6 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { warehouseDeltas := make(map[uint]float64) warehouseDeltas[chickin.ProductWarehouseId] += currentUsageQty if err := s.adjustProductWarehouseQuantities(c.Context(), s.Repository.DB(), warehouseDeltas); err != nil { - s.Log.Errorf("Failed to adjust product warehouses for deleted chickin %d: %+v", chickin.Id, err) return err } } @@ -449,6 +449,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) chickinRepoTx := repository.NewChickinRepository(dbTransaction) + ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction) for _, approvableID := range approvableIDs { if _, err := approvalSvc.CreateApproval( @@ -479,39 +480,55 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category)) + var targetFlag utils.FlagType if category == string(utils.ProjectFlockCategoryGrowing) { - warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId)) - } - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse") - } - - pfkID := approvableID - targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID, &pfkID) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse") - } - if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target") - } + targetFlag = utils.FlagPullet } else if category == string(utils.ProjectFlockCategoryLaying) { - warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId) + targetFlag = utils.FlagLayer + } else { + continue + } + + for _, chickin := range chickins { + populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(c.Context(), chickin.Id) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId)) - } - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse") + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to check population for chickin %d", chickin.Id)) + } + if populationExists { + continue } - pfkID := approvableID - targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "LAYER", dbTransaction, actorID, &pfkID) + sourcePW, err := s.ProductWarehouseRepo.GetByID(c.Context(), chickin.ProductWarehouseId, func(db *gorm.DB) *gorm.DB { + return db.Preload("Product.Flags") + }) if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create LAYER product warehouse") + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for chickin %d", chickin.Id)) } - if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target") + + if err := s.autoAddFlagToProduct(c.Context(), dbTransaction, sourcePW.Product.Id, targetFlag); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to auto-add flag to product %d", sourcePW.Product.Id)) + } + + population := &entity.ProjectFlockPopulation{ + ProjectChickinId: chickin.Id, + ProductWarehouseId: sourcePW.Id, + TotalQty: 0, + TotalUsedQty: 0, + Notes: chickin.Notes, + CreatedBy: actorID, + } + if err := ProjectFlockPopulationRepotx.CreateOne(c.Context(), population, nil); err != nil { + 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)) } } } @@ -534,7 +551,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit warehouseDeltas := make(map[uint]float64) warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty if err := s.adjustProductWarehouseQuantities(c.Context(), dbTransaction, warehouseDeltas); err != nil { - s.Log.Errorf("Failed to adjust product warehouses for rejected chickin %d: %+v", chickin.Id, err) return err } @@ -568,104 +584,35 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit return updated, nil } -func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint, projectFlockKandangId *uint) (*entity.ProductWarehouse, error) { - - products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId) - if err == nil && len(products) > 0 { - existingPW := &products[0] - - if existingPW.ProjectFlockKandangId == nil && projectFlockKandangId != nil { - existingPW.ProjectFlockKandangId = projectFlockKandangId - if err := s.ProductWarehouseRepo.WithTx(dbTransaction).UpdateOne(ctx.Context(), existingPW.Id, existingPW, nil); err != nil { - return nil, fmt.Errorf("failed to update %s product warehouse with project_flock_kandang_id: %w", categoryCode, err) - } - } - return existingPW, nil +// autoAddFlagToProduct adds target flag to product if not already present (idempotent) +func (s *chickinService) autoAddFlagToProduct(ctx context.Context, tx *gorm.DB, productID uint, targetFlag utils.FlagType) error { + if s.ProductRepo == nil { + return nil } - product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode) + currentFlags, err := s.ProductRepo.GetFlags(ctx, productID) if err != nil { - return nil, fmt.Errorf("failed to get %s product: %w", categoryCode, err) - } - if product == nil { - return nil, fmt.Errorf("no %s product found in system", categoryCode) + return fmt.Errorf("failed to get product flags: %w", err) } - newPW := &entity.ProductWarehouse{ - ProductId: product.Id, - WarehouseId: warehouseId, - ProjectFlockKandangId: projectFlockKandangId, - Quantity: 0, + hasTargetFlag := false + currentFlagNames := make([]string, 0, len(currentFlags)) + for _, flag := range currentFlags { + currentFlagNames = append(currentFlagNames, flag.Name) + if flag.Name == string(targetFlag) { + hasTargetFlag = true + } } - if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil { - return nil, fmt.Errorf("failed to create %s product warehouse: %w", categoryCode, err) + if hasTargetFlag { + return nil } - return newPW, nil -} - -func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []entity.ProjectChickin, targetPW *entity.ProductWarehouse, dbTransaction *gorm.DB, actorID uint) error { - - if targetPW == nil || targetPW.Id == 0 { - return fmt.Errorf("invalid target product warehouse") + newFlags := append(currentFlagNames, string(targetFlag)) + if err := s.ProductRepo.SyncFlags(ctx, tx, productID, newFlags); err != nil { + return fmt.Errorf("failed to sync flags: %w", err) } - ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction) - chickinRepoTx := s.Repository.WithTx(dbTransaction) - - var totalQuantityAdded float64 - - for _, chickin := range chickins { - - populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id) - if err != nil { - return fmt.Errorf("failed to check population existence for chickin %d: %w", chickin.Id, err) - } - - if populationExists { - s.Log.Infof("population already exists for chickin %d, skipping", chickin.Id) - continue - } - - quantityToConvert := chickin.UsageQty - - population := &entity.ProjectFlockPopulation{ - ProjectChickinId: chickin.Id, - ProductWarehouseId: targetPW.Id, - TotalQty: 0, // Will be set by FIFO Replenish - TotalUsedQty: 0, - Notes: chickin.Notes, - CreatedBy: actorID, - } - if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil { - return err - } - - // Reset PendingUsageQty to 0 since population has been created - if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{ - "pending_usage_qty": 0, - }, nil); err != nil { - return fmt.Errorf("failed to reset pending usage qty for chickin %d: %w", chickin.Id, err) - } - - // Replenish stock to target ProductWarehouse based on source flag - // StockableKey is PROJECT_CHICKIN but StockableID refers to Population ID - if err := s.ReplenishChickinStocks(ctx.Context(), dbTransaction, &chickin, targetPW, population, actorID); err != nil { - s.Log.Errorf("Failed to replenish stock for chickin %d: %+v", chickin.Id, err) - return err - } - - totalQuantityAdded += quantityToConvert - } - - // NOTE: ProductWarehouse target sudah ditambah melalui ReplenishChickinStocks - // yang dipanggil di atas untuk setiap chickin berdasarkan flag source: - // - DOC → replenish ke PULLET - // - PULLET → replenish ke LAYER - // - LAYER → tidak perlu replenish (sudah final) - // - DOC+PULLET+LAYER → replenish ke dirinya sendiri - return nil } @@ -674,9 +621,6 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB, return nil } - s.Log.Infof("ConsumeChickinStocks: chickin_id=%d, product_warehouse_id=%d, desired_qty=%.3f", - chickin.Id, chickin.ProductWarehouseId, desiredQty) - result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{ UsableKey: chickinUsableKey, UsableID: chickin.Id, @@ -686,13 +630,9 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB, Tx: tx, }) if err != nil { - s.Log.Errorf("Failed to consume FIFO stock for chickin %d: %+v", chickin.Id, err) return err } - s.Log.Infof("ConsumeChickinStocks result: usage_qty=%.3f, pending_qty=%.3f, allocated_allocations=%d", - result.UsageQuantity, result.PendingQuantity, len(result.AddedAllocations)) - if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, result.UsageQuantity, result.PendingQuantity); err != nil { return err } @@ -706,10 +646,7 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB, CreatedBy: actorID, Notes: fmt.Sprintf("Chickin #%d", chickin.Id), } - if err := s.StockLogRepo.CreateOne(ctx, decreaseLog, nil); err != nil { - s.Log.Errorf("Failed to create stock log for chickin %d: %+v", chickin.Id, err) - - } + s.StockLogRepo.CreateOne(ctx, decreaseLog, nil) } return nil @@ -720,93 +657,17 @@ func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB return nil } - sourcePW, err := s.ProductWarehouseRepo.GetByID(ctx, chickin.ProductWarehouseId, func(db *gorm.DB) *gorm.DB { - return db.Preload("Product.Flags") + _, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{ + StockableKey: fifo.StockableKeyProjectFlockPopulation, + StockableID: population.Id, + ProductWarehouseID: targetPW.Id, + Quantity: chickin.UsageQty, + Tx: tx, }) if err != nil { - return err } - if sourcePW == nil || sourcePW.Product.Id == 0 { - return fmt.Errorf("source product warehouse or product not found for chickin %d", chickin.Id) - } - sourceFlags := sourcePW.Product.Flags - if len(sourceFlags) == 0 { - s.Log.Warnf("Source product %d has no flags, skipping replenish for chickin %d", sourcePW.Product.Id, chickin.Id) - return nil - } - - hasDoc := false - hasPullet := false - hasLayer := false - for _, flag := range sourceFlags { - flagName := utils.FlagType(flag.Name) - if flagName == utils.FlagDOC { - hasDoc = true - } else if flagName == utils.FlagPullet { - hasPullet = true - } else if flagName == utils.FlagLayer { - hasLayer = true - } - } - - if hasDoc && hasPullet && hasLayer { - s.Log.Infof("Chickin %d has mixed flags (DOC+PULLET+LAYER), replenishing to source PW %d", chickin.Id, sourcePW.Id) - _, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{ - StockableKey: fifo.StockableKeyProjectFlockPopulation, - StockableID: population.Id, - ProductWarehouseID: sourcePW.Id, - Quantity: chickin.UsageQty, - Tx: tx, - }) - if err != nil { - s.Log.Errorf("Failed to replenish stock to source PW for chickin %d: %+v", chickin.Id, err) - return err - } - return nil - } - - // LAYER only - no replenish needed - if hasLayer && !hasDoc && !hasPullet { - s.Log.Infof("Chickin %d has LAYER flag only, skipping replenish", chickin.Id) - return nil - } - - if hasDoc && !hasPullet && !hasLayer { - s.Log.Infof("Chickin %d has DOC flag, replenishing to PULLET PW %d", chickin.Id, targetPW.Id) - _, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{ - StockableKey: fifo.StockableKeyProjectFlockPopulation, - StockableID: population.Id, - ProductWarehouseID: targetPW.Id, - Quantity: chickin.UsageQty, - Tx: tx, - }) - if err != nil { - s.Log.Errorf("Failed to replenish stock to PULLET PW for chickin %d: %+v", chickin.Id, err) - return err - } - return nil - } - - if hasPullet && !hasDoc && !hasLayer { - s.Log.Infof("Chickin %d has PULLET flag, replenishing to LAYER PW %d", chickin.Id, targetPW.Id) - _, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{ - StockableKey: fifo.StockableKeyProjectFlockPopulation, - StockableID: population.Id, - ProductWarehouseID: targetPW.Id, - Quantity: chickin.UsageQty, - Tx: tx, - }) - if err != nil { - s.Log.Errorf("Failed to replenish stock to LAYER PW for chickin %d: %+v", chickin.Id, err) - return err - } - return nil - } - - // Other combinations (e.g., DOC + PULLET without LAYER) - skip for now - s.Log.Warnf("Chickin %d has unsupported flag combination, skipping replenish", chickin.Id) return nil } @@ -825,7 +686,6 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, UsableID: chickin.Id, Tx: tx, }); err != nil { - s.Log.Errorf("Failed to release FIFO stock for chickin %d: %+v", chickin.Id, err) return err } @@ -842,9 +702,7 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, CreatedBy: actorID, Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id), } - if err := s.StockLogRepo.CreateOne(ctx, increaseLog, nil); err != nil { - s.Log.Errorf("Failed to create stock log for released chickin %d: %+v", chickin.Id, err) - } + s.StockLogRepo.CreateOne(ctx, increaseLog, nil) } return nil