TenantAtlas/specs/146-central-tenant-status-presentation/research.md
ahmido 6ca496233b feat: centralize tenant lifecycle presentation (#175)
## Summary
- add a shared tenant lifecycle presentation contract and referenced-tenant adapter for canonical lifecycle labels and helper copy
- align tenant, chooser, onboarding, archived-banner, and tenantless operation viewer surfaces with the shared lifecycle vocabulary
- add Spec 146 design artifacts, audit notes, and regression coverage for lifecycle presentation across Filament and onboarding surfaces

## Validation
- `vendor/bin/sail bin pint --dirty --format agent`
- `vendor/bin/sail artisan test --compact tests/Feature/Badges/TenantStatusBadgeTest.php tests/Unit/Badges/TenantBadgesTest.php tests/Unit/Tenants/TenantLifecycleTest.php tests/Unit/Support/Tenants/TenantLifecyclePresentationTest.php tests/Feature/Filament/TenantLifecyclePresentationAcrossTenantSurfacesTest.php tests/Feature/Filament/ReferencedTenantLifecyclePresentationTest.php tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php tests/Feature/Filament/TenantViewHeaderUiEnforcementTest.php tests/Feature/Onboarding/TenantLifecyclePresentationCopyTest.php tests/Feature/Onboarding/OnboardingDraftAuthorizationTest.php tests/Feature/Onboarding/OnboardingDraftLifecycleTest.php`

## Notes
- Livewire v4.0+ compliance preserved; this change is presentation-only on existing Filament v5 surfaces.
- Panel provider registration remains unchanged in `bootstrap/providers.php`.
- No global-search behavior changed; no resource was newly made globally searchable or disabled.
- No destructive actions were added or changed.
- No asset registration strategy changed; existing deploy flow for `php artisan filament:assets` remains unchanged.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #175
2026-03-16 18:18:53 +00:00

6.1 KiB

Research: Central Tenant Status Presentation

Decision 1: Build on the existing TenantLifecycle enum and badge catalog instead of creating a parallel status system

  • Decision: Treat App\Support\Tenants\TenantLifecycle as the canonical lifecycle source and extend presentation through a new lifecycle presentation contract layered on top of the existing BadgeCatalog and BadgeRenderer pipeline.
  • Rationale: The codebase already has the correct domain enum (draft, onboarding, active, archived) and an existing BADGE-001-compliant badge registry. Reusing those pieces avoids semantic drift and keeps lifecycle presentation aligned with the authoritative domain model from Spec 143.
  • Alternatives considered:
    • Add more per-surface match or formatStateUsing() closures. Rejected because that would reintroduce scattered UI dictionaries.
    • Put all presentation logic directly on the Tenant model. Rejected because lifecycle presentation density varies by surface and should not overload the domain model with Filament-specific UI concerns.
    • Keep using TenantStatusBadge alone. Rejected because badge label/color/icon is not enough to cover helper text, referenced-tenant notes, and invalid-data distinction.

Decision 2: Separate lifecycle presentation from adjacent status domains explicitly

  • Decision: Keep lifecycle presentation as its own contract and do not merge it with provider connection, app consent, RBAC, verification, onboarding progress, or operation-run state.
  • Rationale: The tenant detail and tenantless operations viewer surfaces already carry multiple status domains. Spec 146 requires lifecycle to remain honest to lifecycle only, otherwise operators misread onboarding, archived, or active state as health or readiness.
  • Alternatives considered:
    • Collapse lifecycle and readiness into one composite badge. Rejected because it obscures whether a tenant is active but unhealthy versus onboarding but valid.
    • Reuse app-status or RBAC helper copy to explain lifecycle. Rejected because it creates cross-domain semantic leakage.

Decision 3: Use one presentation source with multiple density variants

  • Decision: The new presentation contract should return both concise and detailed variants from the same underlying lifecycle meaning.
  • Rationale: Tables and selectors need short badge semantics, while detail views and the tenantless operations viewer need helper text that explains why onboarding or archived tenants still appear. One source with multiple densities satisfies the spec without forcing verbose copy into every surface.
  • Alternatives considered:
    • Make all surfaces badge-only. Rejected because the tenantless operations viewer and detail pages need more context.
    • Make all surfaces badge plus paragraph text. Rejected because selector and list surfaces would become noisy.

Decision 4: Keep invalid fallback only for non-canonical data and make it explicit in tests

  • Decision: Preserve a reserved fallback path for corrupted or unexpected lifecycle values, but do not allow canonical states to reach it.
  • Rationale: TenantStatusBadge currently falls back to BadgeSpec::unknown(). The feature needs a stricter separation so valid canonical states are exhaustive and only truly invalid values use fallback behavior.
  • Alternatives considered:
    • Remove fallback completely. Rejected because legacy or bad data still needs a safe render path.
    • Continue allowing normalized aliases like inactive to map silently without explicit tests. Rejected because future drift would be hard to detect.

Decision 5: Favor focused regression tests over broad UI rewrites

  • Decision: Add unit coverage for the central presentation contract and focused Pest feature tests for the tenant index, tenant detail, choose-tenant/onboarding-linked presentation, and tenantless operations viewer surfaces.
  • Rationale: The existing architecture is already partially centralized. The highest-value risk is regression through future local mappings or fallback behavior, so tests should lock down lifecycle semantics where operators actually see them.
  • Alternatives considered:
    • Add only unit tests. Rejected because cross-surface consistency is the actual acceptance criterion.
    • Add a full browser test suite for every surface. Rejected because feature and unit coverage can prove the contract with lower cost and faster CI.

Supporting Findings

  • App\Support\Tenants\TenantLifecycle already defines the exact four lifecycle states and canonical labels required by the spec.
  • App\Support\Badges\BadgeCatalog and App\Support\Badges\Domains\TenantStatusBadge already centralize badge label/color/icon mapping for tenant lifecycle, but they do not yet provide richer helper semantics.
  • app/Filament/Resources/TenantResource.php already renders tenant status through BadgeRenderer on both the table and infolist surfaces.
  • resources/views/filament/pages/choose-tenant.blade.php uses lifecycle-adjacent prose such as "No active tenants available" and references onboarding/archived records without a dedicated lifecycle presentation object.
  • resources/views/filament/widgets/tenant/tenant-archived-banner.blade.php already carries archived-tenant helper copy, which makes it an in-scope surface that must be aligned with the shared lifecycle contract instead of maintaining standalone wording.
  • app/Filament/Pages/Operations/TenantlessOperationRunViewer.php derives lifecycle-aware canonical-view copy via TenantOperabilityService, which is a good integration point for the shared presentation contract.
  • resources/views/filament/pages/operations/tenantless-operation-run-viewer.blade.php renders the canonical context banner body, making it the concrete viewer surface where referenced-tenant lifecycle wording must stay aligned with tenant management pages.
  • app/Filament/Resources/OperationRunResource.php builds the enterprise-detail payload consumed by the tenantless operations viewer, so referenced-tenant lifecycle summary text there is part of the same viewer surface rather than a separate resource rollout.
  • Filament v5 documentation confirms badge rendering is intended to be driven centrally through shared text/badge semantics rather than repeated local mappings.