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