Skip to main content
Glama
receive-orm-message.ts12.3 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { createReference, getIdentifier, parseHl7DateTime, setIdentifier } from '@medplum/core'; import type { BotEvent, Hl7Field, Hl7Message, Hl7Segment, MedplumClient } from '@medplum/core'; import type { Annotation, CodeableConcept, ContactPoint, Encounter, HumanName, Patient, Practitioner, Resource, ServiceRequest, } from '@medplum/fhirtypes'; export const FACILITY_URL = 'https://lab.acme.org'; export const FACILITY_ORDER_ID = new URL('orderId', FACILITY_URL).toString(); export const FACILITY_PATIENT_ID = new URL('patientId', FACILITY_URL).toString(); export const FACILITY_PRACTITIONER_ID = new URL('patientId', FACILITY_URL).toString(); export const FACILITY_ORDER_CODE_SYSTEM = new URL('orderCode', FACILITY_URL).toString(); export const FACILITY_TIME_ZONE_OFFSET = '-0500'; /** * This Bot demonstrates how to receive a lab order from an SFTP server in the form of HL7v2 ORM messages, and convert them into ServiceRequests and associated resources. * * See: https://hl7-definition.caristix.com/v2/HL7v2.3/TriggerEvents/ORM_O01 * * @param medplum - The Medplum Client object * @param event - The BotEvent object * @returns The data returned by the `list` command */ export async function handler(medplum: MedplumClient, event: BotEvent<Hl7Message>): Promise<Hl7Message> { const message = event.input; // Create the patient if there is no identifier collision // You can substitute this with more complex de-duplication logic let patient = parsePatient(message); patient = await createWithoutDuplicate(medplum, patient, FACILITY_PATIENT_ID); // Create all the referenced Practitioners if there is no identifier collision // You can substitute this with more complex de-duplication logic let practitioners = parseAllPractitioners(message); practitioners = await Promise.all( practitioners.map((practitioner) => createWithoutDuplicate(medplum, practitioner, FACILITY_PRACTITIONER_ID)) ); // Parse out the PV1 segment related to the encounter const encounter = await medplum.createResource(parseEncounter(message, patient, practitioners)); const serviceRequests = parseServiceRequests(message, patient, practitioners, encounter); // Create the ServiceRequest resources await Promise.all( serviceRequests.map((serviceRequest) => createWithoutDuplicate(medplum, serviceRequest, FACILITY_ORDER_ID)) ); return message.buildAck(); } /** * Reference: https://build.fhir.org/ig/HL7/v2-to-fhir/ConceptMap-segment-pid-to-patient.html * @param message - HLv2 message * @returns a FHIR patient resource */ function parsePatient(message: Hl7Message): Patient { const pids = message.getAllSegments('PID'); if (pids.length !== 1) { throw new Error(`Got ${pids.length} PID Segments`); } const pid = pids[0]; const patient: Patient = { resourceType: 'Patient', identifier: [], name: [parseHl7Name(pid.getField(5))], // PID-5: Patient Name birthDate: parseHl7DateTime(pid.getField(7).toString())?.split('T')?.[0], // PID-7: Date of Birth gender: parseHl7Gender(pid.getField(8).toString()), // PID-8: Administrative Sex telecom: [ { system: 'phone', use: 'home', value: pid.getComponent(13, 1) } as ContactPoint, // PID-13: Home Phone { system: 'phone', use: 'work', value: pid.getComponent(14, 1) } as ContactPoint, // PID-14: Business Phone ].filter((telecom) => !!telecom.value?.length), }; const patientId = pid.getField(2); // PID-2: Patient ID if (!patientId) { console.warn('Missing Patient Id'); } else { setIdentifier(patient, FACILITY_PATIENT_ID, patientId.toString()); } const ssn = pid.getComponent(19, 1); // PID-19: Social Security Number if (ssn.length > 0) { patient.identifier?.push({ type: { coding: [ { system: 'http://terminology.hl7.org/CodeSystem/v2-0203', code: 'SS', }, ], }, system: 'http://hl7.org/fhir/sid/us-ssn', value: ssn, }); } return patient; } /** * Parses an HL7v2 PV1 (patient visit) segment into a FHIR `Encounter` resource * @param message - HL7v2 message * @param patient - The FHIR `Patient` resource, parsed from the PID segment * @param practitioners - An array of all `Practitioners` referenced in this message * @returns a FHIR `Encounter` resource */ function parseEncounter(message: Hl7Message, patient: Patient, practitioners: Practitioner[]): Encounter { const segment = message.getSegment('PV1'); const practitionerId = segment?.getComponent(7, 1); const practitioner = practitioners.find( (practitioner) => getIdentifier(practitioner, FACILITY_PRACTITIONER_ID) === practitionerId ); return { resourceType: 'Encounter', status: 'finished', class: { system: ' http://terminology.hl7.org/ValueSet/v3-ActEncounterCode', code: 'AMB', }, subject: createReference(patient), participant: practitioner && [{ individual: createReference(practitioner) }], period: { start: parseHl7DateTime(segment?.getComponent(44, 1), { tzOffset: FACILITY_TIME_ZONE_OFFSET }), end: parseHl7DateTime(segment?.getComponent(45, 1), { tzOffset: FACILITY_TIME_ZONE_OFFSET }), }, }; } /** * Extracts all Practitioner references from the PV1, ORC, and OBR segments of the message * @param message - HL7v2 message * @returns an array Practitioners referenced in the message */ function parseAllPractitioners(message: Hl7Message): Practitioner[] { const practitioners: Record<string, Practitioner> = {}; const fields = [ ...message.getAllSegments('PV1').map((segment) => segment.getField(8)), ...message.getAllSegments('ORC').map((segment) => segment.getField(12)), ...message.getAllSegments('OBR').map((segment) => segment.getField(16)), ]; for (const field of fields) { const practitioner = parsePractitioner(field); const identifier = getIdentifier(practitioner, FACILITY_PRACTITIONER_ID); if (identifier) { practitioners[identifier] = practitioner; } } return Object.values(practitioners); } /** * Converts an HL7v2 XCN type field to a FHIR `Practitioner` resource * @param field - the HL7v2 field * @returns a FHIR `Practitioner` resource */ function parsePractitioner(field: Hl7Field): Practitioner { const identifier = field.getComponent(1); const name = parseHl7Name(field, 1); return { resourceType: 'Practitioner', name: [name], identifier: [ { system: FACILITY_PRACTITIONER_ID, value: identifier, }, ], }; } /** * Parse all OBR segments into individual `ServiceRequest` resources * @param message - HL7v2 message * @param patient - The FHIR `Patient` resource, parsed from from the PID segment * @param practitioners - All of the FHIR `Practitioner`resources referenced in this message * @param encounter - The FHIR `Patient` resource, parsed from from the PV1 segment * @returns an array of FHIR `ServiceRequests` specified in this order */ function parseServiceRequests( message: Hl7Message, patient: Patient, practitioners: Practitioner[], encounter: Encounter ): ServiceRequest[] { // Create a Service Request for each OBR segment const requestSegments = collectSegmentGroupsByType(message, 'OBR'); return requestSegments.map((segment) => parseObrSegment(segment, patient, practitioners, encounter)); } /** * Parse all lines within a specific OBR segment into a FHIR `ServiceRequest` resource, including notes and ordering * physician * @param segments - The HLv2 segments that are part of this OBR * @param patient - The FHIR `Patient` resource, parsed from from the PID segment * @param practitioners - All of the FHIR `Practitioner`resources referenced in this message * @param encounter - The FHIR `Patient` resource, parsed from from the PV1 segment * @returns a FHIR `ServiceRequest` resource related to this order */ function parseObrSegment( segments: Hl7Segment[], patient: Patient, practitioners: Practitioner[], encounter: Encounter ): ServiceRequest { const obr = segments.find((s) => s.getComponent(0, 1) === 'OBR'); if (!obr) { throw new Error('Could not find OBR segment'); } const notes = segments.filter((s) => s.getComponent(0, 1) === 'NTE').map(parseHl7Note); const orderingProviderId = obr.getComponent(16, 1); const orderingProvider = practitioners.find( (practitioner) => getIdentifier(practitioner, FACILITY_PRACTITIONER_ID) === orderingProviderId ); const serviceRequest: ServiceRequest = { resourceType: 'ServiceRequest', intent: 'order', status: 'active', subject: createReference(patient), encounter: createReference(encounter), requisition: { system: FACILITY_ORDER_ID, value: obr.getComponent(2, 1) }, code: parseHl7CodeableConcept(obr.getField(4)), note: notes.length ? notes : undefined, requester: orderingProvider && createReference(orderingProvider), reasonCode: segments .filter((s) => s.getComponent(0, 1) === 'DG1') .map((segment) => parseHl7CodeableConcept(segment.getField(3))) .filter((e): e is CodeableConcept => !!e), }; return serviceRequest; } /** * Checks for an existing resource based on identifier. Creates a new resource if there is no existing resource * @param medplum - the MedplumClient * @param resource - resource to create * @param identifierSystem - system string used to identify duplicates * @returns new or existing resource */ async function createWithoutDuplicate<T extends Resource>( medplum: MedplumClient, resource: T, identifierSystem: string ): Promise<T> { return medplum.createResourceIfNoneExist( resource, `identifier=${identifierSystem}|${getIdentifier(resource, identifierSystem)}` ); } /** * Some * @param message - HL7v2 Message * @param type - The segment group type * @returns An array of segment groups, where ech group */ function collectSegmentGroupsByType(message: Hl7Message, type: string): Hl7Segment[][] { const result: Hl7Segment[][] = []; const segments = message.segments; let start = undefined; for (let i = 0; i < segments.length; i++) { const segment = segments[i]; if (segment.getComponent(0, 1) === type) { if (start !== undefined) { result.push(segments.slice(start, i)); } start = i; } } if (start !== undefined) { result.push(segments.slice(start)); } return result; } /* HL7v2 Parsing Utilities */ function parseHl7CodeableConcept(field: Hl7Field): CodeableConcept | undefined { if (!field.getComponent(1)) { return undefined; } return { coding: [ { code: field.getComponent(1), display: field.getComponent(2), system: translateCodeSystem(field.getComponent(3)), }, ], }; } /** * @param noteSegment - an HL7v2 NTE segment * @returns a FHIR `Annotation` */ function parseHl7Note(noteSegment: Hl7Segment): Annotation { return { text: noteSegment.getComponent(3, 1), }; } function translateCodeSystem(system: string): string | undefined { switch (system.toUpperCase()) { case 'I9': return 'http://hl7.org/fhir/sid/icd-9-cm'; case 'L': return FACILITY_ORDER_CODE_SYSTEM; } return undefined; } // Reference: https://build.fhir.org/ig/HL7/v2-to-fhir/ConceptMap-datatype-xpn-to-humanname.html function parseHl7Name(field: Hl7Field, indexOffset = 0): HumanName { const name: HumanName = { family: field.getComponent(indexOffset + 1), given: [field.getComponent(indexOffset + 2)], }; const middleName = field.getComponent(indexOffset + 3); if (middleName !== '') { name.given?.push(middleName); } const suffix = field.getComponent(indexOffset + 4); if (suffix !== '') { name.suffix = [suffix]; } const prefix = field.getComponent(indexOffset + 5); if (prefix !== '') { name.prefix = [prefix]; } return name; } function parseHl7Gender(gender: string): Patient['gender'] { switch (gender?.toLowerCase().at(0)) { case 'm': return 'male'; case 'f': return 'female'; default: return 'unknown'; } }

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