From fda80336337e4608e5c96e2fde56d79d2cfbeabf Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Thu, 23 Apr 2026 21:18:08 +0200 Subject: [PATCH] refactor(form-builder): make identity-match listener synchronous MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TriggerPersonIdentityMatchOnFormSubmit was queued, which meant identity_match_status stayed null in the HTTP response body for public event_registration submissions — the portal confirmation page rendered without the IdentityMatchBanner until a queue worker caught up. Eager state transitions belong in the request lifecycle. The listener is now an orchestrator that writes pending/matched/none synchronously. When FORM-05 proper lands with heavy matching logic (PersonIdentityService::detectMatchesByValues, fuzzy name matching over the whole org), the heavy work will dispatch as a separate queued job from within this same listener — sync state transition + async resolution. - Remove ShouldQueue, InteractsWithQueue trait, $queue property - Existing try/catch error containment unchanged (sibling listeners §31.10 tag sync, §31.3 shift provisioning keep running) - Add HTTP-response contract test locking in sync behaviour: submit returns data.identity_match.status='pending' on first response, without any queue worker running. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...TriggerPersonIdentityMatchOnFormSubmit.php | 17 ++++--- ...gerPersonIdentityMatchOnFormSubmitTest.php | 47 +++++++++++++++++++ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/api/app/Listeners/FormBuilder/TriggerPersonIdentityMatchOnFormSubmit.php b/api/app/Listeners/FormBuilder/TriggerPersonIdentityMatchOnFormSubmit.php index 193ef4b9..43f816c4 100644 --- a/api/app/Listeners/FormBuilder/TriggerPersonIdentityMatchOnFormSubmit.php +++ b/api/app/Listeners/FormBuilder/TriggerPersonIdentityMatchOnFormSubmit.php @@ -9,8 +9,6 @@ use App\Events\FormBuilder\FormSubmissionSubmitted; use App\Models\FormBuilder\FormSubmission; use App\Models\Person; use App\Services\PersonIdentityService; -use Illuminate\Contracts\Queue\ShouldQueue; -use Illuminate\Queue\InteractsWithQueue; use Illuminate\Support\Facades\Log; /** @@ -28,13 +26,18 @@ use Illuminate\Support\Facades\Log; * Failure mode per §31.1: log at error level, never rethrow so sibling * listeners on the same event (§31.10 tag sync, §31.3 shift provisioning) * keep running. + * + * Runs synchronously (no ShouldQueue) so identity_match_status is + * already written by the time the HTTP submit-response serialises the + * submission — the portal's IdentityMatchBanner then renders on first + * confirmation-page load instead of after a queue worker tick. When + * FORM-05 proper adds heavier value-based matching, that work will + * dispatch as a separate queued job from within this listener so the + * eager state transition stays sync and the slow resolution stays + * async. */ -final class TriggerPersonIdentityMatchOnFormSubmit implements ShouldQueue +final class TriggerPersonIdentityMatchOnFormSubmit { - use InteractsWithQueue; - - public string $queue = 'default'; - public function __construct( private readonly PersonIdentityService $identityService, ) {} diff --git a/api/tests/Feature/FormBuilder/Listeners/TriggerPersonIdentityMatchOnFormSubmitTest.php b/api/tests/Feature/FormBuilder/Listeners/TriggerPersonIdentityMatchOnFormSubmitTest.php index 1afc9515..693fd280 100644 --- a/api/tests/Feature/FormBuilder/Listeners/TriggerPersonIdentityMatchOnFormSubmitTest.php +++ b/api/tests/Feature/FormBuilder/Listeners/TriggerPersonIdentityMatchOnFormSubmitTest.php @@ -4,11 +4,13 @@ declare(strict_types=1); namespace Tests\Feature\FormBuilder\Listeners; +use App\Enums\FormBuilder\FormFieldType; use App\Enums\FormBuilder\FormPurpose; use App\Enums\FormBuilder\FormSubmissionStatus; use App\Events\FormBuilder\FormSubmissionSubmitted; use App\Models\CrowdType; use App\Models\Event; +use App\Models\FormBuilder\FormField; use App\Models\FormBuilder\FormSchema; use App\Models\FormBuilder\FormSubmission; use App\Models\Organisation; @@ -16,6 +18,8 @@ use App\Models\Person; use App\Models\User; use Database\Seeders\RoleSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Str; use Tests\TestCase; /** @@ -169,6 +173,49 @@ final class TriggerPersonIdentityMatchOnFormSubmitTest extends TestCase $this->assertNull($submission->fresh()->identity_match_status); } + public function test_it_writes_identity_match_status_before_http_response_returns(): void + { + // Regression guard: the listener must run synchronously so the + // public submit response already carries identity_match.status. + // If someone reinstates ShouldQueue, the column stays null in + // the response body (only written later by a queue worker) and + // this assertion fails. + Config::set('form_builder.captcha.required_for_purposes', []); + + $this->schema->update([ + 'is_published' => true, + 'public_token' => (string) Str::ulid(), + ]); + FormField::factory()->create([ + 'form_schema_id' => $this->schema->id, + 'field_type' => FormFieldType::TEXT->value, + 'slug' => 'motivatie', + 'label' => 'Motivatie', + 'is_portal_visible' => true, + 'is_admin_only' => false, + ]); + $token = $this->schema->fresh()->public_token; + + $create = $this->postJson( + "/api/v1/public/forms/{$token}/submissions", + [ + 'idempotency_key' => 'sync-regression-001', + 'public_submitter_name' => 'Sync Tester', + 'public_submitter_email' => 'sync-tester@example.test', + ], + ); + $create->assertCreated(); + $submissionId = $create->json('data.id'); + + $this->postJson( + "/api/v1/public/forms/{$token}/submissions/{$submissionId}/submit", + ['values' => ['motivatie' => 'test']], + ) + ->assertCreated() + ->assertJsonPath('data.status', 'submitted') + ->assertJsonPath('data.identity_match.status', 'pending'); + } + public function test_submission_with_no_schema_is_left_untouched(): void { // Guard branch: if the schema relation can't resolve, the listener