Module: BetterAuth::Stripe::Routes::UpgradeSubscription

Defined in:
lib/better_auth/stripe/routes/upgrade_subscription.rb

Class Method Summary collapse

Class Method Details

.endpoint(config) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/better_auth/stripe/routes/upgrade_subscription.rb', line 9

def endpoint(config)
  BetterAuth::Endpoint.new(path: "/subscription/upgrade", method: "POST") do |ctx|
    session = BetterAuth::Routes.current_session(ctx)
    body = BetterAuth::Plugins.normalize_hash(ctx.body)
    subscription_options = BetterAuth::Plugins.stripe_subscription_options(config)
    customer_type = BetterAuth::Plugins.stripe_customer_type!(body)
    reference_id = BetterAuth::Plugins.stripe_reference_id!(ctx, session, customer_type, body[:reference_id], config)
    BetterAuth::Plugins.stripe_authorize_reference!(ctx, session, reference_id, "upgrade-subscription", customer_type, subscription_options, explicit: body.key?(:reference_id))

    user = session.fetch(:user)
    if subscription_options[:require_email_verification] && !user["emailVerified"]
      raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Stripe::ERROR_CODES.fetch("EMAIL_VERIFICATION_REQUIRED"))
    end

    plan = BetterAuth::Plugins.stripe_plan_by_name(config, body[:plan])
    raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Stripe::ERROR_CODES.fetch("SUBSCRIPTION_PLAN_NOT_FOUND")) unless plan

    subscription_to_update = nil
    if body[:subscription_id]
      subscription_to_update = ctx.context.adapter.find_one(model: "subscription", where: [{field: "stripeSubscriptionId", value: body[:subscription_id]}])
      raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Stripe::ERROR_CODES.fetch("SUBSCRIPTION_NOT_FOUND")) unless subscription_to_update && subscription_to_update["referenceId"] == reference_id
    end

    subscriptions = subscription_to_update ? [subscription_to_update] : ctx.context.adapter.find_many(model: "subscription", where: [{field: "referenceId", value: reference_id}])
    reference_customer_id = subscriptions.find { |entry| entry["stripeCustomerId"] }&.fetch("stripeCustomerId", nil)
    customer_id = if customer_type == "organization"
      subscription_to_update&.fetch("stripeCustomerId", nil) || reference_customer_id || BetterAuth::Plugins.stripe_organization_customer(config, ctx, reference_id, body[:metadata])
    else
      subscription_to_update&.fetch("stripeCustomerId", nil) || reference_customer_id || user["stripeCustomerId"] || BetterAuth::Plugins.stripe_create_customer(config, ctx, user, body[:metadata])
    end

    active_or_trialing = subscriptions.find { |entry| BetterAuth::Plugins.stripe_active_or_trialing?(entry) }
    active_stripe_subscriptions = BetterAuth::Plugins.stripe_active_subscriptions(config, customer_id)
    active_stripe = active_stripe_subscriptions.find do |entry|
      if subscription_to_update&.fetch("stripeSubscriptionId", nil) || body[:subscription_id]
        BetterAuth::Plugins.stripe_fetch(entry, "id") == subscription_to_update&.fetch("stripeSubscriptionId", nil) || BetterAuth::Plugins.stripe_fetch(entry, "id") == body[:subscription_id]
      elsif active_or_trialing && active_or_trialing["stripeSubscriptionId"]
        BetterAuth::Plugins.stripe_fetch(entry, "id") == active_or_trialing["stripeSubscriptionId"]
      else
        false
      end
    end

    price_id = BetterAuth::Plugins.stripe_price_id(config, plan, body[:annual])
    raise BetterAuth::APIError.new("BAD_REQUEST", message: "Price ID not found for the selected plan") if price_id.to_s.empty?
    auto_managed_seats = !!(plan[:seat_price_id] && customer_type == "organization")
    member_count = auto_managed_seats ? ctx.context.adapter.count(model: "member", where: [{field: "organizationId", value: reference_id}]) : 0
    requested_seats = auto_managed_seats ? member_count : (body[:seats] || 1)
    seat_only_plan = auto_managed_seats && plan[:seat_price_id] == price_id

    active_resolved = active_stripe && BetterAuth::Plugins.stripe_resolve_plan_item(config, active_stripe)
    active_stripe_item = active_resolved&.fetch(:item, nil) || BetterAuth::Plugins.stripe_subscription_item(active_stripe || {})
    stripe_price_id_value = BetterAuth::Plugins.stripe_fetch(BetterAuth::Plugins.stripe_fetch(active_stripe_item || {}, "price") || {}, "id")
    same_plan = active_or_trialing && active_or_trialing["plan"].to_s.downcase == body[:plan].to_s.downcase
    same_seats = auto_managed_seats || (active_or_trialing && active_or_trialing["seats"].to_i == requested_seats.to_i)
    same_price = !active_stripe || stripe_price_id_value == price_id
    valid_period = !active_or_trialing || !active_or_trialing["periodEnd"] || active_or_trialing["periodEnd"] > Time.now
    if active_or_trialing&.fetch("status", nil) == "active" && same_plan && same_seats && same_price && valid_period
      raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Stripe::ERROR_CODES.fetch("ALREADY_SUBSCRIBED_PLAN"))
    end

    if active_stripe
      BetterAuth::Plugins.stripe_release_plugin_schedule(ctx, config, customer_id, active_stripe, active_or_trialing || subscription_to_update)

      if body[:schedule_at_period_end]
        url = BetterAuth::Plugins.stripe_schedule_plan_change(ctx, config, active_stripe, active_or_trialing, plan, price_id, requested_seats, seat_only_plan, body)
        next ctx.json({url: url, redirect: BetterAuth::Plugins.stripe_redirect?(body)})
      end

      old_plan = active_or_trialing && BetterAuth::Plugins.stripe_plan_by_name(config, active_or_trialing["plan"])
      if BetterAuth::Plugins.stripe_direct_subscription_update?(old_plan, plan, auto_managed_seats)
        url = BetterAuth::Plugins.stripe_update_active_subscription_items(ctx, config, active_stripe, active_or_trialing, old_plan, plan, price_id, requested_seats, seat_only_plan, body)
        next ctx.json({url: url, redirect: BetterAuth::Plugins.stripe_redirect?(body)})
      end

      portal = BetterAuth::Plugins.stripe_client(config).billing_portal.sessions.create(
        customer: customer_id,
        return_url: BetterAuth::Plugins.stripe_url(ctx, body[:return_url] || "/"),
        flow_data: {
          type: "subscription_update_confirm",
          after_completion: {type: "redirect", redirect: {return_url: BetterAuth::Plugins.stripe_url(ctx, body[:return_url] || "/")}},
          subscription_update_confirm: {
            subscription: BetterAuth::Plugins.stripe_fetch(active_stripe, "id"),
            items: [BetterAuth::Plugins.stripe_line_item(config, price_id, requested_seats).merge(id: BetterAuth::Plugins.stripe_fetch(active_stripe_item || {}, "id"))]
          }
        }
      )
      next ctx.json(BetterAuth::Plugins.stripe_stringify_keys(portal).merge(redirect: BetterAuth::Plugins.stripe_redirect?(body)))
    end

    incomplete = subscriptions.find { |entry| entry["status"] == "incomplete" }
    subscription = active_or_trialing || incomplete
    if subscription
      update = {plan: plan[:name].to_s.downcase, seats: requested_seats}
      subscription = ctx.context.adapter.update(model: "subscription", where: [{field: "id", value: subscription.fetch("id")}], update: update) || subscription.merge(update.transform_keys { |key| BetterAuth::Schema.storage_key(key) })
    else
      subscription = ctx.context.adapter.create(
        model: "subscription",
        data: {plan: plan[:name].to_s.downcase, referenceId: reference_id, stripeCustomerId: customer_id, status: "incomplete", seats: requested_seats, limits: plan[:limits]}
      )
    end

    has_ever_trialed = ctx.context.adapter.find_many(model: "subscription", where: [{field: "referenceId", value: reference_id}]).any? do |entry|
      entry["trialStart"] || entry["trialEnd"] || entry["status"] == "trialing"
    end
    free_trial = (!has_ever_trialed && plan[:free_trial]) ? {trial_period_days: plan.dig(:free_trial, :days)} : {}
    checkout_customization = subscription_options[:get_checkout_session_params]&.call(
      {user: user, session: session.fetch(:session), plan: plan, subscription: subscription},
      ctx.request,
      ctx
    ) || {}
    custom_params = BetterAuth::Plugins.stripe_fetch(checkout_customization, "params") || {}
    custom_options = BetterAuth::Plugins.normalize_hash(BetterAuth::Plugins.stripe_fetch(checkout_customization, "options") || {})
    custom_subscription_data = BetterAuth::Plugins.stripe_fetch(custom_params, "subscription_data") || BetterAuth::Plugins.stripe_fetch(custom_params, "subscriptionData") || {}
     = {userId: user.fetch("id"), subscriptionId: subscription.fetch("id"), referenceId: reference_id}
     = BetterAuth::Plugins.(, body[:metadata], BetterAuth::Plugins.stripe_fetch(custom_params, "metadata"))
     = BetterAuth::Plugins.(, body[:metadata], BetterAuth::Plugins.stripe_fetch(custom_subscription_data, "metadata"))
    checkout_params = BetterAuth::Plugins.stripe_deep_merge(
      custom_params,
      customer: customer_id,
      customer_update: (customer_type == "user") ? {name: "auto", address: "auto"} : {address: "auto"},
      locale: body[:locale],
      success_url: BetterAuth::Plugins.stripe_url(ctx, "#{ctx.context.base_url}/subscription/success?callbackURL=#{Rack::Utils.escape(body[:success_url] || "/")}&checkoutSessionId={CHECKOUT_SESSION_ID}"),
      cancel_url: BetterAuth::Plugins.stripe_url(ctx, body[:cancel_url] || "/"),
      line_items: BetterAuth::Plugins.stripe_checkout_line_items(config, plan, price_id, requested_seats, auto_managed_seats, seat_only_plan),
      subscription_data: free_trial.merge(metadata: ),
      mode: "subscription",
      client_reference_id: reference_id,
      metadata: 
    )
    checkout_params[:metadata] = 
    checkout_params[:subscription_data] ||= {}
    checkout_params[:subscription_data][:metadata] = 
    checkout = BetterAuth::Plugins.stripe_client(config).checkout.sessions.create(checkout_params, custom_options.empty? ? nil : custom_options)
    ctx.json(BetterAuth::Plugins.stripe_stringify_keys(checkout).merge(redirect: BetterAuth::Plugins.stripe_redirect?(body)))
  end
end