airwallex 0.3.0 → 0.7.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 +90 -8
- data/README.md +2 -2
- data/lib/airwallex/api_operations/delete.rb +3 -3
- data/lib/airwallex/api_operations/update.rb +4 -4
- data/lib/airwallex/client.rb +15 -11
- data/lib/airwallex/configuration.rb +6 -2
- data/lib/airwallex/middleware/auth_refresh.rb +21 -7
- data/lib/airwallex/resources/account_amendment.rb +29 -0
- data/lib/airwallex/resources/beneficiary.rb +96 -0
- data/lib/airwallex/resources/billing_customer.rb +48 -0
- data/lib/airwallex/resources/billing_price.rb +30 -0
- data/lib/airwallex/resources/billing_product.rb +23 -0
- data/lib/airwallex/resources/billing_subscription.rb +93 -0
- data/lib/airwallex/resources/billing_subscription_item.rb +7 -0
- data/lib/airwallex/resources/charge.rb +19 -0
- data/lib/airwallex/resources/connected_account.rb +136 -0
- data/lib/airwallex/resources/conversion.rb +1 -1
- data/lib/airwallex/resources/customer.rb +5 -2
- data/lib/airwallex/resources/dispute.rb +48 -28
- data/lib/airwallex/resources/funds_split.rb +37 -0
- data/lib/airwallex/resources/global_account.rb +137 -0
- data/lib/airwallex/resources/global_account_alias.rb +62 -0
- data/lib/airwallex/resources/global_account_mandate.rb +26 -0
- data/lib/airwallex/resources/global_account_transaction.rb +7 -0
- data/lib/airwallex/resources/payment_consent.rb +68 -0
- data/lib/airwallex/resources/payment_method.rb +13 -5
- data/lib/airwallex/resources/payment_source.rb +52 -0
- data/lib/airwallex/resources/quote.rb +5 -3
- data/lib/airwallex/resources/rate.rb +2 -7
- data/lib/airwallex/version.rb +1 -1
- data/lib/airwallex/webhook.rb +6 -2
- data/lib/airwallex.rb +15 -0
- metadata +18 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fad10ff0adfb96f6cb5e3a88ec1825a17b840d73658bd6fb6813f18e2746fea1
|
|
4
|
+
data.tar.gz: 9f59e5dfc64382838c7b1485b4ca28b19e9b71248ea51070ff106ad33cb1af32
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: de57501742d42048dd6a9286672c18050a17734ac2e21780f85e2d23e16e856bf406df20842352d55cd46aff22bfb0ade9676e1e6578865a7c966eba833880eb
|
|
7
|
+
data.tar.gz: 809fce3bea1462cecb398388b34863f65edf4ef00a5742ce0c30776c0dcbf942b474daef0551e64cd81b79ddb8a44fb01ef210fd2cec8470dbe844ce4f5968c6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,104 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.7.0] - 2026-08-28
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- GlobalAccount resource (create, retrieve, list, update, `#close`, `#generate_statement_letter`,
|
|
7
|
+
`#transactions`)
|
|
8
|
+
- GlobalAccountAlias nested resource (`#create_alias`, `#alias`, `#aliases`, `#initiate_port`,
|
|
9
|
+
`#submit_verification_code`, `#request_new_verification_code`, `#cancel`)
|
|
10
|
+
- GlobalAccountMandate nested resource (`#mandate`, `#mandates`, `#cancel`)
|
|
11
|
+
- Billing resources: BillingCustomer, BillingProduct, BillingPrice, BillingSubscription (with `#items`/`#item`)
|
|
12
|
+
- PaymentConsent resource (create, retrieve, list, update, `#verify`, `#verify_continue`, `#disable`)
|
|
13
|
+
- PaymentSource resource (create, retrieve, list)
|
|
14
|
+
- ConnectedAccount resource (create, retrieve, list, update, `#submit`, `#agree_to_terms_and_conditions`,
|
|
15
|
+
`#suspend`, `#reactivate`, `.current`, `.wallet_info`, `#legal_entity_id`)
|
|
16
|
+
- AccountAmendment resource (create, retrieve)
|
|
17
|
+
- FundsSplit resource (create, retrieve, list, `#release`)
|
|
18
|
+
- Charge resource (create, retrieve, list)
|
|
19
|
+
- Beneficiary gained `.update`/`#update`, `.validate`, `.verify_account`, `.api_schema`, `.form_schema`, and
|
|
20
|
+
`.supported_financial_institutions`
|
|
21
|
+
- Dispute gained `.update`, `#challenge` (replacing `#submit_evidence`), and `#related_payment_intents`
|
|
22
|
+
- PaymentMethod gained `#disable`
|
|
23
|
+
- New tests covering all of the above (378 total)
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- `APIOperations::Update` now sends `POST #{resource_path}/{id}/update` instead of `PUT #{resource_path}/{id}`
|
|
27
|
+
- `APIOperations::Delete` now sends `POST #{resource_path}/{id}/delete` instead of `DELETE #{resource_path}/{id}`,
|
|
28
|
+
and checks the response instead of always returning `true`
|
|
29
|
+
- `Dispute`'s resource path corrected from `/api/v1/disputes` to `/api/v1/pa/payment_disputes`
|
|
30
|
+
- `Conversion`'s resource path corrected from `/api/v1/conversions` to `/api/v1/fx/conversions`
|
|
31
|
+
- `Configuration#api_version` now defaults to `nil` instead of a hardcoded date, and `x-api-version` is only
|
|
32
|
+
sent when explicitly set
|
|
33
|
+
- `Beneficiary.create`/`.validate`/`.update` payloads corrected: wrapped under a top-level `beneficiary` key,
|
|
34
|
+
with `nickname`/`payer_entity_type`/`transfer_methods`/`transfer_reason` as top-level siblings;
|
|
35
|
+
`entity_type` replaces the nonexistent `beneficiary_type`
|
|
36
|
+
- `Beneficiary#update`/`.update` requires the full payload, not a partial patch
|
|
37
|
+
- `Beneficiary.supported_financial_institutions` requires `account_currency`, `entity_type`,
|
|
38
|
+
`transfer_method`, and `keyword` in addition to `bank_country_code`
|
|
39
|
+
- `Customer.create` requires `merchant_customer_id`
|
|
40
|
+
- `GlobalAccount#generate_statement_letter` requires `account_statement_type` and `registration_info`
|
|
41
|
+
- `ConnectedAccount.create` payload corrected: `account_details` is a required top-level wrapper;
|
|
42
|
+
`legal_entity_type` is `"BUSINESS"`/`"INDIVIDUAL"`, not `"COMPANY"`
|
|
43
|
+
- `BillingCustomer.create` payload corrected to match the real flat schema (`name`/`email`/`type`/
|
|
44
|
+
`default_billing_currency`/`address`/`default_legal_entity_id`)
|
|
45
|
+
- `BillingPrice.create` requires `pricing_model` and `recurring`
|
|
46
|
+
- `BillingSubscription.create` payload corrected: prices attach via an `items:` array, not a flat
|
|
47
|
+
`price_id`; `starts_at` replaces `start_date`; added `legal_entity_id` and `payment_source_id`
|
|
48
|
+
- `AccountAmendment.create` payload corrected to use `target` plus the changed section as a top-level
|
|
49
|
+
sibling, not a generic `changes:` wrapper
|
|
50
|
+
- `PaymentSource.create`'s `external_id` corrected to reference a `PaymentMethod` id, not a `PaymentConsent` id
|
|
51
|
+
- `ConnectedAccount#legal_entity_id` added as the source for `BillingCustomer`/`BillingSubscription`'s
|
|
52
|
+
`legal_entity_id`, rather than a separate resource
|
|
53
|
+
- `Rate`/`Quote` use `sell_currency`/`buy_currency`, not `from_currency`/`to_currency`
|
|
54
|
+
- `Quote.create` requires `validity` (`MIN_1`, `MIN_15`, `MIN_30`, `HR_1`, `HR_4`, `HR_8`, or `HR_24`)
|
|
55
|
+
|
|
56
|
+
### Removed
|
|
57
|
+
- `Rate.list` — the endpoint has no "list all rates" concept, only `.retrieve(sell_currency:, buy_currency:)`
|
|
58
|
+
- `PaymentMethod.delete` and `PaymentMethod#detach` — replaced by `#disable`, the real lifecycle operation
|
|
59
|
+
|
|
60
|
+
## [0.6.0] - 2026-08-28
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
- `Webhook::Event` now correctly exposes the event type. Airwallex sends this field as `"name"` in
|
|
64
|
+
the webhook payload, not `"type"` — the previous `Event#type` reader was reading a key that doesn't
|
|
65
|
+
exist and always returned `nil`. The accessor is renamed to `Event#name` to match Airwallex's actual
|
|
66
|
+
field. **Breaking change:** `event.type` → `event.name`.
|
|
67
|
+
|
|
68
|
+
## [0.5.0] - 2026-08-28
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
- Webhook signature verification no longer rejects valid webhooks when Airwallex sends
|
|
72
|
+
millisecond-precision timestamps. `verify_timestamp` now detects millisecond values (anything at or
|
|
73
|
+
above `MS_THRESHOLD = 10_000_000_000`, which safely distinguishes seconds from milliseconds until the
|
|
74
|
+
year 2286) and normalizes them to seconds before comparing against the tolerance window.
|
|
75
|
+
|
|
76
|
+
## [0.4.0] - 2026-08-27
|
|
77
|
+
|
|
78
|
+
### Fixed
|
|
79
|
+
- `Idempotency` and `AuthRefresh` middleware are now actually registered on the Faraday connection.
|
|
80
|
+
Previously both classes existed but were never wired in, so the "automatic `request_id` generation"
|
|
81
|
+
and "retry once after a 401" behavior documented in the README did not happen at runtime.
|
|
82
|
+
- Fixed a bug in `AuthRefresh` where the 401-retry guard used `env[:request][:auth_retry]`, a key that
|
|
83
|
+
doesn't exist on `Faraday::RequestOptions` and would have raised `NoMethodError` the first time it ran.
|
|
84
|
+
- CI now triggers on pushes to `main` (previously configured for `master`, so pushes never ran CI).
|
|
85
|
+
|
|
86
|
+
### Changed
|
|
87
|
+
- `Client#request` no longer manually manages the `Authorization` header or calls
|
|
88
|
+
`ensure_authenticated!` directly; this is now owned by the `AuthRefresh` middleware.
|
|
89
|
+
|
|
3
90
|
## [0.3.0] - 2025-11-25
|
|
4
91
|
|
|
5
92
|
### Added
|
|
93
|
+
- BatchTransfer resource (create, retrieve, list) for bulk payout operations
|
|
94
|
+
- Dispute resource (retrieve, list, accept, submit_evidence) for chargeback management
|
|
6
95
|
- Foreign Exchange resources:
|
|
7
96
|
- Rate resource (retrieve, list) for real-time exchange rate queries
|
|
8
97
|
- Quote resource (create, retrieve) for locking exchange rates with expiration helpers
|
|
9
98
|
- Conversion resource (create, retrieve, list) for executing currency conversions
|
|
10
99
|
- Balance resource (list, retrieve) for querying account balances across currencies
|
|
11
100
|
- Enhanced List operation to handle both array responses and paginated responses
|
|
12
|
-
-
|
|
101
|
+
- 63 new tests (278 total) covering batch transfers, disputes, FX, and balance operations
|
|
13
102
|
- Comprehensive manual test suite for regression testing
|
|
14
103
|
|
|
15
104
|
### Changed
|
|
@@ -19,13 +108,6 @@
|
|
|
19
108
|
### Fixed
|
|
20
109
|
- List operation now correctly handles Balance API's direct array response format
|
|
21
110
|
|
|
22
|
-
## [0.2.1] - 2025-11-25
|
|
23
|
-
|
|
24
|
-
### Added
|
|
25
|
-
- BatchTransfer resource (create, retrieve, list) for bulk payout operations
|
|
26
|
-
- Dispute resource (retrieve, list, accept, submit_evidence) for chargeback management
|
|
27
|
-
- 25 new tests (240 total) covering batch transfers and disputes
|
|
28
|
-
|
|
29
111
|
## [0.2.0] - 2025-11-25
|
|
30
112
|
|
|
31
113
|
### Added
|
data/README.md
CHANGED
|
@@ -4,12 +4,12 @@ module Airwallex
|
|
|
4
4
|
module APIOperations
|
|
5
5
|
module Delete
|
|
6
6
|
def delete(id, opts = {})
|
|
7
|
-
Airwallex.client.
|
|
8
|
-
"#{resource_path}/#{id}",
|
|
7
|
+
response = Airwallex.client.post(
|
|
8
|
+
"#{resource_path}/#{id}/delete",
|
|
9
9
|
{},
|
|
10
10
|
opts[:headers] || {}
|
|
11
11
|
)
|
|
12
|
-
true
|
|
12
|
+
response == true
|
|
13
13
|
end
|
|
14
14
|
end
|
|
15
15
|
end
|
|
@@ -9,8 +9,8 @@ module Airwallex
|
|
|
9
9
|
|
|
10
10
|
module ClassMethods
|
|
11
11
|
def update(id, params = {}, opts = {})
|
|
12
|
-
response = Airwallex.client.
|
|
13
|
-
"#{resource_path}/#{id}",
|
|
12
|
+
response = Airwallex.client.post(
|
|
13
|
+
"#{resource_path}/#{id}/update",
|
|
14
14
|
params,
|
|
15
15
|
opts[:headers] || {}
|
|
16
16
|
)
|
|
@@ -20,8 +20,8 @@ module Airwallex
|
|
|
20
20
|
|
|
21
21
|
# Instance methods
|
|
22
22
|
def update(params = {})
|
|
23
|
-
response = Airwallex.client.
|
|
24
|
-
"#{self.class.resource_path}/#{id}",
|
|
23
|
+
response = Airwallex.client.post(
|
|
24
|
+
"#{self.class.resource_path}/#{id}/update",
|
|
25
25
|
params
|
|
26
26
|
)
|
|
27
27
|
refresh_from(response)
|
data/lib/airwallex/client.rb
CHANGED
|
@@ -7,6 +7,8 @@ require "json"
|
|
|
7
7
|
|
|
8
8
|
module Airwallex
|
|
9
9
|
class Client
|
|
10
|
+
LOGIN_PATH = "/api/v1/authentication/login"
|
|
11
|
+
|
|
10
12
|
attr_reader :config, :access_token, :token_expires_at
|
|
11
13
|
|
|
12
14
|
def initialize(config = Airwallex.configuration)
|
|
@@ -19,15 +21,11 @@ module Airwallex
|
|
|
19
21
|
|
|
20
22
|
def connection
|
|
21
23
|
@connection ||= Faraday.new(url: config.api_url) do |conn|
|
|
22
|
-
conn
|
|
23
|
-
conn.request :multipart
|
|
24
|
-
conn.request :retry, retry_options
|
|
25
|
-
conn.response :json, content_type: /\bjson$/
|
|
26
|
-
conn.response :logger, config.logger, { headers: true, bodies: true } if config.logger
|
|
24
|
+
configure_middleware(conn)
|
|
27
25
|
|
|
28
26
|
conn.headers["Content-Type"] = "application/json"
|
|
29
27
|
conn.headers["User-Agent"] = user_agent
|
|
30
|
-
conn.headers["x-api-version"] = config.api_version
|
|
28
|
+
conn.headers["x-api-version"] = config.api_version if config.api_version
|
|
31
29
|
|
|
32
30
|
conn.adapter Faraday.default_adapter
|
|
33
31
|
end
|
|
@@ -55,10 +53,9 @@ module Airwallex
|
|
|
55
53
|
|
|
56
54
|
def authenticate!
|
|
57
55
|
@token_mutex.synchronize do
|
|
58
|
-
response = connection.post(
|
|
56
|
+
response = connection.post(LOGIN_PATH) do |req|
|
|
59
57
|
req.headers["x-client-id"] = config.client_id
|
|
60
58
|
req.headers["x-api-key"] = config.api_key
|
|
61
|
-
req.headers.delete("Authorization")
|
|
62
59
|
end
|
|
63
60
|
|
|
64
61
|
handle_response_errors(response)
|
|
@@ -85,12 +82,9 @@ module Airwallex
|
|
|
85
82
|
private
|
|
86
83
|
|
|
87
84
|
def request(method, path, data, headers)
|
|
88
|
-
ensure_authenticated!
|
|
89
|
-
|
|
90
85
|
response = connection.public_send(method) do |req|
|
|
91
86
|
req.url(path)
|
|
92
87
|
req.headers.merge!(headers)
|
|
93
|
-
req.headers["Authorization"] = "Bearer #{access_token}"
|
|
94
88
|
|
|
95
89
|
case method
|
|
96
90
|
when :get, :delete
|
|
@@ -104,6 +98,16 @@ module Airwallex
|
|
|
104
98
|
response.body
|
|
105
99
|
end
|
|
106
100
|
|
|
101
|
+
def configure_middleware(conn)
|
|
102
|
+
conn.use Airwallex::Middleware::Idempotency
|
|
103
|
+
conn.request :json
|
|
104
|
+
conn.request :multipart
|
|
105
|
+
conn.request :retry, retry_options
|
|
106
|
+
conn.use Airwallex::Middleware::AuthRefresh, self
|
|
107
|
+
conn.response :json, content_type: /\bjson$/
|
|
108
|
+
conn.response :logger, config.logger, { headers: true, bodies: true } if config.logger
|
|
109
|
+
end
|
|
110
|
+
|
|
107
111
|
def handle_response_errors(response)
|
|
108
112
|
return if response.success?
|
|
109
113
|
|
|
@@ -10,12 +10,16 @@ module Airwallex
|
|
|
10
10
|
SANDBOX_FILES_URL = "https://files-demo.airwallex.com"
|
|
11
11
|
PRODUCTION_FILES_URL = "https://files.airwallex.com"
|
|
12
12
|
|
|
13
|
-
DEFAULT_API_VERSION = "2024-09-27"
|
|
14
13
|
VALID_ENVIRONMENTS = %i[sandbox production].freeze
|
|
15
14
|
|
|
15
|
+
# api_version is nil by default. Airwallex stores a version on your
|
|
16
|
+
# account and applies it automatically; the x-api-version header is only
|
|
17
|
+
# for exceptional per-call overrides (testing/migrating), not a
|
|
18
|
+
# permanent pin — see https://www.airwallex.com/docs/api/versioning.
|
|
19
|
+
# Set this explicitly if you want reproducible, pinned behavior instead.
|
|
16
20
|
def initialize
|
|
17
21
|
@environment = :sandbox
|
|
18
|
-
@api_version =
|
|
22
|
+
@api_version = nil
|
|
19
23
|
@log_level = :info
|
|
20
24
|
end
|
|
21
25
|
|
|
@@ -9,24 +9,38 @@ module Airwallex
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def call(env)
|
|
12
|
-
# Skip authentication
|
|
13
|
-
return @app.call(env) if
|
|
12
|
+
# Skip authentication entirely for the login endpoint itself
|
|
13
|
+
return @app.call(env) if login_request?(env)
|
|
14
14
|
|
|
15
|
-
# Ensure token is valid before making request
|
|
16
|
-
@client.ensure_authenticated! unless
|
|
15
|
+
# Ensure token is valid before making the request, then attach it
|
|
16
|
+
@client.ensure_authenticated! unless authentication_request?(env)
|
|
17
|
+
authorize!(env)
|
|
17
18
|
|
|
18
19
|
response = @app.call(env)
|
|
19
20
|
|
|
20
21
|
# If we get a 401, try refreshing the token and retrying once
|
|
21
|
-
if response.status == 401
|
|
22
|
+
if response.status == 401
|
|
22
23
|
@client.authenticate!
|
|
23
|
-
env
|
|
24
|
-
env[:request_headers]["Authorization"] = "Bearer #{@client.access_token}"
|
|
24
|
+
authorize!(env)
|
|
25
25
|
response = @app.call(env)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
response
|
|
29
29
|
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def login_request?(env)
|
|
34
|
+
env[:url].path.include?(Client::LOGIN_PATH)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def authentication_request?(env)
|
|
38
|
+
env[:url].path.include?("/authentication/")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def authorize!(env)
|
|
42
|
+
env[:request_headers]["Authorization"] = "Bearer #{@client.access_token}"
|
|
43
|
+
end
|
|
30
44
|
end
|
|
31
45
|
end
|
|
32
46
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
# Represents an amendment to whichever account the request is
|
|
5
|
+
# authenticated as (the platform's own account, or a connected account via
|
|
6
|
+
# x-on-behalf-of). Not nested under /accounts/{id} — scoped implicitly by
|
|
7
|
+
# auth context, same as ConnectedAccount.current.
|
|
8
|
+
#
|
|
9
|
+
# .create requires Admin-level API key permissions, a separate tier from
|
|
10
|
+
# the normal per-resource Read/Write scopes.
|
|
11
|
+
#
|
|
12
|
+
# @example Submit an amendment
|
|
13
|
+
# # `target` identifies the dotted path being amended; the changed
|
|
14
|
+
# # section is a top-level sibling keyed by its own name (e.g.
|
|
15
|
+
# # store_details), not wrapped in a generic "changes" key.
|
|
16
|
+
# amendment = Airwallex::AccountAmendment.create(
|
|
17
|
+
# target: "account_details.store_details",
|
|
18
|
+
# store_details: { store_name: "New Store Name" }
|
|
19
|
+
# )
|
|
20
|
+
class AccountAmendment < APIResource
|
|
21
|
+
extend APIOperations::Create
|
|
22
|
+
extend APIOperations::Retrieve
|
|
23
|
+
|
|
24
|
+
# @return [String] API resource path for account amendments
|
|
25
|
+
def self.resource_path
|
|
26
|
+
"/api/v1/account/amendments"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -1,14 +1,110 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Airwallex
|
|
4
|
+
# @example Create a beneficiary
|
|
5
|
+
# # nickname/payer_entity_type/transfer_methods are top-level siblings of
|
|
6
|
+
# # `beneficiary`, not nested inside it. request_id is optional (the
|
|
7
|
+
# # gem's Idempotency middleware injects one automatically). Call
|
|
8
|
+
# # .api_schema / .form_schema for the exact required fields per
|
|
9
|
+
# # entity_type + bank_country_code.
|
|
10
|
+
# beneficiary = Airwallex::Beneficiary.create(
|
|
11
|
+
# nickname: "Acme Corp",
|
|
12
|
+
# payer_entity_type: "COMPANY",
|
|
13
|
+
# transfer_methods: ["LOCAL"],
|
|
14
|
+
# beneficiary: {
|
|
15
|
+
# entity_type: "COMPANY",
|
|
16
|
+
# company_name: "Acme Corp",
|
|
17
|
+
# address: {
|
|
18
|
+
# country_code: "AU",
|
|
19
|
+
# city: "Melbourne",
|
|
20
|
+
# state: "VIC",
|
|
21
|
+
# postcode: "3000",
|
|
22
|
+
# street_address: "15 William Street"
|
|
23
|
+
# },
|
|
24
|
+
# bank_details: {
|
|
25
|
+
# account_name: "Acme Corp",
|
|
26
|
+
# account_number: "12750852",
|
|
27
|
+
# account_currency: "AUD",
|
|
28
|
+
# bank_country_code: "AU",
|
|
29
|
+
# bank_name: "National Australia Bank",
|
|
30
|
+
# account_routing_type1: "bsb",
|
|
31
|
+
# account_routing_value1: "083064",
|
|
32
|
+
# local_clearing_system: "BANK_TRANSFER" # country-specific enum
|
|
33
|
+
# }
|
|
34
|
+
# }
|
|
35
|
+
# )
|
|
36
|
+
#
|
|
37
|
+
# @example Update a beneficiary
|
|
38
|
+
# # .update doesn't support a partial patch — resend the full payload
|
|
39
|
+
# # with your change merged in.
|
|
40
|
+
# beneficiary.update(
|
|
41
|
+
# transfer_methods: ["LOCAL"],
|
|
42
|
+
# beneficiary: {
|
|
43
|
+
# entity_type: "COMPANY",
|
|
44
|
+
# company_name: "Acme Corp Ltd",
|
|
45
|
+
# bank_details: { account_currency: "AUD", bank_country_code: "AU", ... }
|
|
46
|
+
# }
|
|
47
|
+
# )
|
|
4
48
|
class Beneficiary < APIResource
|
|
5
49
|
extend APIOperations::Create
|
|
6
50
|
extend APIOperations::Retrieve
|
|
7
51
|
extend APIOperations::List
|
|
8
52
|
extend APIOperations::Delete
|
|
53
|
+
include APIOperations::Update
|
|
54
|
+
|
|
55
|
+
API_SCHEMA_PATH = "/api/v1/beneficiary_api_schemas/generate"
|
|
56
|
+
FORM_SCHEMA_PATH = "/api/v1/beneficiary_form_schemas/generate"
|
|
57
|
+
SUPPORTED_FINANCIAL_INSTITUTIONS_PATH = "/api/v1/beneficiary_form_schemas/supported_financial_institutions"
|
|
9
58
|
|
|
10
59
|
def self.resource_path
|
|
11
60
|
"/api/v1/beneficiaries"
|
|
12
61
|
end
|
|
62
|
+
|
|
63
|
+
# Validate beneficiary details (address, bank details, entity type, transfer
|
|
64
|
+
# method) before attempting to create the beneficiary.
|
|
65
|
+
#
|
|
66
|
+
# @param params [Hash] the same shape of params you'd pass to .create
|
|
67
|
+
# @return [Hash] raw validation result
|
|
68
|
+
def self.validate(params = {})
|
|
69
|
+
Airwallex.client.post("#{resource_path}/validate", params)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Verify ownership of a beneficiary's bank account via Confirmation of
|
|
73
|
+
# Payee (CoP) before creation.
|
|
74
|
+
#
|
|
75
|
+
# @param params [Hash] beneficiary bank account details to verify
|
|
76
|
+
# @return [Hash] raw verification result (status, account_name_match_result)
|
|
77
|
+
def self.verify_account(params = {})
|
|
78
|
+
Airwallex.client.post("#{resource_path}/verify_account", params)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Retrieve the API schema (field validation rules) for beneficiary bank
|
|
82
|
+
# details, keyed by beneficiary type / entity type / bank country.
|
|
83
|
+
#
|
|
84
|
+
# @param params [Hash] e.g. beneficiary_type:, bank_country_code:
|
|
85
|
+
# @return [Hash] raw schema response
|
|
86
|
+
def self.api_schema(params = {})
|
|
87
|
+
Airwallex.client.post(API_SCHEMA_PATH, params)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Retrieve the dynamic form schema used to render beneficiary bank-detail
|
|
91
|
+
# forms in the UI, keyed by beneficiary type / entity type / bank country.
|
|
92
|
+
#
|
|
93
|
+
# @param params [Hash] e.g. beneficiary_type:, bank_country_code:
|
|
94
|
+
# @return [Hash] raw schema response
|
|
95
|
+
def self.form_schema(params = {})
|
|
96
|
+
Airwallex.client.post(FORM_SCHEMA_PATH, params)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Search financial institutions supported for a given country, currency,
|
|
100
|
+
# entity type, and transfer method, by name keyword.
|
|
101
|
+
#
|
|
102
|
+
# @param params [Hash] required: bank_country_code:, account_currency:,
|
|
103
|
+
# entity_type:, transfer_method:, keyword: (a bank-name search term,
|
|
104
|
+
# min 3 chars)
|
|
105
|
+
# @return [Hash] raw response listing supported institutions
|
|
106
|
+
def self.supported_financial_institutions(params = {})
|
|
107
|
+
Airwallex.client.get(SUPPORTED_FINANCIAL_INSTITUTIONS_PATH, params)
|
|
108
|
+
end
|
|
13
109
|
end
|
|
14
110
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
# Represents a Billing customer — the entity a BillingSubscription is
|
|
5
|
+
# scoped to. Distinct from Airwallex::Customer, which belongs to Payment
|
|
6
|
+
# Acceptance (/api/v1/pa/customers), not Billing.
|
|
7
|
+
#
|
|
8
|
+
# @example Create a billing customer
|
|
9
|
+
# # Flat payload, no wrapper key (unlike Beneficiary/ConnectedAccount).
|
|
10
|
+
# # type is "INDIVIDUAL" or "BUSINESS". default_legal_entity_id is not a
|
|
11
|
+
# # separate resource — it's account_details.legal_entity_id from a
|
|
12
|
+
# # ConnectedAccount's own retrieve response (see
|
|
13
|
+
# # ConnectedAccount#legal_entity_id).
|
|
14
|
+
# connected_account = Airwallex::ConnectedAccount.retrieve("acct_123")
|
|
15
|
+
# billing_customer = Airwallex::BillingCustomer.create(
|
|
16
|
+
# name: "Acme Corp",
|
|
17
|
+
# email: "billing@acme.example",
|
|
18
|
+
# type: "BUSINESS",
|
|
19
|
+
# default_billing_currency: "AUD",
|
|
20
|
+
# default_legal_entity_id: connected_account.legal_entity_id,
|
|
21
|
+
# address: {
|
|
22
|
+
# street: "200 Collins Street",
|
|
23
|
+
# city: "Melbourne",
|
|
24
|
+
# state: "VIC",
|
|
25
|
+
# postcode: "3000",
|
|
26
|
+
# country_code: "AU"
|
|
27
|
+
# }
|
|
28
|
+
# )
|
|
29
|
+
class BillingCustomer < APIResource
|
|
30
|
+
extend APIOperations::Create
|
|
31
|
+
extend APIOperations::Retrieve
|
|
32
|
+
extend APIOperations::List
|
|
33
|
+
include APIOperations::Update
|
|
34
|
+
|
|
35
|
+
# @return [String] API resource path for billing customers
|
|
36
|
+
def self.resource_path
|
|
37
|
+
"/api/v1/billing/billing_customers"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Retrieve the bank transfer instructions for funding this customer's
|
|
41
|
+
# subscriptions (e.g. for a BT-funded instalment plan).
|
|
42
|
+
#
|
|
43
|
+
# @return [Hash] raw bank transfer instructions
|
|
44
|
+
def bank_transfer_instructions
|
|
45
|
+
Airwallex.client.get("#{self.class.resource_path}/#{id}/bank_transfer_instructions")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
# Represents a Billing price — the amount/currency/cadence attached to a
|
|
5
|
+
# BillingProduct that a BillingSubscription is created against.
|
|
6
|
+
#
|
|
7
|
+
# @example Create a simple per-unit recurring price
|
|
8
|
+
# # Flat payload, no wrapper key. pricing_model is required (PER_UNIT /
|
|
9
|
+
# # FLAT / GRADUATED / VOLUME — GRADUATED uses tiers: instead of
|
|
10
|
+
# # unit_amount:). recurring: is what makes a price usable for a
|
|
11
|
+
# # subscription.
|
|
12
|
+
# price = Airwallex::BillingPrice.create(
|
|
13
|
+
# product_id: product.id,
|
|
14
|
+
# currency: "AUD",
|
|
15
|
+
# pricing_model: "PER_UNIT",
|
|
16
|
+
# unit_amount: 250.00,
|
|
17
|
+
# recurring: { period: 1, period_unit: "MONTH" }
|
|
18
|
+
# )
|
|
19
|
+
class BillingPrice < APIResource
|
|
20
|
+
extend APIOperations::Create
|
|
21
|
+
extend APIOperations::Retrieve
|
|
22
|
+
extend APIOperations::List
|
|
23
|
+
include APIOperations::Update
|
|
24
|
+
|
|
25
|
+
# @return [String] API resource path for billing prices
|
|
26
|
+
def self.resource_path
|
|
27
|
+
"/api/v1/billing/prices"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
# Represents a Billing product — the thing being sold on a subscription
|
|
5
|
+
# (distinct from Payment Acceptance's Customer/PaymentMethod resources).
|
|
6
|
+
#
|
|
7
|
+
# @example Create a product
|
|
8
|
+
# product = Airwallex::BillingProduct.create(name: "Instalment Plan")
|
|
9
|
+
#
|
|
10
|
+
# @example Update a product
|
|
11
|
+
# product.update(name: "Instalment Plan (v2)")
|
|
12
|
+
class BillingProduct < APIResource
|
|
13
|
+
extend APIOperations::Create
|
|
14
|
+
extend APIOperations::Retrieve
|
|
15
|
+
extend APIOperations::List
|
|
16
|
+
include APIOperations::Update
|
|
17
|
+
|
|
18
|
+
# @return [String] API resource path for billing products
|
|
19
|
+
def self.resource_path
|
|
20
|
+
"/api/v1/billing/products"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
# Represents a Billing subscription, e.g. a multi-part instalment plan.
|
|
5
|
+
#
|
|
6
|
+
# @example Create a subscription with a deferred start date
|
|
7
|
+
# # Flat payload, no wrapper key. Prices attach via an items: array of
|
|
8
|
+
# # { price_id:, quantity: }, not a flat price_id. legal_entity_id comes
|
|
9
|
+
# # from ConnectedAccount#legal_entity_id, not a separate resource.
|
|
10
|
+
# # payment_source_id references a PaymentSource (see
|
|
11
|
+
# # Airwallex::PaymentSource for the full consent -> source -> subscription
|
|
12
|
+
# # chain), not a PaymentConsent directly.
|
|
13
|
+
# connected_account = Airwallex::ConnectedAccount.retrieve("acct_123")
|
|
14
|
+
# subscription = Airwallex::BillingSubscription.create(
|
|
15
|
+
# billing_customer_id: billing_customer.id,
|
|
16
|
+
# currency: "AUD",
|
|
17
|
+
# collection_method: "AUTO_CHARGE",
|
|
18
|
+
# legal_entity_id: connected_account.legal_entity_id,
|
|
19
|
+
# payment_source_id: payment_source.id,
|
|
20
|
+
# starts_at: "2026-09-01T00:00:00+1000",
|
|
21
|
+
# items: [{ price_id: price.id, quantity: 1 }]
|
|
22
|
+
# )
|
|
23
|
+
# subscription.status #=> "PENDING"
|
|
24
|
+
#
|
|
25
|
+
# @example Cancel a subscription
|
|
26
|
+
# subscription.cancel
|
|
27
|
+
class BillingSubscription < APIResource
|
|
28
|
+
extend APIOperations::Create
|
|
29
|
+
extend APIOperations::Retrieve
|
|
30
|
+
extend APIOperations::List
|
|
31
|
+
include APIOperations::Update
|
|
32
|
+
|
|
33
|
+
# @return [String] API resource path for billing subscriptions
|
|
34
|
+
def self.resource_path
|
|
35
|
+
"/api/v1/billing/subscriptions"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Cancel this subscription
|
|
39
|
+
#
|
|
40
|
+
# @param params [Hash] additional cancellation params
|
|
41
|
+
# @return [BillingSubscription] self
|
|
42
|
+
def cancel(params = {})
|
|
43
|
+
response = Airwallex.client.post("#{self.class.resource_path}/#{id}/cancel", params)
|
|
44
|
+
refresh_from(response)
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# List the line items on this subscription
|
|
49
|
+
#
|
|
50
|
+
# @param params [Hash] additional query params (e.g. pagination)
|
|
51
|
+
# @return [ListObject<BillingSubscriptionItem>] list of subscription items
|
|
52
|
+
def items(params = {})
|
|
53
|
+
response = Airwallex.client.get("#{self.class.resource_path}/#{id}/items", params)
|
|
54
|
+
|
|
55
|
+
ListObject.new(
|
|
56
|
+
data: extract_item_rows(response),
|
|
57
|
+
has_more: extract_has_more(response),
|
|
58
|
+
next_cursor: extract_next_cursor(response),
|
|
59
|
+
resource_class: BillingSubscriptionItem,
|
|
60
|
+
params: params
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Retrieve a single line item on this subscription
|
|
65
|
+
#
|
|
66
|
+
# @param item_id [String] the subscription item id
|
|
67
|
+
# @return [BillingSubscriptionItem]
|
|
68
|
+
def item(item_id)
|
|
69
|
+
response = Airwallex.client.get("#{self.class.resource_path}/#{id}/items/#{item_id}")
|
|
70
|
+
BillingSubscriptionItem.new(response)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def extract_item_rows(response)
|
|
76
|
+
return response if response.is_a?(Array)
|
|
77
|
+
|
|
78
|
+
response[:items] || response["items"] || response[:data] || response["data"] || []
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def extract_has_more(response)
|
|
82
|
+
return false unless response.is_a?(Hash)
|
|
83
|
+
|
|
84
|
+
response[:has_more] || response["has_more"] || false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def extract_next_cursor(response)
|
|
88
|
+
return nil unless response.is_a?(Hash)
|
|
89
|
+
|
|
90
|
+
response[:next_cursor] || response["next_cursor"]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|