mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-24 07:15:43 +00:00
feat(be): add GetByProjectFlockKandangIDForUpdate method to lock chickin rows and prevent race conditions
This commit is contained in:
@@ -16,6 +16,7 @@ type ProjectChickinRepository interface {
|
|||||||
GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
|
GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
|
||||||
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
|
||||||
GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error)
|
GetTotalChickinQtyByProjectFlockID(ctx context.Context, projectFlockID uint) (float64, error)
|
||||||
|
GetByProjectFlockKandangIDForUpdate(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChickinRepositoryImpl struct {
|
type ChickinRepositoryImpl struct {
|
||||||
@@ -64,6 +65,20 @@ func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context,
|
|||||||
return chickins, nil
|
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) {
|
func (r *ChickinRepositoryImpl) GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
|
||||||
var chickins []entity.ProjectChickin
|
var chickins []entity.ProjectChickin
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
newChikins := make([]*entity.ProjectChickin, 0)
|
newChikins := make([]*entity.ProjectChickin, 0)
|
||||||
chickinQtyMap := make(map[uint]float64)
|
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))
|
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 {
|
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))
|
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)
|
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))
|
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{
|
newChickin := &entity.ProjectChickin{
|
||||||
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
||||||
ChickInDate: chickinDate,
|
ChickInDate: chickinDate,
|
||||||
@@ -176,22 +172,46 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
}
|
}
|
||||||
|
|
||||||
newChikins = append(newChikins, newChickin)
|
newChikins = append(newChikins, newChickin)
|
||||||
chickinQtyMap[uint(idx)] = availableQty
|
chickinQtyMap[uint(idx)] = productWarehouse.Quantity
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(newChikins) == 0 {
|
if len(newChikins) == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "No chickins to create")
|
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 {
|
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))
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
|
||||||
if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil {
|
if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil {
|
||||||
|
|||||||
+1
-1
@@ -191,7 +191,7 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project
|
|||||||
result := make(map[uint]float64)
|
result := make(map[uint]float64)
|
||||||
for _, pw := range products {
|
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)
|
availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err)
|
s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user