diff --git a/internal/modules/production/chickins/repositories/project_chickin.repository.go b/internal/modules/production/chickins/repositories/project_chickin.repository.go index ffdae20b..f4cbf5e3 100644 --- a/internal/modules/production/chickins/repositories/project_chickin.repository.go +++ b/internal/modules/production/chickins/repositories/project_chickin.repository.go @@ -68,11 +68,17 @@ func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, // GetByProjectFlockKandangIDForUpdate locks chickin rows to prevent race condition func (r *ChickinRepositoryImpl) GetByProjectFlockKandangIDForUpdate(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) { var chickins []entity.ProjectChickin + // CRITICAL: Use FOR UPDATE to lock rows and prevent concurrent chickin requests + // This ensures that simultaneous requests wait for each other and read consistent pending_qty err := r.db.WithContext(ctx). - Where("project_flock_kandang_id = ?", projectFlockKandangID). - Order("created_at DESC"). - Find(&chickins). - Error + Raw(` + SELECT * FROM project_chickins + WHERE project_flock_kandang_id = ? + AND deleted_at IS NULL + ORDER BY created_at DESC + FOR UPDATE + `, projectFlockKandangID). + Scan(&chickins).Error if err != nil { return nil, err } diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index e071f3be..3ff5d8d9 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -196,6 +196,8 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti } } + // CRITICAL: Validate chickins sequentially to prevent over-allocation within the same request + // pendingQtyMap is accumulated as we validate each chickin to ensure total pending doesn't exceed available stock for idx, chickin := range newChikins { pendingQty := pendingQtyMap[chickin.ProductWarehouseId] desiredQty := chickinQtyMap[uint(idx)] @@ -210,6 +212,10 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti } chickinQtyMap[uint(idx)] = availableQty + + // ACCUMULATE pending for this product warehouse so NEXT chickin in same request sees it + // This prevents multiple chickins in same request from over-allocating the same stock + pendingQtyMap[chickin.ProductWarehouseId] += availableQty } approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) 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 7994727f..0bfbc378 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -294,7 +294,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) return fiber.NewError(fiber.StatusInternalServerError, "Failed to reduce project flock population") } - if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"quantity": gorm.Expr("qty - ?", sourceDetail.Quantity)}, nil); err != nil { + if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"qty": gorm.Expr("qty - ?", sourceDetail.Quantity)}, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update source warehouse quantity") } } @@ -388,7 +388,7 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i if oldSource.ProductWarehouseId != nil && oldSource.Qty > 0 { if err := productWarehouseRepoTx.PatchOne(c.Context(), *oldSource.ProductWarehouseId, map[string]any{ - "quantity": gorm.Expr("qty + ?", oldSource.Qty), + "qty": gorm.Expr("qty + ?", oldSource.Qty), }, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore warehouse quantity") } @@ -466,7 +466,7 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i return err } - if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"quantity": gorm.Expr("qty - ?", sourceDetail.Quantity)}, nil); err != nil { + if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"qty": gorm.Expr("qty - ?", sourceDetail.Quantity)}, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update source warehouse quantity") } } @@ -544,7 +544,7 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { if source.ProductWarehouseId != nil && source.Qty > 0 { if err := productWarehouseRepoTx.PatchOne(c.Context(), *source.ProductWarehouseId, map[string]any{ - "quantity": gorm.Expr("qty + ?", source.Qty), + "qty": gorm.Expr("qty + ?", source.Qty), }, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore source warehouse quantity") } @@ -766,7 +766,7 @@ func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context, existing, err := productWarehouseRepoTx.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID) if err == nil && existing != nil { - if err := productWarehouseRepoTx.PatchOne(ctx, existing.Id, map[string]any{"quantity": gorm.Expr("qty + ?", quantity)}, nil); err != nil { + if err := productWarehouseRepoTx.PatchOne(ctx, existing.Id, map[string]any{"qty": gorm.Expr("qty + ?", quantity)}, nil); err != nil { return nil, err } return existing, nil