Skip to main content
Glama
handle-prescription-event.test.ts17.7 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { indexSearchParameterBundle, indexStructureDefinitionBundle } from '@medplum/core'; import { readJson, SEARCH_PARAMETER_BUNDLE_FILES } from '@medplum/definitions'; import type { Bot, Bundle, MedicationRequest, Practitioner, Reference, SearchParameter } from '@medplum/fhirtypes'; import { MockClient } from '@medplum/mock'; import { vi } from 'vitest'; import type { PhotonEvent, PhotonWebhook, PrescriptionCreatedEvent, PrescriptionData, PrescriptionDepletedEvent, } from '../photon-types'; import { handleCreatePrescription, handler, handleUpdatePrescription } from './handle-prescription-event'; import { getExistingMedicationRequest } from './utils'; describe('Prescription webhooks', async () => { beforeAll(() => { indexStructureDefinitionBundle(readJson('fhir/r4/profiles-types.json') as Bundle); indexStructureDefinitionBundle(readJson('fhir/r4/profiles-resources.json') as Bundle); for (const filename of SEARCH_PARAMETER_BUNDLE_FILES) { indexSearchParameterBundle(readJson(filename) as Bundle<SearchParameter>); } }); vi.mock('./utils.ts', async () => { const actualModule = await vi.importActual('./utils.ts'); return { ...actualModule, verifyEvent: vi.fn().mockImplementation(() => true), handlePhotonAuth: vi.fn().mockImplementation(() => 'example-auth-token'), }; }); const bot: Reference<Bot> = { reference: 'Bot/123' }; const contentType = 'application/json'; const secrets = { PHOTON_CLIENT_ID: { name: 'Photon Client ID', valueString: 'client-id' }, PHOTON_CLIENT_SECRET: { name: 'Photon Client Secret', valueString: 'client-secret', }, }; test.skip('Handle a prescription created event', async () => { const medplum = new MockClient(); await medplum.createResource({ resourceType: 'Practitioner', identifier: [{ system: 'https://neutron.health', value: 'usr_01J21EPR81W9XRYTY69RQY3R9J' }], name: [{ given: ['Alice'], family: 'Smith' }], }); await medplum.createResource({ resourceType: 'Medication', identifier: [{ system: 'https://neutron.health', value: 'med_01GGT9ZK1327R6SGZDJADSSNKN' }], code: { coding: [ { system: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '203133', display: 'penicillin G potassium' }, ], }, }); const prescription = await handler(medplum, { bot, contentType, input: createdEvent, secrets, }); expect(prescription).toBeDefined(); expect(prescription.status).toBe('draft'); }, 10000); test.skip('Handle a prescription created event and create a new medication', async () => { const medplum = new MockClient(); await medplum.createResource({ resourceType: 'Practitioner', identifier: [{ system: 'https://neutron.health', value: 'usr_01J21EPR81W9XRYTY69RQY3R9J' }], name: [{ given: ['Alice'], family: 'Smith' }], }); const prescription = await handler(medplum, { bot, contentType, input: createdEvent, secrets, }); expect(prescription).toBeDefined(); const createdMedication = await medplum.searchOne('MedicationKnowledge', { code: '4053', }); expect(createdMedication).toBeDefined(); }); test.skip('Handle prescription depleted event', async () => { const medplum = new MockClient(); const existingRequest: MedicationRequest = await medplum.createResource({ resourceType: 'MedicationRequest', status: 'active', intent: 'order', subject: { reference: 'Patient/123' }, }); const prescriptionDepletedEvent: PhotonEvent = { ...depletedEvent, }; prescriptionDepletedEvent.data.externalId = existingRequest.id as string; const updatedPrescription = await handler(medplum, { bot, contentType, input: prescriptionDepletedEvent, secrets, }); expect(updatedPrescription.status).toBe('completed'); expect(updatedPrescription.identifier?.find((id) => id.system === 'https://neutron.health/webhooks')?.value).toBe( '01J5RAYDBXKQR472N1FT9K5ER8' ); }); test.skip('Handle prescription expired event', async () => { const medplum = new MockClient(); const existingRequest: MedicationRequest = await medplum.createResource({ resourceType: 'MedicationRequest', status: 'active', intent: 'order', subject: { reference: 'Patient/123' }, }); const prescriptionExpiredEvent: PhotonEvent = { ...expiredEvent, }; prescriptionExpiredEvent.data.externalId = existingRequest.id as string; const updatedPrescription = await handler(medplum, { bot, contentType, input: prescriptionExpiredEvent, secrets, }); expect(updatedPrescription.status).toBe('stopped'); }); test.skip('Receive an invalid prescription type', async () => { const medplum = new MockClient(); const existingPrescription: MedicationRequest = await medplum.createResource({ resourceType: 'MedicationRequest', identifier: [{ system: 'https://neutron.health', value: 'rx_01G8C1TNF8TZ5N9DAJN66H9KSH' }], status: 'active', intent: 'order', subject: { reference: 'Patient/123' }, }); const invalidPhotonEvent: PhotonEvent = { ...invalidEvent }; invalidEvent.type = 'photon:prescription:canceled' as PhotonEvent['type']; invalidEvent.data.externalId = existingPrescription.id; await expect(() => handler(medplum, { bot, contentType, secrets, input: invalidPhotonEvent })).rejects.toThrow( 'Invalid prescription type' ); }); test.skip('Check for existing prescription', async () => { const medplum = new MockClient(); await medplum.createResource({ resourceType: 'MedicationRequest', status: 'active', intent: 'order', subject: { reference: 'Patient/123' }, identifier: [{ system: 'https://neutron.health', value: 'example-id' }], }); const prescription = await medplum.searchOne('MedicationRequest', { identifier: 'https://neutron.health|example-id', }); const prescriptionData: PrescriptionData = { id: 'example-id', externalId: prescription?.id ?? '', patient: { id: 'example', externalId: 'example', }, }; const existingRequest = await getExistingMedicationRequest(prescriptionData, medplum); expect(existingRequest).toBeDefined(); expect(existingRequest?.id).toBe(prescription?.id); }); test.skip('Updating a prescription that does not exist', async () => { const medplum = new MockClient(); await expect(() => handler(medplum, { input: prescriptionDepletedEvent, secrets, bot, contentType, }) ).rejects.toThrow('Prescription does not exist'); }); test.skip('Receive a non-prescription event', async () => { const medplum = new MockClient(); await expect(() => handler(medplum, { input: orderEvent, secrets, bot, contentType })).rejects.toThrow( 'Not a prescription event' ); }); test.skip('Idempotency test', async () => { const medplum = new MockClient(); const prescriber: Practitioner = await medplum.createResource({ resourceType: 'Practitioner', identifier: [{ system: 'https://neutron.health', value: 'usr_wUofzqEvcA2JCwJ4' }], }); await medplum.createResource({ resourceType: 'Medication', identifier: [{ system: 'https://neutron.health', value: 'med_01G7T2NB6' }], code: { coding: [{ system: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '723', display: 'Amoxicillin' }], }, }); const idempotencyTestEvent: PhotonEvent = { ...idempotencyEvent, }; idempotencyTestEvent.data.patient.externalId = prescriber.id; await handler(medplum, { bot: { reference: 'Bot/123' }, input: idempotencyTestEvent, contentType: 'application/json', secrets, }); const updateResourceSpy = vi.spyOn(medplum, 'updateResource'); const createResourceSpy = vi.spyOn(medplum, 'createResource'); const patchResourceSpy = vi.spyOn(medplum, 'patchResource'); await handler(medplum, { bot: { reference: 'Bot/123' }, input: idempotencyTestEvent, contentType: 'application/json', secrets, }); expect(updateResourceSpy).not.toHaveBeenCalled(); expect(createResourceSpy).not.toHaveBeenCalled(); expect(patchResourceSpy).not.toHaveBeenCalled(); }); test.skip('Updating a prescription that does not exist', async () => { const medplum = new MockClient(); await expect(() => handler(medplum, { input: prescriptionDepletedEvent, secrets: {}, bot, contentType, }) ).rejects.toThrow('Prescription does not exist'); }); test.skip('Receive a non-prescription event', async () => { const medplum = new MockClient(); await expect(() => handler(medplum, { input: orderEvent, secrets, bot, contentType })).rejects.toThrow( 'Not a prescription event' ); }); test.skip('Idempotency test', async () => { const medplum = new MockClient(); const prescriber: Practitioner = await medplum.createResource({ resourceType: 'Practitioner', identifier: [{ system: 'https://neutron.health', value: 'usr_wUofzqEvcA2JCwJ4' }], }); await medplum.createResource({ resourceType: 'Medication', identifier: [{ system: 'https://neutron.health', value: 'med_01G7T2NB6' }], code: { coding: [{ system: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '723', display: 'Amoxicillin' }], }, }); const idempotencyTestEvent: PhotonEvent = { ...idempotencyEvent, }; idempotencyTestEvent.data.patient.externalId = prescriber.id; await handler(medplum, { bot: { reference: 'Bot/123' }, input: idempotencyTestEvent, contentType: 'application/json', secrets, }); const updateResourceSpy = vi.spyOn(medplum, 'updateResource'); const createResourceSpy = vi.spyOn(medplum, 'createResource'); const patchResourceSpy = vi.spyOn(medplum, 'patchResource'); await handler(medplum, { bot: { reference: 'Bot/123' }, input: idempotencyTestEvent, contentType: 'application/json', secrets, }); expect(updateResourceSpy).not.toHaveBeenCalled(); expect(createResourceSpy).not.toHaveBeenCalled(); expect(patchResourceSpy).not.toHaveBeenCalled(); }); test.skip('Create prescription', async () => { const medplum = new MockClient(); await medplum.createResource({ resourceType: 'Practitioner', identifier: [{ system: 'https://neutron.health', value: 'example-prescriber' }], }); await medplum.createResource({ resourceType: 'Medication', identifier: [{ system: 'https://neutron.health', value: 'example-med' }], code: { coding: [ { system: 'http://www.nlm.nih.gov/research/umls/rxnorm', value: '197365', display: 'amoxapine 25 MG Oral Tablet', }, ], }, }); const event: PrescriptionCreatedEvent = { id: '01G8C1TNGH2F03021F23C95261', type: 'photon:prescription:created', specversion: '1.0', datacontenttype: 'application/json', time: '2022-01-01T01:00:00.000Z', subject: 'rx_01G8C1TNF8TZ5N9DAJN66H9KSH', source: 'org:org_KzSVZBQixLRkqj5d', data: { id: 'rx_01G8C1TNF8TZ5N9DAJN66H9KSH', externalId: '1234', dispenseQuantity: 30, dispenseAsWritten: true, dispenseUnit: 'EA', refillsAllowed: 12, daysSupply: 30, instructions: 'Take once daily', notes: 'Very good', effectiveDate: '2022-01-01', expirationDate: '2023-01-01', prescriberId: 'example-prescriber', treatmentId: 'example-med', patient: { id: 'pat_ieUv67viS0lG18JN', externalId: '1234', }, }, }; const medicationRequest = await handleCreatePrescription(event, medplum, 'auth-token'); expect(medicationRequest).toBeDefined(); }); test.skip('Update Prescription', async () => { const medplum = new MockClient(); const existingPrescription = (await medplum.createResource({ resourceType: 'MedicationRequest', status: 'active', intent: 'order', subject: { reference: 'Patient/123' }, })) as MedicationRequest; const depletedEvent: PrescriptionDepletedEvent = { id: '01G8AHJBT081QQWM89X3SVV31F', type: 'photon:prescription:depleted', specversion: '1.0', datacontenttype: 'application/json', time: '2022-01-01T01:00:00.000Z', subject: 'rx_01G8AGBC91W1042CDRB19545EC', source: 'org:org_KzSVZBQixLRkqj5d', data: { id: 'rx_01G8AGBC91W1042CDRB19545EC', externalId: existingPrescription.id as string, patient: { id: 'pat_ieUv67viS0lG18JN', externalId: '1234', }, }, }; const result = await handleUpdatePrescription(depletedEvent, medplum, existingPrescription); expect(result).toBeDefined(); expect(result.status).toBe('completed'); }); }); const prescriptionDepletedEvent: PhotonEvent = { id: '01G8AHJBT081QQWM89X3SVV31F', type: 'photon:prescription:depleted', specversion: '1.0', datacontenttype: 'application/json', time: '2022-01-01T01:00:00.000Z', subject: 'rx_01G8AGBC91W1042CDRB19545EC', source: 'org:org_KzSVZBQixLRkqj5d', data: { id: 'rx_01G8AGBC91W1042CDRB19545EC', externalId: '1234', patient: { id: 'pat_ieUv67viS0lG18JN', externalId: '1234', }, }, }; const orderEvent: PhotonEvent = { id: '01G7Z7TNFH0YEGVZ719TQZQBER', type: 'photon:order:placed', specversion: '1.0', datacontenttype: 'application/json', time: '2022-01-01T01:00:00.000Z', subject: 'ord_01G8AHAFDJ7FV2Y77FVWA19009', source: 'org:org_KzSVZBQixLRkqj5d', data: { id: 'ord_01G8AHAFDJ7FV2Y77FVWA19009', externalId: '1234', patient: { id: 'pat_ieUv67viS0lG18JN', externalId: '1234', }, }, }; const prescriptionCreatedBody: PhotonWebhook['body'] = { id: '01G8C1TNGH2F03021F23C95261', type: 'photon:prescription:created', specversion: '1.0', datacontenttype: 'application/json', time: '2022-01-01T01:00:00.000Z', subject: 'rx_01G8C1TNF8TZ5N9DAJN66H9KSH', source: 'org:org_KzSVZBQixLRkqj5d', data: { id: 'rx_01G8C1TNF8TZ5N9DAJN66H9KSH', externalId: '1234', dispenseQuantity: 30, dispenseAsWritten: true, dispenseUnit: 'EA', refillsAllowed: 12, daysSupply: 30, instructions: 'Take once daily', notes: '', effectiveDate: '2022-01-01', expirationDate: '2023-01-01', prescriberId: 'usr_wUofzqEvcA2JCwJ4', treatmentId: 'med_01G7T2NB6', patient: { id: 'pat_ieUv67viS0lG18JN', externalId: '1234', }, }, }; const idempotencyEvent: PhotonEvent = { ...prescriptionCreatedBody, }; const invalidEvent: PhotonEvent = { id: '01G8C1TNGH2F03021F23C95261', type: 'photon:prescription:created', specversion: '1.0', datacontenttype: 'application/json', time: '2022-01-01T01:00:00.000Z', subject: 'rx_01G8C1TNF8TZ5N9DAJN66H9KSH', source: 'org:org_KzSVZBQixLRkqj5d', data: { id: 'rx_01G8C1TNF8TZ5N9DAJN66H9KSH', externalId: '1234', dispenseQuantity: 30, dispenseAsWritten: true, dispenseUnit: 'EA', refillsAllowed: 12, daysSupply: 30, instructions: 'Take once daily', notes: '', effectiveDate: '2022-01-01', expirationDate: '2023-01-01', prescriberId: 'usr_wUofzqEvcA2JCwJ4', treatmentId: 'med_01G7T2NB6', patient: { id: 'pat_ieUv67viS0lG18JN', externalId: '1234', }, }, }; const depletedEvent: PhotonEvent = { id: '01J5RAYDBXKQR472N1FT9K5ER8', type: 'photon:prescription:depleted', specversion: '1.0', datacontenttype: 'application/json', time: '2024-08-20T16:46:56.381Z', subject: 'rx_01J5RAY5E6JK8YEW5NCFKWNZ6R', source: 'org:org_q5l4IPPdSR95k8Lc', data: { id: 'rx_01J5RAY5E6JK8YEW5NCFKWNZ6R', patient: { id: 'pat_01J5RATDZ6QADK386Z25517ZHG', externalId: 'f8cdefd1-5bbd-4b62-971d-826b1c8e10fe' }, }, }; const createdEvent: PhotonEvent = { id: '01J5RHM013398BX4570H7N6JN3', type: 'photon:prescription:created', specversion: '1.0', datacontenttype: 'application/json', time: '2024-08-20T18:43:35.075Z', subject: 'rx_01J5RHKZZQXWAHMXKPT9CF1S6N', source: 'org:org_q5l4IPPdSR95k8Lc', data: { id: 'rx_01J5RHKZZQXWAHMXKPT9CF1S6N', patient: { id: 'pat_01J5RHGXB2ZJFQ7B694CQGSGT5', externalId: 'ec3bb2b3-474f-4d40-804d-9fdb6149b492' }, dispenseQuantity: 342.9, dispenseAsWritten: true, dispenseUnit: 'Milliliter', refillsAllowed: 0, daysSupply: 1, instructions: '342.9 mL ', notes: '', effectiveDate: '2024-08-20', expirationDate: '2025-08-20', prescriberId: 'usr_01J21EPR81W9XRYTY69RQY3R9J', treatmentId: 'med_01J5VK0D4534R7VMD1P95VZ1RY', }, }; const expiredEvent: PhotonEvent = { id: '01G8AHJBT081QQWM89X3SVV31F', type: 'photon:prescription:expired', specversion: '1.0', datacontenttype: 'application/json', time: '2022-01-01T01:00:00.000Z', subject: 'rx_01G8AGBC91W1042CDRB19545EC', source: 'org:org_KzSVZBQixLRkqj5d', data: { id: 'rx_01G8AGBC91W1042CDRB19545EC', externalId: '1234', patient: { id: 'pat_ieUv67viS0lG18JN', externalId: '1234', }, }, };

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