['closed'], ]; public function __construct(private readonly AnalyticsSessionRepository $sessions) {} /** * Transition a session from open → closed. * Silently skips if the session does not exist (e.g. already expired / beacon race). * * @param int $durationSeconds Wall-clock seconds from started_at to close; caller computes this. */ public function close(string $sessionId, string $exitPath, int $durationSeconds): void { $session = $this->sessions->findById($sessionId); if ($session === null) { return; } $this->guard($session->status, 'closed'); $this->sessions->close($sessionId, $exitPath, $durationSeconds); } private function guard(string $from, string $to): void { $allowed = self::ALLOWED[$from] ?? []; if (!in_array($to, $allowed, true)) { throw new InvalidTransitionException($from, $to); } } }