## 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
8.8 KiB
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 stateaction is confirmation-protected and requires explicit rationale for every explicit lifecycle transition, including an explicit return toactive_paid. - Once a platform operator explicitly sets
active_paid, that remains a stored state like the other three values.nullis reserved for untouched workspaces only.
Relationships:
workspace_settings.workspace_idanchors lifecycle truth to the workspace.workspace_settings.updated_by_user_idremains 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_profilemanaged_tenant_activation_limitreview_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, orallow_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.