$options */ public function generate(Tenant $tenant, User $user, array $options = []): ReviewPack { $options = $this->normalizeOptions($options); $fingerprint = $this->computeFingerprint($tenant, $options); $existing = $this->findExistingPack($tenant, $fingerprint); if ($existing instanceof ReviewPack) { return $existing; } $operationRun = $this->operationRunService->ensureRun( tenant: $tenant, type: OperationRunType::ReviewPackGenerate->value, inputs: [ 'include_pii' => $options['include_pii'], 'include_operations' => $options['include_operations'], ], initiator: $user, ); $reviewPack = ReviewPack::create([ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'operation_run_id' => (int) $operationRun->getKey(), 'initiated_by_user_id' => (int) $user->getKey(), 'status' => ReviewPackStatus::Queued->value, 'options' => $options, 'summary' => [], ]); $this->operationRunService->dispatchOrFail($operationRun, function () use ($reviewPack, $operationRun): void { GenerateReviewPackJob::dispatch( reviewPackId: (int) $reviewPack->getKey(), operationRunId: (int) $operationRun->getKey(), ); }); return $reviewPack; } /** * Compute a deterministic fingerprint for deduplication. * * @param array $options */ public function computeFingerprint(Tenant $tenant, array $options): string { $reportFingerprints = StoredReport::query() ->where('tenant_id', (int) $tenant->getKey()) ->whereIn('report_type', [ StoredReport::REPORT_TYPE_PERMISSION_POSTURE, StoredReport::REPORT_TYPE_ENTRA_ADMIN_ROLES, ]) ->orderBy('report_type') ->pluck('fingerprint') ->toArray(); $maxFindingDate = Finding::query() ->where('tenant_id', (int) $tenant->getKey()) ->whereIn('status', [Finding::STATUS_NEW, Finding::STATUS_ACKNOWLEDGED]) ->max('updated_at'); $data = [ 'tenant_id' => (int) $tenant->getKey(), 'include_pii' => (bool) ($options['include_pii'] ?? true), 'include_operations' => (bool) ($options['include_operations'] ?? true), 'report_fingerprints' => $reportFingerprints, 'max_finding_date' => $maxFindingDate, 'rbac_last_checked_at' => $tenant->rbac_last_checked_at?->toIso8601String(), ]; return hash('sha256', json_encode($data, JSON_THROW_ON_ERROR)); } /** * Generate a signed download URL for a review pack. */ public function generateDownloadUrl(ReviewPack $pack): string { $ttlMinutes = (int) config('tenantpilot.review_pack.download_url_ttl_minutes', 60); return URL::signedRoute( 'admin.review-packs.download', ['reviewPack' => $pack->getKey()], now()->addMinutes($ttlMinutes), ); } /** * Find an existing ready, non-expired pack with the same fingerprint. */ public function findExistingPack(Tenant $tenant, string $fingerprint): ?ReviewPack { return ReviewPack::query() ->forTenant((int) $tenant->getKey()) ->ready() ->where('fingerprint', $fingerprint) ->where('expires_at', '>', now()) ->first(); } /** * Check if a generation run is currently active for this tenant. */ public function checkActiveRun(Tenant $tenant): bool { return OperationRun::query() ->where('tenant_id', (int) $tenant->getKey()) ->where('type', OperationRunType::ReviewPackGenerate->value) ->active() ->exists(); } /** * @param array $options * @return array{include_pii: bool, include_operations: bool} */ private function normalizeOptions(array $options): array { return [ 'include_pii' => (bool) ($options['include_pii'] ?? config('tenantpilot.review_pack.include_pii_default', true)), 'include_operations' => (bool) ($options['include_operations'] ?? config('tenantpilot.review_pack.include_operations_default', true)), ]; } }