belt-pay 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 673206d18a5fad12255912de6c7833796e147e88e99ce32d70ba48ef727ff9e4
4
+ data.tar.gz: d5b2171c33ef68447b501183287ab53d209621e859e40a16e2721d0af60a74a2
5
+ SHA512:
6
+ metadata.gz: 475ef6bdadb87b2712145d0e69f3f74c58a7fa07f7a6d2f293e0a34aa19f6a340794feb1d74e76c7d8dd584a1576650c4572670a443b640f7336e2a4e1f16a6a
7
+ data.tar.gz: e03f179b799302a256b741ff882defab8064909ecef72c25f5ec3043e1c26d76fe2ffde9ae7c1014a6ad76c5d1b25dfe385d9df7ec860404e3a74f6d3c73f87a
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.0.1 — 2026-09-01
4
+
5
+ - Initial release
6
+ - Stripe provider with customer provisioning, payment methods, checkout sessions
7
+ - Subscription management (create, cancel, billing portal)
8
+ - Transaction model for audit logging (DynamoDB)
9
+ - Billable concern for customer models
10
+ - Webhook handler with signature verification
11
+ - Generator: `belt g pay` (Terraform module, webhook Lambda, config, schema)
12
+ - Getting started guide
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stowzilla
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.
data/README.md ADDED
@@ -0,0 +1,271 @@
1
+ # belt-pay
2
+
3
+ Payments and subscriptions for [Belt](https://github.com/stowzilla/belt) applications via Stripe.
4
+
5
+ > **New to Belt Pay?** Check out the [Getting Started Guide](docs/getting-started.md) for a complete walkthrough.
6
+
7
+ ## Installation
8
+
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem 'belt-pay'
13
+ ```
14
+
15
+ Then:
16
+
17
+ ```bash
18
+ bundle install
19
+ belt generate pay
20
+ ```
21
+
22
+ ## What You Get
23
+
24
+ ### From the gem (no generation needed)
25
+
26
+ ```ruby
27
+ # Include in your Customer/User model
28
+ class Customer < ActiveItem::Base
29
+ include Belt::Pay::Billable
30
+ end
31
+
32
+ # Ensure a Stripe customer exists
33
+ customer.ensure_pay_customer!
34
+
35
+ # Create a checkout session (one-time or subscription)
36
+ Belt::Pay.create_checkout(customer,
37
+ line_items: [{ price: 'price_xxx', quantity: 1 }],
38
+ mode: 'subscription',
39
+ success_url: 'https://app.example.com/success?session_id={CHECKOUT_SESSION_ID}',
40
+ cancel_url: 'https://app.example.com/cancel')
41
+
42
+ # Subscribe directly (when payment method is already attached)
43
+ customer.subscribe!(price_id: 'price_xxx')
44
+
45
+ # Check subscription status
46
+ customer.active_subscription? # => true
47
+
48
+ # Customer self-service billing portal
49
+ customer.billing_portal_url(return_url: 'https://app.example.com/settings')
50
+ # => { url: "https://billing.stripe.com/p/session/..." }
51
+
52
+ # Collect payment method (returns client_secret for Stripe Elements)
53
+ result = customer.create_setup_intent
54
+ result.client_secret # => "seti_xxx_secret_yyy"
55
+
56
+ # Attach a payment method
57
+ customer.attach_payment_method('pm_xxx')
58
+
59
+ # View payment details
60
+ customer.payment_method_details
61
+ # => { last4: "4242", brand: "visa", exp_month: 12, exp_year: 2027 }
62
+
63
+ # Transaction history
64
+ customer.transactions
65
+ # => [#<Belt::Pay::Transaction type="subscription" status="completed" ...>]
66
+ ```
67
+
68
+ ### From the generator (`belt g pay`)
69
+
70
+ - **Terraform module** — Secrets Manager for Stripe keys, IAM policies
71
+ - **Lambda entry point** — Dedicated webhook Lambda for Stripe events
72
+ - **Lambda config** — `config/lambda/pay_webhooks.yml`
73
+ - **Schema update** — DynamoDB table for transaction audit log
74
+ - **Route injection** — Adds `/pay/webhooks` endpoint
75
+
76
+ ## Configuration
77
+
78
+ ### Environment Variables
79
+
80
+ | Variable | Purpose | Default |
81
+ |----------|---------|---------|
82
+ | `BELT_PAY_SECRET_NAME` | Secrets Manager secret name for Stripe keys | — |
83
+ | `BELT_PAY_WEBHOOK_SECRET_NAME` | Secret name for webhook signing (falls back to BELT_PAY_SECRET_NAME) | — |
84
+ | `BELT_PAY_MODE` | `test` or `live` | `test` |
85
+ | `APP_NAME` | App name for table naming | — |
86
+ | `ENVIRONMENT` | Environment for table naming | — |
87
+
88
+ ### Programmatic Configuration
89
+
90
+ ```ruby
91
+ Belt::Pay.configure do |config|
92
+ config.provider = :stripe # Only :stripe for now
93
+ config.secret_name = 'myapp-prod-stripe' # Secrets Manager secret
94
+ config.table_name_prefix = 'myapp-prod' # DynamoDB table prefix
95
+ end
96
+ ```
97
+
98
+ ### Secrets Manager Format
99
+
100
+ The Stripe secret should contain:
101
+
102
+ ```json
103
+ {
104
+ "stripe_secret_key": "sk_live_...",
105
+ "stripe_webhook_secret": "whsec_..."
106
+ }
107
+ ```
108
+
109
+ ## Common Patterns
110
+
111
+ ### Annual Subscription (Feature Gating)
112
+
113
+ ```ruby
114
+ # In your controller
115
+ class SubscriptionsController < BeltController::Base
116
+ def create
117
+ price_id = ENV['STRIPE_ANNUAL_PRICE_ID'] # Created in Stripe Dashboard
118
+ result = current_customer.subscribe!(price_id: price_id, metadata: { plan: 'pro' })
119
+ success_response(subscription_id: result[:subscription_id], status: result[:status])
120
+ end
121
+
122
+ def status
123
+ success_response(active: current_customer.active_subscription?)
124
+ end
125
+
126
+ def cancel
127
+ current_customer.cancel_subscription! # Cancels at period end
128
+ success_response(message: 'Subscription will cancel at end of billing period')
129
+ end
130
+
131
+ def portal
132
+ result = current_customer.billing_portal_url(return_url: "#{ENV['FRONTEND_URL']}/settings")
133
+ success_response(url: result[:url])
134
+ end
135
+ end
136
+ ```
137
+
138
+ ### One-Time Payment (Product Purchase)
139
+
140
+ ```ruby
141
+ class CheckoutController < BeltController::Base
142
+ def create
143
+ product = Product.find(params['product_id'])
144
+
145
+ result = Belt::Pay.create_checkout(current_customer,
146
+ line_items: [{
147
+ price_data: {
148
+ currency: 'usd',
149
+ product_data: { name: product.name },
150
+ unit_amount: product.price_cents
151
+ },
152
+ quantity: 1
153
+ }],
154
+ mode: 'payment',
155
+ success_url: "#{ENV['FRONTEND_URL']}/checkout/success?session_id={CHECKOUT_SESSION_ID}",
156
+ cancel_url: "#{ENV['FRONTEND_URL']}/products")
157
+
158
+ success_response(checkout_url: result[:url])
159
+ end
160
+ end
161
+ ```
162
+
163
+ ### Checking Subscription Access (Middleware Pattern)
164
+
165
+ ```ruby
166
+ class ProController < BeltController::Base
167
+ before_action :require_subscription!
168
+
169
+ private
170
+
171
+ def require_subscription!
172
+ unless current_customer.active_subscription?
173
+ error_response('Pro subscription required', 403)
174
+ end
175
+ end
176
+ end
177
+ ```
178
+
179
+ ## Webhook Events
180
+
181
+ The gem automatically handles these Stripe webhook events:
182
+
183
+ | Event | Behavior |
184
+ |-------|----------|
185
+ | `checkout.session.completed` | Marks pending transaction as completed |
186
+ | `checkout.session.expired` | Marks pending transaction as failed |
187
+ | `invoice.paid` | Records subscription renewal transaction |
188
+ | `invoice.payment_failed` | Logs payment failure |
189
+ | `customer.subscription.deleted` | Logs subscription cancellation |
190
+ | `customer.subscription.updated` | Logs subscription status changes |
191
+
192
+ ### Customizing Webhook Behavior
193
+
194
+ Override the webhook controller to add custom logic:
195
+
196
+ ```bash
197
+ belt g pay --controllers
198
+ ```
199
+
200
+ This generates a controller in your app that inherits from the gem's default. Override individual handler methods as needed.
201
+
202
+ ## Transaction Model
203
+
204
+ `Belt::Pay::Transaction` lives inside the gem and records all payment activity:
205
+
206
+ ```ruby
207
+ customer.transactions.each do |txn|
208
+ puts "#{txn.type}: #{txn.amount_cents} #{txn.currency} — #{txn.status}"
209
+ end
210
+ # subscription: 9900 usd — completed
211
+ # subscription_renewal: 9900 usd — completed
212
+ ```
213
+
214
+ ### Fields
215
+
216
+ | Field | Description |
217
+ |-------|-------------|
218
+ | `id` | UUID primary key |
219
+ | `customer_id` | Your app's customer/user ID |
220
+ | `provider` | Payment provider (`stripe`) |
221
+ | `provider_session_id` | Stripe checkout session ID |
222
+ | `provider_subscription_id` | Stripe subscription ID |
223
+ | `type` | `checkout`, `subscription`, `subscription_renewal`, `refund` |
224
+ | `status` | `pending`, `completed`, `failed`, `refunded`, `canceled` |
225
+ | `amount_cents` | Amount in cents |
226
+ | `currency` | ISO currency code |
227
+ | `metadata` | Free-form JSON metadata |
228
+ | `created_at` | ISO 8601 timestamp |
229
+
230
+ ## Terraform Module
231
+
232
+ After running `belt g pay`, add the module to your environment's `main.tf`:
233
+
234
+ ```hcl
235
+ module "pay" {
236
+ source = "../modules/pay"
237
+ app_name = var.app_name
238
+ environment = var.environment
239
+
240
+ # Set these or update secrets manually after apply
241
+ # stripe_secret_key = var.stripe_secret_key
242
+ # stripe_webhook_secret = var.stripe_webhook_secret
243
+ }
244
+ ```
245
+
246
+ ## Stripe Dashboard Setup
247
+
248
+ After deploying, configure your Stripe webhook endpoint:
249
+
250
+ 1. Go to Stripe Dashboard → Developers → Webhooks
251
+ 2. Add endpoint: `https://<your-api-domain>/pay/webhooks`
252
+ 3. Select events:
253
+ - `checkout.session.completed`
254
+ - `checkout.session.expired`
255
+ - `invoice.paid`
256
+ - `invoice.payment_failed`
257
+ - `customer.subscription.deleted`
258
+ - `customer.subscription.updated`
259
+ 4. Copy the signing secret into your Secrets Manager secret
260
+
261
+ ## Removing
262
+
263
+ ```bash
264
+ belt destroy pay
265
+ ```
266
+
267
+ Then remove the module reference from your environment `main.tf` files.
268
+
269
+ ## License
270
+
271
+ MIT
@@ -0,0 +1,352 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'erb'
5
+
6
+ module Belt
7
+ module Generators
8
+ class PayGenerator
9
+ TEMPLATE_DIR = File.expand_path('../pay/templates', __dir__)
10
+
11
+ def self.description
12
+ 'Install payments and subscriptions (Stripe)'
13
+ end
14
+
15
+ def self.run(args)
16
+ if args.include?('--help') || args.include?('-h')
17
+ print_help
18
+ return
19
+ end
20
+
21
+ new(args).generate
22
+ end
23
+
24
+ def self.destroy(args)
25
+ new(args).destroy
26
+ end
27
+
28
+ def self.print_help
29
+ puts <<~HELP
30
+ Install payment and subscription infrastructure for your Belt app.
31
+
32
+ Usage: belt generate pay [options]
33
+
34
+ Options:
35
+ --controllers Generate controller overrides (to customize webhook behavior)
36
+ --force Overwrite existing files
37
+
38
+ What gets created:
39
+ infrastructure/modules/pay/ Terraform module (Secrets Manager, IAM)
40
+ config/lambda/pay_webhooks.yml Lambda configuration (timeout, memory, env)
41
+ lambda/pay_webhooks.rb Lambda entry point for Stripe webhooks
42
+ infrastructure/schema.tf.rb Updated with pay_transactions table
43
+
44
+ What stays in the gem (no generation needed):
45
+ Belt::Pay::Transaction Transaction audit log model
46
+ Belt::Pay::Billable Concern for your Customer model
47
+ Belt::Pay.create_checkout(...) Create checkout sessions
48
+ Belt::Pay.subscribe(...) Manage subscriptions
49
+ Belt::Pay.billing_portal(...) Customer self-service portal
50
+ Belt::Pay::Controllers::WebhooksController Default webhook handler
51
+
52
+ To override the webhook controller:
53
+ belt g pay --controllers
54
+
55
+ After generation:
56
+ 1. Add module reference to your environment's main.tf
57
+ 2. Create Stripe keys in Secrets Manager (or via Terraform)
58
+ 3. Include Belt::Pay::Billable in your Customer/User model
59
+ 4. Deploy: belt apply <env>
60
+ 5. Configure Stripe webhook URL: https://<api-domain>/pay/webhooks
61
+
62
+ Examples:
63
+ belt g pay # Infrastructure only (use gem defaults)
64
+ belt g pay --controllers # Also generate controller overrides
65
+ belt d pay
66
+ HELP
67
+ end
68
+
69
+ def initialize(args)
70
+ @force = args.include?('--force')
71
+ @with_controllers = args.include?('--controllers')
72
+ @app_name = detect_namespace
73
+ end
74
+
75
+ def generate
76
+ generate_terraform_module
77
+ generate_lambda_config
78
+ generate_lambda_entry_point
79
+ generate_controllers if @with_controllers
80
+ inject_schema
81
+ inject_routes
82
+ print_success
83
+ end
84
+
85
+ def destroy
86
+ remove_terraform_module
87
+ remove_lambda_config
88
+ remove_lambda_entry_point
89
+ remove_controllers
90
+ remove_schema
91
+ remove_routes
92
+ puts "\n✓ Pay removed!"
93
+ puts " Don't forget to remove the module reference from your environment main.tf files."
94
+ end
95
+
96
+ private
97
+
98
+ def detect_namespace
99
+ routes_file = find_routes_file_path
100
+ if routes_file && File.exist?(routes_file)
101
+ match = File.read(routes_file).match(/namespace :(\w+)/)
102
+ return match[1] if match
103
+ end
104
+ File.basename(Dir.pwd)
105
+ end
106
+
107
+ def find_routes_file_path
108
+ candidates = ['config/routes.tf.rb', 'infrastructure/routes.tf.rb']
109
+ candidates.find { |f| File.exist?(f) }
110
+ end
111
+
112
+ # ---- Generate ----
113
+
114
+ def generate_terraform_module
115
+ module_dir = 'infrastructure/modules/pay'
116
+
117
+ if Dir.exist?(module_dir) && !@force
118
+ puts " skip #{module_dir}/ (already exists, use --force to overwrite)"
119
+ return
120
+ end
121
+
122
+ FileUtils.mkdir_p(module_dir)
123
+
124
+ write_template('terraform/main.tf.erb', "#{module_dir}/main.tf")
125
+ write_template('terraform/variables.tf.erb', "#{module_dir}/variables.tf")
126
+ write_template('terraform/outputs.tf.erb', "#{module_dir}/outputs.tf")
127
+
128
+ puts " create #{module_dir}/main.tf"
129
+ puts " create #{module_dir}/variables.tf"
130
+ puts " create #{module_dir}/outputs.tf"
131
+ end
132
+
133
+ def generate_lambda_config
134
+ config_dir = 'config/lambda'
135
+ dest = "#{config_dir}/pay_webhooks.yml"
136
+
137
+ if File.exist?(dest) && !@force
138
+ puts " skip #{dest} (already exists)"
139
+ return
140
+ end
141
+
142
+ FileUtils.mkdir_p(config_dir)
143
+ write_template('config/pay_webhooks.yml.erb', dest)
144
+ puts " create #{dest}"
145
+ end
146
+
147
+ def generate_lambda_entry_point
148
+ dest = 'lambda/pay_webhooks.rb'
149
+
150
+ if File.exist?(dest) && !@force
151
+ puts " skip #{dest} (already exists)"
152
+ return
153
+ end
154
+
155
+ write_template('lambda/pay_webhooks.rb.erb', dest)
156
+ puts " create #{dest}"
157
+ end
158
+
159
+ def generate_controllers
160
+ controller_dir = "lambda/controllers/#{@app_name}"
161
+ FileUtils.mkdir_p(controller_dir)
162
+
163
+ dest = "#{controller_dir}/pay_webhooks_controller.rb"
164
+ if File.exist?(dest) && !@force
165
+ puts " skip #{dest} (already exists)"
166
+ else
167
+ write_template('controllers/pay_webhooks_controller.rb.erb', dest)
168
+ puts " create #{dest}"
169
+ end
170
+ end
171
+
172
+ def inject_schema
173
+ schema_file = find_schema_file_path
174
+ return unless schema_file && File.exist?(schema_file)
175
+
176
+ content = File.read(schema_file)
177
+ return if content.include?('pay_transactions') || content.include?('pay-transactions')
178
+
179
+ # Add transactions table to schema
180
+ schema_block = <<~SCHEMA
181
+
182
+ model :pay_transaction do
183
+ partition_key :id, :string
184
+ global_secondary_index :CustomerIndex, partition_key: :customer_id
185
+ global_secondary_index :ProviderSessionIndex, partition_key: :provider_session_id
186
+ end
187
+ SCHEMA
188
+
189
+ # Insert before the closing `end` of the schema.define block
190
+ if content.match?(/^end\s*\z/m)
191
+ content.sub!(/^end\s*\z/m, "#{schema_block}end\n")
192
+ else
193
+ content << "\n#{schema_block}"
194
+ end
195
+
196
+ File.write(schema_file, content)
197
+ puts " update #{schema_file} (added pay_transactions table)"
198
+ end
199
+
200
+ def inject_routes
201
+ routes_file = find_routes_file_path
202
+ return unless routes_file && File.exist?(routes_file)
203
+
204
+ content = File.read(routes_file)
205
+ return if content.include?('pay_webhooks') || content.include?('pay/webhooks')
206
+
207
+ # Add webhook route to the namespace
208
+ namespace_pattern = /^(\s*)namespace :#{Regexp.escape(@app_name)}\b[^\n]*do\s*\n(.*?)^\1end/m
209
+ if content.match?(namespace_pattern)
210
+ webhook_route = " post \"pay/webhooks\", controller: :pay_webhooks, action: :webhook, auth: :none"
211
+ content.sub!(namespace_pattern) do |match|
212
+ indent = ::Regexp.last_match(1)
213
+ match.sub(/^(#{indent})end\z/m, "#{webhook_route}\n#{indent}end")
214
+ end
215
+ end
216
+
217
+ File.write(routes_file, content)
218
+ puts " update #{routes_file} (added pay webhook route)"
219
+ end
220
+
221
+ # ---- Destroy ----
222
+
223
+ def remove_terraform_module
224
+ module_dir = 'infrastructure/modules/pay'
225
+ if Dir.exist?(module_dir)
226
+ FileUtils.rm_rf(module_dir)
227
+ puts " remove #{module_dir}/"
228
+ end
229
+ end
230
+
231
+ def remove_lambda_config
232
+ path = 'config/lambda/pay_webhooks.yml'
233
+ if File.exist?(path)
234
+ File.delete(path)
235
+ puts " remove #{path}"
236
+ end
237
+ end
238
+
239
+ def remove_lambda_entry_point
240
+ path = 'lambda/pay_webhooks.rb'
241
+ if File.exist?(path)
242
+ File.delete(path)
243
+ puts " remove #{path}"
244
+ end
245
+ end
246
+
247
+ def remove_controllers
248
+ path = "lambda/controllers/#{@app_name}/pay_webhooks_controller.rb"
249
+ if File.exist?(path)
250
+ File.delete(path)
251
+ puts " remove #{path}"
252
+ end
253
+ end
254
+
255
+ def remove_schema
256
+ schema_file = find_schema_file_path
257
+ return unless schema_file && File.exist?(schema_file)
258
+
259
+ content = File.read(schema_file)
260
+ original = content.dup
261
+
262
+ content.gsub!(/\n\s*model :pay_transaction do.*?end\n/m, '')
263
+
264
+ if content != original
265
+ File.write(schema_file, content)
266
+ puts " update #{schema_file} (removed pay_transactions table)"
267
+ end
268
+ end
269
+
270
+ def remove_routes
271
+ routes_file = find_routes_file_path
272
+ return unless routes_file && File.exist?(routes_file)
273
+
274
+ content = File.read(routes_file)
275
+ original = content.dup
276
+
277
+ content.gsub!(/^\s*post "pay\/webhooks".*\n/, '')
278
+
279
+ if content != original
280
+ File.write(routes_file, content)
281
+ puts " update #{routes_file}"
282
+ end
283
+ end
284
+
285
+ def find_schema_file_path
286
+ candidates = ['infrastructure/schema.tf.rb', 'config/schema.tf.rb']
287
+ candidates.find { |f| File.exist?(f) }
288
+ end
289
+
290
+ def write_template(template_name, dest_path)
291
+ template_path = File.join(TEMPLATE_DIR, template_name)
292
+ FileUtils.mkdir_p(File.dirname(dest_path))
293
+ content = ERB.new(File.read(template_path), trim_mode: '-').result(binding)
294
+ File.write(dest_path, content)
295
+ end
296
+
297
+ def print_success
298
+ puts <<~SUCCESS
299
+
300
+ ✓ Payments installed!
301
+
302
+ Next steps:
303
+ 1. Add the module to your environment's main.tf:
304
+
305
+ module "pay" {
306
+ source = "../modules/pay"
307
+ app_name = var.app_name
308
+ environment = var.environment
309
+ }
310
+
311
+ 2. Include Belt::Pay::Billable in your Customer/User model:
312
+
313
+ class Customer < ActiveItem::Base
314
+ include Belt::Pay::Billable
315
+ # ...
316
+ end
317
+
318
+ 3. Create your Stripe API keys in Secrets Manager:
319
+ Secret name: <app_name>-<env>-stripe
320
+ Keys: stripe_secret_key, stripe_webhook_secret
321
+
322
+ 4. Deploy:
323
+ belt apply <env>
324
+
325
+ 5. Configure Stripe webhook endpoint:
326
+ URL: https://<your-api-domain>/pay/webhooks
327
+ Events: checkout.session.completed, checkout.session.expired,
328
+ invoice.paid, invoice.payment_failed,
329
+ customer.subscription.deleted, customer.subscription.updated
330
+
331
+ Quick Usage:
332
+ # Create a checkout session
333
+ Belt::Pay.create_checkout(customer,
334
+ line_items: [{ price: 'price_xxx', quantity: 1 }],
335
+ mode: 'subscription',
336
+ success_url: 'https://app.example.com/success',
337
+ cancel_url: 'https://app.example.com/cancel')
338
+
339
+ # Subscribe directly (if payment method already attached)
340
+ customer.subscribe!(price_id: 'price_xxx')
341
+
342
+ # Check subscription status
343
+ customer.active_subscription? # => true
344
+
345
+ # Customer self-service
346
+ customer.billing_portal_url(return_url: 'https://app.example.com/settings')
347
+
348
+ SUCCESS
349
+ end
350
+ end
351
+ end
352
+ end