Participant Identity & External Identifiers (XID)
|
Related reading
|
1. Why this exists
Events are fed by external systems — a legacy WPCA registration database, a timing provider, a partner federation. Each of those systems has its own primary keys for the same human being and the same entry. When we import participants or results we must answer two questions without creating duplicates and without trusting a number blindly:
-
Which person is this? — the same athlete may enter through several source systems over years, each with a different internal id.
-
Which entry (EventParticipant) is this? — within one event, a source system identifies its own registration row by its own key.
The model below pins both answers to durable, queryable identifiers rather than to fragile matching on names or bib numbers.
2. Vocabulary: the two XIDs
"XID" is shorthand for external identifier — an id minted by a system other than ours. There are exactly two, and keeping them distinct is the single most important thing in this design.
| Term | Grain | Stored as | Means |
|---|---|---|---|
Person-XID |
Person (across all events) |
|
"In source system S, this human being is known as U." A person accumulates many Person-XIDs — one per source system they have ever come through. 1:N. |
Participant-XID |
EventParticipant (one event) |
|
"In the source system, this entry into this event is row R." Scoped to the event; the same value may recur in a different event without collision. 1:1 with the EP. |
The distinction is not academic. A result file that carries the source system’s person id needs the Person-XID path (resolve person → that event’s EP). A file that carries the source system’s entry id needs the Participant-XID path (resolve the EP directly). Picking the wrong one silently fails to resolve — see the mode-mismatch guard.
3. Schema
person_external_reference
id bigint PK
person_id bigint FK → wp_users(id) (the Person / User)
registration_system_id bigint FK → registration_system(id) (the source system S)
external_uid varchar (the Person-XID U)
UNIQUE (registration_system_id, external_uid) (U is unique *within* S, never globally)
event_participant
...
registration_id varchar (the Participant-XID, source EP key)
UNIQUE (event_id, person_id) (one entry per person per event)
event
...
source_registration_system_id bigint FK → registration_system(id) (US #769; nullable)
Three points carry the design:
-
PersonExternalReferenceis 1:N, not a column onwp_users. A single column could only hold one source id; an athlete who came through WPCA in 2019 and a partner federation in 2024 has two, both valid, both needed for future resolution. -
Uniqueness is
(registration_system_id, external_uid), neverexternal_uidalone. Two unrelated source systems are free to mint the same string; only the pair identifies a person. -
Event.source_registration_system_idrecords which external system this event’s field came from, so result import can default and scope Person-XID resolution without the operator re-typing it every time (US #769 — see Identity update policy).
4. The four-field identity DTO
Participant import accepts up to four identity inputs per row; the importer uses the first that resolves, in trust order. This replaced an earlier single-"externalId" column that conflated person identity with entry identity.
| DTO field | XID | Resolves |
|---|---|---|
|
(ours) |
Directly to |
|
(ours) |
To |
|
Participant-XID |
To the EP via |
|
Person-XID |
|
4.1. Trust matrix: (is_self, trustPKs)
Whether a supplied PK is trusted depends on whose system it is and whether the operator vouched for it:
RegistrationSystem.is_self |
trustPKs |
Behaviour |
|---|---|---|
|
— |
|
|
|
Rejected. The upload fails fast with |
|
|
Treat foreign PKs as hints only; fall through to identity matching (ID number, then name+DOB) and a sample-based fingerprint check before trusting the link. |
The fingerprint check (a sampled field-by-field comparison of the incoming row against the stored
person) guards against a bad foreign mapping silently merging two different humans. The full
matrix, the fingerprint scoring, and the merge-candidate path are recorded in the design journal
(participant-identity-correlation.adoc).
5. Identity update policy
PersonIdentityUpdatePolicy decides what an import row may change on the person it resolves to. It treats the identity number, the fields derived from a South African ID, and ordinary personal details under three separate regimes. The person is resolved first (see Person Matching): when a different rider has taken over an entry, the row resolves to that rider’s own person, so nothing below is ever applied to the entry’s previous person.
5.1. Identity number and type
-
A blank stored identity is always filled from the row, whatever
updatePIIis set to. -
The type is never taken from the row as given. A number that validates as a South African ID is
NATIONAL. Otherwise an explicitPASSPORThint is honoured, and anything else isOTHER. -
Upgrade. When the stored identity does not validate (a truncated ID, a wrong check digit, a passport or a placeholder) and the row carries a validating South African ID, the row’s ID replaces it, whatever
updatePIIis set to. This is identity reassignment, not a correction of personal details. The type becomesNATIONAL, and date of birth, gender and identity country (ZA) are re-derived from the ID in the same step. -
No upgrade to an ID another person holds. When another person already carries the row’s SA ID, the upgrade is refused (skip reason
SAID_HELD_BY_OTHER_PERSON) and the identity is left as stored. Landing it would leave two persons sharing one ID, and every later row carrying it would fail as a duplicate. The row is warned with the other person’s id, and the pair is filed for merge review. This also covers an external-UID collision: the row’s Person-XID names one person and its SA ID another, so the SA ID is not copied onto the Person-XID person before a human has decided. -
Never downgrade. A validating stored ID is never replaced by a non-validating one (skip reason
WOULD_REPLACE_VALID_WITH_INVALID), whateverupdatePIIis set to. -
Replacing a validating ID with a different validating ID is not an upgrade. It is gated on
updatePII(skip reasonUPDATE_PII_DISABLED).
5.2. Fields derived from a South African ID
When a validating ID is on the person, date of birth, gender and identity country belong to it. When the row’s ID lands, by blank-fill, upgrade or an updatePII overwrite, they are re-derived from it and any values the row supplies for them are ignored. When the stored ID validates and the row’s does not land, a row value that contradicts the ID is rejected (skip reason SAID_ATOMIC).
5.3. Personal details
Names, and date of birth, gender and identity country when no validating ID is in play, are filled when blank. A populated value is overwritten only when updatePII is true. A blank row value never clears a stored one.
5.4. Reporting
| Row warning | Meaning |
|---|---|
|
The upgrade landed and the person was saved. Names the previous and new identity and the derived fields that changed. An identity of ten characters or more is masked to its first and last two characters; a shorter one to its last two only. |
|
The upgrade was applied but saving the person failed, so nothing changed. Re-import the row. |
|
The upgrade was refused because another person holds the SA ID. The pair is filed for merge review. |
|
The re-derived date of birth or gender falls outside the entry’s category (its age window or gender restriction), checked the same way as a category change. The category is left unchanged for an organiser to review. |
An upgraded existing entry is reported UPDATED; a new entry CREATED.
5.5. The entry’s copy
The matched event_participant row keeps its own copy of identity number, identity type, date of birth and gender. The import refreshes that copy from the person in the same transaction, so it follows any change the policy makes.
5.6. Scope
The policy governs the participant import (create and update paths, and the legacy direct-add path, which upgrades too). The membership import and the person merge apply their own identity rules and do not yet route through it; in particular, the person merge keeps a passport or OTHER identity rather than upgrading it.
6. Event source registration system (US #769)
Event.sourceRegistrationSystem is set once, the first time participants are imported from a
non-self source (set-if-absent during EP import). It then becomes the default for that event’s
result imports:
-
Result import defaults its source-system selector from the event; the operator does not re-pick it on every file.
-
The operator may override it on a result import. Changing it warns and asks for confirmation, then persists the new value back onto the event — the design is deliberately a hybrid of "remembered on the event" and "settable per import" so a correction sticks without forcing a separate event-edit step.
-
When
EXTERNAL_PERSON_UIDresolution runs (see below), the source system scopes the Person-XID lookup:PersonExternalReference WHERE registration_system_id = <event/import source> AND external_uid = <file value>.
7. How the importers consume this
| Importer | Identity use |
|---|---|
Reads the four-field DTO; matches/creates the Person; writes the Participant-XID onto
|
|
Resolves the finisher to an EP by one of four modes. |
8. Design rationale — alternatives weighed
| Rejected alternative | Why this design instead |
|---|---|
Single |
Holds only one source system; can’t represent an athlete known to several. Conflates person
identity with per-event entry identity. The 1:N |
Globally-unique |
Different source systems legitimately reuse ids. Uniqueness must be the
|
Trust every foreign PK in the file |
A single bad mapping merges two people irreversibly. The |
Re-prompt the source system on every result import |
Tedious and error-prone across many category files for one event. |