table()) ->where('visitor_id', $visitorId) ->where('session_id', $sessionId) ->where('module', $module) ->first(); } /** * Idempotent: returns existing id if a row already exists for this triple. */ public function start(string $visitorId, string $sessionId, string $module): string { $existing = $this->findByVisitorSessionModule($visitorId, $sessionId, $module); if ($existing) { return $existing->id; } $id = $this->ids->generate(); $now = Carbon::now()->toDateTimeString(); DB::table($this->table())->insert([ 'id' => $id, 'visitor_id' => $visitorId, 'session_id' => $sessionId, 'module' => $module, 'started_at' => $now, 'last_step_at' => $now, 'highest_step' => 0, 'submitted' => 0, 'abandoned' => 0, 'created_at' => $now, 'updated_at' => $now, ]); return $id; } /** * Advance highest_step monotonically. No-op if step is not higher than current value. */ public function advanceStep(string $id, int $step): void { $now = Carbon::now()->toDateTimeString(); DB::table($this->table()) ->where('id', $id) ->where('highest_step', '<', $step) ->update([ 'highest_step' => $step, 'last_step_at' => $now, 'updated_at' => $now, ]); } public function markSubmitted(string $id, string $applicationId, bool $feeWaived): void { $now = Carbon::now()->toDateTimeString(); DB::table($this->table())->where('id', $id)->update([ 'submitted' => 1, 'submitted_at' => $now, 'application_id' => $applicationId, 'fee_waived' => $feeWaived ? 1 : 0, 'abandoned' => 0, 'abandoned_at' => null, 'updated_at' => $now, ]); } public function markAbandoned(string $id): void { $now = Carbon::now()->toDateTimeString(); DB::table($this->table()) ->where('id', $id) ->where('submitted', 0) ->update([ 'abandoned' => 1, 'abandoned_at' => $now, 'updated_at' => $now, ]); } }