mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
252 lines
8.2 KiB
Go
252 lines
8.2 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
|
transferSvc "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
|
|
)
|
|
|
|
func TestBuildMigrationPlanSkipsOverlapAndGroupsEligibleRows(t *testing.T) {
|
|
opts := &commandOptions{
|
|
RunID: "egg-cutover-test",
|
|
IncludeOverlap: false,
|
|
}
|
|
timings := map[uint]locationTiming{
|
|
16: {LocationID: 16, LocationName: "Jamali", Status: "CLEAN_CUTOVER"},
|
|
17: {LocationID: 17, LocationName: "Cijangkar", Status: "OVERLAP"},
|
|
}
|
|
farmID := uint(25)
|
|
farmName := "Gudang Farm Jamali"
|
|
|
|
rows := []legacyEggStockRow{
|
|
{
|
|
LocationID: 16,
|
|
LocationName: "Jamali",
|
|
SourceWarehouseID: 46,
|
|
SourceWarehouseName: "Gudang Jamali 1",
|
|
FarmWarehouseID: &farmID,
|
|
FarmWarehouseName: &farmName,
|
|
ProductWarehouseID: 101,
|
|
ProductID: 8,
|
|
ProductName: "Telur Utuh",
|
|
OnHandQty: 120,
|
|
},
|
|
{
|
|
LocationID: 16,
|
|
LocationName: "Jamali",
|
|
SourceWarehouseID: 46,
|
|
SourceWarehouseName: "Gudang Jamali 1",
|
|
FarmWarehouseID: &farmID,
|
|
FarmWarehouseName: &farmName,
|
|
ProductWarehouseID: 102,
|
|
ProductID: 9,
|
|
ProductName: "Telur Putih",
|
|
OnHandQty: 20,
|
|
},
|
|
{
|
|
LocationID: 17,
|
|
LocationName: "Cijangkar",
|
|
SourceWarehouseID: 51,
|
|
SourceWarehouseName: "Gudang Cijangkar 1",
|
|
FarmWarehouseID: &farmID,
|
|
FarmWarehouseName: &farmName,
|
|
ProductWarehouseID: 103,
|
|
ProductID: 10,
|
|
ProductName: "Telur Jumbo",
|
|
OnHandQty: 10,
|
|
},
|
|
{
|
|
LocationID: 16,
|
|
LocationName: "Jamali",
|
|
SourceWarehouseID: 46,
|
|
SourceWarehouseName: "Gudang Jamali 1",
|
|
ProductWarehouseID: 104,
|
|
ProductID: 11,
|
|
ProductName: "Telur Papacal",
|
|
OnHandQty: 50,
|
|
},
|
|
{
|
|
LocationID: 16,
|
|
LocationName: "Jamali",
|
|
SourceWarehouseID: 46,
|
|
SourceWarehouseName: "Gudang Jamali 1",
|
|
FarmWarehouseID: &farmID,
|
|
FarmWarehouseName: &farmName,
|
|
ProductWarehouseID: 105,
|
|
ProductID: 12,
|
|
ProductName: "Telur Retak",
|
|
OnHandQty: 0,
|
|
},
|
|
}
|
|
|
|
reportRows, groups := buildMigrationPlan(opts, timings, rows)
|
|
|
|
if len(reportRows) != 5 {
|
|
t.Fatalf("expected 5 report rows, got %d", len(reportRows))
|
|
}
|
|
if len(groups) != 1 {
|
|
t.Fatalf("expected 1 eligible transfer group, got %d", len(groups))
|
|
}
|
|
if len(groups[0].Rows) != 2 {
|
|
t.Fatalf("expected 2 eligible products in the transfer group, got %d", len(groups[0].Rows))
|
|
}
|
|
|
|
statusByProduct := make(map[string]string, len(reportRows))
|
|
reasonByProduct := make(map[string]string, len(reportRows))
|
|
for _, row := range reportRows {
|
|
statusByProduct[row.ProductName] = row.Status
|
|
reasonByProduct[row.ProductName] = row.Reason
|
|
}
|
|
|
|
if statusByProduct["Telur Utuh"] != "eligible" || statusByProduct["Telur Putih"] != "eligible" {
|
|
t.Fatalf("expected Jamali egg rows to stay eligible, got statuses %+v", statusByProduct)
|
|
}
|
|
if reasonByProduct["Telur Jumbo"] != "overlap_location" {
|
|
t.Fatalf("expected overlap location skip, got %q", reasonByProduct["Telur Jumbo"])
|
|
}
|
|
if reasonByProduct["Telur Papacal"] != "missing_farm_warehouse" {
|
|
t.Fatalf("expected missing farm warehouse skip, got %q", reasonByProduct["Telur Papacal"])
|
|
}
|
|
if reasonByProduct["Telur Retak"] != "non_positive_qty" {
|
|
t.Fatalf("expected non positive qty skip, got %q", reasonByProduct["Telur Retak"])
|
|
}
|
|
}
|
|
|
|
func TestExecuteApplyBuildsTaggedSystemTransfersAndSummaries(t *testing.T) {
|
|
opts := &commandOptions{
|
|
RunID: "egg-cutover-apply",
|
|
CutoverDate: time.Date(2026, 4, 7, 0, 0, 0, 0, time.UTC),
|
|
ActorID: 99,
|
|
}
|
|
groups := []transferGroup{
|
|
{
|
|
LocationID: 16,
|
|
LocationName: "Jamali",
|
|
SourceWarehouseID: 46,
|
|
SourceWarehouseName: "Gudang Jamali 1",
|
|
FarmWarehouseID: 25,
|
|
FarmWarehouseName: "Gudang Farm Jamali",
|
|
Rows: []*migrationReportRow{
|
|
{ProductID: 8, ProductName: "Telur Utuh", Qty: 120},
|
|
{ProductID: 9, ProductName: "Telur Putih", Qty: 20},
|
|
},
|
|
},
|
|
{
|
|
LocationID: 18,
|
|
LocationName: "Tamansari",
|
|
SourceWarehouseID: 91,
|
|
SourceWarehouseName: "Gudang Tamansari 1",
|
|
FarmWarehouseID: 31,
|
|
FarmWarehouseName: "Gudang Farm Tamansari",
|
|
Rows: []*migrationReportRow{
|
|
{ProductID: 10, ProductName: "Telur Jumbo", Qty: 10},
|
|
},
|
|
},
|
|
}
|
|
|
|
executor := &fakeSystemTransferExecutor{
|
|
createResponses: []*entity.StockTransfer{
|
|
{Id: 1001, MovementNumber: "PND-LTI-1001"},
|
|
},
|
|
createErrors: []error{
|
|
nil,
|
|
errors.New("destination warehouse locked"),
|
|
},
|
|
}
|
|
|
|
summary, err := executeApply(context.Background(), executor, opts, groups)
|
|
if err != nil {
|
|
t.Fatalf("expected no fatal apply error, got %v", err)
|
|
}
|
|
if summary.GroupsPlanned != 2 || summary.GroupsApplied != 1 {
|
|
t.Fatalf("unexpected group summary: %+v", summary)
|
|
}
|
|
if summary.RowsApplied != 2 || summary.RowsFailed != 1 {
|
|
t.Fatalf("unexpected row summary: %+v", summary)
|
|
}
|
|
if len(executor.createRequests) != 2 {
|
|
t.Fatalf("expected 2 create requests, got %d", len(executor.createRequests))
|
|
}
|
|
if !strings.Contains(executor.createRequests[0].TransferReason, "EGG_FARM_CUTOVER|run_id=egg-cutover-apply|location=Jamali|cutover_date=2026-04-07") {
|
|
t.Fatalf("unexpected transfer reason: %s", executor.createRequests[0].TransferReason)
|
|
}
|
|
if executor.createRequests[0].MovementNumber != "" {
|
|
t.Fatalf("apply path should let transfer service generate movement number, got %q", executor.createRequests[0].MovementNumber)
|
|
}
|
|
if groups[0].Rows[0].Status != "applied" || groups[0].Rows[1].Status != "applied" {
|
|
t.Fatalf("expected first group rows to be applied, got %+v", groups[0].Rows)
|
|
}
|
|
if groups[1].Rows[0].Status != "failed" {
|
|
t.Fatalf("expected second group row to fail, got %+v", groups[1].Rows[0])
|
|
}
|
|
if groups[0].Rows[0].TransferID == nil || *groups[0].Rows[0].TransferID != 1001 {
|
|
t.Fatalf("expected first row to keep created transfer id, got %+v", groups[0].Rows[0].TransferID)
|
|
}
|
|
}
|
|
|
|
func TestExecuteRollbackDeletesTransfersDescendingAndMarksFailures(t *testing.T) {
|
|
executor := &fakeSystemTransferExecutor{
|
|
deleteErrors: map[uint]error{
|
|
101: errors.New("already consumed downstream"),
|
|
},
|
|
}
|
|
rows := []rollbackDetailRow{
|
|
{TransferID: 100, ProductName: "Telur Utuh"},
|
|
{TransferID: 101, ProductName: "Telur Jumbo"},
|
|
{TransferID: 100, ProductName: "Telur Putih"},
|
|
}
|
|
|
|
err := executeRollback(context.Background(), executor, rows, 99)
|
|
if err == nil {
|
|
t.Fatal("expected rollback to return the first transfer error")
|
|
}
|
|
if err.Error() != "already consumed downstream" {
|
|
t.Fatalf("unexpected rollback error: %v", err)
|
|
}
|
|
if len(executor.deletedTransferIDs) != 2 {
|
|
t.Fatalf("expected 2 delete calls, got %d", len(executor.deletedTransferIDs))
|
|
}
|
|
if executor.deletedTransferIDs[0] != 101 || executor.deletedTransferIDs[1] != 100 {
|
|
t.Fatalf("expected delete order [101 100], got %v", executor.deletedTransferIDs)
|
|
}
|
|
if rows[0].Status != "rolled_back" || rows[2].Status != "rolled_back" {
|
|
t.Fatalf("expected transfer 100 rows to be rolled back, got %+v", rows)
|
|
}
|
|
if rows[1].Status != "failed" {
|
|
t.Fatalf("expected transfer 101 row to fail, got %+v", rows[1])
|
|
}
|
|
}
|
|
|
|
type fakeSystemTransferExecutor struct {
|
|
createRequests []*transferSvc.SystemTransferRequest
|
|
createResponses []*entity.StockTransfer
|
|
createErrors []error
|
|
deletedTransferIDs []uint
|
|
deleteErrors map[uint]error
|
|
}
|
|
|
|
func (f *fakeSystemTransferExecutor) 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 *fakeSystemTransferExecutor) 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]
|
|
}
|