refactor(form-builder): make identity-match listener synchronous

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) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 21:18:08 +02:00
parent 1875e79ce1
commit fda8033633
2 changed files with 57 additions and 7 deletions

View File

@@ -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