Event Participant Import Functional Requirements

1. Business Context

Event Participants (EPs) are registered for an event through the Registration Portal during normal operations, but operations staff also need to:

  • Bulk-register participants from a spreadsheet on behalf of a club, school, or federation

  • Migrate participants from a legacy system (WPCA, Cycling SA) while preserving cross-system identity

  • Apply post-registration corrections in bulk when payment, category, or contact data was captured incorrectly

  • Seed an event with a roster before registration opens for testing or closed-club events

A single import run may mix all four use cases: create new participants, update existing ones, apply corrections, and carry forward external PKs from the source system. The import must decide per row whether it is an insert or an update, and produce an unambiguous per-row outcome so that a stakeholder can sign off on the result.

1.1. Scope

In scope:

  • Bulk XLSX import for EventParticipant (with associated Person / User, Address, RaceNumber assignment, Order, and custom-list values)

  • Upsert-by-registrationId within a target event

  • Configurable auto-creation of custom-list values (school dropdowns etc.) missing from the master list

  • Per-row outcome (CREATED / UPDATED / SKIPPED / WARNING / ERROR) aggregated into a top-level summary

  • Focused issues list containing only non-successful rows

Out of scope (covered elsewhere):

2. Functional Requirements

2.1. FR-EP-001: Bulk XLSX Import Endpoint (async)

ID FR-EP-001

Title

Bulk XLSX Event Participant Import (async)

Priority

High

Description

The system shall expose a single HTTP endpoint that accepts an XLSX workbook and bulk-registers every non-blank row as an EventParticipant in a named event. The endpoint returns asynchronously: the upload creates a background import job and the caller polls a companion endpoint for the final result DTO.

Acceptance Criteria

  • Upload endpoint: PUT /api/event-participants/import

    • Required query parameter: eventId

    • Optional query parameters: orgId (defaults to the caller’s tenant), sheetIndex (default 0, XLSX only), createCustom1/2/3 (default true)

    • Request body: multipart/form-data with a single file field

    • Returns HTTP 202 Accepted + ImportJobDTO with a Location header pointing at the results endpoint

  • Results endpoint: GET /api/event-participants/import/{jobId}

    • Returns HTTP 200 OK + EventParticipantImportResultDTO once the job reaches COMPLETED or FAILED

    • Returns HTTP 409 Conflict while the job is still processing

    • Returns HTTP 404 Not Found if the job identifier is unknown or belongs to a different import type

  • Only the named sheet is processed (multi-sheet workbooks are supported)

  • The header row (row 0) is used to detect column positions; data starts at row 1

  • Blank rows (first five cells empty) are skipped silently

2.2. FR-EP-002: Flexible Header Recognition

ID FR-EP-002

Title

Dictionary-Based Header Alias Matching

Priority

High

Description

The system shall recognise column headers via a case-insensitive alias dictionary, so operators can use the natural label from their source spreadsheet rather than a fixed EMS-specific name.

Acceptance Criteria

  • Each logical column has one or more aliases (e.g. FirstName, First Name, FN, Name all map to FIRST_NAME)

  • Header matching ignores case and whitespace

  • Missing non-required columns default to null (or the column-specific default)

  • Unknown columns are ignored with a log entry

  • Required columns (FIRST_NAME, LAST_NAME) must be present or the import fails early with a 400-class error

2.3. FR-EP-003: Upsert by Registration ID

ID FR-EP-003

Title

Upsert by registrationId within Event Scope

Priority

High

Description

Where a row supplies a registrationId, the system shall treat this as the natural upsert key within the target event and update the matching EventParticipant in place rather than creating a duplicate.

Acceptance Criteria

  • A row’s registrationId column (also accepted as RegistrationID, ExternalID, ExternalEPID) is matched against EventParticipant.registrationId scoped to the target event

  • A match produces an UPDATED outcome; no match produces a CREATED outcome

  • Re-running the same import against the same event does not produce duplicates

  • Re-running does not inflate totalRows count or churn EventParticipant.id

2.4. FR-EP-004: Person Matching

ID FR-EP-004

Title

Identity-Based Person Matching

Priority

High

Description

When a row does not upsert an existing EP, the system shall attempt to match the row’s identity fields (ID number, email, first + last name + DOB) to an existing Person / User before creating a new one, to prevent duplicate Person records.

Acceptance Criteria

  • Identity number (ID_NUMBER + ID_TYPE + ID_COUNTRY) is the strongest match signal

  • Where no identity number is available, (FIRST_NAME + LAST_NAME + DOB) is attempted

  • Email is used as a secondary signal but not sole match criterion

  • An unambiguous match reuses the existing Person; an ambiguous match produces a WARNING outcome for operator review

  • A successful match on an existing Person still results in an EP CREATED outcome if no EP exists for that Person in the event

Related

Person Matching design

2.5. FR-EP-005: Custom List Value Resolution and Auto-Creation

ID FR-EP-005

Title

Resolve and Optionally Create Custom List Values

Priority

Medium

Description

For custom-list columns (e.g. school dropdown), the system shall resolve the row’s text value against the target CustomList. When the value is not present, behaviour is controlled by per-import flags.

Acceptance Criteria

  • Query parameters createCustom1, createCustom2, createCustom3 control auto-creation per custom-list column (default: true)

  • When the flag is true, a missing value is created as a new CustomListValue on the corresponding CustomList and linked to the row

  • When the flag is false, a missing value produces a WARNING outcome and the EP is still created/updated without the custom value

  • Null-safe: a blank custom-list cell is not an error (the EP simply has no custom value for that column)

2.6. FR-EP-006: Race Number Assignment

ID FR-EP-006

Title

Optional Race Number Assignment on Import

Priority

Medium

Description

Where a row supplies a NUMBER or NUMBER_ID, the system shall attempt to assign that RaceNumber to the participant using the standard assignment service layer (not a direct entity write). An absent or blank NUMBER / NUMBER_ID cell means "do not change" — it is not an instruction to clear an existing assignment.

Acceptance Criteria

  • NUMBER alias matches Bib, BibNumber, RaceNumber, No

  • The number is resolved within the event’s NumberType stock

  • Empty / absent cell: the EP’s existing number_id (if any) is preserved. Imports never clear a race-number assignment — clearing is an explicit admin action outside the import path.

  • Populated cell, EP has no number: the resolved number is assigned (IN_STOCK → ISSUED) and a IMPORT_EP row is written to race_number_state_log.

  • Populated cell, EP already has the same number: idempotent no-op (no state change, no log row).

  • Populated cell, EP already has a different number: the import treats this as a number swap. RaceNumberAssignmentServiceEx.recordNumberAssignment(…​, REASSIGNED, …​) fires on the originating EP, and — per Feature #473 / US #505 — the participant’s future same-type EPs receive a DETACHED row and their number_id is cleared for later reconciliation. The import response records the swap for operator visibility. Opt-out is not available from the import path; to do a one-event-only swap, use the dedicated reassignment endpoint with temporary = true after the import.

  • Unresolvable reference: a NUMBER_ID or NUMBER that does not resolve (not found, ambiguous, wrong state) produces a WARNING and the EP is created/updated without the number.

  • The assignment goes through RaceNumberAssignmentServiceEx so that history and validation are preserved.

  • A missing NUMBER cell is never an error.

2.7. FR-EP-007: Order and Order Line Item Handling

ID FR-EP-007

Title

Associate Order on Import

Priority

Medium

Description

Where a row supplies order information (ORDER_NUMBER, ORDER_STATUS, ORDER_PRODUCT_ID, ORDER_PRODUCT_AMOUNT), the system shall create or resolve a SalesOrder and a corresponding OrderLineItem linking the order to the EP.

Acceptance Criteria

  • A blank / absent order block is permitted — the EP is created without order association (no NPE)

  • Default ORDER_STATUS is P (Pending) when the column is absent

  • Re-import of the same row does not create duplicate order line items for the EP

2.8. FR-EP-008: Per-Row Outcome Classification

ID FR-EP-008

Title

Classify Every Row with a Named Outcome

Priority

High

Description

Every data row shall produce exactly one outcome in the response, chosen from a closed set, so that the operator can reason about the file in aggregate.

Acceptance Criteria

  • Outcome values: CREATED, UPDATED, SKIPPED, WARNING, ERROR

  • CREATED / UPDATED are mutually exclusive success paths

  • SKIPPED means the row was not processed (missing required field, duplicate within the same file, etc.)

  • WARNING means the EP was created/updated but some non-fatal issue was detected (ambiguous Person match, custom-list value not auto-created, race number conflict)

  • ERROR means an exception occurred processing the row; the row was not committed

  • Each response carries the row’s firstName, lastName, registrationId, and a human-readable message

2.9. FR-EP-009: Focused Issues List and Summary

ID FR-EP-009

Title

Summary Counters + Focused Issues List

Priority

High

Description

The response shall contain top-level counters for the five outcomes and an issues list containing only rows that need attention (SKIPPED / WARNING / ERROR), keeping the payload focused on what the operator must action.

Acceptance Criteria

  • Response contains: totalRows, created, updated, skipped, warnings, errors

  • Invariant: totalRows == created + updated + skipped + warnings + errors

  • Rows with outcome CREATED or UPDATED are not individually listed (summary counters are sufficient)

  • Rows with outcome SKIPPED, WARNING, or ERROR appear in issues with full row context

  • A clean import has issues = [] and created + updated == totalRows

2.10. FR-EP-010: Idempotent Re-Import

ID FR-EP-010

Title

Idempotent Re-Import

Priority

High

Description

Re-importing an unchanged file against the same event shall produce the same end-state. Every row that was CREATED on first run must be UPDATED on second run; no duplicates, no spurious CREATED outcomes.

Acceptance Criteria

  • Second import produces updated == first-run.created + first-run.updated, created == 0

  • EventParticipant.id values are preserved across runs

  • Associated Person, Address, RaceNumber assignments, and order line items are not duplicated

2.11. FR-EP-011: Error Isolation

ID FR-EP-011

Title

Row-Level Error Isolation

Priority

High

Description

An exception processing one row shall not prevent the rest of the file from being processed. Each row is its own transactional unit.

Acceptance Criteria

  • A row that throws an exception is recorded with outcome ERROR and a diagnostic message

  • Subsequent rows continue to be processed

  • The response HTTP status is 200 OK even when some rows errored (operator reads the summary to decide next action)

  • Logs contain stack traces for every errored row with enough context (row number, identity, registration id) to locate the source line in the file

2.12. FR-EP-012: External ID Preservation for Cross-System Migration

ID FR-EP-012

Title

Preserve Legacy System External IDs

Priority

Medium

Description

The system shall preserve both Person-level and EP-level external identifiers from a source system, so that downstream integrations (timing exports, federation feeds) can cross-reference records.

Acceptance Criteria

  • PERSON_EXTERNAL_ID (aliases UID, PID) is stored on the Person record when supplied

  • EVENT_PARTICIPANT_EXTERNAL_ID (alias EPREF) is stored on the EventParticipant.externalId when supplied

  • The same external id appearing in a later import does not cause a re-link (external ids are write-once-on-create unless the upsert key matches)

  • External ids are scoped to the organisation; collisions across orgs are permitted

2.13. FR-EP-013: Default Values

ID FR-EP-013

Title

Column-Specific Defaults

Priority

Low

Description

Missing values for certain columns shall fall back to documented defaults rather than failing.

Acceptance Criteria

  • ID_TYPE default: NATIONAL

  • ID_COUNTRY default: ZA

  • ORDER_STATUS default: P (Pending)

  • All other non-required columns default to null

  • Defaults are applied consistently regardless of HTTP delivery mode

3. Non-Functional Requirements

3.1. NFR-EP-001: Throughput

The system shall accept an import upload within 5 seconds end-to-end (synchronous upload response, before background processing starts). Background processing of up to 500 rows shall complete within 60 seconds wall-clock; files up to 1,500 rows are observed to complete within 180 seconds. The async delivery mode removes the prior nginx/gateway-timeout cap — the HTTP request returns 202 Accepted as soon as the job is persisted. Post-upload processing latency is observable via the import.processWholeFile Micrometer timer (tag importType=EVENT_PARTICIPANT).

3.2. NFR-EP-002: Memory

The import shall stream rows rather than materialising the entire result set in memory, so that files up to 10,000 rows do not OOM the admin service pod.

3.3. NFR-EP-003: Auditability

Every import run shall log the target event, operator identity (from authentication context), row counts, and response summary. This is the forensic trail for "who imported what, when?".

4. Traceability

Artefact Reference

ADO Feature

#442 (WPCA Event Migration — EP Import & Result Import Improvements)

ADO User Story

#443 (EP Import: registrationId Population & Upsert)

Design Journal

design-journal/2026-04/wpca-event-migration.adoc

Design Doc

Event Participant Import Design

Operations Runbook

Import Operations Guide