ADR-0002: Result import accepts IN_STOCK as implicit issuance

Status

Accepted

Date

2026-04-24

Deciders

[email protected]

Related

ADR-0001, ADR-0005, Feature #473, US #475

1. Context

The formal number lifecycle is MANUFACTURED → IN_STOCK → ISSUED → IN_USE. In the "happy path" a number is issued to a participant at registration (triggering IN_STOCK → ISSUED) and races under that person (triggering ISSUED → IN_USE on the first result-import row).

Reality is messier. The registration desk is a busy place, and the formal issuance step is not always executed:

  • Participant arrives and reports their regular board is broken, lost, or left at home.

  • Registration staff grabs a number off the stock rack and hands it to them.

  • No "lost number" workflow is run against the old number; no "issue replacement" action is recorded on the new one.

  • The event proceeds; the number races; the timing system picks up the result.

  • The CSV result import is the first time the database learns that the new number was used.

A naive implementation of recordResultImport would see a number in state IN_STOCK on a result row, classify this as an anomaly, emit a WARN, and leave the state alone. That’s wrong on two counts:

  1. It treats a legitimate (if informal) operational pattern as a system error, polluting the anomaly list.

  2. It leaves the race_number in IN_STOCK with person_id = NULL even though the number has clearly been used — making every downstream query wrong.

2. Decision

When recordResultImport encounters a RaceNumber in state IN_STOCK:

  1. Transition the state directly IN_STOCK → IN_USE (skipping ISSUED).

  2. Attach RaceNumber.person from EventParticipant.person.

  3. Stamp last_used = eventDate.

  4. Write a RESULT log entry to race_number_state_log with the note implicit issuance via result import (no prior ASSIGNED log).

  5. Do not emit a WARN. This is a documented path, not an anomaly.

The note on the log entry is load-bearing: it preserves the provenance that no prior ASSIGNED log exists for this number, so an auditor reading the log can distinguish "this number followed the formal process" from "this number was implicitly issued".

3. Consequences

3.1. Positive

  • Ops can do their job at the registration desk without fighting the system. The software catches up to reality instead of forcing reality to catch up to the software.

  • The anomaly list stays useful — only genuinely unexpected states (MANUFACTURED with a result, DESTROYED with a result) surface there.

  • The audit trail is still complete. The "implicit issuance" note is greppable and queryable; dashboards can count implicit vs. formal issuance as a proxy for desk discipline.

  • Downstream state-based queries (IN_USE = "currently racing", IN_STOCK = "available") give correct answers after the first result row lands.

3.2. Negative

  • Mid-event status boards (which read RaceNumber.state) cannot distinguish "this number has been implicitly issued" from "this number is still in stock" until the first result arrives. A number handed to a participant at 08:00 still shows IN_STOCK in a stock-count query until results flow later in the day.

  • Ops has no incentive to run the formal ISSUED path. Over time this might erode data quality around in-flight assignments, because "the system will sort it out from the results" becomes the default.

  • Analytics that care about issuance timing (as opposed to usage timing) need to query race_number_state_log for ASSIGNED/IMPORT_EP rows; the race_number table alone can’t answer "when was this number issued?".

3.3. Neutral

  • The state machine allows IN_STOCK → IN_USE as a valid transition. Other flows (e.g. manual admin actions) could in principle use the same transition; we deliberately do not expose it outside the result-import path.

4. Alternatives Considered

4.1. Alternative A: Reject — force the formal process

Treat IN_STOCK at result-import time as an error. The admin would have to go back, run the lost/replace workflow retroactively, then re-run the import. Rejected: this pushes complexity onto the busiest people in the system. Retroactive data entry from a registration desk that has already moved on is a recipe for worse data, not better.

4.2. Alternative B: Transition via ISSUED (two-step, two log entries)

Emit two log entries — IN_STOCK → ISSUED (reason ASSIGNED) and ISSUED → IN_USE (reason RESULT) — so the log looks the same as the happy path. Rejected: it lies about the history. No operator issued the number; the system inferred it. A forensic audit should be able to tell those cases apart. The single RESULT entry with the "implicit issuance" note is the honest record.

4.3. Alternative C: Flag the number as IN_USE but leave person null

Promote state but skip the person attachment. Rejected: the result row does carry EP context, and the EP does have a person. Leaving person_id null would require special handling in every downstream query ("is the number IN_USE with a person, or without?") — the complexity compounds forever.

5. References

  • Design journal: design-journal/2026-03/number-tag-management.adoc (session 3 — result-import semantics)

  • Code: admin-service/src/main/java/za/co/idealogic/event/admin/service/RaceNumberAssignmentServiceEx.java — method recordResultImport

  • ADO: Feature #473, US #475