TenantAtlas/specs/274-billing-subscription-truth/plan.md
ahmido 35b59eb628 274: Billing subscription truth - add workspace subscription model & tests (#326)
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
2026-05-04 21:15:57 +00:00

24 KiB

Implementation Plan: Billing & Subscription Truth Layer v1

Branch: 274-billing-subscription-truth | Date: 2026-05-04 | Spec: spec.md Input: Feature specification from specs/274-billing-subscription-truth/spec.md

Summary

Prepare one bounded billing and subscription truth follow-through on top of the already-real Specs 247 and 251. The narrow implementation path is to introduce one workspace-owned current subscription record, feed it into the existing commercial lifecycle resolver when present, keep the current lifecycle resolver as the only runtime gate, and surface the resulting truth on the existing system workspace detail page plus a read-only summary on workspace settings.

This slice stays explicitly narrow. Filament remains v5 on Livewire v4, panel-provider registration stays in apps/platform/bootstrap/providers.php, no new globally searchable resource is introduced, and no asset registration change is expected. The plan does not reopen plan-profile or lifecycle foundations, does not introduce payment providers, invoices, checkout, a customer portal, or a second runtime gate, and does not add a new OperationRun family.

Inherited Baseline / Explicit Delta

Inherited baseline

  • WorkspacePlanProfileCatalog and WorkspaceEntitlementResolver already provide bounded plan defaults, override logic, and the current managed-tenant or review-pack entitlement truth from Spec 247.
  • WorkspaceCommercialLifecycleResolver already provides the current shared lifecycle decision, mapping into onboarding activation and review-pack start behavior from Spec 251.
  • ViewWorkspace already exposes the current commercial and entitlement truth on the system workspace detail page and already owns the current bounded commercial mutation surface.
  • WorkspaceSettings already provides the singleton admin-plane settings surface where commercial truth can be summarized without introducing a second management plane.
  • ManagedTenantOnboardingWizard and ReviewPackService already consult shared commercial lifecycle truth before their high-impact start or activation flows.
  • Existing tests under tests/Unit/Entitlements/, tests/Feature/System/, tests/Feature/Filament/Settings/, tests/Feature/Onboarding/, and tests/Feature/ReviewPack/ already prove the current entitlement and lifecycle seams.

Explicit delta in this plan

  • Add one workspace-owned current subscription record with bounded state and current-period fields.
  • Add one bounded WorkspaceSubscriptionResolver that exposes current subscription truth, next relevant date, fallback status, and lifecycle mapping.
  • Refactor WorkspaceCommercialLifecycleResolver to prefer subscription truth when a subscription record exists and to preserve the current fallback path when it does not.
  • Replace or narrow the current system detail mutation action so platform operators manage subscription truth rather than relying on manual lifecycle posture for subscription-backed workspaces.
  • Add a read-only subscription summary to WorkspaceSettings so workspace operators can see the current commercial posture without gaining billing controls.
  • Keep onboarding and review-pack surfaces on the existing shared lifecycle gate and only change the upstream source behind that decision.

Technical Context

Language/Version: PHP 8.4, Laravel 12
Primary Dependencies: Filament v5, Livewire v4, Pest v4, existing WorkspaceEntitlementResolver, existing WorkspaceCommercialLifecycleResolver, existing workspace audit foundation, existing onboarding and review-pack flows
Storage: PostgreSQL via a new workspace-owned workspace_subscriptions table plus existing workspace_settings, audit_logs, and tenant-owned artifact tables
Testing: Pest v4 Unit plus focused Feature coverage
Validation Lanes: fast-feedback, confidence
Target Platform: Laravel monolith in apps/platform, reusing the existing system and admin Filament pages
Project Type: Web application (Laravel monolith with Filament panels)
Performance Goals: no new queue family, no new Graph calls, and DB-only lifecycle or subscription resolution for current workspace pages
Constraints: no payment provider integration, no invoice or checkout flow, no customer portal, no second gate outside WorkspaceCommercialLifecycleResolver, no new panel, no global-search change, and no asset-registration change
Scale/Scope: 1 new workspace-owned entity, 1 new resolver, 2 existing page surfaces, and focused extensions to 4 existing runtime gate families

Likely Affected Repo Surfaces

  • apps/platform/app/Models/Workspace.php for the new workspace-owned subscription relationship.
  • apps/platform/app/Models/WorkspaceSubscription.php as the new current subscription model.
  • apps/platform/database/migrations/*_create_workspace_subscriptions_table.php for new persistence.
  • apps/platform/database/factories/WorkspaceSubscriptionFactory.php for bounded test setup.
  • apps/platform/app/Services/Entitlements/WorkspaceSubscriptionResolver.php as the new bounded commercial source resolver.
  • apps/platform/app/Services/Entitlements/WorkspaceCommercialLifecycleResolver.php for subscription-first precedence and fallback retention.
  • apps/platform/app/Filament/System/Pages/Directory/ViewWorkspace.php for subscription detail rendering and the bounded mutation action.
  • apps/platform/app/Filament/Pages/Settings/WorkspaceSettings.php for the read-only subscription summary.
  • apps/platform/app/Services/Audit/WorkspaceAuditLogger.php and current audit action IDs or metadata for subscription-change attribution.
  • Existing tests under apps/platform/tests/Unit/Entitlements/, apps/platform/tests/Feature/System/, apps/platform/tests/Feature/Filament/Settings/, apps/platform/tests/Feature/Onboarding/, and apps/platform/tests/Feature/ReviewPack/.

Commercial Truth Fit

  • Treat the subscription record as a distinct current-release source of truth, not as more workspace-setting keys.
  • Keep exactly one current record per workspace in v1. Historical change tracking comes from audit rather than a multi-row subscription ledger.
  • Keep subscription state and lifecycle state distinct:
    • subscription state answers the current commercial source truth
    • lifecycle state remains the shared runtime gate for onboarding and review-pack flows
  • Use subscription truth to derive lifecycle when present; otherwise preserve the current manual lifecycle fallback.
  • Keep period-end and trial-end dates visible and explicit, but do not add timer-driven or webhook-driven automation in this slice.

UI / Filament & Livewire Fit

  • Existing operator-facing surfaces remain native Filament surfaces under Livewire v4, and this slice should stay inside those surfaces instead of introducing a new billing console.
  • No new Filament resource or globally searchable resource is required; the current system workspace detail page and workspace settings singleton page are sufficient.
  • The system page keeps one dominant action: update current subscription truth, and that action remains confirmation-protected. The current manual lifecycle action stays available only for fallback-backed workspaces and must remain explicitly secondary and bounded.
  • Workspace settings gains a read-only summary only. No subscription mutation controls appear on /admin.
  • Existing onboarding and review-pack action families remain in place and only change their shared upstream lifecycle source.
  • Provider registration remains unchanged in apps/platform/bootstrap/providers.php, and no new asset strategy is planned. If any shared asset later becomes necessary, deployment remains the normal cd apps/platform && php artisan filament:assets path.

RBAC / Policy Fit

  • Workspace and tenant membership remain isolation boundaries. Wrong-plane or non-member access stays 404; in-scope actors missing capability stay 403.
  • Platform-only subscription mutation should reuse the current dedicated commercial-management capability rather than adding a new capability family unless implementation proves a narrower rename is required.
  • Workspace settings visibility remains read-only on the admin plane and reuses existing settings-view capability checks.
  • Onboarding and review-pack surfaces keep their existing capability checks. Subscription truth must never bypass or replace those RBAC rules.
  • Business-state blocking stays inside the lifecycle decision path and must remain distinguishable from authorization failure.

Audit / Logging Fit

  • Every subscription-truth create or update must write an audit event with old state, new state, actor, and status reason.
  • Existing audit infrastructure should be reused rather than introducing a second billing audit subsystem.
  • The system detail summary should derive last-change attribution from the current record plus audit truth.
  • Blocked onboarding or review-pack attempts still do not need a new blocked-attempt audit family in this slice; existing behavior remains authoritative.

Data & Query Fit

  • workspace_subscriptions.workspace_id must be NOT NULL and unique so the slice stays on one current record per workspace.
  • Keep the new table small and bounded. Current-release fields should be limited to:
    • state
    • billing_reference
    • trial_ends_at
    • current_period_starts_at
    • current_period_ends_at
    • status_reason
    • timestamps
  • Do not add invoice rows, event logs, sync cursors, provider account IDs, or multi-subscription history in v1.
  • WorkspaceCommercialLifecycleResolver remains the single consumer-facing gate and should depend on the new resolver instead of scattering query logic into onboarding or review-pack code.

UI / Surface Guardrail Plan

  • Guardrail scope: changed surfaces
  • Native vs custom classification summary: native Filament
  • Shared-family relevance: system detail summaries, settings summaries, lifecycle messaging, action gating, audit-backed commercial truth
  • State layers in scope: page, detail
  • Audience modes in scope: operator-MSP, support-platform
  • Decision/diagnostic/raw hierarchy plan: decision-first, diagnostics-second, support-raw-third
  • Raw/support gating plan: raw reference detail remains secondary and platform-only
  • One-primary-action / duplicate-truth control: system detail keeps one dominant commercial mutation action; settings remains read-only; onboarding and review-pack surfaces show only the lifecycle result needed for the immediate action
  • Handling modes by drift class or surface: review-mandatory
  • Repository-signal treatment: review-mandatory now; future hard-stop candidate if a second billing surface or gate appears
  • Special surface test profiles: standard-native-filament, shared-detail-family, monitoring-state-page
  • Required tests or manual smoke: functional-core, state-contract
  • Exception path and spread control: none planned; any broader portal, provider sync, or browser-heavy proof demand is out-of-scope drift
  • Active feature PR close-out entry: Guardrail

Shared Pattern & System Fit

  • Cross-cutting feature marker: yes
  • Systems touched: WorkspaceCommercialLifecycleResolver, WorkspaceEntitlementResolver, ViewWorkspace, WorkspaceSettings, onboarding, review-pack start paths, and audit logging
  • Shared abstractions reused: current lifecycle resolver, current entitlement resolver, current audit path, current Filament action surfaces, and current review-pack start UX
  • New abstraction introduced? why?: one bounded WorkspaceSubscriptionResolver, because current repo truth has no distinct subscription source yet and several existing surfaces need the same mapping or fallback semantics
  • Why the existing abstraction was sufficient or insufficient: current lifecycle and entitlement abstractions are sufficient for runtime gating but insufficient for current subscription truth, period dates, fallback signaling, and future sync readiness
  • Bounded deviation / spread control: no local or second subscription decision helper is allowed outside the shared resolver path

OperationRun UX Impact

  • Touches OperationRun start/completion/link UX?: yes, by reuse only
  • Central contract reused: current review-pack start UX remains authoritative
  • Delegated UX behaviors: queued toast, dedupe behavior, and canonical run links stay unchanged when the derived lifecycle allows the start action
  • Surface-owned behavior kept local: the system page owns subscription edit inputs; workspace settings owns the read-only summary; onboarding and review-pack surfaces own only the immediate lifecycle explanation
  • Queued DB-notification policy: unchanged
  • Terminal notification path: unchanged central lifecycle mechanism for existing review-pack runs only
  • Exception path: none

Provider Boundary & Portability Fit

  • Shared provider/platform boundary touched?: no
  • Provider-owned seams: none in this slice
  • Platform-core seams: subscription truth, lifecycle mapping, fallback visibility, and commercial audit
  • Neutral platform terms / contracts preserved: subscription, commercial posture, trial, past due, cancel at period end, ended, fallback lifecycle
  • Retained provider-specific semantics and why: none
  • Bounded extraction or follow-up path: future provider sync only if later explicitly specced

Constitution Check

GATE: Must pass before implementation begins and again before merge.

  • Inventory-first / snapshot truth: PASS. This slice adds workspace-owned commercial truth only and does not reinterpret tenant inventory or snapshot semantics.
  • Read/write separation: PASS. The only mutation is bounded commercial metadata on the current system workspace detail surface.
  • Graph contract path: PASS. No Graph calls or provider contract changes are introduced.
  • Deterministic capabilities: PASS. Existing capability registries remain authoritative.
  • Workspace and tenant isolation: PASS. Existing 404 and 403 semantics remain unchanged.
  • RBAC-UX plane separation: PASS. Mutation stays on /system; /admin remains read-only or contextual.
  • Destructive action discipline: PASS. No new destructive action is planned; any high-impact mutation stays confirmation-protected.
  • Global search safety: PASS. No new searchable resource is introduced.
  • OperationRun / Ops-UX: PASS by reuse only. No new run family or new run-triggering surface is added.
  • Data minimization: PASS. Current subscription truth stays bounded and no provider payloads or payment data are stored.
  • Test governance: PASS. Proof remains in focused unit plus feature lanes.
  • Proportionality / no premature abstraction: PASS. One new entity and one new resolver are the narrowest path that avoids further truth duplication.
  • Persisted truth: PASS. The new table represents real product truth with an independent lifecycle and audit need.
  • Behavioral state: PASS. Subscription states change derived lifecycle and operator behavior.
  • Shared pattern first / UI semantics / Filament-native UI: PASS. Existing Filament surfaces and the current lifecycle gate remain the shared path.
  • Provider boundary: PASS. No provider-specific vocabulary is introduced.
  • Filament / Laravel planning contract: PASS. Filament stays v5 on Livewire v4, provider registration remains in apps/platform/bootstrap/providers.php, no globally searchable resource is added, and no asset registration change is planned.

Gate evaluation: PASS.

Post-design re-check: PASS. research.md, data-model.md, quickstart.md, contracts/workspace-billing-subscription-truth.logical.openapi.yaml, checklists/requirements.md, and tasks.md are present and aligned with the package.

Test Governance Check

  • Test purpose / classification by changed surface: Unit for subscription validation, mapping, and fallback precedence; Feature for system detail, settings summary, onboarding, and review-pack runtime continuity
  • Affected validation lanes: fast-feedback, confidence
  • Why this lane mix is the narrowest sufficient proof: the slice reuses native Filament and current gate seams, so focused unit and feature tests can prove the new truth without browser automation
  • Narrowest proving command(s):
    • export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Entitlements/WorkspaceSubscriptionResolverTest.php tests/Unit/Entitlements/WorkspaceCommercialLifecycleResolverTest.php
    • export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/System/ViewWorkspaceEntitlementsTest.php tests/Feature/System/Spec113/AuthorizationSemanticsTest.php tests/Feature/Filament/Settings/WorkspaceEntitlementsSettingsPageTest.php
    • export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Onboarding/ManagedTenantOnboardingEntitlementTest.php tests/Feature/ReviewPack/ReviewPackEntitlementEnforcementTest.php tests/Feature/ReviewPack/ReviewPackGenerationTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php
    • export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
  • Fixture / helper / factory / seed / context cost risks: moderate but contained; one new factory is needed, but current workspace, onboarding, review-pack, and system-user fixtures can stay reused
  • Expensive defaults or shared helper growth introduced?: no
  • Heavy-family additions, promotions, or visibility changes: none planned
  • Surface-class relief / special coverage rule: standard-native-filament relief for system and settings surfaces; monitoring-state-page coverage for review-pack blocked-start semantics
  • Closing validation and reviewer handoff: reviewers should rely on the exact commands above, confirm that no second runtime gate or second management plane appears, verify that blocked review-pack starts still create no run, and verify that existing review-pack view or download access remains unchanged
  • Budget / baseline / trend follow-up: none expected beyond a small feature-local increase
  • Review-stop questions: did the slice add a second gate, did it widen into provider sync or invoices, did it leave fallback behavior ambiguous, and did it preserve 404 or 403 semantics
  • Escalation path: document-in-feature for contained naming drift; reject-or-split if the slice widens into a billing engine, portal, or second gate
  • Active feature PR close-out entry: Guardrail
  • Why no dedicated follow-up spec is needed: routine subscription-truth upkeep should stay inside this feature unless future provider sync or invoice history is explicitly promoted as a separate slice
  • Test-governance outcome: keep

Review Checklist Status

  • Review checklist artifact: checklists/requirements.md
  • Review outcome class: acceptable-special-case
  • Workflow outcome: keep
  • Test-governance outcome: keep
  • Escalation rule: if implementation adds provider sync, invoice persistence, or a second runtime gate, flip the workflow outcome to split before continuing

Rollout Considerations

  • Land the new entity and resolver first, then refactor lifecycle precedence, then update the system detail page, and only then add the read-only settings summary.
  • Keep the current manual lifecycle path explicit as fallback-backed so workspaces without a subscription record do not regress.
  • Keep onboarding and review-pack behavior untouched except for the new upstream lifecycle source.
  • Keep Filament v5 on Livewire v4, keep provider registration in apps/platform/bootstrap/providers.php, keep global search unchanged, and keep assets unchanged.

Risk Controls

  • Reject any implementation that adds payment-provider adapters, webhook handlers, invoice rows, or a customer billing portal.
  • Reject any implementation that adds direct subscription checks to onboarding or review-pack surfaces instead of routing through lifecycle.
  • Reject any implementation that makes /admin a second commercial mutation plane.
  • Reject any implementation that turns one current subscription record into a broad ledger or history console in this slice.
  • Reject browser-heavy proof as the default validation lane.

Research & Design Outputs

  • research.md should resolve the key design choices: dedicated table versus settings keys, subscription-first lifecycle precedence, read-only admin summary, and strict non-goals around providers or invoices.
  • data-model.md should record the new entity, field validation, state mapping, uniqueness constraints, and derived lifecycle summary shape.
  • quickstart.md should provide the bounded implementation order, reviewer scenarios, explicit confirmation expectations for the system mutation, and focused validation commands.
  • contracts/workspace-billing-subscription-truth.logical.openapi.yaml should capture the logical route and action boundaries for the current system and admin surfaces plus the unchanged downstream gate semantics.
  • checklists/requirements.md should record the prep-time review outcome, workflow outcome, and test-governance outcome.
  • tasks.md should keep the implementation bounded to the current surfaces and the current lifecycle gate.

Project Structure

Documentation (this feature)

specs/274-billing-subscription-truth/
├── checklists/
│   └── requirements.md
├── contracts/
│   └── workspace-billing-subscription-truth.logical.openapi.yaml
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
└── tasks.md

Source Code (expected implementation surfaces)

apps/platform/
├── app/
│   ├── Filament/
│   │   ├── Pages/
│   │   │   └── Settings/
│   │   │       └── WorkspaceSettings.php
│   │   ├── System/Pages/Directory/
│   │   │   └── ViewWorkspace.php
│   │   └── Pages/Workspaces/
│   │       └── ManagedTenantOnboardingWizard.php
│   ├── Models/
│   │   ├── Workspace.php
│   │   └── WorkspaceSubscription.php
│   ├── Services/
│   │   ├── Audit/WorkspaceAuditLogger.php
│   │   └── Entitlements/
│   │       ├── WorkspaceCommercialLifecycleResolver.php
│   │       └── WorkspaceSubscriptionResolver.php
│   └── Support/
│       └── Auth/
│           └── PlatformCapabilities.php
├── database/
│   ├── factories/
│   │   └── WorkspaceSubscriptionFactory.php
│   └── migrations/
│       └── *_create_workspace_subscriptions_table.php
└── tests/
    ├── Unit/Entitlements/
    │   └── WorkspaceSubscriptionResolverTest.php
    └── Feature/
        ├── Filament/Settings/
        │   └── WorkspaceEntitlementsSettingsPageTest.php
        ├── Onboarding/
        │   └── ManagedTenantOnboardingEntitlementTest.php
        ├── ReviewPack/
        │   ├── ReviewPackEntitlementEnforcementTest.php
        │   ├── ReviewPackGenerationTest.php
        │   └── ReviewPackDownloadTest.php
        ├── Reviews/
        │   └── CustomerReviewWorkspacePackAccessTest.php
        └── System/
            ├── Spec113/AuthorizationSemanticsTest.php
            └── ViewWorkspaceEntitlementsTest.php

Complexity Tracking

Violation Why Needed Simpler Alternative Rejected Because
New persisted subscription entity Current commercial truth now needs its own lifecycle, dates, and audit trail More settings keys would keep subscription truth blurred with fallback lifecycle and plan settings

Proportionality Review

  • Current operator problem: support and commercial operators still cannot point to one durable source that explains why a workspace is trial, paid, overdue, cancellation-pending, or ended
  • Existing structure is insufficient because: plan settings and manual lifecycle state together still do not provide a distinct subscription record with its own lifecycle and current-period fields
  • Narrowest correct implementation: one current subscription record plus one resolver layered into the current lifecycle path
  • Ownership cost: one new entity, one migration, one resolver, one read-only admin summary, one system mutation surface, and focused tests
  • Alternative intentionally rejected: more workspace settings keys or a broad billing engine
  • Release truth: current-release truth only; provider sync, invoices, and portal work remain follow-ups