package renewchange import ( "strings" "testing" "time" "github.com/gogf/gf/v2/encoding/gjson" "github.com/gogf/gf/v2/os/gtime" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts" rc "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/repository/renewchange" ) func TestValidateRuntimeRenewDurationRejectsUnsupportedOption(t *testing.T) { pkg := &rc.FlowPackageInfo{ PackageCode: "PKG001", BillingMethod: consts.BillingMethodMonth, PackageName: "测试套餐", RenewableDurationConfig: gjson.New([]any{1, 2, 3}), } options, err := validateRuntimeRenewDuration(pkg, 1) if err != nil { t.Fatalf("validateRuntimeRenewDuration returned error: %v", err) } if len(options) != 3 { t.Fatalf("options = %v, want 3 entries", options) } _, err = validateRuntimeRenewDuration(pkg, 4) if err == nil { t.Fatal("expected unsupported renew duration to be rejected") } if got := err.Error(); !strings.Contains(got, "4年") || !strings.Contains(got, "测试套餐(PKG001)") || !strings.Contains(got, "3年") { t.Fatalf("error message %q does not include duration/package/options", got) } } func TestValidateRuntimeRenewDurationNormalizesLegacyMonthOptions(t *testing.T) { pkg := &rc.FlowPackageInfo{ PackageCode: "PKG001", PackageName: "测试套餐", RenewableDurationConfig: gjson.New([]any{12, 24, 36}), } options, err := validateRuntimeRenewDuration(pkg, 2) if err != nil { t.Fatalf("validateRuntimeRenewDuration returned error: %v", err) } if got := options; len(got) != 3 || got[0] != 1 || got[1] != 2 || got[2] != 3 { t.Fatalf("options = %v, want [1 2 3]", got) } } func TestAddMonthsClampsToTargetMonthEnd(t *testing.T) { begin := time.Date(2027, time.January, 31, 0, 0, 0, 0, time.UTC) want := time.Date(2027, time.February, 28, 0, 0, 0, 0, time.UTC) if got := addMonths(begin, 1); !got.Equal(want) { t.Fatalf("addMonths() = %v, want %v", got, want) } } func TestRenewBaseSubscriptionForCardPrefersLaterCardExpire(t *testing.T) { card := &rc.SimCardInfo{ Iccid: "8986001", CurrentPackageCode: "PKG001", FlowPkgEndAt: gtime.NewFromStr("2026-08-31 00:00:00"), FlowPkgPresentAt: gtime.NewFromStr("2026-09-30 00:00:00"), FlowPkgRealPresent: gtime.NewFromStr("2026-10-31 00:00:00"), } bind := &rc.SimCardBindPkg{ Iccid: "8986001", PackageCode: "PKG001", ExpectedExpireTime: gtime.NewFromStr("2026-08-31 00:00:00"), } if got := renewBaseSubscriptionForCard(card, bind, nil); got == nil || got.ExpectedExpireTime.Format("Y-m-d") != "2026-10-31" { t.Fatalf("renew base expire = %v, want 2026-10-31", got) } } func TestParseExecutionContractConvertsYearDurationToActualMonths(t *testing.T) { main := &rc.BatchMainRow{ BatchNo: "REN20260806000000TEST", BatchType: consts.BatchTypeRenew, TenantId: "000000", DeptId: 1000, CreatedDept: 1000, CreatedBy: 1, BatchInfo: gjson.New(map[string]any{ "customerAgentId": 1001, "packageCode": "PKG001", "packageName": "Test package", "renewUnitPrice": 120, "quantity": 2, "billingMethod": consts.BillingMethodYear, "renewDurationUnit": consts.RenewDurationUnitYear, "durationValue": 1, "durationMonths": 1, "offlineContractCompleted": true, "paymentMode": "offline", }), } contract, err := ParseExecutionContract(main) if err != nil { t.Fatalf("ParseExecutionContract returned error: %v", err) } if contract.DurationDisplayValue != 1 { t.Fatalf("display duration = %d, want 1", contract.DurationDisplayValue) } if contract.DurationMonths != 12 { t.Fatalf("actual duration months = %d, want 12", contract.DurationMonths) } if got := contract.TotalAmount(); got != 240 { t.Fatalf("total amount = %v, want 240", got) } } func TestRuntimeRenewFactsRequiresSameCurrentPeriod(t *testing.T) { main := &rc.BatchMainRow{ BatchNo: "REN20260806000000TEST", TenantId: "000000", } begin := gtime.NewFromStr("2026-01-01 00:00:00") expire := gtime.NewFromStr("2026-12-31 23:59:59") details := []*rc.BatchDetailRow{ {RowNo: 1, ItemId: "898600000000000001"}, {RowNo: 2, ItemId: "898600000000000002"}, } cards := map[string]*rc.SimCardInfo{ details[0].ItemId: { Iccid: details[0].ItemId, DeptId: 117, CurrentPackageCode: "PKG001", PackageStatus: consts.PackageStatusEffective, TenantId: main.TenantId, }, details[1].ItemId: { Iccid: details[1].ItemId, DeptId: 117, CurrentPackageCode: "PKG001", PackageStatus: consts.PackageStatusEffective, TenantId: main.TenantId, }, } binds := map[string]*rc.SimCardBindPkg{ details[0].ItemId: { Iccid: details[0].ItemId, DeptId: 117, PackageCode: "PKG001", EffectiveTime: begin, ExpectedExpireTime: expire, TenantId: main.TenantId, }, details[1].ItemId: { Iccid: details[1].ItemId, DeptId: 117, PackageCode: "PKG001", EffectiveTime: begin, ExpectedExpireTime: expire, TenantId: main.TenantId, }, } now := time.Date(2026, 8, 1, 0, 0, 0, 0, time.Local) if _, _, _, err := runtimeRenewFacts(main, details, cards, binds, nil, now); err != nil { t.Fatalf("runtimeRenewFacts() error = %v", err) } binds[details[1].ItemId].ExpectedExpireTime = gtime.NewFromStr("2027-01-31 23:59:59") if _, _, _, err := runtimeRenewFacts(main, details, cards, binds, nil, now); err == nil { t.Fatalf("runtimeRenewFacts() error = nil, want same period failure") } } func TestValidateRenewExecutionsUsesConfirmedDuration(t *testing.T) { now := time.Date(2026, 8, 3, 10, 0, 0, 0, time.UTC) base := now.AddDate(0, 1, 0) contract := testRenewContract(12, 2) details := []*rc.BatchDetailRow{{RowNo: 1, ItemId: "8986000000000000001"}} cards := map[string]*rc.SimCardInfo{ details[0].ItemId: { Iccid: details[0].ItemId, DeptId: contract.CustomerAgentID, CurrentPackageCode: contract.PackageCode, PackageStatus: consts.PackageStatusEffective, FlowPkgBeginAt: gtime.NewFromTime(now.AddDate(0, -1, 0)), FlowPkgEndAt: gtime.NewFromTime(base), OutboundBatchNo: contract.SourceBatchNo, TenantId: contract.TenantID, }, } binds := map[string]*rc.SimCardBindPkg{ details[0].ItemId: { Iccid: details[0].ItemId, BatchNo: contract.SourceBatchNo, Msisdn: cards[details[0].ItemId].Msisdn, DeptId: contract.CustomerAgentID, PackageCode: contract.PackageCode, ExpectedExpireTime: gtime.NewFromTime(base), TenantId: contract.TenantID, }, } got, err := validateRenewExecutions(contract, details, cards, binds, nil, now) if err != nil { t.Fatalf("validateRenewExecutions returned error: %v", err) } renewBegin := renewBeginDateFromExpire(gtime.NewFromTime(base)) wantEnd := addMonths(renewBegin.Time, 14) if len(got) != 1 || !got[0].end.Equal(wantEnd) { t.Fatalf("renew end = %v, want %v", got[0].end, wantEnd) } } func TestValidateRenewExecutionsUsesLatestBindExpiration(t *testing.T) { now := time.Date(2026, 8, 3, 10, 0, 0, 0, time.UTC) contract := testRenewContract(12, 0) detail := &rc.BatchDetailRow{RowNo: 1, ItemId: "8986000000000000002"} cardEnd := gtime.NewFromTime(time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)) cards := map[string]*rc.SimCardInfo{ detail.ItemId: { Iccid: detail.ItemId, DeptId: contract.CustomerAgentID, CurrentPackageCode: contract.PackageCode, PackageStatus: consts.PackageStatusEffective, FlowPkgEndAt: cardEnd, OutboundBatchNo: contract.SourceBatchNo, TenantId: contract.TenantID, }, } binds := map[string]*rc.SimCardBindPkg{ detail.ItemId: { Iccid: detail.ItemId, BatchNo: contract.SourceBatchNo, Msisdn: cards[detail.ItemId].Msisdn, DeptId: contract.CustomerAgentID, PackageCode: contract.PackageCode, ExpectedExpireTime: gtime.NewFromTime(cardEnd.Time.AddDate(0, 0, 1)), TenantId: contract.TenantID, }, } got, err := validateRenewExecutions(contract, []*rc.BatchDetailRow{detail}, cards, binds, nil, now) if err != nil { t.Fatalf("validateRenewExecutions returned error: %v", err) } renewBegin := renewBeginDateFromExpire(gtime.NewFromTime(cardEnd.Time.AddDate(0, 0, 1))) wantEnd := addMonths(renewBegin.Time, contract.DurationMonths) if len(got) != 1 || !got[0].end.Equal(wantEnd) { t.Fatalf("renew end = %v, want %v", got[0].end, wantEnd) } } func TestValidateRenewExecutionsAllowsRenewalFromExpiredSubscription(t *testing.T) { now := time.Date(2026, 8, 3, 10, 0, 0, 0, time.UTC) contract := testRenewContract(12, 0) detail := &rc.BatchDetailRow{RowNo: 1, ItemId: "8986000000000000006"} expiredAt := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC) cards := map[string]*rc.SimCardInfo{ detail.ItemId: { Iccid: detail.ItemId, DeptId: contract.CustomerAgentID, CurrentPackageCode: contract.PackageCode, PackageStatus: consts.PackageStatusExpired, OutboundBatchNo: contract.SourceBatchNo, TenantId: contract.TenantID, }, } binds := map[string]*rc.SimCardBindPkg{ detail.ItemId: { Iccid: detail.ItemId, BatchNo: contract.SourceBatchNo, DeptId: contract.CustomerAgentID, PackageCode: contract.PackageCode, EffectiveStatus: consts.EffectiveStatusExpired, ExpectedExpireTime: gtime.NewFromTime(expiredAt), TenantId: contract.TenantID, }, } got, err := validateRenewExecutions(contract, []*rc.BatchDetailRow{detail}, cards, binds, nil, now) if err != nil { t.Fatalf("validateRenewExecutions returned error: %v", err) } renewBegin := renewBeginDateFromExpire(gtime.NewFromTime(expiredAt)) wantEnd := addMonths(renewBegin.Time, contract.DurationMonths) if len(got) != 1 || !got[0].begin.Equal(renewBegin.Time) || !got[0].end.Equal(wantEnd) { t.Fatalf("renew range = %v - %v, want %v - %v", got[0].begin, got[0].end, renewBegin.Time, wantEnd) } } func TestValidateRenewExecutionsAppendsAfterFuturePendingExpiration(t *testing.T) { now := time.Date(2026, 8, 3, 10, 0, 0, 0, time.UTC) contract := testRenewContract(12, 0) detail := &rc.BatchDetailRow{RowNo: 1, ItemId: "8986000000000000005"} currentExpire := time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC) futureExpire := time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC) cards := map[string]*rc.SimCardInfo{ detail.ItemId: { Iccid: detail.ItemId, DeptId: contract.CustomerAgentID, CurrentPackageCode: contract.PackageCode, PackageStatus: consts.PackageStatusEffective, OutboundBatchNo: contract.SourceBatchNo, TenantId: contract.TenantID, }, } binds := map[string]*rc.SimCardBindPkg{ detail.ItemId: { Iccid: detail.ItemId, BatchNo: contract.SourceBatchNo, DeptId: contract.CustomerAgentID, PackageCode: contract.PackageCode, ExpectedExpireTime: gtime.NewFromTime(currentExpire), TenantId: contract.TenantID, }, } futureBinds := map[string][]*rc.SimCardBindPkg{ detail.ItemId: { { Iccid: detail.ItemId, BatchNo: "REN-FUTURE", DeptId: contract.CustomerAgentID, PackageCode: contract.PackageCode, ExpectedExpireTime: gtime.NewFromTime(futureExpire), TenantId: contract.TenantID, }, }, } got, err := validateRenewExecutions(contract, []*rc.BatchDetailRow{detail}, cards, binds, futureBinds, now) if err != nil { t.Fatalf("validateRenewExecutions returned error: %v", err) } renewBegin := renewBeginDateFromExpire(gtime.NewFromTime(futureExpire)) wantEnd := addMonths(renewBegin.Time, contract.DurationMonths) if len(got) != 1 || !got[0].begin.Equal(renewBegin.Time) || !got[0].end.Equal(wantEnd) { t.Fatalf("renew range = %v - %v, want %v - %v", got[0].begin, got[0].end, renewBegin.Time, wantEnd) } } func TestValidateRenewExecutionsRejectsPendingExpirationBeyondWindow(t *testing.T) { now := time.Date(2026, 8, 3, 10, 0, 0, 0, time.UTC) contract := testRenewContract(12, 0) detail := &rc.BatchDetailRow{RowNo: 1, ItemId: "8986000000000000007"} currentExpire := time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC) futureExpire := time.Date(2027, 9, 1, 0, 0, 0, 0, time.UTC) cards := map[string]*rc.SimCardInfo{ detail.ItemId: { Iccid: detail.ItemId, DeptId: contract.CustomerAgentID, CurrentPackageCode: contract.PackageCode, PackageStatus: consts.PackageStatusEffective, OutboundBatchNo: contract.SourceBatchNo, TenantId: contract.TenantID, }, } binds := map[string]*rc.SimCardBindPkg{ detail.ItemId: { Iccid: detail.ItemId, BatchNo: contract.SourceBatchNo, DeptId: contract.CustomerAgentID, PackageCode: contract.PackageCode, ExpectedExpireTime: gtime.NewFromTime(currentExpire), TenantId: contract.TenantID, }, } futureBinds := map[string][]*rc.SimCardBindPkg{ detail.ItemId: { { Iccid: detail.ItemId, BatchNo: "REN-FUTURE", DeptId: contract.CustomerAgentID, PackageCode: contract.PackageCode, ExpectedExpireTime: gtime.NewFromTime(futureExpire), TenantId: contract.TenantID, }, }, } _, err := validateRenewExecutions(contract, []*rc.BatchDetailRow{detail}, cards, binds, futureBinds, now) if err == nil || !strings.Contains(err.Error(), renewExpireFailureReason) { t.Fatalf("validateRenewExecutions error = %v, want renew window failure", err) } } func TestValidateRunningRenewDetailsRejectsChangedSnapshot(t *testing.T) { cases := []struct { name string locked []*rc.BatchDetailRow loaded []*rc.BatchDetailRow }{ { name: "count", locked: []*rc.BatchDetailRow{{Id: 1, RowNo: 1, ItemId: "8986001", DetailStatus: consts.BatchDetailStatusRunning}}, loaded: nil, }, { name: "id", locked: []*rc.BatchDetailRow{{Id: 2, RowNo: 1, ItemId: "8986001", DetailStatus: consts.BatchDetailStatusRunning}}, loaded: []*rc.BatchDetailRow{{Id: 1, RowNo: 1, ItemId: "8986001", DetailStatus: consts.BatchDetailStatusRunning}}, }, { name: "row number", locked: []*rc.BatchDetailRow{{Id: 1, RowNo: 2, ItemId: "8986001", DetailStatus: consts.BatchDetailStatusRunning}}, loaded: []*rc.BatchDetailRow{{Id: 1, RowNo: 1, ItemId: "8986001", DetailStatus: consts.BatchDetailStatusRunning}}, }, { name: "iccid", locked: []*rc.BatchDetailRow{{Id: 1, RowNo: 1, ItemId: "8986002", DetailStatus: consts.BatchDetailStatusRunning}}, loaded: []*rc.BatchDetailRow{{Id: 1, RowNo: 1, ItemId: "8986001", DetailStatus: consts.BatchDetailStatusRunning}}, }, { name: "status", locked: []*rc.BatchDetailRow{{Id: 1, RowNo: 1, ItemId: "8986001", DetailStatus: consts.BatchDetailStatusSuccess}}, loaded: []*rc.BatchDetailRow{{Id: 1, RowNo: 1, ItemId: "8986001", DetailStatus: consts.BatchDetailStatusRunning}}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { if err := validateRunningRenewDetails(tc.locked, tc.loaded); err == nil { t.Fatal("expected changed renew details to be rejected") } }) } } func TestValidateRenewExecutionsUsesConfirmedPackageBeginTime(t *testing.T) { now := time.Date(2026, 8, 3, 10, 0, 0, 0, time.Local) contract := testRenewContract(12, 0) confirmedBegin := time.Date(2026, 10, 1, 0, 0, 0, 0, time.Local) contract.PackageBeginTime = gtime.NewFromTime(confirmedBegin) detail := &rc.BatchDetailRow{RowNo: 1, ItemId: "8986000000000000004"} latestExpire := time.Date(2026, 9, 1, 0, 0, 0, 0, time.Local) cards := map[string]*rc.SimCardInfo{ detail.ItemId: { Iccid: detail.ItemId, DeptId: contract.CustomerAgentID, CurrentPackageCode: contract.PackageCode, PackageStatus: consts.PackageStatusEffective, OutboundBatchNo: contract.SourceBatchNo, TenantId: contract.TenantID, }, } binds := map[string]*rc.SimCardBindPkg{ detail.ItemId: { Iccid: detail.ItemId, BatchNo: contract.SourceBatchNo, DeptId: contract.CustomerAgentID, PackageCode: contract.PackageCode, ExpectedExpireTime: gtime.NewFromTime(latestExpire), TenantId: contract.TenantID, }, } got, err := validateRenewExecutions(contract, []*rc.BatchDetailRow{detail}, cards, binds, nil, now) if err != nil { t.Fatalf("validateRenewExecutions returned error: %v", err) } wantEnd := addMonths(confirmedBegin, contract.DurationMonths) if len(got) != 1 || !got[0].begin.Equal(confirmedBegin) || !got[0].end.Equal(wantEnd) { t.Fatalf("renew range = %v - %v, want %v - %v", got[0].begin, got[0].end, confirmedBegin, wantEnd) } } func TestBuildRenewBindUsesConfirmedContract(t *testing.T) { now := time.Date(2026, 8, 3, 10, 0, 0, 0, time.UTC) begin := now.AddDate(0, 1, 0) contract := testRenewContract(12, 3) pkg := &rc.FlowPackageInfo{PackageCode: contract.PackageCode, PackageType: 7} execution := renewExecution{ card: &rc.SimCardInfo{Iccid: "8986000000000000003", Msisdn: "13800000000"}, bind: &rc.SimCardBindPkg{ExpectedExpireTime: gtime.NewFromTime(begin)}, begin: begin, end: contract.ServiceEnd(begin), } bind := buildRenewBind(contract, pkg, execution, "SO20260803100000ABC", "SUB20260803100000ABC", now) if bind.DurationMonths != 12 || bind.GiftDurationMonths != 3 { t.Fatalf("duration = %d+%d, want 12+3", bind.DurationMonths, bind.GiftDurationMonths) } if bind.PackageType != 7 || bind.EffectiveTime.Time != begin { t.Fatalf("bind contract fields were not preserved: %+v", bind) } } func TestBuildRenewDetailSnapshotUsesConfirmedBatchAmount(t *testing.T) { contract := testRenewContract(12, 0) contract.Quantity = 2 execution := renewExecution{ detail: &rc.BatchDetailRow{RowNo: 1}, card: &rc.SimCardInfo{Iccid: "8986000000000000003"}, bind: &rc.SimCardBindPkg{}, begin: time.Date(2026, 8, 3, 0, 0, 0, 0, time.UTC), end: time.Date(2027, 8, 3, 0, 0, 0, 0, time.UTC), } pkg := &rc.FlowPackageInfo{PackageCode: contract.PackageCode, PackageName: contract.PackageName} snapshot := buildRenewDetailSnapshot(execution, contract, pkg, "SO-1", "SUB-1") if got := snapshot.Get("amount").Float64(); got != 240 { t.Fatalf("detail amount = %v, want confirmed batch amount 240", got) } } func TestRenewPackageStatus(t *testing.T) { now := time.Date(2026, 8, 3, 10, 0, 0, 0, time.UTC) if got := renewPackageStatus(now, gtime.NewFromTime(now.AddDate(0, 1, 0)), gtime.NewFromTime(now.AddDate(0, 13, 0))); got != consts.PackageStatusPending { t.Fatalf("future package status = %d, want pending", got) } if got := renewPackageStatus(now, gtime.NewFromTime(now.AddDate(0, -1, 0)), gtime.NewFromTime(now.AddDate(0, 1, 0))); got != consts.PackageStatusEffective { t.Fatalf("active package status = %d, want effective", got) } if got := renewPackageStatus(now, gtime.NewFromTime(now.AddDate(0, -2, 0)), gtime.NewFromTime(now.AddDate(0, -1, 0))); got != consts.PackageStatusExpired { t.Fatalf("expired package status = %d, want expired", got) } } func testRenewContract(duration, gift int) *ExecutionContract { return &ExecutionContract{ BatchNo: "REN20260803100000TEST", BatchType: consts.BatchTypeRenew, SourceBatchNo: "OUT20260801TEST", TenantID: "000000", CustomerAgentID: 1001, SellerAgentID: 1000, CreatedDeptID: 1000, OperatorUserID: 1, PackageCode: "PKG001", PackageName: "Test package", PriceSource: PriceSourceSystem, UnitPrice: 10, Quantity: 1, DurationMonths: duration, GiftDurationMonths: gift, OfflineContractCompleted: true, BuyerNote: "buyer", AdminNote: "admin", Remark: "renew test", } }