airwallex 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -0
- data/README.md +108 -8
- data/lib/airwallex/client/file_uploads.rb +46 -0
- data/lib/airwallex/client/logging.rb +21 -0
- data/lib/airwallex/client.rb +6 -1
- data/lib/airwallex/middleware/auth_refresh.rb +7 -0
- data/lib/airwallex/resources/connected_account_transfer.rb +33 -0
- data/lib/airwallex/resources/funds_split.rb +20 -6
- data/lib/airwallex/resources/funds_split_reversal.rb +31 -0
- data/lib/airwallex/resources/rfi.rb +71 -1
- data/lib/airwallex/resources/transfer.rb +17 -8
- data/lib/airwallex/resources/uploaded_file.rb +65 -0
- data/lib/airwallex/version.rb +1 -1
- data/lib/airwallex.rb +3 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20abbd73aa43d82620559c0f067a6236619d3dad597b778b41b75af6d87fb225
|
|
4
|
+
data.tar.gz: 6a477ebe40ec72d267d42f9b98669d783dd7db32c9cf9d30ed0b671c07810252
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ac0aa15ebc45b61bbd7004010bbdfc261c1243d373cbd59b4dbb141e680b0d46ca079ab25f2d4c4e9017f5d4a4730fff4950f632e5415c82de0728cd3e3837b8
|
|
7
|
+
data.tar.gz: 948df17c305f0ab1c88791b29afe1df2218197c12512cbb52c7f80cb0524c9e1903a7e4b1b25597c2ed447d010fed846df94e48ec988ccdf85019913318fa561
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,59 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.12.0] - 2026-10-06
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `RFI.respond(rfi_id, params, opts)` and `RFI#respond(params, opts)` for
|
|
8
|
+
`POST /api/v1/rfis/{id}/respond`. Params are sent as-is
|
|
9
|
+
(`questions: [{ id:, answer: { type:, ... } }]`) and take `headers:` for
|
|
10
|
+
`x-on-behalf-of`. No `request_id` is added to this body, because the
|
|
11
|
+
endpoint doesn't document one
|
|
12
|
+
- `UploadedFile.upload(io, filename:, content_type:, notes: nil, opts: {})` for
|
|
13
|
+
`POST /api/v1/files/upload` on the files host (`config.files_url`). It sends
|
|
14
|
+
multipart/form-data with `file` and optional `notes` form fields, and
|
|
15
|
+
returns an `UploadedFile` with `#file_id` (`#id` mirrors it)
|
|
16
|
+
- `Client#files_connection` and `Client#upload`: a separate connection to the
|
|
17
|
+
files host with no JSON Content-Type default, no JSON encoding, no
|
|
18
|
+
Idempotency middleware, and no logging of request bodies
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `AuthRefresh` now re-sends the original request body when it retries after
|
|
23
|
+
a 401. It previously re-sent the parsed 401 response body, so any POST that
|
|
24
|
+
hit an expired token was retried with the wrong body. Multipart stream
|
|
25
|
+
bodies are rewound before the retry
|
|
26
|
+
- With `config.logger` set, the logger no longer writes credentials: the
|
|
27
|
+
`x-api-key` header, the `Authorization` bearer token, and the `token` in the
|
|
28
|
+
login response are logged as `[FILTERED]`. They were previously logged in
|
|
29
|
+
plain text
|
|
30
|
+
|
|
31
|
+
## [0.11.0] - 2026-10-01
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- `FundsSplitReversal` resource (`.create`, `.retrieve`, `.list`) for
|
|
36
|
+
`/api/v1/pa/funds_split_reversals`, which moves split funds back from a
|
|
37
|
+
connected account to the platform. `.list` takes the required `funds_split_id:`.
|
|
38
|
+
Partial reversals are allowed until the full split is reversed
|
|
39
|
+
- `ConnectedAccountTransfer` resource (`.create`, `.retrieve`, `.list`) for
|
|
40
|
+
`/api/v1/connected_account_transfers`, which moves funds between the platform
|
|
41
|
+
and a connected account. Pass `headers: { "x-on-behalf-of" => ... }` to
|
|
42
|
+
debit a connected account instead of the platform
|
|
43
|
+
- `Transfer.simulate_transition`, `Transfer#simulate_transition` and
|
|
44
|
+
`Transfer#cancel` accept `headers:`. A transfer created `x-on-behalf-of` a
|
|
45
|
+
connected account can only be found with that same header
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- `FundsSplit` docs (README and class comment) now show the real create body:
|
|
50
|
+
`request_id`, `source_id`, `source_type: "PAYMENT_INTENT"`, `amount` (string),
|
|
51
|
+
`destination`, `auto_release`, `metadata`, with one split per destination.
|
|
52
|
+
The docs previously showed `payment_intent_id:` and `splits: [...]`
|
|
53
|
+
- Documented that `funds_split.*` webhook payloads use `split_id` (not `id`)
|
|
54
|
+
and carry no `request_id`, and that the sandbox rejects a PROCESSING -> PAID
|
|
55
|
+
transfer transition (go through SENT)
|
|
56
|
+
|
|
3
57
|
## [0.10.0] - 2026-09-29
|
|
4
58
|
|
|
5
59
|
### Added
|
data/README.md
CHANGED
|
@@ -16,7 +16,7 @@ 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
|
|
19
|
+
- **Compliance**: Retrieve, list and respond to Requests for Information (RFIs), and upload supporting documents, including for connected accounts
|
|
20
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)
|
|
21
21
|
- **Idempotency**: Automatic request deduplication for safe retries
|
|
22
22
|
- **Pagination**: Unified interface over cursor-based and offset-based pagination
|
|
@@ -375,7 +375,7 @@ payment_source = Airwallex::PaymentSource.create(
|
|
|
375
375
|
)
|
|
376
376
|
```
|
|
377
377
|
|
|
378
|
-
### Scale (Connected Accounts, Funds Splits, Charges)
|
|
378
|
+
### Scale (Connected Accounts, Funds Splits, Reversals, Connected Account Transfers, Charges)
|
|
379
379
|
|
|
380
380
|
For platforms onboarding sub-merchants:
|
|
381
381
|
|
|
@@ -407,17 +407,56 @@ connected_account.submit
|
|
|
407
407
|
Airwallex::ConnectedAccount.current
|
|
408
408
|
Airwallex::ConnectedAccount.wallet_info
|
|
409
409
|
|
|
410
|
-
# Split a
|
|
410
|
+
# Split part of a PaymentIntent's funds to a connected account. One split per
|
|
411
|
+
# destination: create several splits to pay several connected accounts.
|
|
412
|
+
# A reused request_id raises BadRequestError (duplicate_request); splitting
|
|
413
|
+
# more than the PaymentIntent amount raises amount_above_limit.
|
|
411
414
|
split = Airwallex::FundsSplit.create(
|
|
412
|
-
|
|
413
|
-
|
|
415
|
+
request_id: 'split-order-123', # required, max 64
|
|
416
|
+
source_id: payment_intent.id, # required
|
|
417
|
+
source_type: 'PAYMENT_INTENT', # required
|
|
418
|
+
amount: '10.00', # required, string
|
|
419
|
+
destination: connected_account.id,
|
|
420
|
+
auto_release: false, # optional, default true
|
|
421
|
+
metadata: { order_id: '123' } # optional
|
|
422
|
+
)
|
|
423
|
+
split.release # only needed when auto_release: false
|
|
424
|
+
|
|
425
|
+
# Reverse split funds back to the platform. Partial reversals are allowed
|
|
426
|
+
# until the full split is reversed. Statuses: CREATED, RELEASED, SETTLED
|
|
427
|
+
reversal = Airwallex::FundsSplitReversal.create(
|
|
428
|
+
request_id: 'rev-order-123', # required, max 64
|
|
429
|
+
funds_split_id: split.id, # required
|
|
430
|
+
amount: '5.00', # required, string
|
|
431
|
+
metadata: { reason: 'refund' } # optional
|
|
432
|
+
)
|
|
433
|
+
Airwallex::FundsSplitReversal.retrieve(reversal.id)
|
|
434
|
+
Airwallex::FundsSplitReversal.list(funds_split_id: split.id) # funds_split_id is required
|
|
435
|
+
|
|
436
|
+
# Move funds between the platform and a connected account's wallet. Pass
|
|
437
|
+
# x-on-behalf-of to debit a connected account instead of the platform.
|
|
438
|
+
# Statuses: NEW, PENDING, SETTLED, SUSPENDED, FAILED
|
|
439
|
+
cat = Airwallex::ConnectedAccountTransfer.create(
|
|
440
|
+
request_id: 'top-up-123', # required, 1-50
|
|
441
|
+
amount: '100.00',
|
|
442
|
+
currency: 'AUD',
|
|
443
|
+
destination: connected_account.id,
|
|
444
|
+
reason: 'transfer_to_own_account',
|
|
445
|
+
reference: 'Ledger top-up' # required, 1-140
|
|
414
446
|
)
|
|
415
|
-
|
|
447
|
+
Airwallex::ConnectedAccountTransfer.retrieve(cat.id)
|
|
448
|
+
Airwallex::ConnectedAccountTransfer.list(destination: connected_account.id, status: 'SETTLED')
|
|
449
|
+
|
|
450
|
+
# Pay out from a connected account: create the Transfer on its behalf
|
|
451
|
+
Airwallex::Transfer.create(transfer_params, headers: { 'x-on-behalf-of' => connected_account.id })
|
|
416
452
|
|
|
417
453
|
# Charges (create, retrieve, list)
|
|
418
454
|
charge = Airwallex::Charge.retrieve('charge_id')
|
|
419
455
|
```
|
|
420
456
|
|
|
457
|
+
`funds_split.*` webhook payloads name the split `split_id`, not `id`, and carry
|
|
458
|
+
no `request_id`: `{ split_id, status, amount, currency, source_id, source_type, destination, ... }`.
|
|
459
|
+
|
|
421
460
|
### Requests for Information (RFIs)
|
|
422
461
|
|
|
423
462
|
RFIs are compliance questions Airwallex raises against an account. They use
|
|
@@ -438,6 +477,50 @@ Airwallex::RFI.list({ statuses: 'ACTION_REQUIRED', types: 'KYC,KYC_ONGOING' }, h
|
|
|
438
477
|
.auto_paging_each { |r| puts r.id }
|
|
439
478
|
```
|
|
440
479
|
|
|
480
|
+
#### Responding to an RFI
|
|
481
|
+
|
|
482
|
+
Upload any documents first, then answer every question in `active_request`
|
|
483
|
+
in one call. Files go to the separate files host (`files-demo.airwallex.com`
|
|
484
|
+
in sandbox, `files.airwallex.com` in production), up to 20MB each, and the
|
|
485
|
+
returned `file_id` is what the answer references.
|
|
486
|
+
|
|
487
|
+
```ruby
|
|
488
|
+
passport = Airwallex::UploadedFile.upload(
|
|
489
|
+
File.open('passport.jpg', 'rb'),
|
|
490
|
+
filename: 'passport.jpg',
|
|
491
|
+
content_type: 'image/jpeg',
|
|
492
|
+
notes: 'passport-front',
|
|
493
|
+
opts: { headers: headers }
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
rfi = Airwallex::RFI.retrieve('rfi_123', headers: headers)
|
|
497
|
+
rfi.respond(
|
|
498
|
+
{
|
|
499
|
+
questions: [
|
|
500
|
+
{ id: 'q1', answer: { type: 'TEXT', text: 'We only operate in Australia' } },
|
|
501
|
+
{ id: 'q2', answer: { type: 'ATTACHMENT', attachments: [{ file_id: passport.file_id }] } },
|
|
502
|
+
{ id: 'q3', answer: { type: 'CONFIRMATION', confirmed: true } }
|
|
503
|
+
]
|
|
504
|
+
},
|
|
505
|
+
headers: headers
|
|
506
|
+
)
|
|
507
|
+
rfi.status # => "ANSWERED"
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
Answer types are `TEXT` (`text:`), `ATTACHMENT` (`attachments: [{ file_id: }]`),
|
|
511
|
+
`CONFIRMATION` (`confirmed:`), `ADDRESS` (`address: { address_line1:, ... }`),
|
|
512
|
+
`IDENTITY_DOCUMENT` (`identity_document: { type:, number:, issuing_country:,
|
|
513
|
+
front_file_id:, back_file_id: }`) and `LIVENESS`. A `LIVENESS` question is
|
|
514
|
+
completed at its `liveness[:url]` rather than answered here, so do that first.
|
|
515
|
+
The status then goes `ACTION_REQUIRED` → `ANSWERED` → `CLOSED`.
|
|
516
|
+
|
|
517
|
+
Errors: `BadRequestError` with code `invalid_state_for_operation` (the RFI
|
|
518
|
+
isn't `ACTION_REQUIRED`) or `invalid_argument` (a question is unanswered or
|
|
519
|
+
has the wrong answer shape), and `PermissionError` with
|
|
520
|
+
`account_not_authorised_for_operation`. You also get that last one when
|
|
521
|
+
answering RFIs for connected accounts through the API hasn't been enabled
|
|
522
|
+
for your platform; ask your Airwallex Account Manager to turn it on.
|
|
523
|
+
|
|
441
524
|
### Sandbox Simulations
|
|
442
525
|
|
|
443
526
|
Sandbox-only endpoints (`/api/v1/simulation/...`) that stand in for the bank,
|
|
@@ -555,8 +638,18 @@ transfer.simulate_transition(next_status: 'FAILED', failure_type: 'INSUFFICIENT_
|
|
|
555
638
|
|
|
556
639
|
# Class-level equivalent takes a transfer_id directly instead of an instance
|
|
557
640
|
Airwallex::Transfer.simulate_transition('tfr_123', next_status: 'CANCELLED')
|
|
641
|
+
|
|
642
|
+
# A transfer created x-on-behalf-of a connected account can only be found
|
|
643
|
+
# with the same header (cancel takes it too)
|
|
644
|
+
headers = { 'x-on-behalf-of' => 'acct_123' }
|
|
645
|
+
transfer.simulate_transition(next_status: 'SENT', headers: headers)
|
|
646
|
+
Airwallex::Transfer.simulate_transition('tfr_123', next_status: 'PAID', headers: headers)
|
|
647
|
+
transfer.cancel(headers: headers)
|
|
558
648
|
```
|
|
559
649
|
|
|
650
|
+
The sandbox rejects PROCESSING -> PAID with a 500 `operation_failed`. Move the
|
|
651
|
+
transfer to SENT first.
|
|
652
|
+
|
|
560
653
|
#### Connected Accounts & Offboarding
|
|
561
654
|
|
|
562
655
|
```ruby
|
|
@@ -746,6 +839,10 @@ begin
|
|
|
746
839
|
handle_successful_payment(event.data)
|
|
747
840
|
when 'payout.transfer.failed'
|
|
748
841
|
handle_failed_payout(event.data)
|
|
842
|
+
when 'funds_split.settled'
|
|
843
|
+
# funds_split.* payloads identify the split by split_id (not id) and
|
|
844
|
+
# carry no request_id — see the Scale section
|
|
845
|
+
handle_settled_split(event.data)
|
|
749
846
|
end
|
|
750
847
|
rescue Airwallex::SignatureVerificationError => e
|
|
751
848
|
# Invalid signature
|
|
@@ -849,7 +946,7 @@ end
|
|
|
849
946
|
- Customer (create, retrieve, list, update, delete)
|
|
850
947
|
- Dispute (retrieve, list, accept, challenge, related_payment_intents, simulate_create, simulate_escalate, simulate_resolve)
|
|
851
948
|
- **Payouts**:
|
|
852
|
-
- Transfer (create, retrieve, list, cancel, simulate_transition)
|
|
949
|
+
- Transfer (create, retrieve, list, cancel, simulate_transition; cancel/simulate_transition take headers: for x-on-behalf-of)
|
|
853
950
|
- Beneficiary (create, retrieve, list, update, delete, validate, verify_account, api_schema, form_schema, supported_financial_institutions)
|
|
854
951
|
- BatchTransfer (create, retrieve, list)
|
|
855
952
|
- **Foreign Exchange & Multi-Currency**:
|
|
@@ -876,6 +973,8 @@ end
|
|
|
876
973
|
- AccountAmendment (create, retrieve, simulate_approve, simulate_reject) - requires Admin-level API key permissions
|
|
877
974
|
- AccountOffboarding — sandbox Simulation only, scoped to a parent ConnectedAccount (simulate_complete, simulate_cancel)
|
|
878
975
|
- FundsSplit (create, retrieve, list, release)
|
|
976
|
+
- FundsSplitReversal (create, retrieve, list)
|
|
977
|
+
- ConnectedAccountTransfer (create, retrieve, list)
|
|
879
978
|
- Charge (create, retrieve, list)
|
|
880
979
|
- **Issuing** (sandbox Simulation only — no live Card/Cardholder resources yet):
|
|
881
980
|
- IssuingTransaction (simulate_create with single_phase, simulate_capture, simulate_reverse, simulate_refund, simulate_notify_three_ds)
|
|
@@ -883,7 +982,8 @@ end
|
|
|
883
982
|
- **In-Person Payments** (sandbox Simulation only):
|
|
884
983
|
- POSTerminal (simulate_turn_on, simulate_turn_off, simulate_generate_activation_code, simulate_confirm_payment_intent, simulate_payment_scenarios)
|
|
885
984
|
- **Compliance**:
|
|
886
|
-
- RFI (retrieve, list, plus sandbox simulate_create, simulate_close, simulate_follow_up)
|
|
985
|
+
- RFI (retrieve, list, respond, plus sandbox simulate_create, simulate_close, simulate_follow_up)
|
|
986
|
+
- UploadedFile (upload, to the files host)
|
|
887
987
|
- **Webhooks**: Event handling, HMAC-SHA256 signature verification
|
|
888
988
|
|
|
889
989
|
### Coming in Future Versions
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
class Client
|
|
5
|
+
# Multipart uploads to the files host (config.files_url), which is
|
|
6
|
+
# separate from the main API host
|
|
7
|
+
module FileUploads
|
|
8
|
+
# Separate connection for the files host (config.files_url). It sends
|
|
9
|
+
# multipart bodies, so it deliberately has no JSON Content-Type default,
|
|
10
|
+
# no :json request encoding, and no Idempotency middleware (which would
|
|
11
|
+
# add a stray request_id form field). No :retry either: faraday-retry
|
|
12
|
+
# only retries GET/DELETE here, and uploads are POSTs.
|
|
13
|
+
def files_connection
|
|
14
|
+
@files_connection ||= Faraday.new(url: config.files_url) do |conn|
|
|
15
|
+
conn.request :multipart
|
|
16
|
+
conn.use Airwallex::Middleware::AuthRefresh, self
|
|
17
|
+
conn.response :json, content_type: /\bjson$/
|
|
18
|
+
# Never log bodies here: they're the uploaded file's bytes
|
|
19
|
+
configure_logger(conn, bodies: false)
|
|
20
|
+
|
|
21
|
+
conn.headers["User-Agent"] = user_agent
|
|
22
|
+
conn.headers["x-api-version"] = config.api_version if config.api_version
|
|
23
|
+
|
|
24
|
+
conn.adapter Faraday.default_adapter
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# POST a multipart upload to the files host
|
|
29
|
+
#
|
|
30
|
+
# @param path [String] e.g. "/api/v1/files/upload"
|
|
31
|
+
# @param file_part [Faraday::Multipart::FilePart] sent as the "file" field
|
|
32
|
+
# @param params [Hash] extra form fields (e.g. notes:)
|
|
33
|
+
# @param headers [Hash] e.g. { "x-on-behalf-of" => account_id }
|
|
34
|
+
# @return [Hash] parsed response body
|
|
35
|
+
def upload(path, file_part, params = {}, headers = {})
|
|
36
|
+
response = files_connection.post(path) do |req|
|
|
37
|
+
req.headers.merge!(headers)
|
|
38
|
+
req.body = { file: file_part }.merge(params)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
handle_response_errors(response)
|
|
42
|
+
response.body
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
class Client
|
|
5
|
+
module Logging
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# Log requests/responses with credentials masked: the API key header,
|
|
9
|
+
# the bearer token, and the token in the login response body
|
|
10
|
+
def configure_logger(conn, bodies:)
|
|
11
|
+
return unless config.logger
|
|
12
|
+
|
|
13
|
+
conn.response :logger, config.logger, { headers: true, bodies: bodies } do |logger|
|
|
14
|
+
logger.filter(/(x-api-key: )"[^"]*"/i, '\1"[FILTERED]"')
|
|
15
|
+
logger.filter(/(Authorization: )"Bearer [^"]*"/i, '\1"Bearer [FILTERED]"')
|
|
16
|
+
logger.filter(/("token":\s*)"[^"]*"/, '\1"[FILTERED]"')
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/airwallex/client.rb
CHANGED
|
@@ -4,9 +4,14 @@ require "faraday"
|
|
|
4
4
|
require "faraday/multipart"
|
|
5
5
|
require "faraday/retry"
|
|
6
6
|
require "json"
|
|
7
|
+
require_relative "client/file_uploads"
|
|
8
|
+
require_relative "client/logging"
|
|
7
9
|
|
|
8
10
|
module Airwallex
|
|
9
11
|
class Client
|
|
12
|
+
include FileUploads
|
|
13
|
+
include Logging
|
|
14
|
+
|
|
10
15
|
LOGIN_PATH = "/api/v1/authentication/login"
|
|
11
16
|
|
|
12
17
|
attr_reader :config, :access_token, :token_expires_at
|
|
@@ -105,7 +110,7 @@ module Airwallex
|
|
|
105
110
|
conn.request :retry, retry_options
|
|
106
111
|
conn.use Airwallex::Middleware::AuthRefresh, self
|
|
107
112
|
conn.response :json, content_type: /\bjson$/
|
|
108
|
-
conn
|
|
113
|
+
configure_logger(conn, bodies: true)
|
|
109
114
|
end
|
|
110
115
|
|
|
111
116
|
def handle_response_errors(response)
|
|
@@ -16,12 +16,19 @@ module Airwallex
|
|
|
16
16
|
@client.ensure_authenticated! unless authentication_request?(env)
|
|
17
17
|
authorize!(env)
|
|
18
18
|
|
|
19
|
+
# Once a response arrives, env[:body] holds the response body, so
|
|
20
|
+
# keep the request body to re-send it (same as faraday-retry)
|
|
21
|
+
request_body = env[:body]
|
|
19
22
|
response = @app.call(env)
|
|
20
23
|
|
|
21
24
|
# If we get a 401, try refreshing the token and retrying once
|
|
22
25
|
if response.status == 401
|
|
23
26
|
@client.authenticate!
|
|
24
27
|
authorize!(env)
|
|
28
|
+
env[:body] = request_body
|
|
29
|
+
# A multipart upload body is a stream the first attempt already
|
|
30
|
+
# read; rewind it so the retry sends the file again
|
|
31
|
+
request_body.rewind if request_body.respond_to?(:rewind)
|
|
25
32
|
response = @app.call(env)
|
|
26
33
|
end
|
|
27
34
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
# Represents a Connected Account Transfer — moves funds between the platform
|
|
5
|
+
# account and a connected account's wallet. Send x-on-behalf-of to debit a
|
|
6
|
+
# connected account instead of the platform.
|
|
7
|
+
#
|
|
8
|
+
# Statuses: NEW, PENDING, SETTLED, SUSPENDED, FAILED. Create errors include
|
|
9
|
+
# insufficient_fund and request_id_duplicate.
|
|
10
|
+
#
|
|
11
|
+
# @example Fund a connected account from the platform
|
|
12
|
+
# transfer = Airwallex::ConnectedAccountTransfer.create(
|
|
13
|
+
# request_id: "cat-req-123", # required, 1-50
|
|
14
|
+
# amount: "100.00",
|
|
15
|
+
# currency: "AUD",
|
|
16
|
+
# destination: "acct_123", # connected account id
|
|
17
|
+
# reason: "transfer_to_own_account",
|
|
18
|
+
# reference: "Ledger top-up" # required, 1-140
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
# @example List settled transfers to an account
|
|
22
|
+
# Airwallex::ConnectedAccountTransfer.list(destination: "acct_123", status: "SETTLED")
|
|
23
|
+
class ConnectedAccountTransfer < APIResource
|
|
24
|
+
extend APIOperations::Create
|
|
25
|
+
extend APIOperations::Retrieve
|
|
26
|
+
extend APIOperations::List
|
|
27
|
+
|
|
28
|
+
# @return [String] API resource path for connected account transfers
|
|
29
|
+
def self.resource_path
|
|
30
|
+
"/api/v1/connected_account_transfers"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Airwallex
|
|
4
|
-
# Represents a Funds Split —
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# Represents a Funds Split — moves part of a PaymentIntent's funds from the
|
|
5
|
+
# platform account to a Connected Account. Each split has exactly one
|
|
6
|
+
# destination; create one split per connected account.
|
|
7
|
+
#
|
|
8
|
+
# Create errors include duplicate_request (reused request_id) and
|
|
9
|
+
# amount_above_limit (splits exceed the PaymentIntent amount).
|
|
10
|
+
#
|
|
11
|
+
# funds_split.* webhook payloads name the split split_id, not id, and carry
|
|
12
|
+
# no request_id: { split_id:, status:, amount:, currency:, source_id:,
|
|
13
|
+
# source_type:, destination:, ... }.
|
|
7
14
|
#
|
|
8
15
|
# @example Create a split
|
|
9
16
|
# split = Airwallex::FundsSplit.create(
|
|
10
|
-
#
|
|
11
|
-
#
|
|
17
|
+
# request_id: "split-req-123", # required, max 64
|
|
18
|
+
# source_id: intent.id, # required, PaymentIntent id
|
|
19
|
+
# source_type: "PAYMENT_INTENT", # required
|
|
20
|
+
# amount: "50.00", # required, string
|
|
21
|
+
# destination: connected_account.id,
|
|
22
|
+
# auto_release: true, # optional, default true
|
|
23
|
+
# metadata: { order_id: "123" } # optional
|
|
12
24
|
# )
|
|
13
25
|
#
|
|
14
|
-
# @example Release the split funds
|
|
26
|
+
# @example Release the split funds (when created with auto_release: false)
|
|
15
27
|
# split.release
|
|
28
|
+
#
|
|
29
|
+
# @see FundsSplitReversal to move split funds back to the platform
|
|
16
30
|
class FundsSplit < APIResource
|
|
17
31
|
extend APIOperations::Create
|
|
18
32
|
extend APIOperations::Retrieve
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
# Represents a Funds Split Reversal — moves previously split funds back from
|
|
5
|
+
# the connected account (the split's destination) to the platform account.
|
|
6
|
+
# Partial reversals are allowed until the full split amount is reversed.
|
|
7
|
+
#
|
|
8
|
+
# Statuses: CREATED, RELEASED, SETTLED.
|
|
9
|
+
#
|
|
10
|
+
# @example Reverse part of a split
|
|
11
|
+
# reversal = Airwallex::FundsSplitReversal.create(
|
|
12
|
+
# request_id: "rev-req-123", # required, max 64
|
|
13
|
+
# funds_split_id: split.id, # required
|
|
14
|
+
# amount: "5.00", # required, string
|
|
15
|
+
# metadata: { reason: "refund" }
|
|
16
|
+
# )
|
|
17
|
+
# reversal.status # => "CREATED"
|
|
18
|
+
#
|
|
19
|
+
# @example List the reversals of a split
|
|
20
|
+
# Airwallex::FundsSplitReversal.list(funds_split_id: split.id)
|
|
21
|
+
class FundsSplitReversal < APIResource
|
|
22
|
+
extend APIOperations::Create
|
|
23
|
+
extend APIOperations::Retrieve
|
|
24
|
+
extend APIOperations::List
|
|
25
|
+
|
|
26
|
+
# @return [String] API resource path for funds split reversals
|
|
27
|
+
def self.resource_path
|
|
28
|
+
"/api/v1/pa/funds_split_reversals"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -5,7 +5,7 @@ module Airwallex
|
|
|
5
5
|
# Airwallex raises against your account (KYC, an ongoing KYC review, a
|
|
6
6
|
# cardholder, a transaction, payment enablement, or merchant risk).
|
|
7
7
|
#
|
|
8
|
-
# Supports retrieving and
|
|
8
|
+
# Supports retrieving, listing and responding to real RFIs, plus the sandbox Simulation
|
|
9
9
|
# endpoints for raising, closing and following up on them (there is no
|
|
10
10
|
# live create — real RFIs originate from Airwallex's own compliance
|
|
11
11
|
# review, same as Dispute). See
|
|
@@ -30,6 +30,41 @@ module Airwallex
|
|
|
30
30
|
# headers: { "x-on-behalf-of" => "acct_123" }
|
|
31
31
|
# ).auto_paging_each { |rfi| puts rfi.id }
|
|
32
32
|
#
|
|
33
|
+
# @example Respond to an RFI (upload any documents first, see UploadedFile)
|
|
34
|
+
# # Every question in active_request must be answered. Answer types:
|
|
35
|
+
# # TEXT, ATTACHMENT, IDENTITY_DOCUMENT, CONFIRMATION, LIVENESS, ADDRESS.
|
|
36
|
+
# # Any answer can also carry a comment:. LIVENESS has no answer body:
|
|
37
|
+
# # complete the liveness check at question[:liveness][:url] first (an
|
|
38
|
+
# # RFI whose only question is LIVENESS resolves itself), then respond
|
|
39
|
+
# # to the rest.
|
|
40
|
+
# rfi.respond(
|
|
41
|
+
# {
|
|
42
|
+
# questions: [
|
|
43
|
+
# { id: "q1", answer: { type: "TEXT", text: "..." } },
|
|
44
|
+
# { id: "q2", answer: { type: "ATTACHMENT", attachments: [{ file_id: file.file_id }] } },
|
|
45
|
+
# { id: "q3", answer: { type: "CONFIRMATION", confirmed: true } },
|
|
46
|
+
# { id: "q4", answer: { type: "ADDRESS", address: { address_line1: "...", address_line2: "...",
|
|
47
|
+
# suburb: "...", state: "...", postcode: "...",
|
|
48
|
+
# country_code: "AU" } } },
|
|
49
|
+
# { id: "q5", answer: { type: "IDENTITY_DOCUMENT",
|
|
50
|
+
# identity_document: { type: "PASSPORT", number: "...", issuing_country: "AU",
|
|
51
|
+
# front_file_id: "...", back_file_id: "..." } } }
|
|
52
|
+
# ]
|
|
53
|
+
# },
|
|
54
|
+
# headers: { "x-on-behalf-of" => "acct_123" }
|
|
55
|
+
# )
|
|
56
|
+
# rfi.status # => "ANSWERED"
|
|
57
|
+
#
|
|
58
|
+
# Responding: the lifecycle is ACTION_REQUIRED -> ANSWERED -> CLOSED
|
|
59
|
+
# (an RFI also moves to CLOSED when active_request.expires_at passes).
|
|
60
|
+
# Responding needs the risk.rfi:write scope. Errors:
|
|
61
|
+
# - invalid_state_for_operation (400): the RFI isn't ACTION_REQUIRED
|
|
62
|
+
# - invalid_argument (400): a question is unanswered, or an answer has
|
|
63
|
+
# the wrong shape (e.g. TEXT vs ATTACHMENT)
|
|
64
|
+
# - account_not_authorised_for_operation (403): the RFI isn't linked to
|
|
65
|
+
# the account, or answering RFIs for connected accounts through the
|
|
66
|
+
# Native API hasn't been enabled (ask your Airwallex Account Manager)
|
|
67
|
+
#
|
|
33
68
|
# @example Raise a KYC RFI, then close it
|
|
34
69
|
# rfi = Airwallex::RFI.simulate_create(
|
|
35
70
|
# type: "KYC",
|
|
@@ -75,6 +110,31 @@ module Airwallex
|
|
|
75
110
|
)
|
|
76
111
|
end
|
|
77
112
|
|
|
113
|
+
# Respond to an RFI. params are sent as-is (see the class docs for the
|
|
114
|
+
# body shape). Unlike other POSTs, no request_id is added: the endpoint
|
|
115
|
+
# doesn't document one.
|
|
116
|
+
#
|
|
117
|
+
# @param rfi_id [String]
|
|
118
|
+
# @param params [Hash] questions: (required) array of { id:, answer: { type:, ... } }
|
|
119
|
+
# @param opts [Hash] headers: (e.g. { "x-on-behalf-of" => account_id })
|
|
120
|
+
# @return [RFI]
|
|
121
|
+
def self.respond(rfi_id, params = {}, opts = {})
|
|
122
|
+
response = Airwallex.client.post("#{resource_path}/#{rfi_id}/respond", without_request_id(params),
|
|
123
|
+
opts[:headers] || {})
|
|
124
|
+
new(response)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Pre-encode the body as JSON: the Idempotency middleware only adds
|
|
128
|
+
# request_id to Hash bodies, and the JSON middleware passes strings on
|
|
129
|
+
# unchanged
|
|
130
|
+
#
|
|
131
|
+
# @param params [Hash]
|
|
132
|
+
# @return [String]
|
|
133
|
+
def self.without_request_id(params)
|
|
134
|
+
JSON.generate(params)
|
|
135
|
+
end
|
|
136
|
+
private_class_method :without_request_id
|
|
137
|
+
|
|
78
138
|
# Simulate Airwallex raising an RFI
|
|
79
139
|
#
|
|
80
140
|
# @param params [Hash] type: (required, one of "KYC", "KYC_ONGOING",
|
|
@@ -111,6 +171,16 @@ module Airwallex
|
|
|
111
171
|
new(response)
|
|
112
172
|
end
|
|
113
173
|
|
|
174
|
+
# Respond to this RFI. See .respond
|
|
175
|
+
#
|
|
176
|
+
# @param params [Hash] questions: (required)
|
|
177
|
+
# @param opts [Hash] headers: (e.g. { "x-on-behalf-of" => account_id })
|
|
178
|
+
# @return [RFI] self
|
|
179
|
+
def respond(params = {}, opts = {})
|
|
180
|
+
refresh_from(self.class.respond(id, params, opts).to_hash)
|
|
181
|
+
self
|
|
182
|
+
end
|
|
183
|
+
|
|
114
184
|
# Simulate closing this RFI
|
|
115
185
|
#
|
|
116
186
|
# @param opts [Hash] headers: (e.g. { "x-on-behalf-of" => account_id })
|
|
@@ -14,10 +14,14 @@ module Airwallex
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
# Cancel a pending transfer
|
|
17
|
-
|
|
17
|
+
#
|
|
18
|
+
# @param opts [Hash] headers: (e.g. { "x-on-behalf-of" => account_id })
|
|
19
|
+
# @return [Transfer] self
|
|
20
|
+
def cancel(opts = {})
|
|
18
21
|
response = Airwallex.client.post(
|
|
19
22
|
"#{self.class.resource_path}/#{id}/cancel",
|
|
20
|
-
{}
|
|
23
|
+
{},
|
|
24
|
+
opts[:headers] || {}
|
|
21
25
|
)
|
|
22
26
|
refresh_from(response)
|
|
23
27
|
self
|
|
@@ -25,18 +29,21 @@ module Airwallex
|
|
|
25
29
|
|
|
26
30
|
# Simulate this transfer's status advancing one step. The standard
|
|
27
31
|
# lifecycle is SCHEDULED -> PROCESSING -> SENT -> PAID, progressed one
|
|
28
|
-
# step at a time
|
|
29
|
-
#
|
|
32
|
+
# step at a time (the sandbox rejects PROCESSING -> PAID with a 500
|
|
33
|
+
# operation_failed; go through SENT); OVERDUE, FAILED, and CANCELLED can
|
|
34
|
+
# be jumped to directly.
|
|
30
35
|
#
|
|
31
36
|
# @param next_status [String] one of "OVERDUE", "PROCESSING", "SENT",
|
|
32
37
|
# "PAID", "FAILED", "CANCELLED"
|
|
33
38
|
# @param failure_type [String, nil] failure reason code, only
|
|
34
39
|
# meaningful when next_status is "FAILED"
|
|
40
|
+
# @param headers [Hash] e.g. { "x-on-behalf-of" => account_id } — required
|
|
41
|
+
# for a transfer created on behalf of a connected account
|
|
35
42
|
# @return [Transfer] self
|
|
36
|
-
def simulate_transition(next_status:, failure_type: nil)
|
|
43
|
+
def simulate_transition(next_status:, failure_type: nil, headers: {})
|
|
37
44
|
params = { next_status: next_status }
|
|
38
45
|
params[:failure_type] = failure_type if failure_type
|
|
39
|
-
response = Airwallex.client.post("#{SIMULATION_PATH}/#{id}/transition", params)
|
|
46
|
+
response = Airwallex.client.post("#{SIMULATION_PATH}/#{id}/transition", params, headers)
|
|
40
47
|
refresh_from(response)
|
|
41
48
|
self
|
|
42
49
|
end
|
|
@@ -48,11 +55,13 @@ module Airwallex
|
|
|
48
55
|
# "PAID", "FAILED", "CANCELLED"
|
|
49
56
|
# @param failure_type [String, nil] failure reason code, only
|
|
50
57
|
# meaningful when next_status is "FAILED"
|
|
58
|
+
# @param headers [Hash] e.g. { "x-on-behalf-of" => account_id } — required
|
|
59
|
+
# for a transfer created on behalf of a connected account
|
|
51
60
|
# @return [Transfer]
|
|
52
|
-
def self.simulate_transition(transfer_id, next_status:, failure_type: nil)
|
|
61
|
+
def self.simulate_transition(transfer_id, next_status:, failure_type: nil, headers: {})
|
|
53
62
|
params = { next_status: next_status }
|
|
54
63
|
params[:failure_type] = failure_type if failure_type
|
|
55
|
-
response = Airwallex.client.post("#{SIMULATION_PATH}/#{transfer_id}/transition", params)
|
|
64
|
+
response = Airwallex.client.post("#{SIMULATION_PATH}/#{transfer_id}/transition", params, headers)
|
|
56
65
|
new(response)
|
|
57
66
|
end
|
|
58
67
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
# Represents a file uploaded to Airwallex's files host (config.files_url,
|
|
5
|
+
# not the main API host) — e.g. a KYC document. The returned file_id is
|
|
6
|
+
# what other endpoints reference, such as RFI answers
|
|
7
|
+
# (attachments[].file_id, identity_document front_file_id/back_file_id).
|
|
8
|
+
# See https://www.airwallex.com/docs/api/supporting_services/file_service/upload_files
|
|
9
|
+
#
|
|
10
|
+
# Limits: 20MB per file; filename and notes up to 50 characters each.
|
|
11
|
+
# The response has file_id, filename, notes, size (bytes), object_type
|
|
12
|
+
# and created (epoch seconds, not created_at).
|
|
13
|
+
#
|
|
14
|
+
# Named UploadedFile rather than File so that bare File.open/File.read
|
|
15
|
+
# inside the Airwallex module still resolve to ::File.
|
|
16
|
+
#
|
|
17
|
+
# @example Upload a PDF on behalf of a connected account
|
|
18
|
+
# file = Airwallex::UploadedFile.upload(
|
|
19
|
+
# File.open("smo_declaration.pdf", "rb"),
|
|
20
|
+
# filename: "smo_declaration.pdf",
|
|
21
|
+
# content_type: "application/pdf",
|
|
22
|
+
# notes: "SMO declaration",
|
|
23
|
+
# opts: { headers: { "x-on-behalf-of" => "acct_123" } }
|
|
24
|
+
# )
|
|
25
|
+
# file.file_id # => "..."
|
|
26
|
+
class UploadedFile < APIResource
|
|
27
|
+
# @return [String] API path for file uploads, on the files host
|
|
28
|
+
def self.resource_path
|
|
29
|
+
"/api/v1/files/upload"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Upload a file as multipart/form-data (form field "file")
|
|
33
|
+
#
|
|
34
|
+
# @param io [IO, StringIO, String] an IO, or a local file path
|
|
35
|
+
# @param filename [String]
|
|
36
|
+
# @param content_type [String] e.g. "application/pdf", "image/jpeg"
|
|
37
|
+
# @param notes [String, nil] a label, sent as the notes form field
|
|
38
|
+
# @param opts [Hash] headers: (e.g. { "x-on-behalf-of" => account_id })
|
|
39
|
+
# @return [UploadedFile]
|
|
40
|
+
def self.upload(io, filename:, content_type:, notes: nil, opts: {})
|
|
41
|
+
file_part = Faraday::Multipart::FilePart.new(io, content_type, filename)
|
|
42
|
+
params = notes.nil? ? {} : { notes: notes }
|
|
43
|
+
response = Airwallex.client.upload(resource_path, file_part, params, opts[:headers] || {})
|
|
44
|
+
new(response)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# The upload response identifies the file by file_id, not id, so id
|
|
48
|
+
# mirrors file_id to behave like other resources
|
|
49
|
+
def initialize(attributes = {})
|
|
50
|
+
super
|
|
51
|
+
@id ||= @attributes[:file_id]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def refresh_from(data)
|
|
55
|
+
super
|
|
56
|
+
@id ||= @attributes[:file_id]
|
|
57
|
+
self
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @return [String, nil]
|
|
61
|
+
def file_id
|
|
62
|
+
@attributes[:file_id]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/airwallex/version.rb
CHANGED
data/lib/airwallex.rb
CHANGED
|
@@ -47,6 +47,8 @@ require_relative "airwallex/resources/payment_source"
|
|
|
47
47
|
require_relative "airwallex/resources/connected_account"
|
|
48
48
|
require_relative "airwallex/resources/account_amendment"
|
|
49
49
|
require_relative "airwallex/resources/funds_split"
|
|
50
|
+
require_relative "airwallex/resources/funds_split_reversal"
|
|
51
|
+
require_relative "airwallex/resources/connected_account_transfer"
|
|
50
52
|
require_relative "airwallex/resources/charge"
|
|
51
53
|
require_relative "airwallex/resources/deposit"
|
|
52
54
|
require_relative "airwallex/resources/issuing_transaction"
|
|
@@ -54,6 +56,7 @@ require_relative "airwallex/resources/cardholder"
|
|
|
54
56
|
require_relative "airwallex/resources/linked_account"
|
|
55
57
|
require_relative "airwallex/resources/account_offboarding"
|
|
56
58
|
require_relative "airwallex/resources/rfi"
|
|
59
|
+
require_relative "airwallex/resources/uploaded_file"
|
|
57
60
|
require_relative "airwallex/resources/pos_terminal"
|
|
58
61
|
|
|
59
62
|
module Airwallex
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: airwallex
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chayut Orapinpatipat
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -75,6 +75,8 @@ files:
|
|
|
75
75
|
- lib/airwallex/api_operations/update.rb
|
|
76
76
|
- lib/airwallex/api_resource.rb
|
|
77
77
|
- lib/airwallex/client.rb
|
|
78
|
+
- lib/airwallex/client/file_uploads.rb
|
|
79
|
+
- lib/airwallex/client/logging.rb
|
|
78
80
|
- lib/airwallex/configuration.rb
|
|
79
81
|
- lib/airwallex/errors.rb
|
|
80
82
|
- lib/airwallex/list_object.rb
|
|
@@ -93,11 +95,13 @@ files:
|
|
|
93
95
|
- lib/airwallex/resources/cardholder.rb
|
|
94
96
|
- lib/airwallex/resources/charge.rb
|
|
95
97
|
- lib/airwallex/resources/connected_account.rb
|
|
98
|
+
- lib/airwallex/resources/connected_account_transfer.rb
|
|
96
99
|
- lib/airwallex/resources/conversion.rb
|
|
97
100
|
- lib/airwallex/resources/customer.rb
|
|
98
101
|
- lib/airwallex/resources/deposit.rb
|
|
99
102
|
- lib/airwallex/resources/dispute.rb
|
|
100
103
|
- lib/airwallex/resources/funds_split.rb
|
|
104
|
+
- lib/airwallex/resources/funds_split_reversal.rb
|
|
101
105
|
- lib/airwallex/resources/global_account.rb
|
|
102
106
|
- lib/airwallex/resources/global_account_alias.rb
|
|
103
107
|
- lib/airwallex/resources/global_account_mandate.rb
|
|
@@ -114,6 +118,7 @@ files:
|
|
|
114
118
|
- lib/airwallex/resources/refund.rb
|
|
115
119
|
- lib/airwallex/resources/rfi.rb
|
|
116
120
|
- lib/airwallex/resources/transfer.rb
|
|
121
|
+
- lib/airwallex/resources/uploaded_file.rb
|
|
117
122
|
- lib/airwallex/util.rb
|
|
118
123
|
- lib/airwallex/version.rb
|
|
119
124
|
- lib/airwallex/webhook.rb
|