Add upgrade/downgrade options

This commit is contained in:
Jorge Manrubia
2025-12-17 10:02:23 +01:00
parent 9c8c56cdb8
commit c56dbb5f1e
10 changed files with 153 additions and 12 deletions
@@ -0,0 +1,28 @@
class Account::Subscriptions::DowngradesController < ApplicationController
before_action :ensure_admin
def create
portal_session = Stripe::BillingPortal::Session.create(
customer: subscription.stripe_customer_id,
return_url: account_settings_url(anchor: "subscription"),
flow_data: {
type: "subscription_update_confirm",
subscription_update_confirm: {
subscription: subscription.stripe_subscription_id,
items: [ { id: stripe_subscription_item_id, price: Plan.paid.stripe_price_id } ]
}
}
)
redirect_to portal_session.url, allow_other_host: true
end
private
def subscription
@subscription ||= Current.account.subscription
end
def stripe_subscription_item_id
Stripe::Subscription.retrieve(subscription.stripe_subscription_id).items.data.first.id
end
end
@@ -0,0 +1,28 @@
class Account::Subscriptions::UpgradesController < ApplicationController
before_action :ensure_admin
def create
portal_session = Stripe::BillingPortal::Session.create(
customer: subscription.stripe_customer_id,
return_url: account_settings_url(anchor: "subscription"),
flow_data: {
type: "subscription_update_confirm",
subscription_update_confirm: {
subscription: subscription.stripe_subscription_id,
items: [ { id: stripe_subscription_item_id, price: Plan.paid_with_extra_storage.stripe_price_id } ]
}
}
)
redirect_to portal_session.url, allow_other_host: true
end
private
def subscription
@subscription ||= Current.account.subscription
end
def stripe_subscription_item_id
Stripe::Subscription.retrieve(subscription.stripe_subscription_id).items.data.first.id
end
end
@@ -49,7 +49,8 @@ class Stripe::WebhooksController < ApplicationController
status: stripe_subscription.status,
current_period_end: current_period_end_for(stripe_subscription),
cancel_at: stripe_subscription.cancel_at ? Time.at(stripe_subscription.cancel_at) : nil,
next_amount_due_in_cents: next_amount_due_for(stripe_subscription)
next_amount_due_in_cents: next_amount_due_for(stripe_subscription),
plan_key: plan_key_for(stripe_subscription)
}
yield subscription_properties if block_given?
@@ -76,4 +77,9 @@ class Stripe::WebhooksController < ApplicationController
rescue Stripe::InvalidRequestError
nil
end
def plan_key_for(stripe_subscription)
price_id = stripe_subscription.items.data.first&.price&.id
Plan.all.find { |plan| plan.stripe_price_id == price_id }&.key
end
end
+1 -1
View File
@@ -2,6 +2,6 @@ class Account::BillingWaiver < SaasRecord
belongs_to :account
def subscription
@subscription ||= Account::Subscription.new(plan_key: Plan.paid_with_extra_storage)
@subscription ||= Account::Subscription.new(plan: Plan.paid_with_extra_storage)
end
end
@@ -4,4 +4,12 @@
<p class="txt-medium margin-none-block-start"><%= subscription_period_end_action(subscription) %> <strong><%= subscription.current_period_end.to_date.to_fs(:long) %></strong></p>
<% end %>
<%= link_to "Manage Billing", account_billing_portal_path, class: "btn", data: { turbo_prefetch: false } %>
<div class="flex gap-half justify-center">
<%= link_to "Manage Billing", account_billing_portal_path, class: "btn", data: { turbo_prefetch: false } %>
<% if subscription.plan == Plan.paid %>
<%= button_to "Upgrade to Extra Storage", account_subscription_upgrade_path, class: "btn", form: { data: { turbo: false } } %>
<% elsif subscription.plan == Plan.paid_with_extra_storage %>
<%= button_to "Downgrade to Regular", account_subscription_downgrade_path, class: "btn", form: { data: { turbo: false } } %>
<% end %>
</div>
+7 -2
View File
@@ -10,7 +10,7 @@ module Fizzy
Queenbee.host_app = Fizzy
initializer "fizzy_saas.content_security_policy", before: :load_config_initializers do |app|
app.config.x.content_security_policy.form_action = "https://checkout.stripe.com"
app.config.x.content_security_policy.form_action = "https://checkout.stripe.com https://billing.stripe.com"
end
initializer "fizzy_saas.assets" do |app|
@@ -22,7 +22,12 @@ module Fizzy
app.routes.prepend do
namespace :account do
resource :billing_portal, only: :show
resource :subscription
resource :subscription do
scope module: :subscriptions do
resource :upgrade, only: :create
resource :downgrade, only: :create
end
end
end
namespace :stripe do
@@ -0,0 +1,33 @@
require "test_helper"
require "ostruct"
class Account::Subscriptions::DowngradesControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in_as :kevin
accounts(:"37s").subscription.update!(
stripe_subscription_id: "sub_123",
plan_key: Plan.paid_with_extra_storage.key
)
end
test "downgrade redirects to stripe billing portal" do
stripe_subscription = OpenStruct.new(
items: OpenStruct.new(data: [ OpenStruct.new(id: "si_123") ])
)
portal_session = OpenStruct.new(url: "https://billing.stripe.com/session/abc123")
Stripe::Subscription.stubs(:retrieve).with("sub_123").returns(stripe_subscription)
Stripe::BillingPortal::Session.stubs(:create).returns(portal_session)
post account_subscription_downgrade_path
assert_redirected_to "https://billing.stripe.com/session/abc123"
end
test "downgrade requires admin" do
logout_and_sign_in_as :david
post account_subscription_downgrade_path
assert_response :forbidden
end
end
@@ -0,0 +1,33 @@
require "test_helper"
require "ostruct"
class Account::Subscriptions::UpgradesControllerTest < ActionDispatch::IntegrationTest
setup do
sign_in_as :kevin
accounts(:"37s").subscription.update!(
stripe_subscription_id: "sub_123",
plan_key: Plan.paid.key
)
end
test "upgrade redirects to stripe billing portal" do
stripe_subscription = OpenStruct.new(
items: OpenStruct.new(data: [ OpenStruct.new(id: "si_123") ])
)
portal_session = OpenStruct.new(url: "https://billing.stripe.com/session/abc123")
Stripe::Subscription.stubs(:retrieve).with("sub_123").returns(stripe_subscription)
Stripe::BillingPortal::Session.stubs(:create).returns(portal_session)
post account_subscription_upgrade_path
assert_redirected_to "https://billing.stripe.com/session/abc123"
end
test "upgrade requires admin" do
logout_and_sign_in_as :david
post account_subscription_upgrade_path
assert_response :forbidden
end
end
@@ -34,7 +34,7 @@ class Admin::AccountsControllerTest < ActionDispatch::IntegrationTest
sign_in_as :david
untenanted do
patch saas.admin_account_path(accounts(:"37s").external_account_id), params: { account: { overridden_card_count: 500 } }
patch saas.admin_account_path(accounts(:"37s").external_account_id), params: { account: { card_count: 500 } }
assert_redirected_to saas.edit_admin_account_path(accounts(:"37s").external_account_id)
end
+6 -6
View File
@@ -76,31 +76,31 @@ class Account::LimitedTest < ActiveSupport::TestCase
test "detect nearing storage limit" do
# Paid plans have large storage limits
accounts(:"37s").update_column(:bytes_used, 4.gigabytes)
accounts(:"37s").stubs(:bytes_used).returns(4.gigabytes)
assert_not accounts(:"37s").nearing_plan_storage_limit?
# Free plan not near limit
accounts(:initech).update_column(:bytes_used, 400.megabytes)
accounts(:initech).stubs(:bytes_used).returns(400.megabytes)
assert_not accounts(:initech).nearing_plan_storage_limit?
# Free plan near limit
accounts(:initech).update_column(:bytes_used, 600.megabytes)
accounts(:initech).stubs(:bytes_used).returns(600.megabytes)
assert accounts(:initech).nearing_plan_storage_limit?
end
test "detect exceeding storage limit" do
# Free plan under limit
accounts(:initech).update_column(:bytes_used, 900.megabytes)
accounts(:initech).stubs(:bytes_used).returns(900.megabytes)
assert_not accounts(:initech).exceeding_storage_limit?
# Free plan over limit
accounts(:initech).update_column(:bytes_used, 1.1.gigabytes)
accounts(:initech).stubs(:bytes_used).returns(1.1.gigabytes)
assert accounts(:initech).exceeding_storage_limit?
end
test "override bytes_used limits" do
account = accounts(:initech)
account.update_column(:bytes_used, 1.1.gigabytes)
account.stubs(:bytes_used).returns(1.1.gigabytes)
assert account.exceeding_storage_limit?
assert_equal 1.1.gigabytes, account.billed_bytes_used