Import: Event Participant
|
Related reading
|
1. Overview
The Event Participant import registers (or updates) participants in a named event from an XLSX file. Each row carries up to four identity inputs (our EP id, our Person id, the source system’s entry key, the source system’s person id) and is resolved through the participant-identity model — so the same file can be re-run with corrections without creating duplicate people or entries. The source system’s entry key is stored as the Participant-XID (EventParticipant.registrationId); on a confirmed match against a foreign system the source’s person id is written back as a Person-XID (PersonExternalReference). Each row produces one of five outcomes (CREATED / UPDATED / SKIPPED / WARNING / ERROR), and the aggregated response is returned as EventParticipantImportResultDTO.
The HTTP surface is asynchronous from release 2.4: the upload returns HTTP 202 immediately and the caller polls a companion GET endpoint for the result DTO.
2. API
2.1. Endpoint Reference
| Endpoint | Purpose |
|---|---|
|
Upload an XLSX and start the async import. Returns |
|
Retrieve the |
2.2. Async Delivery Mode
The per-entity endpoint is a thin facade over the generic async framework. Behind the scenes EventParticipantRowProcessor runs in whole-file mode, delegating to EventParticipantImportXLS.process(…) — the same bulk importer that produced the response DTO inline in the pre-async implementation. The resulting EventParticipantImportResultDTO is JSON-serialised to ImportJob.resultPayloadJson and echoed back verbatim by the GET endpoint. Response shape is byte-identical to what the prior synchronous PUT returned inline — only the delivery mode changed.
PUT /api/event-participants/import
?eventId={long} (required)
&orgId={long} (optional — defaults to caller's tenant)
&sheetIndex={int} (default: 0)
&createCustom1={true|false} (default: true)
&createCustom2={true|false} (default: true)
&createCustom3={true|false} (default: true)
&sourceSystemId={long} (optional — the external source registration system)
&trustPKs={true|false} (default: false)
&updatePII={true|false} (default: false)
&acknowledgeFingerprintWarning={true|false} (default: false)
Content-Type: multipart/form-data
Body: file=<xlsx>
HTTP/1.1 202 Accepted
Location: /api/event-participants/import/{jobId}
{ "identifier": "<jobId>", "status": "PROCESSING", "skipMappingPhases": true, ... }
# operator polls ...
GET /api/event-participants/import/{jobId}
HTTP/1.1 200 OK # once COMPLETED or FAILED
{ ... EventParticipantImportResultDTO ... }
HTTP/1.1 409 Conflict # while still processing
See Import Operations Runbook for the polling procedure.
2.3. Request Body
Multipart form with a single file part containing the XLSX workbook. One sheet per upload; sheetIndex selects which sheet (0-based, default 0).
Query parameters:
-
eventId(required) — target event; scopes theregistrationIdupsert -
orgId(optional) — override the caller’s tenant; normally inferred from the event -
sheetIndex(default0) — 0-based sheet index within the workbook -
createCustom1/2/3(defaulttrue) — iftrue, a missing value inCUSTOM_LIST_1/2/3is auto-created on the correspondingCustomList; iffalse, missing values produce aWARNINGwithout creating the custom value -
sourceSystemId(optional) — the external source registration system the file came from. Drives the(is_self, trustPKs)trust matrix and the Person-XID write-back, and is recorded set-if-absent ontoEvent.sourceRegistrationSystemso result import can default it (US #769). Omit for a self/our-own import. -
trustPKs(defaultfalse) — when the source is a foreign system, assert that the file’s foreign PKs are reliable enough to resolve by directly. Whenfalse, foreign PKs are hints and the importer falls through to identity matching + the fingerprint check. See the trust matrix. -
updatePII(defaultfalse) — whentrue, an existing person’s PII (name, DOB, contact) may be overwritten from the file; whenfalse, PII is only filled where blank. -
acknowledgeFingerprintWarning(defaultfalse) — set after an operator has reviewed and accepted a flagged fingerprint mismatch, allowing the otherwise-blocked link to proceed.
2.4. Response: EventParticipantImportResultDTO
{
"totalRows": 185,
"created": 20,
"updated": 160,
"skipped": 0,
"warnings": 4,
"errors": 1,
"issues": [
{ "outcome": "WARNING", "firstName": "Jane", "lastName": "Doe",
"registrationId": "29364", "message": "Ambiguous person match" },
{ "outcome": "ERROR", "firstName": "Xyz", "lastName": "Abc",
"message": "CustomList value not found and createCustom1=false" }
]
}
Invariant: totalRows == created + updated + skipped + warnings + errors.
The issues list contains only rows with SKIPPED / WARNING / ERROR. Successful CREATED / UPDATED rows are counted but not individually listed, keeping the payload focused on what the operator must action.
2.5. What to Look At
After polling returns the DTO, these are the patterns to confirm the import landed the way you expected:
-
Clean first run against a fresh event:
created ≈ totalRows,updated = 0,issues = [] -
Re-run of the same file:
created = 0,updated ≈ totalRows,issues = [] -
Correcting a partial first run: some
updated+ somecreatedis fine; inspectissuesfor the remainder
The Import Operations Runbook covers the recovery procedures when the numbers don’t match expectations.
3. Column Definitions
The import supports 32 columns, with 2 required fields.
3.1. Required Fields
| Column | Type | Aliases |
|---|---|---|
FIRST_NAME |
STRING |
FirstName, Name, FN |
LAST_NAME |
STRING |
LastName, Surname, SN, LN |
3.2. Person Fields
| Column | Type | Aliases |
|---|---|---|
TITLE |
STRING |
Title |
ID_NUMBER |
STRING |
ID, IDNumber, IdentityNumber, Passport, IDPassport |
ID_TYPE |
STRING |
IDType (default: NATIONAL) |
ID_COUNTRY |
STRING (FK) |
Country, IDCountry, Nationality (default: ZA) |
GENDER |
STRING |
Gender, Gen, Sex |
DOB |
DATE |
DOB, DateOfBirth, Birthday |
CONTACT_NUMBER |
STRING |
ContactNumber, Cellphone, CellphoneNumber, CellNumber, Telephone, TelephoneNumber, Mobile |
STRING |
Email, EmailAddress |
|
USER_KEY |
LONG |
UserKey |
MAIN_MEMBER_ID |
STRING |
MainMember, MainMemberID |
PERSON_EXTERNAL_ID |
LONG |
UID, PID |
3.3. Address Fields
| Column | Type | Aliases |
|---|---|---|
ADDRESS |
STRING |
Address, Address1 |
TOWN |
STRING |
Town, City, TownCity, CityTown |
POSTAL_CODE |
STRING |
PostalCode, Code, ZIP |
3.4. Event Participation Fields
| Column | Type | Aliases |
|---|---|---|
EVENT_PARTICIPANT_ID |
LONG |
EPID, EventParticipantID |
EVENT_PARTICIPANT_EXTERNAL_ID |
STRING |
EPREF (also |
EVENT_CATEGORY_NAME |
STRING (FK) |
Category, CategoryName, EventCategory, EventCategoryName |
OTHER_NUMBER |
STRING |
OtherNumber |
3.5. Race Number Fields
| Column | Type | Aliases |
|---|---|---|
NUMBER |
STRING |
Number, Bib, BibNumber, RaceNumber, No |
NUMBER_ID |
INTEGER |
NumberId |
TAG_NUMBER |
STRING |
Tag, TagRef, TagReference, Chip |
TAG_ID |
INTEGER |
TagId |
3.6. Order Fields
| Column | Type | Aliases |
|---|---|---|
ORDER_ID |
INTEGER |
OrderID |
ORDER_NUMBER |
STRING |
OrderNumber, Order#, ReceiptNumber |
ORDER_STATUS |
STRING |
OrderStatus (default: P for Pending) |
ORDER_PRODUCT_ID |
INTEGER |
Product, ProductID, OrderProduct, OrderProductID |
ORDER_PRODUCT_AMOUNT |
DECIMAL |
ProductAmount, OrderProductAmount, Amount |
4. Foreign Key Resolution
Fields marked (FK) require resolution to entity IDs:
| Field | Target Entity | Resolution Strategy |
|---|---|---|
ID_COUNTRY |
Country |
Lookup by code (ZA, US) or name |
EVENT_CATEGORY_NAME |
EventCategory |
Lookup by name within event |
CUSTOM_LIST_1/2/3 |
CustomListValue |
Lookup by value within list; auto-create if |
5. Processing Flow
5.1. Row-Level Flow
1. Receive InputStream + orgId + eventId + sheetIndex
2. Load workbook into memory (XSSFWorkbook)
3. Lookup Organisation (tenantService.getOrganisation)
4. Lookup Event (eventService.findOneEntity)
5. For each row:
a. Skip row 0 (header row, detect columns)
b. Check if blank (first 5 cells empty)
c. Create EventParticipantAddRequestDTO
d. Call eventParticipantService.register()
e. Collect response
6. Return list of responses, aggregated into EventParticipantImportResultDTO
5.2. Row Extraction
The createRequestDTO method maps spreadsheet values to DTO fields:
EventParticipantAddRequestDTO request = new EventParticipantAddRequestDTO();
request.setId(parseAsLong(row, map, Column.EVENT_PARTICIPANT_ID));
request.setExternalId(parseAsString(row, map, Column.EVENT_PARTICIPANT_EXTERNAL_ID));
PersonDTO person = new PersonDTO();
person.setFirstName(parseAsString(row, map, Column.FIRST_NAME));
person.setLastName(parseAsString(row, map, Column.LAST_NAME));
// ... more fields
5.3. Default Values
defaultValues.put(Column.ID_TYPE, "NATIONAL");
defaultValues.put(Column.ID_COUNTRY, Country.DEFAULT_COUNTRY_CODE);
defaultValues.put(Column.ORDER_STATUS, "P");
5.4. Identity resolution & upsert
Each row is resolved through the participant-identity model
rather than a single key. Two grains are resolved in turn — which person and which entry — and
the (is_self, trustPKs) matrix governs how much each supplied id is trusted.
Entry (EventParticipant) resolution. The source system’s entry key (RegistrationID, aliases
ExternalID / ExternalEPID) is the upsert key within the event — the
Participant-XID:
-
Resolve existing EP via
eventParticipantRepository.findByEventIdAndRegistrationId(eventId, registrationId) -
If found:
wasExisting = true; update fields (outcome =UPDATED) -
If not found: create a new EP (outcome =
CREATED)
The upsert is scoped to eventId, so the same registrationId recurs across events without
collision — the primary enabler for cross-system migration (WPCA → EMS), where legacy PKs are
preserved in EventParticipant.registrationId.
Person resolution & Person-XID write-back. A row without a trusted entry key relies on Person
matching (our person id if trusted, else identity number, else name+DOB) to avoid duplicate people.
When the source is a foreign system and a person is confirmed, the source’s person id is written
back as a PersonExternalReference (Person-XID) so the next import resolves that person in one
hop. When trustPKs=false, a sampled fingerprint comparison guards the link before it is trusted;
a flagged mismatch blocks until acknowledgeFingerprintWarning is set.
Event source system (set-if-absent). The first import from a non-self sourceSystemId records it
onto Event.sourceRegistrationSystem if not already set, establishing the default that result
import reads back (US #769 — see Participant Identity).
6. Outcome Classification
Every row produces exactly one outcome via the ImportAddResponseDTO.Outcome enum:
| Outcome | Meaning |
|---|---|
|
New |
|
Existing |
|
Row not processed — missing required field, duplicate within the same file, or other pre-validation failure. |
|
EP was created/updated, but a non-fatal issue requires operator review (ambiguous Person match, custom-list value not auto-created, race-number conflict). |
|
Exception occurred processing the row; the row was not committed. Subsequent rows continue. |
EventParticipantServiceEx.register() classifies the outcome. The inner try-catch sets ERROR outcome with the exception message rather than swallowing.
7. Source Code
| File | Purpose |
|---|---|
|
Main import service: column definitions, row extraction, per-row orchestration. |
|
Whole-file bridge — parses |
|
Registration logic invoked per row; classifies outcome. |
|
REST endpoints |
|
Request payload for registration. |
|
Response with outcome per row (aggregated into |
Location: admin-service/src/main/java/za/co/idealogic/event/admin/
8. Outstanding Items
The items below pre-date Feature #442; some were partially addressed during the async cutover. Tracked informally — open an ADO ticket before acting on any of them.
| # | Topic | Status |
|---|---|---|
1 |
Refactor identity-type parsing (duplicated in |
Open |
2 |
Refactor gender parsing (duplicated in |
Open |
3 |
Make |
Open |
4 |
Only create |
Resolved — null-guard added; |
5 |
Validate |
Open — partial hardening in place |
6 |
Reduce redundant Organisation lookups in the import call stack |
Open (performance; not blocking) |
7 |
Reinstate access-control check ( |
Open — security gap; validate before closing |
8 |
Replace |
Partially resolved — inner try-catch now sets |