Skip to main content
Glama
metriport-consolidated-data-webhook.test.ts12.3 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { ContentType, getReferenceString, indexSearchParameterBundle, indexStructureDefinitionBundle, } from '@medplum/core'; import { readJson, SEARCH_PARAMETER_BUNDLE_FILES } from '@medplum/definitions'; import type { Bundle, Encounter, Patient, SearchParameter } from '@medplum/fhirtypes'; import { MockClient } from '@medplum/mock'; import { randomUUID } from 'crypto'; import { convertToTransactionBundle, handler } from './metriport-consolidated-data-webhook'; import { JaneSmithMedplumPatient, JaneSmithMetriportPatient, MetriportConsolidatedDataBundle, } from './metriport-test-data'; describe('Metriport Consolidated Data Webhook', () => { const bot = { reference: 'Bot/123' }; const contentType = 'application/fhir+json'; const secrets = { METRIPORT_API_KEY: { name: 'METRIPORT_API_KEY', valueString: 'test-metriport-api-key' }, METRIPORT_WEBHOOK_KEY: { name: 'METRIPORT_WEBHOOK_KEY', valueString: 'test-metriport-webhook-key' }, }; let medplum: MockClient; beforeAll(() => { indexStructureDefinitionBundle(readJson('fhir/r4/profiles-types.json') as Bundle); indexStructureDefinitionBundle(readJson('fhir/r4/profiles-resources.json') as Bundle); indexStructureDefinitionBundle(readJson('fhir/r4/profiles-medplum.json') as Bundle); for (const filename of SEARCH_PARAMETER_BUNDLE_FILES) { indexSearchParameterBundle(readJson(filename) as Bundle<SearchParameter>); } }); beforeEach(async () => { medplum = new MockClient(); }); test('throws error when missing METRIPORT_API_KEY', async () => { await expect( handler(medplum, { bot, input: {}, contentType, secrets: { METRIPORT_WEBHOOK_KEY: secrets.METRIPORT_WEBHOOK_KEY }, }) ).rejects.toThrow('Missing METRIPORT_API_KEY'); }); test('throws error when missing METRIPORT_WEBHOOK_KEY', async () => { await expect( handler(medplum, { bot, input: {}, contentType, secrets: { METRIPORT_API_KEY: secrets.METRIPORT_API_KEY } }) ).rejects.toThrow('Missing METRIPORT_WEBHOOK_KEY'); }); test('throws error when missing message type', async () => { await expect(handler(medplum, { bot, input: {}, contentType, secrets })).rejects.toThrow('Missing message type'); }); test('throws error when missing medplumPatientId', async () => { const input = { meta: { type: 'medical.consolidated-data', }, }; await expect(handler(medplum, { bot, input, contentType, secrets })).rejects.toThrow( 'Missing medplumPatientId. The startConsolidatedQuery call must include the medplumPatientId metadata.' ); }); test("successfully handles 'ping' message", async () => { const pingSequence = 'test-ping-value'; const input = { meta: { type: 'ping', }, ping: pingSequence, }; await expect(handler(medplum, { bot, input, contentType, secrets })).resolves.toEqual({ pong: pingSequence }); }); test("successfully handles 'medical.consolidated-data' message", async () => { let medplumPatient: Patient = await medplum.createResource(JaneSmithMedplumPatient); const medplumPatientReference = getReferenceString(medplumPatient); const medplumPatientId = medplumPatient.id as string; const metriportPatientId = JaneSmithMetriportPatient.id; const input = { meta: { messageId: randomUUID(), when: new Date().toISOString(), type: 'medical.consolidated-data', data: { medplumPatientId: medplumPatientId, }, }, patients: [ { patientId: metriportPatientId, bundle: { entry: [ { resource: { resourceType: 'DocumentReference', content: [ { attachment: { url: 'https://api.metriport.com/medical/v1/documents/test-document', contentType: 'application/json', }, }, ], }, }, ], }, }, ], }; global.fetch = vi.fn().mockResolvedValueOnce({ json: vi.fn().mockResolvedValueOnce(MetriportConsolidatedDataBundle), }); expect(medplumPatient.identifier).toBeUndefined(); await expect(handler(medplum, { bot, input, contentType, secrets })).resolves.toEqual(true); expect(fetch).toHaveBeenCalledWith('https://api.metriport.com/medical/v1/documents/test-document'); medplumPatient = await medplum.readResource('Patient', medplumPatientId); expect(medplumPatient.identifier).toStrictEqual([ { system: 'https://metriport.com/fhir/identifiers/patient-id', value: metriportPatientId }, ]); const practitioners = await medplum.searchResources( 'Practitioner', 'identifier=73fbeae4-f7e6-425b-b9c7-2ff7c258e24d' ); expect(practitioners).toHaveLength(1); expect(practitioners[0].name?.[0].family).toBe('Smith'); expect(practitioners[0].name?.[0].given).toEqual(['David', 'K']); // Verify DocumentReference was created const docRefs = await medplum.searchResources( 'DocumentReference', 'identifier=52c3523e-4912-4d48-97a8-7e531e0682cb' ); expect(docRefs).toHaveLength(1); expect(docRefs[0].description).toStrictEqual('Progress Notes'); expect(docRefs[0].subject?.reference).toStrictEqual(medplumPatientReference); const locations = await medplum.searchResources('Location', 'identifier=40a528af-5c42-4c05-ae03-f2527137f994'); expect(locations).toHaveLength(1); expect(locations[0].name).toStrictEqual('MARY FREE BED AT SPARROW'); const encounters = await medplum.searchResources('Encounter', 'identifier=9617b8a1-efd0-4d37-bc0c-ae8b5a5a00a5'); expect(encounters).toHaveLength(1); expect(encounters[0].status).toStrictEqual('finished'); expect(encounters[0].subject?.reference).toStrictEqual(medplumPatientReference); const allergies = await medplum.searchResources( 'AllergyIntolerance', 'identifier=51893137-becb-4f5b-963b-7741d1cb8de2' ); expect(allergies).toHaveLength(1); expect(allergies[0].reaction?.[0].substance?.text).toStrictEqual('Dust'); expect(allergies[0].patient?.reference).toStrictEqual(medplumPatientReference); const medications = await medplum.searchResources('Medication', 'identifier=7e6747bd-fc1a-418a-a178-8518db7f95f5'); expect(medications).toHaveLength(1); expect(medications[0].code?.text).toStrictEqual('oxyCODONE-acetaminophen (PERCOCET) 5-325 mg tablet'); const medicationRequests = await medplum.searchResources( 'MedicationRequest', 'identifier=46806a66-3b4c-43dd-ac0d-7d0b10042ee5' ); expect(medicationRequests).toHaveLength(1); expect(medicationRequests[0].medicationReference?.reference).toStrictEqual(getReferenceString(medications[0])); expect(medicationRequests[0].subject?.reference).toStrictEqual(medplumPatientReference); }); }); describe('convertToTransactionBundle', () => { let medplum: MockClient; let patient: Patient; let medplumPatientId: string; beforeEach(async () => { medplum = new MockClient(); patient = await medplum.createResource(JaneSmithMedplumPatient); medplumPatientId = patient.id as string; }); test('converts a searchset bundle to a transaction bundle', () => { const transactionBundle = convertToTransactionBundle(MetriportConsolidatedDataBundle, medplumPatientId); expect(transactionBundle.type).toBe('transaction'); expect(transactionBundle.resourceType).toBe('Bundle'); expect(transactionBundle.entry).toBeDefined(); transactionBundle.entry?.forEach((entry) => { expect(entry.request).toBeDefined(); if (entry.resource?.resourceType === 'Binary' && entry.request?.url?.startsWith('Patient/')) { expect(entry.request?.method).toBe('PATCH'); expect(entry.request?.url).toBe(`Patient/${medplumPatientId}`); expect(entry.resource.contentType).toBe(ContentType.JSON_PATCH); expect(entry.resource.data).toBeDefined(); } else if (entry.resource) { expect(entry.request?.method).toBe('PUT'); expect(entry.resource?.id).toBeUndefined(); expect(entry.request?.url).toMatch(new RegExp(`^${entry.resource.resourceType}\\?identifier=.+$`)); } }); }); test('replaces references with fullUrls', () => { const transactionBundle = convertToTransactionBundle(MetriportConsolidatedDataBundle, medplumPatientId); const encounter = transactionBundle.entry?.find((entry) => entry.resource?.resourceType === 'Encounter') ?.resource as Encounter; expect(encounter?.subject?.reference).toStrictEqual('urn:uuid:0195d965-bfbc-7825-8a8a-b48baf403559'); expect(encounter?.participant?.[0]?.individual?.reference).toStrictEqual( 'urn:uuid:73fbeae4-f7e6-425b-b9c7-2ff7c258e24d' ); expect(encounter?.location?.[0]?.location?.reference).toStrictEqual( 'urn:uuid:40a528af-5c42-4c05-ae03-f2527137f994' ); }); test('adds metriport identifier to resource without existing identifiers', () => { const id = randomUUID(); const bundle: Bundle = { resourceType: 'Bundle', type: 'searchset', entry: [ { fullUrl: `urn:uuid:${id}`, resource: { resourceType: 'Practitioner', id, name: [{ given: ['Anne'], family: 'Doe' }], }, }, ], }; const transactionBundle = convertToTransactionBundle(bundle, medplumPatientId); expect((transactionBundle.entry?.[0].resource as any).identifier).toStrictEqual([ { system: 'https://metriport.com/fhir/identifiers/practitioner-id', value: id, }, ]); }); test('adds metriport identifier to resource with existing identifiers', () => { const id = randomUUID(); const bundle: Bundle = { resourceType: 'Bundle', type: 'searchset', entry: [ { fullUrl: `urn:uuid:${id}`, resource: { resourceType: 'Practitioner', id, name: [{ given: ['Anne'], family: 'Doe' }], gender: 'female', birthDate: '1996-02-10', identifier: [{ system: 'other-system', value: 'other-value' }], }, }, ], }; const transactionBundle = convertToTransactionBundle(bundle, medplumPatientId); expect((transactionBundle.entry?.[0].resource as any).identifier).toStrictEqual([ { system: 'other-system', value: 'other-value' }, { system: 'https://metriport.com/fhir/identifiers/practitioner-id', value: id }, ]); }); test('formats valid date property in DocumentReference', () => { const id = randomUUID(); const bundle: Bundle = { resourceType: 'Bundle', type: 'searchset', entry: [ { fullUrl: `urn:uuid:${id}`, resource: { resourceType: 'DocumentReference', id, status: 'current', content: [{ attachment: { contentType: 'text/plain; charset=UTF-8' } }], date: '2024-01-01', }, }, ], }; const transactionBundle = convertToTransactionBundle(bundle, medplumPatientId); expect((transactionBundle.entry?.[0].resource as any).date).toMatch( /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ ); }); test('removes invalid date property from DocumentReference', () => { const id = randomUUID(); const bundle: Bundle = { resourceType: 'Bundle', type: 'searchset', entry: [ { fullUrl: `urn:uuid:${id}`, resource: { resourceType: 'DocumentReference', id, status: 'current', content: [{ attachment: { contentType: 'text/plain; charset=UTF-8' } }], date: 'invalid-date', }, }, ], }; const transactionBundle = convertToTransactionBundle(bundle, medplumPatientId); expect((transactionBundle.entry?.[0].resource as any).date).toBeUndefined(); }); });

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