package renew import ( "context" "path/filepath" "reflect" "testing" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/consts" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/library/batchinput/text" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model" "git.5iot.com/iotlcm-public/iotlcm-admin-svr/internal/model/entity" "github.com/xuri/excelize/v2" ) func TestExcelTemplateHeaders(t *testing.T) { t.Parallel() expected := []string{ "ICCID", "MSISDN", "IMSI", "续费年限", "是否走线下", } if !reflect.DeepEqual(ExcelTemplateHeaders(), expected) { t.Fatalf("unexpected headers: %#v", ExcelTemplateHeaders()) } } func TestRenewDurationValueToMonths(t *testing.T) { t.Parallel() tests := []struct { name string unit string value int want int }{ {name: "one month", unit: consts.RenewDurationUnitMonth, value: 1, want: 1}, {name: "one year", unit: consts.RenewDurationUnitYear, value: 1, want: 12}, {name: "unknown unit defaults to year", unit: "unknown", value: 2, want: 24}, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() if got := consts.DurationValueToMonths(test.unit, test.value); got != test.want { t.Fatalf("DurationValueToMonths(%q, %d) = %d, want %d", test.unit, test.value, got, test.want) } }) } } func TestRenewDisplayDurationAndAmount(t *testing.T) { t.Parallel() yearSnapshot := &model.TSimCardRenewPriceSnapshot{ RenewDurationUnit: consts.RenewDurationUnitYear, RenewUnitPrice: 10, } yearParam := &model.TSimCardRenewBatchSubmitParam{DurationYears: 1} if got := renewDisplayDurationValue(yearParam); got != 1 { t.Fatalf("renewDisplayDurationValue(year) = %d, want 1", got) } if got := renewActualDurationMonths(yearSnapshot, renewDisplayDurationValue(yearParam)); got != 12 { t.Fatalf("renewActualDurationMonths(year) = %d, want 12", got) } yearAmount, err := resolveRenewAmount(yearSnapshot, yearParam, 2) if err != nil { t.Fatalf("resolveRenewAmount(year) failed: %v", err) } if yearAmount != 20 { t.Fatalf("resolveRenewAmount(year) = %.2f, want 20.00", yearAmount) } monthSnapshot := &model.TSimCardRenewPriceSnapshot{ RenewDurationUnit: consts.RenewDurationUnitMonth, RenewUnitPrice: 10, } monthParam := &model.TSimCardRenewBatchSubmitParam{DurationMonths: 1} if got := renewActualDurationMonths(monthSnapshot, renewDisplayDurationValue(monthParam)); got != 12 { t.Fatalf("renewActualDurationMonths(month snapshot) = %d, want 12", got) } actualMonthParam := &model.TSimCardRenewBatchSubmitParam{DurationMonths: 12} if got := renewDisplayDurationValue(actualMonthParam); got != 1 { t.Fatalf("renewDisplayDurationValue(actual months legacy) = %d, want 1", got) } monthAmount, err := resolveRenewAmount(monthSnapshot, monthParam, 2) if err != nil { t.Fatalf("resolveRenewAmount(month) failed: %v", err) } if monthAmount != 20 { t.Fatalf("resolveRenewAmount(month) = %.2f, want 20.00", monthAmount) } } func TestApplyExecutionOptionsKeepsDisplayDurationValue(t *testing.T) { t.Parallel() in := &model.TSimCardRenewBatchSubmitParam{} applyExecutionOptions(in, &executionOptions{ DurationYears: 2, DurationMonths: 2, PaymentMode: renewPaymentModeBalance, }) if in.DurationYears != 2 || in.DurationMonths != 2 { t.Fatalf("duration should keep display value: %#v", in) } if got := renewDisplayDurationValue(in); got != 2 { t.Fatalf("renewDisplayDurationValue = %d, want 2", got) } } func TestRenewDeptAncestorsContain(t *testing.T) { t.Parallel() tests := []struct { name string ancestors string deptID int64 want bool }{ {name: "contains direct ancestor", ancestors: "0,100,200", deptID: 100, want: true}, {name: "contains with spaces", ancestors: "0, 100 ,200", deptID: 100, want: true}, {name: "does not match partial id", ancestors: "0,1001,200", deptID: 100, want: false}, {name: "empty ancestors", ancestors: "", deptID: 100, want: false}, {name: "invalid dept", ancestors: "0,100,200", deptID: 0, want: false}, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() if got := renewDeptAncestorsContain(test.ancestors, test.deptID); got != test.want { t.Fatalf("renewDeptAncestorsContain(%q, %d) = %v, want %v", test.ancestors, test.deptID, got, test.want) } }) } } func TestParseExcelFileRowsAndOptions(t *testing.T) { t.Parallel() filePath := filepath.Join(t.TempDir(), "renew.xlsx") f := excelize.NewFile() headers := ExcelTemplateHeaders() if err := f.SetSheetRow("Sheet1", "A1", &headers); err != nil { t.Fatalf("set headers failed: %v", err) } row := []any{ "8986000000000000001", "13800008000", "460001234567890", "1", "是", } if err := f.SetSheetRow("Sheet1", "A2", &row); err != nil { t.Fatalf("set row failed: %v", err) } if err := f.SaveAs(filePath); err != nil { t.Fatalf("save excel failed: %v", err) } _ = f.Close() rows, options, err := ParseExcelFileRows(context.Background(), filePath) if err != nil { t.Fatalf("parse excel failed: %v", err) } if len(rows) != 1 { t.Fatalf("unexpected row count: %d", len(rows)) } if rows[0].Reason != "" { t.Fatalf("unexpected row reason: %s", rows[0].Reason) } if rows[0].Msisdn != "13800008000" { t.Fatalf("unexpected msisdn: %s", rows[0].Msisdn) } if rows[0].Imsi != "460001234567890" { t.Fatalf("unexpected imsi: %s", rows[0].Imsi) } if options == nil { t.Fatal("options should not be nil") } if options.DurationYears != 1 { t.Fatalf("unexpected durationYears: %d", options.DurationYears) } if options.PaymentMode != "offline" { t.Fatalf("unexpected paymentMode: %s", options.PaymentMode) } submitParam := options.ToBatchSubmitParam() if submitParam.DurationYears != 1 || submitParam.DurationMonths != 12 { t.Fatalf("unexpected submit duration fields: %#v", submitParam) } if submitParam.PaymentMode != "offline" { t.Fatalf("unexpected submit paymentMode: %#v", submitParam) } if !submitParam.OfflineContractCompleted { t.Fatalf("excel submit should mark offline contract as confirmed: %#v", submitParam) } if submitParam.SourceBatchNo != "" || submitParam.BuyerNote != "" || submitParam.AdminNote != "" || submitParam.Remark != "" { t.Fatalf("excel submit param should not carry removed fields: %#v", submitParam) } confirmParam := options.ToBatchConfirmParam("RB20260706001") if confirmParam.BatchNo != "RB20260706001" { t.Fatalf("unexpected batchNo: %s", confirmParam.BatchNo) } if confirmParam.DurationYears != 1 || confirmParam.DurationMonths != 12 { t.Fatalf("unexpected confirm duration fields: %#v", confirmParam) } if confirmParam.PaymentMode != "offline" { t.Fatalf("unexpected confirm paymentMode: %#v", confirmParam) } } func TestParseExcelFileRowsRejectsMultipleSheetsWithData(t *testing.T) { t.Parallel() filePath := filepath.Join(t.TempDir(), "renew-multi.xlsx") f := excelize.NewFile() headers := ExcelTemplateHeaders() if err := f.SetSheetRow("Sheet1", "A1", &headers); err != nil { t.Fatalf("set headers failed: %v", err) } row := []any{ "8986000000000000001", "13800008000", "460001234567890", "1", "是", } if err := f.SetSheetRow("Sheet1", "A2", &row); err != nil { t.Fatalf("set row failed: %v", err) } _, err := f.NewSheet("Sheet2") if err != nil { t.Fatalf("new sheet failed: %v", err) } if err := f.SetSheetRow("Sheet2", "A1", &headers); err != nil { t.Fatalf("set sheet2 headers failed: %v", err) } if err := f.SetSheetRow("Sheet2", "A2", &row); err != nil { t.Fatalf("set sheet2 row failed: %v", err) } if err := f.SaveAs(filePath); err != nil { t.Fatalf("save excel failed: %v", err) } _ = f.Close() if _, _, err := ParseExcelFileRows(context.Background(), filePath); err == nil { t.Fatal("expected multiple-sheet excel to be rejected") } } func TestPrepareExcelProcessorUsesPrepareBatchType(t *testing.T) { t.Parallel() processor := NewPrepareExcelProcessor() cfg := processor.Config() if cfg.BatchType != consts.BatchTypePrepare { t.Fatalf("prepare excel batch type = %d, want %d", cfg.BatchType, consts.BatchTypePrepare) } if cfg.BatchNoPrefix != consts.BatchNoPrefixRenew { t.Fatalf("prepare excel batch prefix = %q, want %q", cfg.BatchNoPrefix, consts.BatchNoPrefixRenew) } } func TestParseRenewInputLineSupportsSingleAndTripleFields(t *testing.T) { t.Parallel() tests := []struct { name string line text.FieldLine wantCode string wantIccid string wantMsisdn string wantImsi string wantReason string }{ { name: "single value", line: text.FieldLine{RowNo: 2, Fields: []string{"89860401786597927028"}}, wantCode: "89860401786597927028", wantIccid: "89860401786597927028", }, { name: "triple values", line: text.FieldLine{RowNo: 3, Fields: []string{"89860401786597927028", "14100007028", "460046597927028"}}, wantCode: "89860401786597927028", wantIccid: "89860401786597927028", wantMsisdn: "14100007028", wantImsi: "460046597927028", }, { name: "invalid format", line: text.FieldLine{RowNo: 4, Fields: []string{"a", "b"}}, wantReason: "格式错误", }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { t.Parallel() row := parseRenewInputLine(test.line) if row.InputCode != test.wantCode { t.Fatalf("input code = %q, want %q", row.InputCode, test.wantCode) } if row.Iccid != test.wantIccid { t.Fatalf("iccid = %q, want %q", row.Iccid, test.wantIccid) } if row.Msisdn != test.wantMsisdn { t.Fatalf("msisdn = %q, want %q", row.Msisdn, test.wantMsisdn) } if row.Imsi != test.wantImsi { t.Fatalf("imsi = %q, want %q", row.Imsi, test.wantImsi) } if test.wantReason != "" && row.Reason == "" { t.Fatal("expected row reason") } }) } } func TestRenewIdentityResolverResolvesAnyCardIdentity(t *testing.T) { t.Parallel() resolver := newRenewIdentityResolverFromCards([]*entity.TSimCardInfo{ { Id: 1, Iccid: "89860401786597927028", Msisdn: "14100007028", Imsi: "460046597927028", }, { Id: 2, Iccid: "89860401786597927029", Msisdn: "14100007029", Imsi: "460046597927029", }, }) row := &Row{RowNo: 2, InputCode: "14100007028", Msisdn: "14100007028"} resolver.resolveRow(row) if row.Reason != "" { t.Fatalf("unexpected reason: %s", row.Reason) } if row.Iccid != "89860401786597927028" || row.Msisdn != "14100007028" || row.Imsi != "460046597927028" { t.Fatalf("unexpected resolved row: %#v", row) } row = &Row{RowNo: 3, InputCode: "460046597927029", Imsi: "460046597927029"} resolver.resolveRow(row) if row.Reason != "" { t.Fatalf("unexpected reason for imsi row: %s", row.Reason) } if row.Iccid != "89860401786597927029" || row.Msisdn != "14100007029" || row.Imsi != "460046597927029" { t.Fatalf("unexpected imsi resolved row: %#v", row) } } func TestRenewIdentityResolverRejectsAmbiguousOrMismatchedIdentity(t *testing.T) { t.Parallel() resolver := newRenewIdentityResolverFromCards([]*entity.TSimCardInfo{ {Id: 1, Iccid: "89860401786597927028", Msisdn: "14100007028", Imsi: "460046597927028"}, {Id: 2, Iccid: "89860401786597927029", Msisdn: "89860401786597927028", Imsi: "460046597927029"}, }) ambiguous := &Row{RowNo: 2, InputCode: "89860401786597927028", Iccid: "89860401786597927028"} resolver.resolveRow(ambiguous) if ambiguous.Reason == "" { t.Fatal("expected ambiguous reason") } mismatched := &Row{RowNo: 3, InputCode: "89860401786597927028", Iccid: "89860401786597927028", Msisdn: "00000000000"} resolver.resolveRow(mismatched) if mismatched.Reason == "" { t.Fatal("expected mismatch reason") } }