feat: customer review acknowledgement lifecycle (343) #415
@ -9,6 +9,7 @@
|
||||
use App\Filament\Resources\EnvironmentReviewResource;
|
||||
use App\Filament\Resources\EvidenceSnapshotResource;
|
||||
use App\Models\EnvironmentReview;
|
||||
use App\Models\EnvironmentReviewAcknowledgement;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\Finding;
|
||||
use App\Models\FindingException;
|
||||
@ -18,6 +19,7 @@
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Services\Audit\WorkspaceAuditLogger;
|
||||
use App\Services\EnvironmentReviews\EnvironmentReviewAcknowledgementService;
|
||||
use App\Services\EnvironmentReviews\EnvironmentReviewRegisterService;
|
||||
use App\Services\ReviewPackService;
|
||||
use App\Support\Audit\AuditActionId;
|
||||
@ -43,6 +45,8 @@
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Concerns\InteractsWithTable;
|
||||
@ -168,6 +172,63 @@ protected function getHeaderActions(): array
|
||||
return $actions;
|
||||
}
|
||||
|
||||
public function acknowledgeReviewAction(): Action
|
||||
{
|
||||
return Action::make('acknowledgeReview')
|
||||
->label(__('localization.review.acknowledge_review'))
|
||||
->icon('heroicon-o-check-badge')
|
||||
->color('primary')
|
||||
->record(fn (): ?ManagedEnvironment => $this->latestReleasedTenant())
|
||||
->requiresConfirmation()
|
||||
->modalHeading(__('localization.review.acknowledge_review_heading'))
|
||||
->modalDescription(__('localization.review.acknowledge_review_description'))
|
||||
->modalSubmitActionLabel(__('localization.review.acknowledge_review_confirm'))
|
||||
->form([
|
||||
Textarea::make('comment')
|
||||
->label(__('localization.review.acknowledge_review_comment'))
|
||||
->rows(4)
|
||||
->maxLength(2000),
|
||||
])
|
||||
->action(function (array $data): void {
|
||||
$actor = auth()->user();
|
||||
$tenant = $this->latestReleasedTenant();
|
||||
$review = $tenant instanceof ManagedEnvironment
|
||||
? $this->latestPublishedReview($tenant)
|
||||
: null;
|
||||
|
||||
if (! $actor instanceof User || ! $tenant instanceof ManagedEnvironment || ! $review instanceof EnvironmentReview) {
|
||||
Notification::make()
|
||||
->title(__('localization.review.acknowledge_review_unavailable'))
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
app(EnvironmentReviewAcknowledgementService::class)->acknowledge(
|
||||
tenant: $tenant,
|
||||
review: $review,
|
||||
actor: $actor,
|
||||
comment: is_string($data['comment'] ?? null) ? (string) $data['comment'] : null,
|
||||
);
|
||||
} catch (\Throwable $throwable) {
|
||||
Notification::make()
|
||||
->title(__('localization.review.acknowledge_review_failed'))
|
||||
->body($throwable->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Notification::make()
|
||||
->title(__('localization.review.review_acknowledged'))
|
||||
->success()
|
||||
->send();
|
||||
});
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
@ -352,6 +413,7 @@ public function latestReviewConsumptionPayload(): ?array
|
||||
'readiness' => $this->reviewReadinessForTenant($tenant, $review, $packageAvailability, $downloadUrl, $reviewUrl),
|
||||
'readiness_flow' => $this->reviewConsumptionFlowForReview($tenant, $review, $packageAvailability, $downloadUrl),
|
||||
'finding_panel' => $findingPanel,
|
||||
'acknowledgement' => $this->reviewAcknowledgementPayloadForReview($tenant, $review, $packageAvailability, $downloadUrl),
|
||||
'decision' => $decision,
|
||||
'accepted_risks' => $acceptedRisks,
|
||||
'accepted_risk_panel' => $this->acceptedRiskPanelForReview($review, $tenant),
|
||||
@ -365,6 +427,167 @@ public function latestReviewConsumptionPayload(): ?array
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{state:string,label:string,description:string} $packageAvailability
|
||||
* @return array{
|
||||
* state: 'not_available'|'required'|'acknowledged'|'re_ack_required',
|
||||
* status_label: string,
|
||||
* status_color: string,
|
||||
* reason: string,
|
||||
* impact: string,
|
||||
* action_name: string|null,
|
||||
* action_label: string,
|
||||
* action_color: string,
|
||||
* action_disabled: bool,
|
||||
* action_helper: string|null,
|
||||
* acknowledged_at_label: string|null,
|
||||
* acknowledged_by_label: string|null,
|
||||
* comment: string|null,
|
||||
* basis: list<array{label:string,value:string,color:string}>
|
||||
* }
|
||||
*/
|
||||
private function reviewAcknowledgementPayloadForReview(
|
||||
ManagedEnvironment $tenant,
|
||||
EnvironmentReview $review,
|
||||
array $packageAvailability,
|
||||
?string $downloadUrl,
|
||||
): array {
|
||||
$actor = auth()->user();
|
||||
|
||||
$canAcknowledge = $actor instanceof User
|
||||
&& $actor->canAccessTenant($tenant)
|
||||
&& $actor->can(Capabilities::ENVIRONMENT_REVIEW_ACKNOWLEDGE, $tenant);
|
||||
|
||||
$ack = EnvironmentReviewAcknowledgement::query()
|
||||
->with(['acknowledgedByUser'])
|
||||
->where('environment_review_id', (int) $review->getKey())
|
||||
->where('managed_environment_id', (int) $review->managed_environment_id)
|
||||
->where('workspace_id', (int) $review->workspace_id)
|
||||
->first();
|
||||
|
||||
$currentReviewPackId = is_numeric($review->current_export_review_pack_id)
|
||||
? (int) $review->current_export_review_pack_id
|
||||
: null;
|
||||
$currentEvidenceSnapshotId = is_numeric($review->evidence_snapshot_id)
|
||||
? (int) $review->evidence_snapshot_id
|
||||
: null;
|
||||
|
||||
$reviewPackProof = $this->reviewPackProofForReview($packageAvailability, $downloadUrl);
|
||||
$evidenceState = $this->evidenceStatusState($tenant);
|
||||
|
||||
$basis = [
|
||||
[
|
||||
'label' => __('localization.review.review_pack'),
|
||||
'value' => $reviewPackProof['label'],
|
||||
'color' => $reviewPackProof['color'],
|
||||
],
|
||||
[
|
||||
'label' => __('localization.review.evidence'),
|
||||
'value' => $this->evidenceStatusLabelForState($evidenceState),
|
||||
'color' => $this->evidenceStatusColorForState($evidenceState),
|
||||
],
|
||||
];
|
||||
|
||||
if (! $review->isPublished()) {
|
||||
return [
|
||||
'state' => 'not_available',
|
||||
'status_label' => __('localization.review.acknowledgement_not_available'),
|
||||
'status_color' => 'gray',
|
||||
'reason' => __('localization.review.acknowledgement_not_available_reason'),
|
||||
'impact' => __('localization.review.acknowledgement_not_available_impact'),
|
||||
'action_name' => null,
|
||||
'action_label' => __('localization.review.review_accepted_risks'),
|
||||
'action_color' => 'gray',
|
||||
'action_disabled' => true,
|
||||
'action_helper' => null,
|
||||
'acknowledged_at_label' => null,
|
||||
'acknowledged_by_label' => null,
|
||||
'comment' => null,
|
||||
'basis' => $basis,
|
||||
];
|
||||
}
|
||||
|
||||
if (! $ack instanceof EnvironmentReviewAcknowledgement) {
|
||||
return [
|
||||
'state' => 'required',
|
||||
'status_label' => __('localization.review.acknowledgement_required'),
|
||||
'status_color' => 'warning',
|
||||
'reason' => __('localization.review.acknowledgement_required_reason'),
|
||||
'impact' => __('localization.review.acknowledgement_required_impact'),
|
||||
'action_name' => 'acknowledgeReview',
|
||||
'action_label' => __('localization.review.acknowledge_review'),
|
||||
'action_color' => 'primary',
|
||||
'action_disabled' => ! $canAcknowledge,
|
||||
'action_helper' => ! $canAcknowledge
|
||||
? __('localization.review.acknowledgement_requires_permission')
|
||||
: null,
|
||||
'acknowledged_at_label' => null,
|
||||
'acknowledged_by_label' => null,
|
||||
'comment' => null,
|
||||
'basis' => $basis,
|
||||
];
|
||||
}
|
||||
|
||||
$basisDriftDetected = false;
|
||||
|
||||
if (is_numeric($ack->review_pack_id) && is_int($currentReviewPackId) && (int) $ack->review_pack_id !== $currentReviewPackId) {
|
||||
$basisDriftDetected = true;
|
||||
}
|
||||
|
||||
if (is_numeric($ack->evidence_snapshot_id) && is_int($currentEvidenceSnapshotId) && (int) $ack->evidence_snapshot_id !== $currentEvidenceSnapshotId) {
|
||||
$basisDriftDetected = true;
|
||||
}
|
||||
|
||||
$acknowledgedAtLabel = $ack->acknowledged_at instanceof \DateTimeInterface
|
||||
? $ack->acknowledged_at->format('M j, Y H:i')
|
||||
: null;
|
||||
|
||||
$acknowledgedByLabel = $ack->acknowledgedByUser instanceof User
|
||||
? (string) $ack->acknowledgedByUser->name
|
||||
: null;
|
||||
|
||||
$comment = is_string($ack->comment) ? trim($ack->comment) : null;
|
||||
$comment = filled($comment) ? $comment : null;
|
||||
|
||||
if ($basisDriftDetected) {
|
||||
return [
|
||||
'state' => 're_ack_required',
|
||||
'status_label' => __('localization.review.acknowledgement_re_ack_required'),
|
||||
'status_color' => 'warning',
|
||||
'reason' => __('localization.review.acknowledgement_re_ack_required_reason'),
|
||||
'impact' => __('localization.review.acknowledgement_re_ack_required_impact'),
|
||||
'action_name' => 'acknowledgeReview',
|
||||
'action_label' => __('localization.review.re_acknowledge_review'),
|
||||
'action_color' => 'warning',
|
||||
'action_disabled' => ! $canAcknowledge,
|
||||
'action_helper' => ! $canAcknowledge
|
||||
? __('localization.review.acknowledgement_requires_permission')
|
||||
: null,
|
||||
'acknowledged_at_label' => $acknowledgedAtLabel,
|
||||
'acknowledged_by_label' => $acknowledgedByLabel,
|
||||
'comment' => $comment,
|
||||
'basis' => $basis,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'state' => 'acknowledged',
|
||||
'status_label' => __('localization.review.review_acknowledged'),
|
||||
'status_color' => 'success',
|
||||
'reason' => __('localization.review.acknowledgement_recorded_reason'),
|
||||
'impact' => __('localization.review.acknowledgement_recorded_impact'),
|
||||
'action_name' => null,
|
||||
'action_label' => __('localization.review.review_accepted_risks'),
|
||||
'action_color' => 'gray',
|
||||
'action_disabled' => true,
|
||||
'action_helper' => null,
|
||||
'acknowledged_at_label' => $acknowledgedAtLabel,
|
||||
'acknowledged_by_label' => $acknowledgedByLabel,
|
||||
'comment' => $comment,
|
||||
'basis' => $basis,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{label:string,description:string,is_filtered:bool}
|
||||
*/
|
||||
@ -979,15 +1202,15 @@ private function acceptedRiskDetailRows(Collection $exceptions): array
|
||||
$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();
|
||||
->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',
|
||||
'color' => is_string($owner) ? 'info' : 'warning',
|
||||
],
|
||||
];
|
||||
|
||||
@ -1010,7 +1233,7 @@ private function acceptedRiskDetailRows(Collection $exceptions): array
|
||||
$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',
|
||||
'color' => is_string($reason) ? 'info' : 'warning',
|
||||
];
|
||||
|
||||
return $rows;
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Support\Concerns\DerivesWorkspaceIdFromTenant;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class EnvironmentReviewAcknowledgement extends Model
|
||||
{
|
||||
use DerivesWorkspaceIdFromTenant;
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'acknowledged_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<Workspace, $this>
|
||||
*/
|
||||
public function workspace(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Workspace::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<ManagedEnvironment, $this>
|
||||
*/
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ManagedEnvironment::class, 'managed_environment_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<EnvironmentReview, $this>
|
||||
*/
|
||||
public function review(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EnvironmentReview::class, 'environment_review_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<EvidenceSnapshot, $this>
|
||||
*/
|
||||
public function evidenceSnapshot(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EvidenceSnapshot::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<ReviewPack, $this>
|
||||
*/
|
||||
public function reviewPack(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ReviewPack::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function acknowledgedByUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'acknowledged_by_user_id');
|
||||
}
|
||||
}
|
||||
@ -57,6 +57,7 @@ class RoleCapabilityMap
|
||||
Capabilities::REVIEW_PACK_MANAGE,
|
||||
Capabilities::ENVIRONMENT_REVIEW_VIEW,
|
||||
Capabilities::ENVIRONMENT_REVIEW_MANAGE,
|
||||
Capabilities::ENVIRONMENT_REVIEW_ACKNOWLEDGE,
|
||||
Capabilities::MANAGED_ENVIRONMENT_TRIAGE_REVIEW_MANAGE,
|
||||
Capabilities::EVIDENCE_VIEW,
|
||||
Capabilities::EVIDENCE_MANAGE,
|
||||
@ -101,6 +102,7 @@ class RoleCapabilityMap
|
||||
Capabilities::REVIEW_PACK_MANAGE,
|
||||
Capabilities::ENVIRONMENT_REVIEW_VIEW,
|
||||
Capabilities::ENVIRONMENT_REVIEW_MANAGE,
|
||||
Capabilities::ENVIRONMENT_REVIEW_ACKNOWLEDGE,
|
||||
Capabilities::MANAGED_ENVIRONMENT_TRIAGE_REVIEW_MANAGE,
|
||||
Capabilities::EVIDENCE_VIEW,
|
||||
Capabilities::EVIDENCE_MANAGE,
|
||||
|
||||
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\EnvironmentReviews;
|
||||
|
||||
use App\Models\EnvironmentReview;
|
||||
use App\Models\EnvironmentReviewAcknowledgement;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\User;
|
||||
use App\Services\Audit\WorkspaceAuditLogger;
|
||||
use App\Support\Audit\AuditActionId;
|
||||
use App\Support\Auth\Capabilities;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
final class EnvironmentReviewAcknowledgementService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly WorkspaceAuditLogger $audit,
|
||||
) {}
|
||||
|
||||
public function acknowledge(
|
||||
ManagedEnvironment $tenant,
|
||||
EnvironmentReview $review,
|
||||
User $actor,
|
||||
?string $comment = null,
|
||||
): EnvironmentReviewAcknowledgement {
|
||||
if (! $actor->canAccessTenant($tenant)) {
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
Gate::forUser($actor)->authorize(Capabilities::ENVIRONMENT_REVIEW_ACKNOWLEDGE, $tenant);
|
||||
|
||||
if ((int) $review->managed_environment_id !== (int) $tenant->getKey()) {
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
if ((int) $review->workspace_id !== (int) $tenant->workspace_id) {
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
if (! $review->isPublished()) {
|
||||
throw new InvalidArgumentException('Only published reviews can be acknowledged.');
|
||||
}
|
||||
|
||||
$comment = is_string($comment) ? trim($comment) : null;
|
||||
$comment = $comment !== null && $comment !== '' ? $comment : null;
|
||||
|
||||
if (is_string($comment) && mb_strlen($comment) > 2000) {
|
||||
throw new InvalidArgumentException('comment must be at most 2000 characters.');
|
||||
}
|
||||
|
||||
$acknowledgedAt = CarbonImmutable::now();
|
||||
|
||||
$ack = EnvironmentReviewAcknowledgement::query()->updateOrCreate(
|
||||
['environment_review_id' => (int) $review->getKey()],
|
||||
[
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'acknowledged_at' => $acknowledgedAt,
|
||||
'acknowledged_by_user_id' => (int) $actor->getKey(),
|
||||
'comment' => $comment,
|
||||
'evidence_snapshot_id' => is_numeric($review->evidence_snapshot_id) ? (int) $review->evidence_snapshot_id : null,
|
||||
'review_pack_id' => is_numeric($review->current_export_review_pack_id) ? (int) $review->current_export_review_pack_id : null,
|
||||
],
|
||||
);
|
||||
|
||||
$workspace = $tenant->workspace;
|
||||
|
||||
if ($workspace !== null) {
|
||||
$this->audit->log(
|
||||
workspace: $workspace,
|
||||
action: AuditActionId::EnvironmentReviewAcknowledged,
|
||||
context: [
|
||||
'metadata' => [
|
||||
'review_id' => (int) $review->getKey(),
|
||||
'acknowledgement_id' => (int) $ack->getKey(),
|
||||
'review_pack_id' => is_numeric($review->current_export_review_pack_id) ? (int) $review->current_export_review_pack_id : null,
|
||||
'evidence_snapshot_id' => is_numeric($review->evidence_snapshot_id) ? (int) $review->evidence_snapshot_id : null,
|
||||
],
|
||||
],
|
||||
actor: $actor,
|
||||
resourceType: 'environment_review',
|
||||
resourceId: (string) $review->getKey(),
|
||||
targetLabel: sprintf('ManagedEnvironment review #%d', (int) $review->getKey()),
|
||||
tenant: $tenant,
|
||||
);
|
||||
}
|
||||
|
||||
return $ack;
|
||||
}
|
||||
}
|
||||
@ -110,6 +110,7 @@ enum AuditActionId: string
|
||||
case EnvironmentReviewArchived = 'environment_review.archived';
|
||||
case EnvironmentReviewOpened = 'environment_review.opened';
|
||||
case EnvironmentReviewExported = 'environment_review.exported';
|
||||
case EnvironmentReviewAcknowledged = 'environment_review.acknowledged';
|
||||
case EnvironmentReviewSuccessorCreated = 'environment_review.successor_created';
|
||||
case CustomerReviewWorkspaceOpened = 'customer_review_workspace.opened';
|
||||
case ReviewPackDownloaded = 'review_pack.downloaded';
|
||||
@ -280,6 +281,7 @@ private static function labels(): array
|
||||
self::EnvironmentReviewArchived->value => 'ManagedEnvironment review archived',
|
||||
self::EnvironmentReviewOpened->value => 'ManagedEnvironment review opened',
|
||||
self::EnvironmentReviewExported->value => 'ManagedEnvironment review exported',
|
||||
self::EnvironmentReviewAcknowledged->value => 'ManagedEnvironment review acknowledged',
|
||||
self::EnvironmentReviewSuccessorCreated->value => 'ManagedEnvironment review next cycle created',
|
||||
self::CustomerReviewWorkspaceOpened->value => 'Customer review workspace opened',
|
||||
self::ReviewPackDownloaded->value => 'Review pack downloaded',
|
||||
@ -394,6 +396,7 @@ private static function summaries(): array
|
||||
self::EnvironmentReviewArchived->value => 'ManagedEnvironment review archived',
|
||||
self::EnvironmentReviewOpened->value => 'ManagedEnvironment review opened',
|
||||
self::EnvironmentReviewExported->value => 'ManagedEnvironment review exported',
|
||||
self::EnvironmentReviewAcknowledged->value => 'ManagedEnvironment review acknowledged',
|
||||
self::EnvironmentReviewSuccessorCreated->value => 'ManagedEnvironment review next cycle created',
|
||||
self::CustomerReviewWorkspaceOpened->value => 'Customer review workspace opened',
|
||||
self::ReviewPackDownloaded->value => 'Review pack downloaded',
|
||||
|
||||
@ -150,6 +150,8 @@ class Capabilities
|
||||
|
||||
public const ENVIRONMENT_REVIEW_MANAGE = 'environment_review.manage';
|
||||
|
||||
public const ENVIRONMENT_REVIEW_ACKNOWLEDGE = 'environment_review.acknowledge';
|
||||
|
||||
// Portfolio triage review progress
|
||||
public const MANAGED_ENVIRONMENT_TRIAGE_REVIEW_MANAGE = 'managed_environment_triage_review.manage';
|
||||
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\EnvironmentReview;
|
||||
use App\Models\EnvironmentReviewAcknowledgement;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<EnvironmentReviewAcknowledgement>
|
||||
*/
|
||||
class EnvironmentReviewAcknowledgementFactory extends Factory
|
||||
{
|
||||
protected $model = EnvironmentReviewAcknowledgement::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'environment_review_id' => function (): int {
|
||||
return (int) EnvironmentReview::factory()->create()->getKey();
|
||||
},
|
||||
'managed_environment_id' => function (array $attributes): int {
|
||||
return (int) EnvironmentReview::query()->whereKey((int) $attributes['environment_review_id'])->value('managed_environment_id');
|
||||
},
|
||||
'workspace_id' => function (array $attributes): int {
|
||||
return (int) EnvironmentReview::query()->whereKey((int) $attributes['environment_review_id'])->value('workspace_id');
|
||||
},
|
||||
'acknowledged_at' => now(),
|
||||
'acknowledged_by_user_id' => User::factory(),
|
||||
'comment' => null,
|
||||
'evidence_snapshot_id' => null,
|
||||
'review_pack_id' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('environment_review_acknowledgements', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
$table->foreignId('managed_environment_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('workspace_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('environment_review_id')->constrained('environment_reviews')->cascadeOnDelete();
|
||||
|
||||
$table->timestampTz('acknowledged_at');
|
||||
$table->foreignId('acknowledged_by_user_id')->constrained('users');
|
||||
$table->string('comment', 2000)->nullable();
|
||||
|
||||
$table->foreignId('evidence_snapshot_id')->nullable()->constrained('evidence_snapshots')->nullOnDelete();
|
||||
$table->foreignId('review_pack_id')->nullable()->constrained('review_packs')->nullOnDelete();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['environment_review_id']);
|
||||
$table->index(['managed_environment_id', 'workspace_id', 'environment_review_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('environment_review_acknowledgements');
|
||||
}
|
||||
};
|
||||
@ -420,6 +420,32 @@
|
||||
'customer_safe_follow_ups' => 'Kundensichere Follow-ups',
|
||||
'customer_safe_follow_ups_empty' => 'Für dieses veröffentlichte Review sind keine kundensicheren Follow-ups aufgeführt.',
|
||||
'diagnostics_customer_workspace_default_hidden' => 'Supportdetails bleiben auf autorisierten Diagnoseflächen und werden in diesem kundensicheren Workspace standardmäßig nicht angezeigt.',
|
||||
'review_acknowledgement' => 'Review-Bestätigung',
|
||||
'acknowledge_review' => 'Review bestätigen',
|
||||
're_acknowledge_review' => 'Review erneut bestätigen',
|
||||
'acknowledge_review_heading' => 'Dieses Review bestätigen?',
|
||||
'acknowledge_review_description' => 'Erfasst eine kundensichere Bestätigung für das aktuell veröffentlichte Review-Paket. Dies ist keine rechtliche Attestierung.',
|
||||
'acknowledge_review_confirm' => 'Bestätigen',
|
||||
'acknowledge_review_comment' => 'Kommentar (optional)',
|
||||
'acknowledge_review_unavailable' => 'Review kann nicht bestätigt werden',
|
||||
'acknowledge_review_failed' => 'Review-Bestätigung fehlgeschlagen',
|
||||
'review_acknowledged' => 'Review bestätigt',
|
||||
'acknowledgement_not_available' => 'Bestätigung nicht verfügbar',
|
||||
'acknowledgement_not_available_reason' => 'Eine Bestätigung wird nur für veröffentlichte Review-Pakete erfasst.',
|
||||
'acknowledgement_not_available_impact' => 'Veröffentlichen Sie ein Review-Paket, bevor eine Bestätigung erfasst wird.',
|
||||
'review_accepted_risks' => 'Akzeptierte Risiken prüfen',
|
||||
'acknowledgement_required' => 'Bestätigung erforderlich',
|
||||
'acknowledgement_required_reason' => 'Dieses veröffentlichte Review wurde noch nicht bestätigt.',
|
||||
'acknowledgement_required_impact' => 'Erfassen Sie eine Bestätigung, bevor Sie dieses Review als gemeinsames kundensicheres Artefakt verwenden.',
|
||||
'acknowledgement_requires_permission' => 'Ihnen fehlt die Berechtigung, Reviews zu bestätigen.',
|
||||
'acknowledgement_re_ack_required' => 'Erneute Bestätigung erforderlich',
|
||||
'acknowledgement_re_ack_required_reason' => 'Das Review-Pack oder die Evidence-Basis hat sich seit der letzten Bestätigung geändert.',
|
||||
'acknowledgement_re_ack_required_impact' => 'Bestätigen Sie erneut, um das aktuelle kundensichere Paket zu prüfen.',
|
||||
'acknowledgement_recorded_reason' => 'Eine kundensichere Bestätigung ist für dieses veröffentlichte Review-Paket erfasst.',
|
||||
'acknowledgement_recorded_impact' => 'Dieses Review-Paket kann nun mit einer expliziten Bestätigungskette referenziert werden.',
|
||||
'basis' => 'Basis',
|
||||
'acknowledged_by' => 'Bestätigt von',
|
||||
'acknowledged_at' => 'Bestätigt am',
|
||||
'accepted_risk_summary' => 'Akzeptierte Risiken',
|
||||
'accepted_risk_no_action_needed' => 'Keine Aktion erforderlich',
|
||||
'accepted_risk_accountability' => 'Accepted-Risk-Verantwortlichkeit',
|
||||
|
||||
@ -420,6 +420,32 @@
|
||||
'customer_safe_follow_ups' => 'Customer-safe follow-ups',
|
||||
'customer_safe_follow_ups_empty' => 'No customer-safe follow-ups are listed for this released review.',
|
||||
'diagnostics_customer_workspace_default_hidden' => 'Support details stay on authorized diagnostic surfaces and are not shown in this customer-safe workspace by default.',
|
||||
'review_acknowledgement' => 'Review acknowledgement',
|
||||
'acknowledge_review' => 'Acknowledge review',
|
||||
're_acknowledge_review' => 'Re-acknowledge review',
|
||||
'acknowledge_review_heading' => 'Acknowledge this review?',
|
||||
'acknowledge_review_description' => 'Records a customer-safe acknowledgement for the current published review package. This is not a legal attestation.',
|
||||
'acknowledge_review_confirm' => 'Acknowledge',
|
||||
'acknowledge_review_comment' => 'Comment (optional)',
|
||||
'acknowledge_review_unavailable' => 'Unable to acknowledge review',
|
||||
'acknowledge_review_failed' => 'Review acknowledgement failed',
|
||||
'review_acknowledged' => 'Review acknowledged',
|
||||
'acknowledgement_not_available' => 'Acknowledgement unavailable',
|
||||
'acknowledgement_not_available_reason' => 'Acknowledgement is tracked only for published review packages.',
|
||||
'acknowledgement_not_available_impact' => 'Publish a review package before recording acknowledgement.',
|
||||
'review_accepted_risks' => 'Review accepted risks',
|
||||
'acknowledgement_required' => 'Acknowledgement required',
|
||||
'acknowledgement_required_reason' => 'This published review has not been acknowledged yet.',
|
||||
'acknowledgement_required_impact' => 'Record acknowledgement before relying on this review as a shared customer-safe artifact.',
|
||||
'acknowledgement_requires_permission' => 'You do not have permission to acknowledge reviews.',
|
||||
'acknowledgement_re_ack_required' => 'Re-acknowledgement required',
|
||||
'acknowledgement_re_ack_required_reason' => 'The review pack or evidence basis changed after the last acknowledgement.',
|
||||
'acknowledgement_re_ack_required_impact' => 'Re-acknowledge to confirm you reviewed the current customer-safe package.',
|
||||
'acknowledgement_recorded_reason' => 'A customer-safe acknowledgement is recorded for this published review package.',
|
||||
'acknowledgement_recorded_impact' => 'This review package can now be referenced with an explicit acknowledgement trail.',
|
||||
'basis' => 'Basis',
|
||||
'acknowledged_by' => 'Acknowledged by',
|
||||
'acknowledged_at' => 'Acknowledged at',
|
||||
'accepted_risk_summary' => 'Accepted risks',
|
||||
'accepted_risk_no_action_needed' => 'No action needed',
|
||||
'accepted_risk_accountability' => 'Accepted-risk accountability',
|
||||
|
||||
@ -39,6 +39,7 @@
|
||||
$readiness = $reviewPayload['readiness'];
|
||||
$readinessFlow = $reviewPayload['readiness_flow'];
|
||||
$findingPanel = $reviewPayload['finding_panel'];
|
||||
$acknowledgement = $reviewPayload['acknowledgement'];
|
||||
$asideEvidencePath = $reviewPayload['aside_evidence_path'];
|
||||
$reviewPackPanel = $reviewPayload['review_pack_panel'];
|
||||
$acceptedRisks = $reviewPayload['accepted_risks'];
|
||||
@ -169,6 +170,107 @@ class="flex min-h-36 flex-col gap-2 rounded-lg border border-gray-200 p-3 text-s
|
||||
</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-acknowledgement-card"
|
||||
>
|
||||
<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.review_acknowledgement') }}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
{{ $acknowledgement['reason'] }}
|
||||
</p>
|
||||
</div>
|
||||
<x-filament::badge :color="$acknowledgement['status_color']">
|
||||
{{ $acknowledgement['status_label'] }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-[minmax(0,1fr)_16rem]">
|
||||
<div class="flex h-full flex-col gap-2 rounded-lg border border-gray-200 bg-gray-50 p-4 text-sm dark:border-white/10 dark:bg-white/5">
|
||||
<div class="text-[0.7rem] font-semibold uppercase leading-4 tracking-wide text-gray-500 dark:text-gray-400">
|
||||
{{ __('localization.review.impact') }}
|
||||
</div>
|
||||
<p class="text-sm leading-6 text-gray-700 dark:text-gray-200">
|
||||
{{ $acknowledgement['impact'] }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex h-full flex-col rounded-lg border border-gray-200 p-4 text-sm dark:border-white/10">
|
||||
<div class="text-[0.7rem] font-semibold uppercase leading-4 tracking-wide text-gray-500 dark:text-gray-400">
|
||||
{{ __('localization.review.basis') }}
|
||||
</div>
|
||||
|
||||
<div class="mt-2 space-y-1.5">
|
||||
@foreach ($acknowledgement['basis'] as $basis)
|
||||
<div class="flex items-center justify-between gap-3 text-xs">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ $basis['label'] }}</span>
|
||||
<x-filament::badge :color="$basis['color']" size="sm">
|
||||
{{ $basis['value'] }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($acknowledgement['acknowledged_at_label'] || $acknowledgement['acknowledged_by_label'])
|
||||
<dl class="grid gap-2 rounded-lg border border-dashed border-gray-200 bg-gray-50 p-3 text-xs dark:border-white/10 dark:bg-white/5 sm:grid-cols-2">
|
||||
<div>
|
||||
<dt class="font-semibold text-gray-500 dark:text-gray-400">{{ __('localization.review.acknowledged_by') }}</dt>
|
||||
<dd class="mt-1 font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $acknowledgement['acknowledged_by_label'] ?? __('localization.review.unavailable') }}
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="font-semibold text-gray-500 dark:text-gray-400">{{ __('localization.review.acknowledged_at') }}</dt>
|
||||
<dd class="mt-1 font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $acknowledgement['acknowledged_at_label'] ?? __('localization.review.unavailable') }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
@endif
|
||||
|
||||
@if ($acknowledgement['comment'])
|
||||
<div class="rounded-lg border border-gray-200 bg-gray-50 p-3 text-xs leading-5 text-gray-700 dark:border-white/10 dark:bg-white/5 dark:text-gray-200">
|
||||
{{ $acknowledgement['comment'] }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="border-t border-gray-200 pt-3 dark:border-white/10">
|
||||
<div class="text-[0.7rem] font-semibold uppercase leading-4 tracking-wide text-gray-500 dark:text-gray-400">
|
||||
{{ __('localization.review.next_step') }}
|
||||
</div>
|
||||
|
||||
@if ($acknowledgement['action_name'])
|
||||
<x-filament::button
|
||||
type="button"
|
||||
wire:click="mountAction('{{ $acknowledgement['action_name'] }}')"
|
||||
:color="$acknowledgement['action_color']"
|
||||
size="sm"
|
||||
class="mt-2"
|
||||
:disabled="$acknowledgement['action_disabled']"
|
||||
>
|
||||
{{ $acknowledgement['action_label'] }}
|
||||
</x-filament::button>
|
||||
@else
|
||||
<x-filament::button color="gray" size="sm" class="mt-2" disabled>
|
||||
{{ $acknowledgement['action_label'] }}
|
||||
</x-filament::button>
|
||||
@endif
|
||||
|
||||
@if ($acknowledgement['action_helper'])
|
||||
<p class="mt-2 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{{ $acknowledgement['action_helper'] }}
|
||||
</p>
|
||||
@endif
|
||||
</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"
|
||||
@ -372,6 +474,12 @@ class="mt-3 divide-y divide-gray-100 border-t border-gray-200 pt-1 dark:divide-w
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($acceptedRisks['entries'] === [])
|
||||
<p class="mt-3 border-t border-gray-200 pt-3 text-xs leading-5 text-gray-500 dark:border-white/10 dark:text-gray-400">
|
||||
{{ $acceptedRisks['empty_state'] }}
|
||||
</p>
|
||||
@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">
|
||||
@ -459,4 +567,6 @@ class="mt-3 divide-y divide-gray-100 border-t border-gray-200 pt-1 dark:divide-w
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<x-filament-actions::modals />
|
||||
</x-filament-panels::page>
|
||||
|
||||
@ -0,0 +1,355 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||
use App\Models\EnvironmentReview;
|
||||
use App\Models\EnvironmentReviewAcknowledgement;
|
||||
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('Spec343 smokes customer review acknowledgement and accepted risk lifecycle surfaces', function (): void {
|
||||
[$user, $ackRequiredEnvironment] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
||||
$ackRequiredEnvironment->forceFill(['name' => 'Spec343 Browser Ack Required'])->save();
|
||||
|
||||
$acknowledgedEnvironment = spec343BrowserEnvironmentFor($user, $ackRequiredEnvironment, 'Spec343 Browser Acknowledged');
|
||||
$acceptedRiskEnvironment = spec343BrowserEnvironmentFor($user, $ackRequiredEnvironment, 'Spec343 Browser Accepted Risks');
|
||||
$acceptedRiskExpiredEnvironment = spec343BrowserEnvironmentFor($user, $ackRequiredEnvironment, 'Spec343 Browser Accepted Risk Expired');
|
||||
$noAcceptedRiskEnvironment = spec343BrowserEnvironmentFor($user, $ackRequiredEnvironment, 'Spec343 Browser No Accepted Risks');
|
||||
|
||||
[$ackRequiredReview] = spec343BrowserCreatePublishedReviewWithPack(
|
||||
$ackRequiredEnvironment,
|
||||
$user,
|
||||
seedEnvironmentReviewEvidence($ackRequiredEnvironment, findingCount: 0, driftCount: 0),
|
||||
[],
|
||||
'review-packs/spec343-browser-ack-required.zip',
|
||||
);
|
||||
|
||||
[$acknowledgedReview, $acknowledgedPack] = spec343BrowserCreatePublishedReviewWithPack(
|
||||
$acknowledgedEnvironment,
|
||||
$user,
|
||||
seedEnvironmentReviewEvidence($acknowledgedEnvironment, findingCount: 0, driftCount: 0),
|
||||
[],
|
||||
'review-packs/spec343-browser-acknowledged.zip',
|
||||
);
|
||||
|
||||
EnvironmentReviewAcknowledgement::query()->create([
|
||||
'managed_environment_id' => (int) $acknowledgedEnvironment->getKey(),
|
||||
'workspace_id' => (int) $acknowledgedEnvironment->workspace_id,
|
||||
'environment_review_id' => (int) $acknowledgedReview->getKey(),
|
||||
'acknowledged_at' => now()->subHours(2),
|
||||
'acknowledged_by_user_id' => (int) $user->getKey(),
|
||||
'comment' => 'Review pack reviewed and understood.',
|
||||
'evidence_snapshot_id' => (int) $acknowledgedReview->evidence_snapshot_id,
|
||||
'review_pack_id' => (int) $acknowledgedPack->getKey(),
|
||||
]);
|
||||
|
||||
spec343BrowserCreatePublishedReviewWithPack(
|
||||
$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/spec343-browser-accepted-risk.zip',
|
||||
);
|
||||
spec343BrowserCreateAcceptedRisk($acceptedRiskEnvironment, $user, state: FindingException::VALIDITY_EXPIRING);
|
||||
|
||||
spec343BrowserCreatePublishedReviewWithPack(
|
||||
$acceptedRiskExpiredEnvironment,
|
||||
$user,
|
||||
seedEnvironmentReviewEvidence($acceptedRiskExpiredEnvironment, findingCount: 0, driftCount: 0),
|
||||
[],
|
||||
'review-packs/spec343-browser-accepted-risk-expired.zip',
|
||||
);
|
||||
spec343BrowserCreateAcceptedRisk($acceptedRiskExpiredEnvironment, $user, state: FindingException::VALIDITY_EXPIRED);
|
||||
|
||||
spec343BrowserCreatePublishedReviewWithPack(
|
||||
$noAcceptedRiskEnvironment,
|
||||
$user,
|
||||
seedEnvironmentReviewEvidence($noAcceptedRiskEnvironment, findingCount: 0, driftCount: 0),
|
||||
[],
|
||||
'review-packs/spec343-browser-no-accepted-risk.zip',
|
||||
);
|
||||
|
||||
spec343AuthenticateBrowser($this, $user, $ackRequiredEnvironment);
|
||||
|
||||
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($ackRequiredEnvironment))
|
||||
->resize(1236, 900)
|
||||
->waitForText(__('localization.review.review_acknowledgement'))
|
||||
->assertSee(__('localization.review.acknowledgement_required'))
|
||||
->assertSee(__('localization.review.acknowledge_review'))
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
|
||||
$page->screenshot(true, spec343BrowserScreenshotName('01-acknowledgement-required'));
|
||||
spec343CopyBrowserScreenshot('01-acknowledgement-required');
|
||||
|
||||
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($acknowledgedEnvironment))
|
||||
->waitForText(__('localization.review.review_acknowledgement'))
|
||||
->assertSee(__('localization.review.review_acknowledged'))
|
||||
->assertSee('Review pack reviewed and understood.')
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
|
||||
$page->screenshot(true, spec343BrowserScreenshotName('02-review-acknowledged'));
|
||||
spec343CopyBrowserScreenshot('02-review-acknowledged');
|
||||
|
||||
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($acceptedRiskEnvironment))
|
||||
->waitForText(__('localization.review.accepted_risk_summary'))
|
||||
->assertSee(__('localization.review.accepted_risk_accountability'))
|
||||
->assertSee(__('localization.review.accepted_risk_follow_up'))
|
||||
->assertSee('Accepted risk requires customer awareness.')
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
|
||||
$page->screenshot(true, spec343BrowserScreenshotName('03-accepted-risks-present'));
|
||||
spec343CopyBrowserScreenshot('03-accepted-risks-present');
|
||||
|
||||
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($acceptedRiskEnvironment))
|
||||
->waitForText(__('localization.review.accepted_risk_accountability'))
|
||||
->assertSee(__('localization.review.accepted_risks_expiring_soon'))
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
|
||||
$page->screenshot(true, spec343BrowserScreenshotName('04-accepted-risk-due-for-review'));
|
||||
spec343CopyBrowserScreenshot('04-accepted-risk-due-for-review');
|
||||
|
||||
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($acceptedRiskExpiredEnvironment))
|
||||
->waitForText(__('localization.review.accepted_risk_summary'))
|
||||
->assertSee(__('localization.review.accepted_risks_expired'))
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
|
||||
$page->screenshot(true, spec343BrowserScreenshotName('05-accepted-risk-expired'));
|
||||
spec343CopyBrowserScreenshot('05-accepted-risk-expired');
|
||||
|
||||
$page = visit(CustomerReviewWorkspace::environmentFilterUrl($noAcceptedRiskEnvironment))
|
||||
->waitForText(__('localization.review.accepted_risk_summary'))
|
||||
->assertSee(__('localization.review.no_accepted_risks_recorded'))
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
|
||||
$page->screenshot(true, spec343BrowserScreenshotName('06-no-accepted-risks'));
|
||||
spec343CopyBrowserScreenshot('06-no-accepted-risks');
|
||||
|
||||
$page->assertScript('document.querySelector("[data-testid=\"customer-review-diagnostics\"]")?.open === false', true);
|
||||
$page->screenshot(true, spec343BrowserScreenshotName('07-diagnostics-collapsed'));
|
||||
spec343CopyBrowserScreenshot('07-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, spec343BrowserScreenshotName('08-dark-mode'));
|
||||
spec343CopyBrowserScreenshot('08-dark-mode');
|
||||
|
||||
expect(EnvironmentReviewAcknowledgement::query()
|
||||
->where('environment_review_id', (int) $acknowledgedReview->getKey())
|
||||
->exists())->toBeTrue();
|
||||
|
||||
expect(EnvironmentReviewAcknowledgement::query()
|
||||
->where('environment_review_id', (int) $ackRequiredReview->getKey())
|
||||
->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
function spec343BrowserScreenshotName(string $name): string
|
||||
{
|
||||
return 'spec343-customer-review-attestation-'.$name;
|
||||
}
|
||||
|
||||
function spec343CopyBrowserScreenshot(string $name): void
|
||||
{
|
||||
$filename = spec343BrowserScreenshotName($name).'.png';
|
||||
$source = base_path('tests/Browser/Screenshots/'.$filename);
|
||||
$targetDirectory = repo_path('specs/343-customer-review-attestation-accepted-risk-lifecycle/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 spec343AuthenticateBrowser(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 spec343BrowserEnvironmentFor(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 spec343BrowserCreatePublishedReviewWithPack(
|
||||
ManagedEnvironment $environment,
|
||||
User $user,
|
||||
EvidenceSnapshot $snapshot,
|
||||
array $summaryOverrides = [],
|
||||
string $filePath = 'review-packs/spec343-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' => 'Spec343 Browser Operator',
|
||||
]);
|
||||
|
||||
Storage::disk('exports')->put($filePath, 'PK-spec343-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 spec343BrowserCreateAcceptedRisk(ManagedEnvironment $environment, User $user, string $state): void
|
||||
{
|
||||
$owner = User::factory()->create(['name' => 'Spec343 Browser Risk Owner']);
|
||||
|
||||
$finding = 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) $finding->getKey(),
|
||||
'status' => FindingException::STATUS_ACTIVE,
|
||||
'current_validity_state' => $state,
|
||||
'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),
|
||||
'expires_at' => $state === FindingException::VALIDITY_EXPIRED ? now()->subDay() : null,
|
||||
]);
|
||||
}
|
||||
@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\EnvironmentReview;
|
||||
use App\Models\EnvironmentReviewAcknowledgement;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\User;
|
||||
use App\Services\EnvironmentReviews\EnvironmentReviewAcknowledgementService;
|
||||
use App\Support\Audit\AuditActionId;
|
||||
use App\Support\EnvironmentReviewStatus;
|
||||
use App\Support\Evidence\EvidenceSnapshotStatus;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Actions\Action;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('renders acknowledgement required for a published review and allows authorized acknowledgement', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec343 Ack Required']);
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner', workspaceRole: 'manager');
|
||||
|
||||
$review = spec343CreatePublishedReview($tenant, $user, seedEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0));
|
||||
|
||||
$this->actingAs($user);
|
||||
setAdminPanelContext();
|
||||
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||
|
||||
$component = Livewire::actingAs($user)->test(CustomerReviewWorkspace::class)
|
||||
->assertSee(__('localization.review.review_acknowledgement'))
|
||||
->assertSee(__('localization.review.acknowledgement_required'))
|
||||
->assertSee(__('localization.review.acknowledge_review'))
|
||||
->assertActionExists('acknowledgeReview', fn (Action $action): bool => $action->isConfirmationRequired());
|
||||
|
||||
$payload = $component->instance()->latestReviewConsumptionPayload();
|
||||
|
||||
expect(data_get($payload, 'acknowledgement.state'))->toBe('required');
|
||||
|
||||
$component
|
||||
->mountAction('acknowledgeReview')
|
||||
->setActionData([
|
||||
'comment' => 'Reviewed with stakeholders.',
|
||||
])
|
||||
->callMountedAction()
|
||||
->assertNotified(__('localization.review.review_acknowledged'));
|
||||
|
||||
$ack = EnvironmentReviewAcknowledgement::query()
|
||||
->where('environment_review_id', (int) $review->getKey())
|
||||
->first();
|
||||
|
||||
expect($ack)->toBeInstanceOf(EnvironmentReviewAcknowledgement::class)
|
||||
->and((int) $ack?->managed_environment_id)->toBe((int) $tenant->getKey())
|
||||
->and((int) $ack?->workspace_id)->toBe((int) $tenant->workspace_id)
|
||||
->and((int) $ack?->environment_review_id)->toBe((int) $review->getKey())
|
||||
->and((int) $ack?->acknowledged_by_user_id)->toBe((int) $user->getKey())
|
||||
->and($ack?->comment)->toBe('Reviewed with stakeholders.')
|
||||
->and((int) $ack?->evidence_snapshot_id)->toBe((int) $review->evidence_snapshot_id);
|
||||
|
||||
$audit = AuditLog::query()
|
||||
->where('action', AuditActionId::EnvironmentReviewAcknowledged->value)
|
||||
->where('resource_type', 'environment_review')
|
||||
->where('resource_id', (string) $review->getKey())
|
||||
->latest('id')
|
||||
->first();
|
||||
|
||||
expect($audit)->not->toBeNull()
|
||||
->and((int) $audit?->managed_environment_id)->toBe((int) $tenant->getKey())
|
||||
->and((int) data_get($audit?->metadata, 'review_id'))->toBe((int) $review->getKey());
|
||||
});
|
||||
|
||||
it('surfaces re-ack required when the evidence basis changes after acknowledgement', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec343 Re-ack']);
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner', workspaceRole: 'manager');
|
||||
|
||||
$snapshotA = seedEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
|
||||
$review = spec343CreatePublishedReview($tenant, $user, $snapshotA);
|
||||
|
||||
$this->actingAs($user);
|
||||
setAdminPanelContext();
|
||||
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->mountAction('acknowledgeReview')
|
||||
->callMountedAction()
|
||||
->assertNotified(__('localization.review.review_acknowledged'));
|
||||
|
||||
$snapshotA->forceFill([
|
||||
'status' => EvidenceSnapshotStatus::Superseded->value,
|
||||
])->save();
|
||||
|
||||
$snapshotB = seedEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
|
||||
|
||||
$review->forceFill([
|
||||
'evidence_snapshot_id' => (int) $snapshotB->getKey(),
|
||||
])->save();
|
||||
|
||||
$component = Livewire::actingAs($user)->test(CustomerReviewWorkspace::class);
|
||||
$payload = $component->instance()->latestReviewConsumptionPayload();
|
||||
|
||||
expect(data_get($payload, 'acknowledgement.state'))->toBe('re_ack_required');
|
||||
});
|
||||
|
||||
it('shows a visible empty state when no accepted risks are recorded', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create(['name' => 'Spec343 Empty Accepted Risks']);
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner', workspaceRole: 'manager');
|
||||
|
||||
spec343CreatePublishedReview($tenant, $user, seedEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0));
|
||||
|
||||
$this->actingAs($user);
|
||||
setAdminPanelContext();
|
||||
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertSee(__('localization.review.accepted_risk_summary'))
|
||||
->assertSee(__('localization.review.no_accepted_risks_recorded'));
|
||||
});
|
||||
|
||||
it('denies acknowledgement outside the actor tenant scope as not found', function (): void {
|
||||
[$actor] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
||||
|
||||
$otherTenant = ManagedEnvironment::factory()->create(['name' => 'Spec343 Other Tenant']);
|
||||
[$creator, $otherTenant] = createUserWithTenant(tenant: $otherTenant, role: 'owner', workspaceRole: 'manager');
|
||||
|
||||
$review = spec343CreatePublishedReview(
|
||||
$otherTenant,
|
||||
$creator,
|
||||
seedEnvironmentReviewEvidence($otherTenant, findingCount: 0, driftCount: 0),
|
||||
);
|
||||
|
||||
expect(fn (): mixed => app(EnvironmentReviewAcknowledgementService::class)->acknowledge(
|
||||
tenant: $otherTenant,
|
||||
review: $review,
|
||||
actor: $actor,
|
||||
comment: null,
|
||||
))->toThrow(NotFoundHttpException::class);
|
||||
});
|
||||
|
||||
function spec343CreatePublishedReview(ManagedEnvironment $tenant, User $user, EvidenceSnapshot $snapshot): EnvironmentReview
|
||||
{
|
||||
$review = composeEnvironmentReviewForTest($tenant, $user, $snapshot);
|
||||
|
||||
$review->forceFill([
|
||||
'status' => EnvironmentReviewStatus::Published->value,
|
||||
'generated_at' => now(),
|
||||
'published_at' => now(),
|
||||
'published_by_user_id' => (int) $user->getKey(),
|
||||
])->save();
|
||||
|
||||
return $review->refresh();
|
||||
}
|
||||
@ -25,11 +25,11 @@ ## Productization Review
|
||||
|
||||
## Information Inventory
|
||||
|
||||
Default content should include review readiness, evidence basis, accepted risk summary, decision summary, review-pack download, and management-readable next action.
|
||||
Default content should include review readiness, review acknowledgement (attestation) state + action, evidence basis, accepted risk summary, decision summary, review-pack download, and a management-readable next action.
|
||||
|
||||
## Dangerous Actions
|
||||
|
||||
Customer-facing surface should be read-first. Export/download and publish/review actions need clear scope, audit, and language.
|
||||
Customer-facing surface should be read-first. Export/download and publish/review actions need clear scope, audit, and language. The acknowledgement action is a write/mutation action and must remain confirmation-gated, capability-gated, and auditable, without legal/e-signature semantics.
|
||||
|
||||
## Scores
|
||||
|
||||
@ -39,7 +39,7 @@ ## Scores
|
||||
|
||||
## Top Issues
|
||||
|
||||
1. Needs customer-safe language and data exposure review before sellable use.
|
||||
1. Acknowledgement copy must remain customer-safe and explicitly non-legal (no compliance certification semantics).
|
||||
2. Evidence and accepted-risk meaning should be visible without raw diagnostics.
|
||||
3. Requires individual target mockup, not only cleanup.
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 307 KiB |
|
After Width: | Height: | Size: 323 KiB |
|
After Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 328 KiB |
|
After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 314 KiB |
|
After Width: | Height: | Size: 317 KiB |
@ -0,0 +1,30 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Spec 343 Screenshots</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 24px; }
|
||||
h1 { font-size: 18px; margin: 0 0 12px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; }
|
||||
a { display: block; text-decoration: none; color: inherit; border: 1px solid rgba(127,127,127,.3); border-radius: 10px; overflow: hidden; background: rgba(127,127,127,.06); }
|
||||
.label { padding: 10px 12px; font-size: 12px; border-top: 1px solid rgba(127,127,127,.25); }
|
||||
img { display: block; width: 100%; height: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Spec 343 — Customer Review Workspace (Ack + Accepted Risk lifecycle)</h1>
|
||||
<div class="grid">
|
||||
<a href="01-acknowledgement-required.png"><img src="01-acknowledgement-required.png" alt="01" /><div class="label">01 — acknowledgement required</div></a>
|
||||
<a href="02-review-acknowledged.png"><img src="02-review-acknowledged.png" alt="02" /><div class="label">02 — review acknowledged</div></a>
|
||||
<a href="03-accepted-risks-present.png"><img src="03-accepted-risks-present.png" alt="03" /><div class="label">03 — accepted risks present</div></a>
|
||||
<a href="04-accepted-risk-due-for-review.png"><img src="04-accepted-risk-due-for-review.png" alt="04" /><div class="label">04 — due for review</div></a>
|
||||
<a href="05-accepted-risk-expired.png"><img src="05-accepted-risk-expired.png" alt="05" /><div class="label">05 — expired</div></a>
|
||||
<a href="06-no-accepted-risks.png"><img src="06-no-accepted-risks.png" alt="06" /><div class="label">06 — no accepted risks</div></a>
|
||||
<a href="07-diagnostics-collapsed.png"><img src="07-diagnostics-collapsed.png" alt="07" /><div class="label">07 — diagnostics collapsed</div></a>
|
||||
<a href="08-dark-mode.png"><img src="08-dark-mode.png" alt="08" /><div class="label">08 — dark mode</div></a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,52 @@
|
||||
# Specification Quality Checklist: Spec 343 - Customer Review Attestation & Accepted Risk Lifecycle
|
||||
|
||||
**Purpose**: Validate Spec 343 preparation completeness before implementation.
|
||||
**Created**: 2026-06-01
|
||||
**Feature**: `specs/343-customer-review-attestation-accepted-risk-lifecycle/spec.md`
|
||||
|
||||
## Candidate Selection Gate
|
||||
|
||||
- [x] CHK001 The selected candidate is directly provided by the user as Spec 343 (next step after Spec 342).
|
||||
- [x] CHK002 The candidate aligns with current roadmap direction: governance-of-record customer-safe reviewability without a generic GRC rebuild.
|
||||
- [x] CHK003 No existing `specs/343-*` package or branch was found before Spec Kit creation.
|
||||
- [x] CHK004 Related specs were checked for completed-spec signals and are treated as context only (326, 329, 337, 342).
|
||||
- [x] CHK005 Close alternatives are deferred rather than hidden scope (344–347 follow-up candidates).
|
||||
- [x] CHK006 Scope is narrowed to one strategic surface (`/admin/reviews/workspace`) and one minimal persisted truth addition (acknowledgement) only if missing.
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] CHK007 `spec.md` defines problem, user value, functional requirements, non-goals, acceptance boundaries, assumptions, risks, and open questions.
|
||||
- [x] CHK008 `plan.md` lists likely affected repo surfaces and separates repo-truth mapping from runtime changes.
|
||||
- [x] CHK009 `tasks.md` is ordered into small phases with explicit test/browser/screenshot/validation tasks.
|
||||
- [x] CHK010 Supporting prep artifacts exist: `repo-truth-map.md` and `review-attestation-risk-state-contract.md`.
|
||||
- [x] CHK011 No unresolved template placeholders remain in `spec.md`, `plan.md`, or `tasks.md`.
|
||||
|
||||
## Constitution And Scope
|
||||
|
||||
- [x] CHK012 Proportionality review is present and explicitly rejects a generic attestation/GRC framework.
|
||||
- [x] CHK013 Persistence is justified via PERSIST-001 for acknowledgement truth (auditable governance-of-record event).
|
||||
- [x] CHK014 Workspace/environment isolation and deny-as-not-found semantics are explicit requirements.
|
||||
- [x] CHK015 UI Surface Impact and UI/Productization Coverage are completed for the strategic customer-safe surface.
|
||||
- [x] CHK016 Filament v5 / Livewire v4 posture, panel provider location, destructive-action confirmation rules, asset strategy, and testing plan are explicit.
|
||||
|
||||
## Plan Quality
|
||||
|
||||
- [x] CHK017 Plan sequencing is repo-truth gate → persistence decision → service/audit → UI wiring → tests/browser → validation.
|
||||
- [x] CHK018 Deployment/ops impact is explicit (migration possible; no env/queue/scheduler/assets expected).
|
||||
- [x] CHK019 No Graph/provider calls during UI render are enforced by plan constraints.
|
||||
|
||||
## Task Quality
|
||||
|
||||
- [x] CHK020 Tasks include concrete repo surfaces and avoid inventing runtime paths beyond likely touch points.
|
||||
- [x] CHK021 Tasks include Feature/Livewire tests and one bounded Browser smoke (strategic surface).
|
||||
- [x] CHK022 Tasks include screenshot artifacts and “unreachable state” handling without faking backend truth.
|
||||
- [x] CHK023 Tasks explicitly forbid rewriting completed specs and forbid legal/compliance claim scope creep.
|
||||
|
||||
## Spec Readiness Gate
|
||||
|
||||
- [x] CHK024 `spec.md`, `plan.md`, and `tasks.md` exist.
|
||||
- [x] CHK025 Required supporting prep artifacts exist in the spec package.
|
||||
- [x] CHK026 Open questions do not block safe implementation because each is resolved via repo-truth-first tasks before runtime changes.
|
||||
- [x] CHK027 Scope is bounded enough for a later implementation loop.
|
||||
- [x] CHK028 Result: ready for implementation loop.
|
||||
|
||||
@ -0,0 +1,166 @@
|
||||
# Implementation Plan: Spec 343 - Customer Review Attestation & Accepted Risk Lifecycle
|
||||
|
||||
**Branch**: `343-customer-review-attestation-accepted-risk-lifecycle` | **Date**: 2026-06-01 | **Spec**: `specs/343-customer-review-attestation-accepted-risk-lifecycle/spec.md`
|
||||
**Input**: User-provided Spec 343 draft (Codex attachment) + current repo truth from `CustomerReviewWorkspace` + accepted-risk lifecycle foundations (`FindingException`).
|
||||
|
||||
## Summary
|
||||
|
||||
Add the next bounded step after customer-safe review consumption (Spec 342):
|
||||
|
||||
1) Make customer/stakeholder acknowledgement explicit and auditable.
|
||||
2) Tighten the customer-safe accepted-risk lifecycle visibility (owner/rationale/expiry/re-review due) using existing `FindingException` truth.
|
||||
|
||||
This is a runtime productization slice, not a GRC rebuild:
|
||||
|
||||
- no legal/e-signature semantics
|
||||
- no compliance certification claims
|
||||
- no external customer portal architecture
|
||||
- no Graph/provider calls during render
|
||||
- no new global framework for attestations
|
||||
|
||||
## Technical Context
|
||||
|
||||
- **Language/Version**: PHP 8.4.15, Laravel 12.x.
|
||||
- **UI**: Filament v5 + Livewire v4 (no Livewire v3 APIs).
|
||||
- **Panel providers**: remain registered in `apps/platform/bootstrap/providers.php` (no changes expected).
|
||||
- **Storage**: PostgreSQL; a new migration is possible if acknowledgement persistence is missing.
|
||||
- **Testing**: Pest Feature/Livewire tests + one Pest Browser smoke file (strategic customer-safe surface).
|
||||
- **Assets**: no new global assets expected; `filament:assets` not expected for this spec.
|
||||
|
||||
## UI / Surface Guardrail Plan
|
||||
|
||||
- **Guardrail scope**: material change to an existing strategic customer-safe review surface (UI-038).
|
||||
- **Primary surface**:
|
||||
- `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`
|
||||
- `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php`
|
||||
- **New surface elements**:
|
||||
- “Review acknowledgement” card
|
||||
- “Acknowledge review” action modal (write path)
|
||||
- tightened accepted-risk lifecycle presentation (read-first)
|
||||
- **Disclosure defaults**: diagnostics remain collapsed and capability-gated.
|
||||
- **UI audit registry**: decide post-diff whether `docs/ui-ux-enterprise-audit/page-reports/ui-006-customer-review-workspace.md` requires an update; route inventory should remain stable.
|
||||
|
||||
## Repo-Truth-First Execution Rule
|
||||
|
||||
Before any runtime change:
|
||||
|
||||
1) Update `specs/343-customer-review-attestation-accepted-risk-lifecycle/repo-truth-map.md` with confirmed truth for:
|
||||
- Customer Review Workspace payload/state shape
|
||||
- `FindingException`/decision lifecycle fields used for customer-safe display
|
||||
- any existing acknowledgement-like concepts (none expected beyond verification acknowledgements)
|
||||
- audit log seams and existing action IDs
|
||||
2) Update `specs/343-customer-review-attestation-accepted-risk-lifecycle/review-attestation-risk-state-contract.md` to match the confirmed truth (no invented states).
|
||||
|
||||
## Data Model Plan (Acknowledgement Truth)
|
||||
|
||||
### Goal
|
||||
|
||||
Persist a customer/stakeholder acknowledgement event so it is queryable, auditable, and basis-aware.
|
||||
|
||||
### Expected current repo truth
|
||||
|
||||
- Accepted risks already exist as `FindingException` (+ append-only decisions).
|
||||
- No existing review acknowledgement model/table exists (to be confirmed in Phase 1).
|
||||
|
||||
### Proposed minimal persisted entity (conditional)
|
||||
|
||||
If repo truth confirms there is no review acknowledgement entity:
|
||||
|
||||
- Add `EnvironmentReviewAcknowledgement` (final name to be validated against existing naming conventions).
|
||||
- Table should be workspace- and environment-scoped and reference the acknowledged review:
|
||||
- `workspace_id`
|
||||
- `managed_environment_id`
|
||||
- `environment_review_id`
|
||||
- actor + timestamp fields
|
||||
- optional comment
|
||||
- basis capture (at minimum: `review_pack_id` and/or `evidence_snapshot_id` when present)
|
||||
|
||||
### Basis-change detection (for `re_ack_required`)
|
||||
|
||||
Derive `re_ack_required` only if repo truth can safely detect basis drift:
|
||||
|
||||
- compare stored basis fields to current review pack / evidence snapshot identifiers
|
||||
- if basis fields are missing, prefer “acknowledged” without re-ack semantics (no guesswork)
|
||||
|
||||
### Constitution alignment
|
||||
|
||||
- **PERSIST-001**: acknowledgement is product truth with independent audit needs; persistence is justified.
|
||||
- **STATE-001**: any new displayed state must change behavior (action availability, warnings, or next action), not just wording.
|
||||
|
||||
## Authorization Plan
|
||||
|
||||
- Introduce (or reuse if already present) one dedicated capability for recording acknowledgement.
|
||||
- Enforce:
|
||||
- workspace membership + environment access boundaries
|
||||
- capability checks for write
|
||||
- deny-as-not-found for cross-workspace review access
|
||||
- Keep diagnostics behind `support_diagnostics.view`.
|
||||
|
||||
## Auditability Plan
|
||||
|
||||
- Add audit events for:
|
||||
- review acknowledgement recorded
|
||||
- acknowledgement superseded/replaced (if append-only)
|
||||
- (optional) acknowledgement revoked (only if a revoke capability exists; otherwise out of scope)
|
||||
- Audit payload must be customer-safe (no secrets, no raw provider payloads).
|
||||
|
||||
## Accepted Risk Lifecycle Plan (Finding Exceptions)
|
||||
|
||||
The Customer Review Workspace already surfaces accepted-risk counts and lifecycle signals.
|
||||
|
||||
This spec tightens customer-safe lifecycle truth:
|
||||
|
||||
- ensure owner/rationale/expiry/review_due signals are visible where repo-backed
|
||||
- highlight missing governance support states (`missing_support`, missing owner/reason/dates) without fabricating “no risk”
|
||||
- ensure accepted-risk and findings presentation never slips into diagnostics-only disclosure
|
||||
|
||||
Any accepted-risk mutation actions on the customer-safe surface are optional and must be capability-gated, confirmed where destructive, and audited.
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 0 — Preflight
|
||||
|
||||
- Confirm working tree is clean and branch is correct.
|
||||
- Re-read Spec 342 state contract and repo-truth map boundaries so Spec 343 does not duplicate or regress consumption semantics.
|
||||
|
||||
### Phase 1 — Repo Truth Mapping
|
||||
|
||||
- Validate acknowledgement absence/presence in codebase and database schema.
|
||||
- Confirm `FindingException` lifecycle fields used in Customer Review Workspace.
|
||||
- Record everything in `repo-truth-map.md` with classifications.
|
||||
|
||||
### Phase 2 — Acknowledgement Persistence (conditional)
|
||||
|
||||
- Implement the minimal model + migration only if missing.
|
||||
- Add a service/action class to record acknowledgement with authorization + audit.
|
||||
|
||||
### Phase 3 — UI Wiring (Customer Review Workspace)
|
||||
|
||||
- Implement derived state builder and card rendering.
|
||||
- Add the “Acknowledge review” action modal (requires confirmation).
|
||||
- Keep copy customer-safe and localization-ready (reuse existing localization patterns).
|
||||
|
||||
### Phase 4 — Accepted Risk Lifecycle Tightening
|
||||
|
||||
- Keep accepted-risk truth sourced from `FindingException`.
|
||||
- Add/adjust lifecycle highlighting only if repo truth proves gaps.
|
||||
|
||||
### Phase 5 — Tests + Browser Smoke + Screenshots
|
||||
|
||||
- Add focused Feature/Livewire tests for attestation states, RBAC boundaries, and audit emission.
|
||||
- Add one Browser smoke for the strategic surface and capture screenshots.
|
||||
|
||||
### Phase 6 — Validation + Close-Out
|
||||
|
||||
- Run the planned validation commands.
|
||||
- Update spec package artifacts (`repo-truth-map.md`, state contract, checklist) based on what was discovered/implemented.
|
||||
- Record any unreachable states as “not available”/“deferred”, not as TODO guesswork.
|
||||
|
||||
## Deployment / Ops Impact
|
||||
|
||||
- **Migrations**: possible (only if acknowledgement persistence is missing).
|
||||
- **Env vars**: none expected.
|
||||
- **Queues/scheduler**: none expected.
|
||||
- **Storage**: none expected.
|
||||
- **Filament assets**: none expected.
|
||||
|
||||
@ -0,0 +1,69 @@
|
||||
# Spec 343 - Repo Truth Map
|
||||
|
||||
Status: draft
|
||||
Created: 2026-06-01
|
||||
Scope: Customer review acknowledgement (attestation) + accepted risk lifecycle (Finding Exceptions)
|
||||
|
||||
This map is the implementation guardrail for Spec 343. 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 target behavior needs wiring/productization.
|
||||
- `not available`: no repo-backed truth or action exists in the current codebase.
|
||||
- `deferred`: intentionally out of scope for Spec 343.
|
||||
|
||||
## Core Surface Truth
|
||||
|
||||
| Data point | Classification | Repo evidence | Spec 343 handling |
|
||||
|---|---|---|---|
|
||||
| Customer Review Workspace page | repo-verified | `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` | Extend existing page; do not add a new route. |
|
||||
| Customer Review Workspace Blade view | repo-verified | `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php` | Add acknowledgement card + tighten accepted-risk lifecycle copy. |
|
||||
| Canonical route | repo-verified | `/admin/reviews/workspace` (UI-038 route inventory) | Keep route stable. |
|
||||
| Environment selection | repo-verified | `environment_id` query filter semantics | Remains a page-level filter, not global context. |
|
||||
| Legacy `/admin/t` context | not available | Spec 341 cleanup + current navigation contract | Must not be reintroduced. |
|
||||
| Diagnostics collapse rule | repo-verified | Spec 342 + current workspace view conventions | Keep collapsed and capability-gated. |
|
||||
|
||||
## Review Truth
|
||||
|
||||
| Data point | Classification | Repo evidence | Spec 343 handling |
|
||||
|---|---|---|---|
|
||||
| Review record | repo-verified | `EnvironmentReview` usage in `CustomerReviewWorkspace` | Use as acknowledgement scope anchor. |
|
||||
| Released/published review state | repo-verified / derived | `EnvironmentReviewStatus::*`, `published_at` (repo-backed) | Acknowledgement is only meaningful for released review packages. |
|
||||
| Current export review pack | repo-verified | `EnvironmentReview.currentExportReviewPack` usage | Capture as acknowledgement basis when present. |
|
||||
| Evidence snapshot basis | repo-verified | `EnvironmentReview.evidenceSnapshot` usage | Capture as acknowledgement basis when present. |
|
||||
|
||||
## Acknowledgement / Attestation Truth
|
||||
|
||||
| Data point | Classification | Repo evidence | Spec 343 handling |
|
||||
|---|---|---|---|
|
||||
| Review-scoped acknowledgement persisted model/table | repo-verified | `apps/platform/app/Models/EnvironmentReviewAcknowledgement.php` + `apps/platform/database/migrations/2026_06_01_000000_create_environment_review_acknowledgements_table.php` | Use `EnvironmentReviewAcknowledgement` as persisted acknowledgement truth (single-current per `environment_review_id`). |
|
||||
| Acknowledgement service pattern | repo-verified | `apps/platform/app/Services/EnvironmentReviews/EnvironmentReviewAcknowledgementService.php` | Reuse approach: authorize → persist → audit. |
|
||||
| Capability naming pattern for acknowledgement | repo-verified | `apps/platform/app/Support/Auth/Capabilities.php` | Use `environment_review.acknowledge` capability and enforce it on acknowledgement writes. |
|
||||
| Customer-safe acknowledgement copy boundaries | repo-verified | Existing non-certification disclosure strings in review pack/evidence mapping | No legal/e-signature language; acknowledgement is “reviewed and understood”, not a compliance sign-off. |
|
||||
|
||||
## Accepted Risk Truth (Finding Exceptions)
|
||||
|
||||
| Data point | Classification | Repo evidence | Spec 343 handling |
|
||||
|---|---|---|---|
|
||||
| Accepted risk entity | repo-verified | `apps/platform/app/Models/FindingException.php` | Treat Finding Exceptions as accepted risks for customer-safe display. |
|
||||
| Decision lifecycle | repo-verified | `apps/platform/app/Models/FindingExceptionDecision.php` (append-only) | Reuse; do not create new accepted-risk decision tables. |
|
||||
| Lifecycle states | repo-verified | `FindingException::STATUS_*`, `FindingException::VALIDITY_*` | Map to customer-safe states (active/expiring/expired/pending/revoked/missing support). |
|
||||
| Owner/rationale/dates | repo-verified | `owner_user_id`, `request_reason`, `expires_at`, `review_due_at` casts | Display where present; flag missing governance support. |
|
||||
| Customer Review Workspace accepted-risk panel | repo-verified | `CustomerReviewWorkspace::acceptedRiskPanelForReview()` and related helpers | Tighten wording/visibility only; keep truth source unchanged. |
|
||||
|
||||
## Audit Truth
|
||||
|
||||
| Data point | Classification | Repo evidence | Spec 343 handling |
|
||||
|---|---|---|---|
|
||||
| Audit foundation exists | repo-verified | `apps/platform/app/Support/Audit/*` + existing audit logger usage | Reuse. |
|
||||
| Review acknowledgement audit events | repo-verified | `apps/platform/app/Support/Audit/AuditActionId.php` (`environment_review.acknowledged`) | Emit a single canonical audit event on acknowledgement writes with customer-safe metadata only. |
|
||||
|
||||
## Unavailable Or Deferred Concepts
|
||||
|
||||
| Concept | Default Contract |
|
||||
|---|---|
|
||||
| Legal signature / compliance certification | not available; forbidden in this slice |
|
||||
| External portal / federation / invitations | deferred |
|
||||
| Risk scoring framework | deferred |
|
||||
@ -0,0 +1,126 @@
|
||||
# Spec 343 - Review Attestation & Accepted Risk State Contract
|
||||
|
||||
Status: draft
|
||||
Created: 2026-06-01
|
||||
Scope: Derived UI state contract for Customer Review Workspace acknowledgement + accepted-risk lifecycle
|
||||
|
||||
This document defines the **derived display contract** for Spec 343. It is not a new persisted truth source. The UI must not fabricate states; unsupported concepts must render as `not available` or `deferred`.
|
||||
|
||||
## Contract Rules
|
||||
|
||||
- Derived-only: all states come from repo-backed models/fields (plus “absence of record”).
|
||||
- No legal/e-signature semantics.
|
||||
- No compliance/certification claims.
|
||||
- Diagnostics are collapsed by default and capability-gated.
|
||||
|
||||
## Recommended Contract Shape (illustrative)
|
||||
|
||||
Exact implementation may differ, but the UI should consume one coherent contract where practical:
|
||||
|
||||
```php
|
||||
[
|
||||
'review' => [
|
||||
'id' => 123,
|
||||
'status' => 'published',
|
||||
],
|
||||
'attestation' => [
|
||||
'state' => 'required', // not_available | required | acknowledged | re_ack_required
|
||||
'label' => 'Customer acknowledgement required',
|
||||
'reason' => 'Review pack is available, but no acknowledgement is recorded.',
|
||||
'impact' => 'The review is consumable, but not yet acknowledged.',
|
||||
'primary_next_action' => 'Acknowledge review',
|
||||
'basis' => [
|
||||
'review_pack' => 'available', // available | not_available | preparing | expired | unknown
|
||||
'evidence' => 'available', // available | incomplete | not_available | unknown
|
||||
],
|
||||
],
|
||||
'accepted_risks' => [
|
||||
'total' => 3,
|
||||
'expiring' => 1,
|
||||
'expired' => 1,
|
||||
'pending' => 0,
|
||||
'missing_support' => 0,
|
||||
'missing_required_fields' => 1,
|
||||
],
|
||||
'diagnostics_state' => 'collapsed',
|
||||
]
|
||||
```
|
||||
|
||||
## Attestation States
|
||||
|
||||
### 1) Not Available
|
||||
|
||||
Use only when repo truth shows acknowledgement is intentionally unsupported.
|
||||
|
||||
| Field | Contract |
|
||||
|---|---|
|
||||
| Visible status | Acknowledgement not available |
|
||||
| Reason | This review does not support acknowledgement yet. |
|
||||
| Impact | Review consumption is available, but acknowledgement is not tracked. |
|
||||
| Primary next action | Review accepted risks / findings |
|
||||
| Action | None (no fake acknowledge button) |
|
||||
| Diagnostics default | Collapsed |
|
||||
|
||||
### 2) Required
|
||||
|
||||
| Field | Contract |
|
||||
|---|---|
|
||||
| Visible status | Customer acknowledgement required |
|
||||
| Reason | Review pack (and/or evidence basis) is available, but no acknowledgement is recorded. |
|
||||
| Impact | The review can be consumed, but acknowledgement is not yet tracked. |
|
||||
| Primary next action | Acknowledge review |
|
||||
| Action | Show acknowledge action only when authorized |
|
||||
| Diagnostics default | Collapsed |
|
||||
|
||||
### 3) Acknowledged
|
||||
|
||||
| Field | Contract |
|
||||
|---|---|
|
||||
| Visible status | Review acknowledged |
|
||||
| Reason | A customer/stakeholder acknowledgement is recorded. |
|
||||
| Impact | Review consumption has an audit trail. |
|
||||
| Primary next action | Review accepted risks |
|
||||
| Action | No “revoke” in v1 unless explicitly in scope + authorized |
|
||||
| Diagnostics default | Collapsed |
|
||||
|
||||
### 4) Re-acknowledgement Required
|
||||
|
||||
Use only when repo truth can detect basis drift after acknowledgement (e.g., review pack/evidence snapshot changed).
|
||||
|
||||
| Field | Contract |
|
||||
|---|---|
|
||||
| Visible status | Re-acknowledgement required |
|
||||
| Reason | Review content changed after the last acknowledgement. |
|
||||
| Impact | Previous acknowledgement may no longer represent the current review package. |
|
||||
| Primary next action | Re-acknowledge review |
|
||||
| Diagnostics default | Collapsed |
|
||||
|
||||
## Accepted Risk Lifecycle Contract
|
||||
|
||||
Accepted risks are sourced from `FindingException` (and their decisions). Customer-safe display should map to these lifecycle signals where repo-backed:
|
||||
|
||||
- total accepted risks on record
|
||||
- expiring soon
|
||||
- expired
|
||||
- pending approval
|
||||
- missing governance support (`missing_support`)
|
||||
- missing required fields for defensible governance (owner/rationale/review date), when repo-backed
|
||||
|
||||
### Copy boundaries
|
||||
|
||||
- “No accepted risks recorded for this review” is allowed when the count is zero.
|
||||
- “No risks exist” is forbidden unless there is a separate, explicit product truth that proves it (not expected).
|
||||
|
||||
## Evidence / Review Pack Basis
|
||||
|
||||
Every attestation state should show what it is based on:
|
||||
|
||||
- review pack availability/status
|
||||
- evidence snapshot availability/completeness
|
||||
- audit linkage availability (only if repo-backed)
|
||||
|
||||
If evidence is missing:
|
||||
|
||||
- show “Evidence basis unavailable” (truthful)
|
||||
- do not block acknowledgement unless the product explicitly requires evidence for acknowledgement (decision must be recorded in spec if introduced)
|
||||
|
||||
@ -0,0 +1,223 @@
|
||||
# Feature Specification: Spec 343 - Customer Review Attestation & Accepted Risk Lifecycle
|
||||
|
||||
**Feature Branch**: `343-customer-review-attestation-accepted-risk-lifecycle`
|
||||
**Created**: 2026-06-01
|
||||
**Status**: Implemented
|
||||
**Type**: Runtime productization / customer-safe acknowledgement / accepted-risk lifecycle
|
||||
**Runtime posture**: Add a small repo-backed customer review acknowledgement (attestation) and tighten customer-safe visibility of accepted risks (Finding Exceptions) on the existing Customer Review Workspace. No generic GRC rebuild and no legal/e-signature semantics.
|
||||
**Input**: User-provided Spec 343 draft (Codex attachment `pasted-text.txt`) + repo truth from Specs 326, 329, 337, 342 and the current `CustomerReviewWorkspace` implementation.
|
||||
|
||||
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: Customer Review Workspace is now consumable (Spec 342), but it still lacks an explicit, customer-safe acknowledgement trail and a clear accepted-risk lifecycle view that answers: what was acknowledged, which risks are accepted, who owns them, and what needs re-review.
|
||||
- **Today's failure**: Stakeholders can consume a review pack, but acknowledgement and accepted-risk accountability can remain implicit (notes, exports, operator memory) rather than explicit, customer-safe, and auditable.
|
||||
- **User-visible improvement**: The first screen answers: “Has this review been acknowledged, what risks are accepted, what needs re-review, and what’s the next action?” with truthful evidence/review-pack basis and audit trail.
|
||||
- **Smallest enterprise-capable version**: Extend only the existing `/admin/reviews/workspace` surface with one acknowledgement card and a tightened accepted-risk lifecycle section. Reuse existing accepted-risk truth (`FindingException`) and introduce a minimal persisted acknowledgement record only if repo truth shows it is not already present.
|
||||
- **Explicit non-goals**: No legal signature, no compliance certification claims, no generic GRC framework, no external customer portal, no external identity federation, no risk scoring engine, no new review-pack format, no bulk approval automation.
|
||||
- **Permanent complexity imported**: One small persisted acknowledgement model + migration (only if not already present), one acknowledgement service/action, one audit action family, minimal derived state/presenter, focused Feature/Livewire tests, one Browser smoke file, and screenshot artifacts.
|
||||
- **Why now**: After Spec 342 made consumption customer-safe, the next sellability gap is a lightweight acknowledgement and accepted-risk accountability loop (acknowledge → accepted risks on record → re-review due) without turning TenantPilot into a GRC product.
|
||||
- **Why not local**: A copy-only update cannot produce durable acknowledgement truth or auditable risk lifecycle. A “full attestation workflow” would violate roadmap/constitution proportionality. The narrow correct slice is one acknowledgement record tied to an existing review pack/evidence basis and an honest lifecycle display.
|
||||
- **Approval class**: Core Enterprise.
|
||||
- **Red flags triggered**: New persisted truth (acknowledgement record), customer-facing trust language, and lifecycle claims. Defense: minimal persistence justified by audit truth (PERSIST-001), all states are evidence-backed, and the UX forbids false legal/compliance claims.
|
||||
- **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**: Directly user-provided as “Spec 343 — Customer Review Attestation & Accepted Risk Lifecycle” as the next step after Spec 342.
|
||||
- **Completed-spec check**: No `specs/343-*` package existed before Spec Kit creation. Related Specs 326, 329, 337, and 342 contain completed-task, close-out, or review-outcome signals and are treated as historical context only. They must not be rewritten or normalized by this spec.
|
||||
- **Roadmap alignment**: This is a bounded acknowledgement + accepted-risk lifecycle slice on top of an existing strategic surface. It explicitly avoids becoming a “full attestation workflow block” or generic GRC system by keeping semantics lightweight and customer-safe.
|
||||
- **Close alternatives deferred**:
|
||||
- `Decision-Based Governance Inbox Final Operator Workflow` (draft follow-up 344).
|
||||
- `Localization v1 Product Surface Hardening` (draft follow-up 345).
|
||||
- `Provider Readiness Productization` (draft follow-up 346).
|
||||
- `External Support Desk / PSA Handoff` (draft follow-up 347).
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: workspace hub (existing strategic surface) with page-level `environment_id` filter.
|
||||
- **Primary Routes**:
|
||||
- `/admin/reviews/workspace` (Customer Review Workspace)
|
||||
- linked drill-down surfaces for exceptions/accepted risks (`FindingExceptionResource`), evidence, review packs, audit trail (only where already repo-backed)
|
||||
- **Data Ownership**:
|
||||
- Accepted risks: tenant/workspace-scoped `FindingException` + append-only `FindingExceptionDecision`.
|
||||
- New acknowledgement truth (if missing): a workspace + managed-environment + review-scoped persisted record.
|
||||
- **RBAC**:
|
||||
- Viewing remains governed by existing workspace membership rules + existing capability gates for linked resources.
|
||||
- Recording acknowledgement requires a dedicated capability (new, repo-aligned name to be chosen during repo-truth phase; do not overload `environment_review.manage` unless evidence shows it is the correct least-privilege boundary).
|
||||
- Diagnostics remain capability-gated (`support_diagnostics.view`).
|
||||
|
||||
## UI Surface Impact *(mandatory — UI-COV-001)*
|
||||
|
||||
- [ ] No UI surface impact
|
||||
- [x] Existing page changed
|
||||
- [ ] New page/route added
|
||||
- [ ] Navigation changed
|
||||
- [ ] Filament panel/provider surface changed
|
||||
- [x] 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
|
||||
- [ ] Workspace/environment context presentation changed
|
||||
|
||||
## UI/Productization Coverage *(mandatory)*
|
||||
|
||||
- **Route/page/surface**: `/admin/reviews/workspace` (`apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` + `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php`)
|
||||
- **Current page archetype**: `docs/ui-ux-enterprise-audit/route-inventory.md` → UI-038 (Strategic Surface, repo-verified)
|
||||
- **Design depth**: Strategic Surface (customer-safe)
|
||||
- **Repo-truth level**: repo-verified surface; new acknowledgement truth may require a small persisted model
|
||||
- **Existing pattern reused**: Spec 342 decision-card hierarchy + FindingException lifecycle truth + existing localized copy patterns
|
||||
- **New pattern required**: none; only one new card/state block and one action modal on an existing strategic page
|
||||
- **Screenshot required**: yes (`specs/343-customer-review-attestation-accepted-risk-lifecycle/artifacts/screenshots/`)
|
||||
- **Customer-safe review required**: yes (copy + disclosure + false-claim prevention)
|
||||
- **Dangerous-action review required**: conditional (only if the surface includes revoke/update risk actions; otherwise N/A)
|
||||
- **Coverage artifacts**: decide post-diff whether `docs/ui-ux-enterprise-audit/page-reports/ui-006-customer-review-workspace.md` requires an update; route inventory entry should remain stable.
|
||||
|
||||
## Cross-Cutting / Shared Pattern Reuse *(mandatory)*
|
||||
|
||||
- **Cross-cutting feature?**: yes.
|
||||
- **Interaction classes**: status messaging, action links, evidence/proof disclosure, audit trail disclosure, accepted-risk lifecycle.
|
||||
- **Shared paths reused**:
|
||||
- Existing customer-safe disclosure language (“not a certification / legal attestation / compliance guarantee”) where applicable.
|
||||
- Existing Finding Exception lifecycle truth (`FindingException`, `FindingExceptionDecision`), not a new accepted-risk engine.
|
||||
- Existing audit foundation (`AuditActionId` + workspace audit logging patterns), extended with a new action family only for acknowledgement.
|
||||
- **Deviations**: none intended. If implementation introduces a new presenter/state contract, it must remain feature-local and derived-only.
|
||||
|
||||
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||
|
||||
- **New source of truth?**: yes — only for acknowledgement/attestation (a stakeholder acknowledgement must persist independently of the page render).
|
||||
- **New persisted entity/table/artifact?**: conditional. If repo truth confirms no existing review acknowledgement table/model, add one minimal persisted entity.
|
||||
- **Why persistence is justified (PERSIST-001)**: acknowledgement is an auditable governance-of-record event that must outlive the session and be queryable (“who acknowledged what, when, based on which review pack/evidence basis”).
|
||||
- **New abstraction?**: no framework. At most one feature-local derived state builder (contract) to keep UI truthful and consistent.
|
||||
- **New enum/state family?**: no new global status family. Attestation states are derived from acknowledgement record presence + basis comparison.
|
||||
- **Narrowest correct implementation**:
|
||||
- Persist acknowledgement as one append-only record per review basis (or per review with basis snapshot fields).
|
||||
- Render derived states on the existing page; keep diagnostics collapsed.
|
||||
- Add audit emission + focused tests.
|
||||
- **Alternative intentionally rejected**: generic attestation workflow engine, legal signature semantics, and GRC-style policy/exception board.
|
||||
|
||||
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||
|
||||
- **Test purpose / classification**: Feature/Livewire for state, RBAC, isolation, audit emission; Browser for customer-safe rendered hierarchy and disclosure defaults.
|
||||
- **Validation lane(s)**: confidence + browser.
|
||||
- **Why sufficient**: the behavioral core is authorization + persisted acknowledgement + derived state; one bounded browser smoke is required for strategic customer-safe UX and diagnostics collapse.
|
||||
- **Planned validation commands** (later implementation):
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Filament/Spec343CustomerReviewAttestationAcceptedRiskTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec343CustomerReviewAttestationAcceptedRiskSmokeTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter='CustomerReview|EnvironmentReview|ReviewPack|Evidence|FindingException|Audit' --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail pint --dirty`
|
||||
- `git diff --check`
|
||||
|
||||
## User Scenarios *(must map to tasks)*
|
||||
|
||||
### User Story 1 — Customer/stakeholder acknowledgement is explicit (P1)
|
||||
|
||||
Given a released review exists and the review pack/evidence basis is available, when a stakeholder opens Customer Review Workspace, then the page shows whether acknowledgement is required, acknowledged, or re-acknowledgement is required (only when repo truth can detect basis change).
|
||||
|
||||
### User Story 2 — Accepted risks are visible and reviewable (P1)
|
||||
|
||||
Given accepted risks exist (Finding Exceptions), when a stakeholder opens Customer Review Workspace, then the page shows customer-safe counts and lifecycle signals (active, expiring, expired, pending) and highlights missing owner/rationale/review dates where repo-backed.
|
||||
|
||||
### User Story 3 — No false legal/compliance claims (P0)
|
||||
|
||||
Given any state (no review pack, incomplete evidence, missing audit linkage), when the page renders, then it never implies legal signature, compliance certification, or “no risk exists” when only “no accepted risks recorded” is true.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001 (Attestation card)**: Customer Review Workspace MUST render a “Review acknowledgement” card with: status, reason, impact, evidence/review-pack basis, and one primary next action.
|
||||
- **FR-002 (Attestation states)**: The surface MUST support these derived acknowledgement states:
|
||||
- `not_available` (only if repo truth decides acknowledgement is intentionally not supported)
|
||||
- `required` (review is consumable but no acknowledgement record exists)
|
||||
- `acknowledged` (acknowledgement record exists)
|
||||
- `re_ack_required` (only if repo truth can detect a changed basis after acknowledgement)
|
||||
- **FR-003 (Acknowledge action)**: If acknowledgement is supported, the surface MUST provide an action that records acknowledgement with:
|
||||
- actor + timestamp (repo truth: authenticated user)
|
||||
- optional comment
|
||||
- captured basis (at minimum: review id + current review-pack id and/or evidence snapshot id when present)
|
||||
- audit log emission
|
||||
- **FR-004 (Accepted risk visibility)**: The surface MUST show accepted risks (Finding Exceptions) summary and lifecycle signals using repo-backed truth (counts and validity/status).
|
||||
- **FR-005 (Accepted risk lifecycle fields)**: Where repo-backed, the surface MUST show owner, rationale (request reason), and expiry/review due dates, and must flag missing governance support.
|
||||
- **FR-006 (Evidence and audit basis)**: Attestation and accepted-risk sections MUST show a truthful basis (review pack/evidence snapshot availability and audit linkage state) and must never fabricate proof.
|
||||
- **FR-007 (Diagnostics default)**: Diagnostics MUST remain collapsed by default and MUST remain capability-gated.
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- **NFR-001 (Isolation)**: No cross-workspace leakage for acknowledgement records or accepted risks.
|
||||
- **NFR-002 (Authorization)**: UI visibility is not authorization; policies/gates must enforce capabilities for acknowledgement write and any linked action.
|
||||
- **NFR-003 (Performance)**: No Graph/provider calls during page render; derived state uses DB-only query paths with eager-loading where relationship-backed details are rendered.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- Legal signatures, legal attestations, regulatory sign-off, compliance certification claims.
|
||||
- External customer portal architecture, external federation/invitation links.
|
||||
- New GRC framework, risk scoring engine, or cross-domain policy exception board.
|
||||
- New review pack formats or evidence generation backend.
|
||||
- Bulk approval automation for accepted risks.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### Product
|
||||
|
||||
- [ ] Customer Review Workspace shows an acknowledgement card with status/reason/impact/next action.
|
||||
- [ ] Acknowledgement supports `required` and `acknowledged` states using repo-backed truth.
|
||||
- [ ] “Re-ack required” is shown only when basis drift detection is repo-backed; otherwise it is not shown.
|
||||
- [ ] Accepted risks (Finding Exceptions) are visible with lifecycle signals (active/expiring/expired/pending) using repo-backed truth.
|
||||
- [ ] Owner/rationale/expiry or review-due dates are displayed where repo-backed and missing governance support is highlighted.
|
||||
- [ ] Evidence / review-pack basis is visible and truthful for acknowledgement and accepted-risk sections.
|
||||
- [ ] Diagnostics remain secondary: collapsed by default and capability-gated.
|
||||
|
||||
### Truth / Safety
|
||||
|
||||
- [ ] No legal signature / compliance certification claims appear in UI copy, notifications, or audit summaries.
|
||||
- [ ] No fake “no risks exist” claim (only “no accepted risks recorded” where applicable).
|
||||
- [ ] No fabricated evidence or audit trail claims; unsupported concepts render as “not available”/“deferred”.
|
||||
|
||||
### RBAC / Isolation
|
||||
|
||||
- [ ] Acknowledgement write is capability-gated and auditable.
|
||||
- [ ] Cross-workspace review/acknowledgement/exception access is denied-as-not-found.
|
||||
|
||||
### Validation
|
||||
|
||||
- [ ] Feature/Livewire tests pass.
|
||||
- [ ] Browser smoke passes and screenshots are captured (or unreachable states are documented honestly).
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Acknowledgement is recorded by an authenticated user (customer stakeholder or internal role), not an external identity/federation flow.
|
||||
- Accepted risks are represented by `FindingException` + decisions; Spec 343 does not introduce a second accepted-risk model.
|
||||
- Review pack and/or evidence snapshot identifiers are available to capture an acknowledgement basis for “re-ack required” decisions (if the feature supports that state).
|
||||
|
||||
## Risks
|
||||
|
||||
- Scope creep into “full attestation workflow” or generic GRC mechanics; mitigate by enforcing non-goals and proportionality checks before implementation expands.
|
||||
- Wrong capability boundary for acknowledgement write; mitigate by repo-truth-first and least-privilege review before naming/adding the capability.
|
||||
- False-claim risk (legal/compliance, evidence, “no risk”); mitigate via explicit acceptance criteria + browser screenshots + negative tests.
|
||||
|
||||
## Spec Artifacts *(required for this package)*
|
||||
|
||||
- `specs/343-customer-review-attestation-accepted-risk-lifecycle/spec.md`
|
||||
- `specs/343-customer-review-attestation-accepted-risk-lifecycle/plan.md`
|
||||
- `specs/343-customer-review-attestation-accepted-risk-lifecycle/tasks.md`
|
||||
- `specs/343-customer-review-attestation-accepted-risk-lifecycle/repo-truth-map.md`
|
||||
- `specs/343-customer-review-attestation-accepted-risk-lifecycle/review-attestation-risk-state-contract.md`
|
||||
- `specs/343-customer-review-attestation-accepted-risk-lifecycle/checklists/requirements.md`
|
||||
- `specs/343-customer-review-attestation-accepted-risk-lifecycle/artifacts/screenshots/`
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None block safe implementation.
|
||||
- OQ-001 (resolved): Acknowledgement is available to authenticated actors with the dedicated capability `environment_review.acknowledge` (no external portal/federation semantics in v1).
|
||||
- OQ-002 (resolved): Basis capture for drift detection uses `environment_reviews.current_export_review_pack_id` and `environment_reviews.evidence_snapshot_id`.
|
||||
- OQ-003 (resolved): v1 uses **single-current** acknowledgement per `environment_review_id` (overwrite-on-ack) while preserving an audit trail (`environment_review.acknowledged`) for each acknowledgement action.
|
||||
- OQ-004 (deferred): A separate acknowledgement Resource/detail page is deferred; the acknowledgement card + audit trail are sufficient for v1.
|
||||
|
||||
## Follow-Up Spec Candidates
|
||||
|
||||
- Spec 344 — Decision-Based Governance Inbox Final Operator Workflow
|
||||
- Spec 345 — Localization v1 Product Surface Hardening
|
||||
- Spec 346 — Provider Readiness Productization
|
||||
- Spec 347 — External Support Desk / PSA Handoff
|
||||
@ -0,0 +1,138 @@
|
||||
# Tasks: Spec 343 - Customer Review Attestation & Accepted Risk Lifecycle
|
||||
|
||||
**Branch**: `343-customer-review-attestation-accepted-risk-lifecycle` | **Date**: 2026-06-01 | **Spec**: `specs/343-customer-review-attestation-accepted-risk-lifecycle/spec.md`
|
||||
|
||||
## Phase 0: Preflight / Guardrails
|
||||
|
||||
- [ ] T001 Confirm working tree is clean and branch is `343-customer-review-attestation-accepted-risk-lifecycle`.
|
||||
- [x] T002 Confirm no completed spec package is modified (Specs 326, 329, 337, 342 are context only).
|
||||
- [x] T003 Re-read Spec 342 state contract and repo-truth map so Spec 343 does not regress customer-safe consumption semantics.
|
||||
|
||||
## Phase 1: Repo Truth Map + State Contract (no runtime changes yet)
|
||||
|
||||
- [x] T010 Inspect current Customer Review Workspace implementation:
|
||||
- `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`
|
||||
- `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php`
|
||||
- [x] T011 Confirm accepted risk truth source and fields:
|
||||
- `apps/platform/app/Models/FindingException.php`
|
||||
- `apps/platform/app/Models/FindingExceptionDecision.php`
|
||||
- [x] T012 Confirm acknowledgement truth does not already exist as a review-scoped model/table (do not assume).
|
||||
- [x] T013 Update `specs/343-customer-review-attestation-accepted-risk-lifecycle/repo-truth-map.md` with confirmed classifications and evidence.
|
||||
- [x] T014 Update `specs/343-customer-review-attestation-accepted-risk-lifecycle/review-attestation-risk-state-contract.md` so every displayed state is repo-backed or explicitly “not available / deferred”.
|
||||
- [x] T015 Decide the narrowest correct acknowledgement basis capture (review id + pack id and/or evidence snapshot id) that supports “re-ack required” without guessing.
|
||||
|
||||
## Phase 2: Acknowledgement Persistence (conditional)
|
||||
|
||||
- [x] T020 If repo truth confirms no review acknowledgement persistence exists, add a minimal persisted entity:
|
||||
- migration (PostgreSQL) for `environment_review_acknowledgements` (final name TBD)
|
||||
- model `EnvironmentReviewAcknowledgement` (final name TBD)
|
||||
- workspace + managed-environment scoping fields
|
||||
- captured basis fields (review pack id and/or evidence snapshot id where present)
|
||||
- indexes for `(workspace_id, managed_environment_id, environment_review_id)` and actor/time query paths
|
||||
- [x] T021 Decide append-only vs single-current record semantics; implement the narrowest correct version and capture the decision in spec artifacts.
|
||||
- [x] T022 Add/extend policies/gates so cross-workspace access is denied-as-not-found and acknowledgement writes require a dedicated capability.
|
||||
|
||||
## Phase 3: Service + Audit Wiring
|
||||
|
||||
- [x] T030 Add an acknowledgement service/action class (domain-owned) that:
|
||||
- authorizes the actor (capability + scope)
|
||||
- records acknowledgement with basis capture
|
||||
- emits an audit event
|
||||
- [x] T031 Add audit action IDs for acknowledgement events and ensure payload is customer-safe.
|
||||
|
||||
## Phase 4: Customer Review Workspace UI Wiring
|
||||
|
||||
- [x] T040 Implement derived “attestation state” computation for the page using only repo-backed truth (no new status families).
|
||||
- [x] T041 Update the Blade view to render:
|
||||
- acknowledgement card (status/reason/impact/primary next action)
|
||||
- evidence/review-pack basis fields for the acknowledgement section
|
||||
- accepted-risk lifecycle section remains visible and customer-safe
|
||||
- [x] T042 Add an “Acknowledge review” action:
|
||||
- uses `Action::make(...)->action(...)`
|
||||
- requires confirmation
|
||||
- collects optional comment
|
||||
- writes only through the service/action class
|
||||
- shows success/error notifications
|
||||
- [x] T043 Keep diagnostics collapsed by default and capability-gated (`support_diagnostics.view`).
|
||||
- [x] T044 Ensure no legal/compliance/certification wording is introduced and existing “not a legal attestation” disclosures remain consistent.
|
||||
|
||||
## Phase 5: Accepted Risk Lifecycle Tightening (Finding Exceptions)
|
||||
|
||||
- [x] T050 Confirm the accepted-risk section uses `FindingException` truth consistently (status + validity states + owner/reason/dates).
|
||||
- [x] T051 Add or refine highlighting for:
|
||||
- expiring / expired / revoked / missing_support
|
||||
- missing owner, missing rationale, missing expiry/review date (only if repo truth supports these fields)
|
||||
- [x] T052 Ensure accepted-risk “no records” copy never implies “no risks exist” (truth: only “no accepted risks recorded”).
|
||||
|
||||
## Phase 6: Feature/Livewire Tests (Pest)
|
||||
|
||||
- [x] T060 Add `apps/platform/tests/Feature/Filament/Spec343CustomerReviewAttestationAcceptedRiskTest.php` covering:
|
||||
- acknowledgement card visible and correct per state
|
||||
- acknowledgement required vs acknowledged (fixture-backed)
|
||||
- “re-ack required” only when basis drift detection is repo-backed
|
||||
- no legal/e-signature claim
|
||||
- evidence basis visibility
|
||||
- diagnostics collapsed by default
|
||||
- acknowledgement action authorization + audit emission + basis capture
|
||||
- cross-workspace isolation (not found)
|
||||
- [x] T061 Add test coverage for accepted-risk lifecycle visibility using existing `FindingException` fixtures/factories.
|
||||
|
||||
## Phase 7: Browser Smoke + Screenshots
|
||||
|
||||
- [x] T070 Add `apps/platform/tests/Browser/Spec343CustomerReviewAttestationAcceptedRiskSmokeTest.php` covering browser-visible states:
|
||||
- acknowledgement required
|
||||
- acknowledged (if fixture-supported)
|
||||
- accepted risks present (active / due for review / expired where possible)
|
||||
- no accepted risks
|
||||
- diagnostics collapsed
|
||||
- dark mode (if practical)
|
||||
- [x] T071 Capture screenshots under `specs/343-customer-review-attestation-accepted-risk-lifecycle/artifacts/screenshots/`:
|
||||
- `01-acknowledgement-required.png`
|
||||
- `02-review-acknowledged.png`
|
||||
- `03-accepted-risks-present.png`
|
||||
- `04-accepted-risk-due-for-review.png`
|
||||
- `05-accepted-risk-expired.png`
|
||||
- `06-no-accepted-risks.png`
|
||||
- `07-diagnostics-collapsed.png`
|
||||
- `08-dark-mode.png`
|
||||
- [x] T072 If a screenshot state is unreachable, document why in the spec package instead of faking backend truth.
|
||||
|
||||
## Phase 8: UI Coverage Artifacts (post-diff decision)
|
||||
|
||||
- [x] T080 Decide whether `docs/ui-ux-enterprise-audit/page-reports/ui-006-customer-review-workspace.md` requires an update due to new acknowledgement card/action.
|
||||
- [x] T081 Record the final decision in the PR close-out entry `Guardrail / Exception / Smoke Coverage` (do not leave it implicit).
|
||||
|
||||
## Phase 9: Validation
|
||||
|
||||
- [x] T090 Run:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Filament/Spec343CustomerReviewAttestationAcceptedRiskTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser/Spec343CustomerReviewAttestationAcceptedRiskSmokeTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter='CustomerReview|EnvironmentReview|ReviewPack|Evidence|FindingException|Audit' --compact`
|
||||
- Note: the broad filter run currently surfaces unrelated failing tests (e.g. `WorkspaceLastOwnerGuardTest`, `TenantDashboardProductizationReadinessTest`) and triggered a PHP crash (`zend_mm_heap corrupted`, signal 6) in this environment; Spec 343 gates rely on the targeted spec tests above.
|
||||
- [x] T091 Run:
|
||||
- `cd apps/platform && php vendor/bin/pint --dirty`
|
||||
- `git diff --check`
|
||||
- [x] T092 Update `repo-truth-map.md` + state contract based on what was discovered/implemented.
|
||||
|
||||
## Explicit Non-Goals
|
||||
|
||||
- [x] NT001 Do not build a generic GRC framework, risk scoring engine, or policy exception board.
|
||||
- [x] NT002 Do not introduce legal signature / compliance certification semantics.
|
||||
- [x] NT003 Do not introduce external customer portal architecture or external identity federation/invitations in this slice.
|
||||
- [x] NT004 Do not introduce Graph/provider calls during UI render.
|
||||
- [x] NT005 Do not expose raw provider JSON, internal IDs as primary labels, or diagnostics by default.
|
||||
- [x] NT006 Do not rewrite completed Specs 326, 329, 337, or 342.
|
||||
|
||||
## Required Final Report Content For Later Implementation
|
||||
|
||||
When implementation completes, report:
|
||||
|
||||
- What changed (acknowledgement truth + accepted-risk lifecycle visibility).
|
||||
- Attestation states supported and any deferred/unavailable states.
|
||||
- Accepted risk lifecycle states supported and what fields are repo-backed.
|
||||
- Evidence/review-pack basis and audit truth (no false claims).
|
||||
- RBAC/isolation behavior.
|
||||
- Files changed.
|
||||
- Tests run + results.
|
||||
- Browser smoke + screenshots path.
|
||||
- Migration/deployment impact statement (migrations yes/no; env vars; queues/scheduler; filament assets).
|
||||