Automated PR: commit all local changes and add feature 274-billing-subscription-truth. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #326
7.9 KiB
Data Model: Billing & Subscription Truth Layer v1
Date: 2026-05-04
Branch: 274-billing-subscription-truth
Overview
This slice adds one new workspace-owned source of truth: a current subscription record. Existing plan profiles, entitlement overrides, and manual commercial lifecycle fallback remain in their current storage. The new record feeds the existing commercial lifecycle resolver when present.
Persisted Truth
1. Workspace Subscription Aggregate
Persistence: New workspace_subscriptions table
Ownership: Workspace-owned
Scope: One current record per workspace
| Field | Type | Nullable | Validation | Notes |
|---|---|---|---|---|
id |
bigint | no | primary key | Internal record id |
workspace_id |
bigint | no | foreign key, unique | Enforces one current subscription record per workspace |
state |
string | no | must be one of trial, active, past_due, cancel_at_period_end, ended |
Current subscription posture |
billing_reference |
string | yes | trimmed, max 191 chars | Optional contract, subscription, or invoice reference label |
trial_ends_at |
datetime | yes | required when state=trial |
Current trial end date |
current_period_starts_at |
datetime | yes | required when state is active, past_due, or cancel_at_period_end |
Current commercial period start |
current_period_ends_at |
datetime | yes | required when state is active, past_due, cancel_at_period_end, or ended |
Current commercial period end or ended-on boundary |
status_reason |
text | no | required on every explicit mutation path | Operator-visible explanation |
created_at |
datetime | no | standard timestamps | Creation time |
updated_at |
datetime | no | standard timestamps | Latest mutation time |
Write rules:
- Mutation happens from the system plane only.
workspace_idis immutable once the row exists.- The record is updated in place in v1; no historical row chain is created.
- Audit history captures before and after values and actor attribution.
Relationships:
workspace_subscriptions.workspace_idreferencesworkspaces.id.Workspacegains a singular subscription relationship.
Existing Persisted Truth Reused
2. Workspace Entitlement Substrate
Persistence: Existing workspace_settings rows plus code-owned plan catalog
Owner: WorkspaceEntitlementResolver
This slice does not remodel:
- plan profile selection
- first-slice entitlement overrides
- first-slice entitlement usage summaries
These remain the substrate that lifecycle may restrict after subscription mapping.
3. Manual Lifecycle Fallback
Persistence: Existing workspace_settings rows from Spec 251
Owner: WorkspaceCommercialLifecycleResolver
Manual lifecycle state remains valid only as fallback when a workspace does not yet have a current subscription record.
Code-Owned Truth
4. Subscription State Catalog Entry
Persistence: none, code-owned
Ownership: product runtime configuration
| Field | Type | Required | Notes |
|---|---|---|---|
id |
string | yes | Stable internal identifier |
label |
string | yes | Operator-facing label |
description |
string | yes | Short explanation for system and settings summaries |
derived_lifecycle_state |
string | yes | One of the existing Spec 251 lifecycle states |
needs_review_when_past_date |
bool | yes | Whether the record should surface explicit review-required wording when its key date is in the past |
Behavior matrix:
| Subscription state | Derived lifecycle state | Key date surfaced | Notes |
|---|---|---|---|
trial |
trial |
trial_ends_at |
Current trial posture |
active |
active_paid |
current_period_ends_at |
Current paid period |
past_due |
grace |
current_period_ends_at |
Commercial grace posture |
cancel_at_period_end |
active_paid |
current_period_ends_at |
Still active, but cancellation is pending |
ended |
suspended_read_only |
current_period_ends_at |
Commercial access has ended |
Derived Truth
5. Workspace Subscription Summary
Persistence: none, derived at runtime
Owner: WorkspaceSubscriptionResolver
| Field | Type | Required | Notes |
|---|---|---|---|
workspace_id |
int | yes | Workspace being evaluated |
subscription_present |
bool | yes | Whether a current record exists |
state |
string | no | Current subscription state when present |
label |
string | no | Operator-facing state label |
billing_reference |
string | no | Optional reference |
status_reason |
string | no | Operator-visible explanation |
key_date_label |
string | no | Trial ends or Current period ends |
key_date |
datetime | no | Current relevant date |
needs_review |
bool | yes | True when a date-sensitive state is past its visible date |
source |
string | yes | One of workspace_subscription, workspace_setting, or default_active_paid |
fallback_status |
bool | yes | True when the summary is not backed by a current subscription record |
derived_lifecycle_state |
string | yes | Existing lifecycle state consumed downstream |
6. Effective Commercial Lifecycle Decision
Persistence: none, derived at runtime
Owner: existing WorkspaceCommercialLifecycleResolver
The lifecycle decision remains the shared gate shape from Spec 251, but its source changes:
- If a subscription record exists, the lifecycle source becomes
workspace_subscription. - If no subscription record exists, the current
workspace_settingordefault_active_paidsource remains.
Ordering rules:
- Resolve the underlying entitlement substrate.
- Resolve the lifecycle source from subscription truth when present, otherwise from fallback manual lifecycle truth.
- If the substrate already blocks the action, keep the substrate block.
- If the substrate allows the action, apply the lifecycle outcome from the resolved lifecycle state.
Supporting Derived View Models
7. System Workspace Subscription View Model
Consumer: ViewWorkspace
Contains:
- current subscription summary
- derived lifecycle summary
- fallback indicator when no subscription exists
- last-change attribution
- mutation affordance metadata for
Update subscription truth
8. Workspace Settings Subscription Summary View Model
Consumer: WorkspaceSettings
Contains:
- current commercial posture
- whether it is subscription-backed or fallback-backed
- next relevant date
- concise explanation only
State Transitions
There is no multi-row ledger in v1. State changes are explicit updates to the current workspace subscription record plus audit entries.
| From | To | Trigger | Consequence |
|---|---|---|---|
| no record | any valid state | platform operator creates current subscription truth | workspace becomes subscription-backed |
trial |
active |
platform operator transition | derived lifecycle moves from trial to active_paid |
active |
past_due |
platform operator transition | derived lifecycle moves to grace |
active |
cancel_at_period_end |
platform operator transition | derived lifecycle stays active_paid, but period end becomes important context |
past_due |
ended |
platform operator transition | derived lifecycle moves to suspended_read_only |
| any state | any other valid state | platform operator update | current subscription truth changes in place and is auditable |
Boundaries Explicitly Preserved
- No invoice, payment, or provider-sync persistence exists in this slice.
- No multi-record historical subscription ledger exists in this slice.
- No direct subscription gate shape exists on onboarding or review-pack surfaces; lifecycle remains the only gate.
- Existing view and download access to already-generated review packs, evidence, and review history stays governed by the current lifecycle and RBAC rules.