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
@@ -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