Skip to main content
Glama
patientsummary.test.ts20.3 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { LOINC_ALLERGIES_SECTION, LOINC_ASSESSMENTS_SECTION, LOINC_DEVICES_SECTION, LOINC_ENCOUNTERS_SECTION, LOINC_GOALS_SECTION, LOINC_HEALTH_CONCERNS_SECTION, LOINC_IMMUNIZATIONS_SECTION, LOINC_MEDICATIONS_SECTION, LOINC_NOTE_DOCUMENT, LOINC_NOTES_SECTION, LOINC_PLAN_OF_TREATMENT_SECTION, LOINC_PROBLEMS_SECTION, LOINC_PROCEDURES_SECTION, LOINC_RESULTS_SECTION, LOINC_SOCIAL_HISTORY_SECTION, LOINC_VITAL_SIGNS_SECTION, } from '@medplum/ccda'; import type { WithId } from '@medplum/core'; import { ContentType, createReference, getReferenceString, LOINC } from '@medplum/core'; import type { Bundle, CarePlan, ClinicalImpression, Composition, Condition, DiagnosticReport, Encounter, Observation, Organization, Patient, Practitioner, Resource, ServiceRequest, } from '@medplum/fhirtypes'; import express from 'express'; import request from 'supertest'; import { initApp, shutdownApp } from '../../app'; import { loadTestConfig } from '../../config/loader'; import { initTestAuth } from '../../test.setup'; import { OBSERVATION_CATEGORY_SYSTEM, PatientSummaryBuilder } from './patientsummary'; const app = express(); let accessToken: string; describe('Patient Summary Operation', () => { beforeAll(async () => { const config = await loadTestConfig(); await initApp(app, config); accessToken = await initTestAuth(); }); afterAll(async () => { await shutdownApp(); }); test('Success', async () => { // Create organization const orgRes = await request(app) .post(`/fhir/R4/Organization`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Organization' }); expect(orgRes.status).toBe(201); const organization = orgRes.body as WithId<Organization>; // Create practitioner const practRes = await request(app) .post(`/fhir/R4/Practitioner`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Practitioner' }); expect(practRes.status).toBe(201); const practitioner = practRes.body as WithId<Practitioner>; // Create patient const res1 = await request(app) .post(`/fhir/R4/Patient`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], address: [{ use: 'home', line: ['123 Main St'], city: 'Anywhere', state: 'CA', postalCode: '90210' }], telecom: [ { system: 'phone', value: '555-555-5555' }, { system: 'email', value: 'alice@example.com' }, ], managingOrganization: createReference(organization), } satisfies Patient); expect(res1.status).toBe(201); const patient = res1.body as WithId<Patient>; // Create observation const res2 = await request(app) .post(`/fhir/R4/Observation`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Observation', status: 'final', category: [{ coding: [{ system: OBSERVATION_CATEGORY_SYSTEM, code: 'vital-signs' }] }], code: { coding: [{ system: LOINC, code: '12345-6' }] }, subject: createReference(patient), performer: [createReference(practitioner), createReference(organization)], } satisfies Observation); expect(res2.status).toBe(201); const observation = res2.body as WithId<Observation>; // Create condition // This condition references the patient twice, once as subject and once as asserter // This is to test that the condition is only returned once const res3 = await request(app) .post(`/fhir/R4/Condition`) .set('Authorization', 'Bearer ' + accessToken) .set('Content-Type', ContentType.FHIR_JSON) .send({ resourceType: 'Condition', code: { coding: [{ system: LOINC, code: '12345-6' }] }, asserter: createReference(patient), subject: createReference(patient), recorder: createReference(practitioner), } satisfies Condition); expect(res3.status).toBe(201); const condition = res3.body as WithId<Condition>; // Execute the operation const res4 = await request(app) .get(`/fhir/R4/Patient/${patient.id}/$summary`) .set('Authorization', 'Bearer ' + accessToken); expect(res4.status).toBe(200); const result = res4.body as WithId<Bundle>; expect(result.type).toBe('document'); expect(result.entry?.[0]?.resource?.resourceType).toBe('Composition'); expect(result.entry?.[1]?.resource?.resourceType).toBe('Patient'); const composition = result.entry?.[0]?.resource as WithId<Composition>; expectSectionToContain(composition, LOINC_VITAL_SIGNS_SECTION, 'Result', getReferenceString(observation)); expectSectionToContain(composition, LOINC_PROBLEMS_SECTION, 'Problem', getReferenceString(condition)); }); describe('PatientSummaryBuilder', () => { test('Simple categories', () => { const author: Practitioner = { resourceType: 'Practitioner', id: 'author1' }; const patient: Patient = { resourceType: 'Patient', id: 'patient1' }; const patientRef = createReference(patient); const everything: WithId<Resource>[] = [ { resourceType: 'AllergyIntolerance', id: 'allergy1', patient: patientRef }, { resourceType: 'Condition', id: 'condition1', subject: patientRef }, { resourceType: 'ClinicalImpression', id: 'impression1', status: 'completed', subject: patientRef }, { resourceType: 'DeviceUseStatement', id: 'device1', status: 'active', subject: patientRef, device: { display: 'test' }, }, { resourceType: 'DiagnosticReport', id: 'report1', subject: patientRef, status: 'final', code: { text: 'test' }, }, { resourceType: 'Encounter', id: 'encounter1', class: { code: 'test' }, status: 'finished', subject: patientRef, }, { resourceType: 'Goal', id: 'goal1', subject: patientRef, lifecycleStatus: 'accepted', description: { text: 'test' }, }, { resourceType: 'Immunization', id: 'imm1', patient: patientRef, status: 'completed', vaccineCode: { text: 'test' }, }, { resourceType: 'MedicationRequest', id: 'med1', subject: patientRef, status: 'active', intent: 'plan' }, { resourceType: 'Procedure', id: 'proc1', subject: patientRef, status: 'completed' }, { resourceType: 'ServiceRequest', id: 'sr1', subject: patientRef, status: 'active', intent: 'plan' }, ]; const builder = new PatientSummaryBuilder(author, patient, everything); const result = builder.build(); expect(result.entry?.length).toBe(3 + everything.length); // 1 for author, 1 for patient, 1 for composition expect(result.entry?.[0]?.resource?.resourceType).toBe('Composition'); expect(result.entry?.[1]?.resource?.resourceType).toBe('Patient'); const composition = result.entry?.[0]?.resource as WithId<Composition>; expectSectionToContain(composition, LOINC_ALLERGIES_SECTION, 'Substance', 'AllergyIntolerance/allergy1'); expectSectionToContain(composition, LOINC_PROBLEMS_SECTION, 'Problem', 'Condition/condition1'); expectSectionToContain(composition, LOINC_DEVICES_SECTION, 'Device', 'DeviceUseStatement/device1'); expectSectionToContain(composition, LOINC_ENCOUNTERS_SECTION, 'Encounter', 'Encounter/encounter1'); expectSectionToContain(composition, LOINC_RESULTS_SECTION, 'Result', 'DiagnosticReport/report1'); expectSectionToContain(composition, LOINC_GOALS_SECTION, 'Goal', 'Goal/goal1'); expectSectionToContain(composition, LOINC_IMMUNIZATIONS_SECTION, 'Vaccine', 'Immunization/imm1'); expectSectionToContain(composition, LOINC_MEDICATIONS_SECTION, 'Medication', 'MedicationRequest/med1'); expectSectionToContain(composition, LOINC_PROCEDURES_SECTION, 'Procedure', 'Procedure/proc1'); expectSectionToContain(composition, LOINC_PLAN_OF_TREATMENT_SECTION, 'Planned', 'ServiceRequest/sr1'); }); test('ClinicalImpressions', () => { const author: Practitioner = { resourceType: 'Practitioner', id: 'author1' }; const patient: Patient = { resourceType: 'Patient', id: 'patient1' }; const subject = createReference(patient); const everything: WithId<ClinicalImpression>[] = [ { resourceType: 'ClinicalImpression', id: 'assessment', status: 'completed', subject, summary: 'test', }, { resourceType: 'ClinicalImpression', id: 'note', status: 'completed', code: { coding: [{ system: LOINC, code: LOINC_NOTE_DOCUMENT }] }, subject, summary: 'test', }, ]; const builder = new PatientSummaryBuilder(author, patient, everything); const result = builder.build(); expect(result.entry?.length).toBe(3 + everything.length); // 1 for author, 1 for patient, 1 for composition expect(result.entry?.[0]?.resource?.resourceType).toBe('Composition'); expect(result.entry?.[1]?.resource?.resourceType).toBe('Patient'); const composition = result.entry?.[0]?.resource as WithId<Composition>; expectSectionToContain(composition, LOINC_ASSESSMENTS_SECTION, 'Summary', 'ClinicalImpression/assessment'); expectSectionToContain(composition, LOINC_NOTES_SECTION, 'Note', 'ClinicalImpression/note'); }); test('Conditions', () => { const author: Practitioner = { resourceType: 'Practitioner', id: 'author1' }; const patient: Patient = { resourceType: 'Patient', id: 'patient1' }; const subject = createReference(patient); const everything: WithId<Condition>[] = [ { resourceType: 'Condition', id: 'concern', category: [{ coding: [{ system: LOINC, code: LOINC_HEALTH_CONCERNS_SECTION }] }], subject, }, { resourceType: 'Condition', id: 'problem', subject, }, ]; const builder = new PatientSummaryBuilder(author, patient, everything); const result = builder.build(); expect(result.entry?.length).toBe(3 + everything.length); // 1 for author, 1 for patient, 1 for composition expect(result.entry?.[0]?.resource?.resourceType).toBe('Composition'); expect(result.entry?.[1]?.resource?.resourceType).toBe('Patient'); const composition = result.entry?.[0]?.resource as WithId<Composition>; expectSectionToContain(composition, LOINC_HEALTH_CONCERNS_SECTION, 'Concern', 'Condition/concern'); expectSectionToContain(composition, LOINC_PROBLEMS_SECTION, 'Problem', 'Condition/problem'); }); test('Observations', () => { const author: Practitioner = { resourceType: 'Practitioner', id: 'author1' }; const patient: Patient = { resourceType: 'Patient', id: 'patient1' }; const subject = createReference(patient); const categories = [ ['social-history', LOINC_SOCIAL_HISTORY_SECTION], ['vital-signs', LOINC_VITAL_SIGNS_SECTION], ['imaging', LOINC_RESULTS_SECTION], ['laboratory', LOINC_RESULTS_SECTION], ['activity', LOINC_RESULTS_SECTION], ]; const everything = categories.map( (category, index) => ({ resourceType: 'Observation', id: `obs${index}`, subject, status: 'final', category: [{ coding: [{ system: OBSERVATION_CATEGORY_SYSTEM, code: category[0] }] }], code: { text: 'test' }, }) as WithId<Observation> ); const builder = new PatientSummaryBuilder(author, patient, everything); const result = builder.build(); expect(result.entry?.length).toBe(3 + everything.length); // 1 for author, 1 for patient, 1 for composition expect(result.entry?.[0]?.resource?.resourceType).toBe('Composition'); expect(result.entry?.[1]?.resource?.resourceType).toBe('Patient'); const composition = result.entry?.[0]?.resource as WithId<Composition>; for (let i = 0; i < categories.length; i++) { expectSectionToContain(composition, categories[i][1], 'Result', `Observation/obs${i}`); } }); test('Observation containing observation', () => { // If an Observation is a member of another Observation, // then it should not be referenced directly by the Composition entries list. const author: Practitioner = { resourceType: 'Practitioner', id: 'author1' }; const patient: Patient = { resourceType: 'Patient', id: 'patient1' }; const subject = createReference(patient); const childObs: WithId<Observation> = { resourceType: 'Observation', id: `child`, subject, status: 'final', category: [{ coding: [{ system: OBSERVATION_CATEGORY_SYSTEM, code: 'vital-signs' }] }], code: { text: 'test' }, }; const parentObs: WithId<Observation> = { resourceType: 'Observation', id: `parent`, subject, status: 'final', category: [{ coding: [{ system: OBSERVATION_CATEGORY_SYSTEM, code: 'vital-signs' }] }], code: { text: 'test' }, hasMember: [createReference(childObs)], }; const everything = [parentObs, childObs]; const builder = new PatientSummaryBuilder(author, patient, everything); const result = builder.build(); expect(result.entry?.length).toBe(3 + everything.length); expect(result.entry?.[0]?.resource?.resourceType).toBe('Composition'); expect(result.entry?.[1]?.resource?.resourceType).toBe('Patient'); const composition = result.entry?.[0]?.resource as WithId<Composition>; const section = composition.section?.find((s) => s.code?.coding?.[0]?.code === LOINC_VITAL_SIGNS_SECTION); expect(section).toBeDefined(); expect(section?.entry?.length).toBe(1); expect(section?.entry?.[0]?.reference).toBe(getReferenceString(parentObs)); }); test('DiagnosticReport containing observation', () => { // If an Observation is a member of a DiagnosticReport, // then it should not be referenced directly by the Composition entries list. const author: Practitioner = { resourceType: 'Practitioner', id: 'author1' }; const patient: Patient = { resourceType: 'Patient', id: 'patient1' }; const subject = createReference(patient); const childObs: WithId<Observation> = { resourceType: 'Observation', id: `child`, subject, status: 'final', code: { text: 'test' }, }; const parentReport: WithId<DiagnosticReport> = { resourceType: 'DiagnosticReport', id: `parent`, subject, status: 'final', code: { text: 'test' }, result: [createReference(childObs)], }; const everything = [parentReport, childObs]; const builder = new PatientSummaryBuilder(author, patient, everything); const result = builder.build(); expect(result.entry?.length).toBe(3 + everything.length); expect(result.entry?.[0]?.resource?.resourceType).toBe('Composition'); expect(result.entry?.[1]?.resource?.resourceType).toBe('Patient'); const composition = result.entry?.[0]?.resource as WithId<Composition>; const section = composition.section?.find((s) => s.code?.coding?.[0]?.code === LOINC_RESULTS_SECTION); expect(section).toBeDefined(); expect(section?.entry?.length).toBe(1); expect(section?.entry?.[0]?.reference).toBe(getReferenceString(parentReport)); }); test('CarePlan containing ServiceRequest', () => { // If an ServiceRequest is a member of a CarePlan, // then it should not be referenced directly by the Composition entries list. const author: Practitioner = { resourceType: 'Practitioner', id: 'author1' }; const patient: Patient = { resourceType: 'Patient', id: 'patient1' }; const subject = createReference(patient); const child: WithId<ServiceRequest> = { resourceType: 'ServiceRequest', id: `child`, subject, status: 'active', intent: 'plan', code: { text: 'test' }, }; const parent: WithId<CarePlan> = { resourceType: 'CarePlan', id: `parent`, subject, status: 'active', intent: 'plan', activity: [{ reference: createReference(child) }], }; const everything = [parent, child]; const builder = new PatientSummaryBuilder(author, patient, everything); const result = builder.build(); expect(result.entry?.length).toBe(3 + everything.length); expect(result.entry?.[0]?.resource?.resourceType).toBe('Composition'); expect(result.entry?.[1]?.resource?.resourceType).toBe('Patient'); const composition = result.entry?.[0]?.resource as WithId<Composition>; const section = composition.section?.find((s) => s.code?.coding?.[0]?.code === LOINC_PLAN_OF_TREATMENT_SECTION); expect(section).toBeDefined(); expect(section?.entry?.length).toBe(1); expect(section?.entry?.[0]?.reference).toBe(getReferenceString(parent)); }); test('Encounter containing conditions', () => { // If an Observation is a member of a DiagnosticReport, // then it should not be referenced directly by the Composition entries list. const author: Practitioner = { resourceType: 'Practitioner', id: 'author1' }; const patient: Patient = { resourceType: 'Patient', id: 'patient1' }; const subject = createReference(patient); const child: WithId<Condition> = { resourceType: 'Condition', id: 'diagnosis', subject, clinicalStatus: { coding: [{ code: 'active' }] }, category: [{ coding: [{ code: 'encounter-diagnosis' }] }], code: { coding: [{ code: '386661006' }] }, onsetDateTime: '2011-10-05T07:00:00.000Z', recordedDate: '2025-02-24T20:51:00.000Z', recorder: { reference: 'Practitioner/davis' }, }; const parent: WithId<Encounter> = { resourceType: 'Encounter', id: 'encounter', subject, status: 'finished', class: { code: 'test' }, diagnosis: [{ condition: createReference(child) }], period: { start: '2015-06-22T20:00:00.000Z', end: '2015-06-22T21:00:00.000Z' }, }; const everything = [parent, child]; const builder = new PatientSummaryBuilder(author, patient, everything); const result = builder.build(); expect(result.entry?.length).toBe(3 + everything.length); expect(result.entry?.[0]?.resource?.resourceType).toBe('Composition'); expect(result.entry?.[1]?.resource?.resourceType).toBe('Patient'); const composition = result.entry?.[0]?.resource as WithId<Composition>; const section = composition.section?.find((s) => s.code?.coding?.[0]?.code === LOINC_ENCOUNTERS_SECTION); expect(section).toBeDefined(); expect(section?.entry?.length).toBe(1); expect(section?.entry?.[0]?.reference).toBe(getReferenceString(parent)); }); }); }); function expectSectionToContain(composition: Composition, code: string, narrative: string, reference: string): void { const section = composition.section?.find((s) => s.code?.coding?.[0]?.code === code); if (!section) { throw new Error(`Section not found: ${code}`); } if (!section.text?.div?.includes(narrative)) { throw new Error(`Narrative not found in section ${code}: ${narrative} (${section.text?.div})`); } const entry = section?.entry?.find((e) => e.reference === reference); if (!entry) { throw new Error(`Entry not found in section ${code}: ${reference}`); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server