satisfy(ActionSurfaceSlot::ListHeader, 'Header actions provide a single Clear filters action for the customer review workspace.') ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::PrimaryLinkColumn->value) ->withPrimaryLinkColumnReason('Only the dedicated review-open column should navigate away; the rest of the row stays comparative workspace context.') ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'The customer review workspace remains scan-first and does not expose bulk actions.') ->satisfy(ActionSurfaceSlot::ListEmptyState, 'The empty state keeps exactly one Clear filters CTA when filters are active.') ->exempt(ActionSurfaceSlot::DetailHeader, 'The dedicated open link column opens the latest published review detail instead of an inline canonical detail panel.'); } public static function getNavigationGroup(): string { return WorkspaceHubNavigation::workspaceWideGroup(__('localization.review.reporting')); } public static function getNavigationLabel(): string { return __('localization.review.customer_reviews'); } public static function getNavigationUrl(): string { return WorkspaceHubNavigation::environmentFilteredUrl(static::getUrl(panel: 'admin')); } public function getTitle(): string { return __('localization.review.customer_review_workspace'); } public static function environmentFilterUrl(ManagedEnvironment $tenant): string { return static::getUrl(panel: 'admin').'?'.http_build_query([ 'environment_id' => (int) $tenant->getKey(), ]); } /** * @var array|null */ private ?array $authorizedTenants = null; public function mount(): void { $this->authorizePageAccess(); $this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request()); $this->applyRequestedTenantPrefilter(); $this->mountInteractsWithTable(); $this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request()); $this->auditWorkspaceOpen(); } protected function getHeaderActions(): array { $actions = []; $governanceContext = $this->incomingGovernanceContext(); if ($governanceContext?->backLinkUrl !== null) { $actions[] = Action::make('return_to_governance_inbox') ->label($governanceContext->backLinkLabel ?? 'Back to governance inbox') ->icon('heroicon-o-arrow-left') ->color('gray') ->url($governanceContext->backLinkUrl); } $actions[] = Action::make('clear_filters') ->label(__('localization.review.clear_filters')) ->icon('heroicon-o-x-mark') ->color('gray') ->visible(fn (): bool => $this->hasActiveFilters()) ->action(function (): void { $this->clearWorkspaceFilters(); }); return $actions; } public function acknowledgeReviewAction(): Action { return Action::make('acknowledgeReview') ->label(__('localization.review.acknowledge_review')) ->icon('heroicon-o-check-badge') ->color('primary') ->record(fn (): ?ManagedEnvironment => $this->latestReleasedTenant()) ->requiresConfirmation() ->modalHeading(__('localization.review.acknowledge_review_heading')) ->modalDescription(__('localization.review.acknowledge_review_description')) ->modalSubmitActionLabel(__('localization.review.acknowledge_review_confirm')) ->form([ Textarea::make('comment') ->label(__('localization.review.acknowledge_review_comment')) ->rows(4) ->maxLength(2000), ]) ->action(function (array $data): void { $actor = auth()->user(); $tenant = $this->latestReleasedTenant(); $review = $tenant instanceof ManagedEnvironment ? $this->latestPublishedReview($tenant) : null; if (! $actor instanceof User || ! $tenant instanceof ManagedEnvironment || ! $review instanceof EnvironmentReview) { Notification::make() ->title(__('localization.review.acknowledge_review_unavailable')) ->danger() ->send(); return; } try { app(EnvironmentReviewAcknowledgementService::class)->acknowledge( tenant: $tenant, review: $review, actor: $actor, comment: is_string($data['comment'] ?? null) ? (string) $data['comment'] : null, ); } catch (\Throwable $throwable) { Notification::make() ->title(__('localization.review.acknowledge_review_failed')) ->body($throwable->getMessage()) ->danger() ->send(); return; } Notification::make() ->title(__('localization.review.review_acknowledged')) ->success() ->send(); }); } public function createNextReviewAction(): Action { return UiEnforcement::forAction( Action::make('create_next_review') ->label(__('localization.review.create_next_review')) ->icon('heroicon-o-document-duplicate') ->color('primary') ->record(fn (): ?EnvironmentReview => $this->workspaceLifecycleReview()) ->hidden(fn (): bool => ! ($this->workspaceLifecycleReview()?->isPublished() ?? false)) ->requiresConfirmation() ->modalHeading(__('localization.review.create_next_review_heading')) ->modalDescription(__('localization.review.create_next_review_description')) ->modalSubmitActionLabel(__('localization.review.create_next_review_confirm')) ->action(function (): void { $user = auth()->user(); $review = $this->workspaceLifecycleReview(); if (! $user instanceof User || ! $review instanceof EnvironmentReview) { Notification::make() ->title(__('localization.review.create_next_review_unavailable')) ->danger() ->send(); return; } try { $nextReview = app(EnvironmentReviewLifecycleService::class)->createNextReview($review, $user); } catch (\Throwable $throwable) { Notification::make() ->title(__('localization.review.create_next_review_failed')) ->body($throwable->getMessage()) ->danger() ->send(); return; } $tenant = $nextReview->tenant; if (! $tenant instanceof ManagedEnvironment) { Notification::make() ->title(__('localization.review.create_next_review_unavailable')) ->danger() ->send(); return; } $this->redirect(self::appendQuery( EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $nextReview], $tenant), array_filter([ 'source_surface' => self::SOURCE_SURFACE, 'tenant_filter_id' => $this->currentTenantFilterId(), ], static fn (mixed $value): bool => $value !== null && $value !== ''), )); }), ) ->requireCapability(Capabilities::ENVIRONMENT_REVIEW_MANAGE) ->preserveVisibility() ->apply(); } public function table(Table $table): Table { return $table ->query(fn (): Builder => $this->workspaceQuery()) ->defaultSort('name') ->paginated(TablePaginationProfiles::customPage()) ->persistFiltersInSession() ->persistSearchInSession() ->persistSortInSession() ->recordUrl(null) ->columns([ TextColumn::make('name')->label('Environment')->searchable(), TextColumn::make('package_availability') ->label(__('localization.review.governance_package')) ->width('9rem') ->extraHeaderAttributes(['class' => 'whitespace-normal']) ->badge() ->getStateUsing(fn (ManagedEnvironment $record): string => $this->governancePackageAvailabilityLabel($record)) ->color(fn (ManagedEnvironment $record): string => $this->governancePackageAvailabilityColor($record)) ->tooltip(fn (ManagedEnvironment $record): string => $this->governancePackageAvailability($record)['description']), TextColumn::make('latest_review') ->label(__('localization.review.status')) ->width('9rem') ->badge() ->getStateUsing(fn (ManagedEnvironment $record): string => $this->latestReviewStateLabel($record)) ->color(fn (ManagedEnvironment $record): string => $this->latestReviewStateColor($record)), TextColumn::make('evidence_proof_state') ->label(__('localization.review.evidence_status')) ->width('8rem') ->badge() ->getStateUsing(fn (ManagedEnvironment $record): string => $this->evidenceStatusLabel($record)) ->color(fn (ManagedEnvironment $record): string => $this->evidenceStatusColor($record)), TextColumn::make('recommended_next_action') ->label(__('localization.review.next_step')) ->width('10rem') ->extraHeaderAttributes(['class' => 'whitespace-normal']) ->getStateUsing(fn (ManagedEnvironment $record): string => $this->controlRecommendedNextAction($record)) ->wrap(), TextColumn::make('open_review') ->label(__('localization.review.open')) ->width('8rem') ->getStateUsing(fn (): string => __('localization.review.open_review')) ->url(fn (ManagedEnvironment $record): ?string => $this->latestReviewUrl($record)) ->color('primary'), ]) ->filters([ SelectFilter::make('managed_environment_id') ->label('Environment') ->options(fn (): array => $this->tenantFilterOptions()) ->default(fn (): ?string => $this->defaultTenantFilter()) ->query(function (Builder $query, array $data): Builder { $tenantId = $data['value'] ?? null; return is_numeric($tenantId) ? $query->whereKey((int) $tenantId) : $query; }) ->searchable(), ]) ->actions([]) ->bulkActions([]) ->emptyStateHeading(fn (): string => $this->workspaceEmptyStateHeading()) ->emptyStateDescription(fn (): string => $this->workspaceEmptyStateDescription()) ->emptyStateActions([ Action::make('clear_filters_empty') ->label(__('localization.review.clear_filters')) ->icon('heroicon-o-x-mark') ->color('gray') ->visible(fn (): bool => $this->hasActiveFilters()) ->action(fn (): mixed => $this->clearWorkspaceFilters()), ]); } /** * @return array */ public function authorizedTenants(): array { if ($this->authorizedTenants !== null) { return $this->authorizedTenants; } $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User || ! $workspace instanceof Workspace) { return $this->authorizedTenants = []; } return $this->authorizedTenants = app(EnvironmentReviewRegisterService::class)->authorizedTenants($user, $workspace); } public function activeEnvironmentFilterLabel(): ?string { return $this->filteredTenant()?->name; } /** * @return array{label: string, clear_url: string}|null */ public function environmentFilterChip(): ?array { $tenant = $this->filteredTenant(); if (! $tenant instanceof ManagedEnvironment) { return null; } return [ 'label' => (string) $tenant->name, 'clear_url' => $this->cleanWorkspaceHubUrl(static::getUrl(panel: 'admin')), ]; } /** * @return array|null */ public function latestReviewConsumptionPayload(): ?array { $tenant = $this->latestReleasedTenant(); if (! $tenant instanceof ManagedEnvironment) { return null; } $review = $this->latestPublishedReview($tenant); if (! $review instanceof EnvironmentReview) { return null; } $review->loadMissing([ 'currentExportReviewPack.operationRun', 'evidenceSnapshot.operationRun', 'operationRun', 'supersededByReview.sections', ]); $publishedAt = $review->published_at ?? $review->generated_at ?? $review->created_at; $packageAvailability = $this->governancePackageAvailability($tenant); $downloadUrl = $this->reviewPackDownloadUrl($review, $tenant); $reviewUrl = $this->latestReviewUrl($tenant); $evidenceUrl = $this->evidenceSnapshotUrlForReview($review, $tenant); $outputReadiness = $this->reviewPackOutputReadinessForReview($review); $outputGuidance = $this->reviewOutputGuidanceForReview( review: $review, downloadUrl: $downloadUrl, reviewUrl: $reviewUrl, evidenceUrl: $evidenceUrl, ); $resolutionCase = $this->reviewOutputResolutionCaseForReview($review, $outputGuidance); $decision = $this->decisionSummaryForReview($review); $acceptedRisks = $this->acceptedRisksForReview($review); $hasAcceptedRiskFollowUp = $this->acceptedRiskFollowUpRequiredForReview($review); $findingPanel = $this->findingPanelForReview($tenant); $evidencePath = $this->evidencePathForReview($review, $tenant, $packageAvailability, $downloadUrl, $decision, $acceptedRisks); $readiness = $this->reviewReadinessForTenant( tenant: $tenant, review: $review, packageAvailability: $packageAvailability, outputReadiness: $outputReadiness, outputGuidance: $outputGuidance, resolutionCase: $resolutionCase, downloadUrl: $downloadUrl, reviewUrl: $reviewUrl, evidenceUrl: $evidenceUrl, ); return [ 'scope' => $this->reviewScopePayload($tenant), 'latest' => [ 'review_label' => __('localization.review.released_review_for_environment', [ 'environment' => $tenant->name, ]), 'environment_label' => $tenant->name, 'status_label' => $this->latestReviewStateLabel($tenant), 'status_color' => $this->latestReviewStateColor($tenant), 'published_label' => $publishedAt instanceof \DateTimeInterface ? $publishedAt->format('M j, Y H:i') : __('localization.review.unavailable'), 'package_label' => $packageAvailability['label'], 'package_badge_label' => $this->governancePackageAvailabilityLabel($tenant), 'package_color' => $this->governancePackageAvailabilityColor($tenant), 'package_description' => $packageAvailability['description'], 'primary_action_label' => $downloadUrl !== null ? $outputGuidance['qualified_download_label'] : __('localization.review.open_latest_review'), 'primary_action_url' => $downloadUrl ?? $reviewUrl, 'primary_action_icon' => $downloadUrl !== null ? 'heroicon-o-arrow-down-tray' : 'heroicon-o-arrow-top-right-on-square', 'secondary_action_label' => $readiness['secondary_action_label'], 'secondary_action_url' => $readiness['secondary_action_url'], 'secondary_action_icon' => 'heroicon-o-arrow-top-right-on-square', ], 'readiness' => $readiness, 'readiness_flow' => $this->reviewConsumptionFlowForReview($tenant, $review, $packageAvailability, $downloadUrl, $outputReadiness), 'finding_panel' => $findingPanel, 'acknowledgement' => $this->reviewAcknowledgementPayloadForReview($tenant, $review, $packageAvailability, $downloadUrl), 'decision' => $decision, 'accepted_risks' => $acceptedRisks, 'accepted_risk_panel' => $this->acceptedRiskPanelForReview($review, $tenant), 'evidence_basis' => $this->evidenceBasisForReview($review, $packageAvailability, $outputReadiness), 'evidence_path' => $evidencePath, 'aside_evidence_path' => $this->asideEvidencePath($evidencePath), 'review_pack_panel' => $this->reviewPackPanelForReview($review, $tenant, $packageAvailability, $downloadUrl, $outputReadiness), 'follow_ups' => $this->customerSafeFollowUpsForReview($decision), 'diagnostics' => $this->diagnosticsDisclosureForReview(), 'disclosure_rules' => $this->disclosureRuleRows(), ]; } /** * @param array{state:string,label:string,description:string} $packageAvailability * @return array{ * state: 'not_available'|'required'|'acknowledged'|'re_ack_required', * status_label: string, * status_color: string, * reason: string, * impact: string, * action_name: string|null, * action_label: string, * action_color: string, * action_disabled: bool, * action_helper: string|null, * acknowledged_at_label: string|null, * acknowledged_by_label: string|null, * comment: string|null, * basis: list * } */ private function reviewAcknowledgementPayloadForReview( ManagedEnvironment $tenant, EnvironmentReview $review, array $packageAvailability, ?string $downloadUrl, ): array { $actor = auth()->user(); $canAcknowledge = $actor instanceof User && $actor->canAccessTenant($tenant) && $actor->can(Capabilities::ENVIRONMENT_REVIEW_ACKNOWLEDGE, $tenant); $ack = EnvironmentReviewAcknowledgement::query() ->with(['acknowledgedByUser']) ->where('environment_review_id', (int) $review->getKey()) ->where('managed_environment_id', (int) $review->managed_environment_id) ->where('workspace_id', (int) $review->workspace_id) ->first(); $currentReviewPackId = is_numeric($review->current_export_review_pack_id) ? (int) $review->current_export_review_pack_id : null; $currentEvidenceSnapshotId = is_numeric($review->evidence_snapshot_id) ? (int) $review->evidence_snapshot_id : null; $reviewPackProof = $this->reviewPackProofForReview($packageAvailability, $downloadUrl); $evidenceState = $this->evidenceStatusState($tenant); $basis = [ [ 'label' => __('localization.review.review_pack'), 'value' => $reviewPackProof['label'], 'color' => $reviewPackProof['color'], ], [ 'label' => __('localization.review.evidence'), 'value' => $this->evidenceStatusLabelForState($evidenceState), 'color' => $this->evidenceStatusColorForState($evidenceState), ], ]; if (! $review->isPublished()) { return [ 'state' => 'not_available', 'status_label' => __('localization.review.acknowledgement_not_available'), 'status_color' => 'gray', 'reason' => __('localization.review.acknowledgement_not_available_reason'), 'impact' => __('localization.review.acknowledgement_not_available_impact'), 'action_name' => null, 'action_label' => __('localization.review.review_accepted_risks'), 'action_color' => 'gray', 'action_disabled' => true, 'action_helper' => null, 'acknowledged_at_label' => null, 'acknowledged_by_label' => null, 'comment' => null, 'basis' => $basis, ]; } if (! $ack instanceof EnvironmentReviewAcknowledgement) { return [ 'state' => 'required', 'status_label' => __('localization.review.acknowledgement_required'), 'status_color' => 'warning', 'reason' => __('localization.review.acknowledgement_required_reason'), 'impact' => __('localization.review.acknowledgement_required_impact'), 'action_name' => 'acknowledgeReview', 'action_label' => __('localization.review.acknowledge_review'), 'action_color' => 'primary', 'action_disabled' => ! $canAcknowledge, 'action_helper' => ! $canAcknowledge ? __('localization.review.acknowledgement_requires_permission') : null, 'acknowledged_at_label' => null, 'acknowledged_by_label' => null, 'comment' => null, 'basis' => $basis, ]; } $basisDriftDetected = false; if (is_numeric($ack->review_pack_id) && is_int($currentReviewPackId) && (int) $ack->review_pack_id !== $currentReviewPackId) { $basisDriftDetected = true; } if (is_numeric($ack->evidence_snapshot_id) && is_int($currentEvidenceSnapshotId) && (int) $ack->evidence_snapshot_id !== $currentEvidenceSnapshotId) { $basisDriftDetected = true; } $acknowledgedAtLabel = $ack->acknowledged_at instanceof \DateTimeInterface ? $ack->acknowledged_at->format('M j, Y H:i') : null; $acknowledgedByLabel = $ack->acknowledgedByUser instanceof User ? (string) $ack->acknowledgedByUser->name : null; $comment = is_string($ack->comment) ? trim($ack->comment) : null; $comment = filled($comment) ? $comment : null; if ($basisDriftDetected) { return [ 'state' => 're_ack_required', 'status_label' => __('localization.review.acknowledgement_re_ack_required'), 'status_color' => 'warning', 'reason' => __('localization.review.acknowledgement_re_ack_required_reason'), 'impact' => __('localization.review.acknowledgement_re_ack_required_impact'), 'action_name' => 'acknowledgeReview', 'action_label' => __('localization.review.re_acknowledge_review'), 'action_color' => 'warning', 'action_disabled' => ! $canAcknowledge, 'action_helper' => ! $canAcknowledge ? __('localization.review.acknowledgement_requires_permission') : null, 'acknowledged_at_label' => $acknowledgedAtLabel, 'acknowledged_by_label' => $acknowledgedByLabel, 'comment' => $comment, 'basis' => $basis, ]; } return [ 'state' => 'acknowledged', 'status_label' => __('localization.review.review_acknowledged'), 'status_color' => 'success', 'reason' => __('localization.review.acknowledgement_recorded_reason'), 'impact' => __('localization.review.acknowledgement_recorded_impact'), 'action_name' => null, 'action_label' => __('localization.review.review_accepted_risks'), 'action_color' => 'gray', 'action_disabled' => true, 'action_helper' => null, 'acknowledged_at_label' => $acknowledgedAtLabel, 'acknowledged_by_label' => $acknowledgedByLabel, 'comment' => $comment, 'basis' => $basis, ]; } /** * @return array{label:string,description:string,is_filtered:bool} */ private function reviewScopePayload(ManagedEnvironment $tenant): array { $filteredTenant = $this->filteredTenant(); if ($filteredTenant instanceof ManagedEnvironment) { return [ 'label' => __('localization.review.customer_workspace_scope_environment_filtered', [ 'environment' => $filteredTenant->name, ]), 'description' => __('localization.review.customer_workspace_scope_environment_filtered_description'), 'is_filtered' => true, ]; } return [ 'label' => __('localization.review.customer_workspace_scope_workspace_wide'), 'description' => __('localization.review.customer_workspace_scope_workspace_wide_description', [ 'environment' => $tenant->name, ]), 'is_filtered' => false, ]; } /** * @param array{state:string,label:string,description:string} $packageAvailability * @param array $outputReadiness * @param array $outputGuidance * @return array{ * question:string, * label:string, * color:string, * boundary_label:string, * boundary_color:string, * reason:string, * impact:string, * primary_action_label:string, * primary_action_url:?string, * primary_action_icon:string, * secondary_action_label:?string, * secondary_action_url:?string, * secondary_actions:list, * resolution_case:array, * output_guidance:array * } */ private function reviewReadinessForTenant( ManagedEnvironment $tenant, EnvironmentReview $review, array $packageAvailability, array $outputReadiness, array $outputGuidance, array $resolutionCase, ?string $downloadUrl, ?string $reviewUrl, ?string $evidenceUrl, ): array { $hasAcceptedRiskFollowUp = $this->acceptedRiskFollowUpRequiredForReview($review); $findingPanel = $this->findingPanelForReview($tenant); $hasFindingFollowUp = $findingPanel['open_count'] > 0; $effectiveState = $this->effectiveWorkspaceReadinessState( $outputReadiness, $hasFindingFollowUp, $hasAcceptedRiskFollowUp, ); $reasonCode = $hasFindingFollowUp ? 'findings_follow_up_required' : ($hasAcceptedRiskFollowUp ? 'accepted_risk_follow_up_required' : (string) ($outputReadiness['primary_reason'] ?? 'customer_safe_ready')); $actions = $this->workspaceReadinessActions( state: $effectiveState, reasonCode: $reasonCode, downloadUrl: $downloadUrl, reviewUrl: $reviewUrl, evidenceUrl: $evidenceUrl, ); $followUpOverride = in_array($reasonCode, ['findings_follow_up_required', 'accepted_risk_follow_up_required'], true); $presentedResolutionCase = $followUpOverride ? $this->workspaceFollowUpResolutionCase( baseCase: $resolutionCase, effectiveState: $effectiveState, reasonCode: $reasonCode, outputReadiness: $outputReadiness, findingPanel: $findingPanel, packageAvailability: $packageAvailability, actions: $actions, ) : $resolutionCase; $presentedResolutionCase = $this->decorateSuccessorResolutionCase($presentedResolutionCase, $review); $primaryAction = is_array($presentedResolutionCase['primary_action'] ?? null) ? $presentedResolutionCase['primary_action'] : null; $secondaryActions = is_array($presentedResolutionCase['secondary_actions'] ?? null) ? $presentedResolutionCase['secondary_actions'] : []; return [ 'question' => __('localization.review.review_pack_output_status'), 'label' => $followUpOverride ? $this->workspaceReadinessLabel($effectiveState) : (string) ($outputGuidance['label'] ?? $this->workspaceReadinessLabel($effectiveState)), 'color' => $followUpOverride ? $this->workspaceReadinessColor($effectiveState) : (string) ($outputGuidance['color'] ?? $this->workspaceReadinessColor($effectiveState)), 'boundary_label' => $followUpOverride ? $this->workspaceBoundaryLabel((string) ($outputReadiness['customer_safe_state'] ?? 'requires_review')) : (string) ($outputGuidance['boundary_label'] ?? $this->workspaceBoundaryLabel((string) ($outputReadiness['customer_safe_state'] ?? 'requires_review'))), 'boundary_color' => $followUpOverride ? $this->workspaceBoundaryColor((string) ($outputReadiness['customer_safe_state'] ?? 'requires_review')) : (string) ($outputGuidance['boundary_color'] ?? $this->workspaceBoundaryColor((string) ($outputReadiness['customer_safe_state'] ?? 'requires_review'))), 'reason' => (string) ($presentedResolutionCase['reason'] ?? $outputGuidance['primary_reason'] ?? $packageAvailability['description']), 'impact' => (string) ($presentedResolutionCase['impact'] ?? $outputGuidance['impact'] ?? $this->workspaceReadinessImpact(state: $effectiveState, reasonCode: $reasonCode)), 'primary_action_label' => (string) ($primaryAction['label'] ?? $actions['primary_label']), 'primary_action_url' => $primaryAction['url'] ?? $actions['primary_url'], 'primary_action_icon' => (string) ($primaryAction['icon'] ?? $actions['primary_icon']), 'secondary_action_label' => $secondaryActions[0]['label'] ?? null, 'secondary_action_url' => $secondaryActions[0]['url'] ?? null, 'secondary_actions' => $secondaryActions, 'resolution_case' => $presentedResolutionCase, 'output_guidance' => array_replace($outputGuidance, [ 'action_help' => $this->workspaceActionHelpForResolutionCase($presentedResolutionCase, $outputGuidance), ]), ]; } /** * @param array{state:string,label:string,description:string} $packageAvailability * @param array $outputReadiness * @return list */ private function reviewConsumptionFlowForReview( ManagedEnvironment $tenant, EnvironmentReview $review, array $packageAvailability, ?string $downloadUrl, array $outputReadiness, ): array { $evidenceState = $this->evidenceStatusState($tenant); $findingPanel = $this->findingPanelForReview($tenant); $acceptedRisk = $this->acceptedRiskDimensionForReview($review, $tenant); $hasAcceptedRiskFollowUp = $this->acceptedRiskFollowUpRequiredForReview($review); $hasReadyPackage = $packageAvailability['state'] === 'available' && $downloadUrl !== null; $hasMappedReviewData = $this->primaryControlSummary($tenant) !== null; $workspaceState = $this->effectiveWorkspaceReadinessState( $outputReadiness, $findingPanel['open_count'] > 0, $hasAcceptedRiskFollowUp, ); $hasBlockingAttention = $findingPanel['open_count'] > 0 || $hasAcceptedRiskFollowUp || $evidenceState !== 'available' || ! $hasMappedReviewData || $workspaceState !== ReviewPackOutputReadiness::STATE_CUSTOMER_SAFE_READY; $customerOutputLabel = match (true) { $hasReadyPackage && ! $hasBlockingAttention => __('localization.review.ready'), $workspaceState === ReviewPackOutputReadiness::STATE_EXPORT_NOT_READY || ! $hasReadyPackage => __('localization.review.not_ready'), $hasReadyPackage => __('localization.review.needs_review'), default => __('localization.review.not_ready'), }; $customerOutputColor = match (true) { $hasReadyPackage && ! $hasBlockingAttention => 'success', $workspaceState === ReviewPackOutputReadiness::STATE_EXPORT_NOT_READY || ! $hasReadyPackage => 'gray', $hasReadyPackage => 'warning', default => 'gray', }; $customerOutputDescription = match (true) { $hasReadyPackage && ! $hasBlockingAttention => __('localization.review.customer_output_ready_description'), $hasReadyPackage => __('localization.review.customer_output_needs_review_description'), default => __('localization.review.customer_output_not_ready_description'), }; return [ [ 'title' => __('localization.review.review_data'), 'label' => __('localization.review.available'), 'color' => 'success', 'description' => __('localization.review.review_data_available_description'), 'is_current' => false, ], [ 'title' => __('localization.review.evidence'), 'label' => $this->evidenceStatusLabelForState($evidenceState), 'color' => $this->evidenceStatusColorForState($evidenceState), 'description' => $this->evidenceDimensionDescription($evidenceState), 'is_current' => $evidenceState !== 'available', ], [ 'title' => __('localization.review.findings_triaged'), 'label' => $findingPanel['status_label'], 'color' => $findingPanel['status_color'], 'description' => $findingPanel['summary'], 'is_current' => $findingPanel['open_count'] > 0, ], [ 'title' => __('localization.review.accepted_risks_reviewed'), 'label' => $acceptedRisk['label'], 'color' => $acceptedRisk['color'], 'description' => $acceptedRisk['description'], 'is_current' => $this->acceptedRiskFollowUpRequiredForReview($review), ], [ 'title' => __('localization.review.review_pack'), 'label' => $packageAvailability['label'], 'color' => $this->governancePackageAvailabilityColor($tenant), 'description' => $this->reviewPackDimensionDescription($packageAvailability), 'is_current' => $packageAvailability['state'] !== 'available', ], [ 'title' => __('localization.review.customer_output'), 'label' => $customerOutputLabel, 'color' => $customerOutputColor, 'description' => $customerOutputDescription, 'is_current' => ! ($hasReadyPackage && ! $hasBlockingAttention), ], ]; } private function evidenceDimensionDescription(string $state): string { return match ($state) { 'available' => __('localization.review.evidence_dimension_available_description'), 'expired' => __('localization.review.evidence_dimension_expired_description'), 'restricted' => __('localization.review.evidence_dimension_restricted_description'), default => __('localization.review.evidence_dimension_unavailable_description'), }; } /** * @param array{state:string,label:string,description:string} $packageAvailability */ private function reviewPackDimensionDescription(array $packageAvailability): string { return match ($packageAvailability['state']) { 'available' => __('localization.review.review_pack_dimension_available_description'), 'not_available' => __('localization.review.review_pack_dimension_not_generated_description'), 'preparing' => __('localization.review.review_pack_dimension_preparing_description'), 'expired' => __('localization.review.review_pack_dimension_expired_description'), default => __('localization.review.review_pack_dimension_unavailable_description'), }; } /** * @return array{label:string,color:string,description:string} */ private function acceptedRiskDimensionForReview(EnvironmentReview $review, ManagedEnvironment $tenant): array { $acceptedRisks = $this->acceptedRisksForReview($review); $exceptionCount = $this->acceptedRiskExceptionsForTenant($tenant)->count(); $acceptedRiskCount = max($acceptedRisks['count'], $exceptionCount); $hasFollowUp = $this->acceptedRiskFollowUpRequiredForReview($review); if ($hasFollowUp) { return [ 'label' => __('localization.review.accepted_risk_follow_up'), 'color' => 'warning', 'description' => __('localization.review.accepted_risk_dimension_follow_up_description'), ]; } if ($acceptedRiskCount === 0) { return [ 'label' => __('localization.review.accepted_risk_no_action_needed'), 'color' => 'gray', 'description' => __('localization.review.accepted_risk_dimension_no_action_description'), ]; } return [ 'label' => __('localization.review.accepted_risk_on_record', ['count' => $acceptedRiskCount]), 'color' => 'info', 'description' => __('localization.review.accepted_risk_dimension_on_record_description'), ]; } private function acceptedRiskFollowUpRequiredForReview(EnvironmentReview $review): bool { $package = $this->governancePackageSummaryForReview($review); $decisionSummary = is_array($package['decision_summary'] ?? null) ? $package['decision_summary'] : []; if ((string) ($decisionSummary['status'] ?? '') === 'requires_awareness') { return true; } $acceptedEntries = collect($package['accepted_risks'] ?? []) ->filter(static fn (mixed $entry): bool => is_array($entry)); $decisionEntries = collect($package['governance_decisions'] ?? []) ->filter(static fn (mixed $entry): bool => is_array($entry)); if ($acceptedEntries ->merge($decisionEntries) ->contains(static fn (array $entry): bool => in_array( (string) ($entry['governance_state'] ?? ''), self::ACCEPTED_RISK_FOLLOW_UP_STATES, true, ))) { return true; } return FindingException::query() ->where('workspace_id', (int) $review->workspace_id) ->where('managed_environment_id', (int) $review->managed_environment_id) ->current() ->whereIn('current_validity_state', [ FindingException::VALIDITY_EXPIRING, FindingException::VALIDITY_EXPIRED, FindingException::VALIDITY_REVOKED, FindingException::VALIDITY_MISSING_SUPPORT, ]) ->exists(); } /** * @param array{state:string,label:string,description:string} $packageAvailability * @param array $decision * @param array{count:int,entries:list>,empty_state:string} $acceptedRisks * @return list */ private function evidencePathForReview( EnvironmentReview $review, ManagedEnvironment $tenant, array $packageAvailability, ?string $downloadUrl, array $decision, array $acceptedRisks, ): array { return [ $this->evidenceSnapshotProofForReview($review, $tenant), $this->reviewPackProofForReview($packageAvailability, $downloadUrl), $this->decisionTrailProofForReview($decision), $this->acceptedRiskProofForReview($acceptedRisks), $this->operationProofForReview($review, $tenant), $this->exportArtifactProofForReview($packageAvailability, $downloadUrl), ]; } /** * @param list $evidencePath * @return list */ private function asideEvidencePath(array $evidencePath): array { $asideKeys = [ 'evidence_snapshot', 'review_pack', 'decision_trail', 'operation_proof', ]; return collect($evidencePath) ->filter(static fn (array $proof): bool => in_array($proof['key'], $asideKeys, true)) ->map(fn (array $proof): array => array_replace($proof, [ 'label' => $this->asideEvidencePathLabel($proof), 'detail' => $this->asideEvidencePathDetail($proof), ])) ->values() ->all(); } /** * @param array{key:string,label:string,color:string} $proof */ private function asideEvidencePathLabel(array $proof): string { if ($proof['key'] !== 'decision_trail') { return $proof['label']; } return match ($proof['color']) { 'success', 'info' => __('localization.review.available'), 'warning' => __('localization.review.limited'), default => __('localization.review.unavailable'), }; } /** * @param array{key:string,title:string,description:string} $proof */ private function asideEvidencePathDetail(array $proof): string { $description = (string) $proof['description']; $titlePrefix = trim((string) $proof['title']).' '; if (str_starts_with($description, $titlePrefix)) { return Str::ucfirst(Str::replaceStart($titlePrefix, '', $description)); } return $description; } /** * @return array{key:string,title:string,label:string,color:string,description:string,action_label:?string,action_url:?string} */ private function evidenceSnapshotProofForReview(EnvironmentReview $review, ManagedEnvironment $tenant): array { $snapshot = $review->evidenceSnapshot; $state = $this->evidenceStatusState($tenant); $url = $this->evidenceSnapshotUrlForReview($review, $tenant); return [ 'key' => 'evidence_snapshot', 'title' => __('localization.review.evidence_snapshot'), 'label' => $this->evidenceStatusLabelForState($state), 'color' => $this->evidenceStatusColorForState($state), 'description' => $snapshot instanceof EvidenceSnapshot && $snapshot->generated_at !== null ? __('localization.review.evidence_snapshot_available_description', [ 'date' => $snapshot->generated_at->format('M j, Y H:i'), ]) : __('localization.review.evidence_proof_absent'), 'action_label' => $url !== null ? __('localization.review.view_evidence_snapshot') : null, 'action_url' => $url, ]; } /** * @param array{state:string,label:string,description:string} $packageAvailability * @return array{key:string,title:string,label:string,color:string,description:string,action_label:?string,action_url:?string} */ private function reviewPackProofForReview(array $packageAvailability, ?string $downloadUrl): array { return [ 'key' => 'review_pack', 'title' => __('localization.review.review_pack'), 'label' => $packageAvailability['label'], 'color' => match ($packageAvailability['state']) { 'available' => 'success', 'preparing' => 'warning', 'expired', 'unavailable' => 'danger', default => 'gray', }, 'description' => $packageAvailability['description'], 'action_label' => null, 'action_url' => null, ]; } /** * @param array $decision * @return array{key:string,title:string,label:string,color:string,description:string,action_label:?string,action_url:?string} */ private function decisionTrailProofForReview(array $decision): array { return [ 'key' => 'decision_trail', 'title' => __('localization.review.decision_trail'), 'label' => (string) $decision['label'], 'color' => (string) $decision['color'], 'description' => (string) $decision['summary'], 'action_label' => null, 'action_url' => null, ]; } /** * @param array{count:int,entries:list>,empty_state:string} $acceptedRisks * @return array{key:string,title:string,label:string,color:string,description:string,action_label:?string,action_url:?string} */ private function acceptedRiskProofForReview(array $acceptedRisks): array { return [ 'key' => 'accepted_risk_records', 'title' => __('localization.review.accepted_risk_records'), 'label' => $acceptedRisks['count'] === 0 ? __('localization.review.accepted_risk_none') : __('localization.review.accepted_risk_on_record', ['count' => $acceptedRisks['count']]), 'color' => $acceptedRisks['count'] === 0 ? 'success' : 'info', 'description' => $acceptedRisks['count'] === 0 ? $acceptedRisks['empty_state'] : __('localization.review.accepted_risk_records_description'), 'action_label' => null, 'action_url' => null, ]; } /** * @return array{key:string,title:string,label:string,color:string,description:string,action_label:?string,action_url:?string} */ private function operationProofForReview(EnvironmentReview $review, ManagedEnvironment $tenant): array { $run = collect([ $review->operationRun, $review->evidenceSnapshot?->operationRun, $review->currentExportReviewPack?->operationRun, ])->first(fn (mixed $candidate): bool => $candidate instanceof OperationRun); if ($run instanceof OperationRun) { $initiator = is_string($run->initiator_name) && trim($run->initiator_name) !== '' ? trim($run->initiator_name) : null; return [ 'key' => 'operation_proof', 'title' => __('localization.review.operation_proof'), 'label' => __('localization.review.available'), 'color' => 'info', 'description' => $initiator === null ? __('localization.review.operation_proof_available_description') : __('localization.review.operation_proof_available_with_initiator_description', ['initiator' => $initiator]), 'action_label' => OperationRunLinks::openLabel(), 'action_url' => OperationRunLinks::tenantlessView($run), ]; } return [ 'key' => 'operation_proof', 'title' => __('localization.review.operation_proof'), 'label' => __('localization.review.unavailable'), 'color' => 'gray', 'description' => __('localization.review.operation_proof_unavailable_description'), 'action_label' => null, 'action_url' => null, ]; } /** * @param array{state:string,label:string,description:string} $packageAvailability * @return array{key:string,title:string,label:string,color:string,description:string,action_label:?string,action_url:?string} */ private function exportArtifactProofForReview(array $packageAvailability, ?string $downloadUrl): array { return [ 'key' => 'export_artifact', 'title' => __('localization.review.export_artifact'), 'label' => $downloadUrl !== null ? __('localization.review.available') : $packageAvailability['label'], 'color' => $downloadUrl !== null ? 'success' : 'gray', 'description' => $downloadUrl !== null ? __('localization.review.export_artifact_available_description') : __('localization.review.export_artifact_unavailable_description'), 'action_label' => null, 'action_url' => null, ]; } /** * @return array{status_label:string,status_color:string,summary:string,total_count:int,open_count:int,high_impact_count:int,items:list} */ private function findingPanelForReview(ManagedEnvironment $tenant): array { $baseQuery = Finding::query() ->where('workspace_id', (int) $tenant->workspace_id) ->where('managed_environment_id', (int) $tenant->getKey()); $total = (clone $baseQuery)->count(); $open = (clone $baseQuery) ->whereIn('status', Finding::openStatusesForQuery()) ->count(); $highImpact = (clone $baseQuery) ->whereIn('status', Finding::openStatusesForQuery()) ->whereIn('severity', Finding::highSeverityValues()) ->count(); $accepted = (clone $baseQuery) ->where('status', Finding::STATUS_RISK_ACCEPTED) ->count(); $summary = match (true) { $open > 0 && $highImpact > 0 => __('localization.review.findings_high_impact_summary', [ 'open' => trans_choice('localization.review.findings_open_attention_count', $open, ['count' => $open]), 'high' => trans_choice('localization.review.findings_high_impact_count_summary', $highImpact, ['count' => $highImpact]), ]), $open > 0 => trans_choice('localization.review.findings_open_summary', $open, ['count' => $open]), $total > 0 => __('localization.review.findings_no_open_summary', ['total' => $total]), default => __('localization.review.findings_none_action_summary'), }; return [ 'status_label' => $open > 0 ? __('localization.review.needs_review') : __('localization.review.no_action_needed'), 'status_color' => match (true) { $highImpact > 0 => 'danger', $open > 0 => 'warning', default => 'success', }, 'summary' => $summary, 'total_count' => $total, 'open_count' => $open, 'high_impact_count' => $highImpact, 'items' => [ [ 'label' => __('localization.review.findings_total'), 'value' => (string) $total, 'color' => $total > 0 ? 'info' : 'gray', ], [ 'label' => __('localization.review.findings_open'), 'value' => (string) $open, 'color' => $open > 0 ? 'warning' : 'gray', ], [ 'label' => __('localization.review.findings_high_impact'), 'value' => (string) $highImpact, 'color' => $highImpact > 0 ? 'danger' : 'gray', ], [ 'label' => __('localization.review.accepted_risks'), 'value' => (string) $accepted, 'color' => $accepted > 0 ? 'info' : 'gray', ], ], ]; } /** * @return Collection */ private function acceptedRiskExceptionsForTenant(ManagedEnvironment $tenant): Collection { $user = auth()->user(); if (! $user instanceof User || ! $user->can(Capabilities::FINDING_EXCEPTION_VIEW, $tenant)) { return collect(); } return FindingException::query() ->with(['owner', 'approver', 'currentDecision']) ->where('workspace_id', (int) $tenant->workspace_id) ->where('managed_environment_id', (int) $tenant->getKey()) ->current() ->orderByRaw("case when current_validity_state in ('expiring', 'expired', 'missing_support') then 0 else 1 end") ->latest('approved_at') ->latest('requested_at') ->latest('id') ->get(); } /** * @param Collection $exceptions * @return list */ private function acceptedRiskDetailRows(Collection $exceptions): array { if ($exceptions->isEmpty()) { return []; } $owner = $exceptions ->map(static fn (FindingException $exception): ?string => $exception->owner?->name ?? $exception->approver?->name) ->filter(static fn (?string $name): bool => is_string($name) && trim($name) !== '') ->first(); $ownedException = $exceptions ->first(static fn (FindingException $exception): bool => $exception->owner?->name !== null || $exception->approver?->name !== null); $reviewDate = $exceptions ->map(static fn (FindingException $exception): mixed => $exception->review_due_at ?? $exception->expires_at) ->first(static fn (mixed $date): bool => $date instanceof \DateTimeInterface); $hasMissingReviewDate = $exceptions ->contains(static fn (FindingException $exception): bool => $exception->review_due_at === null && $exception->expires_at === null); $reason = $ownedException instanceof FindingException && is_string($ownedException->request_reason) && trim($ownedException->request_reason) !== '' ? trim($ownedException->request_reason) : $exceptions ->map(static fn (FindingException $exception): ?string => is_string($exception->request_reason) ? trim($exception->request_reason) : null) ->filter(static fn (?string $value): bool => is_string($value) && $value !== '') ->first(); $rows = [ [ 'label' => __('localization.review.accepted_risk_owner'), 'value' => is_string($owner) ? $owner : __('localization.review.not_recorded'), 'color' => is_string($owner) ? 'info' : 'warning', ], ]; if ($reviewDate instanceof \DateTimeInterface) { $rows[] = [ 'label' => __('localization.review.accepted_risk_next_review'), 'value' => $reviewDate->format('Y-m-d'), 'color' => 'info', ]; } if ($hasMissingReviewDate) { $rows[] = [ 'label' => __('localization.review.accepted_risk_next_review'), 'value' => __('localization.review.review_date_not_recorded'), 'color' => 'warning', ]; } $rows[] = [ 'label' => __('localization.review.accepted_risk_rationale'), 'value' => is_string($reason) ? Str::limit($reason, 160) : __('localization.review.not_recorded'), 'color' => is_string($reason) ? 'info' : 'warning', ]; return $rows; } /** * @return array{summary_label:string,summary_color:string,items:list,detail_rows:list} */ private function acceptedRiskPanelForReview(EnvironmentReview $review, ManagedEnvironment $tenant): array { $package = $this->governancePackageSummaryForReview($review); $hasFollowUp = $this->acceptedRiskFollowUpRequiredForReview($review); $exceptions = $this->acceptedRiskExceptionsForTenant($tenant); $acceptedEntries = collect($package['accepted_risks'] ?? []) ->filter(static fn (mixed $entry): bool => is_array($entry)); $decisionEntries = collect($package['governance_decisions'] ?? []) ->filter(static fn (mixed $entry): bool => is_array($entry)); $allEntries = $acceptedEntries->merge($decisionEntries); $total = max($allEntries->count(), $exceptions->count()); $expiring = max( $allEntries->where('governance_state', 'expiring_exception')->count(), $exceptions->where('current_validity_state', FindingException::VALIDITY_EXPIRING)->count(), ); $expired = max( $allEntries->where('governance_state', 'expired_exception')->count(), $exceptions->where('current_validity_state', FindingException::VALIDITY_EXPIRED)->count(), ); $pending = max( $allEntries->where('governance_state', 'pending_exception')->count(), $exceptions->where('status', FindingException::STATUS_PENDING)->count(), ); $needsReview = $allEntries ->filter(static fn (array $entry): bool => in_array( (string) ($entry['governance_state'] ?? ''), self::ACCEPTED_RISK_FOLLOW_UP_STATES, true, )) ->count(); $needsReview = max($needsReview, $exceptions ->filter(static fn (FindingException $exception): bool => in_array( (string) $exception->current_validity_state, [ FindingException::VALIDITY_EXPIRING, FindingException::VALIDITY_EXPIRED, FindingException::VALIDITY_REVOKED, FindingException::VALIDITY_MISSING_SUPPORT, ], true, )) ->count()); return [ 'summary_label' => $hasFollowUp ? __('localization.review.accepted_risk_follow_up') : __('localization.review.accepted_risk_no_action_needed'), 'summary_color' => $hasFollowUp ? ($expired > 0 ? 'danger' : 'warning') : 'gray', 'items' => [ [ 'label' => __('localization.review.accepted_risks'), 'value' => (string) $total, 'color' => $total > 0 ? 'info' : 'gray', ], [ 'label' => __('localization.review.accepted_risks_expiring_soon'), 'value' => (string) $expiring, 'color' => $expiring > 0 ? 'warning' : 'gray', ], [ 'label' => __('localization.review.accepted_risks_expired'), 'value' => (string) $expired, 'color' => $expired > 0 ? 'danger' : 'gray', ], [ 'label' => __('localization.review.accepted_risks_pending_approval'), 'value' => (string) $pending, 'color' => $pending > 0 ? 'warning' : 'gray', ], [ 'label' => __('localization.review.accepted_risks_needs_review'), 'value' => (string) $needsReview, 'color' => $needsReview > 0 ? 'warning' : 'gray', ], ], 'detail_rows' => $this->acceptedRiskDetailRows($exceptions), ]; } /** * @param array{state:string,label:string,description:string} $packageAvailability * @param array $outputReadiness * @return array{ * status_label:string, * status_color:string, * description:string, * detail_rows:list, * download_url:?string * } */ private function reviewPackPanelForReview( EnvironmentReview $review, ManagedEnvironment $tenant, array $packageAvailability, ?string $downloadUrl, array $outputReadiness, ): array { $pack = $review->currentExportReviewPack; $snapshot = $review->evidenceSnapshot; $evidenceBasis = $this->evidenceBasisForReview($review, $packageAvailability, $outputReadiness); $sectionSummary = is_array($outputReadiness['section_summary'] ?? null) ? $outputReadiness['section_summary'] : []; $packageExistsState = match (true) { $pack instanceof ReviewPack && $pack->generated_at !== null => 'available', $packageAvailability['state'] === 'preparing' => 'preparing', default => 'unavailable', }; $packageExistsLabel = match ($packageExistsState) { 'available' => __('localization.review.available'), 'preparing' => __('localization.review.preparing'), default => __('localization.review.unavailable'), }; $packageExistsColor = match ($packageExistsState) { 'available' => 'success', 'preparing' => 'warning', default => 'gray', }; $customerSharingState = (string) ($outputReadiness['customer_safe_state'] ?? 'requires_review'); return [ 'status_label' => $packageAvailability['label'], 'status_color' => $this->governancePackageAvailabilityColor($tenant), 'description' => $this->reviewPackPanelDescription($packageAvailability, $outputReadiness), 'sections' => [ [ 'key' => 'package_exists', 'title' => __('localization.review.package_exists'), 'label' => $packageExistsLabel, 'color' => $packageExistsColor, 'description' => $this->reviewPackPackageExistsDescription($packageExistsState), 'rows' => [ [ 'label' => __('localization.review.last_generated'), 'value' => $pack instanceof ReviewPack && $pack->generated_at !== null ? $pack->generated_at->format('M j, Y H:i') : __('localization.review.unavailable'), 'color' => 'gray', ], [ 'label' => __('localization.review.evidence_source'), 'value' => $snapshot instanceof EvidenceSnapshot && $snapshot->generated_at !== null ? $snapshot->generated_at->format('M j, Y H:i') : __('localization.review.unavailable'), 'color' => 'gray', ], [ 'label' => __('localization.review.operation_proof'), 'value' => $pack instanceof ReviewPack && $pack->operationRun instanceof OperationRun ? OperationRunLinks::identifier($pack->operationRun) : __('localization.review.operation_proof_unavailable'), 'color' => $pack instanceof ReviewPack && $pack->operationRun instanceof OperationRun ? 'info' : 'gray', ], ], ], [ 'key' => 'internal_export', 'title' => __('localization.review.internal_export'), 'label' => $downloadUrl !== null ? __('localization.review.export_ready') : __('localization.review.export_not_ready'), 'color' => $downloadUrl !== null ? 'success' : ($packageAvailability['state'] === 'preparing' ? 'warning' : 'gray'), 'description' => $this->reviewPackInternalExportDescription($packageAvailability, $downloadUrl), 'rows' => [ [ 'label' => __('localization.review.export_availability'), 'value' => $downloadUrl !== null ? __('localization.review.export_ready') : __('localization.review.export_not_ready'), 'color' => $downloadUrl !== null ? 'success' : 'gray', ], [ 'label' => __('localization.review.evidence_basis_state'), 'value' => $evidenceBasis['label'], 'color' => $evidenceBasis['color'], ], [ 'label' => __('localization.review.section_completeness'), 'value' => $this->sectionCompletenessLabel($sectionSummary), 'color' => ((int) ($sectionSummary['required_limited'] ?? 0)) > 0 ? 'warning' : 'success', ], ], ], [ 'key' => 'customer_sharing', 'title' => __('localization.review.customer_sharing'), 'label' => $this->workspaceBoundaryLabel($customerSharingState), 'color' => $this->workspaceBoundaryColor($customerSharingState), 'description' => $this->reviewPackCustomerSharingDescription($outputReadiness), 'rows' => [ [ 'label' => __('localization.review.sharing_boundary'), 'value' => $this->workspaceBoundaryLabel($customerSharingState), 'color' => $this->workspaceBoundaryColor($customerSharingState), ], [ 'label' => __('localization.review.pii_state'), 'value' => (bool) ($outputReadiness['contains_pii'] ?? false) ? __('localization.review.contains_pii') : __('localization.review.pii_excluded'), 'color' => (bool) ($outputReadiness['contains_pii'] ?? false) ? 'warning' : 'success', ], [ 'label' => __('localization.review.protected_values'), 'value' => (bool) ($outputReadiness['protected_values_hidden'] ?? true) ? __('localization.review.protected_values_hidden') : __('localization.review.unavailable'), 'color' => (bool) ($outputReadiness['protected_values_hidden'] ?? true) ? 'success' : 'warning', ], [ 'label' => __('localization.review.disclosure'), 'value' => (bool) ($outputReadiness['disclosure_present'] ?? false) ? __('localization.review.disclosure_present') : __('localization.review.unavailable'), 'color' => (bool) ($outputReadiness['disclosure_present'] ?? false) ? 'success' : 'warning', ], ], ], ], 'download_url' => $downloadUrl, ]; } /** * @param array $decision * @return array{entries:list>,empty_state:string} */ private function customerSafeFollowUpsForReview(array $decision): array { $entries = collect($decision['entries'] ?? []) ->filter(static fn (mixed $entry): bool => is_array($entry)) ->map(static fn (array $entry): array => [ 'title' => (string) ($entry['title'] ?? __('localization.review.follow_up')), 'priority' => (string) ($decision['label'] ?? __('localization.review.follow_up')), 'proof' => __('localization.review.decision_trail'), 'summary' => (string) ($entry['summary'] ?? __('localization.review.decision_entry_customer_safe_summary')), 'next_action' => (string) ($entry['next_action'] ?? __('localization.review.decision_summary_requires_awareness_next_action')), ]) ->take(3) ->values() ->all(); return [ 'entries' => $entries, 'empty_state' => __('localization.review.customer_safe_follow_ups_empty'), ]; } /** * @return array{label:string,summary:string} */ private function diagnosticsDisclosureForReview(): array { return [ 'label' => __('localization.review.diagnostics'), 'summary' => __('localization.review.diagnostics_customer_workspace_default_hidden'), ]; } /** * @return list */ private function disclosureRuleRows(): array { return [ [ 'label' => __('localization.review.disclosure_decision'), 'value' => __('localization.review.disclosure_visible'), 'color' => 'info', ], [ 'label' => __('localization.review.disclosure_evidence'), 'value' => __('localization.review.disclosure_visible'), 'color' => 'info', ], [ 'label' => __('localization.review.disclosure_diagnostics'), 'value' => __('localization.review.disclosure_collapsed'), 'color' => 'gray', ], [ 'label' => __('localization.review.disclosure_raw_support'), 'value' => __('localization.review.disclosure_hidden'), 'color' => 'gray', ], ]; } private function authorizePageAccess(): void { $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User) { abort(403); } if (! $workspace instanceof Workspace) { throw new NotFoundHttpException; } $service = app(EnvironmentReviewRegisterService::class); if (! $service->canAccessWorkspace($user, $workspace)) { throw new NotFoundHttpException; } if ($this->authorizedTenants() === []) { throw new NotFoundHttpException; } } private function auditWorkspaceOpen(): void { $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User || ! $workspace instanceof Workspace) { return; } app(WorkspaceAuditLogger::class)->log( workspace: $workspace, action: AuditActionId::CustomerReviewWorkspaceOpened, context: [ 'metadata' => [ 'source_surface' => self::SOURCE_SURFACE, 'tenant_filter_id' => $this->currentTenantFilterId(), 'entitled_tenant_count' => count($this->authorizedTenants()), 'interpretation_version' => $this->currentTenantFilterInterpretationVersion(), 'interpretation_versions' => $this->visibleInterpretationVersions(), ], ], actor: $user, resourceType: 'customer_review_workspace', resourceId: (string) $workspace->getKey(), targetLabel: __('localization.review.customer_review_workspace'), ); } private function workspaceQuery(): Builder { $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User || ! $workspace instanceof Workspace) { return ManagedEnvironment::query()->whereRaw('1 = 0'); } return app(EnvironmentReviewRegisterService::class)->customerWorkspaceTenantQuery($user, $workspace); } /** * @return array */ private function tenantFilterOptions(): array { return collect($this->authorizedTenants()) ->mapWithKeys(static fn (ManagedEnvironment $tenant): array => [ (string) $tenant->getKey() => $tenant->name, ]) ->all(); } private function defaultTenantFilter(): ?string { return null; } private function applyRequestedTenantPrefilter(): void { $workspace = $this->workspace(); if (! $workspace instanceof Workspace) { return; } $filter = WorkspaceHubEnvironmentFilter::fromRequest(request(), $workspace); if (! $filter instanceof WorkspaceHubEnvironmentFilter) { return; } $environmentId = $filter->environmentId(); foreach ($this->authorizedTenants() as $tenant) { if ((int) $tenant->getKey() === $environmentId) { $this->tableFilters['managed_environment_id']['value'] = (string) $environmentId; $this->tableDeferredFilters['managed_environment_id']['value'] = (string) $environmentId; return; } } throw new NotFoundHttpException; } private function hasActiveFilters(): bool { return $this->currentTenantFilterId() !== null; } private function clearWorkspaceFilters(): void { $hadEnvironmentFilter = $this->currentTenantFilterId() !== null; $this->removeTableFilters(); $this->clearWorkspaceHubEnvironmentFilterState(request()); if ($hadEnvironmentFilter) { $this->redirectToCleanWorkspaceHubUrl(static::getUrl(panel: 'admin'), request()); } } private function workspaceEmptyStateHeading(): string { return $this->filteredViewHasNoReleasedReviewsButWorkspaceHasMatches() ? __('localization.review.filtered_no_released_customer_reviews') : __('localization.review.no_released_customer_reviews'); } private function workspaceEmptyStateDescription(): string { if ($this->filteredViewHasNoReleasedReviewsButWorkspaceHasMatches()) { return __('localization.review.filtered_no_released_customer_reviews_description'); } return __('localization.review.no_released_customer_reviews_description'); } private function filteredViewHasNoReleasedReviewsButWorkspaceHasMatches(): bool { $user = auth()->user(); $workspace = $this->workspace(); $tenant = $this->filteredTenant(); if (! $tenant instanceof ManagedEnvironment || ! $user instanceof User || ! $workspace instanceof Workspace) { return false; } if ($this->latestPublishedReview($tenant) instanceof EnvironmentReview) { return false; } return app(EnvironmentReviewRegisterService::class) ->customerWorkspaceLifecycleReviewQuery($user, $workspace) ->exists(); } private function currentTenantFilterId(): ?int { $tenantFilter = data_get($this->tableFilters, 'managed_environment_id.value'); if (! is_numeric($tenantFilter)) { $tenantFilter = data_get(session()->get($this->getTableFiltersSessionKey(), []), 'managed_environment_id.value'); } return is_numeric($tenantFilter) ? (int) $tenantFilter : null; } private function filteredTenant(): ?ManagedEnvironment { $tenantId = $this->currentTenantFilterId(); if (! is_int($tenantId)) { return null; } foreach ($this->authorizedTenants() as $tenant) { if ((int) $tenant->getKey() === $tenantId) { return $tenant; } } return null; } private function workspace(): ?Workspace { $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); return is_numeric($workspaceId) ? Workspace::query()->whereKey((int) $workspaceId)->first() : null; } private function latestReleasedTenant(): ?ManagedEnvironment { $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User || ! $workspace instanceof Workspace) { return null; } $query = app(EnvironmentReviewRegisterService::class)->customerWorkspaceLifecycleReviewQuery($user, $workspace); $tenantFilterId = $this->currentTenantFilterId(); if ($tenantFilterId !== null) { $query->where('managed_environment_id', $tenantFilterId); } $review = $query->first(); if (! $review instanceof EnvironmentReview || ! $review->tenant instanceof ManagedEnvironment) { return null; } $tenant = $review->tenant; $tenant->setRelation('environmentReviews', $review->newCollection([$review])); return $tenant; } private function latestPublishedReview(ManagedEnvironment $tenant): ?EnvironmentReview { if ($tenant->relationLoaded('environmentReviews')) { $review = $tenant->environmentReviews->first(); return $review instanceof EnvironmentReview ? $review : null; } return $tenant->environmentReviews() ->with(['tenant', 'evidenceSnapshot', 'currentExportReviewPack', 'supersededByReview']) ->where(function (Builder $query): void { $query->published() ->orWhere(function (Builder $query): void { $query ->where('status', EnvironmentReviewStatus::Superseded->value) ->whereNotNull('superseded_by_review_id'); }); }) ->orderByDesc('published_at') ->orderByDesc('generated_at') ->orderByDesc('id') ->first(); } private function workspaceLifecycleReview(): ?EnvironmentReview { $tenant = $this->latestReleasedTenant(); if (! $tenant instanceof ManagedEnvironment) { return null; } return $this->latestPublishedReview($tenant); } private function latestReviewUrl(ManagedEnvironment $tenant): ?string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof EnvironmentReview) { return null; } $query = array_filter( array_replace( [self::DETAIL_CONTEXT_QUERY_KEY => 1], [ 'source_surface' => self::SOURCE_SURFACE, 'tenant_filter_id' => $this->currentTenantFilterId(), ], $this->navigationContext()?->toQuery() ?? [], ), static fn (mixed $value): bool => $value !== null && $value !== '', ); return $this->appendQuery(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant), $query); } private function successorReviewUrlForReview(EnvironmentReview $review, ManagedEnvironment $tenant): ?string { if (! is_numeric($review->superseded_by_review_id)) { return null; } $successorReviewId = (int) $review->superseded_by_review_id; if (! EnvironmentReview::query() ->whereKey($successorReviewId) ->where('workspace_id', (int) $review->workspace_id) ->where('managed_environment_id', (int) $review->managed_environment_id) ->exists()) { return null; } return $this->appendQuery( EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $successorReviewId], $tenant), array_filter([ 'source_surface' => self::SOURCE_SURFACE, 'tenant_filter_id' => $this->currentTenantFilterId(), ], static fn (mixed $value): bool => $value !== null && $value !== ''), ); } private function operationUrlForReview(EnvironmentReview $review): ?string { $operationRun = $review->currentExportReviewPack?->operationRun ?? $review->operationRun; if (! $operationRun instanceof OperationRun) { return null; } return OperationRunLinks::tenantlessView((int) $operationRun->getKey()); } private function canManageReview(EnvironmentReview $review): bool { $user = auth()->user(); $tenant = $review->tenant; if (! $user instanceof User || ! $tenant instanceof ManagedEnvironment) { return false; } return app(CapabilityResolver::class)->can($user, $tenant, Capabilities::ENVIRONMENT_REVIEW_MANAGE); } private function reviewPackDownloadUrl(EnvironmentReview $review, ManagedEnvironment $tenant): ?string { $pack = $review->currentExportReviewPack; $user = auth()->user(); if (! $pack instanceof ReviewPack || ! $user instanceof User) { return null; } if ($this->governancePackageAvailability($tenant)['state'] !== 'available') { return null; } if (! $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant)) { return null; } if ($pack->status !== ReviewPackStatus::Ready->value) { return null; } if ($pack->expires_at !== null && $pack->expires_at->isPast()) { return null; } if (! filled($pack->file_path) || ! filled($pack->file_disk)) { return null; } return app(ReviewPackService::class)->generateDownloadUrl($pack, [ 'source_surface' => self::SOURCE_SURFACE, 'review_id' => (int) $review->getKey(), 'tenant_filter_id' => (string) ($this->currentTenantFilterId() ?? $tenant->getKey()), 'interpretation_version' => $review->controlInterpretationVersion(), ]); } private function latestPublishedAt(ManagedEnvironment $tenant): ?\Illuminate\Support\Carbon { return $this->latestPublishedReview($tenant)?->published_at; } private function reviewTruth(ManagedEnvironment $tenant): ?ArtifactTruthEnvelope { $review = $this->latestPublishedReview($tenant); return $review instanceof EnvironmentReview ? app(ArtifactTruthPresenter::class)->forEnvironmentReview($review) : null; } private function reviewOutcome(ManagedEnvironment $tenant): ?CompressedGovernanceOutcome { $presenter = app(ArtifactTruthPresenter::class); $review = $this->latestPublishedReview($tenant); $truth = $this->reviewTruth($tenant); if (! $review instanceof EnvironmentReview || ! $truth instanceof ArtifactTruthEnvelope) { return null; } return $presenter->compressedOutcomeFor($review, SurfaceCompressionContext::reviewRegister()) ?? $presenter->compressedOutcomeFromEnvelope($truth, SurfaceCompressionContext::reviewRegister()); } private function latestReviewStateLabel(ManagedEnvironment $tenant): string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof EnvironmentReview) { return __('localization.review.no_published_review'); } return match ($this->workspaceCustomerOutputState($tenant)) { 'ready' => __('localization.review.ready'), 'not_ready' => __('localization.review.not_ready'), default => __('localization.review.needs_review'), }; } private function latestReviewStateColor(ManagedEnvironment $tenant): string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof EnvironmentReview) { return 'gray'; } return match ($this->workspaceCustomerOutputState($tenant)) { 'ready' => 'success', 'not_ready' => in_array($this->governancePackageAvailability($tenant)['state'], ['expired', 'unavailable'], true) ? 'danger' : 'gray', default => 'warning', }; } private function latestReviewStateIcon(ManagedEnvironment $tenant): ?string { return $this->reviewOutcome($tenant)?->primaryBadge->icon; } private function latestReviewStateIconColor(ManagedEnvironment $tenant): ?string { return $this->reviewOutcome($tenant)?->primaryBadge->iconColor; } private function reviewOutcomeDescription(ManagedEnvironment $tenant): ?string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof EnvironmentReview) { return __('localization.review.no_published_review_available'); } $primaryReason = $this->reviewOutcome($tenant)?->primaryReason; $summary = is_array($review->summary) ? $review->summary : []; $findingOutcomes = $summary['finding_outcomes'] ?? null; if (! is_array($findingOutcomes)) { return $primaryReason; } $findingOutcomeSummary = app(FindingOutcomeSemantics::class)->compactOutcomeSummary($findingOutcomes); if ($findingOutcomeSummary === null) { return $primaryReason; } return trim($primaryReason.' '.__('localization.review.terminal_outcomes').': '.$findingOutcomeSummary.'.'); } private function controlReadinessLabel(ManagedEnvironment $tenant): string { $control = $this->primaryControlSummary($tenant); if ($control === null) { return __('localization.review.control_readiness_unmapped'); } $label = $control['readiness_label'] ?? null; return is_string($label) && trim($label) !== '' ? $label : ComplianceEvidenceMappingV1::readinessLabel((string) ($control['readiness_bucket'] ?? 'review_recommended')); } /** * @return array */ private function governancePackageSummary(ManagedEnvironment $tenant): array { $review = $this->latestPublishedReview($tenant); if (! $review instanceof EnvironmentReview) { return []; } return $this->governancePackageSummaryForReview($review); } /** * @return array */ private function governancePackageSummaryForReview(EnvironmentReview $review): array { $summary = is_array($review->summary) ? $review->summary : []; $package = is_array($summary['governance_package'] ?? null) ? $summary['governance_package'] : []; return $package; } /** * @return array{state:string,label:string,description:string} */ private function governancePackageAvailability(ManagedEnvironment $tenant): array { $review = $this->latestPublishedReview($tenant); if (! $review instanceof EnvironmentReview) { return [ 'state' => 'unavailable', 'label' => __('localization.review.governance_package_unavailable'), 'description' => __('localization.review.no_published_review_available'), ]; } $pack = $review->currentExportReviewPack; $user = auth()->user(); if (! $pack instanceof ReviewPack) { return [ 'state' => 'not_available', 'label' => __('localization.review.review_pack_not_available_yet'), 'description' => __('localization.review.review_pack_not_available_yet_description'), ]; } if (! $user instanceof User || ! $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant)) { return [ 'state' => 'unavailable', 'label' => __('localization.review.unavailable'), 'description' => __('localization.review.review_pack_unavailable_customer_description'), ]; } if ($pack->status === ReviewPackStatus::Expired->value || ($pack->expires_at !== null && $pack->expires_at->isPast())) { return [ 'state' => 'expired', 'label' => __('localization.review.governance_package_expired'), 'description' => __('localization.review.governance_package_expired_description'), ]; } if (in_array($pack->status, [ReviewPackStatus::Queued->value, ReviewPackStatus::Generating->value], true)) { return [ 'state' => 'preparing', 'label' => __('localization.review.review_pack_preparing'), 'description' => __('localization.review.review_pack_preparing_description'), ]; } if ($pack->status !== ReviewPackStatus::Ready->value) { return [ 'state' => 'unavailable', 'label' => __('localization.review.unavailable'), 'description' => __('localization.review.review_pack_unavailable_customer_description'), ]; } if (! filled($pack->file_path) || ! filled($pack->file_disk)) { return [ 'state' => 'not_available', 'label' => __('localization.review.review_pack_not_available_yet'), 'description' => __('localization.review.review_pack_not_available_yet_description'), ]; } return [ 'state' => 'available', 'label' => __('localization.review.available'), 'description' => __('localization.review.review_pack_available_customer_description'), ]; } private function governancePackageAvailabilityLabel(ManagedEnvironment $tenant): string { return match ($this->governancePackageAvailability($tenant)['state']) { 'available' => __('localization.review.available'), 'not_available' => __('localization.review.review_pack_not_available_yet'), 'preparing' => __('localization.review.review_pack_preparing'), 'expired' => __('localization.review.expired'), default => __('localization.review.unavailable'), }; } private function governancePackageAvailabilityColor(ManagedEnvironment $tenant): string { return match ($this->governancePackageAvailability($tenant)['state']) { 'available' => 'success', 'preparing' => 'warning', 'expired', 'unavailable' => 'danger', default => 'gray', }; } private function governancePackageTeaser(ManagedEnvironment $tenant): string { $package = $this->governancePackageSummary($tenant); $executiveSummary = $package['executive_summary'] ?? null; if (is_string($executiveSummary) && trim($executiveSummary) !== '') { return $executiveSummary; } return $this->governancePackageAvailability($tenant)['description']; } /** * @return array */ private function decisionSummaryForReview(EnvironmentReview $review): array { $package = $this->governancePackageSummaryForReview($review); $decisionSummary = is_array($package['decision_summary'] ?? null) ? $package['decision_summary'] : []; if ($decisionSummary === []) { return [ 'status' => 'unavailable', 'label' => __('localization.review.decision_evidence_unavailable'), 'color' => 'warning', 'total_count' => 0, 'summary' => __('localization.review.decision_summary_unavailable_description'), 'next_action' => __('localization.review.decision_summary_unavailable_next_action'), 'entries' => [], ]; } $status = (string) ($decisionSummary['status'] ?? 'unavailable'); if (! in_array($status, ['none', 'requires_awareness', 'unavailable', 'incomplete'], true)) { $status = 'unavailable'; } $entries = collect($decisionSummary['entries'] ?? []) ->filter(static fn (mixed $entry): bool => is_array($entry)) ->map(fn (array $entry): array => [ 'title' => $this->customerSafeText($entry['title'] ?? null, __('localization.review.governance_decisions')), 'summary' => $this->customerSafeText($entry['summary'] ?? null, __('localization.review.decision_entry_customer_safe_summary')), 'next_action' => $this->customerSafeText($entry['next_action'] ?? null, __('localization.review.decision_summary_requires_awareness_next_action')), ]) ->take(3) ->values() ->all(); return [ 'status' => $status, 'label' => $this->decisionSummaryLabel($status), 'color' => $this->decisionSummaryColor($status), 'total_count' => (int) ($decisionSummary['total_count'] ?? count($entries)), 'summary' => $this->customerSafeText( $decisionSummary['summary'] ?? null, $this->decisionSummaryFallbackText($status), ), 'next_action' => $this->customerSafeText( $decisionSummary['next_action'] ?? null, $this->decisionSummaryFallbackNextAction($status), ), 'entries' => $entries, ]; } private function decisionSummaryLabel(string $status): string { return match ($status) { 'requires_awareness' => __('localization.review.governance_decisions_requiring_awareness'), 'none' => __('localization.review.no_decisions_require_awareness'), 'incomplete' => __('localization.review.decision_evidence_incomplete'), default => __('localization.review.decision_evidence_unavailable'), }; } private function decisionSummaryColor(string $status): string { return match ($status) { 'requires_awareness' => 'warning', 'none' => 'success', default => 'gray', }; } private function decisionSummaryFallbackText(string $status): string { return match ($status) { 'requires_awareness' => __('localization.review.decision_summary_requires_awareness_description'), 'none' => __('localization.review.no_decisions_require_awareness_description'), 'incomplete' => __('localization.review.decision_summary_incomplete_description'), default => __('localization.review.decision_summary_unavailable_description'), }; } private function decisionSummaryFallbackNextAction(string $status): string { return match ($status) { 'requires_awareness' => __('localization.review.decision_summary_requires_awareness_next_action'), 'none' => __('localization.review.no_decisions_require_awareness_next_action'), 'incomplete' => __('localization.review.decision_summary_incomplete_next_action'), default => __('localization.review.decision_summary_unavailable_next_action'), }; } /** * @return array{count:int,entries:list>,empty_state:string} */ private function acceptedRisksForReview(EnvironmentReview $review): array { $package = $this->governancePackageSummaryForReview($review); $entries = collect($package['accepted_risks'] ?? []) ->filter(static fn (mixed $entry): bool => is_array($entry)) ->map(fn (array $entry): array => [ 'title' => $this->customerSafeText($entry['title'] ?? null, __('localization.review.accepted_risk_state_on_record')), 'state_label' => $this->acceptedRiskStateLabel(is_string($entry['governance_state'] ?? null) ? $entry['governance_state'] : null), 'summary' => $this->customerSafeText( $entry['customer_summary'] ?? null, __('localization.review.accepted_risk_customer_safe_summary'), ), ]) ->values(); return [ 'count' => $entries->count(), 'entries' => $entries->take(3)->all(), 'empty_state' => __('localization.review.no_accepted_risks_recorded'), ]; } private function acceptedRiskStateLabel(?string $state): string { return match ($state) { 'valid_exception' => __('localization.review.accepted_risk_state_current'), 'expiring_exception' => __('localization.review.accepted_risk_state_review_due'), default => __('localization.review.accepted_risk_state_on_record'), }; } /** * @param array{state:string,label:string,description:string} $packageAvailability * @param array $outputReadiness * @return array */ private function evidenceBasisForReview(EnvironmentReview $review, array $packageAvailability, array $outputReadiness): array { $package = $this->governancePackageSummaryForReview($review); $decision = $this->decisionSummaryForReview($review); $pack = $review->currentExportReviewPack; $evidenceState = (string) ($outputReadiness['evidence_completeness_state'] ?? ''); $state = match (true) { $package === [] => 'unavailable', ! $pack instanceof ReviewPack => 'not_generated', $evidenceState === EnvironmentReviewCompletenessState::Missing->value => 'missing', $evidenceState === EnvironmentReviewCompletenessState::Stale->value => 'stale', $evidenceState === EnvironmentReviewCompletenessState::Partial->value || $decision['status'] === 'incomplete' => 'incomplete', $decision['status'] === 'unavailable' => 'unavailable', $decision['status'] === 'none' => 'no_awareness_required', default => 'complete', }; return [ 'state' => $state, 'label' => $this->evidenceBasisLabel($state), 'summary' => $this->evidenceBasisSummary($state), 'color' => $this->evidenceBasisColor($state), ]; } private function evidenceBasisLabel(string $state): string { return match ($state) { 'complete' => __('localization.review.evidence_basis_complete'), 'no_awareness_required' => __('localization.review.evidence_basis_no_awareness_required'), 'missing' => __('localization.review.evidence_basis_missing'), 'stale' => __('localization.review.evidence_basis_stale'), 'incomplete' => __('localization.review.evidence_basis_incomplete'), 'not_generated' => __('localization.review.evidence_basis_not_generated'), default => __('localization.review.evidence_basis_unavailable'), }; } private function evidenceBasisSummary(string $state): string { return match ($state) { 'complete' => __('localization.review.evidence_basis_complete_description'), 'no_awareness_required' => __('localization.review.evidence_basis_no_awareness_required_description'), 'missing' => __('localization.review.evidence_basis_missing_description'), 'stale' => __('localization.review.evidence_basis_stale_description'), 'incomplete' => __('localization.review.evidence_basis_incomplete_description'), 'not_generated' => __('localization.review.evidence_basis_not_generated_description'), default => __('localization.review.evidence_basis_unavailable_description'), }; } private function evidenceBasisColor(string $state): string { return match ($state) { 'complete', 'no_awareness_required' => 'success', 'missing', 'stale', 'incomplete' => 'warning', default => 'gray', }; } private function evidenceSnapshotUrlForReview(EnvironmentReview $review, ManagedEnvironment $tenant): ?string { $snapshot = $review->evidenceSnapshot; $user = auth()->user(); if (! $snapshot instanceof EvidenceSnapshot || ! $user instanceof User || ! $user->can(Capabilities::EVIDENCE_VIEW, $tenant)) { return null; } return EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'admin'); } /** * @return array */ private function reviewPackOutputReadinessForReview(EnvironmentReview $review): array { return ReviewPackOutputResolutionGuidance::readinessForReview($review); } /** * @return array */ private function reviewOutputGuidanceForReview( EnvironmentReview $review, ?string $downloadUrl, ?string $reviewUrl, ?string $evidenceUrl, ): array { $operationUrl = null; $operationRun = $review->currentExportReviewPack?->operationRun ?? $review->operationRun; if ($operationRun instanceof OperationRun) { $operationUrl = OperationRunLinks::tenantlessView((int) $operationRun->getKey()); } return ReviewPackOutputResolutionGuidance::fromReadiness( $this->reviewPackOutputReadinessForReview($review), [ 'download' => $downloadUrl, 'review' => $reviewUrl, 'evidence' => $evidenceUrl, 'operation' => $operationUrl, ], ); } /** * @param array $outputGuidance * @return array */ private function reviewOutputResolutionCaseForReview(EnvironmentReview $review, array $outputGuidance): array { $tenant = $review->tenant; return ReviewPackOutputResolutionAdapter::fromGuidance( review: $review, guidance: $outputGuidance, sourceSurface: self::SOURCE_SURFACE, context: [ 'urls' => [ 'review' => $tenant instanceof ManagedEnvironment ? $this->latestReviewUrl($tenant) : null, 'evidence' => $tenant instanceof ManagedEnvironment ? $this->evidenceSnapshotUrlForReview($review, $tenant) : null, 'operation' => $this->operationUrlForReview($review), 'download' => $tenant instanceof ManagedEnvironment ? $this->reviewPackDownloadUrl($review, $tenant) : null, 'successor_review' => $tenant instanceof ManagedEnvironment ? $this->successorReviewUrlForReview($review, $tenant) : null, ], 'execution' => [ 'can_manage_review' => $this->canManageReview($review), 'successor_review_status' => $this->successorReviewStatusForReview($review), ], ], ); } private function reviewPackHasReadyExport(?ReviewPack $pack): bool { if (! $pack instanceof ReviewPack) { return false; } if ($pack->status !== ReviewPackStatus::Ready->value) { return false; } if ($pack->expires_at !== null && $pack->expires_at->isPast()) { return false; } return filled($pack->file_path) && filled($pack->file_disk); } /** * @param array $outputReadiness */ private function effectiveWorkspaceReadinessState(array $outputReadiness, bool $hasFindingFollowUp, bool $hasAcceptedRiskFollowUp): string { $state = (string) ($outputReadiness['readiness_state'] ?? ReviewPackOutputReadiness::STATE_EXPORT_NOT_READY); if ($state === ReviewPackOutputReadiness::STATE_CUSTOMER_SAFE_READY && ($hasFindingFollowUp || $hasAcceptedRiskFollowUp)) { return ReviewPackOutputReadiness::STATE_PUBLISHED_WITH_LIMITATIONS; } return $state; } private function workspaceReadinessLabel(string $state): string { return match ($state) { ReviewPackOutputReadiness::STATE_CUSTOMER_SAFE_READY => __('localization.review.customer_safe_review_pack_ready'), ReviewPackOutputReadiness::STATE_INTERNAL_REVIEW_PACKAGE_AVAILABLE => __('localization.review.internal_review_package_available'), ReviewPackOutputReadiness::STATE_EXPORT_NOT_READY => __('localization.review.export_not_ready'), default => __('localization.review.published_with_limitations'), }; } private function workspaceReadinessColor(string $state): string { return match ($state) { ReviewPackOutputReadiness::STATE_CUSTOMER_SAFE_READY => 'success', ReviewPackOutputReadiness::STATE_EXPORT_NOT_READY => 'gray', default => 'warning', }; } private function workspaceBoundaryLabel(string $state): string { return match ($state) { 'customer_safe_ready' => __('localization.review.customer_safe'), 'internal_only' => __('localization.review.internal_only'), 'not_ready' => __('localization.review.not_ready'), default => __('localization.review.requires_review'), }; } private function workspaceBoundaryColor(string $state): string { return match ($state) { 'customer_safe_ready' => 'success', 'internal_only', 'requires_review' => 'warning', default => 'gray', }; } /** * @param array $outputReadiness * @param array{state:string,label:string,description:string} $packageAvailability * @param array{summary:string} $findingPanel */ private function workspaceReadinessReason( string $reasonCode, array $outputReadiness, array $findingPanel, array $packageAvailability, ): string { return match ($reasonCode) { 'findings_follow_up_required' => __('localization.review.findings_follow_up_required_reason', [ 'summary' => $findingPanel['summary'], ]), 'accepted_risk_follow_up_required' => __('localization.review.accepted_risk_follow_up_required_reason'), 'export_not_ready' => __('localization.review.export_not_ready_reason'), 'evidence_basis_missing' => __('localization.review.evidence_basis_missing_reason'), 'evidence_basis_stale' => __('localization.review.evidence_basis_stale_reason'), 'evidence_basis_incomplete' => __('localization.review.evidence_basis_incomplete_reason'), 'required_sections_incomplete' => __('localization.review.required_sections_incomplete_reason', [ 'complete' => (int) data_get($outputReadiness, 'section_summary.required_complete', 0), 'total' => (int) data_get($outputReadiness, 'section_summary.required_total', 0), 'limited' => (int) data_get($outputReadiness, 'section_summary.required_limited', 0), ]), 'publish_blockers_present' => __('localization.review.publish_blockers_present_reason'), 'contains_pii' => __('localization.review.contains_pii_reason'), 'customer_safe_ready' => __('localization.review.customer_safe_review_pack_ready_reason'), default => $packageAvailability['description'], }; } private function workspaceReadinessImpact(string $state, string $reasonCode): string { return match ($reasonCode) { 'findings_follow_up_required' => __('localization.review.findings_follow_up_required_impact'), 'accepted_risk_follow_up_required' => __('localization.review.accepted_risk_follow_up_required_impact'), default => match ($state) { ReviewPackOutputReadiness::STATE_CUSTOMER_SAFE_READY => __('localization.review.customer_safe_review_pack_ready_impact'), ReviewPackOutputReadiness::STATE_INTERNAL_REVIEW_PACKAGE_AVAILABLE => __('localization.review.internal_review_package_available_impact'), ReviewPackOutputReadiness::STATE_EXPORT_NOT_READY => __('localization.review.export_not_ready_impact'), default => __('localization.review.published_with_limitations_impact'), }, }; } /** * @param array $baseCase * @param array $outputReadiness * @param array{summary:string} $findingPanel * @param array{state:string,label:string,description:string} $packageAvailability * @param array{primary_label:string,primary_url:?string,primary_icon:string,secondary_label:?string,secondary_url:?string} $actions * @return array */ private function workspaceFollowUpResolutionCase( array $baseCase, string $effectiveState, string $reasonCode, array $outputReadiness, array $findingPanel, array $packageAvailability, array $actions, ): array { $primaryAction = ResolutionAction::fromArray([ 'key' => 'customer_review_workspace.'.$reasonCode.'.primary_action', 'label' => $actions['primary_label'], 'url' => $actions['primary_url'], 'icon' => $actions['primary_icon'], 'kind' => str_starts_with($actions['primary_icon'], 'heroicon-o-arrow-down-tray') ? 'download' : 'environment_link', ], 'customer_review_workspace.'.$reasonCode.'.primary_action', $actions['primary_label']); $secondaryActions = $actions['secondary_url'] !== null && $actions['secondary_label'] !== null ? [ ResolutionAction::fromArray([ 'key' => 'customer_review_workspace.'.$reasonCode.'.secondary_action', 'label' => $actions['secondary_label'], 'url' => $actions['secondary_url'], 'icon' => 'heroicon-o-arrow-top-right-on-square', 'kind' => str_contains(strtolower($actions['secondary_label']), 'download') ? 'download' : 'environment_link', ], 'customer_review_workspace.'.$reasonCode.'.secondary_action', $actions['secondary_label']), ] : []; return ResolutionCase::make( key: 'customer_review_workspace.'.$reasonCode, scope: is_array($baseCase['scope'] ?? null) ? $baseCase['scope'] : [], severity: 'warning', status: 'action_required', title: $this->workspaceReadinessLabel($effectiveState), reason: $this->workspaceReadinessReason( reasonCode: $reasonCode, outputReadiness: $outputReadiness, findingPanel: $findingPanel, packageAvailability: $packageAvailability, ), impact: $this->workspaceReadinessImpact( state: $effectiveState, reasonCode: $reasonCode, ), primaryAction: $primaryAction, secondaryActions: $secondaryActions, sourceRefs: is_array($baseCase['source_refs'] ?? null) ? $baseCase['source_refs'] : [], evidenceRefs: is_array($baseCase['evidence_refs'] ?? null) ? $baseCase['evidence_refs'] : [], technicalDetails: is_array($baseCase['technical_details'] ?? null) ? $baseCase['technical_details'] : [], ); } /** * @return array{ * primary_label:string, * primary_url:?string, * primary_icon:string, * secondary_label:?string, * secondary_url:?string * } */ private function workspaceReadinessActions( string $state, string $reasonCode, ?string $downloadUrl, ?string $reviewUrl, ?string $evidenceUrl, ): array { if (in_array($reasonCode, ['findings_follow_up_required', 'accepted_risk_follow_up_required'], true)) { return [ 'primary_label' => __('localization.review.open_review'), 'primary_url' => $reviewUrl ?? $evidenceUrl ?? $downloadUrl, 'primary_icon' => 'heroicon-o-arrow-top-right-on-square', 'secondary_label' => match ($state) { ReviewPackOutputReadiness::STATE_INTERNAL_REVIEW_PACKAGE_AVAILABLE => $downloadUrl !== null ? __('localization.review.download_internal_review_pack') : null, default => $downloadUrl !== null ? __('localization.review.download_review_pack_with_limitations') : null, }, 'secondary_url' => $downloadUrl, ]; } return match ($state) { ReviewPackOutputReadiness::STATE_CUSTOMER_SAFE_READY => [ 'primary_label' => $downloadUrl !== null ? __('localization.review.download_customer_safe_review_pack') : __('localization.review.open_latest_review'), 'primary_url' => $downloadUrl ?? $reviewUrl, 'primary_icon' => $downloadUrl !== null ? 'heroicon-o-arrow-down-tray' : 'heroicon-o-arrow-top-right-on-square', 'secondary_label' => $downloadUrl !== null && $reviewUrl !== null ? __('localization.review.open_review') : null, 'secondary_url' => $downloadUrl !== null ? $reviewUrl : null, ], ReviewPackOutputReadiness::STATE_INTERNAL_REVIEW_PACKAGE_AVAILABLE => [ 'primary_label' => __('localization.review.review_package_contents'), 'primary_url' => $reviewUrl ?? $downloadUrl, 'primary_icon' => 'heroicon-o-arrow-top-right-on-square', 'secondary_label' => $downloadUrl !== null ? __('localization.review.download_internal_review_pack') : null, 'secondary_url' => $downloadUrl, ], ReviewPackOutputReadiness::STATE_EXPORT_NOT_READY => [ 'primary_label' => __('localization.review.open_evidence_basis'), 'primary_url' => $evidenceUrl ?? $reviewUrl, 'primary_icon' => 'heroicon-o-arrow-top-right-on-square', 'secondary_label' => $reviewUrl !== null && $reviewUrl !== $evidenceUrl ? __('localization.review.open_review') : null, 'secondary_url' => $reviewUrl !== $evidenceUrl ? $reviewUrl : null, ], default => [ 'primary_label' => __('localization.review.review_output_limitations'), 'primary_url' => $reviewUrl ?? $evidenceUrl ?? $downloadUrl, 'primary_icon' => 'heroicon-o-arrow-top-right-on-square', 'secondary_label' => $downloadUrl !== null ? __('localization.review.download_review_pack_with_limitations') : null, 'secondary_url' => $downloadUrl, ], }; } /** * @param array $resolutionCase * @return array */ private function decorateSuccessorResolutionCase(array $resolutionCase, EnvironmentReview $review): array { if (data_get($resolutionCase, 'primary_action.key') !== 'open_successor_review') { return $resolutionCase; } $successor = $this->successorReviewForReview($review); if (! $successor instanceof EnvironmentReview || ! $successor->isMutable()) { return $resolutionCase; } $canPublishSuccessor = app(\App\Services\EnvironmentReviews\EnvironmentReviewReadinessGate::class)->canPublish($successor); return array_replace($resolutionCase, [ 'title' => __('localization.review.draft_review_exists'), 'reason' => $canPublishSuccessor ? __('localization.review.draft_review_exists_ready_reason') : __('localization.review.draft_review_exists_blocked_reason'), 'impact' => $canPublishSuccessor ? __('localization.review.draft_review_exists_ready_impact') : __('localization.review.draft_review_exists_blocked_impact'), ]); } private function successorReviewForReview(EnvironmentReview $review): ?EnvironmentReview { if ($review->relationLoaded('supersededByReview')) { return $review->supersededByReview instanceof EnvironmentReview ? $review->supersededByReview : null; } if (! is_numeric($review->superseded_by_review_id)) { return null; } return EnvironmentReview::query() ->with(['tenant', 'sections', 'evidenceSnapshot', 'currentExportReviewPack']) ->whereKey((int) $review->superseded_by_review_id) ->where('workspace_id', (int) $review->workspace_id) ->where('managed_environment_id', (int) $review->managed_environment_id) ->first(); } private function successorReviewStatusForReview(EnvironmentReview $review): ?string { return $this->successorReviewForReview($review)?->status; } /** * @param array $resolutionCase * @param array $outputGuidance */ private function workspaceActionHelpForResolutionCase(array $resolutionCase, array $outputGuidance): ?string { $primaryActionKey = is_string(data_get($resolutionCase, 'primary_action.key')) ? (string) data_get($resolutionCase, 'primary_action.key') : null; return match ($primaryActionKey) { 'create_next_review' => __('localization.review.output_action_help_create_next_review'), 'refresh_review' => __('localization.review.output_action_help_refresh_review'), 'publish_review' => __('localization.review.output_action_help_publish_review'), 'open_successor_review' => data_get($resolutionCase, 'title') === __('localization.review.draft_review_exists') ? __('localization.review.output_action_help_open_draft_review') : __('localization.review.output_action_help_open_successor_review'), default => is_string($outputGuidance['action_help'] ?? null) ? (string) $outputGuidance['action_help'] : null, }; } /** * @param array{state:string,label:string,description:string} $packageAvailability * @param array $outputReadiness */ private function reviewPackPanelDescription(array $packageAvailability, array $outputReadiness): string { if ($packageAvailability['state'] !== 'available') { return $packageAvailability['description']; } return match ((string) ($outputReadiness['readiness_state'] ?? ReviewPackOutputReadiness::STATE_EXPORT_NOT_READY)) { ReviewPackOutputReadiness::STATE_CUSTOMER_SAFE_READY => __('localization.review.review_pack_customer_safe_ready_description'), ReviewPackOutputReadiness::STATE_INTERNAL_REVIEW_PACKAGE_AVAILABLE => __('localization.review.review_pack_internal_review_description'), ReviewPackOutputReadiness::STATE_EXPORT_NOT_READY => __('localization.review.review_pack_export_not_ready_description'), default => __('localization.review.review_pack_with_limitations_description'), }; } private function reviewPackPackageExistsDescription(string $state): string { return match ($state) { 'available' => __('localization.review.package_exists_available_description'), 'preparing' => __('localization.review.package_exists_preparing_description'), default => __('localization.review.package_exists_unavailable_description'), }; } /** * @param array{state:string,label:string,description:string} $packageAvailability */ private function reviewPackInternalExportDescription(array $packageAvailability, ?string $downloadUrl): string { if ($downloadUrl !== null) { return __('localization.review.internal_export_ready_description'); } return match ($packageAvailability['state']) { 'preparing' => __('localization.review.internal_export_preparing_description'), 'available' => __('localization.review.internal_export_not_ready_description'), default => __('localization.review.internal_export_unavailable_description'), }; } /** * @param array $outputReadiness */ private function reviewPackCustomerSharingDescription(array $outputReadiness): string { return match ((string) ($outputReadiness['customer_safe_state'] ?? 'requires_review')) { ReviewPackOutputReadiness::STATE_CUSTOMER_SAFE_READY => __('localization.review.customer_sharing_ready_description'), 'internal_only' => __('localization.review.customer_sharing_internal_only_description'), default => __('localization.review.customer_sharing_requires_review_description'), }; } /** * @param array $sectionSummary */ private function sectionCompletenessLabel(array $sectionSummary): string { $requiredTotal = (int) ($sectionSummary['required_total'] ?? 0); $requiredComplete = (int) ($sectionSummary['required_complete'] ?? 0); $requiredLimited = (int) ($sectionSummary['required_limited'] ?? 0); if ($requiredTotal <= 0) { return __('localization.review.unavailable'); } if ($requiredLimited > 0) { return __('localization.review.section_completeness_limited', [ 'complete' => $requiredComplete, 'total' => $requiredTotal, 'limited' => $requiredLimited, ]); } return __('localization.review.section_completeness_complete', [ 'complete' => $requiredComplete, 'total' => $requiredTotal, ]); } private function workspaceCustomerOutputState(ManagedEnvironment $tenant): string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof EnvironmentReview) { return 'not_ready'; } if ($this->primaryControlSummary($tenant) === null || $this->evidenceStatusState($tenant) !== 'available') { return 'not_ready'; } $effectiveState = $this->effectiveWorkspaceReadinessState( $this->reviewPackOutputReadinessForReview($review), $this->findingPanelForReview($tenant)['open_count'] > 0, $this->acceptedRiskFollowUpRequiredForReview($review), ); return match ($effectiveState) { ReviewPackOutputReadiness::STATE_CUSTOMER_SAFE_READY => 'ready', ReviewPackOutputReadiness::STATE_EXPORT_NOT_READY => 'not_ready', default => 'needs_review', }; } private function customerSafeText(mixed $value, string $fallback, int $limit = 220): string { if (! is_string($value) || trim($value) === '') { return $fallback; } return Str::limit(trim($value), $limit); } private function controlReadinessColor(ManagedEnvironment $tenant): string { return match ((string) ($this->primaryControlSummary($tenant)['readiness_bucket'] ?? 'unmapped')) { 'follow_up_required' => 'warning', 'review_recommended' => 'info', 'evidence_on_record' => 'success', default => 'gray', }; } private function controlReadinessDescription(ManagedEnvironment $tenant): string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof EnvironmentReview) { return __('localization.review.no_published_review_available'); } $controls = $review->controlInterpretationControls(); if ($controls === []) { return __('localization.review.control_readiness_unmapped_description'); } $summary = collect($controls) ->take(2) ->map(function (array $control): string { $name = is_string($control['control_name'] ?? null) ? $control['control_name'] : __('localization.review.control'); $label = is_string($control['readiness_label'] ?? null) ? $control['readiness_label'] : ComplianceEvidenceMappingV1::readinessLabel((string) ($control['readiness_bucket'] ?? 'review_recommended')); return $name.': '.$label; }) ->implode(' · '); $remaining = count($controls) - 2; if ($remaining > 0) { $summary .= ' · '.__('localization.review.additional_controls', ['count' => $remaining]); } $limitations = $this->controlLimitationSummary($review); return trim($summary.($limitations !== null ? ' '.$limitations : '')); } private function controlEvidenceBasisSummary(ManagedEnvironment $tenant): string { $control = $this->primaryControlSummary($tenant); if ($control === null) { return __('localization.review.control_evidence_unmapped'); } $summary = $control['evidence_basis_summary'] ?? null; return is_string($summary) && trim($summary) !== '' ? $summary : __('localization.review.control_evidence_unavailable'); } private function controlRecommendedNextAction(ManagedEnvironment $tenant): string { if ($this->primaryControlSummary($tenant) === null) { return __('localization.review.workspace_next_step_control_mapping'); } if ($this->evidenceStatusState($tenant) !== 'available') { return __('localization.review.workspace_next_step_evidence_review'); } $review = $this->latestPublishedReview($tenant); if (! $review instanceof EnvironmentReview) { return __('localization.review.workspace_next_step_review_open'); } $readinessState = $this->effectiveWorkspaceReadinessState( $this->reviewPackOutputReadinessForReview($review), $this->findingPanelForReview($tenant)['open_count'] > 0, $this->acceptedRiskFollowUpRequiredForReview($review), ); return match (true) { $readinessState === ReviewPackOutputReadiness::STATE_EXPORT_NOT_READY => __('localization.review.workspace_next_step_evidence_review'), $this->governancePackageAvailability($tenant)['state'] === 'available' => __('localization.review.workspace_next_step_package_review'), default => __('localization.review.workspace_next_step_review_open'), }; } private function workspaceReviewNeedsAttention(ManagedEnvironment $tenant): bool { return $this->workspaceCustomerOutputState($tenant) !== 'ready'; } private function evidenceStatusState(ManagedEnvironment $tenant): string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof EnvironmentReview) { return 'pending'; } $snapshot = $review->evidenceSnapshot; $user = auth()->user(); if (! $snapshot instanceof EvidenceSnapshot) { return 'pending'; } if (! $user instanceof User || ! $user->can(Capabilities::EVIDENCE_VIEW, $tenant)) { return 'restricted'; } if ((string) $snapshot->status === 'expired' || ($snapshot->expires_at !== null && $snapshot->expires_at->isPast())) { return 'expired'; } return 'available'; } private function evidenceStatusLabelForState(string $state): string { return match ($state) { 'available' => __('localization.review.available'), 'restricted' => __('localization.review.restricted'), 'expired' => __('localization.review.expired'), default => __('localization.review.pending'), }; } private function evidenceStatusColorForState(string $state): string { return match ($state) { 'available' => 'success', 'restricted', 'expired' => 'danger', default => 'gray', }; } private function controlRecommendedNextActionDescription(ManagedEnvironment $tenant): string { $control = $this->primaryControlSummary($tenant); if ($control === null) { return __('localization.review.control_recommendation_unmapped'); } $action = $control['recommended_next_action'] ?? null; return is_string($action) && trim($action) !== '' ? $action : __('localization.review.no_action_needed'); } /** * @return array|null */ private function primaryControlSummary(ManagedEnvironment $tenant): ?array { $review = $this->latestPublishedReview($tenant); if (! $review instanceof EnvironmentReview) { return null; } $controls = collect($review->controlInterpretationControls()); return $controls ->sortBy(static fn (array $control): int => match ((string) ($control['readiness_bucket'] ?? '')) { 'follow_up_required' => 0, 'review_recommended' => 1, 'evidence_on_record' => 2, default => 3, }) ->first(); } private function controlLimitationSummary(EnvironmentReview $review): ?string { $counts = $review->controlInterpretationLimitationCounts(); if ($counts === []) { return null; } $labels = collect($counts) ->filter(static fn (int $count): bool => $count > 0) ->keys() ->map(static fn (string $flag): string => ComplianceEvidenceMappingV1::limitationLabel($flag)) ->values() ->all(); return $labels === [] ? null : __('localization.review.control_limitations_summary', ['limitations' => implode(', ', $labels)]); } private function findingSummary(ManagedEnvironment $tenant): string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof EnvironmentReview) { return __('localization.review.no_published_review_available'); } $summary = is_array($review->summary) ? $review->summary : []; $findingCount = (int) ($summary['finding_count'] ?? 0); $findingOutcomes = is_array($summary['finding_outcomes'] ?? null) ? $summary['finding_outcomes'] : []; $terminalOutcomes = app(FindingOutcomeSemantics::class)->compactOutcomeSummary($findingOutcomes); if ($findingCount === 0) { return __('localization.review.no_findings_recorded'); } if ($terminalOutcomes === null) { return __('localization.review.findings_count_summary', ['count' => $findingCount]); } return __('localization.review.findings_count_with_outcomes', [ 'count' => $findingCount, 'outcomes' => $terminalOutcomes, ]); } private function acceptedRiskSummary(ManagedEnvironment $tenant): string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof EnvironmentReview) { return __('localization.review.no_published_review_available'); } $summary = is_array($review->summary) ? $review->summary : []; $riskAcceptance = is_array($summary['risk_acceptance'] ?? null) ? $summary['risk_acceptance'] : []; $statusMarkedCount = (int) ($riskAcceptance['status_marked_count'] ?? 0); $validGovernedCount = (int) ($riskAcceptance['valid_governed_count'] ?? 0); $warningCount = (int) ($riskAcceptance['warning_count'] ?? 0); $countSummary = match (true) { $statusMarkedCount === 0 => __('localization.review.no_accepted_risks_recorded'), $warningCount > 0 => __('localization.review.accepted_risks_need_follow_up', ['warnings' => $warningCount, 'total' => $statusMarkedCount]), $validGovernedCount > 0 => __('localization.review.accepted_risks_governed', ['count' => $validGovernedCount]), default => __('localization.review.accepted_risks_on_record', ['count' => $statusMarkedCount]), }; $accountability = $this->acceptedRiskAccountability($tenant); return $accountability === null ? $countSummary : $countSummary.' '.$accountability; } private function evidenceProofAvailability(ManagedEnvironment $tenant): string { $review = $this->latestPublishedReview($tenant); if (! $review instanceof EnvironmentReview) { return __('localization.review.no_published_review_available'); } $snapshot = $review->evidenceSnapshot; $user = auth()->user(); if (! $snapshot instanceof EvidenceSnapshot) { return __('localization.review.evidence_proof_absent'); } if (! $user instanceof User || ! $user->can(Capabilities::EVIDENCE_VIEW, $tenant)) { return __('localization.review.evidence_proof_access_unavailable'); } if ((string) $snapshot->status === 'expired' || ($snapshot->expires_at !== null && $snapshot->expires_at->isPast())) { return __('localization.review.evidence_proof_expired'); } return __('localization.review.evidence_proof_available'); } private function evidenceStatusLabel(ManagedEnvironment $tenant): string { return $this->evidenceStatusLabelForState($this->evidenceStatusState($tenant)); } private function evidenceStatusColor(ManagedEnvironment $tenant): string { return $this->evidenceStatusColorForState($this->evidenceStatusState($tenant)); } /** * @return list */ private function visibleInterpretationVersions(): array { $user = auth()->user(); $workspace = $this->workspace(); if (! $user instanceof User || ! $workspace instanceof Workspace) { return []; } return app(EnvironmentReviewRegisterService::class) ->latestPublishedQuery($user, $workspace) ->get() ->map(static fn (EnvironmentReview $review): ?string => $review->controlInterpretationVersion()) ->filter() ->unique() ->values() ->all(); } private function currentTenantFilterInterpretationVersion(): ?string { $tenant = $this->filteredTenant(); if (! $tenant instanceof ManagedEnvironment) { return null; } return $this->latestPublishedReview($tenant)?->controlInterpretationVersion(); } private function acceptedRiskAccountability(ManagedEnvironment $tenant): ?string { $exception = FindingException::query() ->with(['owner', 'approver', 'currentDecision']) ->where('workspace_id', (int) $tenant->workspace_id) ->where('managed_environment_id', (int) $tenant->getKey()) ->current() ->orderByRaw("case when current_validity_state in ('valid', 'expiring') then 0 else 1 end") ->latest('approved_at') ->latest('requested_at') ->latest('id') ->first(); if (! $exception instanceof FindingException) { return null; } $accountable = $exception->owner?->name ?? $exception->approver?->name; $decisionType = $exception->currentDecision?->decision_type; $reviewDue = $exception->review_due_at ?? $exception->expires_at; $reason = is_string($exception->request_reason) ? trim($exception->request_reason) : ''; $parts = []; if (is_string($accountable) && trim($accountable) !== '') { $parts[] = $reviewDue === null ? __('localization.review.accepted_risk_accountable', ['name' => $accountable]) : __('localization.review.accepted_risk_accountable_until', [ 'name' => $accountable, 'date' => $reviewDue->toDateString(), ]); } elseif (is_string($decisionType) && trim($decisionType) !== '') { $parts[] = __('localization.review.accepted_risk_partial_accountability'); } if ($reason !== '') { $parts[] = __('localization.review.accepted_risk_reason', [ 'reason' => Str::limit($reason, 160), ]); } return $parts === [] ? null : implode(' ', $parts); } private function navigationContext(): ?CanonicalNavigationContext { return CanonicalNavigationContext::fromRequest(request()); } private function incomingGovernanceContext(): ?CanonicalNavigationContext { $context = $this->navigationContext(); return $context?->sourceSurface === 'governance.inbox' ? $context : null; } /** * @param array $query */ private function appendQuery(string $url, array $query): string { if ($query === []) { return $url; } return $url.(str_contains($url, '?') ? '&' : '?').http_build_query($query); } }