From 6d03a4f463933a267960f423da30246df28cfaf8 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Mon, 27 May 2024 11:12:25 +0300 Subject: [PATCH] feat(#114): fix bundle format --- mediator/src/controllers/cht.ts | 30 +++-- mediator/src/mappers/cht.ts | 2 +- mediator/src/mappers/openmrs.ts | 14 ++- mediator/src/routes/cht.ts | 10 ++ mediator/src/routes/tests/cht.spec.ts | 5 +- mediator/src/utils/cht.ts | 6 +- mediator/src/utils/fhir.ts | 33 +++-- mediator/src/utils/openmrs_sync.ts | 114 +++++++++++++----- mediator/src/utils/tests/openmrs_sync.spec.ts | 12 +- 9 files changed, 162 insertions(+), 64 deletions(-) diff --git a/mediator/src/controllers/cht.ts b/mediator/src/controllers/cht.ts index 7b07e191..a49768e6 100644 --- a/mediator/src/controllers/cht.ts +++ b/mediator/src/controllers/cht.ts @@ -22,18 +22,19 @@ export async function createPatient(chtPatientDoc: any) { const fhirPatient = buildFhirPatientFromCht(chtPatientDoc.doc); // create or update in the FHIR Server - // note that either way, its a PUT with the id from the patient doc + // even for create, sends a PUT request return updateFhirResource({ ...fhirPatient, resourceType: 'Patient' }); } export async function updatePatientIds(chtFormDoc: any) { // first, get the existing patient from fhir server - const response = await getFHIRPatientResource(chtFormDoc.openmrs_patient_uuid); + const response = await getFHIRPatientResource(chtFormDoc.external_id); if (response.status != 200) { return { status: 500, data: { message: `FHIR responded with ${response.status}`} }; } else if (response.data.total == 0){ - return { status: 404, data: { message: `Patient not found`} }; + // in case the patient is not found, return 200 to prevent retries + return { status: 200, data: { message: `Patient not found`} }; } const fhirPatient = response.data.entry[0].resource; @@ -41,25 +42,30 @@ export async function updatePatientIds(chtFormDoc: any) { // now, we need to get the actual patient doc from cht... const patient_uuid = await getPatientUUIDFromSourceId(chtFormDoc._id); - addId(fhirPatient, chtDocumentIdentifierType, patient_uuid); - - return updateFhirResource({ ...fhirPatient, resourceType: 'Patient' }); + if (patient_uuid){ + addId(fhirPatient, chtDocumentIdentifierType, patient_uuid); + return updateFhirResource({ ...fhirPatient, resourceType: 'Patient' }); + } else { + // in case the patient is not found, return 200 to prevent retries + return { status: 200, data: { message: `Patient not found`} }; + } } export async function createEncounter(chtReport: any) { const fhirEncounter = buildFhirEncounterFromCht(chtReport); + const response = await updateFhirResource(fhirEncounter); - const bundle: fhir4.Bundle = { - resourceType: 'Bundle', - type: 'collection', - entry: [fhirEncounter] + if (response.status != 200 && response.status != 201){ + // in case of an error from fhir server, return it to caller + return response; } for (const entry of chtReport.observations) { if (entry.valueCode || entry.valueString || entry.valueDateTime) { const observation = buildFhirObservationFromCht(chtReport.patient_uuid, fhirEncounter, entry); - bundle.entry?.push(observation); + updateFhirResource(observation); } } - return createFhirResource(bundle); + + return { status: 200, data: {} }; } diff --git a/mediator/src/mappers/cht.ts b/mediator/src/mappers/cht.ts index 2a4b13a8..2014e877 100644 --- a/mediator/src/mappers/cht.ts +++ b/mediator/src/mappers/cht.ts @@ -72,7 +72,7 @@ export function buildFhirPatientFromCht(chtPatient: any): fhir4.Patient { telecom: [phone] }; - copyIdToNamedIdentifier(patient, chtDocumentIdentifierType); + copyIdToNamedIdentifier(patient, patient, chtDocumentIdentifierType); return patient; } diff --git a/mediator/src/mappers/openmrs.ts b/mediator/src/mappers/openmrs.ts index 9b2f09b2..a649fb1f 100644 --- a/mediator/src/mappers/openmrs.ts +++ b/mediator/src/mappers/openmrs.ts @@ -25,7 +25,7 @@ const visitType: fhir4.CodeableConcept = { text: "Home Visit", coding: [{ system: "http://fhir.openmrs.org/code-system/visit-type", - code: "d66e9fe0-7d51-4801-a550-5d462ad1c944", + code: "7b0f5697-27e3-40c4-8bae-f4049abfb4ed", display: "Home Visit", }] } @@ -35,15 +35,22 @@ Build an OpenMRS Visit w/ Visit Note From a fhir Encounter One CHT encounter will become 2 OpenMRS Encounters */ -export function buildOpenMRSVisit(fhirEncounter: fhir4.Encounter): fhir4.Encounter[] { +export function buildOpenMRSVisit(patientId: string, fhirEncounter: fhir4.Encounter): fhir4.Encounter[] { const openMRSVisit = fhirEncounter; openMRSVisit.type = [visitType] - //openMRSVisit.subject.reference = `Patient/${patient_id}` + + const subjectRef: fhir4.Reference = { + reference: `Patient/${patientId}`, + type: "Patient" + }; + + openMRSVisit.subject = subjectRef; const visitRef: fhir4.Reference = { reference: `Encounter/${openMRSVisit.id}`, type: "Encounter" }; + const openMRSVisitNote: fhir4.Encounter = { ...openMRSVisit, id: randomUUID(), @@ -82,6 +89,7 @@ export function buildOpenMRSPatient(fhirPatient: fhir4.Patient): fhir4.Patient { } fhirPatient.name = fhirPatient.name?.map(addId); fhirPatient.identifier = fhirPatient.identifier?.map(addId); + fhirPatient.telecom = fhirPatient.telecom?.map(addId); return fhirPatient; } diff --git a/mediator/src/routes/cht.ts b/mediator/src/routes/cht.ts index 4444d10b..9e1d3de4 100644 --- a/mediator/src/routes/cht.ts +++ b/mediator/src/routes/cht.ts @@ -1,6 +1,7 @@ import { Router } from 'express'; import { requestHandler } from '../utils/request'; import { createPatient, updatePatientIds, createEncounter } from '../controllers/cht' +import { syncPatients, syncEncountersAndObservations} from '../utils/openmrs_sync' const router = Router(); @@ -21,4 +22,13 @@ router.post( requestHandler((req) => createEncounter(req.body)) ); +router.post( + '/sync', + requestHandler(async (req) => { + await syncPatients(); + syncEncountersAndObservations(); + return { status: 200, data: {}}; + }) +); + export default router; diff --git a/mediator/src/routes/tests/cht.spec.ts b/mediator/src/routes/tests/cht.spec.ts index c9a6e5ab..c796cadd 100644 --- a/mediator/src/routes/tests/cht.spec.ts +++ b/mediator/src/routes/tests/cht.spec.ts @@ -2,6 +2,9 @@ import request from 'supertest'; import app from '../../..'; import { ChtPatientFactory, ChtPregnancyForm } from '../../middlewares/schemas/tests/cht-request-factories'; import * as fhir from '../../utils/fhir'; +import axios from 'axios'; + +jest.mock('axios'); describe('POST /cht/patient', () => { it('accepts incoming request with valid patient resource', async () => { @@ -44,6 +47,6 @@ describe('POST /cht/patient', () => { resourceType: 'Patient', }); */ - expect(fhir.createFhirResource).toHaveBeenCalled(); + expect(fhir.updateFhirResource).toHaveBeenCalled(); }); }); diff --git a/mediator/src/utils/cht.ts b/mediator/src/utils/cht.ts index 09a7fba4..b83e4f35 100644 --- a/mediator/src/utils/cht.ts +++ b/mediator/src/utils/cht.ts @@ -87,7 +87,11 @@ export async function getPatientUUIDFromSourceId(source_id: string) { } const patient = await queryCht(query); - return patient.data.docs[0]._id; + if ( patient.data.docs && patient.data.docs.length > 0 ){ + return patient.data.docs[0]._id; + } else { + return '' + } } export async function createChtPatient(fhirPatient: fhir4.Patient) { diff --git a/mediator/src/utils/fhir.ts b/mediator/src/utils/fhir.ts index a8545feb..4a6f4df9 100644 --- a/mediator/src/utils/fhir.ts +++ b/mediator/src/utils/fhir.ts @@ -114,20 +114,24 @@ export async function getFHIRPatients(lastUpdated: Date) { ); } -export function copyIdToNamedIdentifier(resource: fhir4.Patient, fromIDType: fhir4.CodeableConcept){ +export function copyIdToNamedIdentifier(fromResource: any, toResource: fhir4.Patient | fhir4.Encounter, fromIDType: fhir4.CodeableConcept){ const identifier: fhir4.Identifier = { type: fromIDType, - value: resource.id + value: fromResource.id, + use: "secondary" }; - resource.identifier?.push(identifier); - return resource; + const sameIdType = (id: any) => (id.type.text === fromIDType.text) + if (!toResource.identifier?.some(sameIdType)) { + toResource.identifier?.push(identifier); + } + return toResource; } -export function getIdType(resource: fhir4.Patient, idType: fhir4.CodeableConcept): string{ +export function getIdType(resource: fhir4.Patient | fhir4.Encounter, idType: fhir4.CodeableConcept): string{ return resource.identifier?.find((id: any) => id?.type.text == idType.text)?.value || ''; } -export function addId(resource: fhir4.Patient, idType: fhir4.CodeableConcept, value: string){ +export function addId(resource: fhir4.Patient | fhir4.Encounter, idType: fhir4.CodeableConcept, value: string){ const identifier: fhir4.Identifier = { type: idType, value: value @@ -171,7 +175,7 @@ export async function updateFhirResource(doc: fhir4.Resource) { }, }); - return { status: res.status, data: res.data }; + return { status: res?.status, data: res?.data }; } catch (error: any) { logger.error(error); return { status: error.status, data: error.data }; @@ -184,7 +188,20 @@ export async function getFhirResourcesSince(lastUpdated: Date, resourceType: str `${FHIR.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`, axiosOptions ); - return { status: res.status, data: res.data }; + return { status: res?.status, data: res?.data }; + } catch (error: any) { + logger.error(error); + return { status: error.status, data: error.data }; + } +} + +export async function getFhirResource(id: string, resourceType: string) { + try { + const res = await axios.get( + `${FHIR.url}/${resourceType}/${id}`, + axiosOptions + ); + return { status: res?.status, data: res?.data }; } catch (error: any) { logger.error(error); return { status: error.status, data: error.data }; diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index b72b99fc..62000c8f 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -1,37 +1,50 @@ -import { getFhirResourcesSince, updateFhirResource, getIdType } from './fhir' +import { getFhirResourcesSince, updateFhirResource, getIdType, copyIdToNamedIdentifier, getFhirResource } from './fhir' import { getOpenMRSResourcesSince, createOpenMRSResource } from './openmrs' import { buildOpenMRSPatient, buildOpenMRSVisit, openMRSIdentifierType } from '../mappers/openmrs' import { createChtPatient, chtRecordFromObservations } from './cht' -interface ComparisonResult { +interface ComparisonResources { fhirResources: fhir4.Resource[], openMRSResources: fhir4.Resource[] } -async function getResources(resourceType: string): Promise { +/* + Get resources updates in the last day from both OpenMRS and the FHIR server +*/ +async function getResources(resourceType: string): Promise { const lastUpdated = new Date(); lastUpdated.setDate(lastUpdated.getDate() - 1); const fhirResponse = await getFhirResourcesSince(lastUpdated, resourceType); - const fhirResources: fhir4.Resource[] = fhirResponse.data.entry || []; + const fhirResources: fhir4.Resource[] = fhirResponse.data.entry?.map((entry: any) => entry.resource) || []; const openMRSResponse = await getOpenMRSResourcesSince(lastUpdated, resourceType); - const openMRSResources: fhir4.Resource[] = openMRSResponse.data.entry || []; + const openMRSResources: fhir4.Resource[] = openMRSResponse.data.entry?.map((entry: any) => entry.resource) || []; return { fhirResources: fhirResources, openMRSResources: openMRSResources }; } -interface SyncResults { +interface ComparisonResult { toupdate: fhir4.Resource[], incoming: fhir4.Resource[], outgoing: fhir4.Resource[] } +/* + Compares the rsources in OpenMRS and FHIR + the getKey argument is a function that gets an id for each resource + that is expected to be the same value in both OpenMRS and the FHIR Server + + returns lists of resources + that are in OpenMRS but not FHIR (incoming) + that are in FHIR but not OpenMRS (outgoing) + that are in both (toupdate) +*/ export async function compare( getKey: (resource: any) => string, resourceType: string -): Promise { - const results: SyncResults = { +): Promise { + const results: ComparisonResult = { toupdate: [], incoming: [], outgoing: [] @@ -42,10 +55,11 @@ export async function compare( const fhirIds = new Map(comparison.fhirResources.map(resource => [getKey(resource), resource])); comparison.openMRSResources.forEach((openMRSResource) => { - if (fhirIds.has(getKey(openMRSResource))) { + const key = getKey(openMRSResource); + if (fhirIds.has(key)) { // ok so the fhir server already has it results.toupdate.push(openMRSResource); - fhirIds.delete(getKey(openMRSResource)); + fhirIds.delete(key); } else { results.incoming.push(openMRSResource); } @@ -58,13 +72,23 @@ export async function compare( return results; } +/* + Sync Patients between OpenMRS and FHIR + compare patient resources + for incoming, creates them in the FHIR server and forwars to CHT + for outgoing, sends them to OpenMRS, receives the ID back, and updates the ID +*/ export async function syncPatients(){ const getKey = (fhirPatient: any) => { return getIdType(fhirPatient, openMRSIdentifierType) || fhirPatient.id }; - const results: SyncResults = await compare(getKey, 'Patient'); + const results: ComparisonResult = await compare(getKey, 'Patient'); results.incoming.forEach(async (openMRSResource) => { - const response = await updateFhirResource(openMRSResource); - const response2 = await createChtPatient(response.data); + const patient = openMRSResource as fhir4.Patient; + copyIdToNamedIdentifier(patient, patient, openMRSIdentifierType); + const response = await updateFhirResource(patient); + if (response.status == 200 || response.status == 201) { + createChtPatient(response.data); + } }); /* @@ -76,36 +100,55 @@ export async function syncPatients(){ }); */ - results.outgoing.forEach(async (openMRSResource) => { - const patient = openMRSResource as fhir4.Patient; + results.outgoing.forEach(async (resource) => { + const patient = resource as fhir4.Patient; const openMRSPatient = buildOpenMRSPatient(patient); const response = await createOpenMRSResource(openMRSPatient); + // copy openmrs identifier if successful + if (response.status == 200 || response.status == 201) { + copyIdToNamedIdentifier(response.data, patient, openMRSIdentifierType); + updateFhirResource(patient); + } }); } +/* + Sync Encounters and Observations + For incoming encounters, saves them to FHIR Server, then gathers related Observations + And send to CHT the Encounter together with its observations + For outgoing, converts to OpenMRS format and sends to OpenMRS + Updates to Observations and Encounters are not allowed +*/ export async function syncEncountersAndObservations(){ - const getEncounterKey = (fhirEncounter: any) => { return JSON.stringify(fhirEncounter.period); }; - const encounters: SyncResults = await compare(getEncounterKey, 'Encounter'); - const getObservationKey = (fhirObservation: any) => { - return fhirObservation.effectiveDateTime + fhirObservation.code.coding[0].code; + const getEncounterKey = (encounter: any) => { return getIdType(encounter, openMRSIdentifierType) || encounter.id }; + const encounters: ComparisonResult = await compare(getEncounterKey, 'Encounter'); + + // observations are defined by their relataed encounter and code + const getObservationKey = (observation: any) => { + return getEncounter(observation) + observation.code.coding[0].code; }; - const observations: SyncResults = await compare(getObservationKey, 'Observation'); + const observations: ComparisonResult = await compare(getObservationKey, 'Observation'); const encountersToCht = new Map(); - // create encounters and observations in the fhir server + + // for each incoming encounter, save it to fhir + // it will be forwarded to cht below, when its encounters are gathered encounters.incoming.forEach(async (openMRSResource) => { - const response = await updateFhirResource(openMRSResource); + const encounter = openMRSResource as fhir4.Encounter; + copyIdToNamedIdentifier(encounter, encounter, openMRSIdentifierType); + const response = await updateFhirResource(encounter); - // save to map to push to cht later - encountersToCht.set(openMRSResource.id, { + // save to map to push to cht below + encountersToCht.set(getEncounterKey(encounter), { observations: [], - encounter: openMRSResource + encounter: encounter }); }); function getEncounter(observation: fhir4.Observation) { return observation.encounter?.reference?.split('/')[1] || ''; }; + observations.incoming.forEach(async (openMRSResource) => { // group by encounter to forward to cht below const observation = openMRSResource as fhir4.Observation; @@ -113,14 +156,21 @@ export async function syncEncountersAndObservations(){ const response = await updateFhirResource(openMRSResource); }); - encounters.outgoing.forEach(async (openMRSResource) => { - const encounter = openMRSResource as fhir4.Encounter; - const openMRSVisit = buildOpenMRSVisit(encounter); - const response = await createOpenMRSResource(openMRSVisit[0]); - const response2 = await createOpenMRSResource(openMRSVisit[1]); + // for outgoing encounters, get the openMRS patient id and then forward to OpenMRS + encounters.outgoing.forEach(async (resource) => { + const encounter = resource as fhir4.Encounter; + const patientId = encounter.subject?.reference?.split('/')[1] || ''; + const patientResponse = await getFhirResource(patientId, 'Patient'); + if (patientResponse.status == 200) { + const patient = patientResponse.data as fhir4.Patient; + const openMRSPatientId = getIdType(patient, openMRSIdentifierType); + const openMRSVisit = buildOpenMRSVisit(openMRSPatientId, encounter); + const response = await createOpenMRSResource(openMRSVisit[0]); + } }); - observations.outgoing.forEach(async (openMRSResource) => { - const response = await createOpenMRSResource(openMRSResource); + + observations.outgoing.forEach(async (resource) => { + const response = await createOpenMRSResource(resource); }); encountersToCht.forEach(async (key: string, value: any) => { diff --git a/mediator/src/utils/tests/openmrs_sync.spec.ts b/mediator/src/utils/tests/openmrs_sync.spec.ts index a3501a33..513d5e5b 100644 --- a/mediator/src/utils/tests/openmrs_sync.spec.ts +++ b/mediator/src/utils/tests/openmrs_sync.spec.ts @@ -12,15 +12,15 @@ describe('OpenMRS Sync', () => { it('compares resources with the gvien key', async () => { jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ data: { entry: [ - {id: 'outgoing'}, - {id: 'toupdate'} + { resource: {id: 'outgoing'}}, + { resource: {id: 'toupdate'}} ] }, status: 200, }); jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ data: { entry: [ - {id: 'incoming'}, - {id: 'toupdate'} + { resource: {id: 'incoming'}}, + { resource: {id: 'toupdate'}} ] }, status: 200, }); @@ -43,7 +43,7 @@ describe('OpenMRS Sync', () => { status: 200, }); jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ - data: { entry: [ openMRSPatient ] }, + data: { entry: [ { resource: openMRSPatient } ] }, status: 200, }); jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ @@ -65,7 +65,7 @@ describe('OpenMRS Sync', () => { it('sends outgoing Patients to OpenMRS', async () => { const fhirPatient = PatientFactory.build(); jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ - data: { entry: [ fhirPatient ] }, + data: { entry: [ { resource: fhirPatient } ] }, status: 200, }); jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({