fix(labs): never present a converted reading's printed range in the wrong unit

A converted reading's bounds move with its value, but the printed range text
stays in the unit the report used, and every reader preferred that text: the
list showed "Report range 3.9 to 5.5" beside 100.9 mg/dL, and the doctor
report and the FHIR referenceRange.text did the same. The text is still
stored verbatim. The range resolver now takes the reading's sourceUnit (as a
required field, so no reader can forget to select it) and, for a converted
reading, leaves the window's text empty so every surface states the converted
bounds in the marker's unit, and hands the printed text over separately with
its unit. The lab DTO carries it as printedReferenceText, and the note beside
a reading shows it as "as printed in mmol/L: ...".
This commit is contained in:
Marc-André Bombeck committed 2026-10-14 19:10:49 +02:00
1 parent cc0313c955
commit 47a91d6922
29 files changed
+365 -20

No files matched your search

+23 -4
View File
@@ -67435,6 +67435,10 @@ components:
anyOf:
- type: string
- type: "null"
printedReferenceText:
anyOf:
- type: string
- type: "null"
referenceOrigin:
$ref: "#/components/schemas/LabReferenceRangeOrigin"
referenceDivergesFromCatalog:
@@ -67469,6 +67473,7 @@ components:
- sourceReferenceLow
- sourceReferenceHigh
- sourceReferenceText
- printedReferenceText
- referenceOrigin
- referenceDivergesFromCatalog
- takenAt
@@ -67494,9 +67499,12 @@ components:
catalog band state different limits — a surface showing only one of the two is showing a partial answer. The
client renders these values and never recomputes the range. `converted` is true when the stored `value` was
converted into the marker's `unit` from another unit; `sourceValue` and `sourceUnit` are then what the report
printed (full precision, round for display), and the report's own window was converted with it. A reading stored
as stated has `converted: false` and two nulls. The encrypted note is never echoed in list rows; `hasNote` flags
its presence and the single-resource GET returns the decrypted `note`.
printed (full precision, round for display), and the report's own window was converted with it:
`sourceReferenceLow` / `sourceReferenceHigh` are then in the marker's `unit`. The printed range text is not
converted and stays in `sourceUnit`, so for a converted reading `sourceReferenceText` is null and the verbatim
text is in `printedReferenceText` instead, never mixed with the converted numbers. A reading stored as stated
has `converted: false`, two nulls and a null `printedReferenceText`. The encrypted note is never echoed in list
rows; `hasNote` flags its presence and the single-resource GET returns the decrypted `note`.
LabReferenceRangeOrigin:
type: string
enum:
@@ -67613,6 +67621,10 @@ components:
anyOf:
- type: string
- type: "null"
printedReferenceText:
anyOf:
- type: string
- type: "null"
referenceOrigin:
$ref: "#/components/schemas/LabReferenceRangeOrigin"
referenceDivergesFromCatalog:
@@ -67649,6 +67661,7 @@ components:
- sourceReferenceLow
- sourceReferenceHigh
- sourceReferenceText
- printedReferenceText
- referenceOrigin
- referenceDivergesFromCatalog
- takenAt
@@ -67938,6 +67951,10 @@ components:
anyOf:
- type: string
- type: "null"
printedReferenceText:
anyOf:
- type: string
- type: "null"
referenceOrigin:
type: string
enum:
@@ -67981,6 +67998,7 @@ components:
- sourceReferenceLow
- sourceReferenceHigh
- sourceReferenceText
- printedReferenceText
- referenceOrigin
- referenceDivergesFromCatalog
- takenAt
@@ -67992,7 +68010,8 @@ components:
additionalProperties: false
description: A written reading. Its reference window resolves exactly as it does on `/api/labs` — see `LabResult`.
`converted`, `sourceValue` and `sourceUnit` say when the row's unit was converted into its marker's and what the
report printed.
report printed; for a converted row the printed range text is in `printedReferenceText` (in `sourceUnit`) and
`sourceReferenceText` is null.
AiRunEnvelope:
type: object
properties:
+2 -1
View File
@@ -9590,7 +9590,8 @@
"notLinkedYet": "Noch keinem Biomarker zugeordnet",
"sourceRange": {
"fromReport": "Befundbereich {range}",
"catalogDiffers": "dein Bereich {range}"
"catalogDiffers": "dein Bereich {range}",
"printed": "auf dem Befund in {unit}: {range}"
}
},
"modules": {
+2 -1
View File
@@ -9590,7 +9590,8 @@
"notLinkedYet": "Not linked to a biomarker yet",
"sourceRange": {
"fromReport": "Report range {range}",
"catalogDiffers": "your range {range}"
"catalogDiffers": "your range {range}",
"printed": "as printed in {unit}: {range}"
}
},
"modules": {
+2 -1
View File
@@ -9590,7 +9590,8 @@
"notLinkedYet": "Aún no vinculado a un biomarcador",
"sourceRange": {
"fromReport": "Rango del informe {range}",
"catalogDiffers": "tu rango {range}"
"catalogDiffers": "tu rango {range}",
"printed": "impreso en {unit}: {range}"
}
},
"modules": {
+2 -1
View File
@@ -9590,7 +9590,8 @@
"notLinkedYet": "Pas encore lié à un biomarqueur",
"sourceRange": {
"fromReport": "Plage du compte rendu {range}",
"catalogDiffers": "votre plage {range}"
"catalogDiffers": "votre plage {range}",
"printed": "imprimé en {unit} : {range}"
}
},
"modules": {
+2 -1
View File
@@ -9590,7 +9590,8 @@
"notLinkedYet": "Non ancora collegato a un biomarcatore",
"sourceRange": {
"fromReport": "Intervallo del referto {range}",
"catalogDiffers": "il tuo intervallo {range}"
"catalogDiffers": "il tuo intervallo {range}",
"printed": "stampato in {unit}: {range}"
}
},
"modules": {
+2 -1
View File
@@ -9590,7 +9590,8 @@
"notLinkedYet": "아직 바이오마커에 연결되지 않았어요",
"sourceRange": {
"fromReport": "결과지 범위 {range}",
"catalogDiffers": "내 범위 {range}"
"catalogDiffers": "내 범위 {range}",
"printed": "결과지 원문({unit}): {range}"
}
},
"modules": {
+2 -1
View File
@@ -9590,7 +9590,8 @@
"notLinkedYet": "Jeszcze niepowiązane z biomarkerem",
"sourceRange": {
"fromReport": "Zakres z wyniku {range}",
"catalogDiffers": "twój zakres {range}"
"catalogDiffers": "twój zakres {range}",
"printed": "wydrukowano w {unit}: {range}"
}
},
"modules": {
@@ -732,6 +732,7 @@ const OCR_EXCEPTIONS: Readonly<Record<string, string>> = {
"inserted.sourceReferenceLow": OCR_ROW_EXCEPTION,
"inserted.sourceReferenceHigh": OCR_ROW_EXCEPTION,
"inserted.sourceReferenceText": OCR_ROW_EXCEPTION,
"inserted.printedReferenceText": OCR_ROW_EXCEPTION,
"inserted.referenceOrigin": OCR_ROW_EXCEPTION,
"inserted.referenceDivergesFromCatalog": OCR_ROW_EXCEPTION,
"inserted.takenAt": OCR_ROW_EXCEPTION,
@@ -52,6 +52,7 @@ export const GET = apiHandler(async () => {
sourceReferenceLow: true,
sourceReferenceHigh: true,
sourceReferenceText: true,
sourceUnit: true,
takenAt: true,
},
orderBy: { takenAt: "desc" },
@@ -67,6 +68,7 @@ export const GET = apiHandler(async () => {
sourceReferenceLow: r.sourceReferenceLow,
sourceReferenceHigh: r.sourceReferenceHigh,
sourceReferenceText: r.sourceReferenceText,
sourceUnit: r.sourceUnit,
takenAt: r.takenAt,
})),
);
@@ -78,6 +78,7 @@ function reading(id: string, value: number, takenAt: string): LabResultDto {
sourceReferenceLow: null,
sourceReferenceHigh: null,
sourceReferenceText: null,
printedReferenceText: null,
referenceOrigin: "catalog",
referenceDivergesFromCatalog: false,
takenAt,
@@ -0,0 +1,73 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { I18nProvider } from "@/lib/i18n/context";
import { SourceRangeNote } from "../source-range-note";
type Reading = Parameters<typeof SourceRangeNote>[0]["reading"];
function render(reading: Reading) {
return renderToStaticMarkup(
<I18nProvider initialLocale="en">
<SourceRangeNote reading={reading} />
</I18nProvider>,
);
}
const STATED: Reading = {
unit: "mg/dL",
referenceLow: 70,
referenceHigh: 99,
catalogReferenceLow: 70,
catalogReferenceHigh: 99,
sourceReferenceText: "70 - 99",
printedReferenceText: null,
sourceUnit: null,
referenceOrigin: "source",
referenceDivergesFromCatalog: false,
};
// 5.6 mmol/L stored as 100.9 mg/dL, with the report's 3.9 to 5.5 mmol/L window
// converted alongside it.
const CONVERTED: Reading = {
unit: "mg/dL",
referenceLow: 70.27,
referenceHigh: 99.1,
catalogReferenceLow: 70,
catalogReferenceHigh: 100,
sourceReferenceText: null,
printedReferenceText: "3.9 - 5.5",
sourceUnit: "mmol/L",
referenceOrigin: "source",
referenceDivergesFromCatalog: true,
};
describe("<SourceRangeNote>", () => {
it("names a stated reading's window verbatim", () => {
const html = render(STATED);
expect(html).toContain("Report range 70 - 99");
expect(html).not.toContain("as printed");
});
it("states a converted reading's window in the marker's unit", () => {
const html = render(CONVERTED);
expect(html).toContain("Report range 70.27–99.1 mg/dL");
});
it("keeps the printed text as evidence, labelled with the unit it is in", () => {
const html = render(CONVERTED);
expect(html).toContain("as printed in mmol/L: 3.9 - 5.5");
});
it("never puts the printed numbers in the statement about the stored unit", () => {
const html = render(CONVERTED);
// The window in force is followed by the marker's unit, not the printed text.
expect(html).not.toContain("Report range 3.9 - 5.5");
expect(html).toContain("your range 70–100 mg/dL");
});
it("renders nothing when the window in force is not the report's", () => {
expect(render({ ...CONVERTED, referenceOrigin: "catalog" })).toBe("");
});
});
+17 -2
View File
@@ -18,6 +18,11 @@ import { useLabNumber } from "./use-lab-format";
* the same number, so a surface that shows one silently is showing a partial
* answer.
*
* A reading converted into its marker's unit carries bounds that were converted
* with it, but the report's own text is still in the unit it printed. The note
* then states the window in the marker's unit and adds the printed text as
* evidence, labelled with its unit, so no single statement mixes two units.
*
* Neutral by construction: no tint, no icon, muted text. The in/out verdict is
* the badge's job — this element states where the window came from, nothing
* about whether the value is good news.
@@ -34,6 +39,8 @@ export function SourceRangeNote({
| "catalogReferenceLow"
| "catalogReferenceHigh"
| "sourceReferenceText"
| "printedReferenceText"
| "sourceUnit"
| "referenceOrigin"
| "referenceDivergesFromCatalog"
>;
@@ -43,7 +50,9 @@ export function SourceRangeNote({
const labNumber = useLabNumber();
if (reading.referenceOrigin !== "source") return null;
const printed =
// `sourceReferenceText` is null for a converted reading, so the window is
// then stated from the converted bounds, in the marker's unit.
const inForce =
reading.sourceReferenceText ??
`${formatReferenceRange(
reading.referenceLow,
@@ -59,7 +68,13 @@ export function SourceRangeNote({
return (
<span className={className}>
{t("labs.sourceRange.fromReport", { range: printed })}
{t("labs.sourceRange.fromReport", { range: inForce })}
{reading.printedReferenceText && reading.sourceUnit
? ` · ${t("labs.sourceRange.printed", {
range: reading.printedReferenceText,
unit: reading.sourceUnit,
})}`
: null}
{reading.referenceDivergesFromCatalog && catalog
? ` · ${t("labs.sourceRange.catalogDiffers", {
range: `${catalog} ${reading.unit}`.trim(),
+5
View File
@@ -38,6 +38,11 @@ export interface LabResultDto {
sourceReferenceLow: number | null;
sourceReferenceHigh: number | null;
sourceReferenceText: string | null;
/**
* A converted reading's printed range text, verbatim and in `sourceUnit`;
* `sourceReferenceText` is then null. Null for every other reading.
*/
printedReferenceText: string | null;
/** Which window is in force. */
referenceOrigin: ReferenceRangeOrigin;
/** True when the report's window and the catalog band state different limits. */
+1
View File
@@ -143,6 +143,7 @@ export async function buildLabsSnapshotBlock(
sourceReferenceLow: true,
sourceReferenceHigh: true,
sourceReferenceText: true,
sourceUnit: true,
takenAt: true,
biomarkerId: true,
biomarker: {
@@ -56,6 +56,7 @@ export async function loadLabResults(
sourceReferenceLow: true,
sourceReferenceHigh: true,
sourceReferenceText: true,
sourceUnit: true,
takenAt: true,
},
});
@@ -165,6 +165,47 @@ describe("lab-result FHIR Observations", () => {
expect(crp?.referenceRange).toBeUndefined();
});
it("states a converted reading's window only in the value's unit", () => {
// A reading converted from mmol/L: the resolved entry carries the bounds
// in mg/dL and no text (the printed text is in mmol/L and would disagree
// with low and high), so the element carries the two bounds alone.
const bundle = buildFhirDocumentBundle(
makeData({
labResults: [
{
panel: null,
analyte: "Glucose",
value: 100.9,
valueText: null,
unit: "mg/dL",
referenceLow: 70.27,
referenceHigh: 99.1,
catalogReferenceLow: 70,
catalogReferenceHigh: 100,
sourceReferenceText: null,
referenceOrigin: "source" as const,
referenceDivergesFromCatalog: true,
takenAt: "2026-04-10T08:00:00.000Z",
count: 1,
},
],
}),
{ insuranceNumber: "A123456780" },
FIXED_NOW,
);
const [obs] = observationsOf(bundle).filter((o) =>
o.category?.some((c) =>
c.coding?.some((code) => code.code === "laboratory"),
),
);
expect(obs.referenceRange).toEqual([
{
low: expect.objectContaining({ value: 70.27, unit: "mg/dL" }),
high: expect.objectContaining({ value: 99.1, unit: "mg/dL" }),
},
]);
});
it("emits no lab Observations when labResults is null", () => {
const bundle = buildFhirDocumentBundle(
makeData({ labResults: null }),
@@ -37,6 +37,7 @@ function row(
sourceReferenceLow: opts.sourceLow ?? null,
sourceReferenceHigh: opts.sourceHigh ?? null,
sourceReferenceText: opts.sourceText ?? null,
sourceUnit: null,
takenAt: new Date(`${day}T08:00:00.000Z`),
};
}
+1
View File
@@ -172,6 +172,7 @@ export async function generateBiomarkerStatus(args: {
sourceReferenceLow: true,
sourceReferenceHigh: true,
sourceReferenceText: true,
sourceUnit: true,
},
});
if (rows.length === 0) {
@@ -1072,6 +1072,7 @@ export async function fetchLabDraws(
sourceReferenceLow: true,
sourceReferenceHigh: true,
sourceReferenceText: true,
sourceUnit: true,
panel: true,
value: true,
takenAt: true,
+1
View File
@@ -256,6 +256,7 @@ export async function readLabsBriefingBlock(
sourceReferenceLow: true,
sourceReferenceHigh: true,
sourceReferenceText: true,
sourceUnit: true,
takenAt: true,
biomarkerId: true,
biomarker: {
@@ -9,6 +9,7 @@ const NO_SOURCE = {
sourceReferenceLow: null,
sourceReferenceHigh: null,
sourceReferenceText: null,
sourceUnit: null,
};
describe("resolveEffectiveReferenceRange", () => {
@@ -36,6 +37,7 @@ describe("resolveEffectiveReferenceRange", () => {
sourceReferenceLow: 3.9,
sourceReferenceHigh: 5.4,
sourceReferenceText: "3,9 - 5,4",
sourceUnit: null,
});
expect(eff).toMatchObject({
low: 3.9,
@@ -53,6 +55,7 @@ describe("resolveEffectiveReferenceRange", () => {
sourceReferenceLow: 3.5,
sourceReferenceHigh: 5,
sourceReferenceText: "3,5 - 5,0",
sourceUnit: null,
});
expect(eff.origin).toBe("source");
expect(eff.divergesFromCatalog).toBe(false);
@@ -65,6 +68,7 @@ describe("resolveEffectiveReferenceRange", () => {
sourceReferenceLow: null,
sourceReferenceHigh: 4.2,
sourceReferenceText: "< 4,2",
sourceUnit: null,
});
expect(eff).toMatchObject({
low: null,
@@ -81,6 +85,7 @@ describe("resolveEffectiveReferenceRange", () => {
sourceReferenceLow: null,
sourceReferenceHigh: null,
sourceReferenceText: "negativ",
sourceUnit: null,
});
expect(eff).toMatchObject({
low: 3.5,
@@ -96,12 +101,57 @@ describe("resolveEffectiveReferenceRange", () => {
sourceReferenceLow: 3.9,
sourceReferenceHigh: 5.4,
sourceReferenceText: "3,9 - 5,4",
sourceUnit: null,
});
expect(eff.origin).toBe("source");
expect(eff.divergesFromCatalog).toBe(false);
});
});
describe("a converted reading's printed text", () => {
// 5.6 mmol/L stored as mg/dL, with the printed 3.9 to 5.5 mmol/L window
// converted alongside; the text stays as the report printed it.
const CONVERTED = {
sourceReferenceLow: 3.9 * 18.0182,
sourceReferenceHigh: 5.5 * 18.0182,
sourceReferenceText: "3,9 - 5,5",
sourceUnit: "mmol/L",
};
it("is never the window's text, so no statement mixes two units", () => {
const eff = resolveEffectiveReferenceRange(70, 100, CONVERTED);
expect(eff.origin).toBe("source");
expect(eff.low).toBeCloseTo(70.27, 2);
expect(eff.sourceText).toBeNull();
});
it("is kept as evidence with the unit it is written in", () => {
const eff = resolveEffectiveReferenceRange(70, 100, CONVERTED);
expect(eff.printed).toEqual({ text: "3,9 - 5,5", unit: "mmol/L" });
});
it("stays the window's text for a reading stored as stated", () => {
const eff = resolveEffectiveReferenceRange(70, 100, {
...CONVERTED,
sourceUnit: null,
});
expect(eff.sourceText).toBe("3,9 - 5,5");
expect(eff.printed).toBeNull();
});
it("moves an unreadable printed text the same way", () => {
const eff = resolveEffectiveReferenceRange(70, 100, {
sourceReferenceLow: null,
sourceReferenceHigh: null,
sourceReferenceText: "siehe Befund",
sourceUnit: "mmol/L",
});
expect(eff.origin).toBe("catalog");
expect(eff.sourceText).toBeNull();
expect(eff.printed).toEqual({ text: "siehe Befund", unit: "mmol/L" });
});
});
describe("classifyAgainstEffectiveRange", () => {
it("judges a reading by the source window, not the catalog band", () => {
// 5.2 sits inside the catalog band (3.5–5.0 would call it high) but the
@@ -110,6 +160,7 @@ describe("classifyAgainstEffectiveRange", () => {
sourceReferenceLow: 3.9,
sourceReferenceHigh: 5.4,
sourceReferenceText: "3,9 - 5,4",
sourceUnit: null,
});
expect(classifyAgainstEffectiveRange(5.2, eff)).toBe("in-range");
expect(classifyAgainstEffectiveRange(3.7, eff)).toBe("below");
+2
View File
@@ -20,6 +20,8 @@ const baseRow: LabRow = {
sourceReferenceLow: null,
sourceReferenceHigh: null,
sourceReferenceText: null,
sourceValue: null,
sourceUnit: null,
takenAt: new Date("2026-01-01T08:00:00.000Z"),
source: "MANUAL",
biomarkerId: "bm_1",
+35 -2
View File
@@ -65,11 +65,23 @@ export function formatReferenceRange(
/**
* The reference range a lab report printed beside ONE reading, as stored on
* `LabResult.sourceReference*`.
*
* `sourceUnit` is set when the reading was converted into its marker's unit:
* the bounds were converted with the value, but the text is the report's own
* and stays in the unit the report printed. It is required, so a reader cannot
* forget to select it and present that text beside numbers in another unit.
*/
export interface SourceReferenceRange {
sourceReferenceLow: number | null;
sourceReferenceHigh: number | null;
sourceReferenceText: string | null;
sourceUnit: string | null;
}
/** A converted reading's printed range text, with the unit it is written in. */
export interface PrintedReferenceRange {
text: string;
unit: string;
}
/** Where the bounds a reading is judged against came from. */
@@ -84,8 +96,19 @@ export interface EffectiveReferenceRange {
/** The catalog band, kept alongside so a surface can show both. */
catalogLow: number | null;
catalogHigh: number | null;
/** The printed window, kept verbatim. Null when the report stated none. */
/**
* The printed window, kept verbatim, in the reading's unit. Null when the
* report stated none, and null for a converted reading: its text is in the
* unit the report printed, not the unit of the value and bounds beside it,
* and moves to `printed`.
*/
sourceText: string | null;
/**
* For a converted reading only: the window as the report printed it and the
* unit it is in. Evidence, never a statement about the stored numbers; a
* surface that shows it names the unit with it.
*/
printed: PrintedReferenceRange | null;
/**
* True when the source window is in force AND the catalog states a window
* that differs from it. This is the case a reader must be told about: the
@@ -132,7 +155,15 @@ export function resolveEffectiveReferenceRange(
const cHigh = catalogHigh ?? null;
const sLow = source?.sourceReferenceLow ?? null;
const sHigh = source?.sourceReferenceHigh ?? null;
const sourceText = source?.sourceReferenceText ?? null;
const verbatim = source?.sourceReferenceText ?? null;
const sourceUnit = source?.sourceUnit ?? null;
// A converted reading's text is in the printed unit; it is never presented as
// the window of the converted value.
const sourceText = sourceUnit === null ? verbatim : null;
const printed: PrintedReferenceRange | null =
sourceUnit !== null && verbatim !== null
? { text: verbatim, unit: sourceUnit }
: null;
if (sLow !== null || sHigh !== null) {
return {
@@ -142,6 +173,7 @@ export function resolveEffectiveReferenceRange(
catalogLow: cLow,
catalogHigh: cHigh,
sourceText,
printed,
divergesFromCatalog:
(cLow !== null || cHigh !== null) &&
!sameWindow(sLow, sHigh, cLow, cHigh),
@@ -155,6 +187,7 @@ export function resolveEffectiveReferenceRange(
catalogLow: cLow,
catalogHigh: cHigh,
sourceText,
printed,
divergesFromCatalog: false,
};
}
+17 -3
View File
@@ -25,7 +25,11 @@
* - `catalogReferenceLow` / `catalogReferenceHigh` — the catalog band, so a
* surface can paint both and show where they part.
* - `sourceReferenceLow` / `sourceReferenceHigh` / `sourceReferenceText` —
* what the report printed, the last verbatim.
* what the report printed, the last verbatim. For a reading converted into
* its marker's unit the bounds were converted with the value, but the text
* is still in the unit the report printed, so it is never sent as
* `sourceReferenceText` (that would put two units in one statement): it
* travels as `printedReferenceText`, in `sourceUnit`.
*
* `referenceOrigin` names which window is in force and
* `referenceDivergesFromCatalog` flags the case a reader must be told about.
@@ -80,8 +84,8 @@ export interface LabRow {
* When the reading was converted into the marker's unit: the value and unit
* as the report printed them. Both null for a reading stored as stated.
*/
sourceValue?: number | null;
sourceUnit?: string | null;
sourceValue: number | null;
sourceUnit: string | null;
noteEncrypted: Uint8Array | null;
createdAt: Date;
updatedAt: Date;
@@ -97,6 +101,7 @@ export type LabFieldRow = Pick<
| "sourceReferenceLow"
| "sourceReferenceHigh"
| "sourceReferenceText"
| "sourceUnit"
| "panel"
| "biomarkerId"
>;
@@ -115,7 +120,13 @@ export interface ResolvedLabFields {
/** What the source report printed for this reading. */
sourceReferenceLow: number | null;
sourceReferenceHigh: number | null;
/** Null for a converted reading; see `printedReferenceText`. */
sourceReferenceText: string | null;
/**
* A converted reading's printed range text, verbatim, in `sourceUnit` (the
* unit the report printed); null for every other reading.
*/
printedReferenceText: string | null;
referenceOrigin: ReferenceRangeOrigin;
referenceDivergesFromCatalog: boolean;
/** The resolved window as one value, for callers that pass it on. */
@@ -172,6 +183,7 @@ export function resolveLabFields(
sourceReferenceLow: row.sourceReferenceLow,
sourceReferenceHigh: row.sourceReferenceHigh,
sourceReferenceText: effectiveRange.sourceText,
printedReferenceText: effectiveRange.printed?.text ?? null,
referenceOrigin: effectiveRange.origin,
referenceDivergesFromCatalog: effectiveRange.divergesFromCatalog,
effectiveRange,
@@ -206,6 +218,7 @@ export function serialiseLabResult(
sourceReferenceLow: resolved.sourceReferenceLow,
sourceReferenceHigh: resolved.sourceReferenceHigh,
sourceReferenceText: resolved.sourceReferenceText,
printedReferenceText: resolved.printedReferenceText,
referenceOrigin: resolved.referenceOrigin,
referenceDivergesFromCatalog: resolved.referenceDivergesFromCatalog,
takenAt: row.takenAt.toISOString(),
@@ -265,6 +278,7 @@ export function serialiseLabResultDetail(
sourceReferenceLow: resolved.sourceReferenceLow,
sourceReferenceHigh: resolved.sourceReferenceHigh,
sourceReferenceText: resolved.sourceReferenceText,
printedReferenceText: resolved.printedReferenceText,
referenceOrigin: resolved.referenceOrigin,
referenceDivergesFromCatalog: resolved.referenceDivergesFromCatalog,
takenAt: row.takenAt.toISOString(),
+1
View File
@@ -1426,6 +1426,7 @@ export async function getLabHistory(
sourceReferenceLow: true,
sourceReferenceHigh: true,
sourceReferenceText: true,
sourceUnit: true,
takenAt: true,
biomarkerId: true,
biomarker: {
+2 -1
View File
@@ -80,6 +80,7 @@ const labResultRow = z
sourceReferenceLow: z.number().nullable(),
sourceReferenceHigh: z.number().nullable(),
sourceReferenceText: z.string().nullable(),
printedReferenceText: z.string().nullable(),
referenceOrigin: referenceOriginEnum,
referenceDivergesFromCatalog: z.boolean(),
takenAt: z.string(),
@@ -92,7 +93,7 @@ const labResultRow = z
.meta({
id: "LabResult",
description:
"A stored lab result. Exactly one of `value` (numeric) or `valueText` (qualitative, e.g. \"negativ\") is non-null; `rangeStatus` is always `unknown` for a qualitative row (no number to classify). `biomarkerId` links the user-scoped catalog marker (null for legacy free-text rows); when linked, `analyte` / `unit` are RESOLVED server-side from the Biomarker.\n\nThree reference windows ride on the row and the server says which one applies. `referenceLow` / `referenceHigh` are the window IN FORCE and the one `rangeStatus` was computed from — read these to render the range and you are correct without doing any resolution of your own. `sourceReferenceLow` / `sourceReferenceHigh` / `sourceReferenceText` are the range this reading's own report printed (the text verbatim, kept even when no numbers could be read out of it). `catalogReferenceLow` / `catalogReferenceHigh` are the Biomarker band. `referenceOrigin` names the winner and `referenceDivergesFromCatalog` is true when the report's window and the catalog band state different limits — a surface showing only one of the two is showing a partial answer. The client renders these values and never recomputes the range. `converted` is true when the stored `value` was converted into the marker's `unit` from another unit; `sourceValue` and `sourceUnit` are then what the report printed (full precision, round for display), and the report's own window was converted with it. A reading stored as stated has `converted: false` and two nulls. The encrypted note is never echoed in list rows; `hasNote` flags its presence and the single-resource GET returns the decrypted `note`.",
"A stored lab result. Exactly one of `value` (numeric) or `valueText` (qualitative, e.g. \"negativ\") is non-null; `rangeStatus` is always `unknown` for a qualitative row (no number to classify). `biomarkerId` links the user-scoped catalog marker (null for legacy free-text rows); when linked, `analyte` / `unit` are RESOLVED server-side from the Biomarker.\n\nThree reference windows ride on the row and the server says which one applies. `referenceLow` / `referenceHigh` are the window IN FORCE and the one `rangeStatus` was computed from — read these to render the range and you are correct without doing any resolution of your own. `sourceReferenceLow` / `sourceReferenceHigh` / `sourceReferenceText` are the range this reading's own report printed (the text verbatim, kept even when no numbers could be read out of it). `catalogReferenceLow` / `catalogReferenceHigh` are the Biomarker band. `referenceOrigin` names the winner and `referenceDivergesFromCatalog` is true when the report's window and the catalog band state different limits — a surface showing only one of the two is showing a partial answer. The client renders these values and never recomputes the range. `converted` is true when the stored `value` was converted into the marker's `unit` from another unit; `sourceValue` and `sourceUnit` are then what the report printed (full precision, round for display), and the report's own window was converted with it: `sourceReferenceLow` / `sourceReferenceHigh` are then in the marker's `unit`. The printed range text is not converted and stays in `sourceUnit`, so for a converted reading `sourceReferenceText` is null and the verbatim text is in `printedReferenceText` instead, never mixed with the converted numbers. A reading stored as stated has `converted: false`, two nulls and a null `printedReferenceText`. The encrypted note is never echoed in list rows; `hasNote` flags its presence and the single-resource GET returns the decrypted `note`.",
});
const labResultDetail = labResultRow
+2 -1
View File
@@ -129,6 +129,7 @@ const committedRow = z
sourceReferenceLow: z.number().nullable(),
sourceReferenceHigh: z.number().nullable(),
sourceReferenceText: z.string().nullable(),
printedReferenceText: z.string().nullable(),
referenceOrigin: z.enum(["source", "catalog", "none"]),
referenceDivergesFromCatalog: z.boolean(),
takenAt: z.string(),
@@ -141,7 +142,7 @@ const committedRow = z
.meta({
id: "OcrCommittedLabResult",
description:
"A written reading. Its reference window resolves exactly as it does on `/api/labs` — see `LabResult`. `converted`, `sourceValue` and `sourceUnit` say when the row's unit was converted into its marker's and what the report printed.",
"A written reading. Its reference window resolves exactly as it does on `/api/labs` — see `LabResult`. `converted`, `sourceValue` and `sourceUnit` say when the row's unit was converted into its marker's and what the report printed; for a converted row the printed range text is in `printedReferenceText` (in `sourceUnit`) and `sourceReferenceText` is null.",
});
const commitResponse = z
@@ -225,6 +225,78 @@ describe("a printed reference range, end to end", () => {
});
});
it("keeps a converted reading's printed text as evidence, never as the window of the converted value", async () => {
const prisma = getPrismaClient();
// A glucose marker tracked in mg/dL; the report prints mmol/L.
const marker = await prisma.biomarker.create({
data: {
userId: USER_ID,
name: "Glucose",
unit: "mg/dL",
lowerBound: 70,
upperBound: 100,
},
});
const PRINTED_MMOL = "3,9 - 5,5 mmol/l";
const { POST } = await import("@/app/api/labs/route");
const res = await POST(
postReq({
biomarkerId: marker.id,
value: 5.6,
unit: "mmol/L",
sourceReferenceLow: 3.9,
sourceReferenceHigh: 5.5,
sourceReferenceText: PRINTED_MMOL,
takenAt: "2026-07-03T08:00:00.000Z",
}),
);
expect(res.status).toBe(201);
// The record keeps what the report printed, verbatim.
const row = await prisma.labResult.findFirstOrThrow({
where: { userId: USER_ID },
});
expect(row.sourceReferenceText).toBe(PRINTED_MMOL);
expect(row.sourceUnit).toBe("mmol/L");
// The DTO states the window in force in mg/dL only, and hands the printed
// text over separately, beside the unit it is in.
const dto = await readDto("Glucose");
expect(dto.converted).toBe(true);
expect(dto.unit).toBe("mg/dL");
expect(dto.referenceOrigin).toBe("source");
expect(dto.referenceLow).toBeCloseTo(3.9 * 18.0182, 6);
expect(dto.referenceHigh).toBeCloseTo(5.5 * 18.0182, 6);
expect(dto.sourceReferenceText).toBeNull();
expect(dto.printedReferenceText).toBe(PRINTED_MMOL);
expect(dto.sourceUnit).toBe("mmol/L");
// The doctor report has no mmol/L text to print beside mg/dL numbers.
const { loadLabResults } =
await import("@/lib/doctor-report/clinical-records");
const labs = await loadLabResults(
USER_ID,
new Date("2026-01-01T00:00:00.000Z"),
new Date("2026-12-31T23:59:59.000Z"),
);
expect(labs?.[0].sourceReferenceText).toBeNull();
expect(labs?.[0].referenceLow).toBeCloseTo(3.9 * 18.0182, 6);
// FHIR: the text, when present, must agree with low and high. Here it is
// left out, and the bounds carry the marker's unit like the value.
const { labObservations } = await import("@/lib/fhir/resources/labs");
let seq = 0;
const observations = labObservations(
{ labResults: labs } as Parameters<typeof labObservations>[0],
() => `obs-${++seq}`,
);
const range = observations[0].referenceRange?.[0];
expect(range?.text).toBeUndefined();
expect(range?.low?.unit).toBe("mg/dL");
expect(range?.high?.value).toBeCloseTo(5.5 * 18.0182, 6);
expect(observations[0].valueQuantity?.unit).toBe("mg/dL");
});
it("keeps a window it cannot read as text and leaves the catalog in force", async () => {
const prisma = getPrismaClient();
await seedMarker();