better_auth-stripe 0.8.0 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a59a70c7091b88e1239c863a9a01997955d96920052cb1870b3e11736e48747a
4
- data.tar.gz: 80b121e46ffaf456532e56b3c41e019f94358f59ed56e5c21356e2da036e6c9a
3
+ metadata.gz: db7335d48c0ece8f97dcb0d6ef1c29ceec461f643e5d11b3a8e3b902dfad185b
4
+ data.tar.gz: dee15dc386642b8cfeb5d447a1787a55b4c2333281e22a9dc0984020f8e510c1
5
5
  SHA512:
6
- metadata.gz: 2ba47732d4d4ed76aaa44a66792dc9f59d36c45c4155ab6043893cc31ed959424f6c14c4b776ab7fe98586f05fa57f5f61d8a9c6dc831cda73bb137c332598be
7
- data.tar.gz: 191b1ed528b401dfd94a0626c2f7189d67bf44cfcd079cf183ea0874f33fc64e6abdc9187e87e4adf89c97aeecc32755cf42d8f26c417e7f32c6827b8c7013f6
6
+ metadata.gz: 1a9c1161420eabefd7062267e1051e932eb2a2c3a69a3dc4e51a199a778517bc08c6390a2fa6f1b09f9c3ff479ccded8570639f2a5959da737788912b7d50eee
7
+ data.tar.gz: 8d44b61fea832aac0e29eb73414fe612fb86ae4f57b322689d5847029fb957e176e588505b922bf28486c66c7b2dddb8b2410ceaeb763e8b1ff224b043bdef63
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.10.0] - 2026-05-21
6
+
7
+ - Improved Stripe metadata, utility behavior, adapter coverage, and rate-limit coverage.
8
+
5
9
  ## [0.7.0] - 2026-05-05
6
10
 
7
11
  - Changed Stripe webhooks to reject requests when the configured Stripe client does not expose `webhooks.construct_event_async` or `webhooks.construct_event`, preventing unverified payload processing.
data/README.md CHANGED
@@ -51,3 +51,5 @@ Configure plans under `subscription: { enabled: true, plans: [...] }`. Ruby acce
51
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
52
 
53
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.
54
+
55
+ Ruby also routes subscription-cancel billing portal returns through `/subscription/cancel/callback` so a missed webhook can still sync the local cancellation fields before redirecting to the configured callback URL.
@@ -284,28 +284,78 @@ module BetterAuth
284
284
  BetterAuth::Stripe::Utils.plan_line_items(plan)
285
285
  end
286
286
 
287
+ def stripe_line_item_delta(old_plan, plan)
288
+ delta = Hash.new(0)
289
+ stripe_plan_line_items(old_plan || {}).each do |item|
290
+ price = stripe_fetch(item, "price")
291
+ delta[price] -= 1 if price
292
+ end
293
+ stripe_plan_line_items(plan || {}).each do |item|
294
+ price = stripe_fetch(item, "price")
295
+ delta[price] += 1 if price
296
+ end
297
+ delta.delete_if { |_price, count| count == 0 }
298
+ end
299
+
300
+ def stripe_remove_quota(line_item_delta)
301
+ line_item_delta.each_with_object({}) do |(price, delta), result|
302
+ result[price] = -delta if delta.negative?
303
+ end
304
+ end
305
+
306
+ def stripe_consume_positive_delta(line_item_delta, price)
307
+ delta = line_item_delta[price]
308
+ return unless delta&.positive?
309
+
310
+ (delta == 1) ? line_item_delta.delete(price) : line_item_delta[price] = delta - 1
311
+ end
312
+
287
313
  def stripe_schedule_plan_change(ctx, config, active_stripe, db_subscription, plan, price_id, quantity, seat_only_plan, body)
288
314
  schedule = stripe_client(config).subscription_schedules.create(from_subscription: stripe_fetch(active_stripe, "id"))
289
315
  current_phase = Array(stripe_fetch(schedule, "phases")).first || {}
290
316
  current_items = Array(stripe_fetch(current_phase, "items"))
291
317
  active_item = stripe_resolve_plan_item(config, active_stripe)&.fetch(:item, nil) || stripe_subscription_item(active_stripe)
292
318
  active_price_id = stripe_fetch(stripe_fetch(active_item || {}, "price") || {}, "id")
293
- replaced = false
319
+ active_price_present = current_items.any? do |item|
320
+ item_price = stripe_fetch(item, "price")
321
+ item_price = stripe_fetch(item_price, "id") if item_price.is_a?(Hash)
322
+ item_price == active_price_id
323
+ end
324
+ old_plan = db_subscription && stripe_plan_by_name(config, db_subscription["plan"])
325
+ line_item_delta = stripe_line_item_delta(old_plan, plan)
326
+ remove_quota = stripe_remove_quota(line_item_delta)
327
+ price_map = {}
328
+ if plan[:seat_price_id] && old_plan && old_plan[:seat_price_id] && old_plan[:seat_price_id] != plan[:seat_price_id]
329
+ price_map[old_plan[:seat_price_id]] = {price: plan[:seat_price_id], quantity: quantity}
330
+ end
294
331
  new_items = current_items.filter_map do |item|
295
332
  item_price = stripe_fetch(item, "price")
296
333
  item_price = stripe_fetch(item_price, "id") if item_price.is_a?(Hash)
334
+ quota = remove_quota[item_price].to_i
335
+ if quota.positive?
336
+ remove_quota[item_price] = quota - 1
337
+ next nil
338
+ end
339
+
340
+ replacement = price_map[item_price]
341
+ if replacement
342
+ next {price: replacement[:price], quantity: replacement[:quantity] || stripe_fetch(item, "quantity")}.compact
343
+ end
344
+
297
345
  if item_price == active_price_id
298
- replaced = true
299
346
  next nil if seat_only_plan
300
347
 
301
- stripe_line_item(config, price_id, quantity)
348
+ stripe_line_item(config, price_id, plan[:seat_price_id] ? 1 : quantity)
302
349
  else
350
+ stripe_consume_positive_delta(line_item_delta, item_price)
303
351
  {price: item_price, quantity: stripe_fetch(item, "quantity")}.compact
304
352
  end
305
353
  end
306
- new_items << stripe_line_item(config, price_id, quantity) unless replaced || seat_only_plan
307
- new_items << {price: plan[:seat_price_id], quantity: quantity} if plan[:seat_price_id]
308
- new_items.concat(stripe_plan_line_items(plan))
354
+ new_items << stripe_line_item(config, price_id, plan[:seat_price_id] ? 1 : quantity) unless active_price_present || seat_only_plan
355
+ new_items << {price: plan[:seat_price_id], quantity: quantity} if plan[:seat_price_id] && !new_items.any? { |item| item[:price] == plan[:seat_price_id] }
356
+ line_item_delta.each do |price, delta|
357
+ delta.times { new_items << {price: price} } if delta.positive?
358
+ end
309
359
 
310
360
  stripe_client(config).subscription_schedules.update(
311
361
  stripe_fetch(schedule, "id"),
@@ -363,26 +413,32 @@ module BetterAuth
363
413
  def stripe_update_active_subscription_items(ctx, config, active_stripe, db_subscription, old_plan, plan, price_id, quantity, seat_only_plan, body)
364
414
  active_item = stripe_resolve_plan_item(config, active_stripe)&.fetch(:item, nil) || stripe_subscription_item(active_stripe)
365
415
  active_price_id = stripe_fetch(stripe_fetch(active_item || {}, "price") || {}, "id")
366
- old_line_prices = stripe_plan_line_items(old_plan || {}).map { |item| item[:price] }
367
- new_line_prices = stripe_plan_line_items(plan).map { |item| item[:price] }
368
- added_line_prices = new_line_prices - old_line_prices
416
+ line_item_delta = stripe_line_item_delta(old_plan, plan)
417
+ remove_quota = stripe_remove_quota(line_item_delta)
418
+ price_map = {}
419
+ if plan[:seat_price_id] && old_plan && old_plan[:seat_price_id] && old_plan[:seat_price_id] != plan[:seat_price_id]
420
+ price_map[old_plan[:seat_price_id]] = {price: plan[:seat_price_id], quantity: quantity}
421
+ end
369
422
  items = []
370
423
  Array(stripe_fetch(stripe_fetch(active_stripe, "items") || {}, "data")).each do |item|
371
424
  item_price = stripe_fetch(stripe_fetch(item, "price") || {}, "id")
372
- if item_price == active_price_id
425
+ quota = remove_quota[item_price].to_i
426
+ if quota.positive?
427
+ remove_quota[item_price] = quota - 1
428
+ items << {id: stripe_fetch(item, "id"), deleted: true}
429
+ elsif price_map[item_price]
430
+ replacement = price_map[item_price]
431
+ items << {id: stripe_fetch(item, "id"), price: replacement[:price], quantity: replacement[:quantity]}
432
+ elsif item_price == active_price_id
373
433
  items << stripe_line_item(config, price_id, plan[:seat_price_id] ? 1 : quantity).merge(id: stripe_fetch(item, "id")) unless seat_only_plan
374
- elsif old_plan && item_price == old_plan[:seat_price_id] && plan[:seat_price_id]
375
- items << {id: stripe_fetch(item, "id"), price: plan[:seat_price_id], quantity: quantity}
376
- elsif old_line_prices.include?(item_price)
377
- if new_line_prices.include?(item_price)
378
- new_line_prices.delete_at(new_line_prices.index(item_price))
379
- else
380
- items << {id: stripe_fetch(item, "id"), deleted: true}
381
- end
434
+ else
435
+ stripe_consume_positive_delta(line_item_delta, item_price)
382
436
  end
383
437
  end
384
438
  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] }
385
- added_line_prices.each { |price| items << {price: price} }
439
+ line_item_delta.each do |price, delta|
440
+ delta.times { items << {price: price} } if delta.positive?
441
+ end
386
442
  stripe_client(config).subscriptions.update(stripe_fetch(active_stripe, "id"), items: items, proration_behavior: plan[:proration_behavior] || "create_prorations")
387
443
  if db_subscription
388
444
  ctx.context.adapter.update(
@@ -9,6 +9,7 @@ module BetterAuth
9
9
  "EMAIL_VERIFICATION_REQUIRED" => "Email verification required",
10
10
  "SUBSCRIPTION_NOT_FOUND" => "Subscription not found",
11
11
  "SUBSCRIPTION_PLAN_NOT_FOUND" => "Subscription plan not found",
12
+ "FAILED_TO_FETCH_PLANS" => "Failed to fetch plans",
12
13
  "ALREADY_SUBSCRIBED_PLAN" => "You're already subscribed to this plan",
13
14
  "REFERENCE_ID_NOT_ALLOWED" => "Reference id is not allowed",
14
15
  "CUSTOMER_NOT_FOUND" => "Stripe customer not found for this user",
@@ -21,6 +22,7 @@ module BetterAuth
21
22
  "ORGANIZATION_REFERENCE_ID_REQUIRED" => "Reference ID is required. Provide referenceId or set activeOrganizationId in session",
22
23
  "SUBSCRIPTION_NOT_ACTIVE" => "Subscription is not active",
23
24
  "SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION" => "Subscription is not scheduled for cancellation",
25
+ "SUBSCRIPTION_NOT_PENDING_CHANGE" => "Subscription has no pending cancellation or scheduled plan change",
24
26
  "STRIPE_SIGNATURE_NOT_FOUND" => "Stripe signature not found",
25
27
  "STRIPE_WEBHOOK_SECRET_NOT_FOUND" => "Stripe webhook secret not found",
26
28
  "FAILED_TO_CONSTRUCT_STRIPE_EVENT" => "Failed to construct Stripe event",
@@ -53,6 +53,8 @@ module BetterAuth
53
53
 
54
54
  def on_subscription_created(ctx, event)
55
55
  config = stripe_config(ctx)
56
+ return unless config.dig(:subscription, :enabled)
57
+
56
58
  object = BetterAuth::Plugins.normalize_hash(event.dig(:data, :object) || {})
57
59
  customer_id = object[:customer].to_s
58
60
  return if customer_id.empty?
@@ -91,6 +93,8 @@ module BetterAuth
91
93
 
92
94
  def on_subscription_updated(ctx, event)
93
95
  config = stripe_config(ctx)
96
+ return unless config.dig(:subscription, :enabled)
97
+
94
98
  object = BetterAuth::Plugins.normalize_hash(event.dig(:data, :object) || {})
95
99
  resolved = BetterAuth::Stripe::Utils.resolve_plan_item(config, object)
96
100
  return unless resolved
@@ -135,6 +139,8 @@ module BetterAuth
135
139
 
136
140
  def on_subscription_deleted(ctx, event)
137
141
  config = stripe_config(ctx)
142
+ return unless config.dig(:subscription, :enabled)
143
+
138
144
  object = BetterAuth::Plugins.normalize_hash(event.dig(:data, :object) || {})
139
145
  subscription = ctx.context.adapter.find_one(model: "subscription", where: [{field: "stripeSubscriptionId", value: object[:id]}])
140
146
  return unless subscription
@@ -59,7 +59,18 @@ module BetterAuth
59
59
  def metadata_fetch(metadata, key)
60
60
  return nil unless metadata.respond_to?(:[])
61
61
 
62
- metadata[key] || metadata[key.to_sym] || metadata[BetterAuth::Plugins.normalize_key(key)] || metadata[BetterAuth::Plugins.normalize_key(key).to_s]
62
+ candidates = [key, key.to_sym, BetterAuth::Plugins.normalize_key(key), BetterAuth::Plugins.normalize_key(key).to_s]
63
+ if metadata.respond_to?(:key?)
64
+ candidates.each do |candidate|
65
+ return metadata[candidate] if metadata.key?(candidate)
66
+ end
67
+ end
68
+
69
+ candidates.each do |candidate|
70
+ value = metadata[candidate]
71
+ return value unless value.nil?
72
+ end
73
+ nil
63
74
  end
64
75
 
65
76
  def deep_merge(base, override)
@@ -48,6 +48,28 @@ module BetterAuth
48
48
  nil
49
49
  end
50
50
 
51
+ def authorized_subscription?(ctx, session, subscription, action, config)
52
+ reference_id = subscription && subscription["referenceId"]
53
+ return false if reference_id.to_s.empty?
54
+ return true if reference_id == session.fetch(:user).fetch("id")
55
+
56
+ subscription_options = BetterAuth::Stripe::Utils.subscription_options(config)
57
+ if config.dig(:organization, :enabled)
58
+ org = ctx.context.adapter.find_one(model: "organization", where: [{field: "id", value: reference_id}])
59
+ if org
60
+ return false unless subscription_options[:authorize_reference]
61
+
62
+ authorize_reference!(ctx, session, reference_id, action, "organization", subscription_options, explicit: true)
63
+ return true
64
+ end
65
+ end
66
+
67
+ authorize_reference!(ctx, session, reference_id, action, "user", subscription_options, explicit: true)
68
+ true
69
+ rescue BetterAuth::APIError
70
+ false
71
+ end
72
+
51
73
  def validate_trusted_url!(ctx, value, label)
52
74
  return if value.nil? || value.to_s.empty?
53
75
 
@@ -7,7 +7,7 @@ module BetterAuth
7
7
  module_function
8
8
 
9
9
  def endpoint(config)
10
- BetterAuth::Endpoint.new(path: "/subscription/cancel", method: "POST") do |ctx|
10
+ BetterAuth::Endpoint.new(path: "/subscription/cancel", method: "POST", metadata: {openapi: {operationId: "cancelSubscription"}}) do |ctx|
11
11
  session = BetterAuth::Routes.current_session(ctx)
12
12
  body = BetterAuth::Plugins.normalize_hash(ctx.body)
13
13
  BetterAuth::Stripe::Middleware.validate_trusted_urls!(ctx, body, return_url: "returnUrl")
@@ -7,7 +7,7 @@ module BetterAuth
7
7
  module_function
8
8
 
9
9
  def endpoint(config)
10
- BetterAuth::Endpoint.new(path: "/subscription/cancel/callback", method: "GET") do |ctx|
10
+ BetterAuth::Endpoint.new(path: "/subscription/cancel/callback", method: "GET", metadata: {openapi: {operationId: "cancelSubscriptionCallback"}}) do |ctx|
11
11
  query = BetterAuth::Plugins.normalize_hash(ctx.query)
12
12
  callback = query[:callback_url] || "/"
13
13
  BetterAuth::Stripe::Middleware.validate_trusted_url!(ctx, callback, "callbackURL")
@@ -18,6 +18,7 @@ module BetterAuth
18
18
  raise ctx.redirect(BetterAuth::Plugins.stripe_url(ctx, callback)) unless session
19
19
 
20
20
  subscription = ctx.context.adapter.find_one(model: "subscription", where: [{field: "id", value: query[:subscription_id]}])
21
+ raise ctx.redirect(BetterAuth::Plugins.stripe_url(ctx, callback)) if subscription && !BetterAuth::Stripe::Middleware.authorized_subscription?(ctx, session, subscription, "cancel-subscription", config || {})
21
22
  if subscription && !BetterAuth::Plugins.stripe_pending_cancel?(subscription) && subscription["stripeCustomerId"]
22
23
  current = BetterAuth::Plugins.stripe_active_subscriptions(config || {}, subscription["stripeCustomerId"]).find { |entry| BetterAuth::Plugins.stripe_fetch(entry, "id") == subscription["stripeSubscriptionId"] }
23
24
  if current && BetterAuth::Plugins.stripe_stripe_pending_cancel?(current)
@@ -7,7 +7,7 @@ module BetterAuth
7
7
  module_function
8
8
 
9
9
  def endpoint(config)
10
- BetterAuth::Endpoint.new(path: "/subscription/billing-portal", method: "POST") do |ctx|
10
+ BetterAuth::Endpoint.new(path: "/subscription/billing-portal", method: "POST", metadata: {openapi: {operationId: "createBillingPortal"}}) do |ctx|
11
11
  session = BetterAuth::Routes.current_session(ctx)
12
12
  body = BetterAuth::Plugins.normalize_hash(ctx.body)
13
13
  BetterAuth::Stripe::Middleware.validate_trusted_urls!(ctx, body, return_url: "returnUrl")
@@ -7,7 +7,7 @@ module BetterAuth
7
7
  module_function
8
8
 
9
9
  def endpoint(config)
10
- BetterAuth::Endpoint.new(path: "/subscription/list", method: "GET") do |ctx|
10
+ BetterAuth::Endpoint.new(path: "/subscription/list", method: "GET", metadata: {openapi: {operationId: "listActiveSubscriptions"}}) do |ctx|
11
11
  session = BetterAuth::Routes.current_session(ctx)
12
12
  query = BetterAuth::Plugins.normalize_hash(ctx.query)
13
13
  customer_type = BetterAuth::Plugins.stripe_customer_type!(query)
@@ -7,7 +7,7 @@ module BetterAuth
7
7
  module_function
8
8
 
9
9
  def endpoint(config)
10
- BetterAuth::Endpoint.new(path: "/subscription/restore", method: "POST") do |ctx|
10
+ BetterAuth::Endpoint.new(path: "/subscription/restore", method: "POST", metadata: {openapi: {operationId: "restoreSubscription"}}) do |ctx|
11
11
  session = BetterAuth::Routes.current_session(ctx)
12
12
  body = BetterAuth::Plugins.normalize_hash(ctx.body)
13
13
  customer_type = BetterAuth::Plugins.stripe_customer_type!(body)
@@ -26,9 +26,11 @@ module BetterAuth
26
26
  next ctx.json(BetterAuth::Plugins.stripe_stringify_keys(schedule))
27
27
  end
28
28
 
29
- raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Stripe::ERROR_CODES.fetch("SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION")) unless BetterAuth::Plugins.stripe_pending_cancel?(subscription)
29
+ raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Stripe::ERROR_CODES.fetch("SUBSCRIPTION_NOT_PENDING_CHANGE")) unless BetterAuth::Plugins.stripe_pending_cancel?(subscription)
30
30
 
31
- active = BetterAuth::Plugins.stripe_active_subscriptions(config, subscription["stripeCustomerId"]).first
31
+ active = BetterAuth::Plugins.stripe_active_subscriptions(config, subscription["stripeCustomerId"]).find do |entry|
32
+ BetterAuth::Plugins.stripe_fetch(entry, "id") == subscription["stripeSubscriptionId"]
33
+ end
32
34
  raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Stripe::ERROR_CODES.fetch("SUBSCRIPTION_NOT_FOUND")) unless active
33
35
 
34
36
  update_params = if BetterAuth::Plugins.stripe_fetch(active, "cancel_at")
@@ -7,7 +7,7 @@ module BetterAuth
7
7
  module_function
8
8
 
9
9
  def endpoint(config)
10
- BetterAuth::Endpoint.new(path: "/stripe/webhook", method: "POST", disable_body: true) do |ctx|
10
+ BetterAuth::Endpoint.new(path: "/stripe/webhook", method: "POST", metadata: {hide: true}, disable_body: true) do |ctx|
11
11
  signature = ctx.headers["stripe-signature"]
12
12
  raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Stripe::ERROR_CODES.fetch("STRIPE_SIGNATURE_NOT_FOUND")) if signature.to_s.empty?
13
13
 
@@ -36,8 +36,13 @@ module BetterAuth
36
36
  raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Stripe::ERROR_CODES.fetch("FAILED_TO_CONSTRUCT_STRIPE_EVENT")) unless event
37
37
  begin
38
38
  BetterAuth::Plugins.stripe_handle_event(ctx, event)
39
- rescue
40
- raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Stripe::ERROR_CODES.fetch("STRIPE_WEBHOOK_ERROR"))
39
+ rescue => error
40
+ logger = ctx.context.logger
41
+ if logger.respond_to?(:error)
42
+ logger.error("Stripe webhook failed. Error: #{error.message}")
43
+ elsif logger.respond_to?(:call)
44
+ logger.call(:error, "Stripe webhook failed. Error: #{error.message}")
45
+ end
41
46
  end
42
47
  ctx.json({success: true})
43
48
  end
@@ -7,12 +7,12 @@ module BetterAuth
7
7
  module_function
8
8
 
9
9
  def endpoint(config)
10
- BetterAuth::Endpoint.new(path: "/subscription/success", method: "GET") do |ctx|
10
+ BetterAuth::Endpoint.new(path: "/subscription/success", method: "GET", metadata: {openapi: {operationId: "subscriptionSuccess"}}) do |ctx|
11
11
  query = BetterAuth::Plugins.normalize_hash(ctx.query)
12
12
  callback = query[:callback_url] || "/"
13
13
  BetterAuth::Stripe::Middleware.validate_trusted_url!(ctx, callback, "callbackURL")
14
14
  checkout_session_id = query[:checkout_session_id]
15
- subscription_id = query[:subscription_id]
15
+ subscription_id = nil
16
16
  if checkout_session_id
17
17
  callback = callback.to_s.gsub("{CHECKOUT_SESSION_ID}", checkout_session_id.to_s)
18
18
  checkout_session = begin
@@ -34,6 +34,7 @@ module BetterAuth
34
34
 
35
35
  subscription = ctx.context.adapter.find_one(model: "subscription", where: [{field: "id", value: subscription_id}])
36
36
  raise ctx.redirect(BetterAuth::Plugins.stripe_url(ctx, callback)) unless subscription
37
+ raise ctx.redirect(BetterAuth::Plugins.stripe_url(ctx, callback)) unless BetterAuth::Stripe::Middleware.authorized_subscription?(ctx, session, subscription, "subscription-success", config || {})
37
38
  raise ctx.redirect(BetterAuth::Plugins.stripe_url(ctx, callback)) if BetterAuth::Plugins.stripe_active_or_trialing?(subscription)
38
39
 
39
40
  customer_id = subscription["stripeCustomerId"] || session.fetch(:user)["stripeCustomerId"]
@@ -7,7 +7,7 @@ module BetterAuth
7
7
  module_function
8
8
 
9
9
  def endpoint(config)
10
- BetterAuth::Endpoint.new(path: "/subscription/upgrade", method: "POST") do |ctx|
10
+ BetterAuth::Endpoint.new(path: "/subscription/upgrade", method: "POST", metadata: {openapi: {operationId: "upgradeSubscription"}}) do |ctx|
11
11
  session = BetterAuth::Routes.current_session(ctx)
12
12
  body = BetterAuth::Plugins.normalize_hash(ctx.body)
13
13
  BetterAuth::Stripe::Middleware.validate_trusted_urls!(ctx, body, success_url: "successUrl", cancel_url: "cancelUrl", return_url: "returnUrl")
@@ -62,7 +62,7 @@ module BetterAuth
62
62
  stripe_price_id_value = BetterAuth::Plugins.stripe_fetch(BetterAuth::Plugins.stripe_fetch(active_stripe_item || {}, "price") || {}, "id")
63
63
  same_plan = active_or_trialing && active_or_trialing["plan"].to_s.downcase == body[:plan].to_s.downcase
64
64
  same_seats = auto_managed_seats || (active_or_trialing && active_or_trialing["seats"].to_i == requested_seats.to_i)
65
- same_price = !active_stripe || stripe_price_id_value == price_id
65
+ same_price = !!active_stripe && stripe_price_id_value == price_id
66
66
  valid_period = !active_or_trialing || !active_or_trialing["periodEnd"] || active_or_trialing["periodEnd"] > Time.now
67
67
  if active_or_trialing&.fetch("status", nil) == "active" && same_plan && same_seats && same_price && valid_period
68
68
  raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Stripe::ERROR_CODES.fetch("ALREADY_SUBSCRIBED_PLAN"))
@@ -16,7 +16,12 @@ module BetterAuth
16
16
  def fetch(object, key)
17
17
  return nil unless object.respond_to?(:[])
18
18
 
19
- object[key] || object[key.to_sym]
19
+ return object[key] if object.respond_to?(:key?) && object.key?(key)
20
+
21
+ symbol_key = key.to_sym
22
+ return object[symbol_key] if object.respond_to?(:key?) && object.key?(symbol_key)
23
+
24
+ object[key] || object[symbol_key]
20
25
  end
21
26
 
22
27
  def time(value)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BetterAuth
4
4
  module Stripe
5
- VERSION = "0.8.0"
5
+ VERSION = "0.10.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_auth-stripe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Sala
@@ -132,14 +132,14 @@ files:
132
132
  - lib/better_auth/stripe/types.rb
133
133
  - lib/better_auth/stripe/utils.rb
134
134
  - lib/better_auth/stripe/version.rb
135
- homepage: https://github.com/sebasxsala/better-auth
135
+ homepage: https://github.com/sebasxsala/better-auth-rb
136
136
  licenses:
137
137
  - MIT
138
138
  metadata:
139
- homepage_uri: https://github.com/sebasxsala/better-auth
140
- source_code_uri: https://github.com/sebasxsala/better-auth
141
- changelog_uri: https://github.com/sebasxsala/better-auth/blob/main/packages/better_auth-stripe/CHANGELOG.md
142
- bug_tracker_uri: https://github.com/sebasxsala/better-auth/issues
139
+ homepage_uri: https://github.com/sebasxsala/better-auth-rb
140
+ source_code_uri: https://github.com/sebasxsala/better-auth-rb
141
+ changelog_uri: https://github.com/sebasxsala/better-auth-rb/blob/main/packages/better_auth-stripe/CHANGELOG.md
142
+ bug_tracker_uri: https://github.com/sebasxsala/better-auth-rb/issues
143
143
  rdoc_options: []
144
144
  require_paths:
145
145
  - lib