Complete Events–Meet integration phases 2–5.
Deploy Ladill Events / deploy (push) Successful in 41s

Programme sync to Meet rooms, wallet-billed mass comms, webhook lifecycle updates, and registration access bridge with join-window gating on public pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-01 22:31:45 +00:00
co-authored by Cursor
parent 06fedcfc55
commit 05a6be7efe
20 changed files with 798 additions and 115 deletions
@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\Meet\MeetLifecycleService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class MeetWebhookController extends Controller
{
public function __invoke(Request $request, MeetLifecycleService $lifecycle): JsonResponse
{
$secret = (string) config('meet.webhook_secret', '');
if ($secret === '') {
return response()->json(['error' => 'Webhook not configured.'], 503);
}
$signature = (string) $request->header('X-Ladill-Meet-Signature', '');
$body = $request->getContent();
$expected = hash_hmac('sha256', $body, $secret);
if ($signature === '' || ! hash_equals($expected, $signature)) {
return response()->json(['error' => 'Invalid signature.'], 401);
}
/** @var array<string, mixed> $payload */
$payload = $request->json()->all();
$event = (string) ($payload['event'] ?? '');
if ($event === '') {
return response()->json(['error' => 'Missing event.'], 422);
}
$lifecycle->handle($event, $payload);
return response()->json(['ok' => true]);
}
}
@@ -5,19 +5,19 @@ namespace App\Http\Controllers\Events;
use App\Http\Controllers\Controller;
use App\Models\QrCode;
use App\Models\QrEventRegistration;
use App\Services\Billing\SmsService;
use App\Services\Events\EventEmailService;
use App\Services\Events\EventCommsService;
use App\Support\Events\EventBadgeZpl;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Validation\Rule;
use Illuminate\View\View;
class AttendeeController extends Controller
{
public function __construct(
private readonly SmsService $sms,
private readonly EventEmailService $email,
private readonly EventCommsService $comms,
) {}
public function hub(Request $request): View
@@ -133,7 +133,24 @@ class AttendeeController extends Controller
'revenue' => $event->eventRegistrations()->where('status', QrEventRegistration::STATUS_CONFIRMED)->sum('amount_minor') / 100,
];
$content = $event->content();
$format = (string) ($content['format'] ?? 'in_person');
$virtualSessions = collect((array) ($content['virtual_sessions'] ?? []))->filter(fn ($s) => is_array($s));
if (in_array($format, ['virtual', 'hybrid'], true) && $virtualSessions->isNotEmpty()) {
$stats['virtual_sessions'] = $virtualSessions->count();
$stats['virtual_live'] = $virtualSessions->where('meet_status', 'live')->count();
$stats['virtual_ended'] = $virtualSessions->where('meet_status', 'ended')->count();
$stats['virtual_recordings'] = $virtualSessions->filter(fn ($s) => ! empty($s['recording_url']))->count();
}
$programme = $this->programmeFor($event);
$joinUrl = $this->comms->primaryJoinUrl($event);
$commsModes = array_values(array_filter([
$programme ? EventCommsService::MODE_PROGRAMME : null,
$joinUrl !== '' ? EventCommsService::MODE_JOIN : null,
($programme && $joinUrl !== '') ? EventCommsService::MODE_BOTH : null,
]));
return view('events.attendees', [
'qrCode' => $event,
@@ -141,60 +158,64 @@ class AttendeeController extends Controller
'registrations' => $registrations,
'stats' => $stats,
'programme' => $programme,
'commsModes' => $commsModes,
'defaultCommsMode' => $commsModes[0] ?? EventCommsService::MODE_PROGRAMME,
]);
}
public function shareProgramme(QrCode $event): RedirectResponse
public function previewComms(QrCode $event, Request $request): JsonResponse
{
$this->authorize('view', $event);
abort_unless($event->type === QrCode::TYPE_EVENT, 404);
$programme = $this->programmeFor($event);
if (! $programme) {
return back()->with('error', 'No programme outline is attached to this event yet.');
$mode = (string) $request->query('mode', EventCommsService::MODE_PROGRAMME);
abort_unless(in_array($mode, [EventCommsService::MODE_PROGRAMME, EventCommsService::MODE_JOIN, EventCommsService::MODE_BOTH], true), 422);
return response()->json($this->comms->preview($event, $mode));
}
public function shareComms(QrCode $event, Request $request): RedirectResponse
{
$this->authorize('view', $event);
abort_unless($event->type === QrCode::TYPE_EVENT, 404);
$validated = $request->validate([
'mode' => ['required', Rule::in([
EventCommsService::MODE_PROGRAMME,
EventCommsService::MODE_JOIN,
EventCommsService::MODE_BOTH,
])],
]);
$preview = $this->comms->preview($event, $validated['mode']);
if (! $preview['affordable']) {
return back()->with('error', 'Insufficient wallet balance. Add funds in Billing before sending.');
}
$recipients = $event->eventRegistrations()
->where('status', QrEventRegistration::STATUS_CONFIRMED)
->get();
$eventName = $event->content()['name'] ?? $event->label;
$programmeUrl = $programme->publicUrl();
$emailed = 0;
$texted = 0;
foreach ($recipients as $reg) {
if ($reg->attendee_email) {
$this->email->sendProgrammeShare(
(string) $event->user->public_id,
$reg->attendee_email,
$eventName,
$programmeUrl,
$reg->attendee_name,
);
$emailed++;
}
if ($reg->attendee_phone) {
$this->sms->send(
(string) $event->user->public_id,
$reg->attendee_phone,
sprintf(
'Hi %s, the programme for %s is here: %s',
explode(' ', $reg->attendee_name ?? 'there')[0],
$eventName,
$programmeUrl
)
);
$texted++;
}
if ($preview['recipients'] === 0) {
return back()->with('error', 'No confirmed attendees to message yet.');
}
if ($emailed === 0 && $texted === 0) {
return back()->with('error', 'No confirmed attendees with contact details to share with yet.');
$result = $this->comms->send($event, $validated['mode']);
if ($result['emailed'] === 0 && $result['texted'] === 0) {
return back()->with('error', 'No messages were sent — check that attendees have email or phone on file.');
}
return back()->with('success', "Programme shared — {$emailed} email(s) and {$texted} SMS sent.");
return back()->with('success', sprintf(
'Sent — %d email(s), %d SMS, %d skipped.',
$result['emailed'],
$result['texted'],
$result['skipped'],
));
}
/** @deprecated Use shareComms with mode=programme */
public function shareProgramme(QrCode $event): RedirectResponse
{
request()->merge(['mode' => EventCommsService::MODE_PROGRAMME]);
return $this->shareComms($event, request());
}
private function programmeFor(QrCode $event): ?QrCode
+168
View File
@@ -0,0 +1,168 @@
<?php
namespace App\Services\Events;
use App\Models\QrCode;
use App\Models\QrEventRegistration;
use App\Services\Billing\BillingClient;
use App\Services\Billing\SmsService;
use Illuminate\Support\Collection;
class EventCommsService
{
public const MODE_PROGRAMME = 'programme';
public const MODE_JOIN = 'join';
public const MODE_BOTH = 'both';
private const SMS_SEGMENT_CHARS = 160;
private const SMS_PRICE_GHS = 0.03;
private const EMAIL_PRICE_GHS = 0.0135;
public function __construct(
private readonly EventEmailService $email,
private readonly SmsService $sms,
private readonly BillingClient $billing,
private readonly EventProgrammeService $programmes,
) {}
/** @return array{recipients: int, emails: int, sms: int, estimated_ghs: float, affordable: bool} */
public function preview(QrCode $event, string $mode): array
{
$regs = $this->confirmedRegistrations($event);
$emails = $regs->filter(fn ($r) => (bool) $r->attendee_email)->count();
$phones = $regs->filter(fn ($r) => (bool) $r->attendee_phone)->count();
$smsSegments = $phones * $this->estimateSmsSegments($event, $mode);
$estimated = round(($emails * self::EMAIL_PRICE_GHS) + ($smsSegments * self::SMS_PRICE_GHS), 2);
$ownerRef = (string) $event->user->public_id;
$affordable = $estimated <= 0 || $this->billing->canAfford($ownerRef, (int) round($estimated * 100));
return [
'recipients' => $regs->count(),
'emails' => $emails,
'sms' => $phones,
'estimated_ghs' => $estimated,
'affordable' => $affordable,
];
}
/**
* @return array{emailed: int, texted: int, skipped: int}
*/
public function send(QrCode $event, string $mode): array
{
$preview = $this->preview($event, $mode);
abort_unless($preview['affordable'], 402, 'Insufficient wallet balance for this send.');
$programme = $this->programmeFor($event);
$eventName = $event->content()['name'] ?? $event->label;
$programmeUrl = $programme?->publicUrl();
$joinUrl = $this->primaryJoinUrl($event);
$ownerRef = (string) $event->user->public_id;
$emailed = 0;
$texted = 0;
$skipped = 0;
foreach ($this->confirmedRegistrations($event) as $reg) {
$sent = false;
if ($reg->attendee_email && in_array($mode, [self::MODE_PROGRAMME, self::MODE_BOTH], true) && $programmeUrl) {
if ($mode === self::MODE_BOTH) {
$sent = $this->email->sendJoinAndProgramme($ownerRef, $reg->attendee_email, $eventName, $joinUrl, $programmeUrl, $reg->attendee_name);
} else {
$sent = $this->email->sendProgrammeShare($ownerRef, $reg->attendee_email, $eventName, $programmeUrl, $reg->attendee_name);
}
if ($sent) {
$emailed++;
}
} elseif ($reg->attendee_email && $mode === self::MODE_JOIN && $joinUrl !== '') {
$sent = $this->email->sendJoinLink($ownerRef, $reg->attendee_email, $eventName, $joinUrl, $reg->attendee_name);
if ($sent) {
$emailed++;
}
}
if ($reg->attendee_phone) {
$message = $this->smsBody($reg, $eventName, $mode, $programmeUrl, $joinUrl);
if ($message !== '') {
$this->sms->send($ownerRef, $reg->attendee_phone, $message);
$texted++;
$sent = true;
}
}
if (! $sent) {
$skipped++;
}
}
return compact('emailed', 'texted', 'skipped');
}
/** @return Collection<int, QrEventRegistration> */
private function confirmedRegistrations(QrCode $event): Collection
{
return $event->eventRegistrations()
->where('status', QrEventRegistration::STATUS_CONFIRMED)
->get();
}
private function programmeFor(QrCode $event): ?QrCode
{
$programmeId = (int) ($event->content()['programme_qr_id'] ?? 0);
if ($programmeId <= 0) {
return null;
}
return QrCode::where('id', $programmeId)
->where('user_id', $event->user_id)
->where('type', QrCode::TYPE_ITINERARY)
->first();
}
public function primaryJoinUrl(QrCode $event): string
{
foreach ((array) ($event->content()['virtual_sessions'] ?? []) as $session) {
if (! is_array($session)) {
continue;
}
$url = trim((string) ($session['join_url'] ?? ''));
if ($url !== '') {
return $url;
}
}
return '';
}
private function smsBody(QrEventRegistration $reg, string $eventName, string $mode, ?string $programmeUrl, string $joinUrl): string
{
$first = explode(' ', $reg->attendee_name ?? 'there')[0];
return match ($mode) {
self::MODE_PROGRAMME => $programmeUrl
? sprintf('Hi %s, the programme for %s: %s', $first, $eventName, $programmeUrl)
: '',
self::MODE_JOIN => $joinUrl !== ''
? sprintf('Hi %s, join %s here: %s', $first, $eventName, $joinUrl)
: '',
self::MODE_BOTH => ($programmeUrl && $joinUrl !== '')
? sprintf('Hi %s, %s — programme: %s | join: %s', $first, $eventName, $programmeUrl, $joinUrl)
: ($joinUrl !== '' ? sprintf('Hi %s, join %s: %s', $first, $eventName, $joinUrl) : ''),
default => '',
};
}
private function estimateSmsSegments(QrCode $event, string $mode): int
{
$sample = $this->smsBody(
new QrEventRegistration(['attendee_name' => 'Guest']),
$event->content()['name'] ?? $event->label,
$mode,
$this->programmeFor($event)?->publicUrl(),
$this->primaryJoinUrl($event),
);
return max(1, (int) ceil(strlen($sample) / self::SMS_SEGMENT_CHARS));
}
}
+64 -6
View File
@@ -16,16 +16,74 @@ class EventEmailService
string $programmeUrl,
?string $attendeeName = null,
): bool {
$html = View::make('mail.notifications.event-programme', [
'eventName' => $eventName,
'programmeUrl' => $programmeUrl,
'attendeeName' => $attendeeName,
])->render();
return $this->send(
$ownerPublicId,
$to,
'Programme for '.$eventName,
'mail.notifications.event-programme',
compact('eventName', 'programmeUrl', 'attendeeName'),
);
}
public function sendJoinLink(
string $ownerPublicId,
string $to,
string $eventName,
string $joinUrl,
?string $attendeeName = null,
): bool {
return $this->send(
$ownerPublicId,
$to,
'Join '.$eventName,
'mail.notifications.event-join',
compact('eventName', 'joinUrl', 'attendeeName'),
);
}
public function sendJoinAndProgramme(
string $ownerPublicId,
string $to,
string $eventName,
string $joinUrl,
string $programmeUrl,
?string $attendeeName = null,
): bool {
return $this->send(
$ownerPublicId,
$to,
$eventName.' — programme & join link',
'mail.notifications.event-join-programme',
compact('eventName', 'joinUrl', 'programmeUrl', 'attendeeName'),
);
}
public function sendRegistrationConfirmation(
string $ownerPublicId,
string $to,
string $eventName,
string $badgeCode,
?string $joinUrl = null,
?string $attendeeName = null,
): bool {
return $this->send(
$ownerPublicId,
$to,
'Registration confirmed — '.$eventName,
'mail.notifications.event-registration-confirmed',
compact('eventName', 'badgeCode', 'joinUrl', 'attendeeName'),
);
}
/** @param array<string, mixed> $viewData */
private function send(string $ownerPublicId, string $to, string $subject, string $view, array $viewData): bool
{
$html = View::make($view, $viewData)->render();
return $this->platform->send(
$ownerPublicId,
$to,
'Programme for '.$eventName,
$subject,
$html,
strip_tags(str_replace(['<br>', '<br/>', '<br />'], "\n", $html)),
);
@@ -0,0 +1,74 @@
<?php
namespace App\Services\Events;
use App\Models\QrCode;
use Illuminate\Support\Collection;
class EventProgrammeService
{
/** @return array{title: string, days: list<array<string, mixed>>}|null */
public function snapshot(?QrCode $programme): ?array
{
if (! $programme || $programme->type !== QrCode::TYPE_ITINERARY) {
return null;
}
$content = $programme->content();
return [
'title' => (string) ($content['title'] ?? $programme->label),
'subtitle' => (string) ($content['subtitle'] ?? ''),
'location' => (string) ($content['location'] ?? ''),
'days' => array_values((array) ($content['days'] ?? [])),
];
}
/** @param list<string> $itemRefs */
public function itemsForRefs(?array $snapshot, array $itemRefs): array
{
if (! $snapshot) {
return [];
}
$refs = array_flip($itemRefs);
$matched = [];
foreach ((array) ($snapshot['days'] ?? []) as $day) {
foreach ((array) ($day['items'] ?? []) as $item) {
$ref = (string) ($item['ref'] ?? '');
if ($ref !== '' && isset($refs[$ref])) {
$matched[] = $item;
}
}
}
return $matched;
}
/** @param list<array<string, mixed>> $items */
public function hostsFromItems(array $items): array
{
return collect($items)
->pluck('host')
->filter(fn ($host) => is_string($host) && trim($host) !== '')
->map(fn ($host) => trim($host))
->unique()
->values()
->all();
}
/** @return Collection<int, QrCode> */
public function eventsLinkedToProgramme(QrCode $programme): Collection
{
if ($programme->type !== QrCode::TYPE_ITINERARY) {
return collect();
}
return QrCode::query()
->where('user_id', $programme->user_id)
->where('type', QrCode::TYPE_EVENT)
->get()
->filter(fn (QrCode $event) => (int) ($event->content()['programme_qr_id'] ?? 0) === $programme->id);
}
}
@@ -7,6 +7,8 @@ use App\Models\QrEventRegistration;
use App\Services\Billing\BillingClient;
use App\Services\Billing\PaystackService;
use App\Services\Billing\SmsService;
use App\Services\Events\EventEmailService;
use App\Services\Meet\EventMeetAccessService;
use App\Services\Pay\PayClient;
use App\Support\LadillLink;
use Illuminate\Support\Str;
@@ -19,6 +21,8 @@ class EventRegistrationService
private PaystackService $paystack,
private BillingClient $billing,
private SmsService $sms,
private EventEmailService $email,
private EventMeetAccessService $meetAccess,
) {}
/**
@@ -248,9 +252,7 @@ class EventRegistrationService
private function notifyConfirmed(QrEventRegistration $registration): void
{
if (! $registration->attendee_phone) {
return;
}
$registration->loadMissing('qrCode.user');
$ownerPublicId = (string) $registration->qrCode?->user?->public_id;
if ($ownerPublicId === '') {
@@ -260,37 +262,52 @@ class EventRegistrationService
$content = $registration->qrCode?->content() ?? [];
$eventName = $content['name'] ?? $registration->qrCode?->label ?? config('app.name');
$firstName = explode(' ', $registration->attendee_name)[0];
if (($content['mode'] ?? 'ticketing') === 'contributions') {
$message = sprintf(
'Hi %s, thank you for your %s of %s %s to %s. Ref: %s',
$firstName,
$registration->tier_name,
$registration->currency,
number_format($registration->amountCedis(), 2),
$eventName,
$registration->badge_code
);
} else {
$message = sprintf(
'Hi %s, you are registered for %s (%s). Badge code: %s',
$firstName,
$eventName,
$registration->tier_name,
$registration->badge_code
);
}
$joinUrl = $this->nextVirtualJoinUrl($content);
if ($joinUrl !== '') {
$message .= ' Join: '.$joinUrl;
if ($registration->attendee_email) {
$this->email->sendRegistrationConfirmation(
$ownerPublicId,
$registration->attendee_email,
$eventName,
$registration->badge_code,
$joinUrl !== '' ? $joinUrl : null,
$registration->attendee_name,
);
}
$this->sms->send(
$ownerPublicId,
$registration->attendee_phone,
$message,
);
if ($registration->attendee_phone) {
if (($content['mode'] ?? 'ticketing') === 'contributions') {
$message = sprintf(
'Hi %s, thank you for your %s of %s %s to %s. Ref: %s',
$firstName,
$registration->tier_name,
$registration->currency,
number_format($registration->amountCedis(), 2),
$eventName,
$registration->badge_code
);
} else {
$message = sprintf(
'Hi %s, you are registered for %s (%s). Badge code: %s',
$firstName,
$eventName,
$registration->tier_name,
$registration->badge_code
);
}
if ($joinUrl !== '') {
$message .= ' Join: '.$joinUrl;
}
$this->sms->send(
$ownerPublicId,
$registration->attendee_phone,
$message,
);
}
$this->meetAccess->syncRegistration($registration->fresh());
}
/** @param array<string, mixed> $content */
@@ -0,0 +1,75 @@
<?php
namespace App\Services\Meet;
use App\Models\QrCode;
use App\Models\QrEventRegistration;
use Illuminate\Support\Facades\Log;
class EventMeetAccessService
{
public function syncRegistration(QrEventRegistration $registration): void
{
$event = $registration->qrCode;
if (! $event || $event->type !== QrCode::TYPE_EVENT) {
return;
}
$format = (string) ($event->content()['format'] ?? 'in_person');
if (! in_array($format, ['virtual', 'hybrid'], true)) {
return;
}
if ((string) config('meet.key', '') === '' || ! $registration->attendee_email) {
return;
}
$ownerRef = (string) $event->user->public_id;
$client = MeetClient::for($ownerRef);
foreach ((array) ($event->content()['virtual_sessions'] ?? []) as $session) {
if (! is_array($session)) {
continue;
}
$roomUuid = trim((string) ($session['meet_room_uuid'] ?? ''));
if ($roomUuid === '') {
continue;
}
try {
$client->updateRoom($roomUuid, [
'invite_emails' => [$registration->attendee_email],
]);
} catch (\Throwable $e) {
Log::warning('Events Meet invite failed', [
'registration_id' => $registration->id,
'room_uuid' => $roomUuid,
'error' => $e->getMessage(),
]);
}
}
$metadata = (array) ($registration->metadata ?? []);
$metadata['meet_invited_at'] = now()->toIso8601String();
$registration->update(['metadata' => $metadata]);
}
public function isJoinWindowOpen(array $session, int $minutesBefore = 15): bool
{
$scheduled = trim((string) ($session['scheduled_at'] ?? ''));
if ($scheduled === '') {
return true;
}
try {
$start = \Carbon\Carbon::parse($scheduled);
} catch (\Throwable) {
return true;
}
$duration = max(15, (int) ($session['duration_minutes'] ?? 60));
return now()->between($start->copy()->subMinutes($minutesBefore), $start->copy()->addMinutes($duration));
}
}
@@ -4,11 +4,14 @@ namespace App\Services\Meet;
use App\Models\QrCode;
use App\Models\User;
use App\Services\Events\EventProgrammeService;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class EventMeetSyncService
{
public function __construct(private readonly EventProgrammeService $programmes) {}
public function sync(QrCode $event, User $owner): QrCode
{
if ($event->type !== QrCode::TYPE_EVENT) {
@@ -28,6 +31,7 @@ class EventMeetSyncService
return $event;
}
$programmeSnapshot = $this->programmeSnapshotFor($event);
$sessions = (array) ($content['virtual_sessions'] ?? []);
$client = MeetClient::for($owner->public_id);
$synced = [];
@@ -56,6 +60,23 @@ class EventMeetSyncService
: 'town_hall';
$scheduledAt = $session['scheduled_at'] ?? $content['starts_at'] ?? null;
$itemRefs = (array) ($session['programme_item_refs'] ?? []);
$linkedItems = $this->programmes->itemsForRefs($programmeSnapshot, $itemRefs);
$inviteEmails = $this->inviteEmailsFromHosts($this->programmes->hostsFromItems($linkedItems));
$settings = [
'programme_snapshot' => $programmeSnapshot,
'linked_programme_items' => $linkedItems,
];
if ($roomType === 'town_hall' && $linkedItems !== []) {
$settings['panels'] = [[
'id' => 'panel-'.$sessionId,
'name' => $title,
'speaker_refs' => [],
]];
$settings['panel_discussions'] = true;
}
try {
$result = $client->upsertRoom([
@@ -65,6 +86,8 @@ class EventMeetSyncService
'scheduled_at' => $scheduledAt,
'duration_minutes' => max(15, (int) ($session['duration_minutes'] ?? 60)),
'timezone' => config('app.timezone', 'UTC'),
'settings' => $settings,
'invite_emails' => $inviteEmails,
'source' => [
'app' => 'events',
'entity_type' => 'virtual_session',
@@ -101,6 +124,38 @@ class EventMeetSyncService
return $event->fresh();
}
public function syncEventsForProgramme(QrCode $programme, User $owner): void
{
foreach ($this->programmes->eventsLinkedToProgramme($programme) as $event) {
$this->sync($event->fresh(), $owner);
}
}
/** @return array<string, mixed>|null */
private function programmeSnapshotFor(QrCode $event): ?array
{
$programmeId = (int) ($event->content()['programme_qr_id'] ?? 0);
if ($programmeId <= 0) {
return null;
}
$programme = QrCode::where('id', $programmeId)
->where('user_id', $event->user_id)
->where('type', QrCode::TYPE_ITINERARY)
->first();
return $this->programmes->snapshot($programme);
}
/** @param list<string> $hosts */
private function inviteEmailsFromHosts(array $hosts): array
{
return collect($hosts)
->filter(fn ($host) => filter_var($host, FILTER_VALIDATE_EMAIL))
->values()
->all();
}
/** @param list<string> $keepIds */
private function cancelRemovedSessions(QrCode $event, User $owner, array $keepIds): void
{
@@ -0,0 +1,71 @@
<?php
namespace App\Services\Meet;
use App\Models\QrCode;
use Illuminate\Support\Facades\Log;
class MeetLifecycleService
{
/** @param array<string, mixed> $payload */
public function handle(string $event, array $payload): void
{
$room = (array) ($payload['room'] ?? []);
$source = (array) ($room['source'] ?? []);
if (($source['app'] ?? '') !== 'events') {
return;
}
$eventId = (int) ($source['event_id'] ?? 0);
$sessionId = (string) ($source['entity_id'] ?? '');
if ($eventId <= 0 || $sessionId === '') {
return;
}
$qrCode = QrCode::find($eventId);
if (! $qrCode || $qrCode->type !== QrCode::TYPE_EVENT) {
return;
}
$content = $qrCode->content();
$sessions = (array) ($content['virtual_sessions'] ?? []);
$updated = false;
foreach ($sessions as $index => $session) {
if (! is_array($session) || ($session['id'] ?? '') !== $sessionId) {
continue;
}
$sessions[$index] = array_merge($session, $this->patchForEvent($event, $payload, $room));
$updated = true;
break;
}
if (! $updated) {
return;
}
$payloadData = (array) ($qrCode->payload ?? []);
$payloadData['content'] = array_merge($content, ['virtual_sessions' => array_values($sessions)]);
$qrCode->update(['payload' => $payloadData]);
Log::info('Events Meet lifecycle applied', ['event' => $event, 'event_id' => $eventId, 'session_id' => $sessionId]);
}
/** @return array<string, mixed> */
private function patchForEvent(string $event, array $payload, array $room): array
{
return match ($event) {
'meeting.started' => ['meet_status' => 'live', 'meet_live_at' => now()->toIso8601String()],
'meeting.ended' => ['meet_status' => 'ended', 'meet_ended_at' => now()->toIso8601String()],
'meeting.cancelled' => ['meet_status' => 'cancelled'],
'recording.ready' => [
'recording_url' => (string) (($payload['recording']['download_url'] ?? '') ?: ''),
'recording_ready_at' => now()->toIso8601String(),
],
default => [],
};
}
}
+4
View File
@@ -303,6 +303,10 @@ class QrCodeManagerService
$qrCode = $this->meetSync->sync($qrCode->fresh(), $qrCode->user);
}
if ($qrCode->type === QrCode::TYPE_ITINERARY) {
$this->meetSync->syncEventsForProgramme($qrCode->fresh(), $qrCode->user);
}
return $qrCode->fresh(['document']);
}
+3 -2
View File
@@ -374,12 +374,12 @@ class QrPayloadValidator
// Days: [{ label, date, items: [{ time, title, description, location, host }] }]
$days = [];
foreach ((array) ($input['days'] ?? []) as $day) {
foreach ((array) ($input['days'] ?? []) as $dayIndex => $day) {
if (! is_array($day)) {
continue;
}
$items = [];
foreach ((array) ($day['items'] ?? []) as $item) {
foreach ((array) ($day['items'] ?? []) as $itemIndex => $item) {
if (! is_array($item)) {
continue;
}
@@ -388,6 +388,7 @@ class QrPayloadValidator
continue;
}
$items[] = [
'ref' => 'day'.$dayIndex.'-item'.$itemIndex,
'time' => mb_substr(trim((string) ($item['time'] ?? '')), 0, 40),
'title' => mb_substr($itemTitle, 0, 140),
'description' => mb_substr(trim((string) ($item['description'] ?? '')), 0, 400),