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 +7 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE +21 -0
- data/README.md +271 -0
- data/lib/belt/generators/pay_generator.rb +352 -0
- data/lib/belt/pay/billable.rb +94 -0
- data/lib/belt/pay/checkout.rb +50 -0
- data/lib/belt/pay/configuration.rb +23 -0
- data/lib/belt/pay/controllers/webhooks_controller.rb +45 -0
- data/lib/belt/pay/customer_provisioner.rb +41 -0
- data/lib/belt/pay/payment_method_attacher.rb +38 -0
- data/lib/belt/pay/providers/stripe.rb +211 -0
- data/lib/belt/pay/setup_intent_creator.rb +30 -0
- data/lib/belt/pay/subscription.rb +63 -0
- data/lib/belt/pay/templates/config/pay_webhooks.yml.erb +33 -0
- data/lib/belt/pay/templates/controllers/pay_webhooks_controller.rb.erb +26 -0
- data/lib/belt/pay/templates/lambda/pay_webhooks.rb.erb +21 -0
- data/lib/belt/pay/templates/terraform/main.tf.erb +73 -0
- data/lib/belt/pay/templates/terraform/outputs.tf.erb +14 -0
- data/lib/belt/pay/templates/terraform/variables.tf.erb +23 -0
- data/lib/belt/pay/transaction.rb +103 -0
- data/lib/belt/pay/version.rb +7 -0
- data/lib/belt/pay/webhook_handler.rb +153 -0
- data/lib/belt/pay.rb +124 -0
- data/lib/belt-pay.rb +3 -0
- metadata +99 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Pay Module — Stripe Payments & Subscriptions
|
|
2
|
+
# Generated by: belt generate pay
|
|
3
|
+
#
|
|
4
|
+
# Provides:
|
|
5
|
+
# - Secrets Manager secret for Stripe API keys
|
|
6
|
+
# - IAM policy for Lambda to read secrets
|
|
7
|
+
# - DynamoDB table for transaction audit log
|
|
8
|
+
|
|
9
|
+
terraform {
|
|
10
|
+
required_providers {
|
|
11
|
+
aws = {
|
|
12
|
+
source = "hashicorp/aws"
|
|
13
|
+
version = "~> 5.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
locals {
|
|
19
|
+
common_tags = {
|
|
20
|
+
Module = "pay"
|
|
21
|
+
Environment = var.environment
|
|
22
|
+
ManagedBy = "Terraform"
|
|
23
|
+
}
|
|
24
|
+
secret_name = "${var.app_name}-${var.environment}-stripe"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# ============================================================================
|
|
28
|
+
# Secrets Manager — Stripe API Keys
|
|
29
|
+
# ============================================================================
|
|
30
|
+
|
|
31
|
+
resource "aws_secretsmanager_secret" "stripe" {
|
|
32
|
+
name = local.secret_name
|
|
33
|
+
description = "Stripe API keys for ${var.app_name} (${var.environment})"
|
|
34
|
+
|
|
35
|
+
tags = merge(local.common_tags, {
|
|
36
|
+
Name = local.secret_name
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Initial secret value — update manually or via CI after creation
|
|
41
|
+
resource "aws_secretsmanager_secret_version" "stripe" {
|
|
42
|
+
secret_id = aws_secretsmanager_secret.stripe.id
|
|
43
|
+
secret_string = jsonencode({
|
|
44
|
+
stripe_secret_key = var.stripe_secret_key
|
|
45
|
+
stripe_webhook_secret = var.stripe_webhook_secret
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
lifecycle {
|
|
49
|
+
ignore_changes = [secret_string]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# ============================================================================
|
|
54
|
+
# IAM Policy — Read Stripe Secrets
|
|
55
|
+
# ============================================================================
|
|
56
|
+
|
|
57
|
+
data "aws_iam_policy_document" "read_stripe_secrets" {
|
|
58
|
+
statement {
|
|
59
|
+
effect = "Allow"
|
|
60
|
+
actions = [
|
|
61
|
+
"secretsmanager:GetSecretValue"
|
|
62
|
+
]
|
|
63
|
+
resources = [aws_secretsmanager_secret.stripe.arn]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
resource "aws_iam_policy" "read_stripe_secrets" {
|
|
68
|
+
name = "${var.app_name}-${var.environment}-pay-secrets"
|
|
69
|
+
description = "Allow Lambda to read Stripe secrets"
|
|
70
|
+
policy = data.aws_iam_policy_document.read_stripe_secrets.json
|
|
71
|
+
|
|
72
|
+
tags = local.common_tags
|
|
73
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
output "stripe_secret_arn" {
|
|
2
|
+
description = "ARN of the Stripe secrets in Secrets Manager"
|
|
3
|
+
value = aws_secretsmanager_secret.stripe.arn
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
output "stripe_secret_name" {
|
|
7
|
+
description = "Name of the Stripe secret (for Lambda env vars)"
|
|
8
|
+
value = local.secret_name
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
output "read_secrets_policy_arn" {
|
|
12
|
+
description = "ARN of the IAM policy for reading Stripe secrets"
|
|
13
|
+
value = aws_iam_policy.read_stripe_secrets.arn
|
|
14
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
variable "app_name" {
|
|
2
|
+
description = "Application name"
|
|
3
|
+
type = string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
variable "environment" {
|
|
7
|
+
description = "Environment name (dev, staging, prod)"
|
|
8
|
+
type = string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
variable "stripe_secret_key" {
|
|
12
|
+
description = "Stripe secret API key (initial value — update via console/CI)"
|
|
13
|
+
type = string
|
|
14
|
+
default = "sk_test_PLACEHOLDER"
|
|
15
|
+
sensitive = true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
variable "stripe_webhook_secret" {
|
|
19
|
+
description = "Stripe webhook signing secret (initial value — update via console/CI)"
|
|
20
|
+
type = string
|
|
21
|
+
default = "whsec_PLACEHOLDER"
|
|
22
|
+
sensitive = true
|
|
23
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'activeitem'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
|
|
6
|
+
module Belt
|
|
7
|
+
module Pay
|
|
8
|
+
# Internal Transaction model — lives in the gem, not generated into apps.
|
|
9
|
+
# Records all payment events (checkouts, subscriptions, refunds) for audit logging.
|
|
10
|
+
#
|
|
11
|
+
# Override by creating your own Belt::Pay::Transaction class before requiring belt-pay,
|
|
12
|
+
# or monkey-patch individual methods as needed.
|
|
13
|
+
class Transaction < ActiveItem::Base
|
|
14
|
+
self.table_name = -> { Belt::Pay.configuration.transactions_table_name }
|
|
15
|
+
self.primary_key = :id
|
|
16
|
+
|
|
17
|
+
attr_accessor :id, :customer_id, :provider, :provider_customer_id,
|
|
18
|
+
:provider_session_id, :provider_subscription_id, :provider_payment_intent_id,
|
|
19
|
+
:type, :status, :amount_cents, :currency, :description,
|
|
20
|
+
:metadata, :created_at, :updated_at
|
|
21
|
+
|
|
22
|
+
validates :customer_id, presence: true
|
|
23
|
+
validates :type, presence: true
|
|
24
|
+
validates :status, presence: true
|
|
25
|
+
|
|
26
|
+
before_create { self.id ||= SecureRandom.uuid }
|
|
27
|
+
before_create { self.created_at ||= Time.now.utc.iso8601 }
|
|
28
|
+
before_save { self.updated_at = Time.now.utc.iso8601 }
|
|
29
|
+
|
|
30
|
+
# Transaction types
|
|
31
|
+
TYPES = %w[checkout subscription subscription_renewal refund payment_method_update].freeze
|
|
32
|
+
|
|
33
|
+
# Transaction statuses
|
|
34
|
+
STATUSES = %w[pending completed failed refunded canceled].freeze
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
# Record a checkout completion
|
|
38
|
+
def record_checkout(customer_id:, session_id:, amount_cents:, currency: 'usd', metadata: {})
|
|
39
|
+
create!(
|
|
40
|
+
customer_id: customer_id,
|
|
41
|
+
provider: Belt::Pay.configuration.provider.to_s,
|
|
42
|
+
provider_session_id: session_id,
|
|
43
|
+
type: 'checkout',
|
|
44
|
+
status: 'completed',
|
|
45
|
+
amount_cents: amount_cents,
|
|
46
|
+
currency: currency,
|
|
47
|
+
description: 'Checkout session completed',
|
|
48
|
+
metadata: metadata
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Record a subscription creation
|
|
53
|
+
def record_subscription(customer_id:, subscription_id:, amount_cents:, currency: 'usd', metadata: {})
|
|
54
|
+
create!(
|
|
55
|
+
customer_id: customer_id,
|
|
56
|
+
provider: Belt::Pay.configuration.provider.to_s,
|
|
57
|
+
provider_subscription_id: subscription_id,
|
|
58
|
+
type: 'subscription',
|
|
59
|
+
status: 'completed',
|
|
60
|
+
amount_cents: amount_cents,
|
|
61
|
+
currency: currency,
|
|
62
|
+
description: 'Subscription created',
|
|
63
|
+
metadata: metadata
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Record a subscription renewal (invoice paid)
|
|
68
|
+
def record_renewal(customer_id:, subscription_id:, amount_cents:, currency: 'usd', metadata: {})
|
|
69
|
+
create!(
|
|
70
|
+
customer_id: customer_id,
|
|
71
|
+
provider: Belt::Pay.configuration.provider.to_s,
|
|
72
|
+
provider_subscription_id: subscription_id,
|
|
73
|
+
type: 'subscription_renewal',
|
|
74
|
+
status: 'completed',
|
|
75
|
+
amount_cents: amount_cents,
|
|
76
|
+
currency: currency,
|
|
77
|
+
description: 'Subscription renewal',
|
|
78
|
+
metadata: metadata
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Record a refund
|
|
83
|
+
def record_refund(customer_id:, amount_cents:, currency: 'usd', metadata: {})
|
|
84
|
+
create!(
|
|
85
|
+
customer_id: customer_id,
|
|
86
|
+
provider: Belt::Pay.configuration.provider.to_s,
|
|
87
|
+
type: 'refund',
|
|
88
|
+
status: 'completed',
|
|
89
|
+
amount_cents: amount_cents,
|
|
90
|
+
currency: currency,
|
|
91
|
+
description: 'Refund issued',
|
|
92
|
+
metadata: metadata
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Find transactions for a customer
|
|
97
|
+
def for_customer(customer_id)
|
|
98
|
+
where(customer_id: customer_id, index: 'CustomerIndex')
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Belt
|
|
4
|
+
module Pay
|
|
5
|
+
# Handles incoming webhooks from the payment provider.
|
|
6
|
+
# Verifies signatures, dispatches events, and records transactions.
|
|
7
|
+
module WebhookHandler
|
|
8
|
+
class << self
|
|
9
|
+
# Process a raw webhook request.
|
|
10
|
+
# @param payload [String] Raw request body
|
|
11
|
+
# @param signature [String] Provider signature header
|
|
12
|
+
# @return [Hash] { received: true }
|
|
13
|
+
def process(payload:, signature:)
|
|
14
|
+
event = Belt::Pay.provider.verify_webhook(payload, signature)
|
|
15
|
+
dispatch(event)
|
|
16
|
+
{ received: true }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def dispatch(event)
|
|
22
|
+
event_type = event['type'] || event.type
|
|
23
|
+
|
|
24
|
+
Belt::Pay.log(:info, 'Belt::Pay::WebhookHandler: received event', event_type: event_type)
|
|
25
|
+
|
|
26
|
+
case event_type
|
|
27
|
+
when 'checkout.session.completed'
|
|
28
|
+
handle_checkout_completed(event.data.object)
|
|
29
|
+
when 'checkout.session.expired'
|
|
30
|
+
handle_checkout_expired(event.data.object)
|
|
31
|
+
when 'invoice.paid'
|
|
32
|
+
handle_invoice_paid(event.data.object)
|
|
33
|
+
when 'invoice.payment_failed'
|
|
34
|
+
handle_invoice_payment_failed(event.data.object)
|
|
35
|
+
when 'customer.subscription.deleted'
|
|
36
|
+
handle_subscription_deleted(event.data.object)
|
|
37
|
+
when 'customer.subscription.updated'
|
|
38
|
+
handle_subscription_updated(event.data.object)
|
|
39
|
+
else
|
|
40
|
+
Belt::Pay.log(:info, 'Belt::Pay::WebhookHandler: unhandled event type', event_type: event_type)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def handle_checkout_completed(session)
|
|
45
|
+
customer_id = session.metadata&.app_customer_id || session.metadata&.[]('app_customer_id')
|
|
46
|
+
return unless customer_id
|
|
47
|
+
|
|
48
|
+
# Complete the pending transaction
|
|
49
|
+
pending = Transaction.where(
|
|
50
|
+
provider_session_id: session.id,
|
|
51
|
+
index: 'ProviderSessionIndex'
|
|
52
|
+
).first
|
|
53
|
+
|
|
54
|
+
if pending&.status == 'pending'
|
|
55
|
+
pending.status = 'completed'
|
|
56
|
+
pending.amount_cents = session.amount_total
|
|
57
|
+
pending.save!
|
|
58
|
+
else
|
|
59
|
+
# No pending record — create one (webhook-first flow)
|
|
60
|
+
Transaction.record_checkout(
|
|
61
|
+
customer_id: customer_id,
|
|
62
|
+
session_id: session.id,
|
|
63
|
+
amount_cents: session.amount_total || 0,
|
|
64
|
+
metadata: { payment_status: session.payment_status }
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
Belt::Pay.log(:info, 'Belt::Pay::WebhookHandler: checkout completed',
|
|
69
|
+
customer_id: customer_id, session_id: session.id,
|
|
70
|
+
amount_cents: session.amount_total)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def handle_checkout_expired(session)
|
|
74
|
+
# Mark pending transaction as failed
|
|
75
|
+
pending = Transaction.where(
|
|
76
|
+
provider_session_id: session.id,
|
|
77
|
+
index: 'ProviderSessionIndex'
|
|
78
|
+
).first
|
|
79
|
+
|
|
80
|
+
if pending&.status == 'pending'
|
|
81
|
+
pending.status = 'failed'
|
|
82
|
+
pending.description = 'Checkout session expired'
|
|
83
|
+
pending.save!
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
Belt::Pay.log(:info, 'Belt::Pay::WebhookHandler: checkout expired', session_id: session.id)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def handle_invoice_paid(invoice)
|
|
90
|
+
subscription_id = invoice.subscription
|
|
91
|
+
customer_id = invoice.metadata&.app_customer_id || invoice.metadata&.[]('app_customer_id')
|
|
92
|
+
|
|
93
|
+
# Try to resolve customer from subscription metadata if not on invoice
|
|
94
|
+
unless customer_id
|
|
95
|
+
customer_id = resolve_customer_from_subscription(subscription_id)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
return unless customer_id && subscription_id
|
|
99
|
+
|
|
100
|
+
Transaction.record_renewal(
|
|
101
|
+
customer_id: customer_id,
|
|
102
|
+
subscription_id: subscription_id,
|
|
103
|
+
amount_cents: invoice.amount_paid || 0,
|
|
104
|
+
metadata: { invoice_id: invoice.id, billing_reason: invoice.billing_reason }
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
Belt::Pay.log(:info, 'Belt::Pay::WebhookHandler: invoice paid',
|
|
108
|
+
customer_id: customer_id, subscription_id: subscription_id,
|
|
109
|
+
amount_cents: invoice.amount_paid)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def handle_invoice_payment_failed(invoice)
|
|
113
|
+
customer_id = invoice.metadata&.app_customer_id || invoice.metadata&.[]('app_customer_id')
|
|
114
|
+
unless customer_id
|
|
115
|
+
customer_id = resolve_customer_from_subscription(invoice.subscription)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
Belt::Pay.log(:warn, 'Belt::Pay::WebhookHandler: invoice payment failed',
|
|
119
|
+
customer_id: customer_id, invoice_id: invoice.id)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def handle_subscription_deleted(subscription)
|
|
123
|
+
customer_id = subscription.metadata&.app_customer_id || subscription.metadata&.[]('app_customer_id')
|
|
124
|
+
return unless customer_id
|
|
125
|
+
|
|
126
|
+
Belt::Pay.log(:info, 'Belt::Pay::WebhookHandler: subscription deleted',
|
|
127
|
+
customer_id: customer_id, subscription_id: subscription.id)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def handle_subscription_updated(subscription)
|
|
131
|
+
customer_id = subscription.metadata&.app_customer_id || subscription.metadata&.[]('app_customer_id')
|
|
132
|
+
return unless customer_id
|
|
133
|
+
|
|
134
|
+
Belt::Pay.log(:info, 'Belt::Pay::WebhookHandler: subscription updated',
|
|
135
|
+
customer_id: customer_id, subscription_id: subscription.id,
|
|
136
|
+
status: subscription.status)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def resolve_customer_from_subscription(subscription_id)
|
|
140
|
+
return nil unless subscription_id
|
|
141
|
+
|
|
142
|
+
Belt::Pay.provider.ensure_api_key!
|
|
143
|
+
sub = ::Stripe::Subscription.retrieve(subscription_id)
|
|
144
|
+
sub.metadata&.app_customer_id || sub.metadata&.[]('app_customer_id')
|
|
145
|
+
rescue StandardError => e
|
|
146
|
+
Belt::Pay.log(:warn, 'Belt::Pay::WebhookHandler: failed to resolve customer from subscription',
|
|
147
|
+
subscription_id: subscription_id, error: e.message)
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
data/lib/belt/pay.rb
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'pay/version'
|
|
4
|
+
require_relative 'pay/configuration'
|
|
5
|
+
require_relative 'pay/transaction'
|
|
6
|
+
require_relative 'pay/billable'
|
|
7
|
+
require_relative 'pay/checkout'
|
|
8
|
+
require_relative 'pay/subscription'
|
|
9
|
+
require_relative 'pay/customer_provisioner'
|
|
10
|
+
require_relative 'pay/payment_method_attacher'
|
|
11
|
+
require_relative 'pay/setup_intent_creator'
|
|
12
|
+
require_relative 'pay/webhook_handler'
|
|
13
|
+
require_relative 'pay/providers/stripe'
|
|
14
|
+
|
|
15
|
+
module Belt
|
|
16
|
+
module Pay
|
|
17
|
+
class Error < StandardError; end
|
|
18
|
+
class ConfigurationError < Error; end
|
|
19
|
+
class ProviderError < Error; end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
attr_writer :configuration
|
|
23
|
+
|
|
24
|
+
def configuration
|
|
25
|
+
@configuration ||= Configuration.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def configure
|
|
29
|
+
yield(configuration)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def reset_configuration!
|
|
33
|
+
@configuration = Configuration.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# --- Convenience API (provider-agnostic names) ---
|
|
37
|
+
|
|
38
|
+
# Ensure a payment provider customer exists for this user.
|
|
39
|
+
# @param customer [Object] Your app's user/customer model instance
|
|
40
|
+
# @return [String] The provider customer ID
|
|
41
|
+
def ensure_customer(customer)
|
|
42
|
+
CustomerProvisioner.new(customer).call
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Attach a payment method and set as default.
|
|
46
|
+
# @param customer [Object] Your app's user/customer model instance
|
|
47
|
+
# @param payment_method_id [String] Provider payment method token
|
|
48
|
+
# @return [PaymentMethodAttacher::Result]
|
|
49
|
+
def attach_payment_method(customer, payment_method_id)
|
|
50
|
+
PaymentMethodAttacher.new(customer, payment_method_id).call
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Create a setup intent for collecting payment details on the frontend.
|
|
54
|
+
# @param customer [Object] Your app's user/customer model instance
|
|
55
|
+
# @return [SetupIntentCreator::Result]
|
|
56
|
+
def create_setup_intent(customer)
|
|
57
|
+
SetupIntentCreator.new(customer).call
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Create a checkout session for one-time or subscription payments.
|
|
61
|
+
# @param customer [Object] Your app's user/customer model instance
|
|
62
|
+
# @param options [Hash] Checkout options (line_items, mode, success_url, cancel_url, metadata)
|
|
63
|
+
# @return [Hash] { url:, session_id: }
|
|
64
|
+
def create_checkout(customer, **options)
|
|
65
|
+
Checkout.create(customer, **options)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Subscribe a customer to a plan.
|
|
69
|
+
# @param customer [Object] Your app's user/customer model instance
|
|
70
|
+
# @param price_id [String] Provider price/plan ID
|
|
71
|
+
# @param metadata [Hash] Additional metadata
|
|
72
|
+
# @return [Hash] { subscription_id:, status: }
|
|
73
|
+
def subscribe(customer, price_id:, metadata: {})
|
|
74
|
+
Subscription.create(customer, price_id: price_id, metadata: metadata)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Cancel a customer's subscription.
|
|
78
|
+
# @param customer [Object] Your app's user/customer model instance
|
|
79
|
+
# @param immediately [Boolean] Cancel now vs at period end (default: false)
|
|
80
|
+
def cancel_subscription(customer, immediately: false)
|
|
81
|
+
Subscription.cancel(customer, immediately: immediately)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Generate a billing portal URL for customer self-service.
|
|
85
|
+
# @param customer [Object] Your app's user/customer model instance
|
|
86
|
+
# @param return_url [String] URL to redirect back to after portal
|
|
87
|
+
# @return [Hash] { url: }
|
|
88
|
+
def billing_portal(customer, return_url:)
|
|
89
|
+
provider.billing_portal(customer, return_url: return_url)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Get the configured provider adapter instance.
|
|
93
|
+
# @return [Belt::Pay::Providers::Stripe] (or future providers)
|
|
94
|
+
def provider
|
|
95
|
+
@provider ||= resolve_provider
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Reset provider (useful for testing)
|
|
99
|
+
def reset_provider!
|
|
100
|
+
@provider = nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Internal logging helper
|
|
104
|
+
def log(level, message, **kwargs)
|
|
105
|
+
if defined?(Belt::Observability::Logger)
|
|
106
|
+
Belt::Observability::Logger.public_send(level, message, **kwargs)
|
|
107
|
+
elsif configuration.logger
|
|
108
|
+
configuration.logger.public_send(level, message, **kwargs)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def resolve_provider
|
|
115
|
+
case configuration.provider
|
|
116
|
+
when :stripe
|
|
117
|
+
Providers::Stripe.new
|
|
118
|
+
else
|
|
119
|
+
raise ConfigurationError, "Unknown provider: #{configuration.provider}. Supported: :stripe"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
data/lib/belt-pay.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: belt-pay
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Stowzilla
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: belt
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.2'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: stripe
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.0'
|
|
40
|
+
description: Belt plugin providing payment collection, subscriptions, and billing
|
|
41
|
+
management. Ships with Stripe as the default provider. Includes webhook handling,
|
|
42
|
+
customer provisioning, and a Transaction model for audit logging.
|
|
43
|
+
email:
|
|
44
|
+
- andy@stowzilla.com
|
|
45
|
+
- adam@stowzilla.com
|
|
46
|
+
executables: []
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- CHANGELOG.md
|
|
51
|
+
- LICENSE
|
|
52
|
+
- README.md
|
|
53
|
+
- lib/belt-pay.rb
|
|
54
|
+
- lib/belt/generators/pay_generator.rb
|
|
55
|
+
- lib/belt/pay.rb
|
|
56
|
+
- lib/belt/pay/billable.rb
|
|
57
|
+
- lib/belt/pay/checkout.rb
|
|
58
|
+
- lib/belt/pay/configuration.rb
|
|
59
|
+
- lib/belt/pay/controllers/webhooks_controller.rb
|
|
60
|
+
- lib/belt/pay/customer_provisioner.rb
|
|
61
|
+
- lib/belt/pay/payment_method_attacher.rb
|
|
62
|
+
- lib/belt/pay/providers/stripe.rb
|
|
63
|
+
- lib/belt/pay/setup_intent_creator.rb
|
|
64
|
+
- lib/belt/pay/subscription.rb
|
|
65
|
+
- lib/belt/pay/templates/config/pay_webhooks.yml.erb
|
|
66
|
+
- lib/belt/pay/templates/controllers/pay_webhooks_controller.rb.erb
|
|
67
|
+
- lib/belt/pay/templates/lambda/pay_webhooks.rb.erb
|
|
68
|
+
- lib/belt/pay/templates/terraform/main.tf.erb
|
|
69
|
+
- lib/belt/pay/templates/terraform/outputs.tf.erb
|
|
70
|
+
- lib/belt/pay/templates/terraform/variables.tf.erb
|
|
71
|
+
- lib/belt/pay/transaction.rb
|
|
72
|
+
- lib/belt/pay/version.rb
|
|
73
|
+
- lib/belt/pay/webhook_handler.rb
|
|
74
|
+
homepage: https://github.com/stowzilla/belt-pay
|
|
75
|
+
licenses:
|
|
76
|
+
- MIT
|
|
77
|
+
metadata:
|
|
78
|
+
homepage_uri: https://github.com/stowzilla/belt-pay
|
|
79
|
+
source_code_uri: https://github.com/stowzilla/belt-pay
|
|
80
|
+
changelog_uri: https://github.com/stowzilla/belt-pay/blob/main/CHANGELOG.md
|
|
81
|
+
rubygems_mfa_required: 'true'
|
|
82
|
+
rdoc_options: []
|
|
83
|
+
require_paths:
|
|
84
|
+
- lib
|
|
85
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '3.3'
|
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ">="
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '0'
|
|
95
|
+
requirements: []
|
|
96
|
+
rubygems_version: 3.6.9
|
|
97
|
+
specification_version: 4
|
|
98
|
+
summary: Payments and subscriptions for Belt applications via Stripe
|
|
99
|
+
test_files: []
|