feat(be): add GetByProjectFlockKandangIDForUpdate method to lock chickin rows and prevent race conditions

This commit is contained in:
aguhh18
2025-12-31 10:27:15 +07:00
parent 0fc560b91c
commit c60c40af03
3 changed files with 51 additions and 16 deletions
@@ -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).
@@ -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 {
@@ -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)