mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
feat(labs): convert a reading into its marker's unit when the pair is known
Slice 2 of phase 3 of #1095. A reading that stated a unit other than its marker's was refused. When the marker's analyte is known and the registry has the pair, the three write paths now convert it instead: POST /api/labs, the OCR commit and the document commit. The converted value is stored at full precision in the marker's unit, with what the report printed in the existing source_value and source_unit columns. The report's own reference window is in the printed unit, so it is converted with the value; otherwise the verdict would compare a converted value with an unconverted window. A pair the registry does not have is still refused as before (the 422 now says why), as is a marker whose analyte is unknown. The marker's key is filled the first time a reading needs it. The same scan committed again is still a duplicate: a converted row holds the number in the marker's unit, so the duplicate check also matches on what the report printed. Editing the value of a converted reading clears the conversion record, because the number is then the person's own. The lab result DTO and the OpenAPI schemas (labs and the OCR commit) carry converted, sourceValue and sourceUnit. Refs #1095
This commit is contained in:
1 parent
08c69d4a5a
commit
caa7a937fd
18 files changed
+1011
-105
No files matched your search
@@ -44,22 +44,25 @@ beforeEach(() => {
|
||||
|
||||
describe("commitApprovedFact — OBSERVATION unit integrity", () => {
|
||||
it("rejects a numeric observation whose stated unit differs from an existing marker's unit", async () => {
|
||||
// A pair the registry has no conversion for: ferritin in pmol/L against a
|
||||
// marker stored in ng/mL (protein molecular weights vary by source).
|
||||
vi.mocked(decryptFactData).mockReturnValue({
|
||||
label: "Glucose",
|
||||
value: 6.1,
|
||||
unit: "mmol/L",
|
||||
label: "Ferritin",
|
||||
value: 450,
|
||||
unit: "pmol/L",
|
||||
referenceLow: null,
|
||||
referenceHigh: null,
|
||||
effectiveDate: "2025-01-01",
|
||||
} as never);
|
||||
// Existing marker stored in mg/dL — the resolver returns it as-is.
|
||||
// Existing marker stored in ng/mL — the resolver returns it as-is.
|
||||
vi.mocked(resolveOrMintBiomarker).mockResolvedValue({
|
||||
id: "bm-1",
|
||||
name: "Glucose",
|
||||
unit: "mg/dL",
|
||||
lowerBound: 70,
|
||||
upperBound: 100,
|
||||
name: "Ferritin",
|
||||
unit: "ng/mL",
|
||||
lowerBound: 30,
|
||||
upperBound: 400,
|
||||
panel: null,
|
||||
analyteKey: null,
|
||||
});
|
||||
|
||||
await expect(
|
||||
@@ -71,6 +74,69 @@ describe("commitApprovedFact — OBSERVATION unit integrity", () => {
|
||||
expect(prisma.labResult.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("converts a unit the marker's analyte can reach, keeping what the document stated", async () => {
|
||||
vi.mocked(decryptFactData).mockReturnValue({
|
||||
label: "Glucose",
|
||||
value: 6.1,
|
||||
unit: "mmol/L",
|
||||
referenceLow: 3.9,
|
||||
referenceHigh: 5.6,
|
||||
effectiveDate: "2025-01-01",
|
||||
} as never);
|
||||
vi.mocked(resolveOrMintBiomarker).mockResolvedValue({
|
||||
id: "bm-1",
|
||||
name: "Glucose",
|
||||
unit: "mg/dL",
|
||||
lowerBound: 70,
|
||||
upperBound: 100,
|
||||
panel: null,
|
||||
analyteKey: "fasting-glucose",
|
||||
});
|
||||
vi.mocked(prisma.labResult.create).mockResolvedValue({
|
||||
id: "lr-conv",
|
||||
} as never);
|
||||
|
||||
const ref = await commitApprovedFact("user-1", observationFact());
|
||||
expect(ref).toEqual({ recordType: "labResult", recordId: "lr-conv" });
|
||||
const data = vi.mocked(prisma.labResult.create).mock.calls[0]![0].data;
|
||||
expect(data.value).toBeCloseTo(6.1 * 18.0182, 9);
|
||||
expect(data.unit).toBe("mg/dL");
|
||||
expect(data.sourceValue).toBe(6.1);
|
||||
expect(data.sourceUnit).toBe("mmol/L");
|
||||
// The report's own window moved with the value.
|
||||
expect(data.sourceReferenceLow).toBeCloseTo(3.9 * 18.0182, 9);
|
||||
expect(data.sourceReferenceHigh).toBeCloseTo(5.6 * 18.0182, 9);
|
||||
});
|
||||
|
||||
it("stores a reading in the marker's own unit without a conversion record", async () => {
|
||||
vi.mocked(decryptFactData).mockReturnValue({
|
||||
label: "Glucose",
|
||||
value: 95,
|
||||
unit: "mg/dL",
|
||||
referenceLow: null,
|
||||
referenceHigh: null,
|
||||
effectiveDate: "2025-01-01",
|
||||
} as never);
|
||||
vi.mocked(resolveOrMintBiomarker).mockResolvedValue({
|
||||
id: "bm-1",
|
||||
name: "Glucose",
|
||||
unit: "mg/dL",
|
||||
lowerBound: null,
|
||||
upperBound: null,
|
||||
panel: null,
|
||||
analyteKey: "fasting-glucose",
|
||||
});
|
||||
vi.mocked(prisma.labResult.create).mockResolvedValue({
|
||||
id: "lr-same",
|
||||
} as never);
|
||||
|
||||
await commitApprovedFact("user-1", observationFact());
|
||||
const data = vi.mocked(prisma.labResult.create).mock.calls[0]![0].data;
|
||||
expect(data.value).toBe(95);
|
||||
expect(data.sourceValue).toBeNull();
|
||||
expect(data.sourceUnit).toBeNull();
|
||||
});
|
||||
|
||||
it("commits when the stated unit is the marker's unit spelled another way", async () => {
|
||||
vi.mocked(decryptFactData).mockReturnValue({
|
||||
label: "Glucose",
|
||||
@@ -87,6 +153,7 @@ describe("commitApprovedFact — OBSERVATION unit integrity", () => {
|
||||
lowerBound: 70,
|
||||
upperBound: 100,
|
||||
panel: null,
|
||||
analyteKey: null,
|
||||
});
|
||||
vi.mocked(prisma.labResult.create).mockResolvedValue({
|
||||
id: "lr-1",
|
||||
@@ -124,6 +191,7 @@ describe("commitApprovedFact — OBSERVATION unit integrity", () => {
|
||||
lowerBound: null,
|
||||
upperBound: null,
|
||||
panel: null,
|
||||
analyteKey: null,
|
||||
});
|
||||
vi.mocked(prisma.labResult.create).mockResolvedValue({
|
||||
id: "lr-2",
|
||||
|
||||
Reference in new issue
Block a user