Remove payment/subscription system and card/storage limits
Fizzy is now free. Remove the entire Stripe billing system, subscription management, and card/storage limit enforcement. Removes from saas/: Plan model, Account::Billing, Account::Subscription, Account::Limited, Account::OverriddenLimits, Account::BillingWaiver, all subscription/billing controllers and views, Stripe webhook handler, card creation/publishing limit enforcement, admin account override UI, usage report rake task, and all related tests. Removes from main app: Fizzy.saas? guards for subscription panel, SaaS card footer override, near-limit notices, and saas.css stylesheet. Adds migration to drop billing tables from the SaaS database. Non-billing SaaS features (push notifications, signup, authorization, telemetry, console1984/audits1984) are preserved.
This commit is contained in:
@@ -5,12 +5,10 @@ git_source(:bc) { |repo| "https://github.com/basecamp/#{repo}" }
|
|||||||
|
|
||||||
gem "activeresource", require: "active_resource"
|
gem "activeresource", require: "active_resource"
|
||||||
gem "actionpack-xml_parser" # needed by queenbee for XML request body parsing
|
gem "actionpack-xml_parser" # needed by queenbee for XML request body parsing
|
||||||
gem "stripe", "~> 18.0"
|
|
||||||
gem "queenbee", bc: "queenbee-plugin"
|
gem "queenbee", bc: "queenbee-plugin"
|
||||||
gem "fizzy-saas", path: "saas"
|
gem "fizzy-saas", path: "saas"
|
||||||
gem "console1984", bc: "console1984"
|
gem "console1984", bc: "console1984"
|
||||||
gem "audits1984", bc: "audits1984", branch: "flavorjones/coworker-api"
|
gem "audits1984", bc: "audits1984", branch: "flavorjones/coworker-api"
|
||||||
gem "csv"
|
|
||||||
|
|
||||||
# Native push notifications (iOS/Android)
|
# Native push notifications (iOS/Android)
|
||||||
gem "action_push_native"
|
gem "action_push_native"
|
||||||
|
|||||||
@@ -281,7 +281,6 @@ GEM
|
|||||||
bigdecimal
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
csv (3.3.5)
|
|
||||||
date (3.5.1)
|
date (3.5.1)
|
||||||
debug (1.11.1)
|
debug (1.11.1)
|
||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
@@ -632,7 +631,6 @@ GEM
|
|||||||
stimulus-rails (1.3.4)
|
stimulus-rails (1.3.4)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
stringio (3.2.0)
|
stringio (3.2.0)
|
||||||
stripe (18.0.1)
|
|
||||||
thor (1.5.0)
|
thor (1.5.0)
|
||||||
thruster (0.1.18)
|
thruster (0.1.18)
|
||||||
thruster (0.1.18-aarch64-linux)
|
thruster (0.1.18-aarch64-linux)
|
||||||
@@ -721,7 +719,6 @@ DEPENDENCIES
|
|||||||
bundler-audit
|
bundler-audit
|
||||||
capybara
|
capybara
|
||||||
console1984!
|
console1984!
|
||||||
csv
|
|
||||||
debug
|
debug
|
||||||
faker
|
faker
|
||||||
fizzy-saas!
|
fizzy-saas!
|
||||||
@@ -757,7 +754,6 @@ DEPENDENCIES
|
|||||||
sqlite3 (>= 2.0)
|
sqlite3 (>= 2.0)
|
||||||
stackprof
|
stackprof
|
||||||
stimulus-rails
|
stimulus-rails
|
||||||
stripe (~> 18.0)
|
|
||||||
thruster
|
thruster
|
||||||
trilogy (~> 2.10)
|
trilogy (~> 2.10)
|
||||||
turbo-rails!
|
turbo-rails!
|
||||||
|
|||||||
@@ -21,5 +21,3 @@
|
|||||||
<%= render "account/settings/cancellation" %>
|
<%= render "account/settings/cancellation" %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= render "account/settings/subscription_panel" if Fizzy.saas? %>
|
|
||||||
|
|||||||
@@ -13,7 +13,5 @@
|
|||||||
<span>Create and add another</span>
|
<span>Create and add another</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%= render "cards/container/footer/saas/near_notice" if Fizzy.saas? %>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,5 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% if Fizzy.saas? %>
|
<%= render "cards/container/footer/create", card: card %>
|
||||||
<%= render "cards/container/footer/saas/create", card: card %>
|
|
||||||
<% else %>
|
|
||||||
<%= render "cards/container/footer/create", card: card %>
|
|
||||||
<% end %>
|
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
<%= render "layouts/theme_preference" %>
|
<%= render "layouts/theme_preference" %>
|
||||||
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||||
<%= stylesheet_link_tag "fizzy/saas", "data-turbo-track": "reload" if Fizzy.saas? %>
|
|
||||||
<%= javascript_importmap_tags %>
|
<%= javascript_importmap_tags %>
|
||||||
|
|
||||||
<%= tenanted_action_cable_meta_tag %>
|
<%= tenanted_action_cable_meta_tag %>
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
class Admin::AccountSearchesController < AdminController
|
|
||||||
def create
|
|
||||||
redirect_to saas.edit_admin_account_path(params[:q])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -6,10 +6,6 @@ class Admin::StatsController < AdminController
|
|||||||
@accounts_last_7_days = Account.where(created_at: 7.days.ago..).count
|
@accounts_last_7_days = Account.where(created_at: 7.days.ago..).count
|
||||||
@accounts_last_24_hours = Account.where(created_at: 24.hours.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_total = Identity.count
|
||||||
@identities_last_7_days = Identity.where(created_at: 7.days.ago..).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
|
@identities_last_24_hours = Identity.where(created_at: 24.hours.ago..).count
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
module Card::Limited
|
|
||||||
extend ActiveSupport::Concern
|
|
||||||
|
|
||||||
private
|
|
||||||
def ensure_under_limits
|
|
||||||
head :forbidden if Current.account.exceeding_limits?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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 <b>#{ format_currency(subscription.next_amount_due) }</b> on".html_safe
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def format_currency(amount)
|
|
||||||
number_to_currency(amount, precision: (amount % 1).zero? ? 0 : 2)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# To ease testing of limits
|
|
||||||
class Account::OverriddenLimits < SaasRecord
|
|
||||||
belongs_to :account
|
|
||||||
end
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<h3 class="margin-block-start txt-large font-weight-black txt-tight-lines">
|
|
||||||
<% if Current.account.exceeding_card_limit? && Current.account.exceeding_storage_limit? %>
|
|
||||||
You’ve used up your <u><%= number_with_delimiter(Plan.free.card_limit) %></u> free cards and all <u><%= storage_to_human_size(Plan.free.storage_limit) %></u> of free storage
|
|
||||||
<% elsif Current.account.exceeding_card_limit? %>
|
|
||||||
You’ve used up your <u><%= number_with_delimiter(Plan.free.card_limit) %></u> free cards
|
|
||||||
<% elsif Current.account.exceeding_storage_limit? %>
|
|
||||||
You’ve used up all <u><%= storage_to_human_size(Plan.free.storage_limit) %></u> of free storage
|
|
||||||
<% else %>
|
|
||||||
You’ve used <u><%= Current.account.billed_cards_count %></u> free cards out of <%= number_with_delimiter(Plan.free.card_limit) %>
|
|
||||||
<% end %>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p class="margin-block-start-half">To keep using Fizzy <%= "past #{ number_with_delimiter(Plan.free.card_limit) } cards," unless Current.account.exceeding_storage_limit? %> it’s only <strong><%= format_currency(Plan.paid.price) %>/month</strong> for <strong class="txt-nowrap">unlimited cards</strong>, <strong class="txt-nowrap">unlimited users</strong>, and <strong><%= storage_to_human_size(Plan.paid.storage_limit) %></strong> of storage.</p>
|
|
||||||
|
|
||||||
<%= button_to "Upgrade to #{Plan.paid.name} for #{ format_currency(Plan.paid.price) }/month", account_subscription_path, class: "btn settings-subscription__button txt-medium", form: { data: { turbo: false } } %>
|
|
||||||
|
|
||||||
<p>Cancel anytime, no contracts, take your data with you whenever.</p>
|
|
||||||
<p class="settings-subscription__footer txt-small margin-none-block-end">Right now you’re on the <strong><%= Current.account.plan.name %></strong> plan which includes <%= number_with_delimiter(Plan.free.card_limit) %> cards, unlimited users, and <%= storage_to_human_size(Plan.free.storage_limit) %> of storage.</p>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<h3 class="margin-block-start txt-x-large font-weight-black txt-tight-lines">
|
|
||||||
<% if Current.account.exceeding_storage_limit? %>
|
|
||||||
You’ve run out of storage
|
|
||||||
<% elsif Current.account.nearing_plan_storage_limit? %>
|
|
||||||
You’ve used <strong><%= storage_to_human_size(Current.account.billed_bytes_used) %></strong> of storage
|
|
||||||
<% else %>
|
|
||||||
Thank you for buying Fizzy
|
|
||||||
<% end %>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<% if Current.account.nearing_plan_storage_limit? || Current.account.exceeding_storage_limit? %>
|
|
||||||
<p class="margin-block-start-half">
|
|
||||||
The <strong><%= Current.account.plan.name %></strong> plan includes <strong><%= storage_to_human_size(Plan.paid.storage_limit) %></strong>. Upgrade to get <strong><%= storage_to_human_size(Plan.paid_with_extra_storage.storage_limit) %></strong> extra storage for <%= format_currency(Plan.paid_with_extra_storage.price - Plan.paid.price) %>/month more.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<%= 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 %>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
<% if subscription.current_period_end %>
|
|
||||||
<p class="margin-block-start-half"><%= subscription_period_end_action(subscription) %> <strong><%= subscription.current_period_end.to_date.to_fs(:long) %></strong></p>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<%= link_to "Manage your subscription", account_billing_portal_path, class: "btn btn--plain settings-subscription__link txt-link", data: { turbo_prefetch: false } %>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="settings-subscription__footer txt-small margin-block">
|
|
||||||
Right now you’re on the <strong><%= Current.account.plan.name %></strong> plan which includes unlimited cards, unlimited users, and <%= storage_to_human_size(Current.account.plan.storage_limit) %> of storage.
|
|
||||||
|
|
||||||
<% if subscription.plan == Plan.paid && !(Current.account.nearing_plan_storage_limit? || Current.account.exceeding_storage_limit?) %>
|
|
||||||
<%= button_to "Upgrade", account_subscription_upgrade_path, class: "btn btn--plain settings-subscription__link txt-link", form: { class: "flex-inline flex-wrap", data: { turbo: false } } %>
|
|
||||||
to <%= storage_to_human_size(Plan.paid_with_extra_storage.storage_limit) %> storage for <%= format_currency(Plan.paid_with_extra_storage.price - Plan.paid.price) %>/month more.
|
|
||||||
<% elsif subscription.plan == Plan.paid_with_extra_storage && !Current.account.exceeding_storage_limit? %>
|
|
||||||
<%= button_to "Downgrade", account_subscription_downgrade_path, class: "btn btn--plain settings-subscription__link txt-link", form: { class: "flex-inline flex-wrap", data: { turbo: false } } %>
|
|
||||||
to <%= storage_to_human_size(Plan.paid.storage_limit) %> storage.
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<% if Current.user.admin? && !Current.account.comped? %>
|
|
||||||
<section class="panel panel--wide settings-subscription__panel shadow center margin-block-double hide-on-native">
|
|
||||||
<h2 id="subscription" class="divider settings-subscription__divider txt-small txt-uppercase margin-none">Subscription</h2>
|
|
||||||
|
|
||||||
<% if Current.account.plan.free? %>
|
|
||||||
<%= render "account/settings/free_plan" %>
|
|
||||||
<% else %>
|
|
||||||
<%= render "account/settings/paid_plan" %>
|
|
||||||
<% end %>
|
|
||||||
</section>
|
|
||||||
<% end %>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<div class="settings-subscription__notch card-perma__notch card-perma__notch--bottom">
|
|
||||||
<% 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 %>
|
|
||||||
</div>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<strong>You’ve used your <u><%= Plan.free.card_limit %></u> free cards.</strong>
|
|
||||||
<%= link_to account_settings_path(anchor: "subscription"), class: "btn settings-subscription__button" do %>
|
|
||||||
<span>Upgrade to Unlimited</span>
|
|
||||||
<% end %>
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
<strong>You’ve run out of <%= Current.account.plan.free? ? "free storage" : "storage" %>.</strong>
|
|
||||||
<%= link_to account_settings_path(anchor: "subscription"), class: "btn settings-subscription__button" do %>
|
|
||||||
<span>Upgrade to get more</span>
|
|
||||||
<% end %>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<div class="pad-inline pad-block-half">
|
|
||||||
<strong>This account has used <u><%= Plan.free.card_limit %></u> free cards. Upgrade to get more.</strong>
|
|
||||||
</div>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<div class="pad-inline pad-block-half">
|
|
||||||
<strong>This account has run out of <%= Current.account.plan.free? ? "free storage" : "storage" %>. Upgrade to get more.</strong>
|
|
||||||
</div>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<% @page_title = "Thank you" %>
|
|
||||||
|
|
||||||
<% if @stripe_session&.payment_status == "paid" %>
|
|
||||||
<section class="panel panel--wide panel--centered center borderless">
|
|
||||||
<h1 class="txt-x-large font-weight-black margin-none">Thanks for buying Fizzy!</h1>
|
|
||||||
<p class="txt-medium margin-none-block-start">Your payment was successful. You’re now on the <strong><%= Current.account.plan.name %></strong> plan.</p>
|
|
||||||
<%= link_to "Done", account_settings_path, class: "btn settings-subscription__button txt-medium" %>
|
|
||||||
</section>
|
|
||||||
<% end %>
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
<% @page_title = "Account admin" %>
|
|
||||||
|
|
||||||
<% content_for :header do %>
|
|
||||||
<div class="header__actions header__actions--start">
|
|
||||||
<%= back_link_to "Accounts", saas.admin_accounts_path, "keydown.left@document->hotkey#click keydown.esc@document->hotkey#click" %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="panel panel--wide shadow center">
|
|
||||||
<div class="flex flex-column gap txt-medium">
|
|
||||||
<h1 class="txt-x-large font-weight-black margin-none">Edit Account <%= @account.external_account_id %></h1>
|
|
||||||
|
|
||||||
<%= form_with model: @account, url: saas.admin_account_path(@account.external_account_id), class: "flex flex-column gap" do |form| %>
|
|
||||||
<table class="txt-align-start">
|
|
||||||
<tr>
|
|
||||||
<td class="txt-subtle">Account Name:</td>
|
|
||||||
<td class="margin-inline-start-none"><%= @account.name %></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="txt-subtle">Subscription plan:</td>
|
|
||||||
<td class="margin-inline-start-none"><%= @account.plan.name %></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="txt-subtle">Actual card count:</td>
|
|
||||||
<td class="margin-inline-start-none"><%= @account.cards_count %> cards </td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="txt-subtle">Actual bytes used:</td>
|
|
||||||
<td class="margin-inline-start-none"><%= storage_to_human_size(@account.bytes_used) %></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<%= form.label :card_count, class: "txt-subtle" do %>
|
|
||||||
<span>Override card count</span><br><%= "(Exceeds plan limit)" if @account.exceeding_card_limit? %>
|
|
||||||
<% end %>
|
|
||||||
</td>
|
|
||||||
<td><%= form.number_field :card_count, value: @account.overridden_limits&.card_count, class: "input" %></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<%= form.label :bytes_used, class: "txt-subtle" do %>
|
|
||||||
<span>Override bytes used</span><br><%= "(Exceeds plan limit)" if @account.exceeding_storage_limit? %>
|
|
||||||
<% end %>
|
|
||||||
</td>
|
|
||||||
<td><%= form.number_field :bytes_used, value: @account.overridden_limits&.bytes_used, class: "input" %></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn--link full-width">Save changes</button>
|
|
||||||
<% 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 %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<div class="panel panel--centered">
|
|
||||||
<div class="flex flex-column gap txt-medium">
|
|
||||||
<h1 class="txt-x-large font-weight-black margin-none">Find an account</h1>
|
|
||||||
|
|
||||||
<%= 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 %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -33,32 +33,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-column gap-half">
|
|
||||||
<header>
|
|
||||||
<h2 class="divider txt-medium margin-block-start">Accounts Subscribed</h2>
|
|
||||||
</header>
|
|
||||||
<div class="flex gap-half">
|
|
||||||
<div class="flex flex-column gap-quarter flex-item-grow">
|
|
||||||
<div class="txt-x-small">Total</div>
|
|
||||||
<div class="txt-large">
|
|
||||||
<strong><%= @paid_accounts_total %></strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-column gap-quarter flex-item-grow">
|
|
||||||
<div class="txt-x-small">7 days</div>
|
|
||||||
<div class="txt-large">
|
|
||||||
<strong><%= @paid_accounts_last_7_days %></strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-column gap-quarter flex-item-grow">
|
|
||||||
<div class="txt-x-small">24 hours</div>
|
|
||||||
<div class="txt-large">
|
|
||||||
<strong><%= @paid_accounts_last_24_hours %></strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-column gap-half">
|
<div class="flex flex-column gap-half">
|
||||||
<header>
|
<header>
|
||||||
<h2 class="divider txt-medium margin-block-start">Identities Created</h2>
|
<h2 class="divider txt-medium margin-block-start">Identities Created</h2>
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
<% if Current.account.exceeding_limits? %>
|
|
||||||
<%= render "account/subscriptions/upgrade" %>
|
|
||||||
<% else %>
|
|
||||||
<%= render "cards/container/footer/create", card: card %>
|
|
||||||
<% end %>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<div class="settings_subscription__warning full-width pad-inline center margin-block-half">
|
|
||||||
<% 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 %>
|
|
||||||
</div>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
You’ve 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>.
|
|
||||||
-1
@@ -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" %>. <strong><%= link_to "Upgrade to get more", account_settings_path(anchor: "subscription") %></strong>.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
This account has used <%= Current.account.billed_cards_count %> out of <%= Plan.free.card_limit %> free cards. Upgrade soon.
|
|
||||||
-1
@@ -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.
|
|
||||||
@@ -8,10 +8,5 @@ Fizzy::Saas::Engine.routes.draw do
|
|||||||
namespace :admin do
|
namespace :admin do
|
||||||
mount Audits1984::Engine, at: "/console"
|
mount Audits1984::Engine, at: "/console"
|
||||||
get "stats", to: "stats#show"
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
||||||
+1
-33
@@ -10,39 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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
|
ActiveRecord::Schema[8.2].define(version: 2026_03_17_000000) do
|
||||||
create_table "account_billing_waivers", id: :uuid, charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
|
||||||
t.uuid "account_id", null: false
|
|
||||||
t.datetime "created_at", null: false
|
|
||||||
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
|
|
||||||
|
|
||||||
create_table "action_push_native_devices", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "action_push_native_devices", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.string "name"
|
t.string "name"
|
||||||
|
|||||||
@@ -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}"
|
|
||||||
@@ -16,10 +16,6 @@ module Fizzy
|
|||||||
connects_to database: { writing: :saas, reading: :saas }
|
connects_to database: { writing: :saas, reading: :saas }
|
||||||
end
|
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|
|
initializer "fizzy_saas.assets" do |app|
|
||||||
app.config.assets.paths << root.join("app/assets/stylesheets")
|
app.config.assets.paths << root.join("app/assets/stylesheets")
|
||||||
end
|
end
|
||||||
@@ -33,25 +29,6 @@ module Fizzy
|
|||||||
app.paths["config/push"].unshift(root.join("config/push.yml").to_s)
|
app.paths["config/push"].unshift(root.join("config/push.yml").to_s)
|
||||||
end
|
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|
|
initializer "fizzy.saas.mount" do |app|
|
||||||
app.routes.append do
|
app.routes.append do
|
||||||
mount Fizzy::Saas::Engine => "/", as: "saas"
|
mount Fizzy::Saas::Engine => "/", as: "saas"
|
||||||
@@ -98,10 +75,6 @@ module Fizzy
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
initializer "fizzy_saas.stripe" do
|
|
||||||
Stripe.api_key = ENV["STRIPE_SECRET_KEY"]
|
|
||||||
end
|
|
||||||
|
|
||||||
initializer "fizzy_saas.sentry" do
|
initializer "fizzy_saas.sentry" do
|
||||||
if !Rails.env.local? && ENV["SKIP_TELEMETRY"].blank?
|
if !Rails.env.local? && ENV["SKIP_TELEMETRY"].blank?
|
||||||
Sentry.init do |config|
|
Sentry.init do |config|
|
||||||
@@ -160,14 +133,10 @@ module Fizzy
|
|||||||
end
|
end
|
||||||
|
|
||||||
config.to_prepare do
|
config.to_prepare do
|
||||||
::Account.include Account::Billing, Account::Limited
|
|
||||||
::User.include User::NotifiesAccountOfEmailChange
|
|
||||||
::Identity.include Authorization::Identity, Identity::Devices
|
::Identity.include Authorization::Identity, Identity::Devices
|
||||||
::Session.include Session::Devices
|
::Session.include Session::Devices
|
||||||
::Signup.prepend Signup
|
::Signup.prepend Signup
|
||||||
ApplicationController.include Authorization::Controller
|
ApplicationController.include Authorization::Controller
|
||||||
CardsController.include(Card::LimitedCreation)
|
|
||||||
Cards::PublishesController.include(Card::LimitedPublishing)
|
|
||||||
|
|
||||||
Notification.register_push_target(:native)
|
Notification.register_push_target(:native)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
-11
@@ -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 %>
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user