Skip to main content
Glama
sync-patient-from-photon.ts13.4 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { createReference, getDisplayString, normalizeErrorString, RXNORM } from '@medplum/core'; import type { BotEvent, MedplumClient } from '@medplum/core'; import type { Address, AllergyIntolerance, Bundle, BundleEntry, ContactPoint, MedicationRequest, Patient, Practitioner, Reference, } from '@medplum/fhirtypes'; import { randomUUID } from 'crypto'; import type { PhotonAddress, PhotonPatient, PhotonPatientAllergy, PhotonPrescription, PhotonProvider, } from '../photon-types'; import { NEUTRON_HEALTH, NEUTRON_HEALTH_PATIENTS } from './constants'; import { getMedicationElement, handlePhotonAuth, photonGraphqlFetch } from './utils'; export async function handler(medplum: MedplumClient, event: BotEvent): Promise<void> { const photonClientId = event.secrets['PHOTON_CLIENT_ID']?.valueString; const photonClientSecret = event.secrets['PHOTON_CLIENT_SECRET']?.valueString; const photonAuthToken = await handlePhotonAuth(photonClientId, photonClientSecret); const query = ` query patients( $filter: PatientFilter, $after: ID, $first: Int ) { patients ( filter: $filter, after: $after, first: $first ) { id externalId name { title first last } dateOfBirth sex gender email phone allergies { allergen { id name rxcui } comment onset } prescriptions { id externalId prescriber { id externalId name { title first last } email phone address { street1 street2 postalCode country state city } organizations { id name } } state treatment { id codes { rxcui } } dispenseAsWritten dispenseQuantity refillsAllowed refillsRemaining fillsAllowed fillsRemaining daysSupply instructions notes effectiveDate expirationDate writtenAt fills { id treatment { id codes { rxcui } } state requestedAt filledAt } } address { street1 street2 postalCode country state city } } } `; const body = JSON.stringify({ query }); const result = await photonGraphqlFetch(body, photonAuthToken); const photonPatients = result.data.patients as PhotonPatient[]; const batch: Bundle = { resourceType: 'Bundle', type: 'batch', entry: [], }; for (const photonPatient of photonPatients) { // Make sure the patient does not already exist let patient: Patient | undefined = await getExistingPatient(photonPatient, medplum); if (!patient) { // Create the patient resource patient = createPatientResource(photonPatient); } const patientUrl = 'urn:uuid:' + randomUUID(); const patientReference: Reference<Patient> = { reference: patientUrl, display: getDisplayString(patient) }; // Create any allergies the patient has const allergies = createAllergies(patientReference, photonPatient.allergies); // Create any prescriptions const prescriptions = await createPrescriptions(patientReference, medplum, photonPatient.prescriptions); // Add the patient resource to a bundle const patientEntry: BundleEntry = { fullUrl: patientUrl, request: { method: 'PUT', url: `Patient?identifier=${NEUTRON_HEALTH_PATIENTS}|${photonPatient.id}` }, resource: patient, }; batch.entry?.push(patientEntry); // If there are allergies, create entries and add them to the bundle if (allergies) { const allergyEntries: BundleEntry[] = allergies.map((allergy) => { const allergyCode = allergy.code?.coding?.find((code) => code.system === RXNORM)?.code; return { fullUrl: 'urn:uuid:' + randomUUID(), request: { method: 'PUT', url: `AllergyIntolerance?_source=${NEUTRON_HEALTH}&code=${allergyCode}&patient.identifier=${NEUTRON_HEALTH_PATIENTS}|${photonPatient.id}`, }, resource: allergy, }; }); batch.entry?.push(...allergyEntries); } // If there are prescriptions, create entries and add them to the bundle if (prescriptions) { const prescriptionEntries: BundleEntry[] = prescriptions.map((prescription) => { const photonId = prescription.identifier?.find((id) => id.system === NEUTRON_HEALTH)?.value; return { fullUrl: 'urn:uuid:' + randomUUID(), request: { method: 'PUT', url: `MedicationRequest?identifier=${NEUTRON_HEALTH}|${photonId}` }, resource: prescription, }; }); batch.entry?.push(...prescriptionEntries); } } if (batch.entry?.length === 0) { throw new Error('No patients to sync'); } // execute the bundle try { await medplum.executeBatch(batch); } catch (err) { throw new Error(normalizeErrorString(err)); } } /** * Takes a Photon Patient and gets the corresponding Patient resource from your project if it exists. * * @param photonPatient - The Photon Patient being searched for * @param medplum - Medplum Client to search your project * @returns A FHIR Patient resource if it exists in your project */ export async function getExistingPatient( photonPatient: PhotonPatient, medplum: MedplumClient ): Promise<Patient | undefined> { let patient: Patient | undefined; try { patient = await medplum.searchOne('Patient', { identifier: NEUTRON_HEALTH_PATIENTS + '|' + photonPatient.id, }); if (patient) { return patient; } } catch (err) { console.error(`Error for Patient ID ${photonPatient.id}:`, normalizeErrorString(err)); } if (photonPatient.externalId) { try { patient = await medplum.readResource('Patient', photonPatient.externalId); if (patient) { return patient; } } catch (err) { console.error(`Error for Patient ID ${photonPatient.id}:`, normalizeErrorString(err)); } } return undefined; } /** * Creates a Patient resource from the details of a Photon Patient object. * * @param photonPatient - Photon Patient object used to create the Patient in your project * @returns A Patient resource that can be added to a bundle to be executed in a batch */ export function createPatientResource(photonPatient: PhotonPatient): Patient { const telecom: ContactPoint[] = [{ system: 'phone', value: photonPatient.phone }]; if (photonPatient.email) { telecom.push({ system: 'email', value: photonPatient.email }); } const patient: Patient = { resourceType: 'Patient', identifier: [{ system: NEUTRON_HEALTH_PATIENTS, value: photonPatient.id }], name: [{ family: photonPatient.name.last, given: [photonPatient.name.first] }], gender: photonPatient.sex.toLowerCase() as Patient['gender'], birthDate: photonPatient.dateOfBirth, telecom, }; const address = photonPatient.address && formatAddress(photonPatient.address); if (address) { patient.address = address; } return patient; } /** * Creates an array of AllergyIntolerance resources given a Patient's allergies from Photon. * * @param patientReference - A reference to the Patient that has the allergies * @param photonAllergies - The Photon PatientAllergy object used to create the AllergyIntolerance resource * @returns An array of AllergyIntolerance resources */ export function createAllergies( patientReference: Reference<Patient>, photonAllergies?: PhotonPatientAllergy[] ): AllergyIntolerance[] | undefined { if (!photonAllergies || photonAllergies.length === 0) { return undefined; } const allergies: AllergyIntolerance[] = []; for (const photonAllergy of photonAllergies) { const { allergen, comment, onset } = photonAllergy; if (!allergen.rxcui) { continue; } const allergy: AllergyIntolerance = { resourceType: 'AllergyIntolerance', meta: { source: NEUTRON_HEALTH, }, patient: patientReference, code: { coding: [{ system: RXNORM, code: allergen.rxcui, display: allergen.name }] }, onsetDateTime: onset, }; if (comment) { allergy.note = [{ text: comment }]; } allergies.push(allergy); } return allergies; } export async function createPrescriptions( patientReference: Reference<Patient>, medplum: MedplumClient, photonPrescriptions?: PhotonPrescription[] ): Promise<MedicationRequest[] | undefined> { console.log(patientReference); if (!photonPrescriptions || photonPrescriptions.length === 0) { return undefined; } const prescriptions: MedicationRequest[] = []; for (const photonPrescription of photonPrescriptions) { console.log(photonPrescription); if ((await checkForExistingPrescription(medplum, photonPrescription)) || !photonPrescription) { continue; } const { codes, name } = photonPrescription.treatment; const status = getStatusFromPhotonState(photonPrescription.state); const medicationElement = await getMedicationElement(medplum, codes.rxcui, name); const prescriber = await getPrescriber(medplum, photonPrescription.prescriber); const requester: Reference<Practitioner> = prescriber ? createReference(prescriber) : { display: photonPrescription.prescriber.name.full }; const prescription: MedicationRequest = { resourceType: 'MedicationRequest', meta: { source: NEUTRON_HEALTH, }, status, intent: 'order', subject: patientReference, identifier: [{ system: NEUTRON_HEALTH, value: photonPrescription.id }], dispenseRequest: { quantity: { value: photonPrescription.dispenseQuantity, unit: photonPrescription.dispenseUnit, }, numberOfRepeatsAllowed: photonPrescription.refillsAllowed, expectedSupplyDuration: { value: photonPrescription.daysSupply, unit: 'days' }, validityPeriod: { start: photonPrescription.effectiveDate, end: photonPrescription.expirationDate, }, }, substitution: { allowedBoolean: !photonPrescription.dispenseAsWritten }, dosageInstruction: [{ patientInstruction: photonPrescription.instructions }], authoredOn: photonPrescription.writtenAt, medicationCodeableConcept: medicationElement, requester, }; if (photonPrescription.notes) { prescription.note = [{ text: photonPrescription.notes }]; } prescriptions.push(prescription); } return prescriptions; } async function checkForExistingPrescription( medplum: MedplumClient, photonPrescription: PhotonPrescription ): Promise<boolean> { if (!photonPrescription?.externalId || !photonPrescription?.id) { return false; } let prescription: MedicationRequest | undefined; if (photonPrescription.externalId) { prescription = await medplum.readResource('MedicationRequest', photonPrescription.externalId); if (prescription) { return true; } } prescription = await medplum.searchOne('MedicationRequest', { identifier: NEUTRON_HEALTH + `|${photonPrescription.id}`, }); return !!prescription; } export async function getPrescriber( medplum: MedplumClient, photonProvider: PhotonProvider ): Promise<Practitioner | undefined> { const prescriberPhotonId = photonProvider.id; const prescriberMedplumId = photonProvider.externalId; if (prescriberMedplumId) { const practitioner = await medplum.searchOne('Practitioner', { _id: prescriberMedplumId, }); if (practitioner) { return practitioner; } } const trackedPractitioner = await medplum.searchOne('Practitioner', { identifier: NEUTRON_HEALTH + `|${prescriberPhotonId}`, }); if (trackedPractitioner) { return trackedPractitioner; } else { return undefined; } } export function getStatusFromPhotonState( photonPrescriptionState: PhotonPrescription['state'] ): MedicationRequest['status'] { switch (photonPrescriptionState) { case 'ACTIVE': return 'active'; case 'CANCELED': return 'cancelled'; case 'DEPLETED': return 'completed'; case 'EXPIRED': return 'stopped'; default: throw new Error('Invalid state provided'); } } function formatAddress(photonAddress: PhotonAddress): Address[] { const { street1, street2, city, state, postalCode, country } = photonAddress; const line: Address['line'] = [street1]; if (street2) { line.push(street2); } const address: Address = { line, city, state, postalCode, country, }; return [address]; }

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