airwallex 0.8.0 → 0.10.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: '018417ed2b3091ca1c5eafdf99df79c1a81be1d13ddda2524a917d2bc9a2c3ff'
4
+ data.tar.gz: 0abdb483ebe91136353cc77efb5023cb1d0a05b3fd986c8c3ba59750dfeccfdd
5
5
  SHA512:
6
- metadata.gz: 4cd20d177f642d64bdb07922f673483ab3d86499c0a938c8a7db7854f06fd5dff86b10326c09496dbf9a0bc8099a64834fc0ee4a56ff91d822dd721aff911e7a
7
- data.tar.gz: 56b071c9ff52ccf48f6526c6f3a8264d55c1d8cfcc5794d869894535baf7685c7642bdebf498f5c4ed01e362631f62a9721d4151bc6f860cf91b574a2a2d54b7
6
+ metadata.gz: 1b48a815ca9ca948b4695cfa28b12e70c5dc21a7c6ff37781d20fe0b2f219b12daf205ec997eaaf51eca19bce49d3ffd0fbb890773cd99f5573c4733680e1ec6
7
+ data.tar.gz: ff66015473fee32cd8ec4bd7cdbb03072b340111d1c354db181c07e0722c936d7b14565210eabd6583f8cd223be9f4c96889b1e329a79b41709ed4626b474427
data/CHANGELOG.md CHANGED
@@ -1,8 +1,88 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.10.0] - 2026-09-29
4
+
5
+ ### Added
6
+
7
+ - `RFI.retrieve` (`GET /api/v1/rfis/{id}`) and `RFI.list` (`GET /api/v1/rfis`, filterable by
8
+ `statuses`, `types`, `from_created_at`, `end_created_at`, `page_size`). Both take
9
+ `headers:` for `x-on-behalf-of`
10
+ - `RFI.list` supports bookmark pagination: `has_more`/`next_cursor` come from `page_after`, and
11
+ `next_page`/`auto_paging_each` request `page: <page_after>`
12
+ - `ListObject` accepts `cursor_param:` (defaults to `:next_cursor`) and `opts:`, and forwards
13
+ the original `opts` (e.g. `headers:`) to subsequent pages. Resources that don't pass these
14
+ behave exactly as before
15
+ - `RFI.simulate_create`/`.simulate_close`/`.simulate_follow_up` and the instance
16
+ `#simulate_close`/`#simulate_follow_up` accept an optional trailing `opts` hash with `headers:`
17
+
18
+ ### Changed
19
+
20
+ - `RFI.resource_path` is now `/api/v1/rfis`; the simulation path moved to `RFI::SIMULATION_PATH`
21
+ (`/api/v1/simulation/rfis`). Simulation methods hit the same URLs as before
22
+
23
+ ## [0.9.0] - 2026-09-09
24
+
25
+ ### Added
26
+
27
+ - Sandbox Simulation API coverage (all sandbox-only, see
28
+ https://www.airwallex.com/docs/api/simulation/):
29
+ - Deposit resource (`.create`/`.retrieve`/`.list` for real Direct Debit deposits,
30
+ `.simulate_create` for a Global Account bank-transfer deposit, `.simulate_settle`,
31
+ `.simulate_reject`, `.simulate_reverse` — the latter three only operate on
32
+ Direct Debit deposits made via `.create`, confirmed against the live sandbox;
33
+ a bank-transfer deposit from `.simulate_create` auto-settles on its own)
34
+ - IssuingTransaction resource (`.simulate_create` with `single_phase`,
35
+ `.simulate_capture`, `.simulate_reverse`, `.simulate_refund`,
36
+ `.simulate_notify_three_ds`) — `.simulate_create`'s response key is
37
+ `transaction_id` (confirmed against the live sandbox; earlier docs research
38
+ suggested `card_transaction_lifecycle_id`, which the API doesn't return)
39
+ - Cardholder resource (`.simulate_pass_review`)
40
+ - LinkedAccount resource (`.simulate_accept_mandate`, `.simulate_reject_mandate`,
41
+ `.simulate_cancel_mandate`, `.simulate_fail_microdeposits`)
42
+ - AccountOffboarding nested resource (`.simulate_complete`, `.simulate_cancel`),
43
+ scoped to a parent ConnectedAccount
44
+ - RFI resource (`.simulate_create`, `.simulate_close`, `.simulate_follow_up`,
45
+ plus instance equivalents)
46
+ - POSTerminal resource (`.simulate_turn_on`, `.simulate_turn_off`,
47
+ `.simulate_generate_activation_code`, `.simulate_confirm_payment_intent`,
48
+ `.simulate_payment_scenarios`)
49
+ - Transfer gained `.simulate_transition`/`#simulate_transition`
50
+ - Dispute gained `.simulate_create`, `.simulate_escalate`/`#simulate_escalate`,
51
+ `.simulate_resolve`/`#simulate_resolve`
52
+ - ConnectedAccount gained `.simulate_update_status`/`#simulate_update_status`,
53
+ `#simulate_complete_offboarding`, `#simulate_cancel_offboarding`
54
+ - AccountAmendment gained `.simulate_approve`/`#simulate_approve`,
55
+ `.simulate_reject`/`#simulate_reject`
56
+ - PaymentIntent gained `#simulate_shopper_pay`, `#simulate_shopper_reject`
57
+ - PaymentConsent gained `#simulate_shopper_verify`
58
+ - New tests covering all of the above (430 total)
59
+
60
+ ### Fixed
61
+
62
+ - RFI question `answer.type` corrected to the real uppercase enum (`"TEXT"`,
63
+ not `"text"`) — confirmed against the live sandbox
64
+
65
+ ### Verified against the live sandbox
66
+
67
+ - `Deposit.simulate_create`, `Transfer.simulate_transition`,
68
+ `Dispute.simulate_create`, `RFI.simulate_create`/`#simulate_close`,
69
+ `IssuingTransaction.simulate_create`/`.simulate_refund` all confirmed
70
+ working end-to-end
71
+ - `IssuingTransaction.simulate_capture`/`.simulate_reverse` and
72
+ `AccountAmendment.simulate_approve` reach the correct endpoint (accepted,
73
+ not 404) but couldn't be fully exercised on this account — both require
74
+ Admin-level API key permissions this sandbox key doesn't have
75
+ - Found and worked around (in the smoke test only) a pre-existing bug in
76
+ `Client`'s Faraday retry configuration: `retry_options[:exceptions]`
77
+ doesn't include `Faraday::RetriableResponse`, so any POST that gets back
78
+ a `retry_statuses` code (429/500/502/503/504) raises uncaught instead of
79
+ being returned as a normal `Airwallex::Error` — unrelated to the
80
+ Simulation resources, not fixed here
81
+
3
82
  ## [0.8.0] - 2026-09-02
4
83
 
5
84
  ### Fixed
85
+
6
86
  - `Util.deep_symbolize_keys` now recurses into `Array`s, matching `ActiveSupport#deep_transform_keys`'s
7
87
  behavior. Previously any `Array` value was copied through untouched, so hashes nested inside an array
8
88
  (e.g. `beneficiary_form_schemas`'s `fields` list) kept string keys even after "deep" symbolizing.
@@ -22,6 +102,7 @@
22
102
  ## [0.7.0] - 2026-08-28
23
103
 
24
104
  ### Added
105
+
25
106
  - GlobalAccount resource (create, retrieve, list, update, `#close`, `#generate_statement_letter`,
26
107
  `#transactions`)
27
108
  - GlobalAccountAlias nested resource (`#create_alias`, `#alias`, `#aliases`, `#initiate_port`,
@@ -42,6 +123,7 @@
42
123
  - New tests covering all of the above (378 total)
43
124
 
44
125
  ### Fixed
126
+
45
127
  - `APIOperations::Update` now sends `POST #{resource_path}/{id}/update` instead of `PUT #{resource_path}/{id}`
46
128
  - `APIOperations::Delete` now sends `POST #{resource_path}/{id}/delete` instead of `DELETE #{resource_path}/{id}`,
47
129
  and checks the response instead of always returning `true`
@@ -73,12 +155,14 @@
73
155
  - `Quote.create` requires `validity` (`MIN_1`, `MIN_15`, `MIN_30`, `HR_1`, `HR_4`, `HR_8`, or `HR_24`)
74
156
 
75
157
  ### Removed
158
+
76
159
  - `Rate.list` — the endpoint has no "list all rates" concept, only `.retrieve(sell_currency:, buy_currency:)`
77
160
  - `PaymentMethod.delete` and `PaymentMethod#detach` — replaced by `#disable`, the real lifecycle operation
78
161
 
79
162
  ## [0.6.0] - 2026-08-28
80
163
 
81
164
  ### Fixed
165
+
82
166
  - `Webhook::Event` now correctly exposes the event type. Airwallex sends this field as `"name"` in
83
167
  the webhook payload, not `"type"` — the previous `Event#type` reader was reading a key that doesn't
84
168
  exist and always returned `nil`. The accessor is renamed to `Event#name` to match Airwallex's actual
@@ -87,6 +171,7 @@
87
171
  ## [0.5.0] - 2026-08-28
88
172
 
89
173
  ### Fixed
174
+
90
175
  - Webhook signature verification no longer rejects valid webhooks when Airwallex sends
91
176
  millisecond-precision timestamps. `verify_timestamp` now detects millisecond values (anything at or
92
177
  above `MS_THRESHOLD = 10_000_000_000`, which safely distinguishes seconds from milliseconds until the
@@ -95,6 +180,7 @@
95
180
  ## [0.4.0] - 2026-08-27
96
181
 
97
182
  ### Fixed
183
+
98
184
  - `Idempotency` and `AuthRefresh` middleware are now actually registered on the Faraday connection.
99
185
  Previously both classes existed but were never wired in, so the "automatic `request_id` generation"
100
186
  and "retry once after a 401" behavior documented in the README did not happen at runtime.
@@ -103,12 +189,14 @@
103
189
  - CI now triggers on pushes to `main` (previously configured for `master`, so pushes never ran CI).
104
190
 
105
191
  ### Changed
192
+
106
193
  - `Client#request` no longer manually manages the `Authorization` header or calls
107
194
  `ensure_authenticated!` directly; this is now owned by the `AuthRefresh` middleware.
108
195
 
109
196
  ## [0.3.0] - 2025-11-25
110
197
 
111
198
  ### Added
199
+
112
200
  - BatchTransfer resource (create, retrieve, list) for bulk payout operations
113
201
  - Dispute resource (retrieve, list, accept, submit_evidence) for chargeback management
114
202
  - Foreign Exchange resources:
@@ -121,15 +209,18 @@
121
209
  - Comprehensive manual test suite for regression testing
122
210
 
123
211
  ### Changed
212
+
124
213
  - Refactored List operation for better code quality (reduced complexity from 12 to 7)
125
214
  - Balance.retrieve now performs client-side filtering for currency lookup
126
215
 
127
216
  ### Fixed
217
+
128
218
  - List operation now correctly handles Balance API's direct array response format
129
219
 
130
220
  ## [0.2.0] - 2025-11-25
131
221
 
132
222
  ### Added
223
+
133
224
  - Refund resource (create, retrieve, list)
134
225
  - PaymentMethod resource (create, retrieve, list, update, delete, detach)
135
226
  - Customer resource (create, retrieve, list, update, delete)
@@ -139,6 +230,7 @@
139
230
  ## [0.1.0] - 2025-11-25
140
231
 
141
232
  ### Added
233
+
142
234
  - Core infrastructure: Configuration, Client, Error handling
143
235
  - Authentication: Bearer token with automatic refresh
144
236
  - API Resources:
@@ -154,5 +246,6 @@
154
246
  - Ruby 3.1+ support
155
247
 
156
248
  ### Notes
249
+
157
250
  - This is an MVP release focusing on core payment acceptance and payout functionality
158
251
  - Additional resources (FX, cards, refunds) will be added in future versions
data/README.md CHANGED
@@ -16,12 +16,14 @@ 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
+ - **Compliance**: Retrieve and list Requests for Information (RFIs), including for connected accounts
20
+ - **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
21
  - **Idempotency**: Automatic request deduplication for safe retries
20
22
  - **Pagination**: Unified interface over cursor-based and offset-based pagination
21
23
  - **Webhook Security**: HMAC-SHA256 signature verification with replay protection
22
24
  - **Sandbox Support**: Full testing environment for development
23
25
 
24
- **Not yet implemented:** Card issuing.
26
+ **Not yet implemented:** live Card issuing resources (Card, Cardholder create/retrieve/list) — only their sandbox Simulation actions are covered today.
25
27
 
26
28
  ## Installation
27
29
 
@@ -416,6 +418,258 @@ split.release
416
418
  charge = Airwallex::Charge.retrieve('charge_id')
417
419
  ```
418
420
 
421
+ ### Requests for Information (RFIs)
422
+
423
+ RFIs are compliance questions Airwallex raises against an account. They use
424
+ bookmark pagination (`page_after`/`page`), which `ListObject` handles for you,
425
+ and connected accounts are addressed with `x-on-behalf-of`, which is kept
426
+ across pages. The API key needs the **Risk → Request for Information (RFI):
427
+ Read** permission, or Airwallex responds with 401 (`Airwallex::AuthenticationError`).
428
+
429
+ ```ruby
430
+ headers = { 'x-on-behalf-of' => 'acct_123' }
431
+
432
+ rfi = Airwallex::RFI.retrieve('rfi_123', headers: headers)
433
+ rfi.status # => "ACTION_REQUIRED"
434
+ rfi.active_request[:questions].first # => { id: ..., title: { en: ... }, answer: { type: "ATTACHMENT" }, ... }
435
+
436
+ # statuses/types are comma-separated strings; page_size is 1-2000 (default 100)
437
+ Airwallex::RFI.list({ statuses: 'ACTION_REQUIRED', types: 'KYC,KYC_ONGOING' }, headers: headers)
438
+ .auto_paging_each { |r| puts r.id }
439
+ ```
440
+
441
+ ### Sandbox Simulations
442
+
443
+ Sandbox-only endpoints (`/api/v1/simulation/...`) that stand in for the bank,
444
+ card network, issuing bank, or shopper — for driving async/manual lifecycles
445
+ in tests without waiting on real-world events. See
446
+ https://www.airwallex.com/docs/api/simulation/ for the full reference. All
447
+ of these raise `Airwallex::Error` (same as any other endpoint) if called
448
+ against a production API key.
449
+
450
+ #### Deposits
451
+
452
+ ```ruby
453
+ # Simulate an inbound bank transfer landing in a Global Account — auto-settles
454
+ # on its own within a few seconds, same as a real bank transfer
455
+ Airwallex::Deposit.simulate_create(amount: 100.00, global_account_id: 'gacc_123')
456
+
457
+ # .simulate_settle/.simulate_reject/.simulate_reverse are for Direct Debit
458
+ # deposits specifically, made via the real (non-simulation) .create — a real
459
+ # direct debit pull takes days to clear, so the sandbox needs a way to force it
460
+ pending = Airwallex::Deposit.create(funding_source_id: 'la_123', amount: 50.00, currency: 'AUD')
461
+ pending.simulate_settle # -> SETTLED
462
+ # or: pending.simulate_reject -> REJECTED
463
+
464
+ # Reversing only works on an already-SETTLED Direct Debit deposit; it creates
465
+ # an offsetting settled deposit and deactivates the LinkedAccount
466
+ settled = Airwallex::Deposit.create(funding_source_id: 'la_123', amount: 50.00, currency: 'AUD')
467
+ settled.simulate_settle
468
+ settled.simulate_reverse
469
+
470
+ # Class-level equivalents take a deposit_id directly instead of an instance
471
+ Airwallex::Deposit.simulate_settle('dpt_123')
472
+ Airwallex::Deposit.simulate_reject('dpt_123')
473
+ Airwallex::Deposit.simulate_reverse('dpt_123')
474
+ ```
475
+
476
+ #### Issuing Transactions
477
+
478
+ ```ruby
479
+ # Authorize, then capture in full
480
+ txn = Airwallex::IssuingTransaction.simulate_create(
481
+ card_id: 'card_123',
482
+ transaction_amount: 25.00,
483
+ transaction_currency: 'USD'
484
+ )
485
+ Airwallex::IssuingTransaction.simulate_capture(txn.transaction_id)
486
+
487
+ # Authorize, then capture only part of the authorized amount
488
+ txn = Airwallex::IssuingTransaction.simulate_create(
489
+ card_id: 'card_123', transaction_amount: 25.00, transaction_currency: 'USD'
490
+ )
491
+ Airwallex::IssuingTransaction.simulate_capture(txn.transaction_id, transaction_amount: 10.00)
492
+
493
+ # Authorize and clear in a single step (no separate capture needed)
494
+ Airwallex::IssuingTransaction.simulate_create(
495
+ card_id: 'card_123', transaction_amount: 25.00, transaction_currency: 'USD', single_phase: true
496
+ )
497
+
498
+ # Reverse a still-PENDING (uncaptured) authorization instead of capturing it
499
+ txn = Airwallex::IssuingTransaction.simulate_create(
500
+ card_id: 'card_123', transaction_amount: 25.00, transaction_currency: 'USD'
501
+ )
502
+ Airwallex::IssuingTransaction.simulate_reverse(txn.transaction_id)
503
+
504
+ # Refund a CAPTURED transaction back to the card
505
+ Airwallex::IssuingTransaction.simulate_refund(
506
+ card_id: 'card_123', transaction_amount: 25.00, transaction_currency: 'USD'
507
+ )
508
+
509
+ # Simulate a 3DS delegation-mode notification for a card
510
+ Airwallex::IssuingTransaction.simulate_notify_three_ds(card_number: '4111111111111111')
511
+ ```
512
+
513
+ #### Disputes
514
+
515
+ ```ruby
516
+ # Full lifecycle: raised, challenged, then resolved in the merchant's favor
517
+ dispute = Airwallex::Dispute.simulate_create(
518
+ payment_intent_id: payment_intent.id,
519
+ reason_code: '4853',
520
+ stage: 'CHARGEBACK',
521
+ due_at: (Time.now + 86_400).iso8601
522
+ )
523
+ dispute.challenge(customer_communication: 'Proof of delivery')
524
+ dispute.simulate_resolve(in_favor_of: 'MERCHANT')
525
+
526
+ # Or accept without challenging, then resolve in the customer's favor
527
+ dispute = Airwallex::Dispute.simulate_create(
528
+ payment_intent_id: payment_intent.id, reason_code: '4853', stage: 'CHARGEBACK',
529
+ due_at: (Time.now + 86_400).iso8601
530
+ )
531
+ dispute.accept
532
+ dispute.simulate_resolve(in_favor_of: 'CUSTOMER')
533
+
534
+ # Escalate — the issuing bank rejects the challenge evidence and advances the
535
+ # dispute to the next stage (e.g. Chargeback -> Pre-arbitration)
536
+ dispute.challenge(customer_communication: 'Proof of delivery')
537
+ dispute.simulate_escalate(due_at: (Time.now + 86_400).iso8601)
538
+
539
+ # Class-level equivalents take a dispute_id directly instead of an instance
540
+ Airwallex::Dispute.simulate_escalate('dis_123', due_at: (Time.now + 86_400).iso8601)
541
+ Airwallex::Dispute.simulate_resolve('dis_123', in_favor_of: 'MERCHANT')
542
+ ```
543
+
544
+ #### Transfers
545
+
546
+ ```ruby
547
+ # Advance a transfer's status one step at a time: SCHEDULED -> PROCESSING ->
548
+ # SENT -> PAID
549
+ transfer.simulate_transition(next_status: 'PROCESSING')
550
+ transfer.simulate_transition(next_status: 'SENT')
551
+ transfer.simulate_transition(next_status: 'PAID')
552
+
553
+ # OVERDUE, FAILED, and CANCELLED can be jumped to directly
554
+ transfer.simulate_transition(next_status: 'FAILED', failure_type: 'INSUFFICIENT_FUNDS')
555
+
556
+ # Class-level equivalent takes a transfer_id directly instead of an instance
557
+ Airwallex::Transfer.simulate_transition('tfr_123', next_status: 'CANCELLED')
558
+ ```
559
+
560
+ #### Connected Accounts & Offboarding
561
+
562
+ ```ruby
563
+ # Simulate a KYC/KYB review outcome (account must currently be SUBMITTED)
564
+ connected_account.simulate_update_status(next_status: 'ACTIVE')
565
+ # or: connected_account.simulate_update_status(next_status: 'SUSPENDED')
566
+ # or: connected_account.simulate_update_status(next_status: 'ACTION_REQUIRED')
567
+
568
+ # Class-level equivalent takes an account_id directly instead of an instance
569
+ Airwallex::ConnectedAccount.simulate_update_status('acct_123', next_status: 'ACTIVE')
570
+
571
+ # Simulate a pending offboarding completing or being cancelled — via the
572
+ # parent ConnectedAccount instance...
573
+ connected_account.simulate_complete_offboarding('obd_456')
574
+ connected_account.simulate_cancel_offboarding('obd_456')
575
+
576
+ # ...or directly against AccountOffboarding with both ids
577
+ Airwallex::AccountOffboarding.simulate_complete('acct_123', 'obd_456')
578
+ Airwallex::AccountOffboarding.simulate_cancel('acct_123', 'obd_456')
579
+ ```
580
+
581
+ #### Account Amendments
582
+
583
+ ```ruby
584
+ amendment = Airwallex::AccountAmendment.create(
585
+ target: 'account_details.store_details',
586
+ store_details: { store_name: 'New Store Name' }
587
+ )
588
+ amendment.simulate_approve
589
+ # or: amendment.simulate_reject
590
+
591
+ # Class-level equivalents take an amendment_id directly instead of an instance
592
+ Airwallex::AccountAmendment.simulate_approve('amd_123')
593
+ Airwallex::AccountAmendment.simulate_reject('amd_123')
594
+ ```
595
+
596
+ #### Linked Accounts (Direct Debit Mandates)
597
+
598
+ ```ruby
599
+ # Mandate lifecycle: PROCESSING -> ACTIVE, or PROCESSING -> INACTIVE. All
600
+ # four actions return HTTP 200 with an empty body, so the gem returns `true`
601
+ # rather than a resource instance.
602
+ Airwallex::LinkedAccount.simulate_accept_mandate('la_123') # PROCESSING -> ACTIVE
603
+ Airwallex::LinkedAccount.simulate_reject_mandate('la_123') # PROCESSING -> INACTIVE
604
+ Airwallex::LinkedAccount.simulate_cancel_mandate('la_123') # PROCESSING or ACTIVE -> INACTIVE
605
+
606
+ # Simulate a failed micro-deposit verification: REQUIRES_ACTION -> FAILED
607
+ Airwallex::LinkedAccount.simulate_fail_microdeposits('la_123')
608
+ ```
609
+
610
+ #### Shopper Actions
611
+
612
+ ```ruby
613
+ # Simulate the shopper completing (or abandoning) a redirect/3DS challenge
614
+ # raised during PaymentIntent#confirm, using the url from its next_action
615
+ payment_intent.simulate_shopper_pay(url: next_action_url)
616
+ # or: payment_intent.simulate_shopper_reject(url: next_action_url)
617
+
618
+ # Simulate the shopper completing a redirect/3DS challenge raised during
619
+ # PaymentConsent#verify, using the url from its next_action
620
+ payment_consent.simulate_shopper_verify(url: next_action_url)
621
+ ```
622
+
623
+ #### Issuing Cardholders
624
+
625
+ ```ruby
626
+ # Bypass a cardholder's pending RFI review stage
627
+ Airwallex::Cardholder.simulate_pass_review('chd_123')
628
+ ```
629
+
630
+ #### Requests for Information (RFIs)
631
+
632
+ ```ruby
633
+ # Raise a KYC RFI, then close it
634
+ rfi = Airwallex::RFI.simulate_create(type: 'KYC', questions: [{ answer: { type: 'TEXT' } }])
635
+ rfi.simulate_close
636
+
637
+ # Follow up on an RFI — reopen an existing answered question (by id) or
638
+ # append a new one
639
+ rfi = Airwallex::RFI.simulate_create(type: 'KYC', questions: [{ answer: { type: 'TEXT' } }])
640
+ rfi.simulate_follow_up(questions: [{ answer: { type: 'TEXT' } }])
641
+
642
+ # Class-level equivalents take an rfi_id directly instead of an instance
643
+ Airwallex::RFI.simulate_close('rfi_123')
644
+ Airwallex::RFI.simulate_follow_up('rfi_123', questions: [{ answer: { type: 'TEXT' } }])
645
+
646
+ # Raise an RFI against a connected account — params need braces when
647
+ # followed by opts. Fails with BadRequestError ("ongoing RFI case exists")
648
+ # if the account already has an open RFI.
649
+ Airwallex::RFI.simulate_create(
650
+ { type: 'KYC', questions: [{ answer: { type: 'TEXT' } }] },
651
+ headers: { 'x-on-behalf-of' => 'acct_123' }
652
+ )
653
+ ```
654
+
655
+ #### POS Terminals
656
+
657
+ ```ruby
658
+ # Turn a terminal on, then have it confirm a PaymentIntent under a named test
659
+ # scenario
660
+ Airwallex::POSTerminal.simulate_turn_on(terminal_id: 'term_123')
661
+ Airwallex::POSTerminal.simulate_confirm_payment_intent(
662
+ terminal_id: 'term_123', payment_scenario_name: 'approve'
663
+ )
664
+ Airwallex::POSTerminal.simulate_turn_off(terminal_id: 'term_123')
665
+
666
+ # List the test scenario names available to simulate_confirm_payment_intent
667
+ Airwallex::POSTerminal.simulate_payment_scenarios
668
+
669
+ # Generate a terminal activation code
670
+ Airwallex::POSTerminal.simulate_generate_activation_code(request_id: 'req_123')
671
+ ```
672
+
419
673
  ## Usage
420
674
 
421
675
  ### Authentication
@@ -454,7 +708,8 @@ transfer = Airwallex::Transfer.create(
454
708
 
455
709
  ### Pagination
456
710
 
457
- Unified interface across both cursor-based and offset-based endpoints:
711
+ Unified interface across cursor-based, offset-based, and bookmark-based (RFI) endpoints.
712
+ Any `headers:` passed to `.list` (e.g. `x-on-behalf-of`) are kept on every page:
458
713
 
459
714
  ```ruby
460
715
  # Auto-pagination with enumerable
@@ -588,13 +843,13 @@ end
588
843
  ### Currently Implemented Resources
589
844
 
590
845
  - **Payment Acceptance**:
591
- - PaymentIntent (create, retrieve, list, update, confirm, cancel, capture)
846
+ - PaymentIntent (create, retrieve, list, update, confirm, cancel, capture, simulate_shopper_pay, simulate_shopper_reject)
592
847
  - Refund (create, retrieve, list)
593
848
  - PaymentMethod (create, retrieve, list, update, disable)
594
849
  - Customer (create, retrieve, list, update, delete)
595
- - Dispute (retrieve, list, accept, challenge, related_payment_intents)
850
+ - Dispute (retrieve, list, accept, challenge, related_payment_intents, simulate_create, simulate_escalate, simulate_resolve)
596
851
  - **Payouts**:
597
- - Transfer (create, retrieve, list, cancel)
852
+ - Transfer (create, retrieve, list, cancel, simulate_transition)
598
853
  - Beneficiary (create, retrieve, list, update, delete, validate, verify_account, api_schema, form_schema, supported_financial_institutions)
599
854
  - BatchTransfer (create, retrieve, list)
600
855
  - **Foreign Exchange & Multi-Currency**:
@@ -605,6 +860,8 @@ end
605
860
  - **Global Accounts**:
606
861
  - GlobalAccount (create, retrieve, list, update, close, generate_statement_letter, create_alias, aliases, mandate, mandates)
607
862
  - GlobalAccountTransaction, GlobalAccountAlias, GlobalAccountMandate (list/lifecycle actions scoped to a parent account)
863
+ - 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)
864
+ - LinkedAccount — sandbox Simulation only (simulate_accept_mandate, simulate_reject_mandate, simulate_cancel_mandate, simulate_fail_microdeposits)
608
865
  - **Billing & Subscriptions**:
609
866
  - BillingCustomer (create, retrieve, list, update, bank_transfer_instructions)
610
867
  - BillingProduct (create, retrieve, list, update)
@@ -612,18 +869,26 @@ end
612
869
  - BillingSubscription (create, retrieve, list, update, items)
613
870
  - BillingSubscriptionItem (scoped to a parent subscription)
614
871
  - **Recurring Payments**:
615
- - PaymentConsent (create, retrieve, list, update, verify, verify_continue, disable)
872
+ - PaymentConsent (create, retrieve, list, update, verify, verify_continue, disable, simulate_shopper_verify)
616
873
  - PaymentSource (create, retrieve, list)
617
874
  - **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
875
+ - 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)
876
+ - AccountAmendment (create, retrieve, simulate_approve, simulate_reject) - requires Admin-level API key permissions
877
+ - AccountOffboarding — sandbox Simulation only, scoped to a parent ConnectedAccount (simulate_complete, simulate_cancel)
620
878
  - FundsSplit (create, retrieve, list, release)
621
879
  - Charge (create, retrieve, list)
880
+ - **Issuing** (sandbox Simulation only — no live Card/Cardholder resources yet):
881
+ - IssuingTransaction (simulate_create with single_phase, simulate_capture, simulate_reverse, simulate_refund, simulate_notify_three_ds)
882
+ - Cardholder (simulate_pass_review)
883
+ - **In-Person Payments** (sandbox Simulation only):
884
+ - POSTerminal (simulate_turn_on, simulate_turn_off, simulate_generate_activation_code, simulate_confirm_payment_intent, simulate_payment_scenarios)
885
+ - **Compliance**:
886
+ - RFI (retrieve, list, plus sandbox simulate_create, simulate_close, simulate_follow_up)
622
887
  - **Webhooks**: Event handling, HMAC-SHA256 signature verification
623
888
 
624
889
  ### Coming in Future Versions
625
890
 
626
- - Card issuing
891
+ - Live Card issuing resources (Card, Cardholder create/retrieve/list) — only their sandbox Simulation actions are implemented today
627
892
 
628
893
  ## Environment Support
629
894
 
@@ -6,12 +6,19 @@ module Airwallex
6
6
 
7
7
  attr_reader :data, :has_more, :next_cursor
8
8
 
9
- def initialize(data:, has_more:, resource_class:, next_cursor: nil, params: {})
9
+ # @param cursor_param [Symbol] query param the next cursor is sent as
10
+ # (e.g. :page for bookmark-paginated endpoints like RFI)
11
+ # @param opts [Hash] the original request opts (e.g. headers:), forwarded
12
+ # to every subsequent page so x-on-behalf-of etc. are kept
13
+ def initialize(data:, has_more:, resource_class:, next_cursor: nil, params: {}, # rubocop:disable Metrics/ParameterLists
14
+ cursor_param: :next_cursor, opts: {})
10
15
  @data = data.map { |item| resource_class.new(item) }
11
16
  @has_more = has_more
12
17
  @next_cursor = next_cursor
13
18
  @resource_class = resource_class
14
19
  @params = params
20
+ @cursor_param = cursor_param
21
+ @opts = opts
15
22
  end
16
23
 
17
24
  def each(&)
@@ -49,7 +56,7 @@ module Airwallex
49
56
 
50
57
  if @next_cursor
51
58
  # Cursor-based pagination
52
- next_params[:next_cursor] = @next_cursor
59
+ next_params[@cursor_param] = @next_cursor
53
60
  else
54
61
  # Offset-based pagination
55
62
  page_size = @params[:page_size] || @params[:limit] || 20
@@ -57,7 +64,7 @@ module Airwallex
57
64
  next_params[:offset] = current_offset + page_size
58
65
  end
59
66
 
60
- @resource_class.list(next_params)
67
+ @opts.empty? ? @resource_class.list(next_params) : @resource_class.list(next_params, @opts)
61
68
  end
62
69
 
63
70
  # Automatically iterate through all pages
@@ -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