feat(fhir): export the body site and side of a condition

Condition.bodySite carries the free-text site as text. The side rides a
contained BodyStructure reached through the core bodySite extension, coded
with the SNOMED CT concepts of the R4 bodysite-laterality value set, and is
repeated in the text so a receiver that ignores extensions still reads it.
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 12:45:15 +02:00
1 parent 4af6b87a2a
commit 8934a2900e
4 files changed
+269

No files matched your search

@@ -0,0 +1,126 @@
/**
* `Condition.bodySite` from the condition journal's free-text site and side.
*
* The site rides `text`; the side rides a contained `BodyStructure` reached
* through the core `bodySite` extension, and is repeated in `text` so a
* receiver that ignores extensions still reads which side.
*/
import { describe, expect, it } from "vitest";
import type { DoctorReportData } from "@/lib/doctor-report-data";
import { conditionsFromReportData } from "@/lib/fhir/resources";
import type { FhirCondition } from "@/lib/fhir/types";
type Episode = NonNullable<DoctorReportData["illnessEpisodes"]>[number];
function conditionFor(extra: Partial<Episode>): FhirCondition {
const data = {
illnessEpisodes: [
{
label: "Sprain",
type: "INJURY",
lifecycle: "ACUTE",
onsetAt: "2026-04-01T00:00:00.000Z",
resolvedAt: null,
...extra,
},
],
} as unknown as DoctorReportData;
const [condition] = conditionsFromReportData(data).conditions;
if (!condition) throw new Error("no Condition emitted");
return condition;
}
describe("conditionsFromReportData — bodySite", () => {
it("emits no bodySite and nothing contained when no site was recorded", () => {
const c = conditionFor({});
expect(c.bodySite).toBeUndefined();
expect(c.contained).toBeUndefined();
});
it("treats a blank site as no site, even with a side", () => {
const c = conditionFor({ bodySite: " ", laterality: "LEFT" });
expect(c.bodySite).toBeUndefined();
expect(c.contained).toBeUndefined();
});
it("emits the site as text only when no side was stated", () => {
const c = conditionFor({ bodySite: "Lower back", laterality: null });
expect(c.bodySite).toEqual([{ text: "Lower back" }]);
expect(c.contained).toBeUndefined();
});
it.each([
["LEFT", "419161000", "Unilateral left", "left"],
["RIGHT", "419465000", "Unilateral right", "right"],
["BOTH", "51440002", "Bilateral", "both sides"],
])(
"carries %s as SNOMED %s on a contained BodyStructure",
(laterality, code, display, sideText) => {
const c = conditionFor({ bodySite: "Knee", laterality });
expect(c.bodySite).toEqual([
{
extension: [
{
url: "http://hl7.org/fhir/StructureDefinition/bodySite",
valueReference: { reference: "#bodysite-1" },
},
],
text: `Knee (${sideText})`,
},
]);
expect(c.contained).toEqual([
{
resourceType: "BodyStructure",
id: "bodysite-1",
location: { text: "Knee" },
locationQualifier: [
{
coding: [{ system: "http://snomed.info/sct", code, display }],
text: sideText,
},
],
patient: { reference: "Patient/patient-1" },
},
]);
},
);
it("never puts the side into bodySite.coding", () => {
const c = conditionFor({ bodySite: "Knee", laterality: "RIGHT" });
expect(c.bodySite?.[0]?.coding).toBeUndefined();
});
it("falls back to the site alone for an unknown side value", () => {
const c = conditionFor({ bodySite: "Knee", laterality: "SIDEWAYS" });
expect(c.bodySite).toEqual([{ text: "Knee" }]);
expect(c.contained).toBeUndefined();
});
it("keeps each episode's site on its own Condition", () => {
const data = {
illnessEpisodes: [
{
label: "A",
type: "INJURY",
lifecycle: "ACUTE",
onsetAt: "2026-04-01T00:00:00.000Z",
resolvedAt: null,
bodySite: "Wrist",
laterality: "RIGHT",
},
{
label: "B",
type: "INFECTION",
lifecycle: "ACUTE",
onsetAt: "2026-04-02T00:00:00.000Z",
resolvedAt: null,
},
],
} as unknown as DoctorReportData;
const [a, b] = conditionsFromReportData(data).conditions;
expect(a?.bodySite?.[0]?.text).toBe("Wrist (right)");
expect(b?.bodySite).toBeUndefined();
expect(b?.contained).toBeUndefined();
});
});
@@ -100,6 +100,8 @@ function makeData(overrides?: Partial<DoctorReportData>): DoctorReportData {
lifecycle: "ACUTE",
onsetAt: "2026-04-01T00:00:00.000Z",
resolvedAt: "2026-04-10T00:00:00.000Z",
bodySite: "Knee",
laterality: "LEFT",
},
],
mood: { avg: 3.8, count: 20 },
@@ -173,6 +175,34 @@ function resourcesOf(bundle: FhirBundle): FhirResource[] {
return bundle.entry.map((e) => e.resource);
}
// ── Condition.bodySite — the contained BodyStructure resolves in place ───
describe("Condition.bodySite", () => {
it("points its extension at a BodyStructure contained in the same Condition, whose patient resolves", () => {
const bundle = fullBundle();
const patientUrl = bundle.entry.find(
(e) => e.resource.resourceType === "Patient",
)?.fullUrl;
const condition = resourcesOf(bundle).find(
(r) => r.resourceType === "Condition",
);
if (condition?.resourceType !== "Condition")
throw new Error("no Condition");
const ext = condition.bodySite?.[0]?.extension?.[0];
expect(ext?.url).toBe("http://hl7.org/fhir/StructureDefinition/bodySite");
const target = ext?.valueReference.reference;
const structure = condition.contained?.find((c) => `#${c.id}` === target);
expect(structure?.resourceType).toBe("BodyStructure");
expect(structure?.patient.reference).toBe(patientUrl);
// A code from the R4 core bodysite-laterality value set.
expect(
["419161000", "419465000", "51440002"].includes(
structure?.locationQualifier?.[0]?.coding?.[0]?.code ?? "",
),
).toBe(true);
});
});
// ── Test 14 — document identity, entry identity, reference resolution ─────
describe("document Bundle — identity and resolvability", () => {
+81
View File
@@ -10,6 +10,7 @@
import type { DoctorReportData } from "@/lib/doctor-report-data";
import { ILLNESS_TYPE_SNOMED } from "@/lib/fhir/illness-snomed";
import type {
FhirBodyStructure,
FhirCodeableConcept,
FhirCondition,
FhirEncounter,
@@ -19,6 +20,83 @@ import { SNOMED_SYSTEM, patientRef } from "@/lib/fhir/resources/common";
/** SNOMED CT "Disease (disorder)" — the generic, non-diagnostic root concept. */
const DISEASE_SNOMED = { code: "64572001", display: "Disease" } as const;
/**
* The side of a body site, as SNOMED CT laterality qualifiers from the R4 core
* value set `http://hl7.org/fhir/ValueSet/bodysite-laterality`
* (https://hl7.org/fhir/R4/valueset-bodysite-laterality.html); the same three
* concepts sit in `bodystructure-relative-location`, the value set R4 binds to
* `BodyStructure.locationQualifier`. Keyed on the `Laterality` enum.
*/
const LATERALITY_SNOMED: Record<
string,
{ code: string; display: string; text: string }
> = {
LEFT: { code: "419161000", display: "Unilateral left", text: "left" },
RIGHT: { code: "419465000", display: "Unilateral right", text: "right" },
BOTH: { code: "51440002", display: "Bilateral", text: "both sides" },
};
/**
* Core extension `http://hl7.org/fhir/StructureDefinition/bodySite`
* (https://hl7.org/fhir/extensions/StructureDefinition-bodySite.html): a
* Reference(BodyStructure) on `Condition.bodySite`, unchanged in R4.
*/
const BODY_SITE_EXTENSION_URL =
"http://hl7.org/fhir/StructureDefinition/bodySite";
const BODY_STRUCTURE_ID = "bodysite-1";
/**
* `Condition.bodySite` plus the contained `BodyStructure` that carries its
* side, or nothing when no site was recorded.
*
* The site is the user's free text, so it rides `text` only; no SNOMED body
* structure is guessed from it. R4 `Condition.bodySite` has no laterality
* element, and a laterality concept is not a body site, so putting "Left" in
* `bodySite.coding` would claim the condition sits on "left". The standard R4
* route is the core `bodySite` extension pointing at a `BodyStructure` whose
* `locationQualifier` holds the SNOMED CT side. The side is also written into
* `bodySite.text`, because a receiver that ignores extensions must still read
* "Knee (left)" rather than a bare "Knee".
*/
function bodySiteOf(
bodySite: string | null | undefined,
laterality: string | null | undefined,
): Pick<FhirCondition, "bodySite" | "contained"> {
const site = bodySite?.trim();
if (!site) return {};
const side = laterality ? LATERALITY_SNOMED[laterality] : undefined;
if (!side) return { bodySite: [{ text: site }] };
const structure: FhirBodyStructure = {
resourceType: "BodyStructure",
id: BODY_STRUCTURE_ID,
location: { text: site },
locationQualifier: [
{
coding: [
{ system: SNOMED_SYSTEM, code: side.code, display: side.display },
],
text: side.text,
},
],
patient: patientRef,
};
return {
contained: [structure],
bodySite: [
{
extension: [
{
url: BODY_SITE_EXTENSION_URL,
valueReference: { reference: `#${BODY_STRUCTURE_ID}` },
},
],
text: `${site} (${side.text})`,
},
],
};
}
/** R4 `Condition.clinicalStatus` concept for the given resolution state. */
function conditionClinicalStatus(resolved: boolean): FhirCodeableConcept {
return {
@@ -53,9 +131,11 @@ export function conditionsFromReportData(data: DoctorReportData): {
seq += 1;
const conditionId = `condition-${seq}`;
const resolved = ep.resolvedAt !== null;
const site = bodySiteOf(ep.bodySite, ep.laterality);
conditions.push({
resourceType: "Condition",
id: conditionId,
...(site.contained ? { contained: site.contained } : {}),
clinicalStatus: conditionClinicalStatus(resolved),
verificationStatus: {
coding: [
@@ -94,6 +174,7 @@ export function conditionsFromReportData(data: DoctorReportData): {
],
text: ep.label,
},
...(site.bodySite ? { bodySite: site.bodySite } : {}),
subject: patientRef,
onsetDateTime: ep.onsetAt,
...(ep.resolvedAt ? { abatementDateTime: ep.resolvedAt } : {}),
+32
View File
@@ -15,10 +15,21 @@ export interface FhirCoding {
}
export interface FhirCodeableConcept {
/**
* Element extensions. Only `Condition.bodySite` carries one today (the core
* `bodySite` extension pointing at a contained `BodyStructure`).
*/
extension?: FhirExtension[];
coding?: FhirCoding[];
text?: string;
}
/** R4 `Extension`, narrowed to the one value type HealthLog emits. */
export interface FhirExtension {
url: string;
valueReference: FhirReference;
}
export interface FhirReference {
reference: string;
display?: string;
@@ -250,10 +261,17 @@ export interface FhirComposition {
export interface FhirCondition {
resourceType: "Condition";
id: string;
/**
* v1.39.3 — the `BodyStructure` that carries the side of the body site,
* referenced from `bodySite[].extension` by a local `#`-ref.
*/
contained?: FhirBodyStructure[];
clinicalStatus?: FhirCodeableConcept;
verificationStatus?: FhirCodeableConcept;
category?: FhirCodeableConcept[];
code: FhirCodeableConcept;
/** v1.39.3 — where on the body, as the user wrote it (`text` only). */
bodySite?: FhirCodeableConcept[];
subject: FhirReference;
onsetDateTime?: string;
abatementDateTime?: string;
@@ -261,6 +279,20 @@ export interface FhirCondition {
note?: FhirAnnotation[];
}
/**
* v1.39.3 — R4 `BodyStructure`, emitted only as a resource contained in a
* `Condition`. `location` repeats the free-text site; `locationQualifier`
* carries the side as a SNOMED CT laterality concept. `patient` is `1..1` in
* R4.
*/
export interface FhirBodyStructure {
resourceType: "BodyStructure";
id: string;
location?: FhirCodeableConcept;
locationQualifier?: FhirCodeableConcept[];
patient: FhirReference;
}
/**
* v1.18.1 P4 — R4 `Encounter`. Emitted alongside a resolved episode to bound
* the period of care (onset → resolution) so a clinician sees the episode as a