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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user