Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 3m45s
Implemented the first version of review output resolve actions. Included a ReviewOutputResolveActionMapper, commands to seed browser fixtures, updated CustomerReviewWorkspace, EnvironmentReviewResource, UI enforcement, and related views. Also added extensive unit, feature, and browser tests, and updated the design coverage matrix.
1210 lines
52 KiB
PHP
1210 lines
52 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Resources;
|
|
|
|
use App\Exceptions\Entitlements\WorkspaceEntitlementBlockedException;
|
|
use App\Filament\Concerns\InteractsWithTenantOwnedRecords;
|
|
use App\Filament\Concerns\ResolvesPanelTenantContext;
|
|
use App\Filament\Concerns\WorkspaceScopedEnvironmentRoutes;
|
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
|
use App\Filament\Resources\EnvironmentReviewResource\Pages;
|
|
use App\Models\EnvironmentReview;
|
|
use App\Models\EnvironmentReviewSection;
|
|
use App\Models\EvidenceSnapshot;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\ReviewPack;
|
|
use App\Models\User;
|
|
use App\Services\Auth\CapabilityResolver;
|
|
use App\Services\EnvironmentReviews\EnvironmentReviewService;
|
|
use App\Services\ReviewPackService;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\Auth\UiTooltips as AuthUiTooltips;
|
|
use App\Support\Badges\BadgeCatalog;
|
|
use App\Support\Badges\BadgeDomain;
|
|
use App\Support\Badges\BadgeRenderer;
|
|
use App\Support\EnvironmentReviewCompletenessState;
|
|
use App\Support\EnvironmentReviewStatus;
|
|
use App\Support\Findings\FindingOutcomeSemantics;
|
|
use App\Support\Navigation\NavigationScope;
|
|
use App\Support\OperationRunLinks;
|
|
use App\Support\OperationRunType;
|
|
use App\Support\OpsUx\OperationUxPresenter;
|
|
use App\Support\Rbac\UiEnforcement;
|
|
use App\Support\ReasonTranslation\ReasonPresenter;
|
|
use App\Support\ResolutionGuidance\Adapters\ReviewPackOutputResolutionAdapter;
|
|
use App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance;
|
|
use App\Support\ReviewPackStatus;
|
|
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType;
|
|
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthEnvelope;
|
|
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter;
|
|
use App\Support\Ui\GovernanceArtifactTruth\CompressedGovernanceOutcome;
|
|
use App\Support\Ui\GovernanceArtifactTruth\SurfaceCompressionContext;
|
|
use BackedEnum;
|
|
use Filament\Actions;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Infolists\Components\RepeatableEntry;
|
|
use Filament\Infolists\Components\TextEntry;
|
|
use Filament\Infolists\Components\ViewEntry;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Panel;
|
|
use Filament\Resources\Resource;
|
|
use Filament\Schemas\Components\Section;
|
|
use Filament\Schemas\Schema;
|
|
use Filament\Support\Enums\TextSize;
|
|
use Filament\Tables;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Support\Str;
|
|
use UnitEnum;
|
|
|
|
class EnvironmentReviewResource extends Resource
|
|
{
|
|
use InteractsWithTenantOwnedRecords;
|
|
use ResolvesPanelTenantContext;
|
|
use WorkspaceScopedEnvironmentRoutes;
|
|
|
|
protected static bool $isDiscovered = false;
|
|
|
|
protected static ?string $model = EnvironmentReview::class;
|
|
|
|
protected static ?string $slug = 'reviews';
|
|
|
|
protected static ?string $tenantOwnershipRelationshipName = 'tenant';
|
|
|
|
protected static bool $isGloballySearchable = false;
|
|
|
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-document-magnifying-glass';
|
|
|
|
protected static string|UnitEnum|null $navigationGroup = 'Reporting';
|
|
|
|
protected static ?string $navigationLabel = 'Reviews';
|
|
|
|
protected static ?int $navigationSort = 45;
|
|
|
|
public static function shouldRegisterNavigation(): bool
|
|
{
|
|
return NavigationScope::shouldRegisterEnvironmentNavigation()
|
|
&& parent::shouldRegisterNavigation();
|
|
}
|
|
|
|
public static function getSlug(?Panel $panel = null): string
|
|
{
|
|
$slug = $panel?->getId() === 'admin'
|
|
? 'environment-reviews'
|
|
: parent::getSlug($panel);
|
|
|
|
return static::workspaceScopedSlug($slug, $panel);
|
|
}
|
|
|
|
public static function getNavigationGroup(): string
|
|
{
|
|
return __('localization.review.reporting');
|
|
}
|
|
|
|
public static function getNavigationLabel(): string
|
|
{
|
|
return __('localization.review.reviews');
|
|
}
|
|
|
|
public static function getModelLabel(): string
|
|
{
|
|
return __('localization.review.review');
|
|
}
|
|
|
|
public static function getPluralModelLabel(): string
|
|
{
|
|
return __('localization.review.reviews');
|
|
}
|
|
|
|
public static function canViewAny(): bool
|
|
{
|
|
$tenant = static::resolveTenantContextForCurrentPanel();
|
|
$user = auth()->user();
|
|
|
|
if (! $tenant instanceof ManagedEnvironment || ! $user instanceof User) {
|
|
return false;
|
|
}
|
|
|
|
if (! $user->canAccessTenant($tenant)) {
|
|
return false;
|
|
}
|
|
|
|
return $user->can(Capabilities::ENVIRONMENT_REVIEW_VIEW, $tenant);
|
|
}
|
|
|
|
public static function canView(Model $record): bool
|
|
{
|
|
$tenant = static::resolveTenantContextForCurrentPanel();
|
|
$user = auth()->user();
|
|
|
|
if (! $tenant instanceof ManagedEnvironment || ! $user instanceof User || ! $record instanceof EnvironmentReview) {
|
|
return false;
|
|
}
|
|
|
|
if (! $user->canAccessTenant($tenant)) {
|
|
return false;
|
|
}
|
|
|
|
if ((int) $record->managed_environment_id !== (int) $tenant->getKey()) {
|
|
return false;
|
|
}
|
|
|
|
return $user->can('view', $record);
|
|
}
|
|
|
|
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
|
{
|
|
return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::CrudListAndView, ActionSurfaceType::ReadOnlyRegistryReport)
|
|
->satisfy(ActionSurfaceSlot::ListHeader, 'Create review is available from the review library header.')
|
|
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
|
|
->satisfy(ActionSurfaceSlot::ListEmptyState, 'Empty state includes exactly one Create first review CTA.')
|
|
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'ManagedEnvironment reviews do not expose bulk actions in the first slice.')
|
|
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'Clickable-row inspection stays primary while Export executive pack remains the only inline row shortcut.')
|
|
->satisfy(ActionSurfaceSlot::DetailHeader, 'Detail exposes one dominant lifecycle action, groups the remaining lifecycle actions under "More", keeps archive in a danger bucket, and renders operation/export/evidence navigation in contextual summary content.');
|
|
}
|
|
|
|
public static function getEloquentQuery(): Builder
|
|
{
|
|
return static::getTenantOwnedEloquentQuery()
|
|
->with(['tenant', 'evidenceSnapshot', 'operationRun', 'initiator', 'publisher', 'currentExportReviewPack', 'sections'])
|
|
->latest('generated_at')
|
|
->latest('id');
|
|
}
|
|
|
|
public static function resolveScopedRecordOrFail(int|string|null $record): Model
|
|
{
|
|
return static::resolveTenantOwnedRecordOrFail($record);
|
|
}
|
|
|
|
public static function form(Schema $schema): Schema
|
|
{
|
|
return $schema;
|
|
}
|
|
|
|
public static function infolist(Schema $schema): Schema
|
|
{
|
|
return $schema->schema([
|
|
Section::make(__('localization.review.outcome_summary'))
|
|
->schema([
|
|
ViewEntry::make('artifact_truth')
|
|
->hiddenLabel()
|
|
->view('filament.infolists.entries.governance-artifact-truth')
|
|
->state(fn (EnvironmentReview $record): array => static::truthState($record))
|
|
->columnSpanFull(),
|
|
])
|
|
->columnSpanFull(),
|
|
Section::make(__('localization.review.review'))
|
|
->schema([
|
|
TextEntry::make('status')
|
|
->label(__('localization.review.review_status'))
|
|
->badge()
|
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::EnvironmentReviewStatus))
|
|
->color(BadgeRenderer::color(BadgeDomain::EnvironmentReviewStatus))
|
|
->icon(BadgeRenderer::icon(BadgeDomain::EnvironmentReviewStatus))
|
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::EnvironmentReviewStatus)),
|
|
TextEntry::make('completeness_state')
|
|
->label(__('localization.review.completeness'))
|
|
->badge()
|
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::EnvironmentReviewCompleteness))
|
|
->color(BadgeRenderer::color(BadgeDomain::EnvironmentReviewCompleteness))
|
|
->icon(BadgeRenderer::icon(BadgeDomain::EnvironmentReviewCompleteness))
|
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::EnvironmentReviewCompleteness)),
|
|
TextEntry::make('tenant.name')->label(__('localization.review.tenant')),
|
|
TextEntry::make('generated_at')->dateTime()->placeholder('—'),
|
|
TextEntry::make('published_at')->dateTime()->placeholder('—'),
|
|
TextEntry::make('evidenceSnapshot.id')
|
|
->label(__('localization.review.evidence_snapshot'))
|
|
->formatStateUsing(fn (?int $state): string => $state ? '#'.$state : '—')
|
|
->url(fn (EnvironmentReview $record): ?string => $record->evidenceSnapshot
|
|
? EvidenceSnapshotResource::getUrl('view', ['record' => $record->evidenceSnapshot], tenant: $record->tenant)
|
|
: null),
|
|
TextEntry::make('currentExportReviewPack.id')
|
|
->label(__('localization.review.current_export'))
|
|
->formatStateUsing(fn (?int $state): string => $state ? '#'.$state : '—')
|
|
->url(fn (EnvironmentReview $record): ?string => $record->currentExportReviewPack
|
|
? ReviewPackResource::getUrl('view', ['record' => $record->currentExportReviewPack], tenant: $record->tenant)
|
|
: null),
|
|
TextEntry::make('fingerprint')
|
|
->copyable()
|
|
->placeholder('—')
|
|
->hidden(fn (): bool => static::isCustomerWorkspaceMode())
|
|
->columnSpanFull()
|
|
->fontFamily('mono')
|
|
->size(TextSize::ExtraSmall),
|
|
])
|
|
->columns(2)
|
|
->columnSpanFull(),
|
|
Section::make(__('localization.review.output_guidance'))
|
|
->schema([
|
|
ViewEntry::make('output_guidance')
|
|
->hiddenLabel()
|
|
->view('filament.infolists.entries.review-pack-output-guidance')
|
|
->state(fn (EnvironmentReview $record): array => static::outputGuidanceState($record))
|
|
->columnSpanFull(),
|
|
])
|
|
->columnSpanFull(),
|
|
Section::make(__('localization.review.executive_posture'))
|
|
->schema([
|
|
ViewEntry::make('review_summary')
|
|
->hiddenLabel()
|
|
->view('filament.infolists.entries.environment-review-summary')
|
|
->state(fn (EnvironmentReview $record): array => static::summaryPresentation($record))
|
|
->columnSpanFull(),
|
|
])
|
|
->columnSpanFull(),
|
|
Section::make(__('localization.review.sections'))
|
|
->schema([
|
|
RepeatableEntry::make('sections')
|
|
->state(fn (EnvironmentReview $record): array => static::visibleSections($record))
|
|
->hiddenLabel()
|
|
->schema([
|
|
TextEntry::make('title'),
|
|
TextEntry::make('completeness_state')
|
|
->label(__('localization.review.completeness'))
|
|
->badge()
|
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::EnvironmentReviewCompleteness))
|
|
->color(BadgeRenderer::color(BadgeDomain::EnvironmentReviewCompleteness))
|
|
->icon(BadgeRenderer::icon(BadgeDomain::EnvironmentReviewCompleteness))
|
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::EnvironmentReviewCompleteness)),
|
|
TextEntry::make('measured_at')->dateTime()->placeholder('—'),
|
|
Section::make(__('localization.review.details'))
|
|
->schema([
|
|
ViewEntry::make('section_payload')
|
|
->hiddenLabel()
|
|
->view('filament.infolists.entries.environment-review-section')
|
|
->state(fn (EnvironmentReviewSection $record): array => static::sectionPresentation($record))
|
|
->columnSpanFull(),
|
|
])
|
|
->collapsible()
|
|
->collapsed()
|
|
->columnSpanFull(),
|
|
])
|
|
->columns(3),
|
|
])
|
|
->columnSpanFull(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array<int, EnvironmentReviewSection>
|
|
*/
|
|
private static function visibleSections(EnvironmentReview $record): array
|
|
{
|
|
return $record->sections
|
|
->reject(fn (EnvironmentReviewSection $section): bool => static::isCustomerWorkspaceMode() && $section->isControlInterpretation())
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
public static function table(Table $table): Table
|
|
{
|
|
$exportExecutivePackAction = UiEnforcement::forTableAction(
|
|
Actions\Action::make('export_executive_pack')
|
|
->label(__('localization.review.export_executive_pack'))
|
|
->icon('heroicon-o-arrow-down-tray')
|
|
->visible(fn (EnvironmentReview $record): bool => in_array($record->status, [
|
|
EnvironmentReviewStatus::Ready->value,
|
|
EnvironmentReviewStatus::Published->value,
|
|
], true))
|
|
->disabled(fn (EnvironmentReview $record): bool => static::reviewPackGenerationBlocked($record->tenant))
|
|
->action(fn (EnvironmentReview $record): mixed => static::executeExport($record)),
|
|
fn (EnvironmentReview $record): EnvironmentReview => $record,
|
|
)
|
|
->requireCapability(Capabilities::ENVIRONMENT_REVIEW_MANAGE)
|
|
->preserveVisibility()
|
|
->preserveDisabled()
|
|
->apply();
|
|
|
|
$exportExecutivePackAction->tooltip(fn (EnvironmentReview $record): ?string => static::reviewPackGenerationActionTooltip($record->tenant));
|
|
|
|
return $table
|
|
->defaultSort('generated_at', 'desc')
|
|
->persistFiltersInSession()
|
|
->persistSearchInSession()
|
|
->persistSortInSession()
|
|
->recordUrl(fn (EnvironmentReview $record): string => static::environmentScopedUrl('view', ['record' => $record], $record->tenant))
|
|
->columns([
|
|
Tables\Columns\TextColumn::make('status')
|
|
->badge()
|
|
->formatStateUsing(BadgeRenderer::label(BadgeDomain::EnvironmentReviewStatus))
|
|
->color(BadgeRenderer::color(BadgeDomain::EnvironmentReviewStatus))
|
|
->icon(BadgeRenderer::icon(BadgeDomain::EnvironmentReviewStatus))
|
|
->iconColor(BadgeRenderer::iconColor(BadgeDomain::EnvironmentReviewStatus))
|
|
->sortable(),
|
|
Tables\Columns\TextColumn::make('outcome')
|
|
->label(__('localization.review.outcome'))
|
|
->badge()
|
|
->getStateUsing(fn (EnvironmentReview $record): string => static::compressedOutcome($record)->primaryLabel)
|
|
->color(fn (EnvironmentReview $record): string => static::compressedOutcome($record)->primaryBadge->color)
|
|
->icon(fn (EnvironmentReview $record): ?string => static::compressedOutcome($record)->primaryBadge->icon)
|
|
->iconColor(fn (EnvironmentReview $record): ?string => static::compressedOutcome($record)->primaryBadge->iconColor)
|
|
->description(fn (EnvironmentReview $record): ?string => static::compressedOutcome($record)->primaryReason)
|
|
->wrap(),
|
|
Tables\Columns\TextColumn::make('generated_at')->dateTime()->placeholder('—')->sortable(),
|
|
Tables\Columns\TextColumn::make('published_at')->dateTime()->placeholder('—')->sortable(),
|
|
Tables\Columns\IconColumn::make('summary.has_ready_export')
|
|
->label(__('localization.review.export'))
|
|
->boolean(),
|
|
Tables\Columns\TextColumn::make('next_step')
|
|
->label(__('localization.review.next_step'))
|
|
->getStateUsing(fn (EnvironmentReview $record): string => static::compressedOutcome($record)->nextActionText)
|
|
->wrap(),
|
|
Tables\Columns\TextColumn::make('fingerprint')
|
|
->toggleable(isToggledHiddenByDefault: true)
|
|
->searchable(),
|
|
])
|
|
->filters([
|
|
Tables\Filters\SelectFilter::make('status')
|
|
->options(collect(EnvironmentReviewStatus::cases())
|
|
->mapWithKeys(fn (EnvironmentReviewStatus $status): array => [$status->value => Str::headline($status->value)])
|
|
->all()),
|
|
Tables\Filters\SelectFilter::make('completeness_state')
|
|
->options(BadgeCatalog::options(BadgeDomain::EnvironmentReviewCompleteness, EnvironmentReviewCompletenessState::values())),
|
|
\App\Support\Filament\FilterPresets::dateRange('review_date', __('localization.review.review_date'), 'generated_at'),
|
|
])
|
|
->actions([
|
|
$exportExecutivePackAction,
|
|
])
|
|
->bulkActions([])
|
|
->emptyStateHeading(__('localization.review.no_environment_reviews_yet'))
|
|
->emptyStateDescription(__('localization.review.create_first_review_description'))
|
|
->emptyStateActions([
|
|
static::makeCreateReviewAction(
|
|
name: 'create_first_review',
|
|
label: __('localization.review.create_first_review'),
|
|
icon: 'heroicon-o-plus',
|
|
),
|
|
]);
|
|
}
|
|
|
|
public static function getPages(): array
|
|
{
|
|
return [
|
|
'index' => Pages\ListEnvironmentReviews::route('/'),
|
|
'view' => Pages\ViewEnvironmentReview::route('/{record}'),
|
|
];
|
|
}
|
|
|
|
public static function makeCreateReviewAction(
|
|
string $name = 'create_review',
|
|
string $label = 'Create review',
|
|
string $icon = 'heroicon-o-plus',
|
|
): Actions\Action {
|
|
$label = $label === 'Create review'
|
|
? __('localization.review.create_review')
|
|
: $label;
|
|
|
|
return UiEnforcement::forAction(
|
|
Actions\Action::make($name)
|
|
->label($label)
|
|
->icon($icon)
|
|
->form([
|
|
Section::make(__('localization.review.evidence_basis'))
|
|
->schema([
|
|
Select::make('evidence_snapshot_id')
|
|
->label(__('localization.review.evidence_snapshot'))
|
|
->required()
|
|
->options(fn (): array => static::evidenceSnapshotOptions())
|
|
->searchable()
|
|
->helperText(__('localization.review.evidence_basis_helper')),
|
|
]),
|
|
])
|
|
->action(fn (array $data): mixed => static::executeCreateReview($data)),
|
|
)
|
|
->requireCapability(Capabilities::ENVIRONMENT_REVIEW_MANAGE)
|
|
->apply();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
*/
|
|
public static function executeCreateReview(array $data): void
|
|
{
|
|
$tenant = static::resolveTenantContextForCurrentPanel();
|
|
$user = auth()->user();
|
|
|
|
if (! $tenant instanceof ManagedEnvironment || ! $user instanceof User) {
|
|
Notification::make()->danger()->title(__('localization.review.unable_create_missing_context'))->send();
|
|
|
|
return;
|
|
}
|
|
|
|
if (! $user->canAccessTenant($tenant)) {
|
|
abort(404);
|
|
}
|
|
|
|
if (! $user->can(Capabilities::ENVIRONMENT_REVIEW_MANAGE, $tenant)) {
|
|
abort(403);
|
|
}
|
|
|
|
$snapshotId = $data['evidence_snapshot_id'] ?? null;
|
|
$snapshot = is_numeric($snapshotId)
|
|
? EvidenceSnapshot::query()
|
|
->whereKey((int) $snapshotId)
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->first()
|
|
: null;
|
|
|
|
if (! $snapshot instanceof EvidenceSnapshot) {
|
|
Notification::make()->danger()->title(__('localization.review.select_valid_evidence_snapshot'))->send();
|
|
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$review = app(EnvironmentReviewService::class)->create($tenant, $snapshot, $user);
|
|
} catch (\Throwable $throwable) {
|
|
Notification::make()->danger()->title(__('localization.review.unable_create_review'))->body($throwable->getMessage())->send();
|
|
|
|
return;
|
|
}
|
|
|
|
static::truthEnvelope($review->refresh(), fresh: true);
|
|
|
|
if (! $review->wasRecentlyCreated) {
|
|
Notification::make()
|
|
->success()
|
|
->title(__('localization.review.review_already_available'))
|
|
->body(__('localization.review.review_already_available_body'))
|
|
->actions([
|
|
Actions\Action::make('view_review')
|
|
->label(__('localization.review.view_review'))
|
|
->url(static::environmentScopedUrl('view', ['record' => $review], $tenant)),
|
|
])
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
$toast = OperationUxPresenter::queuedToast(OperationRunType::EnvironmentReviewCompose->value)
|
|
->body(__('localization.review.review_composing_background'));
|
|
|
|
if ($review->operation_run_id) {
|
|
$toast->actions([
|
|
Actions\Action::make('view_run')
|
|
->label(__('localization.review.open_operation'))
|
|
->url(OperationRunLinks::tenantlessView((int) $review->operation_run_id)),
|
|
]);
|
|
}
|
|
|
|
$toast->send();
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function reviewPackGenerationDecision(?ManagedEnvironment $tenant = null): array
|
|
{
|
|
$tenant ??= static::resolveTenantContextForCurrentPanel();
|
|
|
|
if (! $tenant instanceof ManagedEnvironment) {
|
|
return [];
|
|
}
|
|
|
|
return app(ReviewPackService::class)->reviewPackGenerationDecisionForTenant($tenant);
|
|
}
|
|
|
|
public static function reviewPackGenerationBlocked(?ManagedEnvironment $tenant = null): bool
|
|
{
|
|
return (bool) (static::reviewPackGenerationDecision($tenant)['is_blocked'] ?? false);
|
|
}
|
|
|
|
public static function reviewPackGenerationBlockReason(?ManagedEnvironment $tenant = null): ?string
|
|
{
|
|
$decision = static::reviewPackGenerationDecision($tenant);
|
|
|
|
if (! (bool) ($decision['is_blocked'] ?? false)) {
|
|
return null;
|
|
}
|
|
|
|
$reason = $decision['block_reason'] ?? null;
|
|
|
|
return is_string($reason) && $reason !== '' ? $reason : null;
|
|
}
|
|
|
|
public static function reviewPackGenerationWarningReason(?ManagedEnvironment $tenant = null): ?string
|
|
{
|
|
$decision = static::reviewPackGenerationDecision($tenant);
|
|
|
|
if (! (bool) ($decision['is_warning'] ?? false)) {
|
|
return null;
|
|
}
|
|
|
|
$reason = $decision['warning_reason'] ?? null;
|
|
|
|
return is_string($reason) && $reason !== '' ? $reason : null;
|
|
}
|
|
|
|
public static function reviewPackGenerationActionTooltip(?ManagedEnvironment $tenant = null): ?string
|
|
{
|
|
$tenant ??= static::panelTenantContext();
|
|
$user = auth()->user();
|
|
|
|
if ($tenant instanceof ManagedEnvironment && $user instanceof User && ! $user->can(Capabilities::ENVIRONMENT_REVIEW_MANAGE, $tenant)) {
|
|
return AuthUiTooltips::insufficientPermission();
|
|
}
|
|
|
|
return static::reviewPackGenerationBlockReason($tenant)
|
|
?? static::reviewPackGenerationWarningReason($tenant);
|
|
}
|
|
|
|
public static function executeExport(EnvironmentReview $review): void
|
|
{
|
|
$review->loadMissing(['tenant', 'currentExportReviewPack']);
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User || ! $review->tenant instanceof ManagedEnvironment) {
|
|
Notification::make()->danger()->title(__('localization.review.unable_export_missing_context'))->send();
|
|
|
|
return;
|
|
}
|
|
|
|
if (! $user->canAccessTenant($review->tenant)) {
|
|
abort(404);
|
|
}
|
|
|
|
if (! $user->can('export', $review)) {
|
|
abort(403);
|
|
}
|
|
|
|
$service = app(ReviewPackService::class);
|
|
|
|
if ($service->checkActiveRunForReview($review)) {
|
|
OperationUxPresenter::alreadyQueuedToast(OperationRunType::ReviewPackGenerate->value)
|
|
->body(__('localization.review.export_already_queued_body'))
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$pack = $service->generateFromReview($review, $user, [
|
|
'include_pii' => true,
|
|
'include_operations' => true,
|
|
]);
|
|
} catch (WorkspaceEntitlementBlockedException $exception) {
|
|
Notification::make()->warning()->title(__('localization.review.executive_pack_export_unavailable'))->body($exception->getMessage())->send();
|
|
|
|
return;
|
|
} catch (\Throwable $throwable) {
|
|
Notification::make()->danger()->title(__('localization.review.unable_export_executive_pack'))->body($throwable->getMessage())->send();
|
|
|
|
return;
|
|
}
|
|
|
|
static::truthEnvelope($review->refresh(), fresh: true);
|
|
app(ArtifactTruthPresenter::class)->forReviewPackFresh($pack->refresh());
|
|
|
|
if (! $pack->wasRecentlyCreated) {
|
|
Notification::make()
|
|
->success()
|
|
->title(__('localization.review.executive_pack_already_available'))
|
|
->body(__('localization.review.executive_pack_already_available_body'))
|
|
->actions([
|
|
Actions\Action::make('view_pack')
|
|
->label(__('localization.review.view_pack'))
|
|
->url(ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $review->tenant)),
|
|
])
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
OperationUxPresenter::queuedToast(OperationRunType::ReviewPackGenerate->value)
|
|
->body(__('localization.review.executive_pack_generating_background'))
|
|
->send();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $parameters
|
|
*/
|
|
public static function environmentScopedUrl(
|
|
string $page = 'index',
|
|
array $parameters = [],
|
|
?ManagedEnvironment $tenant = null,
|
|
): string {
|
|
$panelId = 'admin';
|
|
|
|
return static::getUrl($page, $parameters, panel: $panelId, tenant: $tenant);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
private static function evidenceSnapshotOptions(): array
|
|
{
|
|
$tenant = static::resolveTenantContextForCurrentPanel();
|
|
|
|
if (! $tenant instanceof ManagedEnvironment) {
|
|
return [];
|
|
}
|
|
|
|
return EvidenceSnapshot::query()
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->whereNotNull('generated_at')
|
|
->orderByDesc('generated_at')
|
|
->orderByDesc('id')
|
|
->get()
|
|
->mapWithKeys(static fn (EvidenceSnapshot $snapshot): array => [
|
|
(string) $snapshot->getKey() => sprintf(
|
|
'#%d · %s · %s',
|
|
(int) $snapshot->getKey(),
|
|
BadgeCatalog::spec(BadgeDomain::EvidenceCompleteness, $snapshot->completeness_state)->label,
|
|
$snapshot->generated_at?->format('Y-m-d H:i') ?? __('localization.review.pending')
|
|
),
|
|
])
|
|
->all();
|
|
}
|
|
|
|
private static function reviewCompletenessCountLabel(string $state): string
|
|
{
|
|
return BadgeCatalog::spec(BadgeDomain::EnvironmentReviewCompleteness, $state)->label;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private static function summaryPresentation(EnvironmentReview $record): array
|
|
{
|
|
$summary = is_array($record->summary) ? $record->summary : [];
|
|
$truthEnvelope = static::truthEnvelope($record);
|
|
$reasonPresenter = app(ReasonPresenter::class);
|
|
$highlights = is_array($summary['highlights'] ?? null) ? $summary['highlights'] : [];
|
|
$findingOutcomeSummary = static::findingOutcomeSummary($summary);
|
|
$findingOutcomes = is_array($summary['finding_outcomes'] ?? null) ? $summary['finding_outcomes'] : [];
|
|
$controlInterpretation = is_array($summary['control_interpretation'] ?? null)
|
|
? $summary['control_interpretation']
|
|
: [];
|
|
$packagePresentation = static::governancePackagePresentation($record);
|
|
|
|
if ($findingOutcomeSummary !== null) {
|
|
$highlights[] = __('localization.review.terminal_outcomes').': '.$findingOutcomeSummary.'.';
|
|
}
|
|
|
|
return [
|
|
'operator_explanation' => $truthEnvelope->operatorExplanation?->toArray(),
|
|
'compressed_outcome' => static::compressedOutcome($record)->toArray(),
|
|
'customer_workspace_mode' => static::isCustomerWorkspaceMode(),
|
|
'reason_semantics' => static::isCustomerWorkspaceMode()
|
|
? []
|
|
: $reasonPresenter->semantics($truthEnvelope->reason?->toReasonResolutionEnvelope()),
|
|
'highlights' => $highlights,
|
|
'next_actions' => is_array($summary['recommended_next_actions'] ?? null) ? $summary['recommended_next_actions'] : [],
|
|
'publish_blockers' => is_array($summary['publish_blockers'] ?? null) ? $summary['publish_blockers'] : [],
|
|
'context_links' => static::summaryContextLinks($record, static::isCustomerWorkspaceMode()),
|
|
'control_interpretation' => $controlInterpretation,
|
|
'governance_package' => $packagePresentation,
|
|
'metrics' => static::isCustomerWorkspaceMode() ? static::customerWorkspaceMetrics($record, $summary, $packagePresentation) : [
|
|
['label' => __('localization.review.findings'), 'value' => (string) ($summary['finding_count'] ?? 0)],
|
|
['label' => __('localization.review.reports'), 'value' => (string) ($summary['report_count'] ?? 0)],
|
|
['label' => __('localization.review.operations'), 'value' => (string) ($summary['operation_count'] ?? 0)],
|
|
['label' => __('localization.review.sections'), 'value' => (string) ($summary['section_count'] ?? 0)],
|
|
['label' => __('localization.review.pending_verification'), 'value' => (string) ($findingOutcomes[FindingOutcomeSemantics::OUTCOME_RESOLVED_PENDING_VERIFICATION] ?? 0)],
|
|
['label' => __('localization.review.verified_cleared'), 'value' => (string) ($findingOutcomes[FindingOutcomeSemantics::OUTCOME_VERIFIED_CLEARED] ?? 0)],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $summary
|
|
* @param array<string, mixed> $packagePresentation
|
|
* @return array<int, array{label:string,value:string}>
|
|
*/
|
|
private static function customerWorkspaceMetrics(EnvironmentReview $record, array $summary, array $packagePresentation): array
|
|
{
|
|
$acceptedRisk = is_array($summary['risk_acceptance'] ?? null) ? $summary['risk_acceptance'] : [];
|
|
|
|
return [
|
|
['label' => __('localization.review.governance_package'), 'value' => (string) ($packagePresentation['availability']['label'] ?? __('localization.review.governance_package_unavailable'))],
|
|
['label' => __('localization.review.review_status'), 'value' => static::customerReviewStatusLabel($record)],
|
|
['label' => __('localization.review.evidence_status'), 'value' => static::customerEvidenceStatusLabel($record)],
|
|
['label' => __('localization.review.accepted_risk_status'), 'value' => static::customerAcceptedRiskStatusLabel($acceptedRisk)],
|
|
['label' => __('localization.review.last_review'), 'value' => $record->published_at?->format('Y-m-d') ?? __('localization.review.pending')],
|
|
];
|
|
}
|
|
|
|
private static function customerReviewStatusLabel(EnvironmentReview $record): string
|
|
{
|
|
if ($record->isPublished() && (string) $record->completeness_state === EnvironmentReviewCompletenessState::Complete->value) {
|
|
return __('localization.review.review_completed');
|
|
}
|
|
|
|
if ($record->isPublished()) {
|
|
return __('localization.review.review_requires_attention');
|
|
}
|
|
|
|
return Str::headline((string) $record->status);
|
|
}
|
|
|
|
private static function customerEvidenceStatusLabel(EnvironmentReview $record): string
|
|
{
|
|
$snapshot = $record->evidenceSnapshot;
|
|
$tenant = $record->tenant;
|
|
$user = auth()->user();
|
|
|
|
if (! $snapshot instanceof EvidenceSnapshot) {
|
|
return __('localization.review.evidence_pending');
|
|
}
|
|
|
|
if (! $user instanceof User || ! $tenant instanceof ManagedEnvironment || ! $user->can(Capabilities::EVIDENCE_VIEW, $tenant)) {
|
|
return __('localization.review.evidence_restricted');
|
|
}
|
|
|
|
if ((string) $snapshot->status === 'expired' || ($snapshot->expires_at !== null && $snapshot->expires_at->isPast())) {
|
|
return __('localization.review.evidence_expired');
|
|
}
|
|
|
|
return __('localization.review.evidence_available');
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $acceptedRisk
|
|
*/
|
|
private static function customerAcceptedRiskStatusLabel(array $acceptedRisk): string
|
|
{
|
|
$warningCount = (int) ($acceptedRisk['warning_count'] ?? 0);
|
|
$statusMarkedCount = (int) ($acceptedRisk['status_marked_count'] ?? 0);
|
|
|
|
if ($warningCount > 0) {
|
|
return __('localization.review.accepted_risk_follow_up');
|
|
}
|
|
|
|
if ($statusMarkedCount > 0) {
|
|
return __('localization.review.accepted_risk_on_record', ['count' => $statusMarkedCount]);
|
|
}
|
|
|
|
return __('localization.review.accepted_risk_none');
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private static function governancePackagePresentation(EnvironmentReview $record): array
|
|
{
|
|
$summary = is_array($record->summary) ? $record->summary : [];
|
|
$package = is_array($summary['governance_package'] ?? null) ? $summary['governance_package'] : [];
|
|
|
|
if ($package === []) {
|
|
return [];
|
|
}
|
|
|
|
return array_merge($package, [
|
|
'availability' => static::governancePackageAvailability($record),
|
|
'delivery_note' => __('localization.review.governance_package_delivery_note'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array{state:string,label:string,description:string}
|
|
*/
|
|
private static function governancePackageAvailability(EnvironmentReview $record): array
|
|
{
|
|
$pack = $record->currentExportReviewPack;
|
|
$tenant = $record->tenant;
|
|
$user = auth()->user();
|
|
$controlInterpretation = $record->controlInterpretation();
|
|
$limitations = is_array($controlInterpretation['limitations'] ?? null) ? $controlInterpretation['limitations'] : [];
|
|
$isPartialReview = in_array((string) $record->completeness_state, [
|
|
EnvironmentReviewCompletenessState::Partial->value,
|
|
EnvironmentReviewCompletenessState::Stale->value,
|
|
], true) || $limitations !== [];
|
|
|
|
if (! $pack instanceof ReviewPack) {
|
|
return [
|
|
'state' => 'unavailable',
|
|
'label' => __('localization.review.governance_package_unavailable'),
|
|
'description' => __('localization.review.governance_package_unavailable_description'),
|
|
];
|
|
}
|
|
|
|
if (! $user instanceof User || ! $tenant instanceof ManagedEnvironment || ! $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant)) {
|
|
return [
|
|
'state' => 'blocked',
|
|
'label' => __('localization.review.governance_package_blocked'),
|
|
'description' => __('localization.review.governance_package_blocked_description'),
|
|
];
|
|
}
|
|
|
|
if ($pack->status === ReviewPackStatus::Expired->value || ($pack->expires_at !== null && $pack->expires_at->isPast())) {
|
|
return [
|
|
'state' => 'expired',
|
|
'label' => __('localization.review.governance_package_expired'),
|
|
'description' => __('localization.review.governance_package_expired_description'),
|
|
];
|
|
}
|
|
|
|
if ($pack->status !== ReviewPackStatus::Ready->value) {
|
|
return [
|
|
'state' => 'unavailable',
|
|
'label' => __('localization.review.governance_package_unavailable'),
|
|
'description' => __('localization.review.governance_package_not_ready_description'),
|
|
];
|
|
}
|
|
|
|
if ($isPartialReview) {
|
|
return [
|
|
'state' => 'partial',
|
|
'label' => __('localization.review.governance_package_partial'),
|
|
'description' => __('localization.review.governance_package_partial_description'),
|
|
];
|
|
}
|
|
|
|
return [
|
|
'state' => 'available',
|
|
'label' => __('localization.review.governance_package_available'),
|
|
'description' => __('localization.review.governance_package_available_description'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array{title:string,label:string,url:?string,description:string}>
|
|
*/
|
|
private static function summaryContextLinks(EnvironmentReview $record, bool $customerWorkspaceMode = false): array
|
|
{
|
|
$links = [];
|
|
|
|
if (! $customerWorkspaceMode && is_numeric($record->operation_run_id)) {
|
|
$links[] = [
|
|
'title' => __('localization.review.operation'),
|
|
'label' => __('localization.review.open_operation'),
|
|
'url' => OperationRunLinks::tenantlessView((int) $record->operation_run_id),
|
|
'description' => __('localization.review.operation_description'),
|
|
];
|
|
}
|
|
|
|
if (! $customerWorkspaceMode && $record->currentExportReviewPack && $record->tenant) {
|
|
$links[] = [
|
|
'title' => __('localization.review.executive_pack'),
|
|
'label' => __('localization.review.view_executive_pack'),
|
|
'url' => ReviewPackResource::getUrl('view', ['record' => $record->currentExportReviewPack], tenant: $record->tenant),
|
|
'description' => __('localization.review.executive_pack_description'),
|
|
];
|
|
}
|
|
|
|
if ($record->tenant) {
|
|
$links[] = [
|
|
'title' => __('localization.review.customer_workspace'),
|
|
'label' => __('localization.review.open_customer_workspace'),
|
|
'url' => CustomerReviewWorkspace::environmentFilterUrl($record->tenant),
|
|
'description' => __('localization.review.customer_workspace_description'),
|
|
];
|
|
}
|
|
|
|
if ($record->evidenceSnapshot && $record->tenant) {
|
|
$user = auth()->user();
|
|
$canViewEvidence = $user instanceof User && $user->can(Capabilities::EVIDENCE_VIEW, $record->tenant);
|
|
$evidenceUrl = $canViewEvidence
|
|
? EvidenceSnapshotResource::getUrl('view', ['record' => $record->evidenceSnapshot], tenant: $record->tenant)
|
|
: null;
|
|
|
|
if ($customerWorkspaceMode && $evidenceUrl !== null) {
|
|
$evidenceUrl = static::appendQuery($evidenceUrl, static::customerWorkspaceEvidenceQuery($record));
|
|
}
|
|
|
|
$links[] = [
|
|
'title' => __('localization.review.evidence_snapshot'),
|
|
'label' => __('localization.review.view_evidence_snapshot'),
|
|
'url' => $evidenceUrl,
|
|
'description' => $canViewEvidence
|
|
? __('localization.review.evidence_snapshot_description')
|
|
: __('localization.review.evidence_proof_access_unavailable'),
|
|
];
|
|
}
|
|
|
|
return $links;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private static function sectionPresentation(EnvironmentReviewSection $section): array
|
|
{
|
|
$summary = is_array($section->summary_payload) ? $section->summary_payload : [];
|
|
$render = is_array($section->render_payload) ? $section->render_payload : [];
|
|
$review = $section->environmentReview;
|
|
$tenant = $section->tenant;
|
|
$links = [];
|
|
|
|
if ($section->isControlInterpretation() && $review instanceof EnvironmentReview && $tenant instanceof ManagedEnvironment && $review->evidenceSnapshot instanceof EvidenceSnapshot) {
|
|
$user = auth()->user();
|
|
|
|
if ($user instanceof User && $user->can(Capabilities::EVIDENCE_VIEW, $tenant)) {
|
|
$evidenceUrl = EvidenceSnapshotResource::getUrl('view', ['record' => $review->evidenceSnapshot], tenant: $tenant);
|
|
|
|
if (static::isCustomerWorkspaceMode()) {
|
|
$evidenceUrl = static::appendQuery($evidenceUrl, static::customerWorkspaceEvidenceQuery($review));
|
|
}
|
|
|
|
$links[] = [
|
|
'label' => __('localization.review.view_evidence_snapshot'),
|
|
'url' => $evidenceUrl,
|
|
];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'summary' => collect($summary)->map(function (mixed $value, string $key): ?array {
|
|
if (is_array($value) || $value === null || $value === '') {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'label' => Str::headline($key),
|
|
'value' => (string) $value,
|
|
];
|
|
})->filter()->values()->all(),
|
|
'artifact_sources' => is_array($render['artifact_sources'] ?? null) ? $render['artifact_sources'] : [],
|
|
'highlights' => is_array($render['highlights'] ?? null) ? $render['highlights'] : [],
|
|
'entries' => is_array($render['entries'] ?? null) ? $render['entries'] : [],
|
|
'disclosure' => is_string($render['disclosure'] ?? null) ? $render['disclosure'] : null,
|
|
'next_actions' => is_array($render['next_actions'] ?? null) ? $render['next_actions'] : [],
|
|
'empty_state' => is_string($render['empty_state'] ?? null) ? $render['empty_state'] : null,
|
|
'is_control_interpretation' => $section->isControlInterpretation(),
|
|
'links' => $links,
|
|
];
|
|
}
|
|
|
|
private static function truthEnvelope(EnvironmentReview $record, bool $fresh = false): ArtifactTruthEnvelope
|
|
{
|
|
$presenter = app(ArtifactTruthPresenter::class);
|
|
|
|
return $fresh
|
|
? $presenter->forEnvironmentReviewFresh($record)
|
|
: $presenter->forEnvironmentReview($record);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private static function truthState(EnvironmentReview $record, bool $fresh = false): array
|
|
{
|
|
$presenter = app(ArtifactTruthPresenter::class);
|
|
|
|
return $presenter->surfaceStateFor($record, SurfaceCompressionContext::environmentReview(), $fresh)
|
|
?? static::truthEnvelope($record, $fresh)->toArray(static::compressedOutcome($record, $fresh));
|
|
}
|
|
|
|
private static function compressedOutcome(EnvironmentReview $record, bool $fresh = false): CompressedGovernanceOutcome
|
|
{
|
|
$presenter = app(ArtifactTruthPresenter::class);
|
|
|
|
return $presenter->compressedOutcomeFor($record, SurfaceCompressionContext::environmentReview(), $fresh)
|
|
?? $presenter->compressedOutcomeFromEnvelope(
|
|
static::truthEnvelope($record, $fresh),
|
|
SurfaceCompressionContext::environmentReview(),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $summary
|
|
*/
|
|
private static function findingOutcomeSummary(array $summary): ?string
|
|
{
|
|
$outcomeCounts = $summary['finding_outcomes'] ?? [];
|
|
|
|
if (! is_array($outcomeCounts)) {
|
|
return null;
|
|
}
|
|
|
|
return app(FindingOutcomeSemantics::class)->compactOutcomeSummary($outcomeCounts);
|
|
}
|
|
|
|
private static function isCustomerWorkspaceMode(): bool
|
|
{
|
|
return request()->boolean(CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private static function customerWorkspaceEvidenceQuery(EnvironmentReview $record): array
|
|
{
|
|
return array_filter([
|
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
|
'review_id' => (int) $record->getKey(),
|
|
'interpretation_version' => $record->controlInterpretationVersion(),
|
|
'tenant_filter_id' => request()->query('tenant_filter_id'),
|
|
], static fn (mixed $value): bool => $value !== null && $value !== '');
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $query
|
|
*/
|
|
private static function appendQuery(string $url, array $query): string
|
|
{
|
|
if ($query === []) {
|
|
return $url;
|
|
}
|
|
|
|
return $url.(str_contains($url, '?') ? '&' : '?').http_build_query($query);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function outputGuidanceState(EnvironmentReview $record): array
|
|
{
|
|
$tenant = $record->tenant;
|
|
$user = auth()->user();
|
|
$downloadUrl = static::currentReviewPackDownloadUrlFor($record);
|
|
$reviewUrl = $tenant instanceof ManagedEnvironment
|
|
? static::environmentScopedUrl('view', ['record' => $record], $tenant)
|
|
: null;
|
|
$evidenceUrl = null;
|
|
$operationUrl = null;
|
|
$successorReviewUrl = $tenant instanceof ManagedEnvironment
|
|
? static::successorReviewUrlFor($record, $tenant)
|
|
: null;
|
|
|
|
if (static::isCustomerWorkspaceMode() && $reviewUrl !== null) {
|
|
$reviewUrl = static::appendQuery($reviewUrl, [
|
|
CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1,
|
|
]);
|
|
}
|
|
|
|
if ($record->evidenceSnapshot instanceof EvidenceSnapshot && $tenant instanceof ManagedEnvironment && $user instanceof User && $user->can(Capabilities::EVIDENCE_VIEW, $tenant)) {
|
|
$evidenceUrl = EvidenceSnapshotResource::getUrl('view', ['record' => $record->evidenceSnapshot], tenant: $tenant);
|
|
|
|
if (static::isCustomerWorkspaceMode()) {
|
|
$evidenceUrl = static::appendQuery($evidenceUrl, static::customerWorkspaceEvidenceQuery($record));
|
|
}
|
|
}
|
|
|
|
$operationRun = $record->currentExportReviewPack?->operationRun ?? $record->operationRun;
|
|
|
|
if ($operationRun instanceof \App\Models\OperationRun) {
|
|
$operationUrl = OperationRunLinks::tenantlessView((int) $operationRun->getKey());
|
|
}
|
|
|
|
$guidance = ReviewPackOutputResolutionGuidance::fromReview($record, [
|
|
'download' => $downloadUrl,
|
|
'review' => $reviewUrl,
|
|
'evidence' => $evidenceUrl,
|
|
'operation' => $operationUrl,
|
|
]);
|
|
$guidance['resolution_case'] = ReviewPackOutputResolutionAdapter::fromGuidance(
|
|
review: $record,
|
|
guidance: $guidance,
|
|
sourceSurface: static::isCustomerWorkspaceMode()
|
|
? 'environment_review_detail.customer_workspace'
|
|
: 'environment_review_detail',
|
|
context: [
|
|
'urls' => [
|
|
'review' => $reviewUrl,
|
|
'evidence' => $evidenceUrl,
|
|
'operation' => $operationUrl,
|
|
'download' => $downloadUrl,
|
|
'successor_review' => $successorReviewUrl,
|
|
],
|
|
'execution' => [
|
|
'can_manage_review' => static::canManageReview($record),
|
|
'successor_review_status' => static::successorReviewStatusFor($record),
|
|
],
|
|
],
|
|
);
|
|
|
|
if (! static::isCustomerWorkspaceMode()) {
|
|
if (filled(data_get($guidance, 'resolution_case.primary_action.action_name'))) {
|
|
$guidance['suppress_primary_action_button'] = true;
|
|
}
|
|
|
|
return $guidance;
|
|
}
|
|
|
|
$guidance['detail_mode'] = true;
|
|
$guidance['primary_action'] = null;
|
|
$guidance['secondary_actions'] = [];
|
|
$guidance['next_step_label'] = __('localization.review.review_limitations_below');
|
|
$guidance['context_note'] = __('localization.review.output_guidance_detail_mode_note');
|
|
|
|
return $guidance;
|
|
}
|
|
|
|
private static function successorReviewUrlFor(EnvironmentReview $record, ManagedEnvironment $tenant): ?string
|
|
{
|
|
if (! is_numeric($record->superseded_by_review_id)) {
|
|
return null;
|
|
}
|
|
|
|
$successorReviewId = (int) $record->superseded_by_review_id;
|
|
|
|
if (! EnvironmentReview::query()
|
|
->whereKey($successorReviewId)
|
|
->where('workspace_id', (int) $record->workspace_id)
|
|
->where('managed_environment_id', (int) $record->managed_environment_id)
|
|
->exists()) {
|
|
return null;
|
|
}
|
|
|
|
return static::environmentScopedUrl('view', ['record' => $successorReviewId], $tenant);
|
|
}
|
|
|
|
private static function successorReviewStatusFor(EnvironmentReview $record): ?string
|
|
{
|
|
if ($record->relationLoaded('supersededByReview')) {
|
|
return $record->supersededByReview instanceof EnvironmentReview
|
|
? (string) $record->supersededByReview->status
|
|
: null;
|
|
}
|
|
|
|
if (! is_numeric($record->superseded_by_review_id)) {
|
|
return null;
|
|
}
|
|
|
|
return EnvironmentReview::query()
|
|
->whereKey((int) $record->superseded_by_review_id)
|
|
->where('workspace_id', (int) $record->workspace_id)
|
|
->where('managed_environment_id', (int) $record->managed_environment_id)
|
|
->value('status');
|
|
}
|
|
|
|
private static function canManageReview(EnvironmentReview $record): bool
|
|
{
|
|
$user = auth()->user();
|
|
$tenant = $record->tenant;
|
|
|
|
if (! $user instanceof User || ! $tenant instanceof ManagedEnvironment) {
|
|
return false;
|
|
}
|
|
|
|
return app(CapabilityResolver::class)->can($user, $tenant, Capabilities::ENVIRONMENT_REVIEW_MANAGE);
|
|
}
|
|
|
|
public static function currentReviewPackDownloadUrlFor(EnvironmentReview $record): ?string
|
|
{
|
|
$pack = $record->currentExportReviewPack;
|
|
$tenant = $record->tenant;
|
|
$user = auth()->user();
|
|
|
|
if (! $pack instanceof ReviewPack || ! $tenant instanceof ManagedEnvironment || ! $user instanceof User) {
|
|
return null;
|
|
}
|
|
|
|
if (! $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant)) {
|
|
return null;
|
|
}
|
|
|
|
if ($pack->status !== ReviewPackStatus::Ready->value) {
|
|
return null;
|
|
}
|
|
|
|
if ($pack->expires_at !== null && $pack->expires_at->isPast()) {
|
|
return null;
|
|
}
|
|
|
|
return app(ReviewPackService::class)->generateDownloadUrl($pack, [
|
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
|
'review_id' => (int) $record->getKey(),
|
|
'tenant_filter_id' => request()->query('tenant_filter_id'),
|
|
'interpretation_version' => $record->controlInterpretationVersion(),
|
|
]);
|
|
}
|
|
}
|