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.
@@ -1,531 +0,0 @@
1
- # Sprint 2 Implementation Plan
2
- **Date:** 25 November 2025
3
- **Sprint:** 2 - API Resources & Operations
4
- **Status:** 🔄 IN PROGRESS
5
-
6
- ---
7
-
8
- ## Sprint Goals
9
-
10
- Build the resource abstraction layer that enables intuitive, Ruby-idiomatic API interactions. Implement the first set of core resources (PaymentIntent, Transfer, Beneficiary) with full CRUD operations and pagination support.
11
-
12
- ### Primary Objectives
13
- 1. Create APIResource base class with shared behavior
14
- 2. Implement API operation mixins (Create, Retrieve, Update, List, Delete)
15
- 3. Build unified pagination system (cursor + offset)
16
- 4. Implement 3 core resources with full functionality
17
- 5. Add request/response object wrapping
18
- 6. Implement nested resource support
19
-
20
- ---
21
-
22
- ## Work Items
23
-
24
- ### 1. APIResource Base Class
25
- **Priority: Critical**
26
- **Estimated: 6 hours**
27
-
28
- #### 1.1 Base Resource Structure
29
- Create `lib/airwallex/api_resource.rb`:
30
-
31
- ```ruby
32
- module Airwallex
33
- class APIResource
34
- attr_reader :id
35
- attr_accessor :attributes
36
-
37
- def initialize(attributes = {})
38
- @attributes = Util.deep_symbolize_keys(attributes)
39
- @id = @attributes[:id]
40
- end
41
-
42
- # Class methods
43
- def self.resource_name
44
- # Convert PaymentIntent -> payment_intent
45
- end
46
-
47
- def self.resource_path
48
- # /api/v1/pa/payment_intents
49
- end
50
-
51
- # Instance methods
52
- def refresh
53
- # Re-fetch from API
54
- end
55
-
56
- def to_hash
57
- @attributes
58
- end
59
- end
60
- end
61
- ```
62
-
63
- **Features:**
64
- - Dynamic attribute accessors (`payment_intent.amount`)
65
- - Resource name inference from class name
66
- - Automatic path generation
67
- - Hash/JSON serialization
68
- - Attribute dirty tracking (for updates)
69
-
70
- **Acceptance Criteria:**
71
- - [ ] Base class instantiatable with hash
72
- - [ ] Attributes accessible via dot notation
73
- - [ ] Resource name correctly inferred
74
- - [ ] Resource path generated correctly
75
- - [ ] to_hash returns all attributes
76
- - [ ] refresh fetches latest data
77
-
78
- ---
79
-
80
- ### 2. API Operations Mixins
81
- **Priority: Critical**
82
- **Estimated: 8 hours**
83
-
84
- #### 2.1 Create Operation
85
- `lib/airwallex/api_operations/create.rb`:
86
-
87
- ```ruby
88
- module Airwallex
89
- module APIOperations
90
- module Create
91
- def create(params = {}, opts = {})
92
- response = Airwallex.client.post(
93
- "#{resource_path}/create",
94
- params,
95
- opts[:headers] || {}
96
- )
97
- new(response)
98
- end
99
- end
100
- end
101
- end
102
- ```
103
-
104
- #### 2.2 Retrieve Operation
105
- `lib/airwallex/api_operations/retrieve.rb`:
106
-
107
- ```ruby
108
- module Airwallex
109
- module APIOperations
110
- module Retrieve
111
- def retrieve(id, opts = {})
112
- response = Airwallex.client.get(
113
- "#{resource_path}/#{id}",
114
- {},
115
- opts[:headers] || {}
116
- )
117
- new(response)
118
- end
119
- end
120
- end
121
- end
122
- ```
123
-
124
- #### 2.3 Update Operation
125
- `lib/airwallex/api_operations/update.rb`:
126
-
127
- ```ruby
128
- module Airwallex
129
- module APIOperations
130
- module Update
131
- module ClassMethods
132
- def update(id, params = {}, opts = {})
133
- # PUT request
134
- end
135
- end
136
-
137
- module InstanceMethods
138
- def update(params = {})
139
- # Update current instance
140
- end
141
-
142
- def save
143
- # Save dirty attributes
144
- end
145
- end
146
- end
147
- end
148
- end
149
- ```
150
-
151
- #### 2.4 Delete Operation
152
- `lib/airwallex/api_operations/delete.rb`:
153
-
154
- ```ruby
155
- module Airwallex
156
- module APIOperations
157
- module Delete
158
- def delete(id, opts = {})
159
- Airwallex.client.delete("#{resource_path}/#{id}")
160
- true
161
- end
162
- end
163
- end
164
- end
165
- ```
166
-
167
- #### 2.5 List Operation
168
- `lib/airwallex/api_operations/list.rb`:
169
-
170
- ```ruby
171
- module Airwallex
172
- module APIOperations
173
- module List
174
- def list(params = {}, opts = {})
175
- response = Airwallex.client.get(
176
- resource_path,
177
- params,
178
- opts[:headers] || {}
179
- )
180
-
181
- ListObject.new(
182
- data: response[:items],
183
- has_more: response[:has_more],
184
- resource_class: self
185
- )
186
- end
187
- end
188
- end
189
- end
190
- ```
191
-
192
- **Acceptance Criteria:**
193
- - [ ] Create mixin works for all resources
194
- - [ ] Retrieve fetches single resource by ID
195
- - [ ] Update supports both class and instance methods
196
- - [ ] Delete returns boolean success
197
- - [ ] List returns paginated collection
198
- - [ ] All operations support request options
199
-
200
- ---
201
-
202
- ### 3. Pagination System
203
- **Priority: High**
204
- **Estimated: 6 hours**
205
-
206
- #### 3.1 ListObject
207
- `lib/airwallex/list_object.rb`:
208
-
209
- ```ruby
210
- module Airwallex
211
- class ListObject
212
- include Enumerable
213
-
214
- attr_reader :data, :has_more, :next_cursor
215
-
216
- def initialize(data:, has_more:, resource_class:, next_cursor: nil)
217
- @data = data.map { |item| resource_class.new(item) }
218
- @has_more = has_more
219
- @next_cursor = next_cursor
220
- @resource_class = resource_class
221
- end
222
-
223
- def each(&block)
224
- @data.each(&block)
225
- end
226
-
227
- def next_page(params = {})
228
- # Fetch next page using cursor or offset
229
- end
230
-
231
- def auto_paging_each(&block)
232
- # Automatically fetch all pages
233
- end
234
- end
235
- end
236
- ```
237
-
238
- **Features:**
239
- - Enumerable interface (`each`, `map`, `select`)
240
- - Cursor-based pagination (Airwallex default)
241
- - Offset-based pagination (fallback)
242
- - Auto-paging iterator
243
- - Lazy loading support
244
-
245
- **Acceptance Criteria:**
246
- - [ ] ListObject wraps array of resources
247
- - [ ] Enumerable methods work
248
- - [ ] next_page fetches subsequent page
249
- - [ ] auto_paging_each iterates all pages
250
- - [ ] Handles both cursor and offset pagination
251
-
252
- ---
253
-
254
- ### 4. Resource Implementations
255
- **Priority: High**
256
- **Estimated: 12 hours**
257
-
258
- #### 4.1 PaymentIntent Resource
259
- `lib/airwallex/resources/payment_intent.rb`:
260
-
261
- ```ruby
262
- module Airwallex
263
- class PaymentIntent < APIResource
264
- extend APIOperations::Create
265
- extend APIOperations::Retrieve
266
- extend APIOperations::List
267
- include APIOperations::Update
268
-
269
- def self.resource_path
270
- "/api/v1/pa/payment_intents"
271
- end
272
-
273
- # Custom methods
274
- def confirm(params = {})
275
- response = Airwallex.client.post(
276
- "#{self.class.resource_path}/#{id}/confirm",
277
- params
278
- )
279
- refresh_from(response)
280
- end
281
-
282
- def cancel(params = {})
283
- response = Airwallex.client.post(
284
- "#{self.class.resource_path}/#{id}/cancel",
285
- params
286
- )
287
- refresh_from(response)
288
- end
289
-
290
- def capture(params = {})
291
- # Capture authorized amount
292
- end
293
- end
294
- end
295
- ```
296
-
297
- **Fields:**
298
- - `id`, `amount`, `currency`, `status`
299
- - `merchant_order_id`, `return_url`
300
- - `payment_method`, `captured_amount`
301
- - `created_at`, `updated_at`
302
-
303
- **Methods:**
304
- - `PaymentIntent.create(params)` - Create new intent
305
- - `PaymentIntent.retrieve(id)` - Get by ID
306
- - `PaymentIntent.list(params)` - List with filters
307
- - `#confirm(payment_method)` - Confirm payment
308
- - `#cancel(reason)` - Cancel intent
309
- - `#capture(amount)` - Capture authorized
310
-
311
- #### 4.2 Transfer Resource
312
- `lib/airwallex/resources/transfer.rb`:
313
-
314
- ```ruby
315
- module Airwallex
316
- class Transfer < APIResource
317
- extend APIOperations::Create
318
- extend APIOperations::Retrieve
319
- extend APIOperations::List
320
-
321
- def self.resource_path
322
- "/api/v1/transfers"
323
- end
324
-
325
- def cancel
326
- # Cancel pending transfer
327
- end
328
- end
329
- end
330
- ```
331
-
332
- **Fields:**
333
- - `id`, `beneficiary_id`, `amount`, `source_currency`
334
- - `destination_currency`, `transfer_method`, `status`
335
- - `fee`, `reason`, `created_at`
336
-
337
- **Methods:**
338
- - `Transfer.create(params)` - Create transfer
339
- - `Transfer.retrieve(id)` - Get by ID
340
- - `Transfer.list(params)` - List transfers
341
- - `#cancel` - Cancel pending
342
-
343
- #### 4.3 Beneficiary Resource
344
- `lib/airwallex/resources/beneficiary.rb`:
345
-
346
- ```ruby
347
- module Airwallex
348
- class Beneficiary < APIResource
349
- extend APIOperations::Create
350
- extend APIOperations::Retrieve
351
- extend APIOperations::List
352
- extend APIOperations::Delete
353
-
354
- def self.resource_path
355
- "/api/v1/beneficiaries"
356
- end
357
- end
358
- end
359
- ```
360
-
361
- **Fields:**
362
- - `id`, `bank_details`, `beneficiary_type`
363
- - `company_name`, `first_name`, `last_name`
364
- - `address`, `entity_type`, `created_at`
365
-
366
- **Methods:**
367
- - `Beneficiary.create(params)` - Create beneficiary
368
- - `Beneficiary.retrieve(id)` - Get by ID
369
- - `Beneficiary.list(params)` - List all
370
- - `Beneficiary.delete(id)` - Delete beneficiary
371
-
372
- **Acceptance Criteria:**
373
- - [ ] All 3 resources implement expected methods
374
- - [ ] CRUD operations work end-to-end
375
- - [ ] Custom methods (confirm, cancel, capture) work
376
- - [ ] Pagination works for list operations
377
- - [ ] All fields accessible via dot notation
378
-
379
- ---
380
-
381
- ### 5. Response Object Wrapping
382
- **Priority: Medium**
383
- **Estimated: 4 hours**
384
-
385
- #### 5.1 Response Object
386
- `lib/airwallex/response.rb`:
387
-
388
- ```ruby
389
- module Airwallex
390
- class Response
391
- attr_reader :data, :http_status, :http_headers, :request_id
392
-
393
- def initialize(data:, http_status:, http_headers:)
394
- @data = data
395
- @http_status = http_status
396
- @http_headers = http_headers
397
- @request_id = http_headers['x-request-id']
398
- end
399
-
400
- def success?
401
- (200..299).include?(http_status)
402
- end
403
- end
404
- end
405
- ```
406
-
407
- **Features:**
408
- - Wraps API response with metadata
409
- - Exposes HTTP status and headers
410
- - Provides request_id for support
411
- - Success/failure predicates
412
-
413
- **Acceptance Criteria:**
414
- - [ ] Response object wraps all API calls
415
- - [ ] HTTP metadata accessible
416
- - [ ] request_id extracted from headers
417
- - [ ] Backward compatible with direct data access
418
-
419
- ---
420
-
421
- ### 6. Testing
422
- **Priority: High**
423
- **Estimated: 10 hours**
424
-
425
- #### 6.1 Resource Tests
426
- Create test files:
427
- - `spec/airwallex/api_resource_spec.rb`
428
- - `spec/airwallex/list_object_spec.rb`
429
- - `spec/airwallex/resources/payment_intent_spec.rb`
430
- - `spec/airwallex/resources/transfer_spec.rb`
431
- - `spec/airwallex/resources/beneficiary_spec.rb`
432
-
433
- **Test Coverage:**
434
- - Resource initialization and attribute access
435
- - CRUD operations for each resource
436
- - Pagination (next_page, auto_paging_each)
437
- - Custom resource methods (confirm, cancel, capture)
438
- - Error handling for each operation
439
- - Response object wrapping
440
-
441
- **Acceptance Criteria:**
442
- - [ ] All resources tested with WebMock stubs
443
- - [ ] Pagination tested with multiple pages
444
- - [ ] Error cases covered
445
- - [ ] Test coverage > 90%
446
-
447
- ---
448
-
449
- ## Definition of Done
450
-
451
- - [ ] All 3 resources fully implemented
452
- - [ ] CRUD operations work end-to-end
453
- - [ ] Pagination system complete
454
- - [ ] All tests passing (0 failures)
455
- - [ ] 0 Rubocop offenses
456
- - [ ] Code coverage > 90%
457
- - [ ] README updated with resource examples
458
- - [ ] Manual testing in sandbox complete
459
-
460
- ---
461
-
462
- ## Dependencies
463
-
464
- **New Gems:**
465
- - None (using existing Faraday stack)
466
-
467
- **Internal:**
468
- - Sprint 1 infrastructure (Client, Configuration, Errors)
469
-
470
- ---
471
-
472
- ## Risk Mitigation
473
-
474
- | Risk | Impact | Mitigation |
475
- |------|--------|-----------|
476
- | API response schema changes | High | Pin API version, use VCR cassettes |
477
- | Nested resource complexity | Medium | Start simple, iterate on feedback |
478
- | Pagination edge cases | Medium | Comprehensive test coverage |
479
- | Resource name conflicts | Low | Use explicit resource_path override |
480
-
481
- ---
482
-
483
- ## Success Metrics
484
-
485
- - PaymentIntent.create works in sandbox ✅
486
- - Transfer.create works in sandbox ✅
487
- - Beneficiary CRUD works in sandbox ✅
488
- - Pagination fetches multiple pages ✅
489
- - All tests green ✅
490
- - Zero Rubocop offenses ✅
491
-
492
- ---
493
-
494
- ## Out of Scope (Sprint 3+)
495
-
496
- - Nested resources (PaymentConsent, PaymentMethod)
497
- - Webhook handling (already done in Sprint 1)
498
- - File upload resources
499
- - Batch operations
500
- - Rate limit header parsing
501
- - Connected accounts (x-on-behalf-of)
502
-
503
- ---
504
-
505
- ## Sprint 2 Timeline
506
-
507
- **Week 1:**
508
- - Day 1-2: APIResource base class + mixins
509
- - Day 3-4: Pagination system
510
- - Day 5: PaymentIntent resource
511
-
512
- **Week 2:**
513
- - Day 1: Transfer resource
514
- - Day 2: Beneficiary resource
515
- - Day 3-4: Testing all resources
516
- - Day 5: Documentation + sandbox validation
517
-
518
- ---
519
-
520
- ## Next Steps
521
-
522
- After Sprint 2 completion, we'll have a fully functional gem ready for:
523
- - Publishing to RubyGems (v0.1.0)
524
- - Production use for basic payment workflows
525
- - Community feedback and iteration
526
-
527
- **Sprint 3 Preview:**
528
- - Additional resources (Account, Card, Payout)
529
- - Advanced features (refunds, disputes)
530
- - Performance optimizations
531
- - Enhanced documentation