airwallex 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +76 -0
- data/README.md +211 -25
- data/lib/airwallex/api_operations/delete.rb +3 -3
- data/lib/airwallex/api_operations/update.rb +4 -4
- data/lib/airwallex/api_resource.rb +22 -0
- data/lib/airwallex/client.rb +1 -1
- data/lib/airwallex/configuration.rb +6 -2
- data/lib/airwallex/errors.rb +6 -1
- data/lib/airwallex/resources/account_amendment.rb +29 -0
- data/lib/airwallex/resources/beneficiary.rb +96 -0
- data/lib/airwallex/resources/billing_customer.rb +48 -0
- data/lib/airwallex/resources/billing_price.rb +30 -0
- data/lib/airwallex/resources/billing_product.rb +23 -0
- data/lib/airwallex/resources/billing_subscription.rb +93 -0
- data/lib/airwallex/resources/billing_subscription_item.rb +7 -0
- data/lib/airwallex/resources/charge.rb +19 -0
- data/lib/airwallex/resources/connected_account.rb +136 -0
- data/lib/airwallex/resources/conversion.rb +1 -1
- data/lib/airwallex/resources/customer.rb +5 -2
- data/lib/airwallex/resources/dispute.rb +48 -28
- data/lib/airwallex/resources/funds_split.rb +37 -0
- data/lib/airwallex/resources/global_account.rb +137 -0
- data/lib/airwallex/resources/global_account_alias.rb +62 -0
- data/lib/airwallex/resources/global_account_mandate.rb +26 -0
- data/lib/airwallex/resources/global_account_transaction.rb +7 -0
- data/lib/airwallex/resources/payment_consent.rb +68 -0
- data/lib/airwallex/resources/payment_method.rb +13 -5
- data/lib/airwallex/resources/payment_source.rb +52 -0
- data/lib/airwallex/resources/quote.rb +5 -3
- data/lib/airwallex/resources/rate.rb +2 -7
- data/lib/airwallex/util.rb +8 -5
- data/lib/airwallex/version.rb +1 -1
- data/lib/airwallex.rb +15 -0
- metadata +18 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: be0adbfbafc982fa5c899a9df4230e9c65a68e46d2e7eb248b981e41ccc23bd4
|
|
4
|
+
data.tar.gz: 599bdc078288f934cc5f8b06fced1668605c8e2e6af4763d943a9f227a32e563
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4cd20d177f642d64bdb07922f673483ab3d86499c0a938c8a7db7854f06fd5dff86b10326c09496dbf9a0bc8099a64834fc0ee4a56ff91d822dd721aff911e7a
|
|
7
|
+
data.tar.gz: 56b071c9ff52ccf48f6526c6f3a8264d55c1d8cfcc5794d869894535baf7685c7642bdebf498f5c4ed01e362631f62a9721d4151bc6f860cf91b574a2a2d54b7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,81 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.8.0] - 2026-09-02
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- `Util.deep_symbolize_keys` now recurses into `Array`s, matching `ActiveSupport#deep_transform_keys`'s
|
|
7
|
+
behavior. Previously any `Array` value was copied through untouched, so hashes nested inside an array
|
|
8
|
+
(e.g. `beneficiary_form_schemas`'s `fields` list) kept string keys even after "deep" symbolizing.
|
|
9
|
+
- `Beneficiary.validate`, `.verify_account`, `.api_schema`, `.form_schema`, and
|
|
10
|
+
`.supported_financial_institutions`, `ConnectedAccount.wallet_info`, `BillingCustomer#bank_transfer_instructions`,
|
|
11
|
+
and `GlobalAccount#generate_statement_letter` now return `Util.deep_symbolize_keys`-processed responses,
|
|
12
|
+
via two new shared helpers, `APIResource.symbolized_post`/`.symbolized_get`. Previously these methods
|
|
13
|
+
(unlike `.create`/`.retrieve`/`.update`/`.delete`/`.list`, which already funnel through `APIResource`'s
|
|
14
|
+
symbolization) returned the raw parsed JSON response with string keys, with nothing in the method
|
|
15
|
+
signature or naming to distinguish them from the symbolized methods on the same class.
|
|
16
|
+
- `Airwallex::Error#details` is now symbolized too (via the same `Util.deep_symbolize_keys` fix), instead
|
|
17
|
+
of holding the raw, string-keyed error body.
|
|
18
|
+
|
|
19
|
+
**Breaking change:** any code reading these nine methods'/`#details`' values with string keys
|
|
20
|
+
(`response["field"]`) must switch to symbol keys (`response[:field]`).
|
|
21
|
+
|
|
22
|
+
## [0.7.0] - 2026-08-28
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- GlobalAccount resource (create, retrieve, list, update, `#close`, `#generate_statement_letter`,
|
|
26
|
+
`#transactions`)
|
|
27
|
+
- GlobalAccountAlias nested resource (`#create_alias`, `#alias`, `#aliases`, `#initiate_port`,
|
|
28
|
+
`#submit_verification_code`, `#request_new_verification_code`, `#cancel`)
|
|
29
|
+
- GlobalAccountMandate nested resource (`#mandate`, `#mandates`, `#cancel`)
|
|
30
|
+
- Billing resources: BillingCustomer, BillingProduct, BillingPrice, BillingSubscription (with `#items`/`#item`)
|
|
31
|
+
- PaymentConsent resource (create, retrieve, list, update, `#verify`, `#verify_continue`, `#disable`)
|
|
32
|
+
- PaymentSource resource (create, retrieve, list)
|
|
33
|
+
- ConnectedAccount resource (create, retrieve, list, update, `#submit`, `#agree_to_terms_and_conditions`,
|
|
34
|
+
`#suspend`, `#reactivate`, `.current`, `.wallet_info`, `#legal_entity_id`)
|
|
35
|
+
- AccountAmendment resource (create, retrieve)
|
|
36
|
+
- FundsSplit resource (create, retrieve, list, `#release`)
|
|
37
|
+
- Charge resource (create, retrieve, list)
|
|
38
|
+
- Beneficiary gained `.update`/`#update`, `.validate`, `.verify_account`, `.api_schema`, `.form_schema`, and
|
|
39
|
+
`.supported_financial_institutions`
|
|
40
|
+
- Dispute gained `.update`, `#challenge` (replacing `#submit_evidence`), and `#related_payment_intents`
|
|
41
|
+
- PaymentMethod gained `#disable`
|
|
42
|
+
- New tests covering all of the above (378 total)
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
- `APIOperations::Update` now sends `POST #{resource_path}/{id}/update` instead of `PUT #{resource_path}/{id}`
|
|
46
|
+
- `APIOperations::Delete` now sends `POST #{resource_path}/{id}/delete` instead of `DELETE #{resource_path}/{id}`,
|
|
47
|
+
and checks the response instead of always returning `true`
|
|
48
|
+
- `Dispute`'s resource path corrected from `/api/v1/disputes` to `/api/v1/pa/payment_disputes`
|
|
49
|
+
- `Conversion`'s resource path corrected from `/api/v1/conversions` to `/api/v1/fx/conversions`
|
|
50
|
+
- `Configuration#api_version` now defaults to `nil` instead of a hardcoded date, and `x-api-version` is only
|
|
51
|
+
sent when explicitly set
|
|
52
|
+
- `Beneficiary.create`/`.validate`/`.update` payloads corrected: wrapped under a top-level `beneficiary` key,
|
|
53
|
+
with `nickname`/`payer_entity_type`/`transfer_methods`/`transfer_reason` as top-level siblings;
|
|
54
|
+
`entity_type` replaces the nonexistent `beneficiary_type`
|
|
55
|
+
- `Beneficiary#update`/`.update` requires the full payload, not a partial patch
|
|
56
|
+
- `Beneficiary.supported_financial_institutions` requires `account_currency`, `entity_type`,
|
|
57
|
+
`transfer_method`, and `keyword` in addition to `bank_country_code`
|
|
58
|
+
- `Customer.create` requires `merchant_customer_id`
|
|
59
|
+
- `GlobalAccount#generate_statement_letter` requires `account_statement_type` and `registration_info`
|
|
60
|
+
- `ConnectedAccount.create` payload corrected: `account_details` is a required top-level wrapper;
|
|
61
|
+
`legal_entity_type` is `"BUSINESS"`/`"INDIVIDUAL"`, not `"COMPANY"`
|
|
62
|
+
- `BillingCustomer.create` payload corrected to match the real flat schema (`name`/`email`/`type`/
|
|
63
|
+
`default_billing_currency`/`address`/`default_legal_entity_id`)
|
|
64
|
+
- `BillingPrice.create` requires `pricing_model` and `recurring`
|
|
65
|
+
- `BillingSubscription.create` payload corrected: prices attach via an `items:` array, not a flat
|
|
66
|
+
`price_id`; `starts_at` replaces `start_date`; added `legal_entity_id` and `payment_source_id`
|
|
67
|
+
- `AccountAmendment.create` payload corrected to use `target` plus the changed section as a top-level
|
|
68
|
+
sibling, not a generic `changes:` wrapper
|
|
69
|
+
- `PaymentSource.create`'s `external_id` corrected to reference a `PaymentMethod` id, not a `PaymentConsent` id
|
|
70
|
+
- `ConnectedAccount#legal_entity_id` added as the source for `BillingCustomer`/`BillingSubscription`'s
|
|
71
|
+
`legal_entity_id`, rather than a separate resource
|
|
72
|
+
- `Rate`/`Quote` use `sell_currency`/`buy_currency`, not `from_currency`/`to_currency`
|
|
73
|
+
- `Quote.create` requires `validity` (`MIN_1`, `MIN_15`, `MIN_30`, `HR_1`, `HR_4`, `HR_8`, or `HR_24`)
|
|
74
|
+
|
|
75
|
+
### Removed
|
|
76
|
+
- `Rate.list` — the endpoint has no "list all rates" concept, only `.retrieve(sell_currency:, buy_currency:)`
|
|
77
|
+
- `PaymentMethod.delete` and `PaymentMethod#detach` — replaced by `#disable`, the real lifecycle operation
|
|
78
|
+
|
|
3
79
|
## [0.6.0] - 2026-08-28
|
|
4
80
|
|
|
5
81
|
### Fixed
|
data/README.md
CHANGED
|
@@ -6,17 +6,22 @@ A Ruby client library for the [Airwallex API](https://www.airwallex.com/docs/api
|
|
|
6
6
|
|
|
7
7
|
This gem provides a Ruby interface to Airwallex's payment infrastructure, designed for Ruby 3.1+ applications. It includes core functionality for authentication management, idempotency guarantees, webhook verification, and multi-environment support.
|
|
8
8
|
|
|
9
|
-
**Current Features
|
|
9
|
+
**Current Features:**
|
|
10
10
|
|
|
11
11
|
- **Authentication**: Bearer token authentication with automatic refresh
|
|
12
|
-
- **Payment Acceptance**: Payment
|
|
13
|
-
- **Payouts**:
|
|
12
|
+
- **Payment Acceptance**: Payment intents, refunds, payment methods, customers, disputes
|
|
13
|
+
- **Payouts**: Transfers, batch transfers, and beneficiary management
|
|
14
|
+
- **Foreign Exchange**: Real-time rates, locked quotes, and currency conversions
|
|
15
|
+
- **Global Accounts**: Virtual account numbers (VANs), inbound transaction reconciliation, aliases, and direct debit mandates
|
|
16
|
+
- **Billing**: Products, prices, billing customers, and subscriptions for recurring/instalment billing
|
|
17
|
+
- **Recurring Payments**: Payment consents and payment sources for merchant-initiated (off-session) charges
|
|
18
|
+
- **Scale**: Connected accounts, funds splits, and charges for platforms onboarding sub-merchants
|
|
14
19
|
- **Idempotency**: Automatic request deduplication for safe retries
|
|
15
20
|
- **Pagination**: Unified interface over cursor-based and offset-based pagination
|
|
16
21
|
- **Webhook Security**: HMAC-SHA256 signature verification with replay protection
|
|
17
22
|
- **Sandbox Support**: Full testing environment for development
|
|
18
23
|
|
|
19
|
-
**
|
|
24
|
+
**Not yet implemented:** Card issuing.
|
|
20
25
|
|
|
21
26
|
## Installation
|
|
22
27
|
|
|
@@ -82,14 +87,30 @@ payment_intent.confirm(
|
|
|
82
87
|
```ruby
|
|
83
88
|
# Create a beneficiary
|
|
84
89
|
beneficiary = Airwallex::Beneficiary.create(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
nickname: 'Acme Corp',
|
|
91
|
+
payer_entity_type: 'COMPANY',
|
|
92
|
+
transfer_methods: ['LOCAL'],
|
|
93
|
+
beneficiary: {
|
|
94
|
+
entity_type: 'COMPANY',
|
|
95
|
+
company_name: 'Acme Corp',
|
|
96
|
+
address: {
|
|
97
|
+
country_code: 'AU',
|
|
98
|
+
city: 'Melbourne',
|
|
99
|
+
state: 'VIC',
|
|
100
|
+
postcode: '3000',
|
|
101
|
+
street_address: '15 William Street'
|
|
102
|
+
},
|
|
103
|
+
bank_details: {
|
|
104
|
+
account_name: 'Acme Corp',
|
|
105
|
+
account_number: '12750852',
|
|
106
|
+
account_currency: 'AUD',
|
|
107
|
+
bank_country_code: 'AU',
|
|
108
|
+
bank_name: 'National Australia Bank',
|
|
109
|
+
account_routing_type1: 'bsb',
|
|
110
|
+
account_routing_value1: '083064',
|
|
111
|
+
local_clearing_system: 'BANK_TRANSFER'
|
|
112
|
+
}
|
|
113
|
+
}
|
|
93
114
|
)
|
|
94
115
|
|
|
95
116
|
# Execute transfer
|
|
@@ -127,6 +148,7 @@ refunds = Airwallex::Refund.list(payment_intent_id: payment_intent.id)
|
|
|
127
148
|
```ruby
|
|
128
149
|
# Create a customer
|
|
129
150
|
customer = Airwallex::Customer.create(
|
|
151
|
+
merchant_customer_id: 'internal_customer_001',
|
|
130
152
|
email: 'customer@example.com',
|
|
131
153
|
first_name: 'John',
|
|
132
154
|
last_name: 'Doe'
|
|
@@ -152,6 +174,9 @@ payment_intent.confirm(payment_method_id: payment_method.id)
|
|
|
152
174
|
|
|
153
175
|
# List customer's payment methods
|
|
154
176
|
methods = customer.payment_methods
|
|
177
|
+
|
|
178
|
+
# Disable a payment method (PaymentMethods have no delete/detach endpoint)
|
|
179
|
+
payment_method.disable
|
|
155
180
|
```
|
|
156
181
|
|
|
157
182
|
### Batch Transfers
|
|
@@ -190,8 +215,8 @@ puts "Dispute amount: #{dispute.amount} #{dispute.currency}"
|
|
|
190
215
|
puts "Reason: #{dispute.reason}"
|
|
191
216
|
puts "Evidence due: #{dispute.evidence_due_by}"
|
|
192
217
|
|
|
193
|
-
#
|
|
194
|
-
dispute.
|
|
218
|
+
# Challenge the dispute with evidence
|
|
219
|
+
dispute.challenge(
|
|
195
220
|
customer_communication: 'Email showing delivery confirmation',
|
|
196
221
|
shipping_tracking_number: '1Z999AA10123456784',
|
|
197
222
|
shipping_documentation: 'Proof of delivery with signature'
|
|
@@ -199,6 +224,9 @@ dispute.submit_evidence(
|
|
|
199
224
|
|
|
200
225
|
# Or accept dispute without challenging
|
|
201
226
|
dispute.accept
|
|
227
|
+
|
|
228
|
+
# List the payment intents related to a dispute
|
|
229
|
+
dispute.related_payment_intents
|
|
202
230
|
```
|
|
203
231
|
|
|
204
232
|
### Foreign Exchange & Multi-Currency
|
|
@@ -250,6 +278,144 @@ puts "USD Available: #{usd_balance.available_amount}"
|
|
|
250
278
|
puts "USD Total: #{usd_balance.total_amount}"
|
|
251
279
|
```
|
|
252
280
|
|
|
281
|
+
### Global Accounts (Virtual Account Numbers)
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
# Provision a VAN
|
|
285
|
+
account = Airwallex::GlobalAccount.create(
|
|
286
|
+
country_code: 'AU',
|
|
287
|
+
nick_name: 'booking_12345',
|
|
288
|
+
required_features: [{ transfer_method: 'LOCAL' }]
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# List inbound transactions for reconciliation
|
|
292
|
+
account.transactions.each { |txn| puts "#{txn.amount} from #{txn.remitter}" }
|
|
293
|
+
|
|
294
|
+
# Add and verify an alias (e.g. a phone number or email VAN)
|
|
295
|
+
alias_record = account.create_alias(type: 'PAYID_PHONE', value: '+61400000000')
|
|
296
|
+
alias_record.submit_verification_code(code: '123456')
|
|
297
|
+
account.aliases
|
|
298
|
+
|
|
299
|
+
# Manage direct debit mandates
|
|
300
|
+
account.mandates
|
|
301
|
+
mandate = account.mandate('mandate_id')
|
|
302
|
+
mandate.cancel
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Billing & Subscriptions
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
# Create a billing customer
|
|
309
|
+
billing_customer = Airwallex::BillingCustomer.create(
|
|
310
|
+
name: 'Acme Corp',
|
|
311
|
+
email: 'billing@acme.com',
|
|
312
|
+
type: 'BUSINESS',
|
|
313
|
+
default_billing_currency: 'AUD',
|
|
314
|
+
default_legal_entity_id: connected_account.legal_entity_id,
|
|
315
|
+
address: {
|
|
316
|
+
street: '15 William Street',
|
|
317
|
+
city: 'Melbourne',
|
|
318
|
+
state: 'VIC',
|
|
319
|
+
postcode: '3000',
|
|
320
|
+
country_code: 'AU'
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Define a product and a recurring price
|
|
325
|
+
product = Airwallex::BillingProduct.create(name: 'Pro Plan')
|
|
326
|
+
price = Airwallex::BillingPrice.create(
|
|
327
|
+
product_id: product.id,
|
|
328
|
+
currency: 'AUD',
|
|
329
|
+
unit_amount: 99.00,
|
|
330
|
+
pricing_model: 'PER_UNIT',
|
|
331
|
+
recurring: { period: 1, period_unit: 'MONTH' }
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
# Subscriptions bill against a PaymentSource, not a PaymentConsent directly
|
|
335
|
+
# (see "Recurring Payments" below for how to obtain one)
|
|
336
|
+
subscription = Airwallex::BillingSubscription.create(
|
|
337
|
+
billing_customer_id: billing_customer.id,
|
|
338
|
+
currency: 'AUD',
|
|
339
|
+
collection_method: 'AUTO_CHARGE',
|
|
340
|
+
legal_entity_id: connected_account.legal_entity_id,
|
|
341
|
+
payment_source_id: payment_source.id,
|
|
342
|
+
starts_at: '2026-09-01T00:00:00+1000',
|
|
343
|
+
items: [{ price_id: price.id, quantity: 1 }]
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Inspect subscription line items
|
|
347
|
+
subscription.items
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Recurring Payments (Payment Consents & Sources)
|
|
351
|
+
|
|
352
|
+
Off-session charges flow through three linked resources: a `PaymentMethod` is
|
|
353
|
+
consented to future charges via a `PaymentConsent`, which then backs a
|
|
354
|
+
`PaymentSource` that `BillingSubscription` (and other billing resources) can
|
|
355
|
+
charge automatically.
|
|
356
|
+
|
|
357
|
+
```ruby
|
|
358
|
+
# Consent to future merchant-initiated charges on a saved payment method
|
|
359
|
+
consent = Airwallex::PaymentConsent.create(
|
|
360
|
+
customer_id: customer.id,
|
|
361
|
+
payment_method: { id: payment_method.id },
|
|
362
|
+
next_triggered_by: 'merchant',
|
|
363
|
+
merchant_trigger_reason: 'unscheduled'
|
|
364
|
+
)
|
|
365
|
+
consent.verify(payment_method: { card: { cvc: '123' } })
|
|
366
|
+
|
|
367
|
+
# Wrap the consented payment method as a Payment Source for Billing
|
|
368
|
+
# external_id is the PaymentMethod's id, not the PaymentConsent's id
|
|
369
|
+
payment_source = Airwallex::PaymentSource.create(
|
|
370
|
+
billing_customer_id: billing_customer.id,
|
|
371
|
+
external_id: payment_method.id,
|
|
372
|
+
linked_payment_account_id: connected_account.id
|
|
373
|
+
)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Scale (Connected Accounts, Funds Splits, Charges)
|
|
377
|
+
|
|
378
|
+
For platforms onboarding sub-merchants:
|
|
379
|
+
|
|
380
|
+
```ruby
|
|
381
|
+
# Onboard a connected account
|
|
382
|
+
# account_details is a required top-level wrapper; nickname/primary_contact/
|
|
383
|
+
# customer_agreements are top-level siblings, not nested inside it.
|
|
384
|
+
connected_account = Airwallex::ConnectedAccount.create(
|
|
385
|
+
nickname: 'Sub-merchant Co',
|
|
386
|
+
primary_contact: { email: 'contact@submerchant.com' },
|
|
387
|
+
customer_agreements: {
|
|
388
|
+
agreed_to_terms_and_conditions: true,
|
|
389
|
+
agreed_to_data_usage: true,
|
|
390
|
+
terms_and_conditions: { service_agreement_type: 'FULL' }
|
|
391
|
+
},
|
|
392
|
+
account_details: {
|
|
393
|
+
legal_entity_type: 'BUSINESS',
|
|
394
|
+
business_details: {
|
|
395
|
+
business_name: 'Sub-merchant Co',
|
|
396
|
+
business_structure: 'COMPANY'
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
)
|
|
400
|
+
connected_account.legal_entity_id
|
|
401
|
+
connected_account.agree_to_terms_and_conditions
|
|
402
|
+
connected_account.submit
|
|
403
|
+
|
|
404
|
+
# Inspect the platform's own account
|
|
405
|
+
Airwallex::ConnectedAccount.current
|
|
406
|
+
Airwallex::ConnectedAccount.wallet_info
|
|
407
|
+
|
|
408
|
+
# Split a charge's funds between the platform and a connected account
|
|
409
|
+
split = Airwallex::FundsSplit.create(
|
|
410
|
+
payment_intent_id: payment_intent.id,
|
|
411
|
+
splits: [{ account_id: connected_account.id, amount: 10.00 }]
|
|
412
|
+
)
|
|
413
|
+
split.release
|
|
414
|
+
|
|
415
|
+
# Charges (create, retrieve, list)
|
|
416
|
+
charge = Airwallex::Charge.retrieve('charge_id')
|
|
417
|
+
```
|
|
418
|
+
|
|
253
419
|
## Usage
|
|
254
420
|
|
|
255
421
|
### Authentication
|
|
@@ -367,10 +533,16 @@ end
|
|
|
367
533
|
```
|
|
368
534
|
lib/airwallex/
|
|
369
535
|
├── api_operations/ # CRUD operation mixins (Create, Retrieve, List, Update, Delete)
|
|
370
|
-
├── resources/ # Implemented resources
|
|
371
|
-
│
|
|
372
|
-
│
|
|
373
|
-
│
|
|
536
|
+
├── resources/ # Implemented resources, grouped by API area:
|
|
537
|
+
│ │ # payment acceptance - payment_intent, refund, payment_method, customer, dispute
|
|
538
|
+
│ │ # payouts - transfer, batch_transfer, beneficiary
|
|
539
|
+
│ │ # foreign exchange - rate, quote, conversion, balance
|
|
540
|
+
│ │ # global accounts - global_account(+_alias, _mandate, _transaction)
|
|
541
|
+
│ │ # billing - billing_customer, billing_product, billing_price,
|
|
542
|
+
│ │ # billing_subscription(+_item)
|
|
543
|
+
│ │ # recurring payments - payment_consent, payment_source
|
|
544
|
+
│ │ # scale - connected_account, account_amendment, funds_split, charge
|
|
545
|
+
│ └── ... # see lib/airwallex/resources/ for the full, current list
|
|
374
546
|
├── api_resource.rb # Base resource class with dynamic attributes
|
|
375
547
|
├── list_object.rb # Pagination wrapper
|
|
376
548
|
├── errors.rb # Exception hierarchy
|
|
@@ -418,26 +590,40 @@ end
|
|
|
418
590
|
- **Payment Acceptance**:
|
|
419
591
|
- PaymentIntent (create, retrieve, list, update, confirm, cancel, capture)
|
|
420
592
|
- Refund (create, retrieve, list)
|
|
421
|
-
- PaymentMethod (create, retrieve, list, update,
|
|
593
|
+
- PaymentMethod (create, retrieve, list, update, disable)
|
|
422
594
|
- Customer (create, retrieve, list, update, delete)
|
|
423
|
-
- Dispute (retrieve, list, accept,
|
|
595
|
+
- Dispute (retrieve, list, accept, challenge, related_payment_intents)
|
|
424
596
|
- **Payouts**:
|
|
425
597
|
- Transfer (create, retrieve, list, cancel)
|
|
426
|
-
- Beneficiary (create, retrieve, list, delete)
|
|
598
|
+
- Beneficiary (create, retrieve, list, update, delete, validate, verify_account, api_schema, form_schema, supported_financial_institutions)
|
|
427
599
|
- BatchTransfer (create, retrieve, list)
|
|
428
600
|
- **Foreign Exchange & Multi-Currency**:
|
|
429
|
-
- Rate (retrieve
|
|
601
|
+
- Rate (retrieve) - Real-time exchange rate queries
|
|
430
602
|
- Quote (create, retrieve) - Lock exchange rates with expiration tracking
|
|
431
603
|
- Conversion (create, retrieve, list) - Execute currency conversions
|
|
432
604
|
- Balance (list, retrieve) - Query account balances across currencies
|
|
605
|
+
- **Global Accounts**:
|
|
606
|
+
- GlobalAccount (create, retrieve, list, update, close, generate_statement_letter, create_alias, aliases, mandate, mandates)
|
|
607
|
+
- GlobalAccountTransaction, GlobalAccountAlias, GlobalAccountMandate (list/lifecycle actions scoped to a parent account)
|
|
608
|
+
- **Billing & Subscriptions**:
|
|
609
|
+
- BillingCustomer (create, retrieve, list, update, bank_transfer_instructions)
|
|
610
|
+
- BillingProduct (create, retrieve, list, update)
|
|
611
|
+
- BillingPrice (create, retrieve, list, update)
|
|
612
|
+
- BillingSubscription (create, retrieve, list, update, items)
|
|
613
|
+
- BillingSubscriptionItem (scoped to a parent subscription)
|
|
614
|
+
- **Recurring Payments**:
|
|
615
|
+
- PaymentConsent (create, retrieve, list, update, verify, verify_continue, disable)
|
|
616
|
+
- PaymentSource (create, retrieve, list)
|
|
617
|
+
- **Scale**:
|
|
618
|
+
- ConnectedAccount (create, retrieve, list, update, current, wallet_info, submit, agree_to_terms_and_conditions, suspend, reactivate)
|
|
619
|
+
- AccountAmendment (create, retrieve) - requires Admin-level API key permissions
|
|
620
|
+
- FundsSplit (create, retrieve, list, release)
|
|
621
|
+
- Charge (create, retrieve, list)
|
|
433
622
|
- **Webhooks**: Event handling, HMAC-SHA256 signature verification
|
|
434
623
|
|
|
435
624
|
### Coming in Future Versions
|
|
436
625
|
|
|
437
|
-
- Global accounts
|
|
438
626
|
- Card issuing
|
|
439
|
-
- Subscriptions and billing
|
|
440
|
-
- Virtual account numbers
|
|
441
627
|
|
|
442
628
|
## Environment Support
|
|
443
629
|
|
|
@@ -4,12 +4,12 @@ module Airwallex
|
|
|
4
4
|
module APIOperations
|
|
5
5
|
module Delete
|
|
6
6
|
def delete(id, opts = {})
|
|
7
|
-
Airwallex.client.
|
|
8
|
-
"#{resource_path}/#{id}",
|
|
7
|
+
response = Airwallex.client.post(
|
|
8
|
+
"#{resource_path}/#{id}/delete",
|
|
9
9
|
{},
|
|
10
10
|
opts[:headers] || {}
|
|
11
11
|
)
|
|
12
|
-
true
|
|
12
|
+
response == true
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
end
|
|
@@ -9,8 +9,8 @@ module Airwallex
|
|
|
9
9
|
|
|
10
10
|
module ClassMethods
|
|
11
11
|
def update(id, params = {}, opts = {})
|
|
12
|
-
response = Airwallex.client.
|
|
13
|
-
"#{resource_path}/#{id}",
|
|
12
|
+
response = Airwallex.client.post(
|
|
13
|
+
"#{resource_path}/#{id}/update",
|
|
14
14
|
params,
|
|
15
15
|
opts[:headers] || {}
|
|
16
16
|
)
|
|
@@ -20,8 +20,8 @@ module Airwallex
|
|
|
20
20
|
|
|
21
21
|
# Instance methods
|
|
22
22
|
def update(params = {})
|
|
23
|
-
response = Airwallex.client.
|
|
24
|
-
"#{self.class.resource_path}/#{id}",
|
|
23
|
+
response = Airwallex.client.post(
|
|
24
|
+
"#{self.class.resource_path}/#{id}/update",
|
|
25
25
|
params
|
|
26
26
|
)
|
|
27
27
|
refresh_from(response)
|
|
@@ -24,6 +24,28 @@ module Airwallex
|
|
|
24
24
|
raise NotImplementedError, "#{self} must implement .resource_path"
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Perform a one-off POST that returns a plain, symbolized Hash rather
|
|
28
|
+
# than an APIResource — for actions with no id/refresh lifecycle of
|
|
29
|
+
# their own (e.g. Beneficiary.verify_account, a bank-detail check that
|
|
30
|
+
# never becomes a resource).
|
|
31
|
+
#
|
|
32
|
+
# @param path [String] the request path
|
|
33
|
+
# @param params [Hash] request body
|
|
34
|
+
# @return [Hash] symbolized response
|
|
35
|
+
def self.symbolized_post(path, params = {})
|
|
36
|
+
Util.deep_symbolize_keys(Airwallex.client.post(path, params))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Perform a one-off GET that returns a plain, symbolized Hash rather
|
|
40
|
+
# than an APIResource. See .symbolized_post.
|
|
41
|
+
#
|
|
42
|
+
# @param path [String] the request path
|
|
43
|
+
# @param params [Hash] query params
|
|
44
|
+
# @return [Hash] symbolized response
|
|
45
|
+
def self.symbolized_get(path, params = {})
|
|
46
|
+
Util.deep_symbolize_keys(Airwallex.client.get(path, params))
|
|
47
|
+
end
|
|
48
|
+
|
|
27
49
|
# Dynamic attribute accessors
|
|
28
50
|
def method_missing(method_name, *args, &)
|
|
29
51
|
method_str = method_name.to_s
|
data/lib/airwallex/client.rb
CHANGED
|
@@ -25,7 +25,7 @@ module Airwallex
|
|
|
25
25
|
|
|
26
26
|
conn.headers["Content-Type"] = "application/json"
|
|
27
27
|
conn.headers["User-Agent"] = user_agent
|
|
28
|
-
conn.headers["x-api-version"] = config.api_version
|
|
28
|
+
conn.headers["x-api-version"] = config.api_version if config.api_version
|
|
29
29
|
|
|
30
30
|
conn.adapter Faraday.default_adapter
|
|
31
31
|
end
|
|
@@ -10,12 +10,16 @@ module Airwallex
|
|
|
10
10
|
SANDBOX_FILES_URL = "https://files-demo.airwallex.com"
|
|
11
11
|
PRODUCTION_FILES_URL = "https://files.airwallex.com"
|
|
12
12
|
|
|
13
|
-
DEFAULT_API_VERSION = "2024-09-27"
|
|
14
13
|
VALID_ENVIRONMENTS = %i[sandbox production].freeze
|
|
15
14
|
|
|
15
|
+
# api_version is nil by default. Airwallex stores a version on your
|
|
16
|
+
# account and applies it automatically; the x-api-version header is only
|
|
17
|
+
# for exceptional per-call overrides (testing/migrating), not a
|
|
18
|
+
# permanent pin — see https://www.airwallex.com/docs/api/versioning.
|
|
19
|
+
# Set this explicitly if you want reproducible, pinned behavior instead.
|
|
16
20
|
def initialize
|
|
17
21
|
@environment = :sandbox
|
|
18
|
-
@api_version =
|
|
22
|
+
@api_version = nil
|
|
19
23
|
@log_level = :info
|
|
20
24
|
end
|
|
21
25
|
|
data/lib/airwallex/errors.rb
CHANGED
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module Airwallex
|
|
4
4
|
class Error < StandardError
|
|
5
|
+
# @return [String, nil] error code
|
|
6
|
+
# @return [String, nil] param the offending field path, from the response's `source`
|
|
7
|
+
# @return [Hash, Array, nil] details symbolized field-level error detail, e.g. from a
|
|
8
|
+
# validation_failed response (was raw/string-keyed prior to 0.8.0)
|
|
9
|
+
# @return [Integer, nil] http_status
|
|
5
10
|
attr_reader :code, :message, :param, :details, :http_status
|
|
6
11
|
|
|
7
12
|
def initialize(message = nil, code: nil, param: nil, details: nil, http_status: nil)
|
|
8
13
|
@code = code
|
|
9
14
|
@message = message
|
|
10
15
|
@param = param
|
|
11
|
-
@details = details
|
|
16
|
+
@details = Util.deep_symbolize_keys(details)
|
|
12
17
|
@http_status = http_status
|
|
13
18
|
super(message)
|
|
14
19
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
# Represents an amendment to whichever account the request is
|
|
5
|
+
# authenticated as (the platform's own account, or a connected account via
|
|
6
|
+
# x-on-behalf-of). Not nested under /accounts/{id} — scoped implicitly by
|
|
7
|
+
# auth context, same as ConnectedAccount.current.
|
|
8
|
+
#
|
|
9
|
+
# .create requires Admin-level API key permissions, a separate tier from
|
|
10
|
+
# the normal per-resource Read/Write scopes.
|
|
11
|
+
#
|
|
12
|
+
# @example Submit an amendment
|
|
13
|
+
# # `target` identifies the dotted path being amended; the changed
|
|
14
|
+
# # section is a top-level sibling keyed by its own name (e.g.
|
|
15
|
+
# # store_details), not wrapped in a generic "changes" key.
|
|
16
|
+
# amendment = Airwallex::AccountAmendment.create(
|
|
17
|
+
# target: "account_details.store_details",
|
|
18
|
+
# store_details: { store_name: "New Store Name" }
|
|
19
|
+
# )
|
|
20
|
+
class AccountAmendment < APIResource
|
|
21
|
+
extend APIOperations::Create
|
|
22
|
+
extend APIOperations::Retrieve
|
|
23
|
+
|
|
24
|
+
# @return [String] API resource path for account amendments
|
|
25
|
+
def self.resource_path
|
|
26
|
+
"/api/v1/account/amendments"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -1,14 +1,110 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Airwallex
|
|
4
|
+
# @example Create a beneficiary
|
|
5
|
+
# # nickname/payer_entity_type/transfer_methods are top-level siblings of
|
|
6
|
+
# # `beneficiary`, not nested inside it. request_id is optional (the
|
|
7
|
+
# # gem's Idempotency middleware injects one automatically). Call
|
|
8
|
+
# # .api_schema / .form_schema for the exact required fields per
|
|
9
|
+
# # entity_type + bank_country_code.
|
|
10
|
+
# beneficiary = Airwallex::Beneficiary.create(
|
|
11
|
+
# nickname: "Acme Corp",
|
|
12
|
+
# payer_entity_type: "COMPANY",
|
|
13
|
+
# transfer_methods: ["LOCAL"],
|
|
14
|
+
# beneficiary: {
|
|
15
|
+
# entity_type: "COMPANY",
|
|
16
|
+
# company_name: "Acme Corp",
|
|
17
|
+
# address: {
|
|
18
|
+
# country_code: "AU",
|
|
19
|
+
# city: "Melbourne",
|
|
20
|
+
# state: "VIC",
|
|
21
|
+
# postcode: "3000",
|
|
22
|
+
# street_address: "15 William Street"
|
|
23
|
+
# },
|
|
24
|
+
# bank_details: {
|
|
25
|
+
# account_name: "Acme Corp",
|
|
26
|
+
# account_number: "12750852",
|
|
27
|
+
# account_currency: "AUD",
|
|
28
|
+
# bank_country_code: "AU",
|
|
29
|
+
# bank_name: "National Australia Bank",
|
|
30
|
+
# account_routing_type1: "bsb",
|
|
31
|
+
# account_routing_value1: "083064",
|
|
32
|
+
# local_clearing_system: "BANK_TRANSFER" # country-specific enum
|
|
33
|
+
# }
|
|
34
|
+
# }
|
|
35
|
+
# )
|
|
36
|
+
#
|
|
37
|
+
# @example Update a beneficiary
|
|
38
|
+
# # .update doesn't support a partial patch — resend the full payload
|
|
39
|
+
# # with your change merged in.
|
|
40
|
+
# beneficiary.update(
|
|
41
|
+
# transfer_methods: ["LOCAL"],
|
|
42
|
+
# beneficiary: {
|
|
43
|
+
# entity_type: "COMPANY",
|
|
44
|
+
# company_name: "Acme Corp Ltd",
|
|
45
|
+
# bank_details: { account_currency: "AUD", bank_country_code: "AU", ... }
|
|
46
|
+
# }
|
|
47
|
+
# )
|
|
4
48
|
class Beneficiary < APIResource
|
|
5
49
|
extend APIOperations::Create
|
|
6
50
|
extend APIOperations::Retrieve
|
|
7
51
|
extend APIOperations::List
|
|
8
52
|
extend APIOperations::Delete
|
|
53
|
+
include APIOperations::Update
|
|
54
|
+
|
|
55
|
+
API_SCHEMA_PATH = "/api/v1/beneficiary_api_schemas/generate"
|
|
56
|
+
FORM_SCHEMA_PATH = "/api/v1/beneficiary_form_schemas/generate"
|
|
57
|
+
SUPPORTED_FINANCIAL_INSTITUTIONS_PATH = "/api/v1/beneficiary_form_schemas/supported_financial_institutions"
|
|
9
58
|
|
|
10
59
|
def self.resource_path
|
|
11
60
|
"/api/v1/beneficiaries"
|
|
12
61
|
end
|
|
62
|
+
|
|
63
|
+
# Validate beneficiary details (address, bank details, entity type, transfer
|
|
64
|
+
# method) before attempting to create the beneficiary.
|
|
65
|
+
#
|
|
66
|
+
# @param params [Hash] the same shape of params you'd pass to .create
|
|
67
|
+
# @return [Hash] symbolized validation result
|
|
68
|
+
def self.validate(params = {})
|
|
69
|
+
symbolized_post("#{resource_path}/validate", params)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Verify ownership of a beneficiary's bank account via Confirmation of
|
|
73
|
+
# Payee (CoP) before creation.
|
|
74
|
+
#
|
|
75
|
+
# @param params [Hash] beneficiary bank account details to verify
|
|
76
|
+
# @return [Hash] symbolized verification result (status, account_name_match_result)
|
|
77
|
+
def self.verify_account(params = {})
|
|
78
|
+
symbolized_post("#{resource_path}/verify_account", params)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Retrieve the API schema (field validation rules) for beneficiary bank
|
|
82
|
+
# details, keyed by beneficiary type / entity type / bank country.
|
|
83
|
+
#
|
|
84
|
+
# @param params [Hash] e.g. beneficiary_type:, bank_country_code:
|
|
85
|
+
# @return [Hash] symbolized schema response
|
|
86
|
+
def self.api_schema(params = {})
|
|
87
|
+
symbolized_post(API_SCHEMA_PATH, params)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Retrieve the dynamic form schema used to render beneficiary bank-detail
|
|
91
|
+
# forms in the UI, keyed by beneficiary type / entity type / bank country.
|
|
92
|
+
#
|
|
93
|
+
# @param params [Hash] e.g. beneficiary_type:, bank_country_code:
|
|
94
|
+
# @return [Hash] symbolized schema response
|
|
95
|
+
def self.form_schema(params = {})
|
|
96
|
+
symbolized_post(FORM_SCHEMA_PATH, params)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Search financial institutions supported for a given country, currency,
|
|
100
|
+
# entity type, and transfer method, by name keyword.
|
|
101
|
+
#
|
|
102
|
+
# @param params [Hash] required: bank_country_code:, account_currency:,
|
|
103
|
+
# entity_type:, transfer_method:, keyword: (a bank-name search term,
|
|
104
|
+
# min 3 chars)
|
|
105
|
+
# @return [Hash] symbolized response listing supported institutions
|
|
106
|
+
def self.supported_financial_institutions(params = {})
|
|
107
|
+
symbolized_get(SUPPORTED_FINANCIAL_INSTITUTIONS_PATH, params)
|
|
108
|
+
end
|
|
13
109
|
end
|
|
14
110
|
end
|