mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
feat(fhir): emit Coverage on a bare KVNR + add Composition.text
Align the server's `Coverage` emission with the iOS exporter: a Coverage is now emitted whenever ANY payor signal is present, including a bare KVNR with no insurer name or IKNR. The KVNR-only case carries the `subscriberId` with no contained payor Organization; the insurer-known case is unchanged. A bundle with no payor signal at all still omits Coverage entirely. Also add a top-level `Composition.text` narrative (reusing the same escaped summary the section narrative carries) so a strict US-Core-style validator sees the document-level narrative it expects. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 22c5ed7fbe02993307c3f04cd0498d199c79cf43)
This commit is contained in:
1 parent
ca8781867b
commit
7fa2866154
3 files changed
+78
-22
No files matched your search
@@ -241,7 +241,7 @@ describe("buildFhirDocumentBundle", () => {
|
||||
expect(coverage?.contained?.[0].identifier?.[0].value).toBe("101234567");
|
||||
});
|
||||
|
||||
it("omits the Coverage entirely when neither insurer name nor IKNR is present", () => {
|
||||
it("omits the Coverage entirely when there is no payor signal at all", () => {
|
||||
const bundle = buildFhirDocumentBundle(
|
||||
makeData({
|
||||
patient: {
|
||||
@@ -253,17 +253,57 @@ describe("buildFhirDocumentBundle", () => {
|
||||
// neither insurerName nor insurerIkNumber
|
||||
},
|
||||
}),
|
||||
{ insuranceNumber: "A123456780" },
|
||||
// ...and no KVNR.
|
||||
{ insuranceNumber: null },
|
||||
FIXED_NOW,
|
||||
);
|
||||
expect(coverageOf(bundle)).toBeUndefined();
|
||||
// KVNR still rides on Patient.identifier.
|
||||
});
|
||||
|
||||
it("emits a KVNR-only Coverage (no insurer) to align with the iOS exporter", () => {
|
||||
const bundle = buildFhirDocumentBundle(
|
||||
makeData({
|
||||
patient: {
|
||||
username: "sample-user",
|
||||
dateOfBirth: null,
|
||||
gender: null,
|
||||
heightCm: null,
|
||||
fullName: "Sample Patient",
|
||||
// neither insurerName nor insurerIkNumber — just a bare KVNR.
|
||||
},
|
||||
}),
|
||||
{ insuranceNumber: "A123456780" },
|
||||
FIXED_NOW,
|
||||
);
|
||||
const coverage = coverageOf(bundle);
|
||||
expect(coverage).toBeDefined();
|
||||
expect(coverage?.status).toBe("active");
|
||||
expect(coverage?.beneficiary.reference).toBe("Patient/patient-1");
|
||||
expect(coverage?.subscriberId).toBe("A123456780");
|
||||
// No insurer known → no contained payor Organization, no payor[].
|
||||
expect(coverage?.contained).toBeUndefined();
|
||||
expect(coverage?.payor).toBeUndefined();
|
||||
// KVNR still rides on Patient.identifier too.
|
||||
const patient = bundle.entry.find(
|
||||
(e) => e.resource.resourceType === "Patient",
|
||||
)?.resource as FhirPatient;
|
||||
expect(patient.identifier?.[0].value).toBe("A123456780");
|
||||
});
|
||||
|
||||
it("emits a top-level Composition.text narrative", () => {
|
||||
const bundle = buildFhirDocumentBundle(
|
||||
makeData(),
|
||||
{ insuranceNumber: null },
|
||||
FIXED_NOW,
|
||||
);
|
||||
const composition = bundle.entry[0].resource;
|
||||
expect(composition.resourceType).toBe("Composition");
|
||||
if (composition.resourceType === "Composition") {
|
||||
expect(composition.text?.status).toBe("generated");
|
||||
expect(composition.text?.div).toContain("Health record for");
|
||||
}
|
||||
});
|
||||
|
||||
it("maps weight to LOINC 29463-7 / UCUM kg with the latest reading", () => {
|
||||
const bundle = buildFhirDocumentBundle(
|
||||
makeData(),
|
||||
|
||||
@@ -226,33 +226,36 @@ export function buildFhirDocumentBundle(
|
||||
entries.push({ fullUrl: `urn:uuid:${patientId}`, resource: patient });
|
||||
|
||||
// --- Coverage (insurer; sits right after the Patient) ------------------
|
||||
// Emitted whenever ANY payor info is present (insurer name and/or IKNR).
|
||||
// The payor is a CONTAINED Organization referenced by a local `#`-ref.
|
||||
// The KVNR stays on `Patient.identifier`; here it doubles as the
|
||||
// Coverage `subscriberId` (the member id) when present. When neither an
|
||||
// insurer name nor an IKNR is known, omit Coverage entirely rather than
|
||||
// emit an empty payor.
|
||||
// v1.9.0 — emitted whenever ANY payor signal is present: an insurer
|
||||
// name, an IKNR, OR a bare KVNR. The KVNR-only case aligns the server
|
||||
// with the iOS exporter (which emits a Coverage on a bare member id);
|
||||
// it carries the `subscriberId` with no contained payor Organization.
|
||||
// When an insurer name and/or IKNR is known, the payor is a CONTAINED
|
||||
// Organization referenced by a local `#`-ref. The KVNR stays on
|
||||
// `Patient.identifier` and doubles as the `subscriberId` (member id).
|
||||
const insurerName = data.patient.insurerName ?? null;
|
||||
const insurerIkNumber = data.patient.insurerIkNumber ?? null;
|
||||
if (insurerName || insurerIkNumber) {
|
||||
const orgId = "insurer-org-1";
|
||||
const payorOrg: FhirOrganization = {
|
||||
resourceType: "Organization",
|
||||
id: orgId,
|
||||
};
|
||||
if (insurerIkNumber) {
|
||||
payorOrg.identifier = [{ system: IKNR_SYSTEM, value: insurerIkNumber }];
|
||||
}
|
||||
if (insurerName) payorOrg.name = insurerName;
|
||||
|
||||
const hasPayorOrg = Boolean(insurerName || insurerIkNumber);
|
||||
if (hasPayorOrg || identity.insuranceNumber) {
|
||||
const coverage: FhirCoverage = {
|
||||
resourceType: "Coverage",
|
||||
id: "coverage-1",
|
||||
status: "active",
|
||||
contained: [payorOrg],
|
||||
beneficiary: patientRef,
|
||||
payor: [{ reference: `#${orgId}` }],
|
||||
};
|
||||
if (hasPayorOrg) {
|
||||
const orgId = "insurer-org-1";
|
||||
const payorOrg: FhirOrganization = {
|
||||
resourceType: "Organization",
|
||||
id: orgId,
|
||||
};
|
||||
if (insurerIkNumber) {
|
||||
payorOrg.identifier = [{ system: IKNR_SYSTEM, value: insurerIkNumber }];
|
||||
}
|
||||
if (insurerName) payorOrg.name = insurerName;
|
||||
coverage.contained = [payorOrg];
|
||||
coverage.payor = [{ reference: `#${orgId}` }];
|
||||
}
|
||||
if (identity.insuranceNumber) {
|
||||
coverage.subscriberId = identity.insuranceNumber;
|
||||
}
|
||||
@@ -555,6 +558,13 @@ export function buildFhirDocumentBundle(
|
||||
date: now.toISOString(),
|
||||
author: [{ reference: "Device/healthlog", display: "HealthLog" }],
|
||||
title: "Health Record",
|
||||
// v1.9.0 — top-level document narrative (Coverage nit 2). Reuses the
|
||||
// same escaped plain-text summary the Vital-signs section carries, so
|
||||
// a strict US-Core-style validator sees a `Composition.text`.
|
||||
text: {
|
||||
status: "generated",
|
||||
div: `<div xmlns="http://www.w3.org/1999/xhtml">${escapeXml(narrativeText)}</div>`,
|
||||
},
|
||||
section: [
|
||||
{
|
||||
// Vital-signs section carries the narrative + every Observation ref,
|
||||
|
||||
@@ -175,6 +175,12 @@ export interface FhirComposition {
|
||||
date: string;
|
||||
author: FhirReference[];
|
||||
title: string;
|
||||
/**
|
||||
* v1.9.0 — top-level document narrative. Section narrative is already
|
||||
* present; a strict US-Core-style validator additionally expects a
|
||||
* `Composition.text`. Reuses the same escaped plain-text summary.
|
||||
*/
|
||||
text?: FhirNarrative;
|
||||
section?: FhirCompositionSection[];
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user