TenantAtlas/apps/platform/app/Services/Providers/ProviderConnectionStateProjector.php
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

142 lines
5.2 KiB
PHP

<?php
namespace App\Services\Providers;
use App\Models\ProviderConnection;
use App\Services\Providers\Contracts\HealthResult;
use App\Support\Providers\ProviderConsentStatus;
use App\Support\Providers\ProviderReasonCodes;
use App\Support\Providers\ProviderVerificationStatus;
final class ProviderConnectionStateProjector
{
/**
* @return array{
* consent_status: ProviderConsentStatus,
* verification_status: ProviderVerificationStatus,
* last_error_reason_code: ?string,
* last_error_message: ?string,
* consent_error_code: ?string,
* consent_error_message: ?string,
* consent_revoked_detected: bool
* }
*/
public function projectVerificationOutcome(ProviderConnection $connection, HealthResult $result): array
{
$currentConsentStatus = $this->normalizeConsentStatus($connection->consent_status) ?? ProviderConsentStatus::Unknown;
$effectiveReasonCode = $result->healthy
? null
: $this->effectiveReasonCodeForVerification($currentConsentStatus, $result->reasonCode);
$consentStatus = $this->consentStatusAfterVerification($currentConsentStatus, $effectiveReasonCode, $result->healthy);
$verificationStatus = $this->verificationStatusAfterVerification($effectiveReasonCode, $result);
$consentErrorCode = in_array($consentStatus, [
ProviderConsentStatus::Required,
ProviderConsentStatus::Failed,
ProviderConsentStatus::Revoked,
], true) ? $effectiveReasonCode : null;
return [
'consent_status' => $consentStatus,
'verification_status' => $verificationStatus,
'last_error_reason_code' => $effectiveReasonCode,
'last_error_message' => $result->healthy ? null : $result->message,
'consent_error_code' => $consentErrorCode,
'consent_error_message' => $consentErrorCode === null || $result->healthy ? null : $result->message,
'consent_revoked_detected' => $currentConsentStatus === ProviderConsentStatus::Granted
&& $effectiveReasonCode === ProviderReasonCodes::ProviderConsentRevoked,
];
}
private function normalizeConsentStatus(ProviderConsentStatus|string|null $consentStatus): ?ProviderConsentStatus
{
if ($consentStatus instanceof ProviderConsentStatus) {
return $consentStatus;
}
if (! is_string($consentStatus)) {
return null;
}
return ProviderConsentStatus::tryFrom(trim($consentStatus));
}
private function normalizeVerificationStatus(
ProviderVerificationStatus|string|null $verificationStatus,
): ?ProviderVerificationStatus {
if ($verificationStatus instanceof ProviderVerificationStatus) {
return $verificationStatus;
}
if (! is_string($verificationStatus)) {
return null;
}
return ProviderVerificationStatus::tryFrom(trim($verificationStatus));
}
private function effectiveReasonCodeForVerification(
ProviderConsentStatus $currentConsentStatus,
?string $reasonCode,
): ?string {
if (! is_string($reasonCode) || $reasonCode === '') {
return null;
}
if (
$currentConsentStatus === ProviderConsentStatus::Granted
&& $reasonCode === ProviderReasonCodes::ProviderConsentMissing
) {
return ProviderReasonCodes::ProviderConsentRevoked;
}
return $reasonCode;
}
private function consentStatusAfterVerification(
ProviderConsentStatus $currentConsentStatus,
?string $reasonCode,
bool $healthy,
): ProviderConsentStatus {
if ($healthy) {
return ProviderConsentStatus::Granted;
}
return match ($reasonCode) {
ProviderReasonCodes::ProviderConsentMissing => ProviderConsentStatus::Required,
ProviderReasonCodes::ProviderConsentFailed => ProviderConsentStatus::Failed,
ProviderReasonCodes::ProviderConsentRevoked => ProviderConsentStatus::Revoked,
default => $currentConsentStatus,
};
}
private function verificationStatusAfterVerification(
?string $reasonCode,
HealthResult $result,
): ProviderVerificationStatus {
if ($result->healthy) {
return ProviderVerificationStatus::Healthy;
}
if (in_array($reasonCode, [
ProviderReasonCodes::ProviderConsentMissing,
ProviderReasonCodes::ProviderConsentFailed,
ProviderReasonCodes::ProviderConsentRevoked,
ProviderReasonCodes::PlatformIdentityMissing,
ProviderReasonCodes::PlatformIdentityIncomplete,
ProviderReasonCodes::DedicatedCredentialMissing,
ProviderReasonCodes::DedicatedCredentialInvalid,
ProviderReasonCodes::ProviderConnectionInvalid,
ProviderReasonCodes::ProviderConnectionReviewRequired,
ProviderReasonCodes::ProviderConnectionTypeInvalid,
ProviderReasonCodes::TenantTargetMismatch,
], true)) {
return ProviderVerificationStatus::Blocked;
}
return $this->normalizeVerificationStatus($result->verificationStatus) ?? ProviderVerificationStatus::Error;
}
}