Return Meet attendees to join flow after event registration.
Deploy Ladill Events / deploy (push) Successful in 47s
Deploy Ladill Events / deploy (push) Successful in 47s
Add badge verification API, preserve meet_return through registration and payment, and link confirmation back to Meet with badge auto-login. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
|
||||
use App\Models\QrCode;
|
||||
use App\Models\User;
|
||||
use App\Services\Meet\EventMeetRoomLinkService;
|
||||
use App\Services\Meet\EventRegistrationVerifyService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -46,4 +47,24 @@ class ServiceMeetController extends Controller
|
||||
|
||||
return response()->json(['event' => $result]);
|
||||
}
|
||||
|
||||
public function verifyRegistration(Request $request, QrCode $event): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'owner_ref' => ['required', 'string', 'max:64'],
|
||||
'badge_code' => ['required', 'string', 'max:16'],
|
||||
]);
|
||||
|
||||
$owner = User::query()->where('public_id', $validated['owner_ref'])->firstOrFail();
|
||||
abort_unless($event->user_id === $owner->id, 403);
|
||||
abort_unless($event->type === QrCode::TYPE_EVENT, 404);
|
||||
|
||||
$result = app(EventRegistrationVerifyService::class)->verifyBadge($event, $validated['badge_code']);
|
||||
|
||||
if (! $result) {
|
||||
return response()->json(['message' => 'Badge not found.'], 404);
|
||||
}
|
||||
|
||||
return response()->json(['registration' => $result]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class EventRegistrationController extends Controller
|
||||
'attendee_email' => ['nullable', 'email', 'max:200'],
|
||||
'attendee_phone' => ['required', 'string', 'max:30'],
|
||||
'badge_fields' => ['nullable', 'array'],
|
||||
'meet_return' => ['nullable', 'string', 'max:2048'],
|
||||
]);
|
||||
|
||||
try {
|
||||
@@ -45,7 +46,7 @@ class EventRegistrationController extends Controller
|
||||
'paid' => $result['paid'],
|
||||
'checkout_url' => $result['checkout_url'],
|
||||
'badge_code' => $result['registration']->badge_code,
|
||||
'success_url' => LadillLink::path($qrCode->short_code, 'registered/'.$result['registration']->reference),
|
||||
'success_url' => $this->confirmedUrl($qrCode, $result['registration'], $request->input('meet_return')),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -60,22 +61,43 @@ class EventRegistrationController extends Controller
|
||||
try {
|
||||
$registration = $this->eventService->complete($reference);
|
||||
|
||||
return redirect()->away(LadillLink::path($shortCode, 'registered/'.$registration->reference));
|
||||
return redirect()->away($this->confirmedUrl($registration->qrCode, $registration));
|
||||
} catch (\Throwable $e) {
|
||||
return redirect()->away(LadillLink::url($shortCode))->with('order_error', 'Payment could not be verified. Reference: ' . $reference);
|
||||
}
|
||||
}
|
||||
|
||||
public function confirmed(string $shortCode, string $ref): View
|
||||
public function confirmed(Request $request, string $shortCode, string $ref): View
|
||||
{
|
||||
$registration = QrEventRegistration::query()
|
||||
->where('reference', $ref)
|
||||
->whereHas('qrCode', fn ($q) => $q->where('short_code', $shortCode))
|
||||
->firstOrFail();
|
||||
|
||||
$meetReturn = (string) $request->query('meet_return', '');
|
||||
if ($meetReturn === '') {
|
||||
$meetReturn = (string) (($registration->metadata ?? [])['meet_return'] ?? '');
|
||||
}
|
||||
|
||||
return view('public.qr.event-confirmed', [
|
||||
'registration' => $registration,
|
||||
'qrCode' => $registration->qrCode,
|
||||
'meetReturn' => $meetReturn,
|
||||
]);
|
||||
}
|
||||
|
||||
private function confirmedUrl(QrCode $qrCode, QrEventRegistration $registration, ?string $meetReturn = null): string
|
||||
{
|
||||
$url = LadillLink::path($qrCode->short_code, 'registered/'.$registration->reference);
|
||||
$meetReturn = trim((string) ($meetReturn ?? ''));
|
||||
if ($meetReturn === '') {
|
||||
$meetReturn = trim((string) (($registration->metadata ?? [])['meet_return'] ?? ''));
|
||||
}
|
||||
|
||||
if ($meetReturn !== '') {
|
||||
$url .= '?meet_return='.urlencode($meetReturn);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,10 @@ class QrScanController extends Controller
|
||||
}
|
||||
|
||||
if ($qrCode->usesLandingPage()) {
|
||||
return view('public.qr.landing', ['qrCode' => $qrCode]);
|
||||
return view('public.qr.landing', [
|
||||
'qrCode' => $qrCode,
|
||||
'meetReturn' => trim((string) $request->query('meet_return', '')),
|
||||
]);
|
||||
}
|
||||
|
||||
abort(404);
|
||||
|
||||
@@ -98,6 +98,9 @@ class EventRegistrationService
|
||||
}
|
||||
$amountMinor = (int) round($priceGhs * 100);
|
||||
|
||||
$meetReturn = trim((string) ($data['meet_return'] ?? ''));
|
||||
$metadata = $meetReturn !== '' ? ['meet_return' => $meetReturn] : null;
|
||||
|
||||
$registration = QrEventRegistration::create([
|
||||
'qr_code_id' => $qrCode->id,
|
||||
'user_id' => $qrCode->user_id,
|
||||
@@ -111,6 +114,7 @@ class EventRegistrationService
|
||||
'attendee_phone' => trim((string) ($data['attendee_phone'] ?? '')) ?: null,
|
||||
'badge_fields' => $badgeFields ?: null,
|
||||
'status' => $amountMinor > 0 ? QrEventRegistration::STATUS_PENDING : QrEventRegistration::STATUS_CONFIRMED,
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
|
||||
if ($amountMinor === 0) {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Meet;
|
||||
|
||||
use App\Models\QrCode;
|
||||
use App\Models\QrEventRegistration;
|
||||
|
||||
class EventRegistrationVerifyService
|
||||
{
|
||||
/** @return array<string, mixed>|null */
|
||||
public function verifyBadge(QrCode $event, string $badgeCode): ?array
|
||||
{
|
||||
abort_unless($event->type === QrCode::TYPE_EVENT, 404);
|
||||
|
||||
$normalized = strtoupper(trim($badgeCode));
|
||||
if ($normalized === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$registration = QrEventRegistration::query()
|
||||
->where('qr_code_id', $event->id)
|
||||
->where('badge_code', $normalized)
|
||||
->where('status', QrEventRegistration::STATUS_CONFIRMED)
|
||||
->first();
|
||||
|
||||
if (! $registration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'attendee_name' => $registration->attendee_name,
|
||||
'attendee_email' => $registration->attendee_email,
|
||||
'badge_code' => $registration->badge_code,
|
||||
'tier_name' => $registration->tier_name,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
$meetContinueUrl = '';
|
||||
if (! empty($meetReturn ?? '')) {
|
||||
$meetContinueUrl = $meetReturn . (str_contains($meetReturn, '?') ? '&' : '?') . 'badge=' . urlencode($registration->badge_code);
|
||||
} elseif ($joinUrl !== '') {
|
||||
$meetContinueUrl = $joinUrl . (str_contains($joinUrl, '?') ? '&' : '?') . 'badge=' . urlencode($registration->badge_code);
|
||||
}
|
||||
@endphp
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -81,11 +87,11 @@
|
||||
<p class="mt-5 text-xs text-slate-400">Show this badge code at check-in.</p>
|
||||
@endif
|
||||
|
||||
@if($joinUrl !== '')
|
||||
<a href="{{ $joinUrl }}"
|
||||
@if($meetContinueUrl !== '')
|
||||
<a href="{{ $meetContinueUrl }}"
|
||||
class="mt-6 inline-flex w-full items-center justify-center rounded-xl px-4 py-3 text-sm font-bold text-white shadow-sm transition hover:opacity-90"
|
||||
style="background:{{ $evColor }}">
|
||||
Join webinar
|
||||
Continue to session
|
||||
</a>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@@ -640,6 +640,7 @@
|
||||
$evHeading = $evMode === 'contributions' ? 'Make a contribution' : 'Register';
|
||||
$evRegRoute = $qrCode->publicPath('register');
|
||||
$evCsrf = csrf_token();
|
||||
$evMeetReturn = trim((string) ($meetReturn ?? ''));
|
||||
@endphp
|
||||
<div x-data="{
|
||||
mode: @js($evMode),
|
||||
@@ -648,6 +649,7 @@
|
||||
amount: '',
|
||||
name: '', email: '', phone: '',
|
||||
fields: {},
|
||||
meetReturn: @js($evMeetReturn),
|
||||
loading: false, errorMsg: '',
|
||||
showSheet: false, checkoutUrl: '',
|
||||
selectTier(n, p) { this.tier = n; this.tierPrice = p; },
|
||||
@@ -663,7 +665,7 @@
|
||||
const res = await fetch(@js($evRegRoute), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': @js($evCsrf) },
|
||||
body: JSON.stringify({ tier: this.tier, amount: this.mode === 'contributions' ? this.amount : null, attendee_name: this.name, attendee_email: this.email, attendee_phone: this.phone, badge_fields: this.fields }),
|
||||
body: JSON.stringify({ tier: this.tier, amount: this.mode === 'contributions' ? this.amount : null, attendee_name: this.name, attendee_email: this.email, attendee_phone: this.phone, badge_fields: this.fields, meet_return: this.meetReturn || null }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -15,6 +15,9 @@ Route::middleware(['auth.service:events'])->prefix('service/v1')->group(function
|
||||
Route::post('/events/{event}/link-room', [\App\Http\Controllers\Api\V1\ServiceMeetController::class, 'linkRoom'])
|
||||
->whereNumber('event')
|
||||
->name('api.service.events.link-room');
|
||||
Route::post('/events/{event}/verify-registration', [\App\Http\Controllers\Api\V1\ServiceMeetController::class, 'verifyRegistration'])
|
||||
->whereNumber('event')
|
||||
->name('api.service.events.verify-registration');
|
||||
});
|
||||
|
||||
Route::middleware(['auth:sanctum', \App\Http\Middleware\SetActingAccount::class])->prefix('v1')->group(function () {
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\QrCode;
|
||||
use App\Models\QrEventRegistration;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ServiceMeetVerifyRegistrationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $owner;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config(['events.service_api_keys.meet' => 'test-meet-key']);
|
||||
|
||||
$this->owner = User::factory()->create(['public_id' => 'usr_verify_badge']);
|
||||
}
|
||||
|
||||
public function test_service_api_verifies_confirmed_badge(): void
|
||||
{
|
||||
$event = QrCode::create([
|
||||
'user_id' => $this->owner->id,
|
||||
'short_code' => 'evt-verify-1',
|
||||
'type' => QrCode::TYPE_EVENT,
|
||||
'label' => 'Verify badge event',
|
||||
'payload' => [
|
||||
'content' => [
|
||||
'title' => 'Verify badge event',
|
||||
'format' => 'virtual',
|
||||
'virtual_sessions' => [],
|
||||
],
|
||||
'style' => [],
|
||||
],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
QrEventRegistration::create([
|
||||
'qr_code_id' => $event->id,
|
||||
'user_id' => $this->owner->id,
|
||||
'reference' => 'QRE-TESTVERIFY001',
|
||||
'badge_code' => 'BADGE999',
|
||||
'tier_name' => 'General',
|
||||
'amount_minor' => 0,
|
||||
'currency' => 'GHS',
|
||||
'attendee_name' => 'Ada Lovelace',
|
||||
'attendee_email' => 'ada@example.com',
|
||||
'status' => QrEventRegistration::STATUS_CONFIRMED,
|
||||
]);
|
||||
|
||||
$this->withToken('test-meet-key')
|
||||
->postJson('/api/service/v1/events/'.$event->id.'/verify-registration', [
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'badge_code' => 'badge999',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('registration.attendee_name', 'Ada Lovelace')
|
||||
->assertJsonPath('registration.badge_code', 'BADGE999');
|
||||
}
|
||||
|
||||
public function test_service_api_returns_not_found_for_unknown_badge(): void
|
||||
{
|
||||
$event = QrCode::create([
|
||||
'user_id' => $this->owner->id,
|
||||
'short_code' => 'evt-verify-2',
|
||||
'type' => QrCode::TYPE_EVENT,
|
||||
'label' => 'Missing badge event',
|
||||
'payload' => [
|
||||
'content' => [
|
||||
'title' => 'Missing badge event',
|
||||
'format' => 'virtual',
|
||||
],
|
||||
'style' => [],
|
||||
],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->withToken('test-meet-key')
|
||||
->postJson('/api/service/v1/events/'.$event->id.'/verify-registration', [
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'badge_code' => 'NOPE123',
|
||||
])
|
||||
->assertNotFound();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user