From 306cf11feec27e5a0657a13fa562843caaa93db5 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 23 Dec 2025 12:26:35 +0700 Subject: [PATCH] Feat[BE]: integrate FIFO service for chickin stock management --- .../modules/production/chickins/module.go | 32 ++- .../chickins/services/chickin.service.go | 249 ++++++++++-------- internal/utils/fifo/constants.go | 1 + 3 files changed, 169 insertions(+), 113 deletions(-) diff --git a/internal/modules/production/chickins/module.go b/internal/modules/production/chickins/module.go index f4e91056..df0ebd26 100644 --- a/internal/modules/production/chickins/module.go +++ b/internal/modules/production/chickins/module.go @@ -2,6 +2,7 @@ package chickins import ( "fmt" + "strings" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -9,6 +10,7 @@ import ( 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/utils/fifo" 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" @@ -36,16 +38,44 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * projectflockpopulationrepo := rProjectFlock.NewProjectFlockPopulationRepository(db) projectFlockRepo := rProjectFlock.NewProjectflockRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) + stockAllocationRepo := commonRepo.NewStockAllocationRepository(db) userRepo := rUser.NewUserRepository(db) + fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log) + if err := fifoService.RegisterUsable(fifo.UsableConfig{ + Key: fifo.UsablekeyProjectChickin, + Table: "project_chickins", + Columns: fifo.UsableColumns{ + ID: "id", + ProductWarehouseID: "product_warehouse_id", + UsageQuantity: "usage_qty", + PendingQuantity: "pending_usage_qty", + CreatedAt: "id", + }, + }); err != nil { + if !strings.Contains(strings.ToLower(err.Error()), "already registered") { + panic(fmt.Sprintf("failed to register chickin usable workflow: %v", err)) + } + } + approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil { panic(fmt.Sprintf("failed to register chickin approval workflow: %v", err)) } - chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate) + chickinService := sChickin.NewChickinService( + chickinRepo, + kandangRepo, + warehouseRepo, + productWarehouseRepo, + projectFlockRepo, + projectflockkandangrepo, + projectflockpopulationrepo, + chickinDetailRepo, + validate, + fifoService) userService := sUser.NewUserService(userRepo, validate) ChickinRoutes(router, userService, chickinService) diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 0c513e88..fe78080b 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -1,6 +1,7 @@ package service import ( + "context" "errors" "fmt" "strings" @@ -16,6 +17,7 @@ import ( validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/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" @@ -23,6 +25,8 @@ import ( "gorm.io/gorm" ) +var chickinUsableKey = fifo.UsablekeyProjectChickin + type ChickinService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectChickin, error) @@ -43,9 +47,10 @@ type chickinService struct { ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository ProjectChickinDetailRepo repository.ProjectChickinDetailRepository + FifoSvc commonSvc.FifoService } -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) ChickinService { +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 { return &chickinService{ Log: utils.Log, Validate: validate, @@ -57,6 +62,7 @@ func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo Kan ProjectflockKandangRepo: projectflockkandangRepo, ProjectflockPopulationRepo: projectflockpopulationRepo, ProjectChickinDetailRepo: projectChickinDetailRepo, + FifoSvc: fifoSvc, } } @@ -124,8 +130,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse for Kandang not found") } - category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category)) - actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err @@ -152,20 +156,16 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid ChickInDate format for product warehouse %d", chickinReq.ProductWarehouseId)) } - availableQty, err := s.calculateAvailableQuantity(c, req.ProjectFlockKandangId, productWarehouse, category) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to calculate available quantity for product warehouse %d", chickinReq.ProductWarehouseId)) - } - + availableQty := productWarehouse.Quantity if availableQty <= 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No available quantity for product warehouse %d", chickinReq.ProductWarehouseId)) + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No available stock in product warehouse %d for chickin", chickinReq.ProductWarehouseId)) } newChickin := &entity.ProjectChickin{ ProjectFlockKandangId: req.ProjectFlockKandangId, ChickInDate: chickinDate, - UsageQty: 0, - PendingUsageQty: availableQty, + UsageQty: availableQty, + PendingUsageQty: 0, ProductWarehouseId: chickinReq.ProductWarehouseId, Notes: chickinReq.Note, CreatedBy: actorID, @@ -193,6 +193,25 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti return fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins") } + for _, chickin := range newChikins { + if err := s.ConsumeChickinStocks(c.Context(), dbTransaction, chickin); err != nil { + return err + } + + if chickin.PendingUsageQty > 0 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot create chickin %d - insufficient stock. Required: %.0f, Available: %.0f, Pending: %.0f", chickin.Id, chickin.UsageQty+chickin.PendingUsageQty, chickin.UsageQty, chickin.PendingUsageQty)) + } + } + + warehouseDeltas := make(map[uint]float64) + for _, chickin := range newChikins { + warehouseDeltas[chickin.ProductWarehouseId] -= chickin.UsageQty + } + if err := s.adjustProductWarehouseQuantities(c.Context(), dbTransaction, warehouseDeltas); err != nil { + s.Log.Errorf("Failed to adjust product warehouses: %+v", err) + return err + } + latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, projectFlockKandang.Id, nil) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get latest approval") @@ -287,6 +306,27 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { + chickin, err := s.Repository.GetByID(c.Context(), id, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Chickin not found") + } + return err + } + + if chickin.UsageQty > 0 { + if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin); err != nil { + return err + } + + warehouseDeltas := make(map[uint]float64) + warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty + 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 + } + } + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Chickin not found") @@ -297,54 +337,6 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { return nil } -func (s chickinService) calculateAvailableQuantity(ctx *fiber.Ctx, projectFlockKandangID uint, productWarehouse *entity.ProductWarehouse, category string) (float64, error) { - availableQty := productWarehouse.Quantity - - if category == string(utils.ProjectFlockCategoryGrowing) { - var totalPendingQty float64 - - chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID) - if err == nil { - for _, chickin := range chickins { - - if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 { - totalPendingQty += chickin.PendingUsageQty - } - } - } - - availableQty = productWarehouse.Quantity - totalPendingQty - if availableQty < 0 { - availableQty = 0 - } - } else if category == string(utils.ProjectFlockCategoryLaying) { - var totalPopulation float64 - var totalPendingQty float64 - - populations, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangIDAndProductWarehouseID(ctx.Context(), projectFlockKandangID, productWarehouse.Id) - if err == nil { - for _, pop := range populations { - totalPopulation += pop.TotalQty - } - } - chickins, err := s.Repository.GetByProjectFlockKandangID(ctx.Context(), projectFlockKandangID) - if err == nil { - for _, chickin := range chickins { - - if chickin.ProductWarehouseId == productWarehouse.Id && chickin.DeletedAt.Time.IsZero() && chickin.PendingUsageQty > 0 { - totalPendingQty += chickin.PendingUsageQty - } - } - } - availableQty = productWarehouse.Quantity - totalPopulation - totalPendingQty - if availableQty < 0 { - availableQty = 0 - } - } - - return availableQty, nil -} - func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) { if err := s.Validate.Struct(req); err != nil { return nil, err @@ -373,11 +365,10 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit } for _, id := range approvableIDs { - idCopy := id - if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProjectFlockKandang", ID: &idCopy, Exists: s.ProjectflockKandangRepo.IdExists}); err != nil { + + if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "ProjectFlockKandang", ID: &id, Exists: s.ProjectflockKandangRepo.IdExists}); err != nil { return nil, err } - latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, id, nil) if err != nil { @@ -400,7 +391,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) chickinRepoTx := repository.NewChickinRepository(dbTransaction) - productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction) for _, approvableID := range approvableIDs { if _, err := approvalSvc.CreateApproval( @@ -477,27 +467,17 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit continue } - kandangForRejection, err := s.ProjectflockKandangRepo.GetByID(c.Context(), approvableID) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID)) - } - return fiber.NewError(fiber.StatusInternalServerError, "Failed to get ProjectFlockKandang") - } - - categoryForRejection := strings.ToUpper(strings.TrimSpace(kandangForRejection.ProjectFlock.Category)) - for _, chickin := range chickins { - if categoryForRejection == string(utils.ProjectFlockCategoryGrowing) { - updates := map[string]any{"qty": gorm.Expr("qty + ?", chickin.PendingUsageQty)} + if err := s.ReleaseChickinStocks(c.Context(), dbTransaction, &chickin); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to release stock for rejected chickin %d: %v", chickin.Id, err)) + } - if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found during rejection", chickin.ProductWarehouseId)) - } - return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to restore product warehouse quantity for chickin %d", chickin.Id)) - } + 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 } if err := chickinRepoTx.DeleteOne(c.Context(), chickin.Id); err != nil { @@ -558,7 +538,6 @@ func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId WarehouseId: warehouseId, ProjectFlockKandangId: projectFlockKandangId, Quantity: 0, - // CreatedBy: actorID, } if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil { @@ -574,10 +553,10 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti return fmt.Errorf("invalid target product warehouse") } - chickinRepoTx := repository.NewChickinRepository(dbTransaction) - productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction) ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction) + var totalQuantityAdded float64 + for _, chickin := range chickins { populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id) @@ -590,34 +569,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti continue } - quantityToConvert := chickin.PendingUsageQty - - if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{ - "usage_qty": quantityToConvert, - "pending_usage_qty": 0, - }, nil); err != nil { - return fmt.Errorf("failed to update chickin %d qty: %w", chickin.Id, err) - } - - if chickin.ProductWarehouseId != targetPW.Id { - if err := productWarehouseTx.PatchOne(ctx.Context(), chickin.ProductWarehouseId, map[string]any{ - "qty": gorm.Expr("qty - ?", quantityToConvert), - }, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Source product warehouse %d not found", chickin.ProductWarehouseId)) - } - return fmt.Errorf("failed to deduct source warehouse quantity for chickin %d: %w", chickin.Id, err) - } - } - - if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{ - "qty": gorm.Expr("qty + ?", quantityToConvert), - }, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Target product warehouse %d not found", targetPW.Id)) - } - return fmt.Errorf("failed to update target warehouse quantity: %w", err) - } + quantityToConvert := chickin.UsageQty population := &entity.ProjectFlockPopulation{ ProjectChickinId: chickin.Id, @@ -630,7 +582,80 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil { return err } + + totalQuantityAdded += quantityToConvert + } + + if totalQuantityAdded > 0 { + if err := s.ProductWarehouseRepo.AdjustQuantities(ctx.Context(), map[uint]float64{ + targetPW.Id: totalQuantityAdded, + }, func(db *gorm.DB) *gorm.DB { + return dbTransaction + }); err != nil { + return fmt.Errorf("failed to update target product warehouse quantity: %w", err) + } } return nil } + +func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin) error { + if chickin == nil || s.FifoSvc == nil { + return nil + } + + var desired float64 = chickin.UsageQty + + result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{ + UsableKey: chickinUsableKey, + UsableID: chickin.Id, + ProductWarehouseID: chickin.ProductWarehouseId, + Quantity: desired, + AllowPending: false, + Tx: tx, + }) + if err != nil { + s.Log.Errorf("Failed to consume FIFO stock for chickin %d: %+v", chickin.Id, err) + return err + } + + if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Updates(map[string]interface{}{ + "usage_qty": result.UsageQuantity, + "pending_usage_qty": result.PendingQuantity, + }).Error; err != nil { + return err + } + + return nil +} + +func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin) error { + if chickin == nil || s.FifoSvc == nil { + return nil + } + + if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{ + UsableKey: chickinUsableKey, + UsableID: chickin.Id, + Tx: tx, + }); err != nil { + s.Log.Errorf("Failed to release FIFO stock for chickin %d: %+v", chickin.Id, err) + return err + } + + if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Updates(map[string]interface{}{ + "usage_qty": 0, + "pending_usage_qty": 0, + }).Error; err != nil { + return err + } + + return nil +} + +func (s *chickinService) adjustProductWarehouseQuantities(ctx context.Context, tx *gorm.DB, deltas map[uint]float64) error { + if len(deltas) == 0 { + return nil + } + return s.ProductWarehouseRepo.AdjustQuantities(ctx, deltas, func(*gorm.DB) *gorm.DB { return tx }) +} diff --git a/internal/utils/fifo/constants.go b/internal/utils/fifo/constants.go index c47d3cd7..c1a79444 100644 --- a/internal/utils/fifo/constants.go +++ b/internal/utils/fifo/constants.go @@ -2,4 +2,5 @@ package fifo const ( UsableKeyRecordingStock UsableKey = "RECORDING_STOCK" + UsablekeyProjectChickin UsableKey = "PROJECT_CHICKIN" )