Refuse an export nobody scoped, and stop naming UCUM without a code

Two findings from the deferred-audit list, both live since July.

A FHIR lab quantity stamped the UCUM system unconditionally while the
code was only added when the analyte map resolved a symbol for exactly
that unit. A receiver was told the unit came from UCUM and then handed
no UCUM code, which is a coding claim the bundle cannot back. System
and code now travel together, on the value and on both reference-range
bounds.

The doctor-report export accepted a selection that admits nothing. The
panel disables its own submit in that state, so only a hand-built
request reached it, and it rendered an empty artefact while spending a
rate-limit slot. This route has no documents-only mode that would make
an empty leaf list a legitimate scope, so it refuses with 422. The
share-link route, which does have that mode, is untouched.
This commit is contained in:
Marc-André Bombeck committed 2026-08-15 00:23:44 +02:00
1 parent d2491d5589
commit 09446846b1
4 files changed
+60 -16

No files matched your search

@@ -205,6 +205,18 @@ describe("POST /api/export/health-record — validation", () => {
expect(res.status).toBe(422);
});
it("rejects a selection that admits nothing with 422", async () => {
// The panel disables its own submit in that state, so only a hand-built
// request gets here. Rendering an empty report and spending a rate-limit
// slot on it would be the wrong answer; this route has no documents-only
// mode that would make an empty leaf list a legitimate scope.
const { POST } = await import("../route");
const res = await POST(mkReq({ format: "pdf", selection: sel() }));
expect(res.status).toBe(422);
const body = (await res.json()) as { meta?: { errorCode?: string } };
expect(body.meta?.errorCode).toBe("export.selection.empty");
});
it("rejects a userId smuggled into the body with 422", async () => {
const { POST } = await import("../route");
const res = await POST(
@@ -540,15 +552,11 @@ describe("POST /api/export/health-record — the selection reaches every format"
// leaves: the totals must stay in the database.
expect(await observationCodes(sel("LAB_RESULTS"))).toEqual([]);
// And an empty selection — the "I said nothing" case, which used to mean
// "give me everything".
// "give me everything" and is now refused outright, so the totals cannot
// ride out on a scope nobody chose.
const { POST } = await import("../route");
const res = await POST(mkReq({ format: "fhir", selection: sel() }));
const bundle = (await res.json()) as {
entry: { resource: { resourceType: string } }[];
};
expect(
bundle.entry.filter((e) => e.resource.resourceType === "Observation"),
).toEqual([]);
expect(res.status).toBe(422);
// Asked for by name, they are emitted — the data is withheld, not lost.
const asked = await observationCodes(sel("PHQ9_SCORE", "GAD7_SCORE"));
+11
View File
@@ -129,6 +129,17 @@ export const POST = apiHandler(async (request: NextRequest) => {
);
}
const selection = minted.selection;
// A selection that admits nothing would render an empty report and still
// spend a rate-limit slot on it. The panel disables its own submit button
// in that state; a hand-built request has to be refused here, because this
// route has no documents-only mode that would make an empty leaf list a
// legitimate scope (the share-link route, which does, mints EMPTY itself).
if (selection.leaves.length === 0) {
annotate({ meta: { empty_selection: true } });
return apiError("Select at least one section to export", 422, {
errorCode: "export.selection.empty",
});
}
const range = normaliseDateRange(input.range ?? undefined);
const practiceName = sanitisePracticeName(input.practiceName);
@@ -181,7 +181,12 @@ describe("lab-result FHIR Observations", () => {
});
describe("lab-result LOINC + canonical UCUM coding (v1.18.8)", () => {
function labOf(type: string, unit: string, value = 5.4) {
function labOf(
type: string,
unit: string,
value = 5.4,
bounds?: { low: number; high: number },
) {
const bundle = buildFhirDocumentBundle(
makeData({
labResults: [
@@ -191,8 +196,8 @@ describe("lab-result LOINC + canonical UCUM coding (v1.18.8)", () => {
value,
valueText: null,
unit,
referenceLow: null,
referenceHigh: null,
referenceLow: bounds?.low ?? null,
referenceHigh: bounds?.high ?? null,
catalogReferenceLow: null,
catalogReferenceHigh: null,
sourceReferenceText: null,
@@ -250,13 +255,15 @@ describe("lab-result LOINC + canonical UCUM coding (v1.18.8)", () => {
expect(tsh?.valueQuantity?.code).toBe("m[IU]/L");
});
it("omits the UCUM code when the unit does not match the mapped canonical", () => {
it("omits the UCUM code AND its system when the unit does not match the mapped canonical", () => {
// HbA1c mapped (LOINC present) but recorded in an mmol/mol unit we don't
// canonicalise → keep the LOINC coding, drop the UCUM code, keep display.
// The system goes with the code: a valueQuantity naming UCUM without a
// code claims a coding the bundle does not carry.
const o = labOf("HbA1c", "mmol/mol", 36);
expect(o?.code.coding?.[0].code).toBe("4548-4");
expect(o?.valueQuantity?.system).toBe("http://unitsofmeasure.org");
expect(o?.valueQuantity?.code).toBeUndefined();
expect(o?.valueQuantity?.system).toBeUndefined();
expect(o?.valueQuantity?.unit).toBe("mmol/mol");
});
@@ -264,10 +271,25 @@ describe("lab-result LOINC + canonical UCUM coding (v1.18.8)", () => {
const o = labOf("Selenium", "ug/L", 95);
expect(o?.code.text).toBe("Selenium");
expect(o?.code.coding).toBeUndefined();
// Display unit stays; no coerced UCUM code is invented.
// Display unit stays; no coerced UCUM code is invented, and no code
// system is named for a code that is not there.
expect(o?.valueQuantity?.unit).toBe("ug/L");
expect(o?.valueQuantity?.code).toBeUndefined();
expect(o?.valueQuantity?.system).toBe("http://unitsofmeasure.org");
expect(o?.valueQuantity?.system).toBeUndefined();
});
it("keeps system and code together on the reference range bounds too", () => {
// The bounds are stated in the SAME unit as the value, so an uncoded
// unit must leave them uncoded as well, on both halves.
const uncoded = labOf("HbA1c", "mmol/mol", 36, { low: 20, high: 42 });
expect(uncoded?.referenceRange?.[0].low?.system).toBeUndefined();
expect(uncoded?.referenceRange?.[0].low?.code).toBeUndefined();
expect(uncoded?.referenceRange?.[0].high?.system).toBeUndefined();
const coded = labOf("HDL", "mg/dL", 55, { low: 40, high: 60 });
expect(coded?.referenceRange?.[0].low?.system).toBe(
"http://unitsofmeasure.org",
);
expect(coded?.referenceRange?.[0].low?.code).toBe("mg/dL");
});
});
+5 -2
View File
@@ -80,11 +80,14 @@ export function labObservations(
// A lab quantity: the recorded `unit` is always the display string, and
// the canonical UCUM `code` is stamped only when the analyte map resolved
// one for exactly that unit (no coerced symbol).
// The `system` rides along WITH the code and never alone: naming UCUM as
// the code system while withholding the code claims a coding the bundle
// does not carry, and leaves a receiver unable to machine-compare the
// value it is attached to.
const quantity = (value: number) => ({
value,
unit: lab.unit,
system: UCUM_SYSTEM,
...(coding?.ucum ? { code: coding.ucum } : {}),
...(coding?.ucum ? { system: UCUM_SYSTEM, code: coding.ucum } : {}),
});
// The bounds are stated in the SAME unit as the value, so they carry the
// SAME coding — a bare unit beside a coded value leaves a receiver unable