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 +4 -4
- data/CHANGELOG.md +51 -0
- data/README.md +109 -7
- data/lib/airwallex/api_operations/list.rb +27 -8
- data/lib/airwallex/client.rb +14 -10
- data/lib/airwallex/errors.rb +1 -0
- data/lib/airwallex/middleware/auth_refresh.rb +21 -7
- 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/webhook.rb +6 -2
- data/lib/airwallex.rb +6 -0
- metadata +16 -16
- 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
|
@@ -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/webhook.rb
CHANGED
|
@@ -5,6 +5,8 @@ require "openssl"
|
|
|
5
5
|
module Airwallex
|
|
6
6
|
module Webhook
|
|
7
7
|
DEFAULT_TOLERANCE = 300 # 5 minutes
|
|
8
|
+
# Timestamps below this are seconds, at/above are milliseconds. Valid until year 2286.
|
|
9
|
+
MS_THRESHOLD = 10_000_000_000
|
|
8
10
|
|
|
9
11
|
module_function
|
|
10
12
|
|
|
@@ -37,6 +39,8 @@ module Airwallex
|
|
|
37
39
|
def verify_timestamp(timestamp, tolerance)
|
|
38
40
|
current_time = Time.now.to_i
|
|
39
41
|
timestamp_int = timestamp.to_i
|
|
42
|
+
# Airwallex sends x-timestamp in milliseconds; normalize to seconds before comparing.
|
|
43
|
+
timestamp_int /= 1000 if timestamp_int > MS_THRESHOLD
|
|
40
44
|
|
|
41
45
|
if (current_time - timestamp_int).abs > tolerance
|
|
42
46
|
raise SignatureVerificationError, "Timestamp outside tolerance (#{tolerance}s)"
|
|
@@ -54,11 +58,11 @@ module Airwallex
|
|
|
54
58
|
private_class_method :compute_signature, :verify_timestamp, :secure_compare
|
|
55
59
|
|
|
56
60
|
class Event
|
|
57
|
-
attr_reader :id, :
|
|
61
|
+
attr_reader :id, :name, :data, :created_at
|
|
58
62
|
|
|
59
63
|
def initialize(attributes = {})
|
|
60
64
|
@id = attributes["id"]
|
|
61
|
-
@
|
|
65
|
+
@name = attributes["name"]
|
|
62
66
|
@data = attributes["data"]
|
|
63
67
|
@created_at = attributes["created_at"]
|
|
64
68
|
end
|
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
|
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.
|
|
4
|
+
version: 0.6.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:
|
|
11
|
+
date: 2026-08-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -53,9 +53,10 @@ dependencies:
|
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '2.0'
|
|
55
55
|
description: A comprehensive Ruby gem for integrating with Airwallex's global payment
|
|
56
|
-
infrastructure, including payment acceptance, payouts, foreign exchange
|
|
57
|
-
|
|
58
|
-
webhook verification, and unified pagination
|
|
56
|
+
infrastructure, including payment acceptance, payouts, foreign exchange (FX rates,
|
|
57
|
+
quotes, conversions), and multi-currency balance management. Features automatic
|
|
58
|
+
authentication, idempotency guarantees, webhook verification, and unified pagination
|
|
59
|
+
across all resources.
|
|
59
60
|
email:
|
|
60
61
|
- chayut@sentia.com.au
|
|
61
62
|
executables: []
|
|
@@ -66,16 +67,6 @@ files:
|
|
|
66
67
|
- LICENSE.txt
|
|
67
68
|
- README.md
|
|
68
69
|
- Rakefile
|
|
69
|
-
- docs/internal/20251125_iteration_1_quickstart.md
|
|
70
|
-
- docs/internal/20251125_iteration_1_summary.md
|
|
71
|
-
- docs/internal/20251125_sprint_1_completed.md
|
|
72
|
-
- docs/internal/20251125_sprint_1_plan.md
|
|
73
|
-
- docs/internal/20251125_sprint_2_completed.md
|
|
74
|
-
- docs/internal/20251125_sprint_2_plan.md
|
|
75
|
-
- docs/internal/20251125_sprint_2_unit_tests_completed.md
|
|
76
|
-
- docs/internal/20251125_v0.1.0_publication_checklist.md
|
|
77
|
-
- docs/research/Airwallex API Endpoint Research.md
|
|
78
|
-
- docs/research/Airwallex API Research for Ruby Gem.md
|
|
79
70
|
- lib/airwallex.rb
|
|
80
71
|
- lib/airwallex/api_operations/create.rb
|
|
81
72
|
- lib/airwallex/api_operations/delete.rb
|
|
@@ -89,8 +80,17 @@ files:
|
|
|
89
80
|
- lib/airwallex/list_object.rb
|
|
90
81
|
- lib/airwallex/middleware/auth_refresh.rb
|
|
91
82
|
- lib/airwallex/middleware/idempotency.rb
|
|
83
|
+
- lib/airwallex/resources/balance.rb
|
|
84
|
+
- lib/airwallex/resources/batch_transfer.rb
|
|
92
85
|
- lib/airwallex/resources/beneficiary.rb
|
|
86
|
+
- lib/airwallex/resources/conversion.rb
|
|
87
|
+
- lib/airwallex/resources/customer.rb
|
|
88
|
+
- lib/airwallex/resources/dispute.rb
|
|
93
89
|
- lib/airwallex/resources/payment_intent.rb
|
|
90
|
+
- lib/airwallex/resources/payment_method.rb
|
|
91
|
+
- lib/airwallex/resources/quote.rb
|
|
92
|
+
- lib/airwallex/resources/rate.rb
|
|
93
|
+
- lib/airwallex/resources/refund.rb
|
|
94
94
|
- lib/airwallex/resources/transfer.rb
|
|
95
95
|
- lib/airwallex/util.rb
|
|
96
96
|
- lib/airwallex/version.rb
|
|
@@ -122,7 +122,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
122
122
|
- !ruby/object:Gem::Version
|
|
123
123
|
version: '0'
|
|
124
124
|
requirements: []
|
|
125
|
-
rubygems_version: 3.
|
|
125
|
+
rubygems_version: 3.4.10
|
|
126
126
|
signing_key:
|
|
127
127
|
specification_version: 4
|
|
128
128
|
summary: Production-grade Ruby client for the Airwallex API
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
# Iteration 1 - Quick Start
|
|
2
|
-
|
|
3
|
-
**Sprint 1, Iteration 1**
|
|
4
|
-
**Date:** 25 November 2025
|
|
5
|
-
**Status:** ✅ Complete
|
|
6
|
-
|
|
7
|
-
## What Was Built
|
|
8
|
-
|
|
9
|
-
Core infrastructure for the Airwallex Ruby gem including:
|
|
10
|
-
|
|
11
|
-
1. ✅ Complete directory structure
|
|
12
|
-
2. ✅ Configuration management (sandbox/production)
|
|
13
|
-
3. ✅ HTTP client with Faraday
|
|
14
|
-
4. ✅ Bearer token authentication with auto-refresh
|
|
15
|
-
5. ✅ Comprehensive error handling
|
|
16
|
-
6. ✅ Automatic idempotency
|
|
17
|
-
7. ✅ Webhook signature verification
|
|
18
|
-
8. ✅ Utility helpers
|
|
19
|
-
9. ✅ Zero Rubocop offenses
|
|
20
|
-
|
|
21
|
-
## Quick Test
|
|
22
|
-
|
|
23
|
-
```ruby
|
|
24
|
-
require 'airwallex'
|
|
25
|
-
|
|
26
|
-
# Configure the gem
|
|
27
|
-
Airwallex.configure do |config|
|
|
28
|
-
config.api_key = 'your_api_key'
|
|
29
|
-
config.client_id = 'your_client_id'
|
|
30
|
-
config.environment = :sandbox
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# Check configuration
|
|
34
|
-
puts Airwallex.configuration.api_url
|
|
35
|
-
# => https://api-demo.airwallex.com/api/v1
|
|
36
|
-
|
|
37
|
-
# Client is ready (authentication happens automatically on first request)
|
|
38
|
-
client = Airwallex.client
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Files Created
|
|
42
|
-
|
|
43
|
-
```text
|
|
44
|
-
lib/airwallex.rb - Main module with configuration
|
|
45
|
-
lib/airwallex/version.rb - Version constant
|
|
46
|
-
lib/airwallex/configuration.rb - Configuration class
|
|
47
|
-
lib/airwallex/client.rb - HTTP client with Faraday
|
|
48
|
-
lib/airwallex/errors.rb - Exception hierarchy
|
|
49
|
-
lib/airwallex/util.rb - Helper utilities
|
|
50
|
-
lib/airwallex/webhook.rb - Webhook verification
|
|
51
|
-
lib/airwallex/middleware/idempotency.rb - Auto request_id
|
|
52
|
-
lib/airwallex/middleware/auth_refresh.rb - Token management
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## Key Features
|
|
56
|
-
|
|
57
|
-
### Environment Safety
|
|
58
|
-
- Defaults to `:sandbox` to prevent accidental production transactions
|
|
59
|
-
- Validates environment selection
|
|
60
|
-
- Dynamic URL generation
|
|
61
|
-
|
|
62
|
-
### Authentication
|
|
63
|
-
- Automatic Bearer token exchange
|
|
64
|
-
- 30-minute token lifetime with 5-minute refresh buffer
|
|
65
|
-
- Thread-safe token management
|
|
66
|
-
- Transparent 401 retry
|
|
67
|
-
|
|
68
|
-
### Idempotency
|
|
69
|
-
- Automatic UUID v4 generation for `request_id`
|
|
70
|
-
- Injected into request body (Airwallex specification)
|
|
71
|
-
- Safe request retries
|
|
72
|
-
|
|
73
|
-
### Error Handling
|
|
74
|
-
- HTTP status mapped to specific exceptions
|
|
75
|
-
- Polymorphic error body parsing
|
|
76
|
-
- Detailed error information (`code`, `message`, `param`, `details`)
|
|
77
|
-
|
|
78
|
-
### Webhook Security
|
|
79
|
-
- HMAC-SHA256 signature verification
|
|
80
|
-
- Replay attack protection (5-minute tolerance)
|
|
81
|
-
- Constant-time comparison
|
|
82
|
-
|
|
83
|
-
## What's Next
|
|
84
|
-
|
|
85
|
-
**Iteration 2:** Testing Infrastructure
|
|
86
|
-
- Set up RSpec, WebMock, VCR
|
|
87
|
-
- Write comprehensive tests
|
|
88
|
-
- Achieve 90%+ coverage
|
|
89
|
-
|
|
90
|
-
**Sprint 2:** Resource Implementation
|
|
91
|
-
- APIResource base class
|
|
92
|
-
- Payment Intent resource
|
|
93
|
-
- Transfer resource
|
|
94
|
-
- Pagination system
|
|
95
|
-
|
|
96
|
-
## Validation
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
# Check for errors
|
|
100
|
-
bundle exec rubocop lib/
|
|
101
|
-
# => 9 files inspected, no offenses detected ✅
|
|
102
|
-
|
|
103
|
-
# Test gem loading
|
|
104
|
-
bundle exec ruby -e "require './lib/airwallex'; puts 'OK'"
|
|
105
|
-
# => OK ✅
|
|
106
|
-
|
|
107
|
-
# Test configuration
|
|
108
|
-
bundle exec ruby -e "
|
|
109
|
-
require './lib/airwallex'
|
|
110
|
-
Airwallex.configure { |c| c.api_key = 'test'; c.client_id = 'test' }
|
|
111
|
-
puts Airwallex.configuration.api_url
|
|
112
|
-
"
|
|
113
|
-
# => https://api-demo.airwallex.com/api/v1 ✅
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## Time Spent
|
|
117
|
-
|
|
118
|
-
Approximately 4 hours for:
|
|
119
|
-
- Directory structure setup
|
|
120
|
-
- Core class implementation
|
|
121
|
-
- Faraday middleware
|
|
122
|
-
- Rubocop configuration
|
|
123
|
-
- Documentation
|
|
124
|
-
|
|
125
|
-
## Notes
|
|
126
|
-
|
|
127
|
-
- All code follows Ruby 3.1+ standards
|
|
128
|
-
- No external API calls made yet (no tests)
|
|
129
|
-
- Architecture matches research blueprints exactly
|
|
130
|
-
- Ready for test implementation
|