value, array $context = [] ): ?int { if (!$this->isEnabled()) { return null; } $eventType = $this->normalizeEventType($eventType); if ($eventType === '') { return null; } $outcome = $this->normalizeOutcome($outcome); $requestContext = RequestContext::context(); $session = is_array($_SESSION ?? null) ? $_SESSION : []; $fallbackActorUserId = !empty($session['user']['id']) ? (int) $session['user']['id'] : (ApiAuth::isAuthenticated() ? ApiAuth::userId() : 0); $fallbackActorTenantId = !empty($session['current_tenant']['id']) ? (int) $session['current_tenant']['id'] : (ApiAuth::isAuthenticated() ? (int) (ApiAuth::tenantId() ?? 0) : 0); $actorUserId = (int) ($context['actor_user_id'] ?? $fallbackActorUserId); $actorTenantId = (int) ($context['actor_tenant_id'] ?? $fallbackActorTenantId); $targetId = (int) ($context['target_id'] ?? 0); $targetType = trim((string) ($context['target_type'] ?? '')); $targetUuid = trim((string) ($context['target_uuid'] ?? '')); $errorCode = trim((string) ($context['error_code'] ?? '')); $metadata = is_array($context['metadata'] ?? null) ? $context['metadata'] : []; $before = is_array($context['before'] ?? null) ? $context['before'] : []; $after = is_array($context['after'] ?? null) ? $context['after'] : []; if ($before !== [] || $after !== []) { $metadata = [ ...$metadata, ...$this->systemAuditRedactionService->buildChangeSet($before, $after), ]; } $metadata = $this->systemAuditRedactionService->redactMetadata($metadata); $metadataJson = $metadata !== [] ? json_encode($metadata, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null; if (!is_string($metadataJson)) { $metadataJson = null; } $ip = trim((string) ($requestContext['ip'] ?? '')); $userAgent = trim((string) ($requestContext['user_agent'] ?? '')); $payload = [ 'event_uuid' => RepoQuery::uuidV4(), 'request_id' => $this->normalizeRequestId((string) ($requestContext['request_id'] ?? '')), 'channel' => $this->normalizeChannel((string) ($requestContext['channel'] ?? '')), 'event_type' => $eventType, 'outcome' => $outcome, 'error_code' => $errorCode !== '' ? substr($errorCode, 0, 100) : null, 'actor_user_id' => $actorUserId > 0 ? $actorUserId : null, 'actor_tenant_id' => $actorTenantId > 0 ? $actorTenantId : null, 'target_type' => $targetType !== '' ? substr($targetType, 0, 64) : null, 'target_id' => $targetId > 0 ? $targetId : null, 'target_uuid' => $targetUuid !== '' ? substr($targetUuid, 0, 36) : null, 'method' => $this->normalizeMethod((string) ($requestContext['method'] ?? '')), 'path' => $this->normalizePath((string) ($requestContext['path'] ?? '')), 'ip_hash' => $ip !== '' ? $this->systemAuditRedactionService->hashValue($ip) : null, 'user_agent_hash' => $userAgent !== '' ? $this->systemAuditRedactionService->hashValue($userAgent) : null, 'metadata_json' => $metadataJson, ]; try { $id = $this->systemAuditLogRepository->create($payload); return is_int($id) ? $id : null; } catch (\Throwable) { // Fail-open by design: audit logging must never break business flow. return null; } } public function listPaged(array $filters): array { return $this->systemAuditLogRepository->listPaged($filters); } public function find(int $id): ?array { return $this->systemAuditLogRepository->find($id); } public function filterOptions(int $limit = 200): array { return $this->systemAuditLogRepository->listFilterOptions($limit); } public function purgeExpired(): int { return $this->systemAuditLogRepository->purgeOlderThanDays($this->retentionDays()); } public function retentionDays(): int { $days = (int) $this->settingGateway->getSystemAuditRetentionDays(); if ($days < self::RETENTION_DAYS_MIN || $days > self::RETENTION_DAYS_MAX) { return self::RETENTION_DAYS_FALLBACK; } return $days; } public function isEnabled(): bool { return $this->settingGateway->isSystemAuditEnabled(); } private function normalizeEventType(string $eventType): string { $eventType = trim($eventType); if ($eventType === '') { return ''; } return substr($eventType, 0, 64); } private function normalizeOutcome(string $outcome): string { return SystemAuditOutcome::normalizeOr($outcome, SystemAuditOutcome::Success)->value; } private function normalizeChannel(string $channel): string { return SystemAuditChannel::normalizeOr($channel, SystemAuditChannel::Web)->value; } private function normalizeMethod(string $method): ?string { $method = strtoupper(trim($method)); if ($method === '') { return null; } return substr($method, 0, 8); } private function normalizePath(string $path): ?string { $path = trim($path); if ($path === '') { return null; } return substr($path, 0, 255); } private function normalizeRequestId(string $requestId): ?string { $requestId = strtolower(trim($requestId)); if ($requestId === '') { return null; } if (preg_match('/^[a-f0-9-]{36}$/', $requestId) !== 1) { return null; } return $requestId; } }