对接
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user