Files
sundynix-agentix/sundynix-gateway/internal/store/payment_test.go
T
Blizzard 8e05f4b3fe feat(billing): 支付 P5.1 —— 订单骨架 + 兑换码渠道全闭环
按 PAYMENT_DESIGN.md 施工。零资质渠道先把「订单→入账→对账」的骨架跑真,
微信 Native(P5.2)进来只是多一个 adapter。

- store/payment.go:CreditPack(admin 可配包)/PaymentOrder(支付侧事实源,兑换
  也写单,全部充值一个查法)/RedeemCode(SDX-XXXX-XXXX-XXXX,crypto/rand,剔除
  易混字符)。三模型都不标 isTenantScoped——订单归计费租户,与活跃租户可能
  分叉,插件自动注入会写错归属(RecentRuns 同款教训),显式赋值+显式过滤。
- Redeem 单事务:码 CAS 占用(unused→used 只成功一次,幂等主闸)→建已支付
  订单→账本分录+物化余额。credit_ledger 加 (kind,ref) 部分唯一索引兜底
  ——GrantCredits 此前对 ref 零约束,回调 at-least-once 就是重复入账事故。
- 路由:/billing/packs|orders(查,全员) + /billing/redeem(≥member+审计);
  admin /redeem-codes(生成/列表) + /packs(配包)。
- Web 面账单页:兑换码输入(viewer 不摆输入框,真闸在后端)+最近充值订单流。
- 测试:码形态/200 样本无撞码/参数边界;go+tsc+vitest 全绿。
  live 13 项:生成→核销余额精确+100→重core 400(主闸)→DB 直插重复 ref 被唯一
  索引打回(兜底闸实证)→viewer 403→balance==SUM(ledger) 不变量→审计留痕→
  浏览器 UI 兑换 100→200+订单流展示。

已知余项:admin 生成码暂只有 API(admin 页 UI 下一刀);微信 adapter=P5.2。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 10:18:14 +08:00

40 lines
1.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package store
import (
"context"
"regexp"
"testing"
)
// 兑换码:固定形态 SDX-XXXX-XXXX-XXXX,字母表剔除易混字符(0/O、1/I/L)。
func TestNewRedeemCodeFormat(t *testing.T) {
re := regexp.MustCompile(`^SDX(-[ABCDEFGHJKMNPQRSTUVWXYZ23456789]{4}){3}$`)
seen := map[string]bool{}
for i := 0; i < 200; i++ {
code, err := newRedeemCode()
if err != nil {
t.Fatalf("生成失败: %v", err)
}
if !re.MatchString(code) {
t.Fatalf("形态不符: %q", code)
}
if seen[code] {
t.Fatalf("200 个样本内撞码: %qcrypto/rand 出问题了)", code)
}
seen[code] = true
}
}
// 参数校验在 nil-db 之前生效——admin 手滑发 0 面额/超量不该打到数据库才被拒。
func TestGenerateRedeemCodesBounds(t *testing.T) {
p := &Postgres{} // db==nil
for _, tc := range []struct {
n int
credit int64
}{{0, 1e6}, {201, 1e6}, {1, 0}, {1, -5}} {
if _, err := p.GenerateRedeemCodes(context.Background(), tc.n, tc.credit, ""); err == nil {
t.Errorf("n=%d credit=%d 应被拒", tc.n, tc.credit)
}
}
}