Implemented the first version of the PDF and HTML renderer for review packs. Added ReviewPackRenderedReportController and related blade views to render reports. Updated EnvironmentReviewResource, ReviewPackResource, ReviewPackService, and routing. Added new tests for the renderer and download actions, and updated UI documentation. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #427
585 lines
27 KiB
PHP
585 lines
27 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
|
use App\Filament\Resources\EnvironmentReviewResource;
|
|
use App\Filament\Resources\ReviewPackResource;
|
|
use App\Models\EnvironmentReview;
|
|
use App\Models\EnvironmentReviewSection;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\ReviewPack;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Services\ReviewPackService;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance;
|
|
use App\Support\ReviewPackStatus;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\Support\Str;
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
|
|
class ReviewPackRenderedReportController extends Controller
|
|
{
|
|
public function __invoke(Request $request, ReviewPack $reviewPack): Response
|
|
{
|
|
$reviewPack->loadMissing([
|
|
'tenant.workspace',
|
|
'environmentReview.evidenceSnapshot',
|
|
'environmentReview.currentExportReviewPack',
|
|
'environmentReview.sections',
|
|
]);
|
|
|
|
$user = $request->user();
|
|
$tenant = $reviewPack->tenant;
|
|
$review = $reviewPack->environmentReview;
|
|
|
|
if (! $user instanceof User || ! $tenant instanceof ManagedEnvironment || ! $review instanceof EnvironmentReview) {
|
|
throw new NotFoundHttpException;
|
|
}
|
|
|
|
if (! $user->canAccessTenant($tenant)) {
|
|
throw new NotFoundHttpException;
|
|
}
|
|
|
|
if (! $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant)) {
|
|
abort(403);
|
|
}
|
|
|
|
if ($reviewPack->status !== ReviewPackStatus::Ready->value) {
|
|
throw new NotFoundHttpException;
|
|
}
|
|
|
|
if ($reviewPack->expires_at !== null && $reviewPack->expires_at->isPast()) {
|
|
throw new NotFoundHttpException;
|
|
}
|
|
|
|
if ((int) $review->managed_environment_id !== (int) $tenant->getKey()) {
|
|
throw new NotFoundHttpException;
|
|
}
|
|
|
|
if ((int) ($review->current_export_review_pack_id ?? 0) !== (int) $reviewPack->getKey()) {
|
|
throw new NotFoundHttpException;
|
|
}
|
|
|
|
return response()->view('review-packs.rendered-report', [
|
|
'report' => $this->reportState($request, $reviewPack, $review, $tenant),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function reportState(
|
|
Request $request,
|
|
ReviewPack $reviewPack,
|
|
EnvironmentReview $review,
|
|
ManagedEnvironment $tenant,
|
|
): array {
|
|
$summary = is_array($reviewPack->summary) ? $reviewPack->summary : [];
|
|
$governancePackage = is_array($summary['governance_package'] ?? null) ? $summary['governance_package'] : [];
|
|
$controlInterpretation = is_array($summary['control_interpretation'] ?? null) ? $summary['control_interpretation'] : [];
|
|
$downloadUrl = $this->downloadUrl($request, $reviewPack, $review);
|
|
$reviewUrl = $this->reviewUrl($request, $review, $tenant);
|
|
$reviewPackUrl = $this->reviewPackUrl($request, $reviewPack, $tenant);
|
|
$readiness = ReviewPackOutputResolutionGuidance::readinessForReview($review);
|
|
$guidance = ReviewPackOutputResolutionGuidance::fromReadiness($readiness, [
|
|
'download' => $downloadUrl,
|
|
'review' => $reviewUrl,
|
|
]);
|
|
$decisionSummary = is_array($governancePackage['decision_summary'] ?? null)
|
|
? $governancePackage['decision_summary']
|
|
: [];
|
|
$state = (string) ($guidance['state'] ?? ReviewPackOutputResolutionGuidance::STATE_UNKNOWN);
|
|
$limitations = $this->managementLimitations(is_array($guidance['limitations'] ?? null) ? $guidance['limitations'] : []);
|
|
$evidenceBasis = $this->evidenceBasisState($review);
|
|
|
|
return [
|
|
'title' => __('localization.review.rendered_report'),
|
|
'guidance' => $guidance,
|
|
'state' => $state,
|
|
'hero' => $this->heroState($state, $guidance),
|
|
'branding' => $this->brandingState($tenant),
|
|
'tenant_name' => $tenant->name,
|
|
'review_id' => (int) $review->getKey(),
|
|
'pack_id' => (int) $reviewPack->getKey(),
|
|
'review_status' => (string) $review->status,
|
|
'review_completeness_state' => (string) $review->completeness_state,
|
|
'review_status_label' => Str::headline((string) $review->status),
|
|
'review_completeness_label' => Str::headline((string) $review->completeness_state),
|
|
'generated_at' => $reviewPack->generated_at,
|
|
'published_at' => $review->published_at,
|
|
'download_url' => $downloadUrl,
|
|
'download_label' => $this->downloadLabel($state),
|
|
'review_url' => $reviewUrl,
|
|
'review_pack_url' => $reviewPackUrl,
|
|
'html_only' => true,
|
|
'executive_summary' => $this->plainText(
|
|
$governancePackage['executive_summary'] ?? null,
|
|
__('localization.review.rendered_report_summary_fallback'),
|
|
),
|
|
'management_summary' => $this->managementSummary(
|
|
state: $state,
|
|
guidance: $guidance,
|
|
limitations: $limitations,
|
|
storedExecutiveSummary: $governancePackage['executive_summary'] ?? null,
|
|
),
|
|
'evidence_basis' => $evidenceBasis,
|
|
'evidence_basis_summary' => $evidenceBasis['description'],
|
|
'limitations' => $limitations,
|
|
'top_findings' => is_array($governancePackage['top_findings'] ?? null) ? $governancePackage['top_findings'] : [],
|
|
'accepted_risks' => $this->acceptedRiskItems(
|
|
is_array($governancePackage['accepted_risks'] ?? null) ? $governancePackage['accepted_risks'] : [],
|
|
$state !== ReviewPackOutputResolutionGuidance::STATE_INTERNAL_ONLY,
|
|
),
|
|
'decision_summary' => $decisionSummary,
|
|
'governance_decisions' => is_array($decisionSummary['entries'] ?? null)
|
|
? $decisionSummary['entries']
|
|
: (is_array($governancePackage['governance_decisions'] ?? null) ? $governancePackage['governance_decisions'] : []),
|
|
'next_actions' => is_array($summary['recommended_next_actions'] ?? null) ? $summary['recommended_next_actions'] : [],
|
|
'non_certification_disclosure' => $this->plainText(
|
|
$controlInterpretation['non_certification_disclosure'] ?? null,
|
|
__('localization.review.non_certification_disclosure_text'),
|
|
),
|
|
'section_appendix' => $this->sectionAppendix($reviewPack, $review),
|
|
'technical_details' => is_array($guidance['technical_details'] ?? null) ? $guidance['technical_details'] : [],
|
|
'entrypoint_file' => (string) data_get($summary, 'delivery_bundle.executive_entrypoint_file', ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME),
|
|
'appendix_files' => is_array(data_get($summary, 'delivery_bundle.appendix_files')) ? data_get($summary, 'delivery_bundle.appendix_files') : [],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{prepared_by:string,prepared_for:string,generated_by:string}
|
|
*/
|
|
private function brandingState(ManagedEnvironment $tenant): array
|
|
{
|
|
$workspace = $tenant->workspace;
|
|
|
|
return [
|
|
'prepared_by' => $workspace instanceof Workspace && filled($workspace->name)
|
|
? (string) $workspace->name
|
|
: 'TenantPilot',
|
|
'prepared_for' => filled($tenant->name) ? (string) $tenant->name : __('localization.review.tenant'),
|
|
'generated_by' => 'TenantPilot',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $guidance
|
|
* @return array{title:string,badge:string,summary:string,warning:?string,color:string,customer_safe:bool}
|
|
*/
|
|
private function heroState(string $state, array $guidance): array
|
|
{
|
|
$customerSafe = $state === ReviewPackOutputResolutionGuidance::STATE_CUSTOMER_SAFE_READY;
|
|
|
|
return [
|
|
'title' => match ($state) {
|
|
ReviewPackOutputResolutionGuidance::STATE_CUSTOMER_SAFE_READY => __('localization.review.report_state_customer_safe_ready'),
|
|
ReviewPackOutputResolutionGuidance::STATE_PUBLISHED_WITH_LIMITATIONS => __('localization.review.report_state_with_limitations'),
|
|
ReviewPackOutputResolutionGuidance::STATE_INTERNAL_ONLY => __('localization.review.report_state_internal_with_limitations'),
|
|
default => __('localization.review.report_state_not_customer_ready'),
|
|
},
|
|
'badge' => $customerSafe
|
|
? __('localization.review.customer_safe')
|
|
: (string) ($guidance['boundary_label'] ?? __('localization.review.requires_review')),
|
|
'summary' => match ($state) {
|
|
ReviewPackOutputResolutionGuidance::STATE_CUSTOMER_SAFE_READY => __('localization.review.report_state_customer_safe_ready_summary'),
|
|
ReviewPackOutputResolutionGuidance::STATE_INTERNAL_ONLY => __('localization.review.report_state_internal_summary'),
|
|
ReviewPackOutputResolutionGuidance::STATE_PUBLISHED_WITH_LIMITATIONS => __('localization.review.report_state_limitations_summary'),
|
|
default => __('localization.review.report_state_not_ready_summary'),
|
|
},
|
|
'warning' => $customerSafe ? null : __('localization.review.report_external_sharing_warning'),
|
|
'color' => $customerSafe ? 'success' : ($state === ReviewPackOutputResolutionGuidance::STATE_PUBLICATION_BLOCKED ? 'danger' : 'warning'),
|
|
'customer_safe' => $customerSafe,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $guidance
|
|
* @param list<array<string, string>> $limitations
|
|
* @return array{overall_state:string,reason:string,impact:string,next_action:string,top_limitations:list<string>}
|
|
*/
|
|
private function managementSummary(string $state, array $guidance, array $limitations, mixed $storedExecutiveSummary): array
|
|
{
|
|
$topLimitations = collect($limitations)
|
|
->pluck('summary')
|
|
->filter(static fn (mixed $summary): bool => is_string($summary) && trim($summary) !== '')
|
|
->take(3)
|
|
->values()
|
|
->all();
|
|
|
|
return [
|
|
'overall_state' => (string) ($this->heroState($state, $guidance)['title']),
|
|
'reason' => $this->managementReason($state, $guidance, $storedExecutiveSummary),
|
|
'impact' => $this->plainText($guidance['impact'] ?? null, __('localization.review.report_summary_default_impact')),
|
|
'next_action' => $this->managementNextAction($state, $limitations),
|
|
'top_limitations' => $topLimitations,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $guidance
|
|
*/
|
|
private function managementReason(string $state, array $guidance, mixed $storedExecutiveSummary): string
|
|
{
|
|
if ($state === ReviewPackOutputResolutionGuidance::STATE_CUSTOMER_SAFE_READY) {
|
|
return $this->plainText($storedExecutiveSummary, __('localization.review.report_summary_ready_reason'));
|
|
}
|
|
|
|
return $this->plainText($guidance['primary_reason'] ?? null, __('localization.review.report_summary_limited_reason'));
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, string>> $limitations
|
|
*/
|
|
private function managementNextAction(string $state, array $limitations): string
|
|
{
|
|
$firstKey = $limitations[0]['key'] ?? null;
|
|
|
|
return match (true) {
|
|
$state === ReviewPackOutputResolutionGuidance::STATE_CUSTOMER_SAFE_READY => __('localization.review.report_next_action_ready'),
|
|
$state === ReviewPackOutputResolutionGuidance::STATE_INTERNAL_ONLY => __('localization.review.report_next_action_internal'),
|
|
$firstKey === 'evidence_basis_missing',
|
|
$firstKey === 'evidence_basis_stale',
|
|
$firstKey === 'evidence_basis_incomplete' => __('localization.review.report_next_action_evidence'),
|
|
$firstKey === 'required_sections_incomplete' => __('localization.review.report_next_action_sections'),
|
|
$firstKey === 'publish_blockers_present' => __('localization.review.report_next_action_blockers'),
|
|
default => __('localization.review.report_next_action_limited'),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $limitations
|
|
* @return list<array{key:string,title:string,summary:string,next_action:string,severity:string}>
|
|
*/
|
|
private function managementLimitations(array $limitations): array
|
|
{
|
|
return collect($limitations)
|
|
->map(function (array $limitation): array {
|
|
$key = (string) ($limitation['key'] ?? 'unknown');
|
|
|
|
return [
|
|
'key' => $key,
|
|
'title' => $this->plainText($limitation['label'] ?? null, __('localization.review.output_limitations')),
|
|
'summary' => match ($key) {
|
|
'evidence_basis_missing',
|
|
'evidence_basis_stale',
|
|
'evidence_basis_incomplete' => __('localization.review.report_limitation_evidence_summary'),
|
|
'required_sections_incomplete' => __('localization.review.report_limitation_sections_summary'),
|
|
'contains_pii' => __('localization.review.report_limitation_pii_summary'),
|
|
'publish_blockers_present' => __('localization.review.report_limitation_blockers_summary'),
|
|
'export_not_ready' => __('localization.review.report_limitation_export_summary'),
|
|
'disclosure_missing' => __('localization.review.report_limitation_disclosure_summary'),
|
|
default => $this->plainText($limitation['reason'] ?? null, __('localization.review.report_limitation_default_summary')),
|
|
},
|
|
'next_action' => match ($key) {
|
|
'evidence_basis_missing',
|
|
'evidence_basis_stale',
|
|
'evidence_basis_incomplete' => __('localization.review.report_next_action_evidence'),
|
|
'required_sections_incomplete' => __('localization.review.report_next_action_sections'),
|
|
'contains_pii' => __('localization.review.report_next_action_internal'),
|
|
'publish_blockers_present' => __('localization.review.report_next_action_blockers'),
|
|
default => __('localization.review.report_next_action_limited'),
|
|
},
|
|
'severity' => (string) ($limitation['severity'] ?? 'warning'),
|
|
];
|
|
})
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @return array{label:string,description:string,operator_action:string,snapshot_label:string}
|
|
*/
|
|
private function evidenceBasisState(EnvironmentReview $review): array
|
|
{
|
|
$snapshot = $review->evidenceSnapshot;
|
|
$state = (string) ($snapshot?->completeness_state ?? 'missing');
|
|
$snapshotLabel = $snapshot instanceof \App\Models\EvidenceSnapshot
|
|
? __('localization.review.evidence_snapshot_number', ['id' => (int) $snapshot->getKey()])
|
|
: __('localization.review.evidence_snapshot_missing');
|
|
|
|
return [
|
|
'label' => Str::headline($state),
|
|
'description' => match ($state) {
|
|
'complete' => __('localization.review.report_evidence_complete_description', ['snapshot' => $snapshotLabel]),
|
|
'stale' => __('localization.review.report_evidence_stale_description', ['snapshot' => $snapshotLabel]),
|
|
'partial' => __('localization.review.report_evidence_partial_description', ['snapshot' => $snapshotLabel]),
|
|
default => __('localization.review.report_evidence_missing_description'),
|
|
},
|
|
'operator_action' => match ($state) {
|
|
'complete' => __('localization.review.report_evidence_complete_action'),
|
|
default => __('localization.review.report_next_action_evidence'),
|
|
},
|
|
'snapshot_label' => $snapshotLabel,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $acceptedRisks
|
|
* @return list<array{title:string,status:string,summary:?string,limitation:?string,owner:?string,review_state:string}>
|
|
*/
|
|
private function acceptedRiskItems(array $acceptedRisks, bool $showOwner): array
|
|
{
|
|
return collect($acceptedRisks)
|
|
->filter(static fn (mixed $risk): bool => is_array($risk))
|
|
->map(function (array $risk) use ($showOwner): array {
|
|
$customerSafeSummary = $this->plainText(
|
|
$risk['customer_safe_summary'] ?? ($risk['customer_summary'] ?? null),
|
|
'',
|
|
);
|
|
$status = $this->acceptedRiskStatusLabel((string) ($risk['governance_state'] ?? ($risk['status'] ?? 'unknown')));
|
|
$owner = $showOwner
|
|
? $this->plainText($risk['owner_label'] ?? data_get($risk, 'owner.name'), '')
|
|
: '';
|
|
|
|
return [
|
|
'title' => $this->plainText($risk['title'] ?? null, __('localization.review.accepted_risks')),
|
|
'status' => $status,
|
|
'summary' => $customerSafeSummary !== '' ? $customerSafeSummary : null,
|
|
'limitation' => $customerSafeSummary === '' ? __('localization.review.accepted_risk_customer_safe_summary_missing') : null,
|
|
'owner' => $owner !== '' ? $owner : null,
|
|
'review_state' => $this->acceptedRiskReviewState($risk),
|
|
];
|
|
})
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
private function acceptedRiskStatusLabel(string $state): string
|
|
{
|
|
return match ($state) {
|
|
'valid_exception' => __('localization.review.accepted_risk_state_current'),
|
|
'expiring_exception' => __('localization.review.accepted_risks_expiring_soon'),
|
|
'expired_exception' => __('localization.review.accepted_risks_expired'),
|
|
'revoked_exception', 'rejected_exception' => __('localization.review.accepted_risks_needs_review'),
|
|
'risk_accepted_without_valid_exception' => __('localization.review.accepted_risks_needs_review'),
|
|
default => Str::headline($state),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $risk
|
|
*/
|
|
private function acceptedRiskReviewState(array $risk): string
|
|
{
|
|
$reviewDueAt = $this->plainText($risk['review_due_at'] ?? null, '');
|
|
$expiresAt = $this->plainText($risk['expires_at'] ?? null, '');
|
|
|
|
if ($reviewDueAt !== '') {
|
|
return __('localization.review.accepted_risk_review_due_on', ['date' => $reviewDueAt]);
|
|
}
|
|
|
|
if ($expiresAt !== '') {
|
|
return __('localization.review.accepted_risk_expires_on', ['date' => $expiresAt]);
|
|
}
|
|
|
|
return __('localization.review.review_date_not_recorded');
|
|
}
|
|
|
|
private function downloadLabel(string $state): string
|
|
{
|
|
return match ($state) {
|
|
ReviewPackOutputResolutionGuidance::STATE_CUSTOMER_SAFE_READY => __('localization.review.download_customer_safe_review_pack'),
|
|
ReviewPackOutputResolutionGuidance::STATE_INTERNAL_ONLY => __('localization.review.download_internal_review_pack'),
|
|
default => __('localization.review.download_review_pack_with_limitations'),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
private function sectionAppendix(ReviewPack $reviewPack, EnvironmentReview $review): array
|
|
{
|
|
$includeOperations = (bool) (($reviewPack->options ?? [])['include_operations'] ?? true);
|
|
|
|
return $review->sections
|
|
->filter(fn (EnvironmentReviewSection $section): bool => $includeOperations || $section->section_key !== 'operations_health')
|
|
->sortBy('sort_order')
|
|
->values()
|
|
->map(function (EnvironmentReviewSection $section): array {
|
|
$summaryPayload = is_array($section->summary_payload) ? $section->summary_payload : [];
|
|
$renderPayload = is_array($section->render_payload) ? $section->render_payload : [];
|
|
|
|
return [
|
|
'title' => (string) $section->title,
|
|
'completeness_label' => Str::headline((string) $section->completeness_state),
|
|
'summary_items' => collect($summaryPayload)
|
|
->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()
|
|
->take(6)
|
|
->values()
|
|
->all(),
|
|
'highlights' => collect($renderPayload['highlights'] ?? [])
|
|
->filter(fn (mixed $value): bool => is_string($value) && trim($value) !== '')
|
|
->take(5)
|
|
->values()
|
|
->all(),
|
|
'entries' => $this->sectionEntries($renderPayload, $section->isControlInterpretation()),
|
|
'next_actions' => collect($renderPayload['next_actions'] ?? [])
|
|
->filter(fn (mixed $value): bool => is_string($value) && trim($value) !== '')
|
|
->take(4)
|
|
->values()
|
|
->all(),
|
|
'disclosure' => is_string($renderPayload['disclosure'] ?? null) ? $renderPayload['disclosure'] : null,
|
|
];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $renderPayload
|
|
* @return list<array{title:string,summary:?string}>
|
|
*/
|
|
private function sectionEntries(array $renderPayload, bool $isControlInterpretation): array
|
|
{
|
|
return collect($renderPayload['entries'] ?? [])
|
|
->filter(fn (mixed $entry): bool => is_array($entry))
|
|
->take($isControlInterpretation ? 3 : 4)
|
|
->map(function (array $entry) use ($isControlInterpretation): array {
|
|
if ($isControlInterpretation) {
|
|
$basisItems = collect($entry['evidence_basis_items'] ?? [])
|
|
->filter(fn (mixed $value): bool => is_string($value) && trim($value) !== '')
|
|
->take(2)
|
|
->values()
|
|
->all();
|
|
|
|
$summary = $this->plainText($entry['explanation_text'] ?? null, '');
|
|
|
|
if ($summary === '' && $basisItems !== []) {
|
|
$summary = implode(' ', $basisItems);
|
|
}
|
|
|
|
return [
|
|
'title' => $this->plainText($entry['control_name'] ?? ($entry['title'] ?? null), __('localization.review.control')),
|
|
'summary' => $summary !== '' ? $summary : null,
|
|
];
|
|
}
|
|
|
|
$detailParts = collect([
|
|
$entry['summary'] ?? null,
|
|
isset($entry['severity']) ? Str::headline((string) $entry['severity']) : null,
|
|
isset($entry['status']) ? Str::headline((string) $entry['status']) : null,
|
|
isset($entry['outcome']) ? Str::headline((string) $entry['outcome']) : null,
|
|
])
|
|
->filter(fn (mixed $value): bool => is_string($value) && trim($value) !== '')
|
|
->values()
|
|
->all();
|
|
|
|
return [
|
|
'title' => $this->plainText($entry['title'] ?? ($entry['displayName'] ?? null), __('localization.review.entry')),
|
|
'summary' => $detailParts === [] ? null : implode(' · ', $detailParts),
|
|
];
|
|
})
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
private function downloadUrl(Request $request, ReviewPack $reviewPack, EnvironmentReview $review): ?string
|
|
{
|
|
if (! filled($reviewPack->file_path) || ! filled($reviewPack->file_disk)) {
|
|
return null;
|
|
}
|
|
|
|
return app(ReviewPackService::class)->generateDownloadUrl(
|
|
$reviewPack,
|
|
$this->routeParameters($request, $review),
|
|
);
|
|
}
|
|
|
|
private function reviewUrl(Request $request, EnvironmentReview $review, ManagedEnvironment $tenant): string
|
|
{
|
|
$url = EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant);
|
|
|
|
if (! $request->boolean(CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY)) {
|
|
return $url;
|
|
}
|
|
|
|
return $this->appendQuery($url, [
|
|
CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1,
|
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
|
'tenant_filter_id' => $request->query('tenant_filter_id'),
|
|
]);
|
|
}
|
|
|
|
private function reviewPackUrl(Request $request, ReviewPack $reviewPack, ManagedEnvironment $tenant): string
|
|
{
|
|
$url = ReviewPackResource::getUrl('view', ['record' => $reviewPack], tenant: $tenant, panel: 'admin');
|
|
|
|
if ((string) $request->query('source_surface') !== CustomerReviewWorkspace::SOURCE_SURFACE) {
|
|
return $url;
|
|
}
|
|
|
|
return $this->appendQuery($url, [
|
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
|
'tenant_filter_id' => $request->query('tenant_filter_id'),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, scalar|null>
|
|
*/
|
|
private function routeParameters(Request $request, EnvironmentReview $review): array
|
|
{
|
|
$parameters = [
|
|
'source_surface' => is_string($request->query('source_surface'))
|
|
? (string) $request->query('source_surface')
|
|
: 'review_pack',
|
|
'review_id' => (int) $review->getKey(),
|
|
'tenant_filter_id' => $request->query('tenant_filter_id'),
|
|
'interpretation_version' => $review->controlInterpretationVersion(),
|
|
];
|
|
|
|
if ($request->boolean(CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY)) {
|
|
$parameters[CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY] = 1;
|
|
}
|
|
|
|
return array_filter(
|
|
$parameters,
|
|
static fn (mixed $value): bool => $value !== null && $value !== '',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, scalar|null> $query
|
|
*/
|
|
private function appendQuery(string $url, array $query): string
|
|
{
|
|
if ($query === []) {
|
|
return $url;
|
|
}
|
|
|
|
return $url.(str_contains($url, '?') ? '&' : '?').http_build_query(array_filter(
|
|
$query,
|
|
static fn (mixed $value): bool => $value !== null && $value !== '',
|
|
));
|
|
}
|
|
|
|
private function plainText(mixed $value, string $fallback): string
|
|
{
|
|
if (! is_scalar($value) && $value !== null) {
|
|
return $fallback;
|
|
}
|
|
|
|
$text = preg_replace('/\s+/', ' ', trim((string) $value));
|
|
|
|
if (! is_string($text) || $text === '') {
|
|
return $fallback;
|
|
}
|
|
|
|
return str_starts_with($text, 'localization.') ? $fallback : $text;
|
|
}
|
|
}
|