271 lines
9.8 KiB
Go
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)
|
|
}
|
|
}
|