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', ]); // Create org admin for approval $this->orgAdmin = User::factory()->create(); $this->orgAdmin->assignRole('super_admin'); $this->organisation->users()->attach($this->orgAdmin, ['role' => 'org_admin']); } /** * The golden path: register → approve → portal/me works. */ public function test_full_flow_register_approve_portal_me(): void { Mail::fake(); // ── Step 1: Volunteer registers via public form ── $regResponse = $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [ 'first_name' => 'Vrijwilliger', 'last_name' => 'Test', 'email' => 'vrijwilliger@test.nl', 'password' => 'Wachtwoord1', ]); $regResponse->assertStatus(201); // Verify person was created with user_id $person = Person::where('email', 'vrijwilliger@test.nl')->first(); $this->assertNotNull($person, 'Person record should exist'); $this->assertNotNull($person->user_id, 'Person should have user_id after registration'); $this->assertEquals('pending', $person->status); $user = User::where('email', 'vrijwilliger@test.nl')->first(); $this->assertNotNull($user, 'User account should exist'); $this->assertEquals($user->id, $person->user_id, 'Person.user_id should match created user'); // ── Step 2: Organizer approves the volunteer ─��� Sanctum::actingAs($this->orgAdmin); $approveResponse = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons/{$person->id}/approve" ); $approveResponse->assertOk(); // Verify person is now approved $person->refresh(); $this->assertEquals('approved', $person->status); $this->assertNotNull($person->user_id, 'user_id should still be set after approval'); // ── Step 3: Volunteer logs into portal ── Sanctum::actingAs($user); // Step 3a: GET /auth/me should return portal_events $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'); // Step 3b: GET /portal/me should return person details $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'); } /** * Festival hierarchy: register via sub-event slug → portal/me with parent event_id. */ public function test_full_flow_with_festival_sub_event(): void { Mail::fake(); // Create festival hierarchy $festival = Event::factory()->festival()->create([ 'organisation_id' => $this->organisation->id, 'status' => 'registration_open', ]); $subEvent = Event::factory()->subEvent($festival)->create([ 'status' => 'registration_open', ]); // ── Step 1: Register via sub-event route ── $regResponse = $this->postJson("/api/v1/events/{$subEvent->id}/volunteer-register", [ 'first_name' => 'Festival', 'last_name' => 'Ganger', 'email' => 'festival@test.nl', 'password' => 'Wachtwoord1', ]); $regResponse->assertStatus(201); // Person should be linked to parent (festival), not sub-event $person = Person::where('email', 'festival@test.nl')->first(); $this->assertNotNull($person); $this->assertEquals($festival->id, $person->event_id, 'Person should be linked to parent event'); $this->assertNotNull($person->user_id); $user = User::where('email', 'festival@test.nl')->first(); // ── Step 2: Approve ── Sanctum::actingAs($this->orgAdmin); $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$festival->id}/persons/{$person->id}/approve" )->assertOk(); // ── Step 3: Portal access ── Sanctum::actingAs($user); // portal/me with parent event ID should work $this->getJson("/api/v1/portal/me?event_id={$festival->id}") ->assertOk() ->assertJsonPath('data.email', 'festival@test.nl'); // portal/me with sub-event ID should also work (controller resolves to parent) $this->getJson("/api/v1/portal/me?event_id={$subEvent->id}") ->assertOk() ->assertJsonPath('data.email', 'festival@test.nl'); } /** * Returning volunteer: existing user registers for new event. */ public function test_returning_volunteer_portal_access(): void { Mail::fake(); $existingUser = User::factory()->create([ 'first_name' => 'Bestaand', 'last_name' => 'Lid', 'email' => 'bestaand@test.nl', 'password' => \Illuminate\Support\Facades\Hash::make('BestaandWw1'), ]); // Register for event with existing account $regResponse = $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [ 'first_name' => 'Bestaand', 'last_name' => 'Lid', 'email' => 'bestaand@test.nl', 'password' => 'BestaandWw1', ]); $regResponse->assertStatus(201); $person = Person::where('email', 'bestaand@test.nl')->first(); $this->assertEquals($existingUser->id, $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(); // Portal access Sanctum::actingAs($existingUser); $this->getJson('/api/v1/auth/me') ->assertOk() ->assertJsonCount(1, 'data.portal_events'); $this->getJson("/api/v1/portal/me?event_id={$this->event->id}") ->assertOk() ->assertJsonPath('data.status', 'approved'); } /** * Organizer-created person (no user account) → identity match confirmed → portal/me. */ public function test_organizer_created_person_then_identity_linked(): void { // ── Step 1: Organizer creates a person manually ── $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); $storeResponse = $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', ] ); $storeResponse->assertStatus(201); $person = Person::where('email', 'handmatig@test.nl')->first(); $this->assertNotNull($person); $this->assertNull($person->user_id, 'Organizer-created person should not have user_id'); // ── Step 2: Portal access should fail (no user_id link) ── Sanctum::actingAs($user); $this->getJson("/api/v1/portal/me?event_id={$this->event->id}") ->assertStatus(404); // ── Step 3: Confirm identity match → links user_id ── $match = $person->pendingIdentityMatch; $this->assertNotNull($match, 'Identity match should have been auto-detected'); Sanctum::actingAs($this->orgAdmin); $this->postJson( "/api/v1/organisations/{$this->organisation->id}/identity-matches/{$match->id}/confirm" )->assertOk(); $person->refresh(); $this->assertEquals($user->id, $person->user_id, 'user_id should be set after identity confirm'); // ── Step 4: Portal access should now work ── Sanctum::actingAs($user); $this->getJson("/api/v1/portal/me?event_id={$this->event->id}") ->assertOk() ->assertJsonPath('data.email', 'handmatig@test.nl'); } /** * Pending volunteer can see their status via portal (before approval). */ public function test_pending_volunteer_can_access_portal_me(): void { Mail::fake(); $this->postJson("/api/v1/events/{$this->event->id}/volunteer-register", [ 'first_name' => 'Wachtend', 'last_name' => 'Vrijwilliger', 'email' => 'wachtend@test.nl', 'password' => 'Wachtwoord1', ])->assertStatus(201); $user = User::where('email', 'wachtend@test.nl')->first(); Sanctum::actingAs($user); // Even without approval, portal/me should work (just shows pending status) $this->getJson("/api/v1/portal/me?event_id={$this->event->id}") ->assertOk() ->assertJsonPath('data.status', 'pending'); } }