TenantAtlas/specs/251-commercial-entitlements-billing-state/data-model.md
ahmido 7ee4909212
Some checks failed
Main Confidence / confidence (push) Failing after 1m45s
feat: commercial lifecycle overlay for workspace entitlements (#292)
## Summary
- add the bounded workspace commercial lifecycle overlay from spec 251 on top of the existing entitlement substrate
- expose audited commercial state inspection and mutation on the system workspace detail surface
- gate onboarding activation and review-pack start actions through the shared lifecycle decision while preserving suspended read-only access to existing review, evidence, and generated-pack history
- add focused Pest coverage plus the spec/plan/tasks/data-model/contract artifacts for the feature

## Validation
- targeted Pest unit and feature lanes for lifecycle resolution, system-plane mutation, onboarding gating, review-pack enforcement, download preservation, customer review workspace access, and evidence snapshot access
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- integrated browser smoke on the system workspace detail and the preserved read-only review/evidence/review-pack surfaces

## Notes
- branch: `251-commercial-entitlements-billing-state`
- base: `dev`
- commit: `606e9760`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #292
2026-04-28 13:39:33 +00:00

170 lines
8.8 KiB
Markdown

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