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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -0
  3. data/README.md +211 -25
  4. data/lib/airwallex/api_operations/delete.rb +3 -3
  5. data/lib/airwallex/api_operations/update.rb +4 -4
  6. data/lib/airwallex/api_resource.rb +22 -0
  7. data/lib/airwallex/client.rb +1 -1
  8. data/lib/airwallex/configuration.rb +6 -2
  9. data/lib/airwallex/errors.rb +6 -1
  10. data/lib/airwallex/resources/account_amendment.rb +29 -0
  11. data/lib/airwallex/resources/beneficiary.rb +96 -0
  12. data/lib/airwallex/resources/billing_customer.rb +48 -0
  13. data/lib/airwallex/resources/billing_price.rb +30 -0
  14. data/lib/airwallex/resources/billing_product.rb +23 -0
  15. data/lib/airwallex/resources/billing_subscription.rb +93 -0
  16. data/lib/airwallex/resources/billing_subscription_item.rb +7 -0
  17. data/lib/airwallex/resources/charge.rb +19 -0
  18. data/lib/airwallex/resources/connected_account.rb +136 -0
  19. data/lib/airwallex/resources/conversion.rb +1 -1
  20. data/lib/airwallex/resources/customer.rb +5 -2
  21. data/lib/airwallex/resources/dispute.rb +48 -28
  22. data/lib/airwallex/resources/funds_split.rb +37 -0
  23. data/lib/airwallex/resources/global_account.rb +137 -0
  24. data/lib/airwallex/resources/global_account_alias.rb +62 -0
  25. data/lib/airwallex/resources/global_account_mandate.rb +26 -0
  26. data/lib/airwallex/resources/global_account_transaction.rb +7 -0
  27. data/lib/airwallex/resources/payment_consent.rb +68 -0
  28. data/lib/airwallex/resources/payment_method.rb +13 -5
  29. data/lib/airwallex/resources/payment_source.rb +52 -0
  30. data/lib/airwallex/resources/quote.rb +5 -3
  31. data/lib/airwallex/resources/rate.rb +2 -7
  32. data/lib/airwallex/util.rb +8 -5
  33. data/lib/airwallex/version.rb +1 -1
  34. data/lib/airwallex.rb +15 -0
  35. metadata +18 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec7542a0e82153bbd60acceadb071eba2d48d328d89fa34d72d8a23dc30aeb45
4
- data.tar.gz: b14a15b3b70878c6291bc6ffd447ccf81d4cab5d86e61b806cc0708bd70590e2
3
+ metadata.gz: be0adbfbafc982fa5c899a9df4230e9c65a68e46d2e7eb248b981e41ccc23bd4
4
+ data.tar.gz: 599bdc078288f934cc5f8b06fced1668605c8e2e6af4763d943a9f227a32e563
5
5
  SHA512:
6
- metadata.gz: 1e401e23580f1347cbf2caae60b985138c59e73a22ba758578c7ffd99b2318859f766e7c0e0a13f55bff4da29a6796029a359a12a9c1db32b213c972244e70a3
7
- data.tar.gz: b7779fbdc8227ae72d20eda510799bb6898edb43c74d7aa6a6a30b7110f304be96b2e17673b02e832c2a9638c48f987b6ecf54027fc4b18f754d218fbaec4398
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 (v0.1.0):**
9
+ **Current Features:**
10
10
 
11
11
  - **Authentication**: Bearer token authentication with automatic refresh
12
- - **Payment Acceptance**: Payment intent creation, confirmation, and management
13
- - **Payouts**: Transfer creation and beneficiary management
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
- **Note:** This is an initial MVP release. Additional resources (FX, cards, refunds, etc.) will be added in future versions.
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
- bank_details: {
86
- account_number: '123456789',
87
- account_routing_type1: 'aba',
88
- account_routing_value1: '026009593',
89
- bank_country_code: 'US'
90
- },
91
- beneficiary_type: 'BUSINESS',
92
- company_name: 'Acme Corp'
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
- # Submit evidence to challenge
194
- dispute.submit_evidence(
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
- ├── payment_intent.rb # Payment acceptance
372
- ├── transfer.rb # Payouts
373
- └── beneficiary.rb # Payout beneficiaries
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, delete, detach)
593
+ - PaymentMethod (create, retrieve, list, update, disable)
422
594
  - Customer (create, retrieve, list, update, delete)
423
- - Dispute (retrieve, list, accept, submit_evidence)
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, list) - Real-time exchange rate queries
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.delete(
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.put(
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.put(
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
@@ -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 = DEFAULT_API_VERSION
22
+ @api_version = nil
19
23
  @log_level = :info
20
24
  end
21
25
 
@@ -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