TenantAtlas/apps/platform/app/Support/Providers/TargetScope/ProviderConnectionSurfaceSummary.php
ahmido 1debe40ced feat: implement provider capability registry (#342)
## Summary
- implement the provider capability registry and derived capability evaluation flow
- update provider connections, onboarding, required-permissions diagnostics, and provider blocker translation to use capability-first summaries
- add bounded unit, feature, and browser test coverage plus the prepared Spec 283 artifacts

## Notes
- branch: `283-provider-capability-registry`
- commit: `74e75c3e`
- no additional validation commands were run in this git/PR flow step

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #342
2026-05-08 21:17:05 +00:00

219 lines
8.0 KiB
PHP

<?php
namespace App\Support\Providers\TargetScope;
use App\Models\ProviderConnection;
use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer;
use App\Support\Providers\Capabilities\ProviderCapabilityEvaluator;
use App\Support\Providers\Capabilities\ProviderCapabilityResult;
use App\Support\Providers\ProviderConsentStatus;
use App\Support\Providers\ProviderVerificationStatus;
final class ProviderConnectionSurfaceSummary
{
/**
* @param list<ProviderIdentityContextMetadata> $contextualIdentityDetails
*/
public function __construct(
public readonly string $provider,
public readonly ProviderConnectionTargetScopeDescriptor $targetScope,
public readonly string $consentState,
public readonly string $verificationState,
public readonly string $readinessSummary,
public readonly array $contextualIdentityDetails = [],
public readonly bool $isEnabled = true,
public readonly array $providerCapabilities = [],
public readonly ?array $primaryProviderCapability = null,
) {}
public static function forConnection(ProviderConnection $connection): self
{
/** @var ProviderConnectionTargetScopeNormalizer $normalizer */
$normalizer = app(ProviderConnectionTargetScopeNormalizer::class);
$targetScope = $normalizer->descriptorForConnection($connection);
$consentState = self::stateValue($connection->consent_status);
$verificationState = self::stateValue($connection->verification_status);
$providerCapabilities = self::providerCapabilitiesForConnection($connection);
$primaryProviderCapability = self::primaryProviderCapability($providerCapabilities);
return new self(
provider: trim((string) $connection->provider),
targetScope: $targetScope,
consentState: $consentState,
verificationState: $verificationState,
readinessSummary: self::readinessSummary(
isEnabled: (bool) $connection->is_enabled,
consentState: $consentState,
verificationState: $verificationState,
),
contextualIdentityDetails: $normalizer->contextualIdentityDetailsForConnection($connection),
isEnabled: (bool) $connection->is_enabled,
providerCapabilities: $providerCapabilities,
primaryProviderCapability: $primaryProviderCapability,
);
}
public function targetScopeSummary(): string
{
return $this->targetScope->summary();
}
public function contextualIdentityLine(): ?string
{
if ($this->contextualIdentityDetails === []) {
return null;
}
return collect($this->contextualIdentityDetails)
->map(fn (ProviderIdentityContextMetadata $detail): string => sprintf('%s: %s', $detail->detailLabel, $detail->detailValue))
->implode("\n");
}
public function providerCapabilitySummary(): string
{
if (! is_array($this->primaryProviderCapability)) {
return 'Provider capabilities not evaluated';
}
$label = (string) ($this->primaryProviderCapability['label'] ?? 'Provider capability');
$status = (string) ($this->primaryProviderCapability['status'] ?? 'unknown');
$statusLabel = BadgeRenderer::spec(BadgeDomain::ProviderCapabilityStatus, $status)->label;
return "{$label}: {$statusLabel}";
}
/**
* @return array{
* provider: string,
* target_scope: array<string, string>,
* consent_state: string,
* verification_state: string,
* readiness_summary: string,
* target_scope_summary: string,
* provider_context: array{provider: string, details: list<array<string, string>>},
* contextual_identity_line: ?string,
* is_enabled: bool,
* provider_capabilities: array<int, array<string, mixed>>,
* primary_provider_capability: ?array<string, mixed>,
* provider_capability_summary: string
* }
*/
public function toArray(): array
{
return [
'provider' => $this->provider,
'target_scope' => $this->targetScope->toArray(),
'consent_state' => $this->consentState,
'verification_state' => $this->verificationState,
'readiness_summary' => $this->readinessSummary,
'target_scope_summary' => $this->targetScopeSummary(),
'provider_context' => $this->providerContext(),
'contextual_identity_line' => $this->contextualIdentityLine(),
'is_enabled' => $this->isEnabled,
'provider_capabilities' => $this->providerCapabilities,
'primary_provider_capability' => $this->primaryProviderCapability,
'provider_capability_summary' => $this->providerCapabilitySummary(),
];
}
/**
* @return array{provider: string, details: list<array<string, string>>}
*/
public function providerContext(): array
{
return [
'provider' => $this->provider,
'details' => array_map(
static fn (ProviderIdentityContextMetadata $detail): array => $detail->toArray(),
$this->contextualIdentityDetails,
),
];
}
private static function stateValue(mixed $state): string
{
if ($state instanceof ProviderConsentStatus || $state instanceof ProviderVerificationStatus) {
return $state->value;
}
return trim((string) $state);
}
private static function readinessSummary(bool $isEnabled, string $consentState, string $verificationState): string
{
if (! $isEnabled) {
return 'Disabled';
}
if ($consentState !== ProviderConsentStatus::Granted->value) {
return sprintf(
'Consent %s',
strtolower(BadgeRenderer::spec(BadgeDomain::ProviderConsentStatus, $consentState)->label),
);
}
return match ($verificationState) {
ProviderVerificationStatus::Healthy->value => 'Ready',
ProviderVerificationStatus::Degraded->value => 'Ready with warnings',
ProviderVerificationStatus::Blocked->value => 'Verification blocked',
ProviderVerificationStatus::Error->value => 'Verification failed',
ProviderVerificationStatus::Pending->value => 'Verification pending',
default => 'Verification not run',
};
}
/**
* @return array<int, array<string, mixed>>
*/
private static function providerCapabilitiesForConnection(ProviderConnection $connection): array
{
try {
/** @var ProviderCapabilityEvaluator $evaluator */
$evaluator = app(ProviderCapabilityEvaluator::class);
return array_map(
static fn (ProviderCapabilityResult $result): array => $result->toArray(),
$evaluator->evaluateForConnection($connection),
);
} catch (\Throwable) {
return [];
}
}
/**
* @param array<int, array<string, mixed>> $capabilities
* @return array<string, mixed>|null
*/
private static function primaryProviderCapability(array $capabilities): ?array
{
if ($capabilities === []) {
return null;
}
usort($capabilities, static function (array $a, array $b): int {
$aStatus = (string) ($a['status'] ?? 'unknown');
$bStatus = (string) ($b['status'] ?? 'unknown');
$aPriority = match ($aStatus) {
'blocked' => 0,
'missing' => 1,
'unknown' => 2,
'supported' => 3,
default => 4,
};
$bPriority = match ($bStatus) {
'blocked' => 0,
'missing' => 1,
'unknown' => 2,
'supported' => 3,
default => 4,
};
return $aPriority <=> $bPriority;
});
return $capabilities[0];
}
}