From 7ba01a6dfaf95c66224680fff23835af6ec92ac9 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 08:57:02 +0200 Subject: [PATCH 1/4] docs(runbooks): add form-builder binding failures section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per RFC-WS-6 §Q3 v1.3 + ARCH-BINDINGS §11. Nieuwe runbook-sectie §7 (na §6 Audit trail) die de triage-flow documenteert wanneer GlitchTip een FormBindingApplicatorException event opbrengt: - §7.1 failure_response_code classificatie (schema_config_error / temporary_error / data_integrity_error / unknown_error) drijft het initiële triage-pad - §7.2 form_schema.has_public_token tag onderscheidt klant-zichtbare failures (alert-waardig) van organizer-driven failures (admin-UI only) - §7.3 retry/dismiss decision-matrix met form-failures:retry artisan command + DismissalReasonType enum cases - §7.4 severe-failure escalatie criteria (>10/uur op één schema = P1) - §7.5 cross-references naar RFC, ARCH-BINDINGS, en erasure-runbook Companion van de operationele GlitchTip alert-rule (apart geconfigureerd in de GlitchTip web UI op monitoring.hausdesign.nl). Co-Authored-By: Claude Opus 4.7 --- dev-docs/runbooks/observability-triage.md | 112 ++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/dev-docs/runbooks/observability-triage.md b/dev-docs/runbooks/observability-triage.md index 88b6b466..c2559ee3 100644 --- a/dev-docs/runbooks/observability-triage.md +++ b/dev-docs/runbooks/observability-triage.md @@ -268,3 +268,115 @@ Voor incidents waar 1+ klant geraakt is, log een externe incident-summary (klant, impact, timeline, fix) in een aparte incident-tracker — niet in GlitchTip zelf, want GlitchTip data wordt na 90 dagen gepurged. + +--- + +## §7 Form-builder binding failures + +### Context + +`FormBindingApplicator` draait synchroon binnen +`ApplyBindingsOnFormSubmit::handle` voor elke form-submission. Als die +throwt — schema-misconfiguratie, infra-issue, data-integrity violation +of deadline-timeout — vangt de listener de throw op, schrijft een +`form_submission_action_failures` rij in de outer-transaction, en zet +`apply_status='failed'` op de parent-submission. GlitchTip vangt het +exception-event op met de `crewli-api` project-tags. + +De alert-rule op `apply_status=failed AND form_schema.has_public_token=true` +brengt alleen de klant-zichtbare failures naar boven — organizer-driven +private-form failures zijn zichtbaar in de Form Failures admin-UI zonder +alert, omdat hun blast radius beperkt is tot één organisatie. + +Per RFC-WS-6.md §Q3 v1.3 + ARCH-BINDINGS.md §6 (two-transaction pattern) +en §11 (failures lifecycle). + +### 7.1 Eerste check — `failure_response_code` + +De submission-rij draagt een denormalised classification-token in +`failure_response_code`. Vier waardes: + +| Token | Oorzaak | Triage-pad | +|---|---|---| +| `schema_config_error` | Organizer-config issue (renamed kolom, deleted target-entity, ontbrekende identity-key binding die publish-guards toch doorlieten) | Contact de organizer van het schema; **NIET retryen** — retry produceert dezelfde exception | +| `temporary_error` | Infra-issue (DB-connection, lock-for-update wait, deadline-wrapper timeout) | Retry via `php artisan form-failures:retry --id={failure_ulid}` of admin-UI; als een tweede retry óók faalt, escaleer als infra-incident | +| `data_integrity_error` | Data-shape violation (type-mismatch, FK-violation, soft-deleted target-entity) | Onderzoek de failing binding's `target_entity` + `target_attribute` in `form_field_bindings`; lost meestal op als een schema-config issue verkleed als data | +| `unknown_error` | Iets buiten de binding-applicator hiërarchie (raw `\Throwable`, `IdentityMatchInvariantViolation`) | Triage via de GlitchTip exception-trace; dit is dev-investigation territory | + +De exception-class op de action-failure rij is de canonical truth; +`failure_response_code` is de afgeleide classificatie die door de +response-renderer gebruikt wordt. + +### 7.2 Tweede check — public-token presence + +De GlitchTip event-tag `form_schema.has_public_token` onderscheidt: + +| Tag-waarde | Betekenis | Severity | +|---|---|---| +| `true` | Public submission flow (vrijwilliger registratie-window, public form-fill) | Customer-impact bevestigd; mobiliseer binnen minuten tijdens active festival registratie | +| `false` | Organizer-driven private flow (managed crew roster, internal data import) | Blast radius beperkt tot één organisatie; onderzoek binnen uren | + +### 7.3 Retry vs dismiss + +Gebruik het artisan-command: + +```bash +# Retry een enkele failure +php artisan form-failures:retry --id={failure_ulid} + +# Retry alle open failures voor een submission +php artisan form-failures:retry --submission={submission_ulid} + +# Dry-run eerst als onzeker +php artisan form-failures:retry --id={failure_ulid} --dry-run +``` + +Wanneer wel retryen: + +- `temporary_error` — ja, bijna altijd; de deadline-wrapper of infra-hiccup + is waarschijnlijk inmiddels weg. +- `unknown_error` — alleen na dev-investigatie die bevestigt dat de + throw transient was. + +Wanneer dismissen: + +- `schema_config_error` nadat de organizer het schema heeft gefixt en + opnieuw heeft gesubmit — de originele failure-rij blijft staan als + audit, dismiss met reden `schema_deleted` of `binding_removed` per + het relevante geval. +- `data_integrity_error` nadat dev-investigatie het herleidt tot een + one-off data-state die niet meer geldt — dismiss met + `data_quality_issue`. +- Iedere failure voor een submission die de organizer expliciet als + duplicaat geclassificeerd heeft — dismiss met `duplicate_submission`. + +De `DismissalReasonType` enum heeft zes cases; `OTHER` vereist een +free-text note. Per ARCH-BINDINGS §11.3. + +### 7.4 Severe-failure escalatie + +Als GlitchTip `>10` events in 1 uur toont gescoped op één +`form_schema_id`, behandel als P1: + +1. Acknowledge de alert in het team-channel. +2. Identificeer het schema via `form_submission.form_schema_id` op een + willekeurige failed submission. +3. Pauzeer publieke toegang tot het schema tijdelijk — disable het + public token tot de root-cause geïdentificeerd is. +4. Standaard incident-triage geldt verder. + +Dit patroon wijst bijna altijd op een publish-guard gap: een +schema-configuratie die de guards hadden moeten weigeren is er toch +doorheen geslipt, en elke submission ertegen produceert dezelfde +exception. + +### 7.5 Cross-references + +- [`RFC-WS-6.md`](../RFC-WS-6.md) v1.3.1 — volledige architectuur, in + het bijzonder §Q3 v1.3 additions. +- [`ARCH-BINDINGS.md`](../ARCH-BINDINGS.md) v1.2 — §6 two-transaction + pattern, §11 failures lifecycle, §11.3 dismissal reasons. +- [`observability-erasure.md`](./observability-erasure.md) — Art. 17 + procedure als triage in een GDPR-deletion-verzoek omdraait. +- BACKLOG `TECH-CHANNEL-AUTH-ORG-ADMIN` — bekende follow-up voor + live-update channel-auth uitbreiding (org-admin scope). -- 2.39.5 From 5ac6b4168d1bb083f28a05f3af6cede5c1d845b2 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 08:57:50 +0200 Subject: [PATCH 2/4] docs(rfc-ws-6): mark v1.3.1 as fully implemented MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §1 Status: add Implementation status line citing D1 (PR #10 c6f4d1b) and D2 (PR #11 23a5696), both 2026-05-08. §10 Document history: append v1.3-delta closure entry summarising what D1 and D2 each delivered + what remains as separate operational task (GlitchTip alert rule configuration in the web UI) and frontend follow-up (Echo subscription). No spec changes — purely lifecycle marker update. Co-Authored-By: Claude Opus 4.7 --- dev-docs/RFC-WS-6.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev-docs/RFC-WS-6.md b/dev-docs/RFC-WS-6.md index 177077bb..2b71e42f 100644 --- a/dev-docs/RFC-WS-6.md +++ b/dev-docs/RFC-WS-6.md @@ -4,6 +4,7 @@ - **State:** Authoritative for sessions 1, 2, 3 of WS-6 - **Frozen:** 2026-04-25 (v1.0); refined post-session-2 cleanup as v1.1, then v1.2 (sessie 3a.5), then v1.3 (architectural review 2026-05-07); v1.3.1 (2026-05-08) — code-vs-docs drift closure pre-D1 implementation — see §10 +- **Implementation status:** v1.3.1 fully implemented in main as of 2026-05-08 (D1: PR #10 `c6f4d1b`, D2: PR #11 `23a5696`) - **Version:** v1.3.1 - **Owner:** Bert Hausmans - **Origin:** Architectural session 2026-04-25 (Claude Chat) — 13 design decisions, 4 refinements, 3 observations @@ -859,3 +860,4 @@ WS-7 sessie 1. - **§Q3 — Strict-fail vs compatibility.** Spine confirmed unchanged. Three failure-UX additions: GlitchTip alert rule on `FormBindingApplicatorException` for production public flows; custom exception hierarchy (`FormBindingSchemaConfigException` / `FormBindingInfraException` / `FormBindingDataIntegrityException`) + `failure_response_code` column on `form_submissions` + `error_code` in HTTP response body; "all-or-nothing per pass" gets explicit BACKLOG entry `PARTIAL-BINDING-SUCCESS`. Fourth addition: schema-drift detection as separate BACKLOG entry `FORM-SCHEMA-DRIFT-DETECTION` (not v1.0 scope). - Companion: `ARCH-BINDINGS.md` advances to v1.1 (twelve section edits per the v1.3 amendment companion table); `BACKLOG.md` adds `PARTIAL-BINDING-SUCCESS` and `FORM-SCHEMA-DRIFT-DETECTION` under Form Builder backlog. RFC-WS-6 v1.3, ARCH-BINDINGS v1.1, and the BACKLOG additions land in the same commit. - 2026-05-08 — v1.3.1 — Pre-D1-implementation drift closure. (1) Updated apply_status enumerations throughout §3 to include the PARTIAL case (which exists in code per `BindingPassResult::applyStatus()` and was not anticipated by the v1.3 amendment author). (2) ARCH-BINDINGS §5.6 received a PARTIAL-handling clarification: gate treats PARTIAL identically to FAILED, deferring granular partial-success to BACKLOG `PARTIAL-BINDING-SUCCESS`. (3) ARCH-BINDINGS §7.1 status-columns table extended with `apply_completed_at` row + cross-reference to the D2 retry-service symmetry fix. No spine changes; no behaviour changes; documentation truth-in-naming. Companion: ARCH-BINDINGS.md advances to v1.2. +- 2026-05-08 — **v1.3-delta closure** — RFC v1.3.1 fully implemented in main. **D1** (PR #10 `c6f4d1b`, 2026-05-08) delivered the data-layer prerequisites: `failure_response_code` column on `form_submissions`, abstract `FormBindingApplicatorException` hierarchy with 4 reason-coded subclasses (`FormBindingSchemaConfigException`, `FormBindingInfraException`, `FormBindingApplicatorTimeoutException`, `FormBindingDataIntegrityException`), `IdentityMatchInvariantViolation` sibling DomainException, `FormBindingExceptionClassifier` helper, `FormSubmissionIdentityMatchResolved` broadcast event class, `FormFieldBindingMergeStrategy::validForTargetType` matrix method, plus cast + factory state. **D2** (PR #11 `23a5696`, 2026-05-08) wired the building blocks into the listener chain: `ApplyBindingsOnFormSubmit` writes initial `identity_match_status='pending'`, uses the deadline wrapper, and consumes the classifier in its outer-transaction catch block; `TriggerPersonIdentityMatchOnFormSubmit` becomes queued with the gating-invariant first statement, the strict invariant throw, and broadcast dispatch; `routes/channels.php` introduces broadcasting infrastructure (NEW wiring) with submitter-only authorization (org-admin extension tracked as BACKLOG `TECH-CHANNEL-AUTH-ORG-ADMIN`); queued listeners gain the `apply_status=COMPLETED` first-statement gate; `FormFailureRetryService::recordFailure` consumes the classifier and writes `apply_completed_at` for symmetry with the listener; `apply_deadline_seconds` config key (default 5) added; six existing tests adapted to the v1.3 layout. Test counts: pre-D1 baseline 1551 → post-D2 1621 (+70). 0 Larastan errors. The remaining v1.3 add (Q3 v1.3 add 1 — GlitchTip alert rule on `apply_status=failed AND form_schema.has_public_token=true`) is an operational task, configured in the GlitchTip web UI on `monitoring.hausdesign.nl` outside the code lifecycle; runbook procedure documented in `dev-docs/runbooks/observability-triage.md` §7. Frontend Echo subscription for `FormSubmissionIdentityMatchResolved` is a separate frontend follow-up, out of WS-6 scope. -- 2.39.5 From ce552ec7bed85d399571528ccfedd77bf274e761 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 08:59:40 +0200 Subject: [PATCH 3/4] docs(backlog): WS-6 v1.3-delta closure entry + FORM-05 stub-status touch-up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Append WS-6-V1.3-DELTA closure bullet under "Opgeloste items (mei 2026)" summarising D1 (PR #10 c6f4d1b) + D2 (PR #11 23a5696) deliverables and open follow-ups. Surgical correction to FORM-05 Stub-status paragraph: pre-D2 description claimed TriggerPersonIdentityMatchOnFormSubmit writes initial 'pending'; post-D2 that's ApplyBindingsOnFormSubmit's job per RFC §Q1 v1.3 add 1. The underlying ticket (detectMatchesByValues extension) stays open. No other BACKLOG entries resolved — D1+D2 implemented RFC §Q3 v1.3 changes that pre-existing tickets didn't anticipate. Co-Authored-By: Claude Opus 4.7 --- dev-docs/BACKLOG.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/dev-docs/BACKLOG.md b/dev-docs/BACKLOG.md index 526fa9df..72e9b19d 100644 --- a/dev-docs/BACKLOG.md +++ b/dev-docs/BACKLOG.md @@ -451,11 +451,19 @@ te runnen. 1 regel, geen scope-impact. ### FORM-05 — Smart identity-match on public submission values -**Stub-status (S3a PR 2, 2026-04-23):** Public event_registration -submissions landen al met `identity_match_status='pending'` via de -bestaande `TriggerPersonIdentityMatchOnFormSubmit` listener. De portal -`IdentityMatchBanner` leest dit veld en toont de juiste copy. Contract -ligt vast in `tests/Feature/FormBuilder/Listeners/TriggerPersonIdentityMatchOnFormSubmitTest`. +**Stub-status (S3a PR 2, 2026-04-23; updated post-D2 2026-05-08):** +Post-D2 (RFC §Q1 v1.3 add 1) writes the initial +`identity_match_status='pending'` in `ApplyBindingsOnFormSubmit::handle` +inside the inner transaction. The queued +`TriggerPersonIdentityMatchOnFormSubmit` then writes the final +`'matched'` / `'pending'` / `'none'` status. FORM-05's +`detectMatchesByValues` extension targets the queued listener's path — +adds a value-based matcher branch when `$person->user_id === null` AND +`$matches->isEmpty()` (currently writes `'none'`), giving public +submitters a "we may have found you" path without first creating a +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 @@ -1040,6 +1048,7 @@ ARCH-discussie en RFC. - ~~**WS-TOOLING-001**: Claude Code deterministic guard-rail layer (5 hooks, `crewli-reviewer` subagent op Opus 4.7, 3 slash commands `/sprint-status` `/review-multitenancy` `/sync-docs`, `dev-docs/CLAUDE_CODE_TOOLING.md`). 8/8 smoke tests groen, live integratie geverifieerd. Merge `ad36c06` op 2026-05-05. Follow-ups: TECH-HOOK-001, TECH-CMD-001, TECH-STYLE-001.~~ ✅ - ~~**TECH-DOCS-APPS-PORTAL-PURGE**: per-file DELETE/REWRITE/KEEP_AND_PURGE matrix uitgevoerd op alle 9 docs uit de oorspronkelijke entry, plus de `post-edit-eslint.sh` hook (out-of-scope vondst uit Phase A). Vijf obsolete docs verwijderd (`.cursor/instructions.md`, `.cursor/ARCHITECTURE.md`, `dev-docs/MASTER_PROMPT_CC.md`, `dev-docs/MASTER_PROMPT_CURSOR.md`, `dev-docs/dev-guide.md` — totaal ~80 KB). Drie herschreven (`SETUP.md`, `101_vue.mdc`, hook-script). Twee chirurgisch gepurgeerd (`102_multi_tenancy.mdc`, `CLAUDE_CODE_TOOLING.md`). Externe verwijzingen in README.md, CLAUDE_DESKTOP_SETUP.md, ARCH-CONSOLIDATION-2026-04.md en VIBE_CODING_CHECKLIST.md mee bijgewerkt. WS-3 PR-C op 2026-05-06. Single SPA, single cookie, single deploy host. WS-3 compleet.~~ ✅ - ~~**WS-7 Observability — closure (mei 2026)**: 4 PRs gemerged op `feat/ws-7-observability` (infra `5f6fc07`, backend SDK `bdb89a2..0379016`, frontend SDK `bc47783..5c42f27`, docs `754222f..e9da01f`). 1551 backend + 252 frontend tests groen. Acceptance criteria 1-14 voldaan; observability volledig operationeel op `monitoring.hausdesign.nl`. Implementation criteria 3, 4, 5, 6, 8, 11, 12, 13, 14 via PRs; operationele criteria 1, 2, 7, 9, 10 via deploy-checklist (DNS, TLS, superuser+2FA, prod DSNs, email-alerting, retention 90d, cron backup). Architecturale patronen vastgelegd in `dev-docs/ARCH-OBSERVABILITY.md` (730 regels) + 2 runbooks (`observability-triage.md`, `observability-erasure.md`). Twee GlitchTip projecten (`crewli-api` + `crewli-app`), één DSN per project, runtime context-split via `actor_scope` tag. Patronen: explicit > implicit listener registration, default-in-listener / override-in-middleware voor binary tags, tenant resolution chain (route-param → portal-token → super_admin platform → user fallback). Volgsporen: OBS-1, OBS-4, OBS-6, OBS-7, OBS-9 (zie "Observability follow-ups" sectie hieronder).~~ ✅ +- ~~**WS-6 v1.3-delta — closure (mei 2026)**: Architecturele review-sessie 2026-05-07 identificeerde vijf verfijningen op RFC-WS-6 v1.2 (Q1 listener queueing, Q2 invariant cleanup, Q3 failure-UX additions, plus §19 BACKLOG-pointer). v1.3 amendement gecommit (`845b6e6`, 2026-05-07); v1.3.1 drift closure (`b255879`, 2026-05-08) sloot code-vs-docs gaten pre-implementation. Implementatie geland als D1 + D2: **D1** (PR #10 `c6f4d1b`) leverde de data-laag — `failure_response_code` kolom op `form_submissions`, abstract `FormBindingApplicatorException` hiërarchie + 4 reason-coded subclasses (`FormBindingSchemaConfigException`, `FormBindingInfraException`, `FormBindingApplicatorTimeoutException`, `FormBindingDataIntegrityException`), `IdentityMatchInvariantViolation` sibling, `FormBindingExceptionClassifier` helper, `FormSubmissionIdentityMatchResolved` broadcast event class, `FormFieldBindingMergeStrategy::validForTargetType` matrix method, cast + factory state. **D2** (PR #11 `23a5696`) wired alle building blocks in de listener-chain — `ApplyBindings` initial `pending` write + deadline wrapper + classifier in catch; `TriggerPersonIdentityMatch` queued + gating-invariant + invariant throw + broadcast dispatch; `routes/channels.php` + bootstrap routing (NIEUWE broadcast wiring, submitter-only auth); gating-invariant op `SyncTagPicker`; `AppServiceProvider::boot` v1.3 layout; `FormFailureRetryService::recordFailure` classifier + apply_completed_at symmetrie-fix; `apply_deadline_seconds` config key (default 5). Tests: pre-WS-6 baseline 1208 → pre-D1 1551 → post-D2 1621. 0 Larastan errors. Phase F (`ConditionalRequirement(public_token)` wrapper drop) was no-op — change had silently landed pre-D2. **Open follow-ups:** `TECH-CHANNEL-AUTH-ORG-ADMIN` (extend `submission.{id}` channel auth to org admins na Spatie Permission helper-audit); GlitchTip alert rule op `apply_status=failed AND form_schema.has_public_token=true` (operationele taak in GlitchTip web-UI op `monitoring.hausdesign.nl`; runbook procedure in `dev-docs/runbooks/observability-triage.md` §7); frontend Echo subscription voor `FormSubmissionIdentityMatchResolved` (separate frontend follow-up, out of WS-6 scope, backend-infra ready). `PARTIAL-BINDING-SUCCESS` en `FORM-SCHEMA-DRIFT-DETECTION` blijven open conform v1.3 amendement (trigger-condities nog niet gevuurd). Closure docs-PR: RFC-WS-6.md v1.3.1 implementation-status marker + §10 closure entry, ARCH-BINDINGS.md v1.2 onveranderd, runbook §7 toegevoegd.~~ ✅ --- -- 2.39.5 From c5682f181f0ff10959bfb64ae838635dd3d7586c Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Fri, 8 May 2026 10:11:50 +0200 Subject: [PATCH 4/4] docs(backlog): close no-compromises gaps from WS-6 v1.3-delta review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three edits closing concessies surfaced in chat review of the closure docs-PR: 1. FORM-05 'Resterend werk' sub-paragraph: surgical replacement of resolveStatus references (method removed in D2, PR #11 23a5696). Updated to describe post-D2 reality: gate + invariant + handle()-internal status derivation. Ticket stays open (the detectMatchesByValues extension is unbuilt). 2. FRONTEND-ECHO-IDENTITY-MATCH-SUBSCRIPTION (NEW): tracks the frontend follow-up where the portal IdentityMatchBanner subscribes to the submission.{id} channel for live banner updates. Previously documented in PR #11 body and RFC §Q1 v1.3 add 2 commentary but without an actionable BACKLOG ticket. 3. HARD-DEADLINE-QUERY-TIMEOUT (NEW): tracks the upgrade from soft post-call microtime deadline to a hard deadline that can interrupt hanging MySQL queries (connection-level timeouts, MAX_EXECUTION_TIME hints, or pcntl_alarm). Previously documented as 'soft deadline limitation' inline in code comments without an actionable BACKLOG ticket. No spec changes; no code changes. Closes the chat-identified gaps so WS-6 v1.3-delta closure has zero un-anchored mental TODOs. Co-Authored-By: Claude Opus 4.7 --- dev-docs/BACKLOG.md | 168 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 156 insertions(+), 12 deletions(-) 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 --- -- 2.39.5