better_auth-stripe 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +22 -1
- data/lib/better_auth/plugins/stripe.rb +354 -56
- 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: cd6fea79b678b06d713919a087366ca4d6e0e160e5623e0e2b4c788e5041e7ae
|
|
4
|
+
data.tar.gz: 96fdab018ed1cdce8beb551052b36ebdea06d832daf178ea54727472ebd604a6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f8b8e916a57096cca616e465cdf4d5ab2688b4ed4592c794a8309f4c71523c8550750a8360b2e76e357743cb52258e214ddf04a28bb232e371302926a3af328d
|
|
7
|
+
data.tar.gz: 3b2f8193888d31bb65992147081a022c8952a152b3d5642b80364ad0c18e6c9a5e6cdce8c36c09287ba5823f3ffd95e65a82b92ced1f290024b0c69635bee93d
|
data/CHANGELOG.md
CHANGED
|
@@ -2,4 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.2.0] - 2026-04-29
|
|
6
|
+
|
|
7
|
+
- Aligned Stripe subscription, checkout, portal, webhook, customer, and organization flows with upstream Better Auth behavior.
|
|
8
|
+
- Expanded Stripe documentation and tests for subscription lifecycle and organization billing parity.
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-04-28
|
|
11
|
+
|
|
5
12
|
- 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
|
|
|
@@ -264,11 +278,16 @@ module BetterAuth
|
|
|
264
278
|
|
|
265
279
|
price_id = stripe_price_id(config, plan, body[:annual])
|
|
266
280
|
raise APIError.new("BAD_REQUEST", message: "Price ID not found for the selected plan") if price_id.to_s.empty?
|
|
281
|
+
auto_managed_seats = !!(plan[:seat_price_id] && customer_type == "organization")
|
|
282
|
+
member_count = auto_managed_seats ? ctx.context.adapter.count(model: "member", where: [{field: "organizationId", value: reference_id}]) : 0
|
|
283
|
+
requested_seats = auto_managed_seats ? member_count : (body[:seats] || 1)
|
|
284
|
+
seat_only_plan = auto_managed_seats && plan[:seat_price_id] == price_id
|
|
267
285
|
|
|
268
|
-
|
|
286
|
+
active_resolved = active_stripe && stripe_resolve_plan_item(config, active_stripe)
|
|
287
|
+
active_stripe_item = active_resolved&.fetch(:item, nil) || stripe_subscription_item(active_stripe || {})
|
|
269
288
|
stripe_price_id_value = stripe_fetch(stripe_fetch(active_stripe_item || {}, "price") || {}, "id")
|
|
270
289
|
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 ==
|
|
290
|
+
same_seats = auto_managed_seats || (active_or_trialing && active_or_trialing["seats"].to_i == requested_seats.to_i)
|
|
272
291
|
same_price = !active_stripe || stripe_price_id_value == price_id
|
|
273
292
|
valid_period = !active_or_trialing || !active_or_trialing["periodEnd"] || active_or_trialing["periodEnd"] > Time.now
|
|
274
293
|
if active_or_trialing&.fetch("status", nil) == "active" && same_plan && same_seats && same_price && valid_period
|
|
@@ -276,6 +295,17 @@ module BetterAuth
|
|
|
276
295
|
end
|
|
277
296
|
|
|
278
297
|
if active_stripe
|
|
298
|
+
if body[:schedule_at_period_end]
|
|
299
|
+
url = stripe_schedule_plan_change(ctx, config, active_stripe, active_or_trialing, plan, price_id, requested_seats, seat_only_plan, body)
|
|
300
|
+
next ctx.json({url: url, redirect: stripe_redirect?(body)})
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
old_plan = active_or_trialing && stripe_plan_by_name(config, active_or_trialing["plan"])
|
|
304
|
+
if stripe_direct_subscription_update?(old_plan, plan, auto_managed_seats)
|
|
305
|
+
url = stripe_update_active_subscription_items(ctx, config, active_stripe, active_or_trialing, old_plan, plan, price_id, requested_seats, seat_only_plan, body)
|
|
306
|
+
next ctx.json({url: url, redirect: stripe_redirect?(body)})
|
|
307
|
+
end
|
|
308
|
+
|
|
279
309
|
portal = stripe_client(config).billing_portal.sessions.create(
|
|
280
310
|
customer: customer_id,
|
|
281
311
|
return_url: stripe_url(ctx, body[:return_url] || "/"),
|
|
@@ -284,7 +314,7 @@ module BetterAuth
|
|
|
284
314
|
after_completion: {type: "redirect", redirect: {return_url: stripe_url(ctx, body[:return_url] || "/")}},
|
|
285
315
|
subscription_update_confirm: {
|
|
286
316
|
subscription: stripe_fetch(active_stripe, "id"),
|
|
287
|
-
items: [
|
|
317
|
+
items: [stripe_line_item(config, price_id, requested_seats).merge(id: stripe_fetch(active_stripe_item || {}, "id"))]
|
|
288
318
|
}
|
|
289
319
|
}
|
|
290
320
|
)
|
|
@@ -294,12 +324,12 @@ module BetterAuth
|
|
|
294
324
|
incomplete = subscriptions.find { |entry| entry["status"] == "incomplete" }
|
|
295
325
|
subscription = active_or_trialing || incomplete
|
|
296
326
|
if subscription
|
|
297
|
-
update = {plan: plan[:name].to_s.downcase, seats:
|
|
327
|
+
update = {plan: plan[:name].to_s.downcase, seats: requested_seats}
|
|
298
328
|
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
329
|
else
|
|
300
330
|
subscription = ctx.context.adapter.create(
|
|
301
331
|
model: "subscription",
|
|
302
|
-
data: {plan: plan[:name].to_s.downcase, referenceId: reference_id, stripeCustomerId: customer_id, status: "incomplete", seats:
|
|
332
|
+
data: {plan: plan[:name].to_s.downcase, referenceId: reference_id, stripeCustomerId: customer_id, status: "incomplete", seats: requested_seats, limits: plan[:limits]}
|
|
303
333
|
)
|
|
304
334
|
end
|
|
305
335
|
|
|
@@ -323,9 +353,9 @@ module BetterAuth
|
|
|
323
353
|
customer: customer_id,
|
|
324
354
|
customer_update: (customer_type == "user") ? {name: "auto", address: "auto"} : {address: "auto"},
|
|
325
355
|
locale: body[:locale],
|
|
326
|
-
success_url: stripe_url(ctx, "#{ctx.context.base_url}/subscription/success?callbackURL=#{Rack::Utils.escape(body[:success_url] || "/")}&
|
|
356
|
+
success_url: stripe_url(ctx, "#{ctx.context.base_url}/subscription/success?callbackURL=#{Rack::Utils.escape(body[:success_url] || "/")}&checkoutSessionId={CHECKOUT_SESSION_ID}"),
|
|
327
357
|
cancel_url: stripe_url(ctx, body[:cancel_url] || "/"),
|
|
328
|
-
line_items:
|
|
358
|
+
line_items: stripe_checkout_line_items(config, plan, price_id, requested_seats, auto_managed_seats, seat_only_plan),
|
|
329
359
|
subscription_data: free_trial.merge(metadata: subscription_metadata),
|
|
330
360
|
mode: "subscription",
|
|
331
361
|
client_reference_id: reference_id,
|
|
@@ -343,7 +373,7 @@ module BetterAuth
|
|
|
343
373
|
Endpoint.new(path: "/subscription/cancel", method: "POST") do |ctx|
|
|
344
374
|
session = Routes.current_session(ctx)
|
|
345
375
|
body = normalize_hash(ctx.body)
|
|
346
|
-
customer_type = (body
|
|
376
|
+
customer_type = stripe_customer_type!(body)
|
|
347
377
|
reference_id = stripe_reference_id!(ctx, session, customer_type, body[:reference_id], config)
|
|
348
378
|
stripe_authorize_reference!(ctx, session, reference_id, "cancel-subscription", customer_type, stripe_subscription_options(config), explicit: body.key?(:reference_id))
|
|
349
379
|
subscription = stripe_find_subscription_for_action(ctx, reference_id, body[:subscription_id], active_only: true)
|
|
@@ -378,12 +408,22 @@ module BetterAuth
|
|
|
378
408
|
Endpoint.new(path: "/subscription/restore", method: "POST") do |ctx|
|
|
379
409
|
session = Routes.current_session(ctx)
|
|
380
410
|
body = normalize_hash(ctx.body)
|
|
381
|
-
customer_type = (body
|
|
411
|
+
customer_type = stripe_customer_type!(body)
|
|
382
412
|
reference_id = stripe_reference_id!(ctx, session, customer_type, body[:reference_id], config)
|
|
383
413
|
stripe_authorize_reference!(ctx, session, reference_id, "restore-subscription", customer_type, stripe_subscription_options(config), explicit: body.key?(:reference_id))
|
|
384
414
|
subscription = stripe_find_subscription_for_action(ctx, reference_id, body[:subscription_id], active_only: false)
|
|
385
415
|
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_FOUND")) unless subscription && subscription["stripeCustomerId"]
|
|
386
416
|
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_ACTIVE")) unless stripe_active_or_trialing?(subscription)
|
|
417
|
+
|
|
418
|
+
if subscription["stripeScheduleId"]
|
|
419
|
+
schedule = stripe_client(config).subscription_schedules.retrieve(subscription["stripeScheduleId"])
|
|
420
|
+
if stripe_fetch(schedule, "status") == "active"
|
|
421
|
+
schedule = stripe_client(config).subscription_schedules.release(subscription["stripeScheduleId"])
|
|
422
|
+
end
|
|
423
|
+
ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: {stripeScheduleId: nil})
|
|
424
|
+
next ctx.json(stripe_stringify_keys(schedule))
|
|
425
|
+
end
|
|
426
|
+
|
|
387
427
|
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION")) unless stripe_pending_cancel?(subscription)
|
|
388
428
|
|
|
389
429
|
active = stripe_active_subscriptions(config, subscription["stripeCustomerId"]).first
|
|
@@ -406,14 +446,19 @@ module BetterAuth
|
|
|
406
446
|
Endpoint.new(path: "/subscription/list", method: "GET") do |ctx|
|
|
407
447
|
session = Routes.current_session(ctx)
|
|
408
448
|
query = normalize_hash(ctx.query)
|
|
409
|
-
customer_type = (query
|
|
449
|
+
customer_type = stripe_customer_type!(query)
|
|
410
450
|
reference_id = stripe_reference_id!(ctx, session, customer_type, query[:reference_id], config)
|
|
411
451
|
stripe_authorize_reference!(ctx, session, reference_id, "list-subscription", customer_type, stripe_subscription_options(config), explicit: query.key?(:reference_id))
|
|
412
452
|
plans = stripe_plans(config)
|
|
413
453
|
subscriptions = ctx.context.adapter.find_many(model: "subscription", where: [{field: "referenceId", value: reference_id}]).select { |entry| stripe_active_or_trialing?(entry) }
|
|
414
454
|
ctx.json(subscriptions.map do |entry|
|
|
415
455
|
plan = plans.find { |item| item[:name].to_s.downcase == entry["plan"].to_s.downcase }
|
|
416
|
-
entry
|
|
456
|
+
price_id = if entry["billingInterval"] == "year"
|
|
457
|
+
plan&.fetch(:annual_discount_price_id, nil) || plan&.fetch(:price_id, nil)
|
|
458
|
+
else
|
|
459
|
+
plan&.fetch(:price_id, nil)
|
|
460
|
+
end
|
|
461
|
+
entry.merge("limits" => plan&.fetch(:limits, nil), "priceId" => price_id)
|
|
417
462
|
end)
|
|
418
463
|
end
|
|
419
464
|
end
|
|
@@ -422,7 +467,7 @@ module BetterAuth
|
|
|
422
467
|
Endpoint.new(path: "/subscription/billing-portal", method: "POST") do |ctx|
|
|
423
468
|
session = Routes.current_session(ctx)
|
|
424
469
|
body = normalize_hash(ctx.body)
|
|
425
|
-
customer_type = (body
|
|
470
|
+
customer_type = stripe_customer_type!(body)
|
|
426
471
|
reference_id = stripe_reference_id!(ctx, session, customer_type, body[:reference_id], config)
|
|
427
472
|
stripe_authorize_reference!(ctx, session, reference_id, "billing-portal", customer_type, stripe_subscription_options(config), explicit: body.key?(:reference_id))
|
|
428
473
|
customer_id = if customer_type == "organization"
|
|
@@ -468,13 +513,22 @@ module BetterAuth
|
|
|
468
513
|
Endpoint.new(path: "/subscription/success", method: "GET") do |ctx|
|
|
469
514
|
query = normalize_hash(ctx.query)
|
|
470
515
|
callback = query[:callback_url] || "/"
|
|
471
|
-
|
|
516
|
+
checkout_session_id = query[:checkout_session_id]
|
|
517
|
+
subscription_id = query[:subscription_id]
|
|
518
|
+
if checkout_session_id
|
|
519
|
+
callback = callback.to_s.gsub("{CHECKOUT_SESSION_ID}", checkout_session_id.to_s)
|
|
520
|
+
checkout_session = stripe_client(config || {}).checkout.sessions.retrieve(checkout_session_id)
|
|
521
|
+
metadata = normalize_hash(stripe_fetch(checkout_session || {}, "metadata") || {})
|
|
522
|
+
subscription_id = metadata[:subscription_id]
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
unless subscription_id
|
|
472
526
|
raise ctx.redirect(stripe_url(ctx, callback))
|
|
473
527
|
end
|
|
474
528
|
session = Routes.current_session(ctx, allow_nil: true)
|
|
475
529
|
raise ctx.redirect(stripe_url(ctx, callback)) unless session
|
|
476
530
|
|
|
477
|
-
subscription = ctx.context.adapter.find_one(model: "subscription", where: [{field: "id", value:
|
|
531
|
+
subscription = ctx.context.adapter.find_one(model: "subscription", where: [{field: "id", value: subscription_id}])
|
|
478
532
|
raise ctx.redirect(stripe_url(ctx, callback)) unless subscription
|
|
479
533
|
raise ctx.redirect(stripe_url(ctx, callback)) if stripe_active_or_trialing?(subscription)
|
|
480
534
|
|
|
@@ -483,15 +537,16 @@ module BetterAuth
|
|
|
483
537
|
|
|
484
538
|
stripe_subscription = stripe_active_subscriptions(config || {}, customer_id).first
|
|
485
539
|
if stripe_subscription
|
|
486
|
-
|
|
487
|
-
|
|
540
|
+
resolved = stripe_resolve_plan_item(config || {}, stripe_subscription)
|
|
541
|
+
item = resolved&.fetch(:item, nil)
|
|
542
|
+
plan = resolved&.fetch(:plan, nil)
|
|
488
543
|
if item && plan
|
|
489
544
|
ctx.context.adapter.update(
|
|
490
545
|
model: "subscription",
|
|
491
546
|
where: [{field: "id", value: subscription.fetch("id")}],
|
|
492
|
-
update: stripe_subscription_state(stripe_subscription, include_status: true).merge(
|
|
547
|
+
update: stripe_subscription_state(stripe_subscription, include_status: true, compact: false).merge(
|
|
493
548
|
plan: plan[:name].to_s.downcase,
|
|
494
|
-
seats:
|
|
549
|
+
seats: stripe_resolve_quantity(stripe_subscription, item, plan),
|
|
495
550
|
stripeSubscriptionId: stripe_fetch(stripe_subscription, "id")
|
|
496
551
|
)
|
|
497
552
|
)
|
|
@@ -508,23 +563,27 @@ module BetterAuth
|
|
|
508
563
|
|
|
509
564
|
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
565
|
|
|
511
|
-
event =
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
webhooks.
|
|
566
|
+
event = begin
|
|
567
|
+
if stripe_client(config).respond_to?(:webhooks)
|
|
568
|
+
webhooks = stripe_client(config).webhooks
|
|
569
|
+
if webhooks.respond_to?(:construct_event_async)
|
|
570
|
+
webhooks.construct_event_async(ctx.body, signature, config[:stripe_webhook_secret])
|
|
571
|
+
else
|
|
572
|
+
webhooks.construct_event(ctx.body, signature, config[:stripe_webhook_secret])
|
|
573
|
+
end
|
|
515
574
|
else
|
|
516
|
-
|
|
575
|
+
ctx.body
|
|
517
576
|
end
|
|
518
|
-
|
|
519
|
-
|
|
577
|
+
rescue
|
|
578
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("FAILED_TO_CONSTRUCT_STRIPE_EVENT"))
|
|
520
579
|
end
|
|
521
580
|
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("FAILED_TO_CONSTRUCT_STRIPE_EVENT")) unless event
|
|
522
|
-
|
|
581
|
+
begin
|
|
582
|
+
stripe_handle_event(ctx, event)
|
|
583
|
+
rescue
|
|
584
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("STRIPE_WEBHOOK_ERROR"))
|
|
585
|
+
end
|
|
523
586
|
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
587
|
end
|
|
529
588
|
end
|
|
530
589
|
|
|
@@ -551,10 +610,11 @@ module BetterAuth
|
|
|
551
610
|
return if object[:mode] == "setup" || !config.dig(:subscription, :enabled)
|
|
552
611
|
|
|
553
612
|
stripe_subscription = stripe_client(config).subscriptions.retrieve(object[:subscription])
|
|
554
|
-
|
|
555
|
-
return unless
|
|
613
|
+
resolved = stripe_resolve_plan_item(config, stripe_subscription)
|
|
614
|
+
return unless resolved
|
|
556
615
|
|
|
557
|
-
|
|
616
|
+
item = resolved.fetch(:item)
|
|
617
|
+
plan = resolved.fetch(:plan)
|
|
558
618
|
metadata = normalize_hash(object[:metadata] || {})
|
|
559
619
|
reference_id = object[:client_reference_id] || metadata[:reference_id]
|
|
560
620
|
subscription_id = metadata[:subscription_id]
|
|
@@ -563,7 +623,7 @@ module BetterAuth
|
|
|
563
623
|
update = stripe_subscription_state(stripe_subscription, include_status: true).merge(
|
|
564
624
|
plan: plan[:name].to_s.downcase,
|
|
565
625
|
stripeSubscriptionId: object[:subscription],
|
|
566
|
-
seats:
|
|
626
|
+
seats: stripe_resolve_quantity(stripe_subscription, item, plan),
|
|
567
627
|
trialStart: stripe_time(stripe_fetch(stripe_subscription, "trial_start")),
|
|
568
628
|
trialEnd: stripe_time(stripe_fetch(stripe_subscription, "trial_end"))
|
|
569
629
|
).compact
|
|
@@ -588,9 +648,10 @@ module BetterAuth
|
|
|
588
648
|
|
|
589
649
|
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
650
|
return unless reference
|
|
591
|
-
|
|
592
|
-
return unless
|
|
593
|
-
|
|
651
|
+
resolved = stripe_resolve_plan_item(config, object)
|
|
652
|
+
return unless resolved
|
|
653
|
+
item = resolved.fetch(:item)
|
|
654
|
+
plan = resolved[:plan] || (metadata[:plan] && stripe_plan_by_name(config, metadata[:plan]))
|
|
594
655
|
return unless plan
|
|
595
656
|
|
|
596
657
|
created = ctx.context.adapter.create(
|
|
@@ -600,7 +661,7 @@ module BetterAuth
|
|
|
600
661
|
stripeCustomerId: customer_id,
|
|
601
662
|
stripeSubscriptionId: object[:id],
|
|
602
663
|
plan: plan[:name].to_s.downcase,
|
|
603
|
-
seats:
|
|
664
|
+
seats: stripe_resolve_quantity(object, item, plan),
|
|
604
665
|
limits: plan[:limits]
|
|
605
666
|
).compact
|
|
606
667
|
)
|
|
@@ -610,8 +671,9 @@ module BetterAuth
|
|
|
610
671
|
def stripe_on_subscription_updated(ctx, event)
|
|
611
672
|
config = ctx.context.options.plugins.find { |plugin| plugin.id == "stripe" }&.options || {}
|
|
612
673
|
object = normalize_hash(event.dig(:data, :object) || {})
|
|
613
|
-
|
|
614
|
-
return unless
|
|
674
|
+
resolved = stripe_resolve_plan_item(config, object)
|
|
675
|
+
return unless resolved
|
|
676
|
+
item = resolved.fetch(:item)
|
|
615
677
|
|
|
616
678
|
metadata = normalize_hash(object[:metadata] || {})
|
|
617
679
|
subscription = if metadata[:subscription_id]
|
|
@@ -629,19 +691,25 @@ module BetterAuth
|
|
|
629
691
|
end
|
|
630
692
|
return unless subscription
|
|
631
693
|
|
|
632
|
-
plan =
|
|
694
|
+
plan = resolved[:plan]
|
|
633
695
|
was_pending = stripe_pending_cancel?(subscription)
|
|
634
|
-
update = stripe_subscription_state(object, include_status: true).merge(
|
|
696
|
+
update = stripe_subscription_state(object, include_status: true, compact: false).merge(
|
|
635
697
|
stripeSubscriptionId: object[:id],
|
|
636
|
-
seats:
|
|
698
|
+
seats: stripe_resolve_quantity(object, item, plan)
|
|
637
699
|
)
|
|
638
700
|
update[:plan] = plan[:name].to_s.downcase if plan
|
|
639
701
|
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
|
|
702
|
+
updated = ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: update)
|
|
641
703
|
if object[:status] == "active" && stripe_stripe_pending_cancel?(object) && !was_pending
|
|
642
704
|
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
705
|
end
|
|
644
706
|
config.dig(:subscription, :on_subscription_update)&.call({event: event, subscription: updated || subscription})
|
|
707
|
+
if plan && subscription["status"] == "trialing" && object[:status] == "active"
|
|
708
|
+
plan.dig(:free_trial, :on_trial_end)&.call({subscription: subscription}, ctx)
|
|
709
|
+
end
|
|
710
|
+
if plan && subscription["status"] == "trialing" && object[:status] == "incomplete_expired"
|
|
711
|
+
plan.dig(:free_trial, :on_trial_expired)&.call(subscription, ctx)
|
|
712
|
+
end
|
|
645
713
|
end
|
|
646
714
|
|
|
647
715
|
def stripe_on_subscription_deleted(ctx, event)
|
|
@@ -650,7 +718,7 @@ module BetterAuth
|
|
|
650
718
|
subscription = ctx.context.adapter.find_one(model: "subscription", where: [{field: "stripeSubscriptionId", value: object[:id]}])
|
|
651
719
|
return unless subscription
|
|
652
720
|
|
|
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"))
|
|
721
|
+
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
722
|
config.dig(:subscription, :on_subscription_deleted)&.call({event: event, subscription: subscription, stripeSubscription: object, stripe_subscription: object})
|
|
655
723
|
end
|
|
656
724
|
|
|
@@ -662,8 +730,7 @@ module BetterAuth
|
|
|
662
730
|
end
|
|
663
731
|
|
|
664
732
|
def stripe_find_or_create_user_customer(config, user, metadata = nil, ctx = nil)
|
|
665
|
-
|
|
666
|
-
customer = Array(stripe_fetch(existing, "data")).first
|
|
733
|
+
customer = stripe_find_user_customer(config, user["email"])
|
|
667
734
|
if customer
|
|
668
735
|
stripe_notify_customer_created(config, customer, user, ctx)
|
|
669
736
|
return customer
|
|
@@ -691,8 +758,7 @@ module BetterAuth
|
|
|
691
758
|
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("ORGANIZATION_NOT_FOUND")) unless org
|
|
692
759
|
return org["stripeCustomerId"] if org["stripeCustomerId"]
|
|
693
760
|
|
|
694
|
-
|
|
695
|
-
customer = Array(stripe_fetch(existing, "data")).first
|
|
761
|
+
customer = stripe_find_organization_customer(config, org["id"])
|
|
696
762
|
unless customer
|
|
697
763
|
raw_extra = config.dig(:organization, :get_customer_create_params)&.call(org, ctx) || {}
|
|
698
764
|
extra_metadata = stripe_fetch(raw_extra, "metadata")
|
|
@@ -796,6 +862,41 @@ module BetterAuth
|
|
|
796
862
|
raise APIError.new("UNAUTHORIZED", message: STRIPE_ERROR_CODES.fetch("UNAUTHORIZED")) unless allowed
|
|
797
863
|
end
|
|
798
864
|
|
|
865
|
+
def stripe_customer_type!(source)
|
|
866
|
+
customer_type = (source[:customer_type] || "user").to_s
|
|
867
|
+
raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("INVALID_CUSTOMER_TYPE")) unless %w[user organization].include?(customer_type)
|
|
868
|
+
|
|
869
|
+
customer_type
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
def stripe_find_user_customer(config, email)
|
|
873
|
+
customers = stripe_client(config).customers
|
|
874
|
+
begin
|
|
875
|
+
existing = customers.search(query: "email:\"#{stripe_escape_search(email)}\" AND -metadata[\"customerType\"]:\"organization\"", limit: 1)
|
|
876
|
+
Array(stripe_fetch(existing, "data")).first
|
|
877
|
+
rescue
|
|
878
|
+
listed = customers.list(email: email, limit: 100)
|
|
879
|
+
Array(stripe_fetch(listed, "data")).find do |customer|
|
|
880
|
+
stripe_metadata_fetch(stripe_fetch(customer, "metadata") || {}, "customerType") != "organization"
|
|
881
|
+
end
|
|
882
|
+
end
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
def stripe_find_organization_customer(config, organization_id)
|
|
886
|
+
customers = stripe_client(config).customers
|
|
887
|
+
begin
|
|
888
|
+
existing = customers.search(query: "metadata[\"organizationId\"]:\"#{stripe_escape_search(organization_id)}\" AND metadata[\"customerType\"]:\"organization\"", limit: 1)
|
|
889
|
+
Array(stripe_fetch(existing, "data")).first
|
|
890
|
+
rescue
|
|
891
|
+
listed = customers.list(limit: 100)
|
|
892
|
+
Array(stripe_fetch(listed, "data")).find do |customer|
|
|
893
|
+
metadata = stripe_fetch(customer, "metadata") || {}
|
|
894
|
+
stripe_metadata_fetch(metadata, "organizationId") == organization_id &&
|
|
895
|
+
stripe_metadata_fetch(metadata, "customerType") == "organization"
|
|
896
|
+
end
|
|
897
|
+
end
|
|
898
|
+
end
|
|
899
|
+
|
|
799
900
|
def stripe_find_subscription_for_action(ctx, reference_id, subscription_id, active_only:)
|
|
800
901
|
subscription = if subscription_id
|
|
801
902
|
ctx.context.adapter.find_one(model: "subscription", where: [{field: "stripeSubscriptionId", value: subscription_id}])
|
|
@@ -832,8 +933,192 @@ module BetterAuth
|
|
|
832
933
|
Array(stripe_fetch(stripe_fetch(subscription, "items") || {}, "data")).first
|
|
833
934
|
end
|
|
834
935
|
|
|
835
|
-
def
|
|
936
|
+
def stripe_resolve_plan_item(config, subscription)
|
|
937
|
+
items = Array(stripe_fetch(stripe_fetch(subscription, "items") || {}, "data"))
|
|
938
|
+
first = items.first
|
|
939
|
+
return nil unless first
|
|
940
|
+
|
|
941
|
+
items.each do |item|
|
|
942
|
+
price = stripe_fetch(item, "price") || {}
|
|
943
|
+
plan = stripe_plan_by_price_info(config, stripe_fetch(price, "id"), stripe_fetch(price, "lookup_key"))
|
|
944
|
+
return {item: item, plan: plan} if plan
|
|
945
|
+
end
|
|
946
|
+
{item: first, plan: nil} if items.length == 1
|
|
947
|
+
end
|
|
948
|
+
|
|
949
|
+
def stripe_resolve_quantity(subscription, plan_item, plan = nil)
|
|
950
|
+
items = Array(stripe_fetch(stripe_fetch(subscription, "items") || {}, "data"))
|
|
951
|
+
seat_price_id = plan && plan[:seat_price_id]
|
|
952
|
+
seat_item = seat_price_id && items.find { |item| stripe_fetch(stripe_fetch(item, "price") || {}, "id") == seat_price_id }
|
|
953
|
+
stripe_fetch(seat_item || plan_item, "quantity") || 1
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
def stripe_line_item(config, price_id, quantity)
|
|
957
|
+
item = {price: price_id}
|
|
958
|
+
item[:quantity] = quantity unless stripe_metered_price?(config, price_id)
|
|
959
|
+
item
|
|
960
|
+
end
|
|
961
|
+
|
|
962
|
+
def stripe_checkout_line_items(config, plan, price_id, quantity, auto_managed_seats, seat_only_plan)
|
|
963
|
+
items = []
|
|
964
|
+
items << stripe_line_item(config, price_id, auto_managed_seats ? 1 : quantity) unless seat_only_plan
|
|
965
|
+
items << {price: plan[:seat_price_id], quantity: quantity} if auto_managed_seats && plan[:seat_price_id]
|
|
966
|
+
items.concat(stripe_plan_line_items(plan))
|
|
967
|
+
items
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
def stripe_plan_line_items(plan)
|
|
971
|
+
Array(plan[:line_items]).map do |item|
|
|
972
|
+
item.is_a?(Hash) ? normalize_hash(item) : item
|
|
973
|
+
end
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
def stripe_schedule_plan_change(ctx, config, active_stripe, db_subscription, plan, price_id, quantity, seat_only_plan, body)
|
|
977
|
+
schedule = stripe_client(config).subscription_schedules.create(from_subscription: stripe_fetch(active_stripe, "id"))
|
|
978
|
+
current_phase = Array(stripe_fetch(schedule, "phases")).first || {}
|
|
979
|
+
current_items = Array(stripe_fetch(current_phase, "items"))
|
|
980
|
+
active_item = stripe_resolve_plan_item(config, active_stripe)&.fetch(:item, nil) || stripe_subscription_item(active_stripe)
|
|
981
|
+
active_price_id = stripe_fetch(stripe_fetch(active_item || {}, "price") || {}, "id")
|
|
982
|
+
replaced = false
|
|
983
|
+
new_items = current_items.filter_map do |item|
|
|
984
|
+
item_price = stripe_fetch(item, "price")
|
|
985
|
+
item_price = stripe_fetch(item_price, "id") if item_price.is_a?(Hash)
|
|
986
|
+
if item_price == active_price_id
|
|
987
|
+
replaced = true
|
|
988
|
+
next nil if seat_only_plan
|
|
989
|
+
|
|
990
|
+
stripe_line_item(config, price_id, quantity)
|
|
991
|
+
else
|
|
992
|
+
{price: item_price, quantity: stripe_fetch(item, "quantity")}.compact
|
|
993
|
+
end
|
|
994
|
+
end
|
|
995
|
+
new_items << stripe_line_item(config, price_id, quantity) unless replaced || seat_only_plan
|
|
996
|
+
new_items << {price: plan[:seat_price_id], quantity: quantity} if plan[:seat_price_id]
|
|
997
|
+
new_items.concat(stripe_plan_line_items(plan))
|
|
998
|
+
|
|
999
|
+
stripe_client(config).subscription_schedules.update(
|
|
1000
|
+
stripe_fetch(schedule, "id"),
|
|
1001
|
+
metadata: {source: "@better-auth/stripe"},
|
|
1002
|
+
end_behavior: "release",
|
|
1003
|
+
phases: [
|
|
1004
|
+
{
|
|
1005
|
+
items: current_items.map do |item|
|
|
1006
|
+
item_price = stripe_fetch(item, "price")
|
|
1007
|
+
item_price = stripe_fetch(item_price, "id") if item_price.is_a?(Hash)
|
|
1008
|
+
{price: item_price, quantity: stripe_fetch(item, "quantity")}.compact
|
|
1009
|
+
end,
|
|
1010
|
+
start_date: stripe_fetch(current_phase, "start_date"),
|
|
1011
|
+
end_date: stripe_fetch(current_phase, "end_date")
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
items: new_items,
|
|
1015
|
+
start_date: stripe_fetch(current_phase, "end_date"),
|
|
1016
|
+
proration_behavior: "none"
|
|
1017
|
+
}
|
|
1018
|
+
]
|
|
1019
|
+
)
|
|
1020
|
+
if db_subscription
|
|
1021
|
+
ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: db_subscription.fetch("id")}], update: {stripeScheduleId: stripe_fetch(schedule, "id")})
|
|
1022
|
+
end
|
|
1023
|
+
stripe_url(ctx, body[:return_url] || "/")
|
|
1024
|
+
end
|
|
1025
|
+
|
|
1026
|
+
def stripe_direct_subscription_update?(old_plan, plan, auto_managed_seats)
|
|
1027
|
+
return true if auto_managed_seats && old_plan && old_plan[:seat_price_id] != plan[:seat_price_id]
|
|
1028
|
+
|
|
1029
|
+
stripe_plan_line_items(old_plan || {}).map { |item| item[:price] } != stripe_plan_line_items(plan).map { |item| item[:price] }
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
def stripe_update_active_subscription_items(ctx, config, active_stripe, db_subscription, old_plan, plan, price_id, quantity, seat_only_plan, body)
|
|
1033
|
+
active_item = stripe_resolve_plan_item(config, active_stripe)&.fetch(:item, nil) || stripe_subscription_item(active_stripe)
|
|
1034
|
+
active_price_id = stripe_fetch(stripe_fetch(active_item || {}, "price") || {}, "id")
|
|
1035
|
+
old_line_prices = stripe_plan_line_items(old_plan || {}).map { |item| item[:price] }
|
|
1036
|
+
new_line_prices = stripe_plan_line_items(plan).map { |item| item[:price] }
|
|
1037
|
+
added_line_prices = new_line_prices - old_line_prices
|
|
1038
|
+
items = []
|
|
1039
|
+
Array(stripe_fetch(stripe_fetch(active_stripe, "items") || {}, "data")).each do |item|
|
|
1040
|
+
item_price = stripe_fetch(stripe_fetch(item, "price") || {}, "id")
|
|
1041
|
+
if item_price == active_price_id
|
|
1042
|
+
items << stripe_line_item(config, price_id, plan[:seat_price_id] ? 1 : quantity).merge(id: stripe_fetch(item, "id")) unless seat_only_plan
|
|
1043
|
+
elsif old_plan && item_price == old_plan[:seat_price_id] && plan[:seat_price_id]
|
|
1044
|
+
items << {id: stripe_fetch(item, "id"), price: plan[:seat_price_id], quantity: quantity}
|
|
1045
|
+
elsif old_line_prices.include?(item_price)
|
|
1046
|
+
if new_line_prices.include?(item_price)
|
|
1047
|
+
new_line_prices.delete_at(new_line_prices.index(item_price))
|
|
1048
|
+
else
|
|
1049
|
+
items << {id: stripe_fetch(item, "id"), deleted: true}
|
|
1050
|
+
end
|
|
1051
|
+
end
|
|
1052
|
+
end
|
|
1053
|
+
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] }
|
|
1054
|
+
added_line_prices.each { |price| items << {price: price} }
|
|
1055
|
+
stripe_client(config).subscriptions.update(stripe_fetch(active_stripe, "id"), items: items, proration_behavior: plan[:proration_behavior] || "create_prorations")
|
|
1056
|
+
if db_subscription
|
|
1057
|
+
ctx.context.adapter.update(
|
|
1058
|
+
model: "subscription",
|
|
1059
|
+
where: [{field: "id", value: db_subscription.fetch("id")}],
|
|
1060
|
+
update: {plan: plan[:name].to_s.downcase, seats: quantity, limits: plan[:limits], stripeScheduleId: nil}
|
|
1061
|
+
)
|
|
1062
|
+
end
|
|
1063
|
+
stripe_url(ctx, body[:return_url] || "/")
|
|
1064
|
+
end
|
|
1065
|
+
|
|
1066
|
+
def stripe_sync_organization_seats(config, data, ctx)
|
|
1067
|
+
organization = data[:organization] || data["organization"]
|
|
1068
|
+
return unless config.dig(:subscription, :enabled) && organization && organization["stripeCustomerId"]
|
|
1069
|
+
|
|
1070
|
+
member_count = ctx.context.adapter.count(model: "member", where: [{field: "organizationId", value: organization.fetch("id")}])
|
|
1071
|
+
seat_plans = stripe_plans(config).select { |plan| plan[:seat_price_id] }
|
|
1072
|
+
return if seat_plans.empty?
|
|
1073
|
+
|
|
1074
|
+
subscription = ctx.context.adapter.find_many(model: "subscription", where: [{field: "referenceId", value: organization.fetch("id")}]).find { |entry| stripe_active_or_trialing?(entry) }
|
|
1075
|
+
return unless subscription && subscription["stripeSubscriptionId"]
|
|
1076
|
+
|
|
1077
|
+
plan = seat_plans.find { |entry| entry[:name].to_s.downcase == subscription["plan"].to_s.downcase }
|
|
1078
|
+
return unless plan
|
|
1079
|
+
|
|
1080
|
+
stripe_subscription = stripe_client(config).subscriptions.retrieve(subscription["stripeSubscriptionId"])
|
|
1081
|
+
return unless stripe_active_or_trialing?(stripe_subscription)
|
|
1082
|
+
|
|
1083
|
+
items = Array(stripe_fetch(stripe_fetch(stripe_subscription, "items") || {}, "data"))
|
|
1084
|
+
seat_item = items.find { |item| stripe_fetch(stripe_fetch(item, "price") || {}, "id") == plan[:seat_price_id] }
|
|
1085
|
+
return if seat_item && stripe_fetch(seat_item, "quantity").to_i == member_count.to_i
|
|
1086
|
+
|
|
1087
|
+
update_items = if seat_item
|
|
1088
|
+
[{id: stripe_fetch(seat_item, "id"), quantity: member_count}]
|
|
1089
|
+
else
|
|
1090
|
+
[{price: plan[:seat_price_id], quantity: member_count}]
|
|
1091
|
+
end
|
|
1092
|
+
stripe_client(config).subscriptions.update(subscription["stripeSubscriptionId"], items: update_items, proration_behavior: plan[:proration_behavior] || "create_prorations")
|
|
1093
|
+
ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: {seats: member_count})
|
|
1094
|
+
rescue
|
|
1095
|
+
nil
|
|
1096
|
+
end
|
|
1097
|
+
|
|
1098
|
+
def stripe_metered_price?(config, price_id, lookup_key = nil)
|
|
1099
|
+
price = stripe_resolve_stripe_price(config, price_id, lookup_key)
|
|
1100
|
+
recurring = stripe_fetch(price || {}, "recurring") || {}
|
|
1101
|
+
stripe_fetch(recurring, "usage_type") == "metered"
|
|
1102
|
+
end
|
|
1103
|
+
|
|
1104
|
+
def stripe_resolve_stripe_price(config, price_id, lookup_key = nil)
|
|
1105
|
+
return nil unless stripe_client(config).respond_to?(:prices)
|
|
1106
|
+
|
|
1107
|
+
prices = stripe_client(config).prices
|
|
1108
|
+
if lookup_key
|
|
1109
|
+
result = prices.list(lookup_keys: [lookup_key], active: true, limit: 1)
|
|
1110
|
+
Array(stripe_fetch(result, "data")).first
|
|
1111
|
+
elsif price_id && prices.respond_to?(:retrieve)
|
|
1112
|
+
prices.retrieve(price_id)
|
|
1113
|
+
end
|
|
1114
|
+
rescue
|
|
1115
|
+
nil
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
def stripe_subscription_state(subscription, include_status: true, compact: true)
|
|
836
1119
|
item = stripe_subscription_item(subscription)
|
|
1120
|
+
price = stripe_fetch(item || {}, "price") || {}
|
|
1121
|
+
recurring = stripe_fetch(price, "recurring") || {}
|
|
837
1122
|
state = {
|
|
838
1123
|
periodStart: stripe_time(stripe_fetch(item || subscription, "current_period_start")),
|
|
839
1124
|
periodEnd: stripe_time(stripe_fetch(item || subscription, "current_period_end")),
|
|
@@ -842,10 +1127,20 @@ module BetterAuth
|
|
|
842
1127
|
canceledAt: stripe_time(stripe_fetch(subscription, "canceled_at")),
|
|
843
1128
|
endedAt: stripe_time(stripe_fetch(subscription, "ended_at")),
|
|
844
1129
|
trialStart: stripe_time(stripe_fetch(subscription, "trial_start")),
|
|
845
|
-
trialEnd: stripe_time(stripe_fetch(subscription, "trial_end"))
|
|
1130
|
+
trialEnd: stripe_time(stripe_fetch(subscription, "trial_end")),
|
|
1131
|
+
billingInterval: stripe_fetch(recurring, "interval"),
|
|
1132
|
+
stripeScheduleId: stripe_schedule_id(subscription)
|
|
846
1133
|
}
|
|
847
1134
|
state[:status] = stripe_fetch(subscription, "status") if include_status
|
|
848
|
-
state.compact
|
|
1135
|
+
compact ? state.compact : state
|
|
1136
|
+
end
|
|
1137
|
+
|
|
1138
|
+
def stripe_schedule_id(subscription)
|
|
1139
|
+
schedule = stripe_fetch(subscription, "schedule")
|
|
1140
|
+
return nil if schedule.nil?
|
|
1141
|
+
return schedule if schedule.is_a?(String)
|
|
1142
|
+
|
|
1143
|
+
stripe_id(schedule) || schedule.to_s
|
|
849
1144
|
end
|
|
850
1145
|
|
|
851
1146
|
def stripe_reference_by_customer(ctx, config, customer_id)
|
|
@@ -864,7 +1159,10 @@ module BetterAuth
|
|
|
864
1159
|
.reduce({}) do |acc, entry|
|
|
865
1160
|
next acc unless entry.respond_to?(:each)
|
|
866
1161
|
|
|
867
|
-
acc.merge(entry.each_with_object({})
|
|
1162
|
+
acc.merge(entry.each_with_object({}) do |(key, value), result|
|
|
1163
|
+
metadata_key = stripe_metadata_key(key)
|
|
1164
|
+
result[metadata_key] = value unless STRIPE_UNSAFE_METADATA_KEYS.include?(metadata_key)
|
|
1165
|
+
end)
|
|
868
1166
|
end
|
|
869
1167
|
.merge(internal.transform_keys { |key| stripe_metadata_key(key) })
|
|
870
1168
|
end
|