feat(export): emit a FHIR Coverage resource for the insurer

The health-record FHIR bundle now carries a Coverage resource right
after the Patient. Coverage.status is active, beneficiary references
the Patient, and subscriberId carries the KVNR when present. The payor
is a contained Organization referenced by a local #-ref: its name is
the insurer name and its identifier carries the IKNR under the
http://fhir.de/sid/arge-ik/iknr system.

When the IKNR is absent the Organization keeps its name only; when
neither insurer name nor IKNR is known the Coverage is omitted rather
than emitting an empty payor. The KVNR continues to ride on
Patient.identifier unchanged — Coverage is purely additive.
This commit is contained in:
Marc-André Bombeck committed 2026-06-01 23:24:10 +02:00
1 parent b99b07ad38
commit 90586d6279
4 files changed
+205

No files matched your search

+6
View File
@@ -67,6 +67,10 @@ export interface DoctorReportData {
// renderer treats absent and null identically.
fullName?: string | null;
insurerName?: string | null;
// v1.8.6 — German insurer institution number (IKNR), plaintext. Carried
// here for the FHIR `Coverage` payor Organization identifier. Optional
// so pre-v1.8.6 fixtures still typecheck.
insurerIkNumber?: string | null;
};
/**
* Free-text practice / clinic name printed on the cover. `null` omits the
@@ -334,6 +338,7 @@ export async function collectDoctorReportData(
// decrypts it and hands it to the builders.
fullName: true,
insurerName: true,
insurerIkNumber: true,
},
}),
]);
@@ -575,6 +580,7 @@ export async function collectDoctorReportData(
heightCm: userProfile?.heightCm ?? null,
fullName: userProfile?.fullName ?? null,
insurerName: userProfile?.insurerName ?? null,
insurerIkNumber: userProfile?.insurerIkNumber ?? null,
},
practiceName,
measurements: filteredByType,
+135
View File
@@ -5,9 +5,18 @@ import type {
FhirObservation,
FhirMedicationStatement,
FhirPatient,
FhirCoverage,
FhirDiagnosticReport,
} from "../types";
function coverageOf(
bundle: ReturnType<typeof buildFhirDocumentBundle>,
): FhirCoverage | undefined {
return bundle.entry
.map((e) => e.resource)
.find((r): r is FhirCoverage => r.resourceType === "Coverage");
}
function observationsOf(bundle: ReturnType<typeof buildFhirDocumentBundle>) {
return bundle.entry
.map((e) => e.resource)
@@ -117,6 +126,132 @@ describe("buildFhirDocumentBundle", () => {
expect(patient.identifier).toBeUndefined();
});
it("emits a Coverage with a contained Organization carrying the IKNR + subscriberId KVNR", () => {
const bundle = buildFhirDocumentBundle(
makeData({
patient: {
username: "sample-user",
dateOfBirth: "1985-06-15T00:00:00.000Z",
gender: "MALE",
heightCm: 182,
fullName: "Sample Patient",
insurerName: "Example Insurer",
insurerIkNumber: "101234567",
},
}),
{ 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");
// Payor references the contained Organization via a local #-ref.
const payorRef = coverage?.payor?.[0].reference;
expect(payorRef?.startsWith("#")).toBe(true);
const org = coverage?.contained?.[0];
expect(org?.resourceType).toBe("Organization");
expect(`#${org?.id}`).toBe(payorRef);
expect(org?.name).toBe("Example Insurer");
expect(org?.identifier?.[0].system).toBe(
"http://fhir.de/sid/arge-ik/iknr",
);
expect(org?.identifier?.[0].value).toBe("101234567");
});
it("places the Coverage right after the Patient in the bundle", () => {
const bundle = buildFhirDocumentBundle(
makeData({
patient: {
username: "sample-user",
dateOfBirth: null,
gender: null,
heightCm: null,
fullName: "Sample Patient",
insurerName: "Example Insurer",
insurerIkNumber: "101234567",
},
}),
{ insuranceNumber: null },
FIXED_NOW,
);
const types = bundle.entry.map((e) => e.resource.resourceType);
const patientIdx = types.indexOf("Patient");
const coverageIdx = types.indexOf("Coverage");
expect(patientIdx).toBeGreaterThanOrEqual(0);
expect(coverageIdx).toBe(patientIdx + 1);
});
it("emits a name-only payor Organization when the IKNR is absent", () => {
const bundle = buildFhirDocumentBundle(
makeData({
patient: {
username: "sample-user",
dateOfBirth: null,
gender: null,
heightCm: null,
fullName: "Sample Patient",
insurerName: "Example Insurer",
// no insurerIkNumber
},
}),
{ insuranceNumber: "A123456780" },
FIXED_NOW,
);
const coverage = coverageOf(bundle);
expect(coverage).toBeDefined();
expect(coverage?.subscriberId).toBe("A123456780");
const org = coverage?.contained?.[0];
expect(org?.name).toBe("Example Insurer");
expect(org?.identifier).toBeUndefined();
});
it("emits a Coverage with no subscriberId when the KVNR is absent", () => {
const bundle = buildFhirDocumentBundle(
makeData({
patient: {
username: "sample-user",
dateOfBirth: null,
gender: null,
heightCm: null,
fullName: "Sample Patient",
insurerName: "Example Insurer",
insurerIkNumber: "101234567",
},
}),
{ insuranceNumber: null },
FIXED_NOW,
);
const coverage = coverageOf(bundle);
expect(coverage).toBeDefined();
expect(coverage?.subscriberId).toBeUndefined();
expect(coverage?.contained?.[0].identifier?.[0].value).toBe("101234567");
});
it("omits the Coverage entirely when neither insurer name nor IKNR is present", () => {
const bundle = buildFhirDocumentBundle(
makeData({
patient: {
username: "sample-user",
dateOfBirth: null,
gender: null,
heightCm: null,
fullName: "Sample Patient",
// neither insurerName nor insurerIkNumber
},
}),
{ insuranceNumber: "A123456780" },
FIXED_NOW,
);
expect(coverageOf(bundle)).toBeUndefined();
// KVNR still rides on Patient.identifier.
const patient = bundle.entry.find(
(e) => e.resource.resourceType === "Patient",
)?.resource as FhirPatient;
expect(patient.identifier?.[0].value).toBe("A123456780");
});
it("maps weight to LOINC 29463-7 / UCUM kg with the latest reading", () => {
const bundle = buildFhirDocumentBundle(
makeData(),
+42
View File
@@ -40,6 +40,8 @@ import type {
FhirObservation,
FhirMedicationStatement,
FhirPatient,
FhirCoverage,
FhirOrganization,
FhirComposition,
FhirDiagnosticReport,
FhirReference,
@@ -55,6 +57,9 @@ export interface FhirPatientIdentity {
/** KVNR identifier namespace per the gematik SID. */
const KVNR_SYSTEM = "http://fhir.de/sid/gkv/kvid-10";
/** German insurer institution-number (IKNR) identifier namespace. */
const IKNR_SYSTEM = "http://fhir.de/sid/arge-ik/iknr";
/** Escape the five XML-significant characters for the xhtml narrative. */
function escapeXml(value: string): string {
return value
@@ -140,6 +145,43 @@ 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.
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 coverage: FhirCoverage = {
resourceType: "Coverage",
id: "coverage-1",
status: "active",
contained: [payorOrg],
beneficiary: patientRef,
payor: [{ reference: `#${orgId}` }],
};
if (identity.insuranceNumber) {
coverage.subscriberId = identity.insuranceNumber;
}
entries.push({
fullUrl: `urn:uuid:${coverage.id}`,
resource: coverage,
});
}
let obsSeq = 0;
const pushObservation = (obs: FhirObservation) => {
entries.push({ fullUrl: `urn:uuid:${obs.id}`, resource: obs });
+22
View File
@@ -106,6 +106,27 @@ export interface FhirDiagnosticReport {
result?: FhirReference[];
}
export interface FhirOrganization {
resourceType: "Organization";
id: string;
identifier?: FhirIdentifier[];
name?: string;
}
export interface FhirCoverage {
resourceType: "Coverage";
id: string;
status: "active" | "cancelled" | "draft" | "entered-in-error";
/** Inline payor Organization, referenced from `payor` via a local `#`-ref. */
contained?: FhirOrganization[];
/** The covered party — Reference(Patient). */
beneficiary: FhirReference;
/** German KVNR (the subscriber's member id), when present. */
subscriberId?: string;
/** Insurer(s) — Reference to the contained Organization. */
payor?: FhirReference[];
}
export interface FhirCompositionSection {
title: string;
code?: FhirCodeableConcept;
@@ -128,6 +149,7 @@ export interface FhirComposition {
export type FhirResource =
| FhirComposition
| FhirPatient
| FhirCoverage
| FhirObservation
| FhirMedicationStatement
| FhirDiagnosticReport;