feat: add Airwallex payments and multi-currency support
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user