feat(v2): 多币种 product(ProductPrice 分币价目 + v1 元回退)+ 结算币种由渠道能力驱动
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
+23
-10
@@ -18,15 +18,18 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrProductNotFound = errors.New("gateway: product not found")
|
||||
ErrNoAccount = errors.New("gateway: no enabled account for method/region")
|
||||
ErrOrderNotPending = errors.New("gateway: order not pending")
|
||||
ErrProductNotFound = errors.New("gateway: product not found")
|
||||
ErrNoAccount = errors.New("gateway: no enabled account for method/region")
|
||||
ErrOrderNotPending = errors.New("gateway: order not pending")
|
||||
ErrNoSettleCurrency = errors.New("gateway: channel has no settle currency")
|
||||
ErrCurrencyMismatch = errors.New("gateway: retry method settles a different currency")
|
||||
)
|
||||
|
||||
// ProductResolver maps a client-facing SKU to the authoritative amount/currency.
|
||||
// Amount authority lives in pay (设计 §3.1); the client never sends raw amounts.
|
||||
// ProductResolver maps a client SKU + settlement currency to authoritative amount.
|
||||
// Currency is chosen by the selected channel's SettleCurrencies (设计 §3.1/§4.1),
|
||||
// never sent by the client.
|
||||
type ProductResolver interface {
|
||||
Resolve(sku string) (amountMinor int64, currency, subject, bizCode string, err error)
|
||||
Resolve(sku, currency string) (amountMinor int64, subject, bizCode string, err error)
|
||||
}
|
||||
|
||||
// WebhookEnqueuer receives a domain payload to deliver to the business system.
|
||||
@@ -72,14 +75,19 @@ type OrderResult struct {
|
||||
// account, persists a pending Order + Attempt (P1 OrderStore), and returns the
|
||||
// payment session {render_type, payload}. 加渠道不改 client(设计 §4.2)。
|
||||
func (g *Gateway) CreateOrder(ctx context.Context, in CreateOrderInput) (*OrderResult, error) {
|
||||
amountMinor, currency, subject, bizCode, err := g.products.Resolve(in.SKU)
|
||||
if err != nil {
|
||||
return nil, err // ErrProductNotFound
|
||||
}
|
||||
prov, err := g.providers.Get(in.Method)
|
||||
if err != nil {
|
||||
return nil, err // ErrUnknownMethod
|
||||
}
|
||||
caps := prov.Capabilities()
|
||||
if len(caps.SettleCurrencies) == 0 {
|
||||
return nil, ErrNoSettleCurrency
|
||||
}
|
||||
currency := caps.SettleCurrencies[0] // 结算币种由渠道自述能力驱动(设计 §4.1)
|
||||
amountMinor, subject, bizCode, err := g.products.Resolve(in.SKU, currency)
|
||||
if err != nil {
|
||||
return nil, err // ErrProductNotFound(含"该币种无价")
|
||||
}
|
||||
outNo := util.NewOutTradeNo("pay")
|
||||
acct, err := g.picker.Pick(in.Method, g.region, accounts.PickHint{
|
||||
OutTradeNo: outNo, AmountMinor: amountMinor,
|
||||
@@ -154,6 +162,11 @@ func (g *Gateway) RetryOrder(ctx context.Context, outTradeNo, method string) (*O
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
caps := prov.Capabilities()
|
||||
if len(caps.SettleCurrencies) == 0 || caps.SettleCurrencies[0] != o.Currency {
|
||||
// 换到结算币种不同的渠道重试 = 需重定价,超出 P3 范围(P5 多币种路由)。
|
||||
return nil, ErrCurrencyMismatch
|
||||
}
|
||||
tried, err := g.orders.AttemptAccountIDs(outTradeNo, method)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in New Issue
Block a user