From d2ec31f7a5cd49d3108f0ad0aa86209664fe5d12 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Fri, 3 Jul 2026 13:44:53 +0000 Subject: [PATCH] Fix post-registration 404, add speaker management, and simplify event creation. Proxy registration transaction paths on Events, add a join button on the confirmation page, introduce speaker roster management, and replace the QR design wizard with merchant-style create/show flows. Co-authored-by: Cursor --- .../Controllers/Events/SpeakerController.php | 72 ++++++++++ app/Http/Controllers/Qr/QrCodeController.php | 17 +++ .../RedirectLegacyQrToLadillLink.php | 22 ++- app/Services/Events/EventSpeakerService.php | 127 ++++++++++++++++++ app/Services/Qr/QrCodeManagerService.php | 8 +- app/Services/Qr/QrPayloadValidator.php | 30 +++++ resources/views/events/create.blade.php | 75 +++++++++++ resources/views/events/show.blade.php | 114 ++++++++++++++++ resources/views/events/speakers-hub.blade.php | 46 +++++++ resources/views/events/speakers.blade.php | 89 ++++++++++++ resources/views/partials/sidebar.blade.php | 2 + .../views/public/qr/event-confirmed.blade.php | 17 +++ routes/web.php | 5 + .../EventRegistrationConfirmationTest.php | 64 +++++++++ tests/Feature/EventSpeakerManagementTest.php | 103 ++++++++++++++ tests/Feature/LegacyQrRedirectTest.php | 23 ++++ 16 files changed, 811 insertions(+), 3 deletions(-) create mode 100644 app/Http/Controllers/Events/SpeakerController.php create mode 100644 app/Services/Events/EventSpeakerService.php create mode 100644 resources/views/events/create.blade.php create mode 100644 resources/views/events/show.blade.php create mode 100644 resources/views/events/speakers-hub.blade.php create mode 100644 resources/views/events/speakers.blade.php create mode 100644 tests/Feature/EventRegistrationConfirmationTest.php create mode 100644 tests/Feature/EventSpeakerManagementTest.php diff --git a/app/Http/Controllers/Events/SpeakerController.php b/app/Http/Controllers/Events/SpeakerController.php new file mode 100644 index 0000000..912a78b --- /dev/null +++ b/app/Http/Controllers/Events/SpeakerController.php @@ -0,0 +1,72 @@ +query('search', '')); + + $events = $this->speakers->eventsForAccount($account->id, $search !== '' ? $search : null) + ->map(function (QrCode $event) { + $event->speaker_count = count($this->speakers->rosterFor($event)); + + return $event; + }); + + return view('events.speakers-hub', [ + 'events' => $events, + 'search' => $search, + ]); + } + + public function index(QrCode $event): View + { + $this->authorize('view', $event); + abort_unless($event->type === QrCode::TYPE_EVENT, 404); + + return view('events.speakers', [ + 'qrCode' => $event, + 'roster' => (array) ($event->content()['speakers'] ?? []), + 'programmeAssignments' => $this->speakers->programmeAssignments($event), + 'programme' => $this->speakers->linkedProgramme($event), + ]); + } + + public function update(Request $request, QrCode $event): RedirectResponse + { + $this->authorize('update', $event); + abort_unless($event->type === QrCode::TYPE_EVENT, 404); + + $validated = $request->validate([ + 'speakers' => ['nullable', 'array'], + 'speakers.*.name' => ['nullable', 'string', 'max:120'], + 'speakers.*.email' => ['nullable', 'email', 'max:190'], + 'speakers.*.role' => ['nullable', 'string', 'max:80'], + 'speakers.*.bio' => ['nullable', 'string', 'max:500'], + ]); + + $this->manager->update($event, [ + 'speakers' => $validated['speakers'] ?? [], + ]); + + return redirect() + ->route('speakers.show', $event) + ->with('success', 'Speaker roster saved.'); + } +} diff --git a/app/Http/Controllers/Qr/QrCodeController.php b/app/Http/Controllers/Qr/QrCodeController.php index a17ed6a..9715483 100644 --- a/app/Http/Controllers/Qr/QrCodeController.php +++ b/app/Http/Controllers/Qr/QrCodeController.php @@ -82,6 +82,13 @@ class QrCodeController extends Controller $requestedType = QrCode::TYPE_EVENT; } + if ($requestedType === QrCode::TYPE_EVENT) { + return view('events.create', [ + 'pricePerQr' => QrWallet::pricePerQr(), + 'accountEventDefaults' => $qrSettings->resolvedEventDefaults(), + ]); + } + return view('qr-codes.create', [ 'wallet' => $wallet, 'requestedType' => $requestedType, @@ -213,6 +220,16 @@ class QrCodeController extends Controller ->get(['id', 'label']) : collect(); + if ($qrCode->type === QrCode::TYPE_EVENT) { + return view('events.show', [ + 'qrCode' => $qrCode, + 'previewDataUri' => $previewDataUri, + 'itineraryOptions' => $itineraryOptions, + 'customDomains' => \App\Models\CustomDomain::where('qr_code_id', $qrCode->id)->get(), + 'customDomainsEnabled' => app(\App\Services\CustomDomain\CustomDomainService::class)->enabled(), + ]); + } + return view('qr-codes.show', [ 'qrCode' => $qrCode, 'previewDataUri' => $previewDataUri, diff --git a/app/Http/Middleware/RedirectLegacyQrToLadillLink.php b/app/Http/Middleware/RedirectLegacyQrToLadillLink.php index aaa8203..9d668ba 100644 --- a/app/Http/Middleware/RedirectLegacyQrToLadillLink.php +++ b/app/Http/Middleware/RedirectLegacyQrToLadillLink.php @@ -36,7 +36,7 @@ class RedirectLegacyQrToLadillLink return $next($request); } - if ($this->servesStoredAsset($request)) { + if ($this->servesStoredAsset($request) || $this->servesRegistrationTransaction($request)) { return $next($request); } @@ -64,4 +64,24 @@ class RedirectLegacyQrToLadillLink return (bool) preg_match('#^(image/\d+|item-image/\d+/\d+)$#', $suffix); } + + private function servesRegistrationTransaction(Request $request): bool + { + $shortCode = $request->route('shortCode'); + if (! is_string($shortCode) || $shortCode === '') { + return false; + } + + $path = ltrim($request->path(), '/'); + $prefix = 'q/'.$shortCode.'/'; + if (! str_starts_with($path, $prefix)) { + return false; + } + + $suffix = substr($path, strlen($prefix)); + + return $suffix === 'register' + || str_starts_with($suffix, 'register/') + || str_starts_with($suffix, 'registered/'); + } } diff --git a/app/Services/Events/EventSpeakerService.php b/app/Services/Events/EventSpeakerService.php new file mode 100644 index 0000000..fe43169 --- /dev/null +++ b/app/Services/Events/EventSpeakerService.php @@ -0,0 +1,127 @@ +type !== QrCode::TYPE_EVENT) { + return null; + } + + $programmeId = (int) ($event->content()['programme_qr_id'] ?? 0); + if ($programmeId <= 0) { + return null; + } + + return QrCode::query() + ->where('id', $programmeId) + ->where('user_id', $event->user_id) + ->where('type', QrCode::TYPE_ITINERARY) + ->first(); + } + + /** @return list */ + public function rosterFor(QrCode $event): array + { + if ($event->type !== QrCode::TYPE_EVENT) { + return []; + } + + $content = $event->content(); + $roster = []; + + foreach ((array) ($content['speakers'] ?? []) as $speaker) { + if (! is_array($speaker)) { + continue; + } + + $name = trim((string) ($speaker['name'] ?? '')); + if ($name === '') { + continue; + } + + $roster[] = [ + 'name' => $name, + 'email' => trim((string) ($speaker['email'] ?? '')), + 'role' => trim((string) ($speaker['role'] ?? '')), + 'bio' => trim((string) ($speaker['bio'] ?? '')), + 'source' => 'roster', + ]; + } + + foreach ($this->programmeAssignments($event) as $assignment) { + $name = trim((string) ($assignment['name'] ?? '')); + if ($name === '') { + continue; + } + + $exists = collect($roster)->contains(fn ($row) => strcasecmp($row['name'], $name) === 0); + if ($exists) { + continue; + } + + $roster[] = [ + 'name' => $name, + 'email' => '', + 'role' => trim((string) ($assignment['session'] ?? '')), + 'bio' => '', + 'source' => 'programme', + ]; + } + + return $roster; + } + + /** @return list */ + public function programmeAssignments(QrCode $event): array + { + $programme = $this->linkedProgramme($event); + $snapshot = $this->programmes->snapshot($programme); + if (! $snapshot) { + return []; + } + + $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; + } + + $host = trim((string) ($item['host'] ?? '')); + if ($host === '') { + continue; + } + + $assignments[] = [ + 'name' => $host, + 'session' => trim((string) ($item['title'] ?? '')), + 'day' => $dayLabel, + 'time' => trim((string) ($item['time'] ?? '')), + ]; + } + } + + return $assignments; + } + + /** @return Collection */ + public function eventsForAccount(int $userId, ?string $search = null): Collection + { + return QrCode::query() + ->where('user_id', $userId) + ->where('type', QrCode::TYPE_EVENT) + ->when($search !== null && $search !== '', fn ($q) => $q->where('label', 'like', "%{$search}%")) + ->latest() + ->get(); + } +} diff --git a/app/Services/Qr/QrCodeManagerService.php b/app/Services/Qr/QrCodeManagerService.php index bd96ca6..fbdf1d8 100644 --- a/app/Services/Qr/QrCodeManagerService.php +++ b/app/Services/Qr/QrCodeManagerService.php @@ -43,7 +43,11 @@ class QrCodeManagerService $type = (string) ($data['type'] ?? ''); $validated = $this->payloadValidator->validateForCreate($type, $data); - $style = QrStyleDefaults::merge($data['style'] ?? null); + $styleInput = $data['style'] ?? null; + if ($type === QrCode::TYPE_EVENT && empty(array_filter((array) $styleInput))) { + $styleInput = $user->getOrCreateQrSetting()->resolvedDefaultStyle(); + } + $style = QrStyleDefaults::merge($styleInput); return DB::transaction(function () use ($user, $wallet, $data, $type, $validated, $style) { $documentId = null; @@ -360,7 +364,7 @@ class QrCodeManagerService QrCode::TYPE_VCARD => ['first_name', 'last_name', 'phone', 'email', 'company', 'website', 'address', 'note', 'social'], QrCode::TYPE_BUSINESS => ['name', 'tagline', 'phone', 'email', 'website', 'address', 'hours'], QrCode::TYPE_CHURCH => ['name', 'denomination', 'description', 'phone', 'email', 'website', 'address', 'service_times', 'org_type', 'accepts_payment', 'collection_types', 'brand_color'], - QrCode::TYPE_EVENT => ['name', 'tagline', 'description', 'location', 'starts_at', 'ends_at', 'organizer', 'website', 'brand_color', 'tiers', 'badge_fields', 'badge_size', 'registration_open', 'format', 'virtual_sessions'], + QrCode::TYPE_EVENT => ['name', 'tagline', 'description', 'location', 'starts_at', 'ends_at', 'organizer', 'website', 'brand_color', 'tiers', 'badge_fields', 'badge_size', 'registration_open', 'format', 'virtual_sessions', 'speakers', 'programme_qr_id'], QrCode::TYPE_ITINERARY => ['title', 'subtitle', 'description', 'event_date', 'location', 'brand_color', 'days'], QrCode::TYPE_DOCUMENT => ['allow_download'], QrCode::TYPE_MENU => ['menu_title', 'sections', 'accepts_payment', 'brand_color', 'shipping_type', 'shipping_fee', 'free_shipping_above'], diff --git a/app/Services/Qr/QrPayloadValidator.php b/app/Services/Qr/QrPayloadValidator.php index 479ae96..7fe32d4 100644 --- a/app/Services/Qr/QrPayloadValidator.php +++ b/app/Services/Qr/QrPayloadValidator.php @@ -265,6 +265,7 @@ class QrPayloadValidator : 'in_person'; $virtualSessions = $this->normalizeVirtualSessions($input['virtual_sessions'] ?? [], $format); + $speakers = $this->normalizeSpeakers($input['speakers'] ?? null, $input); // Contributions always take payment; ticketing only for paid tiers; free never. $acceptsPayment = match ($mode) { @@ -297,6 +298,7 @@ class QrPayloadValidator 'programme_qr_id' => ($pid = (int) ($input['programme_qr_id'] ?? 0)) > 0 ? $pid : null, 'format' => $format, 'virtual_sessions' => $virtualSessions, + 'speakers' => $speakers, ], 'destination_url' => null, ]; @@ -352,6 +354,34 @@ class QrPayloadValidator return array_values($normalized); } + /** @return list */ + private function normalizeSpeakers(mixed $speakers, array $input): array + { + $rows = is_array($speakers) ? $speakers : (array) ($input['speakers'] ?? []); + $normalized = []; + + foreach ($rows as $speaker) { + if (! is_array($speaker)) { + continue; + } + + $name = trim((string) ($speaker['name'] ?? '')); + if ($name === '') { + continue; + } + + $email = trim((string) ($speaker['email'] ?? '')); + $normalized[] = [ + 'name' => mb_substr($name, 0, 120), + 'email' => filter_var($email, FILTER_VALIDATE_EMAIL) ? $email : '', + 'role' => mb_substr(trim((string) ($speaker['role'] ?? '')), 0, 80), + 'bio' => mb_substr(trim((string) ($speaker['bio'] ?? '')), 0, 500), + ]; + } + + return array_values($normalized); + } + /** @param array $tiers */ private function eventHasPaidTier(array $tiers): bool { diff --git a/resources/views/events/create.blade.php b/resources/views/events/create.blade.php new file mode 100644 index 0000000..c5d9bf5 --- /dev/null +++ b/resources/views/events/create.blade.php @@ -0,0 +1,75 @@ + + Create event + +
+
+ + + Events + +

Create event

+

Set up registration, ticketing, and your event page. A Ladill Link QR code is generated automatically — customize it later in QR Plus if needed.

+
+ + @if(session('error')) +
{{ session('error') }}
+ @endif + @if($errors->any()) +
+
    + @foreach($errors->all() as $error)
  • {{ $error }}
  • @endforeach +
+
+ @endif + +
+ @csrf + + +
+ + +

Private name to find this event in your dashboard.

+
+ +
+ +
+ {{ \App\Support\LadillLink::baseUrl() }}/ + +
+

Leave blank to auto-generate your Ladill Link.

+
+ + @include('qr-codes.partials.type-fields-create', ['accountEventDefaults' => $accountEventDefaults ?? []]) + +

+ GHS {{ number_format($pricePerQr, 2) }} will be charged from your QR balance when you create this event. +

+ + +
+
+
diff --git a/resources/views/events/show.blade.php b/resources/views/events/show.blade.php new file mode 100644 index 0000000..efa3932 --- /dev/null +++ b/resources/views/events/show.blade.php @@ -0,0 +1,114 @@ + + {{ $qrCode->label }} + @php + $content = $qrCode->content(); + $publicUrl = \App\Support\LadillLink::url($qrCode->short_code); + @endphp + +
+
+
+ + + Events + +

{{ $content['name'] ?? $qrCode->label }}

+

+ Event + @if($qrCode->is_active) + ● Live + @else + ● Inactive + @endif +

+
+ + View registration page + + +
+ + @foreach(['success', 'error'] as $flash) + @if(session($flash)) +
+ {{ session($flash) }} +
+ @endif + @endforeach + @if($errors->any()) +
+
    @foreach($errors->all() as $error)
  • {{ $error }}
  • @endforeach
+
+ @endif + + + +
+
+
+ @if($previewDataUri) + QR code + @endif +

{{ $publicUrl }}

+
+ @foreach(['png' => 'PNG', 'svg' => 'SVG', 'pdf' => 'PDF'] as $fmt => $flabel) + {{ $flabel }} + @endforeach +
+

System-generated QR code. Customize colors, logos, and frames in QR Plus.

+
+ + @if(!empty($customDomainsEnabled)) +
+

Custom domain

+

Optional — your Ladill Link always works.

+ @forelse($customDomains as $cd) +
+ {{ $cd->host }} +
+ @empty +
+ @csrf + + +
+ @endforelse +
+ @endif +
+ +
+ @csrf @method('PATCH') + +
+ + +
+ + + + @include('qr-codes.partials.type-fields-edit', ['itineraryOptions' => $itineraryOptions]) + + +
+
+
+
diff --git a/resources/views/events/speakers-hub.blade.php b/resources/views/events/speakers-hub.blade.php new file mode 100644 index 0000000..285cb2f --- /dev/null +++ b/resources/views/events/speakers-hub.blade.php @@ -0,0 +1,46 @@ + + Speakers + +
+
+

Speakers

+

Manage speaker rosters and programme assignments for each event.

+
+ +
+ + +
+ +
+ @if($events->isEmpty()) +
+

{{ $search ? 'No events match your search.' : 'No events yet.' }}

+ @unless($search) + Create your first event + @endunless +
+ @else +
+ @foreach($events as $event) + @php $content = $event->content(); @endphp + +
+ +
+
+

{{ $content['name'] ?? $event->label }}

+

{{ $event->label }}

+
+ + +
+ @endforeach +
+ @endif +
+
+
diff --git a/resources/views/events/speakers.blade.php b/resources/views/events/speakers.blade.php new file mode 100644 index 0000000..49d0dbb --- /dev/null +++ b/resources/views/events/speakers.blade.php @@ -0,0 +1,89 @@ + + Speakers — {{ $qrCode->label }} + +
+ @foreach(['success', 'error'] as $flash) + @if(session($flash)) +
+ {{ session($flash) }} +
+ @endif + @endforeach + +
+ + + Speakers + +

Speaker management

+

{{ $qrCode->content()['name'] ?? $qrCode->label }}

+
+ + @if($programme) +
+ Linked programme: {{ $programme->label }}. + Hosts assigned on programme sessions appear below and sync to virtual sessions. +
+ @else +
+ Attach a programme on the event settings page to pull session hosts into this roster. +
+ @endif + +
+ @csrf + @method('PUT') + +
+
+

Event speaker roster

+

Add keynote speakers, panelists, and hosts. Use email when you want to invite them to virtual sessions.

+
+ +
+ +
+ +
+ + +
+ + @if(count($programmeAssignments)) +
+

From programme schedule

+

These hosts are assigned on programme sessions. Edit them on the linked programme.

+
+ @foreach($programmeAssignments as $assignment) +
+
+

{{ $assignment['name'] }}

+

{{ $assignment['session'] ?: 'Session' }}@if($assignment['day']) · {{ $assignment['day'] }}@endif@if($assignment['time']) · {{ $assignment['time'] }}@endif

+
+ Programme +
+ @endforeach +
+
+ @endif +
+
diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index 504d5f0..948f421 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -16,6 +16,8 @@ 'icon' => ''], ['name' => 'Programmes', 'route' => route('programmes.index'), 'active' => request()->routeIs('programmes.*'), 'icon' => ''], + ['name' => 'Speakers', 'route' => route('speakers.index'), 'active' => request()->routeIs('speakers.*'), + 'icon' => ''], ['name' => 'Payments & Payouts', 'route' => route('events.payouts'), 'active' => request()->routeIs('events.payouts'), 'icon' => ''], ]; diff --git a/resources/views/public/qr/event-confirmed.blade.php b/resources/views/public/qr/event-confirmed.blade.php index 51242e5..f80facf 100644 --- a/resources/views/public/qr/event-confirmed.blade.php +++ b/resources/views/public/qr/event-confirmed.blade.php @@ -4,6 +4,15 @@ $evName = $c['name'] ?? $qrCode->label; $isContribution = ($c['mode'] ?? 'ticketing') === 'contributions'; $badgeUrl = 'https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=' . urlencode($registration->badge_code); + $joinUrl = ''; + if (in_array($c['format'] ?? 'in_person', ['virtual', 'hybrid'], true)) { + foreach ($c['virtual_sessions'] ?? [] as $session) { + if (is_array($session) && filled($session['join_url'] ?? '')) { + $joinUrl = trim((string) $session['join_url']); + break; + } + } + } @endphp @@ -71,6 +80,14 @@ @else

Show this badge code at check-in.

@endif + + @if($joinUrl !== '') + + Join webinar + + @endif @endif diff --git a/routes/web.php b/routes/web.php index 21a7ba1..24cbd92 100644 --- a/routes/web.php +++ b/routes/web.php @@ -6,6 +6,7 @@ use App\Http\Controllers\Events\AttendeeController; use App\Http\Controllers\Events\OverviewController; use App\Http\Controllers\Events\PayoutsController; use App\Http\Controllers\Events\ProgrammeController; +use App\Http\Controllers\Events\SpeakerController; use App\Http\Controllers\NotificationController; use App\Http\Controllers\Public\EventRegistrationController; use App\Http\Controllers\Public\QrScanController; @@ -91,6 +92,10 @@ Route::middleware(['auth', 'platform.session'])->group(function () { Route::get('/programmes', [ProgrammeController::class, 'index'])->name('programmes.index'); Route::get('/programmes/create', fn () => redirect()->route('events.create', ['type' => 'itinerary']))->name('programmes.create'); + Route::get('/speakers', [SpeakerController::class, 'hub'])->name('speakers.index'); + Route::get('/events/{event}/speakers', [SpeakerController::class, 'index'])->name('speakers.show'); + Route::put('/events/{event}/speakers', [SpeakerController::class, 'update'])->name('speakers.update'); + Route::get('/payouts', [PayoutsController::class, 'index'])->name('events.payouts'); Route::get('/wallet', fn () => redirect()->away(ladill_account_url('/wallet')))->name('account.wallet'); diff --git a/tests/Feature/EventRegistrationConfirmationTest.php b/tests/Feature/EventRegistrationConfirmationTest.php new file mode 100644 index 0000000..f56f15e --- /dev/null +++ b/tests/Feature/EventRegistrationConfirmationTest.php @@ -0,0 +1,64 @@ + 'usr_reg_confirm', + 'name' => 'Event Owner', + 'email' => 'owner-reg-confirm@example.com', + ]); + + $event = QrCode::create([ + 'user_id' => $owner->id, + 'short_code' => 'webinar-demo', + 'type' => QrCode::TYPE_EVENT, + 'label' => 'Product webinar', + 'payload' => [ + 'content' => [ + 'name' => 'Product webinar', + 'format' => 'virtual', + 'virtual_sessions' => [ + ['join_url' => 'https://meet.ladill.com/r/demo-room'], + ], + ], + 'style' => [], + ], + 'is_active' => true, + ]); + + QrEventRegistration::create([ + 'qr_code_id' => $event->id, + 'user_id' => $owner->id, + 'reference' => 'QRE-TESTREF123456', + 'badge_code' => 'BADGE123', + 'tier_name' => 'General admission', + 'amount_minor' => 0, + 'currency' => 'GHS', + 'attendee_name' => 'Jane Doe', + 'attendee_email' => 'jane@example.com', + 'attendee_phone' => '+233200000000', + 'status' => QrEventRegistration::STATUS_CONFIRMED, + ]); + + $this->get('/q/webinar-demo/registered/QRE-TESTREF123456', [ + 'X-Ladill-Internal' => '1', + ]) + ->assertOk() + ->assertSee('Jane Doe', false) + ->assertSee('BADGE123', false) + ->assertSee('Join webinar', false) + ->assertSee('https://meet.ladill.com/r/demo-room', false); + } +} diff --git a/tests/Feature/EventSpeakerManagementTest.php b/tests/Feature/EventSpeakerManagementTest.php new file mode 100644 index 0000000..1dc0651 --- /dev/null +++ b/tests/Feature/EventSpeakerManagementTest.php @@ -0,0 +1,103 @@ +withoutMiddleware(\App\Http\Middleware\EnsurePlatformSession::class); + Http::fake([ + config('billing.api_url').'/balance*' => Http::response(['balance_minor' => 50000]), + ]); + } + + public function test_speakers_sidebar_hub_lists_events(): void + { + $owner = User::factory()->create(['public_id' => 'usr_speakers_hub']); + + QrCode::create([ + 'user_id' => $owner->id, + 'short_code' => 'evt-speakers-1', + 'type' => QrCode::TYPE_EVENT, + 'label' => 'Leadership summit', + 'payload' => ['content' => ['name' => 'Leadership summit', 'speakers' => [['name' => 'Ada Lovelace', 'email' => '', 'role' => 'Keynote', 'bio' => '']]], 'style' => []], + 'is_active' => true, + ]); + + $this->actingAs($owner) + ->get(route('speakers.index')) + ->assertOk() + ->assertSee('Leadership summit', false) + ->assertSee('Speakers', false); + } + + public function test_event_speaker_roster_can_be_updated(): void + { + $owner = User::factory()->create(['public_id' => 'usr_speakers_upd']); + + $event = QrCode::create([ + 'user_id' => $owner->id, + 'short_code' => 'evt-speakers-2', + 'type' => QrCode::TYPE_EVENT, + 'label' => 'Dev day', + 'payload' => ['content' => ['name' => 'Dev day'], 'style' => []], + 'is_active' => true, + ]); + + $this->actingAs($owner) + ->put(route('speakers.update', $event), [ + 'speakers' => [ + ['name' => 'Jane Doe', 'email' => 'jane@example.com', 'role' => 'Panelist', 'bio' => ''], + ], + ]) + ->assertRedirect(route('speakers.show', $event)); + + $event->refresh(); + $this->assertSame('Jane Doe', $event->content()['speakers'][0]['name']); + $this->assertSame('jane@example.com', $event->content()['speakers'][0]['email']); + } + + public function test_event_create_page_has_no_qr_design_wizard(): void + { + $owner = User::factory()->create(['public_id' => 'usr_evt_create_simple']); + + $this->actingAs($owner) + ->get(route('events.create')) + ->assertOk() + ->assertSee('Create event', false) + ->assertSee('generated automatically', false) + ->assertDontSee('Quick style presets', false) + ->assertDontSee('Review & create', false); + } + + public function test_event_show_page_has_downloads_not_qr_customizer(): void + { + $owner = User::factory()->create(['public_id' => 'usr_evt_show_simple']); + + $event = QrCode::create([ + 'user_id' => $owner->id, + 'short_code' => 'evt-show-simple', + 'type' => QrCode::TYPE_EVENT, + 'label' => 'Simple show', + 'payload' => ['content' => ['name' => 'Simple show'], 'style' => []], + 'is_active' => true, + ]); + + $this->actingAs($owner) + ->get(route('events.show', $event)) + ->assertOk() + ->assertSee('Customize colors, logos, and frames in QR Plus.', false) + ->assertSee('Programme outline', false) + ->assertDontSee('Quick style presets', false); + } +} diff --git a/tests/Feature/LegacyQrRedirectTest.php b/tests/Feature/LegacyQrRedirectTest.php index 07b3903..7936b0f 100644 --- a/tests/Feature/LegacyQrRedirectTest.php +++ b/tests/Feature/LegacyQrRedirectTest.php @@ -42,4 +42,27 @@ class LegacyQrRedirectTest extends TestCase $response->assertRedirect(LadillLink::url('demo-event')); } + + public function test_registration_paths_are_not_redirected_to_ladill_link(): void + { + $middleware = new RedirectLegacyQrToLadillLink; + + foreach (['register', 'register/callback', 'registered/QRE-TESTREF123456'] as $suffix) { + $request = Request::create('/q/demo-event/'.$suffix, 'GET'); + $request->setRouteResolver(function () { + return new class + { + public function parameter(string $name): ?string + { + return $name === 'shortCode' ? 'demo-event' : null; + } + }; + }); + + $response = $middleware->handle($request, fn () => response('ok')); + + $this->assertSame(200, $response->getStatusCode(), $suffix); + $this->assertSame('ok', $response->getContent(), $suffix); + } + } }