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
+57 -11
View File
@@ -14,7 +14,6 @@ import (
// Stripe constants.
const (
stripeCurrency = "cny"
stripeEventPaymentSuccess = "payment_intent.succeeded"
stripeEventPaymentFailed = "payment_intent.payment_failed"
)
@@ -34,9 +33,15 @@ func NewStripe(instanceID string, config map[string]string) (*Stripe, error) {
if config["secretKey"] == "" {
return nil, fmt.Errorf("stripe config missing required key: secretKey")
}
cfg := cloneStringMap(config)
currency, err := payment.NormalizePaymentCurrency(cfg["currency"])
if err != nil {
return nil, fmt.Errorf("stripe config currency: %w", err)
}
cfg["currency"] = currency
return &Stripe{
instanceID: instanceID,
config: config,
config: cfg,
}, nil
}
@@ -60,6 +65,24 @@ func (s *Stripe) SupportedTypes() []payment.PaymentType {
return []payment.PaymentType{payment.TypeStripe}
}
func (s *Stripe) MerchantIdentityMetadata() map[string]string {
if s == nil {
return nil
}
return map[string]string{"currency": s.currency()}
}
func (s *Stripe) currency() string {
if s == nil {
return payment.DefaultPaymentCurrency
}
currency, err := payment.NormalizePaymentCurrency(s.config["currency"])
if err != nil {
return payment.DefaultPaymentCurrency
}
return currency
}
// stripePaymentMethodTypes maps our PaymentType to Stripe payment_method_types.
var stripePaymentMethodTypes = map[string][]string{
payment.TypeCard: {"card"},
@@ -72,7 +95,8 @@ var stripePaymentMethodTypes = map[string][]string{
func (s *Stripe) CreatePayment(ctx context.Context, req payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) {
s.ensureInit()
amountInCents, err := payment.YuanToFen(req.Amount)
currency := s.currency()
amountInMinorUnit, err := payment.AmountToMinorUnit(req.Amount, currency)
if err != nil {
return nil, fmt.Errorf("stripe create payment: %w", err)
}
@@ -86,8 +110,8 @@ func (s *Stripe) CreatePayment(ctx context.Context, req payment.CreatePaymentReq
}
params := &stripe.PaymentIntentCreateParams{
Amount: stripe.Int64(amountInCents),
Currency: stripe.String(stripeCurrency),
Amount: stripe.Int64(amountInMinorUnit),
Currency: stripe.String(strings.ToLower(currency)),
PaymentMethodTypes: pmTypes,
Description: stripe.String(req.Subject),
Metadata: map[string]string{"orderId": req.OrderID},
@@ -113,6 +137,7 @@ func (s *Stripe) CreatePayment(ctx context.Context, req payment.CreatePaymentReq
return &payment.CreatePaymentResponse{
TradeNo: pi.ID,
ClientSecret: pi.ClientSecret,
Currency: currency,
}, nil
}
@@ -133,10 +158,14 @@ func (s *Stripe) QueryOrder(ctx context.Context, tradeNo string) (*payment.Query
status = payment.ProviderStatusFailed
}
currency := stripeIntentCurrency(pi.Currency, s.currency())
return &payment.QueryOrderResponse{
TradeNo: pi.ID,
Status: status,
Amount: payment.FenToYuan(pi.Amount),
Amount: payment.MinorUnitToAmount(pi.Amount, currency),
Metadata: map[string]string{
"currency": currency,
},
}, nil
}
@@ -174,12 +203,16 @@ func parseStripePaymentIntent(event *stripe.Event, status string, rawBody string
if err := json.Unmarshal(event.Data.Raw, &pi); err != nil {
return nil, fmt.Errorf("stripe parse payment_intent: %w", err)
}
currency := stripeIntentCurrency(pi.Currency, payment.DefaultPaymentCurrency)
return &payment.PaymentNotification{
TradeNo: pi.ID,
OrderID: pi.Metadata["orderId"],
Amount: payment.FenToYuan(pi.Amount),
Amount: payment.MinorUnitToAmount(pi.Amount, currency),
Status: status,
RawData: rawBody,
Metadata: map[string]string{
"currency": currency,
},
}, nil
}
@@ -187,14 +220,14 @@ func parseStripePaymentIntent(event *stripe.Event, status string, rawBody string
func (s *Stripe) Refund(ctx context.Context, req payment.RefundRequest) (*payment.RefundResponse, error) {
s.ensureInit()
amountInCents, err := payment.YuanToFen(req.Amount)
amountInMinorUnit, err := payment.AmountToMinorUnit(req.Amount, s.currency())
if err != nil {
return nil, fmt.Errorf("stripe refund: %w", err)
}
params := &stripe.RefundCreateParams{
PaymentIntent: stripe.String(req.TradeNo),
Amount: stripe.Int64(amountInCents),
Amount: stripe.Int64(amountInMinorUnit),
Reason: stripe.String(string(stripe.RefundReasonRequestedByCustomer)),
}
params.Context = ctx
@@ -215,6 +248,18 @@ func (s *Stripe) Refund(ctx context.Context, req payment.RefundRequest) (*paymen
}, nil
}
func stripeIntentCurrency(raw stripe.Currency, fallback string) string {
currency, err := payment.NormalizePaymentCurrency(string(raw))
if err != nil || currency == payment.DefaultPaymentCurrency && strings.TrimSpace(string(raw)) == "" {
normalizedFallback, fallbackErr := payment.NormalizePaymentCurrency(fallback)
if fallbackErr == nil {
return normalizedFallback
}
return payment.DefaultPaymentCurrency
}
return currency
}
// resolveStripeMethodTypes converts instance supported_types (comma-separated)
// into Stripe API payment_method_types. Falls back to ["card"] if empty.
func resolveStripeMethodTypes(instanceSubMethods string) []string {
@@ -257,6 +302,7 @@ func (s *Stripe) CancelPayment(ctx context.Context, tradeNo string) error {
// Ensure interface compliance.
var (
_ payment.Provider = (*Stripe)(nil)
_ payment.CancelableProvider = (*Stripe)(nil)
_ payment.Provider = (*Stripe)(nil)
_ payment.CancelableProvider = (*Stripe)(nil)
_ payment.MerchantIdentityProvider = (*Stripe)(nil)
)