diff --git a/src/lib/fhir/__tests__/build-bundle.test.ts b/src/lib/fhir/__tests__/build-bundle.test.ts index 62c840260..c3aa8a1b7 100644 --- a/src/lib/fhir/__tests__/build-bundle.test.ts +++ b/src/lib/fhir/__tests__/build-bundle.test.ts @@ -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(), diff --git a/src/lib/fhir/build-bundle.ts b/src/lib/fhir/build-bundle.ts index 7227ba4d5..2578c6912 100644 --- a/src/lib/fhir/build-bundle.ts +++ b/src/lib/fhir/build-bundle.ts @@ -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: `