Skip to main content
Glama
episodeOfCare.integration.test.ts25.5 kB
import { EpisodeOfCare, OperationOutcome, Patient, Organization, Practitioner, CodeableConcept, } from '@medplum/fhirtypes'; import { createEpisodeOfCare, getEpisodeOfCareById, updateEpisodeOfCare, searchEpisodesOfCare, CreateEpisodeOfCareArgs, UpdateEpisodeOfCareArgs, EpisodeOfCareSearchArgs, EpisodeOfCareStatus, } from '../../src/tools/episodeOfCareUtils'; import { medplum } from '../../src/config/medplumClient'; import { randomUUID } from 'crypto'; // Global vars for test resources let testPatient: Patient; let testOrganization: Organization; let testPractitioner: Practitioner; const createdEpisodeOfCareIds: string[] = []; describe('EpisodeOfCare Utils Integration Tests', () => { beforeAll(async () => { // Create a Patient const patientToCreate: Patient = { resourceType: 'Patient', name: [{ given: ['TestEoCFirstName'], family: `TestEoCLastName-${randomUUID().slice(0,4)}` }], gender: 'male', birthDate: '1970-01-01', }; testPatient = await medplum.createResource(patientToCreate); console.log('Test Patient created for EoC tests:', testPatient.id); // Create an Organization const orgToCreate: Organization = { resourceType: 'Organization', name: `Test EoC Org ${randomUUID().slice(0,4)}`, active: true, }; testOrganization = await medplum.createResource(orgToCreate); console.log('Test Organization created for EoC tests:', testOrganization.id); // Create a Practitioner (Care Manager) const practToCreate: Practitioner = { resourceType: 'Practitioner', name: [{ given: ['TestEoCCareMgrFirst'], family: `TestEoCCareMgrLast-${randomUUID().slice(0,4)}` }], active: true, }; testPractitioner = await medplum.createResource(practToCreate); console.log('Test Practitioner created for EoC tests:', testPractitioner.id); expect(testPatient.id).toBeDefined(); expect(testOrganization.id).toBeDefined(); expect(testPractitioner.id).toBeDefined(); }); afterAll(async () => { console.log(`Cleaning up ${createdEpisodeOfCareIds.length} EpisodeOfCare(s)...`); for (const id of createdEpisodeOfCareIds) { try { await medplum.deleteResource('EpisodeOfCare', id); } catch (error) { console.error(`Error deleting EpisodeOfCare ${id}:`, error); } } // Clean up shared resources if (testPatient?.id) await medplum.deleteResource('Patient', testPatient.id); if (testOrganization?.id) await medplum.deleteResource('Organization', testOrganization.id); if (testPractitioner?.id) await medplum.deleteResource('Practitioner', testPractitioner.id); console.log('Finished cleaning up EoC test resources.'); }); describe('createEpisodeOfCare', () => { test('should create a new EpisodeOfCare with minimal valid data', async () => { const args: CreateEpisodeOfCareArgs = { patientId: testPatient.id as string, status: 'active', }; const result = await createEpisodeOfCare(args); console.log('Create EoC result (minimal):', JSON.stringify(result, null, 2)); expect(result).toBeDefined(); expect(result.resourceType).not.toBe('OperationOutcome'); const eoc = result as EpisodeOfCare; expect(eoc.id).toBeDefined(); if (eoc.id) createdEpisodeOfCareIds.push(eoc.id); expect(eoc.status).toBe('active'); expect(eoc.patient?.reference).toBe(`Patient/${testPatient.id}`); }); test('should create a new EpisodeOfCare with all fields', async () => { const identifierValue = `EOC-ID-${randomUUID().slice(0,8)}`; const args: CreateEpisodeOfCareArgs = { patientId: testPatient.id as string, status: 'planned', managingOrganizationId: testOrganization.id as string, careManagerId: testPractitioner.id as string, type: [ { coding: [ { system: 'http://terminology.hl7.org/CodeSystem/episodeofcare-type', code: 'hacc', display: 'Home and Community Care', }, ], text: 'HACC Type', }, ], periodStart: new Date().toISOString(), identifier: [{ system: 'urn:test:eoc-ids', value: identifierValue }], }; const result = await createEpisodeOfCare(args); console.log('Create EoC result (all fields):', JSON.stringify(result, null, 2)); expect(result).toBeDefined(); expect(result.resourceType).not.toBe('OperationOutcome'); const eoc = result as EpisodeOfCare; expect(eoc.id).toBeDefined(); if (eoc.id) createdEpisodeOfCareIds.push(eoc.id); expect(eoc.status).toBe('planned'); expect(eoc.patient?.reference).toBe(`Patient/${testPatient.id}`); expect(eoc.managingOrganization?.reference).toBe(`Organization/${testOrganization.id}`); expect(eoc.careManager?.reference).toBe(`Practitioner/${testPractitioner.id}`); expect(eoc.type?.[0]?.coding?.[0]?.code).toBe('hacc'); expect(eoc.period?.start).toBeDefined(); expect(eoc.identifier?.[0]?.value).toBe(identifierValue); }); test('should return OperationOutcome if patientId is missing', async () => { const args = { status: 'active', } as CreateEpisodeOfCareArgs; // patientId missing const result = await createEpisodeOfCare(args); expect(result.resourceType).toBe('OperationOutcome'); expect((result as OperationOutcome).issue?.[0]?.diagnostics).toContain('Patient ID is required'); }); test('should return OperationOutcome if status is missing', async () => { const args = { patientId: testPatient.id as string, } as CreateEpisodeOfCareArgs; // status missing const result = await createEpisodeOfCare(args); expect(result.resourceType).toBe('OperationOutcome'); expect((result as OperationOutcome).issue?.[0]?.diagnostics).toContain('Status is required'); }); }); describe('getEpisodeOfCareById', () => { let testEocForGet: EpisodeOfCare; beforeAll(async () => { const args: CreateEpisodeOfCareArgs = { patientId: testPatient.id as string, status: 'active', type: [{ text: 'Routine Checkup EoC for Get Test' }], }; const result = await createEpisodeOfCare(args); expect(result.resourceType).not.toBe('OperationOutcome'); testEocForGet = result as EpisodeOfCare; if (testEocForGet.id) createdEpisodeOfCareIds.push(testEocForGet.id); expect(testEocForGet.id).toBeDefined(); }); test('should retrieve an existing EpisodeOfCare by its ID', async () => { const result = await getEpisodeOfCareById(testEocForGet.id as string); console.log('Get EoC by ID result:', JSON.stringify(result, null, 2)); expect(result).toBeDefined(); expect(result).not.toBeNull(); expect(result!.resourceType).toBe('EpisodeOfCare'); const eoc = result as EpisodeOfCare; expect(eoc.id).toBe(testEocForGet.id); expect(eoc.type?.[0]?.text).toBe('Routine Checkup EoC for Get Test'); }); test('should return null for a non-existent EpisodeOfCare ID', async () => { const nonExistentId = randomUUID(); const result = await getEpisodeOfCareById(nonExistentId); console.log('Get EoC by non-existent ID result:', JSON.stringify(result, null, 2)); expect(result).toBeNull(); }); test('should return OperationOutcome for an empty ID string', async () => { const result = await getEpisodeOfCareById(''); console.log('Get EoC by empty ID result:', JSON.stringify(result, null, 2)); expect(result).toBeDefined(); expect(result!.resourceType).toBe('OperationOutcome'); const outcome = result as OperationOutcome; expect(outcome.issue?.[0]?.diagnostics).toContain('EpisodeOfCare ID is required'); }); }); describe('updateEpisodeOfCare', () => { let eocToUpdate: EpisodeOfCare; beforeEach(async () => { // Create a fresh EpisodeOfCare before each update test const args: CreateEpisodeOfCareArgs = { patientId: testPatient.id as string, status: 'planned', managingOrganizationId: testOrganization.id as string, type: [{ text: 'Initial Type for Update Test' }], periodStart: '2023-01-01T00:00:00Z', }; const result = await createEpisodeOfCare(args); expect(result.resourceType).not.toBe('OperationOutcome'); eocToUpdate = result as EpisodeOfCare; if (eocToUpdate.id) createdEpisodeOfCareIds.push(eocToUpdate.id); expect(eocToUpdate.id).toBeDefined(); }); test('should update status of an EpisodeOfCare', async () => { const updates: UpdateEpisodeOfCareArgs = { status: 'active' }; const result = await updateEpisodeOfCare(eocToUpdate.id as string, updates); console.log('Update EoC status result:', JSON.stringify(result, null, 2)); expect(result.resourceType).toBe('EpisodeOfCare'); const updatedEoc = result as EpisodeOfCare; expect(updatedEoc.status).toBe('active'); expect(updatedEoc.patient?.reference).toBe(`Patient/${testPatient.id}`); // Ensure other fields persist }); test('should update period, managingOrganization, and careManager', async () => { const newEndDate = '2023-12-31T23:59:59Z'; // Create a new org and practitioner for this specific update test to avoid interference const newOrg = await medplum.createResource<Organization>({ resourceType: 'Organization', name: 'New Update Org', active: true }); const newPract = await medplum.createResource<Practitioner>({ resourceType: 'Practitioner', name: [{ given: ['NewUpdateMgr']}], active: true }); expect(newOrg.id).toBeDefined(); expect(newPract.id).toBeDefined(); const updates: UpdateEpisodeOfCareArgs = { periodEnd: newEndDate, managingOrganizationId: newOrg.id as string, careManagerId: newPract.id as string, }; const result = await updateEpisodeOfCare(eocToUpdate.id as string, updates); expect(result.resourceType).toBe('EpisodeOfCare'); const updatedEoc = result as EpisodeOfCare; expect(updatedEoc.period?.end).toBe(newEndDate); expect(updatedEoc.managingOrganization?.reference).toBe(`Organization/${newOrg.id}`); expect(updatedEoc.careManager?.reference).toBe(`Practitioner/${newPract.id}`); // Cleanup new org and practitioner if(newOrg.id) await medplum.deleteResource('Organization', newOrg.id); if(newPract.id) await medplum.deleteResource('Practitioner', newPract.id); }); test('should update (replace) the type array', async () => { const newType: CodeableConcept[] = [ { coding: [{ system: 'http://example.com/new-types', code: 'NT001', display: 'New Type 1' }], text: 'New Type 1' }, ]; const updates: UpdateEpisodeOfCareArgs = { type: newType }; const result = await updateEpisodeOfCare(eocToUpdate.id as string, updates); expect(result.resourceType).toBe('EpisodeOfCare'); const updatedEoc = result as EpisodeOfCare; expect(updatedEoc.type).toEqual(newType); expect(updatedEoc.type?.length).toBe(1); expect(updatedEoc.type?.[0]?.coding?.[0]?.code).toBe('NT001'); }); test('should allow unsetting managingOrganization and careManager by providing null', async () => { const updates: UpdateEpisodeOfCareArgs = { managingOrganizationId: null as any, // Testing the explicit null path careManagerId: null as any, // Testing the explicit null path }; const result = await updateEpisodeOfCare(eocToUpdate.id as string, updates); expect(result.resourceType).toBe('EpisodeOfCare'); const updatedEoc = result as EpisodeOfCare; expect(updatedEoc.managingOrganization).toBeUndefined(); expect(updatedEoc.careManager).toBeUndefined(); }); test('should return OperationOutcome if no updates are provided', async () => { const result = await updateEpisodeOfCare(eocToUpdate.id as string, {}); expect(result.resourceType).toBe('OperationOutcome'); expect((result as OperationOutcome).issue?.[0]?.diagnostics).toContain('No updates provided'); }); test('should return OperationOutcome for updating a non-existent EpisodeOfCare ID', async () => { const nonExistentId = randomUUID(); const updates: UpdateEpisodeOfCareArgs = { status: 'finished' }; const result = await updateEpisodeOfCare(nonExistentId, updates); expect(result.resourceType).toBe('OperationOutcome'); expect((result as OperationOutcome).issue?.[0]?.code).toBe('not-found'); }); }); describe('searchEpisodesOfCare', () => { let searchTestPatientId: string; let searchTestOrgId: string; let searchTestPractitionerId: string; let eocSearch1: EpisodeOfCare, eocSearch2: EpisodeOfCare, eocSearch3: EpisodeOfCare; const searchEocIdentifierSystem = 'urn:test:search-eoc-ids'; const searchEocIdentifierValue1 = `SREOC1-${randomUUID().slice(0,6)}`; const searchEocIdentifierValue2 = `SREOC2-${randomUUID().slice(0,6)}`; beforeAll(async () => { // Assign IDs from global test resources created in the top-level beforeAll searchTestPatientId = testPatient.id as string; searchTestOrgId = testOrganization.id as string; searchTestPractitionerId = testPractitioner.id as string; expect(searchTestPatientId).toBeDefined(); expect(searchTestOrgId).toBeDefined(); expect(searchTestPractitionerId).toBeDefined(); // Create specific EoCs for searching const eoc1Args: CreateEpisodeOfCareArgs = { patientId: searchTestPatientId, status: 'active', type: [{ coding: [{ system: 'http://terminology.hl7.org/CodeSystem/episodeofcare-type', code: 'hacc' }], text: 'Home Care EoC'}], periodStart: '2023-01-10T10:00:00Z', periodEnd: '2023-03-10T10:00:00Z', managingOrganizationId: searchTestOrgId, identifier: [{ system: searchEocIdentifierSystem, value: searchEocIdentifierValue1 }] }; const eoc1Result = await createEpisodeOfCare(eoc1Args); expect(eoc1Result.resourceType).not.toBe('OperationOutcome'); eocSearch1 = eoc1Result as EpisodeOfCare; if (eocSearch1.id) createdEpisodeOfCareIds.push(eocSearch1.id); // Verify managingOrganization on created eocSearch1 const fetchedEoc1 = await medplum.readResource('EpisodeOfCare', eocSearch1.id!); console.log('Fetched eocSearch1 for verification:', JSON.stringify(fetchedEoc1, null, 2)); expect(fetchedEoc1.managingOrganization?.reference).toBe(`Organization/${searchTestOrgId}`); const eoc2Args: CreateEpisodeOfCareArgs = { patientId: searchTestPatientId, status: 'finished', type: [{ coding: [{ system: 'http://snomed.info/sct', code: '394802001' }], text: 'Long term care' }], periodStart: '2022-05-01T00:00:00Z', careManagerId: searchTestPractitionerId, identifier: [{ system: searchEocIdentifierSystem, value: searchEocIdentifierValue2 }] }; const eoc2Result = await createEpisodeOfCare(eoc2Args); expect(eoc2Result.resourceType).not.toBe('OperationOutcome'); eocSearch2 = eoc2Result as EpisodeOfCare; if (eocSearch2.id) createdEpisodeOfCareIds.push(eocSearch2.id); // Verify careManager on created eocSearch2 const fetchedEoc2 = await medplum.readResource('EpisodeOfCare', eocSearch2.id!); console.log('Fetched eocSearch2 for verification:', JSON.stringify(fetchedEoc2, null, 2)); expect(fetchedEoc2.careManager?.reference).toBe(`Practitioner/${searchTestPractitionerId}`); // Create another patient for a specific search test const otherPatient = await medplum.createResource<Patient>({ resourceType: 'Patient', name: [{given: ['OtherSearch'], family: 'Patient'}]}); const eoc3Args: CreateEpisodeOfCareArgs = { patientId: otherPatient.id as string, status: 'active', type: [{ coding: [{ system: 'http://terminology.hl7.org/CodeSystem/episodeofcare-type', code: 'pac' }], text: 'Post Acute Care' }], periodStart: '2023-02-15T00:00:00Z', }; eocSearch3 = (await createEpisodeOfCare(eoc3Args)) as EpisodeOfCare; if (eocSearch3.id) createdEpisodeOfCareIds.push(eocSearch3.id); // We need to clean up this otherPatient, but after all tests if it's not globally defined // For now, let's assume it gets cleaned up if its ID is pushed to a global cleanup array or handled in afterAll for search. // Better: clean up directly if not used elsewhere if (otherPatient.id) { // Add to a temporary list for cleanup in afterAll for this describe block, or clean up if not used by other tests in suite // For simplicity here, we'll delete it if not globally tracked. Assume it's not globally tracked. // This patient is only for this eocSearch3, so it can be deleted after `eocSearch3` is created if not needed for search tests themselves. // Actually, it IS needed if we search by this patient's ID. So it should be cleaned in afterAll of this describe, or globally. // Simplest for now: create and delete within beforeAll, if the EoC itself doesn't need to link to a *persistent during tests* patient. // The current test design uses global testPatient for most. This is an exception. // Let's adjust: create this other patient and add its ID to a list for cleanup in the main afterAll, or handle its cleanup more carefully. // For this example, since it's just used to create eocSearch3, and eocSearch3's reference will be by ID, // we can delete `otherPatient` after `eocSearch3` is created. The reference in `eocSearch3` will still hold the ID. await medplum.deleteResource('Patient', otherPatient.id as string); } expect(eocSearch1.id).toBeDefined(); expect(eocSearch2.id).toBeDefined(); expect(eocSearch3.id).toBeDefined(); }); test('should find EpisodesOfCare by patient ID', async () => { const args: EpisodeOfCareSearchArgs = { patient: searchTestPatientId }; const result = await searchEpisodesOfCare(args); expect(result).not.toHaveProperty('resourceType', 'OperationOutcome'); const eocs = result as EpisodeOfCare[]; expect(eocs.length).toBeGreaterThanOrEqual(2); expect(eocs.some(e => e.id === eocSearch1.id)).toBeTruthy(); expect(eocs.some(e => e.id === eocSearch2.id)).toBeTruthy(); }); test('should find EpisodesOfCare by status (single)', async () => { const args: EpisodeOfCareSearchArgs = { status: 'active' }; const result = await searchEpisodesOfCare(args); expect(result).not.toHaveProperty('resourceType', 'OperationOutcome'); const eocs = result as EpisodeOfCare[]; expect(eocs.some(e => e.id === eocSearch1.id)).toBeTruthy(); expect(eocs.some(e => e.id === eocSearch3.id)).toBeTruthy(); expect(eocs.every(e => e.status === 'active')).toBeTruthy(); }); test('should find EpisodesOfCare by status (comma-separated)', async () => { const args: EpisodeOfCareSearchArgs = { status: 'active,finished' }; const result = await searchEpisodesOfCare(args); expect(result).not.toHaveProperty('resourceType', 'OperationOutcome'); const eocs = result as EpisodeOfCare[]; expect(eocs.some(e => e.id === eocSearch1.id)).toBeTruthy(); // active expect(eocs.some(e => e.id === eocSearch2.id)).toBeTruthy(); // finished expect(eocs.some(e => e.id === eocSearch3.id)).toBeTruthy(); // active }); test('should find EpisodesOfCare by type (code only)', async () => { const args: EpisodeOfCareSearchArgs = { type: 'hacc' }; // from eocSearch1 const result = await searchEpisodesOfCare(args); expect(result).not.toHaveProperty('resourceType', 'OperationOutcome'); const eocs = result as EpisodeOfCare[]; expect(eocs.some(e => e.id === eocSearch1.id)).toBeTruthy(); }); test('should find EpisodesOfCare by type (system|code)', async () => { const args: EpisodeOfCareSearchArgs = { type: 'http://snomed.info/sct|394802001' }; // from eocSearch2 const result = await searchEpisodesOfCare(args); expect(result).not.toHaveProperty('resourceType', 'OperationOutcome'); const eocs = result as EpisodeOfCare[]; expect(eocs.some(e => e.id === eocSearch2.id)).toBeTruthy(); }); test('should find EpisodesOfCare by date (exact start date of one item)', async () => { const args: EpisodeOfCareSearchArgs = { date: '2023-01-10T10:00:00Z' }; // eocSearch1 const result = await searchEpisodesOfCare(args); expect(result).not.toHaveProperty('resourceType', 'OperationOutcome'); const eocs = result as EpisodeOfCare[]; expect(eocs.some(e => e.id === eocSearch1.id)).toBeTruthy(); }); test('should find EpisodesOfCare by date range (covers eocSearch1)', async () => { const args: EpisodeOfCareSearchArgs = { date: ['ge2023-01-01', 'le2023-02-01'] }; // eocSearch1 is within this const result = await searchEpisodesOfCare(args); expect(result).not.toHaveProperty('resourceType', 'OperationOutcome'); const eocs = result as EpisodeOfCare[]; expect(eocs.some(e => e.id === eocSearch1.id)).toBeTruthy(); }); test('should find EpisodeOfCare by identifier (value only)', async () => { const args: EpisodeOfCareSearchArgs = { identifier: searchEocIdentifierValue1 }; const result = await searchEpisodesOfCare(args); expect(result).not.toHaveProperty('resourceType', 'OperationOutcome'); const eocs = result as EpisodeOfCare[]; expect(eocs.length).toBeGreaterThanOrEqual(1); expect(eocs.some(e => e.id === eocSearch1.id)).toBeTruthy(); }); test('should find EpisodeOfCare by identifier (system|value)', async () => { const args: EpisodeOfCareSearchArgs = { identifier: `${searchEocIdentifierSystem}|${searchEocIdentifierValue2}` }; const result = await searchEpisodesOfCare(args); expect(result).not.toHaveProperty('resourceType', 'OperationOutcome'); const eocs = result as EpisodeOfCare[]; expect(eocs.length).toBeGreaterThanOrEqual(1); expect(eocs.some(e => e.id === eocSearch2.id)).toBeTruthy(); }); test('should find EpisodesOfCare by managing-organization', async () => { const targetEoc = await medplum.readResource('EpisodeOfCare', eocSearch1.id!); console.log(`Verifying eocSearch1 (${eocSearch1.id}) for org search. ManagingOrg: ${targetEoc.managingOrganization?.reference}`); const args: EpisodeOfCareSearchArgs = { organization: `Organization/${searchTestOrgId}` }; const result = await searchEpisodesOfCare(args); expect(result).not.toHaveProperty('resourceType', 'OperationOutcome'); const eocs = result as EpisodeOfCare[]; expect(eocs.some(e => e.id === eocSearch1.id)).toBeTruthy(); expect(eocs.every(e => e.managingOrganization?.reference === `Organization/${searchTestOrgId}`)).toBeTruthy(); }); test('should find EpisodesOfCare by care-manager', async () => { const targetEoc = await medplum.readResource('EpisodeOfCare', eocSearch2.id!); console.log(`Verifying eocSearch2 (${eocSearch2.id}) for care-manager search. CareManager: ${targetEoc.careManager?.reference}`); const args: EpisodeOfCareSearchArgs = { 'care-manager': `Practitioner/${searchTestPractitionerId}` }; const result = await searchEpisodesOfCare(args); expect(result).not.toHaveProperty('resourceType', 'OperationOutcome'); const eocs = result as EpisodeOfCare[]; expect(eocs.some(e => e.id === eocSearch2.id)).toBeTruthy(); expect(eocs.every(e => e.careManager?.reference === `Practitioner/${searchTestPractitionerId}`)).toBeTruthy(); }); test('should find EpisodeOfCare with combined criteria (patient and status)', async () => { const args: EpisodeOfCareSearchArgs = { patient: searchTestPatientId, status: 'active' }; const result = await searchEpisodesOfCare(args); expect(result).not.toHaveProperty('resourceType', 'OperationOutcome'); const eocs = result as EpisodeOfCare[]; expect(eocs.some(e => e.id === eocSearch1.id)).toBeTruthy(); expect(eocs.every(e => e.patient?.reference === `Patient/${searchTestPatientId}` && e.status === 'active')).toBeTruthy(); }); test('should return an empty array for criteria that match no EpisodesOfCare', async () => { const args: EpisodeOfCareSearchArgs = { status: 'waitlist', type: 'nonexistent-type' }; const result = await searchEpisodesOfCare(args); expect(result).not.toHaveProperty('resourceType', 'OperationOutcome'); expect(Array.isArray(result)).toBe(true); expect((result as EpisodeOfCare[]).length).toBe(0); }); test('should return OperationOutcome if no search criteria are provided', async () => { const result = await searchEpisodesOfCare({}); expect(result).toHaveProperty('resourceType', 'OperationOutcome'); const outcome = result as OperationOutcome; expect(outcome.issue?.[0]?.diagnostics).toContain('At least one search criterion must be provided'); }); }); });

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/rkirkendall/medplum-mcp'

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