Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
611e1cc
Add additional plan for extra storage
jorgemanrubia Dec 17, 2025
a1a67c6
Extract partials
jorgemanrubia Dec 17, 2025
8205823
Add button to buy extra-storage subscriptions
jorgemanrubia Dec 17, 2025
823cf9f
Add some methods to deal with storage limits
jorgemanrubia Dec 17, 2025
fc2848e
Initial storage notices
jorgemanrubia Dec 17, 2025
61c2277
Place nearing variant above for consistency
jorgemanrubia Dec 17, 2025
9c8c56c
Comped plans use the extra storage variant
jorgemanrubia Dec 17, 2025
c56dbb5
Add upgrade/downgrade options
jorgemanrubia Dec 17, 2025
68cec74
Extract parent class, validate plans can be downgraded/upgraded
jorgemanrubia Dec 17, 2025
4e6274d
Consider storage when preventing backend card creation and publishing…
jorgemanrubia Dec 17, 2025
00cab8a
Add tests for subscription-related messaging
jorgemanrubia Dec 17, 2025
3b99aa5
Remove unused method
jorgemanrubia Dec 17, 2025
b326f78
More idiomatic
jorgemanrubia Dec 17, 2025
5f4330c
Update free plan partial to include storage limits
jzimdars Dec 17, 2025
95bfc40
Tweak plan name
jzimdars Dec 17, 2025
cfd2033
Design paid plan screen
jzimdars Dec 17, 2025
b1a65bd
Consistency
jzimdars Dec 17, 2025
d8b3485
Copy edits for limit notices
jzimdars Dec 17, 2025
8bb59f5
Cherry-pick changes in b87ae1494a8c002a7f7d54d2c2d25f6a2ce2da8d
jzimdars Dec 17, 2025
0f9d1df
Format
jorgemanrubia Dec 18, 2025
7bf7010
Fix tests after the UI/copy changes
jorgemanrubia Dec 18, 2025
d9ce157
Fix upgrade/downgrade buttons
jorgemanrubia Dec 18, 2025
b44b7ba
Add method to locate plans by the stripe price id
jorgemanrubia Dec 18, 2025
dd59771
Use current plan because you might be on the extra storage plan
jzimdars Dec 18, 2025
5ab6b12
Restoring kamal secrets we lost when moving back to Fizzy
jorgemanrubia Dec 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion saas/.kamal/secrets.beta
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Beta/RAILS_MASTER_KEY Beta/MYSQL_ALTER_PASSWORD Beta/MYSQL_ALTER_USER Beta/MYSQL_APP_PASSWORD Beta/MYSQL_APP_USER Beta/MYSQL_READONLY_PASSWORD Beta/MYSQL_READONLY_USER Beta/SECRET_KEY_BASE Beta/VAPID_PUBLIC_KEY Beta/VAPID_PRIVATE_KEY Beta/ACTIVE_STORAGE_ACCESS_KEY_ID Beta/ACTIVE_STORAGE_SECRET_ACCESS_KEY Beta/QUEENBEE_API_TOKEN Beta/SIGNAL_ID_SECRET Beta/SENTRY_DSN Beta/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Beta/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Beta/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Beta/STRIPE_MONTHLY_V1_PRICE_ID Beta/STRIPE_SECRET_KEY Beta/STRIPE_WEBHOOK_SECRET)
SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Beta/RAILS_MASTER_KEY Beta/MYSQL_ALTER_PASSWORD Beta/MYSQL_ALTER_USER Beta/MYSQL_APP_PASSWORD Beta/MYSQL_APP_USER Beta/MYSQL_READONLY_PASSWORD Beta/MYSQL_READONLY_USER Beta/SECRET_KEY_BASE Beta/VAPID_PUBLIC_KEY Beta/VAPID_PRIVATE_KEY Beta/ACTIVE_STORAGE_ACCESS_KEY_ID Beta/ACTIVE_STORAGE_SECRET_ACCESS_KEY Beta/QUEENBEE_API_TOKEN Beta/SIGNAL_ID_SECRET Beta/SENTRY_DSN Beta/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Beta/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Beta/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Beta/STRIPE_MONTHLY_V1_PRICE_ID Beta/STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID Beta/STRIPE_SECRET_KEY Beta/STRIPE_WEBHOOK_SECRET)

GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
Expand All @@ -22,5 +22,6 @@ ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=$(kamal secrets extract ACTIVE_RECORD_ENCRY
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=$(kamal secrets extract ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY $SECRETS)
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=$(kamal secrets extract ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT $SECRETS)
STRIPE_MONTHLY_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_V1_PRICE_ID $SECRETS)
STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID $SECRETS)
STRIPE_SECRET_KEY=$(kamal secrets extract STRIPE_SECRET_KEY $SECRETS)
STRIPE_WEBHOOK_SECRET=$(kamal secrets extract STRIPE_WEBHOOK_SECRET $SECRETS)
3 changes: 2 additions & 1 deletion saas/.kamal/secrets.production
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Production/RAILS_MASTER_KEY Production/MYSQL_ALTER_PASSWORD Production/MYSQL_ALTER_USER Production/MYSQL_APP_PASSWORD Production/MYSQL_APP_USER Production/MYSQL_READONLY_PASSWORD Production/MYSQL_READONLY_USER Production/SECRET_KEY_BASE Production/VAPID_PUBLIC_KEY Production/VAPID_PRIVATE_KEY Production/ACTIVE_STORAGE_ACCESS_KEY_ID Production/ACTIVE_STORAGE_SECRET_ACCESS_KEY Production/QUEENBEE_API_TOKEN Production/SIGNAL_ID_SECRET Production/SENTRY_DSN Production/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Production/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Production/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Production/STRIPE_MONTHLY_V1_PRICE_ID Production/STRIPE_SECRET_KEY Production/STRIPE_WEBHOOK_SECRET)
SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Production/RAILS_MASTER_KEY Production/MYSQL_ALTER_PASSWORD Production/MYSQL_ALTER_USER Production/MYSQL_APP_PASSWORD Production/MYSQL_APP_USER Production/MYSQL_READONLY_PASSWORD Production/MYSQL_READONLY_USER Production/SECRET_KEY_BASE Production/VAPID_PUBLIC_KEY Production/VAPID_PRIVATE_KEY Production/ACTIVE_STORAGE_ACCESS_KEY_ID Production/ACTIVE_STORAGE_SECRET_ACCESS_KEY Production/QUEENBEE_API_TOKEN Production/SIGNAL_ID_SECRET Production/SENTRY_DSN Production/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Production/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Production/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Production/STRIPE_MONTHLY_V1_PRICE_ID Production/STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID Production/STRIPE_SECRET_KEY Production/STRIPE_WEBHOOK_SECRET)

GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
Expand All @@ -22,5 +22,6 @@ ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=$(kamal secrets extract ACTIVE_RECORD_ENCRY
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=$(kamal secrets extract ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY $SECRETS)
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=$(kamal secrets extract ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT $SECRETS)
STRIPE_MONTHLY_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_V1_PRICE_ID $SECRETS)
STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID $SECRETS)
STRIPE_SECRET_KEY=$(kamal secrets extract STRIPE_SECRET_KEY $SECRETS)
STRIPE_WEBHOOK_SECRET=$(kamal secrets extract STRIPE_WEBHOOK_SECRET $SECRETS)
3 changes: 2 additions & 1 deletion saas/.kamal/secrets.staging
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Staging/RAILS_MASTER_KEY Staging/MYSQL_ALTER_PASSWORD Staging/MYSQL_ALTER_USER Staging/MYSQL_APP_PASSWORD Staging/MYSQL_APP_USER Staging/MYSQL_READONLY_PASSWORD Staging/MYSQL_READONLY_USER Staging/SECRET_KEY_BASE Staging/VAPID_PUBLIC_KEY Staging/VAPID_PRIVATE_KEY Staging/ACTIVE_STORAGE_ACCESS_KEY_ID Staging/ACTIVE_STORAGE_SECRET_ACCESS_KEY Staging/QUEENBEE_API_TOKEN Staging/SIGNAL_ID_SECRET Staging/SENTRY_DSN Staging/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Staging/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Staging/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Staging/STRIPE_MONTHLY_V1_PRICE_ID Staging/STRIPE_SECRET_KEY Staging/STRIPE_WEBHOOK_SECRET)
SECRETS=$(kamal secrets fetch --adapter 1password --account basecamp --from Deploy/Fizzy Deployments/BASECAMP_REGISTRY_PASSWORD Deployments/DASH_BASIC_AUTH_SECRET Staging/RAILS_MASTER_KEY Staging/MYSQL_ALTER_PASSWORD Staging/MYSQL_ALTER_USER Staging/MYSQL_APP_PASSWORD Staging/MYSQL_APP_USER Staging/MYSQL_READONLY_PASSWORD Staging/MYSQL_READONLY_USER Staging/SECRET_KEY_BASE Staging/VAPID_PUBLIC_KEY Staging/VAPID_PRIVATE_KEY Staging/ACTIVE_STORAGE_ACCESS_KEY_ID Staging/ACTIVE_STORAGE_SECRET_ACCESS_KEY Staging/QUEENBEE_API_TOKEN Staging/SIGNAL_ID_SECRET Staging/SENTRY_DSN Staging/ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY Staging/ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY Staging/ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT Staging/STRIPE_MONTHLY_V1_PRICE_ID Staging/STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID Staging/STRIPE_SECRET_KEY Staging/STRIPE_WEBHOOK_SECRET)

GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
Expand All @@ -22,5 +22,6 @@ ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=$(kamal secrets extract ACTIVE_RECORD_ENCRY
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=$(kamal secrets extract ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY $SECRETS)
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=$(kamal secrets extract ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT $SECRETS)
STRIPE_MONTHLY_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_V1_PRICE_ID $SECRETS)
STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID $SECRETS)
STRIPE_SECRET_KEY=$(kamal secrets extract STRIPE_SECRET_KEY $SECRETS)
STRIPE_WEBHOOK_SECRET=$(kamal secrets extract STRIPE_WEBHOOK_SECRET $SECRETS)
5 changes: 4 additions & 1 deletion saas/app/assets/stylesheets/fizzy/saas.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
}
}

.settings-subscription__link {
color: var(--settings-subscription-text-color);
}

.settings-subscription__notch {
animation: wiggle 500ms ease;
background: var(--settings-subscription-background);
Expand All @@ -39,7 +43,6 @@
color: var(--settings-subscription-text-color);
margin-inline: auto;
padding: 0 0.3em 0 1.2em;
padding: 0.5ch 0.5ch 0.5ch 2ch;
transform: rotate(-1deg);

@media (max-width: 640px) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Account::Subscriptions::DowngradesController < Account::Subscriptions::UpdatePlanController
before_action :ensure_downgradeable

private
def target_plan
Plan.paid
end

def ensure_downgradeable
head :bad_request unless subscription.plan == Plan.paid_with_extra_storage
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class Account::Subscriptions::UpdatePlanController < 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: target_plan.stripe_price_id } ]
}
}
)

redirect_to portal_session.url, allow_other_host: true
end

private
def target_plan
raise NotImplementedError
end

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
12 changes: 12 additions & 0 deletions saas/app/controllers/account/subscriptions/upgrades_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Account::Subscriptions::UpgradesController < Account::Subscriptions::UpdatePlanController
before_action :ensure_upgradeable

private
def target_plan
Plan.paid_with_extra_storage
end

def ensure_upgradeable
head :bad_request unless subscription.plan == Plan.paid
end
end
10 changes: 7 additions & 3 deletions saas/app/controllers/account/subscriptions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ def create
session = Stripe::Checkout::Session.create \
customer: find_or_create_stripe_customer,
mode: "subscription",
line_items: [ { price: Plan.paid.stripe_price_id, quantity: 1 } ],
line_items: [ { price: plan_param.stripe_price_id, quantity: 1 } ],
success_url: account_subscription_url + "?session_id={CHECKOUT_SESSION_ID}",
cancel_url: account_subscription_url,
metadata: { account_id: Current.account.id, plan_key: Plan.paid.key },
metadata: { account_id: Current.account.id, plan_key: plan_param.key },
automatic_tax: { enabled: true },
tax_id_collection: { enabled: true },
billing_address_collection: "required",
Expand All @@ -22,6 +22,10 @@ def create
end

private
def plan_param
@plan_param ||= Plan[params[:plan_key]] || Plan.paid
end

def set_stripe_session
@stripe_session = Stripe::Checkout::Session.retrieve(params[:session_id]) if params[:session_id]
end
Expand All @@ -36,7 +40,7 @@ def find_stripe_customer

def create_stripe_customer
Stripe::Customer.create(email: Current.user.identity.email_address, name: Current.account.name, metadata: { account_id: Current.account.id }).tap do |customer|
Current.account.create_subscription!(stripe_customer_id: customer.id, plan_key: Plan.paid.key, status: "incomplete")
Current.account.create_subscription!(stripe_customer_id: customer.id, plan_key: plan_param.key, status: "incomplete")
end
end
end
6 changes: 3 additions & 3 deletions saas/app/controllers/admin/accounts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def edit
end

def update
@account.override_limits(card_count: overridden_card_count_param)
@account.override_limits(**overridden_limits_params.to_h.symbolize_keys)
redirect_to saas.edit_admin_account_path(@account.external_account_id), notice: "Account limits updated"
end

Expand All @@ -21,7 +21,7 @@ def set_account
@account = Account.find_by!(external_account_id: params[:id])
end

def overridden_card_count_param
params[:account][:overridden_card_count].to_i
def overridden_limits_params
params.expect(account: [ :card_count, :bytes_used ])
end
end
8 changes: 8 additions & 0 deletions saas/app/controllers/concerns/card/limited.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module Card::Limited
extend ActiveSupport::Concern

private
def ensure_under_limits
head :forbidden if Current.account.exceeding_limits?
end
end
9 changes: 3 additions & 6 deletions saas/app/controllers/concerns/card/limited_creation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ module Card::LimitedCreation
extend ActiveSupport::Concern

included do
include Card::Limited

# Only limit API requests. We let you create drafts in the app to actually show the banner, no matter the card count.
# We limit card publications separately. See +Card::LimitedPublishing+.
before_action :ensure_can_create_cards, only: %i[ create ], if: -> { request.format.json? }
before_action :ensure_under_limits, only: %i[ create ], if: -> { request.format.json? }
end

private
def ensure_can_create_cards
head :forbidden if Current.account.exceeding_card_limit?
end
end
9 changes: 3 additions & 6 deletions saas/app/controllers/concerns/card/limited_publishing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ module Card::LimitedPublishing
extend ActiveSupport::Concern

included do
before_action :ensure_can_publish_cards, only: %i[ create ]
end
include Card::Limited

private
def ensure_can_publish_cards
head :forbidden if Current.account.exceeding_card_limit?
end
before_action :ensure_under_limits, only: %i[ create ]
end
end
8 changes: 7 additions & 1 deletion saas/app/controllers/stripe/webhooks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ def sync_subscription(stripe_subscription_id)
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?
Expand All @@ -76,4 +77,9 @@ def next_amount_due_for(stripe_subscription)
rescue Stripe::InvalidRequestError
nil
end

def plan_key_for(stripe_subscription)
price_id = stripe_subscription.items.data.first&.price&.id
Plan.find_by_price_id(price_id)&.key
end
end
6 changes: 3 additions & 3 deletions saas/app/helpers/subscriptions_helper.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module SubscriptionsHelper
def plan_storage_limit(plan)
number_to_human_size(plan.storage_limit).delete(" ")
def storage_to_human_size(bytes)
number_to_human_size(bytes).delete(" ")
end

def subscription_period_end_action(subscription)
Expand All @@ -9,7 +9,7 @@ def subscription_period_end_action(subscription)
elsif subscription.canceled?
"Your Fizzy subscription ended on"
else
"Your next payment of <b>#{ number_to_currency(subscription.next_amount_due) }</b> will be billed on".html_safe
"Your next payment is <b>#{ number_to_currency(subscription.next_amount_due, strip_insignificant_zeros: true) }</b> on".html_safe
end
end
end
2 changes: 1 addition & 1 deletion saas/app/models/account/billing_waiver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ class Account::BillingWaiver < SaasRecord
belongs_to :account

def subscription
@subscription ||= Account::Subscription.new(plan_key: Plan.paid.key)
@subscription ||= Account::Subscription.new(plan: Plan.paid_with_extra_storage)
end
end
25 changes: 23 additions & 2 deletions saas/app/models/account/limited.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@ module Account::Limited
end

NEAR_CARD_LIMIT_THRESHOLD = 100
NEAR_STORAGE_LIMIT_THRESHOLD = 500.megabytes

def override_limits(card_count:)
(overridden_limits || build_overridden_limits).update!(card_count:)
def override_limits(card_count: nil, bytes_used: nil)
(overridden_limits || build_overridden_limits).update!(card_count:, bytes_used:)
end

def billed_cards_count
overridden_limits&.card_count || cards_count
end

def billed_bytes_used
overridden_limits&.bytes_used || bytes_used
end

def nearing_plan_cards_limit?
plan.limit_cards? && remaining_cards_count < NEAR_CARD_LIMIT_THRESHOLD
end
Expand All @@ -23,6 +28,18 @@ def exceeding_card_limit?
plan.limit_cards? && billed_cards_count > plan.card_limit
end

def nearing_plan_storage_limit?
remaining_storage < NEAR_STORAGE_LIMIT_THRESHOLD
end

def exceeding_storage_limit?
billed_bytes_used > plan.storage_limit
end

def exceeding_limits?
exceeding_card_limit? || exceeding_storage_limit?
end

def reset_overridden_limits
overridden_limits&.destroy
reload_overridden_limits
Expand All @@ -32,4 +49,8 @@ def reset_overridden_limits
def remaining_cards_count
plan.card_limit - billed_cards_count
end

def remaining_storage
plan.storage_limit - billed_bytes_used
end
end
4 changes: 4 additions & 0 deletions saas/app/models/account/subscription.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ def plan
@plan ||= Plan.find(plan_key)
end

def plan=(plan)
self.plan_key = plan.key
end

def to_be_canceled?
active? && cancel_at.present?
end
Expand Down
13 changes: 11 additions & 2 deletions saas/app/models/plan.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
class Plan
PLANS = {
free_v1: { name: "Free", price: 0, card_limit: 1000, storage_limit: 1.gigabytes },
monthly_v1: { name: "Unlimited", price: 20, card_limit: Float::INFINITY, storage_limit: 5.gigabytes, stripe_price_id: ENV["STRIPE_MONTHLY_V1_PRICE_ID"] }
monthly_v1: { name: "Unlimited", price: 20, card_limit: Float::INFINITY, storage_limit: 5.gigabytes, stripe_price_id: ENV["STRIPE_MONTHLY_V1_PRICE_ID"] },
monthly_extra_storage_v1: { name: "Unlimited + Extra Storage", price: 25, card_limit: Float::INFINITY, storage_limit: 500.gigabytes, stripe_price_id: ENV["STRIPE_MONTHLY_EXTRA_STORAGE_V1_PRICE_ID"] }
}

attr_reader :key, :name, :price, :card_limit, :storage_limit, :stripe_price_id
Expand All @@ -19,11 +20,19 @@ def paid
@paid ||= find(:monthly_v1)
end

def paid_with_extra_storage
@paid_with_extra_storage ||= find(:monthly_extra_storage_v1)
end

def find(key)
@all_by_key ||= all.index_by(&:key).with_indifferent_access
@all_by_key[key]
end

def find_by_price_id(price_id)
all.find { |plan| plan.stripe_price_id == price_id }
end

alias [] find
end

Expand All @@ -45,6 +54,6 @@ def paid?
end

def limit_cards?
card_limit != Float::INFINITY
!card_limit.infinite?
end
end
18 changes: 18 additions & 0 deletions saas/app/views/account/settings/_free_plan.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<h3 class="margin-block-start txt-large font-weight-black txt-tight-lines">
<% if Current.account.exceeding_card_limit? && Current.account.exceeding_storage_limit? %>
You’ve used up your <u><%= number_with_delimiter(Plan.free.card_limit) %></u> free cards and all <u><%= storage_to_human_size(Plan.free.storage_limit) %></u> of free storage
<% elsif Current.account.exceeding_card_limit? %>
You’ve used up your <u><%= number_with_delimiter(Plan.free.card_limit) %></u> free cards
<% elsif Current.account.exceeding_storage_limit? %>
You’ve used up all <u><%= storage_to_human_size(Plan.free.storage_limit) %></u> free storage
<% else %>
You’ve used <u><%= Current.account.billed_cards_count %></u> free cards out of <%= number_with_delimiter(Plan.free.card_limit) %>
<% end %>
</h3>

<p class="margin-block-start-half">To keep using Fizzy <%= "past #{ number_with_delimiter(Plan.free.card_limit) } cards," unless Current.account.exceeding_storage_limit? %> it’s only <strong><%= number_to_currency(Plan.paid.price, strip_insignificant_zeros: true) %>/month</strong> for <strong>unlimited cards</strong>, <strong>unlimited users</strong>, and <strong><%= storage_to_human_size(Plan.paid.storage_limit) %></strong> of storage.</p>

<%= button_to "Upgrade to #{Plan.paid.name} for #{ number_to_currency(Plan.paid.price, strip_insignificant_zeros: true) }/month", account_subscription_path, class: "btn settings-subscription__button txt-medium", form: { data: { turbo: false } } %>

<p>Cancel anytime, no contracts, take your data with you whenever.</p>
<p class="settings-subscription__footer txt-small margin-none-block-end">Right now you’re on the <strong><%= Current.account.plan.name %></strong> plan which includes <%= number_with_delimiter(Plan.free.card_limit) %> cards, unlimited users, and <%= storage_to_human_size(Plan.free.storage_limit) %> of storage.</p>
Loading