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),属既有缺陷,本仓未引入亦未修复。
This commit is contained in:
MiFaZhan
2026-10-22 16:37:56 +08:00
commit 2bec257289
361 changed files with 71524 additions and 0 deletions
+407
View File
@@ -0,0 +1,407 @@
package unsub
import (
"context"
"database/sql"
"errors"
"fmt"
"math"
"sort"
"strconv"
"strings"
"time"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
"git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/consts"
unsub "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/service/unsub"
)
const (
agentBalanceTable = "t_agent_balance"
balanceDetailTable = "t_balance_detail"
balanceAccountStatusNormal = 0
financeIncomeExpenseIncome = 0
financeTransactionTypeConsume = 1
financeTransactionChannelBalance = 3
financeTransactionStatusPaid = 1
refundStatusRefunded = "refunded"
refundStatusNoRefund = "no_refund"
refundStatusSkipped = "skipped"
)
type balanceAccountQuery struct {
Id int64 `orm:"id"`
DeptId int64 `orm:"dept_id"`
Balance float64 `orm:"balance"`
Status int `orm:"status"`
TenantId string `orm:"tenant_id"`
}
type balanceDetailQuery struct {
Id int64 `orm:"id"`
IncomeExpenseType int `orm:"income_expense_type"`
TransactionStatus int `orm:"transaction_status"`
Amount float64 `orm:"amount"`
}
func unsubscribeTime(task *unsub.Task, fallback *gtime.Time) *gtime.Time {
if task != nil && task.Batch.StartTime != nil {
return task.Batch.StartTime
}
return fallback
}
func (r *Repository) lockOrdersByOrderCodes(ctx context.Context, tx gdb.TX, tenantID string, orderCounts map[string]int) (map[string]*orderQuery, error) {
result := make(map[string]*orderQuery)
orderCodes := sortedOrderCodes(orderCounts)
if len(orderCodes) == 0 {
return result, nil
}
var rows []*orderQuery
model := r.applyLock(tx.Ctx(ctx).Model(orderTable)).
WhereIn("order_sn", orderCodes).
Fields("id", "order_sn", "customer_agent_id", "pay_type", "pay_status", "pay_order_sn", "total_amount", "paid_amount", "balance", "unrefund_amount", "tenant_id", "created_dept")
if strings.TrimSpace(tenantID) != "" {
model = model.Where("tenant_id", tenantID)
}
if err := model.Scan(&rows); err != nil {
return nil, err
}
for _, row := range rows {
if row != nil && strings.TrimSpace(row.OrderSn) != "" {
result[strings.TrimSpace(row.OrderSn)] = row
}
}
return result, nil
}
func (r *Repository) lockOrderItemsByOrderCodes(ctx context.Context, tx gdb.TX, tenantID string, orderCounts map[string]int) (map[string]*orderItemQuery, error) {
result := make(map[string]*orderItemQuery)
orderCodes := sortedOrderCodes(orderCounts)
if len(orderCodes) == 0 {
return result, nil
}
var rows []*orderItemQuery
model := r.applyLock(tx.Ctx(ctx).Model(orderItemTable)).
WhereIn("order_sn", orderCodes).
Fields("item_id", "order_sn", "price", "quantity", "service_begin_time", "service_end_time", "extra_sku_data")
if strings.TrimSpace(tenantID) != "" {
model = model.Where("tenant_id", tenantID)
}
if err := model.Scan(&rows); err != nil {
return nil, err
}
for _, row := range rows {
if row == nil || strings.TrimSpace(row.OrderSn) == "" {
continue
}
orderCode := strings.TrimSpace(row.OrderSn)
if _, exists := result[orderCode]; !exists {
result[orderCode] = row
}
}
return result, nil
}
func (r *Repository) refundOrders(
ctx context.Context,
tx gdb.TX,
task *unsub.Task,
orderCounts map[string]int,
bindsByIccid map[string]*bindPkgQuery,
orders map[string]*orderQuery,
orderItems map[string]*orderItemQuery,
activeCounts map[string]int,
cutoffTime, transactionTime *gtime.Time,
) (map[string]*unsub.OrderRefund, error) {
if cutoffTime == nil {
cutoffTime = transactionTime
}
if cutoffTime == nil {
return nil, gerror.New("unsub refund cutoff time is empty")
}
result := make(map[string]*unsub.OrderRefund, len(orderCounts))
orderBinds := make(map[string][]*bindPkgQuery, len(orderCounts))
for _, detail := range task.Details {
if detail == nil {
continue
}
bind := bindsByIccid[detail.ICCID]
if bind == nil {
continue
}
orderCode := strings.TrimSpace(bind.OrderCode)
if orderCode != "" {
orderBinds[orderCode] = append(orderBinds[orderCode], bind)
}
}
for _, orderCode := range sortedOrderCodes(orderCounts) {
refund := &unsub.OrderRefund{Status: refundStatusNoRefund}
result[orderCode] = refund
order := orders[orderCode]
if order == nil {
refund.Status = refundStatusSkipped
refund.Reason = "order not found"
g.Log().Warningf(ctx, "unsub balance refund skipped, order not found, batchNo=%s orderNo=%s", task.Batch.BatchNo, orderCode)
continue
}
if order.PayType != consts.PayTypeBalance {
refund.Status = refundStatusSkipped
refund.Reason = "order is not paid by balance"
g.Log().Warningf(ctx, "unsub balance refund skipped, order is not paid by balance, batchNo=%s orderNo=%s payType=%d", task.Batch.BatchNo, orderCode, order.PayType)
continue
}
if order.PayStatus != 0 && order.PayStatus != consts.PayStatusPaid {
refund.Status = refundStatusSkipped
refund.Reason = "order is not paid"
g.Log().Warningf(ctx, "unsub balance refund skipped, order is not paid, batchNo=%s orderNo=%s payStatus=%d", task.Batch.BatchNo, orderCode, order.PayStatus)
continue
}
binds := orderBinds[orderCode]
if len(binds) == 0 {
continue
}
item := orderItems[orderCode]
totalQuantity := resolveOrderQuantity(order, item, activeCounts[orderCode])
if totalQuantity <= 0 {
return nil, gerror.Newf("order %s lacks a valid subscription quantity for refund", orderCode)
}
paidAmount := orderPaidAmount(order, item, totalQuantity)
if paidAmount <= 0 {
refund.Reason = "order paid amount is zero"
continue
}
unitPaidAmount := paidAmount / float64(totalQuantity)
refundAmount := 0.0
for _, bind := range binds {
if bind == nil {
continue
}
if bind.EffectiveTime == nil || bind.DurationMonths <= 0 {
return nil, gerror.Newf("order %s has an invalid paid subscription period", orderCode)
}
refundAmount += unitPaidAmount * calculateRefundRatio(bind.EffectiveTime.Time, cutoffTime.Time, bind.DurationMonths)
}
refund.Amount = roundUnsubAmount(refundAmount)
if refund.Amount <= 0 {
refund.Reason = "paid subscription period has been fully used"
continue
}
refund.SerialNo = unsubscribeRefundSerialNo(task.Batch.BatchNo, orderCode)
existing, err := r.lockRefundDetail(ctx, tx, task.Batch.TenantID, refund.SerialNo)
if err != nil {
return nil, gerror.Wrap(err, "query existing unsub refund detail")
}
if existing.Id != 0 {
if existing.IncomeExpenseType != financeIncomeExpenseIncome || existing.TransactionStatus != financeTransactionStatusPaid {
return nil, gerror.Newf("existing unsub refund detail is invalid, order %s", orderCode)
}
refund.Amount = roundUnsubAmount(existing.Amount)
refund.Status = refundStatusRefunded
refund.Reason = "refund detail already exists"
continue
}
payerDeptID := refundPayerDeptID(order, binds)
if payerDeptID <= 0 {
return nil, gerror.Newf("order %s lacks a balance payer department", orderCode)
}
tenantID := strings.TrimSpace(order.TenantId)
if tenantID == "" {
tenantID = strings.TrimSpace(task.Batch.TenantID)
}
if tenantID == "" {
return nil, gerror.Newf("order %s lacks a tenant id for refund", orderCode)
}
var account balanceAccountQuery
accountModel := r.applyLock(tx.Ctx(ctx).Model(agentBalanceTable)).
Where("dept_id", payerDeptID).
Where("tenant_id", tenantID).
Fields("id", "dept_id", "balance", "status", "tenant_id")
if err := accountModel.Scan(&account); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, gerror.Wrap(err, "query balance account for unsub refund")
}
if account.Id == 0 {
return nil, gerror.Newf("balance account for order %s does not exist", orderCode)
}
if account.Status != balanceAccountStatusNormal {
return nil, gerror.Newf("balance account for order %s is not available", orderCode)
}
before := roundUnsubAmount(account.Balance)
after := roundUnsubAmount(before + refund.Amount)
accountUpdate, err := tx.Ctx(ctx).Model(agentBalanceTable).
Where("id", account.Id).
Data(g.Map{
"balance": after,
"updated_by": task.Batch.CreatedBy,
"updated_at": transactionTime,
}).Update()
if err != nil {
return nil, gerror.Wrap(err, "update balance account for unsub refund")
}
if affected, err := accountUpdate.RowsAffected(); err != nil {
return nil, gerror.Wrap(err, "read balance account update result")
} else if affected != 1 {
return nil, gerror.Newf("balance account for order %s changed during refund", orderCode)
}
createdDept := order.CreatedDept
if createdDept <= 0 {
createdDept = payerDeptID
}
if _, err := tx.Ctx(ctx).Model(balanceDetailTable).Data(g.Map{
"dept_id": payerDeptID,
"serial_no": refund.SerialNo,
"transaction_time": transactionTime,
"income_expense_type": financeIncomeExpenseIncome,
"transaction_type": financeTransactionTypeConsume,
"transaction_channel": financeTransactionChannelBalance,
"transaction_status": financeTransactionStatusPaid,
"amount": refund.Amount,
"before_balance": before,
"balance_after": after,
"order_sn": orderCode,
"remark": fmt.Sprintf("SIM card unsubscribe balance refund, batchNo=%s, orderNo=%s", task.Batch.BatchNo, orderCode),
"tenant_id": tenantID,
"created_dept": createdDept,
"created_by": task.Batch.CreatedBy,
"created_at": transactionTime,
}).Insert(); err != nil {
return nil, gerror.Wrap(err, "insert unsub refund detail")
}
refund.Status = refundStatusRefunded
}
return result, nil
}
func (r *Repository) lockRefundDetail(ctx context.Context, tx gdb.TX, tenantID, serialNo string) (*balanceDetailQuery, error) {
var row balanceDetailQuery
model := r.applyLock(tx.Ctx(ctx).Model(balanceDetailTable)).
Where("tenant_id", tenantID).
Where("serial_no", serialNo).
Fields("id", "income_expense_type", "transaction_status", "amount")
if err := model.Scan(&row); err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
return &row, nil
}
func sortedOrderCodes(orderCounts map[string]int) []string {
orderCodes := make([]string, 0, len(orderCounts))
for orderCode := range orderCounts {
if strings.TrimSpace(orderCode) != "" {
orderCodes = append(orderCodes, orderCode)
}
}
sort.Strings(orderCodes)
return orderCodes
}
func resolveOrderQuantity(order *orderQuery, item *orderItemQuery, activeCount int) int {
if item != nil && item.Quantity > 0 {
return item.Quantity
}
paidAmount := orderPaidAmount(order, nil, 0)
if item != nil && item.Price > 0 && paidAmount > 0 {
quantity := int(math.Round(paidAmount / item.Price))
if quantity > 0 {
return quantity
}
}
return activeCount
}
func orderPaidAmount(order *orderQuery, item *orderItemQuery, quantity int) float64 {
if order != nil {
if order.Balance > 0 {
return order.Balance
}
if order.PaidAmount > 0 {
return order.PaidAmount
}
if order.TotalAmount > 0 {
return order.TotalAmount
}
}
if item != nil && item.Price > 0 && quantity > 0 {
return item.Price * float64(quantity)
}
return 0
}
func refundPayerDeptID(order *orderQuery, binds []*bindPkgQuery) int64 {
if order != nil && order.CustomerAgentId > 0 {
return order.CustomerAgentId
}
for _, bind := range binds {
if bind == nil {
continue
}
if deptID, err := strconv.ParseInt(strings.TrimSpace(bind.DeptId), 10, 64); err == nil && deptID > 0 {
return deptID
}
}
if order != nil && order.CreatedDept > 0 {
return order.CreatedDept
}
return 0
}
func unsubscribeRefundSerialNo(batchNo, orderCode string) string {
return fmt.Sprintf("UNSUBREFUND-%s-%s", strings.TrimSpace(batchNo), strings.TrimSpace(orderCode))
}
func roundUnsubAmount(value float64) float64 {
if value <= 0 {
return 0
}
return math.Round(value*100) / 100
}
func calculateRefundRatio(effectiveAt, cutoffAt time.Time, durationMonths int) float64 {
if effectiveAt.IsZero() || cutoffAt.IsZero() || durationMonths <= 0 {
return 0
}
paidEndAt := addRefundMonths(effectiveAt, durationMonths)
if !cutoffAt.After(effectiveAt) {
return 1
}
if !cutoffAt.Before(paidEndAt) {
return 0
}
totalSeconds := paidEndAt.Sub(effectiveAt).Seconds()
remainingSeconds := paidEndAt.Sub(cutoffAt).Seconds()
if totalSeconds <= 0 || remainingSeconds <= 0 {
return 0
}
ratio := remainingSeconds / totalSeconds
if ratio < 0 {
return 0
}
if ratio > 1 {
return 1
}
return ratio
}
func addRefundMonths(value time.Time, months int) time.Time {
if months <= 0 {
return value
}
targetMonth := time.Date(value.Year(), value.Month()+time.Month(months), 1, value.Hour(), value.Minute(), value.Second(), value.Nanosecond(), value.Location())
day := value.Day()
lastDay := time.Date(targetMonth.Year(), targetMonth.Month()+1, 0, value.Hour(), value.Minute(), value.Second(), value.Nanosecond(), value.Location()).Day()
if day > lastDay {
day = lastDay
}
return time.Date(targetMonth.Year(), targetMonth.Month(), day, value.Hour(), value.Minute(), value.Second(), value.Nanosecond(), value.Location())
}
+37
View File
@@ -0,0 +1,37 @@
package unsub
import (
"math"
"testing"
"time"
)
func TestCalculateRefundRatio(t *testing.T) {
effective := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
cases := []struct {
name string
cutoff time.Time
want float64
}{
{name: "before effective refunds all", cutoff: effective.Add(-time.Hour), want: 1},
{name: "half paid period", cutoff: time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC), want: 184.0 / 365.0},
{name: "during gift period refunds none", cutoff: time.Date(2027, 2, 1, 0, 0, 0, 0, time.UTC), want: 0},
{name: "after paid period refunds none", cutoff: time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC), want: 0},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
got := calculateRefundRatio(effective, testCase.cutoff, 12)
if math.Abs(got-testCase.want) > 0.000001 {
t.Fatalf("calculateRefundRatio() = %v, want %v", got, testCase.want)
}
})
}
}
func TestAddRefundMonthsClampsMonthEnd(t *testing.T) {
got := addRefundMonths(time.Date(2026, 1, 31, 12, 0, 0, 0, time.UTC), 1)
want := time.Date(2026, 2, 28, 12, 0, 0, 0, time.UTC)
if !got.Equal(want) {
t.Fatalf("addRefundMonths() = %v, want %v", got, want)
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,790 @@
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)