Files
2026-09-23 17:42:55 +08:00

271 lines
9.8 KiB
Go

package recharge
import (
"context"
"errors"
"regexp"
"testing"
"time"
biz "kra/internal/biz/recharge"
"kra/internal/data/ledger"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
type testProvider struct{ db *gorm.DB }
func (p testProvider) DB() *gorm.DB { return p.db }
func TestMarkPaidCreditsWalletOnlyOnce(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&ledger.RechargeOrderPO{}, &ledger.WalletPO{}); err != nil {
t.Fatal(err)
}
if err = db.Exec(`CREATE TABLE sys_users (id integer primary key, username text, nick_name text)`).Error; err != nil {
t.Fatal(err)
}
if err = db.Exec(`INSERT INTO sys_users(id,username,nick_name) VALUES (3,'buyer','Buyer')`).Error; err != nil {
t.Fatal(err)
}
order := ledger.RechargeOrderPO{OrderNo: "CZ-1", UserID: 3, Diamonds: 60, AmountCents: 600}
if err = db.Create(&order).Error; err != nil {
t.Fatal(err)
}
repo := NewRepo(testProvider{db: db})
if _, err = repo.MarkPaid(context.Background(), order.OrderNo); err != nil {
t.Fatal(err)
}
if _, err = repo.MarkPaid(context.Background(), order.OrderNo); err != nil {
t.Fatal(err)
}
var wallet ledger.WalletPO
if err = db.Where("user_id = ?", 3).First(&wallet).Error; err != nil {
t.Fatal(err)
}
if wallet.DiamondBalance != 60 || wallet.TotalRechargeCents != 600 {
t.Fatalf("wallet credited more than once: %+v", wallet)
}
}
func TestListReadsPaymentStateFromKRAOrders(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&ledger.RechargeOrderPO{}); err != nil {
t.Fatal(err)
}
if err = db.Exec(`CREATE TABLE sys_users (id integer primary key, username text, nick_name text)`).Error; err != nil {
t.Fatal(err)
}
if err = db.Exec(`CREATE TABLE pay_orders (trade_no text, business_type text, business_id text, provider text, provider_trade_no text, payment_status text, fulfillment_status text, refund_status text)`).Error; err != nil {
t.Fatal(err)
}
if err = db.Exec(`INSERT INTO sys_users(id,username,nick_name) VALUES (3,'buyer','Buyer')`).Error; err != nil {
t.Fatal(err)
}
creditedAt := time.Now()
order := ledger.RechargeOrderPO{OrderNo: "CZ-2", UserID: 3, Diamonds: 300, AmountCents: 3000, CreditedAt: &creditedAt}
if err = db.Create(&order).Error; err != nil {
t.Fatal(err)
}
repo := NewRepo(testProvider{db: db})
pending, err := repo.Get(context.Background(), order.OrderNo)
if err != nil {
t.Fatal(err)
}
if pending.Provider != "" || pending.PaymentStatus != "" {
t.Fatalf("new business order unexpectedly has payment state: %+v", pending)
}
if err = db.Exec(`INSERT INTO pay_orders(trade_no,business_type,business_id,provider,provider_trade_no,payment_status,fulfillment_status,refund_status) VALUES (?,?,?,?,?,?,?,?)`, order.OrderNo, biz.BusinessType, order.OrderNo, "wechat-v3", "WX-1", "paid", "succeeded", "none").Error; err != nil {
t.Fatal(err)
}
items, total, summary, err := repo.List(context.Background(), biz.Filter{Provider: "wechat-v3", PaymentStatus: "paid"}, biz.ListOptions{Limit: 10})
if err != nil {
t.Fatal(err)
}
if total != 1 || len(items) != 1 || summary.PaidCount != 1 || summary.AmountCents != 3000 {
t.Fatalf("unexpected list result: total=%d items=%d summary=%+v", total, len(items), summary)
}
item := items[0]
if item.Provider != "wechat-v3" || item.ProviderTradeNo != "WX-1" || item.PaymentStatus != "paid" || item.FulfillmentStatus != "succeeded" || item.RefundStatus != "none" {
t.Fatalf("payment state was not joined: %+v", item)
}
}
func TestMarkPaidWalletFailureRollsBackCreditMarkerAndAllowsRetry(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&ledger.RechargeOrderPO{}, &ledger.WalletPO{}); err != nil {
t.Fatal(err)
}
order := ledger.RechargeOrderPO{OrderNo: "CZ-ROLLBACK", UserID: 3, Diamonds: 60, AmountCents: 600}
if err = db.Create(&order).Error; err != nil {
t.Fatal(err)
}
if err = db.Create(&ledger.WalletPO{UserID: 3, DiamondBalance: 10}).Error; err != nil {
t.Fatal(err)
}
if err = db.Exec(`CREATE TRIGGER reject_wallet_update BEFORE UPDATE ON live_wallets BEGIN SELECT RAISE(ABORT, 'forced wallet failure'); END`).Error; err != nil {
t.Fatal(err)
}
repo := NewRepo(testProvider{db: db})
if _, err = repo.MarkPaid(context.Background(), order.OrderNo); err == nil {
t.Fatal("forced wallet update failure was ignored")
}
var stored ledger.RechargeOrderPO
if err = db.First(&stored, order.ID).Error; err != nil {
t.Fatal(err)
}
var wallet ledger.WalletPO
if err = db.Where("user_id = ?", 3).First(&wallet).Error; err != nil {
t.Fatal(err)
}
if stored.CreditedAt != nil || wallet.DiamondBalance != 10 || wallet.TotalRechargeCents != 0 {
t.Fatalf("failed fulfillment changed credit marker or wallet: order=%+v wallet=%+v", stored, wallet)
}
if err = db.Exec(`DROP TRIGGER reject_wallet_update`).Error; err != nil {
t.Fatal(err)
}
for attempt := 0; attempt < 2; attempt++ {
if _, err = repo.MarkPaid(context.Background(), order.OrderNo); err != nil {
t.Fatal(err)
}
}
if err = db.Where("user_id = ?", 3).First(&wallet).Error; err != nil {
t.Fatal(err)
}
if err = db.First(&stored, order.ID).Error; err != nil {
t.Fatal(err)
}
if stored.CreditedAt == nil || wallet.DiamondBalance != 70 || wallet.TotalRechargeCents != 600 {
t.Fatalf("retry did not credit exactly once: order=%+v wallet=%+v", stored, wallet)
}
}
func TestRechargePlanStoragePreservesExplicitDisabledState(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&ledger.RechargePlanPO{}, &ledger.RechargeOrderPO{}); err != nil {
t.Fatal(err)
}
if err = db.Table("live_recharge_plans").AutoMigrate(&struct {
Enabled bool `gorm:"default:true"`
}{}); err != nil {
t.Fatal(err)
}
disabled := ledger.RechargePlanPO{Sort: 1, Diamonds: 60, AmountCents: 600, Enabled: false}
if err = db.Create(&disabled).Error; err != nil {
t.Fatal(err)
}
var stored ledger.RechargePlanPO
if err = db.First(&stored, disabled.ID).Error; err != nil {
t.Fatal(err)
}
if stored.Enabled {
t.Fatal("storage default enabled an explicitly disabled recharge plan")
}
repo := NewRepo(testProvider{db: db})
items, err := repo.Plans(context.Background())
if err != nil || len(items) != 0 {
t.Fatalf("public plans include a disabled plan: plans=%+v error=%v", items, err)
}
if _, err = repo.Create(context.Background(), 1, disabled.ID); !errors.Is(err, biz.ErrInvalid) {
t.Fatalf("disabled recharge plan accepted an order: error=%v", err)
}
var orders int64
if err = db.Model(&ledger.RechargeOrderPO{}).Count(&orders).Error; err != nil {
t.Fatal(err)
}
if orders != 0 {
t.Fatalf("disabled recharge plan created %d orders", orders)
}
}
func TestRechargeRepoSavePlansPreservesDomainEnabledValues(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&ledger.RechargePlanPO{}); err != nil {
t.Fatal(err)
}
repo := NewRepo(testProvider{db: db})
if err = repo.SavePlans(context.Background(), []*biz.Plan{
{Sort: 1, Diamonds: 60, AmountCents: 600, Enabled: false},
{Sort: 2, Diamonds: 300, AmountCents: 3000, Enabled: true},
}); err != nil {
t.Fatal(err)
}
var stored []ledger.RechargePlanPO
if err = db.Order("sort ASC").Find(&stored).Error; err != nil {
t.Fatal(err)
}
if len(stored) != 2 || stored[0].Enabled || !stored[1].Enabled {
t.Fatalf("saved plans lost explicit enabled values: %+v", stored)
}
items, err := repo.Plans(context.Background())
if err != nil || len(items) != 1 || items[0].Diamonds != 300 {
t.Fatalf("public plan filter returned disabled entries: plans=%+v error=%v", items, err)
}
}
func TestRechargeRepoConsecutiveOrdersHaveUniquePaymentSafeNumbers(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&ledger.RechargePlanPO{}, &ledger.RechargeOrderPO{}, &ledger.SettingPO{}); err != nil {
t.Fatal(err)
}
for _, statement := range []string{
`CREATE TABLE sys_users (id integer primary key, username text, nick_name text)`,
`CREATE TABLE pay_orders (trade_no text, business_type text, business_id text, provider text, provider_trade_no text, payment_status text, fulfillment_status text, refund_status text)`,
`INSERT INTO sys_users(id,username,nick_name) VALUES (3,'buyer','Buyer')`,
} {
if err = db.Exec(statement).Error; err != nil {
t.Fatal(err)
}
}
if err = db.Create(&ledger.SettingPO{Key: "diamond_rate_cents", Value: "10"}).Error; err != nil {
t.Fatal(err)
}
plan := ledger.RechargePlanPO{Sort: 1, Diamonds: 60, AmountCents: 600, Enabled: true}
if err = db.Create(&plan).Error; err != nil {
t.Fatal(err)
}
repo := NewRepo(testProvider{db: db})
pattern := regexp.MustCompile(`^CZ[A-Z2-7]{26}$`)
const count = 100
seen := make(map[string]struct{}, count)
for index := 0; index < count; index++ {
order, err := repo.Create(context.Background(), 3, plan.ID)
if err != nil {
t.Fatalf("order %d failed: %v", index, err)
}
if !pattern.MatchString(order.OrderNo) || len(order.OrderNo) > 32 {
t.Fatalf("invalid recharge order number: %q", order.OrderNo)
}
if _, exists := seen[order.OrderNo]; exists {
t.Fatalf("duplicate recharge order number: %q", order.OrderNo)
}
seen[order.OrderNo] = struct{}{}
}
var orders int64
if err = db.Model(&ledger.RechargeOrderPO{}).Count(&orders).Error; err != nil {
t.Fatal(err)
}
if orders != count {
t.Fatalf("created orders=%d, want %d", orders, count)
}
}