airwallex 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1288d75bff8a6f00a8c0dfffc318b6df1821cb26c945e062930de6035e83822
4
- data.tar.gz: 5452a4a8aed172764129f05043646e9f074c1c9e62df84246ffe6ff575ba0140
3
+ metadata.gz: db311617af38ee6ed53131e0342f3614f7f0acfbe5e85d1b215995d2dd58086d
4
+ data.tar.gz: 393fab63946953d355e0b20796693c6177df6d8cc30b666ee755ab1c9c57160f
5
5
  SHA512:
6
- metadata.gz: 02e3eea25661113f96af469d3154ede00aae144c671c42fcdf48d2e08d900c74ff2291b9b901ebc2b2072688b840d07d58f6b7eb21bd14aaf2c75d2e4625e6f2
7
- data.tar.gz: 31bb06a6c4a99f4d433c63b5eb9e209e31deeec63dd3d98a516d259801530e874b66b70d4680d386eba0e569589a55e51c59640af3907fbade0e1da9171b6698
6
+ metadata.gz: 381883a72671f0c450ff93504c1988b14c215c986d403536db5e3df3210b4734466b0a7c91474458500c8ff390187d15163387b104e9df50ced3f498ca2afb5e
7
+ data.tar.gz: 6d0bc5fcb4e76857711cff2b6503515f2907895bdb6eadb778787647914a5d739450486ad394ab2ce718ff1ea3687badf2c9b63a4c2b4796480ee7f66f1619cc
data/CHANGELOG.md CHANGED
@@ -1,5 +1,51 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.13.0] - 2026-10-21
4
+
5
+ ### Added
6
+
7
+ - `EmbeddedComponent.authorize(code_challenge:, scope:, identity: nil, headers: {})`
8
+ for `POST /api/v1/authentication/authorize`. It returns a symbolized Hash,
9
+ `{ authorization_code: }`, for the embedded component SDK. The code is
10
+ single use and expires in 30 seconds. Pass `headers:` with `x-on-behalf-of`
11
+ to issue it for a connected account
12
+ - `APIResource.symbolized_post` takes an optional `headers` argument
13
+
14
+ ### Changed
15
+
16
+ - `RFI.respond` and `RFI#respond` now send a normal JSON body, so the
17
+ Idempotency middleware adds a `request_id` like it does for other POSTs.
18
+ 0.12.0 left it out because the endpoint doesn't document one, but a sandbox
19
+ test confirmed Airwallex accepts it
20
+
21
+ ## [0.12.0] - 2026-10-06
22
+
23
+ ### Added
24
+
25
+ - `RFI.respond(rfi_id, params, opts)` and `RFI#respond(params, opts)` for
26
+ `POST /api/v1/rfis/{id}/respond`. Params are sent as-is
27
+ (`questions: [{ id:, answer: { type:, ... } }]`) and take `headers:` for
28
+ `x-on-behalf-of`. No `request_id` is added to this body, because the
29
+ endpoint doesn't document one
30
+ - `UploadedFile.upload(io, filename:, content_type:, notes: nil, opts: {})` for
31
+ `POST /api/v1/files/upload` on the files host (`config.files_url`). It sends
32
+ multipart/form-data with `file` and optional `notes` form fields, and
33
+ returns an `UploadedFile` with `#file_id` (`#id` mirrors it)
34
+ - `Client#files_connection` and `Client#upload`: a separate connection to the
35
+ files host with no JSON Content-Type default, no JSON encoding, no
36
+ Idempotency middleware, and no logging of request bodies
37
+
38
+ ### Fixed
39
+
40
+ - `AuthRefresh` now re-sends the original request body when it retries after
41
+ a 401. It previously re-sent the parsed 401 response body, so any POST that
42
+ hit an expired token was retried with the wrong body. Multipart stream
43
+ bodies are rewound before the retry
44
+ - With `config.logger` set, the logger no longer writes credentials: the
45
+ `x-api-key` header, the `Authorization` bearer token, and the `token` in the
46
+ login response are logged as `[FILTERED]`. They were previously logged in
47
+ plain text
48
+
3
49
  ## [0.11.0] - 2026-10-01
4
50
 
5
51
  ### Added
data/README.md CHANGED
@@ -16,7 +16,8 @@ This gem provides a Ruby interface to Airwallex's payment infrastructure, design
16
16
  - **Billing**: Products, prices, billing customers, and subscriptions for recurring/instalment billing
17
17
  - **Recurring Payments**: Payment consents and payment sources for merchant-initiated (off-session) charges
18
18
  - **Scale**: Connected accounts, funds splits, and charges for platforms onboarding sub-merchants
19
- - **Compliance**: Retrieve and list Requests for Information (RFIs), including for connected accounts
19
+ - **Embedded Components**: Authorization codes for Airwallex's embedded UI components, including for connected accounts
20
+ - **Compliance**: Retrieve, list and respond to Requests for Information (RFIs), and upload supporting documents, including for connected accounts
20
21
  - **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
22
  - **Idempotency**: Automatic request deduplication for safe retries
22
23
  - **Pagination**: Unified interface over cursor-based and offset-based pagination
@@ -477,6 +478,76 @@ Airwallex::RFI.list({ statuses: 'ACTION_REQUIRED', types: 'KYC,KYC_ONGOING' }, h
477
478
  .auto_paging_each { |r| puts r.id }
478
479
  ```
479
480
 
481
+ #### Responding to an RFI
482
+
483
+ Upload any documents first, then answer every question in `active_request`
484
+ in one call. Files go to the separate files host (`files-demo.airwallex.com`
485
+ in sandbox, `files.airwallex.com` in production), up to 20MB each, and the
486
+ returned `file_id` is what the answer references.
487
+
488
+ ```ruby
489
+ passport = Airwallex::UploadedFile.upload(
490
+ File.open('passport.jpg', 'rb'),
491
+ filename: 'passport.jpg',
492
+ content_type: 'image/jpeg',
493
+ notes: 'passport-front',
494
+ opts: { headers: headers }
495
+ )
496
+
497
+ rfi = Airwallex::RFI.retrieve('rfi_123', headers: headers)
498
+ rfi.respond(
499
+ {
500
+ questions: [
501
+ { id: 'q1', answer: { type: 'TEXT', text: 'We only operate in Australia' } },
502
+ { id: 'q2', answer: { type: 'ATTACHMENT', attachments: [{ file_id: passport.file_id }] } },
503
+ { id: 'q3', answer: { type: 'CONFIRMATION', confirmed: true } }
504
+ ]
505
+ },
506
+ headers: headers
507
+ )
508
+ rfi.status # => "ANSWERED"
509
+ ```
510
+
511
+ Answer types are `TEXT` (`text:`), `ATTACHMENT` (`attachments: [{ file_id: }]`),
512
+ `CONFIRMATION` (`confirmed:`), `ADDRESS` (`address: { address_line1:, ... }`),
513
+ `IDENTITY_DOCUMENT` (`identity_document: { type:, number:, issuing_country:,
514
+ front_file_id:, back_file_id: }`) and `LIVENESS`. A `LIVENESS` question is
515
+ completed at its `liveness[:url]` rather than answered here, so do that first.
516
+ The status then goes `ACTION_REQUIRED` → `ANSWERED` → `CLOSED`.
517
+
518
+ Errors: `BadRequestError` with code `invalid_state_for_operation` (the RFI
519
+ isn't `ACTION_REQUIRED`) or `invalid_argument` (a question is unanswered or
520
+ has the wrong answer shape), and `PermissionError` with
521
+ `account_not_authorised_for_operation`. You also get that last one when
522
+ answering RFIs for connected accounts through the API hasn't been enabled
523
+ for your platform; ask your Airwallex Account Manager to turn it on.
524
+
525
+ ### Embedded Components
526
+
527
+ Airwallex's embedded UI components (beneficiary, payout, onboarding, KYB, RFI,
528
+ SCA and tax forms) need an authorization code from your backend.
529
+ `EmbeddedComponent.authorize` mints one using a PKCE S256 challenge. Pass
530
+ `x-on-behalf-of` to issue it for a connected account; without it the code is
531
+ for the platform account. The code is single use and expires in 30 seconds,
532
+ so give it and the `code_verifier` to the browser SDK straight away.
533
+
534
+ ```ruby
535
+ code_verifier = SecureRandom.urlsafe_base64(64)
536
+ code_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)
537
+
538
+ Airwallex::EmbeddedComponent.authorize(
539
+ code_challenge: code_challenge,
540
+ scope: ['transfer:write'], # beneficiary and payout components
541
+ headers: { 'x-on-behalf-of' => 'acct_123' }
542
+ )
543
+ # => { authorization_code: "..." }
544
+ ```
545
+
546
+ The other scopes are `onboarding:write`, `kyb:write`, `rfi:read`, `rfi:write`,
547
+ `sca:read`, `sca:write`, `tax_form:read` and `tax_form:write`. `identity:` is
548
+ only used by embedded SCA. A bad `code_challenge` or `scope` raises
549
+ `BadRequestError` (`field_required`, `invalid_argument` or `validation_failed`).
550
+
480
551
  ### Sandbox Simulations
481
552
 
482
553
  Sandbox-only endpoints (`/api/v1/simulation/...`) that stand in for the bank,
@@ -938,7 +1009,8 @@ end
938
1009
  - **In-Person Payments** (sandbox Simulation only):
939
1010
  - POSTerminal (simulate_turn_on, simulate_turn_off, simulate_generate_activation_code, simulate_confirm_payment_intent, simulate_payment_scenarios)
940
1011
  - **Compliance**:
941
- - RFI (retrieve, list, plus sandbox simulate_create, simulate_close, simulate_follow_up)
1012
+ - RFI (retrieve, list, respond, plus sandbox simulate_create, simulate_close, simulate_follow_up)
1013
+ - UploadedFile (upload, to the files host)
942
1014
  - **Webhooks**: Event handling, HMAC-SHA256 signature verification
943
1015
 
944
1016
  ### Coming in Future Versions
@@ -31,9 +31,10 @@ module Airwallex
31
31
  #
32
32
  # @param path [String] the request path
33
33
  # @param params [Hash] request body
34
+ # @param headers [Hash] e.g. { "x-on-behalf-of" => account_id }
34
35
  # @return [Hash] symbolized response
35
- def self.symbolized_post(path, params = {})
36
- Util.deep_symbolize_keys(Airwallex.client.post(path, params))
36
+ def self.symbolized_post(path, params = {}, headers = {})
37
+ Util.deep_symbolize_keys(Airwallex.client.post(path, params, headers))
37
38
  end
38
39
 
39
40
  # Perform a one-off GET that returns a plain, symbolized Hash rather
@@ -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
@@ -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.response :logger, config.logger, { headers: true, bodies: true } if config.logger
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,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Airwallex
4
+ # Server-side support for Airwallex embedded components (the beneficiary,
5
+ # payout, onboarding, KYB, RFI, SCA and tax form UI components). The
6
+ # browser SDK needs an authorization code minted by your backend; this
7
+ # class mints it. See
8
+ # https://www.airwallex.com/docs/api/authentication/embedded_components/authorize
9
+ #
10
+ # Scopes: transfer:write (beneficiary and payout components),
11
+ # onboarding:write, kyb:write, rfi:read, rfi:write, sca:read, sca:write,
12
+ # tax_form:read, tax_form:write.
13
+ #
14
+ # @example Authorize the payout component for a connected account
15
+ # code_verifier = SecureRandom.urlsafe_base64(64)
16
+ # code_challenge = Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier), padding: false)
17
+ #
18
+ # Airwallex::EmbeddedComponent.authorize(
19
+ # code_challenge: code_challenge,
20
+ # scope: ["transfer:write"],
21
+ # headers: { "x-on-behalf-of" => "acct_123" }
22
+ # )
23
+ # # => { authorization_code: "..." }
24
+ class EmbeddedComponent < APIResource
25
+ AUTHORIZE_PATH = "/api/v1/authentication/authorize"
26
+
27
+ # Issue a single-use authorization code for an embedded component. The
28
+ # code expires in 30 seconds; hand it, with the code_verifier, to the
29
+ # browser SDK straight away.
30
+ #
31
+ # @param code_challenge [String] BASE64URL(SHA256(code_verifier)), the
32
+ # PKCE S256 challenge (RFC 7636 section 4)
33
+ # @param scope [Array<String>] e.g. ["transfer:write"]
34
+ # @param identity [String, nil] only used by embedded SCA
35
+ # @param headers [Hash] e.g. { "x-on-behalf-of" => account_id }; without
36
+ # it the code is issued for the platform account
37
+ # @return [Hash] { authorization_code: }
38
+ def self.authorize(code_challenge:, scope:, identity: nil, headers: {})
39
+ params = { code_challenge: code_challenge, scope: scope }
40
+ params[:identity] = identity if identity
41
+ symbolized_post(AUTHORIZE_PATH, params, headers)
42
+ end
43
+ end
44
+ 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 listing real RFIs, plus the sandbox Simulation
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,17 @@ module Airwallex
75
110
  )
76
111
  end
77
112
 
113
+ # Respond to an RFI. See the class docs for the body shape.
114
+ #
115
+ # @param rfi_id [String]
116
+ # @param params [Hash] questions: (required) array of { id:, answer: { type:, ... } }
117
+ # @param opts [Hash] headers: (e.g. { "x-on-behalf-of" => account_id })
118
+ # @return [RFI]
119
+ def self.respond(rfi_id, params = {}, opts = {})
120
+ response = Airwallex.client.post("#{resource_path}/#{rfi_id}/respond", params, opts[:headers] || {})
121
+ new(response)
122
+ end
123
+
78
124
  # Simulate Airwallex raising an RFI
79
125
  #
80
126
  # @param params [Hash] type: (required, one of "KYC", "KYC_ONGOING",
@@ -111,6 +157,16 @@ module Airwallex
111
157
  new(response)
112
158
  end
113
159
 
160
+ # Respond to this RFI. See .respond
161
+ #
162
+ # @param params [Hash] questions: (required)
163
+ # @param opts [Hash] headers: (e.g. { "x-on-behalf-of" => account_id })
164
+ # @return [RFI] self
165
+ def respond(params = {}, opts = {})
166
+ refresh_from(self.class.respond(id, params, opts).to_hash)
167
+ self
168
+ end
169
+
114
170
  # Simulate closing this RFI
115
171
  #
116
172
  # @param opts [Hash] headers: (e.g. { "x-on-behalf-of" => account_id })
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Airwallex
4
- VERSION = "0.11.0"
4
+ VERSION = "0.13.0"
5
5
  end
data/lib/airwallex.rb CHANGED
@@ -56,6 +56,8 @@ require_relative "airwallex/resources/cardholder"
56
56
  require_relative "airwallex/resources/linked_account"
57
57
  require_relative "airwallex/resources/account_offboarding"
58
58
  require_relative "airwallex/resources/rfi"
59
+ require_relative "airwallex/resources/uploaded_file"
60
+ require_relative "airwallex/resources/embedded_component"
59
61
  require_relative "airwallex/resources/pos_terminal"
60
62
 
61
63
  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.11.0
4
+ version: 0.13.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-10-01 00:00:00.000000000 Z
11
+ date: 2026-10-21 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
@@ -98,6 +100,7 @@ files:
98
100
  - lib/airwallex/resources/customer.rb
99
101
  - lib/airwallex/resources/deposit.rb
100
102
  - lib/airwallex/resources/dispute.rb
103
+ - lib/airwallex/resources/embedded_component.rb
101
104
  - lib/airwallex/resources/funds_split.rb
102
105
  - lib/airwallex/resources/funds_split_reversal.rb
103
106
  - lib/airwallex/resources/global_account.rb
@@ -116,6 +119,7 @@ files:
116
119
  - lib/airwallex/resources/refund.rb
117
120
  - lib/airwallex/resources/rfi.rb
118
121
  - lib/airwallex/resources/transfer.rb
122
+ - lib/airwallex/resources/uploaded_file.rb
119
123
  - lib/airwallex/util.rb
120
124
  - lib/airwallex/version.rb
121
125
  - lib/airwallex/webhook.rb