better_auth-stripe 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +22 -1
- data/lib/better_auth/plugins/stripe.rb +395 -61
- data/lib/better_auth/stripe/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48af5990d2d12e5e32d5393e5af619a69e0bb6a1f8c4c1d12c092eb75486279b
|
|
4
|
+
data.tar.gz: 3fcb40459ec56c1ec7dfaabfd9a4063b1399140b2c9e95704f332dc1acc2ce14
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '019497cea0a84e06440b5405fd220f0da1507a500b4e42c95a7b31e8d8befd2e1636750232d6268b3a80a4302de84b5ee84695fd8e311cb32f4c6b6b80147565'
|
|
7
|
+
data.tar.gz: b5cd4a313e2480e6e2f7420242840dccf1a3081a2f535b123c07f6c55a20220e3313930463bb4221eb23dd7838bf062c673c1330a3e82af3d5b8f6b37ee92685
|
data/CHANGELOG.md
CHANGED
|
@@ -2,4 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.2.1] - 2026-04-30
|
|
6
|
+
|
|
7
|
+
- Fixed Stripe checkout and subscription parity edge cases for reused customer IDs, plugin-owned schedule releases, missing checkout sessions, plan limits, and organization reference validation.
|
|
8
|
+
- Expanded Stripe organization and subscription parity coverage.
|
|
9
|
+
|
|
10
|
+
## [0.2.0] - 2026-04-29
|
|
11
|
+
|
|
12
|
+
- Aligned Stripe subscription, checkout, portal, webhook, customer, and organization flows with upstream Better Auth behavior.
|
|
13
|
+
- Expanded Stripe documentation and tests for subscription lifecycle and organization billing parity.
|
|
14
|
+
|
|
15
|
+
## [0.1.0] - 2026-04-28
|
|
16
|
+
|
|
5
17
|
- Initial external Stripe package extracted from `better_auth`.
|
data/README.md
CHANGED
|
@@ -19,14 +19,35 @@ auth = BetterAuth.auth(
|
|
|
19
19
|
plugins: [
|
|
20
20
|
BetterAuth::Plugins.stripe(
|
|
21
21
|
stripe_api_key: ENV.fetch("STRIPE_SECRET_KEY"),
|
|
22
|
-
stripe_webhook_secret: ENV.fetch("STRIPE_WEBHOOK_SECRET")
|
|
22
|
+
stripe_webhook_secret: ENV.fetch("STRIPE_WEBHOOK_SECRET"),
|
|
23
|
+
subscription: {
|
|
24
|
+
enabled: true,
|
|
25
|
+
plans: [
|
|
26
|
+
{ name: "pro", price_id: "price_monthly", annual_discount_price_id: "price_yearly" },
|
|
27
|
+
{ name: "team", price_id: "price_team", seat_price_id: "price_team_seat" }
|
|
28
|
+
]
|
|
29
|
+
}
|
|
23
30
|
)
|
|
24
31
|
]
|
|
25
32
|
)
|
|
26
33
|
```
|
|
27
34
|
|
|
35
|
+
## Subscription Options
|
|
36
|
+
|
|
37
|
+
Set `subscription: { enabled: true, plans: [...] }` to enable checkout, portal, restore, list, and webhook subscription handling. Plans support `name`, `price_id`, `lookup_key`, `annual_discount_price_id`, `annual_discount_lookup_key`, `limits`, `free_trial`, `seat_price_id`, `line_items`, and `proration_behavior`.
|
|
38
|
+
|
|
39
|
+
Organization subscriptions require `organization: { enabled: true }` and an `authorize_reference` callback. When a plan has `seat_price_id`, organization member changes sync the Stripe seat item quantity.
|
|
40
|
+
|
|
28
41
|
## Notes
|
|
29
42
|
|
|
30
43
|
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
44
|
|
|
32
45
|
Pass `stripe_client:` when you need a custom Stripe client, Stripe Connect behavior, or a test double.
|
|
46
|
+
|
|
47
|
+
## Subscriptions
|
|
48
|
+
|
|
49
|
+
Configure plans under `subscription: { enabled: true, plans: [...] }`. Ruby accepts upstream-equivalent plan keys including `price_id`, `annual_discount_price_id`, lookup-key variants, `limits`, `free_trial`, and `seat_price_id`.
|
|
50
|
+
|
|
51
|
+
For organization subscriptions, `seat_price_id` enables upstream-style seat billing. Checkout sends the base plan item with quantity `1` and a separate seat item whose quantity is the current organization member count. Webhooks read the seat item quantity back into the local `subscription.seats` field.
|
|
52
|
+
|
|
53
|
+
`scheduleAtPeriodEnd` / `schedule_at_period_end` on `/subscription/upgrade` creates a Stripe subscription schedule for active subscriptions, stores `stripeScheduleId`, and returns the configured `returnUrl` instead of opening the billing portal immediately.
|
|
@@ -6,7 +6,7 @@ require "stripe"
|
|
|
6
6
|
module BetterAuth
|
|
7
7
|
module Stripe
|
|
8
8
|
class ClientAdapter
|
|
9
|
-
attr_reader :customers, :checkout, :billing_portal, :subscriptions, :prices, :webhooks
|
|
9
|
+
attr_reader :customers, :checkout, :billing_portal, :subscriptions, :prices, :subscription_schedules, :webhooks
|
|
10
10
|
|
|
11
11
|
def initialize(api_key)
|
|
12
12
|
client = ::Stripe::StripeClient.new(api_key)
|
|
@@ -15,6 +15,7 @@ module BetterAuth
|
|
|
15
15
|
@billing_portal = NamespaceAdapter.new(sessions: ResourceAdapter.new(client.v1.billing_portal.sessions))
|
|
16
16
|
@subscriptions = ResourceAdapter.new(client.v1.subscriptions)
|
|
17
17
|
@prices = ResourceAdapter.new(client.v1.prices)
|
|
18
|
+
@subscription_schedules = ResourceAdapter.new(client.v1.subscription_schedules)
|
|
18
19
|
@webhooks = WebhooksAdapter.new
|
|
19
20
|
end
|
|
20
21
|
end
|
|
@@ -52,6 +53,10 @@ module BetterAuth
|
|
|
52
53
|
def update(id, params = {})
|
|
53
54
|
@resource.update(id, params || {})
|
|
54
55
|
end
|
|
56
|
+
|
|
57
|
+
def release(id)
|
|
58
|
+
@resource.release(id)
|
|
59
|
+
end
|
|
55
60
|
end
|
|
56
61
|
|
|
57
62
|
class WebhooksAdapter
|
|
@@ -95,8 +100,10 @@ module BetterAuth
|
|
|
95
100
|
"STRIPE_WEBHOOK_SECRET_NOT_FOUND" => "Stripe webhook secret not found",
|
|
96
101
|
"FAILED_TO_CONSTRUCT_STRIPE_EVENT" => "Failed to construct Stripe event",
|
|
97
102
|
"STRIPE_WEBHOOK_ERROR" => "Stripe webhook error",
|
|
103
|
+
"INVALID_CUSTOMER_TYPE" => "Customer type must be either user or organization",
|
|
98
104
|
"INVALID_REQUEST_BODY" => "Invalid request body"
|
|
99
105
|
}.freeze
|
|
106
|
+
STRIPE_UNSAFE_METADATA_KEYS = %w[__proto__ constructor prototype].freeze
|
|
100
107
|
|
|
101
108
|
def stripe(options = {})
|
|
102
109
|
config = normalize_hash(options)
|
|
@@ -135,6 +142,8 @@ module BetterAuth
|
|
|
135
142
|
canceledAt: {type: "date", required: false},
|
|
136
143
|
endedAt: {type: "date", required: false},
|
|
137
144
|
seats: {type: "number", required: false},
|
|
145
|
+
billingInterval: {type: "string", required: false},
|
|
146
|
+
stripeScheduleId: {type: "string", required: false},
|
|
138
147
|
limits: {type: "json", required: false}
|
|
139
148
|
}
|
|
140
149
|
}
|
|
@@ -172,6 +181,8 @@ module BetterAuth
|
|
|
172
181
|
data["id"] ||= SecureRandom.hex(16)
|
|
173
182
|
customer = stripe_find_or_create_user_customer(config, data, nil, hook_ctx)
|
|
174
183
|
{data: {id: data["id"], stripeCustomerId: stripe_id(customer)}}
|
|
184
|
+
rescue
|
|
185
|
+
nil
|
|
175
186
|
end
|
|
176
187
|
},
|
|
177
188
|
update: {
|
|
@@ -216,7 +227,10 @@ module BetterAuth
|
|
|
216
227
|
!%w[canceled incomplete incomplete_expired].include?(stripe_fetch(subscription, "status").to_s)
|
|
217
228
|
end
|
|
218
229
|
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("ORGANIZATION_HAS_ACTIVE_SUBSCRIPTION")) if active
|
|
219
|
-
end
|
|
230
|
+
end,
|
|
231
|
+
after_add_member: ->(data, ctx) { stripe_sync_organization_seats(config, data, ctx) },
|
|
232
|
+
after_remove_member: ->(data, ctx) { stripe_sync_organization_seats(config, data, ctx) },
|
|
233
|
+
after_accept_invitation: ->(data, ctx) { stripe_sync_organization_seats(config, data, ctx) }
|
|
220
234
|
}
|
|
221
235
|
end
|
|
222
236
|
|
|
@@ -225,7 +239,7 @@ module BetterAuth
|
|
|
225
239
|
session = Routes.current_session(ctx)
|
|
226
240
|
body = normalize_hash(ctx.body)
|
|
227
241
|
subscription_options = stripe_subscription_options(config)
|
|
228
|
-
customer_type = (body
|
|
242
|
+
customer_type = stripe_customer_type!(body)
|
|
229
243
|
reference_id = stripe_reference_id!(ctx, session, customer_type, body[:reference_id], config)
|
|
230
244
|
stripe_authorize_reference!(ctx, session, reference_id, "upgrade-subscription", customer_type, subscription_options, explicit: body.key?(:reference_id))
|
|
231
245
|
|
|
@@ -243,13 +257,14 @@ module BetterAuth
|
|
|
243
257
|
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_FOUND")) unless subscription_to_update && subscription_to_update["referenceId"] == reference_id
|
|
244
258
|
end
|
|
245
259
|
|
|
260
|
+
subscriptions = subscription_to_update ? [subscription_to_update] : ctx.context.adapter.find_many(model: "subscription", where: [{field: "referenceId", value: reference_id}])
|
|
261
|
+
reference_customer_id = subscriptions.find { |entry| entry["stripeCustomerId"] }&.fetch("stripeCustomerId", nil)
|
|
246
262
|
customer_id = if customer_type == "organization"
|
|
247
|
-
subscription_to_update&.fetch("stripeCustomerId", nil) || stripe_organization_customer(config, ctx, reference_id, body[:metadata])
|
|
263
|
+
subscription_to_update&.fetch("stripeCustomerId", nil) || reference_customer_id || stripe_organization_customer(config, ctx, reference_id, body[:metadata])
|
|
248
264
|
else
|
|
249
|
-
subscription_to_update&.fetch("stripeCustomerId", nil) || user["stripeCustomerId"] || stripe_create_customer(config, ctx, user, body[:metadata])
|
|
265
|
+
subscription_to_update&.fetch("stripeCustomerId", nil) || reference_customer_id || user["stripeCustomerId"] || stripe_create_customer(config, ctx, user, body[:metadata])
|
|
250
266
|
end
|
|
251
267
|
|
|
252
|
-
subscriptions = subscription_to_update ? [subscription_to_update] : ctx.context.adapter.find_many(model: "subscription", where: [{field: "referenceId", value: reference_id}])
|
|
253
268
|
active_or_trialing = subscriptions.find { |entry| stripe_active_or_trialing?(entry) }
|
|
254
269
|
active_stripe_subscriptions = stripe_active_subscriptions(config, customer_id)
|
|
255
270
|
active_stripe = active_stripe_subscriptions.find do |entry|
|
|
@@ -264,11 +279,16 @@ module BetterAuth
|
|
|
264
279
|
|
|
265
280
|
price_id = stripe_price_id(config, plan, body[:annual])
|
|
266
281
|
raise APIError.new("BAD_REQUEST", message: "Price ID not found for the selected plan") if price_id.to_s.empty?
|
|
282
|
+
auto_managed_seats = !!(plan[:seat_price_id] && customer_type == "organization")
|
|
283
|
+
member_count = auto_managed_seats ? ctx.context.adapter.count(model: "member", where: [{field: "organizationId", value: reference_id}]) : 0
|
|
284
|
+
requested_seats = auto_managed_seats ? member_count : (body[:seats] || 1)
|
|
285
|
+
seat_only_plan = auto_managed_seats && plan[:seat_price_id] == price_id
|
|
267
286
|
|
|
268
|
-
|
|
287
|
+
active_resolved = active_stripe && stripe_resolve_plan_item(config, active_stripe)
|
|
288
|
+
active_stripe_item = active_resolved&.fetch(:item, nil) || stripe_subscription_item(active_stripe || {})
|
|
269
289
|
stripe_price_id_value = stripe_fetch(stripe_fetch(active_stripe_item || {}, "price") || {}, "id")
|
|
270
290
|
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 ==
|
|
291
|
+
same_seats = auto_managed_seats || (active_or_trialing && active_or_trialing["seats"].to_i == requested_seats.to_i)
|
|
272
292
|
same_price = !active_stripe || stripe_price_id_value == price_id
|
|
273
293
|
valid_period = !active_or_trialing || !active_or_trialing["periodEnd"] || active_or_trialing["periodEnd"] > Time.now
|
|
274
294
|
if active_or_trialing&.fetch("status", nil) == "active" && same_plan && same_seats && same_price && valid_period
|
|
@@ -276,6 +296,19 @@ module BetterAuth
|
|
|
276
296
|
end
|
|
277
297
|
|
|
278
298
|
if active_stripe
|
|
299
|
+
stripe_release_plugin_schedule(ctx, config, customer_id, active_stripe, active_or_trialing || subscription_to_update)
|
|
300
|
+
|
|
301
|
+
if body[:schedule_at_period_end]
|
|
302
|
+
url = stripe_schedule_plan_change(ctx, config, active_stripe, active_or_trialing, plan, price_id, requested_seats, seat_only_plan, body)
|
|
303
|
+
next ctx.json({url: url, redirect: stripe_redirect?(body)})
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
old_plan = active_or_trialing && stripe_plan_by_name(config, active_or_trialing["plan"])
|
|
307
|
+
if stripe_direct_subscription_update?(old_plan, plan, auto_managed_seats)
|
|
308
|
+
url = stripe_update_active_subscription_items(ctx, config, active_stripe, active_or_trialing, old_plan, plan, price_id, requested_seats, seat_only_plan, body)
|
|
309
|
+
next ctx.json({url: url, redirect: stripe_redirect?(body)})
|
|
310
|
+
end
|
|
311
|
+
|
|
279
312
|
portal = stripe_client(config).billing_portal.sessions.create(
|
|
280
313
|
customer: customer_id,
|
|
281
314
|
return_url: stripe_url(ctx, body[:return_url] || "/"),
|
|
@@ -284,7 +317,7 @@ module BetterAuth
|
|
|
284
317
|
after_completion: {type: "redirect", redirect: {return_url: stripe_url(ctx, body[:return_url] || "/")}},
|
|
285
318
|
subscription_update_confirm: {
|
|
286
319
|
subscription: stripe_fetch(active_stripe, "id"),
|
|
287
|
-
items: [
|
|
320
|
+
items: [stripe_line_item(config, price_id, requested_seats).merge(id: stripe_fetch(active_stripe_item || {}, "id"))]
|
|
288
321
|
}
|
|
289
322
|
}
|
|
290
323
|
)
|
|
@@ -294,12 +327,12 @@ module BetterAuth
|
|
|
294
327
|
incomplete = subscriptions.find { |entry| entry["status"] == "incomplete" }
|
|
295
328
|
subscription = active_or_trialing || incomplete
|
|
296
329
|
if subscription
|
|
297
|
-
update = {plan: plan[:name].to_s.downcase, seats:
|
|
330
|
+
update = {plan: plan[:name].to_s.downcase, seats: requested_seats}
|
|
298
331
|
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
332
|
else
|
|
300
333
|
subscription = ctx.context.adapter.create(
|
|
301
334
|
model: "subscription",
|
|
302
|
-
data: {plan: plan[:name].to_s.downcase, referenceId: reference_id, stripeCustomerId: customer_id, status: "incomplete", seats:
|
|
335
|
+
data: {plan: plan[:name].to_s.downcase, referenceId: reference_id, stripeCustomerId: customer_id, status: "incomplete", seats: requested_seats, limits: plan[:limits]}
|
|
303
336
|
)
|
|
304
337
|
end
|
|
305
338
|
|
|
@@ -323,9 +356,9 @@ module BetterAuth
|
|
|
323
356
|
customer: customer_id,
|
|
324
357
|
customer_update: (customer_type == "user") ? {name: "auto", address: "auto"} : {address: "auto"},
|
|
325
358
|
locale: body[:locale],
|
|
326
|
-
success_url: stripe_url(ctx, "#{ctx.context.base_url}/subscription/success?callbackURL=#{Rack::Utils.escape(body[:success_url] || "/")}&
|
|
359
|
+
success_url: stripe_url(ctx, "#{ctx.context.base_url}/subscription/success?callbackURL=#{Rack::Utils.escape(body[:success_url] || "/")}&checkoutSessionId={CHECKOUT_SESSION_ID}"),
|
|
327
360
|
cancel_url: stripe_url(ctx, body[:cancel_url] || "/"),
|
|
328
|
-
line_items:
|
|
361
|
+
line_items: stripe_checkout_line_items(config, plan, price_id, requested_seats, auto_managed_seats, seat_only_plan),
|
|
329
362
|
subscription_data: free_trial.merge(metadata: subscription_metadata),
|
|
330
363
|
mode: "subscription",
|
|
331
364
|
client_reference_id: reference_id,
|
|
@@ -343,7 +376,7 @@ module BetterAuth
|
|
|
343
376
|
Endpoint.new(path: "/subscription/cancel", method: "POST") do |ctx|
|
|
344
377
|
session = Routes.current_session(ctx)
|
|
345
378
|
body = normalize_hash(ctx.body)
|
|
346
|
-
customer_type = (body
|
|
379
|
+
customer_type = stripe_customer_type!(body)
|
|
347
380
|
reference_id = stripe_reference_id!(ctx, session, customer_type, body[:reference_id], config)
|
|
348
381
|
stripe_authorize_reference!(ctx, session, reference_id, "cancel-subscription", customer_type, stripe_subscription_options(config), explicit: body.key?(:reference_id))
|
|
349
382
|
subscription = stripe_find_subscription_for_action(ctx, reference_id, body[:subscription_id], active_only: true)
|
|
@@ -378,12 +411,22 @@ module BetterAuth
|
|
|
378
411
|
Endpoint.new(path: "/subscription/restore", method: "POST") do |ctx|
|
|
379
412
|
session = Routes.current_session(ctx)
|
|
380
413
|
body = normalize_hash(ctx.body)
|
|
381
|
-
customer_type = (body
|
|
414
|
+
customer_type = stripe_customer_type!(body)
|
|
382
415
|
reference_id = stripe_reference_id!(ctx, session, customer_type, body[:reference_id], config)
|
|
383
416
|
stripe_authorize_reference!(ctx, session, reference_id, "restore-subscription", customer_type, stripe_subscription_options(config), explicit: body.key?(:reference_id))
|
|
384
417
|
subscription = stripe_find_subscription_for_action(ctx, reference_id, body[:subscription_id], active_only: false)
|
|
385
418
|
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_FOUND")) unless subscription && subscription["stripeCustomerId"]
|
|
386
419
|
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_ACTIVE")) unless stripe_active_or_trialing?(subscription)
|
|
420
|
+
|
|
421
|
+
if subscription["stripeScheduleId"]
|
|
422
|
+
schedule = stripe_client(config).subscription_schedules.retrieve(subscription["stripeScheduleId"])
|
|
423
|
+
if stripe_fetch(schedule, "status") == "active"
|
|
424
|
+
schedule = stripe_client(config).subscription_schedules.release(subscription["stripeScheduleId"])
|
|
425
|
+
end
|
|
426
|
+
ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: {stripeScheduleId: nil})
|
|
427
|
+
next ctx.json(stripe_stringify_keys(schedule))
|
|
428
|
+
end
|
|
429
|
+
|
|
387
430
|
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION")) unless stripe_pending_cancel?(subscription)
|
|
388
431
|
|
|
389
432
|
active = stripe_active_subscriptions(config, subscription["stripeCustomerId"]).first
|
|
@@ -406,14 +449,19 @@ module BetterAuth
|
|
|
406
449
|
Endpoint.new(path: "/subscription/list", method: "GET") do |ctx|
|
|
407
450
|
session = Routes.current_session(ctx)
|
|
408
451
|
query = normalize_hash(ctx.query)
|
|
409
|
-
customer_type = (query
|
|
452
|
+
customer_type = stripe_customer_type!(query)
|
|
410
453
|
reference_id = stripe_reference_id!(ctx, session, customer_type, query[:reference_id], config)
|
|
411
454
|
stripe_authorize_reference!(ctx, session, reference_id, "list-subscription", customer_type, stripe_subscription_options(config), explicit: query.key?(:reference_id))
|
|
412
455
|
plans = stripe_plans(config)
|
|
413
456
|
subscriptions = ctx.context.adapter.find_many(model: "subscription", where: [{field: "referenceId", value: reference_id}]).select { |entry| stripe_active_or_trialing?(entry) }
|
|
414
457
|
ctx.json(subscriptions.map do |entry|
|
|
415
458
|
plan = plans.find { |item| item[:name].to_s.downcase == entry["plan"].to_s.downcase }
|
|
416
|
-
entry
|
|
459
|
+
price_id = if entry["billingInterval"] == "year"
|
|
460
|
+
plan&.fetch(:annual_discount_price_id, nil) || plan&.fetch(:price_id, nil)
|
|
461
|
+
else
|
|
462
|
+
plan&.fetch(:price_id, nil)
|
|
463
|
+
end
|
|
464
|
+
entry.merge("limits" => plan&.fetch(:limits, nil), "priceId" => price_id)
|
|
417
465
|
end)
|
|
418
466
|
end
|
|
419
467
|
end
|
|
@@ -422,7 +470,7 @@ module BetterAuth
|
|
|
422
470
|
Endpoint.new(path: "/subscription/billing-portal", method: "POST") do |ctx|
|
|
423
471
|
session = Routes.current_session(ctx)
|
|
424
472
|
body = normalize_hash(ctx.body)
|
|
425
|
-
customer_type = (body
|
|
473
|
+
customer_type = stripe_customer_type!(body)
|
|
426
474
|
reference_id = stripe_reference_id!(ctx, session, customer_type, body[:reference_id], config)
|
|
427
475
|
stripe_authorize_reference!(ctx, session, reference_id, "billing-portal", customer_type, stripe_subscription_options(config), explicit: body.key?(:reference_id))
|
|
428
476
|
customer_id = if customer_type == "organization"
|
|
@@ -468,13 +516,28 @@ module BetterAuth
|
|
|
468
516
|
Endpoint.new(path: "/subscription/success", method: "GET") do |ctx|
|
|
469
517
|
query = normalize_hash(ctx.query)
|
|
470
518
|
callback = query[:callback_url] || "/"
|
|
471
|
-
|
|
519
|
+
checkout_session_id = query[:checkout_session_id]
|
|
520
|
+
subscription_id = query[:subscription_id]
|
|
521
|
+
if checkout_session_id
|
|
522
|
+
callback = callback.to_s.gsub("{CHECKOUT_SESSION_ID}", checkout_session_id.to_s)
|
|
523
|
+
checkout_session = begin
|
|
524
|
+
stripe_client(config || {}).checkout.sessions.retrieve(checkout_session_id)
|
|
525
|
+
rescue
|
|
526
|
+
nil
|
|
527
|
+
end
|
|
528
|
+
raise ctx.redirect(stripe_url(ctx, callback)) unless checkout_session
|
|
529
|
+
|
|
530
|
+
metadata = normalize_hash(stripe_fetch(checkout_session || {}, "metadata") || {})
|
|
531
|
+
subscription_id = metadata[:subscription_id]
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
unless subscription_id
|
|
472
535
|
raise ctx.redirect(stripe_url(ctx, callback))
|
|
473
536
|
end
|
|
474
537
|
session = Routes.current_session(ctx, allow_nil: true)
|
|
475
538
|
raise ctx.redirect(stripe_url(ctx, callback)) unless session
|
|
476
539
|
|
|
477
|
-
subscription = ctx.context.adapter.find_one(model: "subscription", where: [{field: "id", value:
|
|
540
|
+
subscription = ctx.context.adapter.find_one(model: "subscription", where: [{field: "id", value: subscription_id}])
|
|
478
541
|
raise ctx.redirect(stripe_url(ctx, callback)) unless subscription
|
|
479
542
|
raise ctx.redirect(stripe_url(ctx, callback)) if stripe_active_or_trialing?(subscription)
|
|
480
543
|
|
|
@@ -483,15 +546,16 @@ module BetterAuth
|
|
|
483
546
|
|
|
484
547
|
stripe_subscription = stripe_active_subscriptions(config || {}, customer_id).first
|
|
485
548
|
if stripe_subscription
|
|
486
|
-
|
|
487
|
-
|
|
549
|
+
resolved = stripe_resolve_plan_item(config || {}, stripe_subscription)
|
|
550
|
+
item = resolved&.fetch(:item, nil)
|
|
551
|
+
plan = resolved&.fetch(:plan, nil)
|
|
488
552
|
if item && plan
|
|
489
553
|
ctx.context.adapter.update(
|
|
490
554
|
model: "subscription",
|
|
491
555
|
where: [{field: "id", value: subscription.fetch("id")}],
|
|
492
|
-
update: stripe_subscription_state(stripe_subscription, include_status: true).merge(
|
|
556
|
+
update: stripe_subscription_state(stripe_subscription, include_status: true, compact: false).merge(
|
|
493
557
|
plan: plan[:name].to_s.downcase,
|
|
494
|
-
seats:
|
|
558
|
+
seats: stripe_resolve_quantity(stripe_subscription, item, plan),
|
|
495
559
|
stripeSubscriptionId: stripe_fetch(stripe_subscription, "id")
|
|
496
560
|
)
|
|
497
561
|
)
|
|
@@ -508,23 +572,27 @@ module BetterAuth
|
|
|
508
572
|
|
|
509
573
|
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
574
|
|
|
511
|
-
event =
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
webhooks.
|
|
575
|
+
event = begin
|
|
576
|
+
if stripe_client(config).respond_to?(:webhooks)
|
|
577
|
+
webhooks = stripe_client(config).webhooks
|
|
578
|
+
if webhooks.respond_to?(:construct_event_async)
|
|
579
|
+
webhooks.construct_event_async(ctx.body, signature, config[:stripe_webhook_secret])
|
|
580
|
+
else
|
|
581
|
+
webhooks.construct_event(ctx.body, signature, config[:stripe_webhook_secret])
|
|
582
|
+
end
|
|
515
583
|
else
|
|
516
|
-
|
|
584
|
+
ctx.body
|
|
517
585
|
end
|
|
518
|
-
|
|
519
|
-
|
|
586
|
+
rescue
|
|
587
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("FAILED_TO_CONSTRUCT_STRIPE_EVENT"))
|
|
520
588
|
end
|
|
521
589
|
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("FAILED_TO_CONSTRUCT_STRIPE_EVENT")) unless event
|
|
522
|
-
|
|
590
|
+
begin
|
|
591
|
+
stripe_handle_event(ctx, event)
|
|
592
|
+
rescue
|
|
593
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("STRIPE_WEBHOOK_ERROR"))
|
|
594
|
+
end
|
|
523
595
|
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
596
|
end
|
|
529
597
|
end
|
|
530
598
|
|
|
@@ -551,10 +619,11 @@ module BetterAuth
|
|
|
551
619
|
return if object[:mode] == "setup" || !config.dig(:subscription, :enabled)
|
|
552
620
|
|
|
553
621
|
stripe_subscription = stripe_client(config).subscriptions.retrieve(object[:subscription])
|
|
554
|
-
|
|
555
|
-
return unless
|
|
622
|
+
resolved = stripe_resolve_plan_item(config, stripe_subscription)
|
|
623
|
+
return unless resolved
|
|
556
624
|
|
|
557
|
-
|
|
625
|
+
item = resolved.fetch(:item)
|
|
626
|
+
plan = resolved.fetch(:plan)
|
|
558
627
|
metadata = normalize_hash(object[:metadata] || {})
|
|
559
628
|
reference_id = object[:client_reference_id] || metadata[:reference_id]
|
|
560
629
|
subscription_id = metadata[:subscription_id]
|
|
@@ -563,7 +632,7 @@ module BetterAuth
|
|
|
563
632
|
update = stripe_subscription_state(stripe_subscription, include_status: true).merge(
|
|
564
633
|
plan: plan[:name].to_s.downcase,
|
|
565
634
|
stripeSubscriptionId: object[:subscription],
|
|
566
|
-
seats:
|
|
635
|
+
seats: stripe_resolve_quantity(stripe_subscription, item, plan),
|
|
567
636
|
trialStart: stripe_time(stripe_fetch(stripe_subscription, "trial_start")),
|
|
568
637
|
trialEnd: stripe_time(stripe_fetch(stripe_subscription, "trial_end"))
|
|
569
638
|
).compact
|
|
@@ -588,9 +657,10 @@ module BetterAuth
|
|
|
588
657
|
|
|
589
658
|
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
659
|
return unless reference
|
|
591
|
-
|
|
592
|
-
return unless
|
|
593
|
-
|
|
660
|
+
resolved = stripe_resolve_plan_item(config, object)
|
|
661
|
+
return unless resolved
|
|
662
|
+
item = resolved.fetch(:item)
|
|
663
|
+
plan = resolved[:plan] || (metadata[:plan] && stripe_plan_by_name(config, metadata[:plan]))
|
|
594
664
|
return unless plan
|
|
595
665
|
|
|
596
666
|
created = ctx.context.adapter.create(
|
|
@@ -600,7 +670,7 @@ module BetterAuth
|
|
|
600
670
|
stripeCustomerId: customer_id,
|
|
601
671
|
stripeSubscriptionId: object[:id],
|
|
602
672
|
plan: plan[:name].to_s.downcase,
|
|
603
|
-
seats:
|
|
673
|
+
seats: stripe_resolve_quantity(object, item, plan),
|
|
604
674
|
limits: plan[:limits]
|
|
605
675
|
).compact
|
|
606
676
|
)
|
|
@@ -610,8 +680,9 @@ module BetterAuth
|
|
|
610
680
|
def stripe_on_subscription_updated(ctx, event)
|
|
611
681
|
config = ctx.context.options.plugins.find { |plugin| plugin.id == "stripe" }&.options || {}
|
|
612
682
|
object = normalize_hash(event.dig(:data, :object) || {})
|
|
613
|
-
|
|
614
|
-
return unless
|
|
683
|
+
resolved = stripe_resolve_plan_item(config, object)
|
|
684
|
+
return unless resolved
|
|
685
|
+
item = resolved.fetch(:item)
|
|
615
686
|
|
|
616
687
|
metadata = normalize_hash(object[:metadata] || {})
|
|
617
688
|
subscription = if metadata[:subscription_id]
|
|
@@ -629,19 +700,25 @@ module BetterAuth
|
|
|
629
700
|
end
|
|
630
701
|
return unless subscription
|
|
631
702
|
|
|
632
|
-
plan =
|
|
703
|
+
plan = resolved[:plan]
|
|
633
704
|
was_pending = stripe_pending_cancel?(subscription)
|
|
634
|
-
update = stripe_subscription_state(object, include_status: true).merge(
|
|
705
|
+
update = stripe_subscription_state(object, include_status: true, compact: false).merge(
|
|
635
706
|
stripeSubscriptionId: object[:id],
|
|
636
|
-
seats:
|
|
707
|
+
seats: stripe_resolve_quantity(object, item, plan)
|
|
637
708
|
)
|
|
638
709
|
update[:plan] = plan[:name].to_s.downcase if plan
|
|
639
710
|
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
|
|
711
|
+
updated = ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: update)
|
|
641
712
|
if object[:status] == "active" && stripe_stripe_pending_cancel?(object) && !was_pending
|
|
642
713
|
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
714
|
end
|
|
644
715
|
config.dig(:subscription, :on_subscription_update)&.call({event: event, subscription: updated || subscription})
|
|
716
|
+
if plan && subscription["status"] == "trialing" && object[:status] == "active"
|
|
717
|
+
plan.dig(:free_trial, :on_trial_end)&.call({subscription: subscription}, ctx)
|
|
718
|
+
end
|
|
719
|
+
if plan && subscription["status"] == "trialing" && object[:status] == "incomplete_expired"
|
|
720
|
+
plan.dig(:free_trial, :on_trial_expired)&.call(subscription, ctx)
|
|
721
|
+
end
|
|
645
722
|
end
|
|
646
723
|
|
|
647
724
|
def stripe_on_subscription_deleted(ctx, event)
|
|
@@ -650,7 +727,7 @@ module BetterAuth
|
|
|
650
727
|
subscription = ctx.context.adapter.find_one(model: "subscription", where: [{field: "stripeSubscriptionId", value: object[:id]}])
|
|
651
728
|
return unless subscription
|
|
652
729
|
|
|
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"))
|
|
730
|
+
ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: stripe_subscription_state(object, include_status: false, compact: false).merge(status: "canceled", stripeScheduleId: nil))
|
|
654
731
|
config.dig(:subscription, :on_subscription_deleted)&.call({event: event, subscription: subscription, stripeSubscription: object, stripe_subscription: object})
|
|
655
732
|
end
|
|
656
733
|
|
|
@@ -662,8 +739,7 @@ module BetterAuth
|
|
|
662
739
|
end
|
|
663
740
|
|
|
664
741
|
def stripe_find_or_create_user_customer(config, user, metadata = nil, ctx = nil)
|
|
665
|
-
|
|
666
|
-
customer = Array(stripe_fetch(existing, "data")).first
|
|
742
|
+
customer = stripe_find_user_customer(config, user["email"])
|
|
667
743
|
if customer
|
|
668
744
|
stripe_notify_customer_created(config, customer, user, ctx)
|
|
669
745
|
return customer
|
|
@@ -691,8 +767,7 @@ module BetterAuth
|
|
|
691
767
|
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless org
|
|
692
768
|
return org["stripeCustomerId"] if org["stripeCustomerId"]
|
|
693
769
|
|
|
694
|
-
|
|
695
|
-
customer = Array(stripe_fetch(existing, "data")).first
|
|
770
|
+
customer = stripe_find_organization_customer(config, org["id"])
|
|
696
771
|
unless customer
|
|
697
772
|
raw_extra = config.dig(:organization, :get_customer_create_params)&.call(org, ctx) || {}
|
|
698
773
|
extra_metadata = stripe_fetch(raw_extra, "metadata")
|
|
@@ -748,7 +823,12 @@ module BetterAuth
|
|
|
748
823
|
def stripe_plans(config)
|
|
749
824
|
plans = stripe_subscription_options(config)[:plans] || []
|
|
750
825
|
plans = plans.call if plans.respond_to?(:call)
|
|
751
|
-
Array(plans).map
|
|
826
|
+
Array(plans).map do |plan|
|
|
827
|
+
normalized = normalize_hash(plan)
|
|
828
|
+
limits = stripe_fetch(plan, "limits")
|
|
829
|
+
normalized[:limits] = limits if limits
|
|
830
|
+
normalized
|
|
831
|
+
end
|
|
752
832
|
end
|
|
753
833
|
|
|
754
834
|
def stripe_plan_by_name(config, name)
|
|
@@ -778,7 +858,7 @@ module BetterAuth
|
|
|
778
858
|
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("ORGANIZATION_SUBSCRIPTION_NOT_ENABLED")) unless config.dig(:organization, :enabled)
|
|
779
859
|
|
|
780
860
|
reference_id = explicit_reference_id || session.fetch(:session)["activeOrganizationId"]
|
|
781
|
-
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("
|
|
861
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("ORGANIZATION_REFERENCE_ID_REQUIRED")) if reference_id.to_s.empty?
|
|
782
862
|
reference_id
|
|
783
863
|
end
|
|
784
864
|
|
|
@@ -796,6 +876,41 @@ module BetterAuth
|
|
|
796
876
|
raise APIError.new("UNAUTHORIZED", message: STRIPE_ERROR_CODES.fetch("UNAUTHORIZED")) unless allowed
|
|
797
877
|
end
|
|
798
878
|
|
|
879
|
+
def stripe_customer_type!(source)
|
|
880
|
+
customer_type = (source[:customer_type] || "user").to_s
|
|
881
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("INVALID_CUSTOMER_TYPE")) unless %w[user organization].include?(customer_type)
|
|
882
|
+
|
|
883
|
+
customer_type
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
def stripe_find_user_customer(config, email)
|
|
887
|
+
customers = stripe_client(config).customers
|
|
888
|
+
begin
|
|
889
|
+
existing = customers.search(query: "email:\"#{stripe_escape_search(email)}\" AND -metadata[\"customerType\"]:\"organization\"", limit: 1)
|
|
890
|
+
Array(stripe_fetch(existing, "data")).first
|
|
891
|
+
rescue
|
|
892
|
+
listed = customers.list(email: email, limit: 100)
|
|
893
|
+
Array(stripe_fetch(listed, "data")).find do |customer|
|
|
894
|
+
stripe_metadata_fetch(stripe_fetch(customer, "metadata") || {}, "customerType") != "organization"
|
|
895
|
+
end
|
|
896
|
+
end
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
def stripe_find_organization_customer(config, organization_id)
|
|
900
|
+
customers = stripe_client(config).customers
|
|
901
|
+
begin
|
|
902
|
+
existing = customers.search(query: "metadata[\"organizationId\"]:\"#{stripe_escape_search(organization_id)}\" AND metadata[\"customerType\"]:\"organization\"", limit: 1)
|
|
903
|
+
Array(stripe_fetch(existing, "data")).first
|
|
904
|
+
rescue
|
|
905
|
+
listed = customers.list(limit: 100)
|
|
906
|
+
Array(stripe_fetch(listed, "data")).find do |customer|
|
|
907
|
+
metadata = stripe_fetch(customer, "metadata") || {}
|
|
908
|
+
stripe_metadata_fetch(metadata, "organizationId") == organization_id &&
|
|
909
|
+
stripe_metadata_fetch(metadata, "customerType") == "organization"
|
|
910
|
+
end
|
|
911
|
+
end
|
|
912
|
+
end
|
|
913
|
+
|
|
799
914
|
def stripe_find_subscription_for_action(ctx, reference_id, subscription_id, active_only:)
|
|
800
915
|
subscription = if subscription_id
|
|
801
916
|
ctx.context.adapter.find_one(model: "subscription", where: [{field: "stripeSubscriptionId", value: subscription_id}])
|
|
@@ -832,8 +947,214 @@ module BetterAuth
|
|
|
832
947
|
Array(stripe_fetch(stripe_fetch(subscription, "items") || {}, "data")).first
|
|
833
948
|
end
|
|
834
949
|
|
|
835
|
-
def
|
|
950
|
+
def stripe_resolve_plan_item(config, subscription)
|
|
951
|
+
items = Array(stripe_fetch(stripe_fetch(subscription, "items") || {}, "data"))
|
|
952
|
+
first = items.first
|
|
953
|
+
return nil unless first
|
|
954
|
+
|
|
955
|
+
items.each do |item|
|
|
956
|
+
price = stripe_fetch(item, "price") || {}
|
|
957
|
+
plan = stripe_plan_by_price_info(config, stripe_fetch(price, "id"), stripe_fetch(price, "lookup_key"))
|
|
958
|
+
return {item: item, plan: plan} if plan
|
|
959
|
+
end
|
|
960
|
+
{item: first, plan: nil} if items.length == 1
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
def stripe_resolve_quantity(subscription, plan_item, plan = nil)
|
|
964
|
+
items = Array(stripe_fetch(stripe_fetch(subscription, "items") || {}, "data"))
|
|
965
|
+
seat_price_id = plan && plan[:seat_price_id]
|
|
966
|
+
seat_item = seat_price_id && items.find { |item| stripe_fetch(stripe_fetch(item, "price") || {}, "id") == seat_price_id }
|
|
967
|
+
stripe_fetch(seat_item || plan_item, "quantity") || 1
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
def stripe_line_item(config, price_id, quantity)
|
|
971
|
+
item = {price: price_id}
|
|
972
|
+
item[:quantity] = quantity unless stripe_metered_price?(config, price_id)
|
|
973
|
+
item
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
def stripe_checkout_line_items(config, plan, price_id, quantity, auto_managed_seats, seat_only_plan)
|
|
977
|
+
items = []
|
|
978
|
+
items << stripe_line_item(config, price_id, auto_managed_seats ? 1 : quantity) unless seat_only_plan
|
|
979
|
+
items << {price: plan[:seat_price_id], quantity: quantity} if auto_managed_seats && plan[:seat_price_id]
|
|
980
|
+
items.concat(stripe_plan_line_items(plan))
|
|
981
|
+
items
|
|
982
|
+
end
|
|
983
|
+
|
|
984
|
+
def stripe_plan_line_items(plan)
|
|
985
|
+
Array(plan[:line_items]).map do |item|
|
|
986
|
+
item.is_a?(Hash) ? normalize_hash(item) : item
|
|
987
|
+
end
|
|
988
|
+
end
|
|
989
|
+
|
|
990
|
+
def stripe_schedule_plan_change(ctx, config, active_stripe, db_subscription, plan, price_id, quantity, seat_only_plan, body)
|
|
991
|
+
schedule = stripe_client(config).subscription_schedules.create(from_subscription: stripe_fetch(active_stripe, "id"))
|
|
992
|
+
current_phase = Array(stripe_fetch(schedule, "phases")).first || {}
|
|
993
|
+
current_items = Array(stripe_fetch(current_phase, "items"))
|
|
994
|
+
active_item = stripe_resolve_plan_item(config, active_stripe)&.fetch(:item, nil) || stripe_subscription_item(active_stripe)
|
|
995
|
+
active_price_id = stripe_fetch(stripe_fetch(active_item || {}, "price") || {}, "id")
|
|
996
|
+
replaced = false
|
|
997
|
+
new_items = current_items.filter_map do |item|
|
|
998
|
+
item_price = stripe_fetch(item, "price")
|
|
999
|
+
item_price = stripe_fetch(item_price, "id") if item_price.is_a?(Hash)
|
|
1000
|
+
if item_price == active_price_id
|
|
1001
|
+
replaced = true
|
|
1002
|
+
next nil if seat_only_plan
|
|
1003
|
+
|
|
1004
|
+
stripe_line_item(config, price_id, quantity)
|
|
1005
|
+
else
|
|
1006
|
+
{price: item_price, quantity: stripe_fetch(item, "quantity")}.compact
|
|
1007
|
+
end
|
|
1008
|
+
end
|
|
1009
|
+
new_items << stripe_line_item(config, price_id, quantity) unless replaced || seat_only_plan
|
|
1010
|
+
new_items << {price: plan[:seat_price_id], quantity: quantity} if plan[:seat_price_id]
|
|
1011
|
+
new_items.concat(stripe_plan_line_items(plan))
|
|
1012
|
+
|
|
1013
|
+
stripe_client(config).subscription_schedules.update(
|
|
1014
|
+
stripe_fetch(schedule, "id"),
|
|
1015
|
+
metadata: {source: "@better-auth/stripe"},
|
|
1016
|
+
end_behavior: "release",
|
|
1017
|
+
phases: [
|
|
1018
|
+
{
|
|
1019
|
+
items: current_items.map do |item|
|
|
1020
|
+
item_price = stripe_fetch(item, "price")
|
|
1021
|
+
item_price = stripe_fetch(item_price, "id") if item_price.is_a?(Hash)
|
|
1022
|
+
{price: item_price, quantity: stripe_fetch(item, "quantity")}.compact
|
|
1023
|
+
end,
|
|
1024
|
+
start_date: stripe_fetch(current_phase, "start_date"),
|
|
1025
|
+
end_date: stripe_fetch(current_phase, "end_date")
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
items: new_items,
|
|
1029
|
+
start_date: stripe_fetch(current_phase, "end_date"),
|
|
1030
|
+
proration_behavior: "none"
|
|
1031
|
+
}
|
|
1032
|
+
]
|
|
1033
|
+
)
|
|
1034
|
+
if db_subscription
|
|
1035
|
+
ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: db_subscription.fetch("id")}], update: {stripeScheduleId: stripe_fetch(schedule, "id")})
|
|
1036
|
+
end
|
|
1037
|
+
stripe_url(ctx, body[:return_url] || "/")
|
|
1038
|
+
end
|
|
1039
|
+
|
|
1040
|
+
def stripe_release_plugin_schedule(ctx, config, customer_id, active_stripe, db_subscription)
|
|
1041
|
+
return unless stripe_schedule_id(active_stripe)
|
|
1042
|
+
return unless stripe_client(config).respond_to?(:subscription_schedules)
|
|
1043
|
+
|
|
1044
|
+
schedules = stripe_client(config).subscription_schedules.list(customer: customer_id)
|
|
1045
|
+
active_subscription_id = stripe_fetch(active_stripe, "id")
|
|
1046
|
+
existing = Array(stripe_fetch(schedules, "data")).find do |schedule|
|
|
1047
|
+
subscription = stripe_fetch(schedule, "subscription")
|
|
1048
|
+
schedule_subscription_id = subscription.is_a?(Hash) ? stripe_id(subscription) : subscription
|
|
1049
|
+
metadata = stripe_fetch(schedule, "metadata") || {}
|
|
1050
|
+
schedule_subscription_id == active_subscription_id &&
|
|
1051
|
+
stripe_fetch(schedule, "status") == "active" &&
|
|
1052
|
+
stripe_metadata_fetch(metadata, "source") == "@better-auth/stripe"
|
|
1053
|
+
end
|
|
1054
|
+
return unless existing
|
|
1055
|
+
|
|
1056
|
+
stripe_client(config).subscription_schedules.release(stripe_id(existing))
|
|
1057
|
+
if db_subscription
|
|
1058
|
+
ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: db_subscription.fetch("id")}], update: {stripeScheduleId: nil})
|
|
1059
|
+
end
|
|
1060
|
+
end
|
|
1061
|
+
|
|
1062
|
+
def stripe_direct_subscription_update?(old_plan, plan, auto_managed_seats)
|
|
1063
|
+
return true if auto_managed_seats && old_plan && old_plan[:seat_price_id] != plan[:seat_price_id]
|
|
1064
|
+
|
|
1065
|
+
stripe_plan_line_items(old_plan || {}).map { |item| item[:price] } != stripe_plan_line_items(plan).map { |item| item[:price] }
|
|
1066
|
+
end
|
|
1067
|
+
|
|
1068
|
+
def stripe_update_active_subscription_items(ctx, config, active_stripe, db_subscription, old_plan, plan, price_id, quantity, seat_only_plan, body)
|
|
1069
|
+
active_item = stripe_resolve_plan_item(config, active_stripe)&.fetch(:item, nil) || stripe_subscription_item(active_stripe)
|
|
1070
|
+
active_price_id = stripe_fetch(stripe_fetch(active_item || {}, "price") || {}, "id")
|
|
1071
|
+
old_line_prices = stripe_plan_line_items(old_plan || {}).map { |item| item[:price] }
|
|
1072
|
+
new_line_prices = stripe_plan_line_items(plan).map { |item| item[:price] }
|
|
1073
|
+
added_line_prices = new_line_prices - old_line_prices
|
|
1074
|
+
items = []
|
|
1075
|
+
Array(stripe_fetch(stripe_fetch(active_stripe, "items") || {}, "data")).each do |item|
|
|
1076
|
+
item_price = stripe_fetch(stripe_fetch(item, "price") || {}, "id")
|
|
1077
|
+
if item_price == active_price_id
|
|
1078
|
+
items << stripe_line_item(config, price_id, plan[:seat_price_id] ? 1 : quantity).merge(id: stripe_fetch(item, "id")) unless seat_only_plan
|
|
1079
|
+
elsif old_plan && item_price == old_plan[:seat_price_id] && plan[:seat_price_id]
|
|
1080
|
+
items << {id: stripe_fetch(item, "id"), price: plan[:seat_price_id], quantity: quantity}
|
|
1081
|
+
elsif old_line_prices.include?(item_price)
|
|
1082
|
+
if new_line_prices.include?(item_price)
|
|
1083
|
+
new_line_prices.delete_at(new_line_prices.index(item_price))
|
|
1084
|
+
else
|
|
1085
|
+
items << {id: stripe_fetch(item, "id"), deleted: true}
|
|
1086
|
+
end
|
|
1087
|
+
end
|
|
1088
|
+
end
|
|
1089
|
+
items << {price: plan[:seat_price_id], quantity: quantity} if plan[:seat_price_id] && !items.any? { |item| item[:price] == plan[:seat_price_id] || item[:id] && item[:price] == plan[:seat_price_id] }
|
|
1090
|
+
added_line_prices.each { |price| items << {price: price} }
|
|
1091
|
+
stripe_client(config).subscriptions.update(stripe_fetch(active_stripe, "id"), items: items, proration_behavior: plan[:proration_behavior] || "create_prorations")
|
|
1092
|
+
if db_subscription
|
|
1093
|
+
ctx.context.adapter.update(
|
|
1094
|
+
model: "subscription",
|
|
1095
|
+
where: [{field: "id", value: db_subscription.fetch("id")}],
|
|
1096
|
+
update: {plan: plan[:name].to_s.downcase, seats: quantity, limits: plan[:limits], stripeScheduleId: nil}
|
|
1097
|
+
)
|
|
1098
|
+
end
|
|
1099
|
+
stripe_url(ctx, body[:return_url] || "/")
|
|
1100
|
+
end
|
|
1101
|
+
|
|
1102
|
+
def stripe_sync_organization_seats(config, data, ctx)
|
|
1103
|
+
organization = data[:organization] || data["organization"]
|
|
1104
|
+
return unless config.dig(:subscription, :enabled) && organization && organization["stripeCustomerId"]
|
|
1105
|
+
|
|
1106
|
+
member_count = ctx.context.adapter.count(model: "member", where: [{field: "organizationId", value: organization.fetch("id")}])
|
|
1107
|
+
seat_plans = stripe_plans(config).select { |plan| plan[:seat_price_id] }
|
|
1108
|
+
return if seat_plans.empty?
|
|
1109
|
+
|
|
1110
|
+
subscription = ctx.context.adapter.find_many(model: "subscription", where: [{field: "referenceId", value: organization.fetch("id")}]).find { |entry| stripe_active_or_trialing?(entry) }
|
|
1111
|
+
return unless subscription && subscription["stripeSubscriptionId"]
|
|
1112
|
+
|
|
1113
|
+
plan = seat_plans.find { |entry| entry[:name].to_s.downcase == subscription["plan"].to_s.downcase }
|
|
1114
|
+
return unless plan
|
|
1115
|
+
|
|
1116
|
+
stripe_subscription = stripe_client(config).subscriptions.retrieve(subscription["stripeSubscriptionId"])
|
|
1117
|
+
return unless stripe_active_or_trialing?(stripe_subscription)
|
|
1118
|
+
|
|
1119
|
+
items = Array(stripe_fetch(stripe_fetch(stripe_subscription, "items") || {}, "data"))
|
|
1120
|
+
seat_item = items.find { |item| stripe_fetch(stripe_fetch(item, "price") || {}, "id") == plan[:seat_price_id] }
|
|
1121
|
+
return if seat_item && stripe_fetch(seat_item, "quantity").to_i == member_count.to_i
|
|
1122
|
+
|
|
1123
|
+
update_items = if seat_item
|
|
1124
|
+
[{id: stripe_fetch(seat_item, "id"), quantity: member_count}]
|
|
1125
|
+
else
|
|
1126
|
+
[{price: plan[:seat_price_id], quantity: member_count}]
|
|
1127
|
+
end
|
|
1128
|
+
stripe_client(config).subscriptions.update(subscription["stripeSubscriptionId"], items: update_items, proration_behavior: plan[:proration_behavior] || "create_prorations")
|
|
1129
|
+
ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: {seats: member_count})
|
|
1130
|
+
rescue
|
|
1131
|
+
nil
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
def stripe_metered_price?(config, price_id, lookup_key = nil)
|
|
1135
|
+
price = stripe_resolve_stripe_price(config, price_id, lookup_key)
|
|
1136
|
+
recurring = stripe_fetch(price || {}, "recurring") || {}
|
|
1137
|
+
stripe_fetch(recurring, "usage_type") == "metered"
|
|
1138
|
+
end
|
|
1139
|
+
|
|
1140
|
+
def stripe_resolve_stripe_price(config, price_id, lookup_key = nil)
|
|
1141
|
+
return nil unless stripe_client(config).respond_to?(:prices)
|
|
1142
|
+
|
|
1143
|
+
prices = stripe_client(config).prices
|
|
1144
|
+
if lookup_key
|
|
1145
|
+
result = prices.list(lookup_keys: [lookup_key], active: true, limit: 1)
|
|
1146
|
+
Array(stripe_fetch(result, "data")).first
|
|
1147
|
+
elsif price_id && prices.respond_to?(:retrieve)
|
|
1148
|
+
prices.retrieve(price_id)
|
|
1149
|
+
end
|
|
1150
|
+
rescue
|
|
1151
|
+
nil
|
|
1152
|
+
end
|
|
1153
|
+
|
|
1154
|
+
def stripe_subscription_state(subscription, include_status: true, compact: true)
|
|
836
1155
|
item = stripe_subscription_item(subscription)
|
|
1156
|
+
price = stripe_fetch(item || {}, "price") || {}
|
|
1157
|
+
recurring = stripe_fetch(price, "recurring") || {}
|
|
837
1158
|
state = {
|
|
838
1159
|
periodStart: stripe_time(stripe_fetch(item || subscription, "current_period_start")),
|
|
839
1160
|
periodEnd: stripe_time(stripe_fetch(item || subscription, "current_period_end")),
|
|
@@ -842,10 +1163,20 @@ module BetterAuth
|
|
|
842
1163
|
canceledAt: stripe_time(stripe_fetch(subscription, "canceled_at")),
|
|
843
1164
|
endedAt: stripe_time(stripe_fetch(subscription, "ended_at")),
|
|
844
1165
|
trialStart: stripe_time(stripe_fetch(subscription, "trial_start")),
|
|
845
|
-
trialEnd: stripe_time(stripe_fetch(subscription, "trial_end"))
|
|
1166
|
+
trialEnd: stripe_time(stripe_fetch(subscription, "trial_end")),
|
|
1167
|
+
billingInterval: stripe_fetch(recurring, "interval"),
|
|
1168
|
+
stripeScheduleId: stripe_schedule_id(subscription)
|
|
846
1169
|
}
|
|
847
1170
|
state[:status] = stripe_fetch(subscription, "status") if include_status
|
|
848
|
-
state.compact
|
|
1171
|
+
compact ? state.compact : state
|
|
1172
|
+
end
|
|
1173
|
+
|
|
1174
|
+
def stripe_schedule_id(subscription)
|
|
1175
|
+
schedule = stripe_fetch(subscription, "schedule")
|
|
1176
|
+
return nil if schedule.nil?
|
|
1177
|
+
return schedule if schedule.is_a?(String)
|
|
1178
|
+
|
|
1179
|
+
stripe_id(schedule) || schedule.to_s
|
|
849
1180
|
end
|
|
850
1181
|
|
|
851
1182
|
def stripe_reference_by_customer(ctx, config, customer_id)
|
|
@@ -864,7 +1195,10 @@ module BetterAuth
|
|
|
864
1195
|
.reduce({}) do |acc, entry|
|
|
865
1196
|
next acc unless entry.respond_to?(:each)
|
|
866
1197
|
|
|
867
|
-
acc.merge(entry.each_with_object({})
|
|
1198
|
+
acc.merge(entry.each_with_object({}) do |(key, value), result|
|
|
1199
|
+
metadata_key = stripe_metadata_key(key)
|
|
1200
|
+
result[metadata_key] = value unless STRIPE_UNSAFE_METADATA_KEYS.include?(metadata_key)
|
|
1201
|
+
end)
|
|
868
1202
|
end
|
|
869
1203
|
.merge(internal.transform_keys { |key| stripe_metadata_key(key) })
|
|
870
1204
|
end
|