create(); $organisation = Organisation::query()->find($event->organisation_id) ?? Organisation::factory()->create(); $crowdType = CrowdType::factory()->create([ 'organisation_id' => $organisation->id, 'is_active' => true, ]); $submission = $this->makeSubmission($event, 'race@test.nl'); DB::transaction(function () use ($submission, $crowdType): void { // Transaction A inside lockForUpdate window โ€” finds no Person yet $existing = Person::query() ->withoutGlobalScopes() ->where('email', 'race@test.nl') ->where('event_id', $submission->event_id) ->lockForUpdate() ->first(); $this->assertNull($existing); // Inject: simulate transaction B has already inserted the Person // (in real production, lockForUpdate would block this; we // simulate the post-block state to assert recovery semantics) DB::table('persons')->insert([ 'id' => (string) Str::ulid(), 'event_id' => $submission->event_id, 'crowd_type_id' => $crowdType->id, 'email' => 'race@test.nl', 'first_name' => 'Pre', 'last_name' => 'Existing', 'is_blacklisted' => false, 'created_at' => now(), 'updated_at' => now(), ]); // Transaction A's firstOrCreate must recover and return the // existing row rather than creating a duplicate. $person = $this->app->make(PersonProvisioner::class) ->provisionFromSubmission($submission); $this->assertSame('race@test.nl', $person->email); $this->assertSame( 1, Person::query() ->withoutGlobalScopes() ->where('email', 'race@test.nl') ->where('event_id', $submission->event_id) ->count(), ); }); } private function makeSubmission(Event $event, string $email): FormSubmission { $schema = FormSchema::factory()->create(['organisation_id' => $event->organisation_id]); $emailField = FormField::factory()->create(['form_schema_id' => $schema->id, 'slug' => 'email']); $binding = FormFieldBinding::factory()->forField($emailField)->entityOwned('person', 'email') ->create(['is_identity_key' => true, 'merge_strategy' => 'overwrite', 'trust_level' => 80]); $submission = FormSubmission::factory()->forEvent($event)->create(['form_schema_id' => $schema->id]); $submission->schema_snapshot = [ 'fields' => [[ 'id' => (string) $emailField->id, 'slug' => 'email', 'sort_order' => 0, 'bindings' => [[ 'id' => (string) $binding->id, 'mode' => 'entity_owned', 'entity' => 'person', 'column' => 'email', 'merge_strategy' => 'overwrite', 'trust_level' => 80, 'is_identity_key' => true, ]], ]], ]; $submission->save(); $row = new FormValue(); $row->form_submission_id = $submission->id; $row->form_field_id = $emailField->id; $row->setAttribute('value', $email); $row->value_anonymised = false; $row->save(); return $submission->fresh(); } }