*/ public function generationDecision(ReviewPack $reviewPack): array { $readyReport = $this->findReadyReport($reviewPack); if ($readyReport instanceof StoredReport) { return [ 'state' => 'ready', 'is_blocked' => false, 'reason_code' => null, 'reason' => null, 'report_id' => (int) $readyReport->getKey(), ]; } $reviewPack->loadMissing(['tenant', 'environmentReview.currentExportReviewPack']); if ($reviewPack->status !== ReviewPackStatus::Ready->value) { return $this->blocked('review_pack_not_ready', 'Management PDF generation requires a ready Review Pack.'); } if ($reviewPack->expires_at !== null && $reviewPack->expires_at->isPast()) { return $this->blocked('review_pack_expired', 'Management PDF generation requires a non-expired Review Pack.'); } if (! filled($reviewPack->file_disk) || ! filled($reviewPack->file_path)) { return $this->blocked('source_artifact_missing', 'Management PDF generation requires the Review Pack artifact to be present.'); } if (! Storage::disk((string) $reviewPack->file_disk)->exists((string) $reviewPack->file_path)) { return $this->blocked('source_artifact_missing', 'Management PDF generation requires the Review Pack artifact to be present.'); } $review = $reviewPack->environmentReview; if (! $review || (int) ($review->current_export_review_pack_id ?? 0) !== (int) $reviewPack->getKey()) { return $this->blocked('review_pack_not_current', 'Management PDF generation requires the current Review Pack for the released review.'); } $activeReport = $this->findActiveReport($reviewPack); if ($activeReport instanceof StoredReport) { return [ 'state' => 'active', 'is_blocked' => false, 'reason_code' => null, 'reason' => null, 'report_id' => (int) $activeReport->getKey(), 'operation_run_id' => $activeReport->operation_run_id !== null ? (int) $activeReport->operation_run_id : null, ]; } $runtimeDecision = $this->runtimeGate->decision(); if ((bool) ($runtimeDecision['is_blocked'] ?? false)) { return [ 'state' => 'blocked', 'is_blocked' => true, 'reason_code' => $runtimeDecision['reason_code'] ?? 'runtime_blocked', 'reason' => $runtimeDecision['reason'] ?? 'Management PDF generation is blocked by runtime configuration.', 'runtime' => $runtimeDecision, ]; } $disclosureDecision = ManagementReportPdfPayloadBuilder::customerExecutiveDisclosureDecision($reviewPack); if ((bool) ($disclosureDecision['is_blocked'] ?? false)) { return [ 'state' => 'blocked', 'is_blocked' => true, 'reason_code' => $disclosureDecision['reason_code'] ?? 'disclosure_blocked', 'reason' => $disclosureDecision['reason'] ?? 'Management PDF generation is blocked by the customer-facing disclosure policy.', 'runtime' => $runtimeDecision, 'disclosure' => $disclosureDecision['disclosure'] ?? [], 'readiness' => $disclosureDecision['readiness'] ?? [], ]; } return [ 'state' => 'available', 'is_blocked' => false, 'reason_code' => null, 'reason' => null, 'runtime' => $runtimeDecision, 'disclosure' => $disclosureDecision['disclosure'] ?? [], ]; } /** * @return array{mode:string, report:?StoredReport, operation_run:?OperationRun, decision:array} */ public function startGeneration(ReviewPack $reviewPack, User $actor): array { $reviewPack->loadMissing(['tenant.workspace', 'environmentReview']); $tenant = $reviewPack->tenant; if (! $tenant instanceof ManagedEnvironment || ! $actor->canAccessTenant($tenant)) { throw new NotFoundHttpException; } abort_unless($actor->can(Capabilities::REVIEW_PACK_MANAGE, $tenant), 403); $readyReport = $this->findReadyReport($reviewPack); if ($readyReport instanceof StoredReport) { return [ 'mode' => 'ready', 'report' => $readyReport, 'operation_run' => $readyReport->operationRun, 'decision' => $this->generationDecision($reviewPack), ]; } $decision = $this->generationDecision($reviewPack); if ((bool) ($decision['is_blocked'] ?? false)) { $this->logLifecycleEvent( reviewPack: $reviewPack, actor: $actor, action: AuditActionId::ManagementReportPdfGenerationBlocked, status: 'blocked', mode: 'blocked', operationRun: null, report: null, context: ['decision' => $decision], ); return [ 'mode' => 'blocked', 'report' => null, 'operation_run' => null, 'decision' => $decision, ]; } $activeReport = $this->findActiveReport($reviewPack); if ($activeReport instanceof StoredReport) { return [ 'mode' => 'active', 'report' => $activeReport, 'operation_run' => $activeReport->operationRun, 'decision' => $decision, ]; } $fingerprint = $this->computeFingerprint($reviewPack); $retryableReport = $this->findRetryableReport($reviewPack, $fingerprint); $operationRun = $this->operationRunService->ensureRunWithIdentity( tenant: $tenant, type: OperationRunType::ManagementReportGenerate->value, identityInputs: [ 'source_review_pack_id' => (int) $reviewPack->getKey(), 'source_review_pack_fingerprint' => (string) $reviewPack->fingerprint, 'profile' => ReportProfileRegistry::CUSTOMER_EXECUTIVE, 'report_type' => StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF, ], context: [ 'source_review_pack_id' => (int) $reviewPack->getKey(), 'source_environment_review_id' => $reviewPack->environment_review_id !== null ? (int) $reviewPack->environment_review_id : null, 'profile' => ReportProfileRegistry::CUSTOMER_EXECUTIVE, 'report_type' => StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF, 'runtime_gate' => $decision['runtime'] ?? [], ], initiator: $actor, ); $existingRunReport = $this->findReportForRun($operationRun); $report = $existingRunReport ?? $retryableReport ?? StoredReport::create([ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'source_environment_review_id' => $reviewPack->environment_review_id !== null ? (int) $reviewPack->environment_review_id : null, 'source_review_pack_id' => (int) $reviewPack->getKey(), 'operation_run_id' => (int) $operationRun->getKey(), 'generated_by_user_id' => (int) $actor->getKey(), 'report_type' => StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF, 'report_format' => StoredReport::FORMAT_PDF, 'status' => StoredReport::STATUS_QUEUED, 'profile' => ReportProfileRegistry::CUSTOMER_EXECUTIVE, 'fingerprint' => $fingerprint, 'payload' => [ 'state' => StoredReport::STATUS_QUEUED, 'source_review_pack_id' => (int) $reviewPack->getKey(), 'source_environment_review_id' => $reviewPack->environment_review_id !== null ? (int) $reviewPack->environment_review_id : null, 'profile' => ReportProfileRegistry::CUSTOMER_EXECUTIVE, ], ]); if ($report->operation_run_id !== (int) $operationRun->getKey() || $report->status === StoredReport::STATUS_FAILED ) { $report->forceFill([ 'operation_run_id' => (int) $operationRun->getKey(), 'generated_by_user_id' => (int) $actor->getKey(), 'status' => StoredReport::STATUS_QUEUED, 'file_disk' => null, 'file_path' => null, 'file_size' => null, 'sha256' => null, 'generated_at' => null, 'payload' => [ 'state' => StoredReport::STATUS_QUEUED, 'source_review_pack_id' => (int) $reviewPack->getKey(), 'source_environment_review_id' => $reviewPack->environment_review_id !== null ? (int) $reviewPack->environment_review_id : null, 'profile' => ReportProfileRegistry::CUSTOMER_EXECUTIVE, 'retry_of_failed_report' => true, ], ])->save(); } $shouldDispatch = $operationRun->wasRecentlyCreated || ! $existingRunReport instanceof StoredReport; if ($shouldDispatch) { $this->operationRunService->dispatchOrFail($operationRun, function () use ($report, $operationRun): void { GenerateManagementReportPdfJob::dispatch( storedReportId: (int) $report->getKey(), operationRunId: (int) $operationRun->getKey(), ); }); } $this->logLifecycleEvent( reviewPack: $reviewPack, actor: $actor, action: AuditActionId::ManagementReportPdfGenerationRequested, status: 'queued', mode: $shouldDispatch ? 'queued' : 'reused_active_run', operationRun: $operationRun, report: $report, ); return [ 'mode' => $shouldDispatch ? 'queued' : 'active', 'report' => $report, 'operation_run' => $operationRun, 'decision' => $decision, ]; } /** * @param array $parameters */ public function generateDownloadUrl(StoredReport $report, array $parameters = []): string { $ttlMinutes = (int) config('tenantpilot.review_pack.download_url_ttl_minutes', 60); return URL::signedRoute( 'admin.management-report-pdfs.download', array_merge(['storedReport' => $report->getKey()], $parameters), now()->addMinutes($ttlMinutes), ); } public function findReadyReport(ReviewPack $reviewPack): ?StoredReport { return StoredReport::query() ->where('source_review_pack_id', (int) $reviewPack->getKey()) ->where('report_type', StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF) ->where('report_format', StoredReport::FORMAT_PDF) ->where('profile', ReportProfileRegistry::CUSTOMER_EXECUTIVE) ->where('status', StoredReport::STATUS_READY) ->whereNotNull('file_disk') ->whereNotNull('file_path') ->latest('generated_at') ->latest('id') ->first(); } public function findActiveReport(ReviewPack $reviewPack): ?StoredReport { return StoredReport::query() ->with('operationRun') ->where('source_review_pack_id', (int) $reviewPack->getKey()) ->where('report_type', StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF) ->where('report_format', StoredReport::FORMAT_PDF) ->where('profile', ReportProfileRegistry::CUSTOMER_EXECUTIVE) ->whereIn('status', [StoredReport::STATUS_QUEUED, StoredReport::STATUS_GENERATING]) ->whereHas('operationRun', static function ($query): void { $query->whereIn('status', [OperationRunStatus::Queued->value, OperationRunStatus::Running->value]); }) ->latest('id') ->first(); } public function findReportForRun(OperationRun $operationRun): ?StoredReport { return StoredReport::query() ->where('operation_run_id', (int) $operationRun->getKey()) ->where('report_type', StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF) ->latest('id') ->first(); } private function findRetryableReport(ReviewPack $reviewPack, string $fingerprint): ?StoredReport { return StoredReport::query() ->where('managed_environment_id', (int) $reviewPack->managed_environment_id) ->where('source_review_pack_id', (int) $reviewPack->getKey()) ->where('report_type', StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF) ->where('report_format', StoredReport::FORMAT_PDF) ->where('profile', ReportProfileRegistry::CUSTOMER_EXECUTIVE) ->where('fingerprint', $fingerprint) ->where('status', StoredReport::STATUS_FAILED) ->latest('id') ->first(); } public function computeFingerprint(ReviewPack $reviewPack): string { return hash('sha256', json_encode([ 'source_review_pack_id' => (int) $reviewPack->getKey(), 'source_review_pack_fingerprint' => (string) $reviewPack->fingerprint, 'source_environment_review_id' => $reviewPack->environment_review_id !== null ? (int) $reviewPack->environment_review_id : null, 'profile' => ReportProfileRegistry::CUSTOMER_EXECUTIVE, 'report_type' => StoredReport::REPORT_TYPE_MANAGEMENT_REPORT_PDF, 'report_format' => StoredReport::FORMAT_PDF, ], JSON_THROW_ON_ERROR)); } /** * @return array */ private function blocked(string $reasonCode, string $reason): array { return [ 'state' => 'blocked', 'is_blocked' => true, 'reason_code' => $reasonCode, 'reason' => $reason, ]; } /** * @param array $context */ private function logLifecycleEvent( ReviewPack $reviewPack, User $actor, AuditActionId $action, string $status, string $mode, ?OperationRun $operationRun, ?StoredReport $report, array $context = [], ): void { $tenant = $reviewPack->tenant; if (! $tenant instanceof ManagedEnvironment) { return; } $this->auditLogger->log( workspace: $tenant->workspace, action: $action, context: array_replace_recursive([ 'metadata' => [ 'mode' => $mode, 'review_pack_id' => (int) $reviewPack->getKey(), 'environment_review_id' => $reviewPack->environment_review_id !== null ? (int) $reviewPack->environment_review_id : null, 'stored_report_id' => $report instanceof StoredReport ? (int) $report->getKey() : null, 'profile' => ReportProfileRegistry::CUSTOMER_EXECUTIVE, ], ], $context), actor: $actor, status: $status, resourceType: 'stored_report', resourceId: $report instanceof StoredReport ? (string) $report->getKey() : 'review_pack:'.$reviewPack->getKey(), targetLabel: sprintf('Management report PDF for review pack #%d', (int) $reviewPack->getKey()), operationRunId: $operationRun instanceof OperationRun ? (int) $operationRun->getKey() : null, tenant: $tenant, ); } }