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.
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 (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
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
- **Note:** This is an initial MVP release. Additional resources (FX, cards, refunds, etc.) will be added in future versions.
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
- 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'
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
- # Submit evidence to challenge
194
- dispute.submit_evidence(
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
- ├── payment_intent.rb # Payment acceptance
372
- ├── transfer.rb # Payouts
373
- └── beneficiary.rb # Payout beneficiaries
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, delete, detach)
818
+ - PaymentMethod (create, retrieve, list, update, disable)
422
819
  - Customer (create, retrieve, list, update, delete)
423
- - Dispute (retrieve, list, accept, submit_evidence)
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, list) - Real-time exchange rate queries
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
- - Global accounts
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
@@ -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