Files
iotlcm/worker-svr/internal/repository/unsub/repository_test.go
T

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)