frame_payments 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/.standard.yml +3 -0
- data/CHANGELOG.md +48 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/PRODUCTION_READINESS.md +148 -0
- data/README.md +506 -0
- data/Rakefile +10 -0
- data/lib/frame/api_operations/create.rb +16 -0
- data/lib/frame/api_operations/delete.rb +16 -0
- data/lib/frame/api_operations/list.rb +16 -0
- data/lib/frame/api_operations/request.rb +34 -0
- data/lib/frame/api_operations/save.rb +39 -0
- data/lib/frame/api_resource.rb +67 -0
- data/lib/frame/configuration.rb +27 -0
- data/lib/frame/error.rb +48 -0
- data/lib/frame/frame_client.rb +208 -0
- data/lib/frame/frame_object.rb +155 -0
- data/lib/frame/list_object.rb +166 -0
- data/lib/frame/resources/charge_intent.rb +115 -0
- data/lib/frame/resources/customer.rb +125 -0
- data/lib/frame/resources/customer_identity_verification.rb +60 -0
- data/lib/frame/resources/invoice.rb +134 -0
- data/lib/frame/resources/invoice_line_item.rb +80 -0
- data/lib/frame/resources/payment_method.rb +116 -0
- data/lib/frame/resources/product.rb +80 -0
- data/lib/frame/resources/product_phase.rb +80 -0
- data/lib/frame/resources/refund.rb +60 -0
- data/lib/frame/resources/subscription.rb +134 -0
- data/lib/frame/resources/subscription_phase.rb +80 -0
- data/lib/frame/resources/webhook_endpoint.rb +116 -0
- data/lib/frame/resources.rb +26 -0
- data/lib/frame/util.rb +87 -0
- data/lib/frame/version.rb +5 -0
- data/lib/frame_payments.rb +60 -0
- data/sig/frame.rbs +4 -0
- metadata +124 -0
data/README.md
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
# Frame Payments Ruby Library
|
|
2
|
+
|
|
3
|
+
A Ruby library for the Frame Payments API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'frame_payments'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install frame_payments
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
> Note: The gem name is `frame_payments`, but the Ruby namespace is `Frame`.
|
|
28
|
+
|
|
29
|
+
The library needs to be configured with your Frame API key:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require 'frame_payments'
|
|
33
|
+
Frame.api_key = 'your_api_key'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Configuration Options
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
# Configure API settings
|
|
40
|
+
Frame.api_key = 'your_api_key'
|
|
41
|
+
Frame.api_base = 'https://api.framepayments.com' # Default
|
|
42
|
+
Frame.open_timeout = 30 # Connection timeout in seconds
|
|
43
|
+
Frame.read_timeout = 80 # Read timeout in seconds
|
|
44
|
+
Frame.verify_ssl_certs = true # SSL verification (default: true)
|
|
45
|
+
|
|
46
|
+
# Optional: Enable logging (sensitive data is automatically redacted)
|
|
47
|
+
require 'logger'
|
|
48
|
+
Frame.logger = Logger.new(STDOUT)
|
|
49
|
+
Frame.log_level = :info # :debug, :info, :warn, :error
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Customers
|
|
53
|
+
|
|
54
|
+
**Create a customer:**
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
customer = Frame::Customer.create(
|
|
58
|
+
name: 'John Doe',
|
|
59
|
+
email: 'john@example.com',
|
|
60
|
+
phone: '+12345678900',
|
|
61
|
+
metadata: {
|
|
62
|
+
user_id: '12345'
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Retrieve a customer:**
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
customer = Frame::Customer.retrieve('cus_123456789')
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Update a customer:**
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
customer = Frame::Customer.retrieve('cus_123456789')
|
|
77
|
+
customer.name = 'Jane Doe'
|
|
78
|
+
customer.save
|
|
79
|
+
|
|
80
|
+
# Alternative approach
|
|
81
|
+
customer = Frame::Customer.retrieve('cus_123456789')
|
|
82
|
+
customer.save(name: 'Jane Doe')
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**List all customers:**
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
customers = Frame::Customer.list
|
|
89
|
+
customers.each do |customer|
|
|
90
|
+
puts "Customer: #{customer.name}, Email: #{customer.email}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# With pagination
|
|
94
|
+
customers = Frame::Customer.list(page: 1, per_page: 20)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Search customers:**
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
customers = Frame::Customer.search(name: 'John')
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Delete a customer:**
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
Frame::Customer.delete('cus_123456789')
|
|
107
|
+
# or
|
|
108
|
+
customer = Frame::Customer.retrieve('cus_123456789')
|
|
109
|
+
customer.delete
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Block/unblock a customer:**
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
customer = Frame::Customer.retrieve('cus_123456789')
|
|
116
|
+
customer.block
|
|
117
|
+
|
|
118
|
+
# Unblock
|
|
119
|
+
customer.unblock
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Charge Intents
|
|
123
|
+
|
|
124
|
+
**Create a charge intent:**
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
charge_intent = Frame::ChargeIntent.create(
|
|
128
|
+
amount: 10000,
|
|
129
|
+
currency: 'usd',
|
|
130
|
+
customer: 'cus_123456789',
|
|
131
|
+
payment_method: 'pm_123456789',
|
|
132
|
+
description: 'Payment for order #123'
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Retrieve, list, and update:**
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
# Retrieve
|
|
140
|
+
intent = Frame::ChargeIntent.retrieve('ci_123456789')
|
|
141
|
+
|
|
142
|
+
# List
|
|
143
|
+
intents = Frame::ChargeIntent.list(page: 1, per_page: 20)
|
|
144
|
+
|
|
145
|
+
# Update
|
|
146
|
+
intent.description = 'Updated description'
|
|
147
|
+
intent.save
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Authorize, capture, or cancel:**
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
intent = Frame::ChargeIntent.retrieve('ci_123456789')
|
|
154
|
+
intent.authorize # Authorize the payment
|
|
155
|
+
intent.capture # Capture the authorized amount
|
|
156
|
+
intent.cancel # Cancel the intent
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Payment Methods
|
|
160
|
+
|
|
161
|
+
**Create a payment method:**
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
payment_method = Frame::PaymentMethod.create(
|
|
165
|
+
type: 'card',
|
|
166
|
+
card: {
|
|
167
|
+
number: '4242424242424242',
|
|
168
|
+
exp_month: 12,
|
|
169
|
+
exp_year: 2025,
|
|
170
|
+
cvc: '123'
|
|
171
|
+
}
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Attach to a customer:**
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
payment_method.attach('cus_123456789')
|
|
179
|
+
# or
|
|
180
|
+
Frame::PaymentMethod.attach('pm_123456789', 'cus_123456789')
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**List and manage:**
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
# List all payment methods
|
|
187
|
+
methods = Frame::PaymentMethod.list(customer: 'cus_123456789')
|
|
188
|
+
|
|
189
|
+
# Detach from customer
|
|
190
|
+
payment_method.detach
|
|
191
|
+
|
|
192
|
+
# Delete
|
|
193
|
+
payment_method.delete
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Refunds
|
|
197
|
+
|
|
198
|
+
**Create a refund:**
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
refund = Frame::Refund.create(
|
|
202
|
+
charge: 'ch_123456789',
|
|
203
|
+
amount: 5000, # Amount in cents, or omit for full refund
|
|
204
|
+
reason: 'requested_by_customer'
|
|
205
|
+
)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Retrieve and list:**
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
refund = Frame::Refund.retrieve('rf_123456789')
|
|
212
|
+
refunds = Frame::Refund.list(charge: 'ch_123456789')
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Cancel a refund:**
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
refund.cancel
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Invoices
|
|
222
|
+
|
|
223
|
+
**Create an invoice:**
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
invoice = Frame::Invoice.create(
|
|
227
|
+
customer: 'cus_123456789',
|
|
228
|
+
total: 10000,
|
|
229
|
+
currency: 'usd',
|
|
230
|
+
due_date: Time.now.to_i + 86400 # 24 hours from now
|
|
231
|
+
)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Manage invoice lifecycle:**
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
invoice = Frame::Invoice.retrieve('inv_123456789')
|
|
238
|
+
|
|
239
|
+
# Finalize (make it payable)
|
|
240
|
+
invoice.finalize
|
|
241
|
+
|
|
242
|
+
# Mark as paid
|
|
243
|
+
invoice.pay(payment_method: 'pm_123456789')
|
|
244
|
+
|
|
245
|
+
# Void
|
|
246
|
+
invoice.void
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**List invoices:**
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
invoices = Frame::Invoice.list(customer: 'cus_123456789', status: 'paid')
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Invoice Line Items
|
|
256
|
+
|
|
257
|
+
**Create a line item:**
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
line_item = Frame::InvoiceLineItem.create(
|
|
261
|
+
invoice: 'inv_123456789',
|
|
262
|
+
description: 'Product or service',
|
|
263
|
+
quantity: 2,
|
|
264
|
+
unit_amount: 5000
|
|
265
|
+
)
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Update and delete:**
|
|
269
|
+
|
|
270
|
+
```ruby
|
|
271
|
+
line_item.quantity = 3
|
|
272
|
+
line_item.save
|
|
273
|
+
line_item.delete
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Subscriptions
|
|
277
|
+
|
|
278
|
+
**Create a subscription:**
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
subscription = Frame::Subscription.create(
|
|
282
|
+
customer: 'cus_123456789',
|
|
283
|
+
product_phase: 'pph_123456789',
|
|
284
|
+
payment_method: 'pm_123456789'
|
|
285
|
+
)
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Manage subscription:**
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
subscription = Frame::Subscription.retrieve('sub_123456789')
|
|
292
|
+
|
|
293
|
+
# Pause
|
|
294
|
+
subscription.pause
|
|
295
|
+
|
|
296
|
+
# Resume
|
|
297
|
+
subscription.resume
|
|
298
|
+
|
|
299
|
+
# Cancel
|
|
300
|
+
subscription.cancel
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**List subscriptions:**
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
subscriptions = Frame::Subscription.list(customer: 'cus_123456789', status: 'active')
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Products
|
|
310
|
+
|
|
311
|
+
**Create a product:**
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
product = Frame::Product.create(
|
|
315
|
+
name: 'Premium Plan',
|
|
316
|
+
description: 'A premium subscription plan',
|
|
317
|
+
active: true
|
|
318
|
+
)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**List products:**
|
|
322
|
+
|
|
323
|
+
```ruby
|
|
324
|
+
products = Frame::Product.list(active: true)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Product Phases
|
|
328
|
+
|
|
329
|
+
**Create a product phase:**
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
phase = Frame::ProductPhase.create(
|
|
333
|
+
product: 'prod_123456789',
|
|
334
|
+
price: 10000,
|
|
335
|
+
currency: 'usd',
|
|
336
|
+
interval: 'month',
|
|
337
|
+
interval_count: 1
|
|
338
|
+
)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Webhook Endpoints
|
|
342
|
+
|
|
343
|
+
**Create a webhook endpoint:**
|
|
344
|
+
|
|
345
|
+
```ruby
|
|
346
|
+
webhook = Frame::WebhookEndpoint.create(
|
|
347
|
+
url: 'https://example.com/webhook',
|
|
348
|
+
events: ['charge.succeeded', 'charge.failed', 'subscription.created']
|
|
349
|
+
)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**Enable/disable:**
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
webhook.enable
|
|
356
|
+
webhook.disable
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**List webhooks:**
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
webhooks = Frame::WebhookEndpoint.list
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Customer Identity Verifications
|
|
366
|
+
|
|
367
|
+
**Create a verification:**
|
|
368
|
+
|
|
369
|
+
```ruby
|
|
370
|
+
verification = Frame::CustomerIdentityVerification.create(
|
|
371
|
+
customer: 'cus_123456789',
|
|
372
|
+
type: 'identity_document'
|
|
373
|
+
)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**Verify:**
|
|
377
|
+
|
|
378
|
+
```ruby
|
|
379
|
+
verification.verify
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**List verifications:**
|
|
383
|
+
|
|
384
|
+
```ruby
|
|
385
|
+
verifications = Frame::CustomerIdentityVerification.list(customer: 'cus_123456789')
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Error Handling
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
begin
|
|
392
|
+
customer = Frame::Customer.retrieve('invalid_id')
|
|
393
|
+
rescue Frame::InvalidRequestError => e
|
|
394
|
+
puts "Request failed: #{e.message}"
|
|
395
|
+
puts "HTTP Status: #{e.http_status}"
|
|
396
|
+
rescue Frame::AuthenticationError => e
|
|
397
|
+
puts "Authentication failed: #{e.message}"
|
|
398
|
+
rescue Frame::RateLimitError => e
|
|
399
|
+
puts "Rate limit exceeded: #{e.message}"
|
|
400
|
+
rescue Frame::APIError => e
|
|
401
|
+
puts "API error: #{e.message}"
|
|
402
|
+
end
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### Security Best Practices
|
|
406
|
+
|
|
407
|
+
#### API Key Management
|
|
408
|
+
|
|
409
|
+
**Never commit API keys to version control:**
|
|
410
|
+
|
|
411
|
+
```ruby
|
|
412
|
+
# ❌ BAD - Never do this
|
|
413
|
+
Frame.api_key = 'sk_live_1234567890abcdef'
|
|
414
|
+
|
|
415
|
+
# ✅ GOOD - Use environment variables
|
|
416
|
+
Frame.api_key = ENV['FRAME_API_KEY']
|
|
417
|
+
|
|
418
|
+
# ✅ GOOD - Use Rails credentials (Rails apps)
|
|
419
|
+
Frame.api_key = Rails.application.credentials.frame_payments[:api_key]
|
|
420
|
+
|
|
421
|
+
# ✅ GOOD - Use a secrets management service
|
|
422
|
+
Frame.api_key = SecretsManager.get('frame_api_key')
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**Use different keys for different environments:**
|
|
426
|
+
|
|
427
|
+
```ruby
|
|
428
|
+
# Development
|
|
429
|
+
Frame.api_key = ENV['FRAME_API_KEY_DEV']
|
|
430
|
+
|
|
431
|
+
# Production
|
|
432
|
+
Frame.api_key = ENV['FRAME_API_KEY_PROD']
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
#### SSL Verification
|
|
436
|
+
|
|
437
|
+
**Always keep SSL verification enabled in production:**
|
|
438
|
+
|
|
439
|
+
```ruby
|
|
440
|
+
# ✅ GOOD - Default (secure)
|
|
441
|
+
Frame.verify_ssl_certs = true
|
|
442
|
+
|
|
443
|
+
# ⚠️ Only disable for testing/development if absolutely necessary
|
|
444
|
+
Frame.verify_ssl_certs = false # NOT recommended for production
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
#### Logging
|
|
448
|
+
|
|
449
|
+
**The SDK automatically redacts sensitive data in logs**, but be cautious:
|
|
450
|
+
|
|
451
|
+
```ruby
|
|
452
|
+
# Logging is optional and off by default
|
|
453
|
+
# When enabled, sensitive data (API keys, card numbers, etc.) is automatically redacted
|
|
454
|
+
Frame.logger = Logger.new(STDOUT)
|
|
455
|
+
Frame.log_level = :info
|
|
456
|
+
|
|
457
|
+
# Never log raw API responses that might contain sensitive data
|
|
458
|
+
# The SDK handles this automatically, but be careful with custom logging
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
#### Secure Data Handling
|
|
462
|
+
|
|
463
|
+
- **Never log or print API keys** - The SDK redacts them automatically, but avoid custom logging of authentication headers
|
|
464
|
+
- **Use HTTPS only** - The SDK uses HTTPS by default (`https://api.framepayments.com`)
|
|
465
|
+
- **Validate input** - Always validate user input before sending to the API
|
|
466
|
+
- **Handle errors securely** - Don't expose sensitive error details to end users
|
|
467
|
+
- **Keep dependencies updated** - Regularly update the SDK and its dependencies
|
|
468
|
+
|
|
469
|
+
#### Environment Variables
|
|
470
|
+
|
|
471
|
+
**Recommended setup using environment variables:**
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
# .env file (add to .gitignore)
|
|
475
|
+
export FRAME_API_KEY='sk_live_your_key_here'
|
|
476
|
+
export FRAME_API_BASE='https://api.framepayments.com'
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
```ruby
|
|
480
|
+
# In your application
|
|
481
|
+
require 'dotenv/load' # If using dotenv gem
|
|
482
|
+
Frame.api_key = ENV['FRAME_API_KEY']
|
|
483
|
+
Frame.api_base = ENV.fetch('FRAME_API_BASE', 'https://api.framepayments.com')
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
#### Thread Safety
|
|
487
|
+
|
|
488
|
+
The SDK is designed to be thread-safe. The default client uses thread-safe initialization, and each thread can have its own client instance if needed.
|
|
489
|
+
|
|
490
|
+
## Development
|
|
491
|
+
|
|
492
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
493
|
+
|
|
494
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
495
|
+
|
|
496
|
+
## Contributing
|
|
497
|
+
|
|
498
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/frame. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/frame/blob/main/CODE_OF_CONDUCT.md).
|
|
499
|
+
|
|
500
|
+
## License
|
|
501
|
+
|
|
502
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
503
|
+
|
|
504
|
+
## Code of Conduct
|
|
505
|
+
|
|
506
|
+
Everyone interacting in the Frame project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/frame/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frame
|
|
4
|
+
module APIOperations
|
|
5
|
+
module Request
|
|
6
|
+
module ClassMethods
|
|
7
|
+
def request(method, path, params = {}, opts = {})
|
|
8
|
+
Frame::FrameClient.active_client.request(method, path, params, opts)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def request_object(method, path, params = {}, opts = {})
|
|
12
|
+
resp = request(method, path, params, opts)
|
|
13
|
+
Util.convert_to_frame_object(resp, opts)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.included(base)
|
|
18
|
+
base.extend(ClassMethods)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
protected
|
|
22
|
+
|
|
23
|
+
def request(method, path, params = {}, opts = {})
|
|
24
|
+
opts = Util.normalize_opts(opts)
|
|
25
|
+
self.class.request(method, path, params, opts)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def request_object(method, path, params = {}, opts = {})
|
|
29
|
+
opts = Util.normalize_opts(opts)
|
|
30
|
+
self.class.request_object(method, path, params, opts)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frame
|
|
4
|
+
module APIOperations
|
|
5
|
+
module Save
|
|
6
|
+
def save(params = {}, opts = {})
|
|
7
|
+
values = serialize_params(self).merge(params)
|
|
8
|
+
|
|
9
|
+
if values.empty?
|
|
10
|
+
return self
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
updated = request_object(
|
|
14
|
+
:patch,
|
|
15
|
+
resource_url,
|
|
16
|
+
values,
|
|
17
|
+
opts
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
initialize_from(updated)
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def serialize_params(obj)
|
|
25
|
+
params = {}
|
|
26
|
+
|
|
27
|
+
update_attributes = @values.keys.select do |k|
|
|
28
|
+
@original_values.key?(k) && @values[k] != @original_values[k]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
update_attributes.each do |attr|
|
|
32
|
+
params[attr] = obj[attr]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
params
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Frame
|
|
4
|
+
# Base class for all Frame Payments API resources.
|
|
5
|
+
#
|
|
6
|
+
# All API resources inherit from APIResource, which provides common
|
|
7
|
+
# functionality like retrieving resources by ID and building resource URLs.
|
|
8
|
+
#
|
|
9
|
+
# @abstract Subclass and implement {object_name} to create a new resource type.
|
|
10
|
+
class APIResource < FrameObject
|
|
11
|
+
include Frame::APIOperations::Request
|
|
12
|
+
|
|
13
|
+
def self.class_name
|
|
14
|
+
name.split("::")[-1]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Builds the base URL for this resource type
|
|
18
|
+
# @return [String] the resource URL (e.g., "/v1/customers")
|
|
19
|
+
def self.resource_url
|
|
20
|
+
if self == APIResource
|
|
21
|
+
raise NotImplementedError,
|
|
22
|
+
"APIResource is an abstract class. You should perform actions " \
|
|
23
|
+
"on its subclasses (Customer, etc.)"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
"/v1/#{object_name.downcase}s"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Retrieves a resource by its ID
|
|
30
|
+
# @param id [String] the resource ID
|
|
31
|
+
# @param opts [Hash] additional options
|
|
32
|
+
# @return [APIResource] the retrieved resource instance
|
|
33
|
+
# @example
|
|
34
|
+
# customer = Frame::Customer.retrieve('cus_123456789')
|
|
35
|
+
def self.retrieve(id, opts = {})
|
|
36
|
+
id = Util.normalize_id(id)
|
|
37
|
+
instance = new(id, opts)
|
|
38
|
+
instance.refresh(opts)
|
|
39
|
+
instance
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Builds the URL for this specific resource instance
|
|
43
|
+
# @return [String] the resource instance URL
|
|
44
|
+
# @raise [InvalidRequestError] if the resource doesn't have an ID
|
|
45
|
+
def resource_url
|
|
46
|
+
unless (id = self["id"])
|
|
47
|
+
raise InvalidRequestError.new(
|
|
48
|
+
"Could not determine which URL to request: #{self.class} instance " \
|
|
49
|
+
"has invalid ID: #{id.inspect}",
|
|
50
|
+
"id"
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
"#{self.class.resource_url}/#{CGI.escape(id)}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Refreshes the resource data from the API
|
|
57
|
+
# @param opts [Hash] additional options
|
|
58
|
+
# @return [APIResource] self, with updated data
|
|
59
|
+
# @example
|
|
60
|
+
# customer.refresh
|
|
61
|
+
def refresh(opts = {})
|
|
62
|
+
response = request(:get, resource_url, {}, opts)
|
|
63
|
+
initialize_from(response, opts)
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|