diff --git a/api/app/Listeners/FormBuilder/TriggerPersonIdentityMatchOnFormSubmit.php b/api/app/Listeners/FormBuilder/TriggerPersonIdentityMatchOnFormSubmit.php new file mode 100644 index 00000000..193ef4b9 --- /dev/null +++ b/api/app/Listeners/FormBuilder/TriggerPersonIdentityMatchOnFormSubmit.php @@ -0,0 +1,100 @@ +submission->fresh(['schema']); + if ($submission === null) { + return; + } + + $schema = $submission->schema; + if ($schema === null) { + return; + } + + $purpose = $schema->purpose instanceof \BackedEnum + ? $schema->purpose->value + : (string) $schema->purpose; + + if ($purpose !== FormPurpose::EVENT_REGISTRATION->value) { + return; + } + + $status = $this->resolveStatus($submission); + + // Use a raw UPDATE so we don't re-fire Eloquent events / an + // observer cascade on the submission itself. + FormSubmission::query() + ->whereKey($submission->id) + ->update(['identity_match_status' => $status]); + } catch (\Throwable $e) { + Log::error('form-builder.identity-match.listener_failed', [ + 'submission_id' => $event->submission->id, + 'message' => $e->getMessage(), + ]); + } + } + + private function resolveStatus(FormSubmission $submission): string + { + // Public submission without a person subject — mark pending so the + // portal shows the "we koppelen je gegevens zodra..." message. + // Real matching happens when the organiser attaches a person. + if ($submission->subject_type !== 'person' || $submission->subject_id === null) { + return 'pending'; + } + + $person = Person::withoutGlobalScopes()->find($submission->subject_id); + if ($person === null) { + return 'none'; + } + + if ($person->user_id !== null) { + return 'matched'; + } + + $matches = $this->identityService->detectMatches($person); + + return $matches->isNotEmpty() ? 'pending' : 'none'; + } +} diff --git a/api/app/Models/FormBuilder/FormSubmission.php b/api/app/Models/FormBuilder/FormSubmission.php index b4c72099..12a88dca 100644 --- a/api/app/Models/FormBuilder/FormSubmission.php +++ b/api/app/Models/FormBuilder/FormSubmission.php @@ -50,6 +50,7 @@ final class FormSubmission extends Model 'opened_at', 'first_interacted_at', 'idempotency_key', + 'identity_match_status', ]; /** @var array */ diff --git a/api/app/Providers/AppServiceProvider.php b/api/app/Providers/AppServiceProvider.php index bfa11ff2..8e54cdf8 100644 --- a/api/app/Providers/AppServiceProvider.php +++ b/api/app/Providers/AppServiceProvider.php @@ -46,6 +46,7 @@ use App\Models\FormBuilder\FormWebhookDelivery; use App\Models\VolunteerAvailability; use App\Events\FormBuilder\FormSubmissionSubmitted; use App\Listeners\FormBuilder\SyncTagPickerSelectionsOnSubmit; +use App\Listeners\FormBuilder\TriggerPersonIdentityMatchOnFormSubmit; use App\Observers\FormBuilder\FormValueObserver; use App\Observers\PersonObserver; use App\Observers\UserObserver; @@ -130,6 +131,12 @@ class AppServiceProvider extends ServiceProvider SyncTagPickerSelectionsOnSubmit::class, ); + // ARCH §31.1 — identity-match trigger on event_registration. + \Illuminate\Support\Facades\Event::listen( + FormSubmissionSubmitted::class, + TriggerPersonIdentityMatchOnFormSubmit::class, + ); + ResetPassword::createUrlUsing(function ($user, string $token) { return config('crewli.portal_url') . '/wachtwoord-resetten?token=' . $token . '&email=' . urlencode($user->email); }); diff --git a/api/database/migrations/2026_04_22_100000_add_identity_match_status_to_form_submissions.php b/api/database/migrations/2026_04_22_100000_add_identity_match_status_to_form_submissions.php new file mode 100644 index 00000000..db14a639 --- /dev/null +++ b/api/database/migrations/2026_04_22_100000_add_identity_match_status_to_form_submissions.php @@ -0,0 +1,34 @@ +string('identity_match_status', 20)->nullable()->after('anonymised_at'); + $table->index( + ['form_schema_id', 'identity_match_status'], + 'fs_schema_identity_match_idx', + ); + }); + } + + public function down(): void + { + Schema::table('form_submissions', function (Blueprint $table): void { + $table->dropIndex('fs_schema_identity_match_idx'); + $table->dropColumn('identity_match_status'); + }); + } +}; diff --git a/api/database/migrations/2026_04_22_100001_add_idempotency_key_unique_to_form_submissions.php b/api/database/migrations/2026_04_22_100001_add_idempotency_key_unique_to_form_submissions.php new file mode 100644 index 00000000..4d783826 --- /dev/null +++ b/api/database/migrations/2026_04_22_100001_add_idempotency_key_unique_to_form_submissions.php @@ -0,0 +1,43 @@ +dropIndex('fs_idempotency_idx'); + }); + + Schema::table('form_submissions', function (Blueprint $table): void { + $table->unique( + ['form_schema_id', 'idempotency_key'], + 'fs_idempotency_unique', + ); + }); + } + + public function down(): void + { + Schema::table('form_submissions', function (Blueprint $table): void { + $table->dropUnique('fs_idempotency_unique'); + }); + + Schema::table('form_submissions', function (Blueprint $table): void { + $table->index( + ['form_schema_id', 'idempotency_key'], + 'fs_idempotency_idx', + ); + }); + } +};