diff --git a/Gemfile.saas b/Gemfile.saas
index e57fcf206..384f5b4b7 100644
--- a/Gemfile.saas
+++ b/Gemfile.saas
@@ -5,12 +5,10 @@ git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
gem "activeresource", require: "active_resource"
gem "actionpack-xml_parser" # needed by queenbee for XML request body parsing
-gem "stripe", "~> 18.0"
gem "queenbee", bc: "queenbee-plugin"
gem "fizzy-saas", path: "saas"
gem "console1984", bc: "console1984"
gem "audits1984", bc: "audits1984", branch: "flavorjones/coworker-api"
-gem "csv"
# Native push notifications (iOS/Android)
gem "action_push_native"
diff --git a/Gemfile.saas.lock b/Gemfile.saas.lock
index e4ed7406e..6f9b0d088 100644
--- a/Gemfile.saas.lock
+++ b/Gemfile.saas.lock
@@ -281,7 +281,6 @@ GEM
bigdecimal
rexml
crass (1.0.6)
- csv (3.3.5)
date (3.5.1)
debug (1.11.1)
irb (~> 1.10)
@@ -632,7 +631,6 @@ GEM
stimulus-rails (1.3.4)
railties (>= 6.0.0)
stringio (3.2.0)
- stripe (18.0.1)
thor (1.5.0)
thruster (0.1.18)
thruster (0.1.18-aarch64-linux)
@@ -721,7 +719,6 @@ DEPENDENCIES
bundler-audit
capybara
console1984!
- csv
debug
faker
fizzy-saas!
@@ -757,7 +754,6 @@ DEPENDENCIES
sqlite3 (>= 2.0)
stackprof
stimulus-rails
- stripe (~> 18.0)
thruster
trilogy (~> 2.10)
turbo-rails!
diff --git a/app/views/account/settings/show.html.erb b/app/views/account/settings/show.html.erb
index 91bb85aff..fdb118e57 100644
--- a/app/views/account/settings/show.html.erb
+++ b/app/views/account/settings/show.html.erb
@@ -21,5 +21,3 @@
<%= render "account/settings/cancellation" %>
-
-<%= render "account/settings/subscription_panel" if Fizzy.saas? %>
diff --git a/app/views/cards/container/footer/_create.html.erb b/app/views/cards/container/footer/_create.html.erb
index b55cae344..648beffd1 100644
--- a/app/views/cards/container/footer/_create.html.erb
+++ b/app/views/cards/container/footer/_create.html.erb
@@ -13,7 +13,5 @@
Create and add another
<% end %>
-
- <%= render "cards/container/footer/saas/near_notice" if Fizzy.saas? %>
diff --git a/app/views/cards/drafts/_container.html.erb b/app/views/cards/drafts/_container.html.erb
index fa7134b55..9ffc04711 100644
--- a/app/views/cards/drafts/_container.html.erb
+++ b/app/views/cards/drafts/_container.html.erb
@@ -26,9 +26,5 @@
<% end %>
- <% if Fizzy.saas? %>
- <%= render "cards/container/footer/saas/create", card: card %>
- <% else %>
- <%= render "cards/container/footer/create", card: card %>
- <% end %>
+ <%= render "cards/container/footer/create", card: card %>
diff --git a/app/views/layouts/shared/_head.html.erb b/app/views/layouts/shared/_head.html.erb
index 4be536454..9ef153df2 100644
--- a/app/views/layouts/shared/_head.html.erb
+++ b/app/views/layouts/shared/_head.html.erb
@@ -17,7 +17,6 @@
<%= render "layouts/theme_preference" %>
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
- <%= stylesheet_link_tag "fizzy/saas", "data-turbo-track": "reload" if Fizzy.saas? %>
<%= javascript_importmap_tags %>
<%= tenanted_action_cable_meta_tag %>
diff --git a/saas/app/assets/stylesheets/fizzy/saas.css b/saas/app/assets/stylesheets/fizzy/saas.css
deleted file mode 100644
index a3fedc53d..000000000
--- a/saas/app/assets/stylesheets/fizzy/saas.css
+++ /dev/null
@@ -1,70 +0,0 @@
-/* Subscriptions
-/* ------------------------------------------------------------------------ */
-:root {
- --settings-subscription-background: linear-gradient(to bottom, var(--color-canvas), oklch(var(--lch-violet-lighter)));
- --settings-subscription-color: oklch(var(--lch-violet-medium));
- --settings-subscription-text-color: oklch(var(--lch-violet-dark));
-
- .settings-subscription__button {
- --btn-background: var(--settings-subscription-color);
- --btn-border-color: var(--color-canvas);
- --btn-color: var(--color-canvas);
- --focus-ring-color: var(--color-ink);
- }
-
- .settings-subscription__divider {
- --divider-color: currentColor;
-
- color: var(--settings-subscription-color);
- margin-block-start: calc(var(--block-space-half) * -1);
- }
-
- .settings-subscription__footer {
- color: var(--settings-subscription-text-color);
-
- &::before {
- border-block-start: 1px solid var(--settings-subscription-text-color);
- content: "";
- display: block;
- inline-size: 8ch;
- margin: 0 auto 1ch;
- }
- }
-
- .settings-subscription__link {
- color: var(--settings-subscription-text-color);
- }
-
- .settings-subscription__notch {
- animation: wiggle 500ms ease;
- background: var(--settings-subscription-background);
- border-radius: 3em;
- box-shadow: 0 0 0.3em 0.2em var(--settings-subscription-color);
- color: var(--settings-subscription-text-color);
- margin-inline: auto;
- padding: 0.3em 0.3em 0.3em 1.2em;
- transform: rotate(-1deg);
-
- @media (max-width: 640px) {
- border-radius: 1ch;
- padding-block: 1ch;
- padding-inline: 2ch;
- }
-
- .btn {
- /* margin-block: 0.3em; */
- }
- }
-
- .settings-subscription__panel {
- background: var(--settings-subscription-background);
- }
-
- .settings_subscription__warning {
- color: var(--settings-subscription-text-color);
-
- a {
- color: var(--settings-subscription-text-color);
- }
- }
-}
diff --git a/saas/app/controllers/account/billing_portals_controller.rb b/saas/app/controllers/account/billing_portals_controller.rb
deleted file mode 100644
index 67eaffe6c..000000000
--- a/saas/app/controllers/account/billing_portals_controller.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-class Account::BillingPortalsController < ApplicationController
- before_action :ensure_admin
- before_action :ensure_subscribed_account
-
- def show
- redirect_to create_stripe_billing_portal_session.url, allow_other_host: true
- end
-
- private
- def ensure_subscribed_account
- unless Current.account.subscribed?
- redirect_to account_subscription_path, alert: "No billing information found"
- end
- end
-
- def create_stripe_billing_portal_session
- Stripe::BillingPortal::Session.create(customer: Current.account.subscription.stripe_customer_id, return_url: account_settings_url)
- end
-end
diff --git a/saas/app/controllers/account/subscriptions/downgrades_controller.rb b/saas/app/controllers/account/subscriptions/downgrades_controller.rb
deleted file mode 100644
index d06795841..000000000
--- a/saas/app/controllers/account/subscriptions/downgrades_controller.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-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
diff --git a/saas/app/controllers/account/subscriptions/update_plan_controller.rb b/saas/app/controllers/account/subscriptions/update_plan_controller.rb
deleted file mode 100644
index c6a665cec..000000000
--- a/saas/app/controllers/account/subscriptions/update_plan_controller.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-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
diff --git a/saas/app/controllers/account/subscriptions/upgrades_controller.rb b/saas/app/controllers/account/subscriptions/upgrades_controller.rb
deleted file mode 100644
index 1de6d8c6c..000000000
--- a/saas/app/controllers/account/subscriptions/upgrades_controller.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-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
diff --git a/saas/app/controllers/account/subscriptions_controller.rb b/saas/app/controllers/account/subscriptions_controller.rb
deleted file mode 100644
index bbc4ec47a..000000000
--- a/saas/app/controllers/account/subscriptions_controller.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-class Account::SubscriptionsController < ApplicationController
- before_action :ensure_admin
- before_action :set_stripe_session, only: :show
-
- def show
- end
-
- def create
- session = Stripe::Checkout::Session.create \
- customer: find_or_create_stripe_customer,
- mode: "subscription",
- 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_param.key },
- automatic_tax: { enabled: true },
- tax_id_collection: { enabled: true },
- billing_address_collection: "required",
- customer_update: { address: "auto", name: "auto" }
-
- redirect_to session.url, allow_other_host: true
- 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
-
- def find_or_create_stripe_customer
- find_stripe_customer || create_stripe_customer
- end
-
- def find_stripe_customer
- Stripe::Customer.retrieve(Current.account.subscription.stripe_customer_id) if Current.account.subscription&.stripe_customer_id
- end
-
- 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_param.key, status: "incomplete")
- end
- end
-end
diff --git a/saas/app/controllers/admin/account_searches_controller.rb b/saas/app/controllers/admin/account_searches_controller.rb
deleted file mode 100644
index db655a9ef..000000000
--- a/saas/app/controllers/admin/account_searches_controller.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class Admin::AccountSearchesController < AdminController
- def create
- redirect_to saas.edit_admin_account_path(params[:q])
- end
-end
diff --git a/saas/app/controllers/admin/accounts_controller.rb b/saas/app/controllers/admin/accounts_controller.rb
deleted file mode 100644
index 26a2b9c82..000000000
--- a/saas/app/controllers/admin/accounts_controller.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-class Admin::AccountsController < AdminController
- include Admin::AccountScoped
-
- layout "public"
-
- before_action :set_account, only: %i[ edit update ]
-
- def index
- end
-
- def edit
- end
-
- def update
- @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
-
- private
- def set_account
- @account = Account.find_by!(external_account_id: params[:id])
- end
-
- def overridden_limits_params
- params.expect(account: [ :card_count, :bytes_used ])
- end
-end
diff --git a/saas/app/controllers/admin/billing_waivers_controller.rb b/saas/app/controllers/admin/billing_waivers_controller.rb
deleted file mode 100644
index 713891069..000000000
--- a/saas/app/controllers/admin/billing_waivers_controller.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-class Admin::BillingWaiversController < AdminController
- include Admin::AccountScoped
-
- def create
- @account.comp
- redirect_to saas.edit_admin_account_path(@account.external_account_id), notice: "Account comped"
- end
-
- def destroy
- @account.uncomp
- redirect_to saas.edit_admin_account_path(@account.external_account_id), notice: "Account uncomped"
- end
-end
diff --git a/saas/app/controllers/admin/overridden_limits_controller.rb b/saas/app/controllers/admin/overridden_limits_controller.rb
deleted file mode 100644
index fff8ea3e3..000000000
--- a/saas/app/controllers/admin/overridden_limits_controller.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-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
diff --git a/saas/app/controllers/admin/stats_controller.rb b/saas/app/controllers/admin/stats_controller.rb
index 287b49024..6d508fc2c 100644
--- a/saas/app/controllers/admin/stats_controller.rb
+++ b/saas/app/controllers/admin/stats_controller.rb
@@ -6,10 +6,6 @@ class Admin::StatsController < AdminController
@accounts_last_7_days = Account.where(created_at: 7.days.ago..).count
@accounts_last_24_hours = Account.where(created_at: 24.hours.ago..).count
- @paid_accounts_total = Account::Subscription.paid.distinct.count(:account_id)
- @paid_accounts_last_7_days = Account::Subscription.paid.where(created_at: 7.days.ago..).distinct.count(:account_id)
- @paid_accounts_last_24_hours = Account::Subscription.paid.where(created_at: 24.hours.ago..).distinct.count(:account_id)
-
@identities_total = Identity.count
@identities_last_7_days = Identity.where(created_at: 7.days.ago..).count
@identities_last_24_hours = Identity.where(created_at: 24.hours.ago..).count
diff --git a/saas/app/controllers/concerns/admin/account_scoped.rb b/saas/app/controllers/concerns/admin/account_scoped.rb
deleted file mode 100644
index 314720cd2..000000000
--- a/saas/app/controllers/concerns/admin/account_scoped.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-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
diff --git a/saas/app/controllers/concerns/card/limited.rb b/saas/app/controllers/concerns/card/limited.rb
deleted file mode 100644
index 0576c6c4e..000000000
--- a/saas/app/controllers/concerns/card/limited.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-module Card::Limited
- extend ActiveSupport::Concern
-
- private
- def ensure_under_limits
- head :forbidden if Current.account.exceeding_limits?
- end
-end
diff --git a/saas/app/controllers/concerns/card/limited_creation.rb b/saas/app/controllers/concerns/card/limited_creation.rb
deleted file mode 100644
index 0e992008a..000000000
--- a/saas/app/controllers/concerns/card/limited_creation.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-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_under_limits, only: %i[ create ], if: -> { request.format.json? }
- end
-end
diff --git a/saas/app/controllers/concerns/card/limited_publishing.rb b/saas/app/controllers/concerns/card/limited_publishing.rb
deleted file mode 100644
index 8b60a4b53..000000000
--- a/saas/app/controllers/concerns/card/limited_publishing.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module Card::LimitedPublishing
- extend ActiveSupport::Concern
-
- included do
- include Card::Limited
-
- before_action :ensure_under_limits, only: %i[ create ]
- end
-end
diff --git a/saas/app/controllers/stripe/webhooks_controller.rb b/saas/app/controllers/stripe/webhooks_controller.rb
deleted file mode 100644
index 13dcd4699..000000000
--- a/saas/app/controllers/stripe/webhooks_controller.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-class Stripe::WebhooksController < ApplicationController
- allow_unauthenticated_access
- skip_before_action :require_account
- skip_forgery_protection
-
- def create
- if event = verify_webhook_signature
- dispatch_stripe_event(event)
- head :ok
- else
- head :bad_request
- end
- end
-
- private
- def dispatch_stripe_event(event)
- case event.type
- when "checkout.session.completed"
- sync_new_subscription(event.data.object.subscription, plan_key: event.data.object.metadata["plan_key"]) if event.data.object.mode == "subscription"
- when "customer.subscription.updated", "customer.subscription.deleted"
- sync_subscription(event.data.object.id)
- end
- end
-
- def verify_webhook_signature
- payload = request.body.read
- sig_header = request.env["HTTP_STRIPE_SIGNATURE"]
-
- Stripe::Webhook.construct_event(payload, sig_header, ENV["STRIPE_WEBHOOK_SECRET"])
- rescue Stripe::SignatureVerificationError => e
- Rails.logger.error "Stripe webhook signature verification failed: #{e.message}"
- nil
- end
-
- def sync_new_subscription(stripe_subscription_id, plan_key:)
- sync_subscription(stripe_subscription_id) do |subscription_properties|
- subscription_properties[:plan_key] = plan_key if plan_key
- end
- end
-
- # Always fetch fresh subscription data from Stripe to handle out-of-order
- # event delivery. Not relying on payload data.
- def sync_subscription(stripe_subscription_id)
- stripe_subscription = Stripe::Subscription.retrieve(stripe_subscription_id)
-
- if subscription = find_subscription_by_stripe_customer(stripe_subscription.customer)
- subscription_properties = {
- stripe_subscription_id: 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),
- plan_key: plan_key_for(stripe_subscription)
- }
-
- yield subscription_properties if block_given?
- subscription_properties[:stripe_subscription_id] = nil if stripe_subscription.status == "canceled"
-
- subscription.update!(subscription_properties)
- end
- end
-
- def find_subscription_by_stripe_customer(id)
- Account::Subscription.find_by(stripe_customer_id: id)
- end
-
- def current_period_end_for(stripe_subscription)
- timestamp = stripe_subscription.items.data.first&.current_period_end
- Time.at(timestamp) if timestamp
- end
-
- def next_amount_due_for(stripe_subscription)
- return nil if stripe_subscription.status == "canceled"
-
- preview = Stripe::Invoice.create_preview(customer: stripe_subscription.customer, subscription: stripe_subscription.id)
- preview.amount_due
- 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
diff --git a/saas/app/helpers/subscriptions_helper.rb b/saas/app/helpers/subscriptions_helper.rb
deleted file mode 100644
index fd6b5aa47..000000000
--- a/saas/app/helpers/subscriptions_helper.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module SubscriptionsHelper
- def storage_to_human_size(bytes)
- number_to_human_size(bytes).delete(" ")
- end
-
- def subscription_period_end_action(subscription)
- if subscription.to_be_canceled?
- "Your Fizzy subscription ends on"
- elsif subscription.canceled?
- "Your Fizzy subscription ended on"
- else
- "Your next payment is #{ format_currency(subscription.next_amount_due) } on".html_safe
- end
- end
-
- def format_currency(amount)
- number_to_currency(amount, precision: (amount % 1).zero? ? 0 : 2)
- end
-end
diff --git a/saas/app/jobs/account/sync_stripe_customer_email_job.rb b/saas/app/jobs/account/sync_stripe_customer_email_job.rb
deleted file mode 100644
index b84eafa76..000000000
--- a/saas/app/jobs/account/sync_stripe_customer_email_job.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-class Account::SyncStripeCustomerEmailJob < ApplicationJob
- queue_as :default
- retry_on Stripe::StripeError, wait: :polynomially_longer
-
- def perform(subscription)
- subscription.sync_customer_email_to_stripe
- end
-end
diff --git a/saas/app/models/account/billing.rb b/saas/app/models/account/billing.rb
deleted file mode 100644
index c124fd64c..000000000
--- a/saas/app/models/account/billing.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-module Account::Billing
- extend ActiveSupport::Concern
-
- included do
- has_one :subscription, class_name: "Account::Subscription", dependent: :destroy
- has_one :billing_waiver, class_name: "Account::BillingWaiver", dependent: :destroy
-
- set_callback :incinerate, :before, -> { subscription&.cancel }
- set_callback :cancel, :after, -> { subscription&.pause }
- set_callback :reactivate, :before, -> { subscription&.resume }
- end
-
- def plan
- active_subscription&.plan || Plan.free
- end
-
- def subscribed?
- subscription.present?
- end
-
- def comped?
- billing_waiver.present?
- end
-
- def comp
- create_billing_waiver unless billing_waiver
- end
-
- def uncomp
- billing_waiver&.destroy!
- reload_billing_waiver
- end
-
- def owner_email_changed
- Account::SyncStripeCustomerEmailJob.perform_later(subscription) if subscription
- end
-
- private
- def active_subscription
- if comped?
- comped_subscription
- elsif subscription&.active?
- subscription
- end
- end
-
- def comped_subscription
- @comped_subscription ||= billing_waiver&.subscription
- end
-end
diff --git a/saas/app/models/account/billing_waiver.rb b/saas/app/models/account/billing_waiver.rb
deleted file mode 100644
index a9ec35eb8..000000000
--- a/saas/app/models/account/billing_waiver.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class Account::BillingWaiver < SaasRecord
- belongs_to :account
-
- def subscription
- @subscription ||= Account::Subscription.new(plan: Plan.paid_with_extra_storage)
- end
-end
diff --git a/saas/app/models/account/limited.rb b/saas/app/models/account/limited.rb
deleted file mode 100644
index 2918e90a3..000000000
--- a/saas/app/models/account/limited.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-module Account::Limited
- extend ActiveSupport::Concern
-
- included do
- has_one :overridden_limits, class_name: "Account::OverriddenLimits", dependent: :destroy
- end
-
- NEAR_CARD_LIMIT_THRESHOLD = 100
- NEAR_STORAGE_LIMIT_THRESHOLD = 500.megabytes
-
- 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
-
- 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
- end
-
- private
- def remaining_cards_count
- plan.card_limit - billed_cards_count
- end
-
- def remaining_storage
- plan.storage_limit - billed_bytes_used
- end
-end
diff --git a/saas/app/models/account/overridden_limits.rb b/saas/app/models/account/overridden_limits.rb
deleted file mode 100644
index cdf3c51ec..000000000
--- a/saas/app/models/account/overridden_limits.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# To ease testing of limits
-class Account::OverriddenLimits < SaasRecord
- belongs_to :account
-end
diff --git a/saas/app/models/account/subscription.rb b/saas/app/models/account/subscription.rb
deleted file mode 100644
index c1ec9e47c..000000000
--- a/saas/app/models/account/subscription.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-class Account::Subscription < SaasRecord
- belongs_to :account
-
- enum :status, %w[ active past_due unpaid canceled incomplete incomplete_expired trialing paused ].index_by(&:itself)
-
- scope :paid, -> {
- where(status: %w[active trialing past_due])
- .where(plan_key: Plan.all.select(&:paid?).map(&:key))
- .where.not(account_id: Account::BillingWaiver.select(:account_id))
- }
-
- validates :plan_key, presence: true, inclusion: { in: Plan::PLANS.keys.map(&:to_s) }
-
- delegate :paid?, to: :plan
-
- 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
-
- def next_amount_due
- next_amount_due_in_cents ? next_amount_due_in_cents / 100.0 : plan.price
- end
-
- def pause
- if stripe_subscription_id.present?
- Stripe::Subscription.update(
- stripe_subscription_id,
- pause_collection: { behavior: "void" }
- )
- end
- end
-
- def resume
- if stripe_subscription_id.present?
- Stripe::Subscription.update(
- stripe_subscription_id,
- pause_collection: ""
- )
- end
- end
-
- def cancel
- Stripe::Subscription.cancel(stripe_subscription_id) if stripe_subscription_id.present?
- rescue Stripe::InvalidRequestError => e
- # Subscription already deleted/canceled in Stripe - treat as success
- Rails.logger.warn "Stripe subscription #{stripe_subscription_id} not found during cancel: #{e.message}"
- end
-
- def sync_customer_email_to_stripe
- if stripe_customer_id && (email = owner_email)
- Stripe::Customer.update(stripe_customer_id, email: email)
- end
- rescue Stripe::InvalidRequestError => e
- # Customer already deleted in Stripe - treat as success
- Rails.logger.warn "Stripe customer #{stripe_customer_id} not found during email sync: #{e.message}"
- end
-
- private
- # Account owner email for Stripe customer record. Returns nil when:
- # - No owner exists (ownership being transferred, account in limbo)
- # - Owner has no identity (deactivated user)
- def owner_email
- account.users.owner.first&.identity&.email_address
- end
-end
diff --git a/saas/app/models/plan.rb b/saas/app/models/plan.rb
deleted file mode 100644
index e77ea81e8..000000000
--- a/saas/app/models/plan.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-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_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
-
- class << self
- def all
- @all ||= PLANS.map { |key, properties| new(key: key, **properties) }
- end
-
- def free
- @free ||= find(:free_v1)
- end
-
- 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
-
- def initialize(key:, name:, price:, card_limit:, storage_limit:, stripe_price_id: nil)
- @key = key
- @name = name
- @price = price
- @card_limit = card_limit
- @storage_limit = storage_limit
- @stripe_price_id = stripe_price_id
- end
-
- def free?
- price.zero?
- end
-
- def paid?
- !free?
- end
-
- def limit_cards?
- !card_limit.infinite?
- end
-end
diff --git a/saas/app/models/user/notifies_account_of_email_change.rb b/saas/app/models/user/notifies_account_of_email_change.rb
deleted file mode 100644
index daba4ba3d..000000000
--- a/saas/app/models/user/notifies_account_of_email_change.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-module User::NotifiesAccountOfEmailChange
- extend ActiveSupport::Concern
-
- included do
- after_update :notify_account_of_owner_change, if: :account_owner_changed?
- end
-
- private
- # Account owner changed when:
- # - The current owner changed their email
- # - A user just became the owner (ownership transfer)
- def account_owner_changed?
- owner? && identity && (saved_change_to_identity_id? || became_owner?)
- end
-
- def became_owner?
- saved_change_to_role? && role_before_last_save != "owner"
- end
-
- def notify_account_of_owner_change
- account.owner_email_changed
- end
-end
diff --git a/saas/app/views/account/settings/_free_plan.html.erb b/saas/app/views/account/settings/_free_plan.html.erb
deleted file mode 100644
index ca467d8af..000000000
--- a/saas/app/views/account/settings/_free_plan.html.erb
+++ /dev/null
@@ -1,18 +0,0 @@
-
- <% 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) %> of 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 <%= format_currency(Plan.paid.price) %>/month for unlimited cards , unlimited users , and <%= storage_to_human_size(Plan.paid.storage_limit) %> of storage.
-
-<%= button_to "Upgrade to #{Plan.paid.name} for #{ format_currency(Plan.paid.price) }/month", account_subscription_path, class: "btn settings-subscription__button txt-medium", form: { data: { turbo: false } } %>
-
-Cancel anytime, no contracts, take your data with you whenever.
-
diff --git a/saas/app/views/account/settings/_paid_plan.html.erb b/saas/app/views/account/settings/_paid_plan.html.erb
deleted file mode 100644
index 6ea99a066..000000000
--- a/saas/app/views/account/settings/_paid_plan.html.erb
+++ /dev/null
@@ -1,21 +0,0 @@
-
- <% 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 <%= format_currency(Plan.paid_with_extra_storage.price - Plan.paid.price) %>/month more.
-
-
- <%= button_to "Upgrade to #{Plan.paid_with_extra_storage.name} for #{ number_to_currency(Plan.paid_with_extra_storage.price, strip_insignificant_zeros: true) }/month", account_subscription_upgrade_path, class: "btn settings-subscription__button txt-medium", form: { data: { turbo: false } } %>
-<% 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
deleted file mode 100644
index 4eca0ad2d..000000000
--- a/saas/app/views/account/settings/_subscription.html.erb
+++ /dev/null
@@ -1,20 +0,0 @@
-<% if subscription.current_period_end %>
- <%= subscription_period_end_action(subscription) %> <%= subscription.current_period_end.to_date.to_fs(:long) %>
-<% end %>
-
-
- <%= link_to "Manage your subscription", account_billing_portal_path, class: "btn btn--plain settings-subscription__link txt-link", data: { turbo_prefetch: false } %>
-
-
-
-
diff --git a/saas/app/views/account/settings/_subscription_panel.html.erb b/saas/app/views/account/settings/_subscription_panel.html.erb
deleted file mode 100644
index 1cfcda4d8..000000000
--- a/saas/app/views/account/settings/_subscription_panel.html.erb
+++ /dev/null
@@ -1,11 +0,0 @@
-<% if Current.user.admin? && !Current.account.comped? %>
-
- Subscription
-
- <% if Current.account.plan.free? %>
- <%= render "account/settings/free_plan" %>
- <% else %>
- <%= 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
deleted file mode 100644
index 449fecf16..000000000
--- a/saas/app/views/account/subscriptions/_upgrade.html.erb
+++ /dev/null
@@ -1,9 +0,0 @@
-
- <% if Current.user.admin? %>
- <%= 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 %>
- <%= 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
deleted file mode 100644
index 3dc759d9e..000000000
--- a/saas/app/views/account/subscriptions/notices/_admin_exceeding_card_limit.html.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-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
deleted file mode 100644
index 8770c1460..000000000
--- a/saas/app/views/account/subscriptions/notices/_admin_exceeding_storage_limit.html.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-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
deleted file mode 100644
index f3f65f3be..000000000
--- a/saas/app/views/account/subscriptions/notices/_user_exceeding_card_limit.html.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-
- 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
deleted file mode 100644
index f230443a1..000000000
--- a/saas/app/views/account/subscriptions/notices/_user_exceeding_storage_limit.html.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-
- This account has run out of <%= Current.account.plan.free? ? "free storage" : "storage" %>. Upgrade to get more.
-
diff --git a/saas/app/views/account/subscriptions/show.html.erb b/saas/app/views/account/subscriptions/show.html.erb
deleted file mode 100644
index 8d29a379e..000000000
--- a/saas/app/views/account/subscriptions/show.html.erb
+++ /dev/null
@@ -1,9 +0,0 @@
-<% @page_title = "Thank you" %>
-
-<% if @stripe_session&.payment_status == "paid" %>
-
- Thanks for buying Fizzy!
- Your payment was successful. You’re now on the <%= Current.account.plan.name %> plan.
- <%= link_to "Done", account_settings_path, class: "btn settings-subscription__button txt-medium" %>
-
-<% end %>
diff --git a/saas/app/views/admin/accounts/edit.html.erb b/saas/app/views/admin/accounts/edit.html.erb
deleted file mode 100644
index 76c23497c..000000000
--- a/saas/app/views/admin/accounts/edit.html.erb
+++ /dev/null
@@ -1,63 +0,0 @@
-<% @page_title = "Account admin" %>
-
-<% content_for :header do %>
-
-<% end %>
-
-
-
-
-
Edit Account <%= @account.external_account_id %>
-
- <%= form_with model: @account, url: saas.admin_account_path(@account.external_account_id), class: "flex flex-column gap" do |form| %>
-
-
- Account Name:
- <%= @account.name %>
-
-
- Subscription plan:
- <%= @account.plan.name %>
-
-
- Actual card count:
- <%= @account.cards_count %> cards
-
-
- Actual bytes used:
- <%= storage_to_human_size(@account.bytes_used) %>
-
-
-
- <%= form.label :card_count, class: "txt-subtle" do %>
- Override card count <%= "(Exceeds plan limit)" if @account.exceeding_card_limit? %>
- <% end %>
-
- <%= form.number_field :card_count, value: @account.overridden_limits&.card_count, class: "input" %>
-
-
-
- <%= form.label :bytes_used, class: "txt-subtle" do %>
- Override bytes used <%= "(Exceeds plan limit)" if @account.exceeding_storage_limit? %>
- <% end %>
-
- <%= form.number_field :bytes_used, value: @account.overridden_limits&.bytes_used, class: "input" %>
-
-
-
-
Save changes
- <% end %>
-
- <% if @account.overridden_limits %>
- <%= button_to "Reset limits", saas.admin_account_overridden_limits_path(@account.external_account_id), method: :delete, class: "btn btn--negative full-width" %>
- <% end %>
-
- <% if @account.comped? %>
- <%= button_to "Uncomp account", saas.admin_account_billing_waiver_path(@account.external_account_id), method: :delete, class: "btn btn--plain txt-negative txt-underline" %>
- <% else %>
- <%= button_to "Comp account", saas.admin_account_billing_waiver_path(@account.external_account_id), method: :post, class: "btn btn--plain txt-positive txt-underline" %>
- <% end %>
-
-
diff --git a/saas/app/views/admin/accounts/index.html.erb b/saas/app/views/admin/accounts/index.html.erb
deleted file mode 100644
index 96bed2338..000000000
--- a/saas/app/views/admin/accounts/index.html.erb
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
Find an account
-
- <%= form_with url: saas.admin_account_search_path, class: "flex flex-column gap" do |form| %>
- <%= form.text_field :q, placeholder: "Account ID", autofocus: true, class: "input" %>
- <%= form.submit "Search", class: "btn btn--link" %>
- <% end %>
-
-
-
-
-
diff --git a/saas/app/views/admin/stats/show.html.erb b/saas/app/views/admin/stats/show.html.erb
index f93bd3eba..c2b1e1d5d 100644
--- a/saas/app/views/admin/stats/show.html.erb
+++ b/saas/app/views/admin/stats/show.html.erb
@@ -33,32 +33,6 @@
-
-
-
-
-
Total
-
- <%= @paid_accounts_total %>
-
-
-
-
7 days
-
- <%= @paid_accounts_last_7_days %>
-
-
-
-
24 hours
-
- <%= @paid_accounts_last_24_hours %>
-
-
-
-
-
Identities Created
diff --git a/saas/app/views/cards/container/footer/saas/_create.html.erb b/saas/app/views/cards/container/footer/saas/_create.html.erb
deleted file mode 100644
index 8b0edd308..000000000
--- a/saas/app/views/cards/container/footer/saas/_create.html.erb
+++ /dev/null
@@ -1,5 +0,0 @@
-<% if Current.account.exceeding_limits? %>
- <%= render "account/subscriptions/upgrade" %>
-<% else %>
- <%= render "cards/container/footer/create", card: card %>
-<% end %>
diff --git a/saas/app/views/cards/container/footer/saas/_near_notice.html.erb b/saas/app/views/cards/container/footer/saas/_near_notice.html.erb
deleted file mode 100644
index 4cf480f3c..000000000
--- a/saas/app/views/cards/container/footer/saas/_near_notice.html.erb
+++ /dev/null
@@ -1,9 +0,0 @@
-
- <% 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
deleted file mode 100644
index caa7bd958..000000000
--- a/saas/app/views/cards/container/footer/saas/notices/_admin_nearing_card_limit.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-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
deleted file mode 100644
index ab1a3ca34..000000000
--- a/saas/app/views/cards/container/footer/saas/notices/_admin_nearing_storage_limit.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-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
deleted file mode 100644
index 8ab80e162..000000000
--- a/saas/app/views/cards/container/footer/saas/notices/_user_nearing_card_limit.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-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
deleted file mode 100644
index 25428980a..000000000
--- a/saas/app/views/cards/container/footer/saas/notices/_user_nearing_storage_limit.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-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/routes.rb b/saas/config/routes.rb
index 581a2ae02..3eafab92f 100644
--- a/saas/config/routes.rb
+++ b/saas/config/routes.rb
@@ -8,10 +8,5 @@ Fizzy::Saas::Engine.routes.draw do
namespace :admin do
mount Audits1984::Engine, at: "/console"
get "stats", to: "stats#show"
- resource :account_search, only: :create
- resources :accounts do
- resource :overridden_limits, only: :destroy
- resource :billing_waiver, only: [ :create, :destroy ]
- end
end
end
diff --git a/saas/db/migrate/20260317000000_drop_billing_tables.rb b/saas/db/migrate/20260317000000_drop_billing_tables.rb
new file mode 100644
index 000000000..2ac9a808d
--- /dev/null
+++ b/saas/db/migrate/20260317000000_drop_billing_tables.rb
@@ -0,0 +1,7 @@
+class DropBillingTables < ActiveRecord::Migration[8.2]
+ def change
+ drop_table :account_subscriptions
+ drop_table :account_overridden_limits
+ drop_table :account_billing_waivers
+ end
+end
diff --git a/saas/db/saas_schema.rb b/saas/db/saas_schema.rb
index 298cc033a..5ae786764 100644
--- a/saas/db/saas_schema.rb
+++ b/saas/db/saas_schema.rb
@@ -10,39 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.2].define(version: 2026_01_26_230838) 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
- t.datetime "updated_at", null: false
- t.index ["account_id"], name: "index_account_billing_waivers_on_account_id", unique: true
- end
-
- 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
- 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"
- t.datetime "created_at", null: false
- t.datetime "current_period_end"
- t.integer "next_amount_due_in_cents"
- t.string "plan_key"
- t.string "status"
- t.string "stripe_customer_id", null: false
- t.string "stripe_subscription_id"
- t.datetime "updated_at", null: false
- t.index ["account_id"], name: "index_account_subscriptions_on_account_id"
- t.index ["stripe_customer_id"], name: "index_account_subscriptions_on_stripe_customer_id", unique: true
- t.index ["stripe_subscription_id"], name: "index_account_subscriptions_on_stripe_subscription_id", unique: true
- end
-
+ActiveRecord::Schema[8.2].define(version: 2026_03_17_000000) do
create_table "action_push_native_devices", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.datetime "created_at", null: false
t.string "name"
diff --git a/saas/exe/stripe-dev b/saas/exe/stripe-dev
deleted file mode 100755
index d3e63a754..000000000
--- a/saas/exe/stripe-dev
+++ /dev/null
@@ -1,83 +0,0 @@
-#!/usr/bin/env ruby
-#
-# Fetches Stripe development environment variables from 1Password and starts
-# the Stripe CLI webhook listener.
-#
-# Usage: eval "$(bundle exec stripe-dev)"
-
-require "json"
-require "fileutils"
-
-LOG_FILE = "log/stripe.development.log"
-PID_FILE = "tmp/stripe.tunnel.pid"
-
-# Ensure directories exist
-FileUtils.mkdir_p("log")
-FileUtils.mkdir_p("tmp")
-
-# Kill any existing stripe tunnel process
-if File.exist?(PID_FILE)
- old_pid = File.read(PID_FILE).strip.to_i
- if old_pid > 0
- Process.kill("TERM", old_pid) rescue nil
- end
- File.delete(PID_FILE)
-end
-
-# Fetch secrets from 1Password
-secrets_escaped = `kamal secrets fetch \
- --adapter 1password \
- --account 23QPQDKZC5BKBIIG7UGT5GR5RM \
- --from "Deploy/Fizzy" \
- "Development/STRIPE_SECRET_KEY" \
- "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, "")
-
-# Start stripe listen in background
-pid = spawn(
- "stripe", "listen", "--forward-to", "localhost:3006/stripe/webhooks",
- out: [ LOG_FILE, "a" ],
- err: [ LOG_FILE, "a" ]
-)
-Process.detach(pid)
-
-# Save PID for cleanup
-File.write(PID_FILE, pid.to_s)
-
-# Wait for the webhook secret to appear in logs
-webhook_secret = nil
-20.times do
- sleep 0.5
- if File.exist?(LOG_FILE)
- content = File.read(LOG_FILE)
- if match = content.match(/webhook signing secret is (whsec_\w+)/)
- webhook_secret = match[1]
- break
- end
- end
-end
-
-if webhook_secret.nil?
- warn "Warning: Could not capture webhook secret from stripe listen"
-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)
-warn ""
-warn "Stripe CLI listening (PID: #{pid})"
-warn "Logs: #{LOG_FILE}"
diff --git a/saas/lib/fizzy/saas/engine.rb b/saas/lib/fizzy/saas/engine.rb
index 1ba45df42..dea321cfc 100644
--- a/saas/lib/fizzy/saas/engine.rb
+++ b/saas/lib/fizzy/saas/engine.rb
@@ -16,10 +16,6 @@ module Fizzy
connects_to database: { writing: :saas, reading: :saas }
end
- initializer "fizzy_saas.content_security_policy", before: :load_config_initializers do |app|
- app.config.x.content_security_policy.form_action = "https://checkout.stripe.com https://billing.stripe.com"
- end
-
initializer "fizzy_saas.assets" do |app|
app.config.assets.paths << root.join("app/assets/stylesheets")
end
@@ -33,25 +29,6 @@ module Fizzy
app.paths["config/push"].unshift(root.join("config/push.yml").to_s)
end
- initializer "fizzy.saas.routes", after: :add_routing_paths do |app|
- # Routes that rely on the implicit account tenant should go here instead of in +routes.rb+.
- app.routes.prepend do
- namespace :account do
- resource :billing_portal, only: :show
- resource :subscription do
- scope module: :subscriptions do
- resource :upgrade, only: :create
- resource :downgrade, only: :create
- end
- end
- end
-
- namespace :stripe do
- resource :webhooks, only: :create
- end
- end
- end
-
initializer "fizzy.saas.mount" do |app|
app.routes.append do
mount Fizzy::Saas::Engine => "/", as: "saas"
@@ -98,10 +75,6 @@ module Fizzy
end
end
- initializer "fizzy_saas.stripe" do
- Stripe.api_key = ENV["STRIPE_SECRET_KEY"]
- end
-
initializer "fizzy_saas.sentry" do
if !Rails.env.local? && ENV["SKIP_TELEMETRY"].blank?
Sentry.init do |config|
@@ -160,14 +133,10 @@ module Fizzy
end
config.to_prepare do
- ::Account.include Account::Billing, Account::Limited
- ::User.include User::NotifiesAccountOfEmailChange
::Identity.include Authorization::Identity, Identity::Devices
::Session.include Session::Devices
::Signup.prepend Signup
ApplicationController.include Authorization::Controller
- CardsController.include(Card::LimitedCreation)
- Cards::PublishesController.include(Card::LimitedPublishing)
Notification.register_push_target(:native)
diff --git a/saas/lib/tasks/fizzy/usage_report.rake b/saas/lib/tasks/fizzy/usage_report.rake
deleted file mode 100644
index d8b3ba108..000000000
--- a/saas/lib/tasks/fizzy/usage_report.rake
+++ /dev/null
@@ -1,37 +0,0 @@
-require "csv"
-
-namespace :saas do
- desc "Generate a CSV usage report for all active accounts"
- task usage_report: :environment do
- output_path = Rails.root.join("tmp/usage_report.csv")
-
- CSV.open(output_path, "w") do |csv|
- csv << [ "Queenbee ID", "Account Name", "Sign Up Date", "Paid Date", "Comped", "Card Count", "Storage Used (Bytes)", "Last Active" ]
-
- Account.active.includes(:storage_total).in_batches do |batch|
- batch_ids = batch.pluck(:id)
- paid_dates = Account::Subscription.paid.where(account_id: batch_ids)
- .group(:account_id).minimum(:created_at)
- comped_account_ids = Account::BillingWaiver.where(account_id: batch_ids)
- .pluck(:account_id).to_set
- last_active_dates = Card.where(account_id: batch_ids)
- .group(:account_id).maximum(:last_active_at)
-
- batch.each do |account|
- csv << [
- account.external_account_id,
- account.name,
- account.created_at.to_date,
- paid_dates[account.id]&.to_date,
- comped_account_ids.include?(account.id),
- account.cards_count,
- account.bytes_used,
- last_active_dates[account.id]&.to_date
- ]
- end
- end
- end
-
- puts "Report written to #{output_path}"
- end
-end
diff --git a/saas/test/controllers/accounts/billing_portals_controller_test.rb b/saas/test/controllers/accounts/billing_portals_controller_test.rb
deleted file mode 100644
index 00f16e4df..000000000
--- a/saas/test/controllers/accounts/billing_portals_controller_test.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-require "test_helper"
-require "ostruct"
-
-class Account::BillingPortalsControllerTest < ActionDispatch::IntegrationTest
- setup do
- sign_in_as :kevin
- end
-
- test "redirects to stripe billing portal" do
- Current.account.subscription.update!(stripe_customer_id: "cus_test123")
-
- session = OpenStruct.new(url: "https://billing.stripe.com/session123")
- Stripe::BillingPortal::Session.expects(:create)
- .with(customer: "cus_test123", return_url: account_settings_url)
- .returns(session)
-
- get account_billing_portal_path
-
- assert_redirected_to "https://billing.stripe.com/session123"
- end
-
- test "requires admin" do
- logout_and_sign_in_as :david
-
- get account_billing_portal_path
-
- assert_response :forbidden
- end
-end
diff --git a/saas/test/controllers/accounts/subscriptions/card_creation_test.rb b/saas/test/controllers/accounts/subscriptions/card_creation_test.rb
deleted file mode 100644
index 1ddc4bc1c..000000000
--- a/saas/test/controllers/accounts/subscriptions/card_creation_test.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-require "test_helper"
-
-class Account::Subscriptions::CardCreationTest < ActionDispatch::IntegrationTest
- setup do
- sign_in_as :mike
- end
-
- # Nearing limits - shown in card creation footer
-
- test "admin sees nearing card limit notice" do
- accounts(:initech).update_column(:cards_count, 950)
-
- get card_draft_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
- Account.any_instance.stubs(:bytes_used).returns(600.megabytes)
-
- get card_draft_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
- accounts(:initech).update_column(:cards_count, 1001)
-
- get card_draft_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
- Account.any_instance.stubs(:bytes_used).returns(1.1.gigabytes)
-
- get card_draft_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
- logout_and_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
- accounts(:initech).comp
-
- get card_draft_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
deleted file mode 100644
index e4680a1f8..000000000
--- a/saas/test/controllers/accounts/subscriptions/downgrades_controller_test.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-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
deleted file mode 100644
index 63feac2c5..000000000
--- a/saas/test/controllers/accounts/subscriptions/settings_test.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-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
deleted file mode 100644
index 65e268af1..000000000
--- a/saas/test/controllers/accounts/subscriptions/upgrades_controller_test.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-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
deleted file mode 100644
index 9e086df39..000000000
--- a/saas/test/controllers/accounts/subscriptions_controller_test.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-require "test_helper"
-require "ostruct"
-
-class Account::SubscriptionsControllerTest < ActionDispatch::IntegrationTest
- setup do
- sign_in_as :kevin
- end
-
- test "show" do
- get account_subscription_path
- assert_response :success
- end
-
- test "show with session_id retrieves stripe session" do
- Stripe::Checkout::Session.stubs(:retrieve).with("sess_123").returns(OpenStruct.new(id: "sess_123"))
-
- get account_subscription_path(session_id: "sess_123")
- assert_response :success
- end
-
- test "show requires admin" do
- logout_and_sign_in_as :david
-
- get account_subscription_path
- assert_response :forbidden
- end
-
- test "create 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).returns(session)
-
- post account_subscription_path
-
- assert_redirected_to "https://checkout.stripe.com/session123"
- end
-
- test "create requires admin" do
- logout_and_sign_in_as :david
-
- 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
deleted file mode 100644
index 3849c07a5..000000000
--- a/saas/test/controllers/admin/accounts_controller_test.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-require "test_helper"
-
-class Admin::AccountsControllerTest < ActionDispatch::IntegrationTest
- test "staff can access index" do
- sign_in_as :david
-
- untenanted do
- get saas.admin_accounts_path
- end
-
- assert_response :success
- end
-
- test "search account" do
- sign_in_as :david
-
- untenanted do
- post saas.admin_account_search_path, params: { q: accounts(:"37s").external_account_id }
- assert_redirected_to saas.edit_admin_account_path(accounts(:"37s").external_account_id)
- end
- end
-
- test "staff can edit account" do
- sign_in_as :david
-
- untenanted do
- get saas.edit_admin_account_path(accounts(:"37s").external_account_id)
- end
-
- assert_response :success
- end
-
- 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: { card_count: 500 } }
- assert_redirected_to saas.edit_admin_account_path(accounts(:"37s").external_account_id)
- end
-
- assert_equal 500, accounts(:"37s").reload.billed_cards_count
- end
-
- test "non-staff cannot access accounts" do
- sign_in_as :jz
-
- untenanted do
- patch saas.admin_account_path(accounts(:"37s").external_account_id), params: { account: { cards_count: 9999 } }
- end
-
- assert_response :forbidden
- assert_not_equal 9999, accounts(:"37s").reload.cards_count
- end
-end
diff --git a/saas/test/controllers/admin/billing_waivers_controller_test.rb b/saas/test/controllers/admin/billing_waivers_controller_test.rb
deleted file mode 100644
index 8f7d14199..000000000
--- a/saas/test/controllers/admin/billing_waivers_controller_test.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require "test_helper"
-
-class Admin::BillingWaiversControllerTest < ActionDispatch::IntegrationTest
- test "staff can comp an account" do
- sign_in_as :david
- account = accounts(:"37s")
-
- assert_not account.comped?
-
- untenanted do
- post saas.admin_account_billing_waiver_path(account.external_account_id)
- assert_redirected_to saas.edit_admin_account_path(account.external_account_id)
- end
-
- assert account.reload.comped?
- end
-
- test "staff can uncomp an account" do
- sign_in_as :david
- account = accounts(:"37s")
- account.comp
-
- assert account.comped?
-
- untenanted do
- delete saas.admin_account_billing_waiver_path(account.external_account_id)
- assert_redirected_to saas.edit_admin_account_path(account.external_account_id)
- end
-
- assert_not account.reload.comped?
- end
-end
diff --git a/saas/test/controllers/admin/overridden_limits_controller_test.rb b/saas/test/controllers/admin/overridden_limits_controller_test.rb
deleted file mode 100644
index b660e08b0..000000000
--- a/saas/test/controllers/admin/overridden_limits_controller_test.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-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
diff --git a/saas/test/controllers/card/limited_creation_test.rb b/saas/test/controllers/card/limited_creation_test.rb
deleted file mode 100644
index 4105091be..000000000
--- a/saas/test/controllers/card/limited_creation_test.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-require "test_helper"
-
-class Card::LimitedCreationTest < ActionDispatch::IntegrationTest
- test "cannot create cards via JSON when card limit exceeded" do
- sign_in_as :mike
-
- accounts(:initech).update_column(:cards_count, 1001)
-
- 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
-
- test "can create cards via HTML when card limit exceeded but they are drafts" do
- sign_in_as :mike
-
- accounts(:initech).update_column(:cards_count, 1001)
- boards(:miltons_wish_list).cards.drafted.where(creator: users(:mike)).destroy_all
-
- assert_difference -> { Card.count } do
- post board_cards_path(boards(:miltons_wish_list), script_name: accounts(:initech).slug)
- end
-
- assert_response :redirect
- assert Card.last.drafted?
- end
-
- test "cannot force published status via HTML when card limit exceeded" do
- sign_in_as :mike
-
- accounts(:initech).update_column(:cards_count, 1001)
- boards(:miltons_wish_list).cards.drafted.where(creator: users(:mike)).destroy_all
-
- assert_difference -> { Card.count } do
- post board_cards_path(boards(:miltons_wish_list), script_name: accounts(:initech).slug), params: { card: { status: "published" } }
- end
-
- 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
deleted file mode 100644
index 90e4e5696..000000000
--- a/saas/test/controllers/card/limited_publishing_test.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-require "test_helper"
-
-class Card::LimitedPublishingTest < ActionDispatch::IntegrationTest
- test "cannot publish cards when card limit exceeded" do
- sign_in_as :mike
-
- accounts(:initech).update_column(:cards_count, 1001)
-
- post card_publish_path(cards(:unfinished_thoughts), script_name: accounts(:initech).slug)
-
- 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/controllers/stripe/webhooks_controller_test.rb b/saas/test/controllers/stripe/webhooks_controller_test.rb
deleted file mode 100644
index 92f6b7508..000000000
--- a/saas/test/controllers/stripe/webhooks_controller_test.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-require "test_helper"
-require "ostruct"
-
-class Stripe::WebhooksControllerTest < ActionDispatch::IntegrationTest
- setup do
- @account = Account.create!(name: "Test")
- @subscription = @account.create_subscription! \
- plan_key: "monthly_v1",
- status: "incomplete",
- stripe_customer_id: "cus_test123"
- end
-
- test "invalid signature returns bad request" do
- Stripe::Webhook.stubs(:construct_event).raises(Stripe::SignatureVerificationError.new("invalid", "sig"))
-
- post stripe_webhooks_path
- assert_response :bad_request
- end
-
- test "checkout session completed activates subscription" do
- stripe_sub = OpenStruct.new(id: "sub_123", customer: "cus_test123", status: "active", cancel_at: nil, items: stub_items(1.month.from_now.to_i))
-
- event = stripe_event("checkout.session.completed",
- mode: "subscription",
- customer: "cus_test123",
- subscription: "sub_123",
- metadata: { "plan_key" => "monthly_v1" }
- )
-
- Stripe::Webhook.stubs(:construct_event).returns(event)
- Stripe::Subscription.stubs(:retrieve).returns(stripe_sub)
- Stripe::Invoice.stubs(:create_preview).returns(OpenStruct.new(amount_due: 1999))
-
- post stripe_webhooks_path
-
- assert_response :ok
- @subscription.reload
- assert_equal "sub_123", @subscription.stripe_subscription_id
- assert_equal "active", @subscription.status
- end
-
- test "subscription updated changes status and syncs next amount due" do
- @subscription.update!(stripe_subscription_id: "sub_123", status: "active")
-
- stripe_sub = OpenStruct.new(
- id: "sub_123",
- customer: "cus_test123",
- status: "past_due",
- cancel_at: nil,
- items: stub_items(1.month.from_now.to_i)
- )
-
- event = stripe_event("customer.subscription.updated", id: "sub_123")
-
- Stripe::Webhook.stubs(:construct_event).returns(event)
- Stripe::Subscription.stubs(:retrieve).returns(stripe_sub)
- Stripe::Invoice.stubs(:create_preview).returns(OpenStruct.new(amount_due: 1999))
-
- post stripe_webhooks_path
-
- assert_response :ok
- @subscription.reload
- assert_equal "past_due", @subscription.status
- assert_equal 1999, @subscription.next_amount_due_in_cents
- end
-
- test "subscription deleted cancels subscription" do
- @subscription.update!(stripe_subscription_id: "sub_123", status: "active")
-
- stripe_sub = OpenStruct.new(
- id: "sub_123",
- customer: "cus_test123",
- status: "canceled",
- cancel_at: nil,
- items: stub_items(1.month.from_now.to_i)
- )
-
- event = stripe_event("customer.subscription.deleted", id: "sub_123")
-
- Stripe::Webhook.stubs(:construct_event).returns(event)
- Stripe::Subscription.stubs(:retrieve).returns(stripe_sub)
-
- post stripe_webhooks_path
-
- assert_response :ok
- @subscription.reload
- assert_equal "canceled", @subscription.status
- assert_nil @subscription.stripe_subscription_id
- assert_nil @subscription.next_amount_due_in_cents
- end
-
- private
- def stripe_event(type, **attributes)
- OpenStruct.new(type: type, data: OpenStruct.new(object: OpenStruct.new(attributes)))
- end
-
- def stub_items(current_period_end)
- OpenStruct.new(data: [ OpenStruct.new(current_period_end: current_period_end) ])
- end
-end
diff --git a/saas/test/fixtures/account_subscriptions.yml b/saas/test/fixtures/account_subscriptions.yml
deleted file mode 100644
index a18ff961b..000000000
--- a/saas/test/fixtures/account_subscriptions.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-_fixture:
- model_class: Account::Subscription
-
-signals_monthly:
- id: <%= ActiveRecord::FixtureSet.identify("signals_monthly", :uuid) %>
- account_id: <%= ActiveRecord::FixtureSet.identify("37s", :uuid) %>
- plan_key: monthly_v1
- status: active
- stripe_customer_id: cus_test_37signals
- created_at: <%= Time.current %>
- updated_at: <%= Time.current %>
diff --git a/saas/test/models/account/billing_test.rb b/saas/test/models/account/billing_test.rb
deleted file mode 100644
index 0e70d37a5..000000000
--- a/saas/test/models/account/billing_test.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-require "test_helper"
-
-class Account::BillingTest < ActiveSupport::TestCase
- test "plan reflects active subscription" do
- account = accounts(:initech)
-
- # No subscription
- assert_equal Plan.free, account.plan
-
- # Subscription but it is not active
- account.create_subscription!(plan_key: "monthly_v1", status: "canceled", stripe_customer_id: "cus_test")
- assert_equal Plan.free, account.plan
-
- # Active subscription exists
- account.subscription.update!(status: "active")
- assert_equal Plan.paid, account.plan
- end
-
- test "comped account" do
- account = accounts(:"37s")
-
- assert_not account.comped?
-
- account.comp
- assert account.comped?
-
- # Calling comp again does not create duplicate
- account.comp
- assert_equal 1, Account::BillingWaiver.where(account: account).count
- end
-
- test "cancel callback pauses subscription" do
- account = accounts(:"37s")
- user = users(:david)
-
- subscription = mock("subscription")
- subscription.expects(:pause).once
- account.stubs(:subscription).returns(subscription)
-
- account.cancel(initiated_by: user)
- end
-
- test "reactivate callback resumes subscription" do
- account = accounts(:"37s")
- user = users(:david)
-
- # First cancel with a subscription mock
- subscription_for_cancel = mock("subscription")
- subscription_for_cancel.expects(:pause).once
- account.stubs(:subscription).returns(subscription_for_cancel)
-
- account.cancel(initiated_by: user)
-
- # Now stub for reactivation
- subscription_for_reactivate = mock("subscription")
- subscription_for_reactivate.expects(:resume).once
- account.stubs(:subscription).returns(subscription_for_reactivate)
-
- account.reactivate
- end
-
- test "incinerate callback cancels subscription before destroying account" do
- account = accounts(:"37s")
-
- subscription = mock("subscription")
- subscription.expects(:cancel).once
- account.stubs(:subscription).returns(subscription)
-
- account.incinerate
- end
-
- test "owner_email_changed enqueues sync job when subscription exists" do
- account = accounts(:"37s")
- account.create_subscription!(
- stripe_customer_id: "cus_test",
- plan_key: "monthly_v1",
- status: "active"
- )
-
- assert_enqueued_with(job: Account::SyncStripeCustomerEmailJob, args: [ account.subscription ]) do
- account.owner_email_changed
- end
- end
-
- test "owner_email_changed does nothing without subscription" do
- account = accounts(:initech)
- account.subscription&.destroy
-
- assert_no_enqueued_jobs do
- account.owner_email_changed
- end
- end
-end
diff --git a/saas/test/models/account/limited_test.rb b/saas/test/models/account/limited_test.rb
deleted file mode 100644
index fff154451..000000000
--- a/saas/test/models/account/limited_test.rb
+++ /dev/null
@@ -1,122 +0,0 @@
-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 at exactly the threshold (remaining = 100)
- accounts(:initech).update_column(:cards_count, 900)
- assert accounts(:initech).nearing_plan_cards_limit?
-
- # Free plan over the threshold
- 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 at exactly the limit
- accounts(:initech).update_column(:cards_count, 1000)
- assert 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
-
- test "comped accounts are never limited" do
- account = accounts(:initech)
- account.update_column(:cards_count, 1_000_000)
-
- assert account.exceeding_card_limit?
- assert account.nearing_plan_cards_limit?
-
- account.comp
-
- assert_not account.exceeding_card_limit?
- assert_not account.nearing_plan_cards_limit?
- end
-
- test "uncomping an account restores limits" do
- account = accounts(:initech)
- account.update_column(:cards_count, 1_000_000)
- account.comp
-
- assert_not account.exceeding_card_limit?
-
- account.uncomp
-
- 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/account/subscription_test.rb b/saas/test/models/account/subscription_test.rb
deleted file mode 100644
index f9f16f2bb..000000000
--- a/saas/test/models/account/subscription_test.rb
+++ /dev/null
@@ -1,245 +0,0 @@
-require "test_helper"
-
-class Account::SubscriptionTest < ActiveSupport::TestCase
- test "get the account plan" do
- subscription = Account::Subscription.new(plan_key: "free_v1")
- assert_equal Plan[:free_v1], subscription.plan
- end
-
- test "check if account is active" do
- subscription = Account::Subscription.new(status: "active")
- assert subscription.active?
- end
-
- test "check if account is paid" do
- assert Account::Subscription.new(plan_key: "monthly_v1", status: "active").paid?
- assert_not Account::Subscription.new(plan_key: "free_v1", status: "active").paid?
- end
-
- test "paid scope includes active paid subscriptions" do
- subscription = Account::Subscription.create!(
- account: accounts(:initech), plan_key: "monthly_v1", status: "active",
- stripe_customer_id: "cus_paid"
- )
-
- assert_includes Account::Subscription.paid, subscription
- end
-
- test "paid scope includes trialing and past_due subscriptions" do
- trialing = Account::Subscription.create!(
- account: accounts(:initech), plan_key: "monthly_v1", status: "trialing",
- stripe_customer_id: "cus_trialing"
- )
- past_due = Account::Subscription.create!(
- account: accounts(:acme), plan_key: "monthly_extra_storage_v1", status: "past_due",
- stripe_customer_id: "cus_past_due"
- )
-
- assert_includes Account::Subscription.paid, trialing
- assert_includes Account::Subscription.paid, past_due
- end
-
- test "paid scope excludes free plan subscriptions" do
- subscription = Account::Subscription.create!(
- account: accounts(:initech), plan_key: "free_v1", status: "active",
- stripe_customer_id: "cus_free"
- )
-
- assert_not_includes Account::Subscription.paid, subscription
- end
-
- test "paid scope excludes canceled subscriptions" do
- subscription = Account::Subscription.create!(
- account: accounts(:initech), plan_key: "monthly_v1", status: "canceled",
- stripe_customer_id: "cus_canceled"
- )
-
- assert_not_includes Account::Subscription.paid, subscription
- end
-
- test "paid scope excludes comped accounts" do
- subscription = Account::Subscription.create!(
- account: accounts(:initech), plan_key: "monthly_v1", status: "active",
- stripe_customer_id: "cus_comped"
- )
- Account::BillingWaiver.create!(account: accounts(:initech))
-
- assert_not_includes Account::Subscription.paid, subscription
- end
-
- test "pause pauses Stripe subscription with void behavior" do
- subscription = Account::Subscription.new(stripe_subscription_id: "sub_123")
-
- Stripe::Subscription.expects(:update).with(
- "sub_123",
- pause_collection: { behavior: "void" }
- ).returns(true)
-
- subscription.pause
- end
-
- test "pause does nothing when no stripe_subscription_id" do
- subscription = Account::Subscription.new(stripe_subscription_id: nil)
-
- Stripe::Subscription.expects(:update).never
-
- subscription.pause
- end
-
- test "pause raises on Stripe errors" do
- subscription = Account::Subscription.new(stripe_subscription_id: "sub_123")
-
- Stripe::Subscription.stubs(:update).raises(
- Stripe::APIConnectionError.new("Network error")
- )
-
- assert_raises Stripe::APIConnectionError do
- subscription.pause
- end
- end
-
- test "resume resumes Stripe subscription" do
- subscription = Account::Subscription.new(stripe_subscription_id: "sub_123")
-
- Stripe::Subscription.expects(:update).with(
- "sub_123",
- pause_collection: ""
- ).returns(true)
-
- subscription.resume
- end
-
- test "resume does nothing when no stripe_subscription_id" do
- subscription = Account::Subscription.new(stripe_subscription_id: nil)
-
- Stripe::Subscription.expects(:update).never
-
- subscription.resume
- end
-
- test "resume raises on Stripe errors" do
- subscription = Account::Subscription.new(stripe_subscription_id: "sub_123")
-
- Stripe::Subscription.stubs(:update).raises(
- Stripe::AuthenticationError.new("Invalid API key")
- )
-
- assert_raises Stripe::AuthenticationError do
- subscription.resume
- end
- end
-
- test "cancel cancels Stripe subscription" do
- subscription = Account::Subscription.new(stripe_subscription_id: "sub_123")
-
- Stripe::Subscription.expects(:cancel).with("sub_123").returns(true)
-
- subscription.cancel
- end
-
- test "cancel does nothing when no stripe_subscription_id" do
- subscription = Account::Subscription.new(stripe_subscription_id: nil)
-
- Stripe::Subscription.expects(:cancel).never
-
- subscription.cancel
- end
-
- test "cancel treats 404 as success" do
- subscription = Account::Subscription.new(stripe_subscription_id: "sub_deleted")
-
- Stripe::Subscription.stubs(:cancel).raises(
- Stripe::InvalidRequestError.new("No such subscription", {})
- )
-
- assert_nothing_raised do
- subscription.cancel
- end
- end
-
- test "cancel raises on other Stripe errors" do
- subscription = Account::Subscription.new(stripe_subscription_id: "sub_123")
-
- Stripe::Subscription.stubs(:cancel).raises(
- Stripe::RateLimitError.new("Rate limit exceeded")
- )
-
- assert_raises Stripe::RateLimitError do
- subscription.cancel
- end
- end
-
- test "sync_customer_email_to_stripe updates Stripe customer with owner email" do
- account = accounts(:"37s")
- owner = account.users.find_by(role: :owner) || account.users.first.tap { |u| u.update!(role: :owner) }
- subscription = account.create_subscription!(
- stripe_customer_id: "cus_test",
- plan_key: "monthly_v1",
- status: "active"
- )
-
- Stripe::Customer.expects(:update).with("cus_test", email: owner.identity.email_address).once
-
- subscription.sync_customer_email_to_stripe
- end
-
- test "sync_customer_email_to_stripe does nothing without stripe_customer_id" do
- account = accounts(:"37s")
- subscription = account.build_subscription(
- stripe_customer_id: nil,
- plan_key: "free_v1",
- status: "active"
- )
-
- Stripe::Customer.expects(:update).never
-
- subscription.sync_customer_email_to_stripe
- end
-
- test "sync_customer_email_to_stripe does nothing without owner" do
- account = accounts(:"37s")
- account.users.update_all(role: :member)
- subscription = account.create_subscription!(
- stripe_customer_id: "cus_test",
- plan_key: "monthly_v1",
- status: "active"
- )
-
- Stripe::Customer.expects(:update).never
-
- subscription.sync_customer_email_to_stripe
- end
-
- test "sync_customer_email_to_stripe does nothing when owner has no identity" do
- account = accounts(:"37s")
- owner = account.users.find_by(role: :owner) || account.users.first.tap { |u| u.update!(role: :owner) }
- owner.update_column(:identity_id, nil)
- subscription = account.create_subscription!(
- stripe_customer_id: "cus_test",
- plan_key: "monthly_v1",
- status: "active"
- )
-
- Stripe::Customer.expects(:update).never
-
- subscription.sync_customer_email_to_stripe
- end
-
- test "sync_customer_email_to_stripe treats deleted customer as success" do
- account = accounts(:"37s")
- account.users.find_by(role: :owner) || account.users.first.tap { |u| u.update!(role: :owner) }
- subscription = account.create_subscription!(
- stripe_customer_id: "cus_deleted",
- plan_key: "monthly_v1",
- status: "active"
- )
-
- Stripe::Customer.stubs(:update).raises(
- Stripe::InvalidRequestError.new("No such customer", {})
- )
-
- assert_nothing_raised do
- subscription.sync_customer_email_to_stripe
- end
- end
-end
diff --git a/saas/test/models/plan_test.rb b/saas/test/models/plan_test.rb
deleted file mode 100644
index 54ed279d6..000000000
--- a/saas/test/models/plan_test.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require "test_helper"
-
-class PlanTest < ActiveSupport::TestCase
- test "free plan is free" do
- assert Plan[:free_v1].free?
- end
-
- 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
diff --git a/saas/test/models/user/notifies_account_of_email_change_test.rb b/saas/test/models/user/notifies_account_of_email_change_test.rb
deleted file mode 100644
index 442a89fff..000000000
--- a/saas/test/models/user/notifies_account_of_email_change_test.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-require "test_helper"
-
-class User::NotifiesAccountOfEmailChangeTest < ActiveSupport::TestCase
- setup do
- @account = accounts(:"37s")
- @owner = @account.users.find_by(role: :owner) || @account.users.first.tap { |u| u.update!(role: :owner) }
- @member = @account.users.where.not(id: @owner.id).first || @account.users.create!(
- name: "Member",
- identity: Identity.create!(email_address: "member@example.com"),
- role: :member
- )
- end
-
- test "notifies account when owner changes email" do
- @account.expects(:owner_email_changed).once
-
- new_identity = Identity.create!(email_address: "new-owner@example.com")
- @owner.update!(identity: new_identity)
- end
-
- test "does not notify account when non-owner changes email" do
- @account.expects(:owner_email_changed).never
-
- new_identity = Identity.create!(email_address: "new-member@example.com")
- @member.update!(identity: new_identity)
- end
-
- test "does not notify account when owner is deactivated" do
- @account.expects(:owner_email_changed).never
-
- @owner.update!(identity: nil)
- end
-
- test "does not notify account when identity unchanged" do
- @account.expects(:owner_email_changed).never
-
- @owner.update!(name: "New Name")
- end
-
- test "notifies account when user becomes owner" do
- @account.expects(:owner_email_changed).once
-
- @member.update!(role: :owner)
- end
-
- test "does not notify account when owner becomes member" do
- @account.expects(:owner_email_changed).never
-
- @owner.update!(role: :member)
- end
-end