TenantAtlas/specs/292-workspace-tenant-closure/plan.md
ahmido 210508db9d feat: implement workspace and tenant closure lifecycle (#337)
## Summary
- add explicit workspace closure and tenant removal lifecycle truth with a bounded `WorkspaceLifecycleService`
- surface closure and removal posture across admin/system pages, chooser recovery, and canonical historical viewers
- block new review-pack and operation starts for closed workspaces or removed tenants while preserving memberships, audit, and history
- add focused Pest coverage plus the Spec 292 artifacts for the implemented slice

## Testing
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/System/Directory/ViewWorkspaceClosureTest.php tests/Feature/System/Ops/ClosedWorkspaceHistoricalAccessTest.php tests/Feature/Filament/Resources/Workspaces/WorkspaceClosureStatusTest.php tests/Feature/Filament/Resources/TenantResource/TenantWorkspaceRemovalTest.php tests/Feature/Filament/Pages/WorkspaceContextClosureRecoveryTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- manual integrated-browser smoke for admin tenant remove/restore plus chooser recovery and system workspace close/reopen

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #337
2026-05-07 13:12:17 +00:00

24 KiB

Implementation Plan: Workspace & Tenant Closure Lifecycle v1

Branch: 292-workspace-tenant-closure | Date: 2026-05-07 | Spec: spec.md Input: Feature specification from specs/292-workspace-tenant-closure/spec.md

Summary

Prepare one bounded lifecycle runtime follow-through on top of the Spec 262 taxonomy foundation plus the already-real tenant operability and commercial lifecycle seams. The narrow implementation path is to add explicit workspace closed truth on Workspace, explicit removed-from-workspace truth on ManagedEnvironment, keep commercial suspension as a separate shared gate, and reuse the existing system directory, admin resource, chooser, and canonical historical viewer surfaces instead of opening a new workflow console.

This slice stays deliberately 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 the broader taxonomy, does not implement purge or export-before-delete, does not add payment-provider or billing-portal behavior, and does not add a new OperationRun family.

Inherited Baseline / Explicit Delta

Inherited baseline

  • Workspace already carries archived_at, and WorkspaceContext::isWorkspaceSelectable() already uses that archived truth to prevent invalid workspace selection.
  • ManagedEnvironment already carries tenant lifecycle truth, and TenantLifecycle plus TenantOperabilityService already distinguish active context from onboarding or archive semantics.
  • WorkspaceCommercialLifecycleResolver already maps commercial truth into SUSPENDED_READ_ONLY versus normal workspace posture for high-impact actions.
  • EnsureWorkspaceSelected, EnsureFilamentTenantSelected, and DenyNonMemberTenantAccess already provide the current context-recovery and deny-as-not-found seams.
  • WorkspaceMembershipManager, TenantMembershipManager, WorkspaceAuditLogger, and current tenant audit paths already provide bounded audit-safe mutation seams.
  • Existing admin and system surfaces already exist at ViewWorkspace, ViewTenant, ViewRun, ChooseWorkspace, ChooseTenant, WorkspaceResource, TenantResource, and WorkspaceSettings.

Explicit delta in this plan

  • Add explicit closure truth to Workspace using bounded persisted fields for closed posture, actor, and reason.
  • Add explicit removed-from-workspace truth to ManagedEnvironment using bounded persisted fields for removal posture, actor, and reason.
  • Introduce one bounded WorkspaceLifecycleService to orchestrate close/reopen and remove/restore without opening a generic lifecycle framework.
  • Keep archived, suspended read-only, closed, and removed from workspace as four distinct meanings with separate badges, copy, and blocked-action explanations.
  • Update chooser recovery, tenant-context legitimacy, and canonical historical viewers so historical records remain readable while invalid active context is cleared explicitly.
  • Keep all new lifecycle mutations inside current admin and system surfaces and current audit infrastructure.

Technical Context

Language/Version: PHP 8.4, Laravel 12
Primary Dependencies: Filament v5, Livewire v4, Pest v4, existing WorkspaceContext, TenantOperabilityService, WorkspaceCommercialLifecycleResolver, current audit infrastructure, current Filament admin and system pages
Storage: PostgreSQL via targeted new lifecycle fields on existing workspaces and managed_environments tables plus existing audit and history tables
Testing: Pest v4 focused Feature coverage
Validation Lanes: fast-feedback, confidence
Target Platform: Laravel monolith in apps/platform across the existing admin and system Filament panels
Project Type: Web application (Laravel monolith with Filament panels)
Performance Goals: DB-only lifecycle gating, no new queue family, no new Graph calls, no new browser-only proof requirement
Constraints: no purge, no export-before-delete, no payment-provider or billing workflow, no new global-search resource, no new panel, no lifecycle engine, no asset-registration change
Scale/Scope: 2 existing persisted records gain bounded lifecycle truth, 1 bounded service seam, 6 existing operator surfaces, and focused feature-test extensions

Likely Affected Repo Surfaces

  • apps/platform/app/Models/Workspace.php
  • apps/platform/app/Models/ManagedEnvironment.php
  • apps/platform/database/migrations/*_add_workspace_closure_fields.php
  • apps/platform/database/migrations/*_add_managed_environment_workspace_removal_fields.php
  • apps/platform/app/Services/Workspaces/WorkspaceLifecycleService.php
  • apps/platform/app/Support/Workspaces/WorkspaceContext.php
  • apps/platform/app/Services/Tenants/TenantOperabilityService.php
  • apps/platform/app/Services/Entitlements/WorkspaceCommercialLifecycleResolver.php
  • apps/platform/app/Http/Middleware/EnsureWorkspaceSelected.php
  • apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php
  • apps/platform/app/Support/Middleware/DenyNonMemberTenantAccess.php
  • apps/platform/app/Filament/System/Pages/Directory/ViewWorkspace.php
  • apps/platform/app/Filament/System/Pages/Directory/ViewTenant.php
  • apps/platform/app/Filament/System/Pages/Ops/ViewRun.php
  • apps/platform/app/Filament/Pages/ChooseWorkspace.php
  • apps/platform/app/Filament/Pages/ChooseTenant.php
  • apps/platform/app/Filament/Pages/Settings/WorkspaceSettings.php
  • apps/platform/app/Filament/Resources/Workspaces/WorkspaceResource.php
  • apps/platform/app/Filament/Resources/Workspaces/Pages/ViewWorkspace.php
  • apps/platform/app/Filament/Resources/TenantResource.php
  • apps/platform/app/Services/Audit/WorkspaceAuditLogger.php
  • current tenant audit logging path where tenant membership and lifecycle actions are recorded
  • focused feature tests under apps/platform/tests/Feature/System/, apps/platform/tests/Feature/Filament/Resources/, and apps/platform/tests/Feature/Filament/Pages/

Lifecycle Truth Fit

  • Treat workspace closure as a dedicated workspace-owned lifecycle posture, not as archive and not as commercial suspension.
  • Treat tenant removal from workspace as a dedicated workspace-owned posture on the tenant record, not as tenant archive and not as provider absence.
  • Keep commercial suspension in WorkspaceCommercialLifecycleResolver as the shared source of SUSPENDED_READ_ONLY; closure logic must compose with it, not replace it.
  • Preserve current tenant lifecycle and provider-missing semantics; the feature only adds workspace-governed closure and removal truth.
  • Preserve historical readability:
    • closed workspace -> not selectable as current workspace, read-only historical inspection remains available to entitled actors
    • removed tenant -> not selectable as current tenant, canonical historical inspection remains available to entitled actors
    • suspended read-only -> current workspace remains inspectable and current tenant may remain visible, but mutations and starts stay blocked per existing commercial rules

Data & Query Fit

  • Extend workspaces with bounded closure fields only. The expected shape is a timestamp, actor reference, and reason text rather than a broad lifecycle ledger.
  • Extend managed_environments with bounded removed-from-workspace fields only. The expected shape mirrors the workspace closure truth and remains reversible.
  • Add only the indexes needed to filter active versus closed workspaces and active versus removed tenants.
  • Keep lifecycle truth on the primary records instead of introducing a new history table. Audit remains the historical source for who changed posture and when.
  • Preserve existing ownership rules: workspaces stay workspace-owned, tenants stay workspace-owned, and no cross-workspace migration or copy is introduced.

UI / Filament & Livewire Fit

  • Existing operator-facing surfaces remain native Filament surfaces under Livewire v4; this slice must stay inside those surfaces.
  • ViewWorkspace in the system plane keeps one dominant action: Close workspace or Reopen workspace.
  • Admin workspace detail gains read-only posture summary only; no second mutation plane for closure appears on /admin.
  • Managed tenant list keeps row click as the inspect model and moves remove or restore into More; managed tenant detail may expose Remove tenant or Restore tenant as the dominant lifecycle action.
  • Chooser surfaces remain chooser surfaces. They gain explicit recovery messaging, not a new workflow.
  • Canonical historical viewers gain posture badges or supporting text only; they do not become mutation surfaces.
  • Confirmation modals and success or error notifications on the in-scope action surfaces must reuse the canonical verbs Close workspace, Reopen workspace, Remove tenant, and Restore tenant.
  • No new globally searchable Filament resource is introduced, so there is no new global-search Edit or View page requirement to satisfy.
  • Provider registration remains unchanged in apps/platform/bootstrap/providers.php, and no new asset strategy is planned. If future shared assets ever become necessary, deployment remains the normal cd apps/platform && php artisan filament:assets path.

RBAC / Policy Fit

  • Workspace and tenant membership remain the isolation boundaries.
  • Wrong-plane actors and non-members stay 404; in-scope actors missing capability stay 403.
  • Closure and removal actions remain server-side authorized and confirmation-protected.
  • Platform closure uses the current platform workspace-governance seam or the narrowest bounded extension of it. Tenant removal uses the current workspace owner or equivalent tenant-governance seam.
  • The exact write-side enforcement seam is apps/platform/app/Services/Workspaces/WorkspaceLifecycleService.php, and the in-scope mutation entry points are the system workspace detail actions in apps/platform/app/Filament/System/Pages/Directory/ViewWorkspace.php and the tenant lifecycle actions in apps/platform/app/Filament/Resources/TenantResource.php.
  • Unsafe close or remove attempts must preserve current guard failures with a clear operator-facing reason and no partial lifecycle mutation.
  • Closure or removal posture must never substitute for authorization. A historically readable record still requires entitlement.
  • Tenant-context routes under /admin/t/{tenant}/... must reject removed tenants as active context targets while preserving canonical viewers that are not tenant-context routes.

Audit / Logging Fit

  • Every close/reopen and remove/restore mutation must write an audit event with actor, old posture, new posture, timestamp, and reason.
  • Existing audit infrastructure must be reused rather than opening a new lifecycle audit subsystem.
  • Historical viewers should surface closure or removal context through current summary and linked audit rather than duplicating the full audit payload.
  • Blocked active-context starts caused by closure or removal do not need a new audit family in this slice unless existing blocked-action logging already applies.

UI / Surface Guardrail Plan

  • Guardrail scope: changed surfaces
  • Native vs custom classification summary: native Filament
  • Shared-family relevance: status messaging, chooser recovery, detail actions, canonical historical viewers, audit-backed lifecycle copy
  • State layers in scope: shell, 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 audit and low-level identifiers remain secondary and platform-only where applicable
  • One-primary-action / duplicate-truth control: system workspace detail keeps one dominant closure action; tenant detail keeps one dominant remove/restore action; chooser recovery surfaces keep one next step
  • Handling modes by drift class or surface: review-mandatory
  • Repository-signal treatment: review-mandatory now; hard-stop candidate if implementation adds a second closure plane or local lifecycle vocabulary
  • Special surface test profiles: standard-native-filament, global-context-shell, shared-detail-family
  • Required tests or manual smoke: functional-core, state-contract
  • Exception path and spread control: none planned; any lifecycle dashboard, broad workbench, 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: WorkspaceContext, TenantOperabilityService, WorkspaceCommercialLifecycleResolver, chooser pages, system directory pages, admin workspace and tenant resources, audit logging, and canonical historical viewers
  • Shared abstractions reused: current context manager, current commercial lifecycle resolver, current tenant operability service, current audit infrastructure, BadgeCatalog and BadgeRenderer, current Filament action-surface patterns
  • New abstraction introduced? why?: one bounded WorkspaceLifecycleService, because close/reopen and remove/restore each require coordinated record mutation, audit logging, and shared blocked-action consequences across multiple surfaces
  • Why the existing abstraction was sufficient or insufficient: current services already decide selectability, commercial blocking, and audit shape, but no existing service owns the explicit closure or removal mutation path or the shared write-side behavior for it
  • Bounded deviation / spread control: no second closure orchestration helper is allowed outside the bounded service path

OperationRun UX Impact

  • Touches OperationRun start/completion/link UX?: yes, by reuse and blocking only
  • Central contract reused: current shared OperationRun start UX and current canonical monitoring pages remain authoritative
  • Delegated UX behaviors: blocked starts in closed workspaces or removed-tenant contexts must fail before enqueue on apps/platform/app/Filament/Widgets/Tenant/TenantReviewPackCard.php; existing run links and canonical historical viewers remain unchanged for already-created runs
  • Surface-owned behavior kept local: close/reopen and remove/restore input collection plus impact summaries only
  • Queued DB-notification policy: unchanged
  • Terminal notification path: unchanged central lifecycle mechanism
  • Exception path: none

Provider Boundary & Portability Fit

  • Shared provider/platform boundary touched?: no
  • Provider-owned seams: none in this slice
  • Platform-core seams: workspace closure truth, tenant removal truth, chooser recovery, and historical record readability
  • Neutral platform terms / contracts preserved: workspace, tenant, closed, removed from workspace, suspended read-only, history
  • Retained provider-specific semantics and why: none
  • Bounded extraction or follow-up path: provider-missing and other provider lifecycle work stays in separate follow-up specs

Constitution Check

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

  • Inventory-first / snapshot truth: PASS. No inventory or snapshot source is reinterpreted.
  • Read/write separation: PASS. The only new writes are explicit lifecycle mutations with confirmation, audit, and test requirements.
  • Graph contract path: PASS. No Graph calls are introduced.
  • Deterministic capabilities: PASS. Existing capability registries remain authoritative.
  • Workspace and tenant isolation: PASS. Existing 404 and 403 rules remain and are reinforced.
  • RBAC-UX plane separation: PASS. System owns workspace closure mutation; admin remains read-only for workspace closure and bounded for tenant removal inside the current workspace.
  • Destructive action discipline: PASS. Close/reopen and remove/restore are destructive-like and must remain confirmation-protected.
  • Global search safety: PASS. No new searchable resource is introduced.
  • OperationRun / Ops-UX: PASS by reuse only. No new run family is added, and blocked starts stay pre-enqueue.
  • Data minimization: PASS. Only bounded lifecycle truth fields are added on existing records.
  • Test governance: PASS. Focused feature lanes are the narrowest honest proof.
  • Proportionality / no premature abstraction: PASS. One bounded service is the narrowest write-side seam; no generic lifecycle framework is planned.
  • Persisted truth: PASS. Closure and removal posture are real product truth with independent lifecycle and audit need.
  • Behavioral state: PASS. Closed and removed-from-workspace change chooser behavior, mutation legality, and historical-view posture.
  • Shared pattern first / UI semantics / Filament-native UI: PASS. Existing chooser, audit, badge, and Filament surfaces remain the shared path.
  • Provider boundary: PASS. No provider-specific semantics are added.
  • 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.

Test Governance Check

  • Test purpose / classification by changed surface: Feature for system workspace governance, admin read-only posture, managed-tenant remove/restore, chooser recovery, and canonical historical viewer legitimacy
  • Affected validation lanes: fast-feedback, confidence
  • Why this lane mix is the narrowest sufficient proof: the slice is about integrated route, page, capability, chooser, and audit behavior; unit-only proof would miss the real lifecycle consequences
  • Narrowest proving command(s):
    • export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/System/Directory/ViewWorkspaceClosureTest.php tests/Feature/System/Ops/ClosedWorkspaceHistoricalAccessTest.php
    • export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/Resources/Workspaces/WorkspaceClosureStatusTest.php tests/Feature/Filament/Resources/TenantResource/TenantWorkspaceRemovalTest.php tests/Feature/Filament/Pages/WorkspaceContextClosureRecoveryTest.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; setup needs explicit workspace and tenant membership plus lifecycle posture states, but no new provider, browser, or heavy defaults
  • 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 the system and admin detail surfaces; global-context-shell coverage for chooser recovery; shared-detail-family coverage for historical viewers
  • Closing validation and reviewer handoff: reviewers should rely on the exact commands above, verify that no second mutation plane or lifecycle vocabulary appeared, and confirm that blocked starts stay pre-enqueue while historical viewers stay readable
  • Budget / baseline / trend follow-up: none expected beyond a small feature-local feature-test increase
  • Review-stop questions: did the slice collapse closure into archive or suspension, did it break canonical history viewers, did it add a second mutation plane, and did it widen into purge/export/billing scope
  • Escalation path: document-in-feature for contained naming or surface drift; reject-or-split if the slice widens into broader offboarding, purge, or billing workflows
  • Active feature PR close-out entry: Guardrail
  • Why no dedicated follow-up spec is needed: the remaining adjacent work is already known and separately named; this slice is the bounded workspace and tenant closure follow-through itself
  • Test-governance outcome: keep

Rollout Considerations

  • Land persistence and the bounded service seam first, then workspace closure surfaces plus chooser recovery, then tenant removal surfaces plus canonical historical viewer polish.
  • Keep admin workspace detail read-only for closure mutation throughout the slice.
  • Keep commercial suspension readable and separate from closure throughout the rollout.
  • Keep no asset changes, no provider registration changes, no new panel work, and no global-search work.

Risk Controls

  • Reject any implementation that reuses archived_at or commercial suspension as the new closure truth.
  • Reject any implementation that deletes memberships, tenants, workspaces, or historical records as part of close/reopen or remove/restore.
  • Reject any implementation that makes /admin a second workspace-closure mutation plane.
  • Reject any implementation that turns the slice into export, purge, payment-provider, or portal work.
  • Reject browser-heavy proof as the default validation lane.

Project Structure

Documentation (this feature)

specs/292-workspace-tenant-closure/
├── plan.md
├── spec.md
└── tasks.md

Source Code (expected implementation surfaces)

apps/platform/
├── app/
│   ├── Filament/
│   │   ├── Pages/
│   │   │   ├── ChooseTenant.php
│   │   │   ├── ChooseWorkspace.php
│   │   │   └── Settings/
│   │   │       └── WorkspaceSettings.php
│   │   ├── Resources/
│   │   │   ├── TenantResource.php
│   │   │   └── Workspaces/
│   │   │       ├── WorkspaceResource.php
│   │   │       └── Pages/
│   │   │           └── ViewWorkspace.php
│   │   └── System/Pages/
│   │       ├── Directory/
│   │       │   ├── ViewTenant.php
│   │       │   └── ViewWorkspace.php
│   │       └── Ops/
│   │           └── ViewRun.php
│   ├── Http/Middleware/
│   │   └── EnsureWorkspaceSelected.php
│   ├── Models/
│   │   ├── ManagedEnvironment.php
│   │   └── Workspace.php
│   ├── Services/
│   │   ├── Audit/
│   │   │   └── WorkspaceAuditLogger.php
│   │   ├── Entitlements/
│   │   │   └── WorkspaceCommercialLifecycleResolver.php
│   │   ├── Tenants/
│   │   │   └── TenantOperabilityService.php
│   │   └── Workspaces/
│   │       └── WorkspaceLifecycleService.php
│   └── Support/
│       ├── Middleware/
│       │   ├── DenyNonMemberTenantAccess.php
│       │   └── EnsureFilamentTenantSelected.php
│       └── Workspaces/
│           └── WorkspaceContext.php
├── database/
│   └── migrations/
└── tests/
    └── Feature/
        ├── Filament/
        └── System/

Structure Decision: Keep the slice inside the existing Laravel monolith and current Filament admin plus system surfaces. Add only targeted lifecycle fields, one bounded service, and focused feature tests.

Complexity Tracking

Violation Why Needed Simpler Alternative Rejected Because
Bounded lifecycle write service Close/reopen and remove/restore need coordinated mutation, audit, and shared blocked-action behavior Controller or page-local closures would duplicate audit and lifecycle consequences across multiple surfaces

Proportionality Review

  • Current operator problem: Operators cannot deliberately close a workspace or remove a tenant from a workspace while keeping history readable and posture explicit.
  • Existing structure is insufficient because: archive and commercial suspension represent different meanings and cannot safely absorb closure or removal truth.
  • Narrowest correct implementation: add bounded fields on existing records, route writes through one bounded service, and reuse current chooser, audit, and Filament surfaces.
  • Ownership cost created: migrations, one service, shared status and chooser updates, and feature tests.
  • Alternative intentionally rejected: generic lifecycle engine, new closure history tables, or overloading archive and suspension.
  • Release truth: current-release truth.