airwallex 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be0adbfbafc982fa5c899a9df4230e9c65a68e46d2e7eb248b981e41ccc23bd4
4
- data.tar.gz: 599bdc078288f934cc5f8b06fced1668605c8e2e6af4763d943a9f227a32e563
3
+ metadata.gz: 0fc42ac04f0af0416fc2fe200f747a17ec326ea5b071a40d5965043da1ebfd7c
4
+ data.tar.gz: bfc8c907e1ce9d0e702e124bdf0c85f1fb8827f7c90b0b4382665b5a4f1ade00
5
5
  SHA512:
6
- metadata.gz: 4cd20d177f642d64bdb07922f673483ab3d86499c0a938c8a7db7854f06fd5dff86b10326c09496dbf9a0bc8099a64834fc0ee4a56ff91d822dd721aff911e7a
7
- data.tar.gz: 56b071c9ff52ccf48f6526c6f3a8264d55c1d8cfcc5794d869894535baf7685c7642bdebf498f5c4ed01e362631f62a9721d4151bc6f860cf91b574a2a2d54b7
6
+ metadata.gz: 952b7120270923df264d701e631570a673584fd08f73502dc3ec36ff19bdd47577d6d799928cdae40781813cbd5f881de7ce4dcca3488e10ee5ff4d46f67da76
7
+ data.tar.gz: 440bfe68c53e17f2a5b30856f6de51257fffdc32c38e459ebe82d8604c9b018d58ee6369249bc2e2907701f0c79f43bdd9005c08eaaf19aa9ceb81486a2e9999
data/CHANGELOG.md CHANGED
@@ -1,8 +1,68 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.9.0] - 2026-09-09
4
+
5
+ ### Added
6
+
7
+ - Sandbox Simulation API coverage (all sandbox-only, see
8
+ https://www.airwallex.com/docs/api/simulation/):
9
+ - Deposit resource (`.create`/`.retrieve`/`.list` for real Direct Debit deposits,
10
+ `.simulate_create` for a Global Account bank-transfer deposit, `.simulate_settle`,
11
+ `.simulate_reject`, `.simulate_reverse` — the latter three only operate on
12
+ Direct Debit deposits made via `.create`, confirmed against the live sandbox;
13
+ a bank-transfer deposit from `.simulate_create` auto-settles on its own)
14
+ - IssuingTransaction resource (`.simulate_create` with `single_phase`,
15
+ `.simulate_capture`, `.simulate_reverse`, `.simulate_refund`,
16
+ `.simulate_notify_three_ds`) — `.simulate_create`'s response key is
17
+ `transaction_id` (confirmed against the live sandbox; earlier docs research
18
+ suggested `card_transaction_lifecycle_id`, which the API doesn't return)
19
+ - Cardholder resource (`.simulate_pass_review`)
20
+ - LinkedAccount resource (`.simulate_accept_mandate`, `.simulate_reject_mandate`,
21
+ `.simulate_cancel_mandate`, `.simulate_fail_microdeposits`)
22
+ - AccountOffboarding nested resource (`.simulate_complete`, `.simulate_cancel`),
23
+ scoped to a parent ConnectedAccount
24
+ - RFI resource (`.simulate_create`, `.simulate_close`, `.simulate_follow_up`,
25
+ plus instance equivalents)
26
+ - POSTerminal resource (`.simulate_turn_on`, `.simulate_turn_off`,
27
+ `.simulate_generate_activation_code`, `.simulate_confirm_payment_intent`,
28
+ `.simulate_payment_scenarios`)
29
+ - Transfer gained `.simulate_transition`/`#simulate_transition`
30
+ - Dispute gained `.simulate_create`, `.simulate_escalate`/`#simulate_escalate`,
31
+ `.simulate_resolve`/`#simulate_resolve`
32
+ - ConnectedAccount gained `.simulate_update_status`/`#simulate_update_status`,
33
+ `#simulate_complete_offboarding`, `#simulate_cancel_offboarding`
34
+ - AccountAmendment gained `.simulate_approve`/`#simulate_approve`,
35
+ `.simulate_reject`/`#simulate_reject`
36
+ - PaymentIntent gained `#simulate_shopper_pay`, `#simulate_shopper_reject`
37
+ - PaymentConsent gained `#simulate_shopper_verify`
38
+ - New tests covering all of the above (430 total)
39
+
40
+ ### Fixed
41
+
42
+ - RFI question `answer.type` corrected to the real uppercase enum (`"TEXT"`,
43
+ not `"text"`) — confirmed against the live sandbox
44
+
45
+ ### Verified against the live sandbox
46
+
47
+ - `Deposit.simulate_create`, `Transfer.simulate_transition`,
48
+ `Dispute.simulate_create`, `RFI.simulate_create`/`#simulate_close`,
49
+ `IssuingTransaction.simulate_create`/`.simulate_refund` all confirmed
50
+ working end-to-end
51
+ - `IssuingTransaction.simulate_capture`/`.simulate_reverse` and
52
+ `AccountAmendment.simulate_approve` reach the correct endpoint (accepted,
53
+ not 404) but couldn't be fully exercised on this account — both require
54
+ Admin-level API key permissions this sandbox key doesn't have
55
+ - Found and worked around (in the smoke test only) a pre-existing bug in
56
+ `Client`'s Faraday retry configuration: `retry_options[:exceptions]`
57
+ doesn't include `Faraday::RetriableResponse`, so any POST that gets back
58
+ a `retry_statuses` code (429/500/502/503/504) raises uncaught instead of
59
+ being returned as a normal `Airwallex::Error` — unrelated to the
60
+ Simulation resources, not fixed here
61
+
3
62
  ## [0.8.0] - 2026-09-02
4
63
 
5
64
  ### Fixed
65
+
6
66
  - `Util.deep_symbolize_keys` now recurses into `Array`s, matching `ActiveSupport#deep_transform_keys`'s
7
67
  behavior. Previously any `Array` value was copied through untouched, so hashes nested inside an array
8
68
  (e.g. `beneficiary_form_schemas`'s `fields` list) kept string keys even after "deep" symbolizing.
@@ -22,6 +82,7 @@
22
82
  ## [0.7.0] - 2026-08-28
23
83
 
24
84
  ### Added
85
+
25
86
  - GlobalAccount resource (create, retrieve, list, update, `#close`, `#generate_statement_letter`,
26
87
  `#transactions`)
27
88
  - GlobalAccountAlias nested resource (`#create_alias`, `#alias`, `#aliases`, `#initiate_port`,
@@ -42,6 +103,7 @@
42
103
  - New tests covering all of the above (378 total)
43
104
 
44
105
  ### Fixed
106
+
45
107
  - `APIOperations::Update` now sends `POST #{resource_path}/{id}/update` instead of `PUT #{resource_path}/{id}`
46
108
  - `APIOperations::Delete` now sends `POST #{resource_path}/{id}/delete` instead of `DELETE #{resource_path}/{id}`,
47
109
  and checks the response instead of always returning `true`
@@ -73,12 +135,14 @@
73
135
  - `Quote.create` requires `validity` (`MIN_1`, `MIN_15`, `MIN_30`, `HR_1`, `HR_4`, `HR_8`, or `HR_24`)
74
136
 
75
137
  ### Removed
138
+
76
139
  - `Rate.list` — the endpoint has no "list all rates" concept, only `.retrieve(sell_currency:, buy_currency:)`
77
140
  - `PaymentMethod.delete` and `PaymentMethod#detach` — replaced by `#disable`, the real lifecycle operation
78
141
 
79
142
  ## [0.6.0] - 2026-08-28
80
143
 
81
144
  ### Fixed
145
+
82
146
  - `Webhook::Event` now correctly exposes the event type. Airwallex sends this field as `"name"` in
83
147
  the webhook payload, not `"type"` — the previous `Event#type` reader was reading a key that doesn't
84
148
  exist and always returned `nil`. The accessor is renamed to `Event#name` to match Airwallex's actual
@@ -87,6 +151,7 @@
87
151
  ## [0.5.0] - 2026-08-28
88
152
 
89
153
  ### Fixed
154
+
90
155
  - Webhook signature verification no longer rejects valid webhooks when Airwallex sends
91
156
  millisecond-precision timestamps. `verify_timestamp` now detects millisecond values (anything at or
92
157
  above `MS_THRESHOLD = 10_000_000_000`, which safely distinguishes seconds from milliseconds until the
@@ -95,6 +160,7 @@
95
160
  ## [0.4.0] - 2026-08-27
96
161
 
97
162
  ### Fixed
163
+
98
164
  - `Idempotency` and `AuthRefresh` middleware are now actually registered on the Faraday connection.
99
165
  Previously both classes existed but were never wired in, so the "automatic `request_id` generation"
100
166
  and "retry once after a 401" behavior documented in the README did not happen at runtime.
@@ -103,12 +169,14 @@
103
169
  - CI now triggers on pushes to `main` (previously configured for `master`, so pushes never ran CI).
104
170
 
105
171
  ### Changed
172
+
106
173
  - `Client#request` no longer manually manages the `Authorization` header or calls
107
174
  `ensure_authenticated!` directly; this is now owned by the `AuthRefresh` middleware.
108
175
 
109
176
  ## [0.3.0] - 2025-11-25
110
177
 
111
178
  ### Added
179
+
112
180
  - BatchTransfer resource (create, retrieve, list) for bulk payout operations
113
181
  - Dispute resource (retrieve, list, accept, submit_evidence) for chargeback management
114
182
  - Foreign Exchange resources:
@@ -121,15 +189,18 @@
121
189
  - Comprehensive manual test suite for regression testing
122
190
 
123
191
  ### Changed
192
+
124
193
  - Refactored List operation for better code quality (reduced complexity from 12 to 7)
125
194
  - Balance.retrieve now performs client-side filtering for currency lookup
126
195
 
127
196
  ### Fixed
197
+
128
198
  - List operation now correctly handles Balance API's direct array response format
129
199
 
130
200
  ## [0.2.0] - 2025-11-25
131
201
 
132
202
  ### Added
203
+
133
204
  - Refund resource (create, retrieve, list)
134
205
  - PaymentMethod resource (create, retrieve, list, update, delete, detach)
135
206
  - Customer resource (create, retrieve, list, update, delete)
@@ -139,6 +210,7 @@
139
210
  ## [0.1.0] - 2025-11-25
140
211
 
141
212
  ### Added
213
+
142
214
  - Core infrastructure: Configuration, Client, Error handling
143
215
  - Authentication: Bearer token with automatic refresh
144
216
  - API Resources:
@@ -154,5 +226,6 @@
154
226
  - Ruby 3.1+ support
155
227
 
156
228
  ### Notes
229
+
157
230
  - This is an MVP release focusing on core payment acceptance and payout functionality
158
231
  - Additional resources (FX, cards, refunds) will be added in future versions
data/README.md CHANGED
@@ -16,12 +16,13 @@ This gem provides a Ruby interface to Airwallex's payment infrastructure, design
16
16
  - **Billing**: Products, prices, billing customers, and subscriptions for recurring/instalment billing
17
17
  - **Recurring Payments**: Payment consents and payment sources for merchant-initiated (off-session) charges
18
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)
19
20
  - **Idempotency**: Automatic request deduplication for safe retries
20
21
  - **Pagination**: Unified interface over cursor-based and offset-based pagination
21
22
  - **Webhook Security**: HMAC-SHA256 signature verification with replay protection
22
23
  - **Sandbox Support**: Full testing environment for development
23
24
 
24
- **Not yet implemented:** Card issuing.
25
+ **Not yet implemented:** live Card issuing resources (Card, Cardholder create/retrieve/list) — only their sandbox Simulation actions are covered today.
25
26
 
26
27
  ## Installation
27
28
 
@@ -416,6 +417,230 @@ split.release
416
417
  charge = Airwallex::Charge.retrieve('charge_id')
417
418
  ```
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
+
419
644
  ## Usage
420
645
 
421
646
  ### Authentication
@@ -588,13 +813,13 @@ end
588
813
  ### Currently Implemented Resources
589
814
 
590
815
  - **Payment Acceptance**:
591
- - PaymentIntent (create, retrieve, list, update, confirm, cancel, capture)
816
+ - PaymentIntent (create, retrieve, list, update, confirm, cancel, capture, simulate_shopper_pay, simulate_shopper_reject)
592
817
  - Refund (create, retrieve, list)
593
818
  - PaymentMethod (create, retrieve, list, update, disable)
594
819
  - Customer (create, retrieve, list, update, delete)
595
- - Dispute (retrieve, list, accept, challenge, related_payment_intents)
820
+ - Dispute (retrieve, list, accept, challenge, related_payment_intents, simulate_create, simulate_escalate, simulate_resolve)
596
821
  - **Payouts**:
597
- - Transfer (create, retrieve, list, cancel)
822
+ - Transfer (create, retrieve, list, cancel, simulate_transition)
598
823
  - Beneficiary (create, retrieve, list, update, delete, validate, verify_account, api_schema, form_schema, supported_financial_institutions)
599
824
  - BatchTransfer (create, retrieve, list)
600
825
  - **Foreign Exchange & Multi-Currency**:
@@ -605,6 +830,8 @@ end
605
830
  - **Global Accounts**:
606
831
  - GlobalAccount (create, retrieve, list, update, close, generate_statement_letter, create_alias, aliases, mandate, mandates)
607
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)
608
835
  - **Billing & Subscriptions**:
609
836
  - BillingCustomer (create, retrieve, list, update, bank_transfer_instructions)
610
837
  - BillingProduct (create, retrieve, list, update)
@@ -612,18 +839,26 @@ end
612
839
  - BillingSubscription (create, retrieve, list, update, items)
613
840
  - BillingSubscriptionItem (scoped to a parent subscription)
614
841
  - **Recurring Payments**:
615
- - PaymentConsent (create, retrieve, list, update, verify, verify_continue, disable)
842
+ - PaymentConsent (create, retrieve, list, update, verify, verify_continue, disable, simulate_shopper_verify)
616
843
  - PaymentSource (create, retrieve, list)
617
844
  - **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
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)
620
848
  - FundsSplit (create, retrieve, list, release)
621
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)
622
857
  - **Webhooks**: Event handling, HMAC-SHA256 signature verification
623
858
 
624
859
  ### Coming in Future Versions
625
860
 
626
- - Card issuing
861
+ - Live Card issuing resources (Card, Cardholder create/retrieve/list) — only their sandbox Simulation actions are implemented today
627
862
 
628
863
  ## Environment Support
629
864
 
@@ -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
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Airwallex
4
+ # Represents an offboarding request for a Connected Account. Always
5
+ # accessed through its parent ConnectedAccount, since its API path is
6
+ # scoped by account_id.
7
+ #
8
+ # This gem currently only implements the sandbox Simulation endpoints for
9
+ # completing/cancelling an offboarding; it does not yet implement the live
10
+ # create/retrieve/list endpoints (`/api/v1/accounts/{account_id}/offboardings`).
11
+ # See https://www.airwallex.com/docs/api/simulation/connected-account-offboarding
12
+ #
13
+ # @example Simulate an offboarding finishing
14
+ # Airwallex::AccountOffboarding.simulate_complete("acct_123", "obd_456")
15
+ class AccountOffboarding < APIResource
16
+ # @param account_id [String] the parent ConnectedAccount's id
17
+ # @return [String] API resource path for this account's simulated
18
+ # offboarding actions
19
+ def self.resource_path(account_id)
20
+ "/api/v1/simulation/accounts/#{account_id}/offboardings"
21
+ end
22
+
23
+ # Simulate an offboarding completing
24
+ #
25
+ # @param account_id [String]
26
+ # @param offboarding_id [String]
27
+ # @return [AccountOffboarding]
28
+ def self.simulate_complete(account_id, offboarding_id)
29
+ response = Airwallex.client.post("#{resource_path(account_id)}/#{offboarding_id}/complete", {})
30
+ new(response)
31
+ end
32
+
33
+ # Simulate an offboarding being cancelled
34
+ #
35
+ # @param account_id [String]
36
+ # @param offboarding_id [String]
37
+ # @return [AccountOffboarding]
38
+ def self.simulate_cancel(account_id, offboarding_id)
39
+ response = Airwallex.client.post("#{resource_path(account_id)}/#{offboarding_id}/cancel", {})
40
+ new(response)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Airwallex
4
+ # Represents an Issuing Cardholder.
5
+ #
6
+ # This gem currently only implements the sandbox Simulation endpoint for
7
+ # bypassing a cardholder's RFI review stage; it does not yet implement the
8
+ # live create/retrieve/list endpoints.
9
+ # See https://www.airwallex.com/docs/api/simulation/issuing-cardholders
10
+ #
11
+ # @example Bypass a cardholder's pending review
12
+ # Airwallex::Cardholder.simulate_pass_review("chd_123")
13
+ class Cardholder < APIResource
14
+ # @return [String] API resource path for simulated cardholder actions
15
+ def self.resource_path
16
+ "/api/v1/simulation/issuing/cardholders"
17
+ end
18
+
19
+ # Simulate bypassing this cardholder's RFI review stage
20
+ #
21
+ # @param cardholder_id [String]
22
+ # @return [Cardholder]
23
+ def self.simulate_pass_review(cardholder_id)
24
+ response = Airwallex.client.post("#{resource_path}/#{cardholder_id}/pass_review", {})
25
+ new(response)
26
+ end
27
+ end
28
+ end
@@ -57,11 +57,29 @@ module Airwallex
57
57
  CURRENT_ACCOUNT_PATH = "/api/v1/account"
58
58
  WALLET_INFO_PATH = "/api/v1/account/wallet_info"
59
59
 
60
+ # Sandbox-only — see https://www.airwallex.com/docs/api/simulation/connected-accounts
61
+ SIMULATION_PATH = "/api/v1/simulation/accounts"
62
+
60
63
  # @return [String] API resource path for connected accounts
61
64
  def self.resource_path
62
65
  "/api/v1/accounts"
63
66
  end
64
67
 
68
+ # Simulate this account's status transitioning. The account must
69
+ # currently be SUBMITTED.
70
+ #
71
+ # @param account_id [String]
72
+ # @param next_status [String] one of "ACTIVE", "SUSPENDED",
73
+ # "ACTION_REQUIRED"
74
+ # @param force [Boolean] defaults to true
75
+ # @return [ConnectedAccount]
76
+ def self.simulate_update_status(account_id, next_status:, force: true)
77
+ response = Airwallex.client.post(
78
+ "#{SIMULATION_PATH}/#{account_id}/update_status", next_status: next_status, force: force
79
+ )
80
+ new(response)
81
+ end
82
+
65
83
  # Retrieve whichever account the current request is authenticated as
66
84
  #
67
85
  # @param params [Hash] additional params
@@ -132,5 +150,36 @@ module Airwallex
132
150
  refresh_from(response)
133
151
  self
134
152
  end
153
+
154
+ # Simulate this account's status transitioning. The account must
155
+ # currently be SUBMITTED.
156
+ #
157
+ # @param next_status [String] one of "ACTIVE", "SUSPENDED",
158
+ # "ACTION_REQUIRED"
159
+ # @param force [Boolean] defaults to true
160
+ # @return [ConnectedAccount] self
161
+ def simulate_update_status(next_status:, force: true)
162
+ response = Airwallex.client.post(
163
+ "#{self.class::SIMULATION_PATH}/#{id}/update_status", next_status: next_status, force: force
164
+ )
165
+ refresh_from(response)
166
+ self
167
+ end
168
+
169
+ # Simulate a pending offboarding on this account completing
170
+ #
171
+ # @param offboarding_id [String]
172
+ # @return [AccountOffboarding]
173
+ def simulate_complete_offboarding(offboarding_id)
174
+ AccountOffboarding.simulate_complete(id, offboarding_id)
175
+ end
176
+
177
+ # Simulate a pending offboarding on this account being cancelled
178
+ #
179
+ # @param offboarding_id [String]
180
+ # @return [AccountOffboarding]
181
+ def simulate_cancel_offboarding(offboarding_id)
182
+ AccountOffboarding.simulate_cancel(id, offboarding_id)
183
+ end
135
184
  end
136
185
  end