Files

544 lines
20 KiB
Go

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",
}
}