diff --git a/internal/database/migrations/20260603031237_block_marketing_overconsume_telur.down.sql b/internal/database/migrations/20260603031237_block_marketing_overconsume_telur.down.sql new file mode 100644 index 00000000..d3d685cd --- /dev/null +++ b/internal/database/migrations/20260603031237_block_marketing_overconsume_telur.down.sql @@ -0,0 +1,17 @@ +BEGIN; + +-- Revert the TELUR / TELUR_GRADE marketing over-sell block. Removing these rows +-- makes resolveOverConsume() fall back to the default allow rule again (the +-- post-20260313061525 behaviour). The reasons are unique to this migration, so +-- the DELETE only touches rows created here. + +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/database/migrations/20260603031237_block_marketing_overconsume_telur.up.sql b/internal/database/migrations/20260603031237_block_marketing_overconsume_telur.up.sql new file mode 100644 index 00000000..870fde89 --- /dev/null +++ b/internal/database/migrations/20260603031237_block_marketing_overconsume_telur.up.sql @@ -0,0 +1,54 @@ +BEGIN; + +-- Restore the marketing over-sell block for TELUR and TELUR_GRADE only. +-- +-- Migration 20260313061525 narrowed the MARKETING_OUT over-sell block to +-- flag_group_code='AYAM' and deactivated the global rule. That left TELUR / +-- TELUR_GRADE with no matching block, so resolveOverConsume() fell back to the +-- default rule 'fifo_v2_default_allow' (allow_overconsume=TRUE) and egg +-- Delivery Orders could over-sell silently into marketing_delivery_products.pending_qty. +-- +-- These rules make resolveOverConsume('TELUR'|'TELUR_GRADE','MARKETING_OUT') = FALSE, +-- so an egg DO that exceeds available stock is REJECTED (ErrInsufficientStock) +-- instead of being recorded as pending. Scope is "Telur saja" — AYAM and +-- transfer behaviour are intentionally left unchanged. +-- +-- NOTE: run the total_used reconciliation (cmd/reconcile-fifo-total-used) BEFORE +-- applying this in production. Enabling the block while phantom total_used still +-- inflates consumption would reject otherwise-valid egg orders. + +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' +); + +UPDATE fifo_stock_v2_overconsume_rules +SET allow_overconsume = FALSE, priority = 20, is_active = TRUE +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' +); + +UPDATE fifo_stock_v2_overconsume_rules +SET allow_overconsume = FALSE, priority = 20, is_active = TRUE +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/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index d442f457..2cdf2ec6 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -964,7 +964,10 @@ func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gor marketingProduct.ProductWarehouseId, resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt), ); err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err)) + if errors.Is(err, fifoV2.ErrInsufficientStock) { + return fiber.NewError(fiber.StatusBadRequest, "Stok tidak mencukupi untuk memenuhi permintaan delivery order ini") + } + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal mengalokasikan stok: %v", err)) } refreshed, err := deliveryProductRepo.GetByID(ctx, deliveryProduct.Id, nil)