[E08] Import Results
Actor & Context
Actor: event organiser, results officer, tenant admin.
Frequency: per event (initial result import, corrections during the verification window).
Precondition: event has finished; EPs exist; user has EVENT_MANAGER or RESULTS_OFFICER permission.
Entry point: Import results button on E04 Event Results; from E05's Results unverified pill click.
Main Flow
-
Upload screen. File picker (CSV/XLSX), result-import-specific options:
-
Identifier mode (
participantIdMode) —EPID/PERSON_ID/REGISTRATION_ID/EXTERNAL_PERSON_UID. Defaults fromEvent.preferredTimingIdentifier. The four modes drive how the inboundExternalReferenceIDcolumn is interpreted to match the EP record (vocabulary: Person-XID / Participant-XID):-
EPID→ matchEventParticipant.id(ours) -
PERSON_ID→ match(Event.id, Person.id)composite (ours) -
REGISTRATION_ID→ matchEventParticipant.registrationId— the Participant-XID (the source system’s entry key, used when EPs were imported from an external registration system) -
EXTERNAL_PERSON_UID→ match the Person-XID viaPersonExternalReference→ person → that event’s EP, scoped by the selected source registration system (US #769; for files where an external system feeds the timing provider directly)
-
-
Source registration system (
sourceSystemId, US #769) — selector listing non-self registration systems. Defaulted fromEvent.sourceRegistrationSystem(set when participants were imported) with a hint saying so. Required when the mode isEXTERNAL_PERSON_UID; optional otherwise. Changing it from the event’s stored value raises a warn-and-confirm dialog ("This event’s source registration system is 'X', set when participants were imported. You are changing it to 'Y'. This updates the event…"); on confirm the new value is persisted back onto the event. -
Override-confirmation — when the operator picks a mode that differs from
Event.preferredTimingIdentifier, an inline warning appears: "You are overriding the event’s default timing identifier ('PERSON_ID'). Re-importing with the wrong mode can wipe existing results — confirm only if you are sure the file uses the alternate identifier." A confirmation checkbox ("I confirm this file uses the selected identifier") gates the Submit button. Legitimate override case: cross-system migration imports. -
Reconciliation scope (
reconciliationScope, US #771, defaultSINGLE_CATEGORY) — how much of the event the file is authoritative for, governing the delete pass.SINGLE_CATEGORYreconciles only the categories present in the file (safe everyday default);FULL_EVENTtreats the file as the complete event export, removing finishers absent from it anywhere in the event. -
Apply number changes (
applyNumberChanges, default off) — when on, detected number swaps/reassignments are applied toEventParticipant.numberafter the result reconciliation pass. Always reported regardless of toggle; toggle controls whether the change is applied. -
Points calculator (
pointsCalculator) — short code (e.g.wpca-road-league,sa-school-cycling) selecting the points strategy. Defaults to the event’s pre-configured calculator; operator can override per-import.
-
-
User clicks Upload —
PUT /api/result-sets/import(multipart) returns202 AcceptedwithLocation: /api/imports/{uuid}. Bookend writeslocalStorage.setItem('importCaller:' + uuid, 'e08:' + eventId)(per-uuid key — supports concurrent imports). -
Redirect to C05 for column mapping + progress. Cells stage is short-circuited (no FK mappings — categories resolved against
Race.category.name); fingerprint stage is bypassed (no source-system gating). -
C05 redirects back on completion to the summary screen.
-
Summary screen. Per-row reconciliation:
-
Reconciliation counts (created / updated / deleted / unchanged result counts).
-
Per-category panel showing imported vs. existing rows.
-
Unresolved EPs surfaced as actionable issues (US #456) with
ExternalReferenceIDvalue + suggested next-action. -
DNF / DNS / Drop entries reported separately (US #457) with synthetic trailing position applied.
-
Non-data-line counts (US #455) — blank lines, repeated headers, malformed rows, unmatched categories.
-
Number-change panel — list of detected number changes (SIMPLE / SWAP / CONFLICT) with applied / not-applied state.
-
Identifier-mode mismatch warning banner — surfaces the two distinct cases:
-
Soft warning — operator’s chosen mode differs from
Event.preferredTimingIdentifierbut resolution succeeded for most rows. Banner: "You imported with mode 'X'; the event default is 'Y'." Informational; non-blocking. -
Delete-guard warning (as shipped, US #771) — for any category where zero rows resolved, or the resolution failure rate exceeded the threshold (
import.result.reconciliation-guard-threshold, default 0.5), the reconciliation delete pass was skipped for that category: resolvable rows were still imported, but no existing results were deleted. Banner: "Deletion skipped for category 'X' — only NN% of rows resolved (likely wrong identifier mode or source system). Existing results were kept." with a "Try again with different mode" link back to upload (file selection retained) and a per-category failure-rate panel. This is a non-destructive guard, not a whole-import abort — the rows that did resolve are imported, and any deletions that occur are listed in the deleted panel above.
-
-
Link to E04 for follow-up.
-
Alternative Flows
-
AF-1: Re-import during correction window — existing results updated in place via upsert-by-seq; summary distinguishes new vs corrected.
-
AF-2: Re-import after correction window closed — soft warning; user must confirm before submit.
-
AF-3: Cancel / fatal error — same patterns as E06.
-
AF-4: Wrong-mode catastrophic abort — backend’s resolution-failure-rate guard returns 422; summary surfaces the hard-error banner; no data changed; operator picks a different mode and retries.
Acceptance Criteria
-
Use-case page authored.
-
Status
design-todo → handoff-readyafter Claude Design pass. -
:design-url:populated. -
Cross-references C05 + E04 + E05.
-
DNF / DNS / Drop entries surfaced separately per US #457.
-
Unresolved EPs surfaced as actionable issues per US #456.
-
Override-confirmation friction shown when operator picks a mode different from
Event.preferredTimingIdentifier. -
Hard-error banner with retry-with-different-mode affordance when backend returns 422 from the resolution-failure guard.
API Surface
| Call | Purpose |
|---|---|
|
Create import job from uploaded file + result-import options ( |
|
Generic import-job status + progress. Same wire shape as E06. |
|
Reads |
|
Drives the upload-screen "Recent imports" rail. Filtered server-side to |
|
Domain-typed summary DTO for the summary screen. Carries reconciliation counts, per-category breakdown, number changes (SIMPLE/SWAP/CONFLICT with applied state), unmatched categories, non-data-row buckets, unresolved EPs, non-finishers, plus the |
(delegates to C05 for column / progress) |
Shared mapping flow. |
Out of Scope
-
The mapping middle steps — C05.
-
EP-import bookends — E06.
-
Mass result correction tooling — separate Feature.
-
RaceResult.externalReferenceIdaudit column — deferred follow-up (event-database release blocker). -
Category change detection apply (multi-pass Pass 3,
applyCategoryChanges) — deferred follow-up; out of scope for first E08 ship.
Design Anchors
Design Decisions
-
Wire E08 through the async-import framework (2026-05-08). Match E06 and the bookend contract rather than fronting the existing synchronous
import-bulkendpoint. Backend wiresRaceResultRowProcessoras a whole-file processor intoImportRowProcessingService(the existingprocessBulkCsvcross-row reconciliation logic moves under the framework). Upload posts toPUT /api/result-sets/import(202+poll); summary readsGET /api/result-sets/import/{jobId}for the domain DTO. Cells stage skipped (no FK mappings); fingerprint stage skipped (no source-system gating). Reuse of C05 columns stage handles timing-system header drift (e.g.Num. LapsvsNumLaps). -
Three identifier modes for participant resolution (2026-05-04). The inbound
ExternalReferenceIDcolumn is interpreted as one of:-
EPID— our internalEventParticipant.id. Used when our system is authoritative for the event lifecycle. -
PERSON_ID— our internalPerson.id. Default for new events; safe across multiple events for the same person. -
REGISTRATION_ID—EventParticipant.registrationId(the source system’s EP UID). Used when EPs were imported from an external registration system and we want timing to round-trip their identifier back.The mode default per Event lives in
Event.preferredTimingIdentifier(set during EP creation, lockable via the timing-export UI gate). Result import reads this default but lets the operator override per-import — useful when re-importing legacy result files exported with a different mode than the Event currently has set.
-
-
Override-confirmation friction on the upload screen (2026-05-08). When the operator picks a mode that diverges from
Event.preferredTimingIdentifier, the upload screen shows an inline warning + requires a confirmation checkbox before Submit enables. Two layers of defence with the backend’s resolution-failure-rate guard. The legitimate override case (cross-system migration) takes one extra click; the catastrophic-mistake case is harder to commit. Backend guard remains authoritative — friction is belt-and-braces. -
Backend resolution-failure-rate guard against catastrophic delete (2026-05-08). When the row-resolution failure rate exceeds a threshold (e.g. >50% of rows in a category, or every row), the reconciliation pass refuses to delete existing data and returns HTTP 422 with a "likely wrong
participantIdMode`" diagnostic. Surfaced on the summary screen as a hard-error banner with a "Try again with different mode" link back to upload (file selection retained). Closes the silent-data-loss vector observed on event 119 in production (2026-04-20: a single `epidvsregidmistake wiped 182 RaceResult records). -
Fourth identifier mode + source registration system, hybrid default (2026-06-07, US #769, shipped). Added
EXTERNAL_PERSON_UIDfor files carrying the source system’s person id (resolved viaPersonExternalReference→ person → EP). The source system is a hybrid:Event.sourceRegistrationSystemis set-if-absent at participant import and defaults the upload selector; the operator may override per-import, and a confirmed change is persisted back onto the event. Rationale: re-prompting the source on every category file is tedious and error-prone; remembering it on the event with a per-import override + confirm keeps it both convenient and correctable. The selector is required only forEXTERNAL_PERSON_UID. See Participant Identity. -
Reconciliation scope + as-shipped delete guard (2026-06-07, US #771, shipped — supersedes the 2026-05-08 HTTP-422 design above). The catastrophic-delete guard shipped as a non-destructive per-category skip, not a whole-import 422 abort: a category whose resolution failure rate exceeds
import.result.reconciliation-guard-threshold(default 0.5), or where nothing resolved, has its delete pass skipped while resolvable rows still import. Paired with an explicitreconciliationScope(SINGLE_CATEGORYdefault vsFULL_EVENT) so single-category uploads cannot reach beyond their categories. Deletions that do occur are surfaced in the summary’s deleted panel. Reframed because the 422-abort would have blocked legitimate partial imports; the per-category skip is both safer and less disruptive. -
Bookend pattern mirrors E06 (2026-05-04). Same upload + summary structure as E06; same C05 caller-key contract pattern; same async-import state machine (minus fingerprint). Result-import-specific options (identifier mode, apply-number-changes, points calculator) live entirely on the upload screen + summary; the mapping middle steps remain content-type-agnostic.
-
Per-uuid caller-key with
eventIdsuffix (2026-05-08, refined from design pass). E08 writeslocalStorage.setItem('importCaller:' + uuid, 'e08:' + eventId)— per-uuid key (supports concurrent imports) with the:<eventId>suffix carrying the event for the back-link target. C05 host reads + removes the key on terminal state and routes to/events/<eventId>/results/imports/<uuid>. Differs from E06’s bare'e06'value: E06 is portfolio-level, E08 is event-scoped. -
DNF and repeated-header toggles dropped (2026-05-08). The earlier draft proposed upload-screen toggles for DNF status handling (US #457) and repeated-header tolerance (Bug #453). Both behaviours have shipped to admin-service as always-on (PRs #146 + #147 merged). No operator decision is needed; the toggles are noise.
Notes
Backend result-import improvements: PR #146 (merged) + PR #147 (merged). USs #455–#458 detail the row-level reporting nuances the summary screen must surface. The 2026-05-08 design wires the existing whole-file processor (ResultImportXLS.processBulkCsv) into the async framework and adds the catastrophic-delete guard. F12 (Event.preferredTimingIdentifier column + GET exposure) is a precondition for the upload-screen default and ships as its own US in this workstream.