diff --git a/src/lib/labs/reading-conversion.ts b/src/lib/labs/reading-conversion.ts index dffab8114..f52f5e046 100644 --- a/src/lib/labs/reading-conversion.ts +++ b/src/lib/labs/reading-conversion.ts @@ -123,8 +123,11 @@ export async function readingInMarkerUnit( * every reading the marker holds is text there is no number the unit could * contradict, so the first numeric reading gives the marker its unit, the * dimensionless "1" included. The update only matches a marker whose unit is - * still empty, so two readings racing for it cannot both win. `marker.unit` - * is set on the caller's object, which writes the reading under it. + * still empty, so two readings racing for it cannot both win; the loser reads + * the winner's unit and is compared against it. `marker.unit` is set on the + * caller's object, which writes the reading under it. The adoption is not in + * the reading's transaction: if the write fails after it, the marker keeps + * the unit the reading stated, which is the unit its next reading will state. */ async function adoptFirstNumericUnit( client: Pick, @@ -134,14 +137,23 @@ async function adoptFirstNumericUnit( const stated = unit.trim(); if (marker.unit.trim() !== "" || stated === "") return false; const numeric = await client.labResult.count({ - where: { biomarkerId: marker.id, value: { not: null } }, + where: { biomarkerId: marker.id, value: { not: null }, deletedAt: null }, }); if (numeric > 0) return false; const { count } = await client.biomarker.updateMany({ where: { id: marker.id, unit: "" }, data: { unit: stated }, }); - if (count === 0) return false; + if (count === 0) { + // Another reading set the unit first: carry on with the one it chose, + // which the caller then compares as usual. + const current = await client.biomarker.findUnique({ + where: { id: marker.id }, + select: { unit: true }, + }); + if (current) marker.unit = current.unit; + return false; + } marker.unit = stated; return true; } diff --git a/tests/integration/lab-text-marker-adopts-unit.test.ts b/tests/integration/lab-text-marker-adopts-unit.test.ts index e678f1df1..8f5e9c997 100644 --- a/tests/integration/lab-text-marker-adopts-unit.test.ts +++ b/tests/integration/lab-text-marker-adopts-unit.test.ts @@ -166,6 +166,33 @@ describe("a marker whose readings are all text", () => { expect(stored.unit).toBe(""); }); + it("does not count a deleted number as holding the unit", async () => { + const marker = await textMarker(); + await prisma.labResult.create({ + data: { + userId: USER, + biomarkerId: marker.id, + analyte: marker.name, + value: 12, + unit: "", + takenAt: new Date("2026-08-01T08:00:00.000Z"), + source: "MANUAL", + deletedAt: new Date("2026-08-02T08:00:00.000Z"), + }, + }); + const res = await postReading({ + biomarkerId: marker.id, + value: 18, + unit: "1", + takenAt: "2026-10-01T08:00:00.000Z", + }); + expect(res.status).toBe(201); + const stored = await prisma.biomarker.findUniqueOrThrow({ + where: { id: marker.id }, + }); + expect(stored.unit).toBe("1"); + }); + it("leaves a marker with a unit of its own refusing a unitless number", async () => { const marker = await prisma.biomarker.create({ data: { userId: USER, name: "Creatinine", unit: "mg/dL" },