seed(RoleSeeder::class); $this->organisation = Organisation::factory()->create(); $this->volunteerCrowdType = CrowdType::factory()->systemType('VOLUNTEER')->create([ 'organisation_id' => $this->organisation->id, ]); $this->event = Event::factory()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'registration_open', ]); $this->orgAdmin = User::factory()->create(); $this->orgAdmin->assignRole('super_admin'); $this->organisation->users()->attach($this->orgAdmin, ['role' => 'org_admin']); } /** * Golden path: register → person without user → approve → user created → portal works. */ public function test_full_flow_register_approve_creates_user_and_portal_works(): void { Mail::fake(); // ── Step 1: Volunteer registers (no password) ── $regResponse = $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [ 'first_name' => 'Vrijwilliger', 'last_name' => 'Test', 'email' => 'vrijwilliger@test.nl', ]); $regResponse->assertStatus(201); $person = Person::where('email', 'vrijwilliger@test.nl')->first(); $this->assertNotNull($person); $this->assertNull($person->user_id, 'Person should NOT have user_id after registration'); $this->assertEquals('pending', $person->status); $this->assertEquals('self', $person->registration_source); // No user account should exist yet $this->assertDatabaseMissing('users', ['email' => 'vrijwilliger@test.nl']); // ── Step 2: Organizer approves ── Sanctum::actingAs($this->orgAdmin); $approveResponse = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons/{$person->id}/approve" ); $approveResponse->assertOk(); // Approval should have created user account and linked it $person->refresh(); $this->assertEquals('approved', $person->status); $this->assertNotNull($person->user_id, 'user_id should be set after approval'); $user = User::where('email', 'vrijwilliger@test.nl')->first(); $this->assertNotNull($user, 'User account should be created on approval'); $this->assertEquals($person->user_id, $user->id); // ── Step 3: Volunteer accesses portal ── Sanctum::actingAs($user); $meResponse = $this->getJson('/api/v1/auth/me'); $meResponse->assertOk(); $meResponse->assertJsonCount(1, 'data.portal_events'); $meResponse->assertJsonPath('data.portal_events.0.event_id', $this->event->id); $meResponse->assertJsonPath('data.portal_events.0.person_status', 'approved'); $portalMeResponse = $this->getJson("/api/v1/portal/me?event_id={$this->event->id}"); $portalMeResponse->assertOk(); $portalMeResponse->assertJsonPath('data.email', 'vrijwilliger@test.nl'); $portalMeResponse->assertJsonPath('data.status', 'approved'); } /** * Approval links existing user by person.email. */ public function test_approve_links_existing_user_by_person_email(): void { Mail::fake(); // Pre-existing user account $existingUser = User::factory()->create(['email' => 'bestaand@test.nl']); // Register with same email $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [ 'first_name' => 'Bestaand', 'last_name' => 'Lid', 'email' => 'bestaand@test.nl', ])->assertStatus(201); $person = Person::where('email', 'bestaand@test.nl')->first(); $this->assertNull($person->user_id); // Approve Sanctum::actingAs($this->orgAdmin); $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons/{$person->id}/approve" )->assertOk(); // Should link to existing user, not create a new one $person->refresh(); $this->assertEquals($existingUser->id, $person->user_id); $this->assertEquals(1, User::where('email', 'bestaand@test.nl')->count()); } /** * Festival hierarchy: register via sub-event, portal works with both IDs. */ public function test_full_flow_with_festival_sub_event(): void { Mail::fake(); $festival = Event::factory()->festival()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'registration_open', ]); $subEvent = Event::factory()->subEvent($festival)->create([ 'status' => 'registration_open', ]); // Register via sub-event $this->postJson("/api/v1/events/{$subEvent->id}/volunteer-register", [ 'first_name' => 'Festival', 'last_name' => 'Ganger', 'email' => 'festival@test.nl', ])->assertStatus(201); $person = Person::where('email', 'festival@test.nl')->first(); $this->assertEquals($festival->id, $person->event_id, 'Person should be linked to parent event'); // Approve Sanctum::actingAs($this->orgAdmin); $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$festival->id}/persons/{$person->id}/approve" )->assertOk(); $person->refresh(); $user = User::find($person->user_id); // Portal access Sanctum::actingAs($user); $this->getJson("/api/v1/portal/me?event_id={$festival->id}") ->assertOk() ->assertJsonPath('data.email', 'festival@test.nl'); $this->getJson("/api/v1/portal/me?event_id={$subEvent->id}") ->assertOk() ->assertJsonPath('data.email', 'festival@test.nl'); } /** * Authenticated registration still links user_id directly. */ public function test_authenticated_registration_links_user_directly(): void { Mail::fake(); $user = User::factory()->create([ 'first_name' => 'Ingelogd', 'last_name' => 'Gebruiker', 'email' => 'ingelogd@test.nl', ]); $this->organisation->users()->attach($user, ['role' => 'org_member']); Sanctum::actingAs($user); $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", []) ->assertStatus(201); $person = Person::where('email', 'ingelogd@test.nl')->first(); $this->assertEquals($user->id, $person->user_id, 'Authenticated registration should set user_id directly'); } /** * Approval skips account creation if person already has user_id (authenticated registration). */ public function test_approve_skips_account_creation_if_user_already_linked(): void { Mail::fake(); $user = User::factory()->create(['email' => 'al-gelinkt@test.nl']); $this->organisation->users()->attach($user, ['role' => 'org_member']); // Authenticated registration sets user_id Sanctum::actingAs($user); $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", []) ->assertStatus(201); $person = Person::where('email', 'al-gelinkt@test.nl')->first(); $this->assertEquals($user->id, $person->user_id); // Approve — should not create another user Sanctum::actingAs($this->orgAdmin); $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons/{$person->id}/approve" )->assertOk(); $person->refresh(); $this->assertEquals($user->id, $person->user_id); $this->assertEquals(1, User::where('email', 'al-gelinkt@test.nl')->count()); } /** * Organizer-created person → identity match confirmed → portal/me works. */ public function test_organizer_created_person_then_identity_linked(): void { $user = User::factory()->create([ 'first_name' => 'Handmatig', 'last_name' => 'Toegevoegd', 'email' => 'handmatig@test.nl', ]); $this->organisation->users()->attach($user, ['role' => 'org_member']); Sanctum::actingAs($this->orgAdmin); $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons", [ 'crowd_type_id' => $this->volunteerCrowdType->id, 'first_name' => 'Handmatig', 'last_name' => 'Toegevoegd', 'email' => 'handmatig@test.nl', 'status' => 'approved', ] )->assertStatus(201); $person = Person::where('email', 'handmatig@test.nl')->first(); $this->assertNull($person->user_id); // Portal fails without user link Sanctum::actingAs($user); $this->getJson("/api/v1/portal/me?event_id={$this->event->id}") ->assertStatus(404); // Confirm identity match $match = $person->pendingIdentityMatch; $this->assertNotNull($match); Sanctum::actingAs($this->orgAdmin); $this->postJson( "/api/v1/organisations/{$this->organisation->id}/identity-matches/{$match->id}/confirm" )->assertOk(); // Portal now works Sanctum::actingAs($user); $this->getJson("/api/v1/portal/me?event_id={$this->event->id}") ->assertOk() ->assertJsonPath('data.email', 'handmatig@test.nl'); } /** * Fuzzy name matching is skipped for self-registered persons. */ public function test_fuzzy_name_match_skipped_for_self_registered(): void { Mail::fake(); // Create a user with similar name but different email $existingUser = User::factory()->create([ 'first_name' => 'Jan', 'last_name' => 'de Vries', 'email' => 'jan.devries@other.nl', ]); $this->organisation->users()->attach($existingUser, ['role' => 'org_member']); // Self-register with same name but different email $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [ 'first_name' => 'Jan', 'last_name' => 'de Vries', 'email' => 'jan@voorbeeld.nl', ])->assertStatus(201); $person = Person::where('email', 'jan@voorbeeld.nl')->first(); // Should NOT have a fuzzy name match (self-registered) $this->assertNull($person->pendingIdentityMatch); } }