> */ public function rosterWithInviteState(QrCode $event): array { return collect((array) ($event->content()['speakers'] ?? [])) ->map(fn ($speaker) => $this->normalizeSpeakerRow(is_array($speaker) ? $speaker : [])) ->filter(fn ($speaker) => ($speaker['name'] ?? '') !== '') ->values() ->all(); } public function canSendManualInvite(QrCode $event, array $speaker): bool { if (($speaker['email'] ?? '') === '') { return false; } if (! $this->speakers->linkedProgramme($event)) { return true; } if (empty($speaker['invited_at'])) { return true; } return ($speaker['invite_source'] ?? '') === self::SOURCE_MANUAL; } public function sendManualInvite(QrCode $event, User $owner, string $email): bool { $email = strtolower(trim($email)); $speakers = $this->rosterWithInviteState($event); $index = collect($speakers)->search(fn ($row) => strcasecmp((string) ($row['email'] ?? ''), $email) === 0); if ($index === false) { return false; } $speaker = $speakers[$index]; if (! $this->canSendManualInvite($event, $speaker)) { return false; } return $this->deliverInvite($event, $owner, $speaker, self::SOURCE_MANUAL); } public function inviteProgrammeHosts(QrCode $programme, User $owner): int { if ($programme->type !== QrCode::TYPE_ITINERARY) { return 0; } $sent = 0; foreach ($this->programmes->eventsLinkedToProgramme($programme) as $event) { $sent += $this->inviteHostsForEvent($event->fresh(), $owner, $programme); } return $sent; } public function inviteHostsForEvent(QrCode $event, User $owner, ?QrCode $programme = null): int { $programme ??= $this->speakers->linkedProgramme($event); if (! $programme) { return 0; } $hostEmails = $this->hostSpeakerEmailsFromProgramme($programme); if ($hostEmails === []) { return 0; } $sent = 0; $speakers = $this->rosterWithInviteState($event); foreach ($speakers as $speaker) { $email = strtolower((string) ($speaker['email'] ?? '')); if ($email === '' || ! in_array($email, $hostEmails, true)) { continue; } if (($speaker['invite_source'] ?? '') === self::SOURCE_PROGRAMME && ! empty($speaker['invited_at'])) { continue; } if ($this->deliverInvite($event, $owner, $speaker, self::SOURCE_PROGRAMME)) { $sent++; } } return $sent; } /** @return array{event: QrCode, speaker: array}|null */ public function resolvePortal(string $shortCode, string $token): ?array { $token = trim($token); if ($token === '') { return null; } $event = QrCode::query() ->where('short_code', $shortCode) ->where('type', QrCode::TYPE_EVENT) ->where('is_active', true) ->first(); if (! $event) { return null; } foreach ($this->rosterWithInviteState($event) as $speaker) { if (hash_equals((string) ($speaker['invite_token'] ?? ''), $token)) { return ['event' => $event, 'speaker' => $speaker]; } } return null; } /** @return array|null */ public function verifyToken(QrCode $event, string $token): ?array { $token = trim($token); if ($token === '') { return null; } foreach ($this->rosterWithInviteState($event) as $speaker) { if (hash_equals((string) ($speaker['invite_token'] ?? ''), $token)) { return [ 'email' => $speaker['email'], 'name' => $speaker['name'], 'role' => $speaker['role'] ?? '', ]; } } return null; } public function portalUrl(QrCode $event, string $token): string { return route('qr.public.speaker.portal', [ 'shortCode' => $event->short_code, 'token' => $token, ]); } /** @param array $session */ public function speakerJoinUrl(QrCode $event, array $session, string $token): ?string { $joinUrl = trim((string) ($session['join_url'] ?? '')); if ($joinUrl === '') { return null; } $separator = str_contains($joinUrl, '?') ? '&' : '?'; return $joinUrl.$separator.'speaker='.urlencode($token); } /** @return list */ public function assignmentsForSpeaker(QrCode $event, array $speaker): array { $programme = $this->speakers->linkedProgramme($event); $snapshot = $this->programmes->snapshot($programme); if (! $snapshot) { return []; } $email = strtolower((string) ($speaker['email'] ?? '')); $name = trim((string) ($speaker['name'] ?? '')); $assignments = []; foreach ((array) ($snapshot['days'] ?? []) as $day) { $dayLabel = trim((string) ($day['date'] ?? $day['label'] ?? '')); foreach ((array) ($day['items'] ?? []) as $item) { if (! is_array($item)) { continue; } $hostEmail = strtolower(trim((string) ($item['host_speaker_email'] ?? ''))); $hostName = trim((string) ($item['host'] ?? '')); $matches = ($email !== '' && $hostEmail === $email) || ($name !== '' && strcasecmp($hostName, $name) === 0); if (! $matches) { continue; } $assignments[] = [ 'session' => trim((string) ($item['title'] ?? '')), 'day' => $dayLabel, 'time' => trim((string) ($item['time'] ?? '')), 'location' => trim((string) ($item['location'] ?? '')), ]; } } return $assignments; } /** @param array $speaker */ private function deliverInvite(QrCode $event, User $owner, array $speaker, string $source): bool { $email = trim((string) ($speaker['email'] ?? '')); if ($email === '') { return false; } $token = (string) ($speaker['invite_token'] ?? ''); if ($token === '') { $token = Str::random(48); } $eventName = (string) ($event->content()['name'] ?? $event->label); $portalUrl = $this->portalUrl($event, $token); $sent = $this->email->sendSpeakerInvite( $owner->public_id, $email, $eventName, $portalUrl, $speaker['name'] ?? null, ); if (! $sent) { return false; } $this->persistSpeakerInviteMeta($event, $email, $token, $source); return true; } private function persistSpeakerInviteMeta(QrCode $event, string $email, string $token, string $source): void { $email = strtolower(trim($email)); $content = $event->content(); $speakers = []; foreach ((array) ($content['speakers'] ?? []) as $row) { if (! is_array($row)) { continue; } $rowEmail = strtolower(trim((string) ($row['email'] ?? ''))); if ($rowEmail === $email) { $row['invite_token'] = $token; $row['invited_at'] = now()->toIso8601String(); $row['invite_source'] = $source; } $speakers[] = $row; } $payload = (array) ($event->payload ?? []); $payload['content'] = array_merge($content, ['speakers' => $speakers]); $event->payload = $payload; $event->save(); } /** @return list */ private function hostSpeakerEmailsFromProgramme(QrCode $programme): array { $emails = []; foreach ((array) ($programme->content()['days'] ?? []) as $day) { foreach ((array) ($day['items'] ?? []) as $item) { if (! is_array($item)) { continue; } $email = strtolower(trim((string) ($item['host_speaker_email'] ?? ''))); if ($email !== '' && filter_var($email, FILTER_VALIDATE_EMAIL)) { $emails[] = $email; } } } return array_values(array_unique($emails)); } /** @param array $speaker */ private function normalizeSpeakerRow(array $speaker): array { $name = trim((string) ($speaker['name'] ?? '')); $email = trim((string) ($speaker['email'] ?? '')); return [ 'name' => $name, 'email' => filter_var($email, FILTER_VALIDATE_EMAIL) ? $email : '', 'role' => trim((string) ($speaker['role'] ?? '')), 'bio' => trim((string) ($speaker['bio'] ?? '')), 'invite_token' => trim((string) ($speaker['invite_token'] ?? '')), 'invited_at' => $speaker['invited_at'] ?? null, 'invite_source' => $speaker['invite_source'] ?? null, ]; } }