|null */ public function scanContext(User $actor, Opportunity $record): ?array { $attributes = $record->getAttributes(); if (! array_key_exists('organization_id', $attributes) || ! array_key_exists('workflow_context', $attributes) || ! in_array($record->opportunity_category, OpportunityManagement::CATEGORIES, true)) { return null; } return ['opportunity_id' => $record->id, 'category' => $record->opportunity_category, 'organization_id' => $record->organization_id, 'observed_version' => (int) $record->management_version, 'observed_content_revision' => $record->workflow_context['content_revision'] ?? null, 'observed_state' => app(PortalProfileAccess::class)->state($record), 'reported_for_user_id' => $actor->id]; } /** The queue executes as a system process; the initiating account is not impersonated. * @param array $context * @return array{reference:string,recorded:bool} */ public function recordScan(array $context, string $reference, string $errorCode): array { try { if (! Str::isUuid($reference) || ! in_array($errorCode, self::SCAN_ERRORS, true) || ! is_int($context['opportunity_id'] ?? null) || $context['opportunity_id'] <= 0 || ! in_array($context['category'] ?? null, OpportunityManagement::CATEGORIES, true) || ! array_key_exists('organization_id', $context) || ($context['organization_id'] !== null && (! is_string($context['organization_id']) || ! Str::isUuid($context['organization_id']))) || ! is_int($context['reported_for_user_id'] ?? null) || $context['reported_for_user_id'] <= 0 || ! is_int($context['observed_version'] ?? null) || $context['observed_version'] < 0 || ! is_string($context['observed_state'] ?? null)) { throw new \LogicException('Invalid internal scan context.'); } $metadata = ['reference' => $reference, 'opportunity_id' => $context['opportunity_id'], 'category' => $context['category'], 'organization_id' => $context['organization_id'], 'operation' => 'scan_document', 'attempted_etag' => '', 'observed_version' => $context['observed_version'], 'observed_content_revision' => $context['observed_content_revision'] ?? null, 'observed_state' => $context['observed_state'], 'error_code' => $errorCode, 'origin' => 'scan_system', 'reported_for_user_id' => $context['reported_for_user_id']]; $recorded = DB::transaction(function () use ($reference, $metadata) { DB::table('audit_events')->insertOrIgnore(['id' => $reference, 'correlation_id' => $reference, 'source_system' => self::SOURCE, 'actor_user_id' => null, 'entity_type' => 'opportunity-technical-incident', 'entity_id' => $reference, 'action' => 'workflow_failed', 'data_classification' => 'restricted', 'after_data' => json_encode(['incident' => $metadata], JSON_THROW_ON_ERROR)]); $event = DB::table('audit_events')->where('id', $reference)->first(); $old = $event ? json_decode($event->after_data, true, 512, JSON_THROW_ON_ERROR)['incident'] ?? null : null; return $event && $event->source_system === self::SOURCE && $event->entity_type === 'opportunity-technical-incident' && $event->action === 'workflow_failed' && $event->actor_user_id === null && is_array($old) && ($old['origin'] ?? null) === 'scan_system' && ($old['operation'] ?? null) === 'scan_document' && ($old['opportunity_id'] ?? null) === $metadata['opportunity_id'] && ($old['organization_id'] ?? null) === $metadata['organization_id'] && ($old['category'] ?? null) === $metadata['category'] && ($old['reported_for_user_id'] ?? null) === $metadata['reported_for_user_id']; }); if (! $recorded) { throw new \LogicException('Scan incident reference belongs to another event.'); } return ['reference' => $reference, 'recorded' => true]; } catch (Throwable) { try { Log::warning('Opportunity scan incident could not be stored.', ['reference' => $reference, 'error_code' => in_array($errorCode, self::SCAN_ERRORS, true) ? $errorCode : 'scan_worker_failed']); } catch (Throwable) { /* Preserve the original worker/dispatch outcome. */ } return ['reference' => $reference, 'recorded' => false]; } } /** Called after the business transaction has unwound. * @return array{reference:string,recorded:bool} */ public function record(User $actor, Opportunity $before, string $action, string $attemptedEtag, Throwable $failure): array { $reference = (string) Str::uuid(); $metadata = ['reference' => $reference, 'opportunity_id' => $before->id, 'category' => $before->opportunity_category, 'organization_id' => $before->organization_id, 'operation' => $action, 'attempted_etag' => $attemptedEtag, 'observed_version' => (int) $before->management_version, 'observed_content_revision' => $before->workflow_context['content_revision'] ?? null, 'observed_state' => app(PortalProfileAccess::class)->state($before), 'error_code' => ($failure instanceof QueryException || $failure instanceof PDOException) ? 'database_failure' : 'workflow_unavailable']; try { DB::transaction(fn () => DB::table('audit_events')->insert(['id' => $reference, 'correlation_id' => $reference, 'source_system' => self::SOURCE, 'actor_user_id' => $actor->id, 'entity_type' => 'opportunity-technical-incident', 'entity_id' => $reference, 'action' => 'workflow_failed', 'data_classification' => 'restricted', 'after_data' => json_encode(['incident' => $metadata], JSON_THROW_ON_ERROR)])); return ['reference' => $reference, 'recorded' => true]; } catch (Throwable) { // Database failure must not hide the original result or expose its message/SQL. try { Log::warning('Opportunity technical incident could not be stored.', ['reference' => $reference, 'error_code' => $metadata['error_code']]); } catch (Throwable) { /* The response still gives the correlation reference. */ } return ['reference' => $reference, 'recorded' => false]; } } /** Current access and actor ownership are both required, even after revocation. * @return list */ public function recent(User $actor, Opportunity $record): array { app(PortalProfileAccess::class)->authorize($actor, 'view', $record); $result = []; foreach (DB::table('audit_events')->where('source_system', self::SOURCE)->where('entity_type', 'opportunity-technical-incident')->whereRaw("after_data->'incident'->>'opportunity_id' = ?", [(string) $record->id]) ->whereRaw("COALESCE(after_data->'incident'->>'organization_id','') = ?", [$record->organization_id ?? '']) ->whereRaw("COALESCE(after_data->'incident'->>'category','') = ?", [$record->opportunity_category ?? '']) ->where(function ($query) use ($actor) { $query->where('actor_user_id', $actor->id)->orWhere(function ($system) use ($actor) { $system->whereNull('actor_user_id')->whereRaw("after_data->'incident'->>'origin' = 'scan_system'")->whereRaw("after_data->'incident'->>'operation' = 'scan_document'") ->whereRaw("after_data->'incident'->>'reported_for_user_id' = ?", [(string) $actor->id]); }); })->where('action', 'workflow_failed')->orderByDesc('occurred_at')->orderByDesc('id')->limit(10)->get(['id', 'occurred_at', 'after_data']) as $event) { $data = json_decode($event->after_data, true)['incident'] ?? null; if (! is_array($data) || ($data['reference'] ?? null) !== $event->id || ! in_array($data['operation'] ?? null, self::OPERATIONS, true) || ! is_int($data['observed_version'] ?? null) || ! in_array($data['error_code'] ?? null, ['database_failure', 'workflow_unavailable', ...self::SCAN_ERRORS], true) || ! is_string($event->occurred_at)) { continue; } $state = app(TechnicalIncidentHandling::class)->summaryFor($event->id); $result[] = ['reference' => $event->id, 'operation' => $data['operation'], 'version' => $data['observed_version'], 'occurred_at' => $event->occurred_at, 'error_code' => $data['error_code'], 'handling' => $state === null ? null : ['version' => $state['version'], 'status' => $state['status'], 'updated_at' => $state['updated_at']]]; } return $result; } }