diff --git a/internal/modules/production/chickins/repositories/project_chickin.repository.go b/internal/modules/production/chickins/repositories/project_chickin.repository.go index 43cafaac..ffdae20b 100644 --- a/internal/modules/production/chickins/repositories/project_chickin.repository.go +++ b/internal/modules/production/chickins/repositories/project_chickin.repository.go @@ -16,6 +16,7 @@ type ProjectChickinRepository interface { GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error) + GetByProjectFlockKandangIDForUpdate(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) } type ChickinRepositoryImpl struct { @@ -64,6 +65,20 @@ func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, return chickins, nil } +// GetByProjectFlockKandangIDForUpdate locks chickin rows to prevent race condition +func (r *ChickinRepositoryImpl) GetByProjectFlockKandangIDForUpdate(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) { + var chickins []entity.ProjectChickin + err := r.db.WithContext(ctx). + Where("project_flock_kandang_id = ?", projectFlockKandangID). + Order("created_at DESC"). + Find(&chickins). + Error + if err != nil { + return nil, err + } + return chickins, nil +} + func (r *ChickinRepositoryImpl) GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) { var chickins []entity.ProjectChickin err := r.db.WithContext(ctx). diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 871c8fce..5ba665ca 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -137,6 +137,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti if err != nil { return nil, err } + newChikins := make([]*entity.ProjectChickin, 0) chickinQtyMap := make(map[uint]float64) @@ -151,8 +152,8 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti 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)) + if productWarehouse.ProjectFlockKandangId != nil && *productWarehouse.ProjectFlockKandangId != req.ProjectFlockKandangId { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d belongs to different flock. Only product warehouses with project_flock_kandang_id = NULL or = %d can be used", chickinReq.ProductWarehouseId, req.ProjectFlockKandangId)) } chickinDate, err := utils.ParseDateString(chickinReq.ChickInDate) @@ -160,11 +161,6 @@ 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 := 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, @@ -176,22 +172,46 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti } newChikins = append(newChikins, newChickin) - chickinQtyMap[uint(idx)] = availableQty + chickinQtyMap[uint(idx)] = productWarehouse.Quantity } 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 { + repositoryTx := repository.NewChickinRepository(dbTransaction) + existingChikins, err := repositoryTx.GetByProjectFlockKandangIDForUpdate(c.Context(), req.ProjectFlockKandangId) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing chickins") + } + + isFirstTime := len(existingChikins) == 0 + + pendingQtyMap := make(map[uint]float64) + for _, existingChickin := range existingChikins { + if existingChickin.PendingUsageQty > 0 { + pendingQtyMap[existingChickin.ProductWarehouseId] += existingChickin.PendingUsageQty + } + } + + for idx, chickin := range newChikins { + pendingQty := pendingQtyMap[chickin.ProductWarehouseId] + desiredQty := chickinQtyMap[uint(idx)] + + availableQty := desiredQty - pendingQty + if availableQty < 0 { + availableQty = 0 + } + + if availableQty <= 0 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No available stock in product warehouse %d for chickin. Warehouse: %.0f, Pending: %.0f, Available: %.0f", chickin.ProductWarehouseId, desiredQty, pendingQty, availableQty)) + } + + chickinQtyMap[uint(idx)] = availableQty + } + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil { diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 66fee8ce..6176daeb 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -191,7 +191,7 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project result := make(map[uint]float64) for _, pw := range products { - if pw.ProjectFlockKandangId != nil && *pw.ProjectFlockKandangId == projectFlockKandang.Id { + if pw.ProjectFlockKandangId == nil || *pw.ProjectFlockKandangId == projectFlockKandang.Id { availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw) if err != nil { s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err)