+ <% if Current.account.exceeding_card_limit? && Current.account.exceeding_storage_limit? %>
+ You’ve used up your <%= number_with_delimiter(Plan.free.card_limit) %> free cards and all <%= storage_to_human_size(Plan.free.storage_limit) %> of free storage
+ <% elsif Current.account.exceeding_card_limit? %>
+ You’ve used up your <%= number_with_delimiter(Plan.free.card_limit) %> free cards
+ <% elsif Current.account.exceeding_storage_limit? %>
+ You’ve used up all <%= storage_to_human_size(Plan.free.storage_limit) %> free storage
+ <% else %>
+ You’ve used <%= Current.account.billed_cards_count %> free cards out of <%= number_with_delimiter(Plan.free.card_limit) %>
+ <% end %>
+
+
+
To keep using Fizzy <%= "past #{ number_with_delimiter(Plan.free.card_limit) } cards," unless Current.account.exceeding_storage_limit? %> it’s only <%= number_to_currency(Plan.paid.price, strip_insignificant_zeros: true) %>/month for unlimited cards, unlimited users, and <%= storage_to_human_size(Plan.paid.storage_limit) %> of storage.
Cancel anytime, no contracts, take your data with you whenever.
+
Right now you’re on the <%= Current.account.plan.name %> plan which includes <%= number_with_delimiter(Plan.free.card_limit) %> cards, unlimited users, and <%= storage_to_human_size(Plan.free.storage_limit) %> of storage.
diff --git a/saas/app/views/account/settings/_paid_plan.html.erb b/saas/app/views/account/settings/_paid_plan.html.erb
new file mode 100644
index 0000000000..426568eae4
--- /dev/null
+++ b/saas/app/views/account/settings/_paid_plan.html.erb
@@ -0,0 +1,19 @@
+
+ <% if Current.account.exceeding_storage_limit? %>
+ You’ve run out of storage
+ <% elsif Current.account.nearing_plan_storage_limit? %>
+ You’ve used <%= storage_to_human_size(Current.account.billed_bytes_used) %> of storage
+ <% else %>
+ Thank you for buying Fizzy
+ <% end %>
+
+
+<% if Current.account.nearing_plan_storage_limit? || Current.account.exceeding_storage_limit? %>
+
+ The <%= Current.account.plan.name %> plan includes <%= storage_to_human_size(Plan.paid.storage_limit) %>. Upgrade to get <%= storage_to_human_size(Plan.paid_with_extra_storage.storage_limit) %> extra storage for <%= number_to_currency(Plan.paid_with_extra_storage.price - Plan.paid.price, strip_insignificant_zeros: true) %>/month more.
+
+<% end %>
+
+<% if Current.account.subscription %>
+ <%= render "account/settings/subscription", subscription: Current.account.subscription %>
+<% end %>
diff --git a/saas/app/views/account/settings/_subscription.html.erb b/saas/app/views/account/settings/_subscription.html.erb
index 3aef9b8537..c3e6296072 100644
--- a/saas/app/views/account/settings/_subscription.html.erb
+++ b/saas/app/views/account/settings/_subscription.html.erb
@@ -1,7 +1,20 @@
-
<% if Current.account.plan.free? %>
- <% if Current.account.exceeding_card_limit? %>
-
You’ve used up your <%= Plan.free.card_limit %> free cards
- <% else %>
-
You’ve used <%= Current.account.billed_cards_count %> free cards out of <%= Plan.free.card_limit %>
- <% end %>
-
-
If you’d like to keep using Fizzy past <%= Plan.free.card_limit %> cards, it’s only $<%= Plan.paid.price %>/month for unlimited cards + unlimited users. You'll also get <%= plan_storage_limit(Plan.paid) %> of storage.
Cancel anytime, no contracts, take your data with you whenever.
-
Right now you’re on the <%= Current.account.plan.name %> plan which includes <%= Plan.free.card_limit %> cards, unlimited users, and <%= plan_storage_limit(Plan.free) %> of storage.
Right now you’re on the <%= Current.account.plan.name %> plan which includes unlimited cards, unlimited users, and <%= plan_storage_limit(Plan.paid) %> of storage.
- <% end %>
+ <%= render "account/settings/paid_plan" %>
<% end %>
-
<% end %>
diff --git a/saas/app/views/account/subscriptions/_upgrade.html.erb b/saas/app/views/account/subscriptions/_upgrade.html.erb
index d1c06e7f01..449fecf16d 100644
--- a/saas/app/views/account/subscriptions/_upgrade.html.erb
+++ b/saas/app/views/account/subscriptions/_upgrade.html.erb
@@ -1,12 +1,9 @@
<% if Current.user.admin? %>
- You’ve used your <%= Plan.free.card_limit %> free cards.
- <%= link_to account_settings_path(anchor: "subscription"), class: "btn settings-subscription__button" do %>
- Upgrade to Unlimited
- <% end %>
+ <%= render "account/subscriptions/notices/admin_exceeding_card_limit" if Current.account.exceeding_card_limit? %>
+ <%= render "account/subscriptions/notices/admin_exceeding_storage_limit" if Current.account.exceeding_storage_limit? %>
<% else %>
-
- This account has used <%= Plan.free.card_limit %> free cards. Upgrade to get more.
-
+ <%= render "account/subscriptions/notices/user_exceeding_card_limit" if Current.account.exceeding_card_limit? %>
+ <%= render "account/subscriptions/notices/user_exceeding_storage_limit" if Current.account.exceeding_storage_limit? %>
<% end %>
diff --git a/saas/app/views/account/subscriptions/notices/_admin_exceeding_card_limit.html.erb b/saas/app/views/account/subscriptions/notices/_admin_exceeding_card_limit.html.erb
new file mode 100644
index 0000000000..3dc759d9e1
--- /dev/null
+++ b/saas/app/views/account/subscriptions/notices/_admin_exceeding_card_limit.html.erb
@@ -0,0 +1,4 @@
+You’ve used your <%= Plan.free.card_limit %> free cards.
+<%= link_to account_settings_path(anchor: "subscription"), class: "btn settings-subscription__button" do %>
+ Upgrade to Unlimited
+<% end %>
diff --git a/saas/app/views/account/subscriptions/notices/_admin_exceeding_storage_limit.html.erb b/saas/app/views/account/subscriptions/notices/_admin_exceeding_storage_limit.html.erb
new file mode 100644
index 0000000000..8770c14608
--- /dev/null
+++ b/saas/app/views/account/subscriptions/notices/_admin_exceeding_storage_limit.html.erb
@@ -0,0 +1,4 @@
+You’ve run out of <%= Current.account.plan.free? ? "free storage" : "storage" %>.
+<%= link_to account_settings_path(anchor: "subscription"), class: "btn settings-subscription__button" do %>
+ Upgrade to get more
+<% end %>
diff --git a/saas/app/views/account/subscriptions/notices/_user_exceeding_card_limit.html.erb b/saas/app/views/account/subscriptions/notices/_user_exceeding_card_limit.html.erb
new file mode 100644
index 0000000000..f3f65f3be5
--- /dev/null
+++ b/saas/app/views/account/subscriptions/notices/_user_exceeding_card_limit.html.erb
@@ -0,0 +1,3 @@
+
+ This account has used <%= Plan.free.card_limit %> free cards. Upgrade to get more.
+
diff --git a/saas/app/views/account/subscriptions/notices/_user_exceeding_storage_limit.html.erb b/saas/app/views/account/subscriptions/notices/_user_exceeding_storage_limit.html.erb
new file mode 100644
index 0000000000..f230443a12
--- /dev/null
+++ b/saas/app/views/account/subscriptions/notices/_user_exceeding_storage_limit.html.erb
@@ -0,0 +1,3 @@
+
+ This account has run out of <%= Current.account.plan.free? ? "free storage" : "storage" %>. Upgrade to get more.
+
- <% if Current.user.admin? %>
- You’ve used <%= Current.account.billed_cards_count %> out of <%= Plan.free.card_limit %> free cards. <%= link_to "Upgrade to unlimited", account_settings_path(anchor: "subscription") %>.
- <% else %>
- This account has used <%= Current.account.billed_cards_count %> out of <%= Plan.free.card_limit %> free cards. Upgrade soon
- <% end %>
-
-<% end %>
+
+ <% if Current.user.admin? %>
+ <%= render "cards/container/footer/saas/notices/admin_nearing_card_limit" if Current.account.nearing_plan_cards_limit? %>
+ <%= render "cards/container/footer/saas/notices/admin_nearing_storage_limit" if Current.account.nearing_plan_storage_limit? %>
+ <% else %>
+ <%= render "cards/container/footer/saas/notices/user_nearing_card_limit" if Current.account.nearing_plan_cards_limit? %>
+ <%= render "cards/container/footer/saas/notices/user_nearing_storage_limit" if Current.account.nearing_plan_storage_limit? %>
+ <% end %>
+
diff --git a/saas/app/views/cards/container/footer/saas/notices/_admin_nearing_card_limit.html.erb b/saas/app/views/cards/container/footer/saas/notices/_admin_nearing_card_limit.html.erb
new file mode 100644
index 0000000000..caa7bd9584
--- /dev/null
+++ b/saas/app/views/cards/container/footer/saas/notices/_admin_nearing_card_limit.html.erb
@@ -0,0 +1 @@
+You’ve used <%= Current.account.billed_cards_count %> out of <%= Plan.free.card_limit %> free cards. <%= link_to "Upgrade to unlimited", account_settings_path(anchor: "subscription") %>.
diff --git a/saas/app/views/cards/container/footer/saas/notices/_admin_nearing_storage_limit.html.erb b/saas/app/views/cards/container/footer/saas/notices/_admin_nearing_storage_limit.html.erb
new file mode 100644
index 0000000000..ab1a3ca34c
--- /dev/null
+++ b/saas/app/views/cards/container/footer/saas/notices/_admin_nearing_storage_limit.html.erb
@@ -0,0 +1 @@
+You’ve used <%= storage_to_human_size(Current.account.billed_bytes_used) %> out of <%= storage_to_human_size(Current.account.plan.storage_limit) %> <%= Current.account.plan.free? ? "free storage" : "storage" %>. <%= link_to "Upgrade to get more", account_settings_path(anchor: "subscription") %>.
diff --git a/saas/app/views/cards/container/footer/saas/notices/_user_nearing_card_limit.html.erb b/saas/app/views/cards/container/footer/saas/notices/_user_nearing_card_limit.html.erb
new file mode 100644
index 0000000000..8ab80e1629
--- /dev/null
+++ b/saas/app/views/cards/container/footer/saas/notices/_user_nearing_card_limit.html.erb
@@ -0,0 +1 @@
+This account has used <%= Current.account.billed_cards_count %> out of <%= Plan.free.card_limit %> free cards. Upgrade soon.
diff --git a/saas/app/views/cards/container/footer/saas/notices/_user_nearing_storage_limit.html.erb b/saas/app/views/cards/container/footer/saas/notices/_user_nearing_storage_limit.html.erb
new file mode 100644
index 0000000000..25428980a3
--- /dev/null
+++ b/saas/app/views/cards/container/footer/saas/notices/_user_nearing_storage_limit.html.erb
@@ -0,0 +1 @@
+This account has used <%= storage_to_human_size(Current.account.billed_bytes_used) %> out of <%= storage_to_human_size(Current.account.plan.storage_limit) %> <%= Current.account.plan.free? ? "free storage" : "storage" %>. Upgrade soon.
diff --git a/saas/config/deploy.beta.yml b/saas/config/deploy.beta.yml
index 4aefe56638..35607d58e7 100644
--- a/saas/config/deploy.beta.yml
+++ b/saas/config/deploy.beta.yml
@@ -45,6 +45,7 @@ env:
- ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
- ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
- STRIPE_MONTHLY_V1_PRICE_ID
+ - STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID
- STRIPE_SECRET_KEY
- STRIPE_WEBHOOK_SECRET
tags:
diff --git a/saas/config/deploy.production.yml b/saas/config/deploy.production.yml
index 70556948e9..443bc81e86 100644
--- a/saas/config/deploy.production.yml
+++ b/saas/config/deploy.production.yml
@@ -51,6 +51,7 @@ env:
- ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
- ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
- STRIPE_MONTHLY_V1_PRICE_ID
+ - STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID
- STRIPE_SECRET_KEY
- STRIPE_WEBHOOK_SECRET
tags:
diff --git a/saas/config/deploy.staging.yml b/saas/config/deploy.staging.yml
index 2c0ae9516e..bfcc5f4414 100644
--- a/saas/config/deploy.staging.yml
+++ b/saas/config/deploy.staging.yml
@@ -51,6 +51,7 @@ env:
- ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
- ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
- STRIPE_MONTHLY_V1_PRICE_ID
+ - STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID
- STRIPE_SECRET_KEY
- STRIPE_WEBHOOK_SECRET
tags:
diff --git a/saas/db/migrate/20251216000000_add_bytes_used_to_account_overridden_limits.rb b/saas/db/migrate/20251216000000_add_bytes_used_to_account_overridden_limits.rb
new file mode 100644
index 0000000000..85adad44ac
--- /dev/null
+++ b/saas/db/migrate/20251216000000_add_bytes_used_to_account_overridden_limits.rb
@@ -0,0 +1,5 @@
+class AddBytesUsedToAccountOverriddenLimits < ActiveRecord::Migration[8.2]
+ def change
+ add_column :account_overridden_limits, :bytes_used, :bigint
+ end
+end
diff --git a/saas/db/saas_schema.rb b/saas/db/saas_schema.rb
index 59c78f4cba..7e9b80b92d 100644
--- a/saas/db/saas_schema.rb
+++ b/saas/db/saas_schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.2].define(version: 2025_12_15_170000) do
+ActiveRecord::Schema[8.2].define(version: 2025_12_16_000000) do
create_table "account_billing_waivers", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.uuid "account_id", null: false
t.datetime "created_at", null: false
@@ -20,6 +20,7 @@
create_table "account_overridden_limits", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.uuid "account_id", null: false
+ t.bigint "bytes_used"
t.integer "card_count"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
diff --git a/saas/exe/stripe-dev b/saas/exe/stripe-dev
index 2033385ffb..5f4820cc7e 100755
--- a/saas/exe/stripe-dev
+++ b/saas/exe/stripe-dev
@@ -30,13 +30,15 @@ secrets_escaped = `kamal secrets fetch \
--account basecamp \
--from "Deploy/Fizzy" \
"Development/STRIPE_SECRET_KEY" \
- "Development/STRIPE_MONTHLY_V1_PRICE_ID" 2>/dev/null`
+ "Development/STRIPE_MONTHLY_V1_PRICE_ID" \
+ "Development/STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID" 2>/dev/null`
secrets_json = secrets_escaped.gsub(/\\(.)/, '\1')
secrets = JSON.parse(secrets_json)
stripe_secret_key = secrets["Deploy/Fizzy/Development/STRIPE_SECRET_KEY"]
stripe_price_id = secrets["Deploy/Fizzy/Development/STRIPE_MONTHLY_V1_PRICE_ID"]
+stripe_extra_storage_price_id = secrets["Deploy/Fizzy/Development/STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID"]
# Clear previous log file
File.write(LOG_FILE, "")
@@ -72,6 +74,7 @@ end
# Output export statements
puts %Q(export STRIPE_SECRET_KEY="#{stripe_secret_key}")
puts %Q(export STRIPE_MONTHLY_V1_PRICE_ID="#{stripe_price_id}")
+puts %Q(export STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID="#{stripe_extra_storage_price_id}")
puts %Q(export STRIPE_WEBHOOK_SECRET="#{webhook_secret}") if webhook_secret
# Informational message to stderr (won't be eval'd)
diff --git a/saas/lib/fizzy/saas/engine.rb b/saas/lib/fizzy/saas/engine.rb
index 532f0a3845..757de6bebc 100644
--- a/saas/lib/fizzy/saas/engine.rb
+++ b/saas/lib/fizzy/saas/engine.rb
@@ -10,7 +10,7 @@ class Engine < ::Rails::Engine
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 @@ class Engine < ::Rails::Engine
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
diff --git a/saas/test/controllers/accounts/subscriptions/card_creation_test.rb b/saas/test/controllers/accounts/subscriptions/card_creation_test.rb
new file mode 100644
index 0000000000..3f24c82c53
--- /dev/null
+++ b/saas/test/controllers/accounts/subscriptions/card_creation_test.rb
@@ -0,0 +1,79 @@
+require "test_helper"
+
+class Account::Subscriptions::CardCreationTest < ActionDispatch::IntegrationTest
+ # Nearing limits - shown in card creation footer
+
+ test "admin sees nearing card limit notice" do
+ sign_in_as :mike
+
+ accounts(:initech).update_column(:cards_count, 950)
+
+ get card_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
+
+ assert_response :success
+ assert_match /upgrade to unlimited/i, response.body
+ end
+
+ test "admin sees nearing storage limit notice" do
+ sign_in_as :mike
+
+ Account.any_instance.stubs(:bytes_used).returns(600.megabytes)
+
+ get card_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
+
+ assert_response :success
+ assert_match /upgrade to get more/i, response.body
+ end
+
+ # Exceeding limits - shown instead of create buttons
+
+ test "admin sees exceeding card limit notice" do
+ sign_in_as :mike
+
+ accounts(:initech).update_column(:cards_count, 1001)
+
+ get card_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
+
+ assert_response :success
+ assert_match /you’ve used your.*free cards/i, response.body
+ end
+
+ test "admin sees exceeding storage limit notice" do
+ sign_in_as :mike
+
+ Account.any_instance.stubs(:bytes_used).returns(1.1.gigabytes)
+
+ get card_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
+
+ assert_response :success
+ assert_match /you’ve run out of.*free storage/i, response.body
+ end
+
+ # Paid accounts under limits - no notices
+
+ test "paid account under limits sees no notices" do
+ sign_in_as :kevin
+
+ accounts(:"37s").subscription.update!(plan: Plan.paid, status: :active)
+
+ get card_path(cards(:layout), script_name: accounts(:"37s").slug)
+
+ assert_response :success
+ assert_no_match /upgrade/i, response.body
+ assert_no_match /you’ve used your/i, response.body
+ end
+
+ # Comped accounts under limits - no notices
+
+ test "comped account under limits sees no notices" do
+ sign_in_as :mike
+
+ accounts(:initech).comp
+
+ get card_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
+
+ assert_response :success
+ assert_no_match /upgrade/i, response.body
+ assert_no_match /you’ve used your/i, response.body
+ end
+end
diff --git a/saas/test/controllers/accounts/subscriptions/downgrades_controller_test.rb b/saas/test/controllers/accounts/subscriptions/downgrades_controller_test.rb
new file mode 100644
index 0000000000..e4680a1f83
--- /dev/null
+++ b/saas/test/controllers/accounts/subscriptions/downgrades_controller_test.rb
@@ -0,0 +1,35 @@
+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: Plan.paid_with_extra_storage)
+ 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
+
+ test "downgrade requires downgradeable plan" do
+ accounts(:"37s").subscription.update!(plan: Plan.paid)
+
+ post account_subscription_downgrade_path
+ assert_response :bad_request
+ end
+end
diff --git a/saas/test/controllers/accounts/subscriptions/settings_test.rb b/saas/test/controllers/accounts/subscriptions/settings_test.rb
new file mode 100644
index 0000000000..63feac2c5d
--- /dev/null
+++ b/saas/test/controllers/accounts/subscriptions/settings_test.rb
@@ -0,0 +1,62 @@
+require "test_helper"
+
+class Account::Subscriptions::SettingsTest < ActionDispatch::IntegrationTest
+ test "free users see current usage" do
+ sign_in_as :mike
+
+ accounts(:initech).update_column(:cards_count, 3)
+
+ get account_settings_path(script_name: accounts(:initech).slug)
+
+ assert_response :success
+ assert_match /You’ve used.*3.*free cards out of 1,000/i, response.body
+ end
+
+ test "paid users see thank you message" do
+ sign_in_as :kevin
+
+ accounts(:"37s").subscription.update!(plan: Plan.paid, status: :active)
+
+ get account_settings_path(script_name: accounts(:"37s").slug)
+
+ assert_response :success
+ assert_select "h3", text: "Thank you for buying Fizzy"
+ end
+
+ test "regular plan users see upgrade option" do
+ sign_in_as :kevin
+
+ accounts(:"37s").subscription.update!(plan: Plan.paid, status: :active)
+
+ get account_settings_path(script_name: accounts(:"37s").slug)
+
+ assert_response :success
+ assert_select "button", text: /upgrade/i
+ assert_select "button", text: /downgrade/i, count: 0
+ end
+
+ test "extra storage plan users see downgrade option" do
+ sign_in_as :kevin
+
+ accounts(:"37s").subscription.update!(plan: Plan.paid_with_extra_storage, status: :active)
+
+ get account_settings_path(script_name: accounts(:"37s").slug)
+
+ assert_response :success
+ assert_select "button", text: /downgrade/i
+ end
+
+ test "comped accounts see no subscription panel" do
+ sign_in_as :mike
+
+ accounts(:initech).comp
+
+ get account_settings_path(script_name: accounts(:initech).slug)
+
+ assert_response :success
+ assert_no_match /thank you for buying/i, response.body
+ assert_no_match /free cards out of/i, response.body
+ assert_no_match /upgrade/i, response.body
+ assert_no_match /downgrade/i, response.body
+ end
+end
diff --git a/saas/test/controllers/accounts/subscriptions/upgrades_controller_test.rb b/saas/test/controllers/accounts/subscriptions/upgrades_controller_test.rb
new file mode 100644
index 0000000000..65e268af15
--- /dev/null
+++ b/saas/test/controllers/accounts/subscriptions/upgrades_controller_test.rb
@@ -0,0 +1,35 @@
+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: Plan.paid)
+ 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
+
+ test "upgrade requires upgradeable plan" do
+ accounts(:"37s").subscription.update!(plan: Plan.paid_with_extra_storage)
+
+ post account_subscription_upgrade_path
+ assert_response :bad_request
+ end
+end
diff --git a/saas/test/controllers/accounts/subscriptions_controller_test.rb b/saas/test/controllers/accounts/subscriptions_controller_test.rb
index d98ac1d912..9e086df39d 100644
--- a/saas/test/controllers/accounts/subscriptions_controller_test.rb
+++ b/saas/test/controllers/accounts/subscriptions_controller_test.rb
@@ -43,4 +43,18 @@ class Account::SubscriptionsControllerTest < ActionDispatch::IntegrationTest
post account_subscription_path
assert_response :forbidden
end
+
+ test "create with custom plan_key redirects to stripe checkout" do
+ customer = OpenStruct.new(id: "cus_test_37signals")
+ session = OpenStruct.new(url: "https://checkout.stripe.com/session123")
+
+ Stripe::Customer.stubs(:retrieve).returns(customer)
+ Stripe::Checkout::Session.stubs(:create).with do |params|
+ params[:metadata][:plan_key] == :monthly_extra_storage_v1
+ end.returns(session)
+
+ post account_subscription_path(plan_key: :monthly_extra_storage_v1)
+
+ assert_redirected_to "https://checkout.stripe.com/session123"
+ end
end
diff --git a/saas/test/controllers/admin/accounts_controller_test.rb b/saas/test/controllers/admin/accounts_controller_test.rb
index 0b830919af..3849c07a56 100644
--- a/saas/test/controllers/admin/accounts_controller_test.rb
+++ b/saas/test/controllers/admin/accounts_controller_test.rb
@@ -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
diff --git a/saas/test/controllers/card/limited_creation_test.rb b/saas/test/controllers/card/limited_creation_test.rb
index b3477735c9..4105091bea 100644
--- a/saas/test/controllers/card/limited_creation_test.rb
+++ b/saas/test/controllers/card/limited_creation_test.rb
@@ -40,4 +40,16 @@ class Card::LimitedCreationTest < ActionDispatch::IntegrationTest
assert_response :redirect
assert Card.last.drafted?
end
+
+ test "cannot create cards via JSON when storage limit exceeded" do
+ sign_in_as :mike
+
+ Account.any_instance.stubs(:bytes_used).returns(1.1.gigabytes)
+
+ assert_no_difference -> { Card.count } do
+ post board_cards_path(boards(:miltons_wish_list), script_name: accounts(:initech).slug, format: :json)
+ end
+
+ assert_response :forbidden
+ end
end
diff --git a/saas/test/controllers/card/limited_publishing_test.rb b/saas/test/controllers/card/limited_publishing_test.rb
index 2eb57471ea..90e4e56963 100644
--- a/saas/test/controllers/card/limited_publishing_test.rb
+++ b/saas/test/controllers/card/limited_publishing_test.rb
@@ -11,4 +11,15 @@ class Card::LimitedPublishingTest < ActionDispatch::IntegrationTest
assert_response :forbidden
assert cards(:unfinished_thoughts).reload.drafted?
end
+
+ test "cannot publish cards when storage limit exceeded" do
+ sign_in_as :mike
+
+ Account.any_instance.stubs(:bytes_used).returns(1.1.gigabytes)
+
+ post card_publish_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
+
+ assert_response :forbidden
+ assert cards(:unfinished_thoughts).reload.drafted?
+ end
end
diff --git a/saas/test/models/account/limited_test.rb b/saas/test/models/account/limited_test.rb
index 335d6a37c7..828386023e 100644
--- a/saas/test/models/account/limited_test.rb
+++ b/saas/test/models/account/limited_test.rb
@@ -73,4 +73,45 @@ class Account::LimitedTest < ActiveSupport::TestCase
assert account.exceeding_card_limit?
end
+
+ test "detect nearing storage limit" do
+ # Paid plans have large storage limits
+ accounts(:"37s").stubs(:bytes_used).returns(4.gigabytes)
+ assert_not accounts(:"37s").nearing_plan_storage_limit?
+
+ # Free plan not near limit
+ accounts(:initech).stubs(:bytes_used).returns(400.megabytes)
+ assert_not accounts(:initech).nearing_plan_storage_limit?
+
+ # Free plan near limit
+ 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).stubs(:bytes_used).returns(900.megabytes)
+ assert_not accounts(:initech).exceeding_storage_limit?
+
+ # Free plan over limit
+ 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.stubs(:bytes_used).returns(1.1.gigabytes)
+
+ assert account.exceeding_storage_limit?
+ assert_equal 1.1.gigabytes, account.billed_bytes_used
+
+ account.override_limits bytes_used: 500.megabytes
+ assert_not account.exceeding_storage_limit?
+ assert_equal 500.megabytes, account.billed_bytes_used
+ assert_equal 1.1.gigabytes, account.bytes_used # original unchanged
+
+ account.reset_overridden_limits
+ assert account.exceeding_storage_limit?
+ assert_equal 1.1.gigabytes, account.billed_bytes_used
+ end
end
diff --git a/saas/test/models/plan_test.rb b/saas/test/models/plan_test.rb
index 6e0d70b5cc..54ed279d64 100644
--- a/saas/test/models/plan_test.rb
+++ b/saas/test/models/plan_test.rb
@@ -8,4 +8,11 @@ class PlanTest < ActiveSupport::TestCase
test "monthly plan is not free" do
assert_not Plan[:monthly_v1].free?
end
+
+ test "find plan by its price id" do
+ Plan.paid.stubs(:stripe_price_id).returns("price_monthly_v1")
+
+ assert_equal Plan.paid, Plan.find_by_price_id("price_monthly_v1")
+ assert_nil Plan.find_by_price_id("unknown_price_id")
+ end
end