Add speaker invitations with portal page and programme host picker.
Deploy Ladill Events / deploy (push) Successful in 47s
Deploy Ladill Events / deploy (push) Successful in 47s
Speakers require email, get a personal holding page with programme and stage links, auto-invites when programme hosts are saved, and manual send for events without a schedule; also fix wallet copy on event create and anchor attendee comms. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\QrCode;
|
||||
use App\Models\User;
|
||||
use App\Services\Events\EventSpeakerInviteService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EventSpeakerInviteTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutMiddleware(\App\Http\Middleware\EnsurePlatformSession::class);
|
||||
|
||||
config([
|
||||
'smtp.platform_api_key' => 'test-smtp-key',
|
||||
'smtp.platform_api_url' => 'https://ladill.test/api/smtp',
|
||||
'events.service_api_keys.meet' => 'test-meet-key',
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
config('billing.api_url').'/balance*' => Http::response(['balance_minor' => 50000]),
|
||||
'https://ladill.test/api/smtp/messages/send' => Http::response(['success' => true]),
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_speaker_portal_page_renders_for_valid_token(): void
|
||||
{
|
||||
$owner = User::factory()->create(['public_id' => 'usr_sp_portal']);
|
||||
|
||||
$event = QrCode::create([
|
||||
'user_id' => $owner->id,
|
||||
'short_code' => 'evt-sp-portal',
|
||||
'type' => QrCode::TYPE_EVENT,
|
||||
'label' => 'Speaker portal event',
|
||||
'payload' => [
|
||||
'content' => [
|
||||
'name' => 'Speaker portal event',
|
||||
'format' => 'virtual',
|
||||
'virtual_sessions' => [[
|
||||
'id' => 'vs-1',
|
||||
'title' => 'Opening keynote',
|
||||
'meet_room_type' => 'webinar',
|
||||
'join_url' => 'https://meet.test/r/room-uuid',
|
||||
]],
|
||||
'speakers' => [[
|
||||
'name' => 'Jane Speaker',
|
||||
'email' => 'jane@example.com',
|
||||
'role' => 'Keynote',
|
||||
'bio' => '',
|
||||
'invite_token' => 'speaker-token-abc',
|
||||
]],
|
||||
],
|
||||
'style' => [],
|
||||
],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->get(route('qr.public.speaker.portal', ['shortCode' => $event->short_code, 'token' => 'speaker-token-abc']))
|
||||
->assertOk()
|
||||
->assertSee('Jane Speaker', false)
|
||||
->assertSee('Opening keynote', false)
|
||||
->assertSee('speaker-token-abc', false);
|
||||
}
|
||||
|
||||
public function test_manual_speaker_invite_sends_email_and_records_metadata(): void
|
||||
{
|
||||
$owner = User::factory()->create(['public_id' => 'usr_sp_manual']);
|
||||
|
||||
$event = QrCode::create([
|
||||
'user_id' => $owner->id,
|
||||
'short_code' => 'evt-sp-manual',
|
||||
'type' => QrCode::TYPE_EVENT,
|
||||
'label' => 'Manual invite event',
|
||||
'payload' => [
|
||||
'content' => [
|
||||
'name' => 'Manual invite event',
|
||||
'speakers' => [[
|
||||
'name' => 'Alex Host',
|
||||
'email' => 'alex@example.com',
|
||||
'role' => 'Panelist',
|
||||
'bio' => '',
|
||||
]],
|
||||
],
|
||||
'style' => [],
|
||||
],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->from(route('speakers.show', $event))
|
||||
->post(route('speakers.invite', $event), ['email' => 'alex@example.com'])
|
||||
->assertRedirect(route('speakers.show', $event))
|
||||
->assertSessionHas('success');
|
||||
|
||||
Http::assertSent(fn ($request) => str_contains($request->url(), '/messages/send')
|
||||
&& $request['to'] === 'alex@example.com');
|
||||
|
||||
$event->refresh();
|
||||
$speaker = $event->content()['speakers'][0];
|
||||
$this->assertNotEmpty($speaker['invite_token']);
|
||||
$this->assertNotEmpty($speaker['invited_at']);
|
||||
$this->assertSame(EventSpeakerInviteService::SOURCE_MANUAL, $speaker['invite_source']);
|
||||
}
|
||||
|
||||
public function test_programme_save_invites_assigned_speaker_hosts(): void
|
||||
{
|
||||
$owner = User::factory()->create(['public_id' => 'usr_sp_prog']);
|
||||
|
||||
$programme = QrCode::create([
|
||||
'user_id' => $owner->id,
|
||||
'short_code' => 'itn-sp-prog',
|
||||
'type' => QrCode::TYPE_ITINERARY,
|
||||
'label' => 'Summit programme',
|
||||
'payload' => [
|
||||
'content' => [
|
||||
'title' => 'Summit programme',
|
||||
'days' => [[
|
||||
'label' => 'Day 1',
|
||||
'date' => '2026-07-01',
|
||||
'items' => [[
|
||||
'ref' => 'day0-item0',
|
||||
'time' => '09:00',
|
||||
'title' => 'Opening',
|
||||
'description' => '',
|
||||
'location' => '',
|
||||
'host' => 'Pat Speaker',
|
||||
'host_speaker_email' => 'pat@example.com',
|
||||
]],
|
||||
]],
|
||||
],
|
||||
'style' => [],
|
||||
],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$event = QrCode::create([
|
||||
'user_id' => $owner->id,
|
||||
'short_code' => 'evt-sp-prog',
|
||||
'type' => QrCode::TYPE_EVENT,
|
||||
'label' => 'Summit',
|
||||
'payload' => [
|
||||
'content' => [
|
||||
'name' => 'Summit',
|
||||
'programme_qr_id' => $programme->id,
|
||||
'speakers' => [[
|
||||
'name' => 'Pat Speaker',
|
||||
'email' => 'pat@example.com',
|
||||
'role' => 'Host',
|
||||
'bio' => '',
|
||||
]],
|
||||
],
|
||||
'style' => [],
|
||||
],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
app(EventSpeakerInviteService::class)->inviteProgrammeHosts($programme, $owner);
|
||||
|
||||
Http::assertSent(fn ($request) => $request['to'] === 'pat@example.com');
|
||||
|
||||
$event->refresh();
|
||||
$speaker = $event->content()['speakers'][0];
|
||||
$this->assertSame(EventSpeakerInviteService::SOURCE_PROGRAMME, $speaker['invite_source']);
|
||||
}
|
||||
|
||||
public function test_verify_speaker_api_returns_speaker_details(): void
|
||||
{
|
||||
$owner = User::factory()->create(['public_id' => 'usr_sp_api']);
|
||||
|
||||
$event = QrCode::create([
|
||||
'user_id' => $owner->id,
|
||||
'short_code' => 'evt-sp-api',
|
||||
'type' => QrCode::TYPE_EVENT,
|
||||
'label' => 'API event',
|
||||
'payload' => [
|
||||
'content' => [
|
||||
'name' => 'API event',
|
||||
'speakers' => [[
|
||||
'name' => 'Sam Panel',
|
||||
'email' => 'sam@example.com',
|
||||
'role' => 'Panelist',
|
||||
'bio' => '',
|
||||
'invite_token' => 'verify-token-xyz',
|
||||
]],
|
||||
],
|
||||
'style' => [],
|
||||
],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->withToken('test-meet-key')
|
||||
->postJson('/api/service/v1/events/'.$event->id.'/verify-speaker', [
|
||||
'owner_ref' => $owner->public_id,
|
||||
'speaker_token' => 'verify-token-xyz',
|
||||
])->assertOk()
|
||||
->assertJsonPath('speaker.email', 'sam@example.com')
|
||||
->assertJsonPath('speaker.name', 'Sam Panel');
|
||||
}
|
||||
|
||||
public function test_speaker_roster_requires_email(): void
|
||||
{
|
||||
$owner = User::factory()->create(['public_id' => 'usr_sp_req_email']);
|
||||
|
||||
$event = QrCode::create([
|
||||
'user_id' => $owner->id,
|
||||
'short_code' => 'evt-sp-email-req',
|
||||
'type' => QrCode::TYPE_EVENT,
|
||||
'label' => 'Email required',
|
||||
'payload' => ['content' => ['name' => 'Email required'], 'style' => []],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->actingAs($owner)
|
||||
->from(route('speakers.show', $event))
|
||||
->put(route('speakers.update', $event), [
|
||||
'speakers' => [
|
||||
['name' => 'No Email Person', 'email' => '', 'role' => '', 'bio' => ''],
|
||||
],
|
||||
])
|
||||
->assertSessionHasErrors('speakers.0.email');
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class EventSpeakerManagementTest extends TestCase
|
||||
'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' => []],
|
||||
'payload' => ['content' => ['name' => 'Leadership summit', 'speakers' => [['name' => 'Ada Lovelace', 'email' => 'ada@example.com', 'role' => 'Keynote', 'bio' => '']]], 'style' => []],
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
@@ -53,7 +53,7 @@ class EventSpeakerManagementTest extends TestCase
|
||||
'payload' => [
|
||||
'content' => [
|
||||
'name' => 'Summit day',
|
||||
'speakers' => [['name' => 'Ada Lovelace', 'email' => '', 'role' => 'Keynote', 'bio' => '']],
|
||||
'speakers' => [['name' => 'Ada Lovelace', 'email' => 'ada@example.com', 'role' => 'Keynote', 'bio' => '']],
|
||||
],
|
||||
'style' => [],
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user