airwallex 0.6.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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -0
  3. data/lib/airwallex/api_operations/delete.rb +3 -3
  4. data/lib/airwallex/api_operations/update.rb +4 -4
  5. data/lib/airwallex/client.rb +1 -1
  6. data/lib/airwallex/configuration.rb +6 -2
  7. data/lib/airwallex/resources/account_amendment.rb +29 -0
  8. data/lib/airwallex/resources/beneficiary.rb +96 -0
  9. data/lib/airwallex/resources/billing_customer.rb +48 -0
  10. data/lib/airwallex/resources/billing_price.rb +30 -0
  11. data/lib/airwallex/resources/billing_product.rb +23 -0
  12. data/lib/airwallex/resources/billing_subscription.rb +93 -0
  13. data/lib/airwallex/resources/billing_subscription_item.rb +7 -0
  14. data/lib/airwallex/resources/charge.rb +19 -0
  15. data/lib/airwallex/resources/connected_account.rb +136 -0
  16. data/lib/airwallex/resources/conversion.rb +1 -1
  17. data/lib/airwallex/resources/customer.rb +5 -2
  18. data/lib/airwallex/resources/dispute.rb +48 -28
  19. data/lib/airwallex/resources/funds_split.rb +37 -0
  20. data/lib/airwallex/resources/global_account.rb +137 -0
  21. data/lib/airwallex/resources/global_account_alias.rb +62 -0
  22. data/lib/airwallex/resources/global_account_mandate.rb +26 -0
  23. data/lib/airwallex/resources/global_account_transaction.rb +7 -0
  24. data/lib/airwallex/resources/payment_consent.rb +68 -0
  25. data/lib/airwallex/resources/payment_method.rb +13 -5
  26. data/lib/airwallex/resources/payment_source.rb +52 -0
  27. data/lib/airwallex/resources/quote.rb +5 -3
  28. data/lib/airwallex/resources/rate.rb +2 -7
  29. data/lib/airwallex/version.rb +1 -1
  30. data/lib/airwallex.rb +15 -0
  31. metadata +17 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec7542a0e82153bbd60acceadb071eba2d48d328d89fa34d72d8a23dc30aeb45
4
- data.tar.gz: b14a15b3b70878c6291bc6ffd447ccf81d4cab5d86e61b806cc0708bd70590e2
3
+ metadata.gz: fad10ff0adfb96f6cb5e3a88ec1825a17b840d73658bd6fb6813f18e2746fea1
4
+ data.tar.gz: 9f59e5dfc64382838c7b1485b4ca28b19e9b71248ea51070ff106ad33cb1af32
5
5
  SHA512:
6
- metadata.gz: 1e401e23580f1347cbf2caae60b985138c59e73a22ba758578c7ffd99b2318859f766e7c0e0a13f55bff4da29a6796029a359a12a9c1db32b213c972244e70a3
7
- data.tar.gz: b7779fbdc8227ae72d20eda510799bb6898edb43c74d7aa6a6a30b7110f304be96b2e17673b02e832c2a9638c48f987b6ecf54027fc4b18f754d218fbaec4398
6
+ metadata.gz: de57501742d42048dd6a9286672c18050a17734ac2e21780f85e2d23e16e856bf406df20842352d55cd46aff22bfb0ade9676e1e6578865a7c966eba833880eb
7
+ data.tar.gz: 809fce3bea1462cecb398388b34863f65edf4ef00a5742ce0c30776c0dcbf942b474daef0551e64cd81b79ddb8a44fb01ef210fd2cec8470dbe844ce4f5968c6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
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
+
3
60
  ## [0.6.0] - 2026-08-28
4
61
 
5
62
  ### Fixed
@@ -4,12 +4,12 @@ module Airwallex
4
4
  module APIOperations
5
5
  module Delete
6
6
  def delete(id, opts = {})
7
- Airwallex.client.delete(
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.put(
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.put(
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)
@@ -25,7 +25,7 @@ module Airwallex
25
25
 
26
26
  conn.headers["Content-Type"] = "application/json"
27
27
  conn.headers["User-Agent"] = user_agent
28
- conn.headers["x-api-version"] = config.api_version
28
+ conn.headers["x-api-version"] = config.api_version if config.api_version
29
29
 
30
30
  conn.adapter Faraday.default_adapter
31
31
  end
@@ -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 = DEFAULT_API_VERSION
22
+ @api_version = nil
19
23
  @log_level = :info
20
24
  end
21
25
 
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Airwallex
4
+ # Read-only representation of a single Billing subscription line item
5
+ class BillingSubscriptionItem < APIResource
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Airwallex
4
+ # Represents a Charge (Scale product) — funds attributed to a Connected
5
+ # Account, tracked before payout.
6
+ #
7
+ # @example Retrieve a charge
8
+ # charge = Airwallex::Charge.retrieve("chg_123")
9
+ class Charge < APIResource
10
+ extend APIOperations::Create
11
+ extend APIOperations::Retrieve
12
+ extend APIOperations::List
13
+
14
+ # @return [String] API resource path for charges
15
+ def self.resource_path
16
+ "/api/v1/charges"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Airwallex
4
+ # Represents a Connected Account (Scale product) — a sub-account container
5
+ # used to onboard and track funds for a platform's connected entities.
6
+ #
7
+ # @example Create a connected account (business entity)
8
+ # # account_details is a required top-level wrapper. customer_agreements,
9
+ # # nickname, and primary_contact are top-level siblings, not nested
10
+ # # inside it. legal_entity_type is "BUSINESS" or "INDIVIDUAL", not
11
+ # # "COMPANY" (business_structure below is the field that uses "COMPANY").
12
+ # # This is a minimal subset of Airwallex's real KYB schema — the full
13
+ # # schema also supports trustee entities and much more optional detail
14
+ # # (identity documents, store details, business person details, etc).
15
+ # account = Airwallex::ConnectedAccount.create(
16
+ # nickname: "Acme Advertiser",
17
+ # primary_contact: { email: "contact@acme.example" },
18
+ # customer_agreements: {
19
+ # agreed_to_terms_and_conditions: true,
20
+ # agreed_to_data_usage: true,
21
+ # terms_and_conditions: { service_agreement_type: "FULL" }
22
+ # },
23
+ # account_details: {
24
+ # legal_entity_type: "BUSINESS",
25
+ # business_details: {
26
+ # business_name: "Acme Corp",
27
+ # business_structure: "COMPANY",
28
+ # business_address: {
29
+ # address_line1: "200 Collins Street",
30
+ # country_code: "AU",
31
+ # postcode: "3000",
32
+ # state: "VIC",
33
+ # suburb: "Melbourne"
34
+ # }
35
+ # }
36
+ # }
37
+ # )
38
+ #
39
+ # @example Trigger KYC/KYB verification
40
+ # account.submit
41
+ #
42
+ # @example Look up whichever account you're currently authenticated as
43
+ # Airwallex::ConnectedAccount.current
44
+ #
45
+ # @example Get the legal_entity_id needed by BillingCustomer/BillingSubscription
46
+ # account = Airwallex::ConnectedAccount.retrieve("acct_123")
47
+ # account.legal_entity_id
48
+ class ConnectedAccount < APIResource
49
+ extend APIOperations::Create
50
+ extend APIOperations::Retrieve
51
+ extend APIOperations::List
52
+ include APIOperations::Update
53
+
54
+ # Not nested under /accounts/{id} — these endpoints are implicitly
55
+ # scoped to whichever account the request is authenticated as (the
56
+ # platform's own account, or a connected account via x-on-behalf-of).
57
+ CURRENT_ACCOUNT_PATH = "/api/v1/account"
58
+ WALLET_INFO_PATH = "/api/v1/account/wallet_info"
59
+
60
+ # @return [String] API resource path for connected accounts
61
+ def self.resource_path
62
+ "/api/v1/accounts"
63
+ end
64
+
65
+ # Retrieve whichever account the current request is authenticated as
66
+ #
67
+ # @param params [Hash] additional params
68
+ # @return [ConnectedAccount]
69
+ def self.current(params = {})
70
+ response = Airwallex.client.get(CURRENT_ACCOUNT_PATH, params)
71
+ new(response)
72
+ end
73
+
74
+ # Retrieve wallet info for whichever account the current request is
75
+ # authenticated as
76
+ #
77
+ # @param params [Hash] additional params
78
+ # @return [Hash] raw wallet info response
79
+ def self.wallet_info(params = {})
80
+ Airwallex.client.get(WALLET_INFO_PATH, params)
81
+ end
82
+
83
+ # The legal_entity_id needed by BillingCustomer.create's
84
+ # default_legal_entity_id and BillingSubscription.create's
85
+ # legal_entity_id — nested inside account_details on this account's own
86
+ # response, not a separate resource.
87
+ #
88
+ # @return [String, nil]
89
+ def legal_entity_id
90
+ attributes.dig(:account_details, :legal_entity_id)
91
+ end
92
+
93
+ # Submit the account for KYC/KYB verification
94
+ #
95
+ # @param params [Hash] additional submission params
96
+ # @return [ConnectedAccount] self
97
+ def submit(params = {})
98
+ response = Airwallex.client.post("#{self.class.resource_path}/#{id}/submit", params)
99
+ refresh_from(response)
100
+ self
101
+ end
102
+
103
+ # Agree to Airwallex's terms and conditions on behalf of this account
104
+ #
105
+ # @param params [Hash] e.g. agreed_at:, service_agreement_type:,
106
+ # device_data: { ip_address:, user_agent: }
107
+ # @return [ConnectedAccount] self
108
+ def agree_to_terms_and_conditions(params = {})
109
+ response = Airwallex.client.post(
110
+ "#{self.class.resource_path}/#{id}/terms_and_conditions/agree", params
111
+ )
112
+ refresh_from(response)
113
+ self
114
+ end
115
+
116
+ # Suspend this account
117
+ #
118
+ # @param params [Hash] e.g. message: "reason for suspension"
119
+ # @return [ConnectedAccount] self
120
+ def suspend(params = {})
121
+ response = Airwallex.client.post("#{self.class.resource_path}/#{id}/suspend", params)
122
+ refresh_from(response)
123
+ self
124
+ end
125
+
126
+ # Reactivate a suspended account
127
+ #
128
+ # @param params [Hash] e.g. message: "reason for reactivation"
129
+ # @return [ConnectedAccount] self
130
+ def reactivate(params = {})
131
+ response = Airwallex.client.post("#{self.class.resource_path}/#{id}/reactivate", params)
132
+ refresh_from(response)
133
+ self
134
+ end
135
+ end
136
+ end
@@ -37,7 +37,7 @@ module Airwallex
37
37
  extend APIOperations::List
38
38
 
39
39
  def self.resource_path
40
- "/api/v1/conversions"
40
+ "/api/v1/fx/conversions"
41
41
  end
42
42
  end
43
43
  end