[C02] Number Capture

Summary

Tenant-scoped, scanner-driven onboarding screen that pairs a manufactured RaceNumber with its physical tag (RFID / chip). A stock operator works from a list of MANUFACTURED numbers for one NumberType, scans the printed number barcode, scans the attached tag barcode, confirms, and the row transitions to IN_STOCK with RaceNumber.tag_id populated. Companion to T06 (file-based onboarding) — the same operational outcome, different input modality.

Actor & Context

Actor: stock operator (typically a tenant-admin assistant working at a workbench with a handheld dual-scanner rig — one trigger reads number, second reads tag). Scope: tenant.

Frequency: bursty — runs through a manufactured batch in one or two sittings after stock arrives, then idle until the next batch lands.

Precondition:

  • User has TENANT_ADMIN permission (same gate as T02).

  • WS3 (US #478a) is in production: RaceNumber.tag_id FK exists. Without that column the screen has nothing to write to.

  • At least one RaceNumber row exists in MANUFACTURED state for the chosen NumberType (typically created by T06 or by a manual seed script).

  • Operator has a handheld scanner that emits the barcode payload as keyboard input followed by Enter (the standard mode for HID-class scanners).

Entry point: tenant sidebar → InventoryOnboardScan (route /inventory/numbers/onboard/scan per the admin-portal plan). Also reachable from a "Pair tag" button in the row drill-down on T02 when the row is in MANUFACTURED.

Main Flow

  1. Pick a NumberType. Operator selects the NumberType the batch belongs to (e.g. Race-bib-2026, Walking-medal-2026). The screen scopes the working list to that type for the rest of the session.

  2. Load working list. Page loads MANUFACTURED RaceNumbers for that NumberType, filtered to the user’s current tenant, ordered by sequence. List shows sequence, number, current tag_id (empty for fresh stock), and a state pill.

  3. Pick a mode. Two modes, both keyboard-only, switchable at any time:

    • Single-pair mode: process one row at a time. Each row commits as soon as both scans land. Default mode.

    • Bulk mode: queue multiple pairs in a working buffer (visible as a side rail), then commit the buffer atomically when the operator hits the Commit batch button.

  4. Number scan. Operator pulls trigger 1 → scanner emits the number’s barcode payload + Enter. The screen looks up the matching MANUFACTURED row by (numberType, number) from the loaded list. Match → row gets focus, prompt becomes "now scan the tag". No match → AF-1.

  5. Tag scan. Operator pulls trigger 2 → scanner emits the tag’s barcode payload + Enter. Tag barcode is captured into the focused row’s pending pair. No backend call yet.

  6. Confirm. In single-pair mode, the screen requests confirmation (visual flash + the operator hits Enter again, or clicks Confirm) and POSTs POST /api/race-numbers/{id}/tag with the tag barcode. On 2xx the row’s state pill flips to IN_STOCK, tag_id populates, focus advances to the next MANUFACTURED row in sequence. In bulk mode, the pair lands in the working buffer; commit is deferred until Commit batch.

  7. Commit batch (bulk mode only). Operator reviews the buffer, hits Commit. Backend processes each pair sequentially via the same POST /api/race-numbers/{id}/tag endpoint; per-pair failures stay in the buffer with an error chip. Successful pairs leave the buffer and reflect in the working list as IN_STOCK.

  8. Continue. Operator repeats from step 4 until the batch is paired. The KPI strip ("`MANUFACTURED`: 248 / 1000 — paired this session: 752") is the working completion gauge.

Alternative Flows

  • AF-1 — Number scan does not match. Possible causes: wrong NumberType selected, number already paired (no longer MANUFACTURED), number does not exist. Screen surfaces a non-blocking inline error chip on a "scan log" rail showing the unrecognised payload + suspected reason; focus does not advance. Operator re-scans or clicks the chip to dismiss.

  • AF-2 — Tag scan duplicate. The tag barcode already pairs another RaceNumber row in this tenant. Server responds 409 with the conflicting raceNumber.id. Screen offers two paths: Skip (clear the pending pair, stay on the row) or Re-pair (release the previous pairing then commit this one). Re-pair calls DELETE /api/race-numbers/{previousId}/tag followed by the original POST. Re-pair is logged as two state-log entries on the previous row (per ADR-0001’s single-writer rule) and is irreversible.

  • AF-3 — Skip a row. Operator wants to defer a row (e.g. tag missing on the physical number). Hit the Skip control on the focused row → row stays MANUFACTURED, focus advances to the next row. No backend call. Skipped rows surface in a "skipped this session" tab so the operator can return.

  • AF-4 — Undo last commit. In single-pair mode, an Undo control is available for ~5 seconds after the last successful commit. Calls DELETE /api/race-numbers/{id}/tag. After the grace window the pairing must be reverted via T03 / T04 flows; C02 does not surface a generic un-pair affordance because that would re-purpose this screen for what T02 already does well.

  • AF-5 — Re-scan within the same row. Operator scans the tag, realises it’s the wrong tag, scans again before confirming. Latest scan wins; no backend call has happened yet so the buffer simply updates.

  • AF-6 — Bulk commit partial failure. Some pairs in the buffer fail server-side (e.g. tag barcode collision per AF-2). Screen renders a per-pair status next to the buffer entry; failed pairs stay in the buffer with their reason; successful pairs leave. Operator resolves the failed pairs row-by-row.

  • AF-7 — Network drop mid-commit. Single-pair mode renders an inline retry chip on the row; bulk mode keeps the unsubmitted buffer intact. No state-log entries are written for unsubmitted pairs (the SPA never writes optimistically).

  • AF-8 — Tenant scope drift. Defence-in-depth: even though the working list is filtered server-side, the SPA double-checks each scanned row’s organisationId matches the active tenant context before submitting. A mismatch (only possible if the tenant context has been switched in another tab) blocks the commit with a "switch back to <tenant>" notice.

Acceptance Criteria

  • Use-case page authored (this page).

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

  • :design-url: populated.

  • Cross-references T02, T06, T07.

  • Working list is tenant-scoped; cross-tenant rows never appear.

  • Both single-pair and bulk modes produce identical state-log audit entries (one per pair, written via RaceNumberAssignmentServiceEx per ADR-0001).

  • Re-pair (AF-2) writes two state-log entries on the previously paired row (release + dispose-of-pairing).

  • Number scans that don’t match the loaded list never block the operator — they land on the scan log only.

  • Tag-barcode collisions surface a clear Skip / Re-pair choice.

  • All keyboard-only — no mouse interaction required to complete a pair.

API Surface

Call Purpose

GET /api/race-numbers?type=&state=MANUFACTURED&sort=sequence,asc&size=…​

Load the working list. Tenant scope is implicit via the existing organisation filter on RaceNumberQueryService (Track 1 / Thread B verifies; see Risk #3 in the plan).

GET /api/race-numbers/types

Populate the NumberType picker.

POST /api/race-numbers/{id}/tag

Pair a tag barcode to a MANUFACTURED RaceNumber. Server: looks up or creates the Tag for this tenant by barcode, sets RaceNumber.tag_id, transitions state to IN_STOCK, writes a state-log entry through RaceNumberAssignmentServiceEx (ADR-0001 single-writer). 409 on barcode collision; body carries the conflicting raceNumber.id.

DELETE /api/race-numbers/{id}/tag

Release the current tag pairing. Used by AF-2 (re-pair) and AF-4 (undo). State transitions back to MANUFACTURED. State-log entry written.

GET /api/race-numbers/stats?groupBy=state&type=

Drives the KPI strip. Same endpoint Thread B is verifying for T02; reused here filtered by NumberType.

The pairing endpoint (POST /api/race-numbers/{id}/tag) is a Track 3 backend deliverable — see plan § Track 3 / Backend deliverables. Until it lands, this page documents the target shape; implementation is gated on Track 2 merge.

Out of Scope

  • CSV/XLSX-driven onboarding — T06 (entry bookend) and T07 (exit bookend).

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

  • Stock take, return, flag, dispose — T02 / T03 / T04.

  • Pre-event assignment — E07.

  • Auto-assignment UX — explicitly out of this round per the orchestration plan.

Design Anchors

Design Decisions

  • Scanner-as-keyboard input (2026-05-07). The screen treats both scanners as HID keyboard devices emitting <payload><Enter>. No browser-level scanner APIs (Web HID, WebUSB) — those are still patchy across the operator’s likely browser stack and the keyboard-emulation mode is the operational standard for the existing equipment. Implication: the screen needs a single focused input element that round-robins between number and tag based on the screen’s pairing state machine; the operator never touches the keyboard outside of pulling triggers.

  • Two-scan-then-confirm (2026-05-07). Single-pair mode requires an explicit confirm (Enter or click) between the tag scan and the backend POST, even though both scans can land in <500 ms. Rationale: scanners occasionally double-fire; without a confirm step a stray double-fire on the tag scanner would commit a malformed pair. The confirm is the brake. Bulk mode skips per-pair confirm in favour of a single batch-level Commit.

  • Re-pair as explicit two-step (2026-05-07). When AF-2 (tag collision) fires, the screen does NOT silently overwrite the previous pairing. The operator must consciously choose Re-pair, which releases the previous row first (DELETE) and then pairs the current row (POST). Two state-log entries land on the previous row. Rationale: silent overwrite is the bug class that allows lost audit trails; explicit two-step is auditable and reversible only via a normal lifecycle flow (T03/T04).

  • Skip is non-destructive; Undo is grace-window-only (2026-05-07). Skip never writes anything backend-side; the row stays MANUFACTURED. Undo is available for ~5 seconds after a successful commit and writes a state-log entry to revert. Beyond the grace window, a pair can only be reversed via T03 (return) or T04 (dispose) — C02 deliberately does not become a generic un-pair tool.

  • Tenant-scoping is server-enforced + SPA-double-checked (2026-05-07). The list endpoint filters by tenant server-side (Risk #3 verification under Thread B). The SPA additionally double-checks each scanned row’s organisationId matches the active tenant context before commit (AF-8). Rationale: cross-tab tenant switches can momentarily desync the SPA’s tenant context with the server filter; the SPA-side check protects against the operator continuing to scan into the wrong tenant after switching.

  • No per-event scoping (2026-05-07). The list filter does NOT include event scope — manufactured numbers are tenant inventory, not event inventory. Pre-assignment to events is E07's scope.

  • Sequence-ordered working list (2026-05-07). The list is sorted by sequence ascending so the operator can match the physical pile order (manufacturers ship in sequence). Other sorts are available but sequence is the on-load default.

Notes

Backend dependency: this screen requires US #478a (RaceNumber.tag_id FK) in production. Until Track 2 merges, the .adoc spec stands but the screen cannot be implemented end-to-end. The pairing endpoint POST /api/race-numbers/{id}/tag is also a Track 3 backend deliverable — see plan § Track 3 / Backend deliverables.

Resolved Decisions

Q-E1 and Q-E2 (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-E1 (filter-out): the working list shows only MANUFACTURED AND tag_id IS NULL rows. AF-2 absorbs the explicit re-pair flow when needed. The Main Flow above already encodes this.

  • Q-E2 (per-pair POST iteration): bulk mode iterates POST /api/race-numbers/{id}/tag per pair, concurrency limit 1. Matches the ADR-0001 single-writer pattern; keeps the state-log audit trail granular. No batch endpoint.