feat: finalize customer review workspace consumption (342)
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 4m1s
@ -10,6 +10,7 @@
|
||||
use App\Filament\Resources\EvidenceSnapshotResource;
|
||||
use App\Models\EnvironmentReview;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\Finding;
|
||||
use App\Models\FindingException;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\OperationRun;
|
||||
@ -49,6 +50,7 @@
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use UnitEnum;
|
||||
@ -310,7 +312,10 @@ public function latestReviewConsumptionPayload(): ?array
|
||||
$decision = $this->decisionSummaryForReview($review);
|
||||
$acceptedRisks = $this->acceptedRisksForReview($review);
|
||||
$hasAcceptedRiskFollowUp = $this->acceptedRiskFollowUpRequiredForReview($review);
|
||||
|
||||
$findingPanel = $this->findingPanelForReview($tenant);
|
||||
$canShowSecondaryReviewLink = $downloadUrl !== null
|
||||
&& ! $hasAcceptedRiskFollowUp
|
||||
&& $findingPanel['open_count'] === 0;
|
||||
$evidencePath = $this->evidencePathForReview($review, $tenant, $packageAvailability, $downloadUrl, $decision, $acceptedRisks);
|
||||
|
||||
return [
|
||||
@ -336,21 +341,20 @@ public function latestReviewConsumptionPayload(): ?array
|
||||
'primary_action_icon' => $downloadUrl !== null
|
||||
? 'heroicon-o-arrow-down-tray'
|
||||
: 'heroicon-o-arrow-top-right-on-square',
|
||||
'secondary_action_label' => $downloadUrl !== null
|
||||
? ($hasAcceptedRiskFollowUp ? __('localization.review.download_review_pack') : __('localization.review.open_review'))
|
||||
'secondary_action_label' => $canShowSecondaryReviewLink
|
||||
? __('localization.review.open_review')
|
||||
: null,
|
||||
'secondary_action_url' => $downloadUrl !== null
|
||||
? ($hasAcceptedRiskFollowUp ? $downloadUrl : $reviewUrl)
|
||||
'secondary_action_url' => $canShowSecondaryReviewLink
|
||||
? $reviewUrl
|
||||
: null,
|
||||
'secondary_action_icon' => $downloadUrl !== null && $hasAcceptedRiskFollowUp
|
||||
? 'heroicon-o-arrow-down-tray'
|
||||
: 'heroicon-o-arrow-top-right-on-square',
|
||||
'secondary_action_icon' => 'heroicon-o-arrow-top-right-on-square',
|
||||
],
|
||||
'readiness' => $this->reviewReadinessForTenant($tenant, $review, $packageAvailability, $downloadUrl, $reviewUrl),
|
||||
'readiness_dimensions' => $this->readinessDimensionPayloads($tenant, $review, $packageAvailability),
|
||||
'readiness_flow' => $this->reviewConsumptionFlowForReview($tenant, $review, $packageAvailability, $downloadUrl),
|
||||
'finding_panel' => $findingPanel,
|
||||
'decision' => $decision,
|
||||
'accepted_risks' => $acceptedRisks,
|
||||
'accepted_risk_panel' => $this->acceptedRiskPanelForReview($review),
|
||||
'accepted_risk_panel' => $this->acceptedRiskPanelForReview($review, $tenant),
|
||||
'evidence_basis' => $this->evidenceBasisForReview($review, $packageAvailability),
|
||||
'evidence_path' => $evidencePath,
|
||||
'aside_evidence_path' => $this->asideEvidencePath($evidencePath),
|
||||
@ -399,13 +403,20 @@ private function reviewReadinessForTenant(
|
||||
?string $reviewUrl,
|
||||
): array {
|
||||
$hasAcceptedRiskFollowUp = $this->acceptedRiskFollowUpRequiredForReview($review);
|
||||
$findingPanel = $this->findingPanelForReview($tenant);
|
||||
$hasFindingFollowUp = $findingPanel['open_count'] > 0;
|
||||
$hasReadyPackage = $packageAvailability['state'] === 'available' && $downloadUrl !== null;
|
||||
$hasAvailableEvidence = $this->evidenceStatusState($tenant) === 'available';
|
||||
$hasMappedReviewData = $this->primaryControlSummary($tenant) !== null;
|
||||
$hasCustomerSafeProof = $this->primaryControlSummary($tenant) !== null
|
||||
&& $this->evidenceStatusState($tenant) === 'available';
|
||||
&& $hasAvailableEvidence;
|
||||
$isReadyToShare = ! $hasAcceptedRiskFollowUp
|
||||
&& ! $this->workspaceReviewNeedsAttention($tenant)
|
||||
&& $hasReadyPackage;
|
||||
$isShareableWithFollowUp = $hasAcceptedRiskFollowUp && $hasReadyPackage && $hasCustomerSafeProof;
|
||||
&& $findingPanel['open_count'] === 0
|
||||
&& $hasReadyPackage
|
||||
&& $hasAvailableEvidence
|
||||
&& $hasMappedReviewData;
|
||||
$isShareableWithFollowUp = $hasAcceptedRiskFollowUp && ! $hasFindingFollowUp && $hasReadyPackage && $hasCustomerSafeProof;
|
||||
$primaryActionShouldOpenReview = $hasFindingFollowUp || $isShareableWithFollowUp;
|
||||
|
||||
return [
|
||||
'question' => __('localization.review.is_review_ready_to_share'),
|
||||
@ -421,7 +432,10 @@ private function reviewReadinessForTenant(
|
||||
},
|
||||
'reason' => match (true) {
|
||||
$isReadyToShare => __('localization.review.ready_to_share_reason'),
|
||||
$isShareableWithFollowUp => __('localization.review.shareable_with_follow_up_reason'),
|
||||
$isShareableWithFollowUp => __('localization.review.accepted_risk_follow_up_required_reason'),
|
||||
$hasFindingFollowUp => __('localization.review.findings_follow_up_required_reason', [
|
||||
'summary' => $findingPanel['summary'],
|
||||
]),
|
||||
default => $this->customerSafeText(
|
||||
$this->reviewOutcomeDescription($tenant) ?? $packageAvailability['description'],
|
||||
__('localization.review.follow_up_required_before_sharing_reason'),
|
||||
@ -429,16 +443,17 @@ private function reviewReadinessForTenant(
|
||||
},
|
||||
'impact' => match (true) {
|
||||
$isReadyToShare => __('localization.review.ready_to_share_impact'),
|
||||
$isShareableWithFollowUp => __('localization.review.shareable_with_follow_up_impact'),
|
||||
$isShareableWithFollowUp => __('localization.review.accepted_risk_follow_up_required_impact'),
|
||||
$hasFindingFollowUp => __('localization.review.findings_follow_up_required_impact'),
|
||||
default => __('localization.review.follow_up_required_before_sharing_impact'),
|
||||
},
|
||||
'primary_action_label' => $isShareableWithFollowUp
|
||||
'primary_action_label' => $primaryActionShouldOpenReview
|
||||
? __('localization.review.open_review')
|
||||
: ($downloadUrl !== null ? __('localization.review.download_review_pack') : __('localization.review.open_latest_review')),
|
||||
'primary_action_url' => $isShareableWithFollowUp
|
||||
'primary_action_url' => $primaryActionShouldOpenReview
|
||||
? ($reviewUrl ?? $downloadUrl)
|
||||
: ($downloadUrl ?? $reviewUrl),
|
||||
'primary_action_icon' => $isShareableWithFollowUp || $downloadUrl === null
|
||||
'primary_action_icon' => $primaryActionShouldOpenReview || $downloadUrl === null
|
||||
? 'heroicon-o-arrow-top-right-on-square'
|
||||
: 'heroicon-o-arrow-down-tray',
|
||||
];
|
||||
@ -446,42 +461,83 @@ private function reviewReadinessForTenant(
|
||||
|
||||
/**
|
||||
* @param array{state:string,label:string,description:string} $packageAvailability
|
||||
* @return list<array{title:string,label:string,color:string,description:string}>
|
||||
* @return list<array{title:string,label:string,color:string,description:string,is_current:bool}>
|
||||
*/
|
||||
private function readinessDimensionPayloads(
|
||||
private function reviewConsumptionFlowForReview(
|
||||
ManagedEnvironment $tenant,
|
||||
EnvironmentReview $review,
|
||||
array $packageAvailability,
|
||||
?string $downloadUrl,
|
||||
): array {
|
||||
$acceptedRisk = $this->acceptedRiskDimensionForReview($review);
|
||||
$evidenceState = $this->evidenceStatusState($tenant);
|
||||
$findingPanel = $this->findingPanelForReview($tenant);
|
||||
$acceptedRisk = $this->acceptedRiskDimensionForReview($review, $tenant);
|
||||
$hasAcceptedRiskFollowUp = $this->acceptedRiskFollowUpRequiredForReview($review);
|
||||
$hasReadyPackage = $packageAvailability['state'] === 'available' && $downloadUrl !== null;
|
||||
$hasMappedReviewData = $this->primaryControlSummary($tenant) !== null;
|
||||
$hasBlockingAttention = $findingPanel['open_count'] > 0
|
||||
|| $hasAcceptedRiskFollowUp
|
||||
|| $evidenceState !== 'available'
|
||||
|| ! $hasMappedReviewData;
|
||||
|
||||
$customerOutputLabel = match (true) {
|
||||
$hasReadyPackage && ! $hasBlockingAttention => __('localization.review.ready'),
|
||||
$hasReadyPackage => __('localization.review.needs_review'),
|
||||
default => __('localization.review.not_ready'),
|
||||
};
|
||||
$customerOutputColor = match (true) {
|
||||
$hasReadyPackage && ! $hasBlockingAttention => 'success',
|
||||
$hasReadyPackage => 'warning',
|
||||
default => 'gray',
|
||||
};
|
||||
$customerOutputDescription = match (true) {
|
||||
$hasReadyPackage && ! $hasBlockingAttention => __('localization.review.customer_output_ready_description'),
|
||||
$hasReadyPackage => __('localization.review.customer_output_needs_review_description'),
|
||||
default => __('localization.review.customer_output_not_ready_description'),
|
||||
};
|
||||
|
||||
return [
|
||||
[
|
||||
'title' => __('localization.review.readiness'),
|
||||
'label' => $this->latestReviewStateLabel($tenant),
|
||||
'color' => $this->latestReviewStateColor($tenant),
|
||||
'description' => $this->workspaceReviewNeedsAttention($tenant)
|
||||
? __('localization.review.readiness_dimension_follow_up_description')
|
||||
: __('localization.review.readiness_dimension_ready_description'),
|
||||
'title' => __('localization.review.review_data'),
|
||||
'label' => __('localization.review.available'),
|
||||
'color' => 'success',
|
||||
'description' => __('localization.review.review_data_available_description'),
|
||||
'is_current' => false,
|
||||
],
|
||||
[
|
||||
'title' => __('localization.review.evidence'),
|
||||
'label' => $this->evidenceStatusLabelForState($evidenceState),
|
||||
'color' => $this->evidenceStatusColorForState($evidenceState),
|
||||
'description' => $this->evidenceDimensionDescription($evidenceState),
|
||||
'is_current' => $evidenceState !== 'available',
|
||||
],
|
||||
[
|
||||
'title' => __('localization.review.accepted_risk_status'),
|
||||
'title' => __('localization.review.findings_triaged'),
|
||||
'label' => $findingPanel['status_label'],
|
||||
'color' => $findingPanel['status_color'],
|
||||
'description' => $findingPanel['summary'],
|
||||
'is_current' => $findingPanel['open_count'] > 0,
|
||||
],
|
||||
[
|
||||
'title' => __('localization.review.accepted_risks_reviewed'),
|
||||
'label' => $acceptedRisk['label'],
|
||||
'color' => $acceptedRisk['color'],
|
||||
'description' => $acceptedRisk['description'],
|
||||
'is_current' => $this->acceptedRiskFollowUpRequiredForReview($review),
|
||||
],
|
||||
[
|
||||
'title' => __('localization.review.review_pack'),
|
||||
'label' => $packageAvailability['label'],
|
||||
'color' => $this->governancePackageAvailabilityColor($tenant),
|
||||
'description' => $this->reviewPackDimensionDescription($packageAvailability),
|
||||
'is_current' => $packageAvailability['state'] !== 'available',
|
||||
],
|
||||
[
|
||||
'title' => __('localization.review.customer_output'),
|
||||
'label' => $customerOutputLabel,
|
||||
'color' => $customerOutputColor,
|
||||
'description' => $customerOutputDescription,
|
||||
'is_current' => ! ($hasReadyPackage && ! $hasBlockingAttention),
|
||||
],
|
||||
];
|
||||
}
|
||||
@ -514,9 +570,11 @@ private function reviewPackDimensionDescription(array $packageAvailability): str
|
||||
/**
|
||||
* @return array{label:string,color:string,description:string}
|
||||
*/
|
||||
private function acceptedRiskDimensionForReview(EnvironmentReview $review): array
|
||||
private function acceptedRiskDimensionForReview(EnvironmentReview $review, ManagedEnvironment $tenant): array
|
||||
{
|
||||
$acceptedRisks = $this->acceptedRisksForReview($review);
|
||||
$exceptionCount = $this->acceptedRiskExceptionsForTenant($tenant)->count();
|
||||
$acceptedRiskCount = max($acceptedRisks['count'], $exceptionCount);
|
||||
$hasFollowUp = $this->acceptedRiskFollowUpRequiredForReview($review);
|
||||
|
||||
if ($hasFollowUp) {
|
||||
@ -527,7 +585,7 @@ private function acceptedRiskDimensionForReview(EnvironmentReview $review): arra
|
||||
];
|
||||
}
|
||||
|
||||
if ($acceptedRisks['count'] === 0) {
|
||||
if ($acceptedRiskCount === 0) {
|
||||
return [
|
||||
'label' => __('localization.review.accepted_risk_no_action_needed'),
|
||||
'color' => 'gray',
|
||||
@ -536,7 +594,7 @@ private function acceptedRiskDimensionForReview(EnvironmentReview $review): arra
|
||||
}
|
||||
|
||||
return [
|
||||
'label' => __('localization.review.accepted_risk_on_record', ['count' => $acceptedRisks['count']]),
|
||||
'label' => __('localization.review.accepted_risk_on_record', ['count' => $acceptedRiskCount]),
|
||||
'color' => 'info',
|
||||
'description' => __('localization.review.accepted_risk_dimension_on_record_description'),
|
||||
];
|
||||
@ -556,13 +614,27 @@ private function acceptedRiskFollowUpRequiredForReview(EnvironmentReview $review
|
||||
$decisionEntries = collect($package['governance_decisions'] ?? [])
|
||||
->filter(static fn (mixed $entry): bool => is_array($entry));
|
||||
|
||||
return $acceptedEntries
|
||||
if ($acceptedEntries
|
||||
->merge($decisionEntries)
|
||||
->contains(static fn (array $entry): bool => in_array(
|
||||
(string) ($entry['governance_state'] ?? ''),
|
||||
self::ACCEPTED_RISK_FOLLOW_UP_STATES,
|
||||
true,
|
||||
));
|
||||
))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return FindingException::query()
|
||||
->where('workspace_id', (int) $review->workspace_id)
|
||||
->where('managed_environment_id', (int) $review->managed_environment_id)
|
||||
->current()
|
||||
->whereIn('current_validity_state', [
|
||||
FindingException::VALIDITY_EXPIRING,
|
||||
FindingException::VALIDITY_EXPIRED,
|
||||
FindingException::VALIDITY_REVOKED,
|
||||
FindingException::VALIDITY_MISSING_SUPPORT,
|
||||
])
|
||||
->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -687,8 +759,8 @@ private function reviewPackProofForReview(array $packageAvailability, ?string $d
|
||||
default => 'gray',
|
||||
},
|
||||
'description' => $packageAvailability['description'],
|
||||
'action_label' => $downloadUrl !== null ? __('localization.review.download_review_pack') : null,
|
||||
'action_url' => $downloadUrl,
|
||||
'action_label' => null,
|
||||
'action_url' => null,
|
||||
];
|
||||
}
|
||||
|
||||
@ -742,12 +814,18 @@ private function operationProofForReview(EnvironmentReview $review, ManagedEnvir
|
||||
])->first(fn (mixed $candidate): bool => $candidate instanceof OperationRun);
|
||||
|
||||
if ($run instanceof OperationRun) {
|
||||
$initiator = is_string($run->initiator_name) && trim($run->initiator_name) !== ''
|
||||
? trim($run->initiator_name)
|
||||
: null;
|
||||
|
||||
return [
|
||||
'key' => 'operation_proof',
|
||||
'title' => __('localization.review.operation_proof'),
|
||||
'label' => __('localization.review.available'),
|
||||
'color' => 'info',
|
||||
'description' => __('localization.review.operation_proof_available_description'),
|
||||
'description' => $initiator === null
|
||||
? __('localization.review.operation_proof_available_description')
|
||||
: __('localization.review.operation_proof_available_with_initiator_description', ['initiator' => $initiator]),
|
||||
'action_label' => OperationRunLinks::openLabel(),
|
||||
'action_url' => OperationRunLinks::tenantlessView($run),
|
||||
];
|
||||
@ -780,28 +858,191 @@ private function exportArtifactProofForReview(array $packageAvailability, ?strin
|
||||
'description' => $downloadUrl !== null
|
||||
? __('localization.review.export_artifact_available_description')
|
||||
: __('localization.review.export_artifact_unavailable_description'),
|
||||
'action_label' => $downloadUrl !== null ? __('localization.review.download_review_pack') : null,
|
||||
'action_url' => $downloadUrl,
|
||||
'action_label' => null,
|
||||
'action_url' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{summary_label:string,summary_color:string,items:list<array{label:string,value:string,color:string}>}
|
||||
* @return array{status_label:string,status_color:string,summary:string,total_count:int,open_count:int,high_impact_count:int,items:list<array{label:string,value:string,color:string}>}
|
||||
*/
|
||||
private function acceptedRiskPanelForReview(EnvironmentReview $review): array
|
||||
private function findingPanelForReview(ManagedEnvironment $tenant): array
|
||||
{
|
||||
$baseQuery = Finding::query()
|
||||
->where('workspace_id', (int) $tenant->workspace_id)
|
||||
->where('managed_environment_id', (int) $tenant->getKey());
|
||||
|
||||
$total = (clone $baseQuery)->count();
|
||||
$open = (clone $baseQuery)
|
||||
->whereIn('status', Finding::openStatusesForQuery())
|
||||
->count();
|
||||
$highImpact = (clone $baseQuery)
|
||||
->whereIn('status', Finding::openStatusesForQuery())
|
||||
->whereIn('severity', Finding::highSeverityValues())
|
||||
->count();
|
||||
$accepted = (clone $baseQuery)
|
||||
->where('status', Finding::STATUS_RISK_ACCEPTED)
|
||||
->count();
|
||||
|
||||
$summary = match (true) {
|
||||
$open > 0 && $highImpact > 0 => __('localization.review.findings_high_impact_summary', [
|
||||
'open' => trans_choice('localization.review.findings_open_attention_count', $open, ['count' => $open]),
|
||||
'high' => trans_choice('localization.review.findings_high_impact_count_summary', $highImpact, ['count' => $highImpact]),
|
||||
]),
|
||||
$open > 0 => trans_choice('localization.review.findings_open_summary', $open, ['count' => $open]),
|
||||
$total > 0 => __('localization.review.findings_no_open_summary', ['total' => $total]),
|
||||
default => __('localization.review.findings_none_action_summary'),
|
||||
};
|
||||
|
||||
return [
|
||||
'status_label' => $open > 0
|
||||
? __('localization.review.needs_review')
|
||||
: __('localization.review.no_action_needed'),
|
||||
'status_color' => match (true) {
|
||||
$highImpact > 0 => 'danger',
|
||||
$open > 0 => 'warning',
|
||||
default => 'success',
|
||||
},
|
||||
'summary' => $summary,
|
||||
'total_count' => $total,
|
||||
'open_count' => $open,
|
||||
'high_impact_count' => $highImpact,
|
||||
'items' => [
|
||||
[
|
||||
'label' => __('localization.review.findings_total'),
|
||||
'value' => (string) $total,
|
||||
'color' => $total > 0 ? 'info' : 'gray',
|
||||
],
|
||||
[
|
||||
'label' => __('localization.review.findings_open'),
|
||||
'value' => (string) $open,
|
||||
'color' => $open > 0 ? 'warning' : 'gray',
|
||||
],
|
||||
[
|
||||
'label' => __('localization.review.findings_high_impact'),
|
||||
'value' => (string) $highImpact,
|
||||
'color' => $highImpact > 0 ? 'danger' : 'gray',
|
||||
],
|
||||
[
|
||||
'label' => __('localization.review.accepted_risks'),
|
||||
'value' => (string) $accepted,
|
||||
'color' => $accepted > 0 ? 'info' : 'gray',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, FindingException>
|
||||
*/
|
||||
private function acceptedRiskExceptionsForTenant(ManagedEnvironment $tenant): Collection
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user instanceof User || ! $user->can(Capabilities::FINDING_EXCEPTION_VIEW, $tenant)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return FindingException::query()
|
||||
->with(['owner', 'approver', 'currentDecision'])
|
||||
->where('workspace_id', (int) $tenant->workspace_id)
|
||||
->where('managed_environment_id', (int) $tenant->getKey())
|
||||
->current()
|
||||
->orderByRaw("case when current_validity_state in ('expiring', 'expired', 'missing_support') then 0 else 1 end")
|
||||
->latest('approved_at')
|
||||
->latest('requested_at')
|
||||
->latest('id')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, FindingException> $exceptions
|
||||
* @return list<array{label:string,value:string,color:string}>
|
||||
*/
|
||||
private function acceptedRiskDetailRows(Collection $exceptions): array
|
||||
{
|
||||
if ($exceptions->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$owner = $exceptions
|
||||
->map(static fn (FindingException $exception): ?string => $exception->owner?->name ?? $exception->approver?->name)
|
||||
->filter(static fn (?string $name): bool => is_string($name) && trim($name) !== '')
|
||||
->first();
|
||||
$ownedException = $exceptions
|
||||
->first(static fn (FindingException $exception): bool => $exception->owner?->name !== null || $exception->approver?->name !== null);
|
||||
$reviewDate = $exceptions
|
||||
->map(static fn (FindingException $exception): mixed => $exception->review_due_at ?? $exception->expires_at)
|
||||
->first(static fn (mixed $date): bool => $date instanceof \DateTimeInterface);
|
||||
$hasMissingReviewDate = $exceptions
|
||||
->contains(static fn (FindingException $exception): bool => $exception->review_due_at === null && $exception->expires_at === null);
|
||||
$reason = $ownedException instanceof FindingException && is_string($ownedException->request_reason) && trim($ownedException->request_reason) !== ''
|
||||
? trim($ownedException->request_reason)
|
||||
: $exceptions
|
||||
->map(static fn (FindingException $exception): ?string => is_string($exception->request_reason) ? trim($exception->request_reason) : null)
|
||||
->filter(static fn (?string $value): bool => is_string($value) && $value !== '')
|
||||
->first();
|
||||
|
||||
$rows = [
|
||||
[
|
||||
'label' => __('localization.review.accepted_risk_owner'),
|
||||
'value' => is_string($owner) ? $owner : __('localization.review.not_recorded'),
|
||||
'color' => is_string($owner) ? 'info' : 'gray',
|
||||
],
|
||||
];
|
||||
|
||||
if ($reviewDate instanceof \DateTimeInterface) {
|
||||
$rows[] = [
|
||||
'label' => __('localization.review.accepted_risk_next_review'),
|
||||
'value' => $reviewDate->format('Y-m-d'),
|
||||
'color' => 'info',
|
||||
];
|
||||
}
|
||||
|
||||
if ($hasMissingReviewDate) {
|
||||
$rows[] = [
|
||||
'label' => __('localization.review.accepted_risk_next_review'),
|
||||
'value' => __('localization.review.review_date_not_recorded'),
|
||||
'color' => 'warning',
|
||||
];
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'label' => __('localization.review.accepted_risk_rationale'),
|
||||
'value' => is_string($reason) ? Str::limit($reason, 160) : __('localization.review.not_recorded'),
|
||||
'color' => is_string($reason) ? 'info' : 'gray',
|
||||
];
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{summary_label:string,summary_color:string,items:list<array{label:string,value:string,color:string}>,detail_rows:list<array{label:string,value:string,color:string}>}
|
||||
*/
|
||||
private function acceptedRiskPanelForReview(EnvironmentReview $review, ManagedEnvironment $tenant): array
|
||||
{
|
||||
$package = $this->governancePackageSummaryForReview($review);
|
||||
$hasFollowUp = $this->acceptedRiskFollowUpRequiredForReview($review);
|
||||
$exceptions = $this->acceptedRiskExceptionsForTenant($tenant);
|
||||
$acceptedEntries = collect($package['accepted_risks'] ?? [])
|
||||
->filter(static fn (mixed $entry): bool => is_array($entry));
|
||||
$decisionEntries = collect($package['governance_decisions'] ?? [])
|
||||
->filter(static fn (mixed $entry): bool => is_array($entry));
|
||||
$allEntries = $acceptedEntries->merge($decisionEntries);
|
||||
|
||||
$total = $allEntries->count();
|
||||
$expiring = $allEntries->where('governance_state', 'expiring_exception')->count();
|
||||
$expired = $allEntries->where('governance_state', 'expired_exception')->count();
|
||||
$pending = $allEntries->where('governance_state', 'pending_exception')->count();
|
||||
$total = max($allEntries->count(), $exceptions->count());
|
||||
$expiring = max(
|
||||
$allEntries->where('governance_state', 'expiring_exception')->count(),
|
||||
$exceptions->where('current_validity_state', FindingException::VALIDITY_EXPIRING)->count(),
|
||||
);
|
||||
$expired = max(
|
||||
$allEntries->where('governance_state', 'expired_exception')->count(),
|
||||
$exceptions->where('current_validity_state', FindingException::VALIDITY_EXPIRED)->count(),
|
||||
);
|
||||
$pending = max(
|
||||
$allEntries->where('governance_state', 'pending_exception')->count(),
|
||||
$exceptions->where('status', FindingException::STATUS_PENDING)->count(),
|
||||
);
|
||||
$needsReview = $allEntries
|
||||
->filter(static fn (array $entry): bool => in_array(
|
||||
(string) ($entry['governance_state'] ?? ''),
|
||||
@ -809,6 +1050,18 @@ private function acceptedRiskPanelForReview(EnvironmentReview $review): array
|
||||
true,
|
||||
))
|
||||
->count();
|
||||
$needsReview = max($needsReview, $exceptions
|
||||
->filter(static fn (FindingException $exception): bool => in_array(
|
||||
(string) $exception->current_validity_state,
|
||||
[
|
||||
FindingException::VALIDITY_EXPIRING,
|
||||
FindingException::VALIDITY_EXPIRED,
|
||||
FindingException::VALIDITY_REVOKED,
|
||||
FindingException::VALIDITY_MISSING_SUPPORT,
|
||||
],
|
||||
true,
|
||||
))
|
||||
->count());
|
||||
|
||||
return [
|
||||
'summary_label' => $hasFollowUp
|
||||
@ -842,6 +1095,7 @@ private function acceptedRiskPanelForReview(EnvironmentReview $review): array
|
||||
'color' => $needsReview > 0 ? 'warning' : 'gray',
|
||||
],
|
||||
],
|
||||
'detail_rows' => $this->acceptedRiskDetailRows($exceptions),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -354,13 +354,31 @@
|
||||
'ready_to_share_reason' => 'Das veröffentlichte Review, der Nachweispfad und das aktuelle Review-Pack sind für die kundensichere Übergabe verfügbar.',
|
||||
'shareable_with_follow_up_reason' => 'Das Review-Pack ist verfügbar, aber Accepted-Risk-Follow-up muss vor der Übergabe benannt werden.',
|
||||
'follow_up_required_before_sharing_reason' => 'Review-Nachweis oder Paketverfügbarkeit benötigen noch Aufmerksamkeit, bevor dies geteilt werden kann.',
|
||||
'findings_follow_up_required_reason' => ':summary Halten Sie offene Findings vor der Kundenübergabe sichtbar.',
|
||||
'accepted_risk_follow_up_required_reason' => 'Accepted-Risk-Follow-up ist für dieses Review erfasst. Prüfen Sie Owner, Begründung und Review-Datum vor der Übergabe.',
|
||||
'ready_to_share_impact' => 'Stakeholder können das aktuelle Review-Pack und das veröffentlichte Review als Nachweispfad nutzen.',
|
||||
'shareable_with_follow_up_impact' => 'Nutzen Sie das aktuelle Pack nur, wenn das Accepted-Risk-Follow-up in der Kundenübergabe enthalten ist.',
|
||||
'follow_up_required_before_sharing_impact' => 'Behandeln Sie dieses Review erst als teilbar, wenn der nicht verfügbare Nachweis geprüft wurde.',
|
||||
'findings_follow_up_required_impact' => 'Behandeln Sie dieses Review erst als teilbar, wenn offene Findings behoben, akzeptiert oder ausdrücklich geprüft wurden.',
|
||||
'accepted_risk_follow_up_required_impact' => 'Das aktuelle Pack darf nur mit enthaltenem Accepted-Risk-Kontext in die Kundenübergabe.',
|
||||
'impact' => 'Auswirkung',
|
||||
'scope' => 'Scope',
|
||||
'ready' => 'Bereit',
|
||||
'needs_review' => 'Review erforderlich',
|
||||
'not_ready' => 'Nicht bereit',
|
||||
'readiness' => 'Bereitschaft',
|
||||
'evidence' => 'Evidence',
|
||||
'review_consumption_flow' => 'Review-Consumption-Flow',
|
||||
'review_consumption_flow_description' => 'Prüfen Sie die abgeleiteten Review-, Evidence-, Findings-, Accepted-Risk-, Pack- und Kundenausgabe-Status vor der Weitergabe.',
|
||||
'review_data' => 'Review-Daten',
|
||||
'review_data_available_description' => 'Ein veröffentlichtes Review ist für diesen kundensicheren Workspace verfügbar.',
|
||||
'findings_triaged' => 'Findings triagiert',
|
||||
'accepted_risks_reviewed' => 'Akzeptierte Risiken geprüft',
|
||||
'customer_output' => 'Kundensichere Ausgabe',
|
||||
'customer_output_ready_description' => 'Evidence- und Review-Pack-Truth stützen die kundensichere Nutzung.',
|
||||
'customer_output_needs_review_description' => 'Ausgabe ist vorhanden, aber Aufmerksamkeitspunkte müssen bei der Übergabe sichtbar bleiben.',
|
||||
'customer_output_not_ready_description' => 'Kundensichere Ausgabe ist aus dem aktuellen Nachweisstatus nicht bereit.',
|
||||
'current_attention_point' => 'Aktueller Aufmerksamkeitspunkt',
|
||||
'readiness_dimension_description' => 'Die Bereitschaft wird aus veröffentlichtem Review, Evidence, Accepted-Risk- und Review-Pack-Status abgeleitet.',
|
||||
'readiness_dimension_ready_description' => 'Veröffentlichtes Review ist verfügbar.',
|
||||
'readiness_dimension_follow_up_description' => 'Follow-up vor Übergabe erforderlich.',
|
||||
@ -384,6 +402,7 @@
|
||||
'accepted_risk_records_description' => 'Accepted-Risk-Entscheidungen sind in der Evidence-Basis des veröffentlichten Reviews vorhanden.',
|
||||
'operation_proof' => 'Operation-Nachweis',
|
||||
'operation_proof_available_description' => 'Ein zugehöriger Operationsdatensatz existiert für diesen Review-Nachweispfad.',
|
||||
'operation_proof_available_with_initiator_description' => 'Ein zugehöriger Operationsdatensatz existiert für diesen Review-Nachweispfad. Gestartet von :initiator.',
|
||||
'operation_proof_unavailable' => 'Kein Operation-Nachweis verknüpft',
|
||||
'operation_proof_unavailable_description' => 'Für diesen veröffentlichten Review-Pfad ist kein Operation-Nachweis verknüpft.',
|
||||
'export_artifact' => 'Export-Artefakt',
|
||||
@ -403,6 +422,12 @@
|
||||
'diagnostics_customer_workspace_default_hidden' => 'Supportdetails bleiben auf autorisierten Diagnoseflächen und werden in diesem kundensicheren Workspace standardmäßig nicht angezeigt.',
|
||||
'accepted_risk_summary' => 'Akzeptierte Risiken',
|
||||
'accepted_risk_no_action_needed' => 'Keine Aktion erforderlich',
|
||||
'accepted_risk_accountability' => 'Accepted-Risk-Verantwortlichkeit',
|
||||
'accepted_risk_owner' => 'Owner',
|
||||
'accepted_risk_next_review' => 'Nächstes Review',
|
||||
'accepted_risk_rationale' => 'Begründung',
|
||||
'review_date_not_recorded' => 'Review-Datum nicht erfasst',
|
||||
'not_recorded' => 'Nicht erfasst',
|
||||
'accepted_risks_expiring_soon' => 'Läuft bald ab',
|
||||
'accepted_risks_expired' => 'Abgelaufen',
|
||||
'accepted_risks_pending_approval' => 'Freigabe ausstehend',
|
||||
@ -443,6 +468,16 @@
|
||||
'control_recommendation_unmapped' => 'Prüfen Sie unmapped Evidence vor der Kundenauslieferung.',
|
||||
'proof_access_state' => 'Proof-Zugriff',
|
||||
'key_findings' => 'Wichtige Findings',
|
||||
'findings_needing_attention' => 'Findings mit Aufmerksamkeit',
|
||||
'findings_total' => 'Findings gesamt',
|
||||
'findings_open' => 'Offene Findings',
|
||||
'findings_high_impact' => 'Hoher Impact',
|
||||
'findings_high_impact_summary' => ':open; :high.',
|
||||
'findings_open_attention_count' => '{1} 1 offenes Finding benötigt Aufmerksamkeit|[2,*] :count offene Findings benötigen Aufmerksamkeit',
|
||||
'findings_high_impact_count_summary' => '{1} 1 hat hohen Impact|[2,*] :count haben hohen Impact',
|
||||
'findings_open_summary' => '{1} 1 offenes Finding benötigt Kunden- oder Operator-Review.|[2,*] :count offene Findings benötigen Kunden- oder Operator-Review.',
|
||||
'findings_no_open_summary' => ':total Findings sind erfasst, ohne offene Kundenaktions-Findings.',
|
||||
'findings_none_action_summary' => 'Keine offenen Findings erfordern Kundenaktion.',
|
||||
'accepted_risks' => 'Akzeptierte Risiken',
|
||||
'evidence_proof' => 'Evidence-Nachweis',
|
||||
'evidence_status' => 'Nachweise',
|
||||
|
||||
@ -354,13 +354,31 @@
|
||||
'ready_to_share_reason' => 'The released review, evidence path, and current review pack are available for customer-safe handoff.',
|
||||
'shareable_with_follow_up_reason' => 'The review pack is available, but accepted-risk follow-up must be called out before handoff.',
|
||||
'follow_up_required_before_sharing_reason' => 'Review proof or package availability still needs attention before this can be shared.',
|
||||
'findings_follow_up_required_reason' => ':summary Keep open findings visible before customer handoff.',
|
||||
'accepted_risk_follow_up_required_reason' => 'Accepted-risk follow-up is recorded for this review. Review the owner, rationale, and review date before handoff.',
|
||||
'ready_to_share_impact' => 'Stakeholders can use the current review pack and released review as the evidence path.',
|
||||
'shareable_with_follow_up_impact' => 'Use the current pack only with the accepted-risk follow-up included in the customer handoff.',
|
||||
'follow_up_required_before_sharing_impact' => 'Do not treat this review as share-ready until the unavailable proof has been reviewed.',
|
||||
'findings_follow_up_required_impact' => 'Do not treat this review as share-ready until open findings are resolved, accepted, or explicitly reviewed.',
|
||||
'accepted_risk_follow_up_required_impact' => 'The pack can be shared only with the accepted-risk context included in the customer handoff.',
|
||||
'impact' => 'Impact',
|
||||
'scope' => 'Scope',
|
||||
'ready' => 'Ready',
|
||||
'needs_review' => 'Needs review',
|
||||
'not_ready' => 'Not ready',
|
||||
'readiness' => 'Readiness',
|
||||
'evidence' => 'Evidence',
|
||||
'review_consumption_flow' => 'Review consumption flow',
|
||||
'review_consumption_flow_description' => 'Follow the derived review, evidence, findings, accepted-risk, pack, and customer output states before sharing.',
|
||||
'review_data' => 'Review data',
|
||||
'review_data_available_description' => 'A released review is available for this customer-safe workspace.',
|
||||
'findings_triaged' => 'Findings triaged',
|
||||
'accepted_risks_reviewed' => 'Accepted risks reviewed',
|
||||
'customer_output' => 'Customer-safe output',
|
||||
'customer_output_ready_description' => 'Evidence and review-pack truth support customer-safe consumption.',
|
||||
'customer_output_needs_review_description' => 'Output exists, but attention items must stay visible during handoff.',
|
||||
'customer_output_not_ready_description' => 'Customer-safe output is not ready from the current proof state.',
|
||||
'current_attention_point' => 'Current attention point',
|
||||
'readiness_dimension_description' => 'Readiness is derived from the released review, evidence, accepted-risk, and review-pack state.',
|
||||
'readiness_dimension_ready_description' => 'Released review is available.',
|
||||
'readiness_dimension_follow_up_description' => 'Follow-up required before handoff.',
|
||||
@ -384,6 +402,7 @@
|
||||
'accepted_risk_records_description' => 'Accepted-risk decisions are present in the released review evidence basis.',
|
||||
'operation_proof' => 'Operation proof',
|
||||
'operation_proof_available_description' => 'A related operation record exists for this review evidence path.',
|
||||
'operation_proof_available_with_initiator_description' => 'A related operation record exists for this review evidence path. Initiated by :initiator.',
|
||||
'operation_proof_unavailable' => 'No operation proof linked',
|
||||
'operation_proof_unavailable_description' => 'No operation proof link is attached to this released review path.',
|
||||
'export_artifact' => 'Export artifact',
|
||||
@ -403,6 +422,12 @@
|
||||
'diagnostics_customer_workspace_default_hidden' => 'Support details stay on authorized diagnostic surfaces and are not shown in this customer-safe workspace by default.',
|
||||
'accepted_risk_summary' => 'Accepted risks',
|
||||
'accepted_risk_no_action_needed' => 'No action needed',
|
||||
'accepted_risk_accountability' => 'Accepted-risk accountability',
|
||||
'accepted_risk_owner' => 'Owner',
|
||||
'accepted_risk_next_review' => 'Next review',
|
||||
'accepted_risk_rationale' => 'Rationale',
|
||||
'review_date_not_recorded' => 'Review date not recorded',
|
||||
'not_recorded' => 'Not recorded',
|
||||
'accepted_risks_expiring_soon' => 'Expiring soon',
|
||||
'accepted_risks_expired' => 'Expired',
|
||||
'accepted_risks_pending_approval' => 'Pending approval',
|
||||
@ -443,6 +468,16 @@
|
||||
'control_recommendation_unmapped' => 'Review unmapped evidence before customer delivery.',
|
||||
'proof_access_state' => 'Proof access',
|
||||
'key_findings' => 'Key findings',
|
||||
'findings_needing_attention' => 'Findings needing attention',
|
||||
'findings_total' => 'Total findings',
|
||||
'findings_open' => 'Open findings',
|
||||
'findings_high_impact' => 'High impact',
|
||||
'findings_high_impact_summary' => ':open; :high.',
|
||||
'findings_open_attention_count' => '{1} 1 open finding needs attention|[2,*] :count open findings need attention',
|
||||
'findings_high_impact_count_summary' => '{1} 1 is high impact|[2,*] :count are high impact',
|
||||
'findings_open_summary' => '{1} 1 open finding needs customer or operator review.|[2,*] :count open findings need customer or operator review.',
|
||||
'findings_no_open_summary' => ':total findings are recorded, with no open customer-action findings.',
|
||||
'findings_none_action_summary' => 'No open findings require customer action.',
|
||||
'accepted_risks' => 'Accepted risks',
|
||||
'evidence_proof' => 'Evidence proof',
|
||||
'evidence_status' => 'Evidence',
|
||||
|
||||
@ -37,7 +37,8 @@
|
||||
$latest = $reviewPayload['latest'];
|
||||
$scope = $reviewPayload['scope'];
|
||||
$readiness = $reviewPayload['readiness'];
|
||||
$dimensions = $reviewPayload['readiness_dimensions'];
|
||||
$readinessFlow = $reviewPayload['readiness_flow'];
|
||||
$findingPanel = $reviewPayload['finding_panel'];
|
||||
$asideEvidencePath = $reviewPayload['aside_evidence_path'];
|
||||
$reviewPackPanel = $reviewPayload['review_pack_panel'];
|
||||
$acceptedRisks = $reviewPayload['accepted_risks'];
|
||||
@ -102,6 +103,7 @@ class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-whit
|
||||
:href="$readiness['primary_action_url']"
|
||||
:icon="$readiness['primary_action_icon']"
|
||||
target="_blank"
|
||||
data-testid="customer-review-primary-action"
|
||||
>
|
||||
{{ $readiness['primary_action_label'] }}
|
||||
</x-filament::button>
|
||||
@ -113,6 +115,7 @@ class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-whit
|
||||
:href="$latest['secondary_action_url']"
|
||||
color="gray"
|
||||
:icon="$latest['secondary_action_icon']"
|
||||
data-testid="customer-review-secondary-action"
|
||||
>
|
||||
{{ $latest['secondary_action_label'] }}
|
||||
</x-filament::button>
|
||||
@ -121,24 +124,81 @@ class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-whit
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2 2xl:grid-cols-4" data-testid="customer-review-readiness-dimensions">
|
||||
@foreach ($dimensions as $dimension)
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-3 shadow-sm dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="flex min-h-32 flex-col gap-2">
|
||||
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">
|
||||
{{ $dimension['title'] }}
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-900"
|
||||
data-testid="customer-review-readiness-flow"
|
||||
>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ __('localization.review.review_consumption_flow') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
{{ __('localization.review.review_consumption_flow_description') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2 2xl:grid-cols-3">
|
||||
@foreach ($readinessFlow as $step)
|
||||
<div
|
||||
class="flex min-h-36 flex-col gap-2 rounded-lg border border-gray-200 p-3 text-sm dark:border-white/10"
|
||||
data-testid="customer-review-readiness-step"
|
||||
data-step-label="{{ $step['title'] }}"
|
||||
data-step-state="{{ $step['label'] }}"
|
||||
data-step-current="{{ $step['is_current'] ? 'true' : 'false' }}"
|
||||
>
|
||||
<div class="flex flex-wrap items-start gap-2">
|
||||
<div class="min-w-0 font-medium text-gray-950 dark:text-white">
|
||||
{{ $step['title'] }}
|
||||
</div>
|
||||
<x-filament::badge :color="$step['color']" size="sm" class="max-w-full whitespace-normal text-left">
|
||||
{{ $step['label'] }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
<p class="text-xs leading-5 text-gray-600 dark:text-gray-300">
|
||||
{{ $step['description'] }}
|
||||
</p>
|
||||
@if ($step['is_current'])
|
||||
<div class="mt-auto text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||
{{ __('localization.review.current_attention_point') }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<x-filament::badge :color="$dimension['color']">
|
||||
{{ $dimension['label'] }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
<p class="text-xs leading-5 text-gray-600 dark:text-gray-300">
|
||||
{{ $dimension['description'] }}
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-900"
|
||||
data-testid="customer-review-findings-summary"
|
||||
>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ __('localization.review.findings_needing_attention') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
{{ $findingPanel['summary'] }}
|
||||
</p>
|
||||
</div>
|
||||
<x-filament::badge :color="$findingPanel['status_color']">
|
||||
{{ $findingPanel['status_label'] }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<div class="grid gap-2 sm:grid-cols-2 2xl:grid-cols-4">
|
||||
@foreach ($findingPanel['items'] as $item)
|
||||
<div class="flex items-center justify-between gap-3 rounded-lg border border-gray-200 px-3 py-2 text-xs dark:border-white/10">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ $item['label'] }}</span>
|
||||
<x-filament::badge :color="$item['color']" size="sm">
|
||||
{{ $item['value'] }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-900">
|
||||
@ -189,7 +249,10 @@ class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-whit
|
||||
</main>
|
||||
|
||||
<aside class="space-y-3 md:col-span-1 md:sticky md:top-4 md:self-start xl:col-span-4" data-testid="customer-review-evidence-aside">
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-3 shadow-sm dark:border-white/10 dark:bg-gray-900">
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-white p-3 shadow-sm dark:border-white/10 dark:bg-gray-900"
|
||||
data-testid="customer-review-evidence-path-panel"
|
||||
>
|
||||
<h2 class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ __('localization.review.evidence_path') }}
|
||||
</h2>
|
||||
@ -210,17 +273,32 @@ class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-whit
|
||||
<dd class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{{ $proof['detail'] }}
|
||||
</dd>
|
||||
@if ($proof['action_url'])
|
||||
<dd class="mt-2">
|
||||
<x-filament::button
|
||||
tag="a"
|
||||
:href="$proof['action_url']"
|
||||
color="gray"
|
||||
size="xs"
|
||||
>
|
||||
{{ $proof['action_label'] }}
|
||||
</x-filament::button>
|
||||
</dd>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-3 shadow-sm dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-white p-3 shadow-sm dark:border-white/10 dark:bg-gray-900"
|
||||
data-testid="customer-review-review-pack-panel"
|
||||
>
|
||||
<div class="flex flex-wrap items-start gap-2">
|
||||
<h2 class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ __('localization.review.review_pack_state') }}
|
||||
</h2>
|
||||
<x-filament::badge :color="$reviewPackPanel['status_color']" size="sm">
|
||||
<x-filament::badge :color="$reviewPackPanel['status_color']" size="sm" class="max-w-full whitespace-normal text-left">
|
||||
{{ $reviewPackPanel['status_label'] }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
@ -249,12 +327,15 @@ class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-whit
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-3 shadow-sm dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div
|
||||
class="rounded-xl border border-gray-200 bg-white p-3 shadow-sm dark:border-white/10 dark:bg-gray-900"
|
||||
data-testid="customer-review-accepted-risk-panel"
|
||||
>
|
||||
<div class="flex flex-wrap items-start gap-2">
|
||||
<h2 class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
{{ __('localization.review.accepted_risk_summary') }}
|
||||
</h2>
|
||||
<x-filament::badge :color="$acceptedRiskPanel['summary_color']" size="sm">
|
||||
<x-filament::badge :color="$acceptedRiskPanel['summary_color']" size="sm" class="max-w-full whitespace-normal text-left">
|
||||
{{ $acceptedRiskPanel['summary_label'] }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
@ -270,6 +351,45 @@ class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-whit
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if ($acceptedRisks['entries'] !== [])
|
||||
<div
|
||||
class="mt-3 divide-y divide-gray-100 border-t border-gray-200 pt-1 dark:divide-white/10 dark:border-white/10"
|
||||
data-testid="customer-review-accepted-risk-summaries"
|
||||
>
|
||||
@foreach ($acceptedRisks['entries'] as $risk)
|
||||
<div class="py-2 text-xs">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">{{ $risk['title'] }}</span>
|
||||
<x-filament::badge color="gray" size="sm">
|
||||
{{ $risk['state_label'] }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
<div class="mt-1 leading-5 text-gray-500 dark:text-gray-400">
|
||||
{{ $risk['summary'] }}
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($acceptedRiskPanel['detail_rows'] !== [])
|
||||
<div class="mt-3 border-t border-gray-200 pt-3 dark:border-white/10">
|
||||
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">
|
||||
{{ __('localization.review.accepted_risk_accountability') }}
|
||||
</div>
|
||||
<div class="mt-2 space-y-1.5">
|
||||
@foreach ($acceptedRiskPanel['detail_rows'] as $row)
|
||||
<div class="flex items-start justify-between gap-3 text-xs">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ $row['label'] }}</span>
|
||||
<span class="max-w-44 text-right font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $row['value'] }}
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<details class="group mt-3 border-t border-gray-200 pt-3 dark:border-white/10">
|
||||
<summary class="cursor-pointer text-xs font-semibold uppercase text-gray-500 marker:text-gray-400 dark:text-gray-400">
|
||||
{{ __('localization.review.accepted_risk_records') }}
|
||||
|
||||
@ -5,7 +5,9 @@
|
||||
use App\Filament\Resources\EnvironmentReviewResource;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\ReviewPack;
|
||||
use App\Support\EnvironmentReviewCompletenessState;
|
||||
use App\Support\EnvironmentReviewStatus;
|
||||
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@ -39,12 +41,39 @@
|
||||
workspaceRole: 'manager',
|
||||
);
|
||||
|
||||
$publishedSnapshot = seedEnvironmentReviewEvidence($tenantPublished);
|
||||
$noPublishedSnapshot = seedEnvironmentReviewEvidence($tenantWithoutPublished);
|
||||
$publishedSnapshot = seedEnvironmentReviewEvidence($tenantPublished, findingCount: 0, driftCount: 0);
|
||||
$noPublishedSnapshot = seedEnvironmentReviewEvidence($tenantWithoutPublished, findingCount: 0, driftCount: 0);
|
||||
|
||||
$publishedReview = composeEnvironmentReviewForTest($tenantPublished, $user, $publishedSnapshot);
|
||||
$publishedSummary = array_replace_recursive(is_array($publishedReview->summary) ? $publishedReview->summary : [], [
|
||||
'control_interpretation' => [
|
||||
'version_key' => ComplianceEvidenceMappingV1::VERSION_KEY,
|
||||
'controls' => [
|
||||
[
|
||||
'control_key' => 'customer-handoff-readiness',
|
||||
'title' => 'Customer handoff readiness',
|
||||
'readiness_bucket' => 'evidence_on_record',
|
||||
'readiness_label' => 'Evidence on record',
|
||||
'primary_reason' => 'Evidence path is complete.',
|
||||
'recommended_next_action' => 'Share the current review pack.',
|
||||
],
|
||||
],
|
||||
],
|
||||
'governance_package' => [
|
||||
'decision_summary' => [
|
||||
'status' => 'none',
|
||||
'evidence_state' => EnvironmentReviewCompletenessState::Complete->value,
|
||||
'decision_data_state' => 'complete',
|
||||
'total_count' => 0,
|
||||
'summary' => '',
|
||||
'next_action' => '',
|
||||
'entries' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$publishedReview->forceFill([
|
||||
'status' => EnvironmentReviewStatus::Published->value,
|
||||
'summary' => $publishedSummary,
|
||||
'published_at' => now(),
|
||||
'published_by_user_id' => (int) $user->getKey(),
|
||||
])->save();
|
||||
@ -97,7 +126,7 @@
|
||||
->assertSee('Eingeklappt')
|
||||
->assertSee('Review-Pack herunterladen')
|
||||
->assertSee('Das aktuelle Review-Pack ist zum Download bereit.')
|
||||
->assertSee('Keine Entscheidungen mit Aufmerksamkeitsbedarf')
|
||||
->assertSee('In diesem veröffentlichten Review benötigen keine Governance-Entscheidungen Kundenaufmerksamkeit.')
|
||||
->assertSee('Bereit zur Weitergabe')
|
||||
->assertSee('Verfügbar')
|
||||
->assertDontSee('Customer-safe governance package index')
|
||||
|
||||
@ -0,0 +1,393 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||
use App\Models\EnvironmentReview;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\Finding;
|
||||
use App\Models\FindingException;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\ReviewPack;
|
||||
use App\Models\User;
|
||||
use App\Support\EnvironmentReviewCompletenessState;
|
||||
use App\Support\EnvironmentReviewStatus;
|
||||
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
||||
use App\Support\OperationRunOutcome;
|
||||
use App\Support\OperationRunStatus;
|
||||
use App\Support\OperationRunType;
|
||||
use App\Support\ReviewPackStatus;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
pest()->browser()->timeout(60_000);
|
||||
|
||||
beforeEach(function (): void {
|
||||
Storage::fake('exports');
|
||||
});
|
||||
|
||||
it('Spec342 smokes final customer review consumption states', function (): void {
|
||||
[$user, $notReadyEnvironment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
||||
$notReadyEnvironment->forceFill(['name' => 'Spec342 Browser Evidence Incomplete'])->save();
|
||||
|
||||
$readyEnvironment = spec342BrowserEnvironmentFor($user, $notReadyEnvironment, 'Spec342 Browser Ready');
|
||||
$findingsEnvironment = spec342BrowserEnvironmentFor($user, $notReadyEnvironment, 'Spec342 Browser Findings');
|
||||
$acceptedRiskEnvironment = spec342BrowserEnvironmentFor($user, $notReadyEnvironment, 'Spec342 Browser Accepted Risks');
|
||||
|
||||
spec342BrowserCreatePublishedReviewWithPack(
|
||||
$notReadyEnvironment,
|
||||
$user,
|
||||
seedPartialEnvironmentReviewEvidence($notReadyEnvironment, findingCount: 0, driftCount: 0),
|
||||
[
|
||||
'debug_payload' => 'raw payload should stay hidden',
|
||||
'provider_response' => 'provider response should stay hidden',
|
||||
'stack_trace' => 'stack trace should stay hidden',
|
||||
'source_fingerprint' => 'spec342-browser-hidden-fingerprint',
|
||||
'governance_package' => [
|
||||
'decision_summary' => [
|
||||
'status' => 'incomplete',
|
||||
'evidence_state' => EnvironmentReviewCompletenessState::Partial->value,
|
||||
'decision_data_state' => 'incomplete',
|
||||
'total_count' => 1,
|
||||
'summary' => 'Decision evidence is incomplete for this released review.',
|
||||
'next_action' => 'Review the evidence basis before relying on the decision summary.',
|
||||
'entries' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
'review-packs/spec342-browser-evidence-incomplete.zip',
|
||||
);
|
||||
|
||||
spec342BrowserCreatePublishedReviewWithPack(
|
||||
$readyEnvironment,
|
||||
$user,
|
||||
seedEnvironmentReviewEvidence($readyEnvironment, findingCount: 0, driftCount: 0),
|
||||
[],
|
||||
'review-packs/spec342-browser-ready.zip',
|
||||
);
|
||||
|
||||
spec342BrowserCreatePublishedReviewWithPack(
|
||||
$findingsEnvironment,
|
||||
$user,
|
||||
seedEnvironmentReviewEvidence($findingsEnvironment, findingCount: 0, driftCount: 0),
|
||||
[],
|
||||
'review-packs/spec342-browser-findings.zip',
|
||||
);
|
||||
Finding::factory()->create([
|
||||
'managed_environment_id' => (int) $findingsEnvironment->getKey(),
|
||||
'workspace_id' => (int) $findingsEnvironment->workspace_id,
|
||||
'severity' => Finding::SEVERITY_CRITICAL,
|
||||
'status' => Finding::STATUS_NEW,
|
||||
]);
|
||||
|
||||
spec342BrowserCreatePublishedReviewWithPack(
|
||||
$acceptedRiskEnvironment,
|
||||
$user,
|
||||
seedEnvironmentReviewEvidence($acceptedRiskEnvironment, findingCount: 0, driftCount: 0),
|
||||
[
|
||||
'governance_package' => [
|
||||
'accepted_risks' => [
|
||||
[
|
||||
'title' => 'Accepted risk renewal',
|
||||
'governance_state' => 'expiring_exception',
|
||||
'customer_summary' => 'Accepted risk requires customer awareness.',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'review-packs/spec342-browser-accepted-risk.zip',
|
||||
);
|
||||
spec342BrowserCreateAcceptedRisk($acceptedRiskEnvironment, $user);
|
||||
|
||||
spec342AuthenticateBrowser($this, $user, $notReadyEnvironment);
|
||||
|
||||
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($notReadyEnvironment))
|
||||
->resize(1236, 900)
|
||||
->waitForText('Follow-up required before sharing')
|
||||
->assertSee('Evidence incomplete')
|
||||
->assertSee('Customer-safe output')
|
||||
->assertSee('Not ready')
|
||||
->assertSee('Review consumption flow')
|
||||
->assertScript('document.querySelectorAll("[data-testid=\"customer-review-readiness-step\"]").length === 6', true)
|
||||
->assertScript('document.querySelector("[data-step-label=\"Review pack\"]")?.dataset.stepState === "Evidence incomplete"', true)
|
||||
->assertScript('document.querySelector("[data-step-label=\"Customer-safe output\"]")?.dataset.stepState === "Not ready"', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"customer-review-diagnostics\"]")?.open === false', true)
|
||||
->assertDontSee('raw payload should stay hidden')
|
||||
->assertDontSee('provider response should stay hidden')
|
||||
->assertDontSee('stack trace should stay hidden')
|
||||
->assertDontSee('spec342-browser-hidden-fingerprint')
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
$page->screenshot(true, spec342BrowserScreenshotName('01-evidence-incomplete-not-ready'));
|
||||
spec342CopyBrowserScreenshot('01-evidence-incomplete-not-ready');
|
||||
|
||||
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($readyEnvironment))
|
||||
->waitForText('Ready to share')
|
||||
->assertSee('Stakeholders can use the current review pack and released review as the evidence path.')
|
||||
->assertSee('Download review pack')
|
||||
->assertSee('Review pack state')
|
||||
->assertSee('Export ready')
|
||||
->assertSee('Operation proof')
|
||||
->assertSee('Spec342 Browser Operator')
|
||||
->assertSee('No open findings require customer action.')
|
||||
->assertScript('document.querySelectorAll("[data-testid=\"customer-review-primary-action\"]").length === 1', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"customer-review-evidence-path-panel\"]")?.innerText.includes("Download review pack") === false', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"customer-review-secondary-action\"]")?.innerText.includes("Download review pack") === false', true)
|
||||
->assertScript('document.querySelectorAll("[data-testid=\"customer-review-readiness-step\"]").length === 6', true)
|
||||
->assertScript('document.querySelector("[data-step-label=\"Customer-safe output\"]")?.dataset.stepState === "Ready"', true)
|
||||
->assertScript('Array.from(document.querySelectorAll("[data-testid=\"customer-review-readiness-step\"] [class*=\"badge\"], [data-testid=\"customer-review-review-pack-panel\"] [class*=\"badge\"], [data-testid=\"customer-review-accepted-risk-panel\"] [class*=\"badge\"]")).every((badge) => ! badge.innerText.includes("..."))', true)
|
||||
->assertScript('document.body.innerHTML.includes("source_surface=customer_review_workspace")', true)
|
||||
->assertScript('! document.body.innerHTML.includes("/admin/t/") && ! window.location.search.includes("tenant_id=")', true)
|
||||
->assertDontSee('Auditor-ready')
|
||||
->assertDontSee('environment is healthy')
|
||||
->assertDontSee('compliant')
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
$page->screenshot(true, spec342BrowserScreenshotName('02-ready-with-evidence'));
|
||||
spec342CopyBrowserScreenshot('02-ready-with-evidence');
|
||||
|
||||
$page->script('document.querySelector("[data-testid=\"customer-review-review-pack-panel\"]")?.scrollIntoView({ block: "center" });');
|
||||
$page
|
||||
->assertSee('Export availability')
|
||||
->assertSee('Available');
|
||||
$page->screenshot(true, spec342BrowserScreenshotName('03-review-pack-available'));
|
||||
spec342CopyBrowserScreenshot('03-review-pack-available');
|
||||
|
||||
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($findingsEnvironment))
|
||||
->waitForText('Findings needing attention')
|
||||
->assertSee('Follow-up required before sharing')
|
||||
->assertSee('1 open finding needs attention; 1 is high impact.')
|
||||
->assertSee('Keep open findings visible before customer handoff.')
|
||||
->assertSee('Do not treat this review as share-ready until open findings are resolved, accepted, or explicitly reviewed.')
|
||||
->assertSee('High impact')
|
||||
->assertSee('Open review')
|
||||
->assertScript('document.querySelector("[data-testid=\"customer-review-decision-card\"]")?.innerText.includes("Download review pack") === false', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"customer-review-evidence-path-panel\"]")?.innerText.includes("Download review pack") === false', true)
|
||||
->assertScript('document.querySelector("[data-step-label=\"Findings triaged\"]")?.dataset.stepState === "Needs review"', true)
|
||||
->assertScript('document.querySelector("[data-step-label=\"Findings triaged\"]")?.dataset.stepCurrent === "true"', true)
|
||||
->assertScript('document.querySelector("[data-step-label=\"Customer-safe output\"]")?.dataset.stepState === "Needs review"', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
$page->screenshot(true, spec342BrowserScreenshotName('04-findings-need-attention'));
|
||||
spec342CopyBrowserScreenshot('04-findings-need-attention');
|
||||
|
||||
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($acceptedRiskEnvironment))
|
||||
->waitForText('Shareable with follow-up')
|
||||
->assertSee('Accepted-risk follow-up is recorded for this review. Review the owner, rationale, and review date before handoff.')
|
||||
->assertSee('The pack can be shared only with the accepted-risk context included in the customer handoff.')
|
||||
->assertSee('Open review')
|
||||
->assertSee('Follow-up required')
|
||||
->assertSee('Accepted-risk accountability')
|
||||
->assertSee('Spec342 Browser Risk Owner')
|
||||
->assertSee('Customer-approved maintenance window.')
|
||||
->assertSee('Review date not recorded')
|
||||
->assertSee('Accepted risk requires customer awareness.')
|
||||
->assertScript('document.querySelector("[data-testid=\"customer-review-decision-card\"]")?.innerText.includes("Download review pack") === false', true)
|
||||
->assertScript('document.querySelector("[data-step-label=\"Accepted risks reviewed\"]")?.dataset.stepCurrent === "true"', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
$page->screenshot(true, spec342BrowserScreenshotName('05-accepted-risks-present'));
|
||||
spec342CopyBrowserScreenshot('05-accepted-risks-present');
|
||||
|
||||
$page->assertScript('document.querySelector("[data-testid=\"customer-review-diagnostics\"]")?.open === false', true);
|
||||
$page->screenshot(true, spec342BrowserScreenshotName('06-diagnostics-collapsed'));
|
||||
spec342CopyBrowserScreenshot('06-diagnostics-collapsed');
|
||||
|
||||
$page->script("document.documentElement.classList.add('dark');");
|
||||
$page->script('window.scrollTo(0, 0);');
|
||||
$page->assertScript('document.documentElement.classList.contains("dark")', true);
|
||||
$page->screenshot(true, spec342BrowserScreenshotName('07-dark-mode'));
|
||||
spec342CopyBrowserScreenshot('07-dark-mode');
|
||||
});
|
||||
|
||||
function spec342BrowserScreenshotName(string $name): string
|
||||
{
|
||||
return 'spec342-customer-review-workspace-'.$name;
|
||||
}
|
||||
|
||||
function spec342CopyBrowserScreenshot(string $name): void
|
||||
{
|
||||
$filename = spec342BrowserScreenshotName($name).'.png';
|
||||
$source = base_path('tests/Browser/Screenshots/'.$filename);
|
||||
$targetDirectory = repo_path('specs/342-customer-review-workspace-final-consumption-productization/artifacts/screenshots');
|
||||
|
||||
if (! is_dir($targetDirectory)) {
|
||||
@mkdir($targetDirectory, 0755, true);
|
||||
}
|
||||
|
||||
if (! is_file($source)) {
|
||||
$source = \Pest\Browser\Support\Screenshot::path($filename);
|
||||
}
|
||||
|
||||
for ($attempt = 0; $attempt < 10 && ! is_file($source); $attempt++) {
|
||||
usleep(100_000);
|
||||
clearstatcache(true, $source);
|
||||
}
|
||||
|
||||
if (is_file($source) && is_dir($targetDirectory) && is_writable($targetDirectory)) {
|
||||
@copy($source, $targetDirectory.DIRECTORY_SEPARATOR.$name.'.png');
|
||||
}
|
||||
}
|
||||
|
||||
function spec342AuthenticateBrowser(mixed $test, User $user, ManagedEnvironment $environment): void
|
||||
{
|
||||
$workspaceId = (int) $environment->workspace_id;
|
||||
|
||||
$test->actingAs($user)->withSession([
|
||||
WorkspaceContext::SESSION_KEY => $workspaceId,
|
||||
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
|
||||
(string) $workspaceId => (int) $environment->getKey(),
|
||||
],
|
||||
]);
|
||||
|
||||
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
|
||||
session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [
|
||||
(string) $workspaceId => (int) $environment->getKey(),
|
||||
]);
|
||||
|
||||
setAdminPanelContext($environment);
|
||||
}
|
||||
|
||||
function spec342BrowserEnvironmentFor(User $user, ManagedEnvironment $baseEnvironment, string $name): ManagedEnvironment
|
||||
{
|
||||
$environment = ManagedEnvironment::factory()->active()->create([
|
||||
'workspace_id' => (int) $baseEnvironment->workspace_id,
|
||||
'name' => $name,
|
||||
]);
|
||||
|
||||
createUserWithTenant(tenant: $environment, user: $user, role: 'owner', workspaceRole: 'manager');
|
||||
|
||||
return $environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $summaryOverrides
|
||||
* @return array{0: EnvironmentReview, 1: ReviewPack}
|
||||
*/
|
||||
function spec342BrowserCreatePublishedReviewWithPack(
|
||||
ManagedEnvironment $environment,
|
||||
User $user,
|
||||
EvidenceSnapshot $snapshot,
|
||||
array $summaryOverrides = [],
|
||||
string $filePath = 'review-packs/spec342-browser-review-pack.zip',
|
||||
): array {
|
||||
$review = composeEnvironmentReviewForTest($environment, $user, $snapshot);
|
||||
$summary = array_replace_recursive(
|
||||
is_array($review->summary) ? $review->summary : [],
|
||||
[
|
||||
'control_interpretation' => [
|
||||
'version_key' => ComplianceEvidenceMappingV1::VERSION_KEY,
|
||||
'controls' => [
|
||||
[
|
||||
'control_key' => 'customer-output',
|
||||
'title' => 'Customer output',
|
||||
'readiness_bucket' => 'evidence_on_record',
|
||||
'readiness_label' => 'Evidence on record',
|
||||
'primary_reason' => 'Evidence path is complete.',
|
||||
'recommended_next_action' => 'Open the current customer review pack.',
|
||||
],
|
||||
],
|
||||
],
|
||||
'governance_package' => [
|
||||
'decision_summary' => [
|
||||
'status' => 'none',
|
||||
'evidence_state' => EnvironmentReviewCompletenessState::Complete->value,
|
||||
'decision_data_state' => 'complete',
|
||||
'total_count' => 0,
|
||||
'summary' => 'No governance decisions require customer awareness.',
|
||||
'next_action' => 'Open the current customer review pack.',
|
||||
'entries' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
$summaryOverrides,
|
||||
);
|
||||
|
||||
$run = OperationRun::factory()->forTenant($environment)->create([
|
||||
'type' => OperationRunType::ReviewPackGenerate->value,
|
||||
'status' => OperationRunStatus::Completed->value,
|
||||
'outcome' => OperationRunOutcome::Succeeded->value,
|
||||
'started_at' => now()->subMinutes(6),
|
||||
'completed_at' => now()->subMinutes(4),
|
||||
'initiator_name' => 'Spec342 Browser Operator',
|
||||
]);
|
||||
|
||||
Storage::disk('exports')->put($filePath, 'PK-spec342-browser-test');
|
||||
|
||||
$review->forceFill([
|
||||
'status' => EnvironmentReviewStatus::Published->value,
|
||||
'completeness_state' => EnvironmentReviewCompletenessState::Complete->value,
|
||||
'summary' => $summary,
|
||||
'operation_run_id' => (int) $run->getKey(),
|
||||
'generated_at' => now()->subMinutes(5),
|
||||
'published_at' => now()->subMinutes(3),
|
||||
'published_by_user_id' => (int) $user->getKey(),
|
||||
])->save();
|
||||
|
||||
$pack = ReviewPack::factory()->ready()->create([
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'environment_review_id' => (int) $review->getKey(),
|
||||
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
||||
'operation_run_id' => (int) $run->getKey(),
|
||||
'initiated_by_user_id' => (int) $user->getKey(),
|
||||
'status' => ReviewPackStatus::Ready->value,
|
||||
'file_path' => $filePath,
|
||||
'file_disk' => 'exports',
|
||||
'generated_at' => now()->subMinutes(4),
|
||||
]);
|
||||
|
||||
$review->forceFill([
|
||||
'current_export_review_pack_id' => (int) $pack->getKey(),
|
||||
])->save();
|
||||
|
||||
return [$review->refresh(), $pack->refresh()];
|
||||
}
|
||||
|
||||
function spec342BrowserCreateAcceptedRisk(ManagedEnvironment $environment, User $user): void
|
||||
{
|
||||
$owner = User::factory()->create(['name' => 'Spec342 Browser Risk Owner']);
|
||||
$findingWithReviewDate = Finding::factory()->riskAccepted()->create([
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
]);
|
||||
$findingWithoutReviewDate = Finding::factory()->riskAccepted()->create([
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
]);
|
||||
|
||||
FindingException::query()->create([
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'finding_id' => (int) $findingWithReviewDate->getKey(),
|
||||
'status' => FindingException::STATUS_ACTIVE,
|
||||
'current_validity_state' => FindingException::VALIDITY_VALID,
|
||||
'requested_by_user_id' => (int) $user->getKey(),
|
||||
'request_reason' => 'Customer-approved maintenance window.',
|
||||
'owner_user_id' => (int) $owner->getKey(),
|
||||
'approved_by_user_id' => (int) $owner->getKey(),
|
||||
'requested_at' => now()->subDays(3),
|
||||
'approved_at' => now()->subDays(2),
|
||||
'effective_from' => now()->subDays(2),
|
||||
'review_due_at' => now()->addDays(30),
|
||||
]);
|
||||
|
||||
FindingException::query()->create([
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'finding_id' => (int) $findingWithoutReviewDate->getKey(),
|
||||
'status' => FindingException::STATUS_ACTIVE,
|
||||
'current_validity_state' => FindingException::VALIDITY_EXPIRING,
|
||||
'requested_by_user_id' => (int) $user->getKey(),
|
||||
'request_reason' => 'Pending owner confirmation.',
|
||||
'requested_at' => now()->subDay(),
|
||||
'approved_at' => now()->subDay(),
|
||||
'effective_from' => now()->subDay(),
|
||||
'review_due_at' => null,
|
||||
'expires_at' => null,
|
||||
]);
|
||||
}
|
||||
@ -0,0 +1,340 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||
use App\Models\Finding;
|
||||
use App\Models\FindingException;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\ReviewPack;
|
||||
use App\Models\User;
|
||||
use App\Support\EnvironmentReviewCompletenessState;
|
||||
use App\Support\EnvironmentReviewStatus;
|
||||
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
||||
use App\Support\OperationRunStatus;
|
||||
use App\Support\OperationRunType;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('renders a decision-first consumption contract with one primary action and six flow steps', function (): void {
|
||||
$environment = ManagedEnvironment::factory()->create(['name' => 'Spec342 Ready Environment']);
|
||||
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'manager');
|
||||
$snapshot = seedEnvironmentReviewEvidence($environment, findingCount: 0, driftCount: 0);
|
||||
$run = OperationRun::factory()->forTenant($environment)->create([
|
||||
'type' => OperationRunType::ReviewPackGenerate->value,
|
||||
'status' => OperationRunStatus::Completed->value,
|
||||
'initiator_name' => 'Spec342 Operator',
|
||||
]);
|
||||
$review = spec342PublishedReview($environment, $user, $snapshot, [
|
||||
'control_interpretation' => [
|
||||
'version_key' => ComplianceEvidenceMappingV1::VERSION_KEY,
|
||||
'controls' => [
|
||||
[
|
||||
'control_key' => 'customer-output',
|
||||
'title' => 'Customer output',
|
||||
'readiness_bucket' => 'evidence_on_record',
|
||||
'readiness_label' => 'Evidence on record',
|
||||
'primary_reason' => 'Evidence path is complete.',
|
||||
'recommended_next_action' => 'Open the current customer review pack.',
|
||||
],
|
||||
],
|
||||
],
|
||||
'governance_package' => [
|
||||
'decision_summary' => [
|
||||
'status' => 'none',
|
||||
'evidence_state' => EnvironmentReviewCompletenessState::Complete->value,
|
||||
'decision_data_state' => 'complete',
|
||||
'total_count' => 0,
|
||||
'summary' => 'No governance decisions require customer awareness.',
|
||||
'next_action' => 'Open the current customer review pack.',
|
||||
'entries' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$pack = ReviewPack::factory()->ready()->create([
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'environment_review_id' => (int) $review->getKey(),
|
||||
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
||||
'operation_run_id' => (int) $run->getKey(),
|
||||
'initiated_by_user_id' => (int) $user->getKey(),
|
||||
]);
|
||||
$review->forceFill([
|
||||
'current_export_review_pack_id' => (int) $pack->getKey(),
|
||||
'operation_run_id' => (int) $run->getKey(),
|
||||
])->save();
|
||||
|
||||
$component = spec342WorkspaceComponent($user, $environment);
|
||||
|
||||
$component
|
||||
->assertSee('Is this review ready to share?')
|
||||
->assertSee('Ready to share')
|
||||
->assertSee('Stakeholders can use the current review pack and released review as the evidence path.')
|
||||
->assertSee('Review consumption flow')
|
||||
->assertSee('Review data')
|
||||
->assertSee('Findings triaged')
|
||||
->assertSee('Accepted risks reviewed')
|
||||
->assertSee('Customer-safe output')
|
||||
->assertSee('Findings needing attention')
|
||||
->assertSee('No open findings require customer action.')
|
||||
->assertSee('Review pack state')
|
||||
->assertSee('Export ready')
|
||||
->assertSee('Operation proof')
|
||||
->assertSee('Spec342 Operator')
|
||||
->assertDontSee('Auditor-ready')
|
||||
->assertDontSee('environment is healthy')
|
||||
->assertDontSee('compliant');
|
||||
|
||||
$html = $component->html();
|
||||
|
||||
expect(substr_count($html, 'data-testid="customer-review-primary-action"'))->toBe(1)
|
||||
->and(substr_count($html, 'data-testid="customer-review-readiness-step"'))->toBe(6)
|
||||
->and($html)->toContain('source_surface=customer_review_workspace')
|
||||
->and($html)->not->toContain('/admin/t/');
|
||||
});
|
||||
|
||||
it('shows not-ready proof states without raw diagnostics or false output claims', function (): void {
|
||||
$environment = ManagedEnvironment::factory()->create(['name' => 'Spec342 Evidence Missing']);
|
||||
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'readonly');
|
||||
$snapshot = seedPartialEnvironmentReviewEvidence($environment);
|
||||
$review = spec342PublishedReview($environment, $user, $snapshot, [
|
||||
'debug_payload' => 'raw payload should stay hidden',
|
||||
'provider_response' => 'provider response should stay hidden',
|
||||
'stack_trace' => 'stack trace should stay hidden',
|
||||
'source_fingerprint' => 'spec342-hidden-fingerprint',
|
||||
'control_interpretation' => [
|
||||
'version_key' => ComplianceEvidenceMappingV1::VERSION_KEY,
|
||||
'controls' => [
|
||||
[
|
||||
'control_key' => 'customer-output',
|
||||
'title' => 'Customer output',
|
||||
'readiness_bucket' => 'review_recommended',
|
||||
'readiness_label' => 'Review recommended',
|
||||
'primary_reason' => 'Evidence basis needs review.',
|
||||
'recommended_next_action' => 'Review evidence before sharing.',
|
||||
],
|
||||
],
|
||||
],
|
||||
'governance_package' => [
|
||||
'decision_summary' => [
|
||||
'status' => 'incomplete',
|
||||
'total_count' => 1,
|
||||
'summary' => 'Decision evidence is incomplete for this released review.',
|
||||
'next_action' => 'Review the evidence basis before relying on the decision summary.',
|
||||
'entries' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
$pack = ReviewPack::factory()->ready()->create([
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'workspace_id' => (int) $environment->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();
|
||||
|
||||
$component = spec342WorkspaceComponent($user, $environment)
|
||||
->assertSee('Follow-up required before sharing')
|
||||
->assertSee('Evidence incomplete')
|
||||
->assertSee('Customer-safe output')
|
||||
->assertSee('Not ready')
|
||||
->assertSee('Diagnostics')
|
||||
->assertSee('Collapsed')
|
||||
->assertDontSee('Ready to share')
|
||||
->assertDontSee('Export ready')
|
||||
->assertDontSee('Download review pack')
|
||||
->assertDontSee('raw payload should stay hidden')
|
||||
->assertDontSee('provider response should stay hidden')
|
||||
->assertDontSee('stack trace should stay hidden')
|
||||
->assertDontSee('spec342-hidden-fingerprint');
|
||||
|
||||
expect($component->html())->not->toContain('data-testid="customer-review-diagnostics" open');
|
||||
});
|
||||
|
||||
it('uses concrete findings follow-up copy and keeps review-pack download primary-only', function (): void {
|
||||
$environment = ManagedEnvironment::factory()->create(['name' => 'Spec342 Findings Follow-up']);
|
||||
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'manager');
|
||||
$snapshot = seedEnvironmentReviewEvidence($environment, findingCount: 0, driftCount: 0);
|
||||
$run = OperationRun::factory()->forTenant($environment)->create([
|
||||
'type' => OperationRunType::ReviewPackGenerate->value,
|
||||
'status' => OperationRunStatus::Completed->value,
|
||||
'initiator_name' => 'Spec342 Operator',
|
||||
]);
|
||||
$review = spec342PublishedReview($environment, $user, $snapshot);
|
||||
|
||||
$pack = ReviewPack::factory()->ready()->create([
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'environment_review_id' => (int) $review->getKey(),
|
||||
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
||||
'operation_run_id' => (int) $run->getKey(),
|
||||
'initiated_by_user_id' => (int) $user->getKey(),
|
||||
]);
|
||||
$review->forceFill([
|
||||
'current_export_review_pack_id' => (int) $pack->getKey(),
|
||||
'operation_run_id' => (int) $run->getKey(),
|
||||
])->save();
|
||||
|
||||
Finding::factory()->create([
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'severity' => Finding::SEVERITY_CRITICAL,
|
||||
'status' => Finding::STATUS_NEW,
|
||||
]);
|
||||
|
||||
$component = spec342WorkspaceComponent($user, $environment)
|
||||
->assertSee('Follow-up required before sharing')
|
||||
->assertSee('1 open finding needs attention; 1 is high impact. Keep open findings visible before customer handoff.')
|
||||
->assertSee('Do not treat this review as share-ready until open findings are resolved, accepted, or explicitly reviewed.')
|
||||
->assertSee('Open review')
|
||||
->assertSee('Review pack state')
|
||||
->assertDontSee('Download review pack')
|
||||
->assertDontSee('TenantPilot recorded an access, scope, or configuration issue');
|
||||
|
||||
$html = $component->html();
|
||||
|
||||
expect(substr_count($html, 'data-testid="customer-review-primary-action"'))->toBe(1)
|
||||
->and($html)->toContain('data-testid="customer-review-findings-summary"')
|
||||
->and($html)->not->toContain('data-testid="customer-review-readiness-dimensions"');
|
||||
});
|
||||
|
||||
it('surfaces accepted-risk owner rationale and missing review dates when repo-backed', function (): void {
|
||||
$environment = ManagedEnvironment::factory()->create(['name' => 'Spec342 Accepted Risk']);
|
||||
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'readonly');
|
||||
$owner = User::factory()->create(['name' => 'Spec342 Risk Owner']);
|
||||
$finding = Finding::factory()->create([
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'status' => Finding::STATUS_RISK_ACCEPTED,
|
||||
]);
|
||||
$findingWithoutReviewDate = Finding::factory()->create([
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'status' => Finding::STATUS_RISK_ACCEPTED,
|
||||
]);
|
||||
FindingException::query()->create([
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'finding_id' => (int) $findingWithoutReviewDate->getKey(),
|
||||
'status' => FindingException::STATUS_ACTIVE,
|
||||
'current_validity_state' => FindingException::VALIDITY_VALID,
|
||||
'requested_by_user_id' => (int) $user->getKey(),
|
||||
'request_reason' => 'Customer-approved maintenance window.',
|
||||
'owner_user_id' => (int) $owner->getKey(),
|
||||
'approved_by_user_id' => (int) $owner->getKey(),
|
||||
'requested_at' => now()->subDays(3),
|
||||
'approved_at' => now()->subDays(2),
|
||||
'effective_from' => now()->subDays(2),
|
||||
'review_due_at' => now()->addDays(30),
|
||||
]);
|
||||
FindingException::query()->create([
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'finding_id' => (int) $finding->getKey(),
|
||||
'status' => FindingException::STATUS_ACTIVE,
|
||||
'current_validity_state' => FindingException::VALIDITY_EXPIRING,
|
||||
'requested_by_user_id' => (int) $user->getKey(),
|
||||
'request_reason' => 'Pending owner confirmation.',
|
||||
'requested_at' => now()->subDay(),
|
||||
'approved_at' => now()->subDay(),
|
||||
'effective_from' => now()->subDay(),
|
||||
'review_due_at' => null,
|
||||
'expires_at' => null,
|
||||
]);
|
||||
|
||||
$snapshot = seedEnvironmentReviewEvidence($environment, findingCount: 0, driftCount: 0);
|
||||
spec342PublishedReview($environment, $user, $snapshot, [
|
||||
'governance_package' => [
|
||||
'accepted_risks' => [
|
||||
[
|
||||
'title' => 'Accepted risk renewal',
|
||||
'governance_state' => 'expiring_exception',
|
||||
'customer_summary' => 'Accepted risk requires customer awareness.',
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
spec342WorkspaceComponent($user, $environment)
|
||||
->assertSee('Accepted risks')
|
||||
->assertSee('2')
|
||||
->assertSee('Spec342 Risk Owner')
|
||||
->assertSee(now()->addDays(30)->toDateString())
|
||||
->assertSee('Customer-approved maintenance window.')
|
||||
->assertSee('Review date not recorded')
|
||||
->assertSee('Accepted risk requires customer awareness.')
|
||||
->assertDontSee('raw operation');
|
||||
});
|
||||
|
||||
it('keeps environment_id as the only canonical workspace filter and rejects cross-workspace targets', function (): void {
|
||||
$allowed = ManagedEnvironment::factory()->create(['name' => 'Spec342 Allowed Environment']);
|
||||
[$user, $allowed] = createUserWithTenant(tenant: $allowed, role: 'readonly');
|
||||
$other = ManagedEnvironment::factory()->create([
|
||||
'workspace_id' => (int) $allowed->workspace_id,
|
||||
'name' => 'Spec342 Other Environment',
|
||||
]);
|
||||
createUserWithTenant(tenant: $other, user: $user, role: 'readonly');
|
||||
$foreign = ManagedEnvironment::factory()->create(['name' => 'Spec342 Foreign Environment']);
|
||||
|
||||
spec342PublishedReview($allowed, $user, seedEnvironmentReviewEvidence($allowed));
|
||||
spec342PublishedReview($other, $user, seedEnvironmentReviewEvidence($other));
|
||||
|
||||
$this->actingAs($user)->withSession([WorkspaceContext::SESSION_KEY => (int) $allowed->workspace_id]);
|
||||
|
||||
Livewire::withQueryParams([
|
||||
'environment_id' => (int) $allowed->getKey(),
|
||||
'tenant' => (string) $other->external_id,
|
||||
'tenant_id' => (int) $other->getKey(),
|
||||
])
|
||||
->actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertSet('tableFilters.managed_environment_id.value', (string) $allowed->getKey())
|
||||
->assertCanSeeTableRecords([$allowed->fresh()])
|
||||
->assertCanNotSeeTableRecords([$other->fresh()])
|
||||
->assertSee('Environment filter:')
|
||||
->assertSee('Spec342 Allowed Environment')
|
||||
->assertDontSee('/admin/t', false)
|
||||
->assertDontSee('tenant_id=', false);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $allowed->workspace_id])
|
||||
->get(CustomerReviewWorkspace::getUrl(panel: 'admin').'?environment_id='.(string) $foreign->getKey())
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
function spec342WorkspaceComponent(User $user, ManagedEnvironment $environment): mixed
|
||||
{
|
||||
$workspaceId = (int) $environment->workspace_id;
|
||||
|
||||
session()->put(WorkspaceContext::SESSION_KEY, $workspaceId);
|
||||
setAdminPanelContext();
|
||||
|
||||
return Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class);
|
||||
}
|
||||
|
||||
function spec342PublishedReview(
|
||||
ManagedEnvironment $environment,
|
||||
User $user,
|
||||
\App\Models\EvidenceSnapshot $snapshot,
|
||||
array $summaryOverrides = [],
|
||||
): \App\Models\EnvironmentReview {
|
||||
$review = composeEnvironmentReviewForTest($environment, $user, $snapshot);
|
||||
$summary = array_replace_recursive(is_array($review->summary) ? $review->summary : [], $summaryOverrides);
|
||||
|
||||
$review->forceFill([
|
||||
'status' => EnvironmentReviewStatus::Published->value,
|
||||
'summary' => $summary,
|
||||
'generated_at' => now(),
|
||||
'published_at' => now(),
|
||||
'published_by_user_id' => (int) $user->getKey(),
|
||||
])->save();
|
||||
|
||||
return $review->refresh();
|
||||
}
|
||||
@ -192,9 +192,9 @@
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertSee('Customer Review Workspace')
|
||||
->assertSee('Is this review ready to share?')
|
||||
->assertSee('Readiness')
|
||||
->assertSee('Review consumption flow')
|
||||
->assertSee('Evidence')
|
||||
->assertSee('Accepted risk status')
|
||||
->assertSee('Findings needing attention')
|
||||
->assertSee('Evidence path')
|
||||
->assertSeeHtml('data-testid="customer-review-evidence-aside"')
|
||||
->assertSee('Review pack state')
|
||||
@ -218,7 +218,8 @@
|
||||
->assertSee('Hidden')
|
||||
->assertSee('Support details stay on authorized diagnostic surfaces')
|
||||
->assertSee('Customer acceptance checkpoint')
|
||||
->assertSee('Download review pack')
|
||||
->assertSee('Open review')
|
||||
->assertDontSee('Download review pack')
|
||||
->assertDontSee('raw payload should stay hidden')
|
||||
->assertDontSee('stack trace should stay hidden')
|
||||
->assertDontSee('internal exception should stay hidden')
|
||||
@ -289,12 +290,14 @@
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertSee('Is this review ready to share?')
|
||||
->assertSee('Shareable with follow-up')
|
||||
->assertSee('accepted-risk follow-up must be called out before handoff')
|
||||
->assertSee('Accepted-risk follow-up is recorded for this review')
|
||||
->assertSee('The pack can be shared only with the accepted-risk context included in the customer handoff.')
|
||||
->assertSee('Review needed')
|
||||
->assertSee('Follow-up required')
|
||||
->assertSee('Accepted-risk follow-up is required.')
|
||||
->assertSee('Download review pack')
|
||||
->assertSeeInOrder(['Shareable with follow-up', 'Open review', 'Download review pack'])
|
||||
->assertSee('Open review')
|
||||
->assertSeeInOrder(['Shareable with follow-up', 'Open review'])
|
||||
->assertDontSee('Download review pack')
|
||||
->assertDontSee('Ready to share');
|
||||
});
|
||||
|
||||
@ -559,11 +562,11 @@
|
||||
->assertSee('Review needed')
|
||||
->assertSee('Open review')
|
||||
->assertDontSee('Ready for release')
|
||||
->assertDontSee('Risk Owner')
|
||||
->assertDontSee('Vendor patch window accepted by the customer.')
|
||||
->assertSee('Risk Owner')
|
||||
->assertSee('Vendor patch window accepted by the customer.')
|
||||
->assertSee(now()->addDays(14)->toDateString())
|
||||
->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');
|
||||
});
|
||||
|
||||
|
||||
@ -0,0 +1 @@
|
||||
|
||||
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 299 KiB |
|
After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 327 KiB |
|
After Width: | Height: | Size: 327 KiB |
|
After Width: | Height: | Size: 331 KiB |
@ -0,0 +1,61 @@
|
||||
# Specification Quality Checklist: Spec 342 - Customer Review Workspace v1 Final Consumption Productization
|
||||
|
||||
**Purpose**: Validate Spec 342 preparation completeness before implementation.
|
||||
**Created**: 2026-06-01
|
||||
**Feature**: `specs/342-customer-review-workspace-final-consumption-productization/spec.md`
|
||||
|
||||
## Candidate Selection Gate
|
||||
|
||||
- [x] CHK001 The selected candidate is directly user-provided as Spec 342 and maps to `customer-review-workspace-v1-completion` in `docs/product/spec-candidates.md`.
|
||||
- [x] CHK002 The selected candidate aligns with the top current roadmap sellability priority: Customer Review Workspace v1 Completion.
|
||||
- [x] CHK003 No existing `specs/342-*` package or branch was found before Spec Kit creation.
|
||||
- [x] CHK004 Related completed/prepared specs were checked and are context only: 249, 258, 312, 326, 329, 335, 336, 337, 340, and 341.
|
||||
- [x] CHK005 Close alternatives are deferred rather than hidden scope: localization, governance inbox, commercial entitlements, provider readiness, and PSA/support handoff.
|
||||
- [x] CHK006 The scope is a small final-consumption productization slice on the existing `/admin/reviews/workspace` surface.
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] CHK007 `spec.md` defines problem, user value, functional requirements, non-goals, acceptance criteria, success criteria, assumptions, risks, and open questions.
|
||||
- [x] CHK008 `plan.md` lists likely affected repo surfaces and distinguishes implementation touch points from supporting surfaces to inspect.
|
||||
- [x] CHK009 `tasks.md` is ordered into small phases with tests, browser smoke, UI coverage, validation, and explicit non-goals.
|
||||
- [x] CHK010 `repo-truth-map.md` exists and classifies current review/evidence/pack/finding/accepted-risk/audit/operation truth.
|
||||
- [x] CHK011 `customer-review-consumption-state-contract.md` exists and defines derived display states without adding persisted truth.
|
||||
- [x] CHK012 No unresolved template placeholders remain in `spec.md`, `plan.md`, or `tasks.md`.
|
||||
|
||||
## Constitution And Scope
|
||||
|
||||
- [x] CHK013 Spec Candidate Check is filled, scored 11/12, and approved as Core Enterprise.
|
||||
- [x] CHK014 Proportionality review is present for the possible page-local presenter/state contract.
|
||||
- [x] CHK015 The spec does not introduce new migrations, persisted readiness truth, enum/status family, provider framework, or portal architecture.
|
||||
- [x] CHK016 Workspace/environment isolation, deny-as-not-found semantics, and canonical `environment_id` behavior are explicit.
|
||||
- [x] CHK017 UI Surface Impact and UI/Productization Coverage are completed for an existing strategic customer-facing surface.
|
||||
- [x] CHK018 Filament v5 / Livewire v4 compliance, panel provider location, global-search posture, destructive-action rules, asset strategy, and testing plan are explicit.
|
||||
|
||||
## Plan Quality
|
||||
|
||||
- [x] CHK019 The plan sequences work as repo-truth gate → tests first → state contract/presenter → first-screen productization → RBAC/context → browser smoke → validation.
|
||||
- [x] CHK020 The plan states no migrations, env vars, packages, queues, scheduler, storage, Graph scopes, Dokploy, or Filament asset changes are expected.
|
||||
- [x] CHK021 The plan requires spec/plan update before any high-impact action, asset registration, backend generation, or persisted truth scope appears.
|
||||
- [x] CHK022 Test governance is explicit: confidence + one bounded browser smoke.
|
||||
|
||||
## Task Quality
|
||||
|
||||
- [x] CHK023 Tasks include concrete file paths and avoid inventing runtime paths beyond likely implementation touch points.
|
||||
- [x] CHK024 Tasks include Feature/Livewire tests before UI implementation.
|
||||
- [x] CHK025 Tasks include browser states and screenshot artifacts while allowing unreachable states to be documented rather than faked.
|
||||
- [x] CHK026 Tasks include UI coverage close-out and final validation commands.
|
||||
- [x] CHK027 Tasks explicitly forbid rewriting completed specs or reintroducing `/admin/t`/legacy query aliases.
|
||||
|
||||
## Spec Readiness Gate
|
||||
|
||||
- [x] CHK028 `spec.md`, `plan.md`, and `tasks.md` exist.
|
||||
- [x] CHK029 Required supporting prep artifacts exist: `repo-truth-map.md`, `customer-review-consumption-state-contract.md`, and `checklists/requirements.md`.
|
||||
- [x] CHK030 No open question blocks safe implementation; unsupported runtime concepts are explicitly unavailable/deferred.
|
||||
- [x] CHK031 The implementation scope is bounded enough for a later implementation loop.
|
||||
- [x] CHK032 Result: ready for implementation loop.
|
||||
|
||||
## Review Outcome
|
||||
|
||||
- [x] CHK033 Review outcome class: acceptable-special-case.
|
||||
- [x] CHK034 Workflow outcome: keep.
|
||||
- [x] CHK035 Final note location: active feature PR close-out entry `Guardrail / Exception / Smoke Coverage`.
|
||||
@ -0,0 +1,192 @@
|
||||
# Spec 342 - Customer Review Consumption State Contract
|
||||
|
||||
Status: implemented
|
||||
Created: 2026-06-01
|
||||
Scope: Customer Review Workspace first-screen consumption states
|
||||
|
||||
This contract defines display states for Customer Review Workspace. These states are presentation labels derived from existing repo truth. They are not a new enum, lifecycle family, persisted status, or platform workflow framework.
|
||||
|
||||
## Universal Rules
|
||||
|
||||
- Default-visible content must answer: status, reason, impact, and primary next action.
|
||||
- Primary next action must be singular per rendered state.
|
||||
- Diagnostics default is `Collapsed` or `Unavailable`.
|
||||
- Raw provider JSON, raw OperationRun payload, fingerprints, stack traces, internal IDs as primary labels, and platform reason families are hidden by default.
|
||||
- Customer-safe, auditor-ready, evidence-backed, export-ready, healthy, complete, or compliant claims appear only when repo-backed.
|
||||
- OperationRun proof is not evidence output, review-pack output, or customer-safe readiness.
|
||||
- External delivery and attestation are unavailable unless implementation discovers repo-backed truth and updates this contract/spec first.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Spec 342 implemented these states as presentation-only, page-local derived payloads on the existing Customer Review Workspace.
|
||||
- Open findings block a ready-to-share claim even when evidence and review-pack truth are available; the pack/export state remains visible but customer-safe output is `Needs review`.
|
||||
- Accepted-risk summaries are visible by default when repo-backed; full accepted-risk records remain collapsed.
|
||||
- Findings and accepted-risk follow-up states use concrete decision-card copy based on repo-backed counts, owners, rationale, and review-date context instead of generic issue text.
|
||||
- The prior repeated readiness summary cards are absorbed into the six-step consumption flow and right-rail proof panels to reduce duplicate status surfaces.
|
||||
- `Download review pack` appears only as the primary action when the review is actually ready to share; follow-up states keep review-pack availability as status context and route the primary action to the review.
|
||||
- Diagnostics remain collapsed by default, and raw provider/support payloads, stack traces, fingerprints, and raw OperationRun JSON are not rendered in the default customer-safe surface.
|
||||
|
||||
## Flow Steps
|
||||
|
||||
The review readiness flow uses these steps:
|
||||
|
||||
1. Review data
|
||||
2. Evidence
|
||||
3. Findings triaged
|
||||
4. Accepted risks reviewed
|
||||
5. Review pack
|
||||
6. Customer-safe output
|
||||
|
||||
## Presentation Vocabulary
|
||||
|
||||
- `Available`
|
||||
- `Missing`
|
||||
- `Required`
|
||||
- `Generating`
|
||||
- `Failed`
|
||||
- `Ready`
|
||||
- `Needs review`
|
||||
- `Not ready`
|
||||
- `Unavailable`
|
||||
- `Collapsed`
|
||||
- `Deferred`
|
||||
|
||||
## State Contracts
|
||||
|
||||
### 1. Review Not Ready
|
||||
|
||||
| Field | Contract |
|
||||
|---|---|
|
||||
| Visible status | Review not ready |
|
||||
| Reason | Required review data or evidence is missing. |
|
||||
| Impact | This review should not be shared as customer-ready yet. |
|
||||
| Primary next action | Complete review preparation, only if repo-supported and authorized; otherwise show unavailable. |
|
||||
| Findings summary | Unavailable or deferred unless repo-backed review/finding data exists. |
|
||||
| Accepted risks state | Unavailable or deferred unless repo-backed exception data exists. |
|
||||
| Evidence state | Missing or unavailable. |
|
||||
| Review pack state | Unavailable. |
|
||||
| Customer-safe output state | Not ready. |
|
||||
| Export state | Unavailable. |
|
||||
| Diagnostics default | Collapsed/unavailable. |
|
||||
|
||||
### 2. Review Ready, Evidence Missing
|
||||
|
||||
| Field | Contract |
|
||||
|---|---|
|
||||
| Visible status | Review ready, evidence missing |
|
||||
| Reason | Review summary exists, but supporting evidence is not available. |
|
||||
| Impact | Findings can be discussed, but the review is not evidence-backed yet. |
|
||||
| Primary next action | Open review or generate/open evidence only when repo-supported and authorized. |
|
||||
| Findings summary | Available when review-derived or finding data is repo-backed. |
|
||||
| Accepted risks state | Available when exception data is repo-backed. |
|
||||
| Evidence state | Missing. |
|
||||
| Review pack state | Required or unavailable. |
|
||||
| Customer-safe output state | Needs review or not ready; never evidence-backed. |
|
||||
| Export state | Unavailable unless a repo-backed review pack file is independent and valid. |
|
||||
| Diagnostics default | Collapsed. |
|
||||
|
||||
### 3. Review Ready With Evidence
|
||||
|
||||
| Field | Contract |
|
||||
|---|---|
|
||||
| Visible status | Review ready with evidence |
|
||||
| Reason | Review summary and supporting evidence are available. |
|
||||
| Impact | Customer stakeholders can consume the review and inspect evidence. |
|
||||
| Primary next action | Review findings or open evidence, based on repo-backed attention state. |
|
||||
| Findings summary | Open/attention counts where repo-backed. |
|
||||
| Accepted risks state | Visible if accepted risks exist. |
|
||||
| Evidence state | Available. |
|
||||
| Review pack state | Required, generating, failed, or available from `ReviewPack` truth. |
|
||||
| Customer-safe output state | Needs review or ready only when review/pack truth supports it. |
|
||||
| Export state | Available only from ready non-expired pack file truth. |
|
||||
| Diagnostics default | Collapsed. |
|
||||
|
||||
### 4. Review Pack Required
|
||||
|
||||
| Field | Contract |
|
||||
|---|---|
|
||||
| Visible status | Review pack required |
|
||||
| Reason | Review data exists, but no review pack is available. |
|
||||
| Impact | Customer-safe export/download is not ready yet. |
|
||||
| Primary next action | Generate review pack only when existing repo action is supported and authorized; otherwise open review/evidence. |
|
||||
| Findings summary | Available if repo-backed. |
|
||||
| Accepted risks state | Available if repo-backed. |
|
||||
| Evidence state | Available or missing from evidence truth. |
|
||||
| Review pack state | Required. |
|
||||
| Customer-safe output state | Not ready or needs review. |
|
||||
| Export state | Unavailable. |
|
||||
| Diagnostics default | Collapsed. |
|
||||
|
||||
### 5. Review Pack Available
|
||||
|
||||
| Field | Contract |
|
||||
|---|---|
|
||||
| Visible status | Review pack available |
|
||||
| Reason | A review pack is available for this review. |
|
||||
| Impact | The review can be consumed and exported according to workspace policy if the pack is repo-backed as customer-safe and authorized. |
|
||||
| Primary next action | Open review pack or download review pack when authorized and file metadata supports it. |
|
||||
| Findings summary | Still visible; pack availability must not hide findings needing attention. |
|
||||
| Accepted risks state | Still visible; accepted risks must not be hidden in diagnostics. |
|
||||
| Evidence state | Available or needs review based on linked evidence. |
|
||||
| Review pack state | Available. |
|
||||
| Customer-safe output state | Ready only when repo-backed; otherwise needs review. |
|
||||
| Export state | Available only for ready, non-expired pack with file metadata and authorization. |
|
||||
| Diagnostics default | Collapsed. |
|
||||
|
||||
### 6. Findings Need Attention
|
||||
|
||||
| Field | Contract |
|
||||
|---|---|
|
||||
| Visible status | Findings need attention |
|
||||
| Reason | One or more findings require customer review or operator follow-up. |
|
||||
| Impact | Review should not be treated as complete until findings are assigned, accepted, resolved, or otherwise addressed. |
|
||||
| Primary next action | Review findings. |
|
||||
| Findings summary | Required; show repo-backed counts and customer-safe row preview. |
|
||||
| Accepted risks state | Visible separately if accepted risks also exist. |
|
||||
| Evidence state | Available/missing from evidence truth. |
|
||||
| Review pack state | Available/required/generating/failed from pack truth. |
|
||||
| Customer-safe output state | Needs review unless repo truth proves no action remains. |
|
||||
| Export state | Available only when file truth supports it; export does not suppress findings attention. |
|
||||
| Diagnostics default | Collapsed. |
|
||||
|
||||
### 7. Accepted Risks Present
|
||||
|
||||
| Field | Contract |
|
||||
|---|---|
|
||||
| Visible status | Accepted risks present |
|
||||
| Reason | Some findings have accepted risk decisions. |
|
||||
| Impact | Customer stakeholders should review owner, rationale, and expiry/review date where available. |
|
||||
| Primary next action | Review accepted risks. |
|
||||
| Findings summary | Shows related finding context where repo-backed. |
|
||||
| Accepted risks state | Required; show owner/rationale/expiry/review date if present and missing-date copy if not. |
|
||||
| Evidence state | Available/missing from evidence truth. |
|
||||
| Review pack state | Available/required/generating/failed from pack truth. |
|
||||
| Customer-safe output state | Needs review or ready depending on validity and review-pack truth. |
|
||||
| Export state | Available only when file truth supports it. |
|
||||
| Diagnostics default | Collapsed. |
|
||||
|
||||
### 8. No Findings Requiring Action
|
||||
|
||||
| Field | Contract |
|
||||
|---|---|
|
||||
| Visible status | No findings requiring action |
|
||||
| Reason | This review has no open findings requiring customer action. |
|
||||
| Impact | Review can be consumed or exported if evidence and review pack are available. |
|
||||
| Primary next action | Open review pack or open evidence when authorized and repo-backed. |
|
||||
| Findings summary | Shows zero-action state only if repo-backed. |
|
||||
| Accepted risks state | Shows none/current state only if repo-backed. |
|
||||
| Evidence state | Available/missing from evidence truth. |
|
||||
| Review pack state | Available/required/generating/failed from pack truth. |
|
||||
| Customer-safe output state | Ready only when evidence/review-pack truth supports it. |
|
||||
| Export state | Available only when file truth supports it. |
|
||||
| Diagnostics default | Collapsed. |
|
||||
|
||||
## Unavailable Or Deferred Concepts
|
||||
|
||||
| Concept | Default Contract |
|
||||
|---|---|
|
||||
| Customer acknowledgement / attestation | Do not implement in Spec 342. Show unavailable/deferred copy only if useful. |
|
||||
| External delivery / email / PSA handoff | Do not implement in Spec 342. Show unavailable/deferred copy only if useful. |
|
||||
| Auditor-ready certification | Do not claim unless future repo truth supports certification semantics. |
|
||||
| Compliance/healthy claim | Do not claim from absence of findings alone. |
|
||||
| Generic customer portal | Deferred follow-up outside Spec 342. |
|
||||
@ -0,0 +1,267 @@
|
||||
# Implementation Plan: Spec 342 - Customer Review Workspace v1 Final Consumption Productization
|
||||
|
||||
**Branch**: `342-customer-review-workspace-final-consumption-productization` | **Date**: 2026-06-01 | **Spec**: `specs/342-customer-review-workspace-final-consumption-productization/spec.md`
|
||||
**Input**: User-provided Spec 342 draft + roadmap candidate `customer-review-workspace-v1-completion`.
|
||||
|
||||
## Summary
|
||||
|
||||
Finalize the existing Customer Review Workspace as a customer-safe consumption surface.
|
||||
|
||||
This is a runtime UX productization slice only:
|
||||
|
||||
- no new customer portal
|
||||
- no backend review/evidence/review-pack generation rewrite
|
||||
- no new persistence or lifecycle/status family
|
||||
- no new Graph/provider calls
|
||||
- no new routes unless implementation proves a narrow existing route/action link gap
|
||||
- no shell/sidebar/topbar or canonical-link cleanup work
|
||||
|
||||
The implementation should make the first screen answer:
|
||||
|
||||
`Is this review ready to consume, what requires attention, and what evidence/review-pack/export truth supports it?`
|
||||
|
||||
## Technical Context
|
||||
|
||||
- **Language/Version**: PHP 8.4.15, Laravel 12.52.x.
|
||||
- **Primary Dependencies**: Filament v5.2.x, Livewire v4.1.x, Pest v4, Tailwind CSS v4.
|
||||
- **Storage**: PostgreSQL; no schema change expected.
|
||||
- **Testing**: Pest Feature/Livewire tests + one Pest Browser smoke file.
|
||||
- **Validation Lanes**: confidence + browser.
|
||||
- **Target Platform**: `apps/platform` Laravel monolith; Sail locally; Dokploy/container deployment posture unchanged.
|
||||
- **Project Type**: Web application.
|
||||
- **Performance Goals**: DB-only page render; no Microsoft Graph/provider calls during render; reuse eager-loaded relationships where summary state depends on related review/evidence/pack/finding records.
|
||||
- **Constraints**: no false customer-safe/auditor-ready/export-ready/evidence-backed/compliant claims; no raw diagnostics by default; no cross-workspace/environment leakage; no `/admin/t` or legacy query alias resurrection.
|
||||
- **Scale/Scope**: one existing strategic page, related proof/detail links, focused tests, one browser smoke, and spec-local artifacts.
|
||||
|
||||
## UI / Surface Guardrail Plan
|
||||
|
||||
- **Guardrail scope**: material change to an existing strategic customer-safe review surface.
|
||||
- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**:
|
||||
- `/admin/reviews/workspace`
|
||||
- `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`
|
||||
- `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php`
|
||||
- existing linked proof/action surfaces for environment reviews, evidence snapshots, review packs, findings/accepted risks, audit, and operation proof.
|
||||
- **No-impact class, if applicable**: N/A.
|
||||
- **Native vs custom classification summary**: mixed, but existing route uses a native Filament page with Blade composition. New/changed UI should use Filament/shared primitives first.
|
||||
- **Shared-family relevance**: customer-safe review consumption, evidence/report viewer, review pack/export links, status messaging, OperationRun proof links.
|
||||
- **State layers in scope**: page payload, URL-query filter, proof/detail links, diagnostics disclosure.
|
||||
- **Audience modes in scope**: customer/read-only, MSP operator, auditor/account manager, support where authorized.
|
||||
- **Navigation / Filament provider-panel handling**: no navigation or panel provider change expected. Panel providers remain registered in `apps/platform/bootstrap/providers.php`.
|
||||
- **Screenshot or page-report need**: screenshots required under this spec package during browser smoke. UI audit registry update is required only if implementation changes route/archetype/navigation; otherwise record active close-out no-registry-change rationale.
|
||||
|
||||
## Shared Pattern & System Fit
|
||||
|
||||
- **Cross-cutting feature marker**: yes.
|
||||
- **Systems touched**:
|
||||
- Customer Review Workspace page/view/payload helpers.
|
||||
- Spec 337-style evidence/review-pack process-flow semantics.
|
||||
- Existing `ArtifactTruthPresenter` and badge helpers where available.
|
||||
- Existing `OperationRunLinks` for operation proof links.
|
||||
- Existing signed review-pack download route/controller.
|
||||
- Existing policy/capability/UI enforcement paths.
|
||||
- **Shared abstractions reused**: existing review/evidence/review-pack status enums, resource URLs, workspace hub environment filter, `OperationRunLinks`, `ArtifactTruthPresenter`, `BadgeCatalog` / `BadgeRenderer`, `UiEnforcement` where applicable.
|
||||
- **New abstraction introduced? why?**: none by default. A small derived `CustomerReviewWorkspacePresenter` may be introduced only if it consolidates scattered page/view state and remains page-local.
|
||||
- **Why the existing abstraction was sufficient or insufficient**: current models and services provide truth; current page may not centralize final consumption state. A local presenter can reduce duplicate Blade/Page logic without becoming a framework.
|
||||
- **Bounded deviation / spread control**: do not create a generic readiness engine, global state taxonomy, new interface, registry, or persisted truth.
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: existing proof links only.
|
||||
- **Central contract reused**: existing OperationRun link/resource helpers and authorization.
|
||||
- **Delegated UX behaviors**: operation deep-link URL resolution stays in shared helpers/resources; no new operation start/toast/notification lifecycle.
|
||||
- **Surface-owned behavior kept local**: explanatory distinction between operation proof and customer evidence/review-pack/export truth.
|
||||
- **Queued DB-notification policy**: N/A.
|
||||
- **Terminal notification path**: unchanged.
|
||||
- **Exception path**: none.
|
||||
|
||||
## Provider Boundary & Portability Fit
|
||||
|
||||
- **Shared provider/platform boundary touched?**: no new provider boundary.
|
||||
- **Provider-owned seams**: N/A.
|
||||
- **Platform-core seams**: review/evidence/review-pack/finding/audit/operation proof presentation over existing workspace/environment artifacts.
|
||||
- **Neutral platform terms / contracts preserved**: workspace, environment, customer review, evidence, review pack, export, accepted risk, finding, operation proof, audit trail, diagnostics.
|
||||
- **Retained provider-specific semantics and why**: only inside existing customer-safe review/evidence content where repo-backed.
|
||||
- **Bounded extraction or follow-up path**: provider readiness remains a follow-up spec, not part of Spec 342.
|
||||
|
||||
## Current Repo Truth Summary
|
||||
|
||||
- `CustomerReviewWorkspace` already exists as a Filament page with slug `reviews/workspace`, workspace-wide navigation, canonical `environment_id` filtering, header clear-filter action, table context, and existing page-open audit logging.
|
||||
- The current Blade view already renders decision-card, readiness-dimension, follow-up, review-package-index, evidence-aside, review-pack-state, accepted-risk-summary, and diagnostics areas.
|
||||
- `EnvironmentReview` owns review status, completeness, summary, publication timestamps, evidence snapshot, operation run, sections, review packs, and current export review pack.
|
||||
- `EvidenceSnapshot` owns evidence status/completeness, generated/expiry timestamps, items, linked review packs, linked reviews, and operation proof.
|
||||
- `ReviewPack` owns queued/generating/ready/failed/expired state, file metadata, expiry, linked evidence snapshot, environment review, operation run, and signed-download eligibility.
|
||||
- `Finding` and `FindingException` provide finding severity/status, owner/assignee/due fields, accepted-risk/exception status, current validity, owner/rationale/review dates, decision history, and evidence references where loaded.
|
||||
- `ReviewPackDownloadController`, `ReviewPackService`, and review-pack tests already prove signed download and authorization paths.
|
||||
- Existing relevant tests include `tests/Feature/Reviews/*`, `tests/Feature/ReviewPack/*`, `tests/Feature/Evidence/*`, `tests/Feature/Filament/Spec337EvidenceReviewPackProductFlowTest.php`, `tests/Browser/Spec326CustomerReviewWorkspaceProductizationSmokeTest.php`, and `tests/Browser/Spec337EvidenceReviewPackProductFlowSmokeTest.php`.
|
||||
- Related resources inspected for global-search posture should remain disabled or safe; this spec must not enable global search.
|
||||
|
||||
## Implementation Approach
|
||||
|
||||
### Phase 0 — Repo Truth Gate
|
||||
|
||||
1. Re-read this spec, plan, tasks, `repo-truth-map.md`, and `customer-review-consumption-state-contract.md`.
|
||||
2. Re-inspect current Customer Review Workspace page/view and related tests before editing.
|
||||
3. Update `repo-truth-map.md` with any runtime discoveries before code changes.
|
||||
4. Mark unsupported concepts such as acknowledgement/attestation or external delivery as unavailable/deferred.
|
||||
|
||||
### Phase 1 — Tests First
|
||||
|
||||
1. Add Feature/Livewire tests for decision-card status/reason/impact/next-action and hidden raw diagnostics.
|
||||
2. Add tests for findings/accepted-risk summaries and unsupported fields.
|
||||
3. Add tests for evidence/review-pack/export/operation proof separation.
|
||||
4. Add RBAC/context tests for cross-workspace environment filter, diagnostics capability, authorized review-pack download/open actions, and no legacy query alias resurrection.
|
||||
|
||||
### Phase 2 — Consumption State Contract
|
||||
|
||||
1. Use the spec-local contract as the mapping source for visible states.
|
||||
2. If needed, create a small `CustomerReviewWorkspacePresenter` or page-local payload builder that computes:
|
||||
- status, reason, impact, primary next action
|
||||
- review readiness flow
|
||||
- findings summary
|
||||
- accepted-risk summary
|
||||
- evidence state
|
||||
- review-pack state
|
||||
- export state
|
||||
- proof items
|
||||
- diagnostics state
|
||||
3. Keep states derived from existing models only.
|
||||
|
||||
### Phase 3 — First-Screen Productization
|
||||
|
||||
1. Ensure the first viewport leads with the decision card.
|
||||
2. Ensure findings and accepted risks are visible customer-safe summaries.
|
||||
3. Ensure evidence, review-pack, export, audit, and operation proof are visible as secondary proof, not raw diagnostics.
|
||||
4. Keep the review package index/table as secondary context.
|
||||
5. Keep diagnostics collapsed or unavailable by default.
|
||||
|
||||
### Phase 4 — Actions, RBAC, And Safety
|
||||
|
||||
1. Show only repo-backed, authorized primary/secondary actions.
|
||||
2. Keep mutation/generation/repair actions out of the customer-safe default surface unless explicitly authorized and already repo-supported.
|
||||
3. If a high-impact action becomes necessary, stop and update spec/plan before implementation.
|
||||
4. Preserve existing audit logging and avoid secrets/raw payloads.
|
||||
|
||||
### Phase 5 — Browser Smoke And Screenshots
|
||||
|
||||
1. Add one bounded Browser smoke file for representative states.
|
||||
2. Capture screenshots under this spec package.
|
||||
3. Document unreachable states rather than faking fixtures.
|
||||
|
||||
### Phase 6 — Validation And Close-Out
|
||||
|
||||
1. Run focused Feature/Livewire validation.
|
||||
2. Run the bounded Browser smoke.
|
||||
3. Run overlapping filter tests, `pint --dirty`, and `git diff --check`.
|
||||
4. Record UI coverage close-out decision and known gaps.
|
||||
|
||||
## Test Governance Check
|
||||
|
||||
- **Test purpose / classification by changed surface**: Feature/Livewire for state/RBAC/context/false claims; Browser for rendered first-screen hierarchy, diagnostics collapsed, and screenshots.
|
||||
- **Affected validation lanes**: confidence + browser.
|
||||
- **Why this lane mix is the narrowest sufficient proof**: page state and authorization are cheaper and deterministic in Feature tests; rendered hierarchy and diagnostics disclosure are the product value and need browser proof.
|
||||
- **Narrowest proving command(s)**:
|
||||
|
||||
```bash
|
||||
cd apps/platform
|
||||
./vendor/bin/sail artisan test tests/Feature/Filament/Spec342CustomerReviewWorkspaceConsumptionTest.php --compact
|
||||
./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec342CustomerReviewWorkspaceConsumptionSmokeTest.php --compact
|
||||
./vendor/bin/sail artisan test --filter='CustomerReview|ReviewPack|Evidence|AcceptedRisk|Finding|Audit|Spec341' --compact
|
||||
./vendor/bin/sail pint --dirty
|
||||
git diff --check
|
||||
```
|
||||
|
||||
- **Fixture / helper / factory / seed / context cost risks**: reuse existing review/evidence/review-pack/finding/exception/audit/operation fixtures; no broad seeding.
|
||||
- **Expensive defaults or shared helper growth introduced?**: no.
|
||||
- **Heavy-family additions, promotions, or visibility changes**: one explicit browser smoke only.
|
||||
- **Surface-class relief / special coverage rule**: no relief; strategic customer-safe surface.
|
||||
- **Closing validation and reviewer handoff**: confirm state truth, hidden diagnostics, RBAC/context safety, one next action, screenshots, and no false readiness claims.
|
||||
- **Budget / baseline / trend follow-up**: none expected.
|
||||
- **Review-stop questions**: Are any claims not repo-backed? Does any state require backend truth? Did a presenter become a framework? Did browser coverage widen? Did a route/shell/query alias regress?
|
||||
- **Escalation path**: `document-in-feature` for unreachable fixture states; `follow-up-spec` for missing backend capabilities; `reject-or-split` for portal/framework/backend generation.
|
||||
- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage.
|
||||
- **Why no dedicated follow-up spec is needed**: final consumption belongs in this active feature; attestation, localization, governance inbox, provider readiness, and PSA handoff remain separate follow-up specs.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/342-customer-review-workspace-final-consumption-productization/
|
||||
├── spec.md
|
||||
├── plan.md
|
||||
├── tasks.md
|
||||
├── repo-truth-map.md
|
||||
├── customer-review-consumption-state-contract.md
|
||||
├── checklists/
|
||||
│ └── requirements.md
|
||||
└── artifacts/
|
||||
└── screenshots/
|
||||
```
|
||||
|
||||
### Expected implementation touch points
|
||||
|
||||
```text
|
||||
apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php
|
||||
apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php
|
||||
apps/platform/tests/Feature/Filament/Spec342CustomerReviewWorkspaceConsumptionTest.php
|
||||
apps/platform/tests/Browser/Spec342CustomerReviewWorkspaceConsumptionSmokeTest.php
|
||||
```
|
||||
|
||||
### Supporting surfaces to inspect, not broadly redesign
|
||||
|
||||
```text
|
||||
apps/platform/app/Filament/Resources/EnvironmentReviewResource.php
|
||||
apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php
|
||||
apps/platform/app/Filament/Resources/ReviewPackResource.php
|
||||
apps/platform/app/Filament/Resources/FindingExceptionResource.php
|
||||
apps/platform/app/Filament/Resources/StoredReportResource.php
|
||||
apps/platform/app/Models/EnvironmentReview.php
|
||||
apps/platform/app/Models/EvidenceSnapshot.php
|
||||
apps/platform/app/Models/ReviewPack.php
|
||||
apps/platform/app/Models/Finding.php
|
||||
apps/platform/app/Models/FindingException.php
|
||||
apps/platform/app/Models/OperationRun.php
|
||||
apps/platform/app/Support/Navigation/WorkspaceHubEnvironmentFilter.php
|
||||
apps/platform/app/Support/OperationRunLinks.php
|
||||
apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php
|
||||
```
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|---|---|---|
|
||||
| Possible page-local presenter | May be needed to centralize derived state and avoid duplicate page/view logic | Blade-only mapping risks scattered false claims and hard-to-test conditions |
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
- **New persisted truth**: none.
|
||||
- **New abstraction**: none by default; possible page-local presenter only.
|
||||
- **New state/status family**: no; presentation states derive from existing model truth.
|
||||
- **Ownership cost**: state contract, feature tests, browser smoke, screenshots.
|
||||
- **Rejected alternatives**: copy-only patch, portal architecture, persisted readiness table, global readiness engine.
|
||||
|
||||
## Constitution Check
|
||||
|
||||
- Inventory-first: pass; all display states derive from last-observed review/evidence/artifact truth.
|
||||
- Read/write separation: pass; default surface is read-only consumption. Any existing generation/download action remains policy/audit-backed.
|
||||
- Graph contract path: pass; no Graph call during page render.
|
||||
- Deterministic capabilities: pass; existing policies/capabilities remain authoritative.
|
||||
- RBAC-UX: pass; `/admin` only, workspace/environment entitlement required, non-member access is deny-as-not-found.
|
||||
- Workspace/tenant isolation: pass; `environment_id` is filter only and must be entitlement-checked.
|
||||
- Run observability: pass; no new OperationRun lifecycle. Existing runs are proof only.
|
||||
- Data minimization: pass; no secrets/raw payloads/default diagnostics.
|
||||
- Test governance: pass; confidence + one browser smoke are explicit.
|
||||
- Proportionality / no premature abstraction: pass with bounded presenter guard.
|
||||
- Persisted truth / behavioral state: pass; no new persistence or lifecycle state.
|
||||
- Shared pattern first: pass; reuse Spec 337/Product Process Flow semantics and existing artifact/operation/badge helpers.
|
||||
- Provider boundary: pass; no provider seam change.
|
||||
- UI/Productization coverage: pass; existing strategic surface changed, screenshot/close-out coverage required.
|
||||
- Filament v5 / Livewire v4 compliance: required; no Livewire v3 or Filament legacy APIs.
|
||||
- Provider registration: unchanged in `apps/platform/bootstrap/providers.php`.
|
||||
- Global search: do not enable global search for related resources in this spec.
|
||||
- Destructive actions: none expected; any introduced high-impact/destructive action must use `Action::make(...)->action(...)`, `->requiresConfirmation()`, authorization, audit, notification, and tests after spec/plan update.
|
||||
- Asset strategy: no registered assets expected; if assets are introduced, update deployment to run `cd apps/platform && php artisan filament:assets`.
|
||||
|
||||
## Deployment / Ops Impact
|
||||
|
||||
No migrations, env vars, packages, queues, scheduler, storage, Graph scopes, Dokploy config, or Filament assets are expected. Runtime impact is UI/page rendering plus tests. If implementation proves otherwise, update spec/plan before coding further.
|
||||
@ -0,0 +1,97 @@
|
||||
# Spec 342 - Repo Truth Map
|
||||
|
||||
Status: implemented
|
||||
Created: 2026-06-01
|
||||
Scope: Customer Review Workspace final customer-safe consumption
|
||||
|
||||
This map is the implementation guardrail for Spec 342. Runtime work must update this file before changing code when it discovers additional truth, unsupported states, or deferred concepts.
|
||||
|
||||
## Classification Vocabulary
|
||||
|
||||
- `repo-verified`: observed in current application code, tests, specs, or routes.
|
||||
- `derived from existing model`: available by deriving from existing persisted model fields or relationships.
|
||||
- `foundation-real`: foundation exists, but the final customer-consumption behavior may still need wiring or productization.
|
||||
- `not available`: no repo-backed truth or action exists in the current codebase.
|
||||
- `deferred`: intentionally out of scope for Spec 342.
|
||||
|
||||
## Core Surface Truth
|
||||
|
||||
| Data point | Classification | Repo evidence | Spec 342 handling |
|
||||
|---|---|---|---|
|
||||
| Customer Review Workspace page | repo-verified | `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` | Existing route and page remain the target. |
|
||||
| Customer Review Workspace Blade view | repo-verified | `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php` | Productize existing view; do not add a new route. |
|
||||
| Canonical route | repo-verified | Page slug `reviews/workspace`; Filament admin panel route | Keep `/admin/reviews/workspace`. |
|
||||
| Environment filter | repo-verified | `environmentFilterUrl()` emits `environment_id`; workspace hub filter helpers exist | Keep `environment_id` as page filter only. |
|
||||
| Legacy `/admin/t` context | not available | Spec 341 cleanup and current roadmap boundary | Do not reintroduce. |
|
||||
| Page-open audit event | repo-verified | `WorkspaceAuditLogger` usage in `CustomerReviewWorkspace` | Preserve safe audit metadata. |
|
||||
| First-screen decision card | repo-verified | Current Blade renders `customer-review-decision-card`; Spec 342 tests assert one primary action and false-claim prevention | Final consumption decision card is implemented on the existing page. |
|
||||
| Review package index/table | repo-verified | Existing Filament table in `CustomerReviewWorkspace` | Keep secondary to first-screen decision/proof content. |
|
||||
| Diagnostics section | repo-verified | Current Blade has diagnostics/disclosure payload; Spec 342 feature/browser tests assert collapsed default and hidden raw payloads | Kept collapsed by default. |
|
||||
|
||||
## Review Truth
|
||||
|
||||
| Data point | Classification | Repo evidence | Spec 342 handling |
|
||||
|---|---|---|---|
|
||||
| Review record | repo-verified | `EnvironmentReview` model | Use as review source. |
|
||||
| Published/released review | derived from existing model | `EnvironmentReviewStatus::Published`, `published_at` | Use for latest released review. |
|
||||
| Review completeness | repo-verified | `EnvironmentReviewCompletenessState`, `completeness_state` | Render as readiness dimension. |
|
||||
| Review summary | repo-verified | `EnvironmentReview.summary` cast | Use only customer-safe fields; avoid raw payload. |
|
||||
| Current export review pack | repo-verified | `current_export_review_pack_id`, `currentExportReviewPack()` | Use for current export/pack state. |
|
||||
| Review sections | repo-verified | `EnvironmentReviewSection` relationship | Use for customer-safe content only if already productized. |
|
||||
| Customer acknowledgement/attestation | not available | No attestation model or workflow identified in current truth map | Do not implement; optional unavailable copy only. |
|
||||
|
||||
## Evidence And Review Pack Truth
|
||||
|
||||
| Data point | Classification | Repo evidence | Spec 342 handling |
|
||||
|---|---|---|---|
|
||||
| Evidence snapshot | repo-verified | `EvidenceSnapshot` model and relation from `EnvironmentReview` | Use as evidence availability/proof source. |
|
||||
| Evidence status/completeness | repo-verified | `EvidenceSnapshotStatus`, `EvidenceCompletenessState` | Render evidence states truthfully. |
|
||||
| Evidence items | repo-verified | `EvidenceSnapshotItem` relationship | Use as proof count/detail only when customer-safe. |
|
||||
| Stored report | repo-verified | `StoredReport` model and Evidence Overview references | Link/show only when repo-backed for selected review context. |
|
||||
| Review pack | repo-verified | `ReviewPack` model | Use as review-pack state source. |
|
||||
| Review pack status | repo-verified | `ReviewPackStatus`, model constants | Render queued/generating/ready/failed/expired. |
|
||||
| Review pack file metadata | repo-verified | `file_disk`, `file_path`, `file_size`, `sha256`, `generated_at`, `expires_at` | Download/export available only when ready and authorized. |
|
||||
| Signed download route | repo-verified | `ReviewPackDownloadController`, route `admin.review-packs.download` | Use only when existing authorization and file truth permit. |
|
||||
| External delivery/email/PSA | not available | No repo-backed delivery mechanism for this surface | Show unavailable/deferred only if useful. |
|
||||
|
||||
## Findings And Accepted Risk Truth
|
||||
|
||||
| Data point | Classification | Repo evidence | Spec 342 handling |
|
||||
|---|---|---|---|
|
||||
| Finding records | repo-verified | `Finding` model | Use for customer-safe finding summaries. |
|
||||
| Finding status/severity | repo-verified | `Finding` constants and fields | Show counts/labels where repo-backed. |
|
||||
| Owner/assignee/due fields | repo-verified | `owner_user_id`, `assignee_user_id`, `due_at` | Show only if loaded and customer-safe. |
|
||||
| Accepted risk / exception records | repo-verified | `FindingException` model | Summarize accepted risks visibly. |
|
||||
| Exception status | repo-verified | `FindingException` status constants | Use shared badge/status semantics. |
|
||||
| Exception validity | repo-verified | `current_validity_state`, validity constants | Surface expired/expiring/missing-support states. |
|
||||
| Owner/rationale/expiry/review date | repo-verified | `owner_user_id`, `request_reason`, `expires_at`, `review_due_at`, decision records; Spec 342 tests assert owner, rationale, next review, and missing review-date copy | Visible in the accepted-risk summary when repo-backed. |
|
||||
| Exception decision history | repo-verified | `FindingExceptionDecision` relationship | Use as proof only when customer-safe and authorized. |
|
||||
| Accepted risk lifecycle beyond current exception truth | deferred | Follow-up candidate in user draft | Do not add lifecycle backend in Spec 342. |
|
||||
|
||||
## Audit And Operation Proof
|
||||
|
||||
| Data point | Classification | Repo evidence | Spec 342 handling |
|
||||
|---|---|---|---|
|
||||
| OperationRun proof | repo-verified | `OperationRun` model and links from review/evidence/pack | Show as secondary proof only. |
|
||||
| Operation status/outcome | repo-verified | `OperationRunStatus`, `OperationRunOutcome` | Do not collapse into evidence or customer-safe output truth. |
|
||||
| OperationRun raw payload/context | repo-verified but diagnostics-only | `OperationRun.context`, `summary_counts`, `failure_summary` | Hidden by default; capability-gated if shown. |
|
||||
| Audit log | repo-verified | `AuditLog`, `WorkspaceAuditLogger`, page-open event | Preserve and link only if repo-backed/authorized. |
|
||||
| Customer-visible audit export | deferred | Not required by Spec 342 | Leave to follow-up if needed. |
|
||||
|
||||
## RBAC And Context Truth
|
||||
|
||||
| Data point | Classification | Repo evidence | Spec 342 handling |
|
||||
|---|---|---|---|
|
||||
| Workspace membership | repo-verified | `WorkspaceContext`, workspace membership models | Required for page access. |
|
||||
| Managed environment entitlement | repo-verified | `EnvironmentReviewRegisterService::authorizedTenants()` and policies | Required before rendering environment-bound records. |
|
||||
| Capability-aware actions | repo-verified | `Capabilities`, policies, `UiEnforcement` patterns | Use existing action visibility/authorization. |
|
||||
| Cross-workspace `environment_id` | repo-verified guard expectation | Spec 341 and existing navigation/filter tests | Return safe no-access/404. |
|
||||
| Diagnostics capability | repo-verified | Existing capability/policy patterns; customer workspace diagnostics remain collapsed and raw/support data hidden by default | Hide/unavailable when not authorized. |
|
||||
|
||||
## Implementation Close-Out
|
||||
|
||||
- No new persisted entity, enum/status family, generic readiness framework, migration, package, env var, queue, scheduler, Graph scope, route, or Filament asset was introduced.
|
||||
- No standalone presenter class was added. The existing `CustomerReviewWorkspace` page now exposes bounded page-local derived payload helpers for the decision card, six-step flow, findings panel, accepted-risk panel, and proof separation.
|
||||
- Browser screenshots cover not-ready/evidence-incomplete, ready-with-evidence, review-pack-available, findings-need-attention, accepted-risks-present, diagnostics-collapsed, and dark-mode states under `artifacts/screenshots/`.
|
||||
- The explicit "evidence missing" screenshot name from planning was represented by the repo-backed evidence-incomplete/not-ready state; no backend truth was faked to manufacture a separate missing-evidence path.
|
||||
- UI coverage docs were not changed because this spec productizes the existing `/admin/reviews/workspace` route and does not add navigation, a new route, or a new archetype.
|
||||
@ -0,0 +1,336 @@
|
||||
# Feature Specification: Spec 342 - Customer Review Workspace v1 Final Consumption Productization
|
||||
|
||||
**Feature Branch**: `342-customer-review-workspace-final-consumption-productization`
|
||||
**Created**: 2026-06-01
|
||||
**Status**: Draft
|
||||
**Type**: Runtime UX productization / customer-safe review consumption / no backend rewrite
|
||||
**Runtime posture**: Productize existing Customer Review Workspace, Environment Review, Evidence, Review Pack, Finding, Accepted Risk, Audit, and OperationRun proof foundations. Do not introduce a new portal architecture, backend review engine, or persisted readiness truth.
|
||||
**Input**: User-provided Spec 342 draft + `docs/product/spec-candidates.md` P1 Customer Review Workspace v1 Completion lane.
|
||||
|
||||
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: Customer Review Workspace is repo-real and has prior productization work, but final customer-safe consumption still needs one coherent first-screen answer: what was reviewed, what needs attention, which risks were accepted, what evidence/review pack exists, and what next action is expected without raw operator diagnostics.
|
||||
- **Today's failure**: A customer, auditor, account manager, or service owner can still need to infer readiness by reading several panels, tables, generated artifacts, evidence paths, or related operations instead of seeing status, reason, impact, evidence basis, accepted-risk visibility, review-pack/export truth, and one primary next action immediately.
|
||||
- **User-visible improvement**: The review workspace becomes a sellable v1 consumption surface: customer-safe by default, evidence-aware, accepted-risk-aware, review-pack/export-aware, and honest about unavailable/deferred states.
|
||||
- **Smallest enterprise-capable version**: Finalize only the existing `/admin/reviews/workspace` surface and its current page/view/payload contract. Add or adjust a small derived presenter only if current page-local logic is too scattered, and keep it feature-local and derived-only.
|
||||
- **Explicit non-goals**: No new external customer portal, authentication/federation, review generation engine, evidence generation engine, review-pack/PDF/ZIP generation logic, external delivery, billing/entitlement system, AI summarization, new `OperationRun` type, migration, package, scheduler, queue, storage, or broad shell/navigation rewrite.
|
||||
- **Permanent complexity imported**: One spec-local repo truth map, one spec-local consumption state contract, focused Feature/Livewire tests, one Browser smoke file, and screenshot artifacts. Possible small page-local presenter if needed. No new tables, persisted source of truth, enum/status family, public framework, or cross-domain UI taxonomy.
|
||||
- **Why now**: Specs 326, 329, 335, 336, 337, 340, and 341 have productized adjacent review/evidence/proof/process/link surfaces. The remaining P1 roadmap gap is final customer-safe review consumption, not another backend foundation or canonical-link cleanup.
|
||||
- **Why not local**: A copy-only pass would keep readiness scattered and could create false customer-safe or evidence/export claims. A broad portal/framework would overbuild. The narrow correct slice is a repo-truth-bounded final consumption pass on the existing workspace.
|
||||
- **Approval class**: Core Enterprise.
|
||||
- **Red flags triggered**: Customer-facing trust language, many evidence/review/finding concepts, possible presenter/state contract. Defense: all states remain derived from repo-backed truth, raw diagnostics stay collapsed, RBAC/policies remain authoritative, no persistence or new status family is introduced, and unsupported concepts are rendered as unavailable/deferred.
|
||||
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexität: 1 | Produktnähe: 2 | Wiederverwendung: 2 | **Gesamt: 11/12**
|
||||
- **Decision**: approve.
|
||||
|
||||
## Candidate Source And Completed-Spec Guardrail
|
||||
|
||||
- **Candidate source**: User-provided Spec 342 draft, aligned with `docs/product/spec-candidates.md` → `Customer Review Workspace v1 Completion` and `docs/product/roadmap.md` → Customer Review Workspace v1 Completion as the top current productization priority.
|
||||
- **Completed-spec check**: No `specs/342-*` package existed before generation. Related Specs 249, 258, 312, 326, 329, 335, 336, 337, 340, and 341 contain prepared, implemented, completed-task, validation, smoke, or close-out signals and are treated as historical context only. They must not be rewritten or normalized by this spec.
|
||||
- **Spec 326 boundary**: Spec 342 must not repeat Spec 326’s general workspace productization. It closes the narrower final-consumption gap: customer-safe decision card, review consumption states, accepted-risk visibility, evidence/review-pack/export truth, and one next action after Specs 337 and 341.
|
||||
- **Close alternatives deferred**:
|
||||
- `localization-v1-customer-facing-surfaces`: deferred until final review-consumption truth is stable.
|
||||
- `decision-based-governance-inbox-v1`: deferred because operator inbox maturity should build on the customer-safe review output, not replace it.
|
||||
- `commercial-entitlements-billing-state-maturity`: deferred until customer-facing review consumption is clearer.
|
||||
- `provider-readiness-productization` and PSA/support handoff: follow-up lanes only.
|
||||
- **Smallest viable implementation slice**: Existing Customer Review Workspace only: first-screen decision state, review readiness flow, findings summary, accepted-risk summary, evidence/review-pack/export proof panel, diagnostics disclosure, route/context/RBAC safety, tests, browser smoke, and screenshots.
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: workspace canonical-view customer-safe review consumption hub, optionally filtered by canonical `environment_id`.
|
||||
- **Primary Routes**:
|
||||
- Existing route: `/admin/reviews/workspace`.
|
||||
- Existing page class: `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`.
|
||||
- Existing view: `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php`.
|
||||
- Related proof/action links: `EnvironmentReviewResource`, `EvidenceSnapshotResource`, `ReviewPackResource`, `FindingExceptionResource`, `StoredReportResource`, `OperationRunLinks`, and signed review-pack download route.
|
||||
- **Data Ownership**:
|
||||
- Workspace-scoped canonical page; rendered records are workspace- and managed-environment-owned.
|
||||
- Review truth: `EnvironmentReview`, status, published timestamp, summary, completeness, current export review pack.
|
||||
- Evidence truth: `EvidenceSnapshot`, `EvidenceSnapshotItem`, completeness/status, linked operation run.
|
||||
- Review pack/export truth: `ReviewPack`, status, file metadata, expiry, evidence snapshot, environment review, signed download eligibility.
|
||||
- Findings and accepted-risk truth: `Finding`, `FindingException`, `FindingExceptionDecision`, evidence references, review-derived summaries.
|
||||
- Audit/proof truth: `AuditLog`, existing workspace audit logger events, and linked `OperationRun` records.
|
||||
- **RBAC**:
|
||||
- Workspace membership is required.
|
||||
- Managed-environment entitlement is required before showing environment-scoped review/evidence/findings/pack data.
|
||||
- Existing policies/capabilities remain authoritative for review, evidence, review-pack, accepted-risk, finding, audit, diagnostics, export/download, and operation proof visibility.
|
||||
- Non-member or cross-workspace/environment access remains deny-as-not-found.
|
||||
- Member-without-capability behavior follows existing UI hidden/disabled plus server-side policy/gate enforcement.
|
||||
|
||||
For canonical-view specs:
|
||||
|
||||
- **Default filter behavior when tenant-context is active**: Clean `/admin/reviews/workspace` remains workspace-wide and must not inherit remembered environment context, `/admin/t` routes, table filters, legacy query aliases, or hidden topbar scope. `?environment_id=` is a page-level filter only.
|
||||
- **Explicit entitlement checks preventing cross-tenant leakage**: Any `environment_id`, review, evidence snapshot, review pack, finding, accepted risk, audit, or operation proof link must resolve through current workspace and actor entitlement before rendering or navigation.
|
||||
|
||||
## UI Surface Impact *(mandatory — UI-COV-001)*
|
||||
|
||||
Does this spec add, remove, rename, or materially change any reachable UI surface?
|
||||
|
||||
- [ ] No UI surface impact
|
||||
- [x] Existing page changed
|
||||
- [ ] New page/route added
|
||||
- [ ] Navigation changed
|
||||
- [ ] Filament panel/provider surface changed
|
||||
- [ ] New modal/drawer/wizard/action added
|
||||
- [x] New table/form/state added
|
||||
- [x] Customer-facing surface changed
|
||||
- [ ] Dangerous action changed
|
||||
- [x] Status/evidence/review presentation changed
|
||||
- [x] Workspace/environment context presentation changed
|
||||
|
||||
## UI/Productization Coverage *(mandatory when UI Surface Impact is not "No UI surface impact"; otherwise write `N/A - no reachable UI surface impact` plus rationale)*
|
||||
|
||||
- **Route/page/surface**: `/admin/reviews/workspace`, `CustomerReviewWorkspace`, customer review workspace Blade view.
|
||||
- **Current or new page archetype**: Existing Customer Review Workspace / Strategic Surface.
|
||||
- **Design depth**: Strategic Surface.
|
||||
- **Repo-truth level**: repo-verified page and foundational models; final state mapping must be maintained in `repo-truth-map.md` and `customer-review-consumption-state-contract.md`.
|
||||
- **Existing pattern reused**: Filament Page, current Blade composition, existing workspace hub `environment_id` filter chip, Spec 326 customer review layout, Spec 337 evidence/review-pack product flow, existing badge/action/link helpers, existing signed review-pack download route, existing policies.
|
||||
- **New pattern required**: none by default. A small page-local presenter is allowed only if it replaces scattered page/view logic and remains derived-only.
|
||||
- **Screenshot required**: yes, during implementation under `specs/342-customer-review-workspace-final-consumption-productization/artifacts/screenshots/`.
|
||||
- **Page audit required**: no new page audit by default because route/archetype remain the same; implementation must update UI coverage registry artifacts or document in the active close-out why existing route inventory/design coverage remains sufficient.
|
||||
- **Customer-safe review required**: yes. The surface must hide raw JSON, provider diagnostics, internal IDs as primary labels, fingerprints, stack traces, raw operation payloads, and platform reason families by default.
|
||||
- **Dangerous-action review required**: no new dangerous action is expected. If a generation/regeneration/export mutation is added or exposed, update this spec/plan first and require confirmation, server-side authorization, audit, notification, and action tests.
|
||||
- **Coverage files updated or explicitly not needed**:
|
||||
- [ ] `docs/ui-ux-enterprise-audit/route-inventory.md`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/page-reports/...`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/strategic-surfaces.md`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/grouped-follow-up-candidates.md`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/unresolved-pages.md`
|
||||
- [ ] `N/A - no reachable UI surface impact`
|
||||
- **Coverage decision for implementation**: implementation must explicitly choose registry update or active-spec close-out no-registry-change rationale after the runtime diff is known.
|
||||
|
||||
## Cross-Cutting / Shared Pattern Reuse *(mandatory)*
|
||||
|
||||
- **Cross-cutting feature?**: yes.
|
||||
- **Interaction class(es)**: customer-safe review status messaging, decision card, evidence/report viewer, review-pack/export links, accepted-risk summaries, finding summaries, diagnostics disclosure, OperationRun proof links.
|
||||
- **Systems touched**: `CustomerReviewWorkspace`, customer review Blade view, existing review/evidence/review-pack resources, `ReviewPackDownloadController`, `OperationRunLinks`, `ArtifactTruthPresenter`, `BadgeCatalog` / `BadgeRenderer`, `UiEnforcement` or existing capability-aware visibility helpers.
|
||||
- **Existing pattern(s) to extend**: Spec 326 Customer Review Workspace layout/productization, Spec 337 Product Process Flow semantics, current evidence/review-pack state mapping, current workspace hub environment filter, current signed download and audit paths.
|
||||
- **Shared contract / presenter / builder / renderer to reuse**: reuse existing artifact truth, badge, operation link, workspace filter, resource URL, and policy/capability helpers. Use `CustomerReviewWorkspacePresenter` only as a small derived page-local adapter if needed.
|
||||
- **Why the existing shared path is sufficient or insufficient**: Existing repo truth sources and UI primitives are sufficient for state calculation and rendering. They may be insufficient only because the final consumption state is spread across current page methods and Blade fragments; if so, consolidate locally rather than frameworkizing.
|
||||
- **Allowed deviation and why**: bounded page-local presenter/contract is allowed to prevent duplicate logic. No cross-domain readiness framework or new persisted state is allowed.
|
||||
- **Consistency impact**: Status, reason, impact, next-action labels, review-pack/export state, accepted-risk terminology, and operation proof links must stay aligned with Specs 326/337 and current resources.
|
||||
- **Review focus**: no fake customer-safe/evidence/export/auditor-ready claims, no raw diagnostics as first-screen UX, no `/admin/t`, no legacy query aliases, no unauthorized action leakage, no duplicated readiness summaries.
|
||||
|
||||
## OperationRun UX Impact *(mandatory)*
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: existing link/proof display only; no new operation start, completion, dedupe, queueing, terminal notification, or lifecycle behavior.
|
||||
- **Shared OperationRun UX contract/layer reused**: existing `OperationRunLinks` and existing operation/resource routes.
|
||||
- **Delegated start/completion UX behaviors**: N/A for new starts. Existing proof/deep-link behavior must remain delegated to shared operation link helpers and policies.
|
||||
- **Local surface-owned behavior that remains**: explanatory copy that distinguishes operation proof from evidence output and review-pack/export truth.
|
||||
- **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 new provider seam.
|
||||
- **Boundary classification**: platform-core consumption of existing workspace/environment governance artifacts.
|
||||
- **Seams affected**: display and navigation over review/evidence/review-pack/finding/accepted-risk/audit/operation proof.
|
||||
- **Neutral platform terms preserved or introduced**: workspace, environment, customer review, evidence, review pack, export, accepted risk, finding, operation proof, audit trail, diagnostics.
|
||||
- **Provider-specific semantics retained and why**: provider-specific names may appear only inside already persisted/customer-safe evidence or review content. No Graph payloads, provider IDs, or Microsoft-specific platform-core terms should become primary UI language.
|
||||
- **Why this does not deepen provider coupling accidentally**: no Graph calls, provider credentials, provider contract registry, connection scope, or provider taxonomy changes.
|
||||
- **Follow-up path**: provider-readiness productization remains a separate follow-up candidate.
|
||||
|
||||
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)*
|
||||
|
||||
| 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 first screen | yes | Native Filament page plus existing Blade composition | customer-safe review consumption, status messaging | page payload, URL-query | no | Existing route only |
|
||||
| Review decision card | yes | Filament sections/cards/badges/buttons | decision-first status/reason/impact/next action | page payload | no | Derived labels only |
|
||||
| Review readiness flow | yes | existing Product Process Flow conventions where practical | process/status messaging | page payload | no | No new persisted lifecycle |
|
||||
| Findings and accepted-risk summaries | yes | Filament/shared badges and cards | finding/decision/evidence summaries | page payload | no | Repo-backed fields only |
|
||||
| Evidence / review pack / export proof panel | yes | Filament/shared proof panel | evidence/report/review-pack viewer, OperationRun proof | page payload/action links | no | Proof secondary, not diagnostics-first |
|
||||
| Diagnostics disclosure | yes | collapsed/progressive disclosure | support/raw detail | page payload/action visibility | no | Hidden/capability-aware by default |
|
||||
|
||||
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
|
||||
|
||||
| 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 Decision Surface for customer-safe review consumption | Reviewer decides if the review is ready to consume/share or needs follow-up | status, reason, impact, primary next action, findings, accepted risks, evidence, review-pack/export state | review detail, evidence snapshot, review pack, audit, operation proof, diagnostics | Primary because this is the customer-facing consumption hub | Review consumption, not admin mirror | Avoids reconstructing readiness across resources |
|
||||
| Evidence / review pack / export proof panel | Secondary Context / Tertiary Evidence | Reviewer checks the proof basis after reading the decision card | evidence state, review pack state, export availability, operation/audit proof availability | linked artifacts and diagnostics when authorized | Supports primary decision without replacing it | Evidence-first review trust | Separates proof from raw payloads |
|
||||
| Findings / accepted risks summaries | Secondary Context | Reviewer checks attention and accepted-risk accountability | open/high-impact findings, accepted risk owner/rationale/expiry where repo-backed | finding/exception detail and evidence references | Supports review follow-up and customer accountability | Findings/risk workflow | Avoids hiding risk acceptance in diagnostics |
|
||||
|
||||
## Audience-Aware Disclosure *(mandatory when operator-facing surfaces are changed)*
|
||||
|
||||
| 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, auditor/account manager, support where authorized | status, reason, impact, next action, findings/accepted-risk/evidence/review-pack/export summaries | collapsed and secondary | raw JSON, provider diagnostics, operation payloads, internal reason ownership, fingerprints | state-specific action: review findings, open review pack, open evidence, or complete review preparation | raw diagnostics, debug payloads, operator-only repair actions | decision card states readiness once; panels add proof |
|
||||
| Diagnostics disclosure | operator/support only | collapsed indicator or unavailable state | operation metadata, technical details, raw/support fields if authorized | raw payloads only when explicitly revealed and permitted | close/open diagnostics | default hidden | diagnostics never restate the primary decision as a second competing summary |
|
||||
|
||||
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
|
||||
|
||||
| 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 | Workbench / Review Workspace | Customer-safe strategic hub | Consume latest review or follow one next action | Primary action in decision card; linked proof secondary | no row-click requirement for summary cards | proof panel / secondary links | none expected | `/admin/reviews/workspace` | existing review/evidence/pack/finding detail routes | workspace shell + optional visible `environment_id` filter chip | Customer review | readiness, reason, impact, evidence, pack/export, findings, accepted risks | none |
|
||||
| Review package index | List / Table / Registry context | Secondary artifact index | Open a specific released review or review pack | dedicated open column/link only | no | row/proof links only | none expected | `/admin/reviews/workspace` | existing resource detail/download routes | same workspace/filter signals | Review package | package availability, latest review, evidence status, next step | existing table remains secondary |
|
||||
|
||||
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
|
||||
|
||||
| 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 reviewer, auditor, MSP operator | Decide whether a released review can be consumed/shared or needs follow-up | Strategic review workbench | Is this review ready to consume, what needs attention, and what evidence supports it? | status, reason, impact, next action, findings, accepted risks, evidence state, review pack/export state | raw operation payload, provider JSON, fingerprints, internal reason families, stack traces | review readiness, data completeness, finding attention, accepted-risk accountability, evidence state, review pack/export state | read-only consumption; existing linked actions only | state-specific open/review/download action | none expected |
|
||||
|
||||
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||
|
||||
- **New source of truth?**: no.
|
||||
- **New persisted entity/table/artifact?**: no runtime persistence. Spec-local preparation artifacts only.
|
||||
- **New abstraction?**: possible small page-local presenter if needed; no public interface/framework.
|
||||
- **New enum/state/reason family?**: no. Consumption states are presentation labels derived from existing model status/relationships.
|
||||
- **New cross-domain UI framework/taxonomy?**: no.
|
||||
- **Current operator problem**: customer-safe reviewers need a truthful first-screen answer without traversing artifacts and diagnostics.
|
||||
- **Existing structure is insufficient because**: current truth exists, but final consumption state can be scattered across review payloads, evidence panels, accepted-risk summaries, review-pack status, and operation/audit proof.
|
||||
- **Narrowest correct implementation**: derive state locally for the existing page, render one decision hierarchy, and test false-claim prevention.
|
||||
- **Ownership cost**: one feature-local state contract, possible presenter, focused tests, one browser smoke, screenshot upkeep.
|
||||
- **Alternative intentionally rejected**: portal architecture, new lifecycle table, generic readiness engine, global state taxonomy, and copy-only patch.
|
||||
- **Release truth**: current-release truth; this is a sellability/productization slice.
|
||||
|
||||
### Compatibility posture
|
||||
|
||||
This feature assumes a pre-production environment. Backward compatibility, legacy aliases, migration shims, `/admin/t` resurrection, and compatibility-specific tests are out of scope unless this spec is explicitly amended.
|
||||
|
||||
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||
|
||||
- **Test purpose / classification**: Feature/Livewire for state, RBAC, context, and false-claim prevention; Browser for rendered first-screen consumption and collapsed diagnostics.
|
||||
- **Validation lane(s)**: confidence + browser.
|
||||
- **Why this classification and these lanes are sufficient**: state composition and authorization can be proven through focused Feature/Livewire tests; the customer-safe first screen, visual hierarchy, diagnostics disclosure, and screenshot evidence require one bounded browser smoke.
|
||||
- **New or expanded test families**: `Spec342CustomerReviewWorkspaceConsumptionTest.php` and `Spec342CustomerReviewWorkspaceConsumptionSmokeTest.php`.
|
||||
- **Fixture / helper cost impact**: reuse existing workspace/environment/review/evidence/review-pack/finding/exception/audit/operation factories and fixtures; no heavy default seed or shared setup widening.
|
||||
- **Heavy-family visibility / justification**: one explicit Browser smoke family because this is a customer-facing strategic surface.
|
||||
- **Special surface test profile**: global-context-shell + customer-safe strategic review surface.
|
||||
- **Standard-native relief or required special coverage**: no standard-native relief; this surface requires customer-safe disclosure and browser proof.
|
||||
- **Reviewer handoff**: verify no false customer-safe/auditor-ready/export-ready claims, diagnostics collapsed, one next action, no `/admin/t`, no legacy query alias, and no cross-workspace leakage.
|
||||
- **Budget / baseline / trend impact**: none expected beyond one bounded browser smoke.
|
||||
- **Escalation needed**: `document-in-feature` for unreachable states; `follow-up-spec` only if implementation proves missing backend truth such as attestation or external delivery; `reject-or-split` if scope grows into portal/framework/backend generation.
|
||||
- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage.
|
||||
- **Planned validation commands**:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Filament/Spec342CustomerReviewWorkspaceConsumptionTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec342CustomerReviewWorkspaceConsumptionSmokeTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter='CustomerReview|ReviewPack|Evidence|AcceptedRisk|Finding|Audit|Spec341' --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail pint --dirty`
|
||||
- `git diff --check`
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Understand review readiness immediately (Priority: P1)
|
||||
|
||||
A customer-safe reviewer opens Customer Review Workspace and sees whether the latest released review is ready to consume, why, what impact exists, and the one next action before any raw table or diagnostics.
|
||||
|
||||
**Why this priority**: This is the core sellability and trust gap.
|
||||
|
||||
**Independent Test**: Render the page with a released review in multiple repo-backed states and assert the decision card shows status, reason, impact, one next action, and no raw diagnostics.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a workspace member with review access and a released review with linked evidence and review pack, **When** they open `/admin/reviews/workspace`, **Then** they see a customer-safe decision card with review-ready/evidence/pack state and a primary action.
|
||||
2. **Given** a released review without required evidence or pack output, **When** the reviewer opens the workspace, **Then** the page says what is missing and does not claim customer-safe or export-ready output.
|
||||
3. **Given** no released review is available for the selected filter, **When** the reviewer opens the page, **Then** the empty state explains that review consumption is not ready without exposing raw diagnostics.
|
||||
|
||||
### User Story 2 - Review findings and accepted risks safely (Priority: P1)
|
||||
|
||||
A reviewer sees findings needing attention and accepted risks as customer-safe summaries, including repo-backed owner, rationale, expiry/review date, proof, and action where available.
|
||||
|
||||
**Why this priority**: Accepted risks must not be hidden in diagnostics or implied as “all clear”.
|
||||
|
||||
**Independent Test**: Render fixtures with open findings and accepted-risk states; assert customer-safe summaries appear and unsupported fields are unavailable/deferred rather than faked.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** findings require review, **When** the page renders, **Then** it shows findings summary and next action without raw finding payloads.
|
||||
2. **Given** accepted risks exist, **When** the page renders, **Then** accepted-risk state is visible with repo-backed owner/rationale/expiry fields where available.
|
||||
3. **Given** accepted-risk expiry/review date is missing, **When** the page renders, **Then** the missing review date is explicitly disclosed instead of suppressed.
|
||||
|
||||
### User Story 3 - Consume evidence, review pack, export, audit, and operation proof truthfully (Priority: P2)
|
||||
|
||||
A reviewer can distinguish evidence availability, review-pack readiness, export/download state, audit trail, and OperationRun proof without treating operation proof as customer evidence.
|
||||
|
||||
**Why this priority**: Review trust depends on evidence/result truth separation.
|
||||
|
||||
**Independent Test**: Render states for evidence missing, evidence available, pack required/generating/failed/available, export unavailable/available, and linked operation proof.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** evidence exists but no review pack exists, **When** the page renders, **Then** evidence is available and review pack is required.
|
||||
2. **Given** a ready non-expired review pack has file metadata and the actor is authorized, **When** the page renders, **Then** the open/download action is visible.
|
||||
3. **Given** linked OperationRun proof exists, **When** the page renders, **Then** operation proof appears as secondary proof and raw operation JSON remains hidden.
|
||||
|
||||
### User Story 4 - Preserve RBAC, context, and customer-safe disclosure (Priority: P1)
|
||||
|
||||
Unauthorized or cross-workspace users cannot infer review, evidence, pack, finding, accepted-risk, audit, or diagnostics existence; authorized users see only capability-appropriate actions.
|
||||
|
||||
**Why this priority**: Customer-facing review consumption must not weaken tenant/workspace isolation.
|
||||
|
||||
**Independent Test**: Attempt cross-workspace environment filters and missing-capability paths; assert deny-as-not-found/hidden actions and no `/admin/t` or legacy query alias behavior.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a user is not entitled to the workspace/environment, **When** they request a filtered workspace URL, **Then** the page denies as not found or shows no leaked state.
|
||||
2. **Given** a user lacks diagnostics capability, **When** the page renders, **Then** diagnostics are unavailable or hidden and raw/support detail is not visible.
|
||||
3. **Given** legacy query aliases are present, **When** the page opens, **Then** only canonical `environment_id` can narrow the workspace hub.
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
- **FR-001**: The page MUST render a first-screen decision card with status, reason, impact, and one primary next action.
|
||||
- **FR-002**: The page MUST classify customer-review consumption data using `repo-verified`, `derived from existing model`, `foundation-real`, `not available`, or `deferred` in `repo-truth-map.md`.
|
||||
- **FR-003**: The page MUST derive visible consumption states from existing review, evidence, review-pack, finding, accepted-risk, audit, and operation truth; it MUST NOT persist new readiness truth.
|
||||
- **FR-004**: The page MUST distinguish review readiness, evidence state, review-pack state, export/download state, findings attention, accepted-risk accountability, audit trail, and operation proof.
|
||||
- **FR-005**: The page MUST keep diagnostics collapsed or unavailable by default and capability-gated when available.
|
||||
- **FR-006**: The page MUST NOT expose raw provider JSON, raw OperationRun payload, stack traces, fingerprints, internal IDs as primary labels, or provider diagnostics in the default customer-safe surface.
|
||||
- **FR-007**: Review pack/open/download actions MUST appear only when repo-backed and authorized.
|
||||
- **FR-008**: Customer-safe, auditor-ready, export-ready, evidence-backed, healthy, compliant, or complete claims MUST appear only when repo truth supports them.
|
||||
- **FR-009**: Accepted risks MUST be visible in the customer-safe summary when repo-backed and MUST NOT be hidden only in diagnostics.
|
||||
- **FR-010**: Workspace/environment scope MUST remain explicit; `environment_id` is a page-level filter only and legacy query aliases MUST NOT resurrect context.
|
||||
- **FR-011**: Existing authorization, policies, capabilities, and audit behavior MUST remain authoritative for all linked actions and proof surfaces.
|
||||
- **FR-012**: New visible strings MUST follow existing localization conventions where applicable and avoid mixed-language static copy.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
- Page render MUST remain DB-only and MUST NOT call Microsoft Graph or external providers.
|
||||
- The implementation SHOULD reuse native Filament components and shared primitives before local Blade/Tailwind styling.
|
||||
- The implementation MUST preserve Filament v5 / Livewire v4.0+ compliance.
|
||||
- The implementation MUST keep panel provider registration unchanged in `apps/platform/bootstrap/providers.php`.
|
||||
- The implementation MUST avoid new frontend assets; if registered assets become necessary, update deployment notes to include `php artisan filament:assets`.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- External customer portal, invitations, auth/federation, customer attestation backend, PSA/helpdesk integration, email delivery, AI summarization, new evidence/review-pack/export generation, new report format, new OperationRun lifecycle, new migrations, new package, new queue/scheduler/storage/env var, new billing/entitlement system, and broad Customer Review Workspace navigation/shell redesign.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- **AC-001**: Customer Review Workspace default view starts with customer-safe status, reason, impact, and one primary next action.
|
||||
- **AC-002**: Findings and accepted risks are summarized customer-safely when repo-backed; unsupported owner/expiry/evidence fields are unavailable/deferred rather than invented.
|
||||
- **AC-003**: Evidence, review-pack, export/download, audit, and OperationRun proof are separated and truthfully labeled.
|
||||
- **AC-004**: Diagnostics are collapsed or unavailable by default and raw/support details do not appear in first-screen content.
|
||||
- **AC-005**: RBAC and workspace/environment isolation tests cover authorized, missing-capability, cross-workspace, diagnostics, and canonical-filter behavior.
|
||||
- **AC-006**: Browser smoke captures the decision card, findings/accepted-risk/evidence/pack states, diagnostics collapsed, and no `/admin/t` or legacy query alias regression.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Reviewers can answer “is this review ready to consume, what needs attention, and what evidence supports it?” from the first screen in validated fixtures.
|
||||
- No validated state shows a false customer-safe, auditor-ready, evidence-backed, export-ready, healthy, or compliant claim.
|
||||
- Focused Feature/Livewire and Browser validation pass without broad suite or helper drift.
|
||||
|
||||
## Risks
|
||||
|
||||
- Current runtime may already satisfy some Spec 342 states; implementation must avoid churn and update `repo-truth-map.md` before changing code.
|
||||
- Some requested concepts, such as acknowledgement/attestation or external delivery, may be not repo-backed and must remain deferred.
|
||||
- A local presenter can become a hidden framework if not kept page-local and derived-only.
|
||||
- Browser fixture setup can expand cost; keep one bounded smoke file and document unreachable states honestly.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Spec 342 is a manual user-provided promotion and may use number `342` even though automatic branch numbering would be distorted by unrelated remote branch numbers.
|
||||
- Current branch `platform-dev` was clean before Spec Kit branch creation.
|
||||
- The repo remains pre-production under the constitution’s lean doctrine.
|
||||
- Existing Customer Review Workspace page and related resources remain the implementation target; no new route is required.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None block safe implementation. Implementation must mark any unsupported state as `not available` or `deferred` in `repo-truth-map.md` instead of adding backend truth inside this spec.
|
||||
|
||||
## Follow-up Spec Candidates
|
||||
|
||||
- Customer Review Attestation & Accepted Risk Lifecycle.
|
||||
- Decision-Based Governance Inbox Final Operator Workflow.
|
||||
- Localization v1 Product Surface Hardening.
|
||||
- Provider Readiness Productization.
|
||||
- External Support Desk / PSA Handoff.
|
||||
@ -0,0 +1,169 @@
|
||||
# Tasks: Spec 342 - Customer Review Workspace v1 Final Consumption Productization
|
||||
|
||||
**Input**: `specs/342-customer-review-workspace-final-consumption-productization/spec.md`, `plan.md`, `repo-truth-map.md`, and `customer-review-consumption-state-contract.md`
|
||||
**Prerequisites**: Spec artifacts prepared; implementation must start from repo-truth verification.
|
||||
**Implementation status**: implemented and validated with targeted Feature/Livewire and Browser smoke coverage. The broad focused regression command was run; unrelated failures outside this customer-review spec are documented in the final implementation report.
|
||||
|
||||
**Tests**: Required. This changes a strategic customer-safe Filament/Livewire page and must be validated with Feature/Livewire tests plus one bounded Browser smoke.
|
||||
|
||||
## Test Governance Checklist
|
||||
|
||||
- [x] Lane assignment is explicit and narrow: Feature/Livewire for state/RBAC/context, Browser for rendered customer-safe first-screen proof.
|
||||
- [x] New or changed tests stay in the smallest honest family; browser coverage is one explicit Spec 342 smoke file.
|
||||
- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default.
|
||||
- [x] Planned validation commands cover the change without pulling in unrelated heavy-governance cost.
|
||||
- [x] The surface profile is `global-context-shell` + customer-safe strategic review surface.
|
||||
- [x] Any unreachable state is documented in the active spec package rather than faked.
|
||||
|
||||
## Phase 1: Preparation And Repo Truth
|
||||
|
||||
**Purpose**: Prevent duplicate Spec 326 work and false customer-safe/evidence/export claims before runtime edits.
|
||||
|
||||
- [x] T001 Re-read `specs/342-customer-review-workspace-final-consumption-productization/spec.md`, `plan.md`, `repo-truth-map.md`, `customer-review-consumption-state-contract.md`, and this `tasks.md`.
|
||||
- [x] T002 Confirm branch and working tree intent; record `git status --short --branch` and `git log -1 --oneline`.
|
||||
- [x] T003 Inspect current `CustomerReviewWorkspace` page/view and existing customer-review tests before editing:
|
||||
- `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`
|
||||
- `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php`
|
||||
- `apps/platform/tests/Feature/Reviews/`
|
||||
- `apps/platform/tests/Browser/Spec326CustomerReviewWorkspaceProductizationSmokeTest.php`
|
||||
- [x] T004 Inspect related truth surfaces without redesigning them:
|
||||
- `EnvironmentReviewResource`, `EvidenceSnapshotResource`, `ReviewPackResource`, `FindingExceptionResource`, `StoredReportResource`
|
||||
- `EnvironmentReview`, `EvidenceSnapshot`, `ReviewPack`, `Finding`, `FindingException`, `OperationRun`
|
||||
- `ReviewPackDownloadController`, `OperationRunLinks`, `ArtifactTruthPresenter`
|
||||
- [x] T005 Update `repo-truth-map.md` with any newly discovered source, capability, unsupported state, or deferred state before runtime changes.
|
||||
- [x] T006 Confirm no migration, package, env var, queue, scheduler, storage, Graph scope, Dokploy, or Filament asset change is required; if one appears necessary, stop and update spec/plan first.
|
||||
- [x] T007 Confirm Filament v5 / Livewire v4.0+ compliance and no Livewire v3/Filament legacy API use.
|
||||
- [x] T008 Confirm panel provider registration remains `apps/platform/bootstrap/providers.php`.
|
||||
- [x] T009 Confirm related globally searchable resources stay disabled or have safe View/Edit pages; do not enable global search in this spec.
|
||||
|
||||
## Phase 2: Feature Tests First
|
||||
|
||||
**Purpose**: Lock the customer-safe consumption contract before UI changes.
|
||||
|
||||
- [x] T010 Create `apps/platform/tests/Feature/Filament/Spec342CustomerReviewWorkspaceConsumptionTest.php`.
|
||||
- [x] T011 [P] Add tests asserting the decision card renders status, reason, impact, and exactly one primary next action for a released review.
|
||||
- [x] T012 [P] Add tests asserting the page does not start with a raw table/log/diagnostics surface and raw diagnostics are hidden by default.
|
||||
- [x] T013 [P] Add tests for review-not-ready, evidence-missing, evidence-available, review-pack-required, and review-pack-available states where repo fixtures support them.
|
||||
- [x] T014 [P] Add tests asserting no customer-safe, auditor-ready, export-ready, evidence-backed, healthy, or compliant claim appears without repo-backed truth.
|
||||
- [x] T015 [P] Add tests for findings summary visibility, open/high-impact counts where supported, customer-safe row copy, and raw finding payload absence.
|
||||
- [x] T016 [P] Add tests for accepted-risk summary visibility, owner/rationale/expiry/review-date fields where repo-backed, and missing review-date disclosure where applicable.
|
||||
- [x] T017 [P] Add tests for evidence/review-pack/export state separation, including review-pack download/open action visibility only when authorized and backed by ready file metadata.
|
||||
- [x] T018 [P] Add tests for OperationRun proof and audit trail links as secondary proof, with raw OperationRun JSON hidden.
|
||||
- [x] T019 Add RBAC/context tests for unauthorized workspace/environment access, missing diagnostics capability, cross-workspace evidence/review-pack leakage prevention, and no `/admin/t`.
|
||||
- [x] T020 Add canonical filter tests proving `environment_id` is the only page-level filter and legacy query aliases do not resurrect hidden context after Spec 341.
|
||||
|
||||
## Phase 3: Consumption State Contract And Presenter
|
||||
|
||||
**Purpose**: Centralize derived display state without creating new persisted truth or a generic framework.
|
||||
|
||||
- [x] T021 Verify whether existing `CustomerReviewWorkspace` payload helpers can implement the state contract without a new class.
|
||||
- [x] T022 If current page/view logic is scattered, create a small page-local `CustomerReviewWorkspacePresenter` or equivalent derived payload builder; keep it non-persistent and non-generic.
|
||||
- [x] T023 Compute decision-card fields from existing truth: status, reason, impact, primary action label/url/icon, and unavailable/deferred fallbacks.
|
||||
- [x] T024 Compute review readiness flow steps from existing review/evidence/finding/accepted-risk/review-pack/export truth.
|
||||
- [x] T025 Compute findings summary using repo-backed status/severity/owner/due fields only; unsupported fields render unavailable/deferred.
|
||||
- [x] T026 Compute accepted-risk summary using `FindingException` / decision truth only; unsupported attestation/lifecycle fields render unavailable/deferred.
|
||||
- [x] T027 Compute evidence/review-pack/export state separately; do not treat OperationRun completion as evidence availability or customer-safe output.
|
||||
- [x] T028 Compute diagnostics state as collapsed/unavailable by default and capability-aware when shown.
|
||||
|
||||
## Phase 4: Customer-Safe First Screen
|
||||
|
||||
**Purpose**: Make review consumption immediately understandable without raw diagnostics.
|
||||
|
||||
- [x] T029 Update `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` to expose the final consumption payload and preserve current workspace/environment filter behavior.
|
||||
- [x] T030 Update `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php` so the first viewport starts with the decision card and proof summary, not the package index table.
|
||||
- [x] T031 Ensure the decision card asks the customer-safe question and shows status, reason, impact, and one primary next action.
|
||||
- [x] T032 Render review readiness flow with steps: review data, evidence, findings triaged, accepted risks reviewed, review pack, customer output.
|
||||
- [x] T033 Render findings summary and accepted-risk summary as customer-safe content above diagnostics.
|
||||
- [x] T034 Render evidence/review-pack/export proof panel as secondary proof and keep raw/support fields collapsed.
|
||||
- [x] T035 Keep the existing review package index/table as secondary context after the decision and proof sections.
|
||||
- [x] T036 Ensure copy is localization-ready and does not mix German/English static labels on the same surface.
|
||||
|
||||
## Phase 5: Actions, RBAC, And Safety
|
||||
|
||||
**Purpose**: Preserve read-only customer-safe posture and capability-aware action visibility.
|
||||
|
||||
- [x] T037 Show only repo-backed and authorized actions: review findings, open evidence, open review pack, download review pack, open audit trail, or view operation proof.
|
||||
- [x] T038 Hide or mark unavailable any action for acknowledgement/attestation, external delivery, regeneration, approval, revoke, expire, or repair unless it is already repo-backed and authorized.
|
||||
- [x] T039 Ensure unauthorized actions do not leak record existence or hidden diagnostics.
|
||||
- [x] T040 Preserve existing page-open audit logging and avoid secrets/raw payloads in metadata.
|
||||
- [x] T041 If any destructive/high-impact action is introduced unexpectedly, stop and update spec/plan before implementing `Action::make(...)->action(...)`, `->requiresConfirmation()`, authorization, audit, notification, and tests.
|
||||
|
||||
## Phase 6: Workspace / Environment Context
|
||||
|
||||
**Purpose**: Preserve Specs 340 and 341 while productizing review consumption.
|
||||
|
||||
- [x] T042 Verify clean `/admin/reviews/workspace` remains workspace-wide and does not inherit remembered environment context.
|
||||
- [x] T043 Verify `/admin/reviews/workspace?environment_id={id}` filters only page data, shows visible filter context, and keeps workspace shell ownership.
|
||||
- [x] T044 Verify clear filter returns to a clean URL and reload/back/forward do not resurrect hidden scope.
|
||||
- [x] T045 Verify legacy query aliases are ignored or rejected and never establish authority.
|
||||
- [x] T046 Verify cross-workspace or unauthorized `environment_id` remains safe no-access/404.
|
||||
|
||||
## Phase 7: Browser Smoke And Screenshots
|
||||
|
||||
**Purpose**: Prove the rendered customer-safe experience.
|
||||
|
||||
- [x] T047 Create `apps/platform/tests/Browser/Spec342CustomerReviewWorkspaceConsumptionSmokeTest.php`.
|
||||
- [x] T048 Browser state: review not ready; assert decision card, missing reason, no false ready/export/evidence claim, diagnostics collapsed.
|
||||
- [x] T049 Browser state: review ready with evidence; assert evidence state, review-pack state, primary next action, and no raw payload.
|
||||
- [x] T050 Browser state: review pack available; assert open/download action only when authorized and file metadata supports it.
|
||||
- [x] T051 Browser state: findings need attention; assert findings summary and customer-safe next action.
|
||||
- [x] T052 Browser state: accepted risks present; assert accepted-risk summary and no hidden-only risk disclosure.
|
||||
- [x] T053 Browser state: diagnostics collapsed; assert raw diagnostics are absent before explicit/capability-gated disclosure and rendered URLs do not contain `/admin/t` or legacy scope query aliases.
|
||||
- [x] T054 Capture screenshots under `specs/342-customer-review-workspace-final-consumption-productization/artifacts/screenshots/`:
|
||||
- `spec342-customer-review-workspace-01-evidence-incomplete-not-ready.png`
|
||||
- `spec342-customer-review-workspace-02-ready-with-evidence.png`
|
||||
- `spec342-customer-review-workspace-03-review-pack-available.png`
|
||||
- `spec342-customer-review-workspace-04-findings-need-attention.png`
|
||||
- `spec342-customer-review-workspace-05-accepted-risks-present.png`
|
||||
- `spec342-customer-review-workspace-06-diagnostics-collapsed.png`
|
||||
- `spec342-customer-review-workspace-07-dark-mode.png`
|
||||
- [x] T055 If a screenshot state is unreachable, document why in the spec package rather than faking backend truth.
|
||||
|
||||
## Phase 8: UI Coverage And Documentation Artifacts
|
||||
|
||||
**Purpose**: Satisfy UI-COV without unrelated docs churn.
|
||||
|
||||
- [x] T056 Decide after runtime diff whether `docs/ui-ux-enterprise-audit/route-inventory.md` or `design-coverage-matrix.md` needs an update.
|
||||
- [x] T057 UI coverage docs were not changed: this spec productizes the existing `/admin/reviews/workspace` route without new navigation, route inventory, or archetype coverage. The active spec artifacts and browser screenshots provide the bounded UI evidence.
|
||||
- [x] T058 Update `repo-truth-map.md` final classifications for implemented, unavailable, and deferred states.
|
||||
- [x] T059 Update `customer-review-consumption-state-contract.md` if implementation discovers a repo-backed state that changes the contract.
|
||||
- [x] T060 Do not create general documentation files outside required Spec Kit/UI coverage artifacts.
|
||||
|
||||
## Phase 9: Validation
|
||||
|
||||
**Purpose**: Run narrow proof and report honestly.
|
||||
|
||||
- [x] T061 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Filament/Spec342CustomerReviewWorkspaceConsumptionTest.php --compact`.
|
||||
- [x] T062 Run `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec342CustomerReviewWorkspaceConsumptionSmokeTest.php --compact`.
|
||||
- [x] T063 Run `cd apps/platform && ./vendor/bin/sail artisan test --filter='CustomerReview|ReviewPack|Evidence|AcceptedRisk|Finding|Audit|Spec341' --compact` (executed; customer-review regressions fixed, unrelated auth/dashboard/restore/finding-exception failures remain documented in final report).
|
||||
- [x] T064 Run `cd apps/platform && ./vendor/bin/sail pint --dirty`.
|
||||
- [x] T065 Run `git diff --check`.
|
||||
- [x] T066 Report full-suite status honestly if not run.
|
||||
- [x] T067 Confirm no migrations, seeders, packages, env vars, queues, scheduler, storage, deployment assets, backwards compatibility layer, `/admin/t`, or legacy query alias support were added.
|
||||
|
||||
## Explicit Non-Goals
|
||||
|
||||
- [x] NT001 Do not build an external customer portal, external authentication, invitation links, external sharing, or email/PSA delivery.
|
||||
- [x] NT002 Do not implement a new review, evidence, report, review-pack, export, PDF, ZIP, or queue backend.
|
||||
- [x] NT003 Do not introduce new persisted readiness truth, enum/status family, generic readiness framework, or cross-domain UI taxonomy.
|
||||
- [x] NT004 Do not expose raw provider JSON, raw OperationRun payload, internal IDs as primary labels, stack traces, fingerprints, or diagnostics by default.
|
||||
- [x] NT005 Do not rewrite completed Specs 249, 258, 312, 326, 329, 335, 336, 337, 340, or 341.
|
||||
- [x] NT006 Do not change shell/sidebar/topbar/navigation scope contracts or Provider Connection authority.
|
||||
- [x] NT007 Do not add `/admin/t` routes or legacy query alias support.
|
||||
|
||||
## Required Final Report Content For Later Implementation
|
||||
|
||||
When implementation later completes, report:
|
||||
|
||||
- Changed behavior.
|
||||
- Customer Review states and unsupported/deferred states.
|
||||
- Customer-safe consumption and diagnostics default state.
|
||||
- Evidence / Review Pack / Export truth.
|
||||
- Findings and accepted-risk visibility.
|
||||
- RBAC/context behavior.
|
||||
- Files changed.
|
||||
- Tests run and results.
|
||||
- Browser smoke and screenshots path.
|
||||
- Known gaps and follow-up specs.
|
||||
- Full suite run/not run.
|
||||
- Explicit no migrations/packages/env/queues/scheduler/storage/deployment assets/destructive actions/backcompat/legacy aliases statement.
|
||||