Skip to content
This repository was archived by the owner on Dec 16, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
069a72a
Add Stripe-based billing system
jorgemanrubia Dec 10, 2025
3804161
Refresh the subscription instead of relying on event payloads
jorgemanrubia Dec 12, 2025
4641d88
Let's use a single sandbox environment via 1password ENV vars
jorgemanrubia Dec 12, 2025
a7769db
Add script to set stripe env vars from 1password
jorgemanrubia Dec 12, 2025
6c164fe
Dont't fail if not set
jorgemanrubia Dec 12, 2025
94c7ed6
Kill previous tunnel automatically
jorgemanrubia Dec 12, 2025
6e45e61
Document how to work with Stripe locally
jorgemanrubia Dec 12, 2025
78f5a02
Add stripe envs
jorgemanrubia Dec 12, 2025
845f9db
Fix typo with unlimitted
jorgemanrubia Dec 12, 2025
70da6b2
Fix typo
jorgemanrubia Dec 12, 2025
7df2906
Paid plans never exceed limits!
jorgemanrubia Dec 12, 2025
56bedaf
Spelling
jzimdars Dec 12, 2025
09cf2ca
Fancy apostrophes
jzimdars Dec 12, 2025
64f6866
Typo
jzimdars Dec 12, 2025
d4fe376
Layout
jzimdars Dec 12, 2025
7fd0d33
More fancy apostrophes
jzimdars Dec 12, 2025
9721f29
Show different things to non-admins
jzimdars Dec 12, 2025
97e09ea
Give this some padding
jzimdars Dec 12, 2025
c253d44
Don't prevent creating drafts, or you won't get to see the upgrade ba…
jorgemanrubia Dec 15, 2025
0f3d1da
Remove redundant condition
jorgemanrubia Dec 15, 2025
e30538c
Enable automatic taxes and calculate based on mandatory billing address
jorgemanrubia Dec 15, 2025
19cbcf4
Add record to track overridden limits
jorgemanrubia Dec 15, 2025
8106f43
Add system to comp accounts
jorgemanrubia Dec 15, 2025
24fd5b9
Grab and show the next amount to pay from Stripe
jorgemanrubia Dec 15, 2025
1062777
Enable tax id collection
jorgemanrubia Dec 15, 2025
4f7650b
This is needed for vat id collection
jorgemanrubia Dec 15, 2025
3f96baf
Rename param to remove redundant bit
jorgemanrubia Dec 16, 2025
54d5ff8
Prefer if/else over early return
jorgemanrubia Dec 16, 2025
640a09b
More concise
jorgemanrubia Dec 16, 2025
febd9ca
Make active_subscription a private method
jorgemanrubia Dec 16, 2025
6247c90
Extract method
jorgemanrubia Dec 16, 2025
b20ca22
Rename to stripe_session to avoid confusions
jorgemanrubia Dec 16, 2025
6b7fea7
Format
jorgemanrubia Dec 16, 2025
bbfb8f5
Move presentation method to a regular view helper
jorgemanrubia Dec 16, 2025
047b4bc
Remove nested if
jorgemanrubia Dec 16, 2025
3a2d2ad
Format
jorgemanrubia Dec 16, 2025
File filter

Filter by extension

Filter by extension


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

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

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

GITHUB_TOKEN=$(gh config get -h github.com oauth_token)
BASECAMP_REGISTRY_PASSWORD=$(kamal secrets extract BASECAMP_REGISTRY_PASSWORD $SECRETS)
Expand All @@ -21,3 +21,6 @@ SENTRY_DSN=$(kamal secrets extract SENTRY_DSN $SECRETS)
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=$(kamal secrets extract ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY $SECRETS)
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=$(kamal secrets extract ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY $SECRETS)
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=$(kamal secrets extract ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT $SECRETS)
STRIPE_MONTHLY_V1_PRICE_ID=$(kamal secrets extract STRIPE_MONTHLY_V1_PRICE_ID $SECRETS)
STRIPE_SECRET_KEY=$(kamal secrets extract STRIPE_SECRET_KEY $SECRETS)
STRIPE_WEBHOOK_SECRET=$(kamal secrets extract STRIPE_WEBHOOK_SECRET $SECRETS)
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,28 @@ After making changes to this gem, you need to update Fizzy to pick up the change
BUNDLE_GEMFILE=Gemfile.saas bundle update --conservative fizzy-saas
```

## Working with Stripe

The first time, you need to:

1. Install Stripe CLI: https://stripe.com/docs/stripe-cli
2. Run `stripe login` and authorize the environment `37signals Development`

Then, for working on the Stripe integration locally, you need to run this script to start the tunneling and set the environment variables:

```sh
eval "$(BUNDLE_GEMFILE=Gemfile.saas bundle exec stripe-dev)"
bin/dev # You need to start the dev server in the same terminal session
```

This will ask for your 1password authorization to read and set the environment variables that Stripe needs.

### Stripe environments

* [Development](https://dashboard.stripe.com/acct_1SdTFtRus34tgjsJ/test/dashboard)
* [Staging](https://dashboard.stripe.com/acct_1SdTbuRvb8txnPBR/test/dashboard)
* [Production](https://dashboard.stripe.com/acct_1SNy97RwChFE4it8/dashboard)

## Environments

Fizzy is deployed with [Kamal](https://kamal-deploy.org/). You'll need to have the 1Password CLI set up in order to access the secrets that are used when deploying. Provided you have that, it should be as simple as `bin/kamal deploy` to the correct environment.
Expand Down
64 changes: 64 additions & 0 deletions app/assets/stylesheets/fizzy/saas.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* 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 {
content: "————";
display: block;
margin: auto;
text-align: center;
}
}

.settings-subscription__notch {
animation: wiggle 500ms ease;
background: var(--settings-subscription-background);
box-shadow: 0 0 0.3em 0.2em var(--settings-subscription-color);
border-radius: 3em;
color: var(--settings-subscription-text-color);
margin-inline: auto;
padding: 0 0.3em 0 1.2em;
transform: rotate(-1deg);

@media (max-width: 640px) {
padding-block: 0.6em;
padding-inline-start: 0.3em;
}

.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);
}
}
}
15 changes: 0 additions & 15 deletions app/assets/stylesheets/fizzy/saas/application.css

This file was deleted.

19 changes: 19 additions & 0 deletions app/controllers/account/billing_portals_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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
42 changes: 42 additions & 0 deletions app/controllers/account/subscriptions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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.paid.stripe_price_id, quantity: 1 } ],
success_url: account_subscription_url + "?session_id={CHECKOUT_SESSION_ID}",
cancel_url: account_subscription_url,
metadata: { account_id: Current.account.id, plan_key: Plan.paid.key },
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 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.paid.key, status: "incomplete")
end
end
end
5 changes: 5 additions & 0 deletions app/controllers/admin/account_searches_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Admin::AccountSearchesController < AdminController
def create
redirect_to saas.edit_admin_account_path(params[:q])
end
end
27 changes: 27 additions & 0 deletions app/controllers/admin/accounts_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class Admin::AccountsController < AdminController
include Admin::AccountScoped

layout "public"

before_action :set_account, only: %i[ edit update ]

def index
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a simple admin screen where you can modify the card count in a given account. It is intended mostly to test this in staging.

end

def edit
end

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

private
def set_account
@account = Account.find_by!(external_account_id: params[:id])
end

def overridden_card_count_param
params[:account][:overridden_card_count].to_i
end
end
13 changes: 13 additions & 0 deletions app/controllers/admin/billing_waivers_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
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
8 changes: 8 additions & 0 deletions app/controllers/admin/overridden_limits_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class Admin::OverriddenLimitsController < AdminController
include Admin::AccountScoped

def destroy
@account.reset_overridden_limits
redirect_to saas.edit_admin_account_path(@account.external_account_id), notice: "Limits reset"
end
end
20 changes: 20 additions & 0 deletions app/controllers/admin/stats_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class Admin::StatsController < AdminController
layout "public"

def show
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to the gem, but unrelated to the billing system.

@accounts_total = Account.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

@identities_total = Identity.count
@identities_last_7_days = Identity.where(created_at: 7.days.ago..).count
@identities_last_24_hours = Identity.where(created_at: 24.hours.ago..).count

@top_accounts = Account
.where("cards_count > 0")
.order(cards_count: :desc)
.limit(20)

@recent_accounts = Account.order(created_at: :desc).limit(10)
end
end
12 changes: 12 additions & 0 deletions app/controllers/concerns/admin/account_scoped.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Admin::AccountScoped
extend ActiveSupport::Concern

included do
before_action :set_account
end

private
def set_account
@account = Account.find_by!(external_account_id: params[:account_id] || params[:id])
end
end
14 changes: 14 additions & 0 deletions app/controllers/concerns/card/limited_creation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Card::LimitedCreation
extend ActiveSupport::Concern

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

private
def ensure_can_create_cards
head :forbidden if Current.account.exceeding_card_limit?
end
end
12 changes: 12 additions & 0 deletions app/controllers/concerns/card/limited_publishing.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Card::LimitedPublishing
extend ActiveSupport::Concern

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

private
def ensure_can_publish_cards
head :forbidden if Current.account.exceeding_card_limit?
end
end
Loading