mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
fix(fhir): leave out the vital-signs DiagnosticReport when there are no vital signs
A window without a vital sign emitted the panel with result: [], which R4 rejects. The DiagnosticReport now goes out only when it has a result, and the vital-signs section drops an empty entry list rather than sending one.
This commit is contained in:
1 parent
87526e3df6
commit
35327b84c1
5 files changed
+99
-33
No files matched your search
@@ -1478,9 +1478,10 @@ paths:
|
||||
summary: FHIR R4 $everything (v1.11.0)
|
||||
description: "Read-only `Patient/$everything`: every resource the document bundle carries — Composition, Device,
|
||||
Patient, Coverage, Observations (including the cycle ones), MedicationStatements, MedicationAdministrations,
|
||||
Conditions, Encounters, AllergyIntolerances, FamilyMemberHistories and the DiagnosticReport — flattened into one
|
||||
`searchset` Bundle. Scoped to the owner's SAVED report selection; an account that never saved one gets nothing.
|
||||
Auth: `fhir:read` scope. Offset paging via `_count` (≤200) / `_offset`."
|
||||
Conditions, Encounters, AllergyIntolerances, FamilyMemberHistories and the DiagnosticReport (present when the
|
||||
window holds a vital sign) — flattened into one `searchset` Bundle. Scoped to the owner's SAVED report
|
||||
selection; an account that never saved one gets nothing. Auth: `fhir:read` scope. Offset paging via `_count`
|
||||
(≤200) / `_offset`."
|
||||
parameters:
|
||||
- in: query
|
||||
name: _count
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
* the Composition, the authoring Device, Patient, Coverage, every Observation
|
||||
* (vitals / glucose / labs / adherence / mood / wellness AND the cycle set),
|
||||
* MedicationStatements, MedicationAdministrations, Conditions, Encounters,
|
||||
* AllergyIntolerances, FamilyMemberHistories and the DiagnosticReport.
|
||||
* AllergyIntolerances, FamilyMemberHistories and the DiagnosticReport (present
|
||||
* when the window holds a vital sign).
|
||||
*
|
||||
* The set is the document export's, flattened — same builder, same order, same
|
||||
* `urn:uuid` entry identities, so a reference between two resources still
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
FhirPatient,
|
||||
FhirCoverage,
|
||||
FhirDiagnosticReport,
|
||||
FhirComposition,
|
||||
} from "../types";
|
||||
|
||||
function administrationsOf(
|
||||
@@ -1172,6 +1173,57 @@ describe("buildFhirDocumentBundle", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("emits no DiagnosticReport, and no empty array anywhere, when the window holds no vital sign", () => {
|
||||
const bundle = buildFhirDocumentBundle(
|
||||
makeData({ measurements: {}, bmi: null }),
|
||||
{ insuranceNumber: null },
|
||||
FIXED_NOW,
|
||||
);
|
||||
const types = bundle.entry.map((e) => e.resource.resourceType);
|
||||
expect(types).not.toContain("DiagnosticReport");
|
||||
const composition = bundle.entry[0].resource as FhirComposition;
|
||||
const refs = (composition.section ?? []).flatMap((s) => s.entry ?? []);
|
||||
for (const ref of refs) {
|
||||
expect(refTarget(bundle, ref.reference)).not.toMatch(
|
||||
/^DiagnosticReport\//,
|
||||
);
|
||||
}
|
||||
// R4: an array element is absent, never empty.
|
||||
const emptyArrays: string[] = [];
|
||||
const walk = (value: unknown, path: string): void => {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 0) emptyArrays.push(path);
|
||||
value.forEach((item, i) => walk(item, `${path}[${i}]`));
|
||||
} else if (value && typeof value === "object") {
|
||||
for (const [k, v] of Object.entries(value)) walk(v, `${path}.${k}`);
|
||||
}
|
||||
};
|
||||
walk(bundle, "Bundle");
|
||||
expect(emptyArrays).toEqual([]);
|
||||
});
|
||||
|
||||
it("leaves the vital-signs section without `entry` when it has nothing to list", () => {
|
||||
const bundle = buildFhirDocumentBundle(
|
||||
makeData({
|
||||
measurements: {},
|
||||
bmi: null,
|
||||
glucoseStats: {},
|
||||
compliance: {},
|
||||
medications: [],
|
||||
mood: null,
|
||||
labResults: [],
|
||||
} as Partial<DoctorReportData>),
|
||||
{ insuranceNumber: null },
|
||||
FIXED_NOW,
|
||||
);
|
||||
const composition = bundle.entry[0].resource as FhirComposition;
|
||||
const vitals = composition.section?.find((s) => s.title === "Vital signs");
|
||||
expect(vitals?.text?.div).toBeTruthy();
|
||||
expect(
|
||||
vitals && "entry" in vitals ? vitals.entry : undefined,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("appends a DiagnosticReport as the LAST entry routing all Observation refs", () => {
|
||||
const bundle = buildFhirDocumentBundle(
|
||||
makeData(),
|
||||
|
||||
@@ -328,31 +328,39 @@ export function buildFhirDocumentBundle(
|
||||
: []),
|
||||
].join(" ");
|
||||
|
||||
const diagnosticReport: FhirDiagnosticReport = {
|
||||
resourceType: "DiagnosticReport",
|
||||
id: "diagnostic-report-1",
|
||||
status: "final",
|
||||
code: {
|
||||
coding: [
|
||||
{
|
||||
system: LOINC_SYSTEM,
|
||||
code: "85353-1",
|
||||
display:
|
||||
"Vital signs, weight, height, head circumference, oxygen saturation and BMI panel",
|
||||
},
|
||||
],
|
||||
text: "Vital signs panel",
|
||||
},
|
||||
subject: patientRef,
|
||||
effectivePeriod: { start: data.period.start, end: data.period.end },
|
||||
// A vital-signs panel reports vital signs. Routing labs, adherence rates
|
||||
// and survey scores through it would let a receiver read a questionnaire
|
||||
// total as a vital sign because the panel code said so.
|
||||
result: vitalSignsObservationRefs,
|
||||
};
|
||||
const diagnosticReportRef: FhirReference = {
|
||||
reference: `DiagnosticReport/${diagnosticReport.id}`,
|
||||
};
|
||||
// A vital-signs panel with no vital signs reports nothing, and R4 forbids
|
||||
// the empty `result: []` it would otherwise carry. With no vital-sign
|
||||
// Observation in the window the DiagnosticReport is left out entirely,
|
||||
// together with its Composition entry.
|
||||
const diagnosticReport: FhirDiagnosticReport | null =
|
||||
vitalSignsObservationRefs.length === 0
|
||||
? null
|
||||
: {
|
||||
resourceType: "DiagnosticReport",
|
||||
id: "diagnostic-report-1",
|
||||
status: "final",
|
||||
code: {
|
||||
coding: [
|
||||
{
|
||||
system: LOINC_SYSTEM,
|
||||
code: "85353-1",
|
||||
display:
|
||||
"Vital signs, weight, height, head circumference, oxygen saturation and BMI panel",
|
||||
},
|
||||
],
|
||||
text: "Vital signs panel",
|
||||
},
|
||||
subject: patientRef,
|
||||
effectivePeriod: { start: data.period.start, end: data.period.end },
|
||||
// A vital-signs panel reports vital signs. Routing labs, adherence rates
|
||||
// and survey scores through it would let a receiver read a questionnaire
|
||||
// total as a vital sign because the panel code said so.
|
||||
result: vitalSignsObservationRefs,
|
||||
};
|
||||
const diagnosticReportRefs: FhirReference[] = diagnosticReport
|
||||
? [{ reference: `DiagnosticReport/${diagnosticReport.id}` }]
|
||||
: [];
|
||||
const vitalSignsEntries = [...observationRefs, ...diagnosticReportRefs];
|
||||
|
||||
const composition: FhirComposition = {
|
||||
resourceType: "Composition",
|
||||
@@ -390,7 +398,9 @@ export function buildFhirDocumentBundle(
|
||||
status: "generated",
|
||||
div: `<div xmlns="http://www.w3.org/1999/xhtml">${escapeXml(narrativeText)}</div>`,
|
||||
},
|
||||
entry: [...observationRefs, diagnosticReportRef],
|
||||
// `entry` is omitted rather than empty; the section still carries
|
||||
// its narrative, which satisfies `cmp-2`.
|
||||
...(vitalSignsEntries.length > 0 ? { entry: vitalSignsEntries } : {}),
|
||||
},
|
||||
// Medications section carries both the active-medication
|
||||
// statements and the per-dose administration records (v1.9.0).
|
||||
@@ -479,11 +489,13 @@ export function buildFhirDocumentBundle(
|
||||
};
|
||||
|
||||
// The Composition must be the FIRST entry in a document Bundle; the
|
||||
// DiagnosticReport is the LAST.
|
||||
// DiagnosticReport, when there is one, is the LAST.
|
||||
const orderedEntries: FhirBundleEntry[] = [
|
||||
{ fullUrl: `urn:uuid:${randomUUID()}`, resource: composition },
|
||||
...entries,
|
||||
{ fullUrl: `urn:uuid:${randomUUID()}`, resource: diagnosticReport },
|
||||
...(diagnosticReport
|
||||
? [{ fullUrl: `urn:uuid:${randomUUID()}`, resource: diagnosticReport }]
|
||||
: []),
|
||||
];
|
||||
|
||||
// Every entry identity is known only now, so the reference rewrite runs once
|
||||
|
||||
@@ -647,7 +647,7 @@ export const healthRecordPaths: NonNullable<ZodOpenApiObject["paths"]> = {
|
||||
tags: ["FHIR"],
|
||||
summary: "FHIR R4 $everything (v1.11.0)",
|
||||
description:
|
||||
"Read-only `Patient/$everything`: every resource the document bundle carries — Composition, Device, Patient, Coverage, Observations (including the cycle ones), MedicationStatements, MedicationAdministrations, Conditions, Encounters, AllergyIntolerances, FamilyMemberHistories and the DiagnosticReport — flattened into one `searchset` Bundle. Scoped to the owner's SAVED report selection; an account that never saved one gets nothing. Auth: `fhir:read` scope. Offset paging via `_count` (≤200) / `_offset`.",
|
||||
"Read-only `Patient/$everything`: every resource the document bundle carries — Composition, Device, Patient, Coverage, Observations (including the cycle ones), MedicationStatements, MedicationAdministrations, Conditions, Encounters, AllergyIntolerances, FamilyMemberHistories and the DiagnosticReport (present when the window holds a vital sign) — flattened into one `searchset` Bundle. Scoped to the owner's SAVED report selection; an account that never saved one gets nothing. Auth: `fhir:read` scope. Offset paging via `_count` (≤200) / `_offset`.",
|
||||
requestParams: {
|
||||
query: z.object({
|
||||
_count: z.coerce.number().optional(),
|
||||
|
||||
Reference in new issue
Block a user