feat(server/pay): 订单列表端点 + 分币种展示价 + 富字段详情
- GET /v1/pay/orders 订单列表(Store.ListByUser,本地台账倒序) - catalog 增 PriceUsdtMicro(USDT 一口价展示) + SettlementCurrency/DisplayAmountMinor - 下单按支付方式落展示金额/结算币种(crypto→USDT / 法币→CNY),webhook 覆盖实际 - GetOrder 详情回带 sku/plan/method/amount/currency/created_at/paid_at - 新增 List 越权/倒序/富字段 + 分币种展示金额 + 详情富字段测试 - 修 3 个 pre-existing 迁移测试(renumber 后 21→23 遗留) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
@@ -27,6 +27,7 @@ func newHandlerRig(t *testing.T, payFn http.HandlerFunc) (*chi.Mux, *Store) {
|
||||
h := NewHandler(NewClient(srv.URL, "pangolin", testSecret), st, db)
|
||||
r := chi.NewRouter()
|
||||
r.Get("/v1/pay/catalog", h.Catalog)
|
||||
r.Get("/v1/pay/orders", h.List)
|
||||
r.Post("/v1/pay/orders", h.CreateOrder)
|
||||
r.Get("/v1/pay/orders/{orderNo}", h.GetOrder)
|
||||
r.Post("/v1/pay/orders/{orderNo}/retry", h.Retry)
|
||||
@@ -86,7 +87,7 @@ func TestGetOrder_OwnershipEnforced(t *testing.T) {
|
||||
_, _ = w.Write([]byte(`{"data":{"order_no":"pay001","status":"pending",
|
||||
"subject":"s","amount_minor":2999,"currency":"CNY"}}`))
|
||||
})
|
||||
if err := st.Insert(context.Background(), 1, "uuid-1", "pro_month", "pay001", "alipay"); err != nil {
|
||||
if err := st.Insert(context.Background(), 1, "uuid-1", "pro_month", "pay001", "alipay", 2999, "CNY"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 属主可查
|
||||
@@ -116,7 +117,7 @@ func TestRetry_CurrencyMismatchMapped409(t *testing.T) {
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
_, _ = w.Write([]byte(`{"code":"currency_mismatch","message":"换渠道需新单"}`))
|
||||
})
|
||||
_ = st.Insert(context.Background(), 1, "uuid-1", "pro_month", "pay001", "crypto")
|
||||
_ = st.Insert(context.Background(), 1, "uuid-1", "pro_month", "pay001", "crypto", 4990000, "USDT")
|
||||
body := []byte(`{"method":"alipay"}`)
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, authed(httptest.NewRequest(http.MethodPost, "/v1/pay/orders/pay001/retry", bytes.NewReader(body)), 1))
|
||||
@@ -132,6 +133,118 @@ func TestRetry_CurrencyMismatchMapped409(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 下单时按支付方式落「展示金额 + 结算币种」(crypto→USDT 微元 / 法币→CNY 分)。
|
||||
// 单 rig(rig 内 DB 复用),mock 按转发来的 method 回不同 order_no。
|
||||
func TestCreateOrder_StoresDisplayAmountByMethod(t *testing.T) {
|
||||
router, st := newHandlerRig(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
var fwd struct {
|
||||
Method string `json:"method"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&fwd)
|
||||
_, _ = w.Write([]byte(`{"data":{"order_no":"pay-` + fwd.Method + `","session":{
|
||||
"render_type":"redirect","payload":{"url":"https://x"}}}}`))
|
||||
})
|
||||
cases := []struct {
|
||||
method string
|
||||
wantAmt int64
|
||||
wantCurr string
|
||||
}{
|
||||
{"crypto", 4990000, "USDT"}, // pro_month USDT 微元
|
||||
{"alipay", 2999, "CNY"}, // pro_month CNY 分
|
||||
}
|
||||
for _, tc := range cases {
|
||||
body := []byte(`{"sku":"pro_month","method":"` + tc.method + `"}`)
|
||||
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("%s: code = %d, body = %s", tc.method, w.Code, w.Body)
|
||||
}
|
||||
row, err := st.GetForUser(context.Background(), 1, "pay-"+tc.method)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: 台账未落: %v", tc.method, err)
|
||||
}
|
||||
if row.AmountMinor != tc.wantAmt || row.Currency != tc.wantCurr {
|
||||
t.Fatalf("%s: amount=%d curr=%q, want %d/%q", tc.method, row.AmountMinor, row.Currency, tc.wantAmt, tc.wantCurr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 订单列表:只列本人、倒序、含台账富字段。
|
||||
func TestList_OwnershipAndFields(t *testing.T) {
|
||||
router, st := newHandlerRig(t, nil)
|
||||
ctx := context.Background()
|
||||
// user 1 两单(先月后年),user 2 一单(不该出现在 user 1 列表)
|
||||
if err := st.Insert(ctx, 1, "uuid-1", "pro_month", "o1", "alipay", 2999, "CNY"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := st.Insert(ctx, 1, "uuid-1", "pro_year", "o2", "crypto", 34990000, "USDT"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := st.Insert(ctx, 2, "uuid-2", "pro_month", "o3", "alipay", 2999, "CNY"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, authed(httptest.NewRequest(http.MethodGet, "/v1/pay/orders", nil), 1))
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("code = %d, body = %s", w.Code, w.Body)
|
||||
}
|
||||
var resp struct {
|
||||
Orders []orderView `json:"orders"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(resp.Orders) != 2 {
|
||||
t.Fatalf("want 2 orders for user 1, got %d: %+v", len(resp.Orders), resp.Orders)
|
||||
}
|
||||
for _, o := range resp.Orders {
|
||||
if o.OrderNo == "o3" {
|
||||
t.Fatalf("越权:看到了他人订单 o3")
|
||||
}
|
||||
}
|
||||
// 倒序:o2(pro_year, 后插)在前,且富字段完整
|
||||
first := resp.Orders[0]
|
||||
if first.OrderNo != "o2" || first.SKU != "pro_year" || first.Plan != "pro" ||
|
||||
first.Method != "crypto" || first.AmountMinor != 34990000 || first.Currency != "USDT" ||
|
||||
first.Days != 366 || first.Status != "created" || first.CreatedAt == "" {
|
||||
t.Fatalf("首单富字段不符: %+v", first)
|
||||
}
|
||||
}
|
||||
|
||||
// 订单详情:回带台账富字段 + pay_status/activated。
|
||||
func TestGetOrder_EnrichedFields(t *testing.T) {
|
||||
router, st := newHandlerRig(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"data":{"order_no":"pay001","status":"pending","currency":"USDT"}}`))
|
||||
})
|
||||
if err := st.Insert(context.Background(), 1, "uuid-1", "pro_year", "pay001", "crypto", 34990000, "USDT"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
router.ServeHTTP(w, authed(httptest.NewRequest(http.MethodGet, "/v1/pay/orders/pay001", nil), 1))
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("code = %d", w.Code)
|
||||
}
|
||||
var resp struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
SKU string `json:"sku"`
|
||||
Plan string `json:"plan"`
|
||||
Method string `json:"method"`
|
||||
AmountMinor int64 `json:"amount_minor"`
|
||||
Currency string `json:"currency"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
PayStatus string `json:"pay_status"`
|
||||
Activated bool `json:"activated"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp.SKU != "pro_year" || resp.Plan != "pro" || resp.Method != "crypto" ||
|
||||
resp.AmountMinor != 34990000 || resp.Currency != "USDT" || resp.CreatedAt == "" ||
|
||||
resp.PayStatus != "pending" || resp.Activated {
|
||||
t.Fatalf("详情富字段不符: %+v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCatalog(t *testing.T) {
|
||||
router, _ := newHandlerRig(t, nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
Reference in New Issue
Block a user