# 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.