TenantAtlas/specs/188-provider-connection-state-cleanup/plan.md
ahmido 1655cc481e Spec 188: canonical provider connection state cleanup (#219)
## Summary
- migrate provider connections to the canonical three-dimension state model: lifecycle via `is_enabled`, consent via `consent_status`, and verification via `verification_status`
- remove legacy provider status and health badge paths, update admin and system directory surfaces, and align onboarding, consent callback, verification, resolver, and mutation flows with the new model
- add the Spec 188 artifact set, schema migrations, guard coverage, and expanded provider-state tests across admin, system, onboarding, verification, and rendering paths

## Verification
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Auth/SystemPanelAuthTest.php tests/Feature/Filament/TenantGlobalSearchLifecycleScopeTest.php tests/Feature/ProviderConnections/ProviderConnectionEnableDisableTest.php tests/Feature/ProviderConnections/ProviderConnectionTruthCleanupSpec179Test.php`
- integrated browser smoke: validated admin provider list/detail/edit, tenant provider summary, system directory tenant detail, provider-connection search exclusion, and cleaned up the temporary smoke record afterward

## Filament / implementation notes
- Livewire v4.0+ compliance: preserved; this change targets Filament v5 on Livewire v4 and does not introduce older APIs
- Provider registration location: unchanged; Laravel 11+ panel providers remain registered in `bootstrap/providers.php`
- Globally searchable resources: `ProviderConnectionResource` remains intentionally excluded from global search; tenant global search remains enabled and continues to resolve to view pages
- Destructive actions: no new destructive action surface was introduced without confirmation or authorization; existing capability checks continue to gate provider mutations
- Asset strategy: unchanged; no new Filament assets were added, so deploy behavior for `php artisan filament:assets` remains unchanged
- Testing plan covered: system auth, tenant global search, provider lifecycle enable/disable behavior, and provider truth cleanup cutover behavior

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #219
2026-04-10 11:22:56 +00:00

26 KiB

Implementation Plan: Canonical Provider Connection State Cleanup

Branch: 188-provider-connection-state-cleanup | Date: 2026-04-09 | Spec: /Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/188-provider-connection-state-cleanup/spec.md Input: Feature specification from /Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/188-provider-connection-state-cleanup/spec.md

Note: This plan is a hard-cut cleanup. It removes legacy provider connection truth instead of layering a compatibility bridge on top of it.

Summary

Replace legacy provider connection status and health_status with one explicit lifecycle truth, is_enabled, beside the existing canonical consent_status and verification_status. The implementation adds that narrow lifecycle column, moves every runtime reader and writer onto the three canonical dimensions, trims health-check and projector contracts so they stop emitting legacy state, rewrites admin and system surfaces to consume lifecycle, consent, and verification plus diagnostics only, retires legacy provider badge domains, updates factories and Pest helpers, and then drops the removed columns and their indexes in the final migration.

Technical Context

Language/Version: PHP 8.4.15
Primary Dependencies: Laravel 12, Filament v5, Livewire v4, Pest v4, existing ProviderConnection model, ProviderConnectionResolver, ProviderConnectionStateProjector, ProviderConnectionMutationService, ProviderConnectionHealthCheckJob, StartVerification, ProviderConnectionResource, TenantResource, system directory pages, BadgeCatalog, BadgeRenderer, and shared provider-state Blade entries
Storage: PostgreSQL with one narrow schema addition (is_enabled) followed by final removal of legacy status and health_status columns and their indexes
Testing: Pest 4 feature and unit coverage via Laravel Sail, reusing provider connection, onboarding, verification, audit, badge, and system-directory tests plus one new residual guard for removed legacy state usage
Target Platform: Laravel monolith web application running in Sail locally and containerized Linux environments in staging and production
Project Type: web application
Performance Goals: Preserve DB-only rendering guarantees on touched pages, avoid extra per-row remote calls or presenter layers, keep resolver and system-directory queries index-friendly, and keep canonical provider summaries bounded to existing record lookups
Constraints: No dual-read or dual-write compatibility period, no new provider-state framework, no new persisted summary artifact, no new badge taxonomy for lifecycle, no authorization widening, no cross-plane truth drift between /admin and /system, and no destructive-action safety regression
Scale/Scope: One tenant-owned model, one narrow lifecycle column, two removed legacy columns, at least six runtime read paths, at least six runtime write paths, six operator-facing surfaces, two shared Blade views, the centralized badge registry, shared factory and Pest helper scaffolding, and a focused regression pack spanning provider, onboarding, verification, audit, and directory flows

Constitution Check

GATE: Passed before Phase 0 research. Re-check after Phase 1 design.

Principle Pre-Research Post-Design Notes
Inventory-first / snapshots-second PASS PASS The feature changes only live provider-connection truth and its operator-facing interpretation. No snapshot or backup contract changes are introduced.
Read/write separation PASS PASS Existing provider mutations, onboarding callbacks, and verification flows remain the only write paths. No new mutation surface is introduced.
Graph contract path PASS PASS No Graph endpoint family or contract registry expansion is required. Existing provider health checks remain behind the current provider gateway abstractions.
Deterministic capabilities PASS PASS Existing provider view and manage capabilities remain authoritative. No role or capability remapping is introduced.
RBAC-UX authorization semantics PASS PASS /admin and /system stay plane-separated, non-members remain 404, member-but-missing-capability remains 403, and existing provider mutations stay server-authorized.
Workspace and tenant isolation PASS PASS No new route or query broadens tenant visibility. Tenant resource helpers and system-directory views stay scoped to entitled records only.
Run observability / Ops-UX PASS PASS Existing verification and provider health-check runs keep their current OperationRun semantics. This cleanup changes state persistence and rendering only.
Data minimization PASS PASS The plan removes redundant persisted truth and does not add new logs, secrets, or summary artifacts.
Proportionality / no premature abstraction PASS PASS The design adds one narrow lifecycle column and reuses existing badge, resource, and verification infrastructure instead of introducing a provider-state framework or DTO layer.
Persisted truth / behavioral state PASS PASS One new persisted lifecycle truth is justified because disabled currently has real behavioral consequences. Two redundant legacy columns are removed.
UI semantics / few layers PASS PASS Surfaces consume lifecycle, consent, and verification directly from canonical truth with thin helpers. No new readiness taxonomy or presenter stack is introduced.
Badge semantics (BADGE-001) PASS PASS BadgeDomain::BooleanEnabled, ProviderConsentStatus, and ProviderVerificationStatus are reused. Legacy provider badge domains are retired instead of retained as compatibility layers.
Filament-native UI / Action Surface Contract PASS PASS Existing Filament resources, infolists, tables, actions, and read-only directory pages are reused. Destructive-like actions remain confirmed and capability-gated.
Filament UX-001 PASS PASS No new screen type is introduced. Existing list, detail, edit, and read-only directory surfaces remain within current Filament layout patterns.
Filament v5 / Livewire v4 compliance PASS PASS The design stays within the current Filament v5 and Livewire v4 stack.
Provider registration location PASS PASS No panel or provider registration change is required. Laravel 11+ provider registration remains in bootstrap/providers.php.
Global search hard rule PASS PASS TenantResource remains globally searchable and already has view/edit pages. ProviderConnectionResource remains non-globally-searchable.
Destructive action safety PASS PASS Existing enable, disable, and credential-affecting actions keep confirmation, authorization, and audit requirements.
Asset strategy PASS PASS No new Filament assets or deploy-time filament:assets changes are required.
Testing truth (TEST-TRUTH-001) PASS PASS The plan updates real provider and onboarding tests and adds a residual guard rather than creating new testing indirection.

Phase 0 Research

Research outcomes are captured in /Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/188-provider-connection-state-cleanup/research.md.

Key decisions:

  • Persist lifecycle as a single is_enabled boolean and present it as the operator-facing Lifecycle dimension.
  • Keep ProviderConsentStatus and ProviderVerificationStatus as the only consent and verification truths; do not introduce a combined readiness state.
  • Remove legacy projector outputs and trim ProviderConnectionStateProjector to canonical verification-outcome logic only.
  • Replace legacy HealthResult transport fields with canonical verification output and diagnostics.
  • Enforce the hard-cut sequence: readers first, writers second, presentation and badge cleanup third, tests and helpers fourth, schema removal last.
  • Recompute system-directory health rollups from canonical verification and permission truth.
  • Reuse existing centralized badge infrastructure and retire legacy provider badge domains.
  • Change shared provider-state helpers and views to emit lifecycle, consent, and verification only.
  • Update factories and Pest helpers before the final drop migration and add contradiction and residual-guard coverage.

Phase 1 Design

Design artifacts are created under /Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/188-provider-connection-state-cleanup/:

  • research.md: planning decisions and rejected alternatives
  • data-model.md: canonical persistence model, state transitions, mutation rules, and derived surface contracts
  • contracts/provider-connection-state-cleanup.openapi.yaml: internal route, runtime reader/writer, and surface contract
  • quickstart.md: phased implementation and validation workflow

Design highlights:

  • ProviderConnection gains one narrow lifecycle column, is_enabled, and loses status and health_status in the final schema state.
  • Lifecycle is rendered through the existing BooleanEnabled badge semantics and labeled as Lifecycle on provider surfaces.
  • Consent and verification stay on their current enums and become the only non-lifecycle state axes across /admin and /system.
  • HealthResult and ProviderConnectionStateProjector stop serving the removed legacy vocabulary.
  • Provider enable or disable, onboarding, consent callback, verification start, credential mutation, and health-check writers all persist canonical state and diagnostics only.
  • ProviderConnectionResource, TenantResource, the shared provider-state Blade entry, and system directory pages lose legacy provider status and health fields, filters, and badge domains.
  • Factories, Pest helpers, and targeted regression suites are updated before the final drop migration lands.

Phase 1 — Agent Context Update

Run after artifact generation:

  • .specify/scripts/bash/update-agent-context.sh copilot

Project Structure

Documentation (this feature)

specs/188-provider-connection-state-cleanup/
├── spec.md
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│   └── provider-connection-state-cleanup.openapi.yaml
├── checklists/
│   └── requirements.md
└── tasks.md

Source Code (repository root)

apps/platform/
├── app/
│   ├── Filament/
│   │   ├── Pages/
│   │   │   └── Workspaces/
│   │   │       └── ManagedTenantOnboardingWizard.php
│   │   ├── Resources/
│   │   │   ├── ProviderConnectionResource.php
│   │   │   └── TenantResource.php
│   │   └── System/
│   │       └── Pages/
│   │           └── Directory/
│   │               ├── Tenants.php
│   │               └── ViewTenant.php
│   ├── Http/
│   │   └── Controllers/
│   │       ├── AdminConsentCallbackController.php
│   │       └── TenantOnboardingController.php
│   ├── Jobs/
│   │   ├── ProviderConnectionHealthCheckJob.php
│   │   └── ScanEntraAdminRolesJob.php
│   ├── Models/
│   │   └── ProviderConnection.php
│   ├── Services/
│   │   ├── Providers/
│   │   │   ├── Contracts/
│   │   │   │   └── HealthResult.php
│   │   │   ├── MicrosoftProviderHealthCheck.php
│   │   │   ├── ProviderConnectionMutationService.php
│   │   │   ├── ProviderConnectionResolver.php
│   │   │   └── ProviderConnectionStateProjector.php
│   │   └── Verification/
│   │       └── StartVerification.php
│   └── Support/
│       └── Badges/
│           ├── BadgeCatalog.php
│           ├── BadgeDomain.php
│           └── Domains/
│               ├── BooleanEnabledBadge.php
│               ├── ProviderConnectionHealthBadge.php
│               └── ProviderConnectionStatusBadge.php
├── database/
│   ├── factories/
│   │   └── ProviderConnectionFactory.php
│   └── migrations/
│       ├── 2026_01_24_000001_create_provider_connections_table.php
│       ├── 2026_02_04_090020_make_provider_connections_workspace_owned.php
│       ├── 2026_03_13_000001_add_provider_identity_fields_to_provider_connections.php
│       ├── 2026_04_09_000001_add_is_enabled_to_provider_connections.php
│       └── 2026_04_09_000002_drop_legacy_provider_state_columns_from_provider_connections.php
├── resources/
│   └── views/
│       └── filament/
│           ├── infolists/
│           │   └── entries/
│           │       └── provider-connection-state.blade.php
│           └── system/
│               └── pages/
│                   └── directory/
│                       └── view-tenant.blade.php
└── tests/
    ├── Feature/
    │   ├── Audit/
    │   │   ├── ProviderConnectionConsentAuditTest.php
    │   │   ├── ProviderConnectionConsentRevocationAuditTest.php
    │   │   └── ProviderConnectionVerificationAuditTest.php
    │   ├── EntraAdminRoles/
    │   │   └── ScanEntraAdminRolesJobTest.php
    │   ├── Filament/
    │   │   └── ProviderConnectionsDbOnlyTest.php
    │   ├── Guards/
    │   │   └── NoLegacyProviderConnectionStateFallbackTest.php
    │   ├── Onboarding/
    │   │   ├── OnboardingProviderConnectionPlatformDefaultTest.php
    │   │   └── OnboardingProviderConnectionTest.php
    │   ├── ProviderConnections/
    │   │   ├── ProviderConnectionEnableDisableTest.php
    │   │   ├── ProviderConnectionHealthCheckJobTest.php
    │   │   ├── ProviderConnectionHealthCheckStartSurfaceTest.php
    │   │   ├── ProviderConnectionListAuthorizationTest.php
    │   │   ├── ProviderConnectionTruthCleanupSpec179Test.php
    │   │   ├── ProviderConnectionViewsDbOnlyRenderingSpec081Test.php
    │   │   └── ProviderConnectionAuthorizationTest.php
    │   ├── Verification/
    │   │   └── ProviderConnectionHealthCheckWritesReportTest.php
    │   ├── ManagedTenantOnboardingWizardTest.php
    │   └── Tenants/
    │       └── TenantProviderConnectionsCtaTest.php
    └── Unit/
        ├── Badges/
        │   ├── BooleanEnabledBadgesTest.php
        │   └── ProviderConnectionBadgesTest.php
        └── Providers/
            ├── ProviderConnectionBadgeMappingTest.php
            └── ProviderConnectionClassifierTest.php

Structure Decision: Keep the feature entirely inside the existing Laravel/Filament monolith. Update the current model, services, resources, shared Blade views, badge registry, migrations, and focused test files instead of introducing a new provider-state subsystem.

Complexity Tracking

No Constitution Check violation is planned. The feature does trigger proportionality review because it changes persisted truth and the provider-state surface contract.

Violation Why Needed Simpler Alternative Rejected Because

Proportionality Review

  • Current operator problem: The same provider connection can still read as disabled, connected, healthy, consented, or verified depending on which legacy field a runtime path or surface reads, which makes provider state unreliable.
  • Existing structure is insufficient because: consent_status and verification_status already exist, but legacy status and health_status still persist, still drive runtime gates, and still appear in admin and system surfaces.
  • Narrowest correct implementation: Add one explicit lifecycle truth, is_enabled, remove the two legacy columns, and repoint all affected readers, writers, badges, helpers, and tests to lifecycle, consent, and verification directly.
  • Ownership cost created: One migration pair, focused updates across existing services and resources, badge cleanup, shared helper cleanup, and a targeted regression and residual-guard suite.
  • Alternative intentionally rejected: Dual-read or dual-write compatibility shims, a new provider readiness framework, and keeping legacy badge domains as diagnostics.
  • Release truth: Current-release truth cleanup.

Implementation Strategy

Phase A — Add Canonical Lifecycle Truth And Prepare The Model

Goal: Introduce the narrow lifecycle field needed to preserve enabled or disabled behavior without touching removed columns yet.

Step File Change
A.1 apps/platform/database/migrations/2026_04_09_000001_add_is_enabled_to_provider_connections.php Add is_enabled to provider_connections, default it to true, and backfill false where legacy status = disabled.
A.2 apps/platform/app/Models/ProviderConnection.php Add the new lifecycle field to the model contract and stop any model helper from treating status or health_status as canonical state.
A.3 apps/platform/tests/Feature/ProviderConnections/ProviderConnectionEnableDisableTest.php Extend lifecycle tests so the new field becomes the canonical enable or disable truth immediately.

Phase B — Move Runtime Readers Off Legacy Provider State

Goal: Make all canonical readers safe before any writer stops populating legacy columns. This phase includes shared helper, operator-surface, and runtime-gate reads that establish the reader-cutover gate for the hard cut.

Step File Change
B.1 apps/platform/app/Services/Providers/ProviderConnectionResolver.php Replace status === disabled and status === needs_consent fallbacks with is_enabled, consent_status, and verification_status.
B.2 apps/platform/app/Jobs/ScanEntraAdminRolesJob.php Stop filtering on status = connected; gate on canonical lifecycle and consent rules instead.
B.3 apps/platform/app/Filament/Resources/ProviderConnectionResource.php Move action visibility, list filters, and detail context from legacy status or health_status to lifecycle, consent, and verification.
B.4 apps/platform/app/Filament/Resources/TenantResource.php, apps/platform/app/Filament/System/Pages/Directory/Tenants.php, and apps/platform/app/Filament/System/Pages/Directory/ViewTenant.php Stop tenant and system summaries from reading legacy provider columns.
B.5 apps/platform/tests/Feature/ProviderConnections/ProviderConnectionHealthCheckStartSurfaceTest.php, apps/platform/tests/Feature/ProviderConnections/ProviderConnectionListAuthorizationTest.php, apps/platform/tests/Feature/ProviderConnections/ProviderConnectionAuthorizationTest.php, and apps/platform/tests/Feature/EntraAdminRoles/ScanEntraAdminRolesJobTest.php Prove canonical reader cutover before writers change.

Phase C — Move Runtime Writers And Internal Contracts To Canonical State

Goal: Stop all runtime writes and transport contracts from recreating removed legacy fields.

Step File Change
C.1 apps/platform/app/Services/Providers/Contracts/HealthResult.php and apps/platform/app/Services/Providers/MicrosoftProviderHealthCheck.php Replace legacy transport fields with canonical verification output and diagnostics.
C.2 apps/platform/app/Services/Providers/ProviderConnectionStateProjector.php Remove legacy project() and projectForConnection() responsibilities and keep only canonical verification-outcome derivation if it remains shared.
C.3 apps/platform/app/Jobs/ProviderConnectionHealthCheckJob.php and apps/platform/app/Services/Verification/StartVerification.php Persist lifecycle, consent, verification, and diagnostics only.
C.4 apps/platform/app/Http/Controllers/AdminConsentCallbackController.php, apps/platform/app/Http/Controllers/TenantOnboardingController.php, apps/platform/app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php, and apps/platform/app/Services/Providers/ProviderConnectionMutationService.php Remove dual-write behavior and reset canonical verification truth appropriately when onboarding, consent, or credential mutations occur.
C.5 apps/platform/tests/Feature/ProviderConnections/MvpProviderScopeTest.php, apps/platform/tests/Feature/ProviderConnections/ProviderConnectionHealthCheckJobTest.php, apps/platform/tests/Feature/Verification/ProviderConnectionHealthCheckWritesReportTest.php, apps/platform/tests/Feature/Onboarding/OnboardingProviderConnectionTest.php, apps/platform/tests/Feature/Onboarding/OnboardingProviderConnectionPlatformDefaultTest.php, and apps/platform/tests/Feature/ManagedTenantOnboardingWizardTest.php Prove create-path coverage, writer cutover, and contract cleanup.

Phase D — Remove Legacy Presentation And Badge Semantics

Goal: Finalize operator-facing labels, summaries, and badge semantics after the reader and writer cutovers are already in place. This phase is presentation cleanup, not a replacement for the earlier reader-cutover gate.

Step File Change
D.1 apps/platform/app/Filament/Resources/ProviderConnectionResource.php Replace legacy status and health sections, columns, and diagnostic filters with lifecycle, consent, and verification presentation.
D.2 apps/platform/app/Filament/Resources/TenantResource.php and apps/platform/resources/views/filament/infolists/entries/provider-connection-state.blade.php Change the shared tenant provider summary contract so it exposes only lifecycle, consent, verification, and diagnostics.
D.3 apps/platform/app/Filament/System/Pages/Directory/Tenants.php, apps/platform/app/Filament/System/Pages/Directory/ViewTenant.php, and apps/platform/resources/views/filament/system/pages/directory/view-tenant.blade.php Recompute system health rollups and provider rows from canonical verification and permission truth, counting only each tenant's default Microsoft connection in list rollups while keeping detail rows read-only and canonical.
D.4 apps/platform/app/Support/Badges/BadgeDomain.php, apps/platform/app/Support/Badges/BadgeCatalog.php, apps/platform/app/Support/Badges/Domains/ProviderConnectionStatusBadge.php, and apps/platform/app/Support/Badges/Domains/ProviderConnectionHealthBadge.php Retire legacy provider badge domains and reuse BooleanEnabled, provider consent, and provider verification badge mappings.
D.5 apps/platform/tests/Feature/ProviderConnections/ProviderConnectionTruthCleanupSpec179Test.php, apps/platform/tests/Feature/Filament/ProviderConnectionsDbOnlyTest.php, apps/platform/tests/Feature/ProviderConnections/ProviderConnectionViewsDbOnlyRenderingSpec081Test.php, apps/platform/tests/Feature/Filament/TenantGlobalSearchLifecycleScopeTest.php, apps/platform/tests/Feature/Filament/TenantScopingTest.php, apps/platform/tests/Feature/Tenants/TenantProviderConnectionsCtaTest.php, apps/platform/tests/Unit/Badges/BooleanEnabledBadgesTest.php, apps/platform/tests/Unit/Badges/BadgeCatalogTest.php, apps/platform/tests/Unit/Badges/ProviderConnectionBadgesTest.php, and apps/platform/tests/Unit/Providers/ProviderConnectionBadgeMappingTest.php Prove canonical presentation, search safety, DB-only rendering, and centralized badge semantics.

Phase E — Update Shared Test Scaffolding And Add Residual Guards

Goal: Prevent helpers and future regressions from recreating removed provider-state truth.

Step File Change
E.1 apps/platform/database/factories/ProviderConnectionFactory.php and apps/platform/tests/Pest.php Stop creating provider fixtures with legacy status or health_status values and move default healthy fixtures onto lifecycle, consent, and verification only.
E.2 apps/platform/tests/Feature/ProviderConnections/ProviderConnectionEnableDisableTest.php and related provider truth tests Add contradiction scenarios such as disabled plus granted consent and disabled plus verification_status = healthy.
E.3 apps/platform/tests/Feature/Guards/NoLegacyProviderConnectionStateFallbackTest.php Fail if targeted provider-state runtime files still reference removed provider columns or legacy provider badge domains.

Phase F — Remove Legacy Columns And Finish The Hard Cut

Goal: Complete the schema cleanup only after runtime, UI, and test scaffolding no longer depend on the removed fields.

Step File Change
F.1 apps/platform/database/migrations/2026_04_09_000002_drop_legacy_provider_state_columns_from_provider_connections.php Drop status, health_status, and their indexes from provider_connections.
F.2 apps/platform/app/Models/ProviderConnection.php and any remaining touched files Remove any leftover references to the removed columns, including diagnostic accessors, audit metadata keys, and helper-array comments.
F.3 Focused regression pack from quickstart.md Re-run runtime, surface, badge, onboarding, audit, and residual-guard coverage after the drop migration.

Post-Design Constitution Re-Check

The design still passes the Constitution after Phase 1:

  • It removes redundant truth instead of adding layers.
  • It introduces one narrow persisted lifecycle fact because current behavior requires it.
  • It keeps existing authorization, confirmation, and audit rules intact.
  • It keeps Filament v5 and Livewire v4 compliance.
  • It requires no panel-provider registration change in bootstrap/providers.php.
  • It keeps ProviderConnectionResource non-globally-searchable and leaves tenant global search unchanged.
  • It requires no new asset registration or filament:assets deployment work.
  • It keeps testing focused on real provider behavior rather than new indirection.