| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658 |
- package provider
- import (
- "bytes"
- "context"
- "crypto/hmac"
- "crypto/sha256"
- "encoding/hex"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "strings"
- "sync"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/payment"
- "github.com/google/uuid"
- "github.com/shopspring/decimal"
- )
- const (
- airwallexDemoAPIBase = "https://api-demo.airwallex.com/api/v1"
- airwallexProdAPIBase = "https://api.airwallex.com/api/v1"
- airwallexDefaultCountry = "CN"
- airwallexHTTPTimeout = 15 * time.Second
- airwallexMaxResponseSize = 1 << 20
- airwallexMaxErrorSummary = 512
- airwallexTokenSkew = 2 * time.Minute
- airwallexWebhookTolerance = 5 * time.Minute
- airwallexEventPaymentSucceeded = "payment_intent.succeeded"
- airwallexEventPaymentCancelled = "payment_intent.cancelled"
- airwallexPaymentStatusSucceeded = "SUCCEEDED"
- airwallexPaymentStatusCancelled = "CANCELLED"
- airwallexRefundStatusReceived = "RECEIVED"
- airwallexRefundStatusAccepted = "ACCEPTED"
- airwallexRefundStatusSettled = "SETTLED"
- airwallexRefundStatusFailed = "FAILED"
- )
- type Airwallex struct {
- instanceID string
- config map[string]string
- httpClient *http.Client
- }
- type airwallexTokenState struct {
- mu sync.Mutex
- token string
- expiresAt time.Time
- }
- var airwallexAccessTokens sync.Map
- func NewAirwallex(instanceID string, config map[string]string) (*Airwallex, error) {
- for _, k := range []string{"clientId", "apiKey", "webhookSecret", "apiBase"} {
- if strings.TrimSpace(config[k]) == "" {
- return nil, fmt.Errorf("airwallex config missing required key: %s", k)
- }
- }
- cfg := cloneStringMap(config)
- apiBase, err := normalizeAirwallexAPIBase(cfg["apiBase"])
- if err != nil {
- return nil, err
- }
- cfg["apiBase"] = apiBase
- currency, err := payment.NormalizePaymentCurrency(cfg["currency"])
- if err != nil {
- return nil, fmt.Errorf("airwallex config currency: %w", err)
- }
- cfg["currency"] = currency
- countryCode, err := normalizeAirwallexCountryCode(cfg["countryCode"])
- if err != nil {
- return nil, err
- }
- cfg["countryCode"] = countryCode
- return &Airwallex{
- instanceID: instanceID,
- config: cfg,
- httpClient: &http.Client{Timeout: airwallexHTTPTimeout},
- }, nil
- }
- func normalizeAirwallexCountryCode(raw string) (string, error) {
- countryCode := strings.ToUpper(strings.TrimSpace(raw))
- if countryCode == "" {
- return airwallexDefaultCountry, nil
- }
- if len(countryCode) != 2 {
- return "", fmt.Errorf("airwallex config countryCode must be a two-letter ISO country code")
- }
- for _, ch := range countryCode {
- if ch < 'A' || ch > 'Z' {
- return "", fmt.Errorf("airwallex config countryCode must be a two-letter ISO country code")
- }
- }
- return countryCode, nil
- }
- func normalizeAirwallexAPIBase(raw string) (string, error) {
- base := strings.TrimSpace(raw)
- if base == "" {
- return "", fmt.Errorf("airwallex apiBase is required")
- }
- parsed, err := url.Parse(base)
- if err != nil || parsed.Scheme != "https" || parsed.Host == "" {
- return "", fmt.Errorf("airwallex apiBase must be an HTTPS URL")
- }
- host := strings.ToLower(parsed.Host)
- if host != "api-demo.airwallex.com" && host != "api.airwallex.com" {
- return "", fmt.Errorf("airwallex apiBase host must be api-demo.airwallex.com or api.airwallex.com")
- }
- parsed.RawQuery = ""
- parsed.Fragment = ""
- parsed.RawPath = ""
- parsed.Path = strings.TrimRight(parsed.Path, "/")
- if parsed.Path == "" {
- parsed.Path = "/api/v1"
- }
- if parsed.Path != "/api/v1" {
- return "", fmt.Errorf("airwallex apiBase path must be /api/v1")
- }
- return parsed.String(), nil
- }
- func (a *Airwallex) Name() string { return "空中云汇" }
- func (a *Airwallex) ProviderKey() string { return payment.TypeAirwallex }
- func (a *Airwallex) SupportedTypes() []payment.PaymentType {
- return []payment.PaymentType{payment.TypeAirwallex}
- }
- func (a *Airwallex) MerchantIdentityMetadata() map[string]string {
- if a == nil {
- return nil
- }
- metadata := map[string]string{"currency": a.currency()}
- if accountID := strings.TrimSpace(a.config["accountId"]); accountID != "" {
- metadata["account_id"] = accountID
- }
- return metadata
- }
- func (a *Airwallex) currency() string {
- if a == nil {
- return payment.DefaultPaymentCurrency
- }
- currency, err := payment.NormalizePaymentCurrency(a.config["currency"])
- if err != nil {
- return payment.DefaultPaymentCurrency
- }
- return currency
- }
- func (a *Airwallex) CreatePayment(ctx context.Context, req payment.CreatePaymentRequest) (*payment.CreatePaymentResponse, error) {
- amount, err := decimal.NewFromString(req.Amount)
- if err != nil || amount.LessThanOrEqual(decimal.Zero) {
- return nil, fmt.Errorf("airwallex create payment: invalid amount %s", req.Amount)
- }
- token, err := a.accessToken(ctx)
- if err != nil {
- return nil, fmt.Errorf("airwallex auth: %w", err)
- }
- currency := a.currency()
- requestID := airwallexDeterministicRequestID("payment-intent", req.OrderID, req.Amount, currency)
- payload := airwallexCreatePaymentIntentRequest{
- RequestID: requestID,
- Amount: newAirwallexRequestAmount(amount),
- Currency: currency,
- MerchantOrderID: req.OrderID,
- ReturnURL: req.ReturnURL,
- Metadata: map[string]string{
- "order_id": req.OrderID,
- },
- }
- if descriptor := strings.TrimSpace(a.config["descriptor"]); descriptor != "" {
- payload.Descriptor = descriptor
- }
- var intent airwallexPaymentIntent
- if err := a.doJSON(ctx, http.MethodPost, "/pa/payment_intents/create", token, payload, &intent); err != nil {
- return nil, fmt.Errorf("airwallex create payment: %w", err)
- }
- if strings.TrimSpace(intent.ID) == "" || strings.TrimSpace(intent.ClientSecret) == "" {
- return nil, fmt.Errorf("airwallex create payment: missing payment intent id or client secret")
- }
- return &payment.CreatePaymentResponse{
- TradeNo: intent.ID,
- ClientSecret: intent.ClientSecret,
- IntentID: intent.ID,
- Currency: currency,
- CountryCode: a.config["countryCode"],
- PaymentEnv: a.checkoutEnv(),
- }, nil
- }
- func (a *Airwallex) QueryOrder(ctx context.Context, tradeNo string) (*payment.QueryOrderResponse, error) {
- intentID := strings.TrimSpace(tradeNo)
- if intentID == "" {
- return nil, fmt.Errorf("airwallex query order: missing payment intent id")
- }
- token, err := a.accessToken(ctx)
- if err != nil {
- return nil, fmt.Errorf("airwallex auth: %w", err)
- }
- var intent airwallexPaymentIntent
- if err := a.doJSON(ctx, http.MethodGet, "/pa/payment_intents/"+url.PathEscape(intentID), token, nil, &intent); err != nil {
- return nil, fmt.Errorf("airwallex query order: %w", err)
- }
- return &payment.QueryOrderResponse{
- TradeNo: intent.ID,
- Status: airwallexProviderStatus(intent.Status),
- Amount: intent.Amount.InexactFloat64(),
- Metadata: a.intentMetadata(intent, ""),
- }, nil
- }
- func (a *Airwallex) VerifyNotification(_ context.Context, rawBody string, headers map[string]string) (*payment.PaymentNotification, error) {
- if err := verifyAirwallexWebhookSignature(rawBody, headers, a.config["webhookSecret"], time.Now()); err != nil {
- return nil, err
- }
- var event airwallexWebhookEvent
- if err := json.Unmarshal([]byte(rawBody), &event); err != nil {
- return nil, fmt.Errorf("airwallex parse webhook: %w", err)
- }
- switch event.Name {
- case airwallexEventPaymentSucceeded, airwallexEventPaymentCancelled:
- default:
- return nil, nil
- }
- var intent airwallexPaymentIntent
- if err := json.Unmarshal(event.Data.Object, &intent); err != nil {
- return nil, fmt.Errorf("airwallex parse payment intent: %w", err)
- }
- if strings.TrimSpace(intent.ID) == "" || strings.TrimSpace(intent.MerchantOrderID) == "" {
- return nil, fmt.Errorf("airwallex webhook missing payment intent id or merchant_order_id")
- }
- status := payment.ProviderStatusFailed
- if event.Name == airwallexEventPaymentSucceeded {
- if strings.ToUpper(strings.TrimSpace(intent.Status)) != airwallexPaymentStatusSucceeded {
- return nil, fmt.Errorf("airwallex succeeded webhook has non-succeeded status: %s", intent.Status)
- }
- status = payment.NotificationStatusSuccess
- }
- return &payment.PaymentNotification{
- TradeNo: intent.ID,
- OrderID: intent.MerchantOrderID,
- Amount: intent.Amount.InexactFloat64(),
- Status: status,
- RawData: rawBody,
- Metadata: a.intentMetadata(intent, event.accountID()),
- }, nil
- }
- func (a *Airwallex) Refund(ctx context.Context, req payment.RefundRequest) (*payment.RefundResponse, error) {
- intentID := strings.TrimSpace(req.TradeNo)
- if intentID == "" {
- return nil, fmt.Errorf("airwallex refund missing payment intent id")
- }
- amount, err := decimal.NewFromString(req.Amount)
- if err != nil || amount.LessThanOrEqual(decimal.Zero) {
- return nil, fmt.Errorf("airwallex refund: invalid amount %s", req.Amount)
- }
- token, err := a.accessToken(ctx)
- if err != nil {
- return nil, fmt.Errorf("airwallex auth: %w", err)
- }
- payload := airwallexCreateRefundRequest{
- RequestID: airwallexDeterministicRequestID("refund", intentID, req.Amount),
- PaymentIntentID: intentID,
- Amount: newAirwallexRequestAmount(amount),
- Reason: strings.TrimSpace(req.Reason),
- }
- if payload.Reason == "" {
- payload.Reason = "refund"
- }
- var resp airwallexRefund
- if err := a.doJSON(ctx, http.MethodPost, "/pa/refunds/create", token, payload, &resp); err != nil {
- return nil, fmt.Errorf("airwallex refund: %w", err)
- }
- if strings.TrimSpace(resp.ID) == "" {
- return nil, fmt.Errorf("airwallex refund: missing refund id")
- }
- refundResp := &payment.RefundResponse{
- RefundID: resp.ID,
- Status: airwallexRefundProviderStatus(resp.Status),
- }
- if refundResp.Status != payment.ProviderStatusSuccess {
- return refundResp, fmt.Errorf("airwallex refund not settled: status %s", strings.ToUpper(strings.TrimSpace(resp.Status)))
- }
- return refundResp, nil
- }
- func (a *Airwallex) QueryRefund(ctx context.Context, req payment.RefundQueryRequest) (*payment.RefundResponse, error) {
- refundID := strings.TrimSpace(req.RefundID)
- if refundID == "" {
- return nil, fmt.Errorf("airwallex query refund: missing refund id")
- }
- token, err := a.accessToken(ctx)
- if err != nil {
- return nil, fmt.Errorf("airwallex auth: %w", err)
- }
- var resp airwallexRefund
- if err := a.doJSON(ctx, http.MethodGet, "/pa/refunds/"+url.PathEscape(refundID), token, nil, &resp); err != nil {
- return nil, fmt.Errorf("airwallex query refund: %w", err)
- }
- if strings.TrimSpace(resp.ID) == "" {
- resp.ID = refundID
- }
- return &payment.RefundResponse{RefundID: resp.ID, Status: airwallexRefundProviderStatus(resp.Status)}, nil
- }
- func (a *Airwallex) CancelPayment(ctx context.Context, tradeNo string) error {
- intentID := strings.TrimSpace(tradeNo)
- if intentID == "" {
- return nil
- }
- token, err := a.accessToken(ctx)
- if err != nil {
- return fmt.Errorf("airwallex auth: %w", err)
- }
- var intent airwallexPaymentIntent
- if err := a.doJSON(ctx, http.MethodPost, "/pa/payment_intents/"+url.PathEscape(intentID)+"/cancel", token, nil, &intent); err != nil {
- return fmt.Errorf("airwallex cancel payment: %w", err)
- }
- return nil
- }
- func (a *Airwallex) intentMetadata(intent airwallexPaymentIntent, accountID string) map[string]string {
- metadata := map[string]string{
- "currency": strings.ToUpper(strings.TrimSpace(intent.Currency)),
- "status": strings.ToUpper(strings.TrimSpace(intent.Status)),
- }
- if accountID = strings.TrimSpace(accountID); accountID != "" {
- metadata["account_id"] = accountID
- } else if configured := strings.TrimSpace(a.config["accountId"]); configured != "" {
- metadata["account_id"] = configured
- }
- return metadata
- }
- func (a *Airwallex) checkoutEnv() string {
- if strings.EqualFold(a.config["apiBase"], airwallexProdAPIBase) {
- return "prod"
- }
- return "demo"
- }
- func (a *Airwallex) accessToken(ctx context.Context) (string, error) {
- cacheKey := a.tokenCacheKey()
- rawState, _ := airwallexAccessTokens.LoadOrStore(cacheKey, &airwallexTokenState{})
- state, ok := rawState.(*airwallexTokenState)
- if !ok {
- return "", fmt.Errorf("airwallex auth token cache state type mismatch")
- }
- state.mu.Lock()
- defer state.mu.Unlock()
- if state.token != "" && time.Now().Add(airwallexTokenSkew).Before(state.expiresAt) {
- return state.token, nil
- }
- req, err := http.NewRequestWithContext(ctx, http.MethodPost, a.config["apiBase"]+"/authentication/login", nil)
- if err != nil {
- return "", err
- }
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("x-client-id", a.config["clientId"])
- req.Header.Set("x-api-key", a.config["apiKey"])
- if accountID := strings.TrimSpace(a.config["accountId"]); accountID != "" {
- req.Header.Set("x-login-as", accountID)
- }
- body, status, err := a.do(req)
- if err != nil {
- return "", err
- }
- if status < http.StatusOK || status >= http.StatusMultipleChoices {
- return "", formatAirwallexAuthHTTPError(status, body)
- }
- var resp airwallexAuthResponse
- if err := json.Unmarshal(body, &resp); err != nil {
- return "", fmt.Errorf("parse authentication response: %w", err)
- }
- if strings.TrimSpace(resp.Token) == "" {
- return "", fmt.Errorf("authentication response missing token")
- }
- expiresAt, err := parseAirwallexTime(resp.ExpiresAt)
- if err != nil {
- expiresAt = time.Now().Add(25 * time.Minute)
- }
- state.token = resp.Token
- state.expiresAt = expiresAt
- return state.token, nil
- }
- func formatAirwallexAuthHTTPError(status int, body []byte) error {
- summary := summarizeAirwallexResponse(body)
- if status == http.StatusUnauthorized || status == http.StatusForbidden {
- return fmt.Errorf("authentication HTTP %d: %s; Airwallex credentials were rejected, check Client ID/API Key, API Base environment (sandbox: https://api-demo.airwallex.com/api/v1, production: https://api.airwallex.com/api/v1), and Account ID (leave it empty for single-account scoped keys)", status, summary)
- }
- return fmt.Errorf("authentication HTTP %d: %s", status, summary)
- }
- func (a *Airwallex) tokenCacheKey() string {
- sum := sha256.Sum256([]byte(a.config["apiKey"]))
- return a.config["apiBase"] + "|" + a.config["clientId"] + "|" + strings.TrimSpace(a.config["accountId"]) + "|" + hex.EncodeToString(sum[:8])
- }
- func (a *Airwallex) doJSON(ctx context.Context, method, path, token string, payload any, out any) error {
- var bodyReader io.Reader
- if payload != nil {
- body, err := json.Marshal(payload)
- if err != nil {
- return err
- }
- bodyReader = bytes.NewReader(body)
- }
- req, err := http.NewRequestWithContext(ctx, method, a.config["apiBase"]+path, bodyReader)
- if err != nil {
- return err
- }
- req.Header.Set("Content-Type", "application/json")
- if token != "" {
- req.Header.Set("Authorization", "Bearer "+token)
- }
- if accountID := strings.TrimSpace(a.config["accountId"]); accountID != "" {
- req.Header.Set("x-on-behalf-of", accountID)
- }
- body, status, err := a.do(req)
- if err != nil {
- return err
- }
- if status < http.StatusOK || status >= http.StatusMultipleChoices {
- return fmt.Errorf("HTTP %d: %s", status, summarizeAirwallexResponse(body))
- }
- if out == nil || len(bytes.TrimSpace(body)) == 0 {
- return nil
- }
- if err := json.Unmarshal(body, out); err != nil {
- return fmt.Errorf("parse response: %w", err)
- }
- return nil
- }
- func (a *Airwallex) do(req *http.Request) ([]byte, int, error) {
- client := a.httpClient
- if client == nil {
- client = &http.Client{Timeout: airwallexHTTPTimeout}
- }
- resp, err := client.Do(req)
- if err != nil {
- return nil, 0, err
- }
- defer func() { _ = resp.Body.Close() }()
- body, err := io.ReadAll(io.LimitReader(resp.Body, airwallexMaxResponseSize))
- if err != nil {
- return nil, resp.StatusCode, err
- }
- return body, resp.StatusCode, nil
- }
- func airwallexProviderStatus(status string) string {
- switch strings.ToUpper(strings.TrimSpace(status)) {
- case airwallexPaymentStatusSucceeded:
- return payment.ProviderStatusPaid
- case airwallexPaymentStatusCancelled:
- return payment.ProviderStatusFailed
- default:
- return payment.ProviderStatusPending
- }
- }
- func airwallexRefundProviderStatus(status string) string {
- switch strings.ToUpper(strings.TrimSpace(status)) {
- case airwallexRefundStatusSettled:
- return payment.ProviderStatusSuccess
- case airwallexRefundStatusFailed:
- return payment.ProviderStatusFailed
- case airwallexRefundStatusReceived, airwallexRefundStatusAccepted:
- return payment.ProviderStatusPending
- default:
- return payment.ProviderStatusPending
- }
- }
- func airwallexDeterministicRequestID(parts ...string) string {
- hash := sha256.Sum256([]byte(strings.Join(parts, "\x00")))
- var id uuid.UUID
- copy(id[:], hash[:16])
- id[6] = (id[6] & 0x0f) | 0x40
- id[8] = (id[8] & 0x3f) | 0x80
- return id.String()
- }
- func verifyAirwallexWebhookSignature(rawBody string, headers map[string]string, secret string, now time.Time) error {
- secret = strings.TrimSpace(secret)
- if secret == "" {
- return fmt.Errorf("airwallex webhookSecret not configured")
- }
- timestamp := strings.TrimSpace(headers["x-timestamp"])
- signature := strings.ToLower(strings.TrimSpace(headers["x-signature"]))
- if timestamp == "" || signature == "" {
- return fmt.Errorf("airwallex notification missing x-timestamp or x-signature header")
- }
- mac := hmac.New(sha256.New, []byte(secret))
- _, _ = mac.Write([]byte(timestamp))
- _, _ = mac.Write([]byte(rawBody))
- expected := hex.EncodeToString(mac.Sum(nil))
- if !hmac.Equal([]byte(expected), []byte(signature)) {
- return fmt.Errorf("airwallex invalid signature")
- }
- ts, err := parseAirwallexWebhookTimestamp(timestamp)
- if err != nil {
- return err
- }
- if now.IsZero() {
- now = time.Now()
- }
- if diff := now.Sub(ts).Abs(); diff > airwallexWebhookTolerance {
- return fmt.Errorf("airwallex webhook timestamp outside tolerance")
- }
- return nil
- }
- func parseAirwallexWebhookTimestamp(raw string) (time.Time, error) {
- ts, err := decimal.NewFromString(strings.TrimSpace(raw))
- if err != nil {
- return time.Time{}, fmt.Errorf("airwallex invalid webhook timestamp")
- }
- millis := ts.IntPart()
- if millis <= 0 {
- return time.Time{}, fmt.Errorf("airwallex invalid webhook timestamp")
- }
- return time.UnixMilli(millis), nil
- }
- func parseAirwallexTime(raw string) (time.Time, error) {
- raw = strings.TrimSpace(raw)
- if raw == "" {
- return time.Time{}, fmt.Errorf("empty time")
- }
- for _, layout := range []string{time.RFC3339, "2006-01-02T15:04:05-0700", "2006-01-02T15:04:05.000-0700"} {
- if t, err := time.Parse(layout, raw); err == nil {
- return t, nil
- }
- }
- return time.Time{}, fmt.Errorf("invalid time: %s", raw)
- }
- func summarizeAirwallexResponse(body []byte) string {
- summary := strings.Join(strings.Fields(string(body)), " ")
- if summary == "" {
- return "<empty>"
- }
- if len(summary) > airwallexMaxErrorSummary {
- return summary[:airwallexMaxErrorSummary] + "..."
- }
- return summary
- }
- type airwallexAuthResponse struct {
- Token string `json:"token"`
- ExpiresAt string `json:"expires_at"`
- }
- type airwallexCreatePaymentIntentRequest struct {
- RequestID string `json:"request_id"`
- Amount airwallexRequestAmount `json:"amount"`
- Currency string `json:"currency"`
- MerchantOrderID string `json:"merchant_order_id"`
- ReturnURL string `json:"return_url,omitempty"`
- Descriptor string `json:"descriptor,omitempty"`
- Metadata map[string]string `json:"metadata,omitempty"`
- }
- type airwallexCreateRefundRequest struct {
- RequestID string `json:"request_id"`
- PaymentIntentID string `json:"payment_intent_id"`
- Amount airwallexRequestAmount `json:"amount,omitempty"`
- Reason string `json:"reason,omitempty"`
- }
- type airwallexRequestAmount struct {
- decimal.Decimal
- }
- func newAirwallexRequestAmount(amount decimal.Decimal) airwallexRequestAmount {
- return airwallexRequestAmount{Decimal: amount}
- }
- func (a airwallexRequestAmount) MarshalJSON() ([]byte, error) {
- return []byte(a.String()), nil
- }
- func (a *airwallexRequestAmount) UnmarshalJSON(data []byte) error {
- amount, err := decimal.NewFromString(strings.Trim(string(data), `"`))
- if err != nil {
- return err
- }
- a.Decimal = amount
- return nil
- }
- type airwallexPaymentIntent struct {
- ID string `json:"id"`
- RequestID string `json:"request_id"`
- ClientSecret string `json:"client_secret"`
- MerchantOrderID string `json:"merchant_order_id"`
- Amount decimal.Decimal `json:"amount"`
- Currency string `json:"currency"`
- Status string `json:"status"`
- Metadata map[string]string `json:"metadata"`
- }
- type airwallexRefund struct {
- ID string `json:"id"`
- RequestID string `json:"request_id"`
- PaymentIntentID string `json:"payment_intent_id"`
- Amount decimal.Decimal `json:"amount"`
- Currency string `json:"currency"`
- Status string `json:"status"`
- }
- type airwallexWebhookEvent struct {
- ID string `json:"id"`
- Name string `json:"name"`
- AccountID string `json:"accountId"`
- AccountIDSnake string `json:"account_id"`
- Data struct {
- Object json.RawMessage `json:"object"`
- } `json:"data"`
- }
- func (e airwallexWebhookEvent) accountID() string {
- if accountID := strings.TrimSpace(e.AccountID); accountID != "" {
- return accountID
- }
- return strings.TrimSpace(e.AccountIDSnake)
- }
- var (
- _ payment.Provider = (*Airwallex)(nil)
- _ payment.CancelableProvider = (*Airwallex)(nil)
- _ payment.MerchantIdentityProvider = (*Airwallex)(nil)
- )
|