2bec257289
iotlcm-worker-svr:异步执行中枢(RabbitMQ 消费者 + 全量同步编排)。 - Go module:iotlcm-worker-svr → git.5iot.com/iotlcm-public/iotlcm-worker-svr(214 文件) - 凭据与内部标识脱敏:MySQL/Redis 凭据、私网与生产 IP、内网域名、 库名 xiujie_admin → iotlcm、哨兵名占位 - 159 处真实手机号 → 合法 11 位编造号(保留号段与后 4 位,维持互异, 不破坏「校验 MSISDN 格式」等测试断言) - 移除内部资产:CI 触发器、Jenkins 触发测试文档、预演门禁与镜像 digest 记录 - Dockerfile 改用公共基础镜像(golang:1.25-alpine → alpine:3.23), 移除写死的 git 凭据 - README 重写(架构/全量同步模型/配置/已知失败),补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes 已知:3 个测试包在源仓同一提交上同样失败(client/upstream、 service/allocate、service/inbound),属既有缺陷,本仓未引入亦未修复。
414 lines
15 KiB
Go
414 lines
15 KiB
Go
package renewchange
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gogf/gf/v2/encoding/gjson"
|
|
"github.com/gogf/gf/v2/util/gconv"
|
|
|
|
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
|
|
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/do"
|
|
rc "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/repository/renewchange"
|
|
)
|
|
|
|
func TestParseExecutionContractReadsConfirmedFlatContract(t *testing.T) {
|
|
main := &rc.BatchMainRow{
|
|
BatchNo: "REN20260803100250ABCDEFGH",
|
|
BatchType: consts.BatchTypeRenew,
|
|
DeptId: 100,
|
|
CreatedDept: 100,
|
|
CreatedBy: 1,
|
|
TenantId: "000000",
|
|
BatchInfo: gjson.New(map[string]any{
|
|
"customerAgentId": 200,
|
|
"packageCode": "PKG001",
|
|
"packageName": "Year package",
|
|
"renewUnitPrice": 10.25,
|
|
"quantity": 2,
|
|
"durationMonths": 12,
|
|
"giftDurationMonths": 1,
|
|
"offlineContractCompleted": true,
|
|
"buyerNote": "buyer",
|
|
"adminNote": "admin",
|
|
}),
|
|
}
|
|
contract, err := ParseExecutionContract(main)
|
|
if err != nil {
|
|
t.Fatalf("ParseExecutionContract returned error: %v", err)
|
|
}
|
|
if contract.DurationDisplayValue != 1 || contract.DurationMonths != 12 || contract.GiftDurationMonths != 1 {
|
|
t.Fatalf("duration = %d/%d+%d, want display 1 and actual 12+1", contract.DurationDisplayValue, contract.DurationMonths, contract.GiftDurationMonths)
|
|
}
|
|
if contract.CustomerAgentID != 200 || contract.SellerAgentID != 100 {
|
|
t.Fatalf("buyer/seller = %d/%d, want 200/100", contract.CustomerAgentID, contract.SellerAgentID)
|
|
}
|
|
if contract.TotalAmount() != 20.5 {
|
|
t.Fatalf("TotalAmount = %.2f, want 20.50", contract.TotalAmount())
|
|
}
|
|
if contract.ItemPrice() != 10.25 {
|
|
t.Fatalf("ItemPrice = %.2f, want 10.25", contract.ItemPrice())
|
|
}
|
|
}
|
|
|
|
func TestParseExecutionContractRenewDurationValueOverridesLegacyMonthSnapshot(t *testing.T) {
|
|
main := &rc.BatchMainRow{
|
|
BatchNo: "REN20260803100250LEGACY",
|
|
BatchType: consts.BatchTypeRenew,
|
|
DeptId: 100,
|
|
CreatedDept: 100,
|
|
CreatedBy: 1,
|
|
TenantId: "000000",
|
|
BatchInfo: gjson.New(map[string]any{
|
|
"customerAgentId": 200,
|
|
"packageCode": "PKG001",
|
|
"packageName": "Year package",
|
|
"renewUnitPrice": 10.25,
|
|
"quantity": 1,
|
|
"durationValue": 1,
|
|
"durationYears": 1,
|
|
"durationMonths": 1,
|
|
"actualDurationMonths": 1,
|
|
"billingMethod": consts.BillingMethodMonth,
|
|
"renewDurationUnit": consts.RenewDurationUnitMonth,
|
|
"offlineContractCompleted": true,
|
|
}),
|
|
}
|
|
|
|
contract, err := ParseExecutionContract(main)
|
|
if err != nil {
|
|
t.Fatalf("ParseExecutionContract returned error: %v", err)
|
|
}
|
|
if contract.RenewDurationUnit != consts.RenewDurationUnitYear {
|
|
t.Fatalf("renew duration unit = %q, want year", contract.RenewDurationUnit)
|
|
}
|
|
if contract.DurationDisplayValue != 1 || contract.DurationMonths != 12 {
|
|
t.Fatalf("duration = %d/%d, want display 1 and actual 12 months", contract.DurationDisplayValue, contract.DurationMonths)
|
|
}
|
|
}
|
|
|
|
func TestParseExecutionContractFallsBackToLegacyNestedSnapshot(t *testing.T) {
|
|
main := &rc.BatchMainRow{
|
|
BatchNo: "CHG20260803100250ABCDEFGH",
|
|
BatchType: consts.BatchTypeChange,
|
|
DeptId: 100,
|
|
CreatedDept: 100,
|
|
CreatedBy: 1,
|
|
TenantId: "000000",
|
|
BatchInfo: gjson.New(map[string]any{
|
|
"changePriceSnapshot": map[string]any{
|
|
"customerAgentId": 200,
|
|
"newPackageCode": "PKG002",
|
|
"newPackageName": "Changed package",
|
|
"changeUnitPrice": 8.5,
|
|
},
|
|
"quantity": 1,
|
|
"durationMonths": 24,
|
|
"giftDurationMonths": 0,
|
|
"offlineContractCompleted": true,
|
|
}),
|
|
}
|
|
contract, err := ParseExecutionContract(main)
|
|
if err != nil {
|
|
t.Fatalf("ParseExecutionContract returned error: %v", err)
|
|
}
|
|
if contract.PackageCode != "PKG002" || contract.UnitPrice != 8.5 {
|
|
t.Fatalf("legacy snapshot contract = %s/%.2f, want PKG002/8.50", contract.PackageCode, contract.UnitPrice)
|
|
}
|
|
}
|
|
|
|
func TestParseExecutionContractNormalizesSourceBatchBySourceType(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
sourceType int
|
|
mainSourceBatchNo string
|
|
infoSourceBatchNo string
|
|
want string
|
|
}{
|
|
{
|
|
name: "input self reference is not a business source batch",
|
|
sourceType: consts.BatchSourceTypeBatchInput,
|
|
mainSourceBatchNo: "REN20260803161502D52JJU6B",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "file source ignores legacy source value",
|
|
sourceType: consts.BatchSourceTypeFile,
|
|
mainSourceBatchNo: "REN20260803161502D52JJU6B",
|
|
infoSourceBatchNo: "OUT20260803STALE",
|
|
want: "",
|
|
},
|
|
{
|
|
name: "batch selection keeps confirmed source batch",
|
|
sourceType: consts.BatchSourceTypeBatchSelect,
|
|
mainSourceBatchNo: "OUT20260803MAIN",
|
|
infoSourceBatchNo: "OUT20260803CONFIRMED",
|
|
want: "OUT20260803CONFIRMED",
|
|
},
|
|
{
|
|
name: "batch selection falls back to main source batch",
|
|
sourceType: consts.BatchSourceTypeBatchSelect,
|
|
mainSourceBatchNo: "OUT20260803MAIN",
|
|
want: "OUT20260803MAIN",
|
|
},
|
|
{
|
|
name: "batch selection repairs self reference from main source batch",
|
|
sourceType: consts.BatchSourceTypeBatchSelect,
|
|
mainSourceBatchNo: "OUT20260803MAIN",
|
|
infoSourceBatchNo: "REN20260803161502D52JJU6B",
|
|
want: "OUT20260803MAIN",
|
|
},
|
|
{
|
|
name: "legacy self reference is normalized to empty",
|
|
sourceType: consts.BatchSourceTypeBatchSelect,
|
|
mainSourceBatchNo: "REN20260803161502D52JJU6B",
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
info := map[string]any{
|
|
"customerAgentId": 200,
|
|
"packageCode": "PKG001",
|
|
"packageName": "Year package",
|
|
"renewUnitPrice": 10.25,
|
|
"quantity": 1,
|
|
"durationMonths": 12,
|
|
"offlineContractCompleted": true,
|
|
}
|
|
if tt.infoSourceBatchNo != "" {
|
|
info["sourceBatchNo"] = tt.infoSourceBatchNo
|
|
}
|
|
main := &rc.BatchMainRow{
|
|
BatchNo: "REN20260803161502D52JJU6B",
|
|
BatchType: consts.BatchTypeRenew,
|
|
SourceType: tt.sourceType,
|
|
SourceBatchNo: tt.mainSourceBatchNo,
|
|
DeptId: 100,
|
|
CreatedDept: 100,
|
|
CreatedBy: 1,
|
|
TenantId: "000000",
|
|
BatchInfo: gjson.New(info),
|
|
}
|
|
|
|
contract, err := ParseExecutionContract(main)
|
|
if err != nil {
|
|
t.Fatalf("ParseExecutionContract returned error: %v", err)
|
|
}
|
|
if contract.SourceBatchNo != tt.want {
|
|
t.Fatalf("SourceBatchNo = %q, want %q", contract.SourceBatchNo, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseExecutionContractRejectsMissingConfirmedFields(t *testing.T) {
|
|
base := validExecutionContract()
|
|
cases := []struct {
|
|
name string
|
|
mutate func(*ExecutionContract)
|
|
}{
|
|
{name: "customer", mutate: func(c *ExecutionContract) { c.CustomerAgentID = 0 }},
|
|
{name: "duration", mutate: func(c *ExecutionContract) { c.DurationMonths = 0 }},
|
|
{name: "quantity", mutate: func(c *ExecutionContract) { c.Quantity = 0 }},
|
|
{name: "package", mutate: func(c *ExecutionContract) { c.PackageCode = "" }},
|
|
{name: "offline contract", mutate: func(c *ExecutionContract) { c.OfflineContractCompleted = false }},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := *base
|
|
tc.mutate(&got)
|
|
if err := got.Validate(); err == nil {
|
|
t.Fatalf("Validate accepted missing %s", tc.name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildOrderDocumentsUseAggregatePurchasePeriodPrice(t *testing.T) {
|
|
contract := validExecutionContract()
|
|
contract.UnitPrice = 10.25
|
|
contract.Quantity = 2
|
|
contract.DurationMonths = 12
|
|
contract.GiftDurationMonths = 1
|
|
now := time.Date(2026, 8, 3, 10, 2, 50, 0, time.Local)
|
|
orderSN := "SO20260803100250ABCDEFGH"
|
|
|
|
order, err := BuildOrderDO(contract, orderSN, now)
|
|
if err != nil {
|
|
t.Fatalf("BuildOrderDO returned error: %v", err)
|
|
}
|
|
if gconv.String(order.OrderSn) != orderSN {
|
|
t.Fatalf("order_sn = %q, want %q", order.OrderSn, orderSN)
|
|
}
|
|
if gconv.Int64(order.CustomerAgentId) != 200 || gconv.Int64(order.AgentId) != 100 {
|
|
t.Fatalf("order buyer/seller = %v/%v, want 200/100", order.CustomerAgentId, order.AgentId)
|
|
}
|
|
if gconv.Int(order.OrderType) != consts.OrderTypeCardRenew {
|
|
t.Fatalf("order_type = %v, want renew", order.OrderType)
|
|
}
|
|
if gconv.Float64(order.TotalAmount) != 246 || gconv.Float64(order.ProductAmount) != 246 || gconv.Float64(order.PaidAmount) != 246 {
|
|
t.Fatalf("order amount fields = %v/%v/%v, want 246", order.TotalAmount, order.ProductAmount, order.PaidAmount)
|
|
}
|
|
if order.CreatedAt == nil || !order.CreatedAt.Time.Equal(now) || order.UpdatedAt == nil || !order.UpdatedAt.Time.Equal(now) {
|
|
t.Fatalf("order audit times = %v/%v, want %v", order.CreatedAt, order.UpdatedAt, now)
|
|
}
|
|
|
|
beginAt := time.Date(2026, 8, 15, 0, 0, 0, 0, time.Local)
|
|
endAt := contract.ServiceEnd(beginAt)
|
|
item, err := BuildOrderItemDO(contract, OrderProductContract{
|
|
ID: 300, Code: "PKG001", Name: "Year package", Type: 1,
|
|
}, 900, orderSN, beginAt, endAt, now)
|
|
if err != nil {
|
|
t.Fatalf("BuildOrderItemDO returned error: %v", err)
|
|
}
|
|
if gconv.Float64(item.Price) != 123 {
|
|
t.Fatalf("item price = %v, want totalAmount/quantity = 123", item.Price)
|
|
}
|
|
if gconv.Int(item.Quantity) != 2 {
|
|
t.Fatalf("item quantity = %v, want 2", item.Quantity)
|
|
}
|
|
if item.CreatedAt == nil || !item.CreatedAt.Time.Equal(now) || item.UpdatedAt == nil || !item.UpdatedAt.Time.Equal(now) {
|
|
t.Fatalf("item audit times = %v/%v, want %v", item.CreatedAt, item.UpdatedAt, now)
|
|
}
|
|
if got := item.ServiceEndTime.Time; !got.Equal(time.Date(2027, 9, 15, 0, 0, 0, 0, time.Local)) {
|
|
t.Fatalf("service end = %s, want 2027-09-15", got)
|
|
}
|
|
if item.CreatedAt == nil || item.UpdatedAt == nil || !item.CreatedAt.Time.Equal(now) || !item.UpdatedAt.Time.Equal(now) {
|
|
t.Fatalf("order item audit fields = %v/%v, want %v", item.CreatedAt, item.UpdatedAt, now)
|
|
}
|
|
|
|
var extra map[string]any
|
|
if err = json.Unmarshal([]byte(gconv.String(item.ExtraSkuData)), &extra); err != nil {
|
|
t.Fatalf("extra_sku_data is invalid JSON: %v", err)
|
|
}
|
|
if gconv.String(extra["outboundBatchNo"]) != contract.BatchNo || gconv.Int(extra["durationMonths"]) != 12 || gconv.Int(extra["giftDurationMonths"]) != 1 {
|
|
t.Fatalf("extra duration = %v+%v, want 12+1", extra["durationMonths"], extra["giftDurationMonths"])
|
|
}
|
|
if gconv.Float64(extra["amount"]) != 246 {
|
|
t.Fatalf("extra amount = %v, want 246", extra["amount"])
|
|
}
|
|
}
|
|
|
|
func TestBuildOrderUsesChangeOrderType(t *testing.T) {
|
|
contract := validExecutionContract()
|
|
contract.BatchType = consts.BatchTypeChange
|
|
|
|
order, err := BuildOrderDO(contract, "SO20260803100250CHANGE01", time.Date(2026, 8, 3, 10, 2, 50, 0, time.Local))
|
|
if err != nil {
|
|
t.Fatalf("BuildOrderDO returned error: %v", err)
|
|
}
|
|
if gconv.Int(order.OrderType) != consts.OrderTypeCardChange {
|
|
t.Fatalf("order_type = %v, want change", order.OrderType)
|
|
}
|
|
}
|
|
|
|
func TestBuildOrderUsesBalancePaymentSnapshot(t *testing.T) {
|
|
contract := validExecutionContract()
|
|
contract.OfflineContractCompleted = false
|
|
contract.PaymentMode = "balance"
|
|
contract.PayType = consts.PayTypeBalance
|
|
contract.PayAmount = 120
|
|
contract.BalanceAmount = 120
|
|
contract.BalanceDetailSerialNo = "RENPAYREN20260803100250ABCDEFGH"
|
|
contract.UnitPrice = 10
|
|
contract.Quantity = 1
|
|
contract.DurationMonths = 12
|
|
|
|
order, err := BuildOrderDO(contract, "SO20260803100250ABCDEFGH", time.Date(2026, 8, 3, 10, 2, 50, 0, time.Local))
|
|
if err != nil {
|
|
t.Fatalf("BuildOrderDO returned error: %v", err)
|
|
}
|
|
if gconv.Int(order.PayType) != consts.PayTypeBalance {
|
|
t.Fatalf("pay_type = %v, want balance", order.PayType)
|
|
}
|
|
if gconv.String(order.PayOrderSn) != contract.BalanceDetailSerialNo {
|
|
t.Fatalf("pay_order_sn = %q, want %q", order.PayOrderSn, contract.BalanceDetailSerialNo)
|
|
}
|
|
if gconv.Float64(order.Balance) != 120 || gconv.Float64(order.OfflinePaidAmount) != 0 {
|
|
t.Fatalf("balance/offline = %v/%v, want 120/0", order.Balance, order.OfflinePaidAmount)
|
|
}
|
|
}
|
|
|
|
func TestBalancePaymentSnapshotMustMatchTotalAmount(t *testing.T) {
|
|
contract := validExecutionContract()
|
|
contract.OfflineContractCompleted = false
|
|
contract.PaymentMode = "balance"
|
|
contract.PayType = consts.PayTypeBalance
|
|
contract.PayAmount = 1
|
|
contract.BalanceAmount = 1
|
|
contract.BalanceDetailSerialNo = "RENPAYREN20260803100250ABCDEFGH"
|
|
|
|
if err := contract.Validate(); err == nil {
|
|
t.Fatal("Validate accepted balance amount that does not match total amount")
|
|
}
|
|
}
|
|
|
|
func TestBuildOrderRejectsEmptyOrderNumber(t *testing.T) {
|
|
if _, err := BuildOrderDO(validExecutionContract(), "", time.Now()); err == nil {
|
|
t.Fatal("BuildOrderDO accepted an empty order number")
|
|
}
|
|
}
|
|
|
|
func TestBuildRenewCardUpdateOnlySetsRenewFields(t *testing.T) {
|
|
contract := validExecutionContract()
|
|
endAt := time.Date(2027, 8, 3, 0, 0, 0, 0, time.Local)
|
|
now := time.Date(2026, 8, 3, 10, 2, 50, 0, time.Local)
|
|
update, err := BuildRenewCardUpdate(contract, endAt, consts.PackageStatusEffective, now)
|
|
if err != nil {
|
|
t.Fatalf("BuildRenewCardUpdate returned error: %v", err)
|
|
}
|
|
if update.FlowPkgEndAt == nil || !update.FlowPkgEndAt.Time.Equal(endAt) {
|
|
t.Fatalf("flow_pkg_end_at = %v, want %v", update.FlowPkgEndAt, endAt)
|
|
}
|
|
if gconv.Int(update.PackageStatus) != consts.PackageStatusEffective || gconv.Int64(update.UpdatedBy) != contract.OperatorUserID {
|
|
t.Fatalf("package_status/updated_by = %v/%v", update.PackageStatus, update.UpdatedBy)
|
|
}
|
|
if update.UpdatedAt == nil || !update.UpdatedAt.Time.Equal(now) {
|
|
t.Fatalf("updated_at = %v, want %v", update.UpdatedAt, now)
|
|
}
|
|
assertOnlyRenewCardFieldsSet(t, update)
|
|
}
|
|
|
|
func validExecutionContract() *ExecutionContract {
|
|
return &ExecutionContract{
|
|
BatchNo: "REN20260803100250ABCDEFGH",
|
|
BatchType: consts.BatchTypeRenew,
|
|
TenantID: "000000",
|
|
CustomerAgentID: 200,
|
|
SellerAgentID: 100,
|
|
CreatedDeptID: 100,
|
|
OperatorUserID: 1,
|
|
PackageCode: "PKG001",
|
|
PackageName: "Year package",
|
|
UnitPrice: 10,
|
|
Quantity: 1,
|
|
DurationMonths: 12,
|
|
GiftDurationMonths: 0,
|
|
OfflineContractCompleted: true,
|
|
}
|
|
}
|
|
|
|
func assertOnlyRenewCardFieldsSet(t *testing.T, update *do.TSimCardInfo) {
|
|
t.Helper()
|
|
allowed := map[string]bool{"FlowPkgEndAt": true, "PackageStatus": true, "UpdatedBy": true, "UpdatedAt": true}
|
|
value := reflect.ValueOf(update).Elem()
|
|
typ := value.Type()
|
|
for i := 0; i < value.NumField(); i++ {
|
|
fieldType := typ.Field(i)
|
|
if fieldType.Name == "Meta" || allowed[fieldType.Name] {
|
|
continue
|
|
}
|
|
field := value.Field(i)
|
|
switch field.Kind() {
|
|
case reflect.Interface, reflect.Pointer, reflect.Map, reflect.Slice:
|
|
if !field.IsNil() {
|
|
t.Fatalf("unexpected renew card field set: %s", fieldType.Name)
|
|
}
|
|
}
|
|
}
|
|
}
|