package main import ( "context" "testing" "time" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" fifoStockV2 "gitlab.com/mbugroup/lti-api.git/internal/common/service/fifo_stock_v2" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" transferSvc "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services" ) func TestValidateAdjustmentGatherAgainstAllowedIDsEligible(t *testing.T) { result := validateAdjustmentGatherAgainstAllowedIDs(100, []uint{11, 12}, []commonSvc.FifoStockV2GatherRow{ {SourceTable: "adjustment_stocks", SourceID: 11, AvailableQuantity: 70}, {SourceTable: "adjustment_stocks", SourceID: 12, AvailableQuantity: 40}, }) if result.Status != "eligible" { t.Fatalf("expected eligible, got %+v", result) } if result.VerifiedQty != 100 { t.Fatalf("expected verified qty 100, got %v", result.VerifiedQty) } } func TestValidateAdjustmentGatherAgainstAllowedIDsRejectsMixedSource(t *testing.T) { result := validateAdjustmentGatherAgainstAllowedIDs(100, []uint{11}, []commonSvc.FifoStockV2GatherRow{ {SourceTable: "adjustment_stocks", SourceID: 11, AvailableQuantity: 60}, {SourceTable: "recording_eggs", SourceID: 21, AvailableQuantity: 50}, }) if result.Status != "skipped" { t.Fatalf("expected skipped, got %+v", result) } if result.Reason != "mixed_fifo_source_recording_eggs" { t.Fatalf("unexpected reason: %+v", result) } } func TestBuildAdjustmentMigrationPlanUsesValidator(t *testing.T) { opts := &adjustmentCommandOptions{RunID: "egg-adjustment-cutover-test"} farmID := uint(25) farmName := "Gudang Farm Jamali" rows := []adjustmentLegacyEggRow{ { LocationID: 16, LocationName: "Jamali", SourceWarehouseID: 46, SourceWarehouseName: "Gudang Jamali 1", FarmWarehouseID: &farmID, FarmWarehouseName: &farmName, ProductWarehouseID: 101, ProductID: 8, ProductName: "Telur Utuh", RemainingQty: 120, CurrentPWQty: 150, AdjustmentIDs: []uint{1}, }, { LocationID: 16, LocationName: "Jamali", SourceWarehouseID: 46, SourceWarehouseName: "Gudang Jamali 1", FarmWarehouseID: &farmID, FarmWarehouseName: &farmName, ProductWarehouseID: 102, ProductID: 9, ProductName: "Telur Putih", RemainingQty: 20, CurrentPWQty: 40, AdjustmentIDs: []uint{2}, }, { LocationID: 16, LocationName: "Jamali", SourceWarehouseID: 46, SourceWarehouseName: "Gudang Jamali 1", ProductWarehouseID: 103, ProductID: 10, ProductName: "Telur Pecah", RemainingQty: 10, CurrentPWQty: 10, AdjustmentIDs: []uint{3}, }, } validator := &fakeAdjustmentCandidateValidator{ byProduct: map[string]adjustmentCandidateValidation{ "Telur Utuh": {Status: "eligible", VerifiedQty: 120}, "Telur Putih": {Status: "skipped", Reason: "mixed_fifo_source_recording_eggs", VerifiedQty: 10}, }, } reportRows, groups := buildAdjustmentMigrationPlan(context.Background(), opts, map[uint]adjustmentLocationTiming{ 16: {LocationID: 16, LocationName: "Jamali", Status: "CLEAN_CUTOVER"}, }, rows, validator) if len(reportRows) != 3 { t.Fatalf("expected 3 report rows, got %d", len(reportRows)) } if len(groups) != 1 || len(groups[0].Rows) != 1 { t.Fatalf("expected only one eligible grouped row, got %+v", groups) } if reportRows[0].Status != "eligible" || reportRows[0].VerifiedQty != 120 { t.Fatalf("unexpected first row: %+v", reportRows[0]) } if reportRows[1].Reason != "mixed_fifo_source_recording_eggs" { t.Fatalf("unexpected second row reason: %+v", reportRows[1]) } if reportRows[2].Reason != "missing_farm_warehouse" { t.Fatalf("expected missing farm warehouse skip, got %+v", reportRows[2]) } } func TestExecuteAdjustmentApplyRevalidatesRowsAndAppliesSubset(t *testing.T) { opts := &adjustmentCommandOptions{ RunID: "egg-adjustment-cutover-apply", CutoverDate: time.Date(2026, 4, 7, 0, 0, 0, 0, time.UTC), ActorID: 99, } group := adjustmentTransferGroup{ LocationID: 16, LocationName: "Jamali", SourceWarehouseID: 46, SourceWarehouseName: "Gudang Jamali 1", FarmWarehouseID: 25, FarmWarehouseName: "Gudang Farm Jamali", Rows: []*adjustmentMigrationReportRow{ {LocationID: 16, LocationName: "Jamali", SourceWarehouseID: 46, SourceWarehouseName: "Gudang Jamali 1", FarmWarehouseID: uintPtr(25), FarmWarehouseName: strPtr("Gudang Farm Jamali"), ProductWarehouseID: 101, ProductID: 8, ProductName: "Telur Utuh", RemainingQty: 120, CurrentPWQty: 150, AdjustmentIDs: []uint{1}, Status: "eligible"}, {LocationID: 16, LocationName: "Jamali", SourceWarehouseID: 46, SourceWarehouseName: "Gudang Jamali 1", FarmWarehouseID: uintPtr(25), FarmWarehouseName: strPtr("Gudang Farm Jamali"), ProductWarehouseID: 102, ProductID: 9, ProductName: "Telur Putih", RemainingQty: 20, CurrentPWQty: 40, AdjustmentIDs: []uint{2}, Status: "eligible"}, }, } validator := &fakeAdjustmentCandidateValidator{ byProduct: map[string]adjustmentCandidateValidation{ "Telur Utuh": {Status: "eligible", VerifiedQty: 120}, "Telur Putih": {Status: "skipped", Reason: "mixed_fifo_source_recording_eggs", VerifiedQty: 10}, }, } executor := &fakeAdjustmentSystemTransferExecutor{ createResponses: []*entity.StockTransfer{ {Id: 1001, MovementNumber: "PND-LTI-1001"}, }, } summary, err := executeAdjustmentApply(context.Background(), executor, validator, opts, []adjustmentTransferGroup{group}) if err != nil { t.Fatalf("expected no fatal apply error, got %v", err) } if summary.GroupsApplied != 1 { t.Fatalf("expected 1 applied group, got %+v", summary) } if summary.RowsApplied != 1 || summary.RowsFailed != 1 { t.Fatalf("unexpected summary: %+v", summary) } if len(executor.createRequests) != 1 { t.Fatalf("expected 1 create request, got %d", len(executor.createRequests)) } if len(executor.createRequests[0].Products) != 1 || executor.createRequests[0].Products[0].ProductID != 8 { t.Fatalf("expected only Telur Utuh to be transferred, got %+v", executor.createRequests[0].Products) } } type fakeAdjustmentCandidateValidator struct { byProduct map[string]adjustmentCandidateValidation errByProduct map[string]error } func (f *fakeAdjustmentCandidateValidator) ValidateCandidate(ctx context.Context, row adjustmentLegacyEggRow) (adjustmentCandidateValidation, error) { if err, ok := f.errByProduct[row.ProductName]; ok { return adjustmentCandidateValidation{}, err } if result, ok := f.byProduct[row.ProductName]; ok { return result, nil } return adjustmentCandidateValidation{Status: "eligible", VerifiedQty: row.RemainingQty}, nil } type fakeAdjustmentSystemTransferExecutor struct { createRequests []*transferSvc.SystemTransferRequest createResponses []*entity.StockTransfer createErrors []error deletedTransferIDs []uint deleteErrors map[uint]error } func (f *fakeAdjustmentSystemTransferExecutor) CreateSystemTransfer(ctx context.Context, req *transferSvc.SystemTransferRequest) (*entity.StockTransfer, error) { f.createRequests = append(f.createRequests, req) idx := len(f.createRequests) - 1 if idx < len(f.createErrors) && f.createErrors[idx] != nil { return nil, f.createErrors[idx] } if idx < len(f.createResponses) && f.createResponses[idx] != nil { return f.createResponses[idx], nil } return &entity.StockTransfer{Id: uint64(1000 + idx), MovementNumber: "PND-LTI-DEFAULT"}, nil } func (f *fakeAdjustmentSystemTransferExecutor) DeleteSystemTransfer(ctx context.Context, id uint, actorID uint) error { f.deletedTransferIDs = append(f.deletedTransferIDs, id) if f.deleteErrors == nil { return nil } return f.deleteErrors[id] } func uintPtr(v uint) *uint { return &v } func strPtr(v string) *string { return &v } var _ adjustmentCandidateValidator = (*fakeAdjustmentCandidateValidator)(nil) var _ adjustmentSystemTransferExecutor = (*fakeAdjustmentSystemTransferExecutor)(nil) var _ commonSvc.FifoStockV2Lane = fifoStockV2.LaneStockable