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.
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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[test standard]
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Frame
4
+ module APIOperations
5
+ module Create
6
+ def create(params = {}, opts = {})
7
+ request_object(
8
+ :post,
9
+ resource_url,
10
+ params,
11
+ opts
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Frame
4
+ module APIOperations
5
+ module Delete
6
+ def delete(params = {}, opts = {})
7
+ request_object(
8
+ :delete,
9
+ resource_url,
10
+ params,
11
+ opts
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Frame
4
+ module APIOperations
5
+ module List
6
+ def list(params = {}, opts = {})
7
+ request_object(
8
+ :get,
9
+ resource_url,
10
+ params,
11
+ opts
12
+ )
13
+ end
14
+ end
15
+ end
16
+ end
@@ -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