Add record to track overridden limits

Before, we were relying on just changing the cards_count in account, but this
could create problems where the system to calculate the next card number fails due
to the unique constraint.
This commit is contained in:
Jorge Manrubia
2025-12-15 14:49:47 +01:00
parent 725e850802
commit 3ef5e4eeef
19 changed files with 196 additions and 80 deletions
@@ -1,4 +1,6 @@
class Admin::AccountsController < AdminController
include Admin::AccountScoped
layout "public"
before_action :set_account, only: %i[ edit update ]
@@ -10,8 +12,8 @@ class Admin::AccountsController < AdminController
end
def update
@account.update!(account_params)
redirect_to saas.edit_admin_account_path(@account.external_account_id), notice: "Account updated"
@account.override_limits(card_count: overridden_card_count_param)
redirect_to saas.edit_admin_account_path(@account.external_account_id), notice: "Account limits updated"
end
private
@@ -19,7 +21,7 @@ class Admin::AccountsController < AdminController
@account = Account.find_by!(external_account_id: params[:id])
end
def account_params
params.expect(account: [ :cards_count ])
def overridden_card_count_param
params[:account][:overridden_card_count].to_i
end
end
@@ -0,0 +1,8 @@
class Admin::OverriddenLimitsController < AdminController
include Admin::AccountScoped
def destroy
@account.reset_overridden_limits
redirect_to saas.edit_admin_account_path(@account.external_account_id), notice: "Limits reset"
end
end
@@ -0,0 +1,12 @@
module Admin::AccountScoped
extend ActiveSupport::Concern
included do
before_action :set_account
end
private
def set_account
@account = Account.find_by!(external_account_id: params[:account_id] || params[:id])
end
end
-10
View File
@@ -5,8 +5,6 @@ module Account::Billing
has_one :subscription, class_name: "Account::Subscription", dependent: :destroy
end
NEAR_CARD_LIMIT_THRESHOLD = 100
def plan
active_subscription&.plan || Plan.free
end
@@ -18,12 +16,4 @@ module Account::Billing
def subscribed?
subscription.present?
end
def nearing_plan_cards_limit?
plan.limit_cards? && (plan.card_limit - cards_count) < NEAR_CARD_LIMIT_THRESHOLD
end
def exceeding_card_limit?
cards_count > plan.card_limit
end
end
+34
View File
@@ -0,0 +1,34 @@
module Account::Limited
extend ActiveSupport::Concern
included do
has_one :overridden_limits, class_name: "Account::OverriddenLimits", dependent: :destroy
end
NEAR_CARD_LIMIT_THRESHOLD = 100
def override_limits(card_count:)
if overridden_limits
overridden_limits.update(card_count: card_count)
else
create_overridden_limits(card_count: card_count)
end
end
def billed_cards_count
overridden_limits&.card_count || cards_count
end
def nearing_plan_cards_limit?
plan.limit_cards? && (plan.card_limit - billed_cards_count) < NEAR_CARD_LIMIT_THRESHOLD
end
def exceeding_card_limit?
plan.limit_cards? && billed_cards_count > plan.card_limit
end
def reset_overridden_limits
overridden_limits&.destroy
reload_overridden_limits
end
end
@@ -0,0 +1,4 @@
# To ease testing of limits
class Account::OverriddenLimits < SaasRecord
belongs_to :account
end
@@ -6,7 +6,7 @@
<% if Current.account.exceeding_card_limit? %>
<h3 class="margin-block-start margin-block-end-half txt-large font-weight-black">Youve used up your <u><%= Plan.free.card_limit %></u> free cards</h3>
<% else %>
<h3 class="margin-block-start margin-block-end-half txt-large font-weight-black">Youve used <u><%= Current.account.cards_count %></u> free cards out of <%= Plan.free.card_limit %></h3>
<h3 class="margin-block-start margin-block-end-half txt-large font-weight-black">Youve used <u><%= Current.account.billed_cards_count %></u> free cards out of <%= Plan.free.card_limit %></h3>
<% end %>
<p class="margin-none-block-start">If youd like to keep using Fizzy past <%= Plan.free.card_limit %> cards, its only <strong>$<%= Plan.paid.price %>/month</strong> for <strong>unlimited cards</strong> + <strong>unlimited users</strong>. Youll also get <strong><%= number_to_human_size(Plan.paid.formatted_storage_limit) %></strong> of storage. </p>
+29 -10
View File
@@ -1,14 +1,33 @@
<h1>Edit Account <%= @account.external_account_id %></h1>
<div class="panel shadow center" style="--panel-size: 50ch;">
<div class="flex flex-column gap txt-medium">
<h1 class="txt-x-large font-weight-black margin-block-none">Edit Account <%= @account.external_account_id %></h1>
<p><strong>Name:</strong> <%= @account.name %></p>
<dl class="flex flex-column gap-half margin-block-none">
<div class="flex gap-half">
<dt class="font-weight-bold">Name:</dt>
<dd class="margin-inline-start-none"><%= @account.name %></dd>
</div>
<div class="flex gap-half">
<dt class="font-weight-bold">Actual card count:</dt>
<dd class="margin-inline-start-none"><%= @account.cards_count %></dd>
</div>
</dl>
<%= form_with model: @account, url: saas.admin_account_path(@account.external_account_id) do |form| %>
<p>
<%= form.label :cards_count %>
<%= form.number_field :cards_count %>
</p>
<%= form_with model: @account, url: saas.admin_account_path(@account.external_account_id), class: "flex flex-column gap" do |form| %>
<div class="flex flex-column gap-half">
<%= form.label :overridden_card_count, "Override card count", class: "font-weight-bold" %>
<div class="flex align-center gap input input--actor">
<%= form.number_field :overridden_card_count, value: @account.overridden_limits&.card_count, class: "input full-width" %>
</div>
</div>
<%= form.submit "Update" %>
<% end %>
<button type="submit" class="btn btn--reversed">Save</button>
<% end %>
<p><%= link_to "Back", saas.admin_accounts_path %></p>
<% if @account.overridden_limits %>
<%= button_to "Reset limits", saas.admin_account_overridden_limits_path(@account.external_account_id), method: :delete, class: "btn btn--negative" %>
<% end %>
<%= link_to "Back to accounts", saas.admin_accounts_path, class: "btn btn--plain txt-link" %>
</div>
</div>
@@ -1,9 +1,9 @@
<% if Current.account.nearing_plan_cards_limit? %>
<div class="settings_subscription__warning full-width pad-inline center margin-block-half">
<% if Current.user.admin? %>
Youve used <%= Current.account.cards_count %> out of <%= Plan.free.card_limit %> free cards. <strong><%= link_to "Upgrade to unlimited", account_settings_path(anchor: "subscription") %></strong>.
Youve used <%= Current.account.billed_cards_count %> out of <%= Plan.free.card_limit %> free cards. <strong><%= link_to "Upgrade to unlimited", account_settings_path(anchor: "subscription") %></strong>.
<% else %>
This account has used <%= Current.account.cards_count %> out of <%= Plan.free.card_limit %> free cards. <strong>Upgrade soon</strong>
This account has used <%= Current.account.billed_cards_count %> out of <%= Plan.free.card_limit %> free cards. <strong>Upgrade soon</strong>
<% end %>
</div>
<% end %>
+3 -1
View File
@@ -5,6 +5,8 @@ Fizzy::Saas::Engine.routes.draw do
mount Audits1984::Engine, at: "/console"
get "stats", to: "stats#show"
resource :account_search, only: :create
resources :accounts
resources :accounts do
resource :overridden_limits, only: :destroy
end
end
end
@@ -0,0 +1,10 @@
class CreateAccountOverriddenLimits < ActiveRecord::Migration[8.2]
def change
create_table :account_overridden_limits, id: :uuid do |t|
t.references :account, null: false, type: :uuid, index: { unique: true }
t.integer :card_count
t.timestamps
end
end
end
+9 -1
View File
@@ -10,7 +10,15 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.2].define(version: 2025_12_03_144630) do
ActiveRecord::Schema[8.2].define(version: 2025_12_15_140000) do
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.integer "card_count"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_account_overridden_limits_on_account_id", unique: true
end
create_table "account_subscriptions", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.uuid "account_id", null: false
t.datetime "cancel_at"
+2 -2
View File
@@ -127,8 +127,8 @@ module Fizzy
end
config.to_prepare do
::Account.include(Account::Billing)
::Signup.prepend(Fizzy::Saas::Signup)
::Account.include Account::Billing, Account::Limited
::Signup.prepend Fizzy::Saas::Signup
CardsController.include(Card::LimitedCreation)
Cards::PublishesController.include(Card::LimitedPublishing)
-7
View File
@@ -1,13 +1,6 @@
require "rake/testtask"
namespace :test do
# task :prepare_saas => :environment do
# require "rails/test_help"
#
# $LOAD_PATH.unshift Fizzy::Saas::Engine.root.join("test").to_s
# require Fizzy::Saas::Engine.root.join("test/test_helper")
# end
desc "Run tests for fizzy-saas gem"
Rake::TestTask.new(saas: :environment) do |t|
t.libs << "test"
@@ -1,10 +1,6 @@
require "test_helper"
class Admin::AccountsControllerTest < ActionDispatch::IntegrationTest
def saas
Fizzy::Saas::Engine.routes.url_helpers
end
test "staff can access index" do
sign_in_as :david
@@ -34,15 +30,15 @@ class Admin::AccountsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end
test "staff can update cards_count" do
test "staff can override card count" do
sign_in_as :david
untenanted do
patch saas.admin_account_path(accounts(:"37s").external_account_id), params: { account: { cards_count: 500 } }
patch saas.admin_account_path(accounts(:"37s").external_account_id), params: { account: { overridden_card_count: 500 } }
assert_redirected_to saas.edit_admin_account_path(accounts(:"37s").external_account_id)
end
assert_equal 500, accounts(:"37s").reload.cards_count
assert_equal 500, accounts(:"37s").reload.billed_cards_count
end
test "non-staff cannot access accounts" do
@@ -0,0 +1,22 @@
require "test_helper"
class Admin::OverriddenLimitsControllerTest < ActionDispatch::IntegrationTest
test "staff can reset overridden limits" do
sign_in_as :david
account = accounts(:"37s")
# First set an override
account.override_limits(card_count: 500)
assert_equal 500, account.reload.billed_cards_count
# Then reset it
untenanted do
delete saas.admin_account_overridden_limits_path(account.external_account_id)
assert_redirected_to saas.edit_admin_account_path(account.external_account_id)
end
# Verify override was removed
assert_nil account.reload.overridden_limits
assert_equal account.cards_count, account.billed_cards_count
end
end
@@ -1,10 +1,6 @@
require "test_helper"
class Admin::StatsControllerTest < ActionDispatch::IntegrationTest
def saas
Fizzy::Saas::Engine.routes.url_helpers
end
test "staff can access stats" do
sign_in_as :david
-31
View File
@@ -15,35 +15,4 @@ class Account::BillingTest < ActiveSupport::TestCase
account.subscription.update!(status: "active")
assert_equal account.subscription, account.active_subscription
end
test "detect nearing card limit" do
# Paid plans are never limited
accounts(:"37s").update_column(:cards_count, 1_000_000)
assert_not accounts(:"37s").nearing_plan_cards_limit?
# Free plan not near limit
accounts(:initech).update_column(:cards_count, 899)
assert_not accounts(:initech).nearing_plan_cards_limit?
# Free plan near limit
accounts(:initech).update_column(:cards_count, 900)
assert_not accounts(:initech).nearing_plan_cards_limit?
accounts(:initech).update_column(:cards_count, 901)
assert accounts(:initech).nearing_plan_cards_limit?
end
test "detect exceeding card limit" do
# Paid plans are never limited
accounts(:"37s").update_column(:cards_count, 1_000_000)
assert_not accounts(:"37s").exceeding_card_limit?
# Free plan under limit
accounts(:initech).update_column(:cards_count, 999)
assert_not accounts(:initech).exceeding_card_limit?
# Free plan over limit
accounts(:initech).update_column(:cards_count, 1001)
assert accounts(:initech).exceeding_card_limit?
end
end
+51
View File
@@ -0,0 +1,51 @@
require "test_helper"
class Account::LimitedTest < ActiveSupport::TestCase
test "detect nearing card limit" do
# Paid plans are never limited
accounts(:"37s").update_column(:cards_count, 1_000_000)
assert_not accounts(:"37s").nearing_plan_cards_limit?
# Free plan not near limit
accounts(:initech).update_column(:cards_count, 899)
assert_not accounts(:initech).nearing_plan_cards_limit?
# Free plan near limit
accounts(:initech).update_column(:cards_count, 900)
assert_not accounts(:initech).nearing_plan_cards_limit?
accounts(:initech).update_column(:cards_count, 901)
assert accounts(:initech).nearing_plan_cards_limit?
end
test "detect exceeding card limit" do
# Paid plans are never limited
accounts(:"37s").update_column(:cards_count, 1_000_000)
assert_not accounts(:"37s").exceeding_card_limit?
# Free plan under limit
accounts(:initech).update_column(:cards_count, 999)
assert_not accounts(:initech).exceeding_card_limit?
# Free plan over limit
accounts(:initech).update_column(:cards_count, 1001)
assert accounts(:initech).exceeding_card_limit?
end
test "override limits" do
account = accounts(:initech)
account.update_column(:cards_count, 1001)
assert account.exceeding_card_limit?
assert_equal 1001, account.billed_cards_count
account.override_limits card_count: 500
assert_not account.exceeding_card_limit?
assert_equal 500, account.billed_cards_count
assert_equal 1001, account.cards_count # original unchanged
account.reset_overridden_limits
assert account.exceeding_card_limit?
assert_equal 1001, account.billed_cards_count
end
end