feat: add Airwallex payments and multi-currency support

This commit is contained in:
shaw
2026-05-11 11:17:26 +08:00
parent dbc8ae658c
commit b23055af5b
65 changed files with 3164 additions and 162 deletions
+84 -7
View File
@@ -57,8 +57,17 @@ func (s *PaymentService) CreateOrder(ctx context.Context, req CreateOrderRequest
orderAmount = calculateCreditedBalance(req.Amount, cfg.BalanceRechargeMultiplier)
}
feeRate := cfg.RechargeFeeRate
payAmountStr := payment.CalculatePayAmount(limitAmount, feeRate)
payAmount, _ := strconv.ParseFloat(payAmountStr, 64)
methodCurrency := payment.DefaultPaymentCurrency
if s.configService != nil {
methodCurrency, err = s.configService.ValidateMethodCurrencyConsistency(ctx, req.PaymentType)
if err != nil {
return nil, err
}
}
payAmountStr, payAmount, err := calculateCreateOrderPayAmount(limitAmount, feeRate, methodCurrency)
if err != nil {
return nil, err
}
sel, err := s.selectCreateOrderInstance(ctx, req, cfg, payAmount)
if err != nil {
return nil, err
@@ -66,6 +75,19 @@ func (s *PaymentService) CreateOrder(ctx context.Context, req CreateOrderRequest
if err := s.validateSelectedCreateOrderInstance(ctx, req, sel); err != nil {
return nil, err
}
selectedCurrency := payment.DefaultPaymentCurrency
if sel != nil {
selectedCurrency = paymentProviderConfigCurrency(sel.ProviderKey, sel.Config)
}
if selectedCurrency != methodCurrency {
payAmountStr, payAmount, err = calculateCreateOrderPayAmount(limitAmount, feeRate, selectedCurrency)
if err != nil {
return nil, err
}
}
if err := validateSelectedCreateOrderAmountCurrency(payAmountStr, sel); err != nil {
return nil, err
}
oauthResp, err := s.maybeBuildWeChatOAuthRequiredResponseForSelection(ctx, req, limitAmount, payAmount, feeRate, sel)
if err != nil {
return nil, err
@@ -257,7 +279,7 @@ func buildPaymentOrderProviderSnapshot(sel *payment.InstanceSelection, req Creat
if merchantID := strings.TrimSpace(sel.Config["mchId"]); merchantID != "" {
snapshot["merchant_id"] = merchantID
}
snapshot["currency"] = "CNY"
snapshot["currency"] = payment.DefaultPaymentCurrency
}
if providerKey == payment.TypeAlipay {
if merchantAppID := strings.TrimSpace(sel.Config["appId"]); merchantAppID != "" {
@@ -269,6 +291,15 @@ func buildPaymentOrderProviderSnapshot(sel *payment.InstanceSelection, req Creat
snapshot["merchant_id"] = merchantID
}
}
if providerKey == payment.TypeStripe {
snapshot["currency"] = paymentProviderConfigCurrency(providerKey, sel.Config)
}
if providerKey == payment.TypeAirwallex {
if accountID := strings.TrimSpace(sel.Config["accountId"]); accountID != "" {
snapshot["merchant_id"] = accountID
}
snapshot["currency"] = paymentProviderConfigCurrency(providerKey, sel.Config)
}
if len(snapshot) == 1 {
return nil
@@ -377,7 +408,7 @@ func (s *PaymentService) invokeProvider(ctx context.Context, order *dbent.Paymen
return nil, infraerrors.ServiceUnavailable("PAYMENT_PROVIDER_MISCONFIGURED", "provider_misconfigured").
WithMetadata(map[string]string{"provider": sel.ProviderKey, "instance_id": sel.InstanceID})
}
subject := s.buildPaymentSubject(plan, limitAmount, cfg)
subject := s.buildPaymentSubject(plan, limitAmount, cfg, sel)
outTradeNo := order.OutTradeNo
canonicalReturnURL, err := CanonicalizeReturnURL(req.ReturnURL, req.SrcHost, req.SrcURL)
if err != nil {
@@ -466,20 +497,24 @@ func selectedInstanceSupportedTypes(sel *payment.InstanceSelection) string {
return sel.SupportedTypes
}
func (s *PaymentService) buildPaymentSubject(plan *dbent.SubscriptionPlan, limitAmount float64, cfg *PaymentConfig) string {
func (s *PaymentService) buildPaymentSubject(plan *dbent.SubscriptionPlan, limitAmount float64, cfg *PaymentConfig, sel *payment.InstanceSelection) string {
if plan != nil {
if plan.ProductName != "" {
return plan.ProductName
}
return "Sub2API Subscription " + plan.Name
}
amountStr := strconv.FormatFloat(limitAmount, 'f', 2, 64)
currency := payment.DefaultPaymentCurrency
if sel != nil {
currency = paymentProviderConfigCurrency(sel.ProviderKey, sel.Config)
}
amountStr := payment.FormatAmountForCurrency(limitAmount, currency)
pf := strings.TrimSpace(cfg.ProductNamePrefix)
sf := strings.TrimSpace(cfg.ProductNameSuffix)
if pf != "" || sf != "" {
return strings.TrimSpace(pf + " " + amountStr + " " + sf)
}
return "Sub2API " + amountStr + " CNY"
return "Sub2API " + amountStr + " " + currency
}
func (s *PaymentService) maybeBuildWeChatOAuthRequiredResponse(ctx context.Context, req CreateOrderRequest, amount, payAmount, feeRate float64) (*CreateOrderResponse, error) {
@@ -540,6 +575,44 @@ func (s *PaymentService) validateSelectedCreateOrderInstance(ctx context.Context
return nil
}
func calculateCreateOrderPayAmount(limitAmount, feeRate float64, currency string) (string, float64, error) {
if err := validateCreateOrderAmountCurrency(limitAmount, currency); err != nil {
return "", 0, err
}
payAmountStr := payment.CalculatePayAmountForCurrency(limitAmount, feeRate, currency)
if _, err := payment.AmountToMinorUnit(payAmountStr, currency); err != nil {
return "", 0, infraerrors.BadRequest("INVALID_AMOUNT", err.Error()).
WithMetadata(map[string]string{"currency": currency})
}
payAmount, err := strconv.ParseFloat(payAmountStr, 64)
if err != nil {
return "", 0, infraerrors.BadRequest("INVALID_AMOUNT", "invalid payment amount").
WithMetadata(map[string]string{"currency": currency})
}
return payAmountStr, payAmount, nil
}
func validateCreateOrderAmountCurrency(amount float64, currency string) error {
amountStr := strconv.FormatFloat(amount, 'f', -1, 64)
if _, err := payment.AmountToMinorUnit(amountStr, currency); err != nil {
return infraerrors.BadRequest("INVALID_AMOUNT", err.Error()).
WithMetadata(map[string]string{"currency": currency})
}
return nil
}
func validateSelectedCreateOrderAmountCurrency(payAmount string, sel *payment.InstanceSelection) error {
if sel == nil {
return nil
}
currency := paymentProviderConfigCurrency(sel.ProviderKey, sel.Config)
if _, err := payment.AmountToMinorUnit(payAmount, currency); err != nil {
return infraerrors.BadRequest("INVALID_AMOUNT", err.Error()).
WithMetadata(map[string]string{"currency": currency})
}
return nil
}
func requiresWeChatJSAPICompatibleSelection(req CreateOrderRequest, sel *payment.InstanceSelection) bool {
if sel == nil || sel.ProviderKey != payment.TypeWxpay || payment.GetBasePaymentType(req.PaymentType) != payment.TypeWxpay {
return false
@@ -596,6 +669,10 @@ func buildCreateOrderResponse(order *dbent.PaymentOrder, req CreateOrderRequest,
PayURL: pr.PayURL,
QRCode: pr.QRCode,
ClientSecret: pr.ClientSecret,
IntentID: pr.IntentID,
Currency: pr.Currency,
CountryCode: pr.CountryCode,
PaymentEnv: pr.PaymentEnv,
OAuth: pr.OAuth,
JSAPI: pr.JSAPI,
JSAPIPayload: pr.JSAPI,