seed(RoleSeeder::class); $this->organisation = Organisation::factory()->create(); $this->otherOrganisation = Organisation::factory()->create(); $this->orgAdmin = User::factory()->create(); $this->organisation->users()->attach($this->orgAdmin, ['role' => 'org_admin']); $this->outsider = User::factory()->create(); $this->otherOrganisation->users()->attach($this->outsider, ['role' => 'org_admin']); $this->event = Event::factory()->create(['organisation_id' => $this->organisation->id]); $this->crowdType = CrowdType::factory()->systemType('VOLUNTEER')->create([ 'organisation_id' => $this->organisation->id, ]); $this->crewType = CrowdType::factory()->systemType('CREW')->create([ 'organisation_id' => $this->organisation->id, ]); $this->identityService = app(PersonIdentityService::class); } // ────────────────────────────────────────────────────── // Detection tests (10) // ────────────────────────────────────────────────────── public function test_email_match_person_email_equals_user_email_in_same_org(): void { $matchUser = User::factory()->create(['email' => 'jan@example.nl']); $this->organisation->users()->attach($matchUser, ['role' => 'org_member']); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'email' => 'jan@example.nl', 'user_id' => null, ]); $matches = $this->identityService->detectMatches($person); $this->assertCount(1, $matches); $this->assertEquals(IdentityMatchMethod::EMAIL, $matches->first()->matched_on); $this->assertEquals(IdentityMatchConfidence::HIGH, $matches->first()->confidence); $this->assertEquals(IdentityMatchStatus::PENDING, $matches->first()->status); } public function test_email_match_different_org_creates_no_match(): void { $matchUser = User::factory()->create(['email' => 'foreign@example.nl']); $this->otherOrganisation->users()->attach($matchUser, ['role' => 'org_member']); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'email' => 'foreign@example.nl', 'user_id' => null, ]); $matches = $this->identityService->detectMatches($person); $this->assertCount(0, $matches); } public function test_no_match_when_person_already_has_user_id(): void { $linkedUser = User::factory()->create(['email' => 'linked@example.nl']); $this->organisation->users()->attach($linkedUser, ['role' => 'org_member']); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'email' => 'linked@example.nl', 'user_id' => $linkedUser->id, ]); $matches = $this->identityService->detectMatches($person); $this->assertCount(0, $matches); } public function test_no_match_when_person_email_matches_no_user(): void { $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'email' => 'nomatch@nowhere.test', 'first_name' => 'Unique', 'last_name' => 'Nobody', 'user_id' => null, ]); $matches = $this->identityService->detectMatches($person); $this->assertCount(0, $matches); } public function test_fuzzy_name_match_similar_first_name(): void { $matchUser = User::factory()->create([ 'first_name' => 'Bert', 'last_name' => 'Hausmans', 'email' => 'bert@org.nl', ]); $this->organisation->users()->attach($matchUser, ['role' => 'org_member']); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'first_name' => 'Burt', 'last_name' => 'Hausmans', 'email' => 'burt@different.nl', 'user_id' => null, ]); $matches = $this->identityService->detectMatches($person); $this->assertCount(1, $matches); $this->assertEquals(IdentityMatchMethod::NAME_FUZZY, $matches->first()->matched_on); $this->assertEquals(IdentityMatchConfidence::MEDIUM, $matches->first()->confidence); } public function test_no_fuzzy_match_when_names_too_different(): void { $matchUser = User::factory()->create([ 'first_name' => 'Jan', 'last_name' => 'de Vries', 'email' => 'jan@org.nl', ]); $this->organisation->users()->attach($matchUser, ['role' => 'org_member']); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'first_name' => 'Johannes', 'last_name' => 'de Vries', 'email' => 'johannes@different.nl', 'user_id' => null, ]); $matches = $this->identityService->detectMatches($person); $this->assertCount(0, $matches); } public function test_fuzzy_name_match_with_dob_upgrades_to_high(): void { $dob = '1990-06-15'; $matchUser = User::factory()->create([ 'first_name' => 'Lisa', 'last_name' => 'Bakker', 'email' => 'lisa@org.nl', 'date_of_birth' => $dob, ]); $this->organisation->users()->attach($matchUser, ['role' => 'org_member']); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'first_name' => 'Liesa', 'last_name' => 'Bakker', 'email' => 'liesa@different.nl', 'date_of_birth' => $dob, 'user_id' => null, ]); $matches = $this->identityService->detectMatches($person); $this->assertCount(1, $matches); $this->assertEquals(IdentityMatchConfidence::HIGH, $matches->first()->confidence); $this->assertContains('date_of_birth', $matches->first()->match_details['matched_fields']); } public function test_fuzzy_name_without_dob_stays_medium(): void { $matchUser = User::factory()->create([ 'first_name' => 'Lisa', 'last_name' => 'Bakker', 'email' => 'lisa@org.nl', 'date_of_birth' => '1990-06-15', ]); $this->organisation->users()->attach($matchUser, ['role' => 'org_member']); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'first_name' => 'Liesa', 'last_name' => 'Bakker', 'email' => 'liesa@different.nl', 'date_of_birth' => '1991-01-01', // different DOB 'user_id' => null, ]); $matches = $this->identityService->detectMatches($person); $this->assertCount(1, $matches); $this->assertEquals(IdentityMatchConfidence::MEDIUM, $matches->first()->confidence); } public function test_previously_dismissed_pair_not_re_suggested(): void { $matchUser = User::factory()->create(['email' => 'dismissed@example.nl']); $this->organisation->users()->attach($matchUser, ['role' => 'org_member']); // Create person with different email first (so observer doesn't match on create) $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'email' => 'different@example.nl', 'first_name' => 'Unique', 'last_name' => 'TestPerson', 'user_id' => null, ]); // Create and dismiss match manually PersonIdentityMatch::factory()->dismissed()->create([ 'person_id' => $person->id, 'matched_user_id' => $matchUser->id, ]); // Update email to match the user $person->update(['email' => 'dismissed@example.nl']); $person->refresh(); // Calling detectMatches should NOT re-suggest the dismissed pair $matches = $this->identityService->detectMatches($person); $this->assertCount(0, $matches); } public function test_observer_fires_on_person_create(): void { $matchUser = User::factory()->create(['email' => 'observer@example.nl']); $this->organisation->users()->attach($matchUser, ['role' => 'org_member']); Sanctum::actingAs($this->orgAdmin); $this->postJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons", [ 'crowd_type_id' => $this->crowdType->id, 'first_name' => 'Test', 'last_name' => 'Observer', 'email' => 'observer@example.nl', ]); $this->assertDatabaseHas('person_identity_matches', [ 'matched_user_id' => $matchUser->id, 'matched_on' => IdentityMatchMethod::EMAIL->value, 'status' => IdentityMatchStatus::PENDING->value, ]); } // ────────────────────────────────────────────────────── // Confirm tests (5) // ────────────────────────────────────────────────────── public function test_confirm_pending_sets_user_id_and_status(): void { $matchUser = User::factory()->create(['email' => 'confirm@example.nl']); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'email' => 'confirm@example.nl', 'user_id' => null, ]); $match = PersonIdentityMatch::factory()->create([ 'person_id' => $person->id, 'matched_user_id' => $matchUser->id, 'status' => IdentityMatchStatus::PENDING, ]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/identity-matches/{$match->id}/confirm" ); $response->assertOk(); $person->refresh(); $match->refresh(); $this->assertEquals($matchUser->id, $person->user_id); $this->assertEquals(IdentityMatchStatus::CONFIRMED, $match->status); $this->assertEquals($this->orgAdmin->id, $match->confirmed_by_user_id); $this->assertNotNull($match->confirmed_at); } public function test_confirm_dismisses_other_pending_matches(): void { $matchUser1 = User::factory()->create(); $matchUser2 = User::factory()->create(); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => null, ]); $match1 = PersonIdentityMatch::factory()->create([ 'person_id' => $person->id, 'matched_user_id' => $matchUser1->id, 'status' => IdentityMatchStatus::PENDING, ]); $match2 = PersonIdentityMatch::factory()->create([ 'person_id' => $person->id, 'matched_user_id' => $matchUser2->id, 'status' => IdentityMatchStatus::PENDING, ]); $this->identityService->confirmMatch($match1, $this->orgAdmin); $match2->refresh(); $this->assertEquals(IdentityMatchStatus::DISMISSED, $match2->status); $this->assertEquals($this->orgAdmin->id, $match2->dismissed_by_user_id); } public function test_confirm_when_user_already_linked_same_crowd_type_fails(): void { $matchUser = User::factory()->create(); // Already linked with same crowd type Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => $matchUser->id, ]); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => null, ]); $match = PersonIdentityMatch::factory()->create([ 'person_id' => $person->id, 'matched_user_id' => $matchUser->id, 'status' => IdentityMatchStatus::PENDING, ]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/identity-matches/{$match->id}/confirm" ); $response->assertStatus(422); } public function test_confirm_non_pending_fails(): void { $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => null, ]); $match = PersonIdentityMatch::factory()->confirmed()->create([ 'person_id' => $person->id, 'matched_user_id' => User::factory()->create()->id, ]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/identity-matches/{$match->id}/confirm" ); $response->assertStatus(422); } public function test_confirm_triggers_tag_sync(): void { $matchUser = User::factory()->create(['email' => 'tags@example.nl']); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'email' => 'tags@example.nl', 'user_id' => null, ]); $match = PersonIdentityMatch::factory()->create([ 'person_id' => $person->id, 'matched_user_id' => $matchUser->id, 'status' => IdentityMatchStatus::PENDING, ]); // Confirm — should not throw even if no tags exist $this->identityService->confirmMatch($match, $this->orgAdmin); $person->refresh(); $this->assertEquals($matchUser->id, $person->user_id); } // ────────────────────────────────────────────────────── // Dismiss tests (2) // ────────────────────────────────────────────────────── public function test_dismiss_pending_sets_status_dismissed(): void { $matchUser = User::factory()->create(['email' => 'dismiss@example.nl']); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'email' => 'dismiss@example.nl', 'user_id' => null, ]); $match = PersonIdentityMatch::factory()->create([ 'person_id' => $person->id, 'matched_user_id' => $matchUser->id, 'status' => IdentityMatchStatus::PENDING, ]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/identity-matches/{$match->id}/dismiss" ); $response->assertOk(); $match->refresh(); $this->assertEquals(IdentityMatchStatus::DISMISSED, $match->status); $this->assertEquals($this->orgAdmin->id, $match->dismissed_by_user_id); $this->assertNotNull($match->dismissed_at); $this->assertNull($person->fresh()->user_id); } public function test_dismiss_non_pending_fails(): void { $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => null, ]); $match = PersonIdentityMatch::factory()->dismissed()->create([ 'person_id' => $person->id, 'matched_user_id' => User::factory()->create()->id, ]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/identity-matches/{$match->id}/dismiss" ); $response->assertStatus(422); } // ────────────────────────────────────────────────────── // Revert tests (3) // ────────────────────────────────────────────────────── public function test_revert_confirmed_clears_user_id_and_sets_reverted(): void { $matchUser = User::factory()->create(); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => $matchUser->id, ]); $match = PersonIdentityMatch::factory()->confirmed()->create([ 'person_id' => $person->id, 'matched_user_id' => $matchUser->id, ]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/identity-matches/{$match->id}/revert" ); $response->assertOk(); $person->refresh(); $match->refresh(); $this->assertNull($person->user_id); $this->assertEquals(IdentityMatchStatus::REVERTED, $match->status); $this->assertEquals($this->orgAdmin->id, $match->reverted_by_user_id); $this->assertNotNull($match->reverted_at); } public function test_revert_non_confirmed_fails(): void { $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => null, ]); $match = PersonIdentityMatch::factory()->create([ 'person_id' => $person->id, 'matched_user_id' => User::factory()->create()->id, 'status' => IdentityMatchStatus::PENDING, ]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/identity-matches/{$match->id}/revert" ); $response->assertStatus(422); } public function test_revert_creates_activity_log(): void { $matchUser = User::factory()->create(); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => $matchUser->id, ]); $match = PersonIdentityMatch::factory()->confirmed()->create([ 'person_id' => $person->id, 'matched_user_id' => $matchUser->id, ]); Sanctum::actingAs($this->orgAdmin); $this->postJson( "/api/v1/organisations/{$this->organisation->id}/identity-matches/{$match->id}/revert" )->assertOk(); $this->assertDatabaseHas('activity_log', [ 'description' => 'person.identity.match_reverted', 'causer_id' => $this->orgAdmin->id, 'subject_id' => $person->id, ]); } // ────────────────────────────────────────────────────── // Bulk confirm tests (2) // ────────────────────────────────────────────────────── public function test_bulk_confirm_multiple_matches(): void { $matches = []; for ($i = 0; $i < 3; $i++) { $matchUser = User::factory()->create(); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'email' => "bulk{$i}@example.nl", 'user_id' => null, ]); $matches[] = PersonIdentityMatch::factory()->create([ 'person_id' => $person->id, 'matched_user_id' => $matchUser->id, 'status' => IdentityMatchStatus::PENDING, ]); } Sanctum::actingAs($this->orgAdmin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/identity-matches/bulk-confirm", ['match_ids' => collect($matches)->pluck('id')->toArray()] ); $response->assertOk(); $this->assertEquals(3, $response->json('confirmed')); $this->assertEmpty($response->json('errors')); } public function test_bulk_confirm_skips_conflicts(): void { $matchUser1 = User::factory()->create(); $person1 = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => null, ]); $match1 = PersonIdentityMatch::factory()->create([ 'person_id' => $person1->id, 'matched_user_id' => $matchUser1->id, 'status' => IdentityMatchStatus::PENDING, ]); // Match 2: user already linked in same event with same crowd type → should error $matchUser2 = User::factory()->create(); Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => $matchUser2->id, ]); $person2 = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => null, ]); $match2 = PersonIdentityMatch::factory()->create([ 'person_id' => $person2->id, 'matched_user_id' => $matchUser2->id, 'status' => IdentityMatchStatus::PENDING, ]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/identity-matches/bulk-confirm", ['match_ids' => [$match1->id, $match2->id]] ); $response->assertOk(); $this->assertEquals(1, $response->json('confirmed')); $this->assertCount(1, $response->json('errors')); } // ────────────────────────────────────────────────────── // Manual link tests (4) // ────────────────────────────────────────────────────── public function test_manual_link_sets_user_id_and_creates_match(): void { $targetUser = User::factory()->create(); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => null, ]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons/{$person->id}/manual-link", ['user_id' => $targetUser->id] ); $response->assertOk(); $person->refresh(); $this->assertEquals($targetUser->id, $person->user_id); $this->assertDatabaseHas('person_identity_matches', [ 'person_id' => $person->id, 'matched_user_id' => $targetUser->id, 'matched_on' => IdentityMatchMethod::MANUAL->value, 'confidence' => IdentityMatchConfidence::HIGH->value, 'status' => IdentityMatchStatus::CONFIRMED->value, ]); } public function test_manual_link_already_linked_person_fails(): void { $existingUser = User::factory()->create(); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => $existingUser->id, ]); $newUser = User::factory()->create(); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons/{$person->id}/manual-link", ['user_id' => $newUser->id] ); $response->assertStatus(422); } public function test_manual_link_user_already_at_event_same_crowd_type_fails(): void { $targetUser = User::factory()->create(); // User already linked at event with same crowd type Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => $targetUser->id, ]); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => null, ]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons/{$person->id}/manual-link", ['user_id' => $targetUser->id] ); $response->assertStatus(422); } public function test_manual_link_user_at_event_different_crowd_type_succeeds(): void { $targetUser = User::factory()->create(); // User linked as CREW Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crewType->id, 'user_id' => $targetUser->id, ]); // Try to link as VOLUNTEER (different crowd type) $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => null, ]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons/{$person->id}/manual-link", ['user_id' => $targetUser->id] ); $response->assertOk(); $person->refresh(); $this->assertEquals($targetUser->id, $person->user_id); } // ────────────────────────────────────────────────────── // Unlink tests (3) // ────────────────────────────────────────────────────── public function test_unlink_via_confirmed_match_reverts(): void { $matchUser = User::factory()->create(); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => $matchUser->id, ]); PersonIdentityMatch::factory()->confirmed()->create([ 'person_id' => $person->id, 'matched_user_id' => $matchUser->id, ]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons/{$person->id}/unlink" ); $response->assertOk(); $this->assertNull($person->fresh()->user_id); } public function test_unlink_without_match_record_works(): void { $matchUser = User::factory()->create(); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => $matchUser->id, ]); // No match record — direct link Sanctum::actingAs($this->orgAdmin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons/{$person->id}/unlink" ); $response->assertOk(); $this->assertNull($person->fresh()->user_id); } public function test_unlink_not_linked_person_fails(): void { $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => null, ]); Sanctum::actingAs($this->orgAdmin); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons/{$person->id}/unlink" ); $response->assertStatus(422); } // ────────────────────────────────────────────────────── // Authorization tests (2) // ────────────────────────────────────────────────────── public function test_unauthenticated_cannot_access_matches(): void { $response = $this->getJson( "/api/v1/organisations/{$this->organisation->id}/identity-matches" ); $response->assertUnauthorized(); } public function test_wrong_org_user_cannot_confirm(): void { $matchUser = User::factory()->create(); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'user_id' => null, ]); $match = PersonIdentityMatch::factory()->create([ 'person_id' => $person->id, 'matched_user_id' => $matchUser->id, 'status' => IdentityMatchStatus::PENDING, ]); Sanctum::actingAs($this->outsider); $response = $this->postJson( "/api/v1/organisations/{$this->organisation->id}/identity-matches/{$match->id}/confirm" ); $response->assertForbidden(); } // ────────────────────────────────────────────────────── // PersonResource tests // ────────────────────────────────────────────────────── public function test_person_resource_includes_has_user_account(): void { $matchUser = User::factory()->create(); Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'email' => 'linked@example.nl', 'user_id' => $matchUser->id, ]); Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'email' => 'unlinked@example.nl', 'user_id' => null, ]); Sanctum::actingAs($this->orgAdmin); $response = $this->getJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons"); $response->assertOk(); $linkedPerson = collect($response->json('data'))->firstWhere('email', 'linked@example.nl'); $unlinkedPerson = collect($response->json('data'))->firstWhere('email', 'unlinked@example.nl'); $this->assertTrue($linkedPerson['has_user_account']); $this->assertFalse($unlinkedPerson['has_user_account']); } public function test_person_resource_includes_pending_identity_match(): void { $matchUser = User::factory()->create(['email' => 'inline@example.nl']); $person = Person::factory()->create([ 'event_id' => $this->event->id, 'crowd_type_id' => $this->crowdType->id, 'email' => 'inline@example.nl', 'user_id' => null, ]); PersonIdentityMatch::factory()->create([ 'person_id' => $person->id, 'matched_user_id' => $matchUser->id, 'status' => IdentityMatchStatus::PENDING, ]); Sanctum::actingAs($this->orgAdmin); $response = $this->getJson("/api/v1/organisations/{$this->organisation->id}/events/{$this->event->id}/persons"); $response->assertOk(); $personData = collect($response->json('data'))->firstWhere('email', 'inline@example.nl'); $this->assertNotNull($personData); $this->assertArrayHasKey('pending_identity_match', $personData); $this->assertEquals($matchUser->id, $personData['pending_identity_match']['matched_user']['id']); } }