## 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
219 lines
8.0 KiB
PHP
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];
|
|
}
|
|
}
|