package service import ( "context" "errors" "fmt" "strings" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" 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" KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/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" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" 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" "github.com/sirupsen/logrus" "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) CreateOne(ctx *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) DeleteOne(ctx *fiber.Ctx, id uint) error Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) } type chickinService struct { Log *logrus.Logger Validate *validator.Validate Repository repository.ProjectChickinRepository KandangRepo KandangRepo.KandangRepository WarehouseRepo rWarehouse.WarehouseRepository ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository ProjectFlockRepo rProjectFlock.ProjectflockRepository ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository ProjectChickinDetailRepo repository.ProjectChickinDetailRepository FifoSvc commonSvc.FifoService 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 { return &chickinService{ Log: utils.Log, Validate: validate, Repository: repo, KandangRepo: kandangRepo, WarehouseRepo: warehouseRepo, ProductWarehouseRepo: productWarehouseRepo, ProjectFlockRepo: projectFlockRepo, ProjectflockKandangRepo: projectflockkandangRepo, ProjectflockPopulationRepo: projectflockpopulationRepo, ProjectChickinDetailRepo: projectChickinDetailRepo, FifoSvc: fifoSvc, StockLogRepo: rStockLogs.NewStockLogRepository(repo.DB()), } } func (s chickinService) withRelations(db *gorm.DB) *gorm.DB { return db. Preload("CreatedUser"). Preload("ProjectFlockKandang.Kandang"). Preload("ProjectFlockKandang.Kandang.Location"). Preload("ProjectFlockKandang.Kandang.Location.Area"). Preload("ProjectFlockKandang.Kandang.Pic"). Preload("ProjectFlockKandang.ProjectFlock"). Preload("ProjectFlockKandang.ProjectFlock.Area"). Preload("ProjectFlockKandang.ProjectFlock.Fcr"). Preload("ProjectFlockKandang.ProjectFlock.Location"). Preload("ProjectFlockKandang.ProjectFlock.Location.Area") } func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit chickins, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) if params.ProjectFlockKandangId != 0 { return db.Where("project_flock_kandang_id = ?", params.ProjectFlockKandangId) } 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 } func (s chickinService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectChickin, error) { chickin, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found") } if err != nil { s.Log.Errorf("Failed get chickin by id: %+v", err) return nil, err } return chickin, nil } func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]entity.ProjectChickin, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } projectFlockKandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found") } warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectFlockKandang.KandangId) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse for Kandang not found") } actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err } newChikins := make([]*entity.ProjectChickin, 0) chickinQtyMap := make(map[uint]float64) for idx, chickinReq := range req.ChickinRequests { productWarehouse, err := s.ProductWarehouseRepo.GetByID(c.Context(), chickinReq.ProductWarehouseId, nil) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickinReq.ProductWarehouseId)) } if productWarehouse.WarehouseId != warehouse.Id { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d is not bound to kandang's warehouse", chickinReq.ProductWarehouseId)) } if productWarehouse.ProjectFlockKandangId == nil || *productWarehouse.ProjectFlockKandangId != req.ProjectFlockKandangId { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d is not attached to project_flock_kandang %d. Only product warehouses with matching project_flock_kandang_id can be chickin-ed", chickinReq.ProductWarehouseId, req.ProjectFlockKandangId)) } chickinDate, err := utils.ParseDateString(chickinReq.ChickInDate) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid ChickInDate format for product warehouse %d", chickinReq.ProductWarehouseId)) } availableQty := productWarehouse.Quantity if availableQty <= 0 { 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: 0, ProductWarehouseId: chickinReq.ProductWarehouseId, Notes: chickinReq.Note, CreatedBy: actorID, } newChikins = append(newChikins, newChickin) chickinQtyMap[uint(idx)] = availableQty } if len(newChikins) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "No chickins to create") } existingChikins, err := s.Repository.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing chickins") } isFirstTime := len(existingChikins) == 0 err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins") } for idx, chickin := range newChikins { desiredQty := chickinQtyMap[uint(idx)] if err := s.ConsumeChickinStocks(c.Context(), dbTransaction, chickin, desiredQty, actorID); err != nil { 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") } var approvalAction entity.ApprovalAction if isFirstTime { approvalAction = entity.ApprovalActionCreated } else { approvalAction = entity.ApprovalActionUpdated } if latest == nil { if _, err := approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowChickin, projectFlockKandang.Id, utils.ChickinStepPengajuan, &approvalAction, actorID, nil); err != nil { if !errors.Is(err, gorm.ErrDuplicatedKey) { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") } } } else if latest.StepNumber != uint16(utils.ChickinStepPengajuan) { if _, err := approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowChickin, projectFlockKandang.Id, utils.ChickinStepPengajuan, &approvalAction, actorID, nil); err != nil { if !errors.Is(err, gorm.ErrDuplicatedKey) { 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 create chickins") } result := make([]entity.ProjectChickin, 0, len(newChikins)) for _, chickin := range newChikins { loaded, err := s.GetOne(c, chickin.Id) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reload chickin %d with relations: %v", chickin.Id, err)) } result = append(result, *loaded) } if len(result) == 0 { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load created chickins") } return result, nil } func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } updateBody := make(map[string]any) if req.ChickInDate != "" { updateBody["chick_in_date"] = req.ChickInDate } if req.Note != "" { updateBody["notes"] = req.Note } if len(updateBody) == 0 { return s.GetOne(c, id) } if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { 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 } return s.GetOne(c, id) } 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 } actorID, err := m.ActorIDFromContext(c) if err != nil { return err } if chickin.UsageQty > 0 { if err := s.ReleaseChickinStocks(c.Context(), s.Repository.DB(), chickin, actorID); 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") } return err } return 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 } actorID, err := m.ActorIDFromContext(c) if err != nil { return nil, err } approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB())) var action entity.ApprovalAction switch strings.ToUpper(strings.TrimSpace(req.Action)) { case string(entity.ApprovalActionRejected): action = entity.ApprovalActionRejected case string(entity.ApprovalActionApproved): action = entity.ApprovalActionApproved default: return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") } 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 := 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 { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") } if latestApproval == nil { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for ProjectFlockKandang %d - chickins must be created first", id)) } if latestApproval.StepNumber != uint16(utils.ChickinStepPengajuan) { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ProjectFlockKandang %d cannot be approved - current status is not in PENGAJUAN stage", id)) } } step := utils.ChickinStepPengajuan if action == entity.ApprovalActionApproved { step = utils.ChickinStepDisetujui } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) chickinRepoTx := repository.NewChickinRepository(dbTransaction) for _, approvableID := range approvableIDs { if _, err := approvalSvc.CreateApproval( c.Context(), utils.ApprovalWorkflowChickin, approvableID, step, &action, actorID, req.Notes, ); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") } if action == entity.ApprovalActionApproved { chickins, err := chickinRepoTx.GetByProjectFlockKandangID(c.Context(), approvableID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get chickins for approval %d", approvableID)) } kandangForApproval, 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") } category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category)) 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") } } else if category == string(utils.ProjectFlockCategoryLaying) { 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, "LAYER", dbTransaction, actorID, &pfkID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create LAYER product warehouse") } if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target") } } } if action == entity.ApprovalActionRejected { chickins, err := chickinRepoTx.GetPendingByProjectFlockKandangID(c.Context(), approvableID) if err != nil { return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get pending chickins for rejection %d", approvableID)) } if len(chickins) == 0 { continue } for _, chickin := range chickins { 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)) } 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 { if !errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to delete rejected chickin %d", chickin.Id)) } } } } } return nil }) if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") } updated := make([]entity.ProjectChickin, 0) for _, kandangID := range approvableIDs { var chickins []entity.ProjectChickin if err := s.Repository.DB().WithContext(c.Context()).Where("project_flock_kandang_id = ?", kandangID).Find(&chickins).Error; err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load approved chickins") } updated = append(updated, chickins...) } 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 } product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode) 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) } newPW := &entity.ProductWarehouse{ ProductId: product.Id, WarehouseId: warehouseId, ProjectFlockKandangId: projectFlockKandangId, Quantity: 0, } 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) } 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") } ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.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: quantityToConvert, TotalUsedQty: 0, Notes: chickin.Notes, CreatedBy: actorID, } 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, desiredQty float64, actorID uint) error { if chickin == nil || s.FifoSvc == nil { 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, ProductWarehouseID: chickin.ProductWarehouseId, Quantity: desiredQty, AllowPending: true, 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 := 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 } if result.UsageQuantity > 0 { decreaseLog := &entity.StockLog{ Decrease: result.UsageQuantity, LoggableType: string(utils.StockLogTypeChikin), LoggableId: chickin.Id, ProductWarehouseId: chickin.ProductWarehouseId, 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) } } return nil } func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB, chickin *entity.ProjectChickin, actorID uint) error { if chickin == nil || s.FifoSvc == nil { return nil } var currentUsage float64 if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Select("usage_qty").Scan(¤tUsage).Error; err != nil { s.Log.Warnf("Failed to get current usage for chickin %d: %+v", chickin.Id, err) currentUsage = 0 } 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 } // Create stock log for the restoration if currentUsage > 0 { increaseLog := &entity.StockLog{ Increase: currentUsage, LoggableType: string(utils.StockLogTypeChikin), LoggableId: chickin.Id, ProductWarehouseId: chickin.ProductWarehouseId, 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 chickin %d: %+v", chickin.Id, err) // Don't return error here, stock already released } } 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 }) }