package balance import ( "context" "errors" "io" "path/filepath" "testing" _ "github.com/gogf/gf/contrib/drivers/sqlite/v2" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/frame/g" ) type tradeQueryerStub struct { result *TradeQueryResult err error calls int } func (s *tradeQueryerStub) Query(_ context.Context, _ string) (*TradeQueryResult, error) { s.calls++ return s.result, s.err } func TestCheckRechargeTimeout(t *testing.T) { g.Log().SetWriter(io.Discard) ctx := context.Background() if err := gdb.SetConfig(gdb.Config{ "default": gdb.ConfigGroup{{Type: "sqlite", Name: filepath.Join(t.TempDir(), "balance.db")}}, }); err != nil { t.Fatalf("configure test database: %v", err) } defer g.DB().Close(ctx) createBalanceTables(t, ctx) t.Run("paid recharge is credited exactly once", func(t *testing.T) { insertRechargeDetail(t, ctx, "BAL-PAID", 10) insertAgentBalance(t, ctx, 11, "tenant-1", 5) queryer := &tradeQueryerStub{result: &TradeQueryResult{TradeStatus: "TRADE_SUCCESS", TotalAmount: "10.00", TradeNo: "ALI-1"}} service := NewWithQuery(queryer) if err := service.CheckRechargeTimeout(ctx, "BAL-PAID"); err != nil { t.Fatalf("first check failed: %v", err) } if status := rechargeStatus(t, ctx, "BAL-PAID"); status != rechargeStatusPaid { t.Fatalf("expected paid status, got %d", status) } if balance := agentBalanceValue(t, ctx, 11, "tenant-1"); balance != 15 { t.Fatalf("expected balance 15, got %v", balance) } if err := service.CheckRechargeTimeout(ctx, "BAL-PAID"); err != nil { t.Fatalf("duplicate check failed: %v", err) } if queryer.calls != 1 { t.Fatalf("expected one Alipay query, got %d", queryer.calls) } if balance := agentBalanceValue(t, ctx, 11, "tenant-1"); balance != 15 { t.Fatalf("duplicate message changed balance to %v", balance) } }) t.Run("unpaid recharge is expired", func(t *testing.T) { insertRechargeDetail(t, ctx, "BAL-EXPIRED", 10) service := NewWithQuery(&tradeQueryerStub{result: &TradeQueryResult{TradeStatus: "WAIT_BUYER_PAY"}}) if err := service.CheckRechargeTimeout(ctx, "BAL-EXPIRED"); err != nil { t.Fatalf("timeout check failed: %v", err) } if status := rechargeStatus(t, ctx, "BAL-EXPIRED"); status != rechargeStatusExpired { t.Fatalf("expected expired status, got %d", status) } }) t.Run("missing recharge is ignored", func(t *testing.T) { queryer := &tradeQueryerStub{err: errors.New("must not be called")} if err := NewWithQuery(queryer).CheckRechargeTimeout(ctx, "BAL-MISSING"); err != nil { t.Fatalf("missing recharge returned error: %v", err) } if queryer.calls != 0 { t.Fatalf("missing recharge queried Alipay %d times", queryer.calls) } }) t.Run("amount mismatch returns an error without crediting", func(t *testing.T) { insertRechargeDetail(t, ctx, "BAL-MISMATCH", 10) insertAgentBalance(t, ctx, 11, "tenant-1", 15) service := NewWithQuery(&tradeQueryerStub{result: &TradeQueryResult{TradeStatus: "TRADE_SUCCESS", TotalAmount: "9.00"}}) if err := service.CheckRechargeTimeout(ctx, "BAL-MISMATCH"); err == nil { t.Fatal("expected amount mismatch error") } if status := rechargeStatus(t, ctx, "BAL-MISMATCH"); status != rechargeStatusPending { t.Fatalf("mismatch changed status to %d", status) } if balance := agentBalanceValue(t, ctx, 11, "tenant-1"); balance != 15 { t.Fatalf("mismatch changed balance to %v", balance) } }) t.Run("Alipay failure is returned for delayed retry", func(t *testing.T) { insertRechargeDetail(t, ctx, "BAL-RETRY", 10) service := NewWithQuery(&tradeQueryerStub{err: errors.New("Alipay unavailable")}) if err := service.CheckRechargeTimeout(ctx, "BAL-RETRY"); err == nil { t.Fatal("expected query error") } if status := rechargeStatus(t, ctx, "BAL-RETRY"); status != rechargeStatusPending { t.Fatalf("retryable failure changed status to %d", status) } }) } func createBalanceTables(t *testing.T, ctx context.Context) { t.Helper() statements := []string{ `CREATE TABLE t_balance_detail ( id INTEGER PRIMARY KEY AUTOINCREMENT, dept_id INTEGER NOT NULL, transaction_status INTEGER NOT NULL, transaction_channel INTEGER NOT NULL, amount REAL NOT NULL, order_sn TEXT NOT NULL, tenant_id TEXT NOT NULL, created_dept INTEGER NOT NULL, created_by INTEGER NOT NULL, transaction_time DATETIME, before_balance REAL, balance_after REAL, channel_serial_no TEXT, remark TEXT, updated_by INTEGER, updated_at DATETIME )`, `CREATE TABLE t_agent_balance ( id INTEGER PRIMARY KEY AUTOINCREMENT, dept_id INTEGER NOT NULL, balance REAL NOT NULL, withdraw_amount REAL, withdraw_amount_status INTEGER, invoice_amount REAL, can_invoice_amount REAL, credit_limit REAL, used_credit REAL, pending_repayment REAL, status INTEGER, tenant_id TEXT NOT NULL, created_dept INTEGER, created_by INTEGER, created_at DATETIME, updated_by INTEGER, updated_at DATETIME )`, } for _, statement := range statements { if _, err := g.DB().Exec(ctx, statement); err != nil { t.Fatalf("create table: %v", err) } } } func insertRechargeDetail(t *testing.T, ctx context.Context, orderSN string, amount float64) { t.Helper() if _, err := g.DB().Model("t_balance_detail").Ctx(ctx).Data(g.Map{ "dept_id": 11, "transaction_status": rechargeStatusPending, "transaction_channel": rechargeChannelAlipay, "amount": amount, "order_sn": orderSN, "tenant_id": "tenant-1", "created_dept": 11, "created_by": 7, }).Insert(); err != nil { t.Fatalf("insert recharge detail: %v", err) } } func insertAgentBalance(t *testing.T, ctx context.Context, deptID int64, tenantID string, balance float64) { t.Helper() if _, err := g.DB().Model("t_agent_balance").Ctx(ctx).Data(g.Map{"dept_id": deptID, "tenant_id": tenantID, "balance": balance}).Insert(); err != nil { t.Fatalf("insert agent balance: %v", err) } } func rechargeStatus(t *testing.T, ctx context.Context, orderSN string) int { t.Helper() value, err := g.DB().Model("t_balance_detail").Ctx(ctx).Where("order_sn", orderSN).Value("transaction_status") if err != nil { t.Fatalf("get recharge status: %v", err) } return value.Int() } func agentBalanceValue(t *testing.T, ctx context.Context, deptID int64, tenantID string) float64 { t.Helper() value, err := g.DB().Model("t_agent_balance").Ctx(ctx).Where("dept_id", deptID).Where("tenant_id", tenantID).Value("balance") if err != nil { t.Fatalf("get agent balance: %v", err) } return value.Float64() }