mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
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:
1 parent
b99b07ad38
commit
90586d6279
4 files changed
+205
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user