better_auth-stripe 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/README.md +32 -0
- data/lib/better_auth/plugins/stripe.rb +958 -0
- data/lib/better_auth/stripe/version.rb +7 -0
- data/lib/better_auth/stripe.rb +10 -0
- metadata +140 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 19c8cba9e5f67da8b742ec37f48ef758124d373812879ae8af0a49e950962fc5
|
|
4
|
+
data.tar.gz: c848c8261f7376eb71d2c75d7e2a5fd3b3ef6ba384b92c2891e75de41be4a75b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 75dd173be79173cc0a40596f120b54f1160dfce41c98fa9ce2c60a9de13a91860e17b1f6d42840e798dbe44a9b8f50a3fdc31e8bc95c8c3b972955bd2c38c199
|
|
7
|
+
data.tar.gz: be8037299688f25db5354ff51fdec3ba1925a0afa023116d3f054da294f4a8db16f1c2ceafb19c3e41c9e9110b00fa5cfa6cd7d617ce73243394df8ea927ba7a
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# better_auth-stripe
|
|
2
|
+
|
|
3
|
+
Stripe subscription and customer plugin package for Better Auth Ruby.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add the gem and require the package before configuring the plugin:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "better_auth-stripe"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
require "better_auth/stripe"
|
|
15
|
+
|
|
16
|
+
auth = BetterAuth.auth(
|
|
17
|
+
secret: ENV.fetch("BETTER_AUTH_SECRET"),
|
|
18
|
+
database: :memory,
|
|
19
|
+
plugins: [
|
|
20
|
+
BetterAuth::Plugins.stripe(
|
|
21
|
+
stripe_api_key: ENV.fetch("STRIPE_SECRET_KEY"),
|
|
22
|
+
stripe_webhook_secret: ENV.fetch("STRIPE_WEBHOOK_SECRET")
|
|
23
|
+
)
|
|
24
|
+
]
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Notes
|
|
29
|
+
|
|
30
|
+
This package depends on the official `stripe` gem. Keeping Stripe outside `better_auth` avoids installing Stripe SDK dependencies for applications that do not use billing.
|
|
31
|
+
|
|
32
|
+
Pass `stripe_client:` when you need a custom Stripe client, Stripe Connect behavior, or a test double.
|
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "stripe"
|
|
5
|
+
|
|
6
|
+
module BetterAuth
|
|
7
|
+
module Stripe
|
|
8
|
+
class ClientAdapter
|
|
9
|
+
attr_reader :customers, :checkout, :billing_portal, :subscriptions, :prices, :webhooks
|
|
10
|
+
|
|
11
|
+
def initialize(api_key)
|
|
12
|
+
client = ::Stripe::StripeClient.new(api_key)
|
|
13
|
+
@customers = ResourceAdapter.new(client.v1.customers)
|
|
14
|
+
@checkout = NamespaceAdapter.new(sessions: ResourceAdapter.new(client.v1.checkout.sessions))
|
|
15
|
+
@billing_portal = NamespaceAdapter.new(sessions: ResourceAdapter.new(client.v1.billing_portal.sessions))
|
|
16
|
+
@subscriptions = ResourceAdapter.new(client.v1.subscriptions)
|
|
17
|
+
@prices = ResourceAdapter.new(client.v1.prices)
|
|
18
|
+
@webhooks = WebhooksAdapter.new
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class NamespaceAdapter
|
|
23
|
+
def initialize(resources)
|
|
24
|
+
resources.each do |name, resource|
|
|
25
|
+
instance_variable_set(:"@#{name}", resource)
|
|
26
|
+
self.class.attr_reader(name) unless respond_to?(name)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class ResourceAdapter
|
|
32
|
+
def initialize(resource)
|
|
33
|
+
@resource = resource
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def create(params = {}, options = nil)
|
|
37
|
+
options ? @resource.create(params || {}, options) : @resource.create(params || {})
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def list(params = {})
|
|
41
|
+
@resource.list(params || {})
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def search(params = {})
|
|
45
|
+
@resource.search(params || {})
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def retrieve(id)
|
|
49
|
+
@resource.retrieve(id)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def update(id, params = {})
|
|
53
|
+
@resource.update(id, params || {})
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class WebhooksAdapter
|
|
58
|
+
def construct_event(payload, signature, secret)
|
|
59
|
+
::Stripe::Webhook.construct_event(payload, signature, secret)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def construct_event_async(payload, signature, secret)
|
|
63
|
+
construct_event(payload, signature, secret)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
module BetterAuth
|
|
70
|
+
module Plugins
|
|
71
|
+
singleton_class.remove_method(:stripe) if singleton_class.method_defined?(:stripe)
|
|
72
|
+
remove_method(:stripe) if method_defined?(:stripe) || private_method_defined?(:stripe)
|
|
73
|
+
remove_const(:STRIPE_ERROR_CODES) if const_defined?(:STRIPE_ERROR_CODES, false)
|
|
74
|
+
|
|
75
|
+
module_function
|
|
76
|
+
|
|
77
|
+
STRIPE_ERROR_CODES = {
|
|
78
|
+
"UNAUTHORIZED" => "Unauthorized access",
|
|
79
|
+
"EMAIL_VERIFICATION_REQUIRED" => "Email verification required",
|
|
80
|
+
"SUBSCRIPTION_NOT_FOUND" => "Subscription not found",
|
|
81
|
+
"SUBSCRIPTION_PLAN_NOT_FOUND" => "Subscription plan not found",
|
|
82
|
+
"ALREADY_SUBSCRIBED_PLAN" => "You're already subscribed to this plan",
|
|
83
|
+
"REFERENCE_ID_NOT_ALLOWED" => "Reference id is not allowed",
|
|
84
|
+
"CUSTOMER_NOT_FOUND" => "Stripe customer not found for this user",
|
|
85
|
+
"UNABLE_TO_CREATE_CUSTOMER" => "Unable to create customer",
|
|
86
|
+
"UNABLE_TO_CREATE_BILLING_PORTAL" => "Unable to create billing portal session",
|
|
87
|
+
"ORGANIZATION_NOT_FOUND" => "Organization not found",
|
|
88
|
+
"ORGANIZATION_SUBSCRIPTION_NOT_ENABLED" => "Organization subscription is not enabled",
|
|
89
|
+
"AUTHORIZE_REFERENCE_REQUIRED" => "Organization subscriptions require authorizeReference callback to be configured",
|
|
90
|
+
"ORGANIZATION_HAS_ACTIVE_SUBSCRIPTION" => "Cannot delete organization with active subscription",
|
|
91
|
+
"ORGANIZATION_REFERENCE_ID_REQUIRED" => "Reference ID is required. Provide referenceId or set activeOrganizationId in session",
|
|
92
|
+
"SUBSCRIPTION_NOT_ACTIVE" => "Subscription is not active",
|
|
93
|
+
"SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION" => "Subscription is not scheduled for cancellation",
|
|
94
|
+
"STRIPE_SIGNATURE_NOT_FOUND" => "Stripe signature not found",
|
|
95
|
+
"STRIPE_WEBHOOK_SECRET_NOT_FOUND" => "Stripe webhook secret not found",
|
|
96
|
+
"FAILED_TO_CONSTRUCT_STRIPE_EVENT" => "Failed to construct Stripe event",
|
|
97
|
+
"STRIPE_WEBHOOK_ERROR" => "Stripe webhook error",
|
|
98
|
+
"INVALID_REQUEST_BODY" => "Invalid request body"
|
|
99
|
+
}.freeze
|
|
100
|
+
|
|
101
|
+
def stripe(options = {})
|
|
102
|
+
config = normalize_hash(options)
|
|
103
|
+
Plugin.new(
|
|
104
|
+
id: "stripe",
|
|
105
|
+
init: ->(ctx) { {context: {schema: Schema.auth_tables(ctx.options)}} },
|
|
106
|
+
schema: stripe_schema(config),
|
|
107
|
+
endpoints: stripe_endpoints(config),
|
|
108
|
+
error_codes: STRIPE_ERROR_CODES,
|
|
109
|
+
options: config.merge(database_hooks: stripe_database_hooks(config), organization_hooks: stripe_organization_hooks(config))
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def stripe_schema(config)
|
|
114
|
+
schema = {
|
|
115
|
+
user: {
|
|
116
|
+
fields: {
|
|
117
|
+
stripeCustomerId: {type: "string", required: false}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if config.dig(:subscription, :enabled)
|
|
122
|
+
schema[:subscription] = {
|
|
123
|
+
fields: {
|
|
124
|
+
plan: {type: "string", required: true},
|
|
125
|
+
referenceId: {type: "string", required: true},
|
|
126
|
+
stripeCustomerId: {type: "string", required: false},
|
|
127
|
+
stripeSubscriptionId: {type: "string", required: false},
|
|
128
|
+
status: {type: "string", required: false, default_value: "incomplete"},
|
|
129
|
+
periodStart: {type: "date", required: false},
|
|
130
|
+
periodEnd: {type: "date", required: false},
|
|
131
|
+
trialStart: {type: "date", required: false},
|
|
132
|
+
trialEnd: {type: "date", required: false},
|
|
133
|
+
cancelAtPeriodEnd: {type: "boolean", required: false, default_value: false},
|
|
134
|
+
cancelAt: {type: "date", required: false},
|
|
135
|
+
canceledAt: {type: "date", required: false},
|
|
136
|
+
endedAt: {type: "date", required: false},
|
|
137
|
+
seats: {type: "number", required: false},
|
|
138
|
+
limits: {type: "json", required: false}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
if config.dig(:organization, :enabled)
|
|
143
|
+
schema[:organization] = {fields: {stripeCustomerId: {type: "string", required: false}}}
|
|
144
|
+
end
|
|
145
|
+
schema
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def stripe_endpoints(config)
|
|
149
|
+
endpoints = {stripe_webhook: stripe_webhook_endpoint(config)}
|
|
150
|
+
return endpoints unless config.dig(:subscription, :enabled)
|
|
151
|
+
|
|
152
|
+
endpoints.merge(
|
|
153
|
+
upgrade_subscription: stripe_upgrade_subscription_endpoint(config),
|
|
154
|
+
cancel_subscription_callback: stripe_cancel_callback_endpoint(config),
|
|
155
|
+
cancel_subscription: stripe_cancel_subscription_endpoint(config),
|
|
156
|
+
restore_subscription: stripe_restore_subscription_endpoint(config),
|
|
157
|
+
list_active_subscriptions: stripe_list_subscriptions_endpoint(config),
|
|
158
|
+
subscription_success: stripe_success_endpoint(config),
|
|
159
|
+
create_billing_portal: stripe_billing_portal_endpoint(config)
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def stripe_database_hooks(config)
|
|
164
|
+
return {} unless config[:create_customer_on_sign_up]
|
|
165
|
+
|
|
166
|
+
{
|
|
167
|
+
user: {
|
|
168
|
+
create: {
|
|
169
|
+
before: lambda do |data, hook_ctx|
|
|
170
|
+
next unless data["email"] && !data["stripeCustomerId"]
|
|
171
|
+
|
|
172
|
+
data["id"] ||= SecureRandom.hex(16)
|
|
173
|
+
customer = stripe_find_or_create_user_customer(config, data, nil, hook_ctx)
|
|
174
|
+
{data: {id: data["id"], stripeCustomerId: stripe_id(customer)}}
|
|
175
|
+
end
|
|
176
|
+
},
|
|
177
|
+
update: {
|
|
178
|
+
after: lambda do |user, _ctx|
|
|
179
|
+
next unless user && user["stripeCustomerId"]
|
|
180
|
+
|
|
181
|
+
customer = stripe_client(config).customers.retrieve(user["stripeCustomerId"])
|
|
182
|
+
next if stripe_fetch(customer, "deleted")
|
|
183
|
+
next if stripe_fetch(customer, "email") == user["email"]
|
|
184
|
+
|
|
185
|
+
stripe_client(config).customers.update(user["stripeCustomerId"], email: user["email"])
|
|
186
|
+
rescue
|
|
187
|
+
nil
|
|
188
|
+
end
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def stripe_organization_hooks(config)
|
|
195
|
+
return {} unless config.dig(:organization, :enabled)
|
|
196
|
+
|
|
197
|
+
{
|
|
198
|
+
after_update_organization: lambda do |data, _ctx|
|
|
199
|
+
organization = data[:organization] || data["organization"]
|
|
200
|
+
next unless organization && organization["stripeCustomerId"]
|
|
201
|
+
|
|
202
|
+
customer = stripe_client(config).customers.retrieve(organization["stripeCustomerId"])
|
|
203
|
+
next if stripe_fetch(customer, "deleted")
|
|
204
|
+
next if stripe_fetch(customer, "name") == organization["name"]
|
|
205
|
+
|
|
206
|
+
stripe_client(config).customers.update(organization["stripeCustomerId"], name: organization["name"])
|
|
207
|
+
rescue
|
|
208
|
+
nil
|
|
209
|
+
end,
|
|
210
|
+
before_delete_organization: lambda do |data, _ctx|
|
|
211
|
+
organization = data[:organization] || data["organization"]
|
|
212
|
+
next unless organization && organization["stripeCustomerId"]
|
|
213
|
+
|
|
214
|
+
subscriptions = stripe_client(config).subscriptions.list(customer: organization["stripeCustomerId"], status: "all", limit: 100)
|
|
215
|
+
active = Array(stripe_fetch(subscriptions, "data")).any? do |subscription|
|
|
216
|
+
!%w[canceled incomplete incomplete_expired].include?(stripe_fetch(subscription, "status").to_s)
|
|
217
|
+
end
|
|
218
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("ORGANIZATION_HAS_ACTIVE_SUBSCRIPTION")) if active
|
|
219
|
+
end
|
|
220
|
+
}
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def stripe_upgrade_subscription_endpoint(config)
|
|
224
|
+
Endpoint.new(path: "/subscription/upgrade", method: "POST") do |ctx|
|
|
225
|
+
session = Routes.current_session(ctx)
|
|
226
|
+
body = normalize_hash(ctx.body)
|
|
227
|
+
subscription_options = stripe_subscription_options(config)
|
|
228
|
+
customer_type = (body[:customer_type] || "user").to_s
|
|
229
|
+
reference_id = stripe_reference_id!(ctx, session, customer_type, body[:reference_id], config)
|
|
230
|
+
stripe_authorize_reference!(ctx, session, reference_id, "upgrade-subscription", customer_type, subscription_options, explicit: body.key?(:reference_id))
|
|
231
|
+
|
|
232
|
+
user = session.fetch(:user)
|
|
233
|
+
if subscription_options[:require_email_verification] && !user["emailVerified"]
|
|
234
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("EMAIL_VERIFICATION_REQUIRED"))
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
plan = stripe_plan_by_name(config, body[:plan])
|
|
238
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_PLAN_NOT_FOUND")) unless plan
|
|
239
|
+
|
|
240
|
+
subscription_to_update = nil
|
|
241
|
+
if body[:subscription_id]
|
|
242
|
+
subscription_to_update = ctx.context.adapter.find_one(model: "subscription", where: [{field: "stripeSubscriptionId", value: body[:subscription_id]}])
|
|
243
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_FOUND")) unless subscription_to_update && subscription_to_update["referenceId"] == reference_id
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
customer_id = if customer_type == "organization"
|
|
247
|
+
subscription_to_update&.fetch("stripeCustomerId", nil) || stripe_organization_customer(config, ctx, reference_id, body[:metadata])
|
|
248
|
+
else
|
|
249
|
+
subscription_to_update&.fetch("stripeCustomerId", nil) || user["stripeCustomerId"] || stripe_create_customer(config, ctx, user, body[:metadata])
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
subscriptions = subscription_to_update ? [subscription_to_update] : ctx.context.adapter.find_many(model: "subscription", where: [{field: "referenceId", value: reference_id}])
|
|
253
|
+
active_or_trialing = subscriptions.find { |entry| stripe_active_or_trialing?(entry) }
|
|
254
|
+
active_stripe_subscriptions = stripe_active_subscriptions(config, customer_id)
|
|
255
|
+
active_stripe = active_stripe_subscriptions.find do |entry|
|
|
256
|
+
if subscription_to_update&.fetch("stripeSubscriptionId", nil) || body[:subscription_id]
|
|
257
|
+
stripe_fetch(entry, "id") == subscription_to_update&.fetch("stripeSubscriptionId", nil) || stripe_fetch(entry, "id") == body[:subscription_id]
|
|
258
|
+
elsif active_or_trialing && active_or_trialing["stripeSubscriptionId"]
|
|
259
|
+
stripe_fetch(entry, "id") == active_or_trialing["stripeSubscriptionId"]
|
|
260
|
+
else
|
|
261
|
+
false
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
price_id = stripe_price_id(config, plan, body[:annual])
|
|
266
|
+
raise APIError.new("BAD_REQUEST", message: "Price ID not found for the selected plan") if price_id.to_s.empty?
|
|
267
|
+
|
|
268
|
+
active_stripe_item = stripe_subscription_item(active_stripe || {})
|
|
269
|
+
stripe_price_id_value = stripe_fetch(stripe_fetch(active_stripe_item || {}, "price") || {}, "id")
|
|
270
|
+
same_plan = active_or_trialing && active_or_trialing["plan"].to_s.downcase == body[:plan].to_s.downcase
|
|
271
|
+
same_seats = active_or_trialing && active_or_trialing["seats"].to_i == (body[:seats] || 1).to_i
|
|
272
|
+
same_price = !active_stripe || stripe_price_id_value == price_id
|
|
273
|
+
valid_period = !active_or_trialing || !active_or_trialing["periodEnd"] || active_or_trialing["periodEnd"] > Time.now
|
|
274
|
+
if active_or_trialing&.fetch("status", nil) == "active" && same_plan && same_seats && same_price && valid_period
|
|
275
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("ALREADY_SUBSCRIBED_PLAN"))
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
if active_stripe
|
|
279
|
+
portal = stripe_client(config).billing_portal.sessions.create(
|
|
280
|
+
customer: customer_id,
|
|
281
|
+
return_url: stripe_url(ctx, body[:return_url] || "/"),
|
|
282
|
+
flow_data: {
|
|
283
|
+
type: "subscription_update_confirm",
|
|
284
|
+
after_completion: {type: "redirect", redirect: {return_url: stripe_url(ctx, body[:return_url] || "/")}},
|
|
285
|
+
subscription_update_confirm: {
|
|
286
|
+
subscription: stripe_fetch(active_stripe, "id"),
|
|
287
|
+
items: [{id: stripe_fetch(active_stripe_item || {}, "id"), quantity: body[:seats] || 1, price: price_id}]
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
next ctx.json(stripe_stringify_keys(portal).merge(redirect: stripe_redirect?(body)))
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
incomplete = subscriptions.find { |entry| entry["status"] == "incomplete" }
|
|
295
|
+
subscription = active_or_trialing || incomplete
|
|
296
|
+
if subscription
|
|
297
|
+
update = {plan: plan[:name].to_s.downcase, seats: body[:seats] || 1}
|
|
298
|
+
subscription = ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: update) || subscription.merge(update.transform_keys { |key| Schema.storage_key(key) })
|
|
299
|
+
else
|
|
300
|
+
subscription = ctx.context.adapter.create(
|
|
301
|
+
model: "subscription",
|
|
302
|
+
data: {plan: plan[:name].to_s.downcase, referenceId: reference_id, stripeCustomerId: customer_id, status: "incomplete", seats: body[:seats] || 1, limits: plan[:limits]}
|
|
303
|
+
)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
has_ever_trialed = ctx.context.adapter.find_many(model: "subscription", where: [{field: "referenceId", value: reference_id}]).any? do |entry|
|
|
307
|
+
entry["trialStart"] || entry["trialEnd"] || entry["status"] == "trialing"
|
|
308
|
+
end
|
|
309
|
+
free_trial = (!has_ever_trialed && plan[:free_trial]) ? {trial_period_days: plan.dig(:free_trial, :days)} : {}
|
|
310
|
+
checkout_customization = subscription_options[:get_checkout_session_params]&.call(
|
|
311
|
+
{user: user, session: session.fetch(:session), plan: plan, subscription: subscription},
|
|
312
|
+
ctx.request,
|
|
313
|
+
ctx
|
|
314
|
+
) || {}
|
|
315
|
+
custom_params = stripe_fetch(checkout_customization, "params") || {}
|
|
316
|
+
custom_options = normalize_hash(stripe_fetch(checkout_customization, "options") || {})
|
|
317
|
+
custom_subscription_data = stripe_fetch(custom_params, "subscription_data") || stripe_fetch(custom_params, "subscriptionData") || {}
|
|
318
|
+
internal_metadata = {userId: user.fetch("id"), subscriptionId: subscription.fetch("id"), referenceId: reference_id}
|
|
319
|
+
metadata = stripe_subscription_metadata_set(internal_metadata, body[:metadata], stripe_fetch(custom_params, "metadata"))
|
|
320
|
+
subscription_metadata = stripe_subscription_metadata_set(internal_metadata, body[:metadata], stripe_fetch(custom_subscription_data, "metadata"))
|
|
321
|
+
checkout_params = stripe_deep_merge(
|
|
322
|
+
custom_params,
|
|
323
|
+
customer: customer_id,
|
|
324
|
+
customer_update: (customer_type == "user") ? {name: "auto", address: "auto"} : {address: "auto"},
|
|
325
|
+
locale: body[:locale],
|
|
326
|
+
success_url: stripe_url(ctx, "#{ctx.context.base_url}/subscription/success?callbackURL=#{Rack::Utils.escape(body[:success_url] || "/")}&subscriptionId=#{Rack::Utils.escape(subscription.fetch("id"))}"),
|
|
327
|
+
cancel_url: stripe_url(ctx, body[:cancel_url] || "/"),
|
|
328
|
+
line_items: [{price: price_id, quantity: body[:seats] || 1}],
|
|
329
|
+
subscription_data: free_trial.merge(metadata: subscription_metadata),
|
|
330
|
+
mode: "subscription",
|
|
331
|
+
client_reference_id: reference_id,
|
|
332
|
+
metadata: metadata
|
|
333
|
+
)
|
|
334
|
+
checkout_params[:metadata] = metadata
|
|
335
|
+
checkout_params[:subscription_data] ||= {}
|
|
336
|
+
checkout_params[:subscription_data][:metadata] = subscription_metadata
|
|
337
|
+
checkout = stripe_client(config).checkout.sessions.create(checkout_params, custom_options.empty? ? nil : custom_options)
|
|
338
|
+
ctx.json(stripe_stringify_keys(checkout).merge(redirect: stripe_redirect?(body)))
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def stripe_cancel_subscription_endpoint(config)
|
|
343
|
+
Endpoint.new(path: "/subscription/cancel", method: "POST") do |ctx|
|
|
344
|
+
session = Routes.current_session(ctx)
|
|
345
|
+
body = normalize_hash(ctx.body)
|
|
346
|
+
customer_type = (body[:customer_type] || "user").to_s
|
|
347
|
+
reference_id = stripe_reference_id!(ctx, session, customer_type, body[:reference_id], config)
|
|
348
|
+
stripe_authorize_reference!(ctx, session, reference_id, "cancel-subscription", customer_type, stripe_subscription_options(config), explicit: body.key?(:reference_id))
|
|
349
|
+
subscription = stripe_find_subscription_for_action(ctx, reference_id, body[:subscription_id], active_only: true)
|
|
350
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_FOUND")) unless subscription && subscription["stripeCustomerId"]
|
|
351
|
+
|
|
352
|
+
active = stripe_active_subscriptions(config, subscription["stripeCustomerId"])
|
|
353
|
+
if active.empty?
|
|
354
|
+
ctx.context.adapter.delete_many(model: "subscription", where: [{field: "referenceId", value: reference_id}])
|
|
355
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_FOUND"))
|
|
356
|
+
end
|
|
357
|
+
stripe_subscription = active.find { |entry| stripe_fetch(entry, "id") == subscription["stripeSubscriptionId"] }
|
|
358
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_FOUND")) unless stripe_subscription
|
|
359
|
+
|
|
360
|
+
portal = stripe_client(config).billing_portal.sessions.create(
|
|
361
|
+
customer: subscription["stripeCustomerId"],
|
|
362
|
+
return_url: stripe_url(ctx, "#{ctx.context.base_url}/subscription/cancel/callback?callbackURL=#{Rack::Utils.escape(body[:return_url] || "/")}&subscriptionId=#{Rack::Utils.escape(subscription.fetch("id"))}"),
|
|
363
|
+
flow_data: {type: "subscription_cancel", subscription_cancel: {subscription: stripe_fetch(stripe_subscription, "id")}}
|
|
364
|
+
)
|
|
365
|
+
ctx.json(stripe_stringify_keys(portal).merge(redirect: stripe_redirect?(body)))
|
|
366
|
+
rescue APIError
|
|
367
|
+
raise
|
|
368
|
+
rescue => error
|
|
369
|
+
if error.message.include?("already set to be canceled") && subscription && !stripe_pending_cancel?(subscription)
|
|
370
|
+
stripe_sub = stripe_client(config).subscriptions.retrieve(subscription["stripeSubscriptionId"])
|
|
371
|
+
ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: stripe_subscription_state(stripe_sub, include_status: false))
|
|
372
|
+
end
|
|
373
|
+
raise APIError.new("BAD_REQUEST", message: error.message)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def stripe_restore_subscription_endpoint(config)
|
|
378
|
+
Endpoint.new(path: "/subscription/restore", method: "POST") do |ctx|
|
|
379
|
+
session = Routes.current_session(ctx)
|
|
380
|
+
body = normalize_hash(ctx.body)
|
|
381
|
+
customer_type = (body[:customer_type] || "user").to_s
|
|
382
|
+
reference_id = stripe_reference_id!(ctx, session, customer_type, body[:reference_id], config)
|
|
383
|
+
stripe_authorize_reference!(ctx, session, reference_id, "restore-subscription", customer_type, stripe_subscription_options(config), explicit: body.key?(:reference_id))
|
|
384
|
+
subscription = stripe_find_subscription_for_action(ctx, reference_id, body[:subscription_id], active_only: false)
|
|
385
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_FOUND")) unless subscription && subscription["stripeCustomerId"]
|
|
386
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_ACTIVE")) unless stripe_active_or_trialing?(subscription)
|
|
387
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION")) unless stripe_pending_cancel?(subscription)
|
|
388
|
+
|
|
389
|
+
active = stripe_active_subscriptions(config, subscription["stripeCustomerId"]).first
|
|
390
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_FOUND")) unless active
|
|
391
|
+
|
|
392
|
+
update_params = if stripe_fetch(active, "cancel_at")
|
|
393
|
+
{cancel_at: ""}
|
|
394
|
+
elsif stripe_fetch(active, "cancel_at_period_end")
|
|
395
|
+
{cancel_at_period_end: false}
|
|
396
|
+
else
|
|
397
|
+
{}
|
|
398
|
+
end
|
|
399
|
+
restored = stripe_client(config).subscriptions.update(stripe_fetch(active, "id"), update_params)
|
|
400
|
+
ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: {cancelAtPeriodEnd: false, cancelAt: nil, canceledAt: nil})
|
|
401
|
+
ctx.json(stripe_stringify_keys(restored))
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def stripe_list_subscriptions_endpoint(config)
|
|
406
|
+
Endpoint.new(path: "/subscription/list", method: "GET") do |ctx|
|
|
407
|
+
session = Routes.current_session(ctx)
|
|
408
|
+
query = normalize_hash(ctx.query)
|
|
409
|
+
customer_type = (query[:customer_type] || "user").to_s
|
|
410
|
+
reference_id = stripe_reference_id!(ctx, session, customer_type, query[:reference_id], config)
|
|
411
|
+
stripe_authorize_reference!(ctx, session, reference_id, "list-subscription", customer_type, stripe_subscription_options(config), explicit: query.key?(:reference_id))
|
|
412
|
+
plans = stripe_plans(config)
|
|
413
|
+
subscriptions = ctx.context.adapter.find_many(model: "subscription", where: [{field: "referenceId", value: reference_id}]).select { |entry| stripe_active_or_trialing?(entry) }
|
|
414
|
+
ctx.json(subscriptions.map do |entry|
|
|
415
|
+
plan = plans.find { |item| item[:name].to_s.downcase == entry["plan"].to_s.downcase }
|
|
416
|
+
entry.merge("limits" => plan&.fetch(:limits, nil), "priceId" => plan&.fetch(:price_id, nil))
|
|
417
|
+
end)
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def stripe_billing_portal_endpoint(config)
|
|
422
|
+
Endpoint.new(path: "/subscription/billing-portal", method: "POST") do |ctx|
|
|
423
|
+
session = Routes.current_session(ctx)
|
|
424
|
+
body = normalize_hash(ctx.body)
|
|
425
|
+
customer_type = (body[:customer_type] || "user").to_s
|
|
426
|
+
reference_id = stripe_reference_id!(ctx, session, customer_type, body[:reference_id], config)
|
|
427
|
+
stripe_authorize_reference!(ctx, session, reference_id, "billing-portal", customer_type, stripe_subscription_options(config), explicit: body.key?(:reference_id))
|
|
428
|
+
customer_id = if customer_type == "organization"
|
|
429
|
+
org = ctx.context.adapter.find_one(model: "organization", where: [{field: "id", value: reference_id}])
|
|
430
|
+
org&.fetch("stripeCustomerId", nil) || stripe_active_subscription(ctx, reference_id)&.fetch("stripeCustomerId", nil)
|
|
431
|
+
else
|
|
432
|
+
session.fetch(:user)["stripeCustomerId"] || stripe_active_subscription(ctx, reference_id)&.fetch("stripeCustomerId", nil)
|
|
433
|
+
end
|
|
434
|
+
raise APIError.new("NOT_FOUND", message: STRIPE_ERROR_CODES.fetch("CUSTOMER_NOT_FOUND")) unless customer_id
|
|
435
|
+
|
|
436
|
+
portal = stripe_client(config).billing_portal.sessions.create(customer: customer_id, return_url: stripe_url(ctx, body[:return_url] || "/"), locale: body[:locale])
|
|
437
|
+
ctx.json(stripe_stringify_keys(portal).merge(redirect: stripe_redirect?(body)))
|
|
438
|
+
rescue APIError
|
|
439
|
+
raise
|
|
440
|
+
rescue
|
|
441
|
+
raise APIError.new("INTERNAL_SERVER_ERROR", message: STRIPE_ERROR_CODES.fetch("UNABLE_TO_CREATE_BILLING_PORTAL"))
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def stripe_cancel_callback_endpoint(config = nil)
|
|
446
|
+
Endpoint.new(path: "/subscription/cancel/callback", method: "GET") do |ctx|
|
|
447
|
+
query = normalize_hash(ctx.query)
|
|
448
|
+
callback = query[:callback_url] || "/"
|
|
449
|
+
unless query[:subscription_id]
|
|
450
|
+
raise ctx.redirect(stripe_url(ctx, callback))
|
|
451
|
+
end
|
|
452
|
+
session = Routes.current_session(ctx, allow_nil: true)
|
|
453
|
+
raise ctx.redirect(stripe_url(ctx, callback)) unless session
|
|
454
|
+
|
|
455
|
+
subscription = ctx.context.adapter.find_one(model: "subscription", where: [{field: "id", value: query[:subscription_id]}])
|
|
456
|
+
if subscription && !stripe_pending_cancel?(subscription) && subscription["stripeCustomerId"]
|
|
457
|
+
current = stripe_active_subscriptions(config || {}, subscription["stripeCustomerId"]).find { |entry| stripe_fetch(entry, "id") == subscription["stripeSubscriptionId"] }
|
|
458
|
+
if current && stripe_stripe_pending_cancel?(current)
|
|
459
|
+
ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: stripe_subscription_state(current, include_status: true))
|
|
460
|
+
stripe_subscription_options(config || {})[:on_subscription_cancel]&.call({subscription: subscription, stripeSubscription: current, stripe_subscription: current, cancellationDetails: stripe_fetch(current, "cancellation_details"), cancellation_details: stripe_fetch(current, "cancellation_details"), event: nil})
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
raise ctx.redirect(stripe_url(ctx, callback))
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def stripe_success_endpoint(config = nil)
|
|
468
|
+
Endpoint.new(path: "/subscription/success", method: "GET") do |ctx|
|
|
469
|
+
query = normalize_hash(ctx.query)
|
|
470
|
+
callback = query[:callback_url] || "/"
|
|
471
|
+
unless query[:subscription_id]
|
|
472
|
+
raise ctx.redirect(stripe_url(ctx, callback))
|
|
473
|
+
end
|
|
474
|
+
session = Routes.current_session(ctx, allow_nil: true)
|
|
475
|
+
raise ctx.redirect(stripe_url(ctx, callback)) unless session
|
|
476
|
+
|
|
477
|
+
subscription = ctx.context.adapter.find_one(model: "subscription", where: [{field: "id", value: query[:subscription_id]}])
|
|
478
|
+
raise ctx.redirect(stripe_url(ctx, callback)) unless subscription
|
|
479
|
+
raise ctx.redirect(stripe_url(ctx, callback)) if stripe_active_or_trialing?(subscription)
|
|
480
|
+
|
|
481
|
+
customer_id = subscription["stripeCustomerId"] || session.fetch(:user)["stripeCustomerId"]
|
|
482
|
+
raise ctx.redirect(stripe_url(ctx, callback)) unless customer_id
|
|
483
|
+
|
|
484
|
+
stripe_subscription = stripe_active_subscriptions(config || {}, customer_id).first
|
|
485
|
+
if stripe_subscription
|
|
486
|
+
item = stripe_subscription_item(stripe_subscription)
|
|
487
|
+
plan = item && stripe_plan_by_price_info(config || {}, stripe_fetch(stripe_fetch(item, "price") || {}, "id"), stripe_fetch(stripe_fetch(item, "price") || {}, "lookup_key"))
|
|
488
|
+
if item && plan
|
|
489
|
+
ctx.context.adapter.update(
|
|
490
|
+
model: "subscription",
|
|
491
|
+
where: [{field: "id", value: subscription.fetch("id")}],
|
|
492
|
+
update: stripe_subscription_state(stripe_subscription, include_status: true).merge(
|
|
493
|
+
plan: plan[:name].to_s.downcase,
|
|
494
|
+
seats: stripe_fetch(item, "quantity") || 1,
|
|
495
|
+
stripeSubscriptionId: stripe_fetch(stripe_subscription, "id")
|
|
496
|
+
)
|
|
497
|
+
)
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
raise ctx.redirect(stripe_url(ctx, callback))
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def stripe_webhook_endpoint(config)
|
|
505
|
+
Endpoint.new(path: "/stripe/webhook", method: "POST") do |ctx|
|
|
506
|
+
signature = ctx.headers["stripe-signature"]
|
|
507
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("STRIPE_SIGNATURE_NOT_FOUND")) if signature.to_s.empty?
|
|
508
|
+
|
|
509
|
+
raise APIError.new("INTERNAL_SERVER_ERROR", message: STRIPE_ERROR_CODES.fetch("STRIPE_WEBHOOK_SECRET_NOT_FOUND")) if config[:stripe_webhook_secret].to_s.empty?
|
|
510
|
+
|
|
511
|
+
event = if stripe_client(config).respond_to?(:webhooks)
|
|
512
|
+
webhooks = stripe_client(config).webhooks
|
|
513
|
+
if webhooks.respond_to?(:construct_event_async) && (!webhooks.respond_to?(:construct_event) || config[:stripe_async_webhooks] || (webhooks.respond_to?(:async) && webhooks.async))
|
|
514
|
+
webhooks.construct_event_async(ctx.body, signature, config[:stripe_webhook_secret])
|
|
515
|
+
else
|
|
516
|
+
webhooks.construct_event(ctx.body, signature, config[:stripe_webhook_secret])
|
|
517
|
+
end
|
|
518
|
+
else
|
|
519
|
+
ctx.body
|
|
520
|
+
end
|
|
521
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("FAILED_TO_CONSTRUCT_STRIPE_EVENT")) unless event
|
|
522
|
+
stripe_handle_event(ctx, event)
|
|
523
|
+
ctx.json({success: true})
|
|
524
|
+
rescue APIError
|
|
525
|
+
raise
|
|
526
|
+
rescue => error
|
|
527
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("FAILED_TO_CONSTRUCT_STRIPE_EVENT") || error.message)
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def stripe_handle_event(ctx, event)
|
|
532
|
+
event = normalize_hash(event)
|
|
533
|
+
type = event[:type].to_s
|
|
534
|
+
case type
|
|
535
|
+
when "checkout.session.completed"
|
|
536
|
+
stripe_on_checkout_completed(ctx, event)
|
|
537
|
+
when "customer.subscription.created"
|
|
538
|
+
stripe_on_subscription_created(ctx, event)
|
|
539
|
+
when "customer.subscription.updated"
|
|
540
|
+
stripe_on_subscription_updated(ctx, event)
|
|
541
|
+
when "customer.subscription.deleted"
|
|
542
|
+
stripe_on_subscription_deleted(ctx, event)
|
|
543
|
+
end
|
|
544
|
+
config = ctx.context.options.plugins.find { |plugin| plugin.id == "stripe" }&.options || {}
|
|
545
|
+
config[:on_event]&.call(event)
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
def stripe_on_checkout_completed(ctx, event)
|
|
549
|
+
config = ctx.context.options.plugins.find { |plugin| plugin.id == "stripe" }&.options || {}
|
|
550
|
+
object = normalize_hash(event.dig(:data, :object) || {})
|
|
551
|
+
return if object[:mode] == "setup" || !config.dig(:subscription, :enabled)
|
|
552
|
+
|
|
553
|
+
stripe_subscription = stripe_client(config).subscriptions.retrieve(object[:subscription])
|
|
554
|
+
item = stripe_subscription_item(stripe_subscription)
|
|
555
|
+
return unless item
|
|
556
|
+
|
|
557
|
+
plan = stripe_plan_by_price_info(config, stripe_fetch(stripe_fetch(item, "price") || {}, "id"), stripe_fetch(stripe_fetch(item, "price") || {}, "lookup_key"))
|
|
558
|
+
metadata = normalize_hash(object[:metadata] || {})
|
|
559
|
+
reference_id = object[:client_reference_id] || metadata[:reference_id]
|
|
560
|
+
subscription_id = metadata[:subscription_id]
|
|
561
|
+
return unless plan && reference_id && subscription_id
|
|
562
|
+
|
|
563
|
+
update = stripe_subscription_state(stripe_subscription, include_status: true).merge(
|
|
564
|
+
plan: plan[:name].to_s.downcase,
|
|
565
|
+
stripeSubscriptionId: object[:subscription],
|
|
566
|
+
seats: stripe_fetch(item, "quantity"),
|
|
567
|
+
trialStart: stripe_time(stripe_fetch(stripe_subscription, "trial_start")),
|
|
568
|
+
trialEnd: stripe_time(stripe_fetch(stripe_subscription, "trial_end"))
|
|
569
|
+
).compact
|
|
570
|
+
db_subscription = ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription_id}], update: update)
|
|
571
|
+
plan.dig(:free_trial, :on_trial_start)&.call(db_subscription) if db_subscription && update[:trialStart]
|
|
572
|
+
callback = config.dig(:subscription, :on_subscription_complete)
|
|
573
|
+
callback&.call({event: event, subscription: db_subscription, stripeSubscription: stripe_subscription, stripe_subscription: stripe_subscription, plan: plan}, ctx)
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def stripe_on_subscription_created(ctx, event)
|
|
577
|
+
config = ctx.context.options.plugins.find { |plugin| plugin.id == "stripe" }&.options || {}
|
|
578
|
+
object = normalize_hash(event.dig(:data, :object) || {})
|
|
579
|
+
customer_id = object[:customer].to_s
|
|
580
|
+
return if customer_id.empty?
|
|
581
|
+
metadata = normalize_hash(object[:metadata] || {})
|
|
582
|
+
existing = if metadata[:subscription_id]
|
|
583
|
+
ctx.context.adapter.find_one(model: "subscription", where: [{field: "id", value: metadata[:subscription_id]}])
|
|
584
|
+
else
|
|
585
|
+
ctx.context.adapter.find_one(model: "subscription", where: [{field: "stripeSubscriptionId", value: object[:id]}])
|
|
586
|
+
end
|
|
587
|
+
return if existing
|
|
588
|
+
|
|
589
|
+
reference = stripe_reference_by_customer(ctx, config, customer_id) || ((metadata[:reference_id] && metadata[:plan]) ? {reference_id: metadata[:reference_id], customer_type: metadata[:customer_type] || "user"} : nil)
|
|
590
|
+
return unless reference
|
|
591
|
+
item = stripe_subscription_item(object)
|
|
592
|
+
return unless item
|
|
593
|
+
plan = stripe_plan_by_price_info(config, stripe_fetch(stripe_fetch(item, "price") || {}, "id"), stripe_fetch(stripe_fetch(item, "price") || {}, "lookup_key")) || (metadata[:plan] && stripe_plan_by_name(config, metadata[:plan]))
|
|
594
|
+
return unless plan
|
|
595
|
+
|
|
596
|
+
created = ctx.context.adapter.create(
|
|
597
|
+
model: "subscription",
|
|
598
|
+
data: stripe_subscription_state(object, include_status: true).merge(
|
|
599
|
+
referenceId: reference.fetch(:reference_id),
|
|
600
|
+
stripeCustomerId: customer_id,
|
|
601
|
+
stripeSubscriptionId: object[:id],
|
|
602
|
+
plan: plan[:name].to_s.downcase,
|
|
603
|
+
seats: stripe_fetch(item, "quantity"),
|
|
604
|
+
limits: plan[:limits]
|
|
605
|
+
).compact
|
|
606
|
+
)
|
|
607
|
+
config.dig(:subscription, :on_subscription_created)&.call({event: event, subscription: created, stripeSubscription: object, stripe_subscription: object, plan: plan})
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def stripe_on_subscription_updated(ctx, event)
|
|
611
|
+
config = ctx.context.options.plugins.find { |plugin| plugin.id == "stripe" }&.options || {}
|
|
612
|
+
object = normalize_hash(event.dig(:data, :object) || {})
|
|
613
|
+
item = stripe_subscription_item(object)
|
|
614
|
+
return unless item
|
|
615
|
+
|
|
616
|
+
metadata = normalize_hash(object[:metadata] || {})
|
|
617
|
+
subscription = if metadata[:subscription_id]
|
|
618
|
+
ctx.context.adapter.find_one(model: "subscription", where: [{field: "id", value: metadata[:subscription_id]}])
|
|
619
|
+
else
|
|
620
|
+
ctx.context.adapter.find_one(model: "subscription", where: [{field: "stripeSubscriptionId", value: object[:id]}])
|
|
621
|
+
end
|
|
622
|
+
unless subscription
|
|
623
|
+
candidates = ctx.context.adapter.find_many(model: "subscription", where: [{field: "stripeCustomerId", value: object[:customer]}])
|
|
624
|
+
subscription = if candidates.length > 1
|
|
625
|
+
candidates.find { |entry| stripe_active_or_trialing?(entry) }
|
|
626
|
+
else
|
|
627
|
+
candidates.first
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
return unless subscription
|
|
631
|
+
|
|
632
|
+
plan = stripe_plan_by_price_info(config, stripe_fetch(stripe_fetch(item, "price") || {}, "id"), stripe_fetch(stripe_fetch(item, "price") || {}, "lookup_key"))
|
|
633
|
+
was_pending = stripe_pending_cancel?(subscription)
|
|
634
|
+
update = stripe_subscription_state(object, include_status: true).merge(
|
|
635
|
+
stripeSubscriptionId: object[:id],
|
|
636
|
+
seats: stripe_fetch(item, "quantity")
|
|
637
|
+
)
|
|
638
|
+
update[:plan] = plan[:name].to_s.downcase if plan
|
|
639
|
+
update[:limits] = plan[:limits] if plan&.key?(:limits)
|
|
640
|
+
updated = ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: update.compact)
|
|
641
|
+
if object[:status] == "active" && stripe_stripe_pending_cancel?(object) && !was_pending
|
|
642
|
+
config.dig(:subscription, :on_subscription_cancel)&.call({event: event, subscription: subscription, stripeSubscription: object, stripe_subscription: object, cancellationDetails: object[:cancellation_details], cancellation_details: object[:cancellation_details]})
|
|
643
|
+
end
|
|
644
|
+
config.dig(:subscription, :on_subscription_update)&.call({event: event, subscription: updated || subscription})
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def stripe_on_subscription_deleted(ctx, event)
|
|
648
|
+
config = ctx.context.options.plugins.find { |plugin| plugin.id == "stripe" }&.options || {}
|
|
649
|
+
object = normalize_hash(event.dig(:data, :object) || {})
|
|
650
|
+
subscription = ctx.context.adapter.find_one(model: "subscription", where: [{field: "stripeSubscriptionId", value: object[:id]}])
|
|
651
|
+
return unless subscription
|
|
652
|
+
|
|
653
|
+
ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: stripe_subscription_state(object, include_status: false).merge(status: "canceled"))
|
|
654
|
+
config.dig(:subscription, :on_subscription_deleted)&.call({event: event, subscription: subscription, stripeSubscription: object, stripe_subscription: object})
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def stripe_create_customer(config, ctx, user, metadata = nil)
|
|
658
|
+
customer = stripe_find_or_create_user_customer(config, user, metadata, ctx)
|
|
659
|
+
id = stripe_id(customer)
|
|
660
|
+
ctx.context.internal_adapter.update_user(user.fetch("id"), stripeCustomerId: id)
|
|
661
|
+
id
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def stripe_find_or_create_user_customer(config, user, metadata = nil, ctx = nil)
|
|
665
|
+
existing = stripe_client(config).customers.search(query: "email:\"#{stripe_escape_search(user["email"])}\" AND -metadata[\"customerType\"]:\"organization\"", limit: 1)
|
|
666
|
+
customer = Array(stripe_fetch(existing, "data")).first
|
|
667
|
+
if customer
|
|
668
|
+
stripe_notify_customer_created(config, customer, user, ctx)
|
|
669
|
+
return customer
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
raw_extra = config[:get_customer_create_params]&.call(user, ctx) || {}
|
|
673
|
+
extra_metadata = stripe_fetch(raw_extra, "metadata")
|
|
674
|
+
extra = normalize_hash(raw_extra)
|
|
675
|
+
params = stripe_deep_merge(
|
|
676
|
+
extra,
|
|
677
|
+
email: user["email"],
|
|
678
|
+
name: user["name"],
|
|
679
|
+
metadata: stripe_customer_metadata_set({userId: user["id"], customerType: "user"}, metadata, extra_metadata)
|
|
680
|
+
)
|
|
681
|
+
params[:metadata] = stripe_customer_metadata_set({userId: user["id"], customerType: "user"}, metadata, extra_metadata)
|
|
682
|
+
customer = stripe_client(config).customers.create(params)
|
|
683
|
+
stripe_notify_customer_created(config, customer, user, ctx)
|
|
684
|
+
customer
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def stripe_organization_customer(config, ctx, organization_id, metadata = nil)
|
|
688
|
+
raise APIError.new("BAD_REQUEST", message: "Organization integration requires the organization plugin") unless config.dig(:organization, :enabled)
|
|
689
|
+
|
|
690
|
+
org = ctx.context.adapter.find_one(model: "organization", where: [{field: "id", value: organization_id}])
|
|
691
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless org
|
|
692
|
+
return org["stripeCustomerId"] if org["stripeCustomerId"]
|
|
693
|
+
|
|
694
|
+
existing = stripe_client(config).customers.search(query: "metadata[\"organizationId\"]:\"#{stripe_escape_search(org["id"])}\"", limit: 1)
|
|
695
|
+
customer = Array(stripe_fetch(existing, "data")).first
|
|
696
|
+
unless customer
|
|
697
|
+
raw_extra = config.dig(:organization, :get_customer_create_params)&.call(org, ctx) || {}
|
|
698
|
+
extra_metadata = stripe_fetch(raw_extra, "metadata")
|
|
699
|
+
extra = normalize_hash(raw_extra)
|
|
700
|
+
params = stripe_deep_merge(
|
|
701
|
+
extra,
|
|
702
|
+
name: org["name"],
|
|
703
|
+
metadata: stripe_customer_metadata_set({organizationId: org["id"], customerType: "organization"}, metadata, extra_metadata)
|
|
704
|
+
)
|
|
705
|
+
params[:metadata] = stripe_customer_metadata_set({organizationId: org["id"], customerType: "organization"}, metadata, extra_metadata)
|
|
706
|
+
customer = stripe_client(config).customers.create(params)
|
|
707
|
+
config.dig(:organization, :on_customer_create)&.call({stripeCustomer: customer, stripe_customer: customer, organization: org.merge("stripeCustomerId" => stripe_id(customer))}, ctx)
|
|
708
|
+
end
|
|
709
|
+
ctx.context.adapter.update(model: "organization", where: [{field: "id", value: org.fetch("id")}], update: {stripeCustomerId: stripe_id(customer)})
|
|
710
|
+
stripe_id(customer)
|
|
711
|
+
rescue APIError
|
|
712
|
+
raise
|
|
713
|
+
rescue
|
|
714
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("UNABLE_TO_CREATE_CUSTOMER"))
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
def stripe_client(config)
|
|
718
|
+
injected = config[:stripe_client] || config[:client]
|
|
719
|
+
return injected if injected
|
|
720
|
+
return config[:_stripe_client_adapter] if config[:_stripe_client_adapter]
|
|
721
|
+
|
|
722
|
+
api_key = config[:stripe_api_key] || ENV["STRIPE_SECRET_KEY"]
|
|
723
|
+
raise APIError.new("INTERNAL_SERVER_ERROR", message: "Stripe client is required") if api_key.to_s.empty?
|
|
724
|
+
|
|
725
|
+
config[:_stripe_client_adapter] = BetterAuth::Stripe::ClientAdapter.new(api_key)
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
def stripe_id(object)
|
|
729
|
+
stripe_fetch(object, "id")
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
def stripe_fetch(object, key)
|
|
733
|
+
return nil unless object.respond_to?(:[])
|
|
734
|
+
|
|
735
|
+
object[key] || object[key.to_sym]
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def stripe_time(value)
|
|
739
|
+
return nil unless value
|
|
740
|
+
|
|
741
|
+
Time.at(value.to_i)
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
def stripe_subscription_options(config)
|
|
745
|
+
normalize_hash(config[:subscription] || {})
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
def stripe_plans(config)
|
|
749
|
+
plans = stripe_subscription_options(config)[:plans] || []
|
|
750
|
+
plans = plans.call if plans.respond_to?(:call)
|
|
751
|
+
Array(plans).map { |plan| normalize_hash(plan) }
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
def stripe_plan_by_name(config, name)
|
|
755
|
+
stripe_plans(config).find { |plan| plan[:name].to_s.downcase == name.to_s.downcase }
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
def stripe_plan_by_price_info(config, price_id, lookup_key = nil)
|
|
759
|
+
stripe_plans(config).find do |plan|
|
|
760
|
+
plan[:price_id] == price_id || plan[:annual_discount_price_id] == price_id || (lookup_key && (plan[:lookup_key] == lookup_key || plan[:annual_discount_lookup_key] == lookup_key))
|
|
761
|
+
end
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
def stripe_price_id(config, plan, annual = false)
|
|
765
|
+
annual ? (plan[:annual_discount_price_id] || stripe_resolve_lookup(config, plan[:annual_discount_lookup_key])) : (plan[:price_id] || stripe_resolve_lookup(config, plan[:lookup_key]))
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
def stripe_resolve_lookup(config, lookup_key)
|
|
769
|
+
return nil if lookup_key.to_s.empty?
|
|
770
|
+
return nil unless stripe_client(config).respond_to?(:prices)
|
|
771
|
+
|
|
772
|
+
prices = stripe_client(config).prices.list(lookup_keys: [lookup_key], active: true, limit: 1)
|
|
773
|
+
stripe_fetch(Array(stripe_fetch(prices, "data")).first || {}, "id")
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
def stripe_reference_id!(ctx, session, customer_type, explicit_reference_id, config)
|
|
777
|
+
return explicit_reference_id || session.fetch(:user).fetch("id") unless customer_type == "organization"
|
|
778
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("ORGANIZATION_SUBSCRIPTION_NOT_ENABLED")) unless config.dig(:organization, :enabled)
|
|
779
|
+
|
|
780
|
+
reference_id = explicit_reference_id || session.fetch(:session)["activeOrganizationId"]
|
|
781
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) if reference_id.to_s.empty?
|
|
782
|
+
reference_id
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
def stripe_authorize_reference!(ctx, session, reference_id, action, customer_type, subscription_options, explicit: false)
|
|
786
|
+
callback = subscription_options[:authorize_reference]
|
|
787
|
+
if customer_type == "organization"
|
|
788
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("AUTHORIZE_REFERENCE_REQUIRED")) unless callback
|
|
789
|
+
elsif !explicit || reference_id == session.fetch(:user).fetch("id")
|
|
790
|
+
return
|
|
791
|
+
elsif !callback
|
|
792
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("REFERENCE_ID_NOT_ALLOWED"))
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
allowed = callback.call({user: session.fetch(:user), session: session.fetch(:session), referenceId: reference_id, reference_id: reference_id, action: action}, ctx)
|
|
796
|
+
raise APIError.new("UNAUTHORIZED", message: STRIPE_ERROR_CODES.fetch("UNAUTHORIZED")) unless allowed
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
def stripe_find_subscription_for_action(ctx, reference_id, subscription_id, active_only:)
|
|
800
|
+
subscription = if subscription_id
|
|
801
|
+
ctx.context.adapter.find_one(model: "subscription", where: [{field: "stripeSubscriptionId", value: subscription_id}])
|
|
802
|
+
else
|
|
803
|
+
ctx.context.adapter.find_many(model: "subscription", where: [{field: "referenceId", value: reference_id}]).find { |entry| !active_only || stripe_active_or_trialing?(entry) }
|
|
804
|
+
end
|
|
805
|
+
return nil if subscription_id && subscription && subscription["referenceId"] != reference_id
|
|
806
|
+
|
|
807
|
+
subscription
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
def stripe_active_subscription(ctx, reference_id)
|
|
811
|
+
ctx.context.adapter.find_many(model: "subscription", where: [{field: "referenceId", value: reference_id}]).find { |entry| stripe_active_or_trialing?(entry) }
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
def stripe_active_subscriptions(config, customer_id)
|
|
815
|
+
result = stripe_client(config).subscriptions.list(customer: customer_id)
|
|
816
|
+
Array(stripe_fetch(result, "data")).select { |entry| stripe_active_or_trialing?(entry) }
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
def stripe_active_or_trialing?(subscription)
|
|
820
|
+
%w[active trialing].include?(stripe_fetch(subscription, "status").to_s)
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
def stripe_pending_cancel?(subscription)
|
|
824
|
+
!!(stripe_fetch(subscription, "cancelAtPeriodEnd") || stripe_fetch(subscription, "cancelAt"))
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
def stripe_stripe_pending_cancel?(subscription)
|
|
828
|
+
!!(stripe_fetch(subscription, "cancel_at_period_end") || stripe_fetch(subscription, "cancel_at"))
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
def stripe_subscription_item(subscription)
|
|
832
|
+
Array(stripe_fetch(stripe_fetch(subscription, "items") || {}, "data")).first
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
def stripe_subscription_state(subscription, include_status: true)
|
|
836
|
+
item = stripe_subscription_item(subscription)
|
|
837
|
+
state = {
|
|
838
|
+
periodStart: stripe_time(stripe_fetch(item || subscription, "current_period_start")),
|
|
839
|
+
periodEnd: stripe_time(stripe_fetch(item || subscription, "current_period_end")),
|
|
840
|
+
cancelAtPeriodEnd: stripe_fetch(subscription, "cancel_at_period_end"),
|
|
841
|
+
cancelAt: stripe_time(stripe_fetch(subscription, "cancel_at")),
|
|
842
|
+
canceledAt: stripe_time(stripe_fetch(subscription, "canceled_at")),
|
|
843
|
+
endedAt: stripe_time(stripe_fetch(subscription, "ended_at")),
|
|
844
|
+
trialStart: stripe_time(stripe_fetch(subscription, "trial_start")),
|
|
845
|
+
trialEnd: stripe_time(stripe_fetch(subscription, "trial_end"))
|
|
846
|
+
}
|
|
847
|
+
state[:status] = stripe_fetch(subscription, "status") if include_status
|
|
848
|
+
state.compact
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
def stripe_reference_by_customer(ctx, config, customer_id)
|
|
852
|
+
if config.dig(:organization, :enabled)
|
|
853
|
+
org = ctx.context.adapter.find_one(model: "organization", where: [{field: "stripeCustomerId", value: customer_id}])
|
|
854
|
+
return {customer_type: "organization", reference_id: org.fetch("id")} if org
|
|
855
|
+
end
|
|
856
|
+
user = ctx.context.adapter.find_one(model: "user", where: [{field: "stripeCustomerId", value: customer_id}])
|
|
857
|
+
return {customer_type: "user", reference_id: user.fetch("id")} if user
|
|
858
|
+
|
|
859
|
+
nil
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
def stripe_metadata(internal, *user_metadata)
|
|
863
|
+
user_metadata.compact
|
|
864
|
+
.reduce({}) do |acc, entry|
|
|
865
|
+
next acc unless entry.respond_to?(:each)
|
|
866
|
+
|
|
867
|
+
acc.merge(entry.each_with_object({}) { |(key, value), result| result[stripe_metadata_key(key)] = value })
|
|
868
|
+
end
|
|
869
|
+
.merge(internal.transform_keys { |key| stripe_metadata_key(key) })
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
def stripe_customer_metadata_set(internal_fields, *user_metadata)
|
|
873
|
+
stripe_metadata(internal_fields, *user_metadata)
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
def stripe_customer_metadata_get(metadata)
|
|
877
|
+
{
|
|
878
|
+
userId: stripe_metadata_fetch(metadata, "userId"),
|
|
879
|
+
organizationId: stripe_metadata_fetch(metadata, "organizationId"),
|
|
880
|
+
customerType: stripe_metadata_fetch(metadata, "customerType")
|
|
881
|
+
}
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
def stripe_subscription_metadata_set(internal_fields, *user_metadata)
|
|
885
|
+
stripe_metadata(internal_fields, *user_metadata)
|
|
886
|
+
end
|
|
887
|
+
|
|
888
|
+
def stripe_subscription_metadata_get(metadata)
|
|
889
|
+
{
|
|
890
|
+
userId: stripe_metadata_fetch(metadata, "userId"),
|
|
891
|
+
subscriptionId: stripe_metadata_fetch(metadata, "subscriptionId"),
|
|
892
|
+
referenceId: stripe_metadata_fetch(metadata, "referenceId")
|
|
893
|
+
}
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
def stripe_notify_customer_created(config, customer, user, ctx)
|
|
897
|
+
config[:on_customer_create]&.call(
|
|
898
|
+
{
|
|
899
|
+
stripeCustomer: customer,
|
|
900
|
+
stripe_customer: customer,
|
|
901
|
+
user: user.merge("stripeCustomerId" => stripe_id(customer))
|
|
902
|
+
},
|
|
903
|
+
ctx
|
|
904
|
+
)
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
def stripe_metadata_key(key)
|
|
908
|
+
case normalize_key(key)
|
|
909
|
+
when :user_id then "userId"
|
|
910
|
+
when :organization_id then "organizationId"
|
|
911
|
+
when :customer_type then "customerType"
|
|
912
|
+
when :subscription_id then "subscriptionId"
|
|
913
|
+
when :reference_id then "referenceId"
|
|
914
|
+
else
|
|
915
|
+
key.to_s
|
|
916
|
+
end
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
def stripe_metadata_fetch(metadata, key)
|
|
920
|
+
return nil unless metadata.respond_to?(:[])
|
|
921
|
+
|
|
922
|
+
metadata[key] || metadata[key.to_sym] || metadata[normalize_key(key)] || metadata[normalize_key(key).to_s]
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
def stripe_deep_merge(base, override)
|
|
926
|
+
normalize_hash(base).merge(normalize_hash(override)) do |_key, old, new|
|
|
927
|
+
if old.is_a?(Hash) && new.is_a?(Hash)
|
|
928
|
+
stripe_deep_merge(old, new)
|
|
929
|
+
else
|
|
930
|
+
new
|
|
931
|
+
end
|
|
932
|
+
end
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
def stripe_redirect?(body)
|
|
936
|
+
body[:disable_redirect] != true
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
def stripe_stringify_keys(value)
|
|
940
|
+
return value unless value.is_a?(Hash)
|
|
941
|
+
|
|
942
|
+
value.each_with_object({}) do |(key, object), result|
|
|
943
|
+
result[key.to_s] = object
|
|
944
|
+
result[key.to_sym] = object
|
|
945
|
+
end
|
|
946
|
+
end
|
|
947
|
+
|
|
948
|
+
def stripe_url(ctx, url)
|
|
949
|
+
return url if url.to_s.match?(/\A[a-zA-Z][a-zA-Z0-9+\-.]*:/)
|
|
950
|
+
|
|
951
|
+
"#{ctx.context.base_url}#{url.to_s.start_with?("/") ? url : "/#{url}"}"
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
def stripe_escape_search(value)
|
|
955
|
+
value.to_s.gsub("\"", "\\\"")
|
|
956
|
+
end
|
|
957
|
+
end
|
|
958
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: better_auth-stripe
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sebastian Sala
|
|
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: better_auth
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
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'
|
|
33
|
+
- - "<"
|
|
34
|
+
- !ruby/object:Gem::Version
|
|
35
|
+
version: '20'
|
|
36
|
+
type: :runtime
|
|
37
|
+
prerelease: false
|
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '13'
|
|
43
|
+
- - "<"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '20'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: bundler
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - "~>"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '2.5'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '2.5'
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: minitest
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '5.25'
|
|
67
|
+
type: :development
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '5.25'
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: rake
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '13.2'
|
|
81
|
+
type: :development
|
|
82
|
+
prerelease: false
|
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - "~>"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '13.2'
|
|
88
|
+
- !ruby/object:Gem::Dependency
|
|
89
|
+
name: standardrb
|
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - "~>"
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '1.0'
|
|
95
|
+
type: :development
|
|
96
|
+
prerelease: false
|
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - "~>"
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '1.0'
|
|
102
|
+
description: Adds Stripe customer, subscription, checkout, billing portal, and webhook
|
|
103
|
+
routes for Better Auth Ruby.
|
|
104
|
+
email:
|
|
105
|
+
- sebastian.sala.tech@gmail.com
|
|
106
|
+
executables: []
|
|
107
|
+
extensions: []
|
|
108
|
+
extra_rdoc_files: []
|
|
109
|
+
files:
|
|
110
|
+
- CHANGELOG.md
|
|
111
|
+
- README.md
|
|
112
|
+
- lib/better_auth/plugins/stripe.rb
|
|
113
|
+
- lib/better_auth/stripe.rb
|
|
114
|
+
- lib/better_auth/stripe/version.rb
|
|
115
|
+
homepage: https://github.com/sebasxsala/better-auth
|
|
116
|
+
licenses:
|
|
117
|
+
- MIT
|
|
118
|
+
metadata:
|
|
119
|
+
homepage_uri: https://github.com/sebasxsala/better-auth
|
|
120
|
+
source_code_uri: https://github.com/sebasxsala/better-auth
|
|
121
|
+
changelog_uri: https://github.com/sebasxsala/better-auth/blob/main/packages/better_auth-stripe/CHANGELOG.md
|
|
122
|
+
bug_tracker_uri: https://github.com/sebasxsala/better-auth/issues
|
|
123
|
+
rdoc_options: []
|
|
124
|
+
require_paths:
|
|
125
|
+
- lib
|
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - ">="
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: 3.2.0
|
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
132
|
+
requirements:
|
|
133
|
+
- - ">="
|
|
134
|
+
- !ruby/object:Gem::Version
|
|
135
|
+
version: '0'
|
|
136
|
+
requirements: []
|
|
137
|
+
rubygems_version: 3.6.9
|
|
138
|
+
specification_version: 4
|
|
139
|
+
summary: Stripe plugin package for Better Auth Ruby
|
|
140
|
+
test_files: []
|