package iot import ( "strings" "testing" "time" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity" ) func TestAddRenewDurationByUnitKeepsSameCalendarDate(t *testing.T) { begin := time.Date(2026, time.September, 1, 0, 0, 0, 0, time.Local) got := addRenewDurationByUnit(begin, "year", 1) want := time.Date(2027, time.September, 1, 23, 59, 59, 0, time.Local) if !got.Equal(want) { t.Fatalf( "addRenewDurationByUnit() = %s, want %s", got.Format(time.RFC3339), want.Format(time.RFC3339), ) } } func TestValidateRenewConfirmContract(t *testing.T) { snapshot := &model.TSimCardRenewPriceSnapshot{ RenewableDurationOptions: []int{12, 24, 36}, } if err := validateRenewConfirmContract(snapshot, 24); err != nil { t.Fatalf("valid renew contract rejected: %v", err) } if err := validateRenewConfirmContract(snapshot, 6); err == nil { t.Fatal("renew duration outside prepare contract was accepted") } if err := validateRenewConfirmContract(nil, 12); err == nil { t.Fatal("missing renew contract was accepted") } } func TestValidateChangeConfirmContract(t *testing.T) { prepareSnapshot := &model.TSimCardChangePrepareSnapshot{ OldPackageCode: "PKG-OLD", } priceSnapshot := &model.TSimCardChangePriceSnapshot{ NewPackageCode: "PKG-NEW", RenewableDurationOptions: []int{12, 24}, } valid := &model.TSimCardChangeBatchSubmitParam{ NewPackageCode: "PKG-NEW", DurationMonths: 12, EffectiveMode: cardChangeEffectiveNextMonth, } if err := validateChangeConfirmContract(prepareSnapshot, priceSnapshot, cardChangeEffectiveNextMonth, valid); err != nil { t.Fatalf("valid change contract rejected: %v", err) } if err := validateChangeConfirmContract(prepareSnapshot, priceSnapshot, "", valid); err != nil { t.Fatalf("legacy change contract without effective mode rejected: %v", err) } tests := []struct { name string in model.TSimCardChangeBatchSubmitParam }{ { name: "current package selected", in: model.TSimCardChangeBatchSubmitParam{ NewPackageCode: "PKG-OLD", DurationMonths: 12, EffectiveMode: cardChangeEffectiveNextMonth, }, }, { name: "target package changed", in: model.TSimCardChangeBatchSubmitParam{ NewPackageCode: "PKG-OTHER", DurationMonths: 12, EffectiveMode: cardChangeEffectiveNextMonth, }, }, { name: "duration outside contract", in: model.TSimCardChangeBatchSubmitParam{ NewPackageCode: "PKG-NEW", DurationMonths: 6, EffectiveMode: cardChangeEffectiveNextMonth, }, }, { name: "effective mode changed", in: model.TSimCardChangeBatchSubmitParam{ NewPackageCode: "PKG-NEW", DurationMonths: 12, EffectiveMode: cardChangeEffectiveImmediate, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := validateChangeConfirmContract(prepareSnapshot, priceSnapshot, cardChangeEffectiveNextMonth, &tt.in); err == nil { t.Fatal("invalid change contract was accepted") } }) } if err := validateChangeConfirmContract(nil, priceSnapshot, cardChangeEffectiveNextMonth, valid); err == nil { t.Fatal("missing change contract was accepted") } } func TestNormalizeAddPackagePaymentIntent(t *testing.T) { tests := []struct { name string mode string confirmed bool isTopDept bool wantMode string wantConfirm bool wantErr string }{ {name: "child default uses balance", wantMode: "balance"}, {name: "top default uses balance", isTopDept: true, wantMode: "balance"}, {name: "balance clears offline confirmation", mode: "balance", confirmed: true, isTopDept: true, wantMode: "balance"}, {name: "child cannot use offline", mode: "offline", confirmed: true, wantErr: "必须使用余额支付"}, {name: "top offline requires confirmation", mode: "offline", isTopDept: true, wantErr: "请确认线下合同"}, {name: "top offline keeps confirmation", mode: "offline", confirmed: true, isTopDept: true, wantMode: "offline", wantConfirm: true}, {name: "invalid payment mode", mode: "cash", isTopDept: true, wantErr: "付款方式不正确"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { gotMode, gotConfirm, err := normalizeAddPackagePaymentIntent(tt.mode, tt.confirmed, tt.isTopDept) if tt.wantErr != "" { if err == nil || !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("error = %v, want substring %q", err, tt.wantErr) } return } if err != nil { t.Fatalf("normalizeAddPackagePaymentIntent() error = %v", err) } if gotMode != tt.wantMode { t.Fatalf("payment mode = %q, want %q", gotMode, tt.wantMode) } if gotConfirm != tt.wantConfirm { t.Fatalf("offline confirmation = %v, want %v", gotConfirm, tt.wantConfirm) } }) } } func TestNormalizedChangePrepareSourceBatchNo(t *testing.T) { tests := []struct { name string sourceType int mainSource string snapshot string want string }{ { name: "input ignores self reference", sourceType: consts.BatchSourceTypeBatchInput, mainSource: "CHG20260803170000INPUT", snapshot: "CHG20260803170000INPUT", }, { name: "file ignores stale source", sourceType: consts.BatchSourceTypeFile, mainSource: "CHG20260803170000FILE", snapshot: "OUT20260803STALE", }, { name: "batch selection keeps snapshot source", sourceType: consts.BatchSourceTypeBatchSelect, mainSource: "OUT20260803MAIN", snapshot: "OUT20260803CONFIRMED", want: "OUT20260803CONFIRMED", }, { name: "batch selection repairs worker self reference", sourceType: consts.BatchSourceTypeBatchSelect, mainSource: "OUT20260803MAIN", snapshot: "CHG20260803170000BATCH", want: "OUT20260803MAIN", }, { name: "legacy self reference without real source becomes empty", sourceType: consts.BatchSourceTypeBatchSelect, mainSource: "CHG20260803170000BATCH", snapshot: "CHG20260803170000BATCH", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { batchNo := "CHG20260803170000BATCH" if tt.sourceType == consts.BatchSourceTypeBatchInput { batchNo = "CHG20260803170000INPUT" } if tt.sourceType == consts.BatchSourceTypeFile { batchNo = "CHG20260803170000FILE" } batch := &entity.BatchMain{ BatchNo: batchNo, SourceType: tt.sourceType, SourceBatchNo: tt.mainSource, } if got := normalizedChangePrepareSourceBatchNo(batch, tt.snapshot); got != tt.want { t.Fatalf("sourceBatchNo = %q, want %q", got, tt.want) } }) } }