diff --git a/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php b/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php index d4eb74b1..b6241ae6 100644 --- a/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php +++ b/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php @@ -14,6 +14,7 @@ use App\Models\Workspace; use App\Services\Audit\WorkspaceAuditLogger; use App\Services\EnvironmentReviews\EnvironmentReviewRegisterService; +use App\Services\ReviewPackService; use App\Support\Audit\AuditActionId; use App\Support\Auth\Capabilities; use App\Support\Findings\FindingOutcomeSemantics; @@ -206,10 +207,8 @@ public function table(Table $table): Table ]) ->actions([]) ->bulkActions([]) - ->emptyStateHeading(__('localization.review.no_released_customer_reviews')) - ->emptyStateDescription(fn (): string => $this->hasActiveFilters() - ? __('localization.review.clear_filters_description') - : __('localization.review.no_released_customer_reviews_description')) + ->emptyStateHeading(fn (): string => $this->workspaceEmptyStateHeading()) + ->emptyStateDescription(fn (): string => $this->workspaceEmptyStateDescription()) ->emptyStateActions([ Action::make('clear_filters_empty') ->label(__('localization.review.clear_filters')) @@ -239,6 +238,72 @@ public function authorizedTenants(): array return $this->authorizedTenants = app(EnvironmentReviewRegisterService::class)->authorizedTenants($user, $workspace); } + public function activeEnvironmentFilterLabel(): ?string + { + $tenantId = $this->currentTenantFilterId(); + + if ($tenantId === null) { + return null; + } + + $tenant = $this->authorizedTenants()[$tenantId] ?? null; + + return $tenant instanceof ManagedEnvironment ? $tenant->name : null; + } + + /** + * @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; + } + + $publishedAt = $review->published_at ?? $review->generated_at ?? $review->created_at; + $packageAvailability = $this->governancePackageAvailability($tenant); + $downloadUrl = $this->reviewPackDownloadUrl($review, $tenant); + $reviewUrl = $this->latestReviewUrl($tenant); + + return [ + '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 + ? __('localization.review.download_review_pack') + : __('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' => $downloadUrl !== null ? __('localization.review.open_review') : null, + 'secondary_action_url' => $downloadUrl !== null ? $reviewUrl : null, + ], + 'decision' => $this->decisionSummaryForReview($review), + 'accepted_risks' => $this->acceptedRisksForReview($review), + 'evidence_basis' => $this->evidenceBasisForReview($review, $packageAvailability), + ]; + } + private function authorizePageAccess(): void { $user = auth()->user(); @@ -352,6 +417,47 @@ private function clearWorkspaceFilters(): void $this->removeTableFilters(); } + 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 + { + $tenantFilterId = $this->currentTenantFilterId(); + $user = auth()->user(); + $workspace = $this->workspace(); + + if ($tenantFilterId === null || ! $user instanceof User || ! $workspace instanceof Workspace) { + return false; + } + + $selectedTenantHasReleasedReview = EnvironmentReview::query() + ->forWorkspace((int) $workspace->getKey()) + ->where('managed_environment_id', $tenantFilterId) + ->published() + ->exists(); + + if ($selectedTenantHasReleasedReview) { + return false; + } + + return app(EnvironmentReviewRegisterService::class) + ->latestPublishedQuery($user, $workspace) + ->exists(); + } + private function currentTenantFilterId(): ?int { $tenantFilter = data_get($this->tableFilters, 'managed_environment_id.value'); @@ -372,6 +478,34 @@ private function workspace(): ?Workspace : null; } + private function latestReleasedTenant(): ?ManagedEnvironment + { + $user = auth()->user(); + $workspace = $this->workspace(); + + if (! $user instanceof User || ! $workspace instanceof Workspace) { + return null; + } + + $query = app(EnvironmentReviewRegisterService::class)->latestPublishedQuery($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 { $review = $tenant->environmentReviews->first(); @@ -402,6 +536,43 @@ private function latestReviewUrl(ManagedEnvironment $tenant): ?string return $this->appendQuery(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant), $query); } + 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; @@ -457,7 +628,7 @@ private function latestReviewStateColor(ManagedEnvironment $tenant): string return 'success'; } - return in_array($packageState, ['blocked', 'expired'], true) + return in_array($packageState, ['expired', 'unavailable'], true) ? 'danger' : 'warning'; } @@ -523,6 +694,14 @@ private function governancePackageSummary(ManagedEnvironment $tenant): array 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'] : []; @@ -546,25 +725,30 @@ private function governancePackageAvailability(ManagedEnvironment $tenant): arra $pack = $review->currentExportReviewPack; $user = auth()->user(); - $limitations = is_array($review->controlInterpretation()['limitations'] ?? null) ? $review->controlInterpretation()['limitations'] : []; - $isPartialReview = in_array((string) $review->completeness_state, [ - EnvironmentReviewCompletenessState::Partial->value, - EnvironmentReviewCompletenessState::Stale->value, - ], true) || $limitations !== []; + $decisionSummary = data_get($this->governancePackageSummaryForReview($review), 'decision_summary'); + $isPartialReview = is_array($decisionSummary) && ( + in_array((string) ($decisionSummary['status'] ?? ''), ['unavailable', 'incomplete'], true) + || (string) ($decisionSummary['decision_data_state'] ?? '') === 'incomplete' + || in_array((string) ($decisionSummary['evidence_state'] ?? ''), [ + EnvironmentReviewCompletenessState::Partial->value, + EnvironmentReviewCompletenessState::Stale->value, + EnvironmentReviewCompletenessState::Missing->value, + ], true) + ); if (! $pack instanceof ReviewPack) { return [ - 'state' => 'unavailable', - 'label' => __('localization.review.governance_package_unavailable'), - 'description' => __('localization.review.governance_package_unavailable_description'), + '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' => 'blocked', - 'label' => __('localization.review.governance_package_blocked'), - 'description' => __('localization.review.governance_package_blocked_description'), + 'state' => 'unavailable', + 'label' => __('localization.review.unavailable'), + 'description' => __('localization.review.review_pack_unavailable_customer_description'), ]; } @@ -576,26 +760,42 @@ private function governancePackageAvailability(ManagedEnvironment $tenant): arra ]; } + 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.governance_package_unavailable'), - 'description' => __('localization.review.governance_package_not_ready_description'), + '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'), ]; } if ($isPartialReview) { return [ - 'state' => 'partial', - 'label' => __('localization.review.governance_package_partial'), - 'description' => __('localization.review.governance_package_partial_description'), + 'state' => 'evidence_incomplete', + 'label' => __('localization.review.review_pack_evidence_incomplete'), + 'description' => __('localization.review.review_pack_evidence_incomplete_description'), ]; } return [ 'state' => 'available', - 'label' => __('localization.review.governance_package_available'), - 'description' => __('localization.review.governance_package_available_description'), + 'label' => __('localization.review.available'), + 'description' => __('localization.review.review_pack_available_customer_description'), ]; } @@ -603,8 +803,9 @@ private function governancePackageAvailabilityLabel(ManagedEnvironment $tenant): { return match ($this->governancePackageAvailability($tenant)['state']) { 'available' => __('localization.review.available'), - 'partial' => __('localization.review.partial'), - 'blocked' => __('localization.review.blocked'), + 'evidence_incomplete' => __('localization.review.review_pack_evidence_incomplete'), + 'not_available' => __('localization.review.review_pack_not_available_yet'), + 'preparing' => __('localization.review.review_pack_preparing'), 'expired' => __('localization.review.expired'), default => __('localization.review.unavailable'), }; @@ -614,8 +815,8 @@ private function governancePackageAvailabilityColor(ManagedEnvironment $tenant): { return match ($this->governancePackageAvailability($tenant)['state']) { 'available' => 'success', - 'partial' => 'warning', - 'blocked', 'expired' => 'danger', + 'evidence_incomplete', 'preparing' => 'warning', + 'expired', 'unavailable' => 'danger', default => 'gray', }; } @@ -633,6 +834,200 @@ private function governancePackageTeaser(ManagedEnvironment $tenant): string 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 + * @return array + */ + private function evidenceBasisForReview(EnvironmentReview $review, array $packageAvailability): array + { + $package = $this->governancePackageSummaryForReview($review); + $decision = $this->decisionSummaryForReview($review); + $pack = $review->currentExportReviewPack; + + $state = match (true) { + $package === [] => 'unavailable', + ! $pack instanceof ReviewPack => 'not_generated', + $packageAvailability['state'] === 'evidence_incomplete' || $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'), + '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'), + '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', + 'incomplete' => 'warning', + default => 'gray', + }; + } + + 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')) { @@ -706,7 +1101,8 @@ private function controlRecommendedNextAction(ManagedEnvironment $tenant): strin } return match ($this->governancePackageAvailability($tenant)['state']) { - 'available', 'partial' => __('localization.review.workspace_next_step_package_review'), + 'available' => __('localization.review.workspace_next_step_package_review'), + 'evidence_incomplete' => __('localization.review.workspace_next_step_evidence_review'), default => __('localization.review.workspace_next_step_review_open'), }; } diff --git a/apps/platform/lang/de/localization.php b/apps/platform/lang/de/localization.php index 2abab115..efebb40b 100644 --- a/apps/platform/lang/de/localization.php +++ b/apps/platform/lang/de/localization.php @@ -333,6 +333,10 @@ 'customer_workspace_canonical_note' => 'Jede Zeile ist ein Einstieg in die Detailansicht: Dort sehen Sie Paketstatus, Executive-Einstieg, Nachweise, aktuelle Risiken und den nächsten kundensicheren Schritt.', 'customer_workspace_mapping_version' => 'Die Control-Readiness-Interpretation verwendet :version für diesen Workspace.', 'customer_workspace_non_certification_disclosure' => 'Dieser Workspace fasst die aktuelle Review- und Nachweislage für die Service-Auslieferung zusammen. Er ersetzt weder ein formales Auditurteil noch eine Zertifizierung oder rechtliche Attestierung.', + 'latest_released_review' => 'Letztes veröffentlichtes Review', + 'released_review_for_environment' => 'Veröffentlichtes Review für :environment', + 'filtered_by_environment' => 'Gefiltert nach Umgebung: :environment', + 'published_date' => 'Veröffentlicht :date', 'reviews' => 'Reviews', 'clear_filters' => 'Filter löschen', 'tenant' => 'Tenant', @@ -391,6 +395,8 @@ 'no_entitled_tenants' => 'Keine berechtigten Tenants passen zu dieser Ansicht', 'no_released_customer_reviews' => 'Keine veröffentlichten Kundenreviews passen zu dieser Ansicht', 'no_released_customer_reviews_description' => 'Veröffentlichen Sie ein Tenant-Review, bevor es im kundensicheren Workspace erscheint.', + 'filtered_no_released_customer_reviews' => 'Keine veröffentlichten Kundenreviews passen zum aktiven Umgebungsfilter.', + 'filtered_no_released_customer_reviews_description' => 'Löschen Sie den Umgebungsfilter, um andere veröffentlichte Reviews in diesem Workspace zu sehen.', 'clear_filters_description' => 'Löschen Sie die aktuellen Filter, um zum vollständigen Kundenreview-Workspace für Ihre berechtigten Tenants zurückzukehren.', 'adjust_filters_description' => 'Passen Sie die Filter an, um zum vollständigen Kundenreview-Workspace für Ihre berechtigten Tenants zurückzukehren.', 'no_published_review' => 'Kein veröffentlichtes Review', @@ -412,7 +418,16 @@ 'blocked' => 'Blockiert', 'expired' => 'Abgelaufen', 'restricted' => 'Eingeschränkt', + 'preparing' => 'In Vorbereitung', 'review_pack_available' => 'Aktuelles Review-Pack verfügbar', + 'review_pack_available_customer_description' => 'Das aktuelle Review-Pack ist zum Download bereit.', + 'review_pack_preparing' => 'In Vorbereitung', + 'review_pack_preparing_description' => 'Das Review-Pack wird vorbereitet.', + 'review_pack_not_available_yet' => 'Noch nicht verfügbar', + 'review_pack_not_available_yet_description' => 'Für dieses veröffentlichte Review wurde noch kein Review-Pack erzeugt.', + 'review_pack_evidence_incomplete' => 'Evidence unvollständig', + 'review_pack_evidence_incomplete_description' => 'Review-Pack oder Entscheidungszusammenfassung können unvollständig sein.', + 'review_pack_unavailable_customer_description' => 'Das Review-Pack kann derzeit nicht bereitgestellt werden.', 'no_current_review_pack' => 'Noch kein aktuelles Review-Pack verfügbar', 'review_pack_access_unavailable' => 'Review-Pack-Zugriff ist für dieses Konto nicht verfügbar', 'review_pack_unavailable' => 'Review-Pack ist noch nicht bereit', @@ -439,6 +454,33 @@ 'customer_review_pack_not_ready' => 'Das zugeordnete Review-Pack ist noch nicht für den Download bereit.', 'customer_review_pack_expired' => 'Das zugeordnete Review-Pack ist abgelaufen.', 'customer_review_pack_forbidden' => 'Dieses Konto kann das Review lesen, aber das aktuelle Review-Pack nicht herunterladen.', + 'decision_summary' => 'Entscheidungszusammenfassung', + 'no_decisions_require_awareness' => 'Keine Entscheidungen mit Aufmerksamkeitsbedarf', + 'no_decisions_require_awareness_description' => 'In diesem veröffentlichten Review benötigen keine Governance-Entscheidungen Kundenaufmerksamkeit.', + 'no_decisions_require_awareness_next_action' => 'Für den Decision Register ist aus diesem Review keine Kundenaktion erforderlich.', + 'decision_evidence_unavailable' => 'Entscheidungs-Evidence nicht verfügbar', + 'decision_evidence_incomplete' => 'Entscheidungs-Evidence unvollständig', + 'decision_summary_unavailable_description' => 'Kundensichere Entscheidungs-Evidence ist für dieses veröffentlichte Review nicht verfügbar.', + 'decision_summary_unavailable_next_action' => 'Öffnen Sie das veröffentlichte Review, bevor Sie die Entscheidungszusammenfassung als vollständig behandeln.', + 'decision_summary_incomplete_description' => 'Die Entscheidungs-Evidence ist für dieses veröffentlichte Review unvollständig.', + 'decision_summary_incomplete_next_action' => 'Prüfen Sie die Evidence-Basis, bevor Sie sich auf die Entscheidungszusammenfassung verlassen.', + 'decision_summary_requires_awareness_description' => 'Governance-Entscheidungen benötigen Kundenaufmerksamkeit, bevor dieses veröffentlichte Review genutzt wird.', + 'decision_summary_requires_awareness_next_action' => 'Prüfen Sie die Accepted-Risk-Entscheidungsbasis vor der Kundenauslieferung.', + 'decision_entry_customer_safe_summary' => 'Diese Governance-Entscheidung benötigt Nacharbeit vor der Stakeholder-Auslieferung.', + 'accepted_risk_state_current' => 'Akzeptiertes Risiko', + 'accepted_risk_state_review_due' => 'Review fällig', + 'accepted_risk_state_on_record' => 'Akzeptiertes Risiko', + 'accepted_risk_customer_safe_summary' => 'In der Evidence-Basis des veröffentlichten Reviews enthalten.', + 'evidence_basis_complete' => 'Vollständig', + 'evidence_basis_complete_description' => 'Die kundensichere Entscheidungszusammenfassung ist durch Evidence des veröffentlichten Reviews gestützt.', + 'evidence_basis_no_awareness_required' => 'Keine Aufmerksamkeit erforderlich', + 'evidence_basis_no_awareness_required_description' => 'In dieser Evidence-Basis des veröffentlichten Reviews benötigen keine Governance-Entscheidungen Aufmerksamkeit.', + 'evidence_basis_incomplete' => 'Unvollständig', + 'evidence_basis_incomplete_description' => 'Die Entscheidungs-Evidence ist unvollständig und darf nicht als „keine Entscheidungen“ interpretiert werden.', + 'evidence_basis_unavailable' => 'Nicht verfügbar', + 'evidence_basis_unavailable_description' => 'Kundensichere Entscheidungs-Evidence ist für dieses veröffentlichte Review nicht verfügbar.', + 'evidence_basis_not_generated' => 'Nicht erzeugt', + 'evidence_basis_not_generated_description' => 'Das Review-Pack ist noch nicht verfügbar; das veröffentlichte Review bleibt sichtbar.', 'released_governance_record' => 'Veröffentlichter Governance-Nachweis', 'released_governance_record_available' => 'Dieses veröffentlichte Review ist für kundensichere Governance-Nutzung verfügbar.', 'outcome_summary' => 'Ergebniszusammenfassung', diff --git a/apps/platform/lang/en/localization.php b/apps/platform/lang/en/localization.php index fb674060..db4dd903 100644 --- a/apps/platform/lang/en/localization.php +++ b/apps/platform/lang/en/localization.php @@ -333,6 +333,10 @@ 'customer_workspace_canonical_note' => 'Each row is an index entry: open the review detail to inspect package status, the executive entrypoint, supporting evidence, current risks, and the next customer-safe action.', 'customer_workspace_mapping_version' => 'Control readiness interpretation uses :version for this workspace.', 'customer_workspace_non_certification_disclosure' => 'This workspace summarizes current review evidence for service delivery. It does not replace a formal audit opinion, certification, or legal attestation.', + 'latest_released_review' => 'Latest released review', + 'released_review_for_environment' => 'Released review for :environment', + 'filtered_by_environment' => 'Filtered by environment: :environment', + 'published_date' => 'Published :date', 'reviews' => 'Reviews', 'clear_filters' => 'Clear filters', 'tenant' => 'Tenant', @@ -391,6 +395,8 @@ 'no_entitled_tenants' => 'No entitled tenants match this view', 'no_released_customer_reviews' => 'No released customer reviews match this view', 'no_released_customer_reviews_description' => 'Publish an environment review before it appears in the customer-safe workspace.', + 'filtered_no_released_customer_reviews' => 'No released customer reviews match the active environment filter.', + 'filtered_no_released_customer_reviews_description' => 'Clear the environment filter to view other released reviews in this workspace.', 'clear_filters_description' => 'Clear the current filters to return to the full customer review workspace for your entitled tenants.', 'adjust_filters_description' => 'Adjust filters to return to the full customer review workspace for your entitled tenants.', 'no_published_review' => 'No published review', @@ -412,7 +418,16 @@ 'blocked' => 'Blocked', 'expired' => 'Expired', 'restricted' => 'Restricted', + 'preparing' => 'Preparing', 'review_pack_available' => 'Current review pack available', + 'review_pack_available_customer_description' => 'Current review pack is ready to download.', + 'review_pack_preparing' => 'Preparing', + 'review_pack_preparing_description' => 'Review Pack is being prepared.', + 'review_pack_not_available_yet' => 'Not available yet', + 'review_pack_not_available_yet_description' => 'Review Pack has not been generated for this released review yet.', + 'review_pack_evidence_incomplete' => 'Evidence incomplete', + 'review_pack_evidence_incomplete_description' => 'Review Pack or decision summary may be incomplete.', + 'review_pack_unavailable_customer_description' => 'Review Pack cannot be provided right now.', 'no_current_review_pack' => 'No current review pack available yet', 'review_pack_access_unavailable' => 'Review pack access is unavailable for this actor', 'review_pack_unavailable' => 'Review pack is not ready yet', @@ -439,6 +454,33 @@ 'customer_review_pack_not_ready' => 'The attached review pack is not ready for download yet.', 'customer_review_pack_expired' => 'The attached review pack has expired.', 'customer_review_pack_forbidden' => 'This account can read the review but cannot download the current review pack.', + 'decision_summary' => 'Decision summary', + 'no_decisions_require_awareness' => 'No decisions require awareness', + 'no_decisions_require_awareness_description' => 'No governance decisions require customer awareness in this released review.', + 'no_decisions_require_awareness_next_action' => 'No customer action is needed for Decision Register follow-up from this review.', + 'decision_evidence_unavailable' => 'Decision evidence unavailable', + 'decision_evidence_incomplete' => 'Decision evidence incomplete', + 'decision_summary_unavailable_description' => 'Customer-safe decision evidence is unavailable for this released review.', + 'decision_summary_unavailable_next_action' => 'Open the released review before treating the decision summary as complete.', + 'decision_summary_incomplete_description' => 'Decision evidence is incomplete for this released review.', + 'decision_summary_incomplete_next_action' => 'Review the evidence basis before relying on the decision summary.', + 'decision_summary_requires_awareness_description' => 'Governance decisions require customer awareness before relying on this released review.', + 'decision_summary_requires_awareness_next_action' => 'Review the accepted-risk decision basis before customer delivery.', + 'decision_entry_customer_safe_summary' => 'This governance decision needs follow-up before stakeholder delivery.', + 'accepted_risk_state_current' => 'Accepted risk', + 'accepted_risk_state_review_due' => 'Review due', + 'accepted_risk_state_on_record' => 'Accepted risk', + 'accepted_risk_customer_safe_summary' => 'Included in the released review evidence basis.', + 'evidence_basis_complete' => 'Complete', + 'evidence_basis_complete_description' => 'Customer-safe decision summary is backed by released-review evidence.', + 'evidence_basis_no_awareness_required' => 'No awareness required', + 'evidence_basis_no_awareness_required_description' => 'No governance decisions require awareness in this released-review evidence basis.', + 'evidence_basis_incomplete' => 'Incomplete', + 'evidence_basis_incomplete_description' => 'Decision evidence is incomplete and must not be interpreted as no decisions.', + 'evidence_basis_unavailable' => 'Unavailable', + 'evidence_basis_unavailable_description' => 'Customer-safe decision evidence is unavailable for this released review.', + 'evidence_basis_not_generated' => 'Not generated', + 'evidence_basis_not_generated_description' => 'The Review Pack is not available yet; the released review remains visible.', 'released_governance_record' => 'Released governance record', 'released_governance_record_available' => 'This released review is available for customer-safe governance consumption.', 'outcome_summary' => 'Outcome summary', diff --git a/apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php b/apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php index 64dd1334..fedb3c65 100644 --- a/apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php +++ b/apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php @@ -1,4 +1,9 @@ + @php + $activeEnvironmentFilterLabel = $this->activeEnvironmentFilterLabel(); + $reviewPayload = $this->latestReviewConsumptionPayload(); + @endphp +
@@ -16,8 +21,145 @@
{{ __('localization.review.customer_workspace_non_certification_disclosure') }}
+ + @if ($activeEnvironmentFilterLabel) +
+ {{ __('localization.review.filtered_by_environment', ['environment' => $activeEnvironmentFilterLabel]) }} +
+ @endif
+ @if ($reviewPayload) + @php + $latest = $reviewPayload['latest']; + $decision = $reviewPayload['decision']; + $acceptedRisks = $reviewPayload['accepted_risks']; + $evidenceBasis = $reviewPayload['evidence_basis']; + @endphp + + +
+
+
+
+ {{ $latest['review_label'] }} +
+
+ {{ __('localization.review.published_date', ['date' => $latest['published_label']]) }} +
+
+ +
+ + {{ $latest['status_label'] }} + + + {{ $latest['package_badge_label'] }} + +
+ +
+ {{ __('localization.review.review_pack') }} +
+ +

+ {{ $latest['package_description'] }} +

+
+ +
+ @if ($latest['primary_action_url']) + + {{ $latest['primary_action_label'] }} + + @endif + + @if ($latest['secondary_action_url']) + + {{ $latest['secondary_action_label'] }} + + @endif +
+
+
+ +
+ +
+ + {{ $decision['label'] }} + + +

+ {{ $decision['summary'] }} +

+ +

+ {{ $decision['next_action'] }} +

+ + @foreach ($decision['entries'] as $entry) +
+
+ {{ $entry['title'] }} +
+
+ {{ $entry['summary'] }} +
+
+ {{ $entry['next_action'] }} +
+
+ @endforeach +
+
+ + +
+ @forelse ($acceptedRisks['entries'] as $risk) +
+
+ {{ $risk['title'] }} + + {{ $risk['state_label'] }} + +
+
+ {{ $risk['summary'] }} +
+
+ @empty +

+ {{ $acceptedRisks['empty_state'] }} +

+ @endforelse +
+
+ + +
+ + {{ $evidenceBasis['label'] }} + + +

+ {{ $evidenceBasis['summary'] }} +

+
+
+
+ @endif + {{ $this->table }} diff --git a/apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php b/apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php index fc69b322..3320e264 100644 --- a/apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php +++ b/apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php @@ -91,10 +91,12 @@ ->assertSee('Nachweise') ->assertSee('Prüfen Sie für jeden berechtigten ManagedEnvironment den executive-fähigen Status des Governance-Pakets') ->assertSee('Dieser Workspace fasst die aktuelle Review- und Nachweislage für die Service-Auslieferung zusammen. Er ersetzt weder ein formales Auditurteil noch eine Zertifizierung oder rechtliche Attestierung.') - ->assertSee('Teilweise') - ->assertSee('Prüfung erforderlich') + ->assertSee('Letztes veröffentlichtes Review') + ->assertSee('Review-Pack herunterladen') + ->assertSee('Das aktuelle Review-Pack ist zum Download bereit.') + ->assertSee('Keine Entscheidungen mit Aufmerksamkeitsbedarf') + ->assertSee('Zur Veröffentlichung bereit') ->assertSee('Verfügbar') - ->assertSee('Paket prüfen') ->assertDontSee('Customer-safe governance package index') ->assertDontSee('localization.review.customer_safe_review_workspace') ->assertDontSee('Publishable') diff --git a/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php b/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php index db92336d..3cad8390 100644 --- a/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php +++ b/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php @@ -37,7 +37,7 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t ); } -it('keeps the latest released review as the only row action when a ready review pack exists', function (): void { +it('shows a customer-safe download action when the latest released review pack is ready', function (): void { $tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly'); $snapshot = seedEnvironmentReviewEvidence($tenant); @@ -69,13 +69,19 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t Livewire::actingAs($user) ->test(CustomerReviewWorkspace::class) ->assertSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review->fresh()], $tenant), false) - ->assertSee('Governance package') + ->assertSee('Review pack') + ->assertSee('Available') + ->assertSee('Current review pack is ready to download.') + ->assertSee('Download review pack') + ->assertSee('source_surface=customer_review_workspace', false) + ->assertSee('tenant_filter_id', false) ->assertSee('Open review') - ->assertDontSee('Download review pack') - ->assertDontSee('Current review pack available'); + ->assertDontSee('Generate pack') + ->assertDontSee('Regenerate') + ->assertDontSee('Expire'); }); -it('keeps the customer review workspace row action visible while suspended read-only', function (): void { +it('keeps the customer review workspace download action visible while suspended read-only', function (): void { $tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly'); $snapshot = seedEnvironmentReviewEvidence($tenant); @@ -109,12 +115,14 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t Livewire::actingAs($user) ->test(CustomerReviewWorkspace::class) ->assertSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review->fresh()], $tenant), false) + ->assertSee('Download review pack') ->assertSee('Open review') - ->assertDontSee('Download review pack') - ->assertDontSee('Current review pack available'); + ->assertDontSee('Generate pack') + ->assertDontSee('Regenerate') + ->assertDontSee('Expire'); }); -it('does not expose review-pack availability as a workspace row peer action', function (): void { +it('shows a customer-safe missing review-pack state without exposing pack mutation actions', function (): void { $tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly'); $snapshot = seedEnvironmentReviewEvidence($tenant); @@ -134,9 +142,12 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t Livewire::actingAs($user) ->test(CustomerReviewWorkspace::class) ->assertSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review->fresh()], $tenant), false) - ->assertSee('Unavailable') - ->assertDontSee('No current review pack available yet') - ->assertDontSee('Download review pack'); + ->assertSee('Not available yet') + ->assertSee('Review Pack has not been generated for this released review yet.') + ->assertDontSee('Download review pack') + ->assertDontSee('Generate pack') + ->assertDontSee('Regenerate') + ->assertDontSee('Expire'); }); it('shows a partial governance-package state when the released review basis is limitation-aware', function (): void { @@ -171,11 +182,58 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t Livewire::actingAs($user) ->test(CustomerReviewWorkspace::class) ->assertSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review->fresh()], $tenant), false) - ->assertSee('Partial') + ->assertSee('Evidence incomplete') + ->assertSee('Review Pack or decision summary may be incomplete.') ->assertDontSee('Download review pack'); }); -it('shows expired and capability-blocked governance-package states on the workspace row surface', function (): void { +it('shows preparing and unavailable review-pack states without download links', function (): void { + $preparingTenant = ManagedEnvironment::factory()->create(['name' => 'Preparing Pack ManagedEnvironment']); + [$user, $preparingTenant] = createUserWithTenant(tenant: $preparingTenant, role: 'readonly'); + $failedTenant = ManagedEnvironment::factory()->create([ + 'workspace_id' => (int) $preparingTenant->workspace_id, + 'name' => 'Failed Pack ManagedEnvironment', + ]); + createUserWithTenant(tenant: $failedTenant, user: $user, role: 'readonly'); + + foreach ([$preparingTenant, $failedTenant] as $tenant) { + $snapshot = seedEnvironmentReviewEvidence($tenant); + $review = composeEnvironmentReviewForTest($tenant, $user, $snapshot); + $review->forceFill([ + 'status' => EnvironmentReviewStatus::Published->value, + 'published_at' => now(), + 'published_by_user_id' => (int) $user->getKey(), + ])->save(); + + $pack = ($tenant->is($preparingTenant) + ? ReviewPack::factory()->queued() + : ReviewPack::factory()->failed()) + ->create([ + 'managed_environment_id' => (int) $tenant->getKey(), + 'workspace_id' => (int) $tenant->workspace_id, + 'environment_review_id' => (int) $review->getKey(), + 'evidence_snapshot_id' => (int) $snapshot->getKey(), + 'initiated_by_user_id' => (int) $user->getKey(), + ]); + + $review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save(); + } + + $this->actingAs($user); + setAdminPanelContext(); + session()->put(WorkspaceContext::SESSION_KEY, (int) $preparingTenant->workspace_id); + + Livewire::actingAs($user) + ->test(CustomerReviewWorkspace::class) + ->assertCanSeeTableRecords([$preparingTenant->fresh(), $failedTenant->fresh()]) + ->assertSee('Preparing') + ->assertSee('Review Pack is being prepared.') + ->assertSee('Unavailable') + ->assertSee('Review Pack cannot be provided right now.') + ->assertDontSee('Download review pack'); +}); + +it('shows expired and capability-blocked review-pack states on the workspace row surface', function (): void { $expiredTenant = ManagedEnvironment::factory()->create(['name' => 'Expired Pack ManagedEnvironment']); [$user, $expiredTenant] = createUserWithTenant(tenant: $expiredTenant, role: 'readonly'); $blockedTenant = ManagedEnvironment::factory()->create([ @@ -215,7 +273,7 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t ->test(CustomerReviewWorkspace::class) ->assertCanSeeTableRecords([$expiredTenant->fresh(), $blockedTenant->fresh()]) ->assertSee('Expired') - ->assertSee('Blocked') + ->assertSee('Unavailable') ->assertDontSee('Download review pack'); }); diff --git a/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php b/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php index 4145dc81..bff2b573 100644 --- a/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php +++ b/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php @@ -117,6 +117,51 @@ ->assertDontSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $deniedPublishedReview->fresh()], $tenantDenied), false); }); +it('shows the current released review using deterministic published review ordering', function (): void { + $publishedAt = now()->subHour(); + $tenantA = ManagedEnvironment::factory()->create(['name' => 'Alpha ManagedEnvironment']); + [$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'readonly'); + + $tenantB = ManagedEnvironment::factory()->create([ + 'workspace_id' => (int) $tenantA->workspace_id, + 'name' => 'Beta ManagedEnvironment', + ]); + createUserWithTenant(tenant: $tenantB, user: $user, role: 'readonly'); + + $snapshotA = seedEnvironmentReviewEvidence($tenantA); + $snapshotB = seedEnvironmentReviewEvidence($tenantB); + + $alphaReview = composeEnvironmentReviewForTest($tenantA, $user, $snapshotA); + $alphaReview->forceFill([ + 'status' => EnvironmentReviewStatus::Published->value, + 'generated_at' => $publishedAt, + 'published_at' => $publishedAt, + 'published_by_user_id' => (int) $user->getKey(), + ])->save(); + + $betaReview = composeEnvironmentReviewForTest($tenantB, $user, $snapshotB); + $betaReview->forceFill([ + 'status' => EnvironmentReviewStatus::Published->value, + 'generated_at' => $publishedAt, + 'published_at' => $publishedAt, + 'published_by_user_id' => (int) $user->getKey(), + ])->save(); + + $this->actingAs($user); + setAdminPanelContext(); + session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); + + Livewire::actingAs($user) + ->test(CustomerReviewWorkspace::class) + ->assertSeeInOrder([ + 'Latest released review', + 'Beta ManagedEnvironment', + $publishedAt->format('M j, Y'), + 'No decisions require awareness', + ]) + ->assertSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $betaReview->fresh()], $tenantB), false); +}); + it('excludes entitled tenants without a published review from customer workspace rows', function (): void { $tenantPublished = ManagedEnvironment::factory()->create(['name' => 'Published ManagedEnvironment']); [$user, $tenantPublished] = createUserWithTenant(tenant: $tenantPublished, role: 'readonly'); @@ -158,6 +203,45 @@ ->assertSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $publishedReview->fresh()], $tenantPublished), false); }); +it('uses a filter-aware empty state when the active environment has no released review', function (): void { + $tenantPublished = ManagedEnvironment::factory()->create(['name' => 'Published ManagedEnvironment']); + [$user, $tenantPublished] = createUserWithTenant(tenant: $tenantPublished, role: 'readonly'); + + $tenantWithoutPublished = ManagedEnvironment::factory()->create([ + 'workspace_id' => (int) $tenantPublished->workspace_id, + 'name' => 'Filtered ManagedEnvironment', + ]); + createUserWithTenant(tenant: $tenantWithoutPublished, user: $user, role: 'readonly'); + + $publishedReview = composeEnvironmentReviewForTest($tenantPublished, $user, seedEnvironmentReviewEvidence($tenantPublished)); + $publishedReview->forceFill([ + 'status' => EnvironmentReviewStatus::Published->value, + 'published_at' => now(), + 'published_by_user_id' => (int) $user->getKey(), + ])->save(); + + $internalReview = composeEnvironmentReviewForTest($tenantWithoutPublished, $user, seedEnvironmentReviewEvidence($tenantWithoutPublished)); + $internalReview->forceFill([ + 'status' => EnvironmentReviewStatus::Ready->value, + 'published_at' => null, + 'published_by_user_id' => null, + ])->save(); + + $this->actingAs($user); + setAdminPanelContext(); + session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantPublished->workspace_id); + + Livewire::withQueryParams(['managed_environment_id' => (string) $tenantWithoutPublished->getKey()]) + ->actingAs($user) + ->test(CustomerReviewWorkspace::class) + ->assertSet('tableFilters.managed_environment_id.value', (string) $tenantWithoutPublished->getKey()) + ->assertSee('Filtered by environment: Filtered ManagedEnvironment') + ->assertSee('No released customer reviews match the active environment filter.') + ->assertSee('Clear the environment filter to view other released reviews in this workspace.') + ->assertCanNotSeeTableRecords([$tenantPublished->fresh(), $tenantWithoutPublished->fresh()]) + ->assertDontSee('Publish an environment review before it appears in the customer-safe workspace.'); +}); + it('uses a page-level empty state when no entitled tenant has a released review', function (): void { $tenant = ManagedEnvironment::factory()->create(['name' => 'Internal Only ManagedEnvironment']); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly'); @@ -182,7 +266,7 @@ ->assertDontSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $internalOnlyReview->fresh()], $tenant), false); }); -it('summarizes accepted-risk control interpretation and evidence proof availability in customer-safe workspace rows', function (): void { +it('summarizes accepted risks from the released review without exposing internal accountability details', function (): void { $tenant = ManagedEnvironment::factory()->create(['name' => 'Governed ManagedEnvironment']); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly'); $owner = User::factory()->create(['name' => 'Risk Owner']); @@ -223,15 +307,51 @@ Livewire::actingAs($user) ->test(CustomerReviewWorkspace::class) ->assertCanSeeTableRecords([$tenant->fresh()]) + ->assertSee('Accepted risks') + ->assertSee('Accepted risk') + ->assertSee('Included in the released review evidence basis.') ->assertSee('Review required') ->assertSee('Open review') ->assertDontSee('Ready for release') + ->assertDontSee('Risk Owner') + ->assertDontSee('Vendor patch window accepted by the customer.') ->assertDontSee('1 evidence signal(s) reference this control.') ->assertDontSee('1 accepted-risk finding(s) qualify this view.') ->assertDontSee('Review the accepted-risk owner and next review date before customer delivery.') ->assertDontSee('Accepted risk influences this view'); }); +it('renders legacy released reviews without a spec-308 decision summary as customer-safe unavailable evidence', function (): void { + $tenant = ManagedEnvironment::factory()->create(['name' => 'Legacy ManagedEnvironment']); + [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly'); + + $review = composeEnvironmentReviewForTest($tenant, $user, seedEnvironmentReviewEvidence($tenant)); + $review->forceFill([ + 'status' => EnvironmentReviewStatus::Published->value, + 'published_at' => now(), + 'published_by_user_id' => (int) $user->getKey(), + 'summary' => [ + 'finding_count' => 1, + 'debug_payload' => 'raw evidence JSON', + 'source_fingerprint' => 'legacy-fingerprint-abc123', + 'operation_run_url' => '/admin/t/legacy/operation-runs/999', + ], + ])->save(); + + $this->actingAs($user); + setAdminPanelContext(); + session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); + + Livewire::actingAs($user) + ->test(CustomerReviewWorkspace::class) + ->assertSee('Decision evidence unavailable') + ->assertSee('Customer-safe decision evidence is unavailable for this released review.') + ->assertDontSee('raw evidence JSON') + ->assertDontSee('legacy-fingerprint-abc123') + ->assertDontSee('operation-runs') + ->assertDontSee('/admin/t', false); +}); + it('keeps the customer review workspace unfiltered when remembered tenant context is available', function (): void { $tenantA = ManagedEnvironment::factory()->create(['name' => 'Alpha ManagedEnvironment']); [$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'readonly'); diff --git a/specs/312-customer-review-workspace-v1-completion/checklists/requirements.md b/specs/312-customer-review-workspace-v1-completion/checklists/requirements.md new file mode 100644 index 00000000..beb21a53 --- /dev/null +++ b/specs/312-customer-review-workspace-v1-completion/checklists/requirements.md @@ -0,0 +1,59 @@ +# Requirements Checklist: Customer Review Workspace v1 Completion + +**Purpose**: Preparation-readiness checklist for Spec 312. +**Created**: 2026-05-15 +**Feature**: `specs/312-customer-review-workspace-v1-completion/spec.md` + +## Candidate And Scope + +- [x] CHK001 The selected candidate is directly provided by the user and also appears as the first manual-promotion P1 post-Spec-311 candidate in `docs/product/spec-candidates.md`. +- [x] CHK002 The scope is narrowed to Customer Review Workspace productization and existing review/detail/pack handoffs. +- [x] CHK003 Related completed specs 249, 258, 308, and 311 are treated as historical context and dependencies only. +- [x] CHK004 The spec explicitly forbids shell/sidebar/topbar, RBAC, policy, migration, provider, billing, AI, artifact-lifecycle, OperationRun, and Review Pack generation work. +- [x] CHK005 Functional requirements are behavior-oriented and testable. +- [x] CHK006 Acceptance criteria cover latest selection, filter behavior, decision states, accepted risks, evidence basis, pack status, authorization, leakage, links, and legacy fallback. + +## Truth Sources And Proportionality + +- [x] CHK007 The spec identifies review, decision, evidence, review-pack, and execution truth owners. +- [x] CHK008 The proportionality review states no new persisted entity, table, artifact family, abstraction, enum/status family, or cross-domain UI framework is planned. +- [x] CHK009 Legacy pre-Spec-308 behavior is display-only fallback, not migration/backfill/compatibility persistence. +- [x] CHK010 The spec requires deriving from released review/governance-package truth and forbids recomputing a second decision source of truth from raw evidence payloads. + +## UI, Filament, And Disclosure + +- [x] CHK011 The affected surface is classified as an existing Filament page plus existing Blade view composition. +- [x] CHK012 Customer-safe disclosure hierarchy is decision-first, diagnostics absent by default, evidence summarized. +- [x] CHK013 The one-primary-action rule is explicit for latest review, review pack, and filtered empty state contexts. +- [x] CHK014 Filament v5 / Livewire v4 compliance, provider registration location, global search posture, destructive action posture, and asset strategy are required in implementation close-out. +- [x] CHK015 The spec forbids ad-hoc styling systems and new assets. + +## Security, RBAC, And Leakage + +- [x] CHK016 Existing workspace/environment membership, policies, and capabilities remain authoritative. +- [x] CHK017 Non-member and missing-capability semantics are preserved rather than redefined. +- [x] CHK018 Rendered HTML and href leakage assertions are explicitly required. +- [x] CHK019 The spec distinguishes technical IDs in route parameters from visible raw-ID leakage. +- [x] CHK020 `/admin/t` legacy URL leakage is explicitly forbidden. +- [x] CHK021 Operator-only lifecycle and pack mutation actions are explicitly forbidden in the customer-safe workspace. + +## Testing And Validation + +- [x] CHK022 Planned Pest coverage uses existing Reviews, EnvironmentReview, and ReviewPack feature families. +- [x] CHK023 Browser coverage is bounded to the existing CustomerReviewWorkspace smoke because rendered UI changes are expected. +- [x] CHK024 Fixture/helper cost risks are identified and kept feature-local. +- [x] CHK025 Validation commands are concrete and scoped. +- [x] CHK026 Spec 311 regression lane is conditional only if shell/sidebar/topbar-adjacent files are touched. + +## Review Outcome + +- [x] CHK027 Review outcome class: `acceptable-special-case`. +- [x] CHK028 Workflow outcome: `keep`. +- [x] CHK029 Final note location: active feature PR close-out entry `Guardrail / Exception / Smoke Coverage`. +- [x] CHK030 Spec Readiness Gate passes for preparation: `spec.md`, `plan.md`, `tasks.md`, and this checklist exist, contain no template placeholders, and keep implementation scope bounded to Spec 312. + +## Preparation Analyze Notes + +- Candidate Selection Gate: pass. +- Spec Readiness Gate: pass. +- Required implementation stop condition: if implementation appears to require shell/sidebar/topbar, RBAC/policy, migration, OperationRun, persisted status family, asset, or Review Pack generation changes, stop and update the spec/plan before continuing. diff --git a/specs/312-customer-review-workspace-v1-completion/plan.md b/specs/312-customer-review-workspace-v1-completion/plan.md new file mode 100644 index 00000000..d385d028 --- /dev/null +++ b/specs/312-customer-review-workspace-v1-completion/plan.md @@ -0,0 +1,455 @@ +# Implementation Plan: Customer Review Workspace v1 Completion + +**Branch**: `312-customer-review-workspace-v1-completion` | **Date**: 2026-05-15 | **Spec**: `specs/312-customer-review-workspace-v1-completion/spec.md` +**Input**: Feature specification from `specs/312-customer-review-workspace-v1-completion/spec.md` + +## Summary + +Productize the existing Customer Review Workspace as a customer-safe, workspace-wide released-review consumption surface. The implementation should use existing review, governance-package, accepted-risk, evidence, and Review Pack truth; present the latest released review prominently; render customer-safe decision summary, accepted risks, evidence basis, pack status, and filter-aware empty states; preserve customer-safe detail/download handoffs; and avoid all shell/sidebar/topbar, RBAC, persistence, OperationRun, provider, billing, AI, asset, and Review Pack generation changes. + +## Technical Context + +**Language/Version**: PHP 8.4.15, Laravel 12.52.0 +**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, Pest 4.3.1, Tailwind CSS 4.2.2 through existing app assets only +**Storage**: PostgreSQL; no schema changes +**Testing**: Pest 4 feature tests, existing bounded browser smoke if UI changes +**Validation Lanes**: focused confidence lanes for Reviews, EnvironmentReview, ReviewPack; browser lane if rendered UI changes; Spec 311 regression lane only if forbidden/adjacent files are touched unexpectedly +**Target Platform**: Laravel Sail local development; Dokploy/container deployment unchanged +**Project Type**: Laravel monolith under `apps/platform` +**Performance Goals**: workspace-scoped and actor-entitled queries only; no raw evidence payload scans; avoid N+1 regressions for latest review and pack relationships +**Constraints**: no new persistence, no new assets, no new package, no shell/sidebar/topbar work, no RBAC/policy changes unless repo-real inconsistency blocks implementation and is documented first +**Scale/Scope**: one existing Filament page/view plus focused tests and minimal localization + +## UI / Surface Guardrail Plan + +- **Guardrail scope**: changed customer-safe operator/customer-facing surface. +- **Native vs custom classification summary**: existing Filament page plus existing Blade view composition; no new design system or assets. +- **Shared-family relevance**: customer-safe review consumption, status messaging, action links, empty states, evidence/report disclosure. +- **State layers in scope**: page payload, table data, URL query filter, customer-safe detail context. Shell/sidebar/topbar state is explicitly out of scope. +- **Audience modes in scope**: customer-read-only and MSP operator. +- **Decision/diagnostic/raw hierarchy plan**: decision-first latest review and decision summary; accepted risks and evidence basis as supporting content; raw/support details hidden or absent. +- **Raw/support gating plan**: no raw/support data rendered on workspace; existing detail resources remain authoritative. +- **One-primary-action / duplicate-truth control**: one dominant CTA per current review context: download/open pack when authorized and available, otherwise open review; clear filter in filtered empty state. +- **Handling modes by drift class or surface**: shell/sidebar/topbar drift is a blocker and must stop implementation; page-local productization is in scope; broad route/query cleanup is follow-up. +- **Repository-signal treatment**: review-mandatory for UI leakage, `/admin/t` links, raw IDs as visible labels, and action hierarchy. +- **Special surface test profiles**: `standard-native-filament`, `shared-detail-family`, `global-context-shell` assertions only. +- **Required tests or manual smoke**: focused Feature rendered HTML assertions and existing browser smoke if rendered UI changes. +- **Exception path and spread control**: none expected. Any unavoidable local helper must stay private/page-local and be documented in close-out. +- **Active feature PR close-out entry**: `Guardrail / Exception / Smoke Coverage`. + +## Shared Pattern & System Fit + +- **Cross-cutting feature marker**: yes. +- **Systems touched**: `CustomerReviewWorkspace`, `customer-review-workspace.blade.php`, existing review/pack handoff helpers if needed, localization files, focused tests. +- **Shared abstractions reused**: `EnvironmentReviewRegisterService`, `EnvironmentReview::published()`, `EnvironmentReviewResource::tenantScopedUrl()`, `ReviewPackStatus`, `Capabilities::REVIEW_PACK_VIEW`, existing `customer_workspace=1` detail context, existing localization structure. +- **New abstraction introduced? why?**: none planned. Private page helpers are allowed only to shape display payload and keep Blade simple. +- **Why the existing abstraction was sufficient or insufficient**: Existing query and summary paths already contain required truth; the gap is productized presentation and guard tests. +- **Bounded deviation / spread control**: no public presenter/DTO/framework for v1; no new summary persistence. + +## OperationRun UX Impact + +- **Touches OperationRun start/completion/link UX?**: no. +- **Central contract reused**: N/A. +- **Delegated UX behaviors**: unchanged existing Review Pack generation/run behavior. +- **Surface-owned behavior kept local**: read-only pack availability explanation only. +- **Queued DB-notification policy**: N/A. +- **Terminal notification path**: unchanged. +- **Exception path**: none. + +## Provider Boundary & Portability Fit + +- **Shared provider/platform boundary touched?**: no. +- **Provider-owned seams**: N/A. +- **Platform-core seams**: review, evidence, decision, and pack consumption over existing TenantPilot governance artifacts. +- **Neutral platform terms / contracts preserved**: workspace, managed environment, released review, governance decisions, accepted risks, evidence basis, review pack. +- **Retained provider-specific semantics and why**: only existing review/evidence content may include provider-specific names if already present and customer-safe. +- **Bounded extraction or follow-up path**: Provider Connection Scope Hardening remains separate. + +## Constitution Check + +- Inventory-first: PASS. Uses existing released-review/evidence snapshots and does not call Graph or inspect external state. +- Read/write separation: PASS. Customer Review Workspace remains read-only. No mutation CTA in customer-safe surface. +- Graph contract path: PASS. No Graph calls. +- Deterministic capabilities: PASS. Existing capabilities/policies remain authoritative. +- RBAC-UX: PASS. No RBAC change; workspace/environment membership and capability semantics remain existing server-side truth. +- Workspace isolation: PASS. Queries must scope to current workspace and actor-entitled managed environments. +- Tenant/environment isolation: PASS. Environment filter must not reveal out-of-scope records. +- Run observability: PASS. No new long-running work or OperationRun semantics. +- Data minimization: PASS. Raw payloads, fingerprints, OperationRun URLs, storage paths, and provider dumps are forbidden in rendered customer-safe HTML. +- Test governance: PASS with focused Feature coverage plus browser smoke when UI changes. +- Proportionality: PASS. No new persisted truth, enum/status family, public abstraction, or UI framework. +- Filament-native UI: PASS if implementation uses existing Filament page/Blade patterns and no new assets/ad-hoc style system. +- Decision-first operating model: PASS. Latest review and decision summary lead; diagnostics/raw data stay absent. +- Audience-aware disclosure: PASS. Customer-safe default hides raw/internal details. + +## Test Governance Check + +- **Test purpose / classification by changed surface**: Feature tests for workspace rendering, deterministic latest selection, filter behavior, decision summary states, accepted risks, evidence basis, pack status, authorization, links, and leakage. Browser smoke for rendered UI workflow if available. +- **Affected validation lanes**: confidence, browser if UI changes. +- **Why this lane mix is the narrowest sufficient proof**: Behavior is primarily server-rendered Filament/Livewire HTML and existing route/link authorization. Focused feature tests can assert payloads, HTML, hrefs, and action absence; one browser smoke covers visual/user workflow risk. +- **Narrowest proving command(s)**: the focused commands listed in `spec.md` and final validation section below. +- **Fixture / helper / factory / seed / context cost risks**: released reviews, review packs, evidence snapshots, accepted risks, and workspace/environment membership. Helpers must stay feature-local or existing; no new heavy default setup. +- **Expensive defaults or shared helper growth introduced?**: no. +- **Heavy-family additions, promotions, or visibility changes**: no new heavy-governance family; browser smoke uses existing family. +- **Surface-class relief / special coverage rule**: standard-native-filament plus shared-detail-family; global-context-shell lane only if shell files are touched unexpectedly. +- **Closing validation and reviewer handoff**: reviewers verify no forbidden files, no leakage, no lifecycle actions, no `/admin/t`, no raw IDs as visible labels, and no new persistence/assets/RBAC. +- **Budget / baseline / trend follow-up**: none expected. +- **Review-stop questions**: lane fit, hidden fixture cost, UI leakage, shell drift, action hierarchy, pack status truth. +- **Escalation path**: none unless shell/sidebar/topbar or RBAC becomes necessary; then stop and update spec/plan. +- **Active feature PR close-out entry**: `Guardrail / Exception / Smoke Coverage`. +- **Why no dedicated follow-up spec is needed**: Scope is a bounded productization slice; broader link cleanup, localization, artifact lifecycle, billing, provider hardening, and support handoff remain explicit follow-ups. + +## Project Structure + +### Documentation + +```text +specs/312-customer-review-workspace-v1-completion/ +├── checklists/ +│ └── requirements.md +├── spec.md +├── plan.md +└── tasks.md +``` + +### Source Code Likely Affected Later + +```text +apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php +apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php +apps/platform/lang/en/localization.php +apps/platform/lang/de/localization.php +apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php +apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php +apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php +apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php +apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php +apps/platform/tests/Feature/ReviewPack/EnvironmentReviewDerivedReviewPackTest.php +``` + +Only if repo verification proves handoff cannot be preserved otherwise: + +```text +apps/platform/app/Filament/Resources/EnvironmentReviewResource.php +apps/platform/app/Filament/Resources/ReviewPackResource.php +apps/platform/tests/Feature/ReviewPack/ReviewPackDownloadTest.php +apps/platform/tests/Feature/ReviewPack/ReviewPackRbacTest.php +apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php +``` + +### Must Not Change + +```text +apps/platform/app/Support/OperateHub/OperateHubShell.php +apps/platform/app/Support/Tenants/TenantPageCategory.php +apps/platform/app/Support/Navigation/NavigationScope.php +apps/platform/app/Support/Navigation/WorkspaceSidebarNavigation.php +apps/platform/app/Policies/* +apps/platform/database/migrations/* +apps/platform/app/Jobs/GenerateReviewPackJob.php +apps/platform/app/Services/ReviewPackService.php +``` + +`GenerateReviewPackJob` and `ReviewPackService` should generally not change. If a test reveals a display-only field already exists but is unreachable, prefer workspace rendering changes over generation semantics changes. + +**Structure Decision**: Use existing Laravel/Filament structure. Do not create new base folders. + +## Current Repo Findings + +### Customer Review Workspace + +- `CustomerReviewWorkspace` exists at `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`. +- It is an undiscovered Filament page with slug `reviews/workspace` and a Blade view at `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php`. +- It already scopes rows through `EnvironmentReviewRegisterService::customerWorkspaceTenantQuery()`. +- It already supports explicit `tenant` / `managed_environment_id` query prefiltering as a page-level table filter. +- It already avoids remembered environment default filter. +- It already links to review detail with `customer_workspace=1` and `source_surface=customer_review_workspace`. + +### Released Review Selection + +- Repo-real released status is `EnvironmentReviewStatus::Published`. +- `EnvironmentReview::scopePublished()` is available. +- Latest workspace query currently uses `published_at desc`, `generated_at desc`, `id desc`. +- Spec 312 should test that deterministic behavior and only adjust if a global latest section needs a separate query. + +### Decision Summary + +- Spec 308 summary content exists under `EnvironmentReview.summary['governance_package']['decision_summary']`. +- Existing composer also stores `governance_decisions`, `accepted_risks`, and `evidence_basis_summary`. +- Workspace should consume this truth and not rebuild from raw evidence payloads. + +### Review Pack Status + +- Repo-real `ReviewPackStatus` values are `queued`, `generating`, `ready`, `failed`, `expired`. +- Existing page maps some states as `available`, `partial`, `blocked`, `expired`, `unavailable`. +- Spec 312 should refine customer-safe wording without adding persisted states. + +## Phase 0: Preparation and Guardrails + +1. Confirm branch is `312-customer-review-workspace-v1-completion`. +2. Confirm working tree has no uncommitted shell/sidebar/topbar or Spec 311 runtime files. +3. Re-read Spec 308 and Spec 311 artifacts before implementation. +4. Inspect any old WIP patch selectively if present; reject shell/sidebar/topbar files. +5. Confirm no migration, model, policy, OperationRun type, asset, or Review Pack generation change is needed before runtime work begins. + +## Phase 1: Tests First + +Add or extend focused tests before implementation: + +- workspace-wide behavior with active environment filter +- deterministic latest released review selection +- latest/current review section +- decision summary states: requires awareness, none, unavailable, incomplete +- legacy pre-Spec-308 review/pack fallback +- accepted-risk customer-safe display and redaction/non-leakage +- evidence basis states and no "incomplete means no decisions" confusion +- review-pack states: ready/available, missing, queued/generating, expired, failed/unavailable, evidence incomplete +- pack exists but actor lacks download capability +- filtered empty state vs global no-review state +- lifecycle action absence +- no `/admin/t` links +- no raw payload/fingerprint/debug/OperationRun/storage/policy leakage +- visible labels avoid raw IDs as customer-facing meaning + +## Phase 2: Runtime Implementation + +Implement the narrowest page changes: + +- Build/refine a page payload for the current/latest released review. +- Keep latest selection deterministic and workspace/filter scoped. +- Render latest released review section in existing Blade view. +- Render Decision Summary from existing Spec 308 summary content. +- Render legacy unavailable/incomplete decision summary fallback. +- Render accepted risks only from customer-safe released review truth. +- Render evidence basis state and summary text. +- Render review-pack availability state and one primary CTA. +- Hide download/open URL if actor lacks existing capability/policy. +- Improve global and filtered empty states. +- Add minimal localization keys in existing files. +- Preserve detail handoff query parameters. +- Avoid raw evidence payload display/loading. + +## Phase 3: Security and Leakage Pass + +Verify rendered workspace HTML and hrefs: + +- no fingerprints +- no raw evidence JSON +- no provider payload dumps +- no OperationRun URLs +- no storage paths +- no policy internals +- no exception internals +- no internal reason-family labels +- no visible raw IDs as labels/copy +- no `/admin/t` +- no operator-only lifecycle actions +- no pack mutation actions +- no cross-workspace/environment influence + +## Phase 4: Validation + +Run focused lanes: + +```bash +cd apps/platform && ./vendor/bin/sail artisan test --compact \ + tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php \ + tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php \ + tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php \ + tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php +``` + +```bash +cd apps/platform && ./vendor/bin/sail artisan test --compact \ + tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php \ + tests/Feature/EnvironmentReview/EnvironmentReviewCreationTest.php +``` + +```bash +cd apps/platform && ./vendor/bin/sail artisan test --compact \ + tests/Feature/ReviewPack/EnvironmentReviewDerivedReviewPackTest.php \ + tests/Feature/ReviewPack/ReviewPackDownloadTest.php \ + tests/Feature/ReviewPack/ReviewPackRbacTest.php +``` + +Run only if shell/sidebar/topbar-adjacent files are touched unexpectedly: + +```bash +cd apps/platform && ./vendor/bin/sail artisan test --compact \ + tests/Feature/Filament/PanelNavigationSegregationTest.php \ + tests/Feature/Workspaces/GlobalContextShellContractTest.php \ + tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php +``` + +Run because rendered UI changes are expected: + +```bash +cd apps/platform && ./vendor/bin/sail artisan test --compact \ + tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php +``` + +Formatting and diff: + +```bash +cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent +git diff --check +``` + +## Phase 5: Close-out + +Record close-out in this file after implementation: + +```text +# Spec 312 Close-out + +## Changed Files + +## Acceptance Criteria Fulfilled + +## Tests Run + +## Tests Not Run + +## Browser Smoke + +## No Migration Confirmation + +## No RBAC / Policy Confirmation + +## No Asset Confirmation + +## No OperationRun Type Confirmation + +## No New Persistence / Status Family Confirmation + +## No Shell / Sidebar / Topbar Work Confirmation + +## No ReviewPack Generation Semantics Change Confirmation + +## Leakage Guard Confirmation + +## Remaining Gaps + +## Follow-up Candidates +``` + +## Complexity Tracking + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|---|---|---| +| None expected | N/A | N/A | + +Spec 312 intentionally avoids new persistence, new abstractions, new enum/status families, new taxonomies, and new frameworks. Any implementation discovery that appears to require one of these must stop and update the spec/plan before code proceeds. + +## Filament v5 Output Contract For Implementation Close-out + +The implementation close-out must state: + +1. Livewire v4.0+ compliance: expected unchanged, project currently uses Livewire 4.1.4. +2. Provider registration location: no panel provider change; Laravel 12 providers remain in `apps/platform/bootstrap/providers.php`. +3. Global search: no globally searchable resource should be added or changed; if a resource is touched unexpectedly, document View/Edit/global-search posture. +4. Destructive actions: no destructive actions introduced; customer-safe workspace must not render lifecycle mutations. +5. Asset strategy: no new assets; deploy `filament:assets` requirement unchanged. +6. Testing plan/results: record focused Feature lanes, Browser smoke result or reason not run, Pint dirty, and `git diff --check`. +7. Deployment impact: no env, migration, queue, scheduler, storage, or Review Pack generation semantics change expected. + +# Spec 312 Close-out + +## Changed Files + +- `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` +- `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php` +- `apps/platform/lang/en/localization.php` +- `apps/platform/lang/de/localization.php` +- `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php` +- `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php` +- `apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php` +- `specs/312-customer-review-workspace-v1-completion/spec.md` +- `specs/312-customer-review-workspace-v1-completion/plan.md` +- `specs/312-customer-review-workspace-v1-completion/tasks.md` +- `specs/312-customer-review-workspace-v1-completion/checklists/requirements.md` + +## Acceptance Criteria Fulfilled + +- Customer Review Workspace remains workspace-wide and keeps environment selection as a page-level filter. +- Latest released review is selected deterministically with repo-real `published_at desc`, `generated_at desc`, `id desc` ordering and rendered before the list. +- Decision Summary is consumed from existing Spec 308 governance package truth with customer-safe none, awareness-required, unavailable, and incomplete/fallback handling. +- Accepted risks render from released-review governance package content without internal owners, approvers, request reasons, or workflow controls. +- Evidence basis renders a customer-safe state without raw evidence payload, snapshot fingerprints, OperationRun URLs, or storage paths. +- Review Pack status maps existing `queued`, `generating`, `ready`, `failed`, `expired`, missing, blocked, and evidence-incomplete cases to customer-safe copy. +- Ready Review Packs show a dominant signed download CTA only when the existing actor capability allows it; otherwise the page falls back to open-review or neutral status. +- Filtered empty state distinguishes active-filter no-results from workspace-wide no released reviews and offers the existing clear-filter CTA. +- Rendered customer-safe workspace assertions cover no lifecycle actions, no pack mutation actions, no `/admin/t` links, and no raw/internal leakage. + +## Tests Run + +- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` + Result: passed, 25 tests / 153 assertions. +- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php tests/Feature/EnvironmentReview/EnvironmentReviewCreationTest.php` + Result: passed, 5 tests / 68 assertions. +- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/EnvironmentReviewDerivedReviewPackTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/ReviewPack/ReviewPackRbacTest.php` + Result: passed, 22 tests / 93 assertions. +- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php` + Result: passed, 1 test / 48 assertions. +- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` + Result: passed. +- `git diff --check` + Result: passed. + +## Tests Not Run + +- Spec 311 regression lane was not run because no shell/sidebar/topbar/navigation-scope files changed. + +## Browser Smoke + +Passed with the existing Pest browser smoke for the Environment Review detail to Customer Review Workspace handoff and back to read-only customer-safe review detail. The expected visible German copy was updated to the new Spec 312 customer-safe latest-review and Review Pack wording. + +## No Migration Confirmation + +No migrations, tables, columns, models, or schema changes were added. + +## No RBAC / Policy Confirmation + +No RBAC or policy files were changed. Existing `Capabilities::REVIEW_PACK_VIEW`, environment review visibility, workspace membership, and direct download-route authorization remain authoritative. + +## No Asset Confirmation + +No CSS, JavaScript, image, icon pack, Vite, or Filament asset registration changes were added. + +## No OperationRun Type Confirmation + +No OperationRun type, lifecycle, queue, scheduler, or run-notification behavior was added or changed. + +## No New Persistence / Status Family Confirmation + +No new persisted state, enum/status family, table, column, model, or public taxonomy was introduced. Customer-safe Review Pack states are display-only mappings over existing `ReviewPackStatus` truth. + +## No Shell / Sidebar / Topbar Work Confirmation + +No changes were made to `OperateHubShell`, `TenantPageCategory`, `NavigationScope`, `WorkspaceSidebarNavigation`, sidebar composition, topbar context, or route-scope contracts. + +## No ReviewPack Generation Semantics Change Confirmation + +No changes were made to Review Pack generation, regeneration, expire behavior, storage contract, export format, or `ReviewPackService` generation semantics. The workspace only consumes the existing signed download URL helper for authorized ready packs. + +## Leakage Guard Confirmation + +Focused rendered HTML assertions verify no raw evidence JSON, source fingerprints, raw OperationRun URLs, `/admin/t` legacy links, internal owner/approver names, internal request reasons, Generate/Regenerate/Expire actions, or lifecycle mutation actions appear in the customer-safe workspace. + +## Remaining Gaps + +None for Spec 312 scope. Broader localization adoption, canonical query cleanup, pre-Spec-308 backfill, artifact lifecycle, billing, provider hardening, and customer approval workflow remain out of scope. + +## Follow-up Candidates + +Use the follow-up list from the spec: Provider Connection Scope Hardening, Canonical Link / Query Cleanup, Product Truth / Docs Drift Cleanup, Review Pack backfill for pre-Spec-308 artifacts, Customer-facing Localization Adoption v1, Artifact Lifecycle & Retention, Commercial Entitlements / Billing-State Maturity, and Customer Approval Workflow. + +## Filament v5 Output Contract + +- Livewire v4.0+ compliance: unchanged; project uses Livewire 4.1.4 and Filament 5.2.1. +- Provider registration location: no panel provider change; Laravel 12 providers remain in `apps/platform/bootstrap/providers.php`. +- Global search: no globally searchable resource was added or changed. +- Destructive actions: no destructive actions were introduced; customer-safe workspace renders no lifecycle mutations. +- Asset strategy: no new assets; deploy-time `filament:assets` posture is unchanged. +- Testing coverage: focused Feature lanes, Browser smoke, Pint dirty, and `git diff --check` passed as recorded above. +- Deployment impact: no env var, migration, queue, scheduler, storage, or Review Pack generation impact. diff --git a/specs/312-customer-review-workspace-v1-completion/spec.md b/specs/312-customer-review-workspace-v1-completion/spec.md new file mode 100644 index 00000000..c59ee59d --- /dev/null +++ b/specs/312-customer-review-workspace-v1-completion/spec.md @@ -0,0 +1,558 @@ +# Feature Specification: Customer Review Workspace v1 Completion + +**Feature Branch**: `312-customer-review-workspace-v1-completion` +**Created**: 2026-05-15 +**Status**: Draft +**Input**: User-provided full Spec 312 draft plus repo-backed candidate `Customer Review Workspace v1 Completion`. + +## Spec Candidate Check *(mandatory - SPEC-GATE-001)* + +- **Problem**: Customer Review Workspace has repo-real foundations, but the customer-facing review consumption experience is still too list-like and technical. Stakeholders do not immediately understand the latest released review, customer-safe decision summary, accepted risks, evidence basis, review-pack status, and next action in one coherent surface. +- **Today's failure**: A workspace can have released reviews, review packs, decision summaries, accepted-risk data, and evidence truth, yet the customer-safe surface can still require operator translation. Filtered empty states and legacy reviews without Spec 308 summary content can also imply the wrong state or expose too much technical detail if not handled deliberately. +- **User-visible improvement**: Customer Review Workspace becomes a calm workspace-wide review hub that makes the current released review, decisions requiring awareness, accepted risks, evidence basis, review-pack availability, and one primary next action visible without exposing raw diagnostics or operator lifecycle actions. +- **Smallest enterprise-capable version**: Productize only the existing Customer Review Workspace read surface and its existing review/detail/pack handoffs. Use existing `EnvironmentReview.summary`, `governance_package.decision_summary`, `ReviewPack` status truth, existing route helpers, existing RBAC, and focused rendered-HTML tests. No shell, sidebar, topbar, RBAC, persistence, OperationRun, provider, billing, AI, or artifact-lifecycle work. +- **Explicit non-goals**: No changes to `OperateHubShell`, `TenantPageCategory`, `NavigationScope`, `WorkspaceSidebarNavigation`, route-scope contract, RBAC, policies, migrations, tables, columns, models, persisted status families, OperationRun types, Review Pack generation semantics, provider connection scope, billing, AI summaries, artifact lifecycle, PSA handoff, broad localization, canonical query cleanup, or Environment Review / Review Pack resource redesign. +- **Permanent complexity imported**: Feature-local page payload shaping, localized copy keys where surrounding code already uses localization, focused feature/browser coverage, and an implementation close-out entry. No new persisted truth, public framework, enum/status family, model, table, asset bundle, or OperationRun type. +- **Why now**: `docs/product/spec-candidates.md` lists Customer Review Workspace v1 Completion as the first post-Spec-311 P1 manual-promotion candidate. Spec 308 made customer-safe decision summaries available, and Spec 311 completed the workspace/environment shell-scope contract. The next narrow step can now focus on productizing customer-safe consumption. +- **Why not local**: A one-off table label or a detail-only tweak would not solve the review-consumption workflow. A portal rewrite or shell fix would be too broad. The narrow correct slice is to shape and render existing released-review, decision-summary, accepted-risk, evidence, and pack truth on the existing workspace hub. +- **Approval class**: Workflow Compression. +- **Red flags triggered**: Customer-safe disclosure across review, pack, and evidence presentation. Defense: scope is bounded to existing surfaces and existing truth, with explicit no-new-persistence, no-new-status, no-new-RBAC, no-new-shell, and no-new-lifecycle constraints. +- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 2 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 12/12** +- **Decision**: approve. + +## Spec Scope Fields *(mandatory)* + +- **Scope**: workspace-wide customer-safe review consumption surface, with environment selection only as a page-level data filter. +- **Primary Routes**: + - Customer Review Workspace: existing `CustomerReviewWorkspace` route with slug `reviews/workspace`. + - Environment Review detail: existing canonical environment-bound `EnvironmentReviewResource` view route. + - Review Pack delivery: existing Review Pack view/download route or customer-safe signed download flow where already available. +- **Data Ownership**: + - Review truth remains `EnvironmentReview.status`, `EnvironmentReview.published_at`, and `EnvironmentReview.summary`. + - Decision truth remains `FindingException` / `FindingExceptionDecision`, represented through existing released-review governance-package summary. + - Evidence truth remains `EvidenceSnapshot` and existing evidence summary fields. + - Review Pack truth remains `ReviewPack.status`, file path availability, `ReviewPack.summary`, and existing pack metadata. + - Execution truth remains `OperationRun` and must not be rendered as raw customer-safe detail. +- **RBAC**: + - Workspace membership remains required. + - Managed environment entitlement remains required for environment-filtered data and environment-bound detail routes. + - Existing review and review-pack policies/capabilities remain authoritative. + - Non-member / out-of-scope access remains deny-as-not-found according to existing semantics. + - Member missing capability remains denied according to existing policy semantics. + +For canonical-view specs: + +- **Default filter behavior when tenant-context is active**: Customer Review Workspace remains workspace-wide. Remembered environment or Filament tenant context must not become global shell context. Query parameters such as `tenant` or `managed_environment_id` may set only the page-level environment filter when existing behavior supports them. +- **Explicit entitlement checks preventing cross-tenant leakage**: Counts, latest-review selection, decision summaries, review-pack links, and empty-state distinction must be scoped to the current workspace and the actor's managed-environment entitlements. + +## Cross-Cutting / Shared Pattern Reuse *(mandatory)* + +- **Cross-cutting feature?**: yes. +- **Interaction class(es)**: customer-safe review consumption, status messaging, action links, empty states, evidence/report disclosure, review-pack availability. +- **Systems touched**: `CustomerReviewWorkspace`, `customer-review-workspace.blade.php`, existing `EnvironmentReviewResource` handoff only if required, existing Review Pack view/download handoff only if required, existing review workspace feature tests, review-pack tests, and existing browser smoke. +- **Existing pattern(s) to extend**: existing Customer Review Workspace table/page, existing `EnvironmentReviewRegisterService::customerWorkspaceTenantQuery()`, existing `EnvironmentReview::published()` scope, existing `governance_package.decision_summary`, existing `ReviewPackStatus`, existing `Capabilities::REVIEW_PACK_VIEW`, existing localization file structure, existing `customer_workspace=1` detail context. +- **Shared contract / presenter / builder / renderer to reuse**: Reuse `EnvironmentReviewRegisterService`, `EnvironmentReviewResource::tenantScopedUrl()`, existing `ReviewPack` status enum/constant truth, existing `ArtifactTruthPresenter` where already used, existing Filament/Blade page patterns, and existing localization keys. Do not create a public decision-summary presenter framework for v1. +- **Why the existing shared path is sufficient or insufficient**: Existing services already provide authorized workspace/environment review queries and existing summaries contain Spec 308 decision content. The current page is insufficient as a customer-safe consumption surface because it does not yet present latest review, decision summary, accepted risks, evidence basis, pack status, and filter-aware empties as one coherent experience. +- **Allowed deviation and why**: A page-local private helper for derived display payloads is allowed if it keeps the implementation narrow and avoids business logic in Blade. New shared frameworks, new DTO families, and new persisted summaries are not allowed. +- **Consistency impact**: Wording and status mapping must stay aligned across Customer Review Workspace, Environment Review detail handoff, and Review Pack availability. Customer-safe labels must avoid raw IDs, fingerprints, raw payloads, OperationRun URLs, provider dumps, and internal reason families. +- **Review focus**: Verify no shell/sidebar/topbar regression, no raw leakage, no operator-only actions, no new persistence/status families, no policy/RBAC change, no Review Pack generation semantics change, and no `/admin/t` links. + +## OperationRun UX Impact *(mandatory)* + +- **Touches OperationRun start/completion/link UX?**: no. +- **Shared OperationRun UX contract/layer reused**: N/A. Existing Review Pack generation may have OperationRun behavior, but Spec 312 must not create, start, mutate, link, or expose OperationRun raw detail from the customer-safe workspace. +- **Delegated start/completion UX behaviors**: unchanged. +- **Local surface-owned behavior that remains**: Read-only review and pack availability presentation only. +- **Queued DB-notification policy**: N/A. +- **Terminal notification path**: unchanged. +- **Exception required?**: none. + +## Provider Boundary / Platform Core Check *(mandatory)* + +- **Shared provider/platform boundary touched?**: no. +- **Boundary classification**: platform-core customer review consumption over existing governance artifacts. +- **Seams affected**: Existing review, evidence, decision, and review-pack summaries. No Graph contract, provider connection, provider credential, or provider dispatch seam changes. +- **Neutral platform terms preserved or introduced**: workspace, managed environment, customer review, released review, governance decisions requiring awareness, accepted risks, evidence basis, review pack, next action. +- **Provider-specific semantics retained and why**: Existing review/evidence titles may contain Microsoft/Intune terms when that is the existing customer-safe content. Spec 312 must not introduce new provider-specific platform-core vocabulary. +- **Why this does not deepen provider coupling accidentally**: The workspace derives display from TenantPilot review and governance artifact truth, not raw provider payloads or Graph endpoints. +- **Follow-up path**: provider connection scope hardening remains a separate follow-up candidate. + +## UI / Surface Guardrail Impact *(mandatory)* + +| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note | +|---|---:|---|---|---|---:|---| +| Customer Review Workspace overview | yes | Existing Filament page plus existing Blade view composition | customer-safe review consumption, status messaging, empty states, action links | page, table, URL query filter | no | Productize existing page only | +| Latest released review section | yes | Existing Filament/Blade page composition | review handoff, review-pack handoff | page payload | no | One dominant CTA only | +| Decision summary / accepted risks / evidence basis | yes | Existing summary payload rendered as customer-safe content | evidence/report disclosure | page payload | no | No raw payloads or diagnostics | +| Environment Review detail handoff | possible | Existing native Filament resource | released-review detail | detail query context | no | Only if canonical customer-safe handoff needs adjustment | +| Review Pack download/open handoff | possible | Existing resource/download route | artifact delivery | link/action visibility | no | No generation/regeneration/expire action | +| Shell/sidebar/topbar | no | N/A | route-scope contract | none | no | Must not change | + +Implementation intent: use existing Filament-native components and the current Blade view. Do not add CSS, JS, images, icon packs, assets, or ad-hoc styling systems. Keep Filament v5 / Livewire v4.1.4+ compatible. + +## Decision-First Surface Role *(mandatory)* + +| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction | +|---|---|---|---|---|---|---|---| +| Customer Review Workspace | Primary customer-safe consumption surface | Stakeholder or MSP operator understands current released review status and next action | latest released review, customer-safe decision summary, accepted risks, evidence basis, review-pack state, primary CTA | Environment Review detail and Review Pack download/open | Primary because it is the workspace-wide customer hub | Follows released-review consumption, not operator lifecycle work | Removes manual translation across review, pack, evidence, and decision pages | +| Environment Review detail | Secondary detail surface | Reader inspects the selected released review | released review context and existing customer-safe summary | sections, evidence, package detail where allowed | Secondary because it deepens the chosen review | Existing owner of review detail | Prevents rebuilding review detail inside workspace | +| Review Pack delivery | Artifact delivery surface | Reader downloads or opens the prepared package | pack availability/download when authorized | package content | Delivery artifact, not a workspace dashboard | Existing pack owner remains authoritative | Avoids creating parallel artifact lifecycle | + +## Audience-Aware Disclosure *(mandatory)* + +| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention | +|---|---|---|---|---|---|---|---| +| Customer Review Workspace | customer-read-only, MSP operator | latest review, decisions requiring awareness, accepted risks, evidence basis, pack status, filter state | none by default | raw evidence JSON, fingerprints, OperationRun URLs, provider payloads hidden | Download/open review pack when available; otherwise open review; clear filter in filtered empty state | operator-only lifecycle actions and raw/internal metadata | Workspace states the decision once; detail/export provide evidence | +| Environment Review detail handoff | customer-read-only, MSP operator | selected released review summary and package availability | existing detail sections only where allowed | raw/support details not introduced here | existing detail action hierarchy | publish/refresh/archive hidden in customer workspace mode | Detail remains review owner | +| Review Pack handoff | customer-read-only, MSP operator | available/preparing/not available/expired/unavailable state and download/open when authorized | none from workspace | storage path, policy internals, exception internals hidden | download/open only when authorized | generate/regenerate/expire hidden from customer workspace | Pack status uses existing `ReviewPack` truth | + +## UI/UX Surface Classification *(mandatory)* + +| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| Customer Review Workspace | List / Review Workspace | Customer-safe workspace hub | Download/open review pack or open latest review | Dedicated CTA/link, not full-row fake affordance | current no-row-click behavior preserved unless repo-real route/capability allows | secondary links neutral and contextual | none | existing `/admin/reviews/workspace` | existing `EnvironmentReviewResource` view | workspace plus optional environment filter label | Customer reviews | latest review, decision summary, accepted risks, evidence basis, pack state | none | +| Filtered empty state | Empty State | Filter-aware customer review empty | Clear environment filter | Single clear-filter CTA when applicable | N/A | none | none | existing workspace route | N/A | active page-level filter | Customer reviews | filter has no matches vs no released reviews exist | none | +| Review Pack handoff | Detail / Artifact delivery | Download/open existing pack | Download/open pack | Existing route/action | N/A | status explanation neutral | existing operator actions elsewhere only | existing ReviewPack route | existing ReviewPack route/download | environment and review context | Review pack | availability and authorization-safe status | none | + +## Operator Surface Contract *(mandatory)* + +| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions | +|---|---|---|---|---|---|---|---|---|---|---| +| Customer Review Workspace | Customer-safe reader / MSP operator | Consume released review and choose next safe action | Workspace review hub | What is the current released customer review and what needs attention? | latest review, environment filter, decision summary, accepted risks, evidence basis, review-pack availability, primary next action | none by default | review release status, decision awareness, evidence completeness, review-pack availability | none | Download/open review pack, Open review, Clear filter | none | +| Environment Review detail handoff | Customer-safe reader / MSP operator | Inspect selected released review | Existing detail page | What does this released review contain? | existing customer-safe review detail | existing diagnostics only where already allowed | review status, evidence completeness, pack availability | existing read/download only | Open review / Download pack where existing | no new dangerous actions | +| Review Pack handoff | Customer-safe reader / MSP operator | Receive prepared package | Existing artifact flow | Is the review pack available to download/open? | pack state, safe explanation, download/open when authorized | none from workspace | ready/preparing/not available/expired/unavailable/evidence incomplete | existing download only | Download/open pack | no generate/regenerate/expire in workspace | + +## Proportionality Review *(mandatory when structural complexity is introduced)* + +- **New source of truth?**: no. +- **New persisted entity/table/artifact?**: no. +- **New abstraction?**: no public abstraction. Page-local private display helpers are allowed only if needed to avoid Blade logic. +- **New enum/state/reason family?**: no persisted or domain state. Customer-safe display labels are derived from existing `EnvironmentReviewStatus`, `EnvironmentReviewCompletenessState`, `ReviewPackStatus`, and existing summary payloads. +- **New cross-domain UI framework/taxonomy?**: no. +- **Current operator problem**: Customer-safe review consumption still requires operator translation and can confuse filtered absence, incomplete evidence, missing packs, and no decisions. +- **Existing structure is insufficient because**: Current workspace table and intro copy do not yet present the released-review story as latest review plus decision summary, accepted risks, evidence basis, pack state, and one next action. +- **Narrowest correct implementation**: Shape and render a derived page payload from existing review/pack/evidence truth, add localized copy where needed, and cover with focused tests. +- **Ownership cost**: Feature-local tests, localization keys, and page helper maintenance. +- **Alternative intentionally rejected**: New customer portal, new review-pack status machine, new summary persistence, new customer approval workflow, shell/sidebar route fix, and broad localization adoption. +- **Release truth**: Current-release productization over existing review and governance artifacts. + +### Compatibility posture + +This feature assumes pre-production runtime posture but requires legacy customer-safe rendering for released reviews or Review Packs created before Spec 308. The compatibility behavior is display-only: show unavailable/incomplete decision/evidence states without failing or exposing raw payloads. No data backfill, migration shim, dual-write, or persisted legacy alias is allowed. + +## Summary + +Spec 312 finalizes Customer Review Workspace as a customer-safe, workspace-wide review consumption surface for released environment reviews. + +After Spec 308, customer-safe Decision Summary content exists in Environment Reviews and Review Packs. After Spec 311, route scope is separated from page-level environment filters. Spec 312 uses that foundation and productizes consumption only: + +- latest released review visibility +- deterministic latest-review selection +- customer-safe Decision Summary +- accepted risks +- evidence basis +- Review Pack status and download/open handoff +- clear empty states +- no operator-only lifecycle actions +- no raw/internal/debug leakage +- robust legacy fallback for pre-Spec-308 review/pack content + +This is not a shell, navigation, RBAC, provider, billing, AI, artifact-lifecycle, migration, or OperationRun spec. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - See the current released review first (Priority: P1) + +As a customer-safe reader, I want the workspace to show the latest released review prominently so I know which review is current. + +**Why this priority**: Without a deterministic latest/current review, the workspace remains a list that still needs operator explanation. + +**Independent Test**: Seed multiple published reviews with distinct and tied timestamps, open Customer Review Workspace, and assert the latest review section uses `published_at desc`, then `generated_at` or repo-real fallback, then `id desc`. + +**Acceptance Scenarios**: + +1. **Given** multiple released reviews in the workspace, **When** the workspace renders, **Then** the latest/current review section shows the deterministic latest released review. +2. **Given** an active environment filter, **When** matching released reviews exist, **Then** latest selection is scoped to that filter without changing shell/sidebar/topbar context. +3. **Given** multiple released reviews with equal timestamps, **When** latest selection runs, **Then** the repo-real stable tie-breaker selects the same review every time. + +### User Story 2 - Understand decisions, risks, and evidence without raw data (Priority: P1) + +As a stakeholder, I want the workspace to explain decisions requiring awareness, accepted risks, and evidence basis in customer-safe language so I can understand the review without internal diagnostics. + +**Why this priority**: This is the core customer-safe productization value unlocked by Spec 308. + +**Independent Test**: Render workspace HTML for released reviews with decision summary states `requires_awareness`, `none`, `unavailable`, and `incomplete`, and assert customer-safe copy appears while raw payloads, fingerprints, OperationRun URLs, provider dumps, and internal reason families do not. + +**Acceptance Scenarios**: + +1. **Given** a released review with Spec 308 `decision_summary`, **When** the workspace renders, **Then** the customer-safe summary count, status, text, entries, and next action are visible. +2. **Given** no decisions require awareness, **When** the workspace renders, **Then** it says no decisions require awareness and does not imply incomplete evidence. +3. **Given** missing or incomplete decision/evidence summary, **When** the workspace renders, **Then** it shows unavailable/incomplete evidence, not "No decisions". +4. **Given** accepted risks in customer-safe released-review scope, **When** the workspace renders, **Then** it shows title/state/summary/due context without internal workflow details or mutation controls. + +### User Story 3 - Know whether a review pack can be consumed (Priority: P1) + +As a customer-safe reader, I want Review Pack availability to be clear and actionable so I know whether to download/open the package or inspect the review. + +**Why this priority**: Review Pack is the customer artifact, and ambiguity here makes the workspace not sellable. + +**Independent Test**: Render ready, queued/generating, missing, expired, failed, evidence-incomplete, and unauthorized pack scenarios and assert the customer-safe status and CTA visibility. + +**Acceptance Scenarios**: + +1. **Given** a ready downloadable pack and authorized actor, **When** the workspace renders, **Then** the primary CTA is download/open review pack. +2. **Given** no downloadable pack, **When** the workspace renders, **Then** the primary CTA is open review. +3. **Given** a pack exists but actor lacks download capability, **When** the workspace renders, **Then** no download URL appears and the status remains customer-safe. +4. **Given** generating, expired, failed, or missing pack state, **When** the workspace renders, **Then** the page maps it to preparing, expired, unavailable, or not available yet. + +### User Story 4 - Empty states explain filter and availability (Priority: P2) + +As an MSP operator, I want empty states to distinguish "no released reviews" from "active filter has no matching released reviews" so I do not misread workspace readiness. + +**Why this priority**: The page is workspace-wide, and filter-aware empty states are required to preserve the Spec 311 scope contract. + +**Independent Test**: Seed released reviews outside the active environment filter, render the workspace with filter query params, and assert the empty state says the filter has no matches and offers clear filter. + +**Acceptance Scenarios**: + +1. **Given** no released reviews exist in the workspace, **When** the workspace renders, **Then** it says no released customer reviews exist. +2. **Given** released reviews exist elsewhere in the workspace but the active environment filter matches none, **When** the workspace renders, **Then** it explains the active filter has no matches and offers clear filter. +3. **Given** a missing, preparing, expired, unavailable, or evidence-incomplete pack, **When** the review row/section renders, **Then** the status explanation is specific and customer-safe. + +## Edge Cases + +- No released reviews exist. +- Released reviews exist but active environment filter matches none. +- Released reviews exist globally but not for selected environment. +- Multiple released reviews share the same `published_at`. +- A published review has no `published_at` and must use repo-real fallback ordering. +- Review exists but `governance_package.decision_summary` is missing. +- Decision summary exists with zero awareness-required decisions. +- Decision summary exists with decisions requiring awareness. +- Decision evidence is incomplete or unavailable. +- Accepted risks exist but are not customer-safe or are redacted. +- Review Pack is missing. +- Review Pack is queued/generating. +- Review Pack is expired. +- Review Pack is failed/blocked/unavailable. +- Review Pack exists but actor lacks download capability. +- Evidence snapshot is missing, restricted, expired, partial, stale, or incomplete. +- Customer-safe detail handoff must preserve `customer_workspace=1` or repo-real equivalent. +- Existing Review Pack generated before Spec 308 lacks decision summary. +- Query uses legacy `tenant` alias. +- Query uses canonical `managed_environment_id`. +- Canonical route contains technical IDs as route params, but visible labels stay customer-safe. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: Customer Review Workspace MUST remain a workspace-wide hub. Environment selection is a page-level filter only and MUST NOT alter shell, sidebar, topbar, breadcrumb, or global environment context. +- **FR-002**: The page MUST make the latest released review visible when available, including customer-safe review label/title, managed environment label, review status, published date, governance package availability, review-pack status, and primary next action. +- **FR-003**: Latest released review selection MUST be deterministic: current workspace, actor entitlement, optional page-level environment filter, `published_at desc`, repo-real timestamp fallback if needed, `created_at`/`generated_at desc` fallback where repo-real, and final stable identifier desc. +- **FR-004**: Released reviews SHOULD be shown in a scanable customer-safe list/card/table format with status, environment, published date, decision awareness state, review-pack availability, and open/download action. +- **FR-005**: The page MUST surface customer-safe Decision Summary from released-review/governance-package truth, preferring Spec 308 content, then existing customer-safe summary fields, then unavailable/incomplete state. +- **FR-006**: The workspace MUST NOT recompute a second decision source of truth from raw evidence payloads. +- **FR-007**: Legacy released reviews or Review Packs without Spec 308 summary MUST render customer-safe unavailable/incomplete decision/evidence states instead of failing or exposing raw payloads. +- **FR-008**: Accepted risks MUST be shown only when customer-visible, accepted/approved according to existing decision truth, included in released review/governance-package scope, and not revoked/superseded/internal-only unless already marked customer-safe. +- **FR-009**: Evidence basis MUST be concise and customer-safe, using states `complete`, `no_awareness_required`, `incomplete`, `unavailable`, or `not_generated` as derived display labels. +- **FR-010**: Incomplete and unavailable evidence MUST NOT be rendered as "No decisions". +- **FR-011**: Review Pack availability MUST use existing `ReviewPack` status truth and file availability. Ready/available maps to Available, queued/generating maps to Preparing, missing maps to Not available yet, expired maps to Expired, failed/blocked/inaccessible maps to Unavailable, and evidence-incomplete review context maps to Evidence incomplete. +- **FR-012**: Each review context SHOULD have one dominant primary CTA: download/open pack when available and authorized; otherwise open review; in filtered empty state, clear environment filter. +- **FR-013**: Customer Review Workspace MUST NOT render generate, regenerate, expire, approve, reject, renew, revoke, close, delete, force delete, restore, rerun, or similar mutation/lifecycle actions. +- **FR-014**: Empty states MUST distinguish no released reviews, filtered no results, no awareness-required decisions, incomplete/unavailable decision evidence, pack missing, pack preparing, pack expired, and pack unavailable. +- **FR-015**: If active filter has no matching released reviews but workspace-wide released reviews exist for the actor, the empty state MUST explain that the filter has no matches and SHOULD show clear filter. +- **FR-016**: Rendered customer-safe HTML and hrefs MUST NOT contain visible raw IDs as labels, source fingerprints, raw OperationRun URLs, raw evidence JSON, provider payload dumps, debug details, internal reason families, `/admin/t` URLs, storage paths, policy internals, exception internals, or raw provider/customer payloads. +- **FR-017**: Canonical route parameters may contain repo-real IDs where existing routes require them, but those IDs MUST NOT appear as visible customer-facing labels, headings, badges, summaries, empty-state copy, or CTA text. +- **FR-018**: Existing authorization behavior MUST remain unchanged and server-side policies remain authoritative. +- **FR-019**: Existing Review Pack generation, status machine, storage contract, export format semantics, expire/regenerate behavior, and OperationRun lifecycle MUST remain unchanged. +- **FR-020**: New visible headings, badge labels, empty-state copy, and CTA labels SHOULD use localization keys where the surrounding files already use localization. + +### Non-Functional Requirements + +- **NFR-001**: Scope MUST remain minimal to Customer Review Workspace productization. +- **NFR-002**: Rendering MUST be deterministic for the same actor, workspace, filter, review, and pack state. +- **NFR-003**: The page MUST avoid broad unbounded scans by scoping to workspace and actor-entitled environments, applying the optional page filter, using existing relationships, and using existence queries for filtered/global empty distinction. +- **NFR-004**: Implementation MUST use Filament v5 / Livewire v4 patterns and MUST NOT introduce a new design framework. +- **NFR-005**: No new CSS, JS, images, icon packs, assets, tables, columns, models, migrations, persisted status families, or OperationRun types are allowed. +- **NFR-006**: Runtime tests MUST stay in focused Pest feature lanes and the existing bounded browser smoke when rendered UI changes. + +### UX Requirements + +- **UX-001**: Default view order SHOULD be latest review, Decision Summary, Review Pack availability / primary action, Accepted Risks, Evidence Basis, then secondary details. +- **UX-002**: The page MUST avoid button floods, raw diagnostic panels, equal-weight actions, noisy per-row technical links, and debug-like labels. +- **UX-003**: If environment filter is active, it MUST be visibly explained as a filter, not global context. +- **UX-004**: Customer-safe wording SHOULD prefer `Governance decisions requiring awareness`, `Accepted risks`, `Evidence basis`, `Review pack`, `Next action`, `No decisions require awareness`, `Decision evidence unavailable`, and `Review pack is being prepared`. +- **UX-005**: The latest/current review area SHOULD keep one visually dominant action. + +### Security / RBAC Requirements + +- **SEC-001**: Workspace membership remains required. +- **SEC-002**: Managed environment access remains required for filtered data and environment-bound details. +- **SEC-003**: Review and Review Pack policies remain authoritative. +- **SEC-004**: Hiding links in the customer-safe UI is not a security boundary; direct route access must still be denied where policies deny access. +- **SEC-005**: No lifecycle mutation actions are introduced in the customer-safe workspace. +- **SEC-006**: No cross-workspace or cross-environment data influences counts, summaries, links, or empty states. +- **SEC-007**: If a Review Pack exists but the actor lacks download capability, no download URL is rendered. +- **SEC-008**: Rendered customer-safe workspace HTML MUST NOT contain legacy `/admin/t` URLs. + +## Data / Truth Source Requirements + +| Truth | Owner | +|---|---| +| Decision truth | `FindingException`, `FindingExceptionDecision`, represented through released review summary | +| Review truth | `EnvironmentReview.summary`, `EnvironmentReview.status`, `published_at` | +| Evidence truth | `EvidenceSnapshot` and existing evidence summary/completeness fields | +| Review Pack truth | `ReviewPack.summary`, `ReviewPack.status`, file path/status and existing metadata | +| Execution truth | `OperationRun`, not displayed as customer-safe raw detail | + +Customer Review Workspace MUST derive display from existing review, pack, decision, and evidence truth and MUST NOT persist a second truth source. + +## Out of Scope + +- Shell/sidebar/topbar/route-scope work. +- Changes to `OperateHubShell`, `TenantPageCategory`, `NavigationScope`, `WorkspaceSidebarNavigation`. +- RBAC or policy changes unless a repo-real inconsistency is proven and documented before implementation. +- Migrations, new tables, new columns, new models. +- New enum/status/reason families or persisted status families. +- New OperationRun types, lifecycle, start UX, notifications, or raw links. +- Provider connection scope hardening. +- Billing/entitlement/commercial lifecycle work. +- Artifact lifecycle/retention work. +- AI-generated summaries. +- PSA/support-desk handoff. +- Product truth/docs drift cleanup. +- Review-pack artifact family expansion. +- Customer approval workflow. +- Canonical link/query cleanup beyond preserving existing customer-safe handoff. +- Redesign of Environment Review Resource or Review Pack Resource. + +## Acceptance Criteria + +- **AC-001**: Customer Review Workspace continues to render under the existing workspace-wide shell/sidebar/topbar contract from Spec 311. +- **AC-002**: Environment filter limits page data but does not change shell, sidebar, breadcrumb, or global context. +- **AC-003**: When a released review exists, a latest/current review section is visible. +- **AC-004**: Latest selection is deterministic by published timestamp and repo-real tie-breaker. +- **AC-005**: Decision Summary content is visible in customer-safe wording when present. +- **AC-006**: No-awareness state is distinct and calm. +- **AC-007**: Incomplete/unavailable evidence is not presented as "No decisions". +- **AC-008**: Accepted risks are visible in customer-safe form when available. +- **AC-009**: Evidence basis is visible without raw evidence payload. +- **AC-010**: Review Pack state is clear and customer-safe. +- **AC-011**: The primary next action is obvious and not competing with operator-only actions. +- **AC-012**: Rendered customer-safe page does not expose raw/internal/debug/OperationRun/fingerprint data. +- **AC-013**: Customer-safe surface does not introduce lifecycle mutation actions. +- **AC-014**: Existing customer review and review-pack authorization behavior remains green. +- **AC-015**: Opening a review uses canonical environment-bound detail route where applicable and preserves customer-safe origin. +- **AC-016**: Spec 311 shell/sidebar tests remain unaffected unless no shell/sidebar files are touched, in which case existing focused workspace assertions are sufficient. +- **AC-017**: Legacy pre-Spec-308 reviews/packs render customer-safe unavailable/incomplete states. +- **AC-018**: Pack access without capability renders no download URL and no policy/storage internals. +- **AC-019**: Visible labels use title/environment/published date/customer-safe labels rather than raw database identifiers. +- **AC-020**: Filtered empty state explains active environment filter and offers/shows clear-filter path when workspace-wide released reviews exist elsewhere. +- **AC-021**: Rendered customer-safe workspace HTML contains no `/admin/t` links. + +## Success Criteria + +- A stakeholder can identify the current released review and primary next action in one page. +- Decision Summary, accepted risks, evidence basis, and Review Pack status can be understood without raw diagnostics. +- Legacy reviews/packs without Spec 308 content remain safe and renderable. +- Focused tests cover latest selection, filter empties, decision states, accepted risks, evidence basis, pack states, authorization, leakage, links, and lifecycle-action absence. +- No application code outside the stated review workspace/handoff surfaces is required for implementation. + +## Test Strategy + +### Primary test files + +Use or extend repo-real equivalents: + +```text +apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php +apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php +apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php +apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php +apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php +apps/platform/tests/Feature/ReviewPack/EnvironmentReviewDerivedReviewPackTest.php +apps/platform/tests/Feature/ReviewPack/ReviewPackDownloadTest.php +apps/platform/tests/Feature/ReviewPack/ReviewPackRbacTest.php +``` + +Browser smoke is expected if rendered UI changes: + +```text +apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php +``` + +### Planned validation commands + +```bash +cd apps/platform && ./vendor/bin/sail artisan test --compact \ + tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php \ + tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php \ + tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php \ + tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php +``` + +```bash +cd apps/platform && ./vendor/bin/sail artisan test --compact \ + tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php \ + tests/Feature/EnvironmentReview/EnvironmentReviewCreationTest.php +``` + +```bash +cd apps/platform && ./vendor/bin/sail artisan test --compact \ + tests/Feature/ReviewPack/EnvironmentReviewDerivedReviewPackTest.php \ + tests/Feature/ReviewPack/ReviewPackDownloadTest.php \ + tests/Feature/ReviewPack/ReviewPackRbacTest.php +``` + +```bash +cd apps/platform && ./vendor/bin/sail artisan test --compact \ + tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php +``` + +```bash +cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent +git diff --check +``` + +Run Spec 311 regression lane only if shell/sidebar/topbar-adjacent files are unexpectedly touched: + +```bash +cd apps/platform && ./vendor/bin/sail artisan test --compact \ + tests/Feature/Filament/PanelNavigationSegregationTest.php \ + tests/Feature/Workspaces/GlobalContextShellContractTest.php \ + tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php +``` + +## Assumptions + +- Spec 311 is completed and should be treated as a dependency, not reopened. +- Repo-real released status is `EnvironmentReviewStatus::Published` / `published()`. +- Repo-real latest ordering currently uses `published_at desc`, `generated_at desc`, `id desc`; implementation may use `created_at desc` only if repo verification shows it is the correct fallback. +- Current workspace query returns entitled `ManagedEnvironment` rows with latest published review relationship; implementation may refine query/payload but must not broaden access. +- Spec 308 `governance_package.decision_summary` is the preferred customer-safe decision source. +- Review Pack downloadable state depends on existing `ReviewPackStatus::Ready`, file path truth, expiry, and existing capability/policy checks. +- Existing `customer_workspace=1` and `source_surface=customer_review_workspace` are the repo-real customer-safe detail handoff indicators unless implementation verifies a different current contract. + +## Risks + +- Old WIP patch could reintroduce shell/sidebar/topbar fixes. Mitigation: reject shell files and confirm no forbidden files changed. +- Customer-safe page could leak raw/internal data. Mitigation: rendered HTML and href leakage tests. +- Canonical route params could be confused with visible ID leakage. Mitigation: allow route params, forbid visible raw IDs as customer-facing meaning. +- Latest selection could be flaky. Mitigation: deterministic timestamp plus stable identifier tests. +- Page could become too busy. Mitigation: decision-first layout and one primary CTA. +- Review Pack states could drift from existing status truth. Mitigation: map from existing `ReviewPackStatus` and file/capability truth only. +- Authorization could accidentally change. Mitigation: existing RBAC/review-pack tests remain green. +- Legacy pre-Spec-308 summaries could fail or leak. Mitigation: unavailable/incomplete fallback tests. +- Browser smoke could be unavailable locally. Mitigation: document reason and compensate with rendered HTML assertions. + +## Current Customer Review Workspace Behavior + +Repo inspection found: + +- `CustomerReviewWorkspace` is an undiscovered Filament page with slug `reviews/workspace`, localized title/navigation, and table-based workspace rows. +- Workspace rows are `ManagedEnvironment` records scoped through `EnvironmentReviewRegisterService::customerWorkspaceTenantQuery()`. +- The query scopes to current workspace and actor-entitled environments, requires at least one published environment review, and eager-loads one latest published review ordered by `published_at desc`, `generated_at desc`, `id desc`. +- Page-level filter supports `managed_environment_id` and explicit query aliases `tenant` / `managed_environment_id`; remembered environment context is intentionally not defaulted. +- Existing row columns include environment name, governance package availability, review status, evidence status, next step, and open review link. +- Existing customer-safe detail handoff uses `customer_workspace=1`, `source_surface=customer_review_workspace`, and canonical `EnvironmentReviewResource::tenantScopedUrl()`. +- Existing intro Blade section provides customer-safe workspace intro, canonical note, and non-certification disclosure. + +## Missing Productization + +Current page behavior does not yet fully satisfy the provided 312 target: + +- No prominent latest/current review section above the list. +- No dedicated customer-safe Decision Summary section from Spec 308 summary truth. +- Accepted risks are mostly summarized for row logic and are not productized as a clear customer-safe section. +- Evidence basis is not yet explained as the required complete/no-awareness/incomplete/unavailable/not-generated state set. +- Review Pack status mapping needs clearer ready/preparing/missing/expired/unavailable/evidence-incomplete wording and CTA hierarchy. +- Filtered empty state still needs a workspace-wide existence distinction between no released reviews and active filter no matches. +- Legacy pre-Spec-308 summary fallback needs explicit unavailable/incomplete tests. +- Leakage guard needs broader rendered-HTML and href assertions. + +## Existing Safe Data Sources + +- Released review truth: `EnvironmentReview::published()` and `EnvironmentReviewStatus::Published`. +- Latest review query: `EnvironmentReviewRegisterService::customerWorkspaceTenantQuery()` and `latestPublishedQuery()`. +- Decision summary source: `EnvironmentReview.summary['governance_package']['decision_summary']`. +- Accepted risks source: `EnvironmentReview.summary['governance_package']['accepted_risks']` and existing released-review/risk acceptance summary fields. +- Evidence basis source: `EnvironmentReview.summary['governance_package']['evidence_basis_summary']`, `EnvironmentReview.summary['evidence_basis']`, `EvidenceSnapshot`, and completeness fields. +- Review Pack status source: `ReviewPackStatus` enum and `ReviewPack` file/expiry fields. +- Handoff source: existing `EnvironmentReviewResource`, `ReviewPackResource`, and `CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY`. + +## Existing Released Review Selection + +Repo-real selection currently orders published reviews by: + +1. `published_at desc` +2. `generated_at desc` +3. `id desc` + +Spec 312 implementation should keep that repo-real fallback unless code verification proves `created_at` is the safer fallback for a specific query. + +## Existing Review Pack Status Mapping + +Repo-real states: + +- `queued` +- `generating` +- `ready` +- `failed` +- `expired` + +Spec 312 display mapping: + +| Repo-real state | Customer-safe state | +|---|---| +| ready with downloadable file and authorization | Available | +| queued / generating | Preparing | +| null pack or missing file | Not available yet | +| expired or past expiry | Expired | +| failed / blocked / inaccessible / unauthorized | Unavailable | +| review or governance package evidence incomplete | Evidence incomplete | + +## Proposed Minimal Changes + +- Add/extend focused tests first in existing Customer Review Workspace, Review Pack, and Environment Review test families. +- Refine `CustomerReviewWorkspace` display payload and helpers only as needed. +- Update `customer-review-workspace.blade.php` to render a decision-first latest review and summary layout using existing Filament/Blade conventions. +- Add minimal localization keys in existing localization files. +- Preserve existing detail and pack handoffs; only touch `EnvironmentReviewResource` if customer-safe handoff cannot be preserved otherwise. +- Do not change Review Pack generation/status/storage semantics. +- Do not change shell/sidebar/topbar/RBAC/policies/migrations/assets. + +## Follow-up Candidates + +Not part of Spec 312: + +1. Provider Connection Scope Hardening +2. Canonical Link / Query Cleanup +3. Product Truth / Docs Drift Cleanup +4. Environment Resource Context Follow-through +5. Legacy Compatibility / Dead Code Retirement +6. Tenant Helper Naming Cleanup +7. Artifact Lifecycle & Retention +8. Commercial Entitlements / Billing-State Maturity +9. Customer-facing Localization Adoption v1 +10. Review Pack Backfill for pre-Spec-308 artifacts +11. Customer Approval Workflow +12. Review Pack Artifact Family Expansion + +## Done Definition + +Spec 312 is done when: + +- Customer Review Workspace provides a customer-safe released-review consumption experience. +- Latest review, decision summary, accepted risks, evidence basis, pack status, and one primary next action are visible. +- Latest released review selection is deterministic. +- Empty states are clear and filter-aware. +- Legacy reviews/packs without Spec 308 summary render safe unavailable/incomplete states. +- No raw/internal data leaks. +- No operator-only lifecycle actions appear. +- Existing authorization remains unchanged. +- Focused tests pass. +- Browser smoke passes if available; otherwise non-run reason is documented and rendered HTML assertions cover changed UI. +- Pint dirty passes. +- `git diff --check` passes. +- No shell/sidebar/topbar, RBAC, migration, OperationRun type, persisted status family, Review Pack generation semantics, or asset changes were introduced. diff --git a/specs/312-customer-review-workspace-v1-completion/tasks.md b/specs/312-customer-review-workspace-v1-completion/tasks.md new file mode 100644 index 00000000..62f6a8b8 --- /dev/null +++ b/specs/312-customer-review-workspace-v1-completion/tasks.md @@ -0,0 +1,173 @@ +# Tasks: Customer Review Workspace v1 Completion + +**Input**: `specs/312-customer-review-workspace-v1-completion/spec.md`, `specs/312-customer-review-workspace-v1-completion/plan.md` +**Prerequisites**: Spec 308 and Spec 311 are completed context; do not reopen their runtime scope. +**Tests**: Required. Use Pest 4 focused Feature tests and existing bounded Browser smoke when rendered UI changes. + +## Test Governance Checklist + +- [x] Lane assignment is named and is the narrowest sufficient proof for the changed behavior. +- [x] New or changed tests stay in existing focused Review, EnvironmentReview, ReviewPack, and Browser families. +- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default. +- [x] Planned validation commands cover the change without pulling unrelated suite cost. +- [x] The declared `standard-native-filament` / `shared-detail-family` / conditional `global-context-shell` profile is explicit. +- [x] Any material budget, baseline, trend, or escalation note is recorded in the active feature close-out. + +## Phase 1: Setup And Repo Verification + +**Purpose**: Confirm the branch, dependency specs, repo-real names, and forbidden boundaries before implementation. + +- [x] T001 Confirm current branch is `312-customer-review-workspace-v1-completion`. +- [x] T002 Confirm the working tree does not contain uncommitted Spec 311 shell/sidebar/topbar files. +- [x] T003 Read `specs/312-customer-review-workspace-v1-completion/spec.md`, `plan.md`, `tasks.md`, and `checklists/requirements.md`. +- [x] T004 Read `specs/308-decision-register-summary-review-pack/spec.md`, `plan.md`, and `tasks.md` as completed context only. +- [x] T005 Read `specs/311-workspace-environment-surface-scope-contract/spec.md`, `plan.md`, and `tasks.md` as completed shell/scope dependency only. +- [x] T006 Inspect any old WIP patch or stash only selectively, and list candidate Customer Review Workspace files before applying anything. +- [x] T007 Reject any old WIP changes touching `OperateHubShell`, `TenantPageCategory`, `NavigationScope`, `WorkspaceSidebarNavigation`, sidebar composition, topbar context, or route-scope contracts. +- [x] T008 Inspect `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`. +- [x] T009 Inspect `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php`. +- [x] T010 Inspect `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/EnvironmentReviews/EnvironmentReviewRegisterService.php` for repo-real latest review query behavior. +- [x] T011 Inspect `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Models/EnvironmentReview.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/EnvironmentReviewStatus.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Models/ReviewPack.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/ReviewPackStatus.php`. +- [x] T012 Confirm no migration, model, policy, OperationRun type, asset, package, or Review Pack generation semantics change is needed; stop and update spec/plan if false. + +## Phase 2: Tests First - Workspace, Latest Review, And Filters + +**Purpose**: Prove workspace-wide behavior, deterministic latest review, and filter-aware empty states before runtime changes. + +- [x] T013 [US1] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php` to assert a latest/current released review section is visible when released reviews exist. +- [x] T014 [US1] Add deterministic latest selection coverage for `published_at desc`, repo-real timestamp fallback (`generated_at desc` unless implementation verifies another field), and `id desc` tie-breaker. +- [x] T015 [US1] Add coverage proving draft/ready/failed/internal-only reviews cannot become latest customer-visible reviews. +- [x] T016 [US4] Add filtered empty-state coverage where active `managed_environment_id` or `tenant` filter has no matching released reviews but actor-visible released reviews exist elsewhere in the workspace. +- [x] T017 [US4] Add global empty-state coverage where no actor-visible released reviews exist anywhere in the workspace. +- [x] T018 [US4] Add assertions that environment filter labels/copy describe a page-level filter and do not imply global environment context. +- [x] T019 [US4] Add assertions that query parameters do not introduce `/admin/t` links or shell/sidebar/topbar-specific customer workspace behavior. + +## Phase 3: Tests First - Decision Summary, Accepted Risks, Evidence + +**Purpose**: Prove customer-safe summary states and no raw leakage. + +- [x] T020 [US2] Add decision summary `requires_awareness` rendered HTML assertions using existing Spec 308 `governance_package.decision_summary` content. +- [x] T021 [US2] Add decision summary `none` rendered HTML assertions proving "No decisions require awareness" is distinct from incomplete/unavailable evidence. +- [x] T022 [US2] Add decision summary `unavailable` assertions for legacy released review without Spec 308 summary content. +- [x] T023 [US2] Add decision summary `incomplete` assertions for review/evidence states where customer-safe decision evidence is incomplete. +- [x] T024 [US2] Add accepted-risk customer-safe display assertions for title/state/summary/review-due context where existing released-review truth provides it. +- [x] T025 [US2] Add accepted-risk redaction assertions proving internal workflow notes, approvers/owners when not customer-safe, raw exception payloads, and mutation controls do not appear. +- [x] T026 [US2] Add evidence basis assertions for `complete`, `no_awareness_required`, `incomplete`, `unavailable`, and `not_generated` derived display states where repo-real fixtures can support them. +- [x] T027 [US2] Add leakage assertions for raw evidence JSON, provider payload dumps, source fingerprints, internal reason-family labels, raw OperationRun URLs, storage paths, policy internals, and debug details. + +## Phase 4: Tests First - Review Pack Status, Access, Links, And Actions + +**Purpose**: Prove pack status mapping, CTA hierarchy, authorization preservation, and customer-safe handoffs. + +- [x] T028 [US3] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php` for ready/downloadable pack showing Available and the dominant download/open CTA when actor is authorized. +- [x] T029 [US3] Add queued/generating pack coverage mapping to Preparing. +- [x] T030 [US3] Add missing/null pack or missing-file coverage mapping to Not available yet. +- [x] T031 [US3] Add expired pack coverage mapping to Expired. +- [x] T032 [US3] Add failed/blocked/unavailable pack coverage if repo-real status exists, mapping to Unavailable. +- [x] T033 [US3] Add evidence-incomplete review/pack context coverage mapping to Evidence incomplete without changing Review Pack status truth. +- [x] T034 [US3] Add pack exists but actor lacks download capability coverage proving no download URL is rendered. +- [x] T035 [US3] Add CTA hierarchy assertions: download/open pack dominates when available and authorized, otherwise open review, filtered empty state uses clear filter. +- [x] T036 [US3] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` for canonical environment-bound review detail handoff and preservation of `customer_workspace=1` / source context. +- [x] T037 [US3] Add no lifecycle action assertions for approve, reject, renew, revoke, close, delete, force delete, restore, rerun, generate, regenerate, and expire. +- [x] T038 [US3] Add no `/admin/t` link assertions over rendered workspace HTML and generated hrefs. +- [x] T039 [US3] Add visible-label assertions proving raw database identifiers are not used as customer-facing labels, headings, badge text, summaries, empty-state copy, or CTA text. + +## Phase 5: Runtime - Page Payload And Query Behavior + +**Purpose**: Implement only the minimal workspace page shaping required by the failing tests. + +- [x] T040 [US1] Build or refine a private page payload in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` for latest released review, decision summary, accepted risks, evidence basis, pack status, and primary CTA. +- [x] T041 [US1] Keep latest released review selection scoped to current workspace, actor-entitled environments, optional page-level environment filter, and deterministic repo-real ordering. +- [x] T042 [US4] Implement filtered empty-state distinction using scoped existence queries rather than unbounded scans. +- [x] T043 [US4] Preserve existing explicit `tenant` and `managed_environment_id` query prefilter behavior as page-level filters only. +- [x] T044 [US1] Avoid broad loading of raw evidence payloads; eager-load only existing review, evidence summary, and current pack relationships needed for display. + +## Phase 6: Runtime - Customer-safe Rendering + +**Purpose**: Render the customer-safe consumption surface using existing Filament/Blade conventions. + +- [x] T045 [US1] Update `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php` to render the latest/current review section before the list. +- [x] T046 [US2] Render Decision Summary from existing released-review/governance-package summary content and customer-safe fallback states. +- [x] T047 [US2] Render accepted risks only from customer-safe released-review truth; do not query or display internal workflow history as customer-facing content. +- [x] T048 [US2] Render evidence basis with clear complete/no-awareness/incomplete/unavailable/not-generated states. +- [x] T049 [US3] Render review-pack availability with customer-safe state labels and explanations based on existing Review Pack truth. +- [x] T050 [US3] Render exactly one visually dominant primary CTA per current review context. +- [x] T051 [US3] Hide review-pack download/open URL when actor lacks existing capability/policy. +- [x] T052 [US4] Render filter-aware empty states with clear filter copy and a single clear-filter CTA when appropriate. +- [x] T053 [US2] Keep raw/internal/support diagnostics absent from the default workspace page. +- [x] T054 [US2] Use existing Filament/Blade visual language; do not add CSS, JS, images, icon packs, or ad-hoc styling systems. + +## Phase 7: Runtime - Localization And Handoff Preservation + +**Purpose**: Add minimal copy keys and preserve existing route/link contracts. + +- [x] T055 Add minimal English localization keys in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/en/localization.php`. +- [x] T056 Add matching German localization keys in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/de/localization.php`. +- [x] T057 Preserve existing customer-safe review detail handoff query parameters in links from Customer Review Workspace. +- [x] T058 Preserve existing Review Pack download/open authorization and route behavior; do not touch generation/regeneration/expire semantics. +- [x] T059 Only if tests prove unavoidable, update `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EnvironmentReviewResource.php` for customer-safe handoff without redesigning the resource. +- [x] T060 Only if tests prove unavoidable, update `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/ReviewPackResource.php` for customer-safe download/open handoff without redesigning the resource. + +## Phase 8: Guardrails + +**Purpose**: Confirm the implementation stayed inside Spec 312. + +- [x] T061 Confirm no shell/sidebar/topbar files changed: `OperateHubShell`, `TenantPageCategory`, `NavigationScope`, `WorkspaceSidebarNavigation`. +- [x] T062 Confirm no RBAC/policy files changed unless a repo-real inconsistency was documented before implementation. +- [x] T063 Confirm no migrations, tables, columns, models, OperationRun types, persisted status families, or enum/status families were added. +- [x] T064 Confirm no assets, packages, CSS, JS, images, or icon packs were added. +- [x] T065 Confirm no Review Pack generation, storage, status machine, expire/regenerate, or OperationRun lifecycle semantics changed. +- [x] T066 Confirm no operator-only actions appear on the customer-safe workspace. +- [x] T067 Confirm no pack mutation actions appear on the customer-safe workspace. +- [x] T068 Confirm no `/admin/t` links appear in rendered customer-safe workspace HTML. +- [x] T069 Confirm no raw payload, fingerprint, debug, OperationRun, storage path, policy, or provider-payload leakage appears. +- [x] T070 Confirm no cross-workspace or cross-environment data influences counts, summaries, links, or empty states. + +## Phase 9: Validation + +**Purpose**: Run focused lanes and record any non-run reason. + +- [x] T071 Run Customer Review Workspace lane: + `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` +- [x] T072 Run Environment Review lane: + `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php tests/Feature/EnvironmentReview/EnvironmentReviewCreationTest.php` +- [x] T073 Run Review Pack lane: + `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/EnvironmentReviewDerivedReviewPackTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/ReviewPack/ReviewPackRbacTest.php` +- [x] T074 Run Spec 311 regression lane only if shell/sidebar/topbar-adjacent files were touched: + `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/PanelNavigationSegregationTest.php tests/Feature/Workspaces/GlobalContextShellContractTest.php tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php` +- [x] T075 Run browser smoke if available: + `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php` +- [x] T076 Run dirty Pint: + `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` +- [x] T077 Run `git diff --check`. + +## Phase 10: Close-out + +**Purpose**: Record implementation evidence and non-goal confirmations. + +- [x] T078 Record changed files in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/312-customer-review-workspace-v1-completion/plan.md`. +- [x] T079 Record fulfilled acceptance criteria. +- [x] T080 Record tests run with outcomes. +- [x] T081 Record tests not run and exact reason. +- [x] T082 Record browser smoke result or non-run reason plus rendered HTML compensation. +- [x] T083 Confirm no migration, no RBAC/policy change, no asset, no OperationRun type, no new persistence/status family. +- [x] T084 Confirm no Shell/Sidebar/Topbar work. +- [x] T085 Confirm no ReviewPack generation semantics change. +- [x] T086 Record leakage guard confirmation. +- [x] T087 Record remaining gaps and follow-up candidates. + +## Explicit Non-goal Checklist + +- [x] No shell/sidebar/topbar work. +- [x] No `OperateHubShell`, `TenantPageCategory`, `NavigationScope`, or `WorkspaceSidebarNavigation` changes. +- [x] No RBAC or policy changes unless documented as unavoidable before implementation. +- [x] No migrations, models, tables, columns, persisted status families, or OperationRun types. +- [x] No provider connection scope hardening. +- [x] No billing/entitlement work. +- [x] No artifact lifecycle/retention work. +- [x] No AI summary generation. +- [x] No PSA/support handoff. +- [x] No broad localization adoption. +- [x] No Review Pack generation/status/storage semantics change. +- [x] No customer approval workflow. +- [x] No canonical link/query cleanup beyond preserving existing handoff.