$data */ public function checkIn(string $ownerRef, Organization $organization, array $data, ?string $actorRef = null): Visit { $data = $this->processMediaFields($data); $visitorType = $data['visitor_type'] ?? 'visitor'; if (! empty($data['type_fields'])) { $data = $this->visitorTypes->mergeTypeDetailsFromArray($visitorType, $data, $organization); } $visitor = $this->resolveVisitor($ownerRef, $organization, $data); $this->watchlist->assertCanCheckIn($visitor, $organization, $actorRef); $needsApproval = $this->visitorTypes->requiresApproval($visitorType, $organization) || $this->watchlist->visitorNeedsApprovalQueue($visitor); if ($needsApproval) { $data = $this->watchlist->applyApprovalMetadata($data, $visitor); } return DB::transaction(function () use ($ownerRef, $organization, $data, $actorRef, $visitor, $visitorType, $needsApproval) { $duration = (int) ($data['expected_duration_minutes'] ?? config('frontdesk.kiosk.default_visit_duration_minutes', 60)); $expiryHours = $this->visitorTypes->badgeExpiryHours($visitorType, $organization); $visit = Visit::create([ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, 'branch_id' => $data['branch_id'] ?? null, 'reception_desk_id' => $data['reception_desk_id'] ?? null, 'visitor_id' => $visitor->id, 'host_id' => $data['host_id'] ?? null, 'visitor_type' => $visitorType, 'status' => $needsApproval ? Visit::STATUS_WAITING : Visit::STATUS_CHECKED_IN, 'purpose' => $data['purpose'] ?? null, 'expected_duration_minutes' => $duration, 'scheduled_at' => $data['scheduled_at'] ?? null, 'checked_in_at' => $needsApproval ? null : now(), 'badge_expires_at' => $needsApproval ? null : now()->addHours($expiryHours), 'photo_path' => $data['photo_path'] ?? null, 'signature_path' => $data['signature_path'] ?? null, 'policies_accepted' => (bool) ($data['policies_accepted'] ?? false), 'vehicle_info' => $data['vehicle_info'] ?? null, 'contractor_details' => $data['contractor_details'] ?? null, 'delivery_details' => $data['delivery_details'] ?? null, 'allowed_areas' => $data['allowed_areas'] ?? null, 'notes' => $data['notes'] ?? null, 'checked_in_by' => $needsApproval ? null : $actorRef, 'external_ref' => $data['external_ref'] ?? null, 'source' => $data['source'] ?? null, 'integration_metadata' => $data['integration_metadata'] ?? null, ]); if (! $needsApproval) { $this->finalizeCheckIn($visit, $actorRef); } else { AuditLog::record( $ownerRef, 'visit.awaiting_approval', $organization->id, $actorRef, Visit::class, $visit->id, ['visitor' => $visitor->full_name, 'visitor_type' => $visitorType], ); $this->notifications->approvalNeeded($visit); if ($this->watchlist->visitorNeedsApprovalQueue($visitor)) { $this->watchlist->recordFlaggedCheckIn($visitor, $organization, $actorRef, [ 'visit_id' => $visit->id, ]); } } return $visit->load(['visitor', 'host', 'organization']); }); } public function activateCheckIn(Visit $visit, ?string $actorRef = null, bool $policiesAccepted = true): Visit { return DB::transaction(function () use ($visit, $actorRef, $policiesAccepted) { $visit->load(['visitor', 'organization']); $expiryHours = $this->visitorTypes->badgeExpiryHours($visit->visitor_type, $visit->organization); $contractorDetails = $visit->contractor_details ?? []; $deliveryDetails = $visit->delivery_details ?? []; unset($contractorDetails['_awaiting_approval'], $deliveryDetails['_awaiting_approval']); $visit->update([ 'status' => Visit::STATUS_CHECKED_IN, 'checked_in_at' => now(), 'badge_expires_at' => now()->addHours($expiryHours), 'policies_accepted' => $policiesAccepted, 'checked_in_by' => $actorRef, 'contractor_details' => $contractorDetails ?: null, 'delivery_details' => $deliveryDetails ?: null, ]); $this->finalizeCheckIn($visit, $actorRef); return $visit->fresh(['visitor', 'host', 'organization']); }); } protected function finalizeCheckIn(Visit $visit, ?string $actorRef): void { $visitor = $visit->visitor; $visitor->increment('visit_count'); if ($visitor->visit_count >= 5) { $visitor->update(['is_frequent' => true]); } AuditLog::record( $visit->owner_ref, 'visit.checked_in', $visit->organization_id, $actorRef, Visit::class, $visit->id, ['visitor' => $visitor->full_name, 'badge_code' => $visit->badge_code], ); $this->notifications->visitorArrived($visit); $this->webhooks->dispatch('visit.checked_in', $visit); } /** @param array $data */ protected function processMediaFields(array $data): array { foreach (['photo_data' => 'photo_path', 'signature_data' => 'signature_path'] as $input => $target) { if (! empty($data[$input])) { $data[$target] = $this->visitorTypes->storeBase64Image($data[$input], $input === 'photo_data' ? 'photos' : 'signatures'); unset($data[$input]); } elseif (! empty($data[$target]) && is_string($data[$target]) && str_starts_with($data[$target], 'data:image')) { $data[$target] = $this->visitorTypes->storeBase64Image( $data[$target], $target === 'photo_path' ? 'photos' : 'signatures', ); } } return $data; } /** * @param array $data */ public function resolveVisitorForSchedule(string $ownerRef, Organization $organization, array $data): Visitor { return $this->resolveVisitor($ownerRef, $organization, $data); } /** * @param array $data */ protected function resolveVisitor(string $ownerRef, Organization $organization, array $data): Visitor { if (! empty($data['visitor_id'])) { return Visitor::owned($ownerRef)->findOrFail($data['visitor_id']); } $existing = Visitor::owned($ownerRef) ->where('organization_id', $organization->id) ->where(function ($q) use ($data) { if (! empty($data['email'])) { $q->where('email', $data['email']); } if (! empty($data['phone'])) { $q->orWhere('phone', $data['phone']); } }) ->when(empty($data['email']) && empty($data['phone']), fn ($q) => $q->whereRaw('0 = 1')) ->first(); if ($existing) { $existing->update(array_filter([ 'full_name' => $data['full_name'] ?? $existing->full_name, 'company' => $data['company'] ?? $existing->company, 'phone' => $data['phone'] ?? $existing->phone, 'email' => $data['email'] ?? $existing->email, 'photo_path' => $data['photo_path'] ?? $existing->photo_path, ])); return $existing->fresh(); } return Visitor::create([ 'owner_ref' => $ownerRef, 'organization_id' => $organization->id, 'full_name' => $data['full_name'], 'company' => $data['company'] ?? null, 'phone' => $data['phone'] ?? null, 'email' => $data['email'] ?? null, 'photo_path' => $data['photo_path'] ?? null, ]); } }