# Research: Plans, Entitlements & Billing Readiness

**Date**: 2026-04-27  
**Branch**: `247-plans-entitlements-billing-readiness`

## Decision 1: Persist workspace commercial truth in existing `workspace_settings`

- **Decision**: Store the first-slice workspace commercial truth through explicit `WorkspaceSetting` keys in an `entitlements` domain, reusing `SettingsRegistry`, `SettingsResolver`, and `SettingsWriter`.
- **Rationale**: The repo already has validated, audited workspace-scoped settings persistence and a singleton workspace settings page. Reusing that path keeps the slice narrow, keeps audit behavior consistent, and avoids inventing a billing or account persistence model.
- **Alternatives considered**:
  - New `plans`, `subscriptions`, or `customer_accounts` tables: rejected because the spec explicitly forbids broad billing/account scope.
  - One nested JSON blob for all entitlement fields: rejected because explicit keys better fit existing page save/reset patterns, validation, and audit attribution.

## Decision 2: Keep the plan-profile catalog code-owned and bounded

- **Decision**: Represent plan-profile defaults as a small code-owned catalog with one code-owned default profile and bounded named profile identifiers, not as operator-editable data.
- **Rationale**: The first slice needs deterministic defaults when no workspace-specific selection exists, but it does not need a management UI, a billing backoffice, or a pricing model. Code-owned defaults are the narrowest current-release truth.
- **Alternatives considered**:
  - Database-backed plan catalog: rejected because there is no current product workflow for editing plans.
  - External billing/provider sync: rejected because the spec explicitly excludes payment providers and subscription lifecycle work.

## Decision 3: Introduce one bounded `WorkspaceEntitlementResolver`

- **Decision**: Add one bounded resolver that projects effective entitlement decisions from plan defaults, workspace overrides, override rationale, and current usage.
- **Rationale**: Existing settings helpers resolve raw setting values but do not answer the operator question the feature actually needs: what is the effective value, where did it come from, why is it overridden, what is current usage, and may this action proceed now?
- **Alternatives considered**:
  - Rebuild the logic independently on `WorkspaceSettings`, `ManagedTenantOnboardingWizard`, and each review-pack entry surface: rejected because it would immediately create wording drift and inconsistent enforcement.
  - Extend `SettingsResolver` to absorb entitlement-specific usage logic: rejected because that would over-specialize a generic settings utility.

## Decision 4: Keep hard enforcement at the existing mutation and run-start boundaries

- **Decision**: Enforce onboarding entitlement in `ManagedTenantOnboardingWizard::canCompleteOnboarding()` and `completeOnboarding()`, and enforce review-pack entitlement inside `ReviewPackService::generate()` and `generateFromReview()`, while UI surfaces render the same decision state ahead of action execution.
- **Rationale**: Review-pack generation already fans out through several Filament actions, but those surfaces converge on `ReviewPackService`. Putting hard enforcement at the service boundary prevents bypass. Onboarding completion is already owned by the wizard page and should remain there.
- **Alternatives considered**:
  - UI-only disabling on each action surface: rejected because it would not protect direct Livewire action execution.
  - A second cross-cutting action framework for entitlement checks: rejected because the slice only needs one bounded business decision path, not a new platform hook system.

## Decision 5: Preserve explicit RBAC versus business-state semantics

- **Decision**: Keep 404 for non-members and wrong-plane actors, keep 403 for members missing capability, and model entitlement denial as a visible business-state block for otherwise authorized actors.
- **Rationale**: The repo constitution already distinguishes membership isolation from capability denial. Entitlements are neither. Treating entitlement denial as 403 or 404 would erase the operator-visible truth this slice exists to provide.
- **Alternatives considered**:
  - Hide blocked actions completely: rejected because the spec requires operator-visible rationale.
  - Return 403 for entitlement denial: rejected because it conflates product policy with authorization.

## Decision 6: Keep system visibility read-only on the existing workspace directory page

- **Decision**: Expose the resolved plan profile, entitlement values, source, and last-changed attribution on `App\Filament\System\Pages\Directory\ViewWorkspace` and its existing Blade view, with no system-plane mutation control.
- **Rationale**: Platform support needs visibility into current workspace commercial truth, but introducing a second mutation plane would immediately create duplicate truth and cross-plane drift.
- **Alternatives considered**:
  - New system resource or admin-like settings page: rejected because the first slice is explicitly read-only on `/system`.
  - Linking support users back to `/admin` without any local visibility: rejected because it keeps support dependent on plane switching and tribal knowledge.

## Decision 7: Keep review-pack shared OperationRun UX unchanged when entitled

- **Decision**: Preserve existing `OperationUxPresenter`, `OperationRunLinks`, dedupe behavior, and queued background generation semantics whenever review-pack generation is entitled.
- **Rationale**: The feature is about whether generation is allowed, not about rebuilding review-pack run UX. The right insertion point is before run creation, not inside the shared run lifecycle.
- **Alternatives considered**:
  - Localize new review-pack blocked/queued UX per surface: rejected because the repo already centralizes the run-start UX.
  - Add a new entitlement-specific notification family: rejected because blocked attempts should stop quietly with truthful local action messaging and no new run.

## Decision 8: Prove the slice with focused Sail/Pest unit and feature coverage only

- **Decision**: Cover the new resolver/profile defaults with unit tests and prove settings, onboarding, review-pack gating, and system visibility with focused feature tests run through Sail.
- **Rationale**: The business risk is decision correctness and action enforcement, not browser layout or broad workflow orchestration. Unit plus feature lanes are enough to prove the slice without dragging in heavy-governance or browser cost.
- **Alternatives considered**:
  - Browser tests: rejected because no browser-only interaction or layout risk is introduced.
  - Heavy-governance suite expansion: rejected because the scope is bounded and local to existing surfaces.

## Decision 9: Leave Filament panel registration, global search, and assets unchanged

- **Decision**: Do not add panels, providers, global-search resources, or new Filament asset registrations as part of this slice.
- **Rationale**: The feature is workspace-first entitlement truth inside existing admin and system surfaces. Filament infrastructure changes would widen scope without helping the first release.
- **Alternatives considered**:
  - New commercial panel or system sub-panel: rejected because the slice reuses current surfaces.
  - Asset-backed custom billing UI components: rejected because native Filament components and the existing system Blade page are sufficient.