--- audience: operators --- # Plan Management This document describes how operators configure plan ladders, manage automatic provisioning, perform manual transitions, and audit enrollment history. > Developers: see [`plan-architecture.md`](./plan-architecture.md) for where plan code lives and the sole-writer rule. ## Overview A **plan ladder** is a ordered set of plan products. Each organization is **enrolled** on at most one tier per ladder at a time. Transitions between tiers — upgrades, downgrades, initiations, and ends — are recorded in the **transition history** for audit. ## When to use a plan vs. a regular product Plans are an opt-in modeling choice for products that follow **tier semantics**: an organization should be on exactly one tier of a ladder, and moving between tiers means leaving the previous one. If that's not what you want, use a regular product instead. Use a **plan** when: - The product belongs to a graded set (e.g., Free → Standard → Pro) and an organization should hold exactly one at a time. - You need an audit trail of transitions between tiers (who moved the org, when, why). - Stripe subscription state (created/upgraded/downgraded/cancelled) should drive the org's tier directly. Use a **regular product** (addon, usage, one-time) when: - You want to grant the same thing multiple times (multiple gift cycles, stacked credits, parallel benefits). - The product is independent of any tier — it adds to an org's entitlements rather than replacing them. - You want simple grant/revoke semantics without ladder transitions or supersession. If you find yourself wanting to grant the same plan twice to the same org, consider whether you need an **extend** (same tier, new grant) or whether the product should have been modeled as an addon. Plans enforce one-tier-per-ladder by design; addons and usage products do not. The **Extend Current Tier** action exists precisely for the legitimate case of issuing another grant on the same tier. > Developers: this distinction is enforced at the catalog layer (membership in `plan_ladder_tiers` is opt-in per product). See `plan-architecture.md` for the runtime consequence. ## Ladder Configuration ### Creating a Ladder 1. Open the **Operator** panel and select the **Plan Ladders** tab. 2. Enter a unique **slug** (kebab-case identifier), a **display name**, and an optional description. 3. Click **Create Ladder**. ### Attaching Tiers 1. From the ladder list, click **Tiers** on the ladder you want to configure. 2. Select a **published plan product** (products with `product_type = NULL`) and specify its **rank**. 3. Click **Add Tier**. Tiers within a ladder are ordered by rank. Lower ranks are "lower" tiers; higher ranks are "higher" tiers. The typical pattern is: - Rank 0 — Public / Free tier - Rank 1 — Standard tier - Rank 2 — Pro tier ### Reordering Tiers Tiers can be reordered by changing their rank values. The UI enforces unique ranks within a ladder. ### Removing Tiers A tier can be removed only when **no active pool attachments** exist for that tier. The UI shows the active attachment count per tier. ### Deleting a Ladder A ladder can be deleted only when it has **no active attachments** across all its tiers. Deleting a ladder also removes all tier memberships. ## Auto-Provisioning Policy ### Per-Organization-Type Default Plan Each organization type can have a **default plan** configured by pointing at a `billing.plan_ladders` row. The default tier is always the rank-0 tier of that ladder, resolved at use-time. When a new organization of that type is created, the rank-0 product of the configured ladder is automatically granted to the organization's default pool. The dropdown in the Operator panel sources from plan ladders only — every option renders as `LadderName — rank-0 product name` so the resolved default is visible inline. Products that are not the rank-0 tier of any ladder cannot be set as defaults; the Plan Ladders tab is the single source of truth for which products are eligible. > **Applies to new organizations only.** Changing the default plan for an org type does **not** affect existing organizations. ### Backfilling Existing Organizations To apply the current default plan to existing organizations: 1. Open the **Operator** panel and select the **Org Types** tab. 2. Find the org type with a configured default plan. 3. Click **Backfill existing orgs**. 4. Confirm the action. The system iterates all organizations of that type, locks each pool row, and invokes `ReapplyDefaultsForPool`. The result reports: - **N updated** — pools that received a new default grant - **M skipped** — pools that were already at the target tier (idempotent) - **K failed** — pools whose transition errored (per-org reasons listed below the summary) Backfill is rejected if the org type has **no default plan** configured. ## Manual Grant Workflows ### Issue Grant An operator can issue a grant to move a pool to a specific tier: 1. Open the **Operator** panel and select the **Enrollment** tab (or click **Enrollment** from the Organizations list). 2. Select the target organization. 3. In the **Issue Grant** card, choose a **plan product**, optionally set a **valid until** timestamp, and provide a **reason**. 4. Click **Issue Grant**. The system: - Creates a grant with `grant_reason = 'manual'` (or `'trial'` if `valid_until` is set) - Invokes `Transition` to attach the pool to the target tier - Registers a **Temporal workflow** to expire the grant at `valid_until` **only** for trials A grant with no `valid_until` is permanent. A grant with `valid_until` is a time-bounded trial; when it expires, the pool automatically transitions back to the organization's default tier (or detaches if no default is configured). ### Grant Revocation An operator can revoke an active grant: 1. On the enrollment page, locate the grant in the **Active Grants** table. 2. Click **Revoke**. 3. Confirm the action. The system revokes the grant, ends the associated provision, and invokes `Transition(End)` to return the pool to its default tier. ### Extend Current Tier An operator can issue a new grant on the pool's current tier without changing position: 1. On the enrollment page, locate the **Extend current tier** card (visible only when the pool has an active plan attachment). 2. Provide a **reason** (e.g., "extending trial", "comp cycle"). 3. Optionally set **Expires at** — when present, the new grant has a `valid_until` and a Temporal expiration workflow is scheduled. When empty, the extension is open-ended (comp/gift case). 4. Click **Extend tier**. The system: - Creates a new grant chained to the prior grant via `extends_grant_id` - Invokes `Transition` with `Extend=true` to create a new provision + ladder attachment at the same tier - Records a `transition_type='extend'` audit row with `from_rank == to_rank` - Schedules `GrantExpirationWorkflow` keyed off the new grant's ID when `Expires at` is set, so trial extensions expire on their own clock and don't ride the original trial's expiry Use this to extend a trial that is about to expire (set a new `Expires at`), comp an additional cycle during an outage, or gift a second period without disturbing the existing audit trail. ## Enrollment Audit ### Reading Transition History The enrollment page displays a chronological list of all `pool_provision_transitions` rows for each pool. Each row shows: - **Type** — `initiate`, `upgrade`, `downgrade`, `end`, or `extend` - **From / To rank** — the ladder position delta (`extend` shows equal ranks) - **Actor** — `system`, `webhook` (Stripe), or the operator's display name - **Reason** — free-form text or system-generated attribution - **Effective at** — when the change took effect ### Canonicalization vs. `subscription_changes` `pool_provision_transitions` is the **canonical source** for plan-position history: what tier a pool held, when, and under whose authority. `billing.subscription_changes` records commercial mutations of the subscription (status changes, amount changes, etc.). A Stripe-driven upgrade is recorded in **both** tables because they answer different questions. ### Corrections Transition history is **read-only**. Errors or clarifications should be expressed by performing a new transition with an explanatory reason, not by editing historical rows.