polar-ruby 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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +50 -0
  4. data/DEVELOPMENT.md +329 -0
  5. data/EXAMPLES.md +385 -0
  6. data/Gemfile +12 -0
  7. data/Gemfile.lock +115 -0
  8. data/LICENSE +23 -0
  9. data/PROJECT_SUMMARY.md +256 -0
  10. data/README.md +635 -0
  11. data/Rakefile +24 -0
  12. data/examples/demo.rb +106 -0
  13. data/lib/polar/authentication.rb +83 -0
  14. data/lib/polar/client.rb +144 -0
  15. data/lib/polar/configuration.rb +46 -0
  16. data/lib/polar/customer_portal/benefit_grants.rb +41 -0
  17. data/lib/polar/customer_portal/customers.rb +69 -0
  18. data/lib/polar/customer_portal/license_keys.rb +70 -0
  19. data/lib/polar/customer_portal/orders.rb +82 -0
  20. data/lib/polar/customer_portal/subscriptions.rb +51 -0
  21. data/lib/polar/errors.rb +96 -0
  22. data/lib/polar/http_client.rb +150 -0
  23. data/lib/polar/pagination.rb +133 -0
  24. data/lib/polar/resources/base.rb +47 -0
  25. data/lib/polar/resources/benefits.rb +64 -0
  26. data/lib/polar/resources/checkouts.rb +75 -0
  27. data/lib/polar/resources/customers.rb +120 -0
  28. data/lib/polar/resources/events.rb +45 -0
  29. data/lib/polar/resources/files.rb +57 -0
  30. data/lib/polar/resources/license_keys.rb +81 -0
  31. data/lib/polar/resources/metrics.rb +30 -0
  32. data/lib/polar/resources/oauth2.rb +61 -0
  33. data/lib/polar/resources/orders.rb +54 -0
  34. data/lib/polar/resources/organizations.rb +41 -0
  35. data/lib/polar/resources/payments.rb +29 -0
  36. data/lib/polar/resources/products.rb +58 -0
  37. data/lib/polar/resources/subscriptions.rb +55 -0
  38. data/lib/polar/resources/webhooks.rb +81 -0
  39. data/lib/polar/version.rb +5 -0
  40. data/lib/polar/webhooks.rb +174 -0
  41. data/lib/polar.rb +65 -0
  42. metadata +239 -0
data/EXAMPLES.md ADDED
@@ -0,0 +1,385 @@
1
+ # Polar Ruby SDK Examples
2
+
3
+ This directory contains examples demonstrating how to use the Polar Ruby SDK for various use cases.
4
+
5
+ ## Basic Usage
6
+
7
+ ### Simple E-commerce Checkout
8
+
9
+ ```ruby
10
+ require 'polar'
11
+
12
+ # Initialize client
13
+ client = Polar.new(access_token: ENV['POLAR_ACCESS_TOKEN'])
14
+
15
+ # Create a product
16
+ product = client.products.create({
17
+ name: "Digital Course",
18
+ description: "Learn Ruby programming",
19
+ organization_id: "org_123"
20
+ })
21
+
22
+ # Create checkout session
23
+ checkout = client.checkouts.create({
24
+ product_price_id: "price_123",
25
+ success_url: "https://mysite.com/success",
26
+ cancel_url: "https://mysite.com/cancel",
27
+ customer_data: {
28
+ email: "customer@example.com",
29
+ name: "John Doe"
30
+ }
31
+ })
32
+
33
+ puts "Checkout URL: #{checkout['url']}"
34
+ ```
35
+
36
+ ### Subscription Management
37
+
38
+ ```ruby
39
+ require 'polar'
40
+
41
+ client = Polar.new(access_token: ENV['POLAR_ACCESS_TOKEN'])
42
+
43
+ # List active subscriptions
44
+ subscriptions = client.subscriptions.list(
45
+ organization_id: 'org_123',
46
+ status: 'active'
47
+ ).auto_paginate
48
+
49
+ subscriptions.each do |subscription|
50
+ puts "Subscription #{subscription['id']}: #{subscription['customer']['email']}"
51
+ end
52
+
53
+ # Update subscription metadata
54
+ updated = client.subscriptions.update('sub_123', {
55
+ metadata: {
56
+ tier: 'premium',
57
+ support_level: 'priority'
58
+ }
59
+ })
60
+ ```
61
+
62
+ ### License Key Validation
63
+
64
+ ```ruby
65
+ require 'polar'
66
+
67
+ class LicenseValidator
68
+ def initialize
69
+ @client = Polar.new(access_token: ENV['POLAR_ACCESS_TOKEN'])
70
+ end
71
+
72
+ def validate_key(license_key)
73
+ result = @client.customer_portal.license_keys.validate(license_key)
74
+
75
+ if result['valid']
76
+ {
77
+ valid: true,
78
+ customer: result['customer']['email'],
79
+ product: result['benefit']['description'],
80
+ expires_at: result['expires_at']
81
+ }
82
+ else
83
+ { valid: false, reason: result['error'] }
84
+ end
85
+ rescue Polar::Error => e
86
+ { valid: false, error: e.message }
87
+ end
88
+ end
89
+
90
+ # Usage
91
+ validator = LicenseValidator.new
92
+ result = validator.validate_key('license_key_123')
93
+ puts result
94
+ ```
95
+
96
+ ### Webhook Handler
97
+
98
+ ```ruby
99
+ require 'polar'
100
+ require 'sinatra'
101
+ require 'json'
102
+
103
+ # Sinatra webhook endpoint
104
+ post '/webhooks/polar' do
105
+ payload = request.body.read
106
+ headers = request.env.select { |k,v| k.start_with? 'HTTP_' }
107
+ secret = ENV['POLAR_WEBHOOK_SECRET']
108
+
109
+ begin
110
+ event = Polar::Webhooks.validate_event(payload, headers, secret)
111
+
112
+ case event['type']
113
+ when 'order.created'
114
+ handle_order_created(event['data'])
115
+ when 'subscription.created'
116
+ handle_subscription_created(event['data'])
117
+ when 'subscription.cancelled'
118
+ handle_subscription_cancelled(event['data'])
119
+ when 'payment.succeeded'
120
+ handle_payment_succeeded(event['data'])
121
+ end
122
+
123
+ status 200
124
+ { status: 'processed' }.to_json
125
+ rescue Polar::WebhookVerificationError
126
+ status 403
127
+ { error: 'Invalid signature' }.to_json
128
+ end
129
+ end
130
+
131
+ def handle_order_created(order)
132
+ puts "New order: #{order['id']} for #{order['amount']} #{order['currency']}"
133
+ # Send confirmation email, update inventory, etc.
134
+ end
135
+
136
+ def handle_subscription_created(subscription)
137
+ puts "New subscription: #{subscription['id']}"
138
+ # Provision access, send welcome email, etc.
139
+ end
140
+
141
+ def handle_subscription_cancelled(subscription)
142
+ puts "Cancelled subscription: #{subscription['id']}"
143
+ # Revoke access, send cancellation email, etc.
144
+ end
145
+
146
+ def handle_payment_succeeded(payment)
147
+ puts "Payment succeeded: #{payment['id']}"
148
+ # Update payment status, trigger fulfillment, etc.
149
+ end
150
+ ```
151
+
152
+ ## Advanced Examples
153
+
154
+ ### Customer Portal Integration
155
+
156
+ ```ruby
157
+ class CustomerPortal
158
+ def initialize(customer_session)
159
+ @customer_session = customer_session
160
+ @client = Polar.new
161
+ end
162
+
163
+ def dashboard_data
164
+ {
165
+ customer: get_customer_info,
166
+ orders: get_recent_orders,
167
+ subscriptions: get_active_subscriptions,
168
+ license_keys: get_license_keys
169
+ }
170
+ end
171
+
172
+ private
173
+
174
+ def get_customer_info
175
+ @client.customer_portal.customers.get(customer_session: @customer_session)
176
+ end
177
+
178
+ def get_recent_orders(limit: 10)
179
+ @client.customer_portal.orders.list(
180
+ { limit: limit },
181
+ customer_session: @customer_session
182
+ ).auto_paginate
183
+ end
184
+
185
+ def get_active_subscriptions
186
+ @client.customer_portal.subscriptions.list(
187
+ customer_session: @customer_session
188
+ ).auto_paginate.select { |sub| sub['status'] == 'active' }
189
+ end
190
+
191
+ def get_license_keys
192
+ @client.customer_portal.license_keys.list(
193
+ customer_session: @customer_session
194
+ ).auto_paginate
195
+ end
196
+ end
197
+ ```
198
+
199
+ ### Batch Operations
200
+
201
+ ```ruby
202
+ class BatchProcessor
203
+ def initialize
204
+ @client = Polar.new(access_token: ENV['POLAR_ACCESS_TOKEN'])
205
+ end
206
+
207
+ def process_customers_in_batches(organization_id, batch_size: 100)
208
+ customers = @client.customers.list(
209
+ organization_id: organization_id,
210
+ limit: batch_size
211
+ )
212
+
213
+ customers.each do |customer|
214
+ process_customer(customer)
215
+ sleep(0.1) # Rate limiting
216
+ end
217
+ end
218
+
219
+ def export_subscription_data(organization_id)
220
+ export_job = @client.subscriptions.export({
221
+ organization_id: organization_id,
222
+ format: 'csv'
223
+ })
224
+
225
+ puts "Export started: #{export_job['id']}"
226
+ export_job
227
+ end
228
+
229
+ private
230
+
231
+ def process_customer(customer)
232
+ # Example: Update customer metadata
233
+ @client.customers.update(customer['id'], {
234
+ metadata: {
235
+ last_processed: Time.now.iso8601,
236
+ processor: 'batch_job'
237
+ }
238
+ })
239
+ rescue Polar::Error => e
240
+ puts "Failed to process customer #{customer['id']}: #{e.message}"
241
+ end
242
+ end
243
+ ```
244
+
245
+ ### Error Handling Patterns
246
+
247
+ ```ruby
248
+ class RobustPolarClient
249
+ def initialize
250
+ @client = Polar.new(access_token: ENV['POLAR_ACCESS_TOKEN'])
251
+ @logger = Logger.new(STDOUT)
252
+ end
253
+
254
+ def safe_get_customer(customer_id)
255
+ @client.customers.get(customer_id)
256
+ rescue Polar::NotFoundError
257
+ @logger.warn("Customer #{customer_id} not found")
258
+ nil
259
+ rescue Polar::UnauthorizedError => e
260
+ @logger.error("Authentication failed: #{e.message}")
261
+ raise
262
+ rescue Polar::TooManyRequestsError => e
263
+ @logger.warn("Rate limited, retrying after delay")
264
+ sleep(5)
265
+ retry
266
+ rescue Polar::HTTPError => e
267
+ @logger.error("HTTP error #{e.status_code}: #{e.message}")
268
+ nil
269
+ end
270
+
271
+ def create_customer_with_retry(customer_data, max_retries: 3)
272
+ retries = 0
273
+
274
+ begin
275
+ @client.customers.create(customer_data)
276
+ rescue Polar::ValidationError => e
277
+ @logger.error("Validation failed: #{e.message}")
278
+ raise
279
+ rescue Polar::ConnectionError, Polar::TimeoutError => e
280
+ retries += 1
281
+ if retries <= max_retries
282
+ @logger.warn("Connection failed, retry #{retries}/#{max_retries}")
283
+ sleep(2 ** retries)
284
+ retry
285
+ else
286
+ @logger.error("Max retries exceeded: #{e.message}")
287
+ raise
288
+ end
289
+ end
290
+ end
291
+ end
292
+ ```
293
+
294
+ ### Rails Integration
295
+
296
+ ```ruby
297
+ # app/services/polar_service.rb
298
+ class PolarService
299
+ include Singleton
300
+
301
+ def initialize
302
+ @client = Polar.new(
303
+ access_token: Rails.application.credentials.polar_access_token,
304
+ server: Rails.env.production? ? :production : :sandbox,
305
+ logger: Rails.logger
306
+ )
307
+ end
308
+
309
+ def create_checkout_for_user(user, product_price_id)
310
+ @client.checkouts.create({
311
+ product_price_id: product_price_id,
312
+ success_url: Rails.application.routes.url_helpers.checkout_success_url,
313
+ cancel_url: Rails.application.routes.url_helpers.checkout_cancel_url,
314
+ customer_data: {
315
+ email: user.email,
316
+ name: user.full_name,
317
+ external_id: user.id.to_s
318
+ },
319
+ metadata: {
320
+ user_id: user.id,
321
+ created_via: 'rails_app'
322
+ }
323
+ })
324
+ end
325
+
326
+ def sync_customer(user)
327
+ polar_customer = find_or_create_customer(user)
328
+ user.update!(polar_customer_id: polar_customer['id'])
329
+ polar_customer
330
+ end
331
+
332
+ private
333
+
334
+ def find_or_create_customer(user)
335
+ @client.customers.get_external(
336
+ user.id.to_s,
337
+ organization_id: organization_id
338
+ )
339
+ rescue Polar::NotFoundError
340
+ @client.customers.create({
341
+ email: user.email,
342
+ name: user.full_name,
343
+ external_id: user.id.to_s,
344
+ organization_id: organization_id
345
+ })
346
+ end
347
+
348
+ def organization_id
349
+ Rails.application.credentials.polar_organization_id
350
+ end
351
+ end
352
+
353
+ # app/controllers/checkouts_controller.rb
354
+ class CheckoutsController < ApplicationController
355
+ before_action :authenticate_user!
356
+
357
+ def create
358
+ checkout = PolarService.instance.create_checkout_for_user(
359
+ current_user,
360
+ params[:product_price_id]
361
+ )
362
+
363
+ redirect_to checkout['url']
364
+ rescue Polar::Error => e
365
+ flash[:error] = "Checkout failed: #{e.message}"
366
+ redirect_back(fallback_location: root_path)
367
+ end
368
+
369
+ def success
370
+ # Handle successful checkout
371
+ flash[:success] = "Payment successful!"
372
+ redirect_to dashboard_path
373
+ end
374
+
375
+ def cancel
376
+ # Handle cancelled checkout
377
+ flash[:info] = "Checkout cancelled"
378
+ redirect_to pricing_path
379
+ end
380
+ end
381
+ ```
382
+
383
+ For more examples and detailed documentation, visit:
384
+ - [Polar.sh Documentation](https://polar.sh/docs)
385
+ - [API Reference](https://polar.sh/docs/api-reference)
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in polar-ruby.gemspec
4
+ gemspec
5
+
6
+ gem 'rake', '~> 13.0'
7
+ gem 'rspec', '~> 3.0'
8
+ gem 'rubocop', '~> 1.21'
9
+ gem 'simplecov', '~> 0.21'
10
+ gem 'vcr', '~> 6.0'
11
+ gem 'webmock', '~> 3.0'
12
+ gem 'yard', '~> 0.9'
data/Gemfile.lock ADDED
@@ -0,0 +1,115 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ polar-ruby (0.1.0)
5
+ faraday (~> 2.0)
6
+ faraday-multipart (~> 1.0)
7
+ faraday-retry (~> 2.0)
8
+ jwt (~> 2.0)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ addressable (2.8.7)
14
+ public_suffix (>= 2.0.2, < 7.0)
15
+ ast (2.4.3)
16
+ base64 (0.3.0)
17
+ bigdecimal (3.3.1)
18
+ crack (1.0.0)
19
+ bigdecimal
20
+ rexml
21
+ diff-lcs (1.6.2)
22
+ docile (1.4.1)
23
+ faraday (2.14.0)
24
+ faraday-net_http (>= 2.0, < 3.5)
25
+ json
26
+ logger
27
+ faraday-multipart (1.1.1)
28
+ multipart-post (~> 2.0)
29
+ faraday-net_http (3.4.1)
30
+ net-http (>= 0.5.0)
31
+ faraday-retry (2.3.2)
32
+ faraday (~> 2.0)
33
+ hashdiff (1.2.1)
34
+ json (2.15.1)
35
+ jwt (2.10.2)
36
+ base64
37
+ language_server-protocol (3.17.0.5)
38
+ lint_roller (1.1.0)
39
+ logger (1.7.0)
40
+ multipart-post (2.4.1)
41
+ net-http (0.6.0)
42
+ uri
43
+ parallel (1.27.0)
44
+ parser (3.3.9.0)
45
+ ast (~> 2.4.1)
46
+ racc
47
+ prism (1.5.2)
48
+ public_suffix (6.0.2)
49
+ racc (1.8.1)
50
+ rainbow (3.1.1)
51
+ rake (13.3.0)
52
+ regexp_parser (2.11.3)
53
+ rexml (3.4.4)
54
+ rspec (3.13.1)
55
+ rspec-core (~> 3.13.0)
56
+ rspec-expectations (~> 3.13.0)
57
+ rspec-mocks (~> 3.13.0)
58
+ rspec-core (3.13.5)
59
+ rspec-support (~> 3.13.0)
60
+ rspec-expectations (3.13.5)
61
+ diff-lcs (>= 1.2.0, < 2.0)
62
+ rspec-support (~> 3.13.0)
63
+ rspec-mocks (3.13.5)
64
+ diff-lcs (>= 1.2.0, < 2.0)
65
+ rspec-support (~> 3.13.0)
66
+ rspec-support (3.13.6)
67
+ rubocop (1.81.1)
68
+ json (~> 2.3)
69
+ language_server-protocol (~> 3.17.0.2)
70
+ lint_roller (~> 1.1.0)
71
+ parallel (~> 1.10)
72
+ parser (>= 3.3.0.2)
73
+ rainbow (>= 2.2.2, < 4.0)
74
+ regexp_parser (>= 2.9.3, < 3.0)
75
+ rubocop-ast (>= 1.47.1, < 2.0)
76
+ ruby-progressbar (~> 1.7)
77
+ unicode-display_width (>= 2.4.0, < 4.0)
78
+ rubocop-ast (1.47.1)
79
+ parser (>= 3.3.7.2)
80
+ prism (~> 1.4)
81
+ ruby-progressbar (1.13.0)
82
+ simplecov (0.22.0)
83
+ docile (~> 1.1)
84
+ simplecov-html (~> 0.11)
85
+ simplecov_json_formatter (~> 0.1)
86
+ simplecov-html (0.13.2)
87
+ simplecov_json_formatter (0.1.4)
88
+ unicode-display_width (3.2.0)
89
+ unicode-emoji (~> 4.1)
90
+ unicode-emoji (4.1.0)
91
+ uri (1.0.4)
92
+ vcr (6.3.1)
93
+ base64
94
+ webmock (3.25.1)
95
+ addressable (>= 2.8.0)
96
+ crack (>= 0.3.2)
97
+ hashdiff (>= 0.4.0, < 2.0.0)
98
+ yard (0.9.37)
99
+
100
+ PLATFORMS
101
+ arm64-darwin-24
102
+ ruby
103
+
104
+ DEPENDENCIES
105
+ polar-ruby!
106
+ rake (~> 13.0)
107
+ rspec (~> 3.0)
108
+ rubocop (~> 1.21)
109
+ simplecov (~> 0.21)
110
+ vcr (~> 6.0)
111
+ webmock (~> 3.0)
112
+ yard (~> 0.9)
113
+
114
+ BUNDLED WITH
115
+ 2.6.7
data/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Polar Ruby SDK
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ This project is not an official SDK from Polar.sh and is not affiliated with or endorsed by Polar.sh. It is a community-driven. See the README for details.