Skip to content

feat(billing): give Business a 3-seat floor and its own annual variant - #21

Merged
kipavy merged 2 commits into
mainfrom
feat/business-pricing
Sep 12, 2026
Merged

kipavy merged 2 commits into
mainfrom
feat/business-pricing

Conversation

@kipavy

@kipavy kipavy commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Pricing decisions for Business, implemented.

3-seat floor on every per-seat plan

Business had no seat minimum, so one seat at $30/month undercut the cheapest Teams plan — three seats at $54 — while including everything Teams has plus custom roles and the commercial licence. PER_SEAT_PLANS now covers both plans and they share MIN_SEATS = 3:

Teams    floor  3 seats   $54/mo   $45/mo annual
Business floor  3 seats   $90/mo   $75/mo annual

Monotonic: every Business plan costs more than every Teams plan. The owner occupies a seat (owner_seats_used counts team_members, which includes them), so three seats means owner plus two.

Pro stays single-seat and sends no quantity at all.

Business gets its own annual variant

It previously resolved LS_VARIANT_BUSINESS_MONTHLY for both intervals. It now has LS_VARIANT_BUSINESS_YEARLY, at $25/user/month against $30 monthly — the same 17% Teams carries. Pro discounts 22% annually and Teams 17%, so a flat Business price was the outlier.

Testing

cargo test billing    21 passed, 0 failed

business_resolves_both_intervals_once_configured replaces the earlier monthly-only assertion, and every_per_seat_plan_shares_the_three_seat_floor pins the rule that stops Business undercutting Teams.

Deployment

Neither variant exists in LemonSqueezy yet, so Business stays unsellable regardless of this PR — variant_id_for returns None and the handler answers 503. Going live needs both variables set and a redeploy.

Decided alongside this, needing no code: no free trial on the Business variants. Its headline value is the commercial licence, and a free trial of a licence grants AGPL relief for nothing; the functionality is already demonstrable through a Teams trial, and pilots for this tier are better comped per-deal via admin_override. Relevant too: require_business_tier reads users.subscription_tier directly rather than going through effective_tier_for_user, so an expired Business trial would retain custom-role access until the next daily sweep — up to 24 hours. Worth fixing on its own, but it only becomes reachable if Business trials exist.

Business had no seat minimum, so one seat at $30 undercut the cheapest Teams
plan at three seats for $54 while including everything Teams has. Both per-seat
plans now share a 3-seat floor, which keeps the ladder monotonic: Teams starts
at $54/month, Business at $90. The owner occupies a seat, so three seats means
the owner plus two.

Business also resolved a single monthly variant for both intervals. It now has
LS_VARIANT_BUSINESS_YEARLY of its own, priced at $25/user/month against $30
monthly — the same 17% annual discount Teams carries. Pro and Teams already
discounted annually and Business did not, which was the odd one out.

Neither variant exists in LemonSqueezy yet, so the plan stays unsellable until
both environment variables are set.
compose.prod.yml enumerates every environment variable explicitly, so adding
LS_VARIANT_BUSINESS_MONTHLY and _YEARLY to .env.dockhand alone would not reach
the server: variant_id_for would read an unset variable, return None, and the
checkout handler would answer 503 for a plan the site was advertising.
@kipavy
kipavy merged commit 469337c into main Sep 12, 2026
2 checks passed
@kipavy
kipavy deleted the feat/business-pricing branch September 12, 2026 22:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant