Files
iotlcm/internal/repository/unsub/repository_test.go
T
MiFaZhan 2bec257289 chore: 初始开源发布
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),属既有缺陷,本仓未引入亦未修复。
2026-10-22 16:37:56 +08:00

791 lines
29 KiB
Go

package unsub
import (
"context"
"math"
"testing"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/os/gtime"
batchdomain "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/batch"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/dao"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/model/do"
unsub "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/unsub"
)
const (
testTenantID = "000000"
testDeptID = int64(100)
testPackage = "PKG-A"
testSource = "OUT-1"
testInbound = "IN-1"
)
func newTestRepository(t *testing.T) *Repository {
t.Helper()
ctx := context.Background()
db, err := gdb.New(gdb.ConfigNode{
Type: "sqlite",
Link: "sqlite::@file(:memory:)",
})
if err != nil {
t.Fatalf("create sqlite database: %v", err)
}
statements := []string{
`CREATE TABLE batch_main (
source_channel TEXT NOT NULL DEFAULT 'legacy',
execution_mode INTEGER NOT NULL DEFAULT 0,
id INTEGER PRIMARY KEY AUTOINCREMENT,
batch_no TEXT NOT NULL,
batch_type INTEGER NOT NULL,
batch_async_status INTEGER NOT NULL,
run_result INTEGER NOT NULL DEFAULT 0,
fail_reason TEXT,
start_time DATETIME,
end_time DATETIME,
dept_id INTEGER NOT NULL DEFAULT 0,
source_batch_no TEXT,
created_by INTEGER NOT NULL DEFAULT 0,
created_dept INTEGER NOT NULL DEFAULT 0,
tenant_id TEXT NOT NULL DEFAULT '',
batch_info TEXT
)`,
`CREATE TABLE batch_detail (
id INTEGER PRIMARY KEY AUTOINCREMENT,
batch_no TEXT NOT NULL,
batch_type INTEGER NOT NULL,
detail_status INTEGER NOT NULL,
error_reason TEXT,
snapshot TEXT,
item_id TEXT,
row_no INTEGER NOT NULL DEFAULT 0
)`,
`CREATE TABLE t_sim_card_info (
id INTEGER PRIMARY KEY AUTOINCREMENT,
iccid TEXT NOT NULL,
msisdn TEXT,
imsi TEXT,
imei TEXT,
chan_key TEXT,
dept_id INTEGER NOT NULL DEFAULT 0,
created_dept INTEGER NOT NULL DEFAULT 0,
current_package_code TEXT,
package_status INTEGER NOT NULL DEFAULT 0,
inbound_batch_no TEXT,
outbound_batch_no TEXT,
tenant_id TEXT NOT NULL DEFAULT ''
)`,
`CREATE TABLE t_sim_card_bind_pkg (
id INTEGER PRIMARY KEY AUTOINCREMENT,
subscription_no TEXT,
batch_no TEXT,
iccid TEXT,
msisdn TEXT,
dept_id TEXT,
package_type INTEGER NOT NULL DEFAULT 0,
package_code TEXT,
previous_package_code TEXT,
business_type INTEGER NOT NULL DEFAULT 0,
effective_status INTEGER NOT NULL DEFAULT 0,
handle_time DATETIME,
effective_time DATETIME,
expected_expire_time DATETIME,
actual_expire_time DATETIME,
duration_months INTEGER NOT NULL DEFAULT 0,
gift_duration_months INTEGER NOT NULL DEFAULT 0,
order_code TEXT,
remark TEXT,
tenant_id TEXT NOT NULL DEFAULT '',
created_dept INTEGER NOT NULL DEFAULT 0,
created_by INTEGER NOT NULL DEFAULT 0,
created_at DATETIME
)`,
`CREATE TABLE t_sim_bind_package_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
subscription_no TEXT,
batch_no TEXT,
iccid TEXT,
msisdn TEXT,
dept_id TEXT,
package_type INTEGER,
package_code TEXT,
previous_package_code TEXT,
business_type INTEGER,
effective_status INTEGER,
handle_time DATETIME,
effective_time DATETIME,
expected_expire_time DATETIME,
actual_expire_time DATETIME,
duration_months INTEGER,
gift_duration_months INTEGER,
order_code TEXT,
remark TEXT,
tenant_id TEXT,
created_dept INTEGER,
created_by INTEGER,
updated_by INTEGER
)`,
`CREATE TABLE t_sim_card_stock (
id INTEGER PRIMARY KEY AUTOINCREMENT,
batch_no TEXT,
iccid TEXT,
msisdn TEXT,
imsi TEXT,
imei TEXT,
chan_key TEXT,
tenant_id TEXT NOT NULL DEFAULT '',
created_dept INTEGER NOT NULL DEFAULT 0,
created_by INTEGER NOT NULL DEFAULT 0,
updated_by INTEGER NOT NULL DEFAULT 0
)`,
`CREATE TABLE t_order (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_sn TEXT,
customer_agent_id INTEGER NOT NULL DEFAULT 0,
pay_type INTEGER NOT NULL DEFAULT 0,
pay_status INTEGER NOT NULL DEFAULT 0,
pay_order_sn TEXT,
total_amount REAL NOT NULL DEFAULT 0,
paid_amount REAL NOT NULL DEFAULT 0,
balance REAL NOT NULL DEFAULT 0,
unrefund_amount REAL NOT NULL DEFAULT 0,
tenant_id TEXT NOT NULL DEFAULT '',
created_dept INTEGER NOT NULL DEFAULT 0,
return_status INTEGER NOT NULL DEFAULT 0,
updated_by INTEGER NOT NULL DEFAULT 0
)`,
`CREATE TABLE t_order_item (
item_id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER,
order_sn TEXT,
price REAL NOT NULL DEFAULT 0,
quantity INTEGER NOT NULL DEFAULT 0,
service_begin_time DATETIME,
service_end_time DATETIME,
tenant_id TEXT NOT NULL DEFAULT '',
extra_sku_data TEXT,
updated_by INTEGER NOT NULL DEFAULT 0
)`,
`CREATE TABLE t_agent_balance (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dept_id INTEGER NOT NULL DEFAULT 0,
balance REAL NOT NULL DEFAULT 0,
status INTEGER NOT NULL DEFAULT 0,
tenant_id TEXT NOT NULL DEFAULT ''
)`,
`CREATE TABLE t_balance_detail (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dept_id INTEGER NOT NULL DEFAULT 0,
serial_no TEXT NOT NULL,
transaction_time DATETIME,
income_expense_type INTEGER NOT NULL DEFAULT 0,
transaction_type INTEGER NOT NULL DEFAULT 0,
transaction_channel INTEGER NOT NULL DEFAULT 0,
transaction_status INTEGER NOT NULL DEFAULT 0,
amount REAL NOT NULL DEFAULT 0,
before_balance REAL NOT NULL DEFAULT 0,
balance_after REAL NOT NULL DEFAULT 0,
order_sn TEXT,
remark TEXT,
tenant_id TEXT NOT NULL DEFAULT '',
created_dept INTEGER NOT NULL DEFAULT 0,
created_by INTEGER NOT NULL DEFAULT 0,
created_at DATETIME
)`,
}
for _, statement := range statements {
if _, err = db.Exec(ctx, statement); err != nil {
t.Fatalf("create test schema: %v", err)
}
}
t.Cleanup(func() { _ = db.Close(ctx) })
return &Repository{db: db, lockEnabled: false}
}
func seedProcessingBatch(t *testing.T, ctx context.Context, db gdb.DB, batchNo, sourceBatchNo string) {
t.Helper()
info := gjson.New(map[string]any{
"sourceBatchNo": sourceBatchNo,
})
if _, err := db.Model(dao.BatchMain.Table()).Ctx(ctx).Data(do.BatchMain{
BatchNo: batchNo,
BatchType: consts.BatchTypeSimCardUnsub,
BatchAsyncStatus: consts.BatchAsyncStatusProcessing,
RunResult: batchdomain.RunResultRunning,
DeptId: testDeptID,
SourceBatchNo: sourceBatchNo,
CreatedBy: 1,
CreatedDept: 1,
TenantId: testTenantID,
BatchInfo: info,
}).Insert(); err != nil {
t.Fatalf("insert batch main: %v", err)
}
}
func seedRunningDetail(t *testing.T, ctx context.Context, db gdb.DB, batchNo string, rowNo int, iccid, msisdn, imsi, sourceBatchNo, reason string) int64 {
t.Helper()
snap := gjson.New(map[string]any{
"ICCID": iccid,
"MSISDN": msisdn,
"IMSI": imsi,
"authorized": true,
"sourceBatchNo": sourceBatchNo,
})
id, err := db.Model(dao.BatchDetail.Table()).Ctx(ctx).Data(do.BatchDetail{
BatchNo: batchNo,
BatchType: consts.BatchTypeSimCardUnsub,
DetailStatus: consts.BatchDetailStatusRunning,
RowNo: rowNo,
ItemId: iccid,
Snapshot: snap,
ErrorReason: reason,
}).InsertAndGetId()
if err != nil {
t.Fatalf("insert batch detail: %v", err)
}
return id
}
func seedCard(t *testing.T, ctx context.Context, db gdb.DB, iccid, msisdn, imsi, packageCode string, deptID int64) {
t.Helper()
_, err := db.Model(cardInfoTable).Ctx(ctx).Data(do.TSimCardInfo{
Iccid: iccid,
Msisdn: msisdn,
Imsi: imsi,
DeptId: deptID,
CreatedDept: deptID,
CurrentPackageCode: packageCode,
PackageStatus: pkgStatusEffective,
InboundBatchNo: testInbound,
OutboundBatchNo: testSource,
TenantId: testTenantID,
}).Insert()
if err != nil {
t.Fatalf("insert sim card: %v", err)
}
}
func seedActiveBind(t *testing.T, ctx context.Context, db gdb.DB, iccid, packageCode, orderCode, sourceBatchNo string) {
t.Helper()
_, err := db.Model(cardBindPkgTable).Ctx(ctx).Data(map[string]any{
"subscription_no": "SUB-" + iccid,
"batch_no": sourceBatchNo,
"iccid": iccid,
"msisdn": "13" + iccid[len(iccid)-8:],
"dept_id": "100",
"package_type": 1,
"package_code": packageCode,
"business_type": 0,
"effective_status": bindStatusEffective,
"order_code": orderCode,
"remark": "",
"tenant_id": testTenantID,
"created_dept": 1,
"created_by": 1,
}).Insert()
if err != nil {
t.Fatalf("insert bind: %v", err)
}
}
func seedStock(t *testing.T, ctx context.Context, db gdb.DB, iccid string) {
t.Helper()
_, err := db.Model(stockTable).Ctx(ctx).Data(map[string]any{
"iccid": iccid,
"tenant_id": testTenantID,
}).Insert()
if err != nil {
t.Fatalf("insert stock: %v", err)
}
}
func seedOrder(t *testing.T, ctx context.Context, db gdb.DB, orderCode string) {
t.Helper()
_, err := db.Model(orderTable).Ctx(ctx).Data(map[string]any{
"order_sn": orderCode,
"return_status": 0,
"tenant_id": testTenantID,
}).Insert()
if err != nil {
t.Fatalf("insert order: %v", err)
}
}
func seedOrderItem(t *testing.T, ctx context.Context, db gdb.DB, orderCode, extra string) {
t.Helper()
_, err := db.Model(orderItemTable).Ctx(ctx).Data(map[string]any{
"order_sn": orderCode,
"tenant_id": testTenantID,
"extra_sku_data": extra,
}).Insert()
if err != nil {
t.Fatalf("insert order item: %v", err)
}
}
func assertTableCount(t *testing.T, ctx context.Context, db gdb.DB, table string, expected int, where string, args ...any) {
t.Helper()
model := db.Model(table).Ctx(ctx).Where(where, args...)
count, err := model.Count()
if err != nil {
t.Fatalf("count %s: %v", table, err)
}
if count != expected {
t.Fatalf("%s count = %d, want %d", table, count, expected)
}
}
func assertBatchMainStatus(t *testing.T, ctx context.Context, db gdb.DB, batchNo string, wantAsync, wantRun int) {
t.Helper()
var row struct {
Async int `orm:"batch_async_status"`
Run int `orm:"run_result"`
}
if err := db.Model(dao.BatchMain.Table()).Ctx(ctx).
Where(dao.BatchMain.Columns().BatchNo, batchNo).
Fields("batch_async_status", "run_result").
Scan(&row); err != nil {
t.Fatalf("query batch main: %v", err)
}
if row.Async != wantAsync || row.Run != wantRun {
t.Fatalf("batch %s status = (async=%d, run=%d), want (async=%d, run=%d)", batchNo, row.Async, row.Run, wantAsync, wantRun)
}
}
func assertBatchDetailStatus(t *testing.T, ctx context.Context, db gdb.DB, batchNo string, want int) {
t.Helper()
var row struct {
Status int `orm:"detail_status"`
}
if err := db.Model(dao.BatchDetail.Table()).Ctx(ctx).
Where(dao.BatchDetail.Columns().BatchNo, batchNo).
Fields("detail_status").
Scan(&row); err != nil {
t.Fatalf("query batch detail: %v", err)
}
if row.Status != want {
t.Fatalf("batch %s detail status = %d, want %d", batchNo, row.Status, want)
}
}
func TestLoadReadsTaskAndDetails(t *testing.T) {
ctx := context.Background()
repo := newTestRepository(t)
batchNo := "UNS-1"
seedProcessingBatch(t, ctx, repo.db, batchNo, testSource)
seedRunningDetail(t, ctx, repo.db, batchNo, 1, "898600000000001", "13800000001", "460001000000001", testSource, "")
seedRunningDetail(t, ctx, repo.db, batchNo, 2, "898600000000002", "13800000002", "460001000000002", testSource, "")
task, err := repo.Load(ctx, batchNo)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if task.Batch.BatchNo != batchNo {
t.Fatalf("batch no = %q, want %q", task.Batch.BatchNo, batchNo)
}
if task.Batch.SourceBatchNo != testSource {
t.Fatalf("source batch = %q, want %q", task.Batch.SourceBatchNo, testSource)
}
if len(task.Details) != 2 {
t.Fatalf("details = %d, want 2", len(task.Details))
}
if task.Details[0].ICCID != "898600000000001" {
t.Fatalf("first detail ICCID = %q", task.Details[0].ICCID)
}
}
func TestLoadTreatsSelfBatchNoAsEmptySourceBatch(t *testing.T) {
ctx := context.Background()
repo := newTestRepository(t)
batchNo := "UNS-1"
seedProcessingBatch(t, ctx, repo.db, batchNo, batchNo)
seedRunningDetail(t, ctx, repo.db, batchNo, 1, "898600000000001", "13800000001", "460001000000001", "", "")
task, err := repo.Load(ctx, batchNo)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if task.Batch.SourceBatchNo != "" {
t.Fatalf("source batch = %q, want empty when equals batchNo", task.Batch.SourceBatchNo)
}
}
func TestSaveValidationFailureMarksAllDetailsAndMainFailed(t *testing.T) {
ctx := context.Background()
repo := newTestRepository(t)
batchNo := "UNS-1"
seedProcessingBatch(t, ctx, repo.db, batchNo, testSource)
seedRunningDetail(t, ctx, repo.db, batchNo, 1, "898600000000001", "13800000001", "460001000000001", testSource, "snapshot has reason")
seedRunningDetail(t, ctx, repo.db, batchNo, 2, "898600000000002", "13800000002", "460001000000002", testSource, "")
task, err := repo.Load(ctx, batchNo)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if err = repo.SaveValidationFailure(ctx, task); err != nil {
t.Fatalf("SaveValidationFailure() error = %v", err)
}
assertBatchMainStatus(t, ctx, repo.db, batchNo, consts.BatchAsyncStatusFailed, consts.BatchRunResultFailed)
assertBatchDetailStatus(t, ctx, repo.db, batchNo, consts.BatchDetailStatusFailed)
assertTableCount(t, ctx, repo.db, cardInfoTable, 0, "iccid IN (?, ?)", "898600000000001", "898600000000002")
assertTableCount(t, ctx, repo.db, cardBindPkgTable, 0, "iccid IN (?, ?)", "898600000000001", "898600000000002")
assertTableCount(t, ctx, repo.db, stockTable, 0, "1=1")
}
func TestCommitBusinessHappyPath(t *testing.T) {
ctx := context.Background()
repo := newTestRepository(t)
batchNo := "UNS-1"
seedProcessingBatch(t, ctx, repo.db, batchNo, testSource)
seedRunningDetail(t, ctx, repo.db, batchNo, 1, "898600000000001", "13800000001", "460001000000001", testSource, "")
seedRunningDetail(t, ctx, repo.db, batchNo, 2, "898600000000002", "13800000002", "460001000000002", testSource, "")
seedCard(t, ctx, repo.db, "898600000000001", "13800000001", "460001000000001", testPackage, testDeptID)
seedCard(t, ctx, repo.db, "898600000000002", "13800000002", "460001000000002", testPackage, testDeptID)
seedActiveBind(t, ctx, repo.db, "898600000000001", testPackage, "ORD-1", testSource)
seedActiveBind(t, ctx, repo.db, "898600000000002", testPackage, "ORD-1", testSource)
seedOrder(t, ctx, repo.db, "ORD-1")
seedOrderItem(t, ctx, repo.db, "ORD-1", `{}`)
task, err := repo.Load(ctx, batchNo)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if err = repo.CommitBusiness(ctx, task); err != nil {
t.Fatalf("CommitBusiness() error = %v", err)
}
assertTableCount(t, ctx, repo.db, cardInfoTable, 0, "iccid IN (?, ?)", "898600000000001", "898600000000002")
assertTableCount(t, ctx, repo.db, cardBindPkgTable, 0, "iccid IN (?, ?)", "898600000000001", "898600000000002")
assertTableCount(t, ctx, repo.db, bindPackageLogsTable, 2, "iccid IN (?, ?)", "898600000000001", "898600000000002")
assertTableCount(t, ctx, repo.db, stockTable, 2, "iccid IN (?, ?)", "898600000000001", "898600000000002")
assertBatchMainStatus(t, ctx, repo.db, batchNo, consts.BatchAsyncStatusSuccess, consts.BatchRunResultSuccess)
assertBatchDetailStatus(t, ctx, repo.db, batchNo, consts.BatchDetailStatusSuccess)
assertOrderReturnStatus(t, ctx, repo.db, "ORD-1", orderReturnStatusSuccess)
assertOrderItemExtra(t, ctx, repo.db, "ORD-1", batchNo, 2)
}
func TestCommitBusinessRefundsBalanceByPaidPeriodOnly(t *testing.T) {
ctx := context.Background()
repo := newTestRepository(t)
batchNo := "UNS-REFUND-1"
orderCode := "ORD-REFUND-1"
effectiveAt := gtime.New("2026-01-01 00:00:00")
cutoffAt := gtime.New("2026-07-01 00:00:00")
seedProcessingBatch(t, ctx, repo.db, batchNo, testSource)
if _, err := repo.db.Model(dao.BatchMain.Table()).Ctx(ctx).
Where(dao.BatchMain.Columns().BatchNo, batchNo).
Data(map[string]any{"start_time": cutoffAt}).Update(); err != nil {
t.Fatalf("update batch start time: %v", err)
}
seedRunningDetail(t, ctx, repo.db, batchNo, 1, "898600000000001", "13800000001", "460001000000001", testSource, "")
seedCard(t, ctx, repo.db, "898600000000001", "13800000001", "460001000000001", testPackage, testDeptID)
seedActiveBind(t, ctx, repo.db, "898600000000001", testPackage, orderCode, testSource)
if _, err := repo.db.Model(cardBindPkgTable).Ctx(ctx).
Where("order_code", orderCode).
Data(map[string]any{
"effective_time": effectiveAt,
"expected_expire_time": gtime.New("2027-04-01 23:59:59"),
"duration_months": 12,
"gift_duration_months": 3,
}).Update(); err != nil {
t.Fatalf("update bind period: %v", err)
}
if _, err := repo.db.Model(orderTable).Ctx(ctx).Data(map[string]any{
"order_sn": orderCode,
"customer_agent_id": testDeptID,
"pay_type": consts.PayTypeBalance,
"pay_status": consts.PayStatusPaid,
"pay_order_sn": "RENPAY-REFUND-1",
"total_amount": 120,
"paid_amount": 120,
"balance": 120,
"unrefund_amount": 120,
"tenant_id": testTenantID,
"created_dept": testDeptID,
}).Insert(); err != nil {
t.Fatalf("insert refund order: %v", err)
}
if _, err := repo.db.Model(orderItemTable).Ctx(ctx).Data(map[string]any{
"order_sn": orderCode,
"price": 120,
"quantity": 1,
"service_begin_time": effectiveAt,
"service_end_time": gtime.New("2027-04-01 23:59:59"),
"tenant_id": testTenantID,
}).Insert(); err != nil {
t.Fatalf("insert refund order item: %v", err)
}
if _, err := repo.db.Model(agentBalanceTable).Ctx(ctx).Data(map[string]any{
"dept_id": testDeptID,
"balance": 10,
"status": 0,
"tenant_id": testTenantID,
}).Insert(); err != nil {
t.Fatalf("insert refund account: %v", err)
}
task, err := repo.Load(ctx, batchNo)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if err = repo.CommitBusiness(ctx, task); err != nil {
t.Fatalf("CommitBusiness() error = %v", err)
}
var account struct {
Balance float64 `orm:"balance"`
}
if err = repo.db.Model(agentBalanceTable).Ctx(ctx).Where("dept_id", testDeptID).Scan(&account); err != nil {
t.Fatalf("query refund account: %v", err)
}
if want := 70.49; math.Abs(account.Balance-want) > 0.01 {
t.Fatalf("refund balance = %.2f, want %.2f", account.Balance, want)
}
assertTableCount(t, ctx, repo.db, balanceDetailTable, 1, "serial_no = ?", unsubscribeRefundSerialNo(batchNo, orderCode))
assertOrderReturnStatus(t, ctx, repo.db, orderCode, orderReturnStatusSuccess)
}
func TestCommitBusinessFailsOnNoActiveBind(t *testing.T) {
ctx := context.Background()
repo := newTestRepository(t)
batchNo := "UNS-1"
seedProcessingBatch(t, ctx, repo.db, batchNo, testSource)
seedRunningDetail(t, ctx, repo.db, batchNo, 1, "898600000000001", "13800000001", "460001000000001", testSource, "")
seedCard(t, ctx, repo.db, "898600000000001", "13800000001", "460001000000001", testPackage, testDeptID)
task, err := repo.Load(ctx, batchNo)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if err = repo.CommitBusiness(ctx, task); err != nil {
t.Fatalf("CommitBusiness() returned error = %v, want nil (validation failure handled internally)", err)
}
assertTableCount(t, ctx, repo.db, cardInfoTable, 1, "iccid = ?", "898600000000001")
assertTableCount(t, ctx, repo.db, cardBindPkgTable, 0, "iccid = ?", "898600000000001")
assertTableCount(t, ctx, repo.db, bindPackageLogsTable, 0, "iccid = ?", "898600000000001")
assertTableCount(t, ctx, repo.db, stockTable, 0, "iccid = ?", "898600000000001")
assertBatchMainStatus(t, ctx, repo.db, batchNo, consts.BatchAsyncStatusFailed, consts.BatchRunResultFailed)
assertBatchDetailStatus(t, ctx, repo.db, batchNo, consts.BatchDetailStatusFailed)
}
func TestCommitBusinessFailsOnPackageMismatch(t *testing.T) {
ctx := context.Background()
repo := newTestRepository(t)
batchNo := "UNS-1"
seedProcessingBatch(t, ctx, repo.db, batchNo, testSource)
seedRunningDetail(t, ctx, repo.db, batchNo, 1, "898600000000001", "13800000001", "460001000000001", testSource, "")
seedCard(t, ctx, repo.db, "898600000000001", "13800000001", "460001000000001", testPackage, testDeptID)
seedActiveBind(t, ctx, repo.db, "898600000000001", "PKG-OTHER", "ORD-1", testSource)
task, err := repo.Load(ctx, batchNo)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if err = repo.CommitBusiness(ctx, task); err != nil {
t.Fatalf("CommitBusiness() returned error = %v, want nil", err)
}
assertTableCount(t, ctx, repo.db, cardInfoTable, 1, "iccid = ?", "898600000000001")
assertTableCount(t, ctx, repo.db, cardBindPkgTable, 1, "iccid = ?", "898600000000001")
assertTableCount(t, ctx, repo.db, bindPackageLogsTable, 0, "iccid = ?", "898600000000001")
assertTableCount(t, ctx, repo.db, stockTable, 0, "iccid = ?", "898600000000001")
assertBatchMainStatus(t, ctx, repo.db, batchNo, consts.BatchAsyncStatusFailed, consts.BatchRunResultFailed)
}
func TestCommitBusinessFailsOnDeptMismatch(t *testing.T) {
ctx := context.Background()
repo := newTestRepository(t)
batchNo := "UNS-1"
seedProcessingBatch(t, ctx, repo.db, batchNo, testSource)
seedRunningDetail(t, ctx, repo.db, batchNo, 1, "898600000000001", "13800000001", "460001000000001", testSource, "")
seedCard(t, ctx, repo.db, "898600000000001", "13800000001", "460001000000001", testPackage, 999)
seedActiveBind(t, ctx, repo.db, "898600000000001", testPackage, "ORD-1", testSource)
task, err := repo.Load(ctx, batchNo)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if err = repo.CommitBusiness(ctx, task); err != nil {
t.Fatalf("CommitBusiness() returned error = %v, want nil", err)
}
assertTableCount(t, ctx, repo.db, cardInfoTable, 1, "iccid = ?", "898600000000001")
assertTableCount(t, ctx, repo.db, cardBindPkgTable, 1, "iccid = ?", "898600000000001")
assertTableCount(t, ctx, repo.db, bindPackageLogsTable, 0, "iccid = ?", "898600000000001")
}
func TestCommitBusinessFailsOnSourceBatchMismatch(t *testing.T) {
ctx := context.Background()
repo := newTestRepository(t)
batchNo := "UNS-1"
seedProcessingBatch(t, ctx, repo.db, batchNo, testSource)
seedRunningDetail(t, ctx, repo.db, batchNo, 1, "898600000000001", "13800000001", "460001000000001", testSource, "")
seedCard(t, ctx, repo.db, "898600000000001", "13800000001", "460001000000001", testPackage, testDeptID)
seedActiveBind(t, ctx, repo.db, "898600000000001", testPackage, "ORD-1", "OUT-OTHER")
task, err := repo.Load(ctx, batchNo)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if err = repo.CommitBusiness(ctx, task); err != nil {
t.Fatalf("CommitBusiness() returned error = %v, want nil", err)
}
assertTableCount(t, ctx, repo.db, cardInfoTable, 1, "iccid = ?", "898600000000001")
assertTableCount(t, ctx, repo.db, cardBindPkgTable, 1, "iccid = ?", "898600000000001")
assertTableCount(t, ctx, repo.db, stockTable, 0, "iccid = ?", "898600000000001")
assertBatchMainStatus(t, ctx, repo.db, batchNo, consts.BatchAsyncStatusFailed, consts.BatchRunResultFailed)
}
func TestCommitBusinessFailsOnExistingStock(t *testing.T) {
ctx := context.Background()
repo := newTestRepository(t)
batchNo := "UNS-1"
seedProcessingBatch(t, ctx, repo.db, batchNo, testSource)
seedRunningDetail(t, ctx, repo.db, batchNo, 1, "898600000000001", "13800000001", "460001000000001", testSource, "")
seedCard(t, ctx, repo.db, "898600000000001", "13800000001", "460001000000001", testPackage, testDeptID)
seedActiveBind(t, ctx, repo.db, "898600000000001", testPackage, "ORD-1", testSource)
seedStock(t, ctx, repo.db, "898600000000001")
task, err := repo.Load(ctx, batchNo)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if err = repo.CommitBusiness(ctx, task); err != nil {
t.Fatalf("CommitBusiness() returned error = %v, want nil", err)
}
assertTableCount(t, ctx, repo.db, cardInfoTable, 1, "iccid = ?", "898600000000001")
assertTableCount(t, ctx, repo.db, cardBindPkgTable, 1, "iccid = ?", "898600000000001")
assertTableCount(t, ctx, repo.db, stockTable, 1, "iccid = ?", "898600000000001")
assertTableCount(t, ctx, repo.db, bindPackageLogsTable, 0, "iccid = ?", "898600000000001")
}
func TestCommitBusinessFailsOnMultiplePackagesInBatch(t *testing.T) {
ctx := context.Background()
repo := newTestRepository(t)
batchNo := "UNS-1"
seedProcessingBatch(t, ctx, repo.db, batchNo, testSource)
seedRunningDetail(t, ctx, repo.db, batchNo, 1, "898600000000001", "13800000001", "460001000000001", testSource, "")
seedRunningDetail(t, ctx, repo.db, batchNo, 2, "898600000000002", "13800000002", "460001000000002", testSource, "")
seedCard(t, ctx, repo.db, "898600000000001", "13800000001", "460001000000001", "PKG-A", testDeptID)
seedCard(t, ctx, repo.db, "898600000000002", "13800000002", "460001000000002", "PKG-B", testDeptID)
seedActiveBind(t, ctx, repo.db, "898600000000001", "PKG-A", "ORD-1", testSource)
seedActiveBind(t, ctx, repo.db, "898600000000002", "PKG-B", "ORD-2", testSource)
task, err := repo.Load(ctx, batchNo)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if err = repo.CommitBusiness(ctx, task); err != nil {
t.Fatalf("CommitBusiness() returned error = %v, want nil", err)
}
assertTableCount(t, ctx, repo.db, cardInfoTable, 2, "iccid IN (?, ?)", "898600000000001", "898600000000002")
assertTableCount(t, ctx, repo.db, cardBindPkgTable, 2, "iccid IN (?, ?)", "898600000000001", "898600000000002")
assertTableCount(t, ctx, repo.db, bindPackageLogsTable, 0, "1=1")
assertTableCount(t, ctx, repo.db, stockTable, 0, "1=1")
assertBatchMainStatus(t, ctx, repo.db, batchNo, consts.BatchAsyncStatusFailed, consts.BatchRunResultFailed)
}
func TestCommitBusinessRollsBackOnOrderUpdateFailure(t *testing.T) {
ctx := context.Background()
repo := newTestRepository(t)
batchNo := "UNS-1"
seedProcessingBatch(t, ctx, repo.db, batchNo, testSource)
seedRunningDetail(t, ctx, repo.db, batchNo, 1, "898600000000001", "13800000001", "460001000000001", testSource, "")
seedCard(t, ctx, repo.db, "898600000000001", "13800000001", "460001000000001", testPackage, testDeptID)
seedActiveBind(t, ctx, repo.db, "898600000000001", testPackage, "ORD-1", testSource)
seedOrder(t, ctx, repo.db, "ORD-1")
seedOrderItem(t, ctx, repo.db, "ORD-1", `{}`)
if _, err := repo.db.Exec(ctx, "DROP TABLE t_order"); err != nil {
t.Fatalf("drop order table: %v", err)
}
task, err := repo.Load(ctx, batchNo)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if err = repo.CommitBusiness(ctx, task); err == nil {
t.Fatal("expected CommitBusiness to fail when order table is missing")
}
assertTableCount(t, ctx, repo.db, cardInfoTable, 1, "iccid = ?", "898600000000001")
assertTableCount(t, ctx, repo.db, cardBindPkgTable, 1, "iccid = ?", "898600000000001")
assertTableCount(t, ctx, repo.db, bindPackageLogsTable, 0, "iccid = ?", "898600000000001")
assertTableCount(t, ctx, repo.db, stockTable, 0, "iccid = ?", "898600000000001")
assertBatchMainStatus(t, ctx, repo.db, batchNo, batchdomain.AsyncStatusProcessing, batchdomain.RunResultRunning)
}
func TestSaveResultFileFailureOnlyMarksBatchInfo(t *testing.T) {
ctx := context.Background()
repo := newTestRepository(t)
batchNo := "UNS-1"
seedProcessingBatch(t, ctx, repo.db, batchNo, testSource)
seedRunningDetail(t, ctx, repo.db, batchNo, 1, "898600000000001", "13800000001", "460001000000001", testSource, "")
task, err := repo.Load(ctx, batchNo)
if err != nil {
t.Fatalf("Load() error = %v", err)
}
if err = repo.updateResultFileStatus(ctx, task, "failed", 0, 0, "disk full"); err != nil {
t.Fatalf("updateResultFileStatus() error = %v", err)
}
var row struct {
Info string `orm:"batch_info"`
}
if err = repo.db.Model(dao.BatchMain.Table()).Ctx(ctx).
Where(dao.BatchMain.Columns().BatchNo, batchNo).
Fields("batch_info").
Scan(&row); err != nil {
t.Fatalf("query batch main: %v", err)
}
info := gjson.New([]byte(row.Info))
if info.Get("resultFile.status").String() != "failed" {
t.Fatalf("resultFile.status = %s, want failed", info.Get("resultFile.status").String())
}
if info.Get("resultFile.error").String() != "disk full" {
t.Fatalf("resultFile.error = %s, want disk full", info.Get("resultFile.error").String())
}
assertBatchMainStatus(t, ctx, repo.db, batchNo, batchdomain.AsyncStatusProcessing, batchdomain.RunResultRunning)
}
func assertOrderReturnStatus(t *testing.T, ctx context.Context, db gdb.DB, orderCode string, want int) {
t.Helper()
var row struct {
ReturnStatus int `orm:"return_status"`
}
if err := db.Model(orderTable).Ctx(ctx).
Where("order_sn", orderCode).
Fields("return_status").
Scan(&row); err != nil {
t.Fatalf("query order return status: %v", err)
}
if row.ReturnStatus != want {
t.Fatalf("order %s return_status = %d, want %d", orderCode, row.ReturnStatus, want)
}
}
func assertOrderItemExtra(t *testing.T, ctx context.Context, db gdb.DB, orderCode, batchNo string, wantQuantity int) {
t.Helper()
var row struct {
Extra string `orm:"extra_sku_data"`
}
if err := db.Model(orderItemTable).Ctx(ctx).
Where("order_sn", orderCode).
Fields("extra_sku_data").
Scan(&row); err != nil {
t.Fatalf("query order item extra: %v", err)
}
info := gjson.New([]byte(row.Extra))
if info.Get("lastUnsubscribeBatchNo").String() != batchNo {
t.Fatalf("lastUnsubscribeBatchNo = %s, want %s", info.Get("lastUnsubscribeBatchNo").String(), batchNo)
}
if int(info.Get("unsubscribedQuantity").Int()) != wantQuantity {
t.Fatalf("unsubscribedQuantity = %d, want %d", info.Get("unsubscribedQuantity").Int(), wantQuantity)
}
}
var _ unsub.Repository = (*Repository)(nil)