airwallex 0.1.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 +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +377 -0
- data/Rakefile +12 -0
- data/docs/internal/20251125_iteration_1_quickstart.md +130 -0
- data/docs/internal/20251125_iteration_1_summary.md +342 -0
- data/docs/internal/20251125_sprint_1_completed.md +448 -0
- data/docs/internal/20251125_sprint_1_plan.md +389 -0
- data/docs/internal/20251125_sprint_2_completed.md +559 -0
- data/docs/internal/20251125_sprint_2_plan.md +531 -0
- data/docs/internal/20251125_sprint_2_unit_tests_completed.md +264 -0
- data/docs/research/Airwallex API Endpoint Research.md +410 -0
- data/docs/research/Airwallex API Research for Ruby Gem.md +383 -0
- data/lib/airwallex/api_operations/create.rb +16 -0
- data/lib/airwallex/api_operations/delete.rb +16 -0
- data/lib/airwallex/api_operations/list.rb +23 -0
- data/lib/airwallex/api_operations/retrieve.rb +16 -0
- data/lib/airwallex/api_operations/update.rb +44 -0
- data/lib/airwallex/api_resource.rb +96 -0
- data/lib/airwallex/client.rb +132 -0
- data/lib/airwallex/configuration.rb +67 -0
- data/lib/airwallex/errors.rb +64 -0
- data/lib/airwallex/list_object.rb +85 -0
- data/lib/airwallex/middleware/auth_refresh.rb +32 -0
- data/lib/airwallex/middleware/idempotency.rb +29 -0
- data/lib/airwallex/resources/beneficiary.rb +14 -0
- data/lib/airwallex/resources/payment_intent.rb +44 -0
- data/lib/airwallex/resources/transfer.rb +23 -0
- data/lib/airwallex/util.rb +58 -0
- data/lib/airwallex/version.rb +5 -0
- data/lib/airwallex/webhook.rb +67 -0
- data/lib/airwallex.rb +49 -0
- data/sig/airwallex.rbs +4 -0
- metadata +128 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
# **Architectural Blueprint and Technical Implementation Strategy for the Airwallex Ruby Gem**
|
|
4
|
+
|
|
5
|
+
## **1\. Executive Summary and Strategic Alignment**
|
|
6
|
+
|
|
7
|
+
The development of a robust, production-grade Ruby gem for the Airwallex API represents a significant infrastructure undertaking that requires a nuanced understanding of distributed financial systems. This report provides a comprehensive architectural analysis of the Airwallex API ecosystem, derived from an exhaustive review of official documentation, technical specifications, and integration patterns. The primary objective is to lay the foundational blueprints for a Ruby 3.1+ library utilizing rubocop for static analysis and faraday for HTTP transport layer abstraction.
|
|
8
|
+
|
|
9
|
+
The Airwallex API is a sprawling financial infrastructure interface that encompasses global business accounts, payments acceptance, foreign exchange (FX), and card issuing.1 Unlike typical REST APIs where a failed request is a mere inconvenience or a UI glitch, errors in this domain can result in duplicate financial transactions, locked funds, regulatory non-compliance, or significant monetary loss. Therefore, a client library interacting with this system must prioritize **correctness, idempotency, and type safety** above all else.
|
|
10
|
+
|
|
11
|
+
This research highlights critical architectural patterns necessary for the gem. First, the API utilizes a strict separation of concerns between its sandbox and production environments, extending beyond simple URL changes to include distinct credential sets and rate-limiting behaviors.2 Second, the authentication mechanism is multi-layered, involving not just static API keys but also short-lived Bearer tokens that require active lifecycle management, and in some cases, even shorter-lived Strong Customer Authentication (SCA) tokens.4 Third, the API is in a transitional state regarding pagination, necessitating a gem architecture that can seamlessly handle both legacy offset-based and modern cursor-based traversal strategies.1 Finally, the enforcement of idempotency via request\_id in the payload body—rather than the header—presents a specific implementation detail that deviates from common industry standards like Stripe, requiring careful middleware design.1
|
|
12
|
+
|
|
13
|
+
The following sections detail the API structure, endpoint behaviors, and implementation strategies required to build a gem that meets the rigorous demands of enterprise financial software.
|
|
14
|
+
|
|
15
|
+
## **2\. API Environment and Connection Architecture**
|
|
16
|
+
|
|
17
|
+
The fundamental connectivity layer of the Airwallex Ruby gem must be designed to handle the strict environmental segmentation enforced by the platform. The Airwallex API operates on a model where the testing (Sandbox) and live (Production) environments are completely isolated universes. This separation extends beyond simple base URL changes; it involves distinct credential sets, rate limits, and behavioral guarantees, which the gem must abstract for the end-user to prevent catastrophic configuration errors.2
|
|
18
|
+
|
|
19
|
+
### **2.1. Endpoint Topology and Configuration**
|
|
20
|
+
|
|
21
|
+
The gem must support configuration for at least two primary environments. Hardcoding these URLs is discouraged; instead, a configuration block pattern should be employed to allow runtime injection of these values, facilitating testing against mocks or proxy servers. The primary entry points for the API differ significantly between environments.
|
|
22
|
+
|
|
23
|
+
| Environment | Base URL | Purpose |
|
|
24
|
+
| :---- | :---- | :---- |
|
|
25
|
+
| **Sandbox** | https://api-demo.airwallex.com/api/v1/ | Development, integration testing, and simulation of financial flows without real money movement. This environment mimics production behavior but does not connect to real banking networks.1 |
|
|
26
|
+
| **Production** | https://api.airwallex.com/api/v1/ | Live financial transactions. Strict security, higher rate limits, and real money movement apply.1 |
|
|
27
|
+
| **Files (Sandbox)** | https://files-demo.airwallex.com | Dedicated host for uploading documents for KYC/KYB checks in the test environment. Separation of file hosting reduces load on the transactional API.7 |
|
|
28
|
+
| **Files (Production)** | https://files.airwallex.com | Dedicated host for live compliance documents and evidence submission.7 |
|
|
29
|
+
|
|
30
|
+
**Architectural Implication:** The Airwallex.configure block in the Ruby gem must accept an environment symbol (e.g., :sandbox or :production). The internal client must dynamically interpolate the correct API and Files hostnames based on this selection. Defaulting to :production is unsafe; the safer default is :sandbox to prevent accidental real-money transactions during initial setup. Furthermore, the gem should validate that the credentials provided match the selected environment format, as Client IDs often differ in prefix or format between environments.7
|
|
31
|
+
|
|
32
|
+
### **2.2. Protocol and Transport Security**
|
|
33
|
+
|
|
34
|
+
Communication is strictly HTTPS, ensuring transport layer security for all data in transit. The API follows REST principles, utilizing standard HTTP verbs (GET, POST, PUT, DELETE) to represent resource operations.1 The transport layer, which will be managed by the faraday gem, must be configured to enforce TLS 1.2 or higher. Financial APIs frequently deprecate older protocols (TLS 1.0/1.1) to maintain PCI-DSS compliance, and the Ruby gem should proactively enforce this to avoid negotiation failures on older systems.
|
|
35
|
+
|
|
36
|
+
Headers and Content Negotiation:
|
|
37
|
+
The gem must inject a specific set of headers into every request to ensure successful processing. Failure to provide these results in 400 Bad Request or 401 Unauthorized errors.
|
|
38
|
+
|
|
39
|
+
* **Content-Type:** Must be strictly application/json for all operational endpoints. The API expects JSON payloads and will fail to parse requests sent as application/x-www-form-urlencoded or other formats, except for specific OAuth token exchanges.1
|
|
40
|
+
* **User-Agent:** While not explicitly mandated by the snippets, it is best practice for the gem to identify itself (e.g., Airwallex-Ruby/1.0.0 Ruby/3.1.0). This aids in debugging with Airwallex support and allows the platform to track SDK usage versions.
|
|
41
|
+
* **x-api-version:** The API supports date-based versioning (e.g., 2019-09-09). This allows the API to evolve without breaking existing integrations.9 The gem should pin to a specific, tested version of the API to ensure stability. Overriding this via headers is possible but should be reserved for migration testing.10 The report notes that major changes to resources like "Global Accounts" and "Billing" are tied to specific API versions, making explicit version pinning in the gem critical for predictable behavior.1
|
|
42
|
+
|
|
43
|
+
### **2.3. Rate Limiting and Throttling Strategies**
|
|
44
|
+
|
|
45
|
+
Airwallex implements a robust rate-limiting strategy that the gem must handle gracefully to ensure system resilience. These limits are designed to protect the platform from abuse and ensure fair resource allocation.3
|
|
46
|
+
|
|
47
|
+
Limits Analysis:
|
|
48
|
+
The limits are bifurcated by environment and scope:
|
|
49
|
+
|
|
50
|
+
* **Production:**
|
|
51
|
+
* **Global Limit:** 100 requests per second. This is the total throughput allowed for the account.3
|
|
52
|
+
* **Endpoint Limit:** 20 requests per second. This prevents hammering a specific resource (e.g., repeatedly querying the same balance endpoint).3
|
|
53
|
+
* **Concurrency:** Capped at 50 simultaneous requests. This limits the number of open connections awaiting a response.3
|
|
54
|
+
* **Sandbox:**
|
|
55
|
+
* **Global Limit:** 20 requests per second.3
|
|
56
|
+
* **Endpoint Limit:** 10 requests per second.3
|
|
57
|
+
* **Concurrency:** Capped at 10 simultaneous requests.3
|
|
58
|
+
|
|
59
|
+
Handling Strategy:
|
|
60
|
+
When a limit is exceeded, the API returns 429 Too Many Requests. While some APIs provide explicit headers like X-RateLimit-Remaining to allow clients to throttle preemptively, the Airwallex snippets emphasize the status code as the primary signal.3
|
|
61
|
+
Gem Implementation:
|
|
62
|
+
The Faraday stack should include a Retry middleware configured with sophisticated logic. Generic retries are dangerous in financial APIs due to the risk of non-idempotent execution (double-charging).
|
|
63
|
+
|
|
64
|
+
* **Safe Retries:** GET requests (read operations) can be safely retried on 429 errors with exponential backoff.
|
|
65
|
+
* **Unsafe Retries:** POST (creation/mutation) requests must **only** be retried if the request included a mechanism for idempotency. As discussed in later sections, Airwallex uses request\_id in the body for this. If the gem sends a request without a reliable ID and receives a 429 or a timeout (504), it *cannot* safely retry without risking duplication.
|
|
66
|
+
* **Backoff:** The gem should implement Jittered Exponential Backoff. This introduces randomness into the wait time between retries, preventing "thundering herd" problems where all client threads retry simultaneously when the rate limit resets.
|
|
67
|
+
|
|
68
|
+
## **3\. Authentication and Authorization Architectures**
|
|
69
|
+
|
|
70
|
+
Authentication is the gatekeeper of the Airwallex API. The research indicates that a single authentication method is insufficient; the gem must support multiple strategies to cater to different use cases, including Direct Merchant integration and Platform/Connect models. The complexity lies in the lifecycle management of the access tokens, which are short-lived and must be refreshed regularly.4
|
|
71
|
+
|
|
72
|
+
### **3.1. Bearer Token Authentication (The Standard Flow)**
|
|
73
|
+
|
|
74
|
+
The primary mechanism for authenticating API requests is the **Bearer Token**. Unlike simpler APIs that might use the API key directly in every request header, Airwallex requires an exchange of long-lived credentials for a short-lived access token. This adds a layer of security, as the long-lived keys are exposed less frequently.9
|
|
75
|
+
|
|
76
|
+
**The Handshake Mechanism:**
|
|
77
|
+
|
|
78
|
+
1. **Credentials:** The user possesses a Client ID and an API Key (either Admin or Scoped) obtained from the Airwallex dashboard.4 Admin keys have broad access, while scoped keys are restricted to specific permissions, adhering to the principle of least privilege.4
|
|
79
|
+
2. **Exchange:** The gem must POST these credentials to /api/v1/authentication/login.1
|
|
80
|
+
* Headers: x-client-id, x-api-key.
|
|
81
|
+
* Body: Empty or specific login-as params if acting on behalf of another.
|
|
82
|
+
3. **Response:** A JSON object containing the token and expiration details.1
|
|
83
|
+
* Example: {"token": "eyJhb...", "expires\_at": "..."}.
|
|
84
|
+
4. **Usage:** Subsequent requests must include Authorization: Bearer \<token\> in the header.1
|
|
85
|
+
|
|
86
|
+
Token Lifecycle Management:
|
|
87
|
+
The access token is valid for 30 minutes.7 This relatively short lifespan necessitates an auto-refresh mechanism within the gem.
|
|
88
|
+
|
|
89
|
+
* **Lazy Refresh:** The client checks if the token is expired (or about to expire, e.g., within 5 minutes) before making a request. If expired, it calls the login endpoint again.
|
|
90
|
+
* **Middleware Approach:** For a Ruby gem, a **Lazy Refresh** approach within the Faraday middleware is preferred. The middleware can intercept 401 Unauthorized responses, check if the token might have expired, refresh the token, and replay the request transparently. This shields the developer from manually handling token expiration errors.
|
|
91
|
+
|
|
92
|
+
### **3.2. Platform and Connected Account Authentication (OAuth)**
|
|
93
|
+
|
|
94
|
+
For platforms managing other businesses (Connected Accounts), the authentication flow differs significantly. The gem must support the OAuth pattern to act on behalf of other users. This allows a platform (e.g., a marketplace) to manage payments for its sellers.11
|
|
95
|
+
|
|
96
|
+
**Mechanism:**
|
|
97
|
+
|
|
98
|
+
1. **Authorization Code:** Obtained via user redirection to an Airwallex authorization page.
|
|
99
|
+
2. **Token Exchange:** The code is exchanged for a refresh\_token (valid 90 days) and an access\_token.11
|
|
100
|
+
3. **Acting on Behalf:**
|
|
101
|
+
* **Header:** x-on-behalf-of: \<account\_id\> is used when a platform performs actions on a connected account's resources.13
|
|
102
|
+
* **Scoped Keys:** Alternatively, scoped API keys can be generated for specific accounts.4
|
|
103
|
+
|
|
104
|
+
Gem Architecture:
|
|
105
|
+
The gem should introduce a Session or Client class that can be initialized either with (client\_id, api\_key) for direct mode or (access\_token, refresh\_token) for OAuth mode. This polymorphism allows the same resource methods (e.g., Payment.create) to work regardless of the underlying auth strategy. The OAuth flow specifically requires a mechanism to persist and update the refresh\_token since it rotates; the gem should expose hooks to allow the host application to save the new refresh token to its database whenever it changes.11
|
|
106
|
+
|
|
107
|
+
### **3.3. Strong Customer Authentication (SCA)**
|
|
108
|
+
|
|
109
|
+
A critical edge case identified in the research is SCA enforcement for sensitive data retrieval (e.g., older transactions, full balances).5 This is a regulatory requirement in many jurisdictions.
|
|
110
|
+
|
|
111
|
+
* **Trigger:** Accessing sensitive endpoints may trigger an SCA requirement if the session is not already stepped-up.
|
|
112
|
+
* **Token:** A specific SCA token (valid for only **5 minutes**) is issued after the user completes 2FA.5
|
|
113
|
+
* **Implication:** The gem must be capable of handling "Step-up" authentication exceptions. If an API call fails due to missing SCA (likely a 403 or specific error code), the gem should raise a specific Airwallex::SCARequired error. This error object should ideally contain the details needed for the developer to initiate the user-facing 2FA flow. Once the SCA token is obtained, the developer would pass it back to the gem (perhaps via a headers option) to retry the request.5
|
|
114
|
+
|
|
115
|
+
## **4\. Idempotency and Distributed Consistency**
|
|
116
|
+
|
|
117
|
+
In financial software, idempotency is the mechanism that ensures a $100 transfer executed twice due to a network timeout results in a single $100 charge, not $200. It is the bedrock of data integrity in distributed systems.
|
|
118
|
+
|
|
119
|
+
### **4.1. The request\_id Paradigm**
|
|
120
|
+
|
|
121
|
+
The research reveals a crucial nuance in how Airwallex handles idempotency compared to other providers like Stripe. While Stripe uses a specific header (Idempotency-Key), Airwallex documentation explicitly and repeatedly instructs developers to include request\_id inside the **request body** for transactional endpoints like Conversions, Payouts, and Payments.1
|
|
122
|
+
|
|
123
|
+
**Behavioral Specification:**
|
|
124
|
+
|
|
125
|
+
* **Mechanism:** When a request is received with a request\_id that has been seen before:
|
|
126
|
+
* If the request parameters match the original request, the API returns the *original* successful response. This is a "replay" and is safe.
|
|
127
|
+
* If the request parameters differ, or if the original request failed in a non-recoverable way, the API may return a request\_id\_duplicate error.15
|
|
128
|
+
* **Concurrency:** If request\_id is a duplicate of a request that is currently *processing*, the API behavior is implicitly a race condition handling scenario, typically resulting in the second request waiting or being rejected.
|
|
129
|
+
|
|
130
|
+
### **4.2. Implementation in Ruby**
|
|
131
|
+
|
|
132
|
+
The gem should abstract this to prevent developer error. It is easy to forget to add a unique ID, so the gem should enforce "safe by default" behavior.
|
|
133
|
+
|
|
134
|
+
1. **Auto-Generation:** If the user does not supply a request\_id in the arguments for a create or update method, the gem should automatically generate a UUID (v4).
|
|
135
|
+
2. **Explicit Override:** Allow the user to pass a specific request\_id for their own reconciliation logic (e.g., matching their internal database ID).
|
|
136
|
+
|
|
137
|
+
Ruby
|
|
138
|
+
|
|
139
|
+
\# Conceptual implementation of Idempotency Middleware
|
|
140
|
+
def create(params \= {})
|
|
141
|
+
\# Check if request\_id exists in params, if not, generate it
|
|
142
|
+
params\[:request\_id\] ||\= SecureRandom.uuid
|
|
143
|
+
post('/transfers', params)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
This approach protects users who may not fully understand the risks of network retries. If a timeout occurs, and the user (or the gem's retry middleware) retries the request, the presence of the preserved request\_id ensures that Airwallex treats it as the same transaction.
|
|
147
|
+
|
|
148
|
+
## **5\. Pagination Standards and Transition**
|
|
149
|
+
|
|
150
|
+
The Airwallex API is currently undergoing a transition in pagination standards. The gem must support both legacy and modern patterns to ensure full coverage of all endpoints without confusing the developer.1
|
|
151
|
+
|
|
152
|
+
### **5.1. Offset-Based (Legacy)**
|
|
153
|
+
|
|
154
|
+
Older endpoints (and some current ones like Payouts) utilize an offset-based approach.13
|
|
155
|
+
|
|
156
|
+
* **Parameters:** page\_num (integer, 0-indexed) and page\_size (integer).
|
|
157
|
+
* **Mechanism:** To get the next page, the client increments page\_num.
|
|
158
|
+
* **Drawbacks:** Performance degrades on deep pages (the "offset problem" in databases); prone to data drift if items are inserted or deleted while paging (shifting the offset).
|
|
159
|
+
|
|
160
|
+
### **5.2. Cursor-Based (Modern)**
|
|
161
|
+
|
|
162
|
+
Newer endpoints, particularly "Global Accounts" and "Billing," have shifted to cursor-based pagination.1
|
|
163
|
+
|
|
164
|
+
* **Parameters:** page\_before or page\_after (cursor strings), and page\_size.
|
|
165
|
+
* **Mechanism:** The response contains a has\_more boolean and/or next\_page\_cursor.
|
|
166
|
+
* **Advantages:** O(1) fetch time regardless of depth; consistent ordering even with high-velocity writes.
|
|
167
|
+
|
|
168
|
+
### **5.3. The AutoPaginator Pattern**
|
|
169
|
+
|
|
170
|
+
To provide a superior developer experience (DX), the gem should implement an Enumerable wrapper. This allows Ruby developers to iterate through resources without managing loops, cursors, or page numbers manually.
|
|
171
|
+
|
|
172
|
+
* Design:
|
|
173
|
+
The list methods should return a Airwallex::List object. This object includes the raw items and metadata. It should also implement an auto\_paging\_each method that yields items one by one, transparently fetching the next page when the current buffer is exhausted.
|
|
174
|
+
* **Abstraction:** The AutoPaginator class must inspect the response to determine if it needs to use page\_num or page\_after for the next request, shielding the user from the underlying API inconsistency. It effectively polyfills a unified interface over the divergent API behaviors.
|
|
175
|
+
|
|
176
|
+
## **6\. Resource-Specific Architectures**
|
|
177
|
+
|
|
178
|
+
Different domains within the Airwallex API have unique requirements that the gem must model accurately.
|
|
179
|
+
|
|
180
|
+
### **6.1. Payments and 3DS**
|
|
181
|
+
|
|
182
|
+
The Payments API is complex, involving PaymentIntents (the state of the transaction) and PaymentAttempts (the specific execution). A key complexity here is **3D Secure (3DS)** authentication.19
|
|
183
|
+
|
|
184
|
+
* **Flow:** When confirming a payment intent via the API (server-side), the response might indicate that 3DS is required.
|
|
185
|
+
* **Gem Role:** The gem must return a structured object that clearly indicates the next\_action required. If the status is requires\_customer\_action, the gem should expose the redirect\_url or 3DS data needed for the frontend to complete the challenge.
|
|
186
|
+
|
|
187
|
+
### **6.2. Payouts and Schema Validation**
|
|
188
|
+
|
|
189
|
+
Global payouts require varying beneficiary data based on the destination country and currency (e.g., Routing Number in the US vs. IBAN in Europe vs. CNAPS in China). Airwallex handles this via a **Schema API**.20
|
|
190
|
+
|
|
191
|
+
* **Problem:** Hardcoding validation rules in the gem is futile as banking regulations change frequently.
|
|
192
|
+
* **Solution:** The gem should provide easy access to these schema endpoints (e.g., Beneficiary.schema(country: 'US', currency: 'USD')). It acts as a conduit for the dynamic validation logic provided by the server, allowing the host application to build dynamic forms.
|
|
193
|
+
|
|
194
|
+
### **6.3. Foreign Exchange (FX)**
|
|
195
|
+
|
|
196
|
+
FX operations involve real-time rates and locking mechanisms.1
|
|
197
|
+
|
|
198
|
+
* **Rates vs. Quotes:** The gem must distinguish between getting a strict Rate (indicative) and creating a Quote (locked for a specific duration).
|
|
199
|
+
* **Errors:** Specific errors like quote\_expired or insufficient\_funds are common.22 The gem should map these to specific exception classes to allow for programmatic handling (e.g., if a quote expires, automatically request a new one).
|
|
200
|
+
|
|
201
|
+
### **6.4. Issuing and Remote Authorization**
|
|
202
|
+
|
|
203
|
+
The Issuing API allows for creating cards and controlling spend. A key feature is **Remote Authorization**, where Airwallex asks the user's server to approve a transaction in real-time.23
|
|
204
|
+
|
|
205
|
+
* **Webhook Criticality:** This relies entirely on webhooks. The gem's webhook verification logic (discussed below) is critical here, as a forged authorization request could lead to fraudulent card spend.
|
|
206
|
+
|
|
207
|
+
## **7\. Data Models and Type Safety**
|
|
208
|
+
|
|
209
|
+
Financial APIs deal with precise data types. The gem must be rigorous in how it handles dates, currency, and numbers.
|
|
210
|
+
|
|
211
|
+
### **7.1. Date and Time Formats**
|
|
212
|
+
|
|
213
|
+
The research strictly identifies **ISO 8601** as the required format for dates and times.13
|
|
214
|
+
|
|
215
|
+
* **Requirement:** Fields like transfer\_date, conversion\_date, and from\_created\_at demand strings like "2023-10-27T10:00:00Z".
|
|
216
|
+
* **Validation:** Error codes 025, 026, 027 correspond explicitly to invalid date formats.24
|
|
217
|
+
* **Implementation:** The gem should accept standard Ruby Date and Time objects. The internal serializer must intercept these objects and format them to ISO 8601 strings (.iso8601) before transmission. This prevents the common "invalid format" errors that frustrate developers.
|
|
218
|
+
|
|
219
|
+
### **7.2. Currency and Monetary Values**
|
|
220
|
+
|
|
221
|
+
While the snippets don't explicitly detail decimal precision, financial APIs typically use either major units (10.50 USD) or minor units (1050 cents). The snippets show transaction\_amount as 11.11 27, implying **major units** (floats/decimals).
|
|
222
|
+
|
|
223
|
+
* **Risk:** Floating point math in Ruby (1.1 \+ 2.2\!= 3.3) is dangerous for finance.
|
|
224
|
+
* **Gem Strategy:** The gem should encourage or enforce the use of BigDecimal for all monetary amounts to ensure precision is preserved during serialization and calculation.
|
|
225
|
+
|
|
226
|
+
## **8\. Webhook Integrity and Event Processing**
|
|
227
|
+
|
|
228
|
+
Webhooks are the nervous system of an Airwallex integration, notifying the app of asynchronous events (deposits, payout completions). Security here is paramount to prevent replay attacks or forged events.
|
|
229
|
+
|
|
230
|
+
### **8.1. Signature Verification**
|
|
231
|
+
|
|
232
|
+
The research confirms the use of **HMAC-SHA256** for signature verification.28
|
|
233
|
+
|
|
234
|
+
* **Headers:**
|
|
235
|
+
* x-timestamp: Unix timestamp of the event generation.
|
|
236
|
+
* x-signature: The hex digest.
|
|
237
|
+
* **Algorithm:** The signature is generated by HMAC\_SHA256(secret, timestamp \+ body).29 Note the concatenation of timestamp and body.
|
|
238
|
+
* **Timing Attack Prevention:** The gem must use a constant-time comparison algorithm (e.g., OpenSSL.secure\_compare) to check the signature. A simple string comparison (==) is vulnerable to timing attacks where an attacker can guess the signature byte-by-byte based on how long the comparison takes.
|
|
239
|
+
|
|
240
|
+
### **8.2. Replay Protection**
|
|
241
|
+
|
|
242
|
+
The gem must enforce a tolerance window for the x-timestamp.23
|
|
243
|
+
|
|
244
|
+
* **Logic:** Current Time \- Header Timestamp \< Tolerance.
|
|
245
|
+
* **Standard:** A tolerance of 5 minutes (300 seconds) is standard industry practice.
|
|
246
|
+
* If the timestamp is too old, the webhook should be rejected, even if the signature is valid. This prevents an attacker from capturing a valid request and replaying it later to confuse the system or trigger duplicate processing logic.
|
|
247
|
+
|
|
248
|
+
### **8.3. Event Object Construction**
|
|
249
|
+
|
|
250
|
+
The gem should factory Airwallex::Event objects from the JSON payload. Ideally, these objects should be immutable structs to prevent accidental modification during processing.
|
|
251
|
+
|
|
252
|
+
* **Properties:** id, type (e.g., payment\_intent.succeeded), data (the resource), created\_at.
|
|
253
|
+
* **Parsing:** The data attribute should be parsed into the specific resource class (e.g., Airwallex::PaymentIntent), allowing methods like event.data.amount to work naturally, rather than forcing the developer to traverse a raw hash.
|
|
254
|
+
|
|
255
|
+
## **9\. Error Handling Architecture**
|
|
256
|
+
|
|
257
|
+
The robustness of a client library is defined by how it handles failure. The Airwallex API provides structured error information that the gem must parse and expose intelligibly.
|
|
258
|
+
|
|
259
|
+
### **9.1. HTTP Status Mapping**
|
|
260
|
+
|
|
261
|
+
The gem must map HTTP status codes to specific Exception classes 30:
|
|
262
|
+
|
|
263
|
+
* 400 \-\> Airwallex::BadRequestError: Malformed request, missing headers.
|
|
264
|
+
* 401 \-\> Airwallex::AuthenticationError: Invalid API key, expired token.
|
|
265
|
+
* 403 \-\> Airwallex::PermissionError: SCA required, insufficient scopes.
|
|
266
|
+
* 404 \-\> Airwallex::NotFoundError: Resource not found.
|
|
267
|
+
* 429 \-\> Airwallex::RateLimitError: Throttling active.
|
|
268
|
+
* 500+ \-\> Airwallex::APIError: Server-side issues.
|
|
269
|
+
|
|
270
|
+
### **9.2. Error Body Polymorphism**
|
|
271
|
+
|
|
272
|
+
The error body structure is polymorphic, presenting a challenge for parsing.15 The gem must be able to handle all variations.
|
|
273
|
+
|
|
274
|
+
**Standard Error:**
|
|
275
|
+
|
|
276
|
+
JSON
|
|
277
|
+
|
|
278
|
+
{
|
|
279
|
+
"code": "insufficient\_fund",
|
|
280
|
+
"message": "The account has insufficient funds...",
|
|
281
|
+
"source": "charge"
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
**Complex Error (with Details):**
|
|
285
|
+
|
|
286
|
+
JSON
|
|
287
|
+
|
|
288
|
+
{
|
|
289
|
+
"code": "request\_id\_duplicate",
|
|
290
|
+
"details": { "source\_id": "...", "source\_type": "..." },
|
|
291
|
+
"message": "request\_id '...' has been used..."
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
**Validation Error (with Nested Source):**
|
|
295
|
+
|
|
296
|
+
JSON
|
|
297
|
+
|
|
298
|
+
{
|
|
299
|
+
"code": "invalid\_argument",
|
|
300
|
+
"message": "Type should be one of...",
|
|
301
|
+
"source": "individual.employers.business\_identifiers.type"
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
Strategy:
|
|
305
|
+
The base Airwallex::Error class must have attributes for code, message, param (mapped from source), and details. The initializer must inspect the JSON body and populate details only if it exists. This ensures that debugging complex validation errors (like a deeply nested field in a KYC object) is possible programmatically, without the developer needing to print and manually inspect the raw JSON response.
|
|
306
|
+
|
|
307
|
+
## **10\. Library Structure and Dependencies**
|
|
308
|
+
|
|
309
|
+
To align with the user's requirement for Ruby 3.1+, Rubocop, and Faraday, the following structural decisions are recommended.
|
|
310
|
+
|
|
311
|
+
### **10.1. Dependencies**
|
|
312
|
+
|
|
313
|
+
* **Runtime:**
|
|
314
|
+
* faraday (\~\> 2.0): The backbone of the HTTP transport.
|
|
315
|
+
* faraday-retry: Essential for implementing the exponential backoff logic for 429s and 5xx errors.
|
|
316
|
+
* json: Standard parsing.
|
|
317
|
+
* bigdecimal: For financial math.
|
|
318
|
+
* **Development:**
|
|
319
|
+
* rubocop: Enforcing style guidelines. The configuration should be strict, enforcing typed signatures (RBS) where possible to leverage Ruby 3.1 capabilities.
|
|
320
|
+
* webmock / vcr: For recording and replaying API interactions during tests. This is crucial for testing complex flows like OAuth without needing a live browser or manually rotating keys.
|
|
321
|
+
* rspec: The standard testing framework.
|
|
322
|
+
|
|
323
|
+
### **10.2. Directory Structure**
|
|
324
|
+
|
|
325
|
+
A clean, modular structure is essential for maintainability.
|
|
326
|
+
|
|
327
|
+
lib/
|
|
328
|
+
├── airwallex/
|
|
329
|
+
│ ├── api\_operations/ \# Mixins for standard CRUD (Create, Retrieve, List)
|
|
330
|
+
│ ├── resources/ \# Class definitions (Payment, Payout, Beneficiary)
|
|
331
|
+
│ ├── errors.rb \# Exception hierarchy and mapping logic
|
|
332
|
+
│ ├── client.rb \# The main HTTP entry point and configuration holder
|
|
333
|
+
│ ├── webhook.rb \# Signature verification logic
|
|
334
|
+
│ ├── util.rb \# Helpers for pagination, logging, and time formatting
|
|
335
|
+
│ ├── middleware/ \# Custom Faraday middleware
|
|
336
|
+
│ │ ├── auth\_refresh.rb \# Logic for lazy token refreshing
|
|
337
|
+
│ │ └── idempotency.rb \# Logic for injecting request\_id
|
|
338
|
+
│ └── version.rb
|
|
339
|
+
└── airwallex.rb \# Configuration and module entry
|
|
340
|
+
|
|
341
|
+
### **10.3. The APIResource Pattern**
|
|
342
|
+
|
|
343
|
+
To reduce code duplication, the gem should implement an APIResource base class. Subclasses (e.g., Payment, Transfer) will inherit dynamic behavior.
|
|
344
|
+
|
|
345
|
+
* **Metaprogramming:** Use Ruby's method\_missing or define\_method to create accessors for JSON fields dynamically. This makes the gem resilient to API changes; if Airwallex adds a new field risk\_score, the gem supports it immediately without a version update. However, for known critical fields (like id, status), explicit accessors should be defined for better IDE autocompletion support.
|
|
346
|
+
|
|
347
|
+
## **11\. Conclusion**
|
|
348
|
+
|
|
349
|
+
Building the Airwallex Ruby gem requires navigating a sophisticated landscape of modern API patterns (Cursor pagination, HMAC webhooks) and strict financial constraints (ISO dates, strict idempotency). The blueprints provided here prioritize **safety and correctness**. By enforcing request\_id generation via middleware, handling the dual-pagination architectures with a unified abstraction, and managing the complex authentication flows transparently, the resulting gem will provide a stable foundation for Ruby developers to integrate global financial operations into their applications. The separation of environment configurations, robust error polymorphism handling, and adherence to SCA requirements ensures that the gem is not just a wrapper, but a piece of reliable financial infrastructure.
|
|
350
|
+
|
|
351
|
+
#### **Works cited**
|
|
352
|
+
|
|
353
|
+
1. Airwallex API Reference, accessed November 25, 2025, [https://www.airwallex.com/docs/api](https://www.airwallex.com/docs/api)
|
|
354
|
+
2. Sandbox environment overview | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/developer-tools/sandbox-environment/sandbox-environment-overview](https://www.airwallex.com/docs/developer-tools/sandbox-environment/sandbox-environment-overview)
|
|
355
|
+
3. Rate limits | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/developer-tools/api/rate-limits](https://www.airwallex.com/docs/developer-tools/api/rate-limits)
|
|
356
|
+
4. Manage API keys | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/developer-tools/api/manage-api-keys](https://www.airwallex.com/docs/developer-tools/api/manage-api-keys)
|
|
357
|
+
5. SCA for transaction data retrieval | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/payments-for-platforms/compliance-support/strong-customer-authentication-(sca)/sca-for-transaction-data-retrieval](https://www.airwallex.com/docs/payments-for-platforms/compliance-support/strong-customer-authentication-\(sca\)/sca-for-transaction-data-retrieval)
|
|
358
|
+
6. Create a conversion | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/transactional-fx\_\_create-a-conversion](https://www.airwallex.com/docs/transactional-fx__create-a-conversion)
|
|
359
|
+
7. Quickstart with Postman | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/developer-tools/api/quickstart-with-postman](https://www.airwallex.com/docs/developer-tools/api/quickstart-with-postman)
|
|
360
|
+
8. Integration checklist | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/transactional-fx/test-and-go-live/integration-checklist](https://www.airwallex.com/docs/transactional-fx/test-and-go-live/integration-checklist)
|
|
361
|
+
9. API | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/developer-tools/api](https://www.airwallex.com/docs/developer-tools/api)
|
|
362
|
+
10. Native API | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/payments/online-payments/native-api](https://www.airwallex.com/docs/payments/online-payments/native-api)
|
|
363
|
+
11. Existing customers | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/developer-tools/partner-connections/implement-your-authorization-flow/existing-customers](https://www.airwallex.com/docs/developer-tools/partner-connections/implement-your-authorization-flow/existing-customers)
|
|
364
|
+
12. New Airwallex customers, accessed November 25, 2025, [https://www.airwallex.com/docs/developer-tools/partner-connections/implement-your-authorization-flow/new-airwallex-customers](https://www.airwallex.com/docs/developer-tools/partner-connections/implement-your-authorization-flow/new-airwallex-customers)
|
|
365
|
+
13. Create a transfer | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/payouts\_\_create-a-transfer](https://www.airwallex.com/docs/payouts__create-a-transfer)
|
|
366
|
+
14. Sample integration | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/billing/subscriptions/subscriptions-via-api/sample-integration](https://www.airwallex.com/docs/billing/subscriptions/subscriptions-via-api/sample-integration)
|
|
367
|
+
15. Error codes | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/connected-accounts/move-funds/error-codes](https://www.airwallex.com/docs/connected-accounts/move-funds/error-codes)
|
|
368
|
+
16. Error codes | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/global-treasury\_\_error-codes](https://www.airwallex.com/docs/global-treasury__error-codes)
|
|
369
|
+
17. Server-side SDKs (Beta) | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/developer-tools/sdks/server-side-sdks-(beta)](https://www.airwallex.com/docs/developer-tools/sdks/server-side-sdks-\(beta\))
|
|
370
|
+
18. Manage Global Accounts | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/global-treasury\_\_global-accounts\_\_manage-global-accounts](https://www.airwallex.com/docs/global-treasury__global-accounts__manage-global-accounts)
|
|
371
|
+
19. 3D Secure authentication | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/payments/online-payments/native-api/3d-secure-authentication](https://www.airwallex.com/docs/payments/online-payments/native-api/3d-secure-authentication)
|
|
372
|
+
20. Using API and form schemas | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/payments-for-platforms/pay-out-funds/using-api-and-form-schemas](https://www.airwallex.com/docs/payments-for-platforms/pay-out-funds/using-api-and-form-schemas)
|
|
373
|
+
21. Using API and form schemas | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/payouts\_\_using-api-and-form-schemas](https://www.airwallex.com/docs/payouts__using-api-and-form-schemas)
|
|
374
|
+
22. Error codes | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/transactional-fx\_\_error-codes](https://www.airwallex.com/docs/transactional-fx__error-codes)
|
|
375
|
+
23. Respond to authorization requests | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/issuing\_\_remote-authorization\_\_respond-to-authorization-requests](https://www.airwallex.com/docs/issuing__remote-authorization__respond-to-authorization-requests)
|
|
376
|
+
24. Transfer error codes | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/payouts/errors/transfer-error-codes](https://www.airwallex.com/docs/payouts/errors/transfer-error-codes)
|
|
377
|
+
25. Error codes (older versions) | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/payouts\_\_transfers\_\_error-codes-(older-versions)](https://www.airwallex.com/docs/payouts__transfers__error-codes-\(older-versions\))
|
|
378
|
+
26. Create a batch transfer | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/payouts/batch-transfers/create-a-batch-transfer](https://www.airwallex.com/docs/payouts/batch-transfers/create-a-batch-transfer)
|
|
379
|
+
27. Respond to authorization requests | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/issuing/card-controls/remote-authorization/respond-to-authorization-requests](https://www.airwallex.com/docs/issuing/card-controls/remote-authorization/respond-to-authorization-requests)
|
|
380
|
+
28. Listen for webhook events | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/developer-tools/webhooks/listen-for-webhook-events](https://www.airwallex.com/docs/developer-tools/webhooks/listen-for-webhook-events)
|
|
381
|
+
29. Code examples | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/developer-tools/webhooks/listen-for-webhook-events/code-examples](https://www.airwallex.com/docs/developer-tools/webhooks/listen-for-webhook-events/code-examples)
|
|
382
|
+
30. Error response codes | Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/payments/troubleshooting/error-response-codes](https://www.airwallex.com/docs/payments/troubleshooting/error-response-codes)
|
|
383
|
+
31. Error codes \- Airwallex Docs, accessed November 25, 2025, [https://www.airwallex.com/docs/issuing/troubleshooting/error-codes](https://www.airwallex.com/docs/issuing/troubleshooting/error-codes)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
module APIOperations
|
|
5
|
+
module Create
|
|
6
|
+
def create(params = {}, opts = {})
|
|
7
|
+
response = Airwallex.client.post(
|
|
8
|
+
"#{resource_path}/create",
|
|
9
|
+
params,
|
|
10
|
+
opts[:headers] || {}
|
|
11
|
+
)
|
|
12
|
+
new(response)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
module APIOperations
|
|
5
|
+
module List
|
|
6
|
+
def list(params = {}, opts = {})
|
|
7
|
+
response = Airwallex.client.get(
|
|
8
|
+
resource_path,
|
|
9
|
+
params,
|
|
10
|
+
opts[:headers] || {}
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
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"],
|
|
17
|
+
resource_class: self,
|
|
18
|
+
params: params
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
module APIOperations
|
|
5
|
+
module Retrieve
|
|
6
|
+
def retrieve(id, opts = {})
|
|
7
|
+
response = Airwallex.client.get(
|
|
8
|
+
"#{resource_path}/#{id}",
|
|
9
|
+
{},
|
|
10
|
+
opts[:headers] || {}
|
|
11
|
+
)
|
|
12
|
+
new(response)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
module APIOperations
|
|
5
|
+
module Update
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.extend(ClassMethods)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
module ClassMethods
|
|
11
|
+
def update(id, params = {}, opts = {})
|
|
12
|
+
response = Airwallex.client.put(
|
|
13
|
+
"#{resource_path}/#{id}",
|
|
14
|
+
params,
|
|
15
|
+
opts[:headers] || {}
|
|
16
|
+
)
|
|
17
|
+
new(response)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Instance methods
|
|
22
|
+
def update(params = {})
|
|
23
|
+
response = Airwallex.client.put(
|
|
24
|
+
"#{self.class.resource_path}/#{id}",
|
|
25
|
+
params
|
|
26
|
+
)
|
|
27
|
+
refresh_from(response)
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def save
|
|
32
|
+
return self unless dirty?
|
|
33
|
+
|
|
34
|
+
# Only send changed attributes
|
|
35
|
+
params = {}
|
|
36
|
+
changed_attributes.each do |attr|
|
|
37
|
+
params[attr] = @attributes[attr]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
update(params)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Airwallex
|
|
4
|
+
class APIResource
|
|
5
|
+
attr_reader :id, :attributes
|
|
6
|
+
|
|
7
|
+
def initialize(attributes = {})
|
|
8
|
+
@attributes = Util.deep_symbolize_keys(attributes || {})
|
|
9
|
+
@id = @attributes[:id]
|
|
10
|
+
@previous_attributes = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Convert class name to resource name
|
|
14
|
+
# PaymentIntent -> payment_intent
|
|
15
|
+
def self.resource_name
|
|
16
|
+
name.split("::")[-1]
|
|
17
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
18
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
19
|
+
.downcase
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Override in subclasses to specify custom path
|
|
23
|
+
def self.resource_path
|
|
24
|
+
raise NotImplementedError, "#{self} must implement .resource_path"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Dynamic attribute accessors
|
|
28
|
+
def method_missing(method_name, *args, &)
|
|
29
|
+
method_str = method_name.to_s
|
|
30
|
+
|
|
31
|
+
if method_str.end_with?("=")
|
|
32
|
+
# Setter
|
|
33
|
+
attr_name = method_str.chop.to_sym
|
|
34
|
+
@previous_attributes[attr_name] = @attributes[attr_name] unless @previous_attributes.key?(attr_name)
|
|
35
|
+
@attributes[attr_name] = args[0]
|
|
36
|
+
elsif @attributes.key?(method_name)
|
|
37
|
+
# Getter
|
|
38
|
+
@attributes[method_name]
|
|
39
|
+
else
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
45
|
+
method_str = method_name.to_s
|
|
46
|
+
method_str.end_with?("=") || @attributes.key?(method_name) || super
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Refresh resource from API
|
|
50
|
+
def refresh
|
|
51
|
+
response = Airwallex.client.get("#{self.class.resource_path}/#{id}")
|
|
52
|
+
refresh_from(response)
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Update internal state from response
|
|
57
|
+
def refresh_from(data)
|
|
58
|
+
@attributes = Util.deep_symbolize_keys(data || {})
|
|
59
|
+
@id = @attributes[:id]
|
|
60
|
+
@previous_attributes = {}
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Check if any attributes have changed
|
|
65
|
+
def dirty?
|
|
66
|
+
!@previous_attributes.empty?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Get changed attributes
|
|
70
|
+
def changed_attributes
|
|
71
|
+
@previous_attributes.keys
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Convert to hash
|
|
75
|
+
def to_hash
|
|
76
|
+
@attributes.dup
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
alias to_h to_hash
|
|
80
|
+
|
|
81
|
+
# Convert to JSON
|
|
82
|
+
def to_json(*args)
|
|
83
|
+
to_hash.to_json(*args)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# String representation
|
|
87
|
+
def inspect
|
|
88
|
+
id_str = id ? " id=#{id}" : ""
|
|
89
|
+
"#<#{self.class}:0x#{object_id.to_s(16)}#{id_str}> JSON: #{to_json}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def to_s
|
|
93
|
+
to_json
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|