airwallex 0.2.0 → 0.3.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 +26 -0
- data/README.md +107 -5
- data/lib/airwallex/api_operations/list.rb +27 -8
- data/lib/airwallex/errors.rb +1 -0
- data/lib/airwallex/resources/balance.rb +59 -0
- data/lib/airwallex/resources/batch_transfer.rb +32 -0
- data/lib/airwallex/resources/conversion.rb +43 -0
- data/lib/airwallex/resources/customer.rb +40 -0
- data/lib/airwallex/resources/dispute.rb +68 -0
- data/lib/airwallex/resources/payment_method.rb +51 -0
- data/lib/airwallex/resources/quote.rb +51 -0
- data/lib/airwallex/resources/rate.rb +33 -0
- data/lib/airwallex/resources/refund.rb +34 -0
- data/lib/airwallex/version.rb +1 -1
- data/lib/airwallex.rb +6 -0
- metadata +14 -14
- data/docs/internal/20251125_iteration_1_quickstart.md +0 -130
- data/docs/internal/20251125_iteration_1_summary.md +0 -342
- data/docs/internal/20251125_sprint_1_completed.md +0 -448
- data/docs/internal/20251125_sprint_1_plan.md +0 -389
- data/docs/internal/20251125_sprint_2_completed.md +0 -559
- data/docs/internal/20251125_sprint_2_plan.md +0 -531
- data/docs/internal/20251125_sprint_2_unit_tests_completed.md +0 -264
- data/docs/internal/20251125_v0.1.0_publication_checklist.md +0 -162
- data/docs/research/Airwallex API Endpoint Research.md +0 -410
- data/docs/research/Airwallex API Research for Ruby Gem.md +0 -383
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aff1a8f847f36f804c6ff9738480a8acd8e9aae867a9a3846311521337544c15
|
|
4
|
+
data.tar.gz: af199726a6f9cdcf4eef3d47c1ac1c24fd3ce92f9dd564c57730e75a8e4230ce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 41ddfe80904d71772a0ed87a6f6f26ff3f857b8502bfde57c6e8b42bd53a162d6235ac6ae99ca52a11f2e4688dbe0682a96806ec2e97f1e2e3f617c038766c45
|
|
7
|
+
data.tar.gz: 907a5e84b3a38f3b4664ccf41d5ac4d62e04804073034845dfd9503b880113f2bc32d8c365ca9cca96dd6870ac4ebc1283ca93da93fb59128789b001b556ef47
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2025-11-25
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Foreign Exchange resources:
|
|
7
|
+
- Rate resource (retrieve, list) for real-time exchange rate queries
|
|
8
|
+
- Quote resource (create, retrieve) for locking exchange rates with expiration helpers
|
|
9
|
+
- Conversion resource (create, retrieve, list) for executing currency conversions
|
|
10
|
+
- Balance resource (list, retrieve) for querying account balances across currencies
|
|
11
|
+
- Enhanced List operation to handle both array responses and paginated responses
|
|
12
|
+
- 38 new tests (278 total) covering FX and balance operations
|
|
13
|
+
- Comprehensive manual test suite for regression testing
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Refactored List operation for better code quality (reduced complexity from 12 to 7)
|
|
17
|
+
- Balance.retrieve now performs client-side filtering for currency lookup
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- List operation now correctly handles Balance API's direct array response format
|
|
21
|
+
|
|
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
|
+
|
|
3
29
|
## [0.2.0] - 2025-11-25
|
|
4
30
|
|
|
5
31
|
### 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
|
|
@@ -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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
15
|
-
has_more: response
|
|
16
|
-
next_cursor: response
|
|
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
|
data/lib/airwallex/errors.rb
CHANGED
|
@@ -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
|
|
@@ -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
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
# Quote resource for locked exchange rates
|
|
5
|
+
#
|
|
6
|
+
# Create quotes to lock exchange rates for a short period (typically 30-60 seconds).
|
|
7
|
+
# Use quotes to guarantee the rate when executing conversions.
|
|
8
|
+
#
|
|
9
|
+
# @example Create a quote
|
|
10
|
+
# quote = Airwallex::Quote.create(
|
|
11
|
+
# buy_currency: 'EUR',
|
|
12
|
+
# sell_currency: 'USD',
|
|
13
|
+
# sell_amount: 1000.00
|
|
14
|
+
# )
|
|
15
|
+
# puts "Locked rate: #{quote.client_rate}, expires: #{quote.expires_at}"
|
|
16
|
+
#
|
|
17
|
+
# @example Use quote for conversion
|
|
18
|
+
# conversion = Airwallex::Conversion.create(quote_id: quote.id)
|
|
19
|
+
#
|
|
20
|
+
class Quote < APIResource
|
|
21
|
+
extend APIOperations::Create
|
|
22
|
+
extend APIOperations::Retrieve
|
|
23
|
+
|
|
24
|
+
def self.resource_path
|
|
25
|
+
"/api/v1/fx/quotes"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Check if quote has expired
|
|
29
|
+
#
|
|
30
|
+
# @return [Boolean] true if quote is expired
|
|
31
|
+
def expired?
|
|
32
|
+
return false unless respond_to?(:expires_at) && expires_at
|
|
33
|
+
|
|
34
|
+
Time.parse(expires_at) < Time.now
|
|
35
|
+
rescue ArgumentError
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get seconds until expiration
|
|
40
|
+
#
|
|
41
|
+
# @return [Integer, nil] seconds remaining, 0 if expired, nil if no expiration
|
|
42
|
+
def seconds_until_expiration
|
|
43
|
+
return nil unless respond_to?(:expires_at) && expires_at
|
|
44
|
+
|
|
45
|
+
remaining = Time.parse(expires_at) - Time.now
|
|
46
|
+
[remaining.to_i, 0].max
|
|
47
|
+
rescue ArgumentError
|
|
48
|
+
0
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
# Rate resource for real-time exchange rates
|
|
5
|
+
#
|
|
6
|
+
# Get indicative exchange rates for currency pairs.
|
|
7
|
+
# Rates are real-time but not locked - use Quote for guaranteed rates.
|
|
8
|
+
#
|
|
9
|
+
# @example Get current rate
|
|
10
|
+
# rate = Airwallex::Rate.retrieve(buy_currency: 'EUR', sell_currency: 'USD')
|
|
11
|
+
# puts "1 USD = #{rate.client_rate} EUR"
|
|
12
|
+
#
|
|
13
|
+
# @example Get multiple rates (Note: API may not support multiple at once)
|
|
14
|
+
# rate = Airwallex::Rate.retrieve(
|
|
15
|
+
# buy_currency: 'EUR',
|
|
16
|
+
# sell_currency: 'USD'
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
class Rate < APIResource
|
|
20
|
+
extend APIOperations::Retrieve
|
|
21
|
+
extend APIOperations::List
|
|
22
|
+
|
|
23
|
+
def self.resource_path
|
|
24
|
+
"/api/v1/fx/rates/current"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Override retrieve to handle query parameters instead of ID
|
|
28
|
+
def self.retrieve(params = {})
|
|
29
|
+
response = Airwallex.client.get(resource_path, params)
|
|
30
|
+
new(response)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
# Represents a refund of a payment intent
|
|
5
|
+
#
|
|
6
|
+
# Refunds can be full or partial. Multiple refunds can be created for a single
|
|
7
|
+
# payment intent as long as the total refunded amount doesn't exceed the original amount.
|
|
8
|
+
#
|
|
9
|
+
# @example Create a full refund
|
|
10
|
+
# refund = Airwallex::Refund.create(
|
|
11
|
+
# payment_intent_id: "pi_123",
|
|
12
|
+
# amount: 100.00,
|
|
13
|
+
# reason: "requested_by_customer"
|
|
14
|
+
# )
|
|
15
|
+
#
|
|
16
|
+
# @example Create a partial refund
|
|
17
|
+
# refund = Airwallex::Refund.create(
|
|
18
|
+
# payment_intent_id: "pi_123",
|
|
19
|
+
# amount: 25.00
|
|
20
|
+
# )
|
|
21
|
+
#
|
|
22
|
+
# @example List refunds for a payment
|
|
23
|
+
# refunds = Airwallex::Refund.list(payment_intent_id: "pi_123")
|
|
24
|
+
class Refund < APIResource
|
|
25
|
+
extend APIOperations::Create
|
|
26
|
+
extend APIOperations::Retrieve
|
|
27
|
+
extend APIOperations::List
|
|
28
|
+
|
|
29
|
+
# @return [String] API resource path for refunds
|
|
30
|
+
def self.resource_path
|
|
31
|
+
"/api/v1/pa/refunds"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/airwallex/version.rb
CHANGED
data/lib/airwallex.rb
CHANGED
|
@@ -27,6 +27,12 @@ require_relative "airwallex/resources/beneficiary"
|
|
|
27
27
|
require_relative "airwallex/resources/refund"
|
|
28
28
|
require_relative "airwallex/resources/payment_method"
|
|
29
29
|
require_relative "airwallex/resources/customer"
|
|
30
|
+
require_relative "airwallex/resources/batch_transfer"
|
|
31
|
+
require_relative "airwallex/resources/dispute"
|
|
32
|
+
require_relative "airwallex/resources/rate"
|
|
33
|
+
require_relative "airwallex/resources/quote"
|
|
34
|
+
require_relative "airwallex/resources/conversion"
|
|
35
|
+
require_relative "airwallex/resources/balance"
|
|
30
36
|
|
|
31
37
|
module Airwallex
|
|
32
38
|
class << self
|