diff --git a/dev-docs/BACKLOG.md b/dev-docs/BACKLOG.md index 72e9b19d..84b4c3a5 100644 --- a/dev-docs/BACKLOG.md +++ b/dev-docs/BACKLOG.md @@ -465,22 +465,122 @@ Person. De portal `IdentityMatchBanner` leest dit veld en toont de juiste copy. Contract ligt vast in `tests/Feature/FormBuilder/Listeners/TriggerPersonIdentityMatchOnFormSubmitTest`. -**Resterend werk (de eigenlijke FORM-05):** public form submissions -(subject_type=null) krijgen momenteel *altijd* 'pending' omdat er nog -geen Person bestaat om tegen te matchen. Breid uit met: +**Resterend werk (de eigenlijke FORM-05):** post-D2 (PR #11 `23a5696`) +schrijft de queued `TriggerPersonIdentityMatchOnFormSubmit` voor +self-registered public submissions (`registration_source='self'`) +standaard `'none'` zodra de exact-email-match in +`PersonIdentityService::detectMatches` leeg terugkomt — het bestaande +fuzzy-name fallback-pad wordt expliciet overgeslagen voor self-registered +Persons (zie `PersonIdentityService::detectMatches` regel 117–119, *"skip +fuzzy matching for self-registered persons — they provided their own +email, so that's the canonical identity"*). Public submitters die een +typo in hun email maken of een ander email-account gebruiken dan eerder +bij de organisatie geregistreerd, krijgen daardoor geen "we may have +found you" signaal en moeten organiser-side handmatig gereconcilieerd +worden. -- Nieuwe methode op PersonIdentityService: - `detectMatchesByValues(array $values, string $organisationId): MatchResult` -- Een extra tak in `TriggerPersonIdentityMatchOnFormSubmit::resolveStatus` - die voor public submissions de values uit `FormSubmission->values` - extraheert (email / first_name / last_name via de schema binding), - deze methode aanroept, en 'matched' / 'pending' / 'none' schrijft. +Breid uit met: + +- Nieuwe methode op `PersonIdentityService`: + + ```php + public function detectMatchesByValues( + array $values, + string $organisationId, + ): Collection { + // Multi-signal match: email + first_name + last_name + DOB + + // optionele custom-field whitelist, gewogen naar + // betrouwbaarheid van de bron-velden. Werkt los van + // $person->registration_source, dus self-registered submissions + // krijgen óók fuzzy-name + DOB candidates terug. + } + ``` + +- Een extra match-arm in de status-derivation van + `TriggerPersonIdentityMatchOnFormSubmit::handle()`. D2 inlinete de + status-derivation in `handle()` zelf — er is geen aparte + `resolveStatus` methode meer. De huidige logica: + + ```php + $matches = $this->identityService->detectMatches($person); + $status = match (true) { + $person->user_id !== null => 'matched', + $matches->isNotEmpty() => 'pending', + default => 'none', + }; + ``` + + wordt uitgebreid met een value-based fallback wanneer de Person geen + `user_id` heeft én de exact-email matcher leeg terugkomt: + + ```php + $matches = $this->identityService->detectMatches($person); + $valueMatches = $matches->isEmpty() && $person->user_id === null + ? $this->identityService->detectMatchesByValues( + $this->extractFormValuesForMatching($submission), + $submission->organisation_id, + ) + : collect(); + $status = match (true) { + $person->user_id !== null => 'matched', + $matches->isNotEmpty() || $valueMatches->isNotEmpty() => 'pending', + default => 'none', + }; + ``` + + De `extractFormValuesForMatching` helper trekt de relevante velden + (email / first_name / last_name / date_of_birth + eventuele + whitelisted custom-fields) uit `FormSubmission->values` via de + schema-binding metadata. Zo krijgt de portal-UX een betekenisvol signaal in plaats van een -constante 'pending'. +`'none'` voor elke public submitter die niet exact-email matched. -Prioriteit: Medium. Kan gebundeld worden met de organizer -`person_identity_matches` UI (ook nog een frontend gap). +**Prioriteit:** Medium. Bundel met de organizer +`person_identity_matches` UI (frontend gap) én met +`FRONTEND-ECHO-IDENTITY-MATCH-SUBSCRIPTION` (de portal banner update zo +dat hij de eindstatus real-time ontvangt). + +--- + +### FRONTEND-ECHO-IDENTITY-MATCH-SUBSCRIPTION + +**Aanleiding:** WS-6 v1.3-delta D2 (PR #11 `23a5696`, 2026-05-08) introduceerde +de `FormSubmissionIdentityMatchResolved` broadcast event class en bijbehorende +`submission.{id}` private channel. De backend dispatcht het event maar de +frontend portal IdentityMatchBanner subscribet nog niet — de banner refresht +momenteel via TanStack Query refetch-on-window-focus, wat een interim-pad is +maar niet de ontworpen UX. + +**Wat:** + +- Pinia store-extension OF inline `Echo.private('submission.${submissionId}')` + subscription in de IdentityMatchBanner component +- Op event-receipt: invalidate de TanStack Query cache key voor de submission + resource zodat de banner copy automatisch update naar de finale staat + ('matched' / 'pending' / 'none') +- Authorization wordt al afgedwongen door `routes/channels.php` callback + (submitter-only voor nu — zie ook TECH-CHANNEL-AUTH-ORG-ADMIN) +- E2E test: simuleer een form submission, verify dat de banner copy in real-time + van "we're checking matches…" naar de finale status flipt zonder reload + +**Vereisten:** + +- Laravel Echo client setup in `apps/app/` (verifieren: bestaat al voor andere + channels of moet nog worden toegevoegd?) +- Soketi server in development docker-compose en in productie deployment + (verifieren: WS-7 noemt Soketi in stack maar is broadcast wiring al getest?) + +**Trigger-conditie:** Naar voren halen wanneer (a) de UX van TanStack +refetch-on-focus klagende klanten oplevert, OF (b) de eerstvolgende form-builder +frontend sprint (RFC-FORM-BUILDER-UI / S3b) gepland wordt — neem het mee in +diezelfde sprint zodat alle Echo-wiring tegelijk landt. + +**Estimate:** 0.5–1 dag (afhankelijk van of Echo client setup nog moet). + +**Refs:** `dev-docs/RFC-WS-6.md` v1.3.1 §Q1 v1.3 add 2, `dev-docs/ARCH-BINDINGS.md` +v1.2 §5.3, `apps/app/src/...` (portal IdentityMatchBanner location to be +identified during implementation), `routes/channels.php`. --- @@ -1027,6 +1127,50 @@ ARCH-discussie en RFC. --- +### HARD-DEADLINE-QUERY-TIMEOUT + +**Aanleiding:** WS-6 v1.3-delta D2 (PR #11 `23a5696`, 2026-05-08) introduceerde +`FormBindingApplicator::withDeadline(int $seconds)` — een **soft** deadline via +post-call microtime check. Deze beschermt tegen "applicator runs slow over many +bindings" (de long tail) maar **kan niet onderbreken** tijdens een hangende +MySQL-query of een lock-for-update wait die langer duurt dan de deadline. + +Voor enterprise SaaS op piekmomenten (vrijwilligersregistratie-windows, +festival check-in flows) is een hangende query een reëel risico — PHP-FPM +worker blokkeert, queue depth loopt op, downstream impact. + +**Wat:** + +Drie potentiële uitvoeringspaden, te kiezen tijdens RFC-fase: + +1. **MySQL connection-level timeouts**: zet `read_timeout` en `write_timeout` + op de Eloquent connection config voor de inner-transaction context. + Eenvoudig, maar globale impact (alle queries via die connection krijgen de + timeout). + +2. **Per-query `MAX_EXECUTION_TIME` hints**: MySQL 5.7+ ondersteunt + `/*+ MAX_EXECUTION_TIME(N) */` query hints voor SELECT statements. Niet + ondersteund door MariaDB. Gericht maar database-vendor-specifiek. + +3. **`pcntl_alarm` + signal handler**: hard interrupt via SIGALRM in de queue + worker context (CLI sapi). Werkt niet in HTTP context (PHP-FPM disablet + pcntl in de meeste configuraties). Combineren met optie 1 voor HTTP context. + +**Trigger-conditie:** Naar voren halen wanneer (a) eerste productie-incident +waar `FormBindingApplicatorTimeoutException` getrigerd wordt en de soft +deadline blijkt onvoldoende (post-mortem toont een hangende query), OF (b) +SLO-vereisten van een enterprise klant strikte response-time-garanties +opleggen die de soft deadline niet kan halen. + +**Estimate:** 2-3 dagen (RFC + implementatie + tests + connection config voor +alle environments). + +**Refs:** `dev-docs/RFC-WS-6.md` v1.3.1 §Q1 v1.3 add 4, `dev-docs/ARCH-BINDINGS.md` +v1.2 §5.3, `api/app/FormBuilder/Bindings/FormBindingApplicator.php` (soft +deadline implementation). + +--- + ### ~~TECH-02 — scopeForFestival helper op Event model~~ ✅ OPGELOST ---