173 lines
7.9 KiB
Markdown
173 lines
7.9 KiB
Markdown
# 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_id` is 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_id` references `workspaces.id`.
|
|
- `Workspace` gains 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_setting` or `default_active_paid` source remains.
|
|
|
|
**Ordering rules**:
|
|
|
|
1. Resolve the underlying entitlement substrate.
|
|
2. Resolve the lifecycle source from subscription truth when present, otherwise from fallback manual lifecycle truth.
|
|
3. If the substrate already blocks the action, keep the substrate block.
|
|
4. 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.
|