[T06] Import Manufactured Numbers

Revision required — this page does not match the real operator workflow

Raised 2026-09-03 from the Darling Brew Gravel-X onboarding. Status moved handoff-readydesign-todo. Do not hand this off or build it as written. See Feature #1132 for the corrected specification.

Number and tag onboarding has always been done by direct SQL; admin-portal has never been used operationally. So this page is unbuilt, not disproven — nothing in production contradicts it. But the workflow it assumes is not the one the operator actually has:

The worked example to design against is the 2026-09-05 Gravel-X batch: 350 rows, one board missing (excluded entirely, not half-created), 349 numbers + 349 tags + 349 pairings, numbers zero-padded to 3 because that RaceNumberType was manufactured that way — padding is a property of the type, not a global rule.

Summary

Tenant-scoped entry bookend of the manufactured-numbers async import flow. A stock operator uploads the CSV/XLSX their manufacturer returned (sequence + number + barcode triples) and hands off to C05 for processing. The exit bookend is T07. Mirrors E06 in structure but with one defining difference: the column schema is fixed (sequence, number, barcode are non-negotiable), so there is no source-system dropdown and C05’s column-mapping stage is auto-applied silently — see C05's Fixed-schema imports design decision.

Actor & Context

Actor: stock operator (tenant-admin assistant). Same actor as C02; T06 is the file-based alternative when the manufacturer ships a digital deliverable instead of physical-sequence-only stock.

Frequency: bursty per delivery — once per manufactured batch, then idle.

Precondition:

  • User has TENANT_ADMIN permission.

  • WS3 (US #478a) is in production: RaceNumber.tag_id FK exists.

  • ImportType.NUMBER_ONBOARDING is registered and NumberOnboardingRowProcessor is live (Track 3 backend deliverable).

  • The operator has a CSV/XLSX file conforming to the fixed schema.

Entry point: tenant sidebar → InventoryOnboardImport (route /inventory/numbers/onboard/import). Resume from My imports for a paused job.

Main Flow

  1. Upload screen. Single full-screen page.

    • Page title: "Import manufactured numbers — <tenant name>".

    • Fixed-schema banner — prominent, top-of-page: "This import expects a fixed schema. Required columns: sequence, number, barcode. Other columns are ignored. No mapping step required." A small Download template button next to the banner serves a tenant-neutral CSV template with the three headers and one example row.

    • File picker (CSV/XLSX) with drag-and-drop zone.

    • NumberType picker (REQUIRED) — dropdown listing the tenant’s NumberType rows. Drives which inventory the imported rows land under. Helper text: "Which catalogue does this batch belong to?"

    • No source-system dropdown. Numbers do not have a source-system concept; the file is always understood as "raw manufacturer output". The C05 target-field filter does not apply to T06.

    • Recent imports list below the inputs — date, filename, NumberType, row count, status. Clicking a row opens the matching T07 read-only.

    • Right-rail "About this screen" panel — collapsible; default expanded; per-screen localStorage key. Copy explains the fixed schema, what barcode may be (RFID/chip vs absent), and what state the rows will land in.

    • Primary action: Upload — disabled until file picked AND NumberType picked. Tooltip on the disabled button explains whichever requirement is missing.

  2. Submit. POST /api/imports?importType=NUMBER_ONBOARDING&numberTypeId=X (multipart). On 201, take the returned uuid and write localStorage.setItem('importCaller:' + uuid, 't06'). Navigate to C05 at /imports/{uuid}.

  3. Mapping flow (delegated). C05 auto-applies the fixed-schema column mapping silently — Stage 1 (Columns) renders briefly with a stage-bar pill labelled Auto-applied and immediately advances. Stage 2 (Cells) only runs if any rows reference an unknown FK (e.g. a numberTypeKey column that didn’t match server-side; for T06’s three-column schema this is rare). Stages 3-5 (Verifying / Processing / Done) follow the standard C05 contract — note that Verifying is always skipped for T06 because the fingerprint check is identity-flag-driven and T06 has no is_self/trustPKs flags. The Verifying pill renders SKIPPED with the standard "skipped — file is from an external system" tooltip.

  4. Terminal redirect. C05 reads importCaller:{uuid} on COMPLETED / FAILED / CANCELLED and redirects to T07 at /inventory/numbers/onboard/imports/{uuid}/summary.

Alternative Flows

  • AF-1 — File missing required column. The fixed-schema validator rejects the upload at POST /api/imports with 400 + a message identifying the absent column. Upload screen surfaces the rejection inline; file is not consumed.

  • AF-2 — Barcode column has empty cells. Permitted: rows without a barcode are accepted as MANUFACTURED with tag_id left NULL — these get paired later via C02. The summary screen reports the count under "rows imported without a tag".

  • AF-3 — Duplicate (numberType, sequence) within the file. Detected during validation; the import job lands in PROCESSING but the duplicate rows accumulate in the row-results bucket and surface in T07's "duplicate sequence" section.

  • AF-4 — (numberType, sequence) already exists in the database. Idempotent — the row is treated as a no-op (state-log entry recorded for audit, no schema change). T07 reports the count under "rows already on file".

  • AF-5 — Cancel during PROCESSING. Standard C05 cancel; T07 renders with CANCELLED state and the partial breakdown.

  • AF-6 — Pause / resume. Standard async-import behaviour; resume via My imports.

Acceptance Criteria

  • Use-case page authored (this page).

  • Status design-todo → in-design → handoff-ready after Claude Design pass and Coordination Backlog resolution.

  • :design-url: populated.

  • Cross-references C05, T07, C02, T02.

  • Fixed-schema banner is visible at the top of the upload screen and identifies the three required columns.

  • No source-system dropdown is rendered.

  • NumberType picker is required; Upload button is disabled without it.

  • On submit the caller key importCaller:{uuid} is written with literal 't06'.

  • C05 column-mapping stage transparently auto-applies (per the Fixed-schema imports decision in C05).

  • Rows without a barcode are accepted and counted separately in T07.

API Surface

Call Purpose

POST /api/imports?importType=NUMBER_ONBOARDING&numberTypeId=X

Create the import job. Multipart upload (file). Server validates the fixed-schema header up-front and returns 400 with the missing column name if invalid.

GET /api/number-types?tenantId=<implicit>

Populate the NumberType picker.

GET /api/imports?importType=NUMBER_ONBOARDING&size=N

Drives the "Recent imports" list (filtered to T06’s importType).

GET /api/race-numbers/templates/manufactured-numbers.csv

Serves the static "Download template" CSV. Tenant-neutral; schema only. (Optional — could be a shipped static asset rather than a dynamic endpoint.)

(delegates to C05 for column / cell / progress)

Shared mapping flow with auto-applied column mapping.

Out of Scope

  • The mapping middle steps — C05.

  • The summary screen — T07.

  • Scanner-based onboarding — C02.

  • Manufacturer-data-file export (the upstream "send sequence to manufacturer" tool) — separate Track 3 screen, not yet authored.

  • Per-tenant fixed-schema variants (e.g. additional colour / additionalInfo optional columns) — backend already accepts these but the .adoc spec keeps the banner copy strict to avoid operator confusion. The optional columns are listed in the Notes section as a forward-looking item.

Design Anchors

Design Decisions

  • Fixed schema, no source-system selector (2026-05-07). Numbers do not have a provenance concept analogous to participants — there is no "WPCA Legacy vs Entry Ninja" axis for manufactured stock. The schema is dictated by the manufacturer’s deliverable shape and is the same across every tenant. Implication for C05: the Fixed-schema imports path applies (see C05’s design decision); column mapping auto-applies silently and the operator never sees Stage 1 as a working step.

  • NumberType is the one required selector (2026-05-07). The CSV doesn’t carry NumberType per row because manufactured batches are NumberType-homogeneous (one batch = one bib design = one type). Putting NumberType on the upload screen avoids a mandatory numberTypeKey column inside the file that the operator would have to populate identically on every row.

  • Barcode column is optional per row (2026-05-07). A row without a barcode is acceptable — it lands as MANUFACTURED with tag_id NULL and is later paired via C02. This is the default state for tag-less stock (race medals, T-shirts that double as numbers, etc.) and the mixed case where some bibs have RFID chips and some don’t.

  • Idempotent re-import (2026-05-07). (numberTypeId, sequence) is the natural key. Re-importing the same file (or a corrected file with overlapping sequences) is a no-op for already-imported rows, with a state-log entry recorded for audit. Rationale: manufacturers occasionally re-send the file with corrections; making T06 idempotent removes a footgun.

  • No update mode (2026-05-07). Unlike E06's Overwrite / Append distinction, T06 has no such toggle. The semantics are always "create-on-miss, no-op-on-hit". If the operator wants to change a paired tag, that’s C02's re-pair flow (AF-2). If the operator wants to dispose of stock, that’s T04. Each lifecycle action has one canonical screen.

  • No identity flags / no fingerprint stage (2026-05-07). T06 carries no trustPKs / updatePII / is_self flags. The C05 Verifying pill renders SKIPPED with the existing tooltip pattern (per project memory feedback_explain_disabled_ui.md). Tooltip text: "Skipped — manufactured-number imports do not need fingerprint verification."

Notes

Backend dependency: requires ImportType.NUMBER_ONBOARDING registered in ImportFieldDefinitionRegistry plus the new NumberOnboardingRowProcessor. Both are Track 3 backend deliverables (see plan § Track 3). Until they land, this page documents the target shape; the screen cannot be implemented end-to-end.

The fixed schema may grow optional columns over time:

  • colour — for tenants that order multi-colour bibs and want the colour recorded against each RaceNumber.

  • additionalInfo — free-text per-row note (e.g. batch-internal lot number).

These are out of the current spec. The banner copy stays strict ("Required columns: sequence, number, `barcode`") so the operator’s mental model is uncluttered; if a future delivery introduces an optional column, the banner is updated explicitly rather than hedging now.

Both columns are now required, and carried by US #1270 — but they are not equally settled. The Southern Suburbs Schools 2026 series (events 149, 150, 151) showed both are load-bearing rather than conveniences, and both were backfilled by hand on 2026-09-17:

  • Colour identifies the race on the day. That tenant orders one bib colour per age group — Pale Red for Prep Prep, Pale Yellow for ages 7-8, Pale Blue for 9-10, Pale Orange for 11-13. With nothing recorded, no check could tell that a rider’s board matched their race, and six riders at event 151 started on the wrong colour. All six were caught by eye.

  • Roughly half of a batch is personalised. 390 of 805 boards carry a rider’s first name printed on them — a board reading "Robin" is made for one rider and should not be handed to another. Unrecorded, nothing objects when it is.

Both values are already in the manufacturer’s spreadsheet; the import discards them today. Their storage differs, and the difference matters to anyone building this:

  • Colour is settled. race_number.colour already exists and already carries values for 17,825 boards of other types. The work is to read the column, not to model it.

  • The printed name has no column yet. race_number.additional_info is empty across that tenant’s boards, which is why the manual backfill used it, but it holds condition notes ("Scrapped", "U/17") on 2,111 boards of other types. Overloading it is cheap; a dedicated column keeps the two meanings apart. That decision is open and sits with US #1270 — do not read additionalInfo here as the chosen schema.

When these land, this Notes block and the fixed-schema banner copy are both updated — per the rule above, the banner is made explicit rather than left hedging.

Resolved Decisions

Q-E3 and Q-E4 (the questions parked during initial authoring) were resolved by Coordination Decision CD-8 in the number/tag UI orchestration on 2026-05-07. Both took the proposed Default leans:

  • Q-E3 (backend endpoint): the template CSV ships as GET /api/race-numbers/templates/manufactured-numbers.csv, served by admin-service through the gateway. The SPA download link in the upload screen targets that path. Implementation lives in the backend US (US #733).

  • Q-E4 (row-level validation): sequence validation is row-level (positive integer, up-to-bound check) and surfaced in T07's "Other issues" bucket. Matches the rest of the import flow’s validate-at-row-level convention.