better_auth-stripe 0.2.1 → 0.6.2

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.
@@ -1,76 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "securerandom"
4
- require "stripe"
5
-
6
- module BetterAuth
7
- module Stripe
8
- class ClientAdapter
9
- attr_reader :customers, :checkout, :billing_portal, :subscriptions, :prices, :subscription_schedules, :webhooks
10
-
11
- def initialize(api_key)
12
- client = ::Stripe::StripeClient.new(api_key)
13
- @customers = ResourceAdapter.new(client.v1.customers)
14
- @checkout = NamespaceAdapter.new(sessions: ResourceAdapter.new(client.v1.checkout.sessions))
15
- @billing_portal = NamespaceAdapter.new(sessions: ResourceAdapter.new(client.v1.billing_portal.sessions))
16
- @subscriptions = ResourceAdapter.new(client.v1.subscriptions)
17
- @prices = ResourceAdapter.new(client.v1.prices)
18
- @subscription_schedules = ResourceAdapter.new(client.v1.subscription_schedules)
19
- @webhooks = WebhooksAdapter.new
20
- end
21
- end
22
-
23
- class NamespaceAdapter
24
- def initialize(resources)
25
- resources.each do |name, resource|
26
- instance_variable_set(:"@#{name}", resource)
27
- self.class.attr_reader(name) unless respond_to?(name)
28
- end
29
- end
30
- end
31
-
32
- class ResourceAdapter
33
- def initialize(resource)
34
- @resource = resource
35
- end
36
-
37
- def create(params = {}, options = nil)
38
- options ? @resource.create(params || {}, options) : @resource.create(params || {})
39
- end
40
-
41
- def list(params = {})
42
- @resource.list(params || {})
43
- end
44
-
45
- def search(params = {})
46
- @resource.search(params || {})
47
- end
48
-
49
- def retrieve(id)
50
- @resource.retrieve(id)
51
- end
52
-
53
- def update(id, params = {})
54
- @resource.update(id, params || {})
55
- end
56
-
57
- def release(id)
58
- @resource.release(id)
59
- end
60
- end
61
-
62
- class WebhooksAdapter
63
- def construct_event(payload, signature, secret)
64
- ::Stripe::Webhook.construct_event(payload, signature, secret)
65
- end
66
-
67
- def construct_event_async(payload, signature, secret)
68
- construct_event(payload, signature, secret)
69
- end
70
- end
71
- end
72
- end
73
-
74
3
  module BetterAuth
75
4
  module Plugins
76
5
  singleton_class.remove_method(:stripe) if singleton_class.method_defined?(:stripe)
@@ -79,656 +8,79 @@ module BetterAuth
79
8
 
80
9
  module_function
81
10
 
82
- STRIPE_ERROR_CODES = {
83
- "UNAUTHORIZED" => "Unauthorized access",
84
- "EMAIL_VERIFICATION_REQUIRED" => "Email verification required",
85
- "SUBSCRIPTION_NOT_FOUND" => "Subscription not found",
86
- "SUBSCRIPTION_PLAN_NOT_FOUND" => "Subscription plan not found",
87
- "ALREADY_SUBSCRIBED_PLAN" => "You're already subscribed to this plan",
88
- "REFERENCE_ID_NOT_ALLOWED" => "Reference id is not allowed",
89
- "CUSTOMER_NOT_FOUND" => "Stripe customer not found for this user",
90
- "UNABLE_TO_CREATE_CUSTOMER" => "Unable to create customer",
91
- "UNABLE_TO_CREATE_BILLING_PORTAL" => "Unable to create billing portal session",
92
- "ORGANIZATION_NOT_FOUND" => "Organization not found",
93
- "ORGANIZATION_SUBSCRIPTION_NOT_ENABLED" => "Organization subscription is not enabled",
94
- "AUTHORIZE_REFERENCE_REQUIRED" => "Organization subscriptions require authorizeReference callback to be configured",
95
- "ORGANIZATION_HAS_ACTIVE_SUBSCRIPTION" => "Cannot delete organization with active subscription",
96
- "ORGANIZATION_REFERENCE_ID_REQUIRED" => "Reference ID is required. Provide referenceId or set activeOrganizationId in session",
97
- "SUBSCRIPTION_NOT_ACTIVE" => "Subscription is not active",
98
- "SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION" => "Subscription is not scheduled for cancellation",
99
- "STRIPE_SIGNATURE_NOT_FOUND" => "Stripe signature not found",
100
- "STRIPE_WEBHOOK_SECRET_NOT_FOUND" => "Stripe webhook secret not found",
101
- "FAILED_TO_CONSTRUCT_STRIPE_EVENT" => "Failed to construct Stripe event",
102
- "STRIPE_WEBHOOK_ERROR" => "Stripe webhook error",
103
- "INVALID_CUSTOMER_TYPE" => "Customer type must be either user or organization",
104
- "INVALID_REQUEST_BODY" => "Invalid request body"
105
- }.freeze
106
- STRIPE_UNSAFE_METADATA_KEYS = %w[__proto__ constructor prototype].freeze
11
+ STRIPE_ERROR_CODES = BetterAuth::Stripe::ERROR_CODES
12
+ STRIPE_UNSAFE_METADATA_KEYS = BetterAuth::Stripe::Metadata::UNSAFE_KEYS
107
13
 
108
14
  def stripe(options = {})
109
- config = normalize_hash(options)
110
- Plugin.new(
111
- id: "stripe",
112
- init: ->(ctx) { {context: {schema: Schema.auth_tables(ctx.options)}} },
113
- schema: stripe_schema(config),
114
- endpoints: stripe_endpoints(config),
115
- error_codes: STRIPE_ERROR_CODES,
116
- options: config.merge(database_hooks: stripe_database_hooks(config), organization_hooks: stripe_organization_hooks(config))
117
- )
15
+ BetterAuth::Stripe::PluginFactory.build(options)
118
16
  end
119
17
 
120
18
  def stripe_schema(config)
121
- schema = {
122
- user: {
123
- fields: {
124
- stripeCustomerId: {type: "string", required: false}
125
- }
126
- }
127
- }
128
- if config.dig(:subscription, :enabled)
129
- schema[:subscription] = {
130
- fields: {
131
- plan: {type: "string", required: true},
132
- referenceId: {type: "string", required: true},
133
- stripeCustomerId: {type: "string", required: false},
134
- stripeSubscriptionId: {type: "string", required: false},
135
- status: {type: "string", required: false, default_value: "incomplete"},
136
- periodStart: {type: "date", required: false},
137
- periodEnd: {type: "date", required: false},
138
- trialStart: {type: "date", required: false},
139
- trialEnd: {type: "date", required: false},
140
- cancelAtPeriodEnd: {type: "boolean", required: false, default_value: false},
141
- cancelAt: {type: "date", required: false},
142
- canceledAt: {type: "date", required: false},
143
- endedAt: {type: "date", required: false},
144
- seats: {type: "number", required: false},
145
- billingInterval: {type: "string", required: false},
146
- stripeScheduleId: {type: "string", required: false},
147
- limits: {type: "json", required: false}
148
- }
149
- }
150
- end
151
- if config.dig(:organization, :enabled)
152
- schema[:organization] = {fields: {stripeCustomerId: {type: "string", required: false}}}
153
- end
154
- schema
19
+ BetterAuth::Stripe::Schema.schema(config)
155
20
  end
156
21
 
157
22
  def stripe_endpoints(config)
158
- endpoints = {stripe_webhook: stripe_webhook_endpoint(config)}
159
- return endpoints unless config.dig(:subscription, :enabled)
160
-
161
- endpoints.merge(
162
- upgrade_subscription: stripe_upgrade_subscription_endpoint(config),
163
- cancel_subscription_callback: stripe_cancel_callback_endpoint(config),
164
- cancel_subscription: stripe_cancel_subscription_endpoint(config),
165
- restore_subscription: stripe_restore_subscription_endpoint(config),
166
- list_active_subscriptions: stripe_list_subscriptions_endpoint(config),
167
- subscription_success: stripe_success_endpoint(config),
168
- create_billing_portal: stripe_billing_portal_endpoint(config)
169
- )
23
+ BetterAuth::Stripe::Routes.endpoints(config)
170
24
  end
171
25
 
172
26
  def stripe_database_hooks(config)
173
- return {} unless config[:create_customer_on_sign_up]
174
-
175
- {
176
- user: {
177
- create: {
178
- before: lambda do |data, hook_ctx|
179
- next unless data["email"] && !data["stripeCustomerId"]
180
-
181
- data["id"] ||= SecureRandom.hex(16)
182
- customer = stripe_find_or_create_user_customer(config, data, nil, hook_ctx)
183
- {data: {id: data["id"], stripeCustomerId: stripe_id(customer)}}
184
- rescue
185
- nil
186
- end
187
- },
188
- update: {
189
- after: lambda do |user, _ctx|
190
- next unless user && user["stripeCustomerId"]
191
-
192
- customer = stripe_client(config).customers.retrieve(user["stripeCustomerId"])
193
- next if stripe_fetch(customer, "deleted")
194
- next if stripe_fetch(customer, "email") == user["email"]
195
-
196
- stripe_client(config).customers.update(user["stripeCustomerId"], email: user["email"])
197
- rescue
198
- nil
199
- end
200
- }
201
- }
202
- }
27
+ BetterAuth::Stripe::PluginFactory.database_hooks(config)
203
28
  end
204
29
 
205
30
  def stripe_organization_hooks(config)
206
- return {} unless config.dig(:organization, :enabled)
207
-
208
- {
209
- after_update_organization: lambda do |data, _ctx|
210
- organization = data[:organization] || data["organization"]
211
- next unless organization && organization["stripeCustomerId"]
212
-
213
- customer = stripe_client(config).customers.retrieve(organization["stripeCustomerId"])
214
- next if stripe_fetch(customer, "deleted")
215
- next if stripe_fetch(customer, "name") == organization["name"]
216
-
217
- stripe_client(config).customers.update(organization["stripeCustomerId"], name: organization["name"])
218
- rescue
219
- nil
220
- end,
221
- before_delete_organization: lambda do |data, _ctx|
222
- organization = data[:organization] || data["organization"]
223
- next unless organization && organization["stripeCustomerId"]
224
-
225
- subscriptions = stripe_client(config).subscriptions.list(customer: organization["stripeCustomerId"], status: "all", limit: 100)
226
- active = Array(stripe_fetch(subscriptions, "data")).any? do |subscription|
227
- !%w[canceled incomplete incomplete_expired].include?(stripe_fetch(subscription, "status").to_s)
228
- end
229
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("ORGANIZATION_HAS_ACTIVE_SUBSCRIPTION")) if active
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) }
234
- }
31
+ BetterAuth::Stripe::OrganizationHooks.hooks(config)
235
32
  end
236
33
 
237
34
  def stripe_upgrade_subscription_endpoint(config)
238
- Endpoint.new(path: "/subscription/upgrade", method: "POST") do |ctx|
239
- session = Routes.current_session(ctx)
240
- body = normalize_hash(ctx.body)
241
- subscription_options = stripe_subscription_options(config)
242
- customer_type = stripe_customer_type!(body)
243
- reference_id = stripe_reference_id!(ctx, session, customer_type, body[:reference_id], config)
244
- stripe_authorize_reference!(ctx, session, reference_id, "upgrade-subscription", customer_type, subscription_options, explicit: body.key?(:reference_id))
245
-
246
- user = session.fetch(:user)
247
- if subscription_options[:require_email_verification] && !user["emailVerified"]
248
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("EMAIL_VERIFICATION_REQUIRED"))
249
- end
250
-
251
- plan = stripe_plan_by_name(config, body[:plan])
252
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_PLAN_NOT_FOUND")) unless plan
253
-
254
- subscription_to_update = nil
255
- if body[:subscription_id]
256
- subscription_to_update = ctx.context.adapter.find_one(model: "subscription", where: [{field: "stripeSubscriptionId", value: body[:subscription_id]}])
257
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_FOUND")) unless subscription_to_update && subscription_to_update["referenceId"] == reference_id
258
- end
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)
262
- customer_id = if customer_type == "organization"
263
- subscription_to_update&.fetch("stripeCustomerId", nil) || reference_customer_id || stripe_organization_customer(config, ctx, reference_id, body[:metadata])
264
- else
265
- subscription_to_update&.fetch("stripeCustomerId", nil) || reference_customer_id || user["stripeCustomerId"] || stripe_create_customer(config, ctx, user, body[:metadata])
266
- end
267
-
268
- active_or_trialing = subscriptions.find { |entry| stripe_active_or_trialing?(entry) }
269
- active_stripe_subscriptions = stripe_active_subscriptions(config, customer_id)
270
- active_stripe = active_stripe_subscriptions.find do |entry|
271
- if subscription_to_update&.fetch("stripeSubscriptionId", nil) || body[:subscription_id]
272
- stripe_fetch(entry, "id") == subscription_to_update&.fetch("stripeSubscriptionId", nil) || stripe_fetch(entry, "id") == body[:subscription_id]
273
- elsif active_or_trialing && active_or_trialing["stripeSubscriptionId"]
274
- stripe_fetch(entry, "id") == active_or_trialing["stripeSubscriptionId"]
275
- else
276
- false
277
- end
278
- end
279
-
280
- price_id = stripe_price_id(config, plan, body[:annual])
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
286
-
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 || {})
289
- stripe_price_id_value = stripe_fetch(stripe_fetch(active_stripe_item || {}, "price") || {}, "id")
290
- same_plan = active_or_trialing && active_or_trialing["plan"].to_s.downcase == body[:plan].to_s.downcase
291
- same_seats = auto_managed_seats || (active_or_trialing && active_or_trialing["seats"].to_i == requested_seats.to_i)
292
- same_price = !active_stripe || stripe_price_id_value == price_id
293
- valid_period = !active_or_trialing || !active_or_trialing["periodEnd"] || active_or_trialing["periodEnd"] > Time.now
294
- if active_or_trialing&.fetch("status", nil) == "active" && same_plan && same_seats && same_price && valid_period
295
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("ALREADY_SUBSCRIBED_PLAN"))
296
- end
297
-
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
-
312
- portal = stripe_client(config).billing_portal.sessions.create(
313
- customer: customer_id,
314
- return_url: stripe_url(ctx, body[:return_url] || "/"),
315
- flow_data: {
316
- type: "subscription_update_confirm",
317
- after_completion: {type: "redirect", redirect: {return_url: stripe_url(ctx, body[:return_url] || "/")}},
318
- subscription_update_confirm: {
319
- subscription: stripe_fetch(active_stripe, "id"),
320
- items: [stripe_line_item(config, price_id, requested_seats).merge(id: stripe_fetch(active_stripe_item || {}, "id"))]
321
- }
322
- }
323
- )
324
- next ctx.json(stripe_stringify_keys(portal).merge(redirect: stripe_redirect?(body)))
325
- end
326
-
327
- incomplete = subscriptions.find { |entry| entry["status"] == "incomplete" }
328
- subscription = active_or_trialing || incomplete
329
- if subscription
330
- update = {plan: plan[:name].to_s.downcase, seats: requested_seats}
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) })
332
- else
333
- subscription = ctx.context.adapter.create(
334
- model: "subscription",
335
- data: {plan: plan[:name].to_s.downcase, referenceId: reference_id, stripeCustomerId: customer_id, status: "incomplete", seats: requested_seats, limits: plan[:limits]}
336
- )
337
- end
338
-
339
- has_ever_trialed = ctx.context.adapter.find_many(model: "subscription", where: [{field: "referenceId", value: reference_id}]).any? do |entry|
340
- entry["trialStart"] || entry["trialEnd"] || entry["status"] == "trialing"
341
- end
342
- free_trial = (!has_ever_trialed && plan[:free_trial]) ? {trial_period_days: plan.dig(:free_trial, :days)} : {}
343
- checkout_customization = subscription_options[:get_checkout_session_params]&.call(
344
- {user: user, session: session.fetch(:session), plan: plan, subscription: subscription},
345
- ctx.request,
346
- ctx
347
- ) || {}
348
- custom_params = stripe_fetch(checkout_customization, "params") || {}
349
- custom_options = normalize_hash(stripe_fetch(checkout_customization, "options") || {})
350
- custom_subscription_data = stripe_fetch(custom_params, "subscription_data") || stripe_fetch(custom_params, "subscriptionData") || {}
351
- internal_metadata = {userId: user.fetch("id"), subscriptionId: subscription.fetch("id"), referenceId: reference_id}
352
- metadata = stripe_subscription_metadata_set(internal_metadata, body[:metadata], stripe_fetch(custom_params, "metadata"))
353
- subscription_metadata = stripe_subscription_metadata_set(internal_metadata, body[:metadata], stripe_fetch(custom_subscription_data, "metadata"))
354
- checkout_params = stripe_deep_merge(
355
- custom_params,
356
- customer: customer_id,
357
- customer_update: (customer_type == "user") ? {name: "auto", address: "auto"} : {address: "auto"},
358
- locale: body[:locale],
359
- success_url: stripe_url(ctx, "#{ctx.context.base_url}/subscription/success?callbackURL=#{Rack::Utils.escape(body[:success_url] || "/")}&checkoutSessionId={CHECKOUT_SESSION_ID}"),
360
- cancel_url: stripe_url(ctx, body[:cancel_url] || "/"),
361
- line_items: stripe_checkout_line_items(config, plan, price_id, requested_seats, auto_managed_seats, seat_only_plan),
362
- subscription_data: free_trial.merge(metadata: subscription_metadata),
363
- mode: "subscription",
364
- client_reference_id: reference_id,
365
- metadata: metadata
366
- )
367
- checkout_params[:metadata] = metadata
368
- checkout_params[:subscription_data] ||= {}
369
- checkout_params[:subscription_data][:metadata] = subscription_metadata
370
- checkout = stripe_client(config).checkout.sessions.create(checkout_params, custom_options.empty? ? nil : custom_options)
371
- ctx.json(stripe_stringify_keys(checkout).merge(redirect: stripe_redirect?(body)))
372
- end
35
+ BetterAuth::Stripe::Routes::UpgradeSubscription.endpoint(config)
373
36
  end
374
37
 
375
38
  def stripe_cancel_subscription_endpoint(config)
376
- Endpoint.new(path: "/subscription/cancel", method: "POST") do |ctx|
377
- session = Routes.current_session(ctx)
378
- body = normalize_hash(ctx.body)
379
- customer_type = stripe_customer_type!(body)
380
- reference_id = stripe_reference_id!(ctx, session, customer_type, body[:reference_id], config)
381
- stripe_authorize_reference!(ctx, session, reference_id, "cancel-subscription", customer_type, stripe_subscription_options(config), explicit: body.key?(:reference_id))
382
- subscription = stripe_find_subscription_for_action(ctx, reference_id, body[:subscription_id], active_only: true)
383
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_FOUND")) unless subscription && subscription["stripeCustomerId"]
384
-
385
- active = stripe_active_subscriptions(config, subscription["stripeCustomerId"])
386
- if active.empty?
387
- ctx.context.adapter.delete_many(model: "subscription", where: [{field: "referenceId", value: reference_id}])
388
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_FOUND"))
389
- end
390
- stripe_subscription = active.find { |entry| stripe_fetch(entry, "id") == subscription["stripeSubscriptionId"] }
391
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_FOUND")) unless stripe_subscription
392
-
393
- portal = stripe_client(config).billing_portal.sessions.create(
394
- customer: subscription["stripeCustomerId"],
395
- return_url: stripe_url(ctx, "#{ctx.context.base_url}/subscription/cancel/callback?callbackURL=#{Rack::Utils.escape(body[:return_url] || "/")}&subscriptionId=#{Rack::Utils.escape(subscription.fetch("id"))}"),
396
- flow_data: {type: "subscription_cancel", subscription_cancel: {subscription: stripe_fetch(stripe_subscription, "id")}}
397
- )
398
- ctx.json(stripe_stringify_keys(portal).merge(redirect: stripe_redirect?(body)))
399
- rescue APIError
400
- raise
401
- rescue => error
402
- if error.message.include?("already set to be canceled") && subscription && !stripe_pending_cancel?(subscription)
403
- stripe_sub = stripe_client(config).subscriptions.retrieve(subscription["stripeSubscriptionId"])
404
- ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: stripe_subscription_state(stripe_sub, include_status: false))
405
- end
406
- raise APIError.new("BAD_REQUEST", message: error.message)
407
- end
39
+ BetterAuth::Stripe::Routes::CancelSubscription.endpoint(config)
408
40
  end
409
41
 
410
42
  def stripe_restore_subscription_endpoint(config)
411
- Endpoint.new(path: "/subscription/restore", method: "POST") do |ctx|
412
- session = Routes.current_session(ctx)
413
- body = normalize_hash(ctx.body)
414
- customer_type = stripe_customer_type!(body)
415
- reference_id = stripe_reference_id!(ctx, session, customer_type, body[:reference_id], config)
416
- stripe_authorize_reference!(ctx, session, reference_id, "restore-subscription", customer_type, stripe_subscription_options(config), explicit: body.key?(:reference_id))
417
- subscription = stripe_find_subscription_for_action(ctx, reference_id, body[:subscription_id], active_only: false)
418
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_FOUND")) unless subscription && subscription["stripeCustomerId"]
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
-
430
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_SCHEDULED_FOR_CANCELLATION")) unless stripe_pending_cancel?(subscription)
431
-
432
- active = stripe_active_subscriptions(config, subscription["stripeCustomerId"]).first
433
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("SUBSCRIPTION_NOT_FOUND")) unless active
434
-
435
- update_params = if stripe_fetch(active, "cancel_at")
436
- {cancel_at: ""}
437
- elsif stripe_fetch(active, "cancel_at_period_end")
438
- {cancel_at_period_end: false}
439
- else
440
- {}
441
- end
442
- restored = stripe_client(config).subscriptions.update(stripe_fetch(active, "id"), update_params)
443
- ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: {cancelAtPeriodEnd: false, cancelAt: nil, canceledAt: nil})
444
- ctx.json(stripe_stringify_keys(restored))
445
- end
43
+ BetterAuth::Stripe::Routes::RestoreSubscription.endpoint(config)
446
44
  end
447
45
 
448
46
  def stripe_list_subscriptions_endpoint(config)
449
- Endpoint.new(path: "/subscription/list", method: "GET") do |ctx|
450
- session = Routes.current_session(ctx)
451
- query = normalize_hash(ctx.query)
452
- customer_type = stripe_customer_type!(query)
453
- reference_id = stripe_reference_id!(ctx, session, customer_type, query[:reference_id], config)
454
- stripe_authorize_reference!(ctx, session, reference_id, "list-subscription", customer_type, stripe_subscription_options(config), explicit: query.key?(:reference_id))
455
- plans = stripe_plans(config)
456
- subscriptions = ctx.context.adapter.find_many(model: "subscription", where: [{field: "referenceId", value: reference_id}]).select { |entry| stripe_active_or_trialing?(entry) }
457
- ctx.json(subscriptions.map do |entry|
458
- plan = plans.find { |item| item[:name].to_s.downcase == entry["plan"].to_s.downcase }
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)
465
- end)
466
- end
47
+ BetterAuth::Stripe::Routes::ListActiveSubscriptions.endpoint(config)
467
48
  end
468
49
 
469
50
  def stripe_billing_portal_endpoint(config)
470
- Endpoint.new(path: "/subscription/billing-portal", method: "POST") do |ctx|
471
- session = Routes.current_session(ctx)
472
- body = normalize_hash(ctx.body)
473
- customer_type = stripe_customer_type!(body)
474
- reference_id = stripe_reference_id!(ctx, session, customer_type, body[:reference_id], config)
475
- stripe_authorize_reference!(ctx, session, reference_id, "billing-portal", customer_type, stripe_subscription_options(config), explicit: body.key?(:reference_id))
476
- customer_id = if customer_type == "organization"
477
- org = ctx.context.adapter.find_one(model: "organization", where: [{field: "id", value: reference_id}])
478
- org&.fetch("stripeCustomerId", nil) || stripe_active_subscription(ctx, reference_id)&.fetch("stripeCustomerId", nil)
479
- else
480
- session.fetch(:user)["stripeCustomerId"] || stripe_active_subscription(ctx, reference_id)&.fetch("stripeCustomerId", nil)
481
- end
482
- raise APIError.new("NOT_FOUND", message: STRIPE_ERROR_CODES.fetch("CUSTOMER_NOT_FOUND")) unless customer_id
483
-
484
- portal = stripe_client(config).billing_portal.sessions.create(customer: customer_id, return_url: stripe_url(ctx, body[:return_url] || "/"), locale: body[:locale])
485
- ctx.json(stripe_stringify_keys(portal).merge(redirect: stripe_redirect?(body)))
486
- rescue APIError
487
- raise
488
- rescue
489
- raise APIError.new("INTERNAL_SERVER_ERROR", message: STRIPE_ERROR_CODES.fetch("UNABLE_TO_CREATE_BILLING_PORTAL"))
490
- end
51
+ BetterAuth::Stripe::Routes::CreateBillingPortal.endpoint(config)
491
52
  end
492
53
 
493
- def stripe_cancel_callback_endpoint(config = nil)
494
- Endpoint.new(path: "/subscription/cancel/callback", method: "GET") do |ctx|
495
- query = normalize_hash(ctx.query)
496
- callback = query[:callback_url] || "/"
497
- unless query[:subscription_id]
498
- raise ctx.redirect(stripe_url(ctx, callback))
499
- end
500
- session = Routes.current_session(ctx, allow_nil: true)
501
- raise ctx.redirect(stripe_url(ctx, callback)) unless session
502
-
503
- subscription = ctx.context.adapter.find_one(model: "subscription", where: [{field: "id", value: query[:subscription_id]}])
504
- if subscription && !stripe_pending_cancel?(subscription) && subscription["stripeCustomerId"]
505
- current = stripe_active_subscriptions(config || {}, subscription["stripeCustomerId"]).find { |entry| stripe_fetch(entry, "id") == subscription["stripeSubscriptionId"] }
506
- if current && stripe_stripe_pending_cancel?(current)
507
- ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: stripe_subscription_state(current, include_status: true))
508
- stripe_subscription_options(config || {})[:on_subscription_cancel]&.call({subscription: subscription, stripeSubscription: current, stripe_subscription: current, cancellationDetails: stripe_fetch(current, "cancellation_details"), cancellation_details: stripe_fetch(current, "cancellation_details"), event: nil})
509
- end
510
- end
511
- raise ctx.redirect(stripe_url(ctx, callback))
512
- end
54
+ def stripe_cancel_callback_endpoint(config)
55
+ BetterAuth::Stripe::Routes::CancelSubscriptionCallback.endpoint(config)
513
56
  end
514
57
 
515
- def stripe_success_endpoint(config = nil)
516
- Endpoint.new(path: "/subscription/success", method: "GET") do |ctx|
517
- query = normalize_hash(ctx.query)
518
- callback = query[:callback_url] || "/"
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
535
- raise ctx.redirect(stripe_url(ctx, callback))
536
- end
537
- session = Routes.current_session(ctx, allow_nil: true)
538
- raise ctx.redirect(stripe_url(ctx, callback)) unless session
539
-
540
- subscription = ctx.context.adapter.find_one(model: "subscription", where: [{field: "id", value: subscription_id}])
541
- raise ctx.redirect(stripe_url(ctx, callback)) unless subscription
542
- raise ctx.redirect(stripe_url(ctx, callback)) if stripe_active_or_trialing?(subscription)
543
-
544
- customer_id = subscription["stripeCustomerId"] || session.fetch(:user)["stripeCustomerId"]
545
- raise ctx.redirect(stripe_url(ctx, callback)) unless customer_id
546
-
547
- stripe_subscription = stripe_active_subscriptions(config || {}, customer_id).first
548
- if stripe_subscription
549
- resolved = stripe_resolve_plan_item(config || {}, stripe_subscription)
550
- item = resolved&.fetch(:item, nil)
551
- plan = resolved&.fetch(:plan, nil)
552
- if item && plan
553
- ctx.context.adapter.update(
554
- model: "subscription",
555
- where: [{field: "id", value: subscription.fetch("id")}],
556
- update: stripe_subscription_state(stripe_subscription, include_status: true, compact: false).merge(
557
- plan: plan[:name].to_s.downcase,
558
- seats: stripe_resolve_quantity(stripe_subscription, item, plan),
559
- stripeSubscriptionId: stripe_fetch(stripe_subscription, "id")
560
- )
561
- )
562
- end
563
- end
564
- raise ctx.redirect(stripe_url(ctx, callback))
565
- end
58
+ def stripe_success_endpoint(config)
59
+ BetterAuth::Stripe::Routes::SubscriptionSuccess.endpoint(config)
566
60
  end
567
61
 
568
62
  def stripe_webhook_endpoint(config)
569
- Endpoint.new(path: "/stripe/webhook", method: "POST") do |ctx|
570
- signature = ctx.headers["stripe-signature"]
571
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("STRIPE_SIGNATURE_NOT_FOUND")) if signature.to_s.empty?
572
-
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?
574
-
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
583
- else
584
- ctx.body
585
- end
586
- rescue
587
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("FAILED_TO_CONSTRUCT_STRIPE_EVENT"))
588
- end
589
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("FAILED_TO_CONSTRUCT_STRIPE_EVENT")) unless event
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
595
- ctx.json({success: true})
596
- end
63
+ BetterAuth::Stripe::Routes::StripeWebhook.endpoint(config)
597
64
  end
598
65
 
599
66
  def stripe_handle_event(ctx, event)
600
- event = normalize_hash(event)
601
- type = event[:type].to_s
602
- case type
603
- when "checkout.session.completed"
604
- stripe_on_checkout_completed(ctx, event)
605
- when "customer.subscription.created"
606
- stripe_on_subscription_created(ctx, event)
607
- when "customer.subscription.updated"
608
- stripe_on_subscription_updated(ctx, event)
609
- when "customer.subscription.deleted"
610
- stripe_on_subscription_deleted(ctx, event)
611
- end
612
- config = ctx.context.options.plugins.find { |plugin| plugin.id == "stripe" }&.options || {}
613
- config[:on_event]&.call(event)
67
+ BetterAuth::Stripe::Hooks.handle_event(ctx, event)
614
68
  end
615
69
 
616
70
  def stripe_on_checkout_completed(ctx, event)
617
- config = ctx.context.options.plugins.find { |plugin| plugin.id == "stripe" }&.options || {}
618
- object = normalize_hash(event.dig(:data, :object) || {})
619
- return if object[:mode] == "setup" || !config.dig(:subscription, :enabled)
620
-
621
- stripe_subscription = stripe_client(config).subscriptions.retrieve(object[:subscription])
622
- resolved = stripe_resolve_plan_item(config, stripe_subscription)
623
- return unless resolved
624
-
625
- item = resolved.fetch(:item)
626
- plan = resolved.fetch(:plan)
627
- metadata = normalize_hash(object[:metadata] || {})
628
- reference_id = object[:client_reference_id] || metadata[:reference_id]
629
- subscription_id = metadata[:subscription_id]
630
- return unless plan && reference_id && subscription_id
631
-
632
- update = stripe_subscription_state(stripe_subscription, include_status: true).merge(
633
- plan: plan[:name].to_s.downcase,
634
- stripeSubscriptionId: object[:subscription],
635
- seats: stripe_resolve_quantity(stripe_subscription, item, plan),
636
- trialStart: stripe_time(stripe_fetch(stripe_subscription, "trial_start")),
637
- trialEnd: stripe_time(stripe_fetch(stripe_subscription, "trial_end"))
638
- ).compact
639
- db_subscription = ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription_id}], update: update)
640
- plan.dig(:free_trial, :on_trial_start)&.call(db_subscription) if db_subscription && update[:trialStart]
641
- callback = config.dig(:subscription, :on_subscription_complete)
642
- callback&.call({event: event, subscription: db_subscription, stripeSubscription: stripe_subscription, stripe_subscription: stripe_subscription, plan: plan}, ctx)
71
+ BetterAuth::Stripe::Hooks.on_checkout_completed(ctx, event)
643
72
  end
644
73
 
645
74
  def stripe_on_subscription_created(ctx, event)
646
- config = ctx.context.options.plugins.find { |plugin| plugin.id == "stripe" }&.options || {}
647
- object = normalize_hash(event.dig(:data, :object) || {})
648
- customer_id = object[:customer].to_s
649
- return if customer_id.empty?
650
- metadata = normalize_hash(object[:metadata] || {})
651
- existing = if metadata[:subscription_id]
652
- ctx.context.adapter.find_one(model: "subscription", where: [{field: "id", value: metadata[:subscription_id]}])
653
- else
654
- ctx.context.adapter.find_one(model: "subscription", where: [{field: "stripeSubscriptionId", value: object[:id]}])
655
- end
656
- return if existing
657
-
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)
659
- return unless reference
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]))
664
- return unless plan
665
-
666
- created = ctx.context.adapter.create(
667
- model: "subscription",
668
- data: stripe_subscription_state(object, include_status: true).merge(
669
- referenceId: reference.fetch(:reference_id),
670
- stripeCustomerId: customer_id,
671
- stripeSubscriptionId: object[:id],
672
- plan: plan[:name].to_s.downcase,
673
- seats: stripe_resolve_quantity(object, item, plan),
674
- limits: plan[:limits]
675
- ).compact
676
- )
677
- config.dig(:subscription, :on_subscription_created)&.call({event: event, subscription: created, stripeSubscription: object, stripe_subscription: object, plan: plan})
75
+ BetterAuth::Stripe::Hooks.on_subscription_created(ctx, event)
678
76
  end
679
77
 
680
78
  def stripe_on_subscription_updated(ctx, event)
681
- config = ctx.context.options.plugins.find { |plugin| plugin.id == "stripe" }&.options || {}
682
- object = normalize_hash(event.dig(:data, :object) || {})
683
- resolved = stripe_resolve_plan_item(config, object)
684
- return unless resolved
685
- item = resolved.fetch(:item)
686
-
687
- metadata = normalize_hash(object[:metadata] || {})
688
- subscription = if metadata[:subscription_id]
689
- ctx.context.adapter.find_one(model: "subscription", where: [{field: "id", value: metadata[:subscription_id]}])
690
- else
691
- ctx.context.adapter.find_one(model: "subscription", where: [{field: "stripeSubscriptionId", value: object[:id]}])
692
- end
693
- unless subscription
694
- candidates = ctx.context.adapter.find_many(model: "subscription", where: [{field: "stripeCustomerId", value: object[:customer]}])
695
- subscription = if candidates.length > 1
696
- candidates.find { |entry| stripe_active_or_trialing?(entry) }
697
- else
698
- candidates.first
699
- end
700
- end
701
- return unless subscription
702
-
703
- plan = resolved[:plan]
704
- was_pending = stripe_pending_cancel?(subscription)
705
- update = stripe_subscription_state(object, include_status: true, compact: false).merge(
706
- stripeSubscriptionId: object[:id],
707
- seats: stripe_resolve_quantity(object, item, plan)
708
- )
709
- update[:plan] = plan[:name].to_s.downcase if plan
710
- update[:limits] = plan[:limits] if plan&.key?(:limits)
711
- updated = ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: update)
712
- if object[:status] == "active" && stripe_stripe_pending_cancel?(object) && !was_pending
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]})
714
- end
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
79
+ BetterAuth::Stripe::Hooks.on_subscription_updated(ctx, event)
722
80
  end
723
81
 
724
82
  def stripe_on_subscription_deleted(ctx, event)
725
- config = ctx.context.options.plugins.find { |plugin| plugin.id == "stripe" }&.options || {}
726
- object = normalize_hash(event.dig(:data, :object) || {})
727
- subscription = ctx.context.adapter.find_one(model: "subscription", where: [{field: "stripeSubscriptionId", value: object[:id]}])
728
- return unless subscription
729
-
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))
731
- config.dig(:subscription, :on_subscription_deleted)&.call({event: event, subscription: subscription, stripeSubscription: object, stripe_subscription: object})
83
+ BetterAuth::Stripe::Hooks.on_subscription_deleted(ctx, event)
732
84
  end
733
85
 
734
86
  def stripe_create_customer(config, ctx, user, metadata = nil)
@@ -801,86 +153,51 @@ module BetterAuth
801
153
  end
802
154
 
803
155
  def stripe_id(object)
804
- stripe_fetch(object, "id")
156
+ BetterAuth::Stripe::Utils.id(object)
805
157
  end
806
158
 
807
159
  def stripe_fetch(object, key)
808
- return nil unless object.respond_to?(:[])
809
-
810
- object[key] || object[key.to_sym]
160
+ BetterAuth::Stripe::Utils.fetch(object, key)
811
161
  end
812
162
 
813
163
  def stripe_time(value)
814
- return nil unless value
815
-
816
- Time.at(value.to_i)
164
+ BetterAuth::Stripe::Utils.time(value)
817
165
  end
818
166
 
819
167
  def stripe_subscription_options(config)
820
- normalize_hash(config[:subscription] || {})
168
+ BetterAuth::Stripe::Utils.subscription_options(config)
821
169
  end
822
170
 
823
171
  def stripe_plans(config)
824
- plans = stripe_subscription_options(config)[:plans] || []
825
- plans = plans.call if plans.respond_to?(:call)
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
172
+ BetterAuth::Stripe::Utils.plans(config)
832
173
  end
833
174
 
834
175
  def stripe_plan_by_name(config, name)
835
- stripe_plans(config).find { |plan| plan[:name].to_s.downcase == name.to_s.downcase }
176
+ BetterAuth::Stripe::Utils.plan_by_name(config, name)
836
177
  end
837
178
 
838
179
  def stripe_plan_by_price_info(config, price_id, lookup_key = nil)
839
- stripe_plans(config).find do |plan|
840
- plan[:price_id] == price_id || plan[:annual_discount_price_id] == price_id || (lookup_key && (plan[:lookup_key] == lookup_key || plan[:annual_discount_lookup_key] == lookup_key))
841
- end
180
+ BetterAuth::Stripe::Utils.plan_by_price_info(config, price_id, lookup_key)
842
181
  end
843
182
 
844
183
  def stripe_price_id(config, plan, annual = false)
845
- annual ? (plan[:annual_discount_price_id] || stripe_resolve_lookup(config, plan[:annual_discount_lookup_key])) : (plan[:price_id] || stripe_resolve_lookup(config, plan[:lookup_key]))
184
+ BetterAuth::Stripe::Utils.price_id(config, plan, annual)
846
185
  end
847
186
 
848
187
  def stripe_resolve_lookup(config, lookup_key)
849
- return nil if lookup_key.to_s.empty?
850
- return nil unless stripe_client(config).respond_to?(:prices)
851
-
852
- prices = stripe_client(config).prices.list(lookup_keys: [lookup_key], active: true, limit: 1)
853
- stripe_fetch(Array(stripe_fetch(prices, "data")).first || {}, "id")
188
+ BetterAuth::Stripe::Utils.resolve_lookup(config, lookup_key)
854
189
  end
855
190
 
856
191
  def stripe_reference_id!(ctx, session, customer_type, explicit_reference_id, config)
857
- return explicit_reference_id || session.fetch(:user).fetch("id") unless customer_type == "organization"
858
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("ORGANIZATION_SUBSCRIPTION_NOT_ENABLED")) unless config.dig(:organization, :enabled)
859
-
860
- reference_id = explicit_reference_id || session.fetch(:session)["activeOrganizationId"]
861
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("ORGANIZATION_REFERENCE_ID_REQUIRED")) if reference_id.to_s.empty?
862
- reference_id
192
+ BetterAuth::Stripe::Middleware.reference_id!(ctx, session, customer_type, explicit_reference_id, config)
863
193
  end
864
194
 
865
195
  def stripe_authorize_reference!(ctx, session, reference_id, action, customer_type, subscription_options, explicit: false)
866
- callback = subscription_options[:authorize_reference]
867
- if customer_type == "organization"
868
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("AUTHORIZE_REFERENCE_REQUIRED")) unless callback
869
- elsif !explicit || reference_id == session.fetch(:user).fetch("id")
870
- return
871
- elsif !callback
872
- raise APIError.new("BAD_REQUEST", message: STRIPE_ERROR_CODES.fetch("REFERENCE_ID_NOT_ALLOWED"))
873
- end
874
-
875
- allowed = callback.call({user: session.fetch(:user), session: session.fetch(:session), referenceId: reference_id, reference_id: reference_id, action: action}, ctx)
876
- raise APIError.new("UNAUTHORIZED", message: STRIPE_ERROR_CODES.fetch("UNAUTHORIZED")) unless allowed
196
+ BetterAuth::Stripe::Middleware.authorize_reference!(ctx, session, reference_id, action, customer_type, subscription_options, explicit: explicit)
877
197
  end
878
198
 
879
199
  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
200
+ BetterAuth::Stripe::Middleware.customer_type!(source)
884
201
  end
885
202
 
886
203
  def stripe_find_user_customer(config, email)
@@ -932,59 +249,39 @@ module BetterAuth
932
249
  end
933
250
 
934
251
  def stripe_active_or_trialing?(subscription)
935
- %w[active trialing].include?(stripe_fetch(subscription, "status").to_s)
252
+ BetterAuth::Stripe::Utils.active_or_trialing?(subscription)
936
253
  end
937
254
 
938
255
  def stripe_pending_cancel?(subscription)
939
- !!(stripe_fetch(subscription, "cancelAtPeriodEnd") || stripe_fetch(subscription, "cancelAt"))
256
+ BetterAuth::Stripe::Utils.pending_cancel?(subscription)
940
257
  end
941
258
 
942
259
  def stripe_stripe_pending_cancel?(subscription)
943
- !!(stripe_fetch(subscription, "cancel_at_period_end") || stripe_fetch(subscription, "cancel_at"))
260
+ BetterAuth::Stripe::Utils.stripe_pending_cancel?(subscription)
944
261
  end
945
262
 
946
263
  def stripe_subscription_item(subscription)
947
- Array(stripe_fetch(stripe_fetch(subscription, "items") || {}, "data")).first
264
+ BetterAuth::Stripe::Utils.subscription_item(subscription)
948
265
  end
949
266
 
950
267
  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
268
+ BetterAuth::Stripe::Utils.resolve_plan_item(config, subscription)
961
269
  end
962
270
 
963
271
  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
272
+ BetterAuth::Stripe::Utils.resolve_quantity(subscription, plan_item, plan)
968
273
  end
969
274
 
970
275
  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
276
+ BetterAuth::Stripe::Utils.line_item(config, price_id, quantity)
974
277
  end
975
278
 
976
279
  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
280
+ BetterAuth::Stripe::Utils.checkout_line_items(config, plan, price_id, quantity, auto_managed_seats, seat_only_plan)
982
281
  end
983
282
 
984
283
  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
284
+ BetterAuth::Stripe::Utils.plan_line_items(plan)
988
285
  end
989
286
 
990
287
  def stripe_schedule_plan_change(ctx, config, active_stripe, db_subscription, plan, price_id, quantity, seat_only_plan, body)
@@ -1060,9 +357,7 @@ module BetterAuth
1060
357
  end
1061
358
 
1062
359
  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] }
360
+ BetterAuth::Stripe::Utils.direct_subscription_update?(old_plan, plan, auto_managed_seats)
1066
361
  end
1067
362
 
1068
363
  def stripe_update_active_subscription_items(ctx, config, active_stripe, db_subscription, old_plan, plan, price_id, quantity, seat_only_plan, body)
@@ -1100,131 +395,47 @@ module BetterAuth
1100
395
  end
1101
396
 
1102
397
  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
398
+ BetterAuth::Stripe::OrganizationHooks.sync_seats(config, data, ctx)
1132
399
  end
1133
400
 
1134
401
  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"
402
+ BetterAuth::Stripe::Utils.metered_price?(config, price_id, lookup_key)
1138
403
  end
1139
404
 
1140
405
  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
406
+ BetterAuth::Stripe::Utils.resolve_stripe_price(config, price_id, lookup_key)
1152
407
  end
1153
408
 
1154
409
  def stripe_subscription_state(subscription, include_status: true, compact: true)
1155
- item = stripe_subscription_item(subscription)
1156
- price = stripe_fetch(item || {}, "price") || {}
1157
- recurring = stripe_fetch(price, "recurring") || {}
1158
- state = {
1159
- periodStart: stripe_time(stripe_fetch(item || subscription, "current_period_start")),
1160
- periodEnd: stripe_time(stripe_fetch(item || subscription, "current_period_end")),
1161
- cancelAtPeriodEnd: stripe_fetch(subscription, "cancel_at_period_end"),
1162
- cancelAt: stripe_time(stripe_fetch(subscription, "cancel_at")),
1163
- canceledAt: stripe_time(stripe_fetch(subscription, "canceled_at")),
1164
- endedAt: stripe_time(stripe_fetch(subscription, "ended_at")),
1165
- trialStart: stripe_time(stripe_fetch(subscription, "trial_start")),
1166
- trialEnd: stripe_time(stripe_fetch(subscription, "trial_end")),
1167
- billingInterval: stripe_fetch(recurring, "interval"),
1168
- stripeScheduleId: stripe_schedule_id(subscription)
1169
- }
1170
- state[:status] = stripe_fetch(subscription, "status") if include_status
1171
- compact ? state.compact : state
410
+ BetterAuth::Stripe::Utils.subscription_state(subscription, include_status: include_status, compact: compact)
1172
411
  end
1173
412
 
1174
413
  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
414
+ BetterAuth::Stripe::Utils.schedule_id(subscription)
1180
415
  end
1181
416
 
1182
417
  def stripe_reference_by_customer(ctx, config, customer_id)
1183
- if config.dig(:organization, :enabled)
1184
- org = ctx.context.adapter.find_one(model: "organization", where: [{field: "stripeCustomerId", value: customer_id}])
1185
- return {customer_type: "organization", reference_id: org.fetch("id")} if org
1186
- end
1187
- user = ctx.context.adapter.find_one(model: "user", where: [{field: "stripeCustomerId", value: customer_id}])
1188
- return {customer_type: "user", reference_id: user.fetch("id")} if user
1189
-
1190
- nil
418
+ BetterAuth::Stripe::Middleware.reference_by_customer(ctx, config, customer_id)
1191
419
  end
1192
420
 
1193
421
  def stripe_metadata(internal, *user_metadata)
1194
- user_metadata.compact
1195
- .reduce({}) do |acc, entry|
1196
- next acc unless entry.respond_to?(:each)
1197
-
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)
1202
- end
1203
- .merge(internal.transform_keys { |key| stripe_metadata_key(key) })
422
+ BetterAuth::Stripe::Metadata.merge(internal, *user_metadata)
1204
423
  end
1205
424
 
1206
425
  def stripe_customer_metadata_set(internal_fields, *user_metadata)
1207
- stripe_metadata(internal_fields, *user_metadata)
426
+ BetterAuth::Stripe::Metadata.customer_set(internal_fields, *user_metadata)
1208
427
  end
1209
428
 
1210
429
  def stripe_customer_metadata_get(metadata)
1211
- {
1212
- userId: stripe_metadata_fetch(metadata, "userId"),
1213
- organizationId: stripe_metadata_fetch(metadata, "organizationId"),
1214
- customerType: stripe_metadata_fetch(metadata, "customerType")
1215
- }
430
+ BetterAuth::Stripe::Metadata.customer_get(metadata)
1216
431
  end
1217
432
 
1218
433
  def stripe_subscription_metadata_set(internal_fields, *user_metadata)
1219
- stripe_metadata(internal_fields, *user_metadata)
434
+ BetterAuth::Stripe::Metadata.subscription_set(internal_fields, *user_metadata)
1220
435
  end
1221
436
 
1222
437
  def stripe_subscription_metadata_get(metadata)
1223
- {
1224
- userId: stripe_metadata_fetch(metadata, "userId"),
1225
- subscriptionId: stripe_metadata_fetch(metadata, "subscriptionId"),
1226
- referenceId: stripe_metadata_fetch(metadata, "referenceId")
1227
- }
438
+ BetterAuth::Stripe::Metadata.subscription_get(metadata)
1228
439
  end
1229
440
 
1230
441
  def stripe_notify_customer_created(config, customer, user, ctx)
@@ -1239,54 +450,31 @@ module BetterAuth
1239
450
  end
1240
451
 
1241
452
  def stripe_metadata_key(key)
1242
- case normalize_key(key)
1243
- when :user_id then "userId"
1244
- when :organization_id then "organizationId"
1245
- when :customer_type then "customerType"
1246
- when :subscription_id then "subscriptionId"
1247
- when :reference_id then "referenceId"
1248
- else
1249
- key.to_s
1250
- end
453
+ BetterAuth::Stripe::Metadata.metadata_key(key)
1251
454
  end
1252
455
 
1253
456
  def stripe_metadata_fetch(metadata, key)
1254
- return nil unless metadata.respond_to?(:[])
1255
-
1256
- metadata[key] || metadata[key.to_sym] || metadata[normalize_key(key)] || metadata[normalize_key(key).to_s]
457
+ BetterAuth::Stripe::Metadata.metadata_fetch(metadata, key)
1257
458
  end
1258
459
 
1259
460
  def stripe_deep_merge(base, override)
1260
- normalize_hash(base).merge(normalize_hash(override)) do |_key, old, new|
1261
- if old.is_a?(Hash) && new.is_a?(Hash)
1262
- stripe_deep_merge(old, new)
1263
- else
1264
- new
1265
- end
1266
- end
461
+ BetterAuth::Stripe::Metadata.deep_merge(base, override)
1267
462
  end
1268
463
 
1269
464
  def stripe_redirect?(body)
1270
- body[:disable_redirect] != true
465
+ BetterAuth::Stripe::Utils.redirect?(body)
1271
466
  end
1272
467
 
1273
468
  def stripe_stringify_keys(value)
1274
- return value unless value.is_a?(Hash)
1275
-
1276
- value.each_with_object({}) do |(key, object), result|
1277
- result[key.to_s] = object
1278
- result[key.to_sym] = object
1279
- end
469
+ BetterAuth::Stripe::Metadata.stringify_keys(value)
1280
470
  end
1281
471
 
1282
472
  def stripe_url(ctx, url)
1283
- return url if url.to_s.match?(/\A[a-zA-Z][a-zA-Z0-9+\-.]*:/)
1284
-
1285
- "#{ctx.context.base_url}#{url.to_s.start_with?("/") ? url : "/#{url}"}"
473
+ BetterAuth::Stripe::Utils.url(ctx, url)
1286
474
  end
1287
475
 
1288
476
  def stripe_escape_search(value)
1289
- value.to_s.gsub("\"", "\\\"")
477
+ BetterAuth::Stripe::Utils.escape_search(value)
1290
478
  end
1291
479
  end
1292
480
  end