import { medplum, ensureAuthenticated } from '../../src/config/medplumClient';
import {
createMedicationRequest,
CreateMedicationRequestArgs,
getMedicationRequestById,
updateMedicationRequest,
UpdateMedicationRequestArgs,
searchMedicationRequests,
SearchMedicationRequestsArgs
} from '../../src/tools/medicationRequestUtils';
import { createPatient, CreatePatientArgs } from '../../src/tools/patientUtils';
import { createPractitioner, CreatePractitionerArgs } from '../../src/tools/practitionerUtils';
import { createEncounter, CreateEncounterArgs } from '../../src/tools/encounterUtils';
import { Patient, Practitioner, Encounter, MedicationRequest, CodeableConcept, Dosage } from '@medplum/fhirtypes';
import { randomUUID } from 'crypto';
const rxnormSystem = 'http://www.nlm.nih.gov/research/umls/rxnorm';
describe('MedicationRequest Tool Integration Tests', () => {
let testPatient: Patient | null;
let testPractitioner: Practitioner | null;
let testEncounter: Encounter | null;
beforeAll(async () => {
await ensureAuthenticated();
const patientArgs: CreatePatientArgs = {
firstName: 'MedReqTest',
lastName: `Patient-${randomUUID().substring(0, 8)}`,
birthDate: '1975-05-25',
gender: 'female',
};
testPatient = await createPatient(patientArgs);
if (!testPatient || !testPatient.id) throw new Error('Test patient creation failed for MedicationRequest tests');
console.log(`Created test patient for MedicationRequest tests: ${testPatient.id}`);
const practitionerArgs: CreatePractitionerArgs = {
givenName: 'MedReqTest',
familyName: `Doctor-${randomUUID().substring(0, 8)}`,
};
testPractitioner = await createPractitioner(practitionerArgs);
if (!testPractitioner || !testPractitioner.id) throw new Error('Test practitioner creation failed for MedicationRequest tests');
console.log(`Created test practitioner for MedicationRequest tests: ${testPractitioner.id}`);
const encounterArgs: CreateEncounterArgs = {
status: 'finished',
classCode: 'AMB',
patientId: testPatient.id,
practitionerIds: [testPractitioner.id],
identifierValue: `MEDREQ-ENC-${randomUUID().substring(0,8)}`
};
testEncounter = await createEncounter(encounterArgs);
if (!testEncounter || !testEncounter.id) throw new Error('Test encounter creation failed for MedicationRequest tests');
console.log(`Created test encounter for MedicationRequest tests: ${testEncounter.id}`);
});
describe('createMedicationRequest', () => {
it('should create a new medication request successfully', async () => {
if (!testPatient?.id || !testEncounter?.id || !testPractitioner?.id) {
throw new Error('Test entities not initialized for createMedicationRequest test');
}
const medReqArgs: CreateMedicationRequestArgs = {
status: 'active',
intent: 'order',
medicationCodeableConcept: {
coding: [{
system: rxnormSystem,
code: '834060', // Lisinopril 10 MG Oral Tablet
display: 'Lisinopril 10 MG Oral Tablet'
}],
text: 'Lisinopril 10 MG Oral Tablet'
},
subjectId: testPatient.id,
encounterId: testEncounter.id,
authoredOn: new Date().toISOString(),
requesterId: testPractitioner.id,
dosageInstruction: [{
text: 'Take one tablet by mouth daily'
}],
identifier: { value: `medreq-create-${randomUUID().substring(0,8)}`, system: 'urn:custom:medreq'}
};
const newMedReq = await createMedicationRequest(medReqArgs);
expect(newMedReq).toBeDefined();
expect(newMedReq.resourceType).toBe('MedicationRequest');
expect(newMedReq.id).toBeDefined();
expect(newMedReq.status).toBe('active');
expect(newMedReq.intent).toBe('order');
expect(newMedReq.medicationCodeableConcept?.coding?.[0]?.code).toBe('834060');
expect(newMedReq.subject?.reference).toBe(`Patient/${testPatient.id}`);
expect(newMedReq.encounter?.reference).toBe(`Encounter/${testEncounter.id}`);
expect(newMedReq.requester?.reference).toBe(`Practitioner/${testPractitioner.id}`);
expect(newMedReq.dosageInstruction?.[0]?.text).toBe('Take one tablet by mouth daily');
expect(newMedReq.identifier?.[0]?.value).toBe(medReqArgs.identifier?.value);
console.log(`Created MedicationRequest (Lisinopril): ${newMedReq.id}`);
});
it('should throw an error if status is missing', async () => {
const medReqArgs = {
intent: 'order',
medicationCodeableConcept: { text: 'Aspirin' },
subjectId: testPatient!.id!
} as unknown as CreateMedicationRequestArgs;
await expect(createMedicationRequest(medReqArgs)).rejects.toThrow('MedicationRequest status is required.');
});
it('should throw an error if intent is missing', async () => {
const medReqArgs = {
status: 'active',
medicationCodeableConcept: { text: 'Aspirin' },
subjectId: testPatient!.id!
} as unknown as CreateMedicationRequestArgs;
await expect(createMedicationRequest(medReqArgs)).rejects.toThrow('MedicationRequest intent is required.');
});
it('should throw an error if medication is missing', async () => {
const medReqArgs = {
status: 'active',
intent: 'order',
subjectId: testPatient!.id!
} as unknown as CreateMedicationRequestArgs;
await expect(createMedicationRequest(medReqArgs)).rejects.toThrow('Medication (medicationCodeableConcept or medicationReference) is required.');
});
it('should throw an error if subjectId is missing', async () => {
const medReqArgs = {
status: 'active',
intent: 'order',
medicationCodeableConcept: { text: 'Aspirin' }
} as unknown as CreateMedicationRequestArgs;
await expect(createMedicationRequest(medReqArgs)).rejects.toThrow('Subject (Patient ID) is required to create a MedicationRequest.');
});
});
describe('getMedicationRequestById', () => {
let createdMedReq: MedicationRequest | null;
beforeAll(async () => {
if (!testPatient?.id) throw new Error('Test patient not initialized for getMedicationRequestById tests');
const medReqArgs: CreateMedicationRequestArgs = {
status: 'active',
intent: 'proposal',
medicationCodeableConcept: { text: 'Amoxicillin 250mg tablet' },
subjectId: testPatient.id,
identifier: { value: `medreq-get-${randomUUID().substring(0,8)}`}
};
createdMedReq = await createMedicationRequest(medReqArgs);
if (!createdMedReq || !createdMedReq.id) throw new Error('Test MedicationRequest creation failed for get tests');
console.log(`Created test MedicationRequest for getMedicationRequestById tests: ${createdMedReq.id}`);
});
it('should retrieve an existing medication request successfully', async () => {
expect(createdMedReq?.id).toBeDefined();
const fetchedMedReq = await getMedicationRequestById({ medicationRequestId: createdMedReq!.id! });
expect(fetchedMedReq).toBeDefined();
expect(fetchedMedReq!.id).toBe(createdMedReq!.id);
expect(fetchedMedReq!.status).toBe('active');
expect(fetchedMedReq!.medicationCodeableConcept?.text).toBe('Amoxicillin 250mg tablet');
});
it('should return null for a non-existent medication request ID', async () => {
const nonExistentId = randomUUID();
const fetchedMedReq = await getMedicationRequestById({ medicationRequestId: nonExistentId });
expect(fetchedMedReq).toBeNull();
});
it('should throw an error if medication request ID is not provided', async () => {
// @ts-ignore: Testing invalid input
await expect(getMedicationRequestById({})).rejects.toThrow('MedicationRequest ID is required');
await expect(getMedicationRequestById({ medicationRequestId: '' })).rejects.toThrow('MedicationRequest ID is required');
});
});
describe('updateMedicationRequest', () => {
let medReqToUpdate: MedicationRequest | null;
const initialStatus = 'draft';
const updatedStatus = 'active';
beforeEach(async () => {
if (!testPatient?.id) throw new Error('Test patient not initialized for updateMedicationRequest tests');
const medReqArgs: CreateMedicationRequestArgs = {
status: initialStatus,
intent: 'plan',
medicationCodeableConcept: {
coding: [{ system: rxnormSystem, code: '313782', display: 'Acetaminophen 325 MG Oral Tablet' }],
text: 'Acetaminophen 325 MG Oral Tablet'
},
subjectId: testPatient.id,
identifier: { value: `medreq-update-${randomUUID().substring(0,8)}`}
};
medReqToUpdate = await createMedicationRequest(medReqArgs);
if (!medReqToUpdate || !medReqToUpdate.id) throw new Error('Test MedicationRequest creation failed for update tests');
console.log(`Created MedicationRequest for update test: ${medReqToUpdate.id}, status: ${medReqToUpdate.status}`);
});
it('should update an existing medication request successfully', async () => {
expect(medReqToUpdate?.id).toBeDefined();
const newDosageInstruction: Dosage[] = [{
sequence: 1,
text: 'Take 1 tablet every 4-6 hours as needed for pain',
timing: { repeat: { frequency: 1, period: 6, periodUnit: 'h' } },
route: { coding: [{ system: 'http://snomed.info/sct', code: '26643006', display: 'Oral route' }] }
}];
const updates: UpdateMedicationRequestArgs = {
status: updatedStatus,
intent: 'order', // Change intent
authoredOn: new Date().toISOString(),
dosageInstruction: newDosageInstruction,
note: 'Patient advised to not exceed 4g/day.'
};
const updatedMedReq = await updateMedicationRequest(medReqToUpdate!.id!, updates);
expect(updatedMedReq).toBeDefined();
expect(updatedMedReq.id).toBe(medReqToUpdate!.id);
expect(updatedMedReq.status).toBe(updatedStatus);
expect(updatedMedReq.intent).toBe('order');
expect(updatedMedReq.authoredOn).toBeDefined();
expect(updatedMedReq.dosageInstruction?.[0]?.text).toBe(newDosageInstruction[0].text);
expect(updatedMedReq.note?.[0]?.text).toBe('Patient advised to not exceed 4g/day.');
console.log(`Updated MedicationRequest: ${updatedMedReq.id}, new status: ${updatedMedReq.status}`);
});
it('should clear optional fields when null is provided', async () => {
expect(medReqToUpdate?.id).toBeDefined();
const initialEncounterId = testEncounter!.id!;
// First, add an encounter to it
let tempUpdatedMedReq = await updateMedicationRequest(medReqToUpdate!.id!, { encounterId: initialEncounterId, note: 'Initial note' });
expect(tempUpdatedMedReq.encounter?.reference).toBe(`Encounter/${initialEncounterId}`);
expect(tempUpdatedMedReq.note).toBeDefined();
const updatesToClear: UpdateMedicationRequestArgs = {
encounterId: null,
note: null,
dosageInstruction: null
};
const clearedMedReq = await updateMedicationRequest(medReqToUpdate!.id!, updatesToClear);
expect(clearedMedReq.encounter).toBeUndefined();
expect(clearedMedReq.note).toBeUndefined();
expect(clearedMedReq.dosageInstruction).toBeUndefined();
});
it('should throw an error if medication request ID is not provided', async () => {
await expect(updateMedicationRequest('', { status: 'active' })).rejects.toThrow('MedicationRequest ID is required');
});
it('should throw an error if updates object is empty', async () => {
expect(medReqToUpdate?.id).toBeDefined();
// @ts-ignore: Testing invalid input
await expect(updateMedicationRequest(medReqToUpdate!.id!, {})).rejects.toThrow('Updates object cannot be empty');
});
});
describe('searchMedicationRequests', () => {
let medReq1: MedicationRequest | null, medReq2: MedicationRequest | null, medReq3_other_patient: MedicationRequest | null;
const medCode1 = '197360'; // Atorvastatin 10mg tablet (Lipitor)
const medText1 = 'Atorvastatin 10mg tablet';
const medCode2 = '866924'; // Metformin 500 MG Extended Release Oral Tablet
const medText2 = 'Metformin 500 MG ER Oral Tablet';
let searchPatientId: string;
let searchPractitionerId: string;
let otherPatientId: string;
let medReq1Identifier: string;
beforeAll(async () => {
if (!testPatient?.id || !testPractitioner?.id) {
throw new Error('Test entities not initialized for searchMedicationRequests tests');
}
searchPatientId = testPatient.id;
searchPractitionerId = testPractitioner.id;
medReq1Identifier = `medreq-search-${randomUUID().substring(0,8)}`;
// Create another patient for exclusivity tests
const otherPatientArgs: CreatePatientArgs = { firstName: 'OtherMed', lastName: 'Patient', birthDate: '1990-01-01', gender: 'other' };
const tempOtherPatient = await createPatient(otherPatientArgs);
if (!tempOtherPatient || !tempOtherPatient.id) throw new Error('Creation of other patient failed for search tests');
otherPatientId = tempOtherPatient.id;
medReq1 = await createMedicationRequest({
status: 'active',
intent: 'order',
medicationCodeableConcept: { coding: [{ system: rxnormSystem, code: medCode1, display: medText1 }], text: medText1 },
subjectId: searchPatientId,
requesterId: searchPractitionerId,
authoredOn: '2023-01-15T10:00:00Z',
identifier: {value: medReq1Identifier, system: 'test-medreq-system'}
});
medReq2 = await createMedicationRequest({
status: 'completed',
intent: 'order',
medicationCodeableConcept: { coding: [{ system: rxnormSystem, code: medCode2, display: medText2 }], text: medText2 },
subjectId: searchPatientId,
requesterId: searchPractitionerId, // Same requester
authoredOn: '2023-02-20T11:00:00Z'
});
medReq3_other_patient = await createMedicationRequest({
status: 'active',
intent: 'order',
medicationCodeableConcept: { coding: [{ system: rxnormSystem, code: medCode1, display: medText1 }], text: medText1 }, // Same med as medReq1
subjectId: otherPatientId, // Different patient
requesterId: searchPractitionerId,
authoredOn: '2023-03-10T09:00:00Z'
});
if (!medReq1 || !medReq2 || !medReq3_other_patient) throw new Error('Failed to create medication requests for search tests');
console.log(`Created medication requests for search tests: ${medReq1.id}, ${medReq2.id}, ${medReq3_other_patient.id}`);
});
it('should find medication requests by patient ID', async () => {
const results = await searchMedicationRequests({ patientId: searchPatientId });
expect(results.length).toBeGreaterThanOrEqual(2);
const resultIds = results.map(r => r.id);
expect(resultIds).toContain(medReq1!.id);
expect(resultIds).toContain(medReq2!.id);
expect(resultIds).not.toContain(medReq3_other_patient!.id);
});
it('should find medication requests by status for a specific patient', async () => {
const resultsActive = await searchMedicationRequests({ patientId: searchPatientId, status: 'active' });
expect(resultsActive.some(r => r.id === medReq1!.id)).toBe(true);
expect(resultsActive.every(r => r.status === 'active' && r.subject?.reference === `Patient/${searchPatientId}`)).toBe(true);
const resultsCompleted = await searchMedicationRequests({ patientId: searchPatientId, status: 'completed' });
expect(resultsCompleted.some(r => r.id === medReq2!.id)).toBe(true);
expect(resultsCompleted.every(r => r.status === 'completed' && r.subject?.reference === `Patient/${searchPatientId}`)).toBe(true);
});
it('should find medication requests by intent', async () => {
const results = await searchMedicationRequests({ patientId: searchPatientId, intent: 'order' });
expect(results.length).toBeGreaterThanOrEqual(2); // Both medReq1 and medReq2 are 'order'
expect(results.every(r => r.intent === 'order')).toBe(true);
});
it('should find medication requests by medication code', async () => {
// Allow time for search indexing
await new Promise(resolve => setTimeout(resolve, 200));
// Test that code search works
const resultsByCodeOnly = await searchMedicationRequests({ code: medCode1 });
expect(resultsByCodeOnly.length).toBeGreaterThan(0);
expect(resultsByCodeOnly.every(r => r.medicationCodeableConcept?.coding?.some(c => c.code === medCode1))).toBe(true);
});
it('should find medication requests by authoredOn date', async () => {
// Using a range for the whole day to make the test more robust
const resultsRange = await searchMedicationRequests({ patientId: searchPatientId, authoredon: 'ge2023-01-15T00:00:00Z&authoredon=le2023-01-15T23:59:59Z' });
expect(resultsRange.some(r => r.id === medReq1!.id)).toBe(true);
});
it('should find medication requests by requester', async () => {
const results = await searchMedicationRequests({ requester: `Practitioner/${searchPractitionerId}` });
expect(results.length).toBeGreaterThanOrEqual(3); // All 3 created MRs have this requester
const resultIds = results.map(r => r.id);
expect(resultIds).toContain(medReq1!.id);
expect(resultIds).toContain(medReq2!.id);
expect(resultIds).toContain(medReq3_other_patient!.id);
});
it('should find medication request by identifier', async () => {
const results = await searchMedicationRequests({ identifier: `${medReq1!.identifier![0].system}|${medReq1Identifier}` });
expect(results.length).toBeGreaterThanOrEqual(1);
expect(results.some(o => o.id === medReq1!.id)).toBe(true);
const resultsShort = await searchMedicationRequests({ identifier: medReq1Identifier });
expect(resultsShort.length).toBeGreaterThanOrEqual(1);
expect(resultsShort.some(o => o.id === medReq1!.id)).toBe(true);
});
it('should return an empty array for criteria that match no medication requests', async () => {
const results = await searchMedicationRequests({ patientId: searchPatientId, code: 'NON-EXISTENT-CODE' });
expect(results.length).toBe(0);
});
it('should warn and return results or empty for search with no criteria', async () => {
const consoleWarnSpy = jest.spyOn(console, 'warn');
const results = await searchMedicationRequests({});
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('MedicationRequest search called with no specific criteria'));
expect(results).toBeInstanceOf(Array);
consoleWarnSpy.mockRestore();
});
});
});