[T03] Stock Return Workflow
Summary
Stock-return workflow. Operator scans / imports / pastes a list of returned numbers; system clears person_id and transitions each to IN_STOCK (or preserves UNFIT_FOR_SERVICE and surfaces "recommend disposal"). Treats pre-assigned and stock-held tags identically per the project feedback feedback_returns_stock_model.md.
The endpoint (POST /api/race-numbers/return) is idempotent by construction: re-submitting a number that’s already in IN_STOCK, MANUFACTURED, or DESTROYED produces a NO_OP outcome row, no state mutation, no person_id change. Operators can re-submit a partial list without fear (see Idempotency & double-scan below).
Actor & Context
Actor: stock operator, tenant admin.
Frequency: post-event; periodic stock-takes; ad-hoc loan returns.
Precondition: user has TENANT_ADMIN permission; physical numbers in hand or scanned data ready.
Entry point:
-
Tenant-admin sidebar
Inventory ▸ Return(ADO-541-sidebar-tenant-scope) — empty list. -
T02 bulk-select toolbar
Returnaction — list pre-loaded with the selected ids.
Layout
T03 renders inside C01’s shell — sidebar (tenant scope) + topbar are C01’s. T03 owns the main content area.
| Region | Content |
|---|---|
Topbar |
Breadcrumb |
Stage 1 — Build returned list |
Two-column layout: left = scan / paste / type input (focus-on-mount); right = the returned-list builder showing every entry the operator has added with its lookup status ( |
Stage 2 — Confirmation |
Pre-flight summary card. Counts by predicted outcome (using the row’s current state to predict): |
Stage 3 — Outcome summary |
Per-number outcome list. Same four codes ( |
Right-rail "About this screen" panel |
Persistent collapsible panel (cross-cutting pattern from C04 / C05 / E06). Stage-aware copy: in Stage 1 explains what counts as a valid scan; in Stage 2 explains the four outcome codes; in Stage 3 explains what |
Main Flow
-
Operator arrives at T03 — directly via sidebar, or pre-loaded from a T02 bulk-select.
-
Stage 1 — Build the returned list. Operator scans / pastes / types numbers. For each entry:
-
Client tries to resolve the input to a
RaceNumber.idvia search (number, sequence, or tag-mate barcode — match exactly one, scoped to the tenant). On unique match → row added with current state and predicted outcome chip. On zero matches → row added inNot foundstate. On multiple matches → row added inAmbiguousstate with a small picker (rare; only when a barcode partially matches multiple records). -
Duplicates within the session are silently rolled up — second scan of the same number shows a
Already in listindicator on the existing row, increments a small "scanned ×N" counter, and does not add a new row.
-
-
Stage 2 — Confirmation. Operator reviews the predicted-outcome counts and the optional note.
SubmitcallsPOST /api/race-numbers/returnwith{ numberIds, note }. -
Stage 3 — Outcome summary. Render the response DTO. Group by outcome; offer
Dispose these Nshortcut on theDISPOSAL_RECOMMENDEDgroup; per-row drill-down link. -
From Stage 3 the operator can
Return more numbers(resets to Stage 1, fresh empty list), launch T04 dispose, or clickDoneto return to T02.
Idempotency & double-scan
The return endpoint is idempotent by construction (per RaceNumberResourceEx#returnNumbers Javadoc):
-
ISSUED/IN_USE→IN_STOCK,person_idcleared, logRETURNEDentry. OutcomeRETURNED. -
UNFIT_FOR_SERVICE→ state preserved,person_idcleared, logRETURNEDentry with "recommend disposal" note. OutcomeDISPOSAL_RECOMMENDED. -
IN_STOCK/DESTROYED/MANUFACTURED→ no-op (no state change, noperson_idchange, no log entry). OutcomeNO_OP. -
id not found → outcome
NOT_FOUND.
UI consequences for double-scan:
-
Within a session — rolled up client-side. The second scan shows the
Already in listindicator on the existing row and increments the visible "scanned ×N" counter so the operator gets feedback that the scanner did fire, without bloating the list. Submit only sends the unique id once. -
Across sessions — re-submitting a number that’s already been returned (now
IN_STOCK) produces aNO_OPoutcome row in Stage 3; no state mutation, noperson_idmutation. The summary’sNO_OPgroup surfaces all such rows together so the operator can confirm at a glance that the re-submit was harmless. -
Re-submit after partial network failure — the same property holds: any rows that landed last time are now
IN_STOCKand produceNO_OP; rows that didn’t land transition normally. Operators can safely re-submit the entire returned list rather than reconstructing the partial set. This is the design intent — call it out in the right-rail "About this screen" panel.
The client does NOT pre-filter IN_STOCK rows out of the submit body. Sending them is harmless (idempotent) and the resulting NO_OP outcome rows are evidence of the operator’s actual scanning effort.
Outcome aggregation copy (Stage 3)
| Outcome | Header copy | Sub-context |
|---|---|---|
|
"Returned to stock — N" |
"These numbers were with a participant or in use; they’re now back in stock and available for re-issue." |
|
"Recommend disposal — N" |
"These numbers were flagged UNFIT before return. Ownership cleared. Use Dispose these N below to mark them DESTROYED, or keep them flagged for later." Includes a primary |
|
"Already in stock or terminal — N" |
"Nothing to do for these — they were already in stock, never tracked, or already destroyed. Safe outcome of a double-scan or a re-submit after a partial failure." |
|
"Not recognised — N" |
"We couldn’t find a number matching this scan. Most often a typo, a foreign-system number, or a damaged barcode. Use Edit list to correct or remove." |
The NOT_FOUND group also offers Edit list → returns to Stage 1 with the current list rehydrated, focus on the first NOT_FOUND row’s input.
Alternative Flows
-
AF-1 — Scanned ID not found: row added in
Not foundstate (Stage 1) and surfaces in theNOT_FOUNDgroup at Stage 3. Operator can edit / remove / re-scan. -
AF-2 — UNFIT in the returned list: surfaces as
DISPOSAL_RECOMMENDEDat Stage 3 with theDispose these Nshortcut to T04. -
AF-3 — Loan number return: clears
person_id(state appropriate to the row); preservesevent_participant.number_idhistory for past results (per the lifecycle journal — EP→number link is intentionally retained). -
AF-4 — Operator re-submits the same list mid-session (e.g. clicks Submit twice): client disables the
Submitbutton while the request is in flight; if the network fails, the button re-enables and the operator can retry — re-submission is idempotent (see above). -
AF-5 — Partial server-side failure (e.g. one id throws): per-id outcome row appears in Stage 3 under a fifth code only if the backend returns it. (Today’s endpoint always returns one of the four codes; we don’t pre-design for new codes.)
-
AF-6 — Operator scans a
DESTROYEDnumber:NO_OPoutcome with sub-context "this number was previously destroyed" so the surprise is visible. No special path; just clear copy. -
AF-7 — Bulk pre-load from T02 → T03: arriving with the list rehydrated, Stage 1 still allows editing (add/remove rows, append new scans) before confirming. Operator might combine a T02-selected set with hand-scans before submitting.
Acceptance Criteria
-
Three-stage UI: build → confirm → outcome.
-
Pre-assigned + stock-held returns treated identically; ownership (
person_id) cleared uniformly. EP→number history preserved. -
UNFIT_FOR_SERVICErows surface inDISPOSAL_RECOMMENDEDoutcome with aDispose these Nshortcut to T04. -
Double-scan within a session is rolled up client-side with a visible "scanned ×N" counter; submit body contains the id once.
-
Re-submit across sessions yields
NO_OPoutcome rows; no state mutation. Outcome summary explains this is a safe outcome. -
NOT_FOUNDrows offer anEdit listaction that returns to Stage 1 with the list rehydrated and focus on the first NOT_FOUND row’s input. -
Drill-down link per outcome row navigates to T02 with the row’s bib id pre-opened in the audit-log side panel (loads from
GET /api/race-numbers/{id}/state-logper CD-5). -
Right-rail "About this screen" panel renders stage-aware copy.
-
T02 → T03 pre-load via route state works; operator can edit the list before submit.
API Surface
| Call | Purpose |
|---|---|
|
Batch return. Request: |
|
Stage 1 client-side resolution of scan input → |
|
Drill-down link from outcome rows (CD-5). T03 navigates to T02 with |
|
Used indirectly via the |
Out of Scope
-
Stock browsing — T02.
-
Bulk flag/dispose for UNFIT — T04 (T03 only links to it).
-
Tag-level scanning (scan a tag barcode to identify the paired number) — pending WS3 (
RaceNumber.tag_idFK; Thread D ships #478a; tag-mate scanning UI lives under C02 Track 3). -
CSV import of returned numbers — out of scope this round; pasted spreadsheet rows are absorbed into the Stage 1 input (one number per line) but a structured CSV import path is deferred.
-
Customer notification on return — out of scope this round.
Design Anchors
-
T02 — entry point and post-flight return
-
T04 —
Dispose these Nshortcut fromDISPOSAL_RECOMMENDED -
ADR-0001 —
RaceNumberAssignmentServiceEx.returnNumbersis the single state-log writer -
Project memory:
feedback_returns_stock_model.md— uniform ownership clearing rule -
feedback_explain_disabled_ui.md— disabled-action tooltip rule
Design Decisions
-
Idempotent by construction; UI does not pre-filter (2026-05-07). The backend is idempotent for
IN_STOCK/MANUFACTURED/DESTROYEDinputs (returnsNO_OP). The UI does NOT strip these out client-side because (a) the operator’s intent is "I scanned these"; (b)NO_OPoutcomes are positive evidence of effort; (c) it makes session-restart and partial-failure recovery trivial — re-submit the whole list. -
Within-session de-dup is client-side only (2026-05-07). Same number scanned twice in one session: client rolls it up to a single list entry with a visible "scanned ×N" counter. Submit body contains the id once. Across sessions, no de-dup — re-submit is harmless and produces a
NO_OPoutcome row. -
Three stages, never two (2026-05-07). Build → confirm → summary. The confirmation stage is non-negotiable even when the list is small — operators are about to mutate stock state and the predicted-outcome counts are the last point at which they can back out without a state change. Inline auto-submit (skip Stage 2) was considered and rejected: it removes the only point in the flow where they see what’s about to happen.
-
DISPOSAL_RECOMMENDED routes to T04 (2026-05-07). Returning UNFIT numbers does not auto-dispose them — the dispose action remains explicit (per the lifecycle journal’s WS1a session 2 decision). T03’s outcome summary makes the next step a single click via the
Dispose these Nshortcut to T04, but the actual→ DESTROYEDtransition still goes through T04’s confirm dialog with its irreversible-DESTROYED warning copy. -
Right-rail "About this screen" panel (2026-05-07). Same cross-cutting pattern as C04 / C05 / E06. Stage-aware copy. Default expanded on first open, collapse state persisted in
localStorage. -
No CSV import in v1 (2026-05-07). Pasted spreadsheet rows (one per line) are absorbed into the Stage 1 input — that’s enough for v1. A structured CSV import would mirror E06/E08 + C05 and is deferred until a real ops complaint surfaces.
Open Questions
-
Scan-resolution endpoint — Stage 1 needs to resolve scan input to a
RaceNumber.id. The existing/stock?…&q=covers the common case but is paginated (excessive for one row). If Thread B’s criteria sweep adds a dedicated single-row lookup (e.g.GET /api/race-numbers/lookup?value=…), T03 prefers that. If not, T03 calls/stock?q=…&size=2and treats>1 resultas anAmbiguousoutcome. Action: Thread B reports whether a dedicated lookup is added during the sweep; T03 adopts whatever’s there. Sole remaining open question after CD-4/-5/-6/-7.
Notes
Honour the feedback rule: returns clear ownership uniformly; do NOT scope by event_id or recency. Post-event return clears person_id while preserving EventParticipant.number_id history for past results.
|
Status: |
Appendix A: Claude Design Prompts
|
Prompts persisted for audit trail. Most recent first. The v1 prompt is the active hand-off prompt; it will be re-emitted if Coordination Backlog resolutions change the API surface materially. |
v1 — 2026-05-07 — derived from this .adoc
Status: paste-ready. Source: this .adoc at :status: handoff-ready. State-log endpoint locked per CD-5; list endpoint at /stock per CD-6. Tenant scoping deferred per CD-7.
I'm designing a screen for the EMS admin portal — a Spring Boot + Angular SPA
admin tool used by stock operators and tenant admins to manage race-number
inventory. Visual language matches existing portal screens you've designed
in this project (C01, C03, E01, E05, E06, T02). JHipster 8 / Angular 16 /
PrimeNG / ng-bootstrap stack; match fonts, spacing, palette, density.
Design **T03 — the Stock Return Workflow**: a three-stage screen where a
stock operator scans / pastes / types a list of race numbers being returned
to stock, reviews predicted outcomes, submits, and sees per-number outcome
rows.
T03 sits inside C01's shell. Sidebar is in the `tenant` scope (a new third
scope alongside `portfolio` and `event`, added in this design round) under
the "Inventory" group: Stock (T02), Return (THIS), Bulk (T04), Onboard,
Manufacturer export. T03 owns the main content area.
A cross-cutting design rule: any disabled, hidden-by-policy, or limited
affordance MUST surface its reason inline (tooltip on hover, helper text,
empty-state copy). Operators should never have to guess why a control is
unavailable.
A second cross-cutting rule: every full-screen flow in this portal
includes a persistent collapsible right-rail "About this screen" panel
with stage-aware copy — same pattern as C04, C05, E06.
============================================================
Three stages on ONE screen, driven by component state
============================================================
The flow is: BUILD list → CONFIRM → OUTCOME SUMMARY
stage 1 stage 2 stage 3
A small stage indicator at the top reads `Build · Confirm · Done`. Pills
are read-only (not clickable).
Persistent UI on every stage:
- Stage indicator at top.
- Right-rail "About this screen" panel — collapsible, default expanded,
collapse state persisted in localStorage. Stage-aware copy.
- Top-right "Reset" button — clears the list, returns to Stage 1.
============================================================
Stage 1 — Build the returned list
============================================================
Two-column layout:
- Left column (1fr) — Scan input.
- Big focus-on-mount text input with placeholder "Scan, paste, or type
a number — Enter to add". Accepts: race-number `number` (e.g.
"P3014"), sequence (e.g. "3014"), or tag-mate barcode (long digit
string).
- Below the input: a small live "Last scan: <value>" line so the
operator gets visual feedback that the keywedge fired.
- Optional textarea below for pasting multiple lines (one per line).
Pasted text is absorbed into the list, one entry per line.
- Helper text: "USB keywedge, hand-scanner, or paste from a
spreadsheet — one number per line."
- Optional "Note (applies to this submission)" text input below the
main input. Free text. Passes through to the API as the request's
`note` field.
- Right column (1.4fr) — The returned-list builder.
- Each entry is a row with:
- the scanned input (left, monospace)
- the resolved bib number + state pill (centre) — blank until
lookup resolves
- a predicted-outcome chip on the right — one of:
RETURNED · DISPOSAL_RECOMMENDED · NO_OP · NOT_FOUND · AMBIGUOUS
with a tooltip showing what each predicts.
- delete (X) button on the row's far right.
- When a number is scanned twice in the same session, the existing
row gains a "scanned ×N" badge; no second row is added.
- Stage-1 footer: "{N} entries — Continue" primary button (disabled
while {N} === 0 or any AMBIGUOUS row remains unresolved); helper
counts: `{X} found · {Y} not found · {Z} ambiguous`.
============================================================
Stage 2 — Confirmation
============================================================
Centred summary card:
- Title: "Confirm return — {N} numbers"
- Predicted-outcome counts as four large tiles:
Returned to stock · Recommend disposal · Already in stock · Not recognised
Each tile shows the count and a single-line description of what
happens (or doesn't) for that outcome — see the .adoc copy table.
- Optional note (read-only — shown if the operator typed one in
Stage 1).
- Two buttons at the bottom:
- Secondary: "Back — edit list" → returns to Stage 1 with the list
intact and focus on the input field.
- Primary: "Submit return" — disables on click, fires
POST /api/race-numbers/return, transitions to Stage 3 on response.
A failure-state inline notice appears in this card if the request
fails: "Couldn't submit. {error}. Try again — your list is preserved."
The Submit button re-enables on failure.
============================================================
Stage 3 — Outcome summary
============================================================
Layout:
- Page header: "Returned {N} numbers" + finished-at timestamp.
- Tabbed sections, one per outcome code, in this order with these
exact tab titles + sub-context blurbs:
1. "Returned to stock — N"
"These numbers were with a participant or in use; they're now
back in stock and available for re-issue."
2. "Recommend disposal — N"
"These numbers were flagged UNFIT before return. Ownership
cleared. Use Dispose these N below to mark them DESTROYED,
or keep them flagged for later."
Tab body has a primary button "Dispose these {N}" at the top
that navigates to T04's confirm dialog with the UNFIT ids
pre-loaded.
3. "Already in stock or terminal — N"
"Nothing to do for these — they were already in stock, never
tracked, or already destroyed. Safe outcome of a double-scan
or a re-submit after a partial failure."
4. "Not recognised — N"
"We couldn't find a number matching this scan. Most often a
typo, a foreign-system number, or a damaged barcode. Use
Edit list to correct or remove."
Tab body has an "Edit list" action that returns to Stage 1
with the list rehydrated and focus on the first NOT_FOUND
row's input.
- Tabs that have zero count are still visible but greyed; click
shows the tab body with an empty state.
- Per-row entries inside each tab: scanned input (left), resolved bib
number + state pill (centre), drill-down link "View audit log →" on
the right. Drill-down link navigates to T02 with the bib id in the
drill-down panel pre-opened (T02 reads `?openLog={id}` from the URL
and opens the side panel automatically).
- Footer:
- Secondary: "Return more numbers" — resets to Stage 1 with an
empty list.
- Primary: "Done" — navigates back to T02.
============================================================
Idempotency / double-scan behaviour
============================================================
The right-rail "About this screen" panel must explain on Stage 1, 2,
and 3 that:
- The endpoint is idempotent — re-submitting numbers that are already
in stock produces "Already in stock" outcome rows; nothing else
happens.
- A scanner double-firing on the same number produces a single list
entry with a "scanned ×N" badge — submit body still contains the
number once.
- Operators can safely re-submit the entire list after a partial
failure — rows that landed last time return as "Already in stock"
this time; rows that didn't land transition normally.
============================================================
API surface
============================================================
POST /api/race-numbers/return
Request: { "numberIds": [Long...], "note": "string?" }
Response: { "outcomes": [{ "numberId": Long, "outcome": "RETURNED"|"DISPOSAL_RECOMMENDED"|"NO_OP"|"NOT_FOUND", "scannedInput": "string?", "stateBefore": "...", "stateAfter": "..." }, ...] }
(Stage 1 lookup endpoint — TBD on Thread B's criteria sweep — may be
either a dedicated single-row lookup OR a paginated /stock?q= query.
Render the lookup with whatever shape Thread B confirms.)
POST /api/race-numbers/dispose (used by the "Dispose these N" shortcut)
See T04.
GET /api/race-numbers/{id}/state-log
Returns: List<RaceNumberStateLogDTO> ordered by createdDate DESC.
T03 does NOT call this directly; the drill-down link navigates to
T02 with `?openLog={id}` and T02's side panel calls the endpoint.
============================================================
Output
============================================================
For each of these screen states:
- Stage 1 — empty list (just the input)
- Stage 1 — list with mixed rows (RETURNED + DISPOSAL_RECOMMENDED +
NOT_FOUND + a "scanned ×3" double-scan row)
- Stage 2 — confirmation with 12 / 4 / 2 / 1 outcome counts
- Stage 3 — outcome summary with all four tabs populated, "Recommend
disposal" tab open showing the Dispose-these-N shortcut button
- Stage 3 — outcome summary with a NOT_FOUND-heavy result, "Not
recognised" tab open showing the Edit-list affordance
- Stage 2 failure inline notice
Provide:
- HTML/JSX + matching styles per stage / state.
- Screen README explaining the three stages, the idempotency
contract (and how it surfaces in the UI), the four outcome codes,
and the API endpoints consumed.
- Cross-screen note: T03 hands off to T04 (modal launch) for the
Dispose-these-N shortcut, and back to T02 for `Done`. The T02 and
T04 screens are designed separately in this same round.