Matching Instrument Results to LIMS Orders by Accession Number

Problem Statement

The accession number is the one identifier printed on the specimen label, echoed by the analyzer, and stored on the LIMS order — which makes it the natural join between a result and the work that was requested. But the accession the instrument sends back is not always the accession you can trust: it may arrive in the filler field of one message and the placer field of another, it may be zero-padded on the label and unpadded in the frame, and a single transposed digit produces a different but still numeric value that can silently collide with another patient’s order. This page implements accession-based result-to-order binding as a concrete Python path: pull the accession from the right HL7 field or instrument frame, validate its check digit before it is ever used as a key, look up the single open order it names, and quarantine the result when no open order is found — never guessing a nearby one.

Prerequisites

Before wiring this into the pipeline, confirm the following baseline:

  • Runtime: Python 3.11+, pydantic>=2.6, and pytest>=8 with pytest-asyncio for the async fixtures.
  • Parsed message: the inbound ORU or instrument frame is already tokenized upstream by the Instrument Data Ingestion & HL7/CSV Pipelines tier, with segment fields addressable — this page reads fields, it does not re-implement the HL7 lexer defined in HL7 v2 Segment Mapping.
  • Order store: an out-of-process store of orders queryable by accession, exposing which orders are still open (status = "ordered").
  • Check-digit scheme: the accession-numbering scheme your LIMS issues, including its check-digit algorithm. This page uses a mod-10 (Luhn) example; substitute your facility’s scheme.
  • Context: the reconciliation contract from Patient Identity & Order Matching, since a resolved order is necessary but not sufficient — demographics must still corroborate before release.

Step-by-Step Implementation

Step 1: Extract the accession from the message

The accession can appear in several places depending on the feed. In HL7 v2 ORU messages it is most reliably the filler order number in OBR-3, sometimes the placer order number in OBR-2, and occasionally repeated in an OBX sub-identifier or a proprietary instrument frame field. Extract it with an explicit precedence and normalize immediately — trim, upcase, strip separators — so downstream comparison is against a canonical form.

python
from __future__ import annotations

import re
from pydantic import BaseModel, ConfigDict, field_validator

_SEP = re.compile(r"[\s\-_/]")


class AccessionRef(BaseModel):
    model_config = ConfigDict(frozen=True, extra="forbid")

    raw: str
    source_field: str  # "OBR-3", "OBR-2", "OBX-4", or "frame"

    @field_validator("raw")
    @classmethod
    def _normalize(cls, v: str) -> str:
        norm = _SEP.sub("", v).strip().upper()
        if not norm:
            raise ValueError("empty accession")
        return norm


def extract_accession(obr: dict[str, str], frame: dict[str, str]) -> AccessionRef:
    """Pull the accession by precedence: filler > placer > OBX > frame."""
    for field, source in (
        (obr.get("OBR-3"), "OBR-3"),
        (obr.get("OBR-2"), "OBR-2"),
        (frame.get("obx_sub_id"), "OBX-4"),
        (frame.get("specimen_id"), "frame"),
    ):
        if field and field.strip():
            return AccessionRef(raw=field, source_field=source)
    raise ValueError("no accession present in OBR, OBX, or instrument frame")

Recording source_field is not cosmetic: when a feed regresses to sending the accession in an unexpected field, the audit record shows which field was actually used, which is what lets you tell a mapping bug from a labeling error.

Step 2: Validate the check digit before using the key

Never look up an order with an unvalidated accession. A transposed digit yields a syntactically valid, wrong accession that can match a different patient’s order — the exact mechanism of a wrong-patient result. Validate the check digit first and reject at the boundary if it fails; the identifier is malformed, not merely unmatched.

python
def _luhn_ok(digits: str) -> bool:
    total, parity = 0, len(digits) % 2
    for i, ch in enumerate(digits):
        d = ord(ch) - 48
        if d < 0 or d > 9:
            return False
        if i % 2 == parity:
            d *= 2
            if d > 9:
                d -= 9
        total += d
    return total % 10 == 0


def validate_accession(acc: AccessionRef) -> str:
    """Return the numeric accession if its check digit verifies, else raise."""
    digits = "".join(ch for ch in acc.raw if ch.isdigit())
    if len(digits) < 2:
        raise ValueError(f"accession {acc.raw!r} too short to carry a check digit")
    if not _luhn_ok(digits):
        raise ValueError(f"accession {acc.raw!r} failed check-digit validation")
    return acc.raw

If your scheme uses a different algorithm — mod 11, a base-36 alphanumeric scheme, or a facility-specific weighting — swap _luhn_ok but keep the contract: a bad check digit is a hard reject, never a fallback to fuzzy lookup.

Step 3: Look up the single open order

With a validated accession, query the order store for open orders bearing that accession. Exactly one open order is a match. Zero is the routine no-order-found case that must quarantine. More than one is a data-integrity defect — an accession is meant to be unique to one order — and must quarantine while raising an alarm.

python
import datetime as dt
from enum import Enum
from typing import Protocol


class Disposition(str, Enum):
    BOUND = "BOUND"
    QUARANTINE_NO_ORDER = "QUARANTINE_NO_ORDER"
    QUARANTINE_AMBIGUOUS = "QUARANTINE_AMBIGUOUS"


class Order(BaseModel):
    model_config = ConfigDict(frozen=True, extra="forbid")

    order_id: str
    accession: str
    identity_key: str
    status: str  # "ordered" while open


class OrderStore(Protocol):
    async def open_by_accession(self, accession: str) -> list[Order]: ...


class BindResult(BaseModel):
    disposition: Disposition
    accession: str
    source_field: str
    order_id: str | None = None
    identity_key: str | None = None
    reason: str | None = None


class AccessionMatcher:
    def __init__(self, store: OrderStore) -> None:
        self._store = store

    async def bind(self, acc: AccessionRef) -> BindResult:
        accession = validate_accession(acc)
        orders = await self._store.open_by_accession(accession)
        if not orders:
            return BindResult(
                disposition=Disposition.QUARANTINE_NO_ORDER,
                accession=accession, source_field=acc.source_field,
                reason="no_open_order_for_accession",
            )
        if len(orders) > 1:
            return BindResult(
                disposition=Disposition.QUARANTINE_AMBIGUOUS,
                accession=accession, source_field=acc.source_field,
                reason="multiple_open_orders_for_accession",
            )
        order = orders[0]
        return BindResult(
            disposition=Disposition.BOUND,
            accession=accession, source_field=acc.source_field,
            order_id=order.order_id, identity_key=order.identity_key,
        )

Step 4: Quarantine the no-order-found result

A quarantined result is held, never dropped and never speculatively bound. The routine cause is timing — the result beat its order into the system — so the quarantine queue is re-drained on a schedule and on order-placement events, and a result that finds its order on a later pass binds cleanly. Emit a record for every disposition so both the bind and the hold are auditable.

python
async def process(acc: AccessionRef, store: OrderStore,
                  quarantine: "QuarantineSink") -> BindResult:
    outcome = await AccessionMatcher(store).bind(acc)
    if outcome.disposition is not Disposition.BOUND:
        await quarantine.hold(
            accession=outcome.accession,
            reason=outcome.reason or "unspecified",
            source_field=outcome.source_field,
            held_at=dt.datetime.now(dt.timezone.utc),
        )
    return outcome
Accession extraction, validation, lookup, and quarantine flow A top-to-bottom flow. The accession is extracted from the OBR, OBX, or instrument frame. A check-digit validation step branches right to a hard reject when the check digit fails. A valid accession drives an open-order lookup that reads the open-orders store on the right. The lookup forks at the bottom two ways: a single open order binds the result and order pair; no open order quarantines the result for later re-drain. Extract accession OBR-3 · OBR-2 · OBX-4 · frame Validate check digit mod-10 verify before use invalid Reject malformed key Lookup open order by accession · status = ordered Open Orders out-of-process one order no order Bind (result, order) Quarantine held · re-drained later

Verification & Testing

Prove the two dispositions that hide wrong-patient errors: a bad check digit must reject, and a missing order must quarantine rather than bind to anything.

python
import pytest

OPEN = Order(order_id="ORD-9", accession="ACC90209",
             identity_key="pt-3", status="ordered")


class OneOpen:
    async def open_by_accession(self, accession: str) -> list[Order]:
        return [OPEN] if accession == "ACC90209" else []


@pytest.mark.asyncio
async def test_valid_accession_binds_single_order():
    ref = AccessionRef(raw="acc-90209", source_field="OBR-3")
    out = await AccessionMatcher(OneOpen()).bind(ref)
    assert out.disposition is Disposition.BOUND
    assert out.order_id == "ORD-9"


@pytest.mark.asyncio
async def test_no_order_quarantines_not_binds():
    ref = AccessionRef(raw="ACC90290", source_field="OBR-3")
    out = await AccessionMatcher(OneOpen()).bind(ref)
    assert out.disposition is Disposition.QUARANTINE_NO_ORDER


def test_bad_check_digit_rejects():
    with pytest.raises(ValueError, match="check-digit"):
        validate_accession(AccessionRef(raw="90210", source_field="frame"))

Expected: all three pass. test_valid_accession_binds_single_order confirms normalization and the happy path (acc-90209 upcases and de-hyphenates to ACC90209). test_no_order_quarantines_not_binds proves a near-miss accession (ACC90290, an adjacent-digit transposition of the open one that still passes its own check digit) does not bind to the neighboring open order — it quarantines. test_bad_check_digit_rejects proves the malformed key (90210, a bad check digit) never reaches the store. Add a golden fixture for the ambiguous case — two open orders on one accession — asserting QUARANTINE_AMBIGUOUS and an alarm, so an integrity defect is caught in CI.

Compliance Note

CLIA §493.1242 requires positive specimen identification and an audit trail of the specimen throughout testing. Validating the accession check digit before any lookup, binding only to a single open order, and quarantining — rather than guessing — when no order is found are the operational expression of that positive-identification duty. Recording the source_field, the validated accession, the disposition, and the reason code for every result satisfies the 21 CFR Part 11 expectation that the binding decision be attributable and reconstructable; writing those records to an append-only sink under the CLIA §493.1105 retention window keeps them available for inspection. Because the accession keys a result to a patient’s order, the same demographic corroboration the Patient Identity & Order Matching stage requires must still run before release — a matched accession alone is not a released result.

Troubleshooting

Results quarantine as "no order" but the orders clearly exist.

Root cause: the accession is being read from a different field than the order stores it under — the instrument echoes it in OBR-2 (placer) while the order was filed under OBR-3 (filler), or one side is zero-padded and the other is not. Fix: confirm the extraction precedence matches the feed and normalize identically on both sides; the recorded source_field on the quarantine record tells you which field actually supplied the value.

A transposed accession bound to the wrong patient's order.

Root cause: the accession was used as a lookup key without check-digit validation, so a transposition produced a different valid accession that happened to name another open order. Fix: call validate_accession before any store query and treat a failed check digit as a hard reject, never a fallback to a fuzzy or nearest-match lookup.

Two open orders share one accession and the result binds nondeterministically.

Root cause: the accession-numbering scheme allowed reuse, or an order was duplicated, so more than one open order carries the same accession. Fix: return QUARANTINE_AMBIGUOUS whenever the lookup yields more than one open order and raise a configuration alarm — an accession must be unique to a single order, so this is a data-integrity defect to correct upstream, not a tie to break.

Early-arriving results never clear quarantine even after the order posts.

Root cause: the quarantine queue is written but never re-drained, so a result that beat its order into the system stays held forever. Fix: re-drain the quarantine on a schedule and on order-placement events, re-running bind for each held accession; a result whose order has since opened binds cleanly on the next pass without manual intervention.

Part of: Patient Identity & Order Matching.