feat(server/pay): 优惠套餐(Promo SKU)每账号限购一次——已付过再下单 409 PROMO_ALREADY_USED
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 23s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 17s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 18s
ci-pangolin / Lint — shellcheck (pull_request) Successful in 6s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 35s
ci-pangolin / Flutter — analyze + test (pull_request) Successful in 32s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 3s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 4s
ci-pangolin / Go — build + test (pull_request) Failing after 10s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 9s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Failing after 4m40s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Failing after 21s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (pull_request) Successful in 23s
ci-pangolin / Cleartext Scan — Android 禁明文 (pull_request) Successful in 17s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (pull_request) Successful in 18s
ci-pangolin / Lint — shellcheck (pull_request) Successful in 6s
ci-pangolin / OpenAPI Sync Check (pull_request) Successful in 35s
ci-pangolin / Flutter — analyze + test (pull_request) Successful in 32s
ci-pangolin / Codegen Drift — token 生成物未漂移 (pull_request) Successful in 3s
ci-pangolin / DS-flow — 原型/跨端同源/代码色单源闸 (pull_request) Successful in 4s
ci-pangolin / Go — build + test (pull_request) Failing after 10s
ci-pangolin / E2E Smoke — L4 进程级端到端 (pull_request) Failing after 9s
ci-pangolin / Go — integration (mysql/redis testcontainers) (pull_request) Failing after 4m40s
ci-pangolin / Golden — 视觉回归 (全量:components/auth/desktop/tablet) (pull_request) Failing after 21s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9G7E3wmAYL9KeYCVZVsqu
This commit is contained in:
@@ -278,6 +278,84 @@ func TestGetOrder_EnrichedFields(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 优惠档(Promo SKU)限购一次:已有 paid 台账 → 下单 409 PROMO_ALREADY_USED,
|
||||
// 且不打上游(fake pay client 一旦被调用即 t.Fatal)。
|
||||
func TestCreateOrder_PromoAlreadyUsed409(t *testing.T) {
|
||||
router, st := newHandlerRig(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("已购过优惠档,不该打到 pay 上游")
|
||||
})
|
||||
if err := st.Insert(context.Background(), 1, "uuid-1", "pro_month_promo", "promo-old", "alipay", 600, "CNY"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := st.db.ExecContext(context.Background(),
|
||||
`UPDATE pay_purchases SET status = 'paid' WHERE out_trade_no = 'promo-old'`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := []byte(`{"sku":"pro_month_promo","method":"alipay"}`)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, authed(httptest.NewRequest(http.MethodPost, "/v1/pay/orders", bytes.NewReader(body)), 1))
|
||||
if w.Code != http.StatusConflict {
|
||||
t.Fatalf("code = %d, body = %s", w.Code, w.Body)
|
||||
}
|
||||
var e struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &e); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if e.Code != "PROMO_ALREADY_USED" {
|
||||
t.Fatalf("code = %q, want PROMO_ALREADY_USED", e.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// 优惠档历史只有 created/canceled(未 paid)→ 不受限,正常放行下单。
|
||||
func TestCreateOrder_PromoNotYetPaidAllowed(t *testing.T) {
|
||||
router, st := newHandlerRig(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"data":{"order_no":"promo-new","session":{
|
||||
"render_type":"redirect","payload":{"url":"https://alipay.example/x"}}}}`))
|
||||
})
|
||||
if err := st.Insert(context.Background(), 1, "uuid-1", "pro_month_promo", "promo-created", "alipay", 600, "CNY"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := st.MarkCanceled(context.Background(), 1, "promo-created"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := []byte(`{"sku":"pro_month_promo","method":"alipay"}`)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, authed(httptest.NewRequest(http.MethodPost, "/v1/pay/orders", bytes.NewReader(body)), 1))
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("code = %d, body = %s", w.Code, w.Body)
|
||||
}
|
||||
row, err := st.GetForUser(context.Background(), 1, "promo-new")
|
||||
if err != nil {
|
||||
t.Fatalf("台账未落: %v", err)
|
||||
}
|
||||
if row.SKU != "pro_month_promo" || row.Status != "created" {
|
||||
t.Fatalf("台账行不符: %+v", row)
|
||||
}
|
||||
}
|
||||
|
||||
// 非 promo SKU 即使已 paid 也不受限购约束(限购只针对 Promo==true 的 SKU)。
|
||||
func TestCreateOrder_NonPromoSKUUnlimited(t *testing.T) {
|
||||
router, st := newHandlerRig(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"data":{"order_no":"month-2","session":{
|
||||
"render_type":"redirect","payload":{"url":"https://alipay.example/x"}}}}`))
|
||||
})
|
||||
if err := st.Insert(context.Background(), 1, "uuid-1", "pro_month", "month-1", "alipay", 2999, "CNY"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := st.db.ExecContext(context.Background(),
|
||||
`UPDATE pay_purchases SET status = 'paid' WHERE out_trade_no = 'month-1'`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := []byte(`{"sku":"pro_month","method":"alipay"}`)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, authed(httptest.NewRequest(http.MethodPost, "/v1/pay/orders", bytes.NewReader(body)), 1))
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("code = %d, body = %s", w.Code, w.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatalog(t *testing.T) {
|
||||
router, _ := newHandlerRig(t, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
Reference in New Issue
Block a user