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.
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+
5
+ module Belt
6
+ module Pay
7
+ # Mix into your Customer/User model to get payment capabilities.
8
+ #
9
+ # @example
10
+ # class Customer < ActiveItem::Base
11
+ # include Belt::Pay::Billable
12
+ # end
13
+ #
14
+ # customer.ensure_pay_customer!
15
+ # customer.subscribe!(price_id: 'price_xxx')
16
+ # customer.active_subscription?
17
+ # customer.cancel_subscription!
18
+ # customer.transactions
19
+ #
20
+ module Billable
21
+ extend ActiveSupport::Concern
22
+
23
+ included do
24
+ attr_accessor :pay_customer_id, :pay_subscription_id, :pay_payment_method_id
25
+ end
26
+
27
+ # Ensure this customer has a provider customer account.
28
+ # Creates one if it doesn't exist. Idempotent.
29
+ # @return [String] The provider customer ID
30
+ def ensure_pay_customer!
31
+ Belt::Pay.ensure_customer(self)
32
+ end
33
+
34
+ # Attach a payment method and set as default.
35
+ # @param payment_method_id [String] Provider payment method token (e.g., 'pm_xxx')
36
+ # @return [PaymentMethodAttacher::Result]
37
+ def attach_payment_method(payment_method_id)
38
+ Belt::Pay.attach_payment_method(self, payment_method_id)
39
+ end
40
+
41
+ # Create a setup intent for collecting payment details.
42
+ # @return [SetupIntentCreator::Result] { client_secret:, pay_customer_id: }
43
+ def create_setup_intent
44
+ Belt::Pay.create_setup_intent(self)
45
+ end
46
+
47
+ # Subscribe to a plan.
48
+ # @param price_id [String] Provider price ID
49
+ # @param metadata [Hash] Additional metadata to store on the subscription
50
+ # @return [Hash] { subscription_id:, status: }
51
+ def subscribe!(price_id:, metadata: {})
52
+ result = Belt::Pay.subscribe(self, price_id: price_id, metadata: metadata)
53
+ self.pay_subscription_id = result[:subscription_id]
54
+ save(validate: false)
55
+ result
56
+ end
57
+
58
+ # Check if customer has an active subscription.
59
+ # @return [Boolean]
60
+ def active_subscription?
61
+ return false unless pay_subscription_id
62
+
63
+ Belt::Pay::Subscription.active?(self)
64
+ end
65
+
66
+ # Cancel the current subscription.
67
+ # @param immediately [Boolean] Cancel now (true) or at period end (false, default)
68
+ def cancel_subscription!(immediately: false)
69
+ Belt::Pay.cancel_subscription(self, immediately: immediately)
70
+ self.pay_subscription_id = nil if immediately
71
+ save(validate: false) if immediately
72
+ end
73
+
74
+ # Generate a billing portal URL for self-service management.
75
+ # @param return_url [String] URL to redirect back to
76
+ # @return [Hash] { url: }
77
+ def billing_portal_url(return_url:)
78
+ Belt::Pay.billing_portal(self, return_url: return_url)
79
+ end
80
+
81
+ # Get payment method details (last4, brand, expiry).
82
+ # @return [Hash, nil] { last4:, brand:, exp_month:, exp_year: }
83
+ def payment_method_details
84
+ Belt::Pay.provider.payment_method_details(self)
85
+ end
86
+
87
+ # Get all transactions for this customer.
88
+ # @return [Array<Belt::Pay::Transaction>]
89
+ def transactions
90
+ Belt::Pay::Transaction.for_customer(id)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Belt
4
+ module Pay
5
+ # Creates checkout sessions for one-time or subscription payments.
6
+ module Checkout
7
+ class << self
8
+ # Create a checkout session.
9
+ # @param customer [Object] App model
10
+ # @param line_items [Array<Hash>] Items to charge for
11
+ # @param mode [String] 'payment' or 'subscription'
12
+ # @param success_url [String]
13
+ # @param cancel_url [String]
14
+ # @param metadata [Hash]
15
+ # @return [Hash] { url:, session_id: }
16
+ def create(customer, line_items:, mode: 'payment', success_url:, cancel_url:, metadata: {}, **options)
17
+ # Ensure provider customer exists
18
+ CustomerProvisioner.new(customer).call
19
+
20
+ result = Belt::Pay.provider.create_checkout_session(
21
+ customer.pay_customer_id,
22
+ line_items: line_items,
23
+ mode: mode,
24
+ success_url: success_url,
25
+ cancel_url: cancel_url,
26
+ metadata: metadata.merge(app_customer_id: customer.id),
27
+ **options
28
+ )
29
+
30
+ # Record pending transaction
31
+ Transaction.create!(
32
+ customer_id: customer.id,
33
+ provider: Belt::Pay.configuration.provider.to_s,
34
+ provider_session_id: result[:session_id],
35
+ type: mode == 'subscription' ? 'subscription' : 'checkout',
36
+ status: 'pending',
37
+ currency: 'usd',
38
+ description: "Checkout session created (#{mode})",
39
+ metadata: metadata
40
+ )
41
+
42
+ Belt::Pay.log(:info, 'Belt::Pay::Checkout: session created',
43
+ customer_id: customer.id, session_id: result[:session_id], mode: mode)
44
+
45
+ result
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Belt
4
+ module Pay
5
+ class Configuration
6
+ attr_accessor :provider, :secret_name, :webhook_secret_name,
7
+ :table_name_prefix, :logger, :test_mode
8
+
9
+ def initialize
10
+ @provider = :stripe
11
+ @secret_name = ENV['BELT_PAY_SECRET_NAME']
12
+ @webhook_secret_name = ENV['BELT_PAY_WEBHOOK_SECRET_NAME']
13
+ @table_name_prefix = "#{ENV['APP_NAME']}-#{ENV['ENVIRONMENT']}"
14
+ @logger = nil
15
+ @test_mode = ENV['BELT_PAY_MODE'] != 'live'
16
+ end
17
+
18
+ def transactions_table_name
19
+ "#{table_name_prefix}-pay-transactions"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Belt
4
+ module Pay
5
+ module Controllers
6
+ # Default webhook controller for Stripe events.
7
+ # Lives in the gem — override by generating a controller into your app:
8
+ # belt g pay --controllers
9
+ class WebhooksController < BeltController::Base
10
+ skip_before_action :authenticate!, only: [:webhook]
11
+
12
+ # POST /pay/webhooks
13
+ def webhook
14
+ payload = raw_body
15
+ signature = headers['Stripe-Signature'] || headers['stripe-signature']
16
+
17
+ unless signature
18
+ return error_response('Missing Stripe-Signature header', 400)
19
+ end
20
+
21
+ result = Belt::Pay::WebhookHandler.process(payload: payload, signature: signature)
22
+ success_response(result)
23
+ rescue ::Stripe::SignatureVerificationError => e
24
+ Belt::Pay.log(:warn, 'Belt::Pay webhook signature verification failed', error: e.message)
25
+ error_response('Invalid signature', 400)
26
+ rescue StandardError => e
27
+ Belt::Pay.log(:error, 'Belt::Pay webhook processing error', error: e.message)
28
+ error_response('Webhook processing error', 500)
29
+ end
30
+
31
+ private
32
+
33
+ # Get the raw request body for signature verification.
34
+ # Stripe requires the exact raw body — not parsed JSON.
35
+ def raw_body
36
+ event.dig('body') || ''
37
+ end
38
+
39
+ def headers
40
+ event.dig('headers') || {}
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Belt
4
+ module Pay
5
+ # Ensures a payment provider customer exists for a given app customer.
6
+ # Idempotent — safe to call multiple times.
7
+ class CustomerProvisioner
8
+ attr_reader :customer
9
+
10
+ def initialize(customer)
11
+ @customer = customer
12
+ end
13
+
14
+ # @return [String] The provider customer ID
15
+ def call
16
+ return customer.pay_customer_id if customer.pay_customer_id
17
+
18
+ # Check for concurrent write (another request may have created one)
19
+ fresh = customer.class.find(customer.id)
20
+ if fresh&.pay_customer_id
21
+ customer.pay_customer_id = fresh.pay_customer_id
22
+ Belt::Pay.log(:info, 'Belt::Pay::CustomerProvisioner: found existing (concurrent write)',
23
+ customer_id: customer.id, pay_customer_id: fresh.pay_customer_id)
24
+ return fresh.pay_customer_id
25
+ end
26
+
27
+ # Create via provider
28
+ pay_customer_id = Belt::Pay.provider.create_customer(customer)
29
+
30
+ customer.pay_customer_id = pay_customer_id
31
+ unless customer.save(validate: false)
32
+ raise Error, "Belt::Pay::CustomerProvisioner: failed to persist pay_customer_id=#{pay_customer_id}"
33
+ end
34
+
35
+ Belt::Pay.log(:info, 'Belt::Pay::CustomerProvisioner: created',
36
+ customer_id: customer.id, pay_customer_id: pay_customer_id)
37
+ pay_customer_id
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Belt
4
+ module Pay
5
+ # Attaches a payment method to a customer's provider account and sets it as default.
6
+ class PaymentMethodAttacher
7
+ Result = Struct.new(:pay_customer_id, :payment_method_id, keyword_init: true)
8
+
9
+ attr_reader :customer, :payment_method_id
10
+
11
+ def initialize(customer, payment_method_id)
12
+ @customer = customer
13
+ @payment_method_id = payment_method_id
14
+ end
15
+
16
+ # @return [Result]
17
+ def call
18
+ # Ensure provider customer exists
19
+ CustomerProvisioner.new(customer).call
20
+
21
+ # Attach via provider
22
+ Belt::Pay.provider.attach_payment_method(customer.pay_customer_id, payment_method_id)
23
+
24
+ # Persist the payment method ID
25
+ customer.pay_payment_method_id = payment_method_id
26
+ unless customer.save(validate: false)
27
+ raise Error, "Belt::Pay::PaymentMethodAttacher: failed to persist payment_method_id=#{payment_method_id}"
28
+ end
29
+
30
+ Belt::Pay.log(:info, 'Belt::Pay::PaymentMethodAttacher: completed',
31
+ customer_id: customer.id, pay_customer_id: customer.pay_customer_id,
32
+ payment_method_id: payment_method_id)
33
+
34
+ Result.new(pay_customer_id: customer.pay_customer_id, payment_method_id: payment_method_id)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stripe'
4
+
5
+ module Belt
6
+ module Pay
7
+ module Providers
8
+ # Stripe provider adapter.
9
+ # Handles API key management and provides the Stripe-specific implementations
10
+ # that the generic Belt::Pay API delegates to.
11
+ class Stripe
12
+ # Ensure the Stripe API key is set for the current request.
13
+ # Reads from Secrets Manager via the configured secret name.
14
+ def ensure_api_key!
15
+ ::Stripe.api_key ||= fetch_api_key
16
+ raise ConfigurationError, 'Stripe API key not configured' unless ::Stripe.api_key
17
+ end
18
+
19
+ # Get the webhook signing secret for signature verification.
20
+ # @return [String]
21
+ def webhook_signing_secret
22
+ @webhook_signing_secret ||= fetch_webhook_secret
23
+ end
24
+
25
+ # Create a Stripe customer for the given user.
26
+ # @param customer [Object] App model with #id, #email
27
+ # @return [String] Stripe customer ID (cus_xxx)
28
+ def create_customer(customer)
29
+ ensure_api_key!
30
+ stripe_customer = ::Stripe::Customer.create(
31
+ email: customer.respond_to?(:email) ? customer.email : nil,
32
+ metadata: { app_customer_id: customer.id }
33
+ )
34
+ stripe_customer.id
35
+ end
36
+
37
+ # Attach a payment method to a Stripe customer and set as default.
38
+ # @param pay_customer_id [String] Stripe customer ID
39
+ # @param payment_method_id [String] Payment method ID (pm_xxx)
40
+ def attach_payment_method(pay_customer_id, payment_method_id)
41
+ ensure_api_key!
42
+ ::Stripe::PaymentMethod.attach(payment_method_id, { customer: pay_customer_id })
43
+ ::Stripe::Customer.update(pay_customer_id, {
44
+ invoice_settings: { default_payment_method: payment_method_id }
45
+ })
46
+ rescue ::Stripe::InvalidRequestError => e
47
+ raise unless e.message.include?('already been attached')
48
+
49
+ # Idempotent — already attached, just set as default
50
+ ::Stripe::Customer.update(pay_customer_id, {
51
+ invoice_settings: { default_payment_method: payment_method_id }
52
+ })
53
+ end
54
+
55
+ # Create a SetupIntent for collecting payment details.
56
+ # @param pay_customer_id [String] Stripe customer ID
57
+ # @param customer_id [String] App customer ID (for metadata)
58
+ # @return [Hash] { client_secret:, setup_intent_id: }
59
+ def create_setup_intent(pay_customer_id, customer_id:)
60
+ ensure_api_key!
61
+ setup_intent = ::Stripe::SetupIntent.create({
62
+ customer: pay_customer_id,
63
+ payment_method_types: ['card'],
64
+ usage: 'off_session',
65
+ metadata: { app_customer_id: customer_id }
66
+ })
67
+ { client_secret: setup_intent.client_secret, setup_intent_id: setup_intent.id }
68
+ end
69
+
70
+ # Create a Checkout Session.
71
+ # @param pay_customer_id [String] Stripe customer ID
72
+ # @param options [Hash] line_items, mode, success_url, cancel_url, metadata
73
+ # @return [Hash] { url:, session_id: }
74
+ def create_checkout_session(pay_customer_id, **options)
75
+ ensure_api_key!
76
+ params = {
77
+ customer: pay_customer_id,
78
+ line_items: options[:line_items],
79
+ mode: options[:mode] || 'payment',
80
+ success_url: options[:success_url],
81
+ cancel_url: options[:cancel_url],
82
+ metadata: options[:metadata] || {}
83
+ }
84
+ # For subscription mode, allow trial periods
85
+ params[:subscription_data] = options[:subscription_data] if options[:subscription_data]
86
+
87
+ session = ::Stripe::Checkout::Session.create(params)
88
+ { url: session.url, session_id: session.id }
89
+ end
90
+
91
+ # Create a subscription directly (without Checkout).
92
+ # @param pay_customer_id [String] Stripe customer ID
93
+ # @param price_id [String] Stripe price ID
94
+ # @param metadata [Hash]
95
+ # @return [Hash] { subscription_id:, status:, current_period_end: }
96
+ def create_subscription(pay_customer_id, price_id:, metadata: {})
97
+ ensure_api_key!
98
+ subscription = ::Stripe::Subscription.create({
99
+ customer: pay_customer_id,
100
+ items: [{ price: price_id }],
101
+ metadata: metadata,
102
+ payment_behavior: 'default_incomplete',
103
+ expand: ['latest_invoice.payment_intent']
104
+ })
105
+ {
106
+ subscription_id: subscription.id,
107
+ status: subscription.status,
108
+ current_period_end: Time.at(subscription.current_period_end).utc.iso8601
109
+ }
110
+ end
111
+
112
+ # Cancel a subscription.
113
+ # @param subscription_id [String] Stripe subscription ID
114
+ # @param immediately [Boolean] Cancel now or at period end
115
+ def cancel_subscription(subscription_id, immediately: false)
116
+ ensure_api_key!
117
+ if immediately
118
+ ::Stripe::Subscription.cancel(subscription_id)
119
+ else
120
+ ::Stripe::Subscription.update(subscription_id, { cancel_at_period_end: true })
121
+ end
122
+ end
123
+
124
+ # Check if a subscription is active.
125
+ # @param subscription_id [String] Stripe subscription ID
126
+ # @return [Boolean]
127
+ def subscription_active?(subscription_id)
128
+ ensure_api_key!
129
+ sub = ::Stripe::Subscription.retrieve(subscription_id)
130
+ %w[active trialing].include?(sub.status)
131
+ rescue ::Stripe::InvalidRequestError
132
+ false
133
+ end
134
+
135
+ # Generate a billing portal session URL.
136
+ # @param pay_customer_id [String] Stripe customer ID
137
+ # @param return_url [String]
138
+ # @return [Hash] { url: }
139
+ def billing_portal(customer, return_url:)
140
+ ensure_api_key!
141
+ pay_customer_id = customer.pay_customer_id
142
+ raise Error, 'Customer has no payment account' unless pay_customer_id
143
+
144
+ session = ::Stripe::BillingPortal::Session.create({
145
+ customer: pay_customer_id,
146
+ return_url: return_url
147
+ })
148
+ { url: session.url }
149
+ end
150
+
151
+ # Get payment method details for a customer.
152
+ # @param customer [Object] App model with #pay_customer_id
153
+ # @return [Hash, nil] { last4:, brand:, exp_month:, exp_year: }
154
+ def payment_method_details(customer)
155
+ ensure_api_key!
156
+ return nil unless customer.pay_customer_id
157
+
158
+ stripe_customer = ::Stripe::Customer.retrieve(customer.pay_customer_id)
159
+ default_pm_id = stripe_customer.invoice_settings&.default_payment_method
160
+ return nil unless default_pm_id
161
+
162
+ pm = ::Stripe::PaymentMethod.retrieve(default_pm_id)
163
+ return nil unless pm&.card
164
+
165
+ { last4: pm.card.last4, brand: pm.card.brand,
166
+ exp_month: pm.card.exp_month, exp_year: pm.card.exp_year }
167
+ rescue ::Stripe::InvalidRequestError
168
+ nil
169
+ end
170
+
171
+ # Verify a webhook signature and parse the event.
172
+ # @param payload [String] Raw request body
173
+ # @param signature [String] Stripe-Signature header
174
+ # @return [Stripe::Event]
175
+ def verify_webhook(payload, signature)
176
+ ::Stripe::Webhook.construct_event(payload, signature, webhook_signing_secret)
177
+ end
178
+
179
+ private
180
+
181
+ def fetch_api_key
182
+ secret_name = Belt::Pay.configuration.secret_name
183
+ return nil unless secret_name
184
+
185
+ require_secrets_helper
186
+ SecretsHelper.get_secret(secret_name: secret_name, key: 'stripe_secret_key', required: true)
187
+ rescue StandardError => e
188
+ Belt::Pay.log(:error, 'Failed to fetch Stripe API key', error: e.message)
189
+ nil
190
+ end
191
+
192
+ def fetch_webhook_secret
193
+ secret_name = Belt::Pay.configuration.webhook_secret_name || Belt::Pay.configuration.secret_name
194
+ return nil unless secret_name
195
+
196
+ require_secrets_helper
197
+ SecretsHelper.get_secret(secret_name: secret_name, key: 'stripe_webhook_secret', required: true)
198
+ rescue StandardError => e
199
+ Belt::Pay.log(:error, 'Failed to fetch Stripe webhook secret', error: e.message)
200
+ nil
201
+ end
202
+
203
+ def require_secrets_helper
204
+ require 'belt/secrets_helper' if defined?(Belt::SecretsHelper)
205
+ rescue LoadError
206
+ # SecretsHelper not available — key must be set via Stripe.api_key directly
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Belt
4
+ module Pay
5
+ # Creates a SetupIntent for securely collecting payment details on the frontend.
6
+ class SetupIntentCreator
7
+ Result = Struct.new(:client_secret, :pay_customer_id, keyword_init: true)
8
+
9
+ attr_reader :customer
10
+
11
+ def initialize(customer)
12
+ @customer = customer
13
+ end
14
+
15
+ # @return [Result]
16
+ def call
17
+ # Ensure provider customer exists
18
+ pay_customer_id = CustomerProvisioner.new(customer).call
19
+
20
+ # Create setup intent via provider
21
+ result = Belt::Pay.provider.create_setup_intent(pay_customer_id, customer_id: customer.id)
22
+
23
+ Belt::Pay.log(:info, 'Belt::Pay::SetupIntentCreator: created',
24
+ customer_id: customer.id, pay_customer_id: pay_customer_id)
25
+
26
+ Result.new(client_secret: result[:client_secret], pay_customer_id: pay_customer_id)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Belt
4
+ module Pay
5
+ # Manages subscriptions — create, cancel, check status.
6
+ module Subscription
7
+ class << self
8
+ # Create a subscription for a customer.
9
+ # @param customer [Object] App model with Billable included
10
+ # @param price_id [String] Provider price/plan ID
11
+ # @param metadata [Hash]
12
+ # @return [Hash] { subscription_id:, status:, current_period_end: }
13
+ def create(customer, price_id:, metadata: {})
14
+ CustomerProvisioner.new(customer).call
15
+
16
+ result = Belt::Pay.provider.create_subscription(
17
+ customer.pay_customer_id,
18
+ price_id: price_id,
19
+ metadata: metadata.merge(app_customer_id: customer.id)
20
+ )
21
+
22
+ # Record the transaction
23
+ Transaction.record_subscription(
24
+ customer_id: customer.id,
25
+ subscription_id: result[:subscription_id],
26
+ amount_cents: 0, # Amount comes from invoice.paid webhook
27
+ metadata: metadata.merge(price_id: price_id)
28
+ )
29
+
30
+ Belt::Pay.log(:info, 'Belt::Pay::Subscription: created',
31
+ customer_id: customer.id, subscription_id: result[:subscription_id],
32
+ status: result[:status])
33
+
34
+ result
35
+ end
36
+
37
+ # Cancel a subscription.
38
+ # @param customer [Object] App model
39
+ # @param immediately [Boolean]
40
+ def cancel(customer, immediately: false)
41
+ subscription_id = customer.pay_subscription_id
42
+ raise Error, 'Customer has no active subscription' unless subscription_id
43
+
44
+ Belt::Pay.provider.cancel_subscription(subscription_id, immediately: immediately)
45
+
46
+ Belt::Pay.log(:info, 'Belt::Pay::Subscription: canceled',
47
+ customer_id: customer.id, subscription_id: subscription_id,
48
+ immediately: immediately)
49
+ end
50
+
51
+ # Check if a customer's subscription is active.
52
+ # @param customer [Object] App model
53
+ # @return [Boolean]
54
+ def active?(customer)
55
+ subscription_id = customer.pay_subscription_id
56
+ return false unless subscription_id
57
+
58
+ Belt::Pay.provider.subscription_active?(subscription_id)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,33 @@
1
+ # Lambda configuration for the pay_webhooks function.
2
+ # Handles incoming Stripe webhook events (checkout, invoice, subscription).
3
+ #
4
+ # Generated by: belt generate pay
5
+ #
6
+ # Keys:
7
+ # timeout Lambda timeout in seconds
8
+ # memory_size Lambda memory in MB
9
+ # env_vars Environment variables (static values or ref() for Terraform values)
10
+ #
11
+ # ref(name) references are resolved from lambda_env_refs in Terraform.
12
+
13
+ default: &default
14
+ timeout: 30
15
+ memory_size: 256
16
+ env_vars:
17
+ BELT_PAY_SECRET_NAME: ref(stripe_secret_name)
18
+ BELT_PAY_WEBHOOK_SECRET_NAME: ref(stripe_secret_name)
19
+ BELT_PAY_MODE: "live"
20
+
21
+ dev:
22
+ <<: *default
23
+ env_vars:
24
+ BELT_PAY_SECRET_NAME: ref(stripe_secret_name)
25
+ BELT_PAY_WEBHOOK_SECRET_NAME: ref(stripe_secret_name)
26
+ BELT_PAY_MODE: "test"
27
+
28
+ prod:
29
+ <<: *default
30
+ env_vars:
31
+ BELT_PAY_SECRET_NAME: ref(stripe_secret_name)
32
+ BELT_PAY_WEBHOOK_SECRET_NAME: ref(stripe_secret_name)
33
+ BELT_PAY_MODE: "live"
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generated by: belt generate pay --controllers
4
+ # Override this controller to customize webhook handling behavior.
5
+ # The gem's default controller handles standard Stripe events automatically.
6
+
7
+ require 'belt-pay'
8
+
9
+ <% ns = @app_name.split('_').map(&:capitalize).join %>
10
+ module <%= ns %>Controllers
11
+ class PayWebhooksController < Belt::Pay::Controllers::WebhooksController
12
+ # Override webhook to add custom behavior before/after processing
13
+ # def webhook
14
+ # # Custom pre-processing
15
+ # result = super
16
+ # # Custom post-processing
17
+ # result
18
+ # end
19
+
20
+ # Add custom event handlers by overriding the webhook handler:
21
+ #
22
+ # Belt::Pay::WebhookHandler.define_method(:handle_custom_event) do |event_data|
23
+ # # Your custom logic here
24
+ # end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Generated by: belt generate pay
4
+ # Lambda entry point for Stripe webhook events.
5
+ # Receives POST requests from Stripe when payment events occur.
6
+
7
+ require_relative 'config/environment'
8
+ require 'belt-pay'
9
+
10
+ include Belt::LambdaHandler
11
+
12
+ ROUTER = Belt::ActionRouter.new(
13
+ routes: [
14
+ { verb: 'POST', path: '/pay/webhooks', controller: 'pay_webhooks', action: 'webhook', auth: 'none' }
15
+ ],
16
+ namespace: 'pay_webhooks'
17
+ )
18
+
19
+ def execute(path:, body:, event:)
20
+ ROUTER.route(event: event, body: body)
21
+ end