isInside(), 422, 'Visitor is not currently checked in.'); $visit->update([ 'status' => Visit::STATUS_CHECKED_OUT, 'checked_out_at' => now(), 'checked_out_by' => $actorRef, ]); AuditLog::record( $visit->owner_ref, 'visit.checked_out', $visit->organization_id, $actorRef, Visit::class, $visit->id, ['visitor' => $visit->visitor->full_name], ); $this->notifications->visitorCheckedOut($visit); $this->webhooks->dispatch('visit.checked_out', $visit->fresh(['visitor', 'host'])); return $visit->fresh(['visitor', 'host']); } public function checkOutByQrToken(string $ownerRef, string $qrToken, ?string $actorRef = null): Visit { $visit = Visit::owned($ownerRef) ->where('qr_token', $qrToken) ->currentlyInside() ->firstOrFail(); return $this->checkOut($visit, $actorRef); } public function checkOutFromKiosk( string $ownerRef, Organization $organization, ?string $badgeCode = null, ?string $qrInput = null, ?int $branchId = null, ): Visit { $visit = $this->resolveCheckedInVisitForKiosk($ownerRef, $organization, $badgeCode, $qrInput, $branchId); return $this->checkOut($visit); } protected function resolveCheckedInVisitForKiosk( string $ownerRef, Organization $organization, ?string $badgeCode, ?string $qrInput, ?int $branchId, ): Visit { $query = Visit::owned($ownerRef) ->where('organization_id', $organization->id) ->currentlyInside() ->with('visitor'); if ($branchId !== null) { $query->where(function ($q) use ($branchId) { $q->whereNull('branch_id')->orWhere('branch_id', $branchId); }); } $badgeCode = strtoupper(trim((string) $badgeCode)); if ($badgeCode !== '') { return (clone $query)->where('badge_code', $badgeCode)->firstOrFail(); } $qrToken = $this->parseVisitQrToken((string) $qrInput); abort_unless($qrToken !== null, 422, 'Invalid badge scan.'); return (clone $query)->where('qr_token', $qrToken)->firstOrFail(); } protected function parseVisitQrToken(string $input): ?string { $trimmed = trim($input); if ($trimmed === '') { return null; } if (preg_match('#/q/([A-Za-z0-9]+)#', $trimmed, $matches)) { return $matches[1]; } if (preg_match('/^[A-Za-z0-9]{16,64}$/', $trimmed)) { return $trimmed; } return null; } }