feat(v2): order 补 biz_code + webhook payload 带 product_biz_code(收口 P2 延后项)
This commit is contained in:
@@ -72,7 +72,7 @@ type OrderResult struct {
|
|||||||
// account, persists a pending Order + Attempt (P1 OrderStore), and returns the
|
// account, persists a pending Order + Attempt (P1 OrderStore), and returns the
|
||||||
// payment session {render_type, payload}. 加渠道不改 client(设计 §4.2)。
|
// payment session {render_type, payload}. 加渠道不改 client(设计 §4.2)。
|
||||||
func (g *Gateway) CreateOrder(ctx context.Context, in CreateOrderInput) (*OrderResult, error) {
|
func (g *Gateway) CreateOrder(ctx context.Context, in CreateOrderInput) (*OrderResult, error) {
|
||||||
amountMinor, currency, subject, _, err := g.products.Resolve(in.SKU)
|
amountMinor, currency, subject, bizCode, err := g.products.Resolve(in.SKU)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err // ErrProductNotFound
|
return nil, err // ErrProductNotFound
|
||||||
}
|
}
|
||||||
@@ -93,7 +93,7 @@ func (g *Gateway) CreateOrder(ctx context.Context, in CreateOrderInput) (*OrderR
|
|||||||
|
|
||||||
if err := g.orders.CreateOrder(&model.OrderV2{
|
if err := g.orders.CreateOrder(&model.OrderV2{
|
||||||
OutTradeNo: outNo, BizSystem: in.BizSystem, BizRef: in.BizRef,
|
OutTradeNo: outNo, BizSystem: in.BizSystem, BizRef: in.BizRef,
|
||||||
Subject: subject, AmountMinor: amountMinor, Currency: currency,
|
BizCode: bizCode, Subject: subject, AmountMinor: amountMinor, Currency: currency,
|
||||||
Status: model.OrderPendingV2,
|
Status: model.OrderPendingV2,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -81,6 +81,23 @@ func TestCreateOrderPipeline(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateOrderPersistsBizCode(t *testing.T) {
|
||||||
|
g, _, _, orders := newGateway(t)
|
||||||
|
res, err := g.CreateOrder(context.Background(), gateway.CreateOrderInput{
|
||||||
|
SKU: "pro_year", Method: "fake", BizSystem: "pangolin", BizRef: "u-1",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create: %v", err)
|
||||||
|
}
|
||||||
|
o, err := orders.GetOrder(res.OrderNo)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get order: %v", err)
|
||||||
|
}
|
||||||
|
if o.BizCode != "pro_year" { // stubResolver 的 bizCode
|
||||||
|
t.Fatalf("order.BizCode = %q, want pro_year", o.BizCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateOrderErrors(t *testing.T) {
|
func TestCreateOrderErrors(t *testing.T) {
|
||||||
g, _, _, _ := newGateway(t)
|
g, _, _, _ := newGateway(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|||||||
@@ -82,14 +82,15 @@ func (g *Gateway) enqueuePaymentSucceeded(att *model.Attempt, paidAt time.Time)
|
|||||||
return nil // 独立收款无业务方回调;或已翻转/已取消
|
return nil // 独立收款无业务方回调;或已翻转/已取消
|
||||||
}
|
}
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
"event_type": "payment.succeeded",
|
"event_type": "payment.succeeded",
|
||||||
"out_trade_no": o.OutTradeNo,
|
"out_trade_no": o.OutTradeNo,
|
||||||
"biz_system": o.BizSystem,
|
"biz_system": o.BizSystem,
|
||||||
"biz_ref": o.BizRef,
|
"biz_ref": o.BizRef,
|
||||||
"amount_minor": o.AmountMinor,
|
"product_biz_code": o.BizCode, // 设计 §5:业务方按套餐码映射权益(时长/档位),不硬编码 product_id
|
||||||
"currency": o.Currency,
|
"amount_minor": o.AmountMinor,
|
||||||
"channel": att.Channel,
|
"currency": o.Currency,
|
||||||
"paid_at": paidAt.Format(time.RFC3339),
|
"channel": att.Channel,
|
||||||
|
"paid_at": paidAt.Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
return g.webhook.Enqueue(o.OutTradeNo, o.BizSystem, "payment.succeeded", data)
|
return g.webhook.Enqueue(o.OutTradeNo, o.BizSystem, "payment.succeeded", data)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,29 @@ func TestSettleHappyIdempotentAndWebhook(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSettleWebhookCarriesProductBizCode(t *testing.T) {
|
||||||
|
g, fp, spy, orders := newGateway(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
res, _ := g.CreateOrder(ctx, gateway.CreateOrderInput{
|
||||||
|
SKU: "pro_year", Method: "fake", BizSystem: "pangolin", BizRef: "u-9",
|
||||||
|
})
|
||||||
|
ref := attemptRef(t, orders)
|
||||||
|
fp.SetQueryResult(ref, provider.PaidEvent{
|
||||||
|
ProviderRef: ref, Status: provider.PaidSucceeded,
|
||||||
|
PaidAmountMinor: 29990000, PaidCurrency: "USDT",
|
||||||
|
})
|
||||||
|
if _, err := g.SyncPendingAttempts(ctx, 10); err != nil {
|
||||||
|
t.Fatalf("sync: %v", err)
|
||||||
|
}
|
||||||
|
if len(spy.calls) != 1 {
|
||||||
|
t.Fatalf("want 1 webhook, got %d", len(spy.calls))
|
||||||
|
}
|
||||||
|
if spy.calls[0]["product_biz_code"] != "pro_year" {
|
||||||
|
t.Fatalf("payload product_biz_code = %v, want pro_year", spy.calls[0]["product_biz_code"])
|
||||||
|
}
|
||||||
|
_ = res
|
||||||
|
}
|
||||||
|
|
||||||
func TestSettleGuards(t *testing.T) {
|
func TestSettleGuards(t *testing.T) {
|
||||||
g, _, spy, orders := newGateway(t)
|
g, _, spy, orders := newGateway(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ type OrderV2 struct {
|
|||||||
BizSystem string `gorm:"index;size:32"` // 接入方(pangolin/jiu),空=独立收款
|
BizSystem string `gorm:"index;size:32"` // 接入方(pangolin/jiu),空=独立收款
|
||||||
BizRef string `gorm:"size:128"` // 接入方业务单号,回调原样带回
|
BizRef string `gorm:"size:128"` // 接入方业务单号,回调原样带回
|
||||||
ProductID uint64 `gorm:"index"`
|
ProductID uint64 `gorm:"index"`
|
||||||
|
BizCode string `gorm:"index;size:64"` // 套餐码副本(下单时从 product 复制),回调带回业务方按码映射权益(设计 §5)
|
||||||
Subject string `gorm:"size:128"`
|
Subject string `gorm:"size:128"`
|
||||||
// 金额:int64 最小单位 + 币种(presentment/charge/settlement 三态在 charge 落地此处)
|
// 金额:int64 最小单位 + 币种(presentment/charge/settlement 三态在 charge 落地此处)
|
||||||
AmountMinor int64 `gorm:"not null"`
|
AmountMinor int64 `gorm:"not null"`
|
||||||
@@ -74,7 +75,7 @@ type Attempt struct {
|
|||||||
AmountMinor int64 `gorm:"not null"`
|
AmountMinor int64 `gorm:"not null"`
|
||||||
Currency string `gorm:"size:16;not null"`
|
Currency string `gorm:"size:16;not null"`
|
||||||
Status AttemptStatus `gorm:"index;size:16;not null"`
|
Status AttemptStatus `gorm:"index;size:16;not null"`
|
||||||
ExpiresAt *time.Time // 本次尝试超时
|
ExpiresAt *time.Time // 本次尝试超时
|
||||||
PaidAt *time.Time
|
PaidAt *time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user