Fix transfer to laying delete and fix chikin delete with response recording

This commit is contained in:
ragilap
2026-03-09 13:10:06 +07:00
parent 45cc057dd4
commit 3a8cc47fa0
14 changed files with 1091 additions and 86 deletions
@@ -15,6 +15,11 @@ const (
transferLayingInFunctionCode = "TRANSFER_TO_LAYING_IN"
transferLayingStockableLane = "STOCKABLE"
transferLayingSourceTable = "laying_transfer_targets"
transferLayingOutFunctionCode = "TRANSFER_TO_LAYING_OUT"
transferLayingUsableLane = "USABLE"
transferLayingUsableSourceTable = "laying_transfers"
transferLayingLegacyUsableSourceTable = "laying_transfer_sources"
)
func reflowTransferLayingScope(
@@ -85,3 +90,90 @@ func resolveTransferLayingFlagGroupByProductWarehouse(ctx context.Context, tx *g
return strings.TrimSpace(selected.FlagGroupCode), nil
}
type transferLayingUsableRouteRule struct {
FlagGroupCode string `gorm:"column:flag_group_code"`
SourceTable string `gorm:"column:source_table"`
}
func resolveTransferLayingUsableFlagGroupByProductWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) (string, error) {
rows := make([]transferLayingUsableRouteRule, 0)
err := tx.WithContext(ctx).
Table("fifo_stock_v2_route_rules rr").
Select("rr.flag_group_code, rr.source_table").
Joins("JOIN fifo_stock_v2_flag_groups fg ON fg.code = rr.flag_group_code AND fg.is_active = TRUE").
Where("rr.is_active = TRUE").
Where("rr.lane = ?", transferLayingUsableLane).
Where("rr.function_code = ?", transferLayingOutFunctionCode).
Where(`
EXISTS (
SELECT 1
FROM product_warehouses pw
JOIN flags f ON f.flagable_id = pw.product_id
JOIN fifo_stock_v2_flag_members fm ON fm.flag_name = f.name AND fm.is_active = TRUE
WHERE pw.id = ?
AND f.flagable_type = ?
AND fm.flag_group_code = rr.flag_group_code
)
`, productWarehouseID, entity.FlagableTypeProduct).
Order("rr.id ASC").
Find(&rows).Error
if err != nil {
return "", err
}
return validateTransferLayingUsableRouteRules(rows, productWarehouseID)
}
func validateTransferLayingUsableRouteRules(rows []transferLayingUsableRouteRule, productWarehouseID uint) (string, error) {
if len(rows) == 0 {
return "", fmt.Errorf(
"konfigurasi FIFO v2 TRANSFER_TO_LAYING_OUT tidak ditemukan untuk source warehouse %d",
productWarehouseID,
)
}
var selectedFlagGroup string
hasHeaderRule := false
hasLegacyRule := false
for _, row := range rows {
sourceTable := strings.ToLower(strings.TrimSpace(row.SourceTable))
flagGroupCode := strings.TrimSpace(row.FlagGroupCode)
switch sourceTable {
case transferLayingUsableSourceTable:
if flagGroupCode == "" {
return "", fmt.Errorf("konfigurasi FIFO v2 TRANSFER_TO_LAYING_OUT memiliki flag_group_code kosong")
}
hasHeaderRule = true
if selectedFlagGroup == "" {
selectedFlagGroup = flagGroupCode
continue
}
if selectedFlagGroup != flagGroupCode {
return "", fmt.Errorf(
"konfigurasi FIFO v2 TRANSFER_TO_LAYING_OUT ambigu untuk source warehouse %d",
productWarehouseID,
)
}
case transferLayingLegacyUsableSourceTable:
hasLegacyRule = true
}
}
if hasLegacyRule {
return "", fmt.Errorf(
"konfigurasi FIFO v2 legacy untuk TRANSFER_TO_LAYING_OUT masih aktif (source_table=%s)",
transferLayingLegacyUsableSourceTable,
)
}
if !hasHeaderRule {
return "", fmt.Errorf(
"konfigurasi FIFO v2 TRANSFER_TO_LAYING_OUT aktif untuk source_table=%s tidak ditemukan",
transferLayingUsableSourceTable,
)
}
return selectedFlagGroup, nil
}
@@ -0,0 +1,56 @@
package service
import (
"strings"
"testing"
)
func TestValidateTransferLayingUsableRouteRules(t *testing.T) {
t.Run("valid header rule", func(t *testing.T) {
flagGroup, err := validateTransferLayingUsableRouteRules([]transferLayingUsableRouteRule{
{FlagGroupCode: "AYAM", SourceTable: transferLayingUsableSourceTable},
}, 10)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if flagGroup != "AYAM" {
t.Fatalf("unexpected flag group: %s", flagGroup)
}
})
t.Run("missing usable header rule", func(t *testing.T) {
_, err := validateTransferLayingUsableRouteRules(nil, 10)
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(strings.ToLower(err.Error()), "tidak ditemukan") {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("legacy rule still active", func(t *testing.T) {
_, err := validateTransferLayingUsableRouteRules([]transferLayingUsableRouteRule{
{FlagGroupCode: "AYAM", SourceTable: transferLayingUsableSourceTable},
{FlagGroupCode: "AYAM", SourceTable: transferLayingLegacyUsableSourceTable},
}, 10)
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(strings.ToLower(err.Error()), "legacy") {
t.Fatalf("unexpected error: %v", err)
}
})
t.Run("ambiguous active header rules", func(t *testing.T) {
_, err := validateTransferLayingUsableRouteRules([]transferLayingUsableRouteRule{
{FlagGroupCode: "AYAM", SourceTable: transferLayingUsableSourceTable},
{FlagGroupCode: "PAKAN", SourceTable: transferLayingUsableSourceTable},
}, 10)
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(strings.ToLower(err.Error()), "ambigu") {
t.Fatalf("unexpected error: %v", err)
}
})
}
@@ -1026,6 +1026,33 @@ func (s transferLayingService) Unexecute(c *fiber.Ctx, id uint) (*entity.LayingT
}
}
flagGroupCode, err := resolveTransferLayingUsableFlagGroupByProductWarehouse(
c.Context(),
dbTransaction,
*transfer.SourceProductWarehouseId,
)
if err != nil {
return fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Konfigurasi FIFO v2 transfer laying tidak valid: %v", err),
)
}
activeConsumeAllocCount, err := s.countActiveTransferSourceConsumeAllocations(
c.Context(),
dbTransaction,
transfer.Id,
*transfer.SourceProductWarehouseId,
)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi alokasi FIFO source transfer laying")
}
if transfer.SourceUsageQty > 1e-6 && activeConsumeAllocCount == 0 {
return fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf("Unexecute transfer laying %s gagal: alokasi FIFO source tidak ditemukan", transfer.TransferNumber),
)
}
for _, target := range targets {
if target.ProductWarehouseId == nil || *target.ProductWarehouseId == 0 {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Target product warehouse tidak ditemukan untuk transfer %d", transfer.Id))
@@ -1067,21 +1094,40 @@ func (s transferLayingService) Unexecute(c *fiber.Ctx, id uint) (*entity.LayingT
}
}
asOf := normalizeDateOnlyUTC(transfer.TransferDate)
rollbackResult, err := s.FifoStockV2Svc.Rollback(c.Context(), commonSvc.FifoStockV2RollbackRequest{
ProductWarehouseID: *transfer.SourceProductWarehouseId,
Usable: commonSvc.FifoStockV2Ref{
ID: transfer.Id,
LegacyTypeKey: fifo.UsableKeyTransferToLayingOut.String(),
FunctionCode: transferLayingOutFunctionCode,
},
Reason: fmt.Sprintf("transfer laying unexecute #%s [%s]", transfer.TransferNumber, flagGroupCode),
Tx: dbTransaction,
})
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal rollback FIFO v2 source transfer laying: %v", err))
}
releasedQty := 0.0
if rollbackResult != nil {
releasedQty = rollbackResult.ReleasedQty
}
if transfer.SourceUsageQty > 1e-6 && releasedQty < transfer.SourceUsageQty-1e-6 {
return fiber.NewError(
fiber.StatusBadRequest,
fmt.Sprintf(
"Rollback FIFO v2 source transfer laying tidak lengkap. Dibutuhkan %.3f, terlepas %.3f",
transfer.SourceUsageQty,
releasedQty,
),
)
}
if err := repoTx.PatchOne(c.Context(), transfer.Id, map[string]any{
"source_usage_qty": 0,
"source_pending_usage_qty": 0,
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Gagal reset kuantitas source transfer laying")
}
if _, err := s.FifoStockV2Svc.Reflow(c.Context(), commonSvc.FifoStockV2ReflowRequest{
FlagGroupCode: transferToLayingFlagGroupCode,
ProductWarehouseID: *transfer.SourceProductWarehouseId,
AsOf: &asOf,
Tx: dbTransaction,
}); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gagal rollback FIFO v2 source transfer laying: %v", err))
}
if err := fifoV2.ReleasePopulationConsumptionByUsable(
c.Context(),
dbTransaction,
@@ -1576,6 +1622,34 @@ func (s *transferLayingService) hasDownstreamRecordingOnTarget(
return true, normalizeDateOnlyUTC(earliest.RecordDatetime), nil
}
func (s *transferLayingService) countActiveTransferSourceConsumeAllocations(
ctx context.Context,
tx *gorm.DB,
transferID uint,
productWarehouseID uint,
) (int64, error) {
if transferID == 0 || productWarehouseID == 0 {
return 0, nil
}
if tx == nil {
return 0, errors.New("transaction is required")
}
var count int64
if err := tx.WithContext(ctx).
Model(&entity.StockAllocation{}).
Where("product_warehouse_id = ?", productWarehouseID).
Where("usable_type = ?", fifo.UsableKeyTransferToLayingOut.String()).
Where("usable_id = ?", transferID).
Where("status = ?", entity.StockAllocationStatusActive).
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func (s *transferLayingService) resyncPopulationUsageByProjectFlockKandang(ctx context.Context, tx *gorm.DB, projectFlockKandangID uint) error {
if projectFlockKandangID == 0 {
return nil