*/ public static function fromReview(EnvironmentReview $review, array $urls = []): array { return self::fromReadiness(self::readinessForReview($review), $urls); } /** * @return array */ public static function readinessForReview(EnvironmentReview $review): array { $review->loadMissing(['sections', 'evidenceSnapshot', 'currentExportReviewPack']); $pack = $review->currentExportReviewPack; $packSummary = is_array($pack?->summary ?? null) ? $pack->summary : []; $controlInterpretation = is_array($packSummary['control_interpretation'] ?? null) ? $packSummary['control_interpretation'] : []; $snapshot = $review->evidenceSnapshot; $summary = is_array($review->summary) ? $review->summary : []; $sections = self::outputSections($review, $pack); $requiredSections = $sections ->filter(static fn (mixed $section): bool => (bool) $section->required) ->values(); $includePii = (bool) (is_array($pack?->options ?? null) ? ($pack->options['include_pii'] ?? true) : true); $nonCertificationDisclosure = trim((string) ($controlInterpretation['non_certification_disclosure'] ?? '')); return ReviewPackOutputReadiness::derive( reviewStatus: (string) $review->status, reviewCompletenessState: (string) $review->completeness_state, evidenceCompletenessState: $snapshot instanceof EvidenceSnapshot ? (string) $snapshot->completeness_state : EnvironmentReviewCompletenessState::Missing->value, sectionStateCounts: self::sectionStateCounts($sections), requiredSectionCount: $requiredSections->count(), requiredSectionStateCounts: self::sectionStateCounts($requiredSections), publishBlockers: is_array($summary['publish_blockers'] ?? null) ? $summary['publish_blockers'] : [], hasReadyExport: self::hasReadyExport($pack), includePii: $includePii, protectedValuesHidden: ! $includePii, disclosurePresent: $nonCertificationDisclosure !== '', ); } /** * @param array $readiness * @param array{download?:?string,review?:?string,evidence?:?string,operation?:?string} $urls * @return array{ * state:string, * label:string, * status_label:string, * color:string, * severity:string, * boundary_state:string, * boundary_label:string, * boundary_color:string, * primary_reason:string, * impact:string, * qualified_download_label:string, * limitation_count:int, * limitation_summary:?string, * action_help:?string, * primary_action:array{key:string,label:string,url:?string,kind:string,icon:string}|null, * secondary_actions:list, * limitations:list}>, * technical_details:array * } */ public static function fromReadiness(array $readiness, array $urls = []): array { $limitations = self::limitations($readiness, $urls); $state = self::state($readiness, $limitations); $primaryLimitation = $limitations[0] ?? null; $primaryAction = self::primaryAction( state: $state, primaryLimitationKey: is_array($primaryLimitation) ? (string) $primaryLimitation['key'] : null, urls: $urls, ); $secondaryActions = self::secondaryActions($state, $primaryAction, $urls); $boundaryState = self::boundaryState($state, $readiness); return [ 'state' => $state, 'label' => self::label($state), 'status_label' => self::statusLabel($state), 'color' => self::color($state), 'severity' => self::severity($state), 'boundary_state' => $boundaryState, 'boundary_label' => self::boundaryLabel($boundaryState), 'boundary_color' => self::boundaryColor($boundaryState), 'primary_reason' => is_array($primaryLimitation) ? self::shortReason((string) $primaryLimitation['key']) : self::defaultPrimaryReason($state), 'impact' => self::impact($state), 'qualified_download_label' => self::qualifiedDownloadLabel($state), 'limitation_count' => count($limitations), 'limitation_summary' => $limitations === [] ? null : trans_choice('localization.review.output_limitations_summary', count($limitations), ['count' => count($limitations)]), 'action_help' => self::actionHelp($state), 'primary_action' => $primaryAction, 'secondary_actions' => $secondaryActions, 'limitations' => $limitations, 'technical_details' => self::technicalDetails($readiness), ]; } private static function hasReadyExport(?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); } /** * @return Collection */ private static function outputSections(EnvironmentReview $review, ?ReviewPack $pack): Collection { return $review->sections ->filter(static fn (mixed $section): bool => self::includesOperations($pack) || $section->section_key !== 'operations_health') ->values(); } private static function includesOperations(?ReviewPack $pack): bool { return (bool) (is_array($pack?->options ?? null) ? ($pack->options['include_operations'] ?? true) : true); } /** * @param Collection $sections * @return array */ private static function sectionStateCounts(Collection $sections): array { return $sections ->countBy(static fn (mixed $section): string => (string) $section->completeness_state) ->map(static fn (int $count): int => max(0, $count)) ->all(); } /** * @param list,priority:int}> $limitations */ private static function state(array $readiness, array $limitations): string { $limitationKeys = collect($limitations)->pluck('key'); return match (true) { $limitationKeys->contains('publish_blockers_present') => self::STATE_PUBLICATION_BLOCKED, ! (bool) ($readiness['has_ready_export'] ?? false) => self::STATE_EXPORT_NOT_READY, (bool) ($readiness['contains_pii'] ?? false) => self::STATE_INTERNAL_ONLY, $limitations !== [] => self::STATE_PUBLISHED_WITH_LIMITATIONS, (string) ($readiness['readiness_state'] ?? '') === ReviewPackOutputReadiness::STATE_CUSTOMER_SAFE_READY => self::STATE_CUSTOMER_SAFE_READY, default => self::STATE_UNKNOWN, }; } /** * @param array $readiness * @param array{download?:?string,review?:?string,evidence?:?string,operation?:?string} $urls * @return list,priority:int}> */ private static function limitations(array $readiness, array $urls): array { $sectionSummary = is_array($readiness['section_summary'] ?? null) ? $readiness['section_summary'] : []; $sectionCounts = is_array($readiness['section_state_counts'] ?? null) ? $readiness['section_state_counts'] : []; $limitations = collect(is_array($readiness['limitations'] ?? null) ? $readiness['limitations'] : []) ->filter(static fn (mixed $limitation): bool => is_array($limitation) && is_string($limitation['code'] ?? null)) ->map(function (array $limitation) use ($readiness, $sectionSummary, $sectionCounts, $urls): ?array { $code = (string) $limitation['code']; return match ($code) { 'publish_blockers_present' => [ 'key' => $code, 'label' => __('localization.review.publication_blocked'), 'severity' => 'danger', 'reason' => __('localization.review.publication_blocked_description'), 'action' => self::action('resolve_review_blockers', $urls['review'] ?? $urls['evidence'] ?? null), 'details' => [ __('localization.review.technical_detail_review_status_value', ['value' => (string) ($readiness['review_status'] ?? __('localization.review.unavailable'))]), ], 'priority' => 100, ], 'export_not_ready' => [ 'key' => $code, 'label' => __('localization.review.export_not_ready'), 'severity' => 'warning', 'reason' => __('localization.review.export_not_ready_guidance_reason'), 'action' => self::action('review_output_limitations', $urls['review'] ?? $urls['evidence'] ?? null), 'details' => [ __('localization.review.technical_detail_ready_export_value', ['value' => __('localization.review.no')]), ], 'priority' => 90, ], 'evidence_basis_missing', 'evidence_basis_stale', 'evidence_basis_incomplete' => [ 'key' => $code, 'label' => __('localization.review.evidence_basis_incomplete_guidance'), 'severity' => 'warning', 'reason' => __('localization.review.evidence_basis_incomplete_guidance_reason'), 'action' => self::action('open_evidence_basis', $urls['evidence'] ?? $urls['review'] ?? null), 'details' => [ __('localization.review.technical_detail_evidence_state_value', ['value' => (string) ($readiness['evidence_completeness_state'] ?? __('localization.review.unavailable'))]), ], 'priority' => match ($code) { 'evidence_basis_missing' => 82, 'evidence_basis_stale' => 81, default => 80, }, ], 'required_sections_incomplete' => [ 'key' => $code, 'label' => __('localization.review.required_review_sections_missing'), 'severity' => 'warning', 'reason' => trans_choice( 'localization.review.required_review_sections_missing_reason', (int) ($sectionSummary['required_limited'] ?? 0), ['count' => (int) ($sectionSummary['required_limited'] ?? 0)], ), 'action' => self::action('review_section_limitations', $urls['review'] ?? $urls['evidence'] ?? null), 'details' => [ __('localization.review.technical_detail_section_counts_value', [ 'complete' => (int) ($sectionCounts[EnvironmentReviewCompletenessState::Complete->value] ?? 0), 'partial' => (int) ($sectionCounts[EnvironmentReviewCompletenessState::Partial->value] ?? 0), 'missing' => (int) ($sectionCounts[EnvironmentReviewCompletenessState::Missing->value] ?? 0), 'stale' => (int) ($sectionCounts[EnvironmentReviewCompletenessState::Stale->value] ?? 0), ]), ], 'priority' => 70, ], 'contains_pii' => [ 'key' => $code, 'label' => __('localization.review.internal_package_includes_pii'), 'severity' => 'warning', 'reason' => __('localization.review.internal_package_includes_pii_reason'), 'action' => self::action('review_pii_redaction_state', $urls['review'] ?? $urls['download'] ?? null), 'details' => [ __('localization.review.technical_detail_contains_pii_value', ['value' => __('localization.review.yes')]), ], 'priority' => 60, ], 'disclosure_missing' => [ 'key' => $code, 'label' => __('localization.review.output_disclosure_missing'), 'severity' => 'warning', 'reason' => __('localization.review.output_disclosure_missing_reason'), 'action' => self::action('review_output_limitations', $urls['review'] ?? $urls['download'] ?? null), 'details' => [ __('localization.review.technical_detail_disclosure_present_value', ['value' => __('localization.review.no')]), ], 'priority' => 50, ], default => null, }; }) ->filter() ->sortByDesc('priority') ->values() ->all(); return $limitations; } /** * @param array{download?:?string,review?:?string,evidence?:?string,operation?:?string} $urls * @return array{key:string,label:string,url:?string,kind:string,icon:string}|null */ private static function primaryAction(string $state, ?string $primaryLimitationKey, array $urls): ?array { $actionKey = match ($primaryLimitationKey) { 'publish_blockers_present' => 'resolve_review_blockers', 'evidence_basis_missing', 'evidence_basis_stale', 'evidence_basis_incomplete' => 'open_evidence_basis', 'required_sections_incomplete' => 'review_section_limitations', 'contains_pii' => 'review_pii_redaction_state', 'disclosure_missing' => 'review_output_limitations', 'export_not_ready' => 'review_output_limitations', default => match ($state) { self::STATE_CUSTOMER_SAFE_READY => 'download_customer_safe_review_pack', self::STATE_INTERNAL_ONLY => 'review_pii_redaction_state', self::STATE_EXPORT_NOT_READY => 'review_output_limitations', self::STATE_PUBLICATION_BLOCKED => 'resolve_review_blockers', self::STATE_PUBLISHED_WITH_LIMITATIONS => 'review_output_limitations', default => 'review_output_limitations', }, }; return self::action($actionKey, self::primaryActionUrl($actionKey, $urls)); } /** * @param array{download?:?string,review?:?string,evidence?:?string,operation?:?string} $urls * @param array{key:string,label:string,url:?string,kind:string,icon:string}|null $primaryAction * @return list */ private static function secondaryActions(string $state, ?array $primaryAction, array $urls): array { $actions = match ($state) { self::STATE_CUSTOMER_SAFE_READY => [ self::action('open_review', $urls['review'] ?? null), ], self::STATE_INTERNAL_ONLY => [ self::action('download_internal_review_pack', $urls['download'] ?? null), self::action('open_review', $urls['review'] ?? null), ], self::STATE_EXPORT_NOT_READY => [ self::action('open_evidence_basis', $urls['evidence'] ?? null), self::action('open_review', $urls['review'] ?? null), ], self::STATE_PUBLICATION_BLOCKED => [ self::action('download_review_pack_with_limitations', $urls['download'] ?? null), self::action('open_evidence_basis', $urls['evidence'] ?? null), self::action('open_operation_proof', $urls['operation'] ?? null), ], self::STATE_PUBLISHED_WITH_LIMITATIONS => [ self::action('download_review_pack_with_limitations', $urls['download'] ?? null), self::action('open_review', $urls['review'] ?? null), self::action('open_evidence_basis', $urls['evidence'] ?? null), ], default => [ self::action('open_review', $urls['review'] ?? null), ], }; return collect($actions) ->filter(static fn (?array $action): bool => is_array($action) && filled($action['url'])) ->reject(static fn (array $action): bool => $primaryAction !== null && $action['label'] === $primaryAction['label'] && $action['url'] === $primaryAction['url']) ->unique(static fn (array $action): string => $action['label'].'|'.$action['url']) ->values() ->all(); } /** * @param array{download?:?string,review?:?string,evidence?:?string,operation?:?string} $urls */ private static function primaryActionUrl(string $actionKey, array $urls): ?string { return match ($actionKey) { 'download_customer_safe_review_pack', 'download_internal_review_pack', 'download_review_pack_with_limitations' => $urls['download'] ?? null, 'open_evidence_basis' => $urls['evidence'] ?? $urls['review'] ?? null, 'review_section_limitations', 'resolve_review_blockers', 'review_output_limitations', 'review_pii_redaction_state', 'open_review' => $urls['review'] ?? $urls['evidence'] ?? $urls['download'] ?? null, 'open_operation_proof' => $urls['operation'] ?? null, default => $urls['review'] ?? $urls['download'] ?? $urls['evidence'] ?? null, }; } /** * @return array{key:string,label:string,url:?string,kind:string,icon:string}|null */ private static function action(string $actionKey, ?string $url): ?array { return [ 'key' => $actionKey, 'label' => match ($actionKey) { 'download_customer_safe_review_pack' => __('localization.review.download_customer_safe_review_pack'), 'download_internal_review_pack' => __('localization.review.download_internal_review_pack'), 'download_review_pack_with_limitations' => __('localization.review.download_review_pack_with_limitations'), 'open_evidence_basis' => __('localization.review.open_evidence_basis'), 'review_section_limitations' => __('localization.review.review_section_limitations'), 'review_pii_redaction_state' => __('localization.review.review_pii_redaction_state'), 'resolve_review_blockers' => __('localization.review.resolve_review_blockers'), 'open_operation_proof' => __('localization.review.open_operation_proof'), 'open_review' => __('localization.review.open_review'), default => __('localization.review.review_output_limitations'), }, 'url' => $url, 'kind' => str_starts_with($actionKey, 'download_') ? 'download' : 'environment_link', 'icon' => str_starts_with($actionKey, 'download_') ? 'heroicon-o-arrow-down-tray' : 'heroicon-o-arrow-top-right-on-square', ]; } private static function label(string $state): string { return match ($state) { self::STATE_PUBLICATION_BLOCKED => __('localization.review.output_not_customer_ready'), self::STATE_CUSTOMER_SAFE_READY => __('localization.review.customer_safe_review_pack_ready'), self::STATE_INTERNAL_ONLY => __('localization.review.internal_review_package_available'), self::STATE_EXPORT_NOT_READY => __('localization.review.export_not_ready'), self::STATE_PUBLISHED_WITH_LIMITATIONS => __('localization.review.published_with_limitations'), default => __('localization.review.requires_review'), }; } private static function statusLabel(string $state): string { return match ($state) { self::STATE_PUBLICATION_BLOCKED => __('localization.review.publication_blocked'), self::STATE_CUSTOMER_SAFE_READY => __('localization.review.customer_safe_review_pack_ready'), self::STATE_INTERNAL_ONLY => __('localization.review.internal_review_package_available'), self::STATE_EXPORT_NOT_READY => __('localization.review.export_not_ready'), self::STATE_PUBLISHED_WITH_LIMITATIONS => __('localization.review.published_with_limitations'), default => __('localization.review.requires_review'), }; } private static function color(string $state): string { return match ($state) { self::STATE_CUSTOMER_SAFE_READY => 'success', self::STATE_PUBLICATION_BLOCKED => 'danger', self::STATE_EXPORT_NOT_READY => 'gray', default => 'warning', }; } private static function severity(string $state): string { return match ($state) { self::STATE_CUSTOMER_SAFE_READY => 'success', self::STATE_PUBLICATION_BLOCKED => 'danger', self::STATE_EXPORT_NOT_READY => 'warning', default => 'warning', }; } /** * @param array $readiness */ private static function boundaryState(string $state, array $readiness): string { return match ($state) { self::STATE_CUSTOMER_SAFE_READY => 'customer_safe_ready', self::STATE_INTERNAL_ONLY => 'internal_only', self::STATE_EXPORT_NOT_READY => 'not_ready', self::STATE_PUBLICATION_BLOCKED, self::STATE_PUBLISHED_WITH_LIMITATIONS => 'requires_review', default => (string) ($readiness['customer_safe_state'] ?? 'requires_review'), }; } private static function boundaryLabel(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 static function boundaryColor(string $state): string { return match ($state) { 'customer_safe_ready' => 'success', 'internal_only', 'requires_review' => 'warning', default => 'gray', }; } private static function defaultPrimaryReason(string $state): string { return match ($state) { self::STATE_PUBLICATION_BLOCKED => __('localization.review.publication_blocked_description'), self::STATE_EXPORT_NOT_READY => __('localization.review.export_not_ready_guidance_reason'), self::STATE_INTERNAL_ONLY => __('localization.review.internal_package_includes_pii_reason'), self::STATE_CUSTOMER_SAFE_READY => __('localization.review.customer_safe_review_pack_ready_reason'), default => __('localization.review.review_pack_with_limitations_description'), }; } private static function shortReason(string $limitationKey): string { return match ($limitationKey) { 'publish_blockers_present' => __('localization.review.publication_blocked_short_reason'), 'export_not_ready' => __('localization.review.export_not_ready_short_reason'), 'evidence_basis_missing', 'evidence_basis_stale', 'evidence_basis_incomplete' => __('localization.review.evidence_basis_incomplete_short_reason'), 'required_sections_incomplete' => __('localization.review.required_review_sections_missing_short_reason'), 'contains_pii' => __('localization.review.internal_package_includes_pii_short_reason'), 'disclosure_missing' => __('localization.review.output_disclosure_missing_short_reason'), default => __('localization.review.review_output_limitations'), }; } private static function impact(string $state): string { return match ($state) { self::STATE_PUBLICATION_BLOCKED => __('localization.review.publication_blocked_impact'), self::STATE_EXPORT_NOT_READY => __('localization.review.export_not_ready_impact'), self::STATE_INTERNAL_ONLY => __('localization.review.internal_review_package_available_impact'), self::STATE_CUSTOMER_SAFE_READY => __('localization.review.customer_safe_review_pack_ready_impact'), default => __('localization.review.published_with_limitations_impact'), }; } private static function actionHelp(string $state): ?string { return match ($state) { self::STATE_PUBLICATION_BLOCKED => __('localization.review.output_action_help_publication_blocked'), self::STATE_PUBLISHED_WITH_LIMITATIONS => __('localization.review.output_action_help_published_with_limitations'), self::STATE_INTERNAL_ONLY => __('localization.review.output_action_help_internal_only'), self::STATE_EXPORT_NOT_READY => __('localization.review.output_action_help_export_not_ready'), self::STATE_CUSTOMER_SAFE_READY => __('localization.review.output_action_help_customer_safe_ready'), default => null, }; } private static function qualifiedDownloadLabel(string $state): string { return match ($state) { self::STATE_CUSTOMER_SAFE_READY => __('localization.review.download_customer_safe_review_pack'), self::STATE_INTERNAL_ONLY => __('localization.review.download_internal_review_pack'), default => __('localization.review.download_review_pack_with_limitations'), }; } /** * @param array $readiness * @return array */ private static function technicalDetails(array $readiness): array { $sectionSummary = is_array($readiness['section_summary'] ?? null) ? $readiness['section_summary'] : []; return [ __('localization.review.review_status') => (string) ($readiness['review_status'] ?? __('localization.review.unavailable')), __('localization.review.output_readiness') => self::statusLabel(self::state($readiness, self::limitations($readiness, []))), __('localization.review.publication_sharing_state') => self::boundaryLabel(self::boundaryState( self::state($readiness, self::limitations($readiness, [])), $readiness, )), __('localization.review.has_ready_export') => (bool) ($readiness['has_ready_export'] ?? false) ? __('localization.review.yes') : __('localization.review.no'), __('localization.review.evidence_basis_state') => (string) ($readiness['evidence_completeness_state'] ?? __('localization.review.unavailable')), __('localization.review.section_completeness') => __('localization.review.technical_detail_required_sections_value', [ 'complete' => (int) ($sectionSummary['required_complete'] ?? 0), 'total' => (int) ($sectionSummary['required_total'] ?? 0), 'limited' => (int) ($sectionSummary['required_limited'] ?? 0), ]), __('localization.review.pii_state') => (bool) ($readiness['contains_pii'] ?? false) ? __('localization.review.yes') : __('localization.review.no'), __('localization.review.protected_values') => (bool) ($readiness['protected_values_hidden'] ?? true) ? __('localization.review.protected_values_hidden') : __('localization.review.unavailable'), __('localization.review.disclosure') => (bool) ($readiness['disclosure_present'] ?? false) ? __('localization.review.disclosure_present') : __('localization.review.no'), ]; } }