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
@@ -8,6 +8,7 @@ import (
dbent "github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance"
"github.com/Wei-Shaw/sub2api/internal/payment"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
)
// GetAvailableMethodLimits collects all payment types from enabled provider
@@ -25,7 +26,12 @@ func (s *PaymentConfigService) GetAvailableMethodLimits(ctx context.Context) (*M
Methods: make(map[string]MethodLimits, len(typeInstances)),
}
for pt, insts := range typeInstances {
currency, ok := s.pcAggregateMethodCurrency(insts)
if !ok {
continue
}
ml := pcAggregateMethodLimits(pt, insts)
ml.Currency = currency
resp.Methods[ml.PaymentType] = ml
}
resp.GlobalMin, resp.GlobalMax = pcComputeGlobalRange(resp.Methods)
@@ -82,11 +88,81 @@ func (s *PaymentConfigService) GetMethodLimits(ctx context.Context, types []stri
matching = append(matching, inst)
}
}
result = append(result, pcAggregateMethodLimits(pt, matching))
currency, ok := s.pcAggregateMethodCurrency(matching)
if !ok {
continue
}
ml := pcAggregateMethodLimits(pt, matching)
ml.Currency = currency
result = append(result, ml)
}
return result, nil
}
func (s *PaymentConfigService) ValidateMethodCurrencyConsistency(ctx context.Context, paymentType string) (string, error) {
method := NormalizeVisibleMethod(paymentType)
if method == "" || s == nil || s.entClient == nil {
return payment.DefaultPaymentCurrency, nil
}
instances, err := s.entClient.PaymentProviderInstance.Query().
Where(paymentproviderinstance.EnabledEQ(true)).All(ctx)
if err != nil {
return "", fmt.Errorf("query provider instances: %w", err)
}
typeInstances := pcGroupByPaymentType(instances)
typeInstances = s.pcApplyEnabledVisibleMethodInstances(ctx, typeInstances, instances)
matching := typeInstances[method]
if len(matching) == 0 {
return payment.DefaultPaymentCurrency, nil
}
currency, ok := s.pcAggregateMethodCurrency(matching)
if !ok {
return "", infraerrors.ServiceUnavailable(
"PAYMENT_METHOD_CURRENCY_CONFLICT",
"payment method has enabled provider instances with mixed currencies",
).WithMetadata(map[string]string{"payment_type": method})
}
return currency, nil
}
func (s *PaymentConfigService) pcAggregateMethodCurrency(instances []*dbent.PaymentProviderInstance) (string, bool) {
currency := ""
for _, inst := range instances {
next := s.pcInstancePaymentCurrency(inst)
if next == "" {
continue
}
if currency == "" {
currency = next
continue
}
if currency != next {
return "", false
}
}
if currency == "" {
return payment.DefaultPaymentCurrency, true
}
return currency, true
}
func (s *PaymentConfigService) pcInstancePaymentCurrency(inst *dbent.PaymentProviderInstance) string {
if inst == nil {
return payment.DefaultPaymentCurrency
}
cfg := map[string]string{}
if s != nil {
decrypted, err := s.decryptConfig(inst.Config)
if err == nil && decrypted != nil {
cfg = decrypted
}
}
return paymentProviderConfigCurrency(inst.ProviderKey, cfg)
}
// pcGroupByPaymentType groups instances by user-facing payment type.
// For Stripe providers, ALL sub-types (card, link, alipay, wxpay) map to "stripe"
// because the user sees a single "Stripe" button, not individual sub-methods.