feat: person identity matching with detection, confirmation and audit trail

Implements enterprise-grade identity resolution (detect → suggest → confirm)
for Person ↔ User linking. Matches are detected automatically on person
creation and user account creation, then surfaced to organisers for explicit
confirmation or dismissal. No silent auto-linking.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 12:50:25 +02:00
parent 239fe57a11
commit 4b182b449a
20 changed files with 1463 additions and 2 deletions

View File

@@ -122,6 +122,45 @@ Returns 422 with `errors`, `current_status`, `requested_status`, and `allowed_tr
- `POST /events/{event}/persons/{person}/approve`
- `DELETE /events/{event}/persons/{person}`
## Identity Matches
- `GET /organisations/{org}/identity-matches` — list pending matches for the organisation (paginated, 25 per page)
- `GET /organisations/{org}/persons/{person}/identity-match` — show pending match for a specific person
- `POST /organisations/{org}/identity-matches/{match}/confirm` — confirm a match (links `person.user_id`)
- `POST /organisations/{org}/identity-matches/{match}/dismiss` — dismiss a match (hidden, person stays unlinked)
- `POST /organisations/{org}/identity-matches/bulk-confirm` — bulk confirm multiple matches
### Detection
Matches are created automatically:
- When a person is created (via `POST /events/{event}/persons`) with an email matching an existing user → pending match created
- When a new user account is created (invitation acceptance) with an email matching unlinked persons → pending matches created
No silent auto-linking. Every identity link requires explicit confirmation.
### Bulk Confirm
`POST /organisations/{org}/identity-matches/bulk-confirm`
Body: `{ "match_ids": ["ulid1", "ulid2", ...] }` (max 100)
Response: `{ "confirmed": 2, "errors": [{ "match_id": "ulid3", "error": "User already has a person record in this event." }] }`
### PersonResource enrichment
`GET /events/{event}/persons` includes `pending_identity_match` inline when a pending match exists:
```json
{
"pending_identity_match": {
"match_id": "ulid",
"matched_user": { "id": "ulid", "name": "Jan", "email": "jan@example.nl" },
"matched_on": "email",
"confidence": "exact"
}
}
```
## Crowd Lists
- `GET /events/{event}/crowd-lists`