*/ public function resourceTypeQuery(): Builder { return TenantConfigurationResourceType::query() ->active() ->orderBy('workload') ->orderBy('source_class') ->orderBy('canonical_type'); } /** * @return Builder */ public function resourceInstanceQuery(ManagedEnvironment $environment): Builder { return TenantConfigurationResource::query() ->where('workspace_id', (int) $environment->workspace_id) ->where('managed_environment_id', (int) $environment->getKey()) ->with([ 'resourceType:id,canonical_type,display_name,source_class,workload,support_state,default_coverage_level', 'providerConnection:id,workspace_id,managed_environment_id,display_name,provider', 'latestEvidence:id,resource_id,operation_run_id,payload_hash,evidence_state,coverage_level,capture_outcome,source_contract_key,source_endpoint,source_version,source_schema_hash,captured_at', 'latestEvidence.operationRun:id,workspace_id,managed_environment_id,type,status,outcome,created_at,completed_at', ]) ->latest('latest_captured_at') ->latest('id'); } /** * @return array */ public function summary(ManagedEnvironment $environment): array { $resources = $this->resourceInstanceQuery($environment)->get([ 'id', 'workspace_id', 'managed_environment_id', 'provider_connection_id', 'resource_type_id', 'source_class', 'latest_evidence_state', 'latest_identity_state', 'latest_claim_state', ]); $resourceTypes = TenantConfigurationResourceType::query() ->active() ->get(['id', 'source_class']); $blockers = $this->activationBlockers($environment); $readinessState = $this->readinessState($resources->count(), $blockers); return [ 'readiness_state' => $readinessState, 'readiness_reason' => $this->readinessReason($resources->count(), $blockers), 'readiness_next_step' => $this->readinessNextStep($resources->count(), $blockers), 'resource_types_total' => $resourceTypes->count(), 'resources_total' => $resources->count(), 'content_backed_count' => $resources ->where('latest_evidence_state', EvidenceState::ContentBacked) ->count(), 'activation_blocker_count' => $blockers->sum('count'), 'identity_conflict_count' => $resources ->where('latest_identity_state', IdentityState::IdentityConflict) ->count(), 'claim_allowed_count' => $resources ->where('latest_claim_state', ClaimState::ClaimAllowed) ->count(), 'claim_limited_count' => $resources ->where('latest_claim_state', ClaimState::ClaimLimited) ->count(), 'claim_blocked_count' => $resources ->where('latest_claim_state', ClaimState::ClaimBlocked) ->count(), 'beta_experimental_count' => $resourceTypes ->where('source_class', SourceClass::GraphBetaExperimental) ->count(), 'graph_fallback_count' => $resourceTypes ->where('source_class', SourceClass::GraphV1Fallback) ->count(), 'top_blockers' => $blockers->take(6)->values()->all(), ]; } /** * @return Collection */ public function activationBlockers(ManagedEnvironment $environment): Collection { $groups = []; $this->resourceInstanceQuery($environment) ->get([ 'id', 'workspace_id', 'managed_environment_id', 'provider_connection_id', 'resource_type_id', 'canonical_type', 'source_display_name', 'source_class', 'latest_evidence_state', 'latest_identity_state', 'latest_claim_state', ]) ->each(function (TenantConfigurationResource $resource) use (&$groups): void { foreach ($this->blockersForResource($resource) as $blocker) { $groups[$blocker] ??= [ 'blocker' => $blocker, 'label' => self::blockerLabel($blocker), 'count' => 0, 'priority' => self::blockerPriority($blocker), 'example_resource' => null, 'example_type' => null, ]; $groups[$blocker]['count']++; $groups[$blocker]['example_resource'] ??= (string) ($resource->source_display_name ?: $resource->canonical_resource_key); $groups[$blocker]['example_type'] ??= (string) $resource->canonical_type; } }); return collect($groups) ->sort(function (array $left, array $right): int { return ($left['priority'] <=> $right['priority']) ?: ($right['count'] <=> $left['count']) ?: strnatcasecmp((string) $left['blocker'], (string) $right['blocker']); }) ->values(); } /** * @return array */ public function providerConnectionOptions(ManagedEnvironment $environment): array { return ProviderConnection::query() ->where('workspace_id', (int) $environment->workspace_id) ->where('managed_environment_id', (int) $environment->getKey()) ->orderBy('display_name') ->pluck('display_name', 'id') ->mapWithKeys(fn (string $label, int|string $id): array => [(string) $id => $label]) ->all(); } /** * @return array */ public function supportedScopeOptions(): array { return app(SupportedScopeResolver::class) ->activeScopes() ->mapWithKeys(fn (TenantConfigurationSupportedScope $scope): array => [ (string) $scope->scope_key => (string) $scope->display_name, ]) ->all(); } /** * @return list */ public function includedCanonicalTypesForScope(string $scopeKey): array { $scope = app(SupportedScopeResolver::class)->findActive($scopeKey); if (! $scope instanceof TenantConfigurationSupportedScope) { return []; } try { $resolved = app(SupportedScopeResolver::class)->resolveDefinition( $scope, TenantConfigurationResourceType::query() ->active() ->get(['canonical_type', 'source_class']), ); } catch (UnexpectedValueException) { return []; } return $resolved['included_resource_types']; } public function scopeInclusionLabel(TenantConfigurationResourceType $resourceType, ?string $scopeKey = null): string { $scopeKey = $scopeKey ?: $this->defaultScopeKey(); if (! is_string($scopeKey) || $scopeKey === '') { return 'No active scope'; } return in_array((string) $resourceType->canonical_type, $this->includedCanonicalTypesForScope($scopeKey), true) ? 'Included' : 'Not included'; } public function defaultScopeKey(): ?string { $scope = app(SupportedScopeResolver::class) ->activeScopes() ->first(); return $scope instanceof TenantConfigurationSupportedScope ? (string) $scope->scope_key : null; } /** * @return array */ public function inspectDetails(TenantConfigurationResource $resource, ManagedEnvironment $environment, ?User $user): array { $resource->loadMissing([ 'resourceType:id,canonical_type,display_name,source_class,workload,support_state,default_coverage_level', 'providerConnection:id,workspace_id,managed_environment_id,display_name,provider', 'latestEvidence:id,resource_id,operation_run_id,payload_hash,evidence_state,coverage_level,capture_outcome,source_contract_key,source_endpoint,source_version,source_schema_hash,captured_at', 'latestEvidence.operationRun:id,workspace_id,managed_environment_id,type,status,outcome,created_at,completed_at', ]); if ( (int) $resource->workspace_id !== (int) $environment->workspace_id || (int) $resource->managed_environment_id !== (int) $environment->getKey() ) { return []; } $run = $resource->latestEvidence?->operationRun; $runUrl = $run !== null && $user instanceof User && Gate::forUser($user)->allows('view', $run) ? OperationRunLinks::view($run, $environment) : null; return [ 'resource' => (string) ($resource->source_display_name ?: $resource->canonical_resource_key), 'canonical_resource_key' => (string) $resource->canonical_resource_key, 'canonical_type' => (string) $resource->canonical_type, 'resource_type' => (string) ($resource->resourceType?->display_name ?: $resource->canonical_type), 'provider_connection' => (string) ($resource->providerConnection?->display_name ?: 'Unassigned provider connection'), 'coverage_level' => $resource->latestEvidence?->coverage_level?->value, 'evidence_state' => $resource->latest_evidence_state?->value, 'identity_state' => $resource->latest_identity_state?->value, 'claim_state' => $resource->latest_claim_state?->value, 'source_class' => $resource->source_class?->value, 'evidence_hash' => $resource->latestEvidence?->payload_hash ?: $resource->latest_payload_hash, 'last_captured' => $resource->latest_captured_at?->toDayDateTimeString(), 'source_contract_key' => $resource->latestEvidence?->source_contract_key, 'source_endpoint' => $resource->latestEvidence?->source_endpoint, 'source_version' => $resource->latestEvidence?->source_version, 'source_schema_hash' => $resource->latestEvidence?->source_schema_hash, 'capture_outcome' => $resource->latestEvidence?->capture_outcome?->value, 'identity_reason_code' => $this->safeIdentityReasonCode($resource), 'operation_run_url' => $runUrl, 'operation_run_label' => $run !== null ? 'Operation #'.$run->getKey() : null, 'blockers' => collect($this->blockersForResource($resource)) ->map(fn (string $blocker): string => self::blockerLabel($blocker)) ->values() ->all(), ]; } /** * @return array */ public function resourceTypeInspectDetails(TenantConfigurationResourceType $resourceType, ?string $scopeKey = null): array { $scopeKey = $scopeKey ?: $this->defaultScopeKey(); $scope = is_string($scopeKey) && $scopeKey !== '' ? app(SupportedScopeResolver::class)->findActive($scopeKey) : null; return [ 'name' => (string) $resourceType->display_name, 'canonical_type' => (string) $resourceType->canonical_type, 'workload' => self::humanize(self::safeStateValue($resourceType->workload)), 'resource_class' => self::humanize(self::safeStateValue($resourceType->resource_class)), 'source_class' => self::safeStateValue($resourceType->source_class), 'support_state' => self::safeStateValue($resourceType->support_state), 'default_coverage_level' => self::safeStateValue($resourceType->default_coverage_level), 'default_evidence_state' => self::safeStateValue($resourceType->default_evidence_state), 'default_identity_state' => self::safeStateValue($resourceType->default_identity_state), 'default_claim_state' => self::safeStateValue($resourceType->default_claim_state), 'restore_tier' => self::humanize(self::safeStateValue($resourceType->restore_tier)), 'supported_scope' => $this->scopeInclusionLabel($resourceType, $scopeKey), 'scope' => $scope instanceof TenantConfigurationSupportedScope ? (string) $scope->display_name : null, 'scope_key' => $scopeKey, 'allows_beta_claims' => (bool) $resourceType->allows_beta_claims, 'allows_graph_fallback_claims' => (bool) $resourceType->allows_graph_fallback_claims, ]; } /** * @return array */ public static function coverageLevelOptions(): array { return self::enumOptions(CoverageLevel::cases()); } /** * @return array */ public static function evidenceStateOptions(): array { return self::enumOptions(EvidenceState::cases()); } /** * @return array */ public static function identityStateOptions(): array { return self::enumOptions(IdentityState::cases()); } /** * @return array */ public static function claimStateOptions(): array { return self::enumOptions(ClaimState::cases()); } /** * @return array */ public static function sourceClassOptions(): array { return self::enumOptions(SourceClass::cases()); } /** * @return array */ public static function supportStateOptions(): array { return self::enumOptions(SupportState::cases()); } /** * @return array */ public static function workloadOptions(): array { return self::enumOptions(Workload::cases()); } /** * @param array $cases * @return array */ private static function enumOptions(array $cases): array { return collect($cases) ->mapWithKeys(fn (\BackedEnum $case): array => [ (string) $case->value => self::humanize((string) $case->value), ]) ->all(); } private static function humanize(string $value): string { return str($value)->replace('_', ' ')->headline()->toString(); } private static function safeStateValue(mixed $state): string { return $state instanceof \BackedEnum ? (string) $state->value : (string) $state; } /** * @return list */ private function blockersForResource(TenantConfigurationResource $resource): array { $blockers = []; $identityState = $resource->latest_identity_state instanceof IdentityState ? $resource->latest_identity_state : IdentityState::tryFrom((string) $resource->latest_identity_state); $evidenceState = $resource->latest_evidence_state instanceof EvidenceState ? $resource->latest_evidence_state : EvidenceState::tryFrom((string) $resource->latest_evidence_state); $claimState = $resource->latest_claim_state instanceof ClaimState ? $resource->latest_claim_state : ClaimState::tryFrom((string) $resource->latest_claim_state); $sourceClass = $resource->source_class instanceof SourceClass ? $resource->source_class : SourceClass::tryFrom((string) $resource->source_class); if (in_array($identityState, [ IdentityState::IdentityConflict, IdentityState::MissingExternalId, IdentityState::UnsupportedIdentity, ], true)) { $blockers[] = $identityState->value; } if ($claimState === ClaimState::ClaimBlocked) { $blockers[] = ClaimState::ClaimBlocked->value; } if (in_array($evidenceState, [ EvidenceState::NotCaptured, EvidenceState::PermissionBlocked, EvidenceState::SourceUnavailable, EvidenceState::SchemaUnknown, EvidenceState::CaptureFailed, ], true)) { $blockers[] = $evidenceState->value; } if ($sourceClass === SourceClass::GraphBetaExperimental) { $blockers[] = 'beta_experimental'; } return array_values(array_unique($blockers)); } private function readinessState(int $resourceCount, Collection $blockers): string { if ($resourceCount === 0) { return 'unknown'; } $hasHardBlocker = $blockers->contains(fn (array $blocker): bool => in_array($blocker['blocker'], [ IdentityState::IdentityConflict->value, IdentityState::MissingExternalId->value, IdentityState::UnsupportedIdentity->value, ClaimState::ClaimBlocked->value, ], true)); if ($hasHardBlocker) { return 'blocked'; } return $blockers->isNotEmpty() ? 'needs_attention' : 'ready'; } private function readinessReason(int $resourceCount, Collection $blockers): string { if ($resourceCount === 0) { return 'No Coverage v2 resource rows exist for this managed environment.'; } $topBlocker = $blockers->first(); if (is_array($topBlocker)) { return sprintf( '%s is the highest-priority activation blocker.', (string) ($topBlocker['label'] ?? 'Coverage v2 readiness'), ); } return 'Captured Coverage v2 resources have no activation blockers.'; } private function readinessNextStep(int $resourceCount, Collection $blockers): string { if ($resourceCount === 0) { return 'Review capture prerequisites before using Coverage v2 as activation proof.'; } $topBlocker = $blockers->first(); if (is_array($topBlocker)) { $example = $topBlocker['example_resource'] ?? $topBlocker['example_type'] ?? null; if (is_string($example) && $example !== '') { return sprintf('Inspect %s and resolve the blocker before cutover planning.', $example); } return 'Inspect the top blocker group before cutover planning.'; } return 'Review the read-only details before cutover planning.'; } private static function blockerLabel(string $blocker): string { return match ($blocker) { 'identity_conflict' => 'Identity conflict', 'missing_external_id' => 'Missing external ID', 'unsupported_identity' => 'Unsupported identity', 'claim_blocked' => 'Claim blocked', 'permission_blocked' => 'Permission blocked', 'source_unavailable' => 'Source unavailable', 'schema_unknown' => 'Schema unknown', 'capture_failed' => 'Capture failed', 'not_captured' => 'Not captured', 'beta_experimental' => 'Beta experimental', default => self::humanize($blocker), }; } private static function blockerPriority(string $blocker): int { return match ($blocker) { 'identity_conflict' => 10, 'missing_external_id' => 11, 'unsupported_identity' => 12, 'claim_blocked' => 20, 'permission_blocked' => 30, 'source_unavailable' => 31, 'schema_unknown' => 32, 'capture_failed' => 33, 'not_captured' => 34, 'beta_experimental' => 90, default => 100, }; } private function safeIdentityReasonCode(TenantConfigurationResource $resource): ?string { $diagnostics = is_array($resource->identity_diagnostics) ? $resource->identity_diagnostics : []; $reasonCode = $diagnostics['reason_code'] ?? null; return is_string($reasonCode) && $reasonCode !== '' ? $reasonCode : null; } }