## Summary\n- Implements the ReviewPublicationResolutionWorkflow for Spec 386.\n- Adds resolution case/step persistence, policies, services, audit action IDs, and Filament integration.\n- Updates specs, UI/UX documentation, screenshots, and Pest coverage.\n\n## Tests\n- Not run during this handoff; branch was already clean and pushed.\n\n## Target\n- Base: platform-dev\n- Head/topic: 386-review-publication-resolution-workflow-v1 Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #457
601 lines
25 KiB
PHP
601 lines
25 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Resources\EnvironmentReviewResource\Pages;
|
|
|
|
use App\Filament\Resources\EnvironmentReviewResource;
|
|
use App\Filament\Resources\EvidenceSnapshotResource;
|
|
use App\Filament\Resources\ReviewPackResource;
|
|
use App\Models\EnvironmentReview;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ReviewPublicationResolutionCase;
|
|
use App\Models\ReviewPublicationResolutionStep;
|
|
use App\Models\User;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\Auth\UiTooltips as AuthUiTooltips;
|
|
use App\Support\OperationRunLinks;
|
|
use App\Support\OpsUx\OperationUxPresenter;
|
|
use App\Support\Rbac\UiEnforcement;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionActionService;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionCaseStatus;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionService;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionStepAuthorizer;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionStepKey;
|
|
use App\Support\ReviewPublicationResolution\ReviewPublicationResolutionStepStatus;
|
|
use Filament\Actions;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Resources\Pages\Concerns\InteractsWithRecord;
|
|
use Filament\Resources\Pages\Page;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Livewire\Attributes\Locked;
|
|
|
|
class ResolveReviewPublication extends Page
|
|
{
|
|
use InteractsWithRecord;
|
|
|
|
protected static string $resource = EnvironmentReviewResource::class;
|
|
|
|
protected string $view = 'filament.resources.environment-review-resource.pages.resolve-review-publication';
|
|
|
|
#[Locked]
|
|
public ?int $resolutionCaseId = null;
|
|
|
|
public function mount(int|string $record): void
|
|
{
|
|
$this->record = $this->resolveRecord($record);
|
|
$this->authorizeAccess();
|
|
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User || ! $this->record instanceof EnvironmentReview) {
|
|
abort(403);
|
|
}
|
|
|
|
$caseService = app(ReviewPublicationResolutionService::class);
|
|
$case = $user->can('refresh', $this->record)
|
|
? $caseService->openOrResume(
|
|
review: $this->record,
|
|
actor: $user,
|
|
sourceSurface: 'environment_review_detail',
|
|
)
|
|
: $caseService->activeCaseForReview($this->record);
|
|
|
|
if ($case instanceof ReviewPublicationResolutionCase && ! $user->can('view', $case)) {
|
|
abort(404);
|
|
}
|
|
|
|
if (! $user->can('refresh', $this->record) && ! ($case instanceof ReviewPublicationResolutionCase)) {
|
|
abort(403);
|
|
}
|
|
|
|
if (! $case instanceof ReviewPublicationResolutionCase) {
|
|
Notification::make()
|
|
->success()
|
|
->title('Review is ready for publication')
|
|
->body('No publication resolution case was needed for the current review state.')
|
|
->send();
|
|
|
|
$this->redirect(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $this->record], $this->record->tenant));
|
|
|
|
return;
|
|
}
|
|
|
|
$this->resolutionCaseId = (int) $case->getKey();
|
|
$this->heading = $case->status === ReviewPublicationResolutionCaseStatus::ReadyToContinue->value
|
|
? 'Review is ready to return to publication'
|
|
: 'Review can\'t be published yet';
|
|
$this->subheading = $this->record->tenant?->getFilamentName();
|
|
}
|
|
|
|
public function getTitle(): string
|
|
{
|
|
return $this->pageOutcomeTitle();
|
|
}
|
|
|
|
public function getHeading(): string
|
|
{
|
|
return $this->pageOutcomeTitle();
|
|
}
|
|
|
|
protected function resolveRecord(int|string $key): Model
|
|
{
|
|
return EnvironmentReviewResource::resolveScopedRecordOrFail($key);
|
|
}
|
|
|
|
protected function getHeaderActions(): array
|
|
{
|
|
return array_values(array_filter([
|
|
$this->executeCurrentStepAction(),
|
|
Actions\Action::make('back_to_review')
|
|
->label('Back to review')
|
|
->icon('heroicon-o-arrow-left')
|
|
->color('gray')
|
|
->url(fn (): string => $this->reviewUrl()),
|
|
Actions\ActionGroup::make([
|
|
$this->cancelResolutionAction(),
|
|
])
|
|
->label('More')
|
|
->icon('heroicon-o-ellipsis-vertical')
|
|
->color('gray')
|
|
->visible(fn (): bool => $this->resolutionCase()?->statusEnum()->isActive() ?? false),
|
|
]));
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function caseState(): array
|
|
{
|
|
$case = $this->resolutionCase();
|
|
|
|
if (! $case instanceof ReviewPublicationResolutionCase) {
|
|
return [
|
|
'case' => null,
|
|
'steps' => [],
|
|
];
|
|
}
|
|
|
|
$case->loadMissing(['steps.operationRun', 'environmentReview.tenant', 'tenant']);
|
|
$tenant = $case->tenant;
|
|
$currentStep = $case->currentStep();
|
|
|
|
return [
|
|
'case' => [
|
|
'id' => (int) $case->getKey(),
|
|
'status' => (string) $case->status,
|
|
'status_label' => $this->statusLabel((string) $case->status),
|
|
'status_color' => $this->caseStatusColor((string) $case->status),
|
|
'current_step_key' => $case->current_step_key,
|
|
'summary' => is_array($case->summary) ? $case->summary : [],
|
|
'created_at' => $case->created_at?->diffForHumans(),
|
|
'last_evaluated_at' => $case->last_evaluated_at?->diffForHumans(),
|
|
],
|
|
'decision' => $this->decisionState($case, $currentStep),
|
|
'steps' => $case->steps
|
|
->map(fn (ReviewPublicationResolutionStep $step): array => $this->stepState($step, $tenant))
|
|
->values()
|
|
->all(),
|
|
];
|
|
}
|
|
|
|
private function executeCurrentStepAction(): Actions\Action
|
|
{
|
|
$action = Actions\Action::make('execute_current_step')
|
|
->label(fn (): string => $this->currentStepActionLabel())
|
|
->icon(fn (): string => $this->currentStepActionIcon())
|
|
->color('primary')
|
|
->visible(fn (): bool => $this->hasExecutableCurrentStep())
|
|
->disabled(fn (): bool => $this->currentStepIsRunning() || ! $this->canExecuteCurrentStep())
|
|
->tooltip(fn (): ?string => $this->currentStepActionTooltip())
|
|
->requiresConfirmation()
|
|
->modalHeading(fn (): string => $this->currentStepActionLabel())
|
|
->modalDescription(fn (): string => $this->currentStepConfirmationDescription())
|
|
->modalSubmitActionLabel('Continue')
|
|
->action(function (): void {
|
|
$case = $this->resolutionCase();
|
|
$user = auth()->user();
|
|
|
|
if (! $case instanceof ReviewPublicationResolutionCase || ! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
if (! $this->canExecuteCurrentStep()) {
|
|
abort(403);
|
|
}
|
|
|
|
$result = app(ReviewPublicationResolutionActionService::class)->executeCurrentStep($case, $user);
|
|
$updatedCase = $result['case'];
|
|
$this->resolutionCaseId = (int) $updatedCase->getKey();
|
|
|
|
if ($result['operation_run'] instanceof OperationRun && is_string($result['operation_type'])) {
|
|
OperationUxPresenter::queuedToast($result['operation_type'])->send();
|
|
|
|
return;
|
|
}
|
|
|
|
Notification::make()
|
|
->success()
|
|
->title('Resolution step completed')
|
|
->send();
|
|
|
|
if ($updatedCase->status === ReviewPublicationResolutionCaseStatus::Completed->value) {
|
|
$this->redirect($this->reviewUrl());
|
|
}
|
|
});
|
|
|
|
return $action;
|
|
}
|
|
|
|
private function cancelResolutionAction(): Actions\Action
|
|
{
|
|
return UiEnforcement::forAction(
|
|
Actions\Action::make('cancel_resolution')
|
|
->label('Cancel resolution')
|
|
->icon('heroicon-o-x-circle')
|
|
->color('danger')
|
|
->visible(fn (): bool => $this->resolutionCase()?->statusEnum()->isActive() ?? false)
|
|
->requiresConfirmation()
|
|
->modalHeading('Cancel publication resolution')
|
|
->modalDescription('This only cancels the TenantPilot resolution case. It does not modify the provider tenant or publish the review.')
|
|
->action(function (): void {
|
|
$case = $this->resolutionCase();
|
|
$user = auth()->user();
|
|
|
|
if (! $case instanceof ReviewPublicationResolutionCase || ! $user instanceof User) {
|
|
abort(403);
|
|
}
|
|
|
|
app(ReviewPublicationResolutionService::class)->cancel($case, $user);
|
|
|
|
Notification::make()
|
|
->success()
|
|
->title('Resolution cancelled')
|
|
->send();
|
|
|
|
$this->redirect($this->reviewUrl());
|
|
}),
|
|
)
|
|
->requireCapability(Capabilities::ENVIRONMENT_REVIEW_MANAGE)
|
|
->preserveVisibility()
|
|
->apply();
|
|
}
|
|
|
|
private function authorizeAccess(): void
|
|
{
|
|
$tenant = EnvironmentReviewResource::panelTenantContext();
|
|
$record = $this->getRecord();
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User || ! $tenant instanceof ManagedEnvironment || ! $record instanceof EnvironmentReview) {
|
|
abort(404);
|
|
}
|
|
|
|
if ((int) $record->managed_environment_id !== (int) $tenant->getKey()) {
|
|
abort(404);
|
|
}
|
|
|
|
if (! $user->canAccessTenant($tenant)) {
|
|
abort(404);
|
|
}
|
|
|
|
if (! $user->can('view', $record)) {
|
|
abort(403);
|
|
}
|
|
}
|
|
|
|
private function resolutionCase(): ?ReviewPublicationResolutionCase
|
|
{
|
|
if (! is_numeric($this->resolutionCaseId)) {
|
|
return null;
|
|
}
|
|
|
|
$case = ReviewPublicationResolutionCase::query()
|
|
->with(['steps.operationRun', 'environmentReview.tenant', 'tenant'])
|
|
->find((int) $this->resolutionCaseId);
|
|
|
|
if (! $case instanceof ReviewPublicationResolutionCase) {
|
|
return null;
|
|
}
|
|
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User || ! $user->can('view', $case)) {
|
|
abort(403);
|
|
}
|
|
|
|
return $case;
|
|
}
|
|
|
|
private function pageOutcomeTitle(): string
|
|
{
|
|
$case = $this->resolutionCase();
|
|
|
|
if ($case instanceof ReviewPublicationResolutionCase
|
|
&& $case->status === ReviewPublicationResolutionCaseStatus::ReadyToContinue->value) {
|
|
return 'Review is ready to return to publication';
|
|
}
|
|
|
|
return 'Review can\'t be published yet';
|
|
}
|
|
|
|
private function currentStep(): ?ReviewPublicationResolutionStep
|
|
{
|
|
return $this->resolutionCase()?->currentStep();
|
|
}
|
|
|
|
private function hasExecutableCurrentStep(): bool
|
|
{
|
|
$step = $this->currentStep();
|
|
|
|
if (! $step instanceof ReviewPublicationResolutionStep) {
|
|
return false;
|
|
}
|
|
|
|
return $step->statusEnum() !== ReviewPublicationResolutionStepStatus::Pending;
|
|
}
|
|
|
|
private function currentStepIsRunning(): bool
|
|
{
|
|
return $this->currentStep()?->statusEnum() === ReviewPublicationResolutionStepStatus::Running;
|
|
}
|
|
|
|
private function canExecuteCurrentStep(): bool
|
|
{
|
|
$case = $this->resolutionCase();
|
|
$user = auth()->user();
|
|
|
|
if (! $case instanceof ReviewPublicationResolutionCase || ! $user instanceof User) {
|
|
return false;
|
|
}
|
|
|
|
return app(ReviewPublicationResolutionStepAuthorizer::class)->canExecuteCurrentStep($user, $case);
|
|
}
|
|
|
|
private function currentStepActionTooltip(): ?string
|
|
{
|
|
if ($this->currentStepIsRunning()) {
|
|
return 'The linked operation is still running.';
|
|
}
|
|
|
|
return $this->canExecuteCurrentStep() ? null : AuthUiTooltips::insufficientPermission();
|
|
}
|
|
|
|
private function currentStepActionLabel(): string
|
|
{
|
|
return $this->currentStepActionLabelFor($this->currentStep()?->stepKeyEnum());
|
|
}
|
|
|
|
private function currentStepActionLabelFor(?ReviewPublicationResolutionStepKey $stepKey): string
|
|
{
|
|
return match ($stepKey) {
|
|
ReviewPublicationResolutionStepKey::CompleteRequiredReports => 'Update required reports',
|
|
ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot => 'Collect evidence',
|
|
ReviewPublicationResolutionStepKey::RefreshReviewComposition => 'Refresh review',
|
|
ReviewPublicationResolutionStepKey::GenerateReviewPack => 'Generate review pack',
|
|
ReviewPublicationResolutionStepKey::ReturnToPublication => 'Return to publication',
|
|
default => 'Continue',
|
|
};
|
|
}
|
|
|
|
private function currentStepActionIcon(): string
|
|
{
|
|
return $this->currentStepActionIconFor($this->currentStep()?->stepKeyEnum());
|
|
}
|
|
|
|
private function currentStepActionIconFor(?ReviewPublicationResolutionStepKey $stepKey): string
|
|
{
|
|
return match ($stepKey) {
|
|
ReviewPublicationResolutionStepKey::CompleteRequiredReports,
|
|
ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot,
|
|
ReviewPublicationResolutionStepKey::RefreshReviewComposition => 'heroicon-o-arrow-path',
|
|
ReviewPublicationResolutionStepKey::GenerateReviewPack => 'heroicon-o-arrow-down-tray',
|
|
ReviewPublicationResolutionStepKey::ReturnToPublication => 'heroicon-o-check-badge',
|
|
default => 'heroicon-o-play',
|
|
};
|
|
}
|
|
|
|
private function reviewUrl(): string
|
|
{
|
|
$record = $this->getRecord();
|
|
|
|
if (! $record instanceof EnvironmentReview) {
|
|
return url('/admin');
|
|
}
|
|
|
|
return EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $record], $record->tenant);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function decisionState(ReviewPublicationResolutionCase $case, ?ReviewPublicationResolutionStep $currentStep): array
|
|
{
|
|
$summary = is_array($case->summary) ? $case->summary : [];
|
|
$missingReports = collect((array) ($summary['missing_report_dimensions'] ?? []))
|
|
->map(fn (mixed $dimension): string => $this->reportRequirementLabel((string) $dimension))
|
|
->filter(static fn (string $dimension): bool => $dimension !== '')
|
|
->values()
|
|
->all();
|
|
$blockers = collect((array) ($summary['publication_blockers'] ?? []))
|
|
->map(fn (mixed $blocker): string => $this->operatorBlockerLabel((string) $blocker))
|
|
->filter(static fn (string $blocker): bool => $blocker !== '')
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
$blockerCount = max((int) ($summary['publication_blocker_count'] ?? 0), count($blockers), count($missingReports));
|
|
$stepKey = $currentStep?->stepKeyEnum();
|
|
$readyToReturn = (string) $case->status === ReviewPublicationResolutionCaseStatus::ReadyToContinue->value
|
|
|| $stepKey === ReviewPublicationResolutionStepKey::ReturnToPublication;
|
|
|
|
return [
|
|
'headline' => $readyToReturn ? 'Review is ready to return to publication' : 'Review can\'t be published yet',
|
|
'status_badge_label' => $readyToReturn ? 'Ready to continue' : 'Publication blocked',
|
|
'status_badge_color' => $readyToReturn ? 'success' : 'warning',
|
|
'blocked_summary' => $this->blockedSummary($blockerCount, count($missingReports)),
|
|
'blockers' => $blockers,
|
|
'missing_reports' => $missingReports,
|
|
'next_action_label' => $this->currentStepActionLabelFor($stepKey),
|
|
'next_action_description' => $this->nextStepDescription($stepKey),
|
|
'after_this' => $this->afterNextStepDescription($stepKey),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function stepState(ReviewPublicationResolutionStep $step, ?ManagedEnvironment $tenant): array
|
|
{
|
|
$operationRun = $step->operationRun;
|
|
$stepKey = $step->stepKeyEnum();
|
|
|
|
return [
|
|
'key' => (string) $step->step_key,
|
|
'label' => (string) data_get($step->summary, 'label', $step->step_key),
|
|
'operator_label' => $this->operatorStepLabel($stepKey),
|
|
'description' => (string) data_get($step->summary, 'description', ''),
|
|
'operator_description' => $this->operatorStepDescription($stepKey),
|
|
'state_description' => (string) data_get($step->summary, 'state_description', ''),
|
|
'status' => (string) $step->status,
|
|
'status_label' => $this->statusLabel((string) $step->status),
|
|
'status_color' => $this->stepStatusColor((string) $step->status),
|
|
'proof_type' => $step->proof_type,
|
|
'proof_id' => $step->proof_id,
|
|
'proof_status' => $step->proof_status,
|
|
'proof_url' => $this->proofUrl($step, $tenant),
|
|
'operation_run_id' => $operationRun instanceof OperationRun ? (int) $operationRun->getKey() : null,
|
|
'operation_url' => $operationRun instanceof OperationRun && $tenant instanceof ManagedEnvironment
|
|
? OperationRunLinks::view($operationRun, $tenant)
|
|
: null,
|
|
];
|
|
}
|
|
|
|
private function proofUrl(ReviewPublicationResolutionStep $step, ?ManagedEnvironment $tenant): ?string
|
|
{
|
|
if (! $tenant instanceof ManagedEnvironment || ! is_numeric($step->proof_id) || ! is_string($step->proof_type)) {
|
|
return null;
|
|
}
|
|
|
|
return match ($step->proof_type) {
|
|
'environment_review' => EnvironmentReviewResource::environmentScopedUrl('view', ['record' => (int) $step->proof_id], $tenant),
|
|
'evidence_snapshot' => EvidenceSnapshotResource::getUrl('view', ['record' => (int) $step->proof_id], tenant: $tenant),
|
|
'review_pack' => ReviewPackResource::getUrl('view', ['record' => (int) $step->proof_id], tenant: $tenant),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
private function statusLabel(string $status): string
|
|
{
|
|
return str($status)->replace('_', ' ')->title()->toString();
|
|
}
|
|
|
|
private function caseStatusColor(string $status): string
|
|
{
|
|
return match ($status) {
|
|
ReviewPublicationResolutionCaseStatus::Completed->value => 'success',
|
|
ReviewPublicationResolutionCaseStatus::Blocked->value => 'danger',
|
|
ReviewPublicationResolutionCaseStatus::WaitingForRun->value => 'info',
|
|
ReviewPublicationResolutionCaseStatus::ReadyToContinue->value => 'warning',
|
|
ReviewPublicationResolutionCaseStatus::Cancelled->value,
|
|
ReviewPublicationResolutionCaseStatus::Superseded->value => 'gray',
|
|
default => 'primary',
|
|
};
|
|
}
|
|
|
|
private function stepStatusColor(string $status): string
|
|
{
|
|
return match ($status) {
|
|
ReviewPublicationResolutionStepStatus::Completed->value => 'success',
|
|
ReviewPublicationResolutionStepStatus::Failed->value => 'danger',
|
|
ReviewPublicationResolutionStepStatus::Running->value => 'info',
|
|
ReviewPublicationResolutionStepStatus::Actionable->value => 'warning',
|
|
ReviewPublicationResolutionStepStatus::Superseded->value => 'gray',
|
|
default => 'gray',
|
|
};
|
|
}
|
|
|
|
private function reportRequirementLabel(string $dimension): string
|
|
{
|
|
return match ($dimension) {
|
|
'permission_posture' => 'Permission posture',
|
|
'entra_admin_roles' => 'Entra admin roles',
|
|
default => str($dimension)->replace('_', ' ')->title()->toString(),
|
|
};
|
|
}
|
|
|
|
private function nextStepDescription(?ReviewPublicationResolutionStepKey $stepKey): string
|
|
{
|
|
return match ($stepKey) {
|
|
ReviewPublicationResolutionStepKey::CompleteRequiredReports => 'TenantPilot will update the missing required reports. It will not publish the review.',
|
|
ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot => 'This will collect a current evidence snapshot for the review. It will not publish the review.',
|
|
ReviewPublicationResolutionStepKey::RefreshReviewComposition => 'This will refresh the review from current evidence. It will not publish the review.',
|
|
ReviewPublicationResolutionStepKey::GenerateReviewPack => 'This will generate the review pack needed for customer-ready output. It will not publish the review.',
|
|
ReviewPublicationResolutionStepKey::ReturnToPublication => 'All checks are resolved. Return to the review and use the existing publish action when you are ready.',
|
|
default => 'TenantPilot will continue the next safe preparation step. It will not publish the review.',
|
|
};
|
|
}
|
|
|
|
private function afterNextStepDescription(?ReviewPublicationResolutionStepKey $stepKey): string
|
|
{
|
|
return match ($stepKey) {
|
|
ReviewPublicationResolutionStepKey::ReturnToPublication => 'Publishing remains a separate action on the review page.',
|
|
default => 'After this, TenantPilot re-checks the evidence, refreshes the review if needed, generates the review pack if needed, and sends you back to the review. Publishing remains a separate action.',
|
|
};
|
|
}
|
|
|
|
private function currentStepConfirmationDescription(): string
|
|
{
|
|
return match ($this->currentStep()?->stepKeyEnum()) {
|
|
ReviewPublicationResolutionStepKey::CompleteRequiredReports => 'TenantPilot will update the missing required reports. This will not publish the review.',
|
|
ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot => 'TenantPilot will collect a current evidence snapshot for this review. This will not publish the review.',
|
|
ReviewPublicationResolutionStepKey::RefreshReviewComposition => 'TenantPilot will refresh the review from current evidence. This will not publish the review.',
|
|
ReviewPublicationResolutionStepKey::GenerateReviewPack => 'TenantPilot will generate the review pack needed for customer-ready output. This will not publish the review.',
|
|
ReviewPublicationResolutionStepKey::ReturnToPublication => 'TenantPilot will return you to the review. Publishing remains a separate action.',
|
|
default => 'TenantPilot will continue the next safe preparation step. This will not publish the review.',
|
|
};
|
|
}
|
|
|
|
private function blockedSummary(int $blockerCount, int $missingReportCount): string
|
|
{
|
|
if ($missingReportCount > 0) {
|
|
return sprintf(
|
|
'TenantPilot found %d required %s that must be updated before this review can become customer-ready.',
|
|
$missingReportCount,
|
|
$missingReportCount === 1 ? 'report' : 'reports',
|
|
);
|
|
}
|
|
|
|
if ($blockerCount > 0) {
|
|
return sprintf(
|
|
'TenantPilot found %d missing %s before this review can become customer-ready.',
|
|
$blockerCount,
|
|
$blockerCount === 1 ? 'requirement' : 'requirements',
|
|
);
|
|
}
|
|
|
|
return 'TenantPilot is checking the remaining publication prerequisites before this review returns to the publish action.';
|
|
}
|
|
|
|
private function operatorBlockerLabel(string $blocker): string
|
|
{
|
|
$blocker = trim($blocker);
|
|
|
|
if ($blocker === '') {
|
|
return '';
|
|
}
|
|
|
|
if (str($blocker)->lower()->contains('report-backed evidence')) {
|
|
return 'Required reports are missing.';
|
|
}
|
|
|
|
return $blocker;
|
|
}
|
|
|
|
private function operatorStepLabel(?ReviewPublicationResolutionStepKey $stepKey): string
|
|
{
|
|
return match ($stepKey) {
|
|
ReviewPublicationResolutionStepKey::ValidateReviewReadiness => 'Check readiness',
|
|
ReviewPublicationResolutionStepKey::CompleteRequiredReports => 'Update required reports',
|
|
ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot => 'Collect evidence snapshot',
|
|
ReviewPublicationResolutionStepKey::RefreshReviewComposition => 'Refresh review',
|
|
ReviewPublicationResolutionStepKey::GenerateReviewPack => 'Generate review pack',
|
|
ReviewPublicationResolutionStepKey::ReturnToPublication => 'Return to publication',
|
|
default => 'Continue preparation',
|
|
};
|
|
}
|
|
|
|
private function operatorStepDescription(?ReviewPublicationResolutionStepKey $stepKey): string
|
|
{
|
|
return match ($stepKey) {
|
|
ReviewPublicationResolutionStepKey::ValidateReviewReadiness => 'TenantPilot checks whether the review is safe to prepare for publication.',
|
|
ReviewPublicationResolutionStepKey::CompleteRequiredReports => 'Required reports must be current before publication can continue.',
|
|
ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot => 'The review needs a complete and current evidence snapshot.',
|
|
ReviewPublicationResolutionStepKey::RefreshReviewComposition => 'The review is rebuilt from the latest evidence and report state.',
|
|
ReviewPublicationResolutionStepKey::GenerateReviewPack => 'A current review pack is prepared before returning to publication.',
|
|
ReviewPublicationResolutionStepKey::ReturnToPublication => 'Return to the review and decide whether to publish.',
|
|
default => 'TenantPilot prepares the next safe publication prerequisite.',
|
|
};
|
|
}
|
|
}
|