package renewchange import ( "context" "testing" "time" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/entity" rc "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/repository/renewchange" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/os/gtime" ) func TestPrepareExecutorCanReclaimStaleProcessingBatch(t *testing.T) { now := time.Date(2026, 8, 26, 14, 0, 0, 0, time.Local) executor := &PrepareExecutor{nowFunc: func() time.Time { return now }} tests := []struct { name string main *entity.BatchMain canRun bool }{ { name: "fresh processing batch", main: &entity.BatchMain{ BatchType: consts.BatchTypePrepare, BatchAsyncStatus: consts.BatchAsyncStatusProcessing, StartTime: gtime.NewFromTime(now.Add(-prepareProcessingRetryAfter + time.Second)), }, }, { name: "stale processing batch", main: &entity.BatchMain{ BatchType: consts.BatchTypePrepare, BatchAsyncStatus: consts.BatchAsyncStatusProcessing, StartTime: gtime.NewFromTime(now.Add(-prepareProcessingRetryAfter)), }, canRun: true, }, { name: "missing start time", main: &entity.BatchMain{ BatchType: consts.BatchTypePrepare, BatchAsyncStatus: consts.BatchAsyncStatusProcessing, }, canRun: true, }, { name: "terminal batch", main: &entity.BatchMain{ BatchType: consts.BatchTypePrepare, BatchAsyncStatus: consts.BatchAsyncStatusSuccess, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got, err := executor.CanExecuteProcessing(context.Background(), test.main) if err != nil { t.Fatalf("CanExecuteProcessing() error = %v", err) } if got != test.canRun { t.Fatalf("CanExecuteProcessing() = %v, want %v", got, test.canRun) } }) } } func TestLoadPrepareOptionsDurationIsExplicitOnly(t *testing.T) { opts := loadPrepareOptions(gjson.New(map[string]any{ "targetBatchType": 101, })) if opts.durationSet { t.Fatalf("durationSet = true, want false") } if opts.durationValue != 0 { t.Fatalf("durationValue = %d, want 0", opts.durationValue) } opts = loadPrepareOptions(gjson.New(map[string]any{ "durationMonths": 6, })) if !opts.durationSet { t.Fatalf("durationSet = false, want true") } if opts.durationValue != 6 { t.Fatalf("durationValue = %d, want 6", opts.durationValue) } opts = loadPrepareOptions(gjson.New(map[string]any{ "durationMonths": 12, })) if !opts.durationSet { t.Fatalf("durationSet = false, want true") } if opts.durationValue != 1 { t.Fatalf("durationValue = %d, want 1", opts.durationValue) } } func TestChangeDurationOptionsUseMonthsAtExecutionBoundary(t *testing.T) { got, err := changeDurationOptions(gjson.New([]int{1, 2, 3})) if err != nil { t.Fatalf("changeDurationOptions(years) error = %v", err) } if want := []int{12, 24, 36}; len(got) != len(want) { t.Fatalf("changeDurationOptions(years) = %#v, want %#v", got, want) } else { for i := range want { if got[i] != want[i] { t.Fatalf("changeDurationOptions(years) = %#v, want %#v", got, want) } } } got, err = changeDurationOptions(gjson.New([]int{12, 24, 36})) if err != nil { t.Fatalf("changeDurationOptions(legacy months) error = %v", err) } if want := []int{12, 24, 36}; len(got) != len(want) { t.Fatalf("changeDurationOptions(legacy months) = %#v, want %#v", got, want) } else { for i := range want { if got[i] != want[i] { t.Fatalf("changeDurationOptions(legacy months) = %#v, want %#v", got, want) } } } } func TestMergeRowErrorsKeepsValidationReasons(t *testing.T) { got := mergeRowErrors( map[int]string{2: "parse failed"}, map[int]string{1: "duration not allowed", 2: "validation failed", 0: "batch failed"}, ) if got[1] != "duration not allowed" { t.Fatalf("row 1 reason = %q", got[1]) } if got[2] != "parse failed" { t.Fatalf("row 2 reason = %q", got[2]) } if got[0] != "batch failed" { t.Fatalf("row 0 reason = %q", got[0]) } } func TestResolvePrepareIdentityRowsFromCardsCanonicalizesMSISDN(t *testing.T) { rows := []*RenewRow{ {RowNo: 1, ICCID: "13800008000"}, {RowNo: 2, ICCID: "8986002", MSISDN: "bad-msisdn"}, } rowErrors := map[int]string{} resolvePrepareIdentityRowsFromCards(rows, rowErrors, []*rc.SimCardInfo{ {Id: 1, Iccid: "8986001", Msisdn: "13800008000", Imsi: "460001"}, {Id: 2, Iccid: "8986002", Msisdn: "13800008002", Imsi: "460002"}, }) if rows[0].ICCID != "8986001" || rows[0].MSISDN != "13800008000" || rows[0].IMSI != "460001" { t.Fatalf("row 1 was not canonicalized: %#v", rows[0]) } if rowErrors[1] != "" { t.Fatalf("row 1 error = %q, want empty", rowErrors[1]) } if rowErrors[2] == "" { t.Fatal("row 2 should fail when ICCID and MSISDN point to different identities") } } func TestCollectPrepareIccidsMarksDuplicateAfterCanonicalization(t *testing.T) { rows := []*RenewRow{ {RowNo: 1, ICCID: "8986001"}, {RowNo: 2, ICCID: "8986001"}, } rowErrors := map[int]string{} got := collectPrepareIccids(rows, rowErrors) if len(got) != 1 || got[0] != "8986001" { t.Fatalf("iccids = %#v, want single canonical ICCID", got) } if rowErrors[1] == "" || rowErrors[2] == "" { t.Fatalf("duplicate errors not marked: %#v", rowErrors) } } func TestBuildPrepareInfoWritesRenewFlatContract(t *testing.T) { got := buildPrepareInfo(nil, 101, &prepareOutcome{ customer: &customerInfo{ AgentID: 9, DeptID: 117, Name: "customer", }, priceSnap: &RenewPriceSnapshot{ CustomerAgentId: 117, PackageCode: "PKG-1", PackageName: "package 1", PriceSource: PriceSourceSystem, AgentDefaultPriceId: 11, TargetPriceId: 22, RenewUnitPrice: 3.5, RenewableDurationOptions: []int{1, 3, 12}, RenewBeginDate: "2026-08-31", RenewBeginDateMin: "2026-08-31", RenewBeginDateMax: "2026-08-31", SameRenewBeginDate: true, }, }, 2) if got["customerAgentId"] != int64(117) { t.Fatalf("customerAgentId = %v, want 117", got["customerAgentId"]) } if got["packageCode"] != "PKG-1" { t.Fatalf("packageCode = %v, want PKG-1", got["packageCode"]) } if got["quantity"] != 2 { t.Fatalf("quantity = %v, want 2", got["quantity"]) } if got["renewUnitPrice"] != 3.5 { t.Fatalf("renewUnitPrice = %v, want 3.5", got["renewUnitPrice"]) } if got["priceSnapshot"] == nil { t.Fatalf("priceSnapshot is nil") } if got["renewBeginDate"] != "2026-08-31" { t.Fatalf("renewBeginDate = %v, want 2026-08-31", got["renewBeginDate"]) } if got["sameRenewBeginDate"] != true { t.Fatalf("sameRenewBeginDate = %v, want true", got["sameRenewBeginDate"]) } } func TestFillRenewBeginSnapshotSummarizesRenewBeginDate(t *testing.T) { snapshot := &RenewPriceSnapshot{} fillRenewBeginSnapshot(snapshot, []*RenewRow{ {RowNo: 1, OldExpireTime: gtime.NewFromStr("2026-09-10 00:00:00")}, {RowNo: 2, OldExpireTime: gtime.NewFromStr("2026-08-31 23:59:59")}, }, nil) if snapshot.RenewBeginDate != "" { t.Fatalf("renewBeginDate = %q, want empty for mixed dates", snapshot.RenewBeginDate) } if snapshot.RenewBeginDateMin != "2026-08-31" { t.Fatalf("renewBeginDateMin = %q, want 2026-08-31", snapshot.RenewBeginDateMin) } if snapshot.RenewBeginDateMax != "2026-09-10" { t.Fatalf("renewBeginDateMax = %q, want 2026-09-10", snapshot.RenewBeginDateMax) } if snapshot.SameRenewBeginDate { t.Fatalf("sameRenewBeginDate = true, want false") } fillRenewBeginSnapshot(snapshot, []*RenewRow{ {RowNo: 1, OldExpireTime: gtime.NewFromStr("2026-10-01 00:00:00")}, {RowNo: 2, OldExpireTime: gtime.NewFromStr("2026-10-01 23:59:59")}, }, nil) if snapshot.RenewBeginDate != "2026-10-01" { t.Fatalf("renewBeginDate = %q, want 2026-10-01", snapshot.RenewBeginDate) } if !snapshot.SameRenewBeginDate { t.Fatalf("sameRenewBeginDate = false, want true") } } func TestRenewBeginDateFromExpireUsesExpireDate(t *testing.T) { beginAt := renewBeginDateFromExpire(gtime.NewFromStr("2026-08-15 00:00:00")) if beginAt == nil { t.Fatal("renew begin date is nil") } if got := beginAt.Format("Y-m-d H:i:s"); got != "2026-08-15 23:59:59" { t.Fatalf("renew begin date = %s, want 2026-08-15 23:59:59", got) } if got := addMonths(beginAt.Time, 12).Format("2006-01-02 15:04:05"); got != "2027-08-15 23:59:59" { t.Fatalf("renew end date = %s, want 2027-08-15 23:59:59", got) } nextBeginAt := renewBeginDateFromExpire(gtime.NewFromStr("2027-07-31 00:00:00")) if nextBeginAt == nil { t.Fatal("next renew begin date is nil") } if got := nextBeginAt.Format("Y-m-d H:i:s"); got != "2027-07-31 23:59:59" { t.Fatalf("next renew begin date = %s, want 2027-07-31 23:59:59", got) } giftBeginAt := renewBeginDateFromExpire(gtime.NewFromStr("2026-08-31 00:00:00")) if giftBeginAt == nil { t.Fatal("gift renew begin date is nil") } if got := giftBeginAt.Format("Y-m-d H:i:s"); got != "2026-08-31 23:59:59" { t.Fatalf("gift renew begin date = %s, want 2026-08-31 23:59:59", got) } if got := addMonths(giftBeginAt.Time, 13).Format("2006-01-02 15:04:05"); got != "2027-09-30 23:59:59" { t.Fatalf("renew end date with gift month = %s, want 2027-09-30 23:59:59", got) } } func TestValidateRenewSameCurrentPeriod(t *testing.T) { sameRows := []*RenewRow{ { RowNo: 1, OldEffTime: gtime.NewFromStr("2026-01-01 00:00:00"), OldExpireTime: gtime.NewFromStr("2026-12-31 23:59:59"), }, { RowNo: 2, OldEffTime: gtime.NewFromStr("2026-01-01 00:00:00"), OldExpireTime: gtime.NewFromStr("2026-12-31 23:59:59"), }, } if err := validateRenewSameCurrentPeriod(sameRows, nil); err != nil { t.Fatalf("validateRenewSameCurrentPeriod() error = %v", err) } diffBeginRows := []*RenewRow{ { RowNo: 1, OldEffTime: gtime.NewFromStr("2026-01-01 00:00:00"), OldExpireTime: gtime.NewFromStr("2026-12-31 23:59:59"), }, { RowNo: 2, OldEffTime: gtime.NewFromStr("2026-02-01 00:00:00"), OldExpireTime: gtime.NewFromStr("2026-12-31 23:59:59"), }, } if err := validateRenewSameCurrentPeriod(diffBeginRows, nil); err == nil { t.Fatalf("validateRenewSameCurrentPeriod() error = nil, want begin mismatch") } diffExpireRows := []*RenewRow{ { RowNo: 1, OldEffTime: gtime.NewFromStr("2026-01-01 00:00:00"), OldExpireTime: gtime.NewFromStr("2026-12-31 23:59:59"), }, { RowNo: 2, OldEffTime: gtime.NewFromStr("2026-01-01 00:00:00"), OldExpireTime: gtime.NewFromStr("2027-01-31 23:59:59"), }, } if err := validateRenewSameCurrentPeriod(diffExpireRows, nil); err == nil { t.Fatalf("validateRenewSameCurrentPeriod() error = nil, want expire mismatch") } missingTimeRows := []*RenewRow{ { RowNo: 1, OldEffTime: gtime.NewFromStr("2026-01-01 00:00:00"), OldExpireTime: gtime.NewFromStr("2026-12-31 23:59:59"), }, {RowNo: 2, OldEffTime: gtime.NewFromStr("2026-01-01 00:00:00")}, } if err := validateRenewSameCurrentPeriod(missingTimeRows, nil); err == nil { t.Fatalf("validateRenewSameCurrentPeriod() error = nil, want missing time") } rowErrors := map[int]string{2: "existing validation error"} if err := validateRenewSameCurrentPeriod(missingTimeRows, rowErrors); err != nil { t.Fatalf("validateRenewSameCurrentPeriod() with rowErrors error = %v", err) } } func TestRenewPriceOwnerUsesCurrentCustomerParent(t *testing.T) { if got := renewPriceOwnerDeptID(&rc.SysDeptRow{DeptId: 117, ParentId: 100}); got != 100 { t.Fatalf("price owner dept = %d, want 100", got) } if got := renewPriceOwnerDeptID(&rc.SysDeptRow{DeptId: 100, ParentId: 0}); got != 0 { t.Fatalf("top-level price owner dept = %d, want 0", got) } } func TestRenewExpireAfterAllowedWindow(t *testing.T) { now := time.Date(2026, 8, 7, 16, 50, 39, 0, time.Local) if renewExpireAfterAllowedWindow(nil, now) { t.Fatal("nil expire time should not be rejected") } if renewExpireAfterAllowedWindow(gtime.NewFromTime(now.AddDate(0, 6, 0)), now) { t.Fatal("exactly 6 months ahead should be allowed") } if !renewExpireAfterAllowedWindow(gtime.NewFromTime(now.AddDate(0, 6, 0).Add(time.Second)), now) { t.Fatal("beyond 6 months should be rejected") } } func TestBuildPrepareInfoWritesChangeFlatContract(t *testing.T) { got := buildPrepareInfo(nil, 102, &prepareOutcome{ customer: &customerInfo{ DeptID: 117, Name: "customer", }, changeSnap: &ChangePrepareSnapshot{ SourceBatchNo: "CHG-1", CustomerAgentId: 117, CustomerName: "customer", OldPackageCode: "OLD", OldPackageName: "old package", OldPackageType: 101, OldPackageFlowMb: 1024, Quantity: 1, }, changePrice: &ChangePriceSnapshot{ CustomerAgentId: 117, NewPackageCode: "NEW", NewPackageName: "new package", PriceSource: PriceSourceAgentDefault, AgentDefaultPriceId: 33, TargetPriceId: 44, ChangeUnitPrice: 6.5, RenewableDurationOptions: []int{1, 2}, }, }, 1) if got["customerAgentId"] != int64(117) { t.Fatalf("customerAgentId = %v, want 117", got["customerAgentId"]) } if got["sourceBatchNo"] != "CHG-1" { t.Fatalf("sourceBatchNo = %v, want CHG-1", got["sourceBatchNo"]) } if got["oldPackageCode"] != "OLD" { t.Fatalf("oldPackageCode = %v, want OLD", got["oldPackageCode"]) } if got["newPackageCode"] != "NEW" { t.Fatalf("newPackageCode = %v, want NEW", got["newPackageCode"]) } if got["changeUnitPrice"] != 6.5 { t.Fatalf("changeUnitPrice = %v, want 6.5", got["changeUnitPrice"]) } if got["changePrepareSnapshot"] == nil || got["changePriceSnapshot"] == nil { t.Fatalf("change snapshots are missing") } } func TestBuildPrepareDetailSnapshotIncludesChangeSubscriptionContract(t *testing.T) { out := &prepareOutcome{ rows: map[int]*RenewRow{ 1: { RowNo: 1, ICCID: "8986001", MSISDN: "13800000000", IMSI: "460001", OldPkgCode: "OLD", OldPkgName: "old package", OldBindSubNo: "SUB-OLD", OldBindOrdNo: "SO-OLD", SourceBatchNo: "OUT-1", }, }, changePrice: &ChangePriceSnapshot{ NewPackageCode: "NEW", NewPackageName: "new package", }, } snapshot := buildPrepareDetailSnapshot(&rc.BatchDetailRow{RowNo: 1}, out, 102) want := map[string]string{ "oldPackageCode": "OLD", "oldPackageName": "old package", "oldSubscriptionNo": "SUB-OLD", "oldOrderSn": "SO-OLD", "sourceBatchNo": "OUT-1", "newPackageCode": "NEW", "newPackageName": "new package", } for key, expected := range want { if got := snapshot.Get(key).String(); got != expected { t.Fatalf("%s = %q, want %q", key, got, expected) } } } func TestPriceByPackageUsesAdminContractFormula(t *testing.T) { pkg := &rc.FlowPackageInfo{ CostPrice: 10, SysDefaultRatio: 120, SysDefaultProfit: 1.23, } if got := priceByPackage(pkg, nil); got != 13.23 { t.Fatalf("system price = %v, want 13.23", got) } got := priceByPackage(pkg, &rc.FlowPackagePrice{ AgentCostRatio: 80, AgentCostProfit: 2.5, }) if got != 10.5 { t.Fatalf("override price = %v, want 10.5", got) } }