This commit is contained in:
Yvan
2026-09-22 20:20:23 +08:00
parent bf33e87d72
commit ceaea78b0f
42 changed files with 2845 additions and 154 deletions
+7 -7
View File
@@ -3,7 +3,6 @@ package recharge
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
@@ -13,6 +12,7 @@ import (
biz "kra/internal/biz/recharge"
"kra/internal/data/ledger"
"kra/internal/utils/serial"
)
type Provider interface{ DB() *gorm.DB }
@@ -39,7 +39,7 @@ func (r *repo) SavePlans(ctx context.Context, plans []*biz.Plan) error {
return err
}
for _, p := range plans {
if err := tx.Create(&ledger.RechargePlanPO{Sort: p.Sort, Diamonds: p.Diamonds, AmountCents: p.AmountCents, Enabled: true}).Error; err != nil {
if err := tx.Create(&ledger.RechargePlanPO{Sort: p.Sort, Diamonds: p.Diamonds, AmountCents: p.AmountCents, Enabled: p.Enabled}).Error; err != nil {
return err
}
}
@@ -127,7 +127,11 @@ func (r *repo) Create(ctx context.Context, userID, planID uint) (*biz.Order, err
rate = v
}
}
v := ledger.RechargeOrderPO{OrderNo: number("CZ"), UserID: userID, Diamonds: plan.Diamonds, AmountCents: plan.AmountCents, RateCents: rate}
orderNo, err := serial.New("CZ")
if err != nil {
return nil, err
}
v := ledger.RechargeOrderPO{OrderNo: orderNo, UserID: userID, Diamonds: plan.Diamonds, AmountCents: plan.AmountCents, RateCents: rate}
if err := r.db(ctx).Create(&v).Error; err != nil {
return nil, err
}
@@ -185,7 +189,3 @@ func orderBy(v string) string {
}
return "live_recharge_orders.created_at DESC"
}
func number(p string) string {
return fmt.Sprintf("%s%s%06d", p, time.Now().Format("20060102150405"), time.Now().UnixNano()%1000000)
}
+173
View File
@@ -2,6 +2,8 @@ package recharge
import (
"context"
"errors"
"regexp"
"testing"
"time"
@@ -95,3 +97,174 @@ func TestListReadsPaymentStateFromKRAOrders(t *testing.T) {
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)
}
}