airwallex 0.2.0 → 0.6.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: 24329b3008cb6844904a266d7cfed62b05bb0743935edd22a7ea1e3c1a98606e
4
- data.tar.gz: 73d9c743bfa6da7e652da8663b98965985aefcbe575d373f322a7023232278f3
3
+ metadata.gz: ec7542a0e82153bbd60acceadb071eba2d48d328d89fa34d72d8a23dc30aeb45
4
+ data.tar.gz: b14a15b3b70878c6291bc6ffd447ccf81d4cab5d86e61b806cc0708bd70590e2
5
5
  SHA512:
6
- metadata.gz: caae41e87cb5071ed6d8fdba00d873c6d90a6eabf20406a33e6261f960d8b65cf4355a71df4cac01c60ea5cb74705e9491843f74c1458559c8e24c1d5bebb044
7
- data.tar.gz: 0b0924430871eb4065f78b75e2e1a6cad937089ebfec9801dbebcb8d5b7aa6e3ae49bbd882f8adb8406d0c3a17fd7a81956495dd9e194ea9349c41109b1faf2e
6
+ metadata.gz: 1e401e23580f1347cbf2caae60b985138c59e73a22ba758578c7ffd99b2318859f766e7c0e0a13f55bff4da29a6796029a359a12a9c1db32b213c972244e70a3
7
+ data.tar.gz: b7779fbdc8227ae72d20eda510799bb6898edb43c74d7aa6a6a30b7110f304be96b2e17673b02e832c2a9638c48f987b6ecf54027fc4b18f754d218fbaec4398
data/CHANGELOG.md CHANGED
@@ -1,5 +1,56 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2026-08-28
4
+
5
+ ### Fixed
6
+ - `Webhook::Event` now correctly exposes the event type. Airwallex sends this field as `"name"` in
7
+ the webhook payload, not `"type"` — the previous `Event#type` reader was reading a key that doesn't
8
+ exist and always returned `nil`. The accessor is renamed to `Event#name` to match Airwallex's actual
9
+ field. **Breaking change:** `event.type` → `event.name`.
10
+
11
+ ## [0.5.0] - 2026-08-28
12
+
13
+ ### Fixed
14
+ - Webhook signature verification no longer rejects valid webhooks when Airwallex sends
15
+ millisecond-precision timestamps. `verify_timestamp` now detects millisecond values (anything at or
16
+ above `MS_THRESHOLD = 10_000_000_000`, which safely distinguishes seconds from milliseconds until the
17
+ year 2286) and normalizes them to seconds before comparing against the tolerance window.
18
+
19
+ ## [0.4.0] - 2026-08-27
20
+
21
+ ### Fixed
22
+ - `Idempotency` and `AuthRefresh` middleware are now actually registered on the Faraday connection.
23
+ Previously both classes existed but were never wired in, so the "automatic `request_id` generation"
24
+ and "retry once after a 401" behavior documented in the README did not happen at runtime.
25
+ - Fixed a bug in `AuthRefresh` where the 401-retry guard used `env[:request][:auth_retry]`, a key that
26
+ doesn't exist on `Faraday::RequestOptions` and would have raised `NoMethodError` the first time it ran.
27
+ - CI now triggers on pushes to `main` (previously configured for `master`, so pushes never ran CI).
28
+
29
+ ### Changed
30
+ - `Client#request` no longer manually manages the `Authorization` header or calls
31
+ `ensure_authenticated!` directly; this is now owned by the `AuthRefresh` middleware.
32
+
33
+ ## [0.3.0] - 2025-11-25
34
+
35
+ ### Added
36
+ - BatchTransfer resource (create, retrieve, list) for bulk payout operations
37
+ - Dispute resource (retrieve, list, accept, submit_evidence) for chargeback management
38
+ - Foreign Exchange resources:
39
+ - Rate resource (retrieve, list) for real-time exchange rate queries
40
+ - Quote resource (create, retrieve) for locking exchange rates with expiration helpers
41
+ - Conversion resource (create, retrieve, list) for executing currency conversions
42
+ - Balance resource (list, retrieve) for querying account balances across currencies
43
+ - Enhanced List operation to handle both array responses and paginated responses
44
+ - 63 new tests (278 total) covering batch transfers, disputes, FX, and balance operations
45
+ - Comprehensive manual test suite for regression testing
46
+
47
+ ### Changed
48
+ - Refactored List operation for better code quality (reduced complexity from 12 to 7)
49
+ - Balance.retrieve now performs client-side filtering for currency lookup
50
+
51
+ ### Fixed
52
+ - List operation now correctly handles Balance API's direct array response format
53
+
3
54
  ## [0.2.0] - 2025-11-25
4
55
 
5
56
  ### Added
data/README.md CHANGED
@@ -154,6 +154,102 @@ payment_intent.confirm(payment_method_id: payment_method.id)
154
154
  methods = customer.payment_methods
155
155
  ```
156
156
 
157
+ ### Batch Transfers
158
+
159
+ ```ruby
160
+ # Create a batch of transfers for bulk payouts
161
+ batch = Airwallex::BatchTransfer.create(
162
+ request_id: "batch_#{Time.now.to_i}",
163
+ source_currency: 'USD',
164
+ transfers: [
165
+ { beneficiary_id: 'ben_001', amount: 100.00, reason: 'Seller payout' },
166
+ { beneficiary_id: 'ben_002', amount: 250.00, reason: 'Affiliate payment' },
167
+ { beneficiary_id: 'ben_003', amount: 500.00, reason: 'Vendor payment' }
168
+ ]
169
+ )
170
+
171
+ # Check batch status
172
+ batch = Airwallex::BatchTransfer.retrieve(batch.id)
173
+ puts "Completed: #{batch.success_count}/#{batch.total_count}"
174
+
175
+ # Check individual transfer statuses
176
+ batch.transfers.each do |transfer|
177
+ puts "#{transfer.id}: #{transfer.status}"
178
+ end
179
+ ```
180
+
181
+ ### Managing Disputes
182
+
183
+ ```ruby
184
+ # List all open disputes
185
+ disputes = Airwallex::Dispute.list(status: 'OPEN')
186
+
187
+ # Get specific dispute
188
+ dispute = Airwallex::Dispute.retrieve('dis_123')
189
+ puts "Dispute amount: #{dispute.amount} #{dispute.currency}"
190
+ puts "Reason: #{dispute.reason}"
191
+ puts "Evidence due: #{dispute.evidence_due_by}"
192
+
193
+ # Submit evidence to challenge
194
+ dispute.submit_evidence(
195
+ customer_communication: 'Email showing delivery confirmation',
196
+ shipping_tracking_number: '1Z999AA10123456784',
197
+ shipping_documentation: 'Proof of delivery with signature'
198
+ )
199
+
200
+ # Or accept dispute without challenging
201
+ dispute.accept
202
+ ```
203
+
204
+ ### Foreign Exchange & Multi-Currency
205
+
206
+ ```ruby
207
+ # Get real-time exchange rate
208
+ rate = Airwallex::Rate.retrieve(
209
+ buy_currency: 'EUR',
210
+ sell_currency: 'USD'
211
+ )
212
+ puts "Current rate: #{rate.client_rate}"
213
+
214
+ # Lock in a rate with a quote (valid for 24 hours)
215
+ quote = Airwallex::Quote.create(
216
+ buy_currency: 'EUR',
217
+ sell_currency: 'USD',
218
+ sell_amount: 10000.00,
219
+ validity: 'HR_24'
220
+ )
221
+
222
+ puts "Locked rate: #{quote.client_rate}"
223
+ puts "Expires in: #{quote.seconds_until_expiration} seconds"
224
+ puts "Is expired? #{quote.expired?}"
225
+
226
+ # Execute conversion using locked quote
227
+ conversion = Airwallex::Conversion.create(
228
+ quote_id: quote.id,
229
+ reason: 'Multi-currency settlement'
230
+ )
231
+
232
+ # Or convert at current market rate
233
+ conversion = Airwallex::Conversion.create(
234
+ buy_currency: 'EUR',
235
+ sell_currency: 'USD',
236
+ sell_amount: 5000.00,
237
+ reason: 'Currency exchange'
238
+ )
239
+
240
+ # Check account balances
241
+ balances = Airwallex::Balance.list
242
+ balances.each do |balance|
243
+ next if balance.available_amount <= 0
244
+ puts "#{balance.currency}: #{balance.available_amount} available"
245
+ end
246
+
247
+ # Get specific currency balance
248
+ usd_balance = Airwallex::Balance.retrieve('USD')
249
+ puts "USD Available: #{usd_balance.available_amount}"
250
+ puts "USD Total: #{usd_balance.total_amount}"
251
+ ```
252
+
157
253
  ## Usage
158
254
 
159
255
  ### Authentication
@@ -223,8 +319,8 @@ begin
223
319
  timestamp,
224
320
  tolerance: 300 # 5 minutes
225
321
  )
226
-
227
- case event.type
322
+
323
+ case event.name
228
324
  when 'payment_intent.succeeded'
229
325
  handle_successful_payment(event.data)
230
326
  when 'payout.transfer.failed'
@@ -319,23 +415,29 @@ end
319
415
 
320
416
  ### Currently Implemented Resources
321
417
 
322
- - **Payment Acceptance**:
418
+ - **Payment Acceptance**:
323
419
  - PaymentIntent (create, retrieve, list, update, confirm, cancel, capture)
324
420
  - Refund (create, retrieve, list)
325
421
  - PaymentMethod (create, retrieve, list, update, delete, detach)
326
422
  - Customer (create, retrieve, list, update, delete)
327
- - **Payouts**:
423
+ - Dispute (retrieve, list, accept, submit_evidence)
424
+ - **Payouts**:
328
425
  - Transfer (create, retrieve, list, cancel)
329
426
  - Beneficiary (create, retrieve, list, delete)
427
+ - BatchTransfer (create, retrieve, list)
428
+ - **Foreign Exchange & Multi-Currency**:
429
+ - Rate (retrieve, list) - Real-time exchange rate queries
430
+ - Quote (create, retrieve) - Lock exchange rates with expiration tracking
431
+ - Conversion (create, retrieve, list) - Execute currency conversions
432
+ - Balance (list, retrieve) - Query account balances across currencies
330
433
  - **Webhooks**: Event handling, HMAC-SHA256 signature verification
331
434
 
332
435
  ### Coming in Future Versions
333
436
 
334
- - Disputes and chargebacks
335
- - Foreign exchange (rates, quotes, conversions)
336
437
  - Global accounts
337
438
  - Card issuing
338
- - Batch transfers
439
+ - Subscriptions and billing
440
+ - Virtual account numbers
339
441
 
340
442
  ## Environment Support
341
443
 
@@ -4,20 +4,39 @@ module Airwallex
4
4
  module APIOperations
5
5
  module List
6
6
  def list(params = {}, opts = {})
7
- response = Airwallex.client.get(
8
- resource_path,
9
- params,
10
- opts[:headers] || {}
11
- )
7
+ response = Airwallex.client.get(resource_path, params, opts[:headers] || {})
8
+ build_list_object(response, params)
9
+ end
10
+
11
+ private
12
12
 
13
+ def build_list_object(response, params)
13
14
  ListObject.new(
14
- data: response[:items] || response["items"] || [],
15
- has_more: response[:has_more] || response["has_more"] || false,
16
- next_cursor: response[:next_cursor] || response["next_cursor"],
15
+ data: extract_data(response),
16
+ has_more: extract_has_more(response),
17
+ next_cursor: extract_next_cursor(response),
17
18
  resource_class: self,
18
19
  params: params
19
20
  )
20
21
  end
22
+
23
+ def extract_data(response)
24
+ return response if response.is_a?(Array)
25
+
26
+ response[:items] || response["items"] || response[:data] || response["data"] || []
27
+ end
28
+
29
+ def extract_has_more(response)
30
+ return false unless response.is_a?(Hash)
31
+
32
+ response[:has_more] || response["has_more"] || false
33
+ end
34
+
35
+ def extract_next_cursor(response)
36
+ return nil unless response.is_a?(Hash)
37
+
38
+ response[:next_cursor] || response["next_cursor"]
39
+ end
21
40
  end
22
41
  end
23
42
  end
@@ -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,11 +21,7 @@ module Airwallex
19
21
 
20
22
  def connection
21
23
  @connection ||= Faraday.new(url: config.api_url) do |conn|
22
- conn.request :json
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
@@ -55,10 +53,9 @@ module Airwallex
55
53
 
56
54
  def authenticate!
57
55
  @token_mutex.synchronize do
58
- response = connection.post("/api/v1/authentication/login") do |req|
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
 
@@ -59,6 +59,7 @@ module Airwallex
59
59
  class RateLimitError < Error; end
60
60
  class APIError < Error; end
61
61
  class InsufficientFundsError < Error; end
62
+ class QuoteExpiredError < BadRequestError; end
62
63
  class SCARequiredError < PermissionError; end
63
64
  class SignatureVerificationError < Error; end
64
65
  end
@@ -9,24 +9,38 @@ module Airwallex
9
9
  end
10
10
 
11
11
  def call(env)
12
- # Skip authentication refresh for login endpoint
13
- return @app.call(env) if env[:url].path.include?("/authentication/login")
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 env[:url].path.include?("/authentication/")
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 && !env[:request].fetch(:auth_retry, false)
22
+ if response.status == 401
22
23
  @client.authenticate!
23
- env[:request][:auth_retry] = true
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,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Airwallex
4
+ # Balance resource for account balance queries
5
+ #
6
+ # Query account balances across all currencies or for specific currencies.
7
+ # Shows available, pending, and reserved amounts.
8
+ #
9
+ # @example Get all balances
10
+ # balances = Airwallex::Balance.list
11
+ # balances.each do |balance|
12
+ # puts "#{balance.currency}: #{balance.available_amount}"
13
+ # end
14
+ #
15
+ # @example Get specific currency balance
16
+ # usd_balance = Airwallex::Balance.retrieve('USD')
17
+ # puts "Available: #{usd_balance.available_amount}"
18
+ # puts "Pending: #{usd_balance.pending_amount}"
19
+ # puts "Reserved: #{usd_balance.reserved_amount}"
20
+ #
21
+ class Balance < APIResource
22
+ extend APIOperations::List
23
+
24
+ def self.resource_path
25
+ "/api/v1/balances/current"
26
+ end
27
+
28
+ # Retrieve balance for a specific currency
29
+ #
30
+ # @param currency [String] Currency code (e.g., 'USD', 'EUR')
31
+ # @return [Airwallex::Balance] Balance object for the currency
32
+ def self.retrieve(currency)
33
+ response = Airwallex.client.get(resource_path, currency: currency)
34
+ # Balance API returns array directly at top level
35
+ balances_array = response.is_a?(Array) ? response : response[:data] || response["data"] || []
36
+ balances = ListObject.new(
37
+ data: balances_array,
38
+ has_more: false,
39
+ resource_class: self
40
+ )
41
+
42
+ # Filter to find the requested currency
43
+ balance = balances.find { |b| b.currency&.upcase == currency.upcase }
44
+ raise NotFoundError, "Balance not found for currency: #{currency}" unless balance
45
+
46
+ balance
47
+ end
48
+
49
+ # Calculate total balance
50
+ #
51
+ # @return [Float] Sum of available, pending, and reserved amounts
52
+ def total_amount
53
+ available = respond_to?(:available_amount) ? available_amount.to_f : 0.0
54
+ pending = respond_to?(:pending_amount) ? pending_amount.to_f : 0.0
55
+ reserved = respond_to?(:reserved_amount) ? reserved_amount.to_f : 0.0
56
+ (available + pending + reserved).round(2)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Airwallex
4
+ # BatchTransfer resource for bulk payout operations
5
+ #
6
+ # Batch transfers allow creating multiple transfers in a single API call,
7
+ # improving efficiency for bulk payout scenarios like marketplace payouts or payroll.
8
+ #
9
+ # @example Create a batch transfer
10
+ # batch = Airwallex::BatchTransfer.create(
11
+ # request_id: "batch_#{Time.now.to_i}",
12
+ # source_currency: "USD",
13
+ # transfers: [
14
+ # { beneficiary_id: "ben_001", amount: 100.00, reason: "Payout 1" },
15
+ # { beneficiary_id: "ben_002", amount: 200.00, reason: "Payout 2" }
16
+ # ]
17
+ # )
18
+ #
19
+ # @example Retrieve a batch transfer
20
+ # batch = Airwallex::BatchTransfer.retrieve("batch_123")
21
+ # batch.transfers.each { |t| puts "#{t.id}: #{t.status}" }
22
+ #
23
+ class BatchTransfer < APIResource
24
+ extend APIOperations::Create
25
+ extend APIOperations::Retrieve
26
+ extend APIOperations::List
27
+
28
+ def self.resource_path
29
+ "/api/v1/batch_transfers"
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Airwallex
4
+ # Conversion resource for currency exchange
5
+ #
6
+ # Execute currency conversions with locked quotes or at market rates.
7
+ # Conversions move funds between currency balances in your account.
8
+ #
9
+ # @example Convert with locked quote
10
+ # quote = Airwallex::Quote.create(
11
+ # buy_currency: 'EUR',
12
+ # sell_currency: 'USD',
13
+ # sell_amount: 1000.00
14
+ # )
15
+ # conversion = Airwallex::Conversion.create(
16
+ # quote_id: quote.id,
17
+ # request_id: "conv_#{Time.now.to_i}"
18
+ # )
19
+ #
20
+ # @example Convert at market rate
21
+ # conversion = Airwallex::Conversion.create(
22
+ # buy_currency: 'EUR',
23
+ # sell_currency: 'USD',
24
+ # sell_amount: 500.00,
25
+ # request_id: "conv_#{Time.now.to_i}"
26
+ # )
27
+ #
28
+ # @example List conversion history
29
+ # conversions = Airwallex::Conversion.list(
30
+ # sell_currency: 'USD',
31
+ # status: 'COMPLETED'
32
+ # )
33
+ #
34
+ class Conversion < APIResource
35
+ extend APIOperations::Create
36
+ extend APIOperations::Retrieve
37
+ extend APIOperations::List
38
+
39
+ def self.resource_path
40
+ "/api/v1/conversions"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Airwallex
4
+ # Represents a customer for organizing payment methods and transactions
5
+ #
6
+ # Customers allow you to group payment methods and track payment history
7
+ # for individual users or accounts.
8
+ #
9
+ # @example Create a customer
10
+ # customer = Airwallex::Customer.create(
11
+ # email: "john@example.com",
12
+ # first_name: "John",
13
+ # last_name: "Doe",
14
+ # metadata: { internal_id: "user_789" }
15
+ # )
16
+ #
17
+ # @example List payment methods for a customer
18
+ # methods = Airwallex::PaymentMethod.list(customer_id: customer.id)
19
+ class Customer < APIResource
20
+ extend APIOperations::Create
21
+ extend APIOperations::Retrieve
22
+ extend APIOperations::List
23
+ extend APIOperations::Update
24
+ include APIOperations::Update
25
+ extend APIOperations::Delete
26
+
27
+ # @return [String] API resource path for customers
28
+ def self.resource_path
29
+ "/api/v1/pa/customers"
30
+ end
31
+
32
+ # List payment methods for this customer
33
+ #
34
+ # @param params [Hash] additional parameters
35
+ # @return [ListObject<PaymentMethod>] list of payment methods
36
+ def payment_methods(params = {})
37
+ PaymentMethod.list(params.merge(customer_id: id))
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Airwallex
4
+ # Dispute resource for handling chargebacks and payment disputes
5
+ #
6
+ # Disputes represent chargebacks or payment disputes initiated by cardholders.
7
+ # Merchants can view disputes, submit evidence to challenge them, or accept them.
8
+ #
9
+ # @example List open disputes
10
+ # disputes = Airwallex::Dispute.list(status: 'OPEN')
11
+ #
12
+ # @example Retrieve a dispute
13
+ # dispute = Airwallex::Dispute.retrieve('dis_123')
14
+ #
15
+ # @example Submit evidence
16
+ # dispute = Airwallex::Dispute.retrieve('dis_123')
17
+ # dispute.submit_evidence(
18
+ # customer_communication: "Email showing delivery confirmation",
19
+ # shipping_tracking_number: "1Z999AA10123456784"
20
+ # )
21
+ #
22
+ # @example Accept a dispute
23
+ # dispute = Airwallex::Dispute.retrieve('dis_123')
24
+ # dispute.accept
25
+ #
26
+ class Dispute < APIResource
27
+ extend APIOperations::Retrieve
28
+ extend APIOperations::List
29
+
30
+ def self.resource_path
31
+ "/api/v1/disputes"
32
+ end
33
+
34
+ # Accept a dispute without challenging it
35
+ #
36
+ # @return [Airwallex::Dispute] The updated dispute object
37
+ def accept
38
+ response = Airwallex.client.post("#{resource_path}/#{id}/accept", {})
39
+ refresh_from(response)
40
+ self
41
+ end
42
+
43
+ # Submit evidence to challenge a dispute
44
+ #
45
+ # @param evidence [Hash] Evidence details
46
+ # @option evidence [String] :customer_communication Email or chat logs
47
+ # @option evidence [String] :shipping_tracking_number Tracking number
48
+ # @option evidence [String] :shipping_documentation Proof of shipping
49
+ # @option evidence [String] :customer_signature Signed receipt
50
+ # @option evidence [String] :receipt Proof of purchase
51
+ # @option evidence [String] :refund_policy Refund policy document
52
+ # @option evidence [String] :cancellation_policy Cancellation policy
53
+ # @option evidence [String] :additional_information Other relevant info
54
+ #
55
+ # @return [Airwallex::Dispute] The updated dispute object
56
+ def submit_evidence(evidence)
57
+ response = Airwallex.client.post("#{resource_path}/#{id}/evidence", evidence)
58
+ refresh_from(response)
59
+ self
60
+ end
61
+
62
+ private
63
+
64
+ def resource_path
65
+ self.class.resource_path
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Airwallex
4
+ # Represents a payment method (card, bank account, etc.) that can be reused
5
+ #
6
+ # Payment methods allow you to store customer payment credentials securely
7
+ # and reuse them for future payments without collecting details again.
8
+ #
9
+ # @example Create a card payment method
10
+ # pm = Airwallex::PaymentMethod.create(
11
+ # type: "card",
12
+ # card: {
13
+ # number: "4242424242424242",
14
+ # expiry_month: "12",
15
+ # expiry_year: "2025",
16
+ # cvc: "123"
17
+ # },
18
+ # billing: {
19
+ # first_name: "John",
20
+ # email: "john@example.com"
21
+ # }
22
+ # )
23
+ #
24
+ # @example Use saved payment method
25
+ # payment_intent.confirm(payment_method_id: pm.id)
26
+ #
27
+ # @example Update billing details
28
+ # pm.update(billing: { address: { postal_code: "10001" } })
29
+ class PaymentMethod < APIResource
30
+ extend APIOperations::Create
31
+ extend APIOperations::Retrieve
32
+ extend APIOperations::List
33
+ extend APIOperations::Update
34
+ include APIOperations::Update
35
+ extend APIOperations::Delete
36
+
37
+ # @return [String] API resource path for payment methods
38
+ def self.resource_path
39
+ "/api/v1/pa/payment_methods"
40
+ end
41
+
42
+ # Detach this payment method from its customer
43
+ #
44
+ # @return [PaymentMethod] self
45
+ def detach
46
+ response = Airwallex.client.post("#{self.class.resource_path}/#{id}/detach", {})
47
+ refresh_from(response)
48
+ self
49
+ end
50
+ end
51
+ end