# Data Model: Commercial Entitlements and Billing-State Maturity **Date**: 2026-04-28 **Branch**: `251-commercial-entitlements-billing-state` ## Overview This slice adds no new table. Persisted truth stays in existing `workspace_settings` rows, while the commercial lifecycle overlay and action-family outcomes remain derived. ## Persisted Truth ### 1. Workspace Commercial Lifecycle Setting Aggregate **Persistence**: Existing `App\Models\WorkspaceSetting` rows **Ownership**: Workspace-owned **Scope**: One workspace, no new tenant-owned or platform-owned persistence The slice reuses explicit settings keys under the existing `entitlements` domain. | Setting key | Type | Nullable | Validation | Notes | |-------------|------|----------|------------|-------| | `entitlements.commercial_lifecycle_state` | string | yes | when present, must be one of `trial`, `grace`, `active_paid`, `suspended_read_only` | `null` means the workspace has never been explicitly set and resolves to the implicit default `active_paid` | | `entitlements.commercial_lifecycle_reason` | string | yes | required on every explicit lifecycle state change; trimmed; max 500 chars | Operator-entered rationale shown on system and contextual admin surfaces | **Write rules**: - Lifecycle mutation happens from the system plane only and updates state plus rationale together through the existing workspace settings write/audit path. - The future `Change commercial state` action is confirmation-protected and requires explicit rationale for every explicit lifecycle transition, including an explicit return to `active_paid`. - Once a platform operator explicitly sets `active_paid`, that remains a stored state like the other three values. `null` is reserved for untouched workspaces only. **Relationships**: - `workspace_settings.workspace_id` anchors lifecycle truth to the workspace. - `workspace_settings.updated_by_user_id` remains the attribution source for state change metadata. ## Existing Substrate Truth Reused ### 2. Workspace Entitlement Substrate Summary **Persistence**: Existing Spec 247 workspace entitlement settings + code-owned plan-profile catalog **Owner**: `WorkspaceEntitlementResolver` This slice does not remodel the substrate. It reuses: - `plan_profile` - `managed_tenant_activation_limit` - `review_pack_generation_enabled` - substrate rationale/source/current-usage metadata The lifecycle overlay may warn or restrict after substrate resolution, but it must never expand access beyond what the substrate already allows. ## Code-Owned Truth ### 3. Commercial Lifecycle State Catalog Entry **Persistence**: none, code-owned **Ownership**: Product/runtime configuration **Scope**: current release only | Field | Type | Required | Notes | |-------|------|----------|-------| | `id` | string | yes | Stable internal identifier stored in `entitlements.commercial_lifecycle_state` | | `label` | string | yes | Operator-facing state label | | `description` | string | yes | Short explanation for system detail and contextual messaging | | `onboarding_outcome` | string | yes | `allow` or `block` | | `review_pack_start_outcome` | string | yes | `allow`, `warn`, or `block` | | `preserves_read_only_history` | bool | yes | Whether existing review/evidence/generated-pack consumption remains explicitly preserved | | `is_default` | bool | yes | Exactly one default entry: `active_paid` | **Behavior matrix**: | State | Onboarding activation | Review-pack starts | Existing review/evidence/download access | |-------|-----------------------|--------------------|------------------------------------------| | `trial` | allow | allow | allow | | `active_paid` | allow | allow | allow | | `grace` | block | warn (start still allowed) | allow | | `suspended_read_only` | block | block | allow | ## Derived Truth ### 4. Effective Commercial Lifecycle Decision **Persistence**: none, derived at runtime **Owner**: bounded `WorkspaceCommercialLifecycleResolver` | Field | Type | Required | Notes | |-------|------|----------|-------| | `workspace_id` | int | yes | Workspace being evaluated | | `state` | string | yes | Effective lifecycle state | | `label` | string | yes | Operator-facing label | | `source` | string | yes | `default_active_paid` or `workspace_setting`; any rendered source label must come from one shared mapping | | `rationale` | string | no | Explicit operator rationale when source is `workspace_setting` | | `last_changed_at` | datetime | no | Derived from the most recent lifecycle-related `WorkspaceSetting` row | | `last_changed_by` | string | no | Derived actor attribution | | `entitlement_summary` | object | yes | Existing Spec 247 substrate summary reused for support/context | | `action_decisions` | object | yes | Per-action-family outcomes described below | ### 5. Commercial Lifecycle Action Decision **Persistence**: none, derived at runtime | Field | Type | Required | Notes | |-------|------|----------|-------| | `action_key` | string | yes | One of `managed_tenant_activation`, `review_pack_start`, `review_history_read`, `evidence_read`, `generated_pack_read` | | `outcome` | string | yes | `allow`, `warn`, `block`, or `allow_read_only` | | `reason_family` | string | no | `commercial_lifecycle`, `entitlement_substrate`, or `null` when fully allowed | | `message` | string | no | Operator-safe explanation or warning | | `lifecycle_state` | string | yes | Effective state that produced the action decision | | `underlying_entitlement_key` | string | no | Present for onboarding/review-pack start decisions to preserve substrate traceability | **Decision ordering rules**: - The substrate entitlement decision runs first. - If the substrate already blocks the action, the lifecycle overlay must not replace that reason. - If the substrate allows the action, the lifecycle overlay may warn or block according to the state matrix. - Authorization is not part of this derived decision; 404 and 403 semantics remain outside and happen earlier. ## Supporting Derived View Models ### 6. System Workspace Commercial Lifecycle View Model **Persistence**: none **Consumer**: `App\Filament\System\Pages\Directory\ViewWorkspace` Contains: - effective lifecycle state, label, rationale, and last-change attribution - the two in-scope action-family outcomes - the reused entitlement substrate summary for support context - the one dominant mutation affordance metadata for `Change commercial state` ### 7. Contextual Admin Lifecycle Gate View Models **Persistence**: none **Consumers**: `ManagedTenantOnboardingWizard`, review-pack entry surfaces, and suspended read-only history surfaces Contains: - the immediate action-family outcome (`allow`, `warn`, `block`, or `allow_read_only`) - one operator-safe explanation - enough substrate context to keep lifecycle blocks distinct from underlying entitlement blocks ## Derived Query Dependencies | Need | Source | Notes | |------|--------|-------| | Underlying plan-profile and entitlement truth | `WorkspaceEntitlementResolver` | Remains the canonical substrate | | Lifecycle last-change attribution | existing `workspace_settings.updated_by_user_id` and timestamps | Derived from lifecycle-related rows only | | Active managed-tenant usage | existing tenant/workspace runtime truth | Reused from the substrate summary | | Existing review/history/evidence/download availability | existing review pack, review, evidence snapshot, and RBAC truth | No new persistence needed | | Review-pack no-run proof | existing `review_packs` and `operation_runs` tables | Used only in tests to prove blocked starts do not write new run state | ## State Transitions There is no new table-backed lifecycle entity. State changes are explicit workspace-setting transitions plus audit entries. | From | To | Trigger | Consequence | |------|----|---------|-------------| | `null` (implicit default) | any explicit state | platform operator saves lifecycle state on the system detail page | workspace now has explicit commercial posture, rationale, and attribution | | `trial` | `grace` | platform operator state change | new managed-tenant activation blocks; review-pack starts remain allowed with warning | | `grace` | `suspended_read_only` | platform operator state change | onboarding and new review-pack starts block; history/evidence/download remain available | | `suspended_read_only` | `active_paid` | platform operator state change | future starts again defer to underlying entitlement truth | | any explicit state | another explicit state | platform operator state change | previous state is replaced; audit history preserves the transition trail | ## Boundaries Explicitly Preserved - No new billing/customer/subscription entity exists. - No new automated timers, expiry jobs, renewal reminders, or scheduled transitions are introduced. - No new broad suspension contract is added for unrelated mutable surfaces. - Existing read-only review/evidence/generated-pack access remains governed by current RBAC and redaction rules.