From 33bae94d4339d001ccdd0a0347102ce07eee8991 Mon Sep 17 00:00:00 2001 From: giovanni Date: Sat, 6 Jun 2026 07:49:34 +0700 Subject: [PATCH] fix over consume by code, revert migration overconsume sell --- .../common/service/fifo_stock_v2/allocate.go | 7 +++- ...block_marketing_overconsume_telur.down.sql | 25 ++++++++++++ ...t_block_marketing_overconsume_telur.up.sql | 17 +++++++++ .../services/deliveryorder.service.go | 38 +++++++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 internal/database/migrations/20260605235554_revert_block_marketing_overconsume_telur.down.sql create mode 100644 internal/database/migrations/20260605235554_revert_block_marketing_overconsume_telur.up.sql diff --git a/internal/common/service/fifo_stock_v2/allocate.go b/internal/common/service/fifo_stock_v2/allocate.go index e7588286..075feee1 100644 --- a/internal/common/service/fifo_stock_v2/allocate.go +++ b/internal/common/service/fifo_stock_v2/allocate.go @@ -195,9 +195,12 @@ func (s *fifoStockV2Service) allocateInternal(ctx context.Context, tx *gorm.DB, if remaining > 0 { if !allowOverConsume { - return nil, fmt.Errorf("%w: requested %.3f, allocated %.3f", ErrInsufficientStock, req.NeedQty, result.AllocatedQty) + s.logger.Warnf("FIFO v2: clearing historical pending (%.3f) for %s/%d at PW=%d — over-consume is blocked by rule", + remaining, req.Usable.LegacyTypeKey, req.Usable.ID, req.ProductWarehouseID) + result.PendingQty = 0 + } else { + result.PendingQty = remaining } - result.PendingQty = remaining } if err := s.applyUsableDeltas(tx, *usableRule, req.Usable.ID, result.AllocatedQty, result.PendingQty); err != nil { diff --git a/internal/database/migrations/20260605235554_revert_block_marketing_overconsume_telur.down.sql b/internal/database/migrations/20260605235554_revert_block_marketing_overconsume_telur.down.sql new file mode 100644 index 00000000..c4c7332b --- /dev/null +++ b/internal/database/migrations/20260605235554_revert_block_marketing_overconsume_telur.down.sql @@ -0,0 +1,25 @@ +BEGIN; + +-- Rollback: re-insert TELUR/TELUR_GRADE block rules yang dihapus oleh migration ini. + +INSERT INTO fifo_stock_v2_overconsume_rules(flag_group_code, function_code, lane, allow_overconsume, priority, reason, is_active) +SELECT 'TELUR', 'MARKETING_OUT', 'USABLE', FALSE, 20, 'fifo_v2_exception_marketing_block_telur', TRUE +WHERE NOT EXISTS ( + SELECT 1 FROM fifo_stock_v2_overconsume_rules + WHERE lane = 'USABLE' + AND function_code = 'MARKETING_OUT' + AND flag_group_code = 'TELUR' + AND reason = 'fifo_v2_exception_marketing_block_telur' +); + +INSERT INTO fifo_stock_v2_overconsume_rules(flag_group_code, function_code, lane, allow_overconsume, priority, reason, is_active) +SELECT 'TELUR_GRADE', 'MARKETING_OUT', 'USABLE', FALSE, 20, 'fifo_v2_exception_marketing_block_telur_grade', TRUE +WHERE NOT EXISTS ( + SELECT 1 FROM fifo_stock_v2_overconsume_rules + WHERE lane = 'USABLE' + AND function_code = 'MARKETING_OUT' + AND flag_group_code = 'TELUR_GRADE' + AND reason = 'fifo_v2_exception_marketing_block_telur_grade' +); + +COMMIT; diff --git a/internal/database/migrations/20260605235554_revert_block_marketing_overconsume_telur.up.sql b/internal/database/migrations/20260605235554_revert_block_marketing_overconsume_telur.up.sql new file mode 100644 index 00000000..542e9725 --- /dev/null +++ b/internal/database/migrations/20260605235554_revert_block_marketing_overconsume_telur.up.sql @@ -0,0 +1,17 @@ +BEGIN; + +-- Revert rules yang ditambahkan oleh migration 20260603031237_block_marketing_overconsume_telur. +-- TELUR/TELUR_GRADE kembali fallback ke default allow rule (allow_overconsume=TRUE) +-- karena validasi stok sekarang ditangani di service layer (code validation) bukan lewat +-- config overconsume FIFO v2. + +DELETE FROM fifo_stock_v2_overconsume_rules +WHERE lane = 'USABLE' + AND function_code = 'MARKETING_OUT' + AND flag_group_code IN ('TELUR', 'TELUR_GRADE') + AND reason IN ( + 'fifo_v2_exception_marketing_block_telur', + 'fifo_v2_exception_marketing_block_telur_grade' + ); + +COMMIT; diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index 929e0ab0..73a30426 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -972,6 +972,19 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor if err := deliveryProductRepo.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") } + if requestedQty > 0 { + available, err := s.checkAvailableStockQty(ctx, tx, marketingProduct.ProductWarehouseId) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Gagal memeriksa ketersediaan stok") + } + if requestedQty > available { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf( + "Stok tidak mencukupi: dibutuhkan %g, tersedia %g", + requestedQty, available, + )) + } + } + if err := reflowMarketingScope( ctx, s.FifoStockV2Svc, @@ -1505,3 +1518,28 @@ func uniqueUintIDs(ids []uint) []uint { } return result } + +// checkAvailableStockQty returns the net available qty for a product warehouse: +// gross qty (product_warehouses.qty) minus the sum of active CONSUME allocations +// in stock_allocations. This gives the true available stock accounting for all +// other delivery orders that have already consumed from the same warehouse. +func (s deliveryOrdersService) checkAvailableStockQty(ctx context.Context, tx *gorm.DB, productWarehouseId uint) (float64, error) { + var pw entity.ProductWarehouse + if err := tx.WithContext(ctx).Select("qty").First(&pw, productWarehouseId).Error; err != nil { + return 0, err + } + + var usedQty float64 + if err := tx.WithContext(ctx).Raw(` + SELECT COALESCE(SUM(qty), 0) + FROM stock_allocations + WHERE stockable_type = 'product_warehouses' + AND stockable_id = ? + AND status = 'ACTIVE' + AND allocation_purpose = 'CONSUME' + `, productWarehouseId).Scan(&usedQty).Error; err != nil { + return 0, err + } + + return pw.Quantity - usedQty, nil +}