seed(RoleSeeder::class); $this->org = Organisation::factory()->create(); $this->event = Event::factory()->create(['organisation_id' => $this->org->id]); $this->schema = FormSchema::factory()->create([ 'organisation_id' => $this->org->id, 'purpose' => FormPurpose::EVENT_REGISTRATION, 'owner_type' => 'event', 'owner_id' => $this->event->id, ]); } /** @param array $overrides */ private function submission(array $overrides = []): FormSubmission { return FormSubmission::create(array_merge([ 'form_schema_id' => $this->schema->id, 'subject_type' => null, 'subject_id' => null, 'status' => FormSubmissionStatus::SUBMITTED->value, 'submitted_at' => now(), 'is_test' => false, 'public_submitter_email' => 'default@example.test', ], $overrides)); } public function test_returns_empty_when_no_other_submissions_exist(): void { $current = $this->submission(['public_submitter_email' => 'only@example.test']); $priors = app(FormSubmissionDuplicateDetector::class)->findPriorSubmissions($current); $this->assertTrue($priors->isEmpty()); } public function test_returns_one_prior_for_same_email_same_schema_submitted(): void { $this->submission([ 'public_submitter_email' => 'dup@example.test', 'submitted_at' => now()->subDay(), ]); $current = $this->submission(['public_submitter_email' => 'dup@example.test']); $priors = app(FormSubmissionDuplicateDetector::class)->findPriorSubmissions($current); $this->assertSame(1, $priors->count()); } public function test_returns_priors_ordered_by_submitted_at_ascending(): void { $first = $this->submission([ 'public_submitter_email' => 'dup@example.test', 'submitted_at' => now()->subDays(3), ]); $second = $this->submission([ 'public_submitter_email' => 'dup@example.test', 'submitted_at' => now()->subDays(1), ]); $current = $this->submission(['public_submitter_email' => 'dup@example.test']); $priors = app(FormSubmissionDuplicateDetector::class)->findPriorSubmissions($current); $this->assertSame(2, $priors->count()); $this->assertSame($first->id, $priors[0]->id); $this->assertSame($second->id, $priors[1]->id); } public function test_excludes_current_submission(): void { $current = $this->submission(['public_submitter_email' => 'solo@example.test']); $priors = app(FormSubmissionDuplicateDetector::class)->findPriorSubmissions($current); $this->assertFalse($priors->contains('id', $current->id)); } public function test_excludes_drafts(): void { $this->submission([ 'public_submitter_email' => 'dup@example.test', 'status' => FormSubmissionStatus::DRAFT->value, 'submitted_at' => null, ]); $current = $this->submission(['public_submitter_email' => 'dup@example.test']); $priors = app(FormSubmissionDuplicateDetector::class)->findPriorSubmissions($current); $this->assertTrue($priors->isEmpty()); } public function test_excludes_other_schemas_with_same_email(): void { $otherSchema = FormSchema::factory()->create([ 'organisation_id' => $this->org->id, 'purpose' => FormPurpose::EVENT_REGISTRATION, 'owner_type' => 'event', 'owner_id' => $this->event->id, ]); FormSubmission::create([ 'form_schema_id' => $otherSchema->id, 'subject_type' => null, 'subject_id' => null, 'status' => FormSubmissionStatus::SUBMITTED->value, 'submitted_at' => now()->subDay(), 'is_test' => false, 'public_submitter_email' => 'scope@example.test', ]); $current = $this->submission(['public_submitter_email' => 'scope@example.test']); $priors = app(FormSubmissionDuplicateDetector::class)->findPriorSubmissions($current); $this->assertTrue($priors->isEmpty()); } public function test_returns_empty_when_current_submission_has_no_email(): void { $current = $this->submission(['public_submitter_email' => null]); $priors = app(FormSubmissionDuplicateDetector::class)->findPriorSubmissions($current); $this->assertTrue($priors->isEmpty()); } public function test_email_match_is_case_insensitive_and_trimmed(): void { $this->submission([ 'public_submitter_email' => ' Dup@Example.TEST ', 'submitted_at' => now()->subDay(), ]); $current = $this->submission(['public_submitter_email' => 'dup@example.test']); $priors = app(FormSubmissionDuplicateDetector::class)->findPriorSubmissions($current); $this->assertSame(1, $priors->count()); } public function test_format_for_response_shapes_count_and_first_date(): void { $first = $this->submission([ 'public_submitter_email' => 'dup@example.test', 'submitted_at' => now()->subDays(3), ]); $this->submission([ 'public_submitter_email' => 'dup@example.test', 'submitted_at' => now()->subDay(), ]); $current = $this->submission(['public_submitter_email' => 'dup@example.test']); $payload = app(FormSubmissionDuplicateDetector::class)->formatForResponse($current); $this->assertIsArray($payload); $this->assertSame(2, $payload['count']); $this->assertSame( $first->submitted_at->toIso8601String(), $payload['first_submitted_at'], ); } public function test_format_for_response_returns_null_when_no_priors(): void { $current = $this->submission(['public_submitter_email' => 'only@example.test']); $payload = app(FormSubmissionDuplicateDetector::class)->formatForResponse($current); $this->assertNull($payload); } }