,status:'granted'|'missing'|'blocked'|'expired'|'unknown'|'not_applicable',details:array|null} * @phpstan-type FeatureImpact array{feature:string,missing:int,required_application:int,required_delegated:int,blocked:bool} * @phpstan-type CapabilityGroup array{provider_capability_key:string,label:string,status:string,provider_requirement_keys:array,missing_requirement_keys:array,evidence_counts:array{requirements:int,missing:int,errors:int},message:string} * @phpstan-type FilterState array{status:'missing'|'granted'|'all',type:'application'|'delegated'|'all',features:array,search:string} * @phpstan-type ViewModel array{ * tenant: array{id:int,external_id:string,name:string}, * overview: array{ * overall: string, * counts: array, * feature_impacts: array, * capability_groups: array, * primary_capability_group: CapabilityGroup|null, * freshness: array{last_refreshed_at:?string,is_stale:bool} * }, * permissions: array, * filters: FilterState, * copy: array{application:string,delegated:string} * } */ public function __construct(private readonly ManagedEnvironmentPermissionService $permissionService) {} /** * @param array $filters * @return ViewModel */ public function build(ManagedEnvironment $tenant, array $filters = []): array { $comparison = $this->readinessComparison($tenant); /** @var array $allPermissions */ $allPermissions = collect($comparison['permissions'] ?? []) ->filter(fn (mixed $row): bool => is_array($row)) ->map(fn (array $row): array => self::normalizePermissionRow($row)) ->values() ->all(); $state = self::normalizeFilterState($filters); $filteredPermissions = self::applyFilterState($allPermissions, $state); $freshness = is_array($comparison['freshness'] ?? null) ? array_replace([ 'last_refreshed_at' => null, 'is_stale' => true, ], $comparison['freshness']) : self::deriveFreshness(self::parseLastRefreshedAt($comparison['last_refreshed_at'] ?? null)); $summaryPermissions = $allPermissions; $capabilityGroups = self::deriveCapabilityGroups($allPermissions, $freshness); return [ 'tenant' => [ 'id' => (int) $tenant->getKey(), 'external_id' => (string) $tenant->external_id, 'name' => (string) $tenant->name, ], 'overview' => [ 'overall' => self::deriveOverallStatus($summaryPermissions, (bool) ($freshness['is_stale'] ?? true)), 'counts' => self::deriveCounts($summaryPermissions), 'feature_impacts' => self::deriveFeatureImpacts($summaryPermissions), 'capability_groups' => $capabilityGroups, 'primary_capability_group' => self::primaryCapabilityGroup($capabilityGroups), 'freshness' => $freshness, ], 'permissions' => $filteredPermissions, 'filters' => $state, 'copy' => [ 'application' => self::deriveCopyPayload($allPermissions, 'application', $state['features']), 'delegated' => self::deriveCopyPayload($allPermissions, 'delegated', $state['features']), ], ]; } /** * @param array $permissions * @param array{last_refreshed_at:?string,is_stale:bool} $freshness * @return array */ public static function deriveCapabilityGroups(array $permissions, array $freshness): array { /** @var ProviderCapabilityRegistry $registry */ $registry = app(ProviderCapabilityRegistry::class); return array_map( static fn (ProviderCapabilityDefinition $definition): array => self::deriveCapabilityGroup( definition: $definition, permissions: $permissions, isStale: (bool) ($freshness['is_stale'] ?? true), ), array_values($registry->all()), ); } /** * @param array $groups * @return CapabilityGroup|null */ public static function primaryCapabilityGroup(array $groups): ?array { if ($groups === []) { return null; } usort($groups, static function (array $a, array $b): int { $aStatus = ProviderCapabilityStatus::tryFrom((string) ($a['status'] ?? 'unknown')) ?? ProviderCapabilityStatus::Unknown; $bStatus = ProviderCapabilityStatus::tryFrom((string) ($b['status'] ?? 'unknown')) ?? ProviderCapabilityStatus::Unknown; return $aStatus->priority() <=> $bStatus->priority(); }); return $groups[0]; } /** * @param array $permissions * @return CapabilityGroup */ private static function deriveCapabilityGroup( ProviderCapabilityDefinition $definition, array $permissions, bool $isStale, ): array { $rowsByRequirement = []; foreach ($definition->providerRequirementKeys as $requirementKey) { $rowsByRequirement[$requirementKey] = ManagedEnvironmentPermissionCheckClusters::rowsForRequirementKey($permissions, $requirementKey); } $rows = array_values(array_merge(...array_values($rowsByRequirement ?: [[]]))); $missingRows = array_values(array_filter( $rows, static fn (array $row): bool => ($row['status'] ?? null) === 'missing', )); $blockedRows = array_values(array_filter( $rows, static fn (array $row): bool => ($row['status'] ?? null) === 'blocked', )); $expiredRows = array_values(array_filter( $rows, static fn (array $row): bool => ($row['status'] ?? null) === 'expired', )); $unknownRows = array_values(array_filter( $rows, static fn (array $row): bool => ($row['status'] ?? null) === 'unknown', )); $missingRequirementKeys = []; foreach ($rowsByRequirement as $requirementKey => $requirementRows) { foreach ($requirementRows as $row) { if (in_array(($row['status'] ?? null), ['missing', 'blocked', 'expired', 'unknown'], true)) { $missingRequirementKeys[] = (string) $requirementKey; break; } } } $status = match (true) { $rows === [] => ProviderCapabilityStatus::NotApplicable, $blockedRows !== [] => ProviderCapabilityStatus::Blocked, $missingRows !== [] => ProviderCapabilityStatus::Missing, $expiredRows !== [] || $unknownRows !== [] => ProviderCapabilityStatus::Unknown, $isStale => ProviderCapabilityStatus::Unknown, default => ProviderCapabilityStatus::Supported, }; $message = match ($status) { ProviderCapabilityStatus::Supported => "{$definition->label} capability is supported by stored permission evidence.", ProviderCapabilityStatus::Missing => "{$definition->label} capability is missing required provider permissions.", ProviderCapabilityStatus::Unknown => "{$definition->label} capability needs refreshed permission evidence.", ProviderCapabilityStatus::Blocked => "{$definition->label} capability is blocked.", ProviderCapabilityStatus::NotApplicable => "{$definition->label} capability has no mapped permission rows for this tenant.", }; return [ 'provider_capability_key' => $definition->key, 'label' => $definition->label, 'status' => $status->value, 'provider_requirement_keys' => $definition->providerRequirementKeys, 'missing_requirement_keys' => array_values(array_unique($missingRequirementKeys)), 'evidence_counts' => [ 'requirements' => count($rows), 'missing' => count($missingRows), 'errors' => 0, 'blocked' => count($blockedRows), 'expired' => count($expiredRows), 'unknown' => count($unknownRows), ], 'message' => $message, ]; } /** * @param array $permissions */ public static function deriveOverallStatus(array $permissions, bool $hasStaleFreshness = false): string { $hasBlockedApplication = collect($permissions)->contains( fn (array $row): bool => in_array($row['status'], ['missing', 'blocked'], true) && $row['type'] === 'application', ); if ($hasBlockedApplication) { return VerificationReportOverall::Blocked->value; } $hasNeedsReview = collect($permissions)->contains( fn (array $row): bool => in_array($row['status'], ['expired', 'unknown'], true), ); $hasMissingDelegated = collect($permissions)->contains( fn (array $row): bool => $row['status'] === 'missing' && $row['type'] === 'delegated', ); if ($hasNeedsReview || $hasMissingDelegated || $hasStaleFreshness) { return VerificationReportOverall::NeedsAttention->value; } return VerificationReportOverall::Ready->value; } /** * @return array{last_refreshed_at:?string,is_stale:bool} */ public static function deriveFreshness(?CarbonInterface $lastRefreshedAt, ?CarbonInterface $referenceTime = null): array { $reference = $referenceTime instanceof Carbon ? $referenceTime->copy() : ($referenceTime !== null ? Carbon::instance($referenceTime) : now()); $lastRefreshed = $lastRefreshedAt instanceof Carbon ? $lastRefreshedAt : ($lastRefreshedAt !== null ? Carbon::instance($lastRefreshedAt) : null); $isStale = $lastRefreshed === null || $lastRefreshed->lt($reference->copy()->subDays(30)); return [ 'last_refreshed_at' => $lastRefreshed?->toIso8601String(), 'is_stale' => $isStale, ]; } /** * @param array $permissions * @return array */ public static function deriveCounts(array $permissions): array { $counts = [ 'missing_application' => 0, 'missing_delegated' => 0, 'missing' => 0, 'granted' => 0, 'blocked' => 0, 'expired' => 0, 'unknown' => 0, 'not_applicable' => 0, 'required' => 0, ]; foreach ($permissions as $row) { if (($row['status'] ?? null) === 'missing') { if (($row['type'] ?? null) === 'delegated') { $counts['missing_delegated'] += 1; } else { $counts['missing_application'] += 1; } $counts['missing'] += 1; continue; } if (($row['status'] ?? null) === 'granted') { $counts['granted'] += 1; continue; } if (in_array(($row['status'] ?? null), ['blocked', 'expired', 'unknown', 'not_applicable'], true)) { $counts[(string) $row['status']] += 1; continue; } if (($row['status'] ?? null) === 'error') { $counts['unknown'] += 1; } } $counts['required'] = $counts['missing'] + $counts['granted'] + $counts['blocked'] + $counts['expired'] + $counts['unknown']; return $counts; } /** * @param array $permissions * @return array */ public static function deriveFeatureImpacts(array $permissions): array { /** @var array $impacts */ $impacts = []; foreach ($permissions as $row) { $features = array_values(array_unique($row['features'] ?? [])); foreach ($features as $feature) { if (! isset($impacts[$feature])) { $impacts[$feature] = [ 'feature' => $feature, 'missing' => 0, 'required_application' => 0, 'required_delegated' => 0, 'blocked' => false, ]; } if (($row['type'] ?? null) === 'delegated') { $impacts[$feature]['required_delegated'] += 1; } else { $impacts[$feature]['required_application'] += 1; } if (in_array(($row['status'] ?? null), ['missing', 'blocked', 'expired', 'unknown'], true)) { $impacts[$feature]['missing'] += 1; if (($row['type'] ?? null) === 'application' && in_array(($row['status'] ?? null), ['missing', 'blocked'], true)) { $impacts[$feature]['blocked'] = true; } } } } $values = array_values($impacts); usort($values, static function (array $a, array $b): int { $blocked = (int) ($b['blocked'] <=> $a['blocked']); if ($blocked !== 0) { return $blocked; } $missing = (int) (($b['missing'] ?? 0) <=> ($a['missing'] ?? 0)); if ($missing !== 0) { return $missing; } return strcmp((string) ($a['feature'] ?? ''), (string) ($b['feature'] ?? '')); }); return $values; } /** * Copy payload semantics: * - Always Missing-only * - Always Type fixed by button (application vs delegated) * - Respects Feature filter only * - Ignores Search * * @param array $permissions * @param 'application'|'delegated' $type * @param array $featureFilter */ public static function deriveCopyPayload(array $permissions, string $type, array $featureFilter = []): string { $featureFilter = array_values(array_unique(array_filter(array_map('strval', $featureFilter)))); $payload = collect($permissions) ->filter(function (array $row) use ($type, $featureFilter): bool { if (($row['status'] ?? null) !== 'missing') { return false; } if (($row['type'] ?? null) !== $type) { return false; } if ($featureFilter === []) { return true; } $rowFeatures = $row['features'] ?? []; return count(array_intersect($featureFilter, $rowFeatures)) > 0; }) ->pluck('key') ->map(fn (mixed $key): string => (string) $key) ->filter() ->unique() ->sort() ->values() ->all(); return implode("\n", $payload); } /** * @param array $permissions * @return array */ public static function applyFilterState(array $permissions, array $state): array { $status = $state['status'] ?? 'missing'; $type = $state['type'] ?? 'all'; $features = $state['features'] ?? []; $search = $state['search'] ?? ''; $search = is_string($search) ? trim($search) : ''; $searchLower = strtolower($search); $features = array_values(array_unique(array_filter(array_map('strval', $features)))); $filtered = collect($permissions) ->filter(function (array $row) use ($status, $type, $features): bool { $rowStatus = $row['status'] ?? null; $rowType = $row['type'] ?? null; if ($status === 'missing' && ! in_array($rowStatus, ['missing', 'blocked', 'expired', 'unknown'], true)) { return false; } if ($status === 'granted' && $rowStatus !== 'granted') { return false; } if ($type !== 'all' && $rowType !== $type) { return false; } if ($features === []) { return true; } $rowFeatures = $row['features'] ?? []; return count(array_intersect($features, $rowFeatures)) > 0; }) ->when($searchLower !== '', function ($collection) use ($searchLower) { return $collection->filter(function (array $row) use ($searchLower): bool { $key = strtolower((string) ($row['key'] ?? '')); $description = strtolower((string) ($row['description'] ?? '')); return str_contains($key, $searchLower) || ($description !== '' && str_contains($description, $searchLower)); }); }) ->values() ->all(); usort($filtered, static function (array $a, array $b): int { $weight = static function (array $row): int { return match ($row['status'] ?? null) { 'missing' => 0, 'blocked' => 1, 'expired' => 2, 'unknown' => 3, 'granted' => 4, default => 5, }; }; $cmp = $weight($a) <=> $weight($b); if ($cmp !== 0) { return $cmp; } return strcmp((string) ($a['key'] ?? ''), (string) ($b['key'] ?? '')); }); return $filtered; } /** * @param array $filters * @return FilterState */ public static function normalizeFilterState(array $filters): array { $status = (string) ($filters['status'] ?? 'missing'); $type = (string) ($filters['type'] ?? 'all'); $features = $filters['features'] ?? []; $search = (string) ($filters['search'] ?? ''); if (! in_array($status, ['missing', 'granted', 'all'], true)) { $status = 'missing'; } if (! in_array($type, ['application', 'delegated', 'all'], true)) { $type = 'all'; } if (! is_array($features)) { $features = []; } $features = array_values(array_unique(array_filter(array_map('strval', $features)))); return [ 'status' => $status, 'type' => $type, 'features' => $features, 'search' => $search, ]; } /** * @param array $row * @return ManagedEnvironmentPermissionRow */ private static function normalizePermissionRow(array $row): array { $key = (string) ($row['key'] ?? ''); $type = (string) ($row['type'] ?? 'application'); $description = $row['description'] ?? null; $features = $row['features'] ?? []; $status = (string) ($row['status'] ?? 'missing'); $details = $row['details'] ?? null; if (! in_array($type, ['application', 'delegated'], true)) { $type = 'application'; } if (! is_string($description) || $description === '') { $description = null; } if (! is_array($features)) { $features = []; } $features = array_values(array_unique(array_filter(array_map('strval', $features)))); if ($status === 'error') { $status = 'unknown'; } if (! in_array($status, ['granted', 'missing', 'blocked', 'expired', 'unknown', 'not_applicable'], true)) { $status = 'missing'; } if (! is_array($details)) { $details = null; } return [ 'key' => $key, 'type' => $type, 'description' => $description, 'features' => $features, 'status' => $status, 'details' => $details, ]; } private static function parseLastRefreshedAt(mixed $value): ?Carbon { if ($value instanceof Carbon) { return $value; } if ($value instanceof CarbonInterface) { return Carbon::instance($value); } if (is_string($value) && $value !== '') { try { return Carbon::parse($value); } catch (\Throwable) { return null; } } return null; } /** * @return array */ private function readinessComparison(ManagedEnvironment $tenant): array { try { $actor = auth()->user(); $actor = $actor instanceof User ? $actor : null; $readiness = app(ProviderReadinessResolver::class) ->forEnvironment($tenant, $actor) ->toArray(); return [ 'permissions' => is_array($readiness['permission_rows'] ?? null) ? $readiness['permission_rows'] : [], 'last_refreshed_at' => data_get($readiness, 'freshness.last_refreshed_at'), 'freshness' => is_array($readiness['freshness'] ?? null) ? $readiness['freshness'] : [], 'counts' => is_array($readiness['counts'] ?? null) ? $readiness['counts'] : [], 'readiness' => $readiness, ]; } catch (\Throwable) { return [ 'permissions' => $this->unknownRequiredPermissionRows(), 'last_refreshed_at' => null, 'freshness' => [ 'last_refreshed_at' => null, 'is_stale' => true, ], 'counts' => [], 'readiness' => [], ]; } } /** * @return array,status:string,details:null}> */ private function unknownRequiredPermissionRows(): array { return collect($this->permissionService->getRequiredPermissions()) ->filter(static fn (mixed $permission): bool => is_array($permission) && filled($permission['key'] ?? null)) ->map(static fn (array $permission): array => [ 'key' => (string) $permission['key'], 'type' => in_array(($permission['type'] ?? null), ['application', 'delegated'], true) ? (string) $permission['type'] : 'application', 'description' => is_string($permission['description'] ?? null) ? (string) $permission['description'] : null, 'features' => is_array($permission['features'] ?? null) ? array_values($permission['features']) : [], 'status' => 'unknown', 'details' => null, ]) ->values() ->all(); } }