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) } }