package store import ( "context" "os" "testing" "github.com/jmoiron/sqlx" ) func financeTimeTestDB(t *testing.T) *sqlx.DB { t.Helper() dsn := os.Getenv("OLONS_TEST_MYSQL_DSN") if dsn == "" { t.Skip("set OLONS_TEST_MYSQL_DSN to run the finance time integration tests") } db, err := sqlx.Connect("mysql", dsn) if err != nil { t.Fatal(err) } db.SetMaxOpenConns(1) db.SetMaxIdleConns(1) t.Cleanup(func() { _ = db.Close() }) return db } func TestFinanceRecordTimesUseCorrectSourceZone(t *testing.T) { db := financeTimeTestDB(t) // Temporary tables shadow only this connection's tables; no business rows are changed. for _, query := range []string{ `CREATE TEMPORARY TABLE hg_payment_channel (id BIGINT,tenant_id BIGINT,name VARCHAR(64))`, `CREATE TEMPORARY TABLE hg_recharge_order ( id BIGINT,tenant_id BIGINT,member_id BIGINT,recharge_no VARCHAR(64),channel_id BIGINT, request_amount BIGINT DEFAULT 50000,paid_amount BIGINT DEFAULT 50000,currency VARCHAR(8) DEFAULT 'CNY', voucher_attachment_ids JSON,recharge_status INT DEFAULT 3,remark VARCHAR(255) DEFAULT '', review_reason_code VARCHAR(64) DEFAULT '',review_reason VARCHAR(255) DEFAULT '',external_no VARCHAR(64) DEFAULT '', paid_at DATETIME,reviewed_at DATETIME,credited_at DATETIME,created_at DATETIME,updated_at DATETIME)`, `CREATE TEMPORARY TABLE hg_withdrawal_order ( id BIGINT,tenant_id BIGINT,member_id BIGINT,withdrawal_no VARCHAR(64),payment_account_id BIGINT DEFAULT 1, channel_id BIGINT,request_amount BIGINT DEFAULT 50000,fee_amount BIGINT DEFAULT 0,actual_amount BIGINT DEFAULT 50000, currency VARCHAR(8) DEFAULT 'CNY',account_mask VARCHAR(64) DEFAULT '',bank_name VARCHAR(64) DEFAULT '', holder_mask VARCHAR(64) DEFAULT '',risk_result JSON,withdrawal_status INT DEFAULT 5, review_reason_code VARCHAR(64) DEFAULT '',review_reason VARCHAR(255) DEFAULT '',external_no VARCHAR(64) DEFAULT '', reviewed_at DATETIME,processed_at DATETIME,completed_at DATETIME,created_at DATETIME,updated_at DATETIME)`, } { if _, err := db.Exec(query); err != nil { t.Fatal(err) } } s := &Store{db: db} ctx := context.Background() for _, test := range []struct{ zone, createdAt string }{ {"+00:00", "2026-10-22 15:59:00"}, {"+08:00", "2026-10-22 23:59:00"}, } { t.Run(test.zone, func(t *testing.T) { for _, query := range []string{`DELETE FROM hg_recharge_order`, `DELETE FROM hg_withdrawal_order`} { if _, err := db.Exec(query); err != nil { t.Fatal(err) } } if _, err := db.Exec(`SET SESSION time_zone=?`, test.zone); err != nil { t.Fatal(err) } if _, err := db.Exec(`INSERT INTO hg_recharge_order (id,tenant_id,member_id,recharge_no,channel_id,created_at,updated_at,paid_at,reviewed_at,credited_at) VALUES(1,0,9,'RT1',1,?,'2026-10-23 00:01:00','2026-10-23 00:01:00','2026-10-23 00:01:00','2026-10-23 00:01:00'), (2,0,9,'RT2',1,?,?,NULL,NULL,NULL)`, test.createdAt, test.createdAt, test.createdAt); err != nil { t.Fatal(err) } if _, err := db.Exec(`INSERT INTO hg_withdrawal_order (id,tenant_id,member_id,withdrawal_no,channel_id,created_at,updated_at,reviewed_at,processed_at,completed_at) VALUES(1,0,9,'WT1',1,?,'2026-10-23 00:01:00','2026-10-23 00:01:00','2026-10-23 00:01:00','2026-10-23 00:01:00'), (2,0,9,'WT2',1,?,?,NULL,NULL,NULL)`, test.createdAt, test.createdAt, test.createdAt); err != nil { t.Fatal(err) } recharges, err := s.RechargeOrders(ctx, 0, 9, 0, 0, 50) if err != nil || recharges == nil || len(recharges.Items) != 2 { t.Fatalf("recharges = %+v, err = %v", recharges, err) } withdrawals, err := s.WithdrawalOrders(ctx, 0, 9, 0, 0, 50) if err != nil || withdrawals == nil || len(withdrawals.Items) != 2 { t.Fatalf("withdrawals = %+v, err = %v", withdrawals, err) } for _, item := range recharges.Items { if item.CreatedAt != "2026-10-22T15:59:00Z" || (item.ID == 1 && item.UpdatedAt != "2026-10-22T16:01:00Z") || (item.ID == 2 && item.UpdatedAt != item.CreatedAt) { t.Errorf("recharge list time = %+v", item) } } for _, item := range withdrawals.Items { if item.CreatedAt != "2026-10-22T15:59:00Z" || (item.ID == 1 && item.UpdatedAt != "2026-10-22T16:01:00Z") || (item.ID == 2 && item.UpdatedAt != item.CreatedAt) { t.Errorf("withdrawal list time = %+v", item) } } recharge, err := s.RechargeOrder(ctx, 0, 9, 1) if err != nil { t.Fatal(err) } if recharge.PaidAt != "2026-10-22T16:01:00Z" || recharge.ReviewedAt != recharge.PaidAt || recharge.CreditedAt != recharge.PaidAt { t.Errorf("recharge review timestamps = %+v", recharge) } withdrawal, err := s.WithdrawalOrder(ctx, 0, 9, 1) if err != nil { t.Fatal(err) } if withdrawal.ReviewedAt != "2026-10-22T16:01:00Z" || withdrawal.ProcessedAt != withdrawal.ReviewedAt || withdrawal.CompletedAt != withdrawal.ReviewedAt { t.Errorf("withdrawal review timestamps = %+v", withdrawal) } recharge, err = s.RechargeOrder(ctx, 0, 9, 2) if err != nil || recharge == nil || recharge.PaidAt != "" || recharge.ReviewedAt != "" || recharge.CreditedAt != "" { t.Errorf("empty recharge review times = %+v, err = %v", recharge, err) } withdrawal, err = s.WithdrawalOrder(ctx, 0, 9, 2) if err != nil || withdrawal == nil || withdrawal.ReviewedAt != "" || withdrawal.ProcessedAt != "" || withdrawal.CompletedAt != "" { t.Errorf("empty withdrawal review times = %+v, err = %v", withdrawal, err) } }) } } func TestWalletAndOrderLedgerTimesKeepUTCAndBeijingWriters(t *testing.T) { db := financeTimeTestDB(t) if _, err := db.Exec(`CREATE TEMPORARY TABLE hg_wallet_ledger ( id BIGINT,tenant_id BIGINT DEFAULT 0,member_id BIGINT DEFAULT 9,ledger_no VARCHAR(64), biz_type VARCHAR(64) DEFAULT 'commission',biz_no VARCHAR(64) DEFAULT 'ORDER1',entry_type VARCHAR(64) DEFAULT 'available', direction INT DEFAULT 1,amount BIGINT DEFAULT 100,before_amount BIGINT DEFAULT 1000,after_amount BIGINT DEFAULT 1100, currency VARCHAR(8) DEFAULT 'CNY',remark VARCHAR(255) DEFAULT '',idempotency_key VARCHAR(128),occurred_at DATETIME)`); err != nil { t.Fatal(err) } fixtures := []struct{ key, remark, occurredAt, want string }{ {"task:1:settle:commission", "任务完成自动返佣", "2026-10-22 16:00:07", "2026-10-22T16:00:07Z"}, {"recharge:2", "储入审核到账", "2026-10-22 23:59:07", "2026-10-22T15:59:07Z"}, {"withdrawal:W1:available", "申请取出冻结", "2026-10-22 15:58:07", "2026-10-22T15:58:07Z"}, {"withdraw:1:consume", "取出审核", "2026-10-22 23:57:07", "2026-10-22T15:57:07Z"}, {"task:2:admin-match:commission", "后台匹配订单完成返佣", "2026-10-22 23:56:07", "2026-10-22T15:56:07Z"}, {"task:3:match:available", "自动派单扣除可用余额", "2026-10-22 15:55:07", "2026-10-22T15:55:07Z"}, {"task:4:match:available", "手动订单强制付款扣除可用余额", "2026-10-22 23:54:07", "2026-10-22T15:54:07Z"}, {"task:5:match:frozen", "会员确认手动订单冻结本金", "2026-10-22 15:53:07", "2026-10-22T15:53:07Z"}, {"task:6:expire:available", "订单超时退回订单本金", "2026-10-22 15:52:07", "2026-10-22T15:52:07Z"}, } for i, fixture := range fixtures { if _, err := db.Exec(`INSERT INTO hg_wallet_ledger (id,ledger_no,idempotency_key,remark,occurred_at) VALUES(?,?,?,?,?)`, i+1, fixture.key, fixture.key, fixture.remark, fixture.occurredAt); err != nil { t.Fatal(err) } } for _, zone := range []string{"+00:00", "+08:00"} { t.Run(zone, func(t *testing.T) { if _, err := db.Exec(`SET SESSION time_zone=?`, zone); err != nil { t.Fatal(err) } s := &Store{db: db} page, err := s.WalletLedgers(context.Background(), 0, 9, "", 0, 0, 50) if err != nil || page == nil || len(page.Items) != len(fixtures) { t.Fatalf("ledger page = %+v, err = %v", page, err) } for i, fixture := range fixtures { if page.Items[i].LedgerNo != fixture.key || page.Items[i].OccurredAt != fixture.want { t.Errorf("ledger %d = %+v, want %s at %s", i, page.Items[i], fixture.key, fixture.want) } } rows, err := s.OrderLedgers(context.Background(), 0, 9, []string{"ORDER1"}) if err != nil || len(rows) != len(fixtures) { t.Fatalf("order ledgers = %+v, err = %v", rows, err) } for i, row := range rows { fixture := fixtures[len(fixtures)-i-1] if row["ledger_no"] != fixture.key || row["occurred_at"] != fixture.want { t.Errorf("order ledger = %+v, want %s at %s", row, fixture.key, fixture.want) } } }) } }