Import: Results
|
Related reading
|
1. Overview
Race results are imported from a CSV produced by the timing system (RaceDay Scoring). A single file typically contains rows for multiple EventCategory groups — the importer groups them by the Registration Event column and reconciles each group against the matching Race’s ResultSet. Results are upserted by seq (row order within a group), so re-importing the same file updates existing records in place rather than churning row IDs.
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.
|
The previous single-race XLSX endpoint ( |
2. API
2.1. Endpoint Reference
| Endpoint | Purpose |
|---|---|
|
Upload a multi-category CSV 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 RaceResultRowProcessor runs in whole-file mode, delegating to ResultImportXLS.processBulkCsv(…) — the same bulk importer that produced the response DTO inline in the pre-async implementation. The resulting BulkResultImportResponseDTO 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/result-sets/import-bulk
?eventId={long} (required)
&participantIdMode={epid|regid|pid} (default: epid)
&applyNumberChanges={true|false} (default: false)
&pointsCalculator={short-code|FQCN} (default: SA School)
Content-Type: multipart/form-data
Body: file=<csv>
HTTP/1.1 202 Accepted
Location: /api/result-sets/import/{jobId}
{ "identifier": "<jobId>", "status": "PROCESSING", "skipMappingPhases": true, ... }
# operator polls ...
GET /api/result-sets/import/{jobId}
HTTP/1.1 200 OK # once COMPLETED or FAILED
{ ... BulkResultImportResponseDTO ... }
HTTP/1.1 409 Conflict # while still processing
See Import Operations Runbook for the polling procedure, cancellation semantics, and failure recovery steps.
2.3. Request Body
Multipart form with a single file part containing the CSV. A single file may contain multiple concatenated per-category sections — each with its own header row and blank-line separator — and the importer groups rows by the Registration Event column.
Query parameters:
-
eventId(required) — target event; all groups in the file must resolve toEventCategoryrecords within this event -
participantIdMode(defaultepid) — how to interpretExternal Reference ID: EP id (epid), legacy registrationId (regid), or Person/User id (pid). Choosing wrongly can empty the ResultSet — see Mode-Mismatch Risk (Outstanding #10). -
applyNumberChanges(defaultfalse) — whentrue,SIMPLEnumber changes (target bib is unassigned stock) are auto-applied;SWAPandCONFLICTare always reported but never auto-applied -
pointsCalculator(defaultSA School) — short code or FQCN; see Points Calculators
2.4. Response: BulkResultImportResponseDTO
{
"eventId": 117,
"fileLines": 615,
"summary": {
"dataRows": 592,
"imported": 591,
"skipped": 1,
"nonDataRows": {
"blankLines": 11,
"repeatedHeaders": 11,
"malformedRows": 0,
"totalNonData": 22
}
},
"categories": [
{"categoryName": "Sub Nipper Boys", "raceId": 4131, "resultSetId": 527,
"rowCount": 107, "importedCount": 107, "skippedCount": 0}
],
"unmatchedCategories": [],
"numberChanges": [...]
}
Invariants:
-
fileLines == 1 (header) + summary.nonDataRows.totalNonData + summary.dataRows -
summary.dataRows == summary.imported + summary.skipped
If the maths doesn’t add up, the parser bailed mid-file. Non-zero summary.skipped is the primary "look here" signal.
Bucket definitions:
| Bucket | What it counts |
|---|---|
|
Data rows successfully written to a ResultSet (created or updated by upsert-by-seq). |
|
Data rows that could not be processed: unresolved |
|
Empty separator lines, expected in concatenated exports. Skipped silently. |
|
Rows whose category column equals the literal header text |
|
Category names that did not resolve to an |
2.5. What to Look At
After polling returns the DTO, these are the patterns to confirm the import landed the way you expected:
-
First import of an event:
imported == dataRows,skipped = 0,unmatchedCategories = [] -
Re-import after a correction: same as first-import, plus
RaceResult.idvalues in the database are unchanged compared to before the re-import (upsert-by-seq preserves identity) -
Normal "number change noise": if the event’s
NumberTypehas noRaceNumberstock loaded, every row triggers aCONFLICTnumber-change report. This is operational data config, not an import error. -
summary.imported = 0: almost always aparticipantIdModemismatch — see Mode-Mismatch Risk (Outstanding #10). Stop and investigate before re-running.
See Import Operations Runbook for the full recovery procedures.
3. Column Definitions
The timing system (RaceDay Scoring) produces a CSV with one header row at the top, followed by rows for each finisher. The same export may concatenate per-category sheets — each sheet retains its own header row and a blank line between sections.
| Column | Required | Notes |
|---|---|---|
|
yes |
Identifier resolved per |
|
yes |
Maps to |
|
- |
Numeric finishing position. Non-numeric values ( |
|
- |
|
|
- |
Integer lap count, stored on |
|
- |
Triggers number-change detection against |
|
- |
Currently informational only (used for logs and the issues list). |
Header normalization strips spaces, slashes, dashes, underscores, and periods, then uppercases — so Num. Laps, NumLaps, NUM_LAPS all collapse to the same key.
4. Processing Flow
4.1. Multi-Pass Architecture
The bulk CSV import processes the file in sequential passes. Each pass reads state collected by the previous pass.
PASS 1 — Results Reconciliation
For each Registration Event group in CSV:
- Resolve EventCategory, Race, ResultSet
- Reconcile ResultSet (upsert-by-seq, delete untouched)
- Detect number changes: collect list of (EP, oldBib, newBib)
- Detect category changes (design-only): collect (EP, oldCat, newCat)
PASS 2 — Number Changes (if applyNumberChanges=true)
- Classify collected changes: SIMPLE / SWAP / CONFLICT
- Apply SIMPLE changes via RaceNumberAssignmentServiceEx
- Report SWAP / CONFLICT as DETECTED
PASS 3 — Category Changes (if applyCategoryChanges=true)
- NOT YET IMPLEMENTED (design captured in the journal)
- Would cascade via StartGroupParticipantServiceEx
Why the passes matter: collecting all changes before applying enables swap detection for numbers (EP_A and EP_B trading bibs 5 and 7). It also means the operator can review number-change reports from Pass 1 before deciding to re-run with applyNumberChanges=true.
See design-journal/2026-04/result-import-improvements.adoc for the full multi-pass design including the deferred Pass 3 (category changes).
4.2. Upsert-by-Seq
seq is the 1-based row index within each Registration Event group. On import:
-
Build a map of existing
RaceResultbyseqwithin the ResultSet. -
For each CSV row in the group: if a matching
seqexists, update in place; otherwise insert. -
After processing all rows for a group: delete any existing
RaceResultwhoseseqwas not touched (participant removed from the export). -
Legacy rows with
seq = NULL(created before this scheme) are deleted on first import after deploy.
This preserves RaceResult.id across re-imports, which keeps any external references stable. Verified in production 2026-04-20 on WPCA Road League events 120 and 121 (515/515 rr_id values preserved across a re-import).
4.3. Full Reconciliation Semantics
The ResultSet is an exact mirror of the CSV per Registration Event group. This is a full reconciliation, not a partial upsert. After a successful import:
-
Every CSV row has a matching
RaceResult(inserted or updated) -
Every pre-existing
RaceResultwhoseseqis absent from the CSV has been deleted -
A participant whose
Registration Eventvalue differs from the prior import naturally disappears from the old ResultSet and appears in the new one
This semantic has a sharp edge: if every row fails to resolve (wrong participantIdMode, stale file), the ResultSet is emptied. See Mode-Mismatch Risk (Outstanding #10).
4.4. Number Change Detection
For each row with a Bib value, the imported bib is compared against EventParticipant.number. Mismatches are collected, then classified:
| Type | Meaning |
|---|---|
|
Target bib’s |
|
Two participants each hold the other’s new bib. Detected and reported with |
|
Target bib is held by another person and there is no matching reverse change, OR the bib does not exist as a |
applyNumberChanges=true applies SIMPLE changes via RaceNumberAssignmentServiceEx.addNumberNoLaterThan(…).
5. Points Calculators
The PointsCalculatorFactory resolves the pointsCalculator query-parameter value to a concrete calculator. Both short codes and fully-qualified class names are accepted.
| Short code | Class | Scheme |
|---|---|---|
(default, no param) |
|
SA School Cycling Series — sliding scale down to ~position 67 |
|
|
WPCA Road League — top 12: 50/45/40/36/34/32/30/28/26/24/22/20; position 13+: 2 points (participation); non-finishers: 0 |
Calculators are invoked once per RaceResult after position is parsed. Non-finishers (position = NULL) receive 0 points regardless of the calculator’s position→points function, enforced by a regression test in ResultImportXLSIT.
6. Source Code
| File | Purpose |
|---|---|
|
Bulk CSV importer. |
|
Whole-file bridge — parses |
|
REST endpoints |
|
Response payload, including |
|
Find-or-create ResultSet for a race. |
|
Number assignment with duplicate detection used by |
|
Pluggable points calculation; SA School Cycling and WPCA Road League implementations available. |
Location: admin-service/src/main/java/za/co/idealogic/event/admin/
7. Outstanding Items
Tracked in design-journal/2026-04/result-import-improvements.adoc (see Outstanding Issues table):
-
#2: DNF/DNS/Drop status handling — non-numeric
Placevalues currently becomeposition = NULL. Partial admin-service-only subset shipped (ADO-457); schema changes deferred (ADO-458). -
#3: Category change detection & apply (Pass 3) — designed in the journal, not implemented.
-
#7: Surface unresolved EPs in an
issueslist on the response. ADO US #456. -
#9: This document kept in sync with the latest implementation state.
-
#10: Mode-mismatch catastrophic delete guard — see Mode-Mismatch Risk (Outstanding #10). Production incident 2026-04-20, no ADO ticket yet.
7.1. Mode-Mismatch Risk (Outstanding #10)
When every CSV row fails participant resolution, all rows are counted as skipped, no seq is touched, and the reconciliation delete pass removes every pre-existing row as "absent from the file". This is a silent catastrophic delete with no distinguishing signal in the response — summary and category counts look the same as a file containing zero matching participants.
Surfaced in production on 2026-04-20 (event 119 wiped by an epid vs regid operator error). Proposed fix: gate the delete pass on a resolution-success threshold per category and return a 422-style diagnostic when the threshold is not met.
Until the guard ships, operators must rely on the pre-flight checklist in Import Operations Guide.