Platform SaaS Plans

Platform SaaS Plans is the catalog Alex uses for what a tenant pays IVPrior (ADR-021 Stripe Billing). It is not customer checkout (ADR-040 Connect). Creating or deleting a plan here MUST NOT emit PlanChanged:v1 — that event is for a tenant’s contracted subscription.

Overview

Platform SaaS Plans list

Figure: Operations → Plans list with status, intervals, tenant count, and row actions.

What each field means

Status

  • Draft — saved, not on public pricing (GET /public/plans) or signup. The Publish toggle is off.
  • Published — the Publish toggle is on and the checklist passed. The plan appears on /pricing, S-105 #pricing, and signup.
  • Deleted (UI verb) — archive. The row leaves the default list. Existing Subscription.planId is unchanged.

Code

Generated from Name on create (LiteLITE). Shown in the list. Not on the wizard form and not editable later.

KPI / gate: Publish requires at least one interval flag on, a price > 0 and a pasted Stripe Price ID for every enabled interval, ≥1 English bullet and ≥1 Spanish bullet, and entitlements max_locations + max_users. If ai_assistant > 0, AI monthly quotas are required.

Interval flags (monthlyEnabled / yearlyEnabled)

These are explicit flags, not inferred from whether a Stripe ID is filled. A card shows a Monthly/Annual toggle only when both flags are on. Public savings = 12 × monthlyPrice − yearlyPrice when yearly is cheaper.

Bullets vs entitlements

  • Bullets — marketing copy on public cards. Each row is one bullet. The Languages column shows the same circular flags as the header language switcher (US = English, Spain = Spanish). Full color means that locale has text; a faded grey flag means the translation is missing. New bullet opens a modal that requires every language. Adding a locale later means one more field in that modal, not a second list.
  • Entitlements — enforced limits (FEAT-019). max_locations is a cap on Location count for the Tenant (not restaurants). Also max_users, ai_assistant (0/1), plus AI monthly quotas when assistant is on. Example: bullets can say “Unlimited locations” while max_locations is 5 — enforcement follows the entitlement.

Savings formula when both intervals are on and yearly is cheaper: 12 × monthlyPrice − yearlyPrice (percent = that amount ÷ 12 × monthly). Example: $49 monthly and $470 yearly → $118 ≈ 20%.

Featured

At most one Published plan is “Most popular”. The list shows that as a badge next to the name (there is no Popular column). Publishing another featured plan unsets the previous one without extra confirmation.

Order

The Order column sits after Tenants (before row actions). It is the sequence of public pricing cards (GET /public/plans sorts sortOrder ASC). Move a row with the up/down arrows (requires platform.plans.update). The wizard no longer has a Sort order field — a new plan is appended at the end.

Delete vs 409

Delete with zero subscriptions archives (204). If any tenant still has that planId, the API returns 409, the dialog explains that subscriptions block deletion, and the row stays. Bulk delete is all-or-nothing: if one selected plan has subscribers, none are archived. Trial and Enterprise are ordinary catalog rows in this screen — they are not a “system” type.

Permissions (platform.plans.*)

  • read — nav Operations → Plans, list, read-only form
  • create — Add plan, Duplicate
  • update — Save, Publish, Unpublish, Most popular
  • delete — Row Delete, bulk Delete selected

Without read, the nav item is hidden and the URL shows forbidden.

Create or edit a plan

  1. Open Operations → Plans.
  2. Click Add plan or ⋮ → Edit.
  3. Complete Identity · Billing · Marketing · Limits (clickable pills). Next is outline, never the primary green button.
  4. Identity has a Publish toggle — not a second “Self-serve” switch. Turning it off on a Published plan asks to unpublish (subscriptions stay).
  5. In Marketing, feature bullets are a table (search, export, pagination). New bullet opens a modal with every language required.
  6. Turning Publish on (or Finish with the toggle on for a new plan) runs the checklist. A 422 shows a banner plus invalid fields.

Test of the reader

If public pricing still showed Lite/Plus/Premium after you unpublished Plus, the catalog fetch is stale or Plus was not the published featured plan — Published selfServe rows are the source of truth, not a frontend enum.