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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Airwallex
4
- VERSION = "0.2.0"
4
+ VERSION = "0.6.0"
5
5
  end
@@ -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, :type, :data, :created_at
61
+ attr_reader :id, :name, :data, :created_at
58
62
 
59
63
  def initialize(attributes = {})
60
64
  @id = attributes["id"]
61
- @type = attributes["type"]
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.2.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: 2025-11-25 00:00:00.000000000 Z
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, card issuing,
57
- and treasury. Features automatic authentication management, idempotency guarantees,
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.5.11
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