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:
@@ -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