Import Bookend Contract

1. Overview

The async-import flow has two halves:

  • Bookends — importer-specific upload + summary screens. E06 (Import Participants), E08 (Import Results), T06 (Import Manufactured Numbers), and any future M01 / S01 flow each own their own pair. Bookends know the file shape, the source-system semantics, and the per-row outcome rendering for that import type.

  • Common stages (C05) — shared columns / cells / verifying / processing screens that the operator walks through between the bookends. Codified once in c05-mapping/c05-host.component.ts and the four stage-*.component.ts siblings. Agnostic to import type — driven entirely by the IImportJob model the bookend’s upload returns.

This page defines the contract that lets a new bookend pair reuse the C05 host without modification. Read Async Import Architecture first — it covers the state machine, wire shapes, F-feature surfaces, and the source-system gating matrix that the contract below is built on.

2. Architecture

bookend-contract

The bookends call admin-service through their per-entity REST resource (PUT /api/event-participants/import, PUT /api/result-sets/import-bulk, …​) which orchestrates the generic framework internally. The C05 host calls the generic framework’s /api/imports/{uuid}/* endpoints directly — those are import-type agnostic. This is the boundary that makes the C05 host reusable.

3. Bookend responsibilities

An importer-specific bookend pair (upload + summary) must provide the following — each item is enforced either by the type system or by the runtime contract on admin-service.

Concern Bookend responsibility

Importer key (string)

A short stable identifier (participants, results, numbers, members). Pinned at the SPA boundary in the bookend’s import-service module and threaded through every F10 lookup call. Backend’s ColumnMappingTemplate.importerKey and analyzeAndCreateMappings(importType) use it as a partition key.

ImportType (Java enum)

One of EVENT_PARTICIPANT, MEMBERSHIP, RESULT (extend the enum to add a flow). The framework dispatches to the matching ImportRowProcessor via this value.

Row processor

A Spring @Component implementing ImportRowProcessor for the ImportType. Either whole-file (supportsWholeFile()=true + processWholeFile) or row-by-row (processRow). See Whole-File Bridge for when each applies. The row processor owns the F-feature emission for that flow — typed failure metadata, merge candidates, fingerprint findings — see Wire-shape contract.

Field-definition registry entries

For every column the import recognises: name, aliases (case-insensitive), required, foreignKey, mode flags (selfOnly / externalOnly). Registered with ImportFieldDefinitionRegistry keyed by ImportType. Drives auto-matching and the C05 columns stage’s filtered target-field dropdown.

Source-system semantics

Whether the import flow uses the (is_self, trustPKs, updatePII) gating. EP does — every other flow today does not. Bookends that don’t need it set sourceSystemId to null on upload and the C05 host renders the simpler verifying-skip path.

Upload form fields

The bookend’s REST resource accepts a flow-specific multipart form. EP-import takes (eventId, sourceSystemId, trustPKs, updatePII, templateVariantKey, acknowledgeFingerprintWarning, sheetIndex, createCustom1/2/3). Result-import takes (eventId, participantIdMode, applyNumberChanges, pointsCalculator). Whatever the form takes, it lands in ImportJob.configJson and the row processor parses what it needs.

Caller-key + routing

The bookend writes a per-uuid caller key to localStorage on upload (e.g. e06:<eventId>) so the C05 host can resolve the back-link target on entry. C05 reads the caller key — never embeds the originator in its own routes.

Summary rendering

The summary bookend renders the per-row outcomes its flow produces. Generic outcomes (CREATED, UPDATED, FAILED) render the same way across flows; typed outcomes (UNRESOLVED_PERSON, FK_MISMATCH) only fire when the row processor emits the matching RowFailureMetadata.category. Bookends that don’t produce typed outcomes can render the generic branch only.

Recent-imports list

Each upload bookend’s "Recent imports" panel filters GET /api/imports?recent=N by its own ImportType so a portfolio of mixed flows doesn’t surface unrelated jobs.

4. Common-stage assumptions about IImportJob

The C05 host treats IImportJob as a tagged-union state object. Each stage reads a specific subset and mutates none of it directly — submission flows through service-layer methods (submitColumnMappings, submitCellMappings, resumeFromFingerprint, cancel).

Stage Reads from IImportJob

c05-host

state (drives stage-routing); eventId for caller-key lookup; failureReason + finishedAt for the redirect on terminal entry.

stage-columns

state === 'COLUMN_MAPPING' gate; sourceSystem (drives the F10 lookup tuple — implicitly via getColumnMappingPreview(uuid) on the service). The stage fetches its own data via getColumnMappingPreview rather than reading columnMapping from the job — the host doesn’t pre-bundle it.

stage-cells

state === 'CELL_MAPPING' gate; cellMapping.groups (host pre-bundles via fetchCellMappingBundle from the service-layer adapter).

stage-verifying

state ∈ {FINGERPRINT_CHECK, FINGERPRINT_WARN, FINGERPRINT_ABORT} gate; fingerprint.{sampleSize, inconsistentCount, samples} (host pre-bundles via mapFingerprint); sourceSystem.isSelf for predicate display.

stage-processing

state === 'PROCESSING' gate; progress.rowsProcessed, progress.startedAt, rowCount for the elapsed/rate labels. startedAt comes from server-side processing_started_at — the stage falls back gracefully when null.

Summary bookend

state ∈ {COMPLETED, FAILED, CANCELLED} gate; failureReason, finishedAt, the IImportResultsPage (counts, rows, mergeCandidatesCreated, ignoredColumns) returned by getResults.

The C05 host polls getJob(uuid) every few seconds while the operator is on a non-terminal stage; once the job reaches terminal state, polling stops and the host redirects to the summary bookend (resolved via the caller key).

5. Wire-shape contract

The contract above relies on these typed surfaces. Bookends should not invent their own wire shapes for these — extending the existing DTOs keeps the C05 host agnostic.

Surface Purpose

ImportJobDTO (admin-service) → IImportJob (admin-portal)

The polling target. Already carries the F1–F10 surface: sourceSystemId, trustPKs, updatePII, templateVariantKey, acknowledgeFingerprintWarning, processingStartedAt, fingerprint sub-DTO, the cell-mapping bundle (assembled service-side), and the failure-reason / finished-at terminal fields.

ImportColumnMappingDTOIColumnMappingRow

Columns stage. Carries id, sourceHeader, targetField, status, samples (Feature #686 / F10b — first 3 source-column values).

ImportCellMappingDTOICellMappingValue

Cells stage. Carries id, targetField, sourceValue, targetEntityId, status, occurrenceCount. Grouped client-side by targetField into ICellMappingGroup.

FingerprintFindingsDTOIImportJob.fingerprint

Verifying stage. Per-row sample comparisons with field-level deltas.

RowFailureMetadataIImportRowResult.{outcome, rawFirstName, rawLastName, fkType, fkValue}

Summary stage. The typed (category, fkType, fkValue, rawFirstName, rawLastName) sub-DTO drives the typed-branch UX. Producers that emit the typed metadata get the typed branches; producers that don’t get the generic FAILED branch.

MergeCandidateRef (carried in ImportRowResult.merge_candidates_json)

Summary stage. F8 merge-candidate refs surface in the per-row drill-down + the cross-row merge-review panel.

ColumnMappingTemplateDTO

F10 — the variant picker (E06) and the C05 auto-apply both consume the same shape. mappings is parsed eagerly from mappingJson so the SPA receives (sourceHeader, targetField) pairs rather than a raw blob.

6. Cookbook — adding a new import flow

The shape that worked for EP-import scales. To add a new flow (e.g. Membership Import = M01, or Result Import-redux on the new framework):

  1. Decide row-by-row vs whole-file.

    • Row-by-row when each input row maps to one output row independently (membership add). Use processRow(…​); the framework handles iteration and per-row commit boundaries. ImportRowResult is persisted per row out of the box.

    • Whole-file when cross-row logic dominates (RESULT: number-change detection, per-category seq upserts). Set supportsWholeFile()=true and implement processWholeFile(…​). Counter-write contract on the managed ImportJob matters — see Whole-File Bridge.

  2. Extend ImportType. Add the flow’s enum value in event-database (one-char code matters for the converter). Bump the database SNAPSHOT.

  3. Implement the row processor. A @Component extending AbstractRowProcessor that returns the new ImportType from getImportType(). Wire whatever services it needs (e.g. MembershipServiceEx, RaceResultServiceEx). Decide the F-feature opt-ins:

    • If the flow has source-system semantics (SELF / EXTERNAL) → opt into the gating matrix in the controller.

    • If the flow benefits from F7 fingerprint check → override supportsFingerprintCheck() to return true.

    • If the flow has typed failure modes → emit RowFailureMetadata from the failure paths via recordFailureMetadata(…​).

  4. Register field definitions. Per column: name, aliases (the C05 columns stage’s auto-match runs against these case-insensitively), required, foreignKey (drives the cells stage), mode flags (selfOnly / externalOnly for source-system filtering). The ImportFieldDefinitionRegistry lookup is keyed by ImportType.

  5. Add the per-entity REST resource. Mirror EventParticipantResourceEx.importEventParticipants:

    • PUT /api/<entity>/import — multipart with the flow’s form fields. Hydrate configJson from the form params and call importJobService.createAndAutoStartImportJob(…​) (interactive C05 flow) or createWholeFileImportJob(…​) (one-shot).

    • GET /api/<entity>/import/{jobId} — domain-typed response DTO once COMPLETED/FAILED. Parse resultPayloadJson to the flow’s response shape.

  6. Build the upload bookend (Angular). Mirror e06-upload.component.ts:

    • Source-system dropdown (or skip if not applicable) — call ImportService.listSourceSystems().

    • Flow-specific form fields (event picker, period picker, …​).

    • Variant picker (F10) — call ImportService.listTemplateVariants(sourceSystemId). Three cardinality states.

    • Recent-imports panel — ImportService.listRecent(N) filtered to the flow’s ImportType.

    • Submit handler calls the per-entity upload endpoint, writes the caller key, navigates to /imports/<uuid>.

  7. Build the summary bookend. Mirror e06-summary.component.ts:

    • Header switches on terminal state (COMPLETED / FAILED / CANCELLED + the FINGERPRINT_ABORT-converted-to-FAILED case).

    • Counts panel (IImportResultsPage.counts).

    • Per-row results table — render typed outcomes via the corresponding metadata fields (rawFirstName/rawLastName for UNRESOLVED_PERSON, fkType/fkValue for FK_MISMATCH, generic description for FAILED).

    • Merge-candidate panel — surfaces mergeCandidatesCreated (F8). Hide when empty.

    • Ignored-column panel — surfaces ignoredColumns (F9). Hide when empty.

  8. Register routes. The flow’s own bookend routes plus the shared /imports/<uuid> C05 route. The C05 host resolves back-link targets via the caller key — no flow-specific routing inside C05.

  9. Tests. Per the EP-import precedent:

    • Backend: RowFailureMetadataTest analogue if the flow emits typed failures; <entity>ImportXLSTest for the whole-file path; ImportRowResultDTOTest already covers the failure-metadata parsing.

    • Frontend: ImportService unit tests (when added — admin-portal currently has no frontend test stack); UI smoke via the C05 host’s existing fixture-driven tests.

  10. Docs. Update F-Feature Status if the flow consumes a new F-feature; cross-link the per-flow design doc and use-case `.adoc`s.

7. Worked example — Result Import (E08)

E08 is the next bookend pair to land. It reuses the contract above with these specifics:

Concern E08 specifics

Importer key

results

ImportType

RESULT (already exists)

Row processor

RaceResultRowProcessor (already exists, whole-file via ResultImportXLS.processBulkCsv)

Source-system semantics

Not applicable — result imports come from timing systems, not registration systems. The upload form skips the source-system dropdown and the (is_self, trustPKs) matrix; the verifying stage is short-circuited.

Upload form

eventId, participantIdMode (epid | regid | pid — replaces the EP-flow’s sourceSystemId/trustPKs), applyNumberChanges (default false), pointsCalculator (short code or FQCN). Already wired on PUT /api/result-sets/import-bulk.

Variant picker (F10)

Same shape as E06 — same /by-source endpoint with importerKey=results. Result variants are likely thinner than EP (one per timing-system export schema) but the cardinality state machine is identical.

Typed failure metadata

Today’s RaceResultRowProcessor emits result.error(…​) calls only — RowFailureMetadata is not yet wired. Future retrofit mirrors the EP pattern: NotFoundException for unresolved category / race / number → FK_MISMATCH(entity, parameters). Until that lands, the E08 summary’s typed-branch slots stay empty and the generic FAILED branch carries the message.

Summary

BulkResultImportResponseDTO already exists with categories / number changes / unmatched categories / reconciliation counters. The E08 summary bookend renders these flow-specific panels in addition to the generic IImportResultsPage shape.

Use-case .adoc

docs-event/modules/use-cases/pages/E08-import-results.adoc (currently :status: design-todo). Status flips to in-design when the bookend pair starts and built when it lands.

The C05 host (columns / cells / processing stages) is reused unchanged. Verifying stage is bypassed because results don’t have a fingerprint check.

8. Worked example sketch — Membership Import (M01)

Hypothetical, pending US sequencing:

Concern M01 sketch

Importer key

members

ImportType

MEMBERSHIP (already exists)

Row processor

MembershipRowProcessor (already exists, row-by-row — no bulk importer for membership add)

Source-system semantics

Likely applicable — federation imports may come from external systems (other federations, club databases). Same (is_self, trustPKs, updatePII) matrix as EP would apply if Person identity matching is in scope. To be confirmed during M01 design.

Upload form

periodId (membership period), sourceSystemId (if applicable), trustPKs / updatePII (if applicable), sheetIndex. Existing PUT /api/memberships/import already takes periodId + sheetIndex; source-system fields would extend the form.

Typed failure metadata

Same producer retrofit pattern as EP — emit FK_MISMATCH for MembershipType / Discipline / Person lookup failures.

Summary

MembershipImportResultDTO already exists. Aggregates per-row ImportRowResult rows on demand.

Open design questions to resolve when M01 starts: whether Person matching reuses EventParticipantServiceEx.register’s F4 matching (probably yes — the same identity-correlation logic applies); whether membership-period scoping changes the cell-mapping FK targets (e.g. `MembershipType candidates filtered by period).

9. What still varies between flows

The contract isn’t perfectly uniform — three concerns where flows legitimately differ:

  • Source-system semantics. Only EP (and probably future M01) have them. The C05 host already gates on sourceSystem != null; flows that opt out simply pass null and the verifying stage is a no-op.

  • Typed failure categories. RowFailureMetadata.category is a String for forward-compat — flows can emit category values the SPA hasn’t seen yet (frontend falls through to GENERIC). Adding a new typed branch on the summary screen is a paired backend (producer retrofit) + frontend (adapter case) change.

  • Whole-file vs row-by-row. Each flow picks one and the framework dispatches automatically. The bridge is documented in Whole-File Bridge.

  • Async Import Architecture — state machine, wire shapes, F-feature catalogue, source-system gating matrix. Read first.

  • [C05 Import Mapping Flow] — the shared host’s design spec.

  • [E06 Import Participants] — the bookend that drove the contract.

  • [E08 Import Results] — the next bookend (currently design-todo).