airwallex 0.7.0 → 0.9.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 +92 -0
- data/README.md +449 -28
- data/lib/airwallex/api_resource.rb +22 -0
- data/lib/airwallex/errors.rb +6 -1
- data/lib/airwallex/resources/account_amendment.rb +39 -0
- data/lib/airwallex/resources/account_offboarding.rb +43 -0
- data/lib/airwallex/resources/beneficiary.rb +10 -10
- data/lib/airwallex/resources/billing_customer.rb +2 -2
- data/lib/airwallex/resources/cardholder.rb +28 -0
- data/lib/airwallex/resources/connected_account.rb +51 -2
- data/lib/airwallex/resources/deposit.rb +115 -0
- data/lib/airwallex/resources/dispute.rb +83 -1
- data/lib/airwallex/resources/global_account.rb +2 -2
- data/lib/airwallex/resources/issuing_transaction.rb +100 -0
- data/lib/airwallex/resources/linked_account.rb +64 -0
- data/lib/airwallex/resources/payment_consent.rb +15 -0
- data/lib/airwallex/resources/payment_intent.rb +26 -0
- data/lib/airwallex/resources/pos_terminal.rb +71 -0
- data/lib/airwallex/resources/rfi.rb +77 -0
- data/lib/airwallex/resources/transfer.rb +36 -0
- data/lib/airwallex/util.rb +8 -5
- data/lib/airwallex/version.rb +1 -1
- data/lib/airwallex.rb +7 -0
- metadata +10 -3
data/README.md
CHANGED
|
@@ -6,17 +6,23 @@ 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
|
|
19
|
+
- **Sandbox Simulations**: Deposits, issuing transactions, disputes, transfer status, Direct Debit mandates, connected account/offboarding review outcomes, RFIs, and POS terminals — see [Sandbox Simulations](#sandbox-simulations)
|
|
14
20
|
- **Idempotency**: Automatic request deduplication for safe retries
|
|
15
21
|
- **Pagination**: Unified interface over cursor-based and offset-based pagination
|
|
16
22
|
- **Webhook Security**: HMAC-SHA256 signature verification with replay protection
|
|
17
23
|
- **Sandbox Support**: Full testing environment for development
|
|
18
24
|
|
|
19
|
-
**
|
|
25
|
+
**Not yet implemented:** live Card issuing resources (Card, Cardholder create/retrieve/list) — only their sandbox Simulation actions are covered today.
|
|
20
26
|
|
|
21
27
|
## Installation
|
|
22
28
|
|
|
@@ -82,14 +88,30 @@ payment_intent.confirm(
|
|
|
82
88
|
```ruby
|
|
83
89
|
# Create a beneficiary
|
|
84
90
|
beneficiary = Airwallex::Beneficiary.create(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
nickname: 'Acme Corp',
|
|
92
|
+
payer_entity_type: 'COMPANY',
|
|
93
|
+
transfer_methods: ['LOCAL'],
|
|
94
|
+
beneficiary: {
|
|
95
|
+
entity_type: 'COMPANY',
|
|
96
|
+
company_name: 'Acme Corp',
|
|
97
|
+
address: {
|
|
98
|
+
country_code: 'AU',
|
|
99
|
+
city: 'Melbourne',
|
|
100
|
+
state: 'VIC',
|
|
101
|
+
postcode: '3000',
|
|
102
|
+
street_address: '15 William Street'
|
|
103
|
+
},
|
|
104
|
+
bank_details: {
|
|
105
|
+
account_name: 'Acme Corp',
|
|
106
|
+
account_number: '12750852',
|
|
107
|
+
account_currency: 'AUD',
|
|
108
|
+
bank_country_code: 'AU',
|
|
109
|
+
bank_name: 'National Australia Bank',
|
|
110
|
+
account_routing_type1: 'bsb',
|
|
111
|
+
account_routing_value1: '083064',
|
|
112
|
+
local_clearing_system: 'BANK_TRANSFER'
|
|
113
|
+
}
|
|
114
|
+
}
|
|
93
115
|
)
|
|
94
116
|
|
|
95
117
|
# Execute transfer
|
|
@@ -127,6 +149,7 @@ refunds = Airwallex::Refund.list(payment_intent_id: payment_intent.id)
|
|
|
127
149
|
```ruby
|
|
128
150
|
# Create a customer
|
|
129
151
|
customer = Airwallex::Customer.create(
|
|
152
|
+
merchant_customer_id: 'internal_customer_001',
|
|
130
153
|
email: 'customer@example.com',
|
|
131
154
|
first_name: 'John',
|
|
132
155
|
last_name: 'Doe'
|
|
@@ -152,6 +175,9 @@ payment_intent.confirm(payment_method_id: payment_method.id)
|
|
|
152
175
|
|
|
153
176
|
# List customer's payment methods
|
|
154
177
|
methods = customer.payment_methods
|
|
178
|
+
|
|
179
|
+
# Disable a payment method (PaymentMethods have no delete/detach endpoint)
|
|
180
|
+
payment_method.disable
|
|
155
181
|
```
|
|
156
182
|
|
|
157
183
|
### Batch Transfers
|
|
@@ -190,8 +216,8 @@ puts "Dispute amount: #{dispute.amount} #{dispute.currency}"
|
|
|
190
216
|
puts "Reason: #{dispute.reason}"
|
|
191
217
|
puts "Evidence due: #{dispute.evidence_due_by}"
|
|
192
218
|
|
|
193
|
-
#
|
|
194
|
-
dispute.
|
|
219
|
+
# Challenge the dispute with evidence
|
|
220
|
+
dispute.challenge(
|
|
195
221
|
customer_communication: 'Email showing delivery confirmation',
|
|
196
222
|
shipping_tracking_number: '1Z999AA10123456784',
|
|
197
223
|
shipping_documentation: 'Proof of delivery with signature'
|
|
@@ -199,6 +225,9 @@ dispute.submit_evidence(
|
|
|
199
225
|
|
|
200
226
|
# Or accept dispute without challenging
|
|
201
227
|
dispute.accept
|
|
228
|
+
|
|
229
|
+
# List the payment intents related to a dispute
|
|
230
|
+
dispute.related_payment_intents
|
|
202
231
|
```
|
|
203
232
|
|
|
204
233
|
### Foreign Exchange & Multi-Currency
|
|
@@ -250,6 +279,368 @@ puts "USD Available: #{usd_balance.available_amount}"
|
|
|
250
279
|
puts "USD Total: #{usd_balance.total_amount}"
|
|
251
280
|
```
|
|
252
281
|
|
|
282
|
+
### Global Accounts (Virtual Account Numbers)
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
# Provision a VAN
|
|
286
|
+
account = Airwallex::GlobalAccount.create(
|
|
287
|
+
country_code: 'AU',
|
|
288
|
+
nick_name: 'booking_12345',
|
|
289
|
+
required_features: [{ transfer_method: 'LOCAL' }]
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# List inbound transactions for reconciliation
|
|
293
|
+
account.transactions.each { |txn| puts "#{txn.amount} from #{txn.remitter}" }
|
|
294
|
+
|
|
295
|
+
# Add and verify an alias (e.g. a phone number or email VAN)
|
|
296
|
+
alias_record = account.create_alias(type: 'PAYID_PHONE', value: '+61400000000')
|
|
297
|
+
alias_record.submit_verification_code(code: '123456')
|
|
298
|
+
account.aliases
|
|
299
|
+
|
|
300
|
+
# Manage direct debit mandates
|
|
301
|
+
account.mandates
|
|
302
|
+
mandate = account.mandate('mandate_id')
|
|
303
|
+
mandate.cancel
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Billing & Subscriptions
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
# Create a billing customer
|
|
310
|
+
billing_customer = Airwallex::BillingCustomer.create(
|
|
311
|
+
name: 'Acme Corp',
|
|
312
|
+
email: 'billing@acme.com',
|
|
313
|
+
type: 'BUSINESS',
|
|
314
|
+
default_billing_currency: 'AUD',
|
|
315
|
+
default_legal_entity_id: connected_account.legal_entity_id,
|
|
316
|
+
address: {
|
|
317
|
+
street: '15 William Street',
|
|
318
|
+
city: 'Melbourne',
|
|
319
|
+
state: 'VIC',
|
|
320
|
+
postcode: '3000',
|
|
321
|
+
country_code: 'AU'
|
|
322
|
+
}
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Define a product and a recurring price
|
|
326
|
+
product = Airwallex::BillingProduct.create(name: 'Pro Plan')
|
|
327
|
+
price = Airwallex::BillingPrice.create(
|
|
328
|
+
product_id: product.id,
|
|
329
|
+
currency: 'AUD',
|
|
330
|
+
unit_amount: 99.00,
|
|
331
|
+
pricing_model: 'PER_UNIT',
|
|
332
|
+
recurring: { period: 1, period_unit: 'MONTH' }
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Subscriptions bill against a PaymentSource, not a PaymentConsent directly
|
|
336
|
+
# (see "Recurring Payments" below for how to obtain one)
|
|
337
|
+
subscription = Airwallex::BillingSubscription.create(
|
|
338
|
+
billing_customer_id: billing_customer.id,
|
|
339
|
+
currency: 'AUD',
|
|
340
|
+
collection_method: 'AUTO_CHARGE',
|
|
341
|
+
legal_entity_id: connected_account.legal_entity_id,
|
|
342
|
+
payment_source_id: payment_source.id,
|
|
343
|
+
starts_at: '2026-09-01T00:00:00+1000',
|
|
344
|
+
items: [{ price_id: price.id, quantity: 1 }]
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Inspect subscription line items
|
|
348
|
+
subscription.items
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Recurring Payments (Payment Consents & Sources)
|
|
352
|
+
|
|
353
|
+
Off-session charges flow through three linked resources: a `PaymentMethod` is
|
|
354
|
+
consented to future charges via a `PaymentConsent`, which then backs a
|
|
355
|
+
`PaymentSource` that `BillingSubscription` (and other billing resources) can
|
|
356
|
+
charge automatically.
|
|
357
|
+
|
|
358
|
+
```ruby
|
|
359
|
+
# Consent to future merchant-initiated charges on a saved payment method
|
|
360
|
+
consent = Airwallex::PaymentConsent.create(
|
|
361
|
+
customer_id: customer.id,
|
|
362
|
+
payment_method: { id: payment_method.id },
|
|
363
|
+
next_triggered_by: 'merchant',
|
|
364
|
+
merchant_trigger_reason: 'unscheduled'
|
|
365
|
+
)
|
|
366
|
+
consent.verify(payment_method: { card: { cvc: '123' } })
|
|
367
|
+
|
|
368
|
+
# Wrap the consented payment method as a Payment Source for Billing
|
|
369
|
+
# external_id is the PaymentMethod's id, not the PaymentConsent's id
|
|
370
|
+
payment_source = Airwallex::PaymentSource.create(
|
|
371
|
+
billing_customer_id: billing_customer.id,
|
|
372
|
+
external_id: payment_method.id,
|
|
373
|
+
linked_payment_account_id: connected_account.id
|
|
374
|
+
)
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Scale (Connected Accounts, Funds Splits, Charges)
|
|
378
|
+
|
|
379
|
+
For platforms onboarding sub-merchants:
|
|
380
|
+
|
|
381
|
+
```ruby
|
|
382
|
+
# Onboard a connected account
|
|
383
|
+
# account_details is a required top-level wrapper; nickname/primary_contact/
|
|
384
|
+
# customer_agreements are top-level siblings, not nested inside it.
|
|
385
|
+
connected_account = Airwallex::ConnectedAccount.create(
|
|
386
|
+
nickname: 'Sub-merchant Co',
|
|
387
|
+
primary_contact: { email: 'contact@submerchant.com' },
|
|
388
|
+
customer_agreements: {
|
|
389
|
+
agreed_to_terms_and_conditions: true,
|
|
390
|
+
agreed_to_data_usage: true,
|
|
391
|
+
terms_and_conditions: { service_agreement_type: 'FULL' }
|
|
392
|
+
},
|
|
393
|
+
account_details: {
|
|
394
|
+
legal_entity_type: 'BUSINESS',
|
|
395
|
+
business_details: {
|
|
396
|
+
business_name: 'Sub-merchant Co',
|
|
397
|
+
business_structure: 'COMPANY'
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
)
|
|
401
|
+
connected_account.legal_entity_id
|
|
402
|
+
connected_account.agree_to_terms_and_conditions
|
|
403
|
+
connected_account.submit
|
|
404
|
+
|
|
405
|
+
# Inspect the platform's own account
|
|
406
|
+
Airwallex::ConnectedAccount.current
|
|
407
|
+
Airwallex::ConnectedAccount.wallet_info
|
|
408
|
+
|
|
409
|
+
# Split a charge's funds between the platform and a connected account
|
|
410
|
+
split = Airwallex::FundsSplit.create(
|
|
411
|
+
payment_intent_id: payment_intent.id,
|
|
412
|
+
splits: [{ account_id: connected_account.id, amount: 10.00 }]
|
|
413
|
+
)
|
|
414
|
+
split.release
|
|
415
|
+
|
|
416
|
+
# Charges (create, retrieve, list)
|
|
417
|
+
charge = Airwallex::Charge.retrieve('charge_id')
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Sandbox Simulations
|
|
421
|
+
|
|
422
|
+
Sandbox-only endpoints (`/api/v1/simulation/...`) that stand in for the bank,
|
|
423
|
+
card network, issuing bank, or shopper — for driving async/manual lifecycles
|
|
424
|
+
in tests without waiting on real-world events. See
|
|
425
|
+
https://www.airwallex.com/docs/api/simulation/ for the full reference. All
|
|
426
|
+
of these raise `Airwallex::Error` (same as any other endpoint) if called
|
|
427
|
+
against a production API key.
|
|
428
|
+
|
|
429
|
+
#### Deposits
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
# Simulate an inbound bank transfer landing in a Global Account — auto-settles
|
|
433
|
+
# on its own within a few seconds, same as a real bank transfer
|
|
434
|
+
Airwallex::Deposit.simulate_create(amount: 100.00, global_account_id: 'gacc_123')
|
|
435
|
+
|
|
436
|
+
# .simulate_settle/.simulate_reject/.simulate_reverse are for Direct Debit
|
|
437
|
+
# deposits specifically, made via the real (non-simulation) .create — a real
|
|
438
|
+
# direct debit pull takes days to clear, so the sandbox needs a way to force it
|
|
439
|
+
pending = Airwallex::Deposit.create(funding_source_id: 'la_123', amount: 50.00, currency: 'AUD')
|
|
440
|
+
pending.simulate_settle # -> SETTLED
|
|
441
|
+
# or: pending.simulate_reject -> REJECTED
|
|
442
|
+
|
|
443
|
+
# Reversing only works on an already-SETTLED Direct Debit deposit; it creates
|
|
444
|
+
# an offsetting settled deposit and deactivates the LinkedAccount
|
|
445
|
+
settled = Airwallex::Deposit.create(funding_source_id: 'la_123', amount: 50.00, currency: 'AUD')
|
|
446
|
+
settled.simulate_settle
|
|
447
|
+
settled.simulate_reverse
|
|
448
|
+
|
|
449
|
+
# Class-level equivalents take a deposit_id directly instead of an instance
|
|
450
|
+
Airwallex::Deposit.simulate_settle('dpt_123')
|
|
451
|
+
Airwallex::Deposit.simulate_reject('dpt_123')
|
|
452
|
+
Airwallex::Deposit.simulate_reverse('dpt_123')
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
#### Issuing Transactions
|
|
456
|
+
|
|
457
|
+
```ruby
|
|
458
|
+
# Authorize, then capture in full
|
|
459
|
+
txn = Airwallex::IssuingTransaction.simulate_create(
|
|
460
|
+
card_id: 'card_123',
|
|
461
|
+
transaction_amount: 25.00,
|
|
462
|
+
transaction_currency: 'USD'
|
|
463
|
+
)
|
|
464
|
+
Airwallex::IssuingTransaction.simulate_capture(txn.transaction_id)
|
|
465
|
+
|
|
466
|
+
# Authorize, then capture only part of the authorized amount
|
|
467
|
+
txn = Airwallex::IssuingTransaction.simulate_create(
|
|
468
|
+
card_id: 'card_123', transaction_amount: 25.00, transaction_currency: 'USD'
|
|
469
|
+
)
|
|
470
|
+
Airwallex::IssuingTransaction.simulate_capture(txn.transaction_id, transaction_amount: 10.00)
|
|
471
|
+
|
|
472
|
+
# Authorize and clear in a single step (no separate capture needed)
|
|
473
|
+
Airwallex::IssuingTransaction.simulate_create(
|
|
474
|
+
card_id: 'card_123', transaction_amount: 25.00, transaction_currency: 'USD', single_phase: true
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Reverse a still-PENDING (uncaptured) authorization instead of capturing it
|
|
478
|
+
txn = Airwallex::IssuingTransaction.simulate_create(
|
|
479
|
+
card_id: 'card_123', transaction_amount: 25.00, transaction_currency: 'USD'
|
|
480
|
+
)
|
|
481
|
+
Airwallex::IssuingTransaction.simulate_reverse(txn.transaction_id)
|
|
482
|
+
|
|
483
|
+
# Refund a CAPTURED transaction back to the card
|
|
484
|
+
Airwallex::IssuingTransaction.simulate_refund(
|
|
485
|
+
card_id: 'card_123', transaction_amount: 25.00, transaction_currency: 'USD'
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
# Simulate a 3DS delegation-mode notification for a card
|
|
489
|
+
Airwallex::IssuingTransaction.simulate_notify_three_ds(card_number: '4111111111111111')
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
#### Disputes
|
|
493
|
+
|
|
494
|
+
```ruby
|
|
495
|
+
# Full lifecycle: raised, challenged, then resolved in the merchant's favor
|
|
496
|
+
dispute = Airwallex::Dispute.simulate_create(
|
|
497
|
+
payment_intent_id: payment_intent.id,
|
|
498
|
+
reason_code: '4853',
|
|
499
|
+
stage: 'CHARGEBACK',
|
|
500
|
+
due_at: (Time.now + 86_400).iso8601
|
|
501
|
+
)
|
|
502
|
+
dispute.challenge(customer_communication: 'Proof of delivery')
|
|
503
|
+
dispute.simulate_resolve(in_favor_of: 'MERCHANT')
|
|
504
|
+
|
|
505
|
+
# Or accept without challenging, then resolve in the customer's favor
|
|
506
|
+
dispute = Airwallex::Dispute.simulate_create(
|
|
507
|
+
payment_intent_id: payment_intent.id, reason_code: '4853', stage: 'CHARGEBACK',
|
|
508
|
+
due_at: (Time.now + 86_400).iso8601
|
|
509
|
+
)
|
|
510
|
+
dispute.accept
|
|
511
|
+
dispute.simulate_resolve(in_favor_of: 'CUSTOMER')
|
|
512
|
+
|
|
513
|
+
# Escalate — the issuing bank rejects the challenge evidence and advances the
|
|
514
|
+
# dispute to the next stage (e.g. Chargeback -> Pre-arbitration)
|
|
515
|
+
dispute.challenge(customer_communication: 'Proof of delivery')
|
|
516
|
+
dispute.simulate_escalate(due_at: (Time.now + 86_400).iso8601)
|
|
517
|
+
|
|
518
|
+
# Class-level equivalents take a dispute_id directly instead of an instance
|
|
519
|
+
Airwallex::Dispute.simulate_escalate('dis_123', due_at: (Time.now + 86_400).iso8601)
|
|
520
|
+
Airwallex::Dispute.simulate_resolve('dis_123', in_favor_of: 'MERCHANT')
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
#### Transfers
|
|
524
|
+
|
|
525
|
+
```ruby
|
|
526
|
+
# Advance a transfer's status one step at a time: SCHEDULED -> PROCESSING ->
|
|
527
|
+
# SENT -> PAID
|
|
528
|
+
transfer.simulate_transition(next_status: 'PROCESSING')
|
|
529
|
+
transfer.simulate_transition(next_status: 'SENT')
|
|
530
|
+
transfer.simulate_transition(next_status: 'PAID')
|
|
531
|
+
|
|
532
|
+
# OVERDUE, FAILED, and CANCELLED can be jumped to directly
|
|
533
|
+
transfer.simulate_transition(next_status: 'FAILED', failure_type: 'INSUFFICIENT_FUNDS')
|
|
534
|
+
|
|
535
|
+
# Class-level equivalent takes a transfer_id directly instead of an instance
|
|
536
|
+
Airwallex::Transfer.simulate_transition('tfr_123', next_status: 'CANCELLED')
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
#### Connected Accounts & Offboarding
|
|
540
|
+
|
|
541
|
+
```ruby
|
|
542
|
+
# Simulate a KYC/KYB review outcome (account must currently be SUBMITTED)
|
|
543
|
+
connected_account.simulate_update_status(next_status: 'ACTIVE')
|
|
544
|
+
# or: connected_account.simulate_update_status(next_status: 'SUSPENDED')
|
|
545
|
+
# or: connected_account.simulate_update_status(next_status: 'ACTION_REQUIRED')
|
|
546
|
+
|
|
547
|
+
# Class-level equivalent takes an account_id directly instead of an instance
|
|
548
|
+
Airwallex::ConnectedAccount.simulate_update_status('acct_123', next_status: 'ACTIVE')
|
|
549
|
+
|
|
550
|
+
# Simulate a pending offboarding completing or being cancelled — via the
|
|
551
|
+
# parent ConnectedAccount instance...
|
|
552
|
+
connected_account.simulate_complete_offboarding('obd_456')
|
|
553
|
+
connected_account.simulate_cancel_offboarding('obd_456')
|
|
554
|
+
|
|
555
|
+
# ...or directly against AccountOffboarding with both ids
|
|
556
|
+
Airwallex::AccountOffboarding.simulate_complete('acct_123', 'obd_456')
|
|
557
|
+
Airwallex::AccountOffboarding.simulate_cancel('acct_123', 'obd_456')
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
#### Account Amendments
|
|
561
|
+
|
|
562
|
+
```ruby
|
|
563
|
+
amendment = Airwallex::AccountAmendment.create(
|
|
564
|
+
target: 'account_details.store_details',
|
|
565
|
+
store_details: { store_name: 'New Store Name' }
|
|
566
|
+
)
|
|
567
|
+
amendment.simulate_approve
|
|
568
|
+
# or: amendment.simulate_reject
|
|
569
|
+
|
|
570
|
+
# Class-level equivalents take an amendment_id directly instead of an instance
|
|
571
|
+
Airwallex::AccountAmendment.simulate_approve('amd_123')
|
|
572
|
+
Airwallex::AccountAmendment.simulate_reject('amd_123')
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
#### Linked Accounts (Direct Debit Mandates)
|
|
576
|
+
|
|
577
|
+
```ruby
|
|
578
|
+
# Mandate lifecycle: PROCESSING -> ACTIVE, or PROCESSING -> INACTIVE. All
|
|
579
|
+
# four actions return HTTP 200 with an empty body, so the gem returns `true`
|
|
580
|
+
# rather than a resource instance.
|
|
581
|
+
Airwallex::LinkedAccount.simulate_accept_mandate('la_123') # PROCESSING -> ACTIVE
|
|
582
|
+
Airwallex::LinkedAccount.simulate_reject_mandate('la_123') # PROCESSING -> INACTIVE
|
|
583
|
+
Airwallex::LinkedAccount.simulate_cancel_mandate('la_123') # PROCESSING or ACTIVE -> INACTIVE
|
|
584
|
+
|
|
585
|
+
# Simulate a failed micro-deposit verification: REQUIRES_ACTION -> FAILED
|
|
586
|
+
Airwallex::LinkedAccount.simulate_fail_microdeposits('la_123')
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
#### Shopper Actions
|
|
590
|
+
|
|
591
|
+
```ruby
|
|
592
|
+
# Simulate the shopper completing (or abandoning) a redirect/3DS challenge
|
|
593
|
+
# raised during PaymentIntent#confirm, using the url from its next_action
|
|
594
|
+
payment_intent.simulate_shopper_pay(url: next_action_url)
|
|
595
|
+
# or: payment_intent.simulate_shopper_reject(url: next_action_url)
|
|
596
|
+
|
|
597
|
+
# Simulate the shopper completing a redirect/3DS challenge raised during
|
|
598
|
+
# PaymentConsent#verify, using the url from its next_action
|
|
599
|
+
payment_consent.simulate_shopper_verify(url: next_action_url)
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
#### Issuing Cardholders
|
|
603
|
+
|
|
604
|
+
```ruby
|
|
605
|
+
# Bypass a cardholder's pending RFI review stage
|
|
606
|
+
Airwallex::Cardholder.simulate_pass_review('chd_123')
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
#### Requests for Information (RFIs)
|
|
610
|
+
|
|
611
|
+
```ruby
|
|
612
|
+
# Raise a KYC RFI, then close it
|
|
613
|
+
rfi = Airwallex::RFI.simulate_create(type: 'KYC', questions: [{ answer: { type: 'TEXT' } }])
|
|
614
|
+
rfi.simulate_close
|
|
615
|
+
|
|
616
|
+
# Follow up on an RFI — reopen an existing answered question (by id) or
|
|
617
|
+
# append a new one
|
|
618
|
+
rfi = Airwallex::RFI.simulate_create(type: 'KYC', questions: [{ answer: { type: 'TEXT' } }])
|
|
619
|
+
rfi.simulate_follow_up(questions: [{ answer: { type: 'TEXT' } }])
|
|
620
|
+
|
|
621
|
+
# Class-level equivalents take an rfi_id directly instead of an instance
|
|
622
|
+
Airwallex::RFI.simulate_close('rfi_123')
|
|
623
|
+
Airwallex::RFI.simulate_follow_up('rfi_123', questions: [{ answer: { type: 'TEXT' } }])
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
#### POS Terminals
|
|
627
|
+
|
|
628
|
+
```ruby
|
|
629
|
+
# Turn a terminal on, then have it confirm a PaymentIntent under a named test
|
|
630
|
+
# scenario
|
|
631
|
+
Airwallex::POSTerminal.simulate_turn_on(terminal_id: 'term_123')
|
|
632
|
+
Airwallex::POSTerminal.simulate_confirm_payment_intent(
|
|
633
|
+
terminal_id: 'term_123', payment_scenario_name: 'approve'
|
|
634
|
+
)
|
|
635
|
+
Airwallex::POSTerminal.simulate_turn_off(terminal_id: 'term_123')
|
|
636
|
+
|
|
637
|
+
# List the test scenario names available to simulate_confirm_payment_intent
|
|
638
|
+
Airwallex::POSTerminal.simulate_payment_scenarios
|
|
639
|
+
|
|
640
|
+
# Generate a terminal activation code
|
|
641
|
+
Airwallex::POSTerminal.simulate_generate_activation_code(request_id: 'req_123')
|
|
642
|
+
```
|
|
643
|
+
|
|
253
644
|
## Usage
|
|
254
645
|
|
|
255
646
|
### Authentication
|
|
@@ -367,10 +758,16 @@ end
|
|
|
367
758
|
```
|
|
368
759
|
lib/airwallex/
|
|
369
760
|
├── api_operations/ # CRUD operation mixins (Create, Retrieve, List, Update, Delete)
|
|
370
|
-
├── resources/ # Implemented resources
|
|
371
|
-
│
|
|
372
|
-
│
|
|
373
|
-
│
|
|
761
|
+
├── resources/ # Implemented resources, grouped by API area:
|
|
762
|
+
│ │ # payment acceptance - payment_intent, refund, payment_method, customer, dispute
|
|
763
|
+
│ │ # payouts - transfer, batch_transfer, beneficiary
|
|
764
|
+
│ │ # foreign exchange - rate, quote, conversion, balance
|
|
765
|
+
│ │ # global accounts - global_account(+_alias, _mandate, _transaction)
|
|
766
|
+
│ │ # billing - billing_customer, billing_product, billing_price,
|
|
767
|
+
│ │ # billing_subscription(+_item)
|
|
768
|
+
│ │ # recurring payments - payment_consent, payment_source
|
|
769
|
+
│ │ # scale - connected_account, account_amendment, funds_split, charge
|
|
770
|
+
│ └── ... # see lib/airwallex/resources/ for the full, current list
|
|
374
771
|
├── api_resource.rb # Base resource class with dynamic attributes
|
|
375
772
|
├── list_object.rb # Pagination wrapper
|
|
376
773
|
├── errors.rb # Exception hierarchy
|
|
@@ -416,28 +813,52 @@ end
|
|
|
416
813
|
### Currently Implemented Resources
|
|
417
814
|
|
|
418
815
|
- **Payment Acceptance**:
|
|
419
|
-
- PaymentIntent (create, retrieve, list, update, confirm, cancel, capture)
|
|
816
|
+
- PaymentIntent (create, retrieve, list, update, confirm, cancel, capture, simulate_shopper_pay, simulate_shopper_reject)
|
|
420
817
|
- Refund (create, retrieve, list)
|
|
421
|
-
- PaymentMethod (create, retrieve, list, update,
|
|
818
|
+
- PaymentMethod (create, retrieve, list, update, disable)
|
|
422
819
|
- Customer (create, retrieve, list, update, delete)
|
|
423
|
-
- Dispute (retrieve, list, accept,
|
|
820
|
+
- Dispute (retrieve, list, accept, challenge, related_payment_intents, simulate_create, simulate_escalate, simulate_resolve)
|
|
424
821
|
- **Payouts**:
|
|
425
|
-
- Transfer (create, retrieve, list, cancel)
|
|
426
|
-
- Beneficiary (create, retrieve, list, delete)
|
|
822
|
+
- Transfer (create, retrieve, list, cancel, simulate_transition)
|
|
823
|
+
- Beneficiary (create, retrieve, list, update, delete, validate, verify_account, api_schema, form_schema, supported_financial_institutions)
|
|
427
824
|
- BatchTransfer (create, retrieve, list)
|
|
428
825
|
- **Foreign Exchange & Multi-Currency**:
|
|
429
|
-
- Rate (retrieve
|
|
826
|
+
- Rate (retrieve) - Real-time exchange rate queries
|
|
430
827
|
- Quote (create, retrieve) - Lock exchange rates with expiration tracking
|
|
431
828
|
- Conversion (create, retrieve, list) - Execute currency conversions
|
|
432
829
|
- Balance (list, retrieve) - Query account balances across currencies
|
|
830
|
+
- **Global Accounts**:
|
|
831
|
+
- GlobalAccount (create, retrieve, list, update, close, generate_statement_letter, create_alias, aliases, mandate, mandates)
|
|
832
|
+
- GlobalAccountTransaction, GlobalAccountAlias, GlobalAccountMandate (list/lifecycle actions scoped to a parent account)
|
|
833
|
+
- Deposit (create, retrieve, list — Direct Debit only; simulate_create for a Global Account bank-transfer deposit, simulate_settle/simulate_reject/simulate_reverse for Direct Debit deposits made via create)
|
|
834
|
+
- LinkedAccount — sandbox Simulation only (simulate_accept_mandate, simulate_reject_mandate, simulate_cancel_mandate, simulate_fail_microdeposits)
|
|
835
|
+
- **Billing & Subscriptions**:
|
|
836
|
+
- BillingCustomer (create, retrieve, list, update, bank_transfer_instructions)
|
|
837
|
+
- BillingProduct (create, retrieve, list, update)
|
|
838
|
+
- BillingPrice (create, retrieve, list, update)
|
|
839
|
+
- BillingSubscription (create, retrieve, list, update, items)
|
|
840
|
+
- BillingSubscriptionItem (scoped to a parent subscription)
|
|
841
|
+
- **Recurring Payments**:
|
|
842
|
+
- PaymentConsent (create, retrieve, list, update, verify, verify_continue, disable, simulate_shopper_verify)
|
|
843
|
+
- PaymentSource (create, retrieve, list)
|
|
844
|
+
- **Scale**:
|
|
845
|
+
- ConnectedAccount (create, retrieve, list, update, current, wallet_info, submit, agree_to_terms_and_conditions, suspend, reactivate, simulate_update_status, simulate_complete_offboarding, simulate_cancel_offboarding)
|
|
846
|
+
- AccountAmendment (create, retrieve, simulate_approve, simulate_reject) - requires Admin-level API key permissions
|
|
847
|
+
- AccountOffboarding — sandbox Simulation only, scoped to a parent ConnectedAccount (simulate_complete, simulate_cancel)
|
|
848
|
+
- FundsSplit (create, retrieve, list, release)
|
|
849
|
+
- Charge (create, retrieve, list)
|
|
850
|
+
- **Issuing** (sandbox Simulation only — no live Card/Cardholder resources yet):
|
|
851
|
+
- IssuingTransaction (simulate_create with single_phase, simulate_capture, simulate_reverse, simulate_refund, simulate_notify_three_ds)
|
|
852
|
+
- Cardholder (simulate_pass_review)
|
|
853
|
+
- **In-Person Payments** (sandbox Simulation only):
|
|
854
|
+
- POSTerminal (simulate_turn_on, simulate_turn_off, simulate_generate_activation_code, simulate_confirm_payment_intent, simulate_payment_scenarios)
|
|
855
|
+
- **Compliance** (sandbox Simulation only):
|
|
856
|
+
- RFI (simulate_create, simulate_close, simulate_follow_up)
|
|
433
857
|
- **Webhooks**: Event handling, HMAC-SHA256 signature verification
|
|
434
858
|
|
|
435
859
|
### Coming in Future Versions
|
|
436
860
|
|
|
437
|
-
-
|
|
438
|
-
- Card issuing
|
|
439
|
-
- Subscriptions and billing
|
|
440
|
-
- Virtual account numbers
|
|
861
|
+
- Live Card issuing resources (Card, Cardholder create/retrieve/list) — only their sandbox Simulation actions are implemented today
|
|
441
862
|
|
|
442
863
|
## Environment Support
|
|
443
864
|
|
|
@@ -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/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
|
|
@@ -21,9 +21,48 @@ module Airwallex
|
|
|
21
21
|
extend APIOperations::Create
|
|
22
22
|
extend APIOperations::Retrieve
|
|
23
23
|
|
|
24
|
+
# Sandbox-only — see https://www.airwallex.com/docs/api/simulation/account-amendments
|
|
25
|
+
SIMULATION_PATH = "/api/v1/simulation/account/amendments"
|
|
26
|
+
|
|
24
27
|
# @return [String] API resource path for account amendments
|
|
25
28
|
def self.resource_path
|
|
26
29
|
"/api/v1/account/amendments"
|
|
27
30
|
end
|
|
31
|
+
|
|
32
|
+
# Simulate approving a PENDING amendment
|
|
33
|
+
#
|
|
34
|
+
# @param amendment_id [String]
|
|
35
|
+
# @return [AccountAmendment]
|
|
36
|
+
def self.simulate_approve(amendment_id)
|
|
37
|
+
response = Airwallex.client.post("#{SIMULATION_PATH}/#{amendment_id}/approve", {})
|
|
38
|
+
new(response)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Simulate rejecting a PENDING amendment
|
|
42
|
+
#
|
|
43
|
+
# @param amendment_id [String]
|
|
44
|
+
# @return [AccountAmendment]
|
|
45
|
+
def self.simulate_reject(amendment_id)
|
|
46
|
+
response = Airwallex.client.post("#{SIMULATION_PATH}/#{amendment_id}/reject", {})
|
|
47
|
+
new(response)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Simulate approving this amendment
|
|
51
|
+
#
|
|
52
|
+
# @return [AccountAmendment] self
|
|
53
|
+
def simulate_approve
|
|
54
|
+
response = Airwallex.client.post("#{self.class::SIMULATION_PATH}/#{id}/approve", {})
|
|
55
|
+
refresh_from(response)
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Simulate rejecting this amendment
|
|
60
|
+
#
|
|
61
|
+
# @return [AccountAmendment] self
|
|
62
|
+
def simulate_reject
|
|
63
|
+
response = Airwallex.client.post("#{self.class::SIMULATION_PATH}/#{id}/reject", {})
|
|
64
|
+
refresh_from(response)
|
|
65
|
+
self
|
|
66
|
+
end
|
|
28
67
|
end
|
|
29
68
|
end
|