From 5e4451f9b788d634d981ccec4777daee984cee0e Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Thu, 14 Mar 2024 10:26:11 +0300 Subject: [PATCH 01/67] feat(#114): Create OpenMRS Mediator for Patient resource --- mediator/src/middlewares/schemas/patient.ts | 23 +------- .../schemas/tests/fhir-resource-factories.ts | 7 ++- mediator/src/routes/patient.ts | 10 +++- mediator/src/routes/tests/patient.spec.ts | 7 +-- mediator/src/utils/openmrs.ts | 55 +++++++++++++++++++ 5 files changed, 71 insertions(+), 31 deletions(-) create mode 100644 mediator/src/utils/openmrs.ts diff --git a/mediator/src/middlewares/schemas/patient.ts b/mediator/src/middlewares/schemas/patient.ts index 385e76ae..4bffd3d4 100644 --- a/mediator/src/middlewares/schemas/patient.ts +++ b/mediator/src/middlewares/schemas/patient.ts @@ -1,26 +1,9 @@ import joi from 'joi'; export const PatientSchema = joi.object({ - identifier: joi - .array() - .items( - joi.object({ - system: joi.string().valid('cht').required(), - value: joi.string().uuid().required(), - }) - ) - .min(1) - .required(), - name: joi - .array() - .items( - joi.object({ - family: joi.string().required(), - given: joi.array().length(1).required(), - }) - ) - .min(1) - .required(), + id: joi.string().uuid().required(), + name: joi.string().required(), gender: joi.string().required(), birthDate: joi.string().required(), + phone: joi.string().required(), }); diff --git a/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts b/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts index d35f460b..fdc2f81e 100644 --- a/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts +++ b/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts @@ -14,10 +14,11 @@ export const HumanNameFactory = Factory.define('humanName') .attr('given', ['John']); export const PatientFactory = Factory.define('patient') - .attr('identifier', identifier) - .attr('name', () => [HumanNameFactory.build()]) + .attr('id', randomUUID) + .attr('name', 'Patient Zero') .attr('gender', 'male') - .attr('birthDate', '2000-01-01'); + .attr('birthDate', '2000-01-01') + .attr('phone', '+97723423411'); export const EncounterFactory = Factory.define('encounter') .attr('identifier', identifier) diff --git a/mediator/src/routes/patient.ts b/mediator/src/routes/patient.ts index a65b07a8..76df1dfc 100644 --- a/mediator/src/routes/patient.ts +++ b/mediator/src/routes/patient.ts @@ -1,6 +1,7 @@ import { Router } from 'express'; import { validateBodyAgainst } from '../middlewares'; -import { createFhirResource, validateFhirResource } from '../utils/fhir'; +import { createFhirResource } from '../utils/fhir'; +import { buildOpenMRSPatient } from '../utils/openmrs'; import { PatientSchema } from '../middlewares/schemas/patient'; import { requestHandler } from '../utils/request'; @@ -10,8 +11,11 @@ const resourceType = 'Patient'; router.post( '/', - validateBodyAgainst(validateFhirResource(resourceType), PatientSchema), - requestHandler((req) => createFhirResource({ ...req.body, resourceType })) + validateBodyAgainst(PatientSchema), + requestHandler((req) => { + const openMRSPatient = buildOpenMRSPatient(req.body); + return createFhirResource({ ...openMRSPatient, resourceType }); + }) ); export default router; diff --git a/mediator/src/routes/tests/patient.spec.ts b/mediator/src/routes/tests/patient.spec.ts index 25f2c8fd..4d509c83 100644 --- a/mediator/src/routes/tests/patient.spec.ts +++ b/mediator/src/routes/tests/patient.spec.ts @@ -16,14 +16,11 @@ describe('POST /patient', () => { expect(res.status).toBe(201); expect(res.body).toEqual({}); - expect(fhir.createFhirResource).toHaveBeenCalledWith({ - ...data, - resourceType: 'Patient', - }); expect(fhir.createFhirResource).toHaveBeenCalled(); }); - it('doesn\'t accept incoming request with invalid patient resource', async () => { + // TODO: reenable when validating fhir resource after mapping + it.skip('doesn\'t accept incoming request with invalid patient resource', async () => { const data = PatientFactory.build({ birthDate: 'INVALID_BIRTH_DATE' }); const res = await request(app).post('/patient').send(data); diff --git a/mediator/src/utils/openmrs.ts b/mediator/src/utils/openmrs.ts new file mode 100644 index 00000000..f6a5e518 --- /dev/null +++ b/mediator/src/utils/openmrs.ts @@ -0,0 +1,55 @@ +import { randomUUID } from 'crypto'; + +interface OpenMRSIdentifier extends fhir4.Identifier { + id: string //uuid +} + +interface OpenMRSHumanName extends fhir4.HumanName { + id: string //uuid +} + +const phoneIdentifierType: fhir4.CodeableConcept = { + text: 'Phone Number' +} + +const chtIdentifierType: fhir4.CodeableConcept = { + text: 'CHT ID' +} + +export function buildOpenMRSPatient(chtPatient: Record): fhir4.Patient { + const nameParts = chtPatient.name.split(" "); + const familyName = nameParts.pop() || ""; + const givenNames = nameParts; + + const name: OpenMRSHumanName = { + family: familyName, + given: givenNames, + id: randomUUID(), + }; + + const phoneIdentifier: OpenMRSIdentifier = { + id: randomUUID(), + type: phoneIdentifierType, + value: chtPatient.phone, + use: 'usual' + }; + + const chtIdentifier: OpenMRSIdentifier = { + id: randomUUID(), + type: chtIdentifierType, + value: chtPatient.id, + system: 'cht', + use: 'official' + }; + + const patient: fhir4.Patient = { + resourceType: 'Patient', + name: [name], + birthDate: chtPatient.birthDate, + id: chtPatient.id, + identifier: [phoneIdentifier, chtIdentifier], + gender: chtPatient.gender + }; + + return patient; +} From 4e3de0fafaf6a820ffe91e64bd1e313694098275 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Sun, 7 Apr 2024 14:21:41 +0300 Subject: [PATCH 02/67] feat(#114): add endpoints for openmrs and cht --- mediator/index.ts | 8 +++ mediator/src/utils/cht.ts | 55 ++++++++++++++ mediator/src/utils/openmrs.ts | 131 +++++++++++++++++++++++++++++++++- 3 files changed, 191 insertions(+), 3 deletions(-) diff --git a/mediator/index.ts b/mediator/index.ts index 767af50a..46cdb7d8 100644 --- a/mediator/index.ts +++ b/mediator/index.ts @@ -8,6 +8,8 @@ import serviceRequestRoutes from './src/routes/service-request'; import encounterRoutes from './src/routes/encounter'; import organizationRoutes from './src/routes/organization'; import endpointRoutes from './src/routes/endpoint'; +import chtRoutes from './src/routes/cht'; +import openmrsRoutes from './src/routes/openmrs'; import { registerMediatorCallback } from './src/utils/openhim'; import os from 'os'; @@ -24,12 +26,18 @@ app.get('*', (_: Request, res: Response) => { res.send({status: 'success', osuptime: osUptime, processuptime: processUptime}); }); +// routes for valid fhir resources app.use('/patient', patientRoutes); app.use('/service-request', serviceRequestRoutes); app.use('/encounter', encounterRoutes); app.use('/organization', organizationRoutes); app.use('/endpoint', endpointRoutes); +// routes for cht docs +app.use('/cht', chtRoutes); +// routes for openmrs +app.use('/openmrs', openmrsRoutes); + if (process.env.NODE_ENV !== 'test') { app.listen(PORT, () => logger.info(`Server listening on port ${PORT}`)); diff --git a/mediator/src/utils/cht.ts b/mediator/src/utils/cht.ts index e0c96000..680ed8b9 100644 --- a/mediator/src/utils/cht.ts +++ b/mediator/src/utils/cht.ts @@ -3,6 +3,7 @@ import { CHT } from '../../config'; import { generateBasicAuthUrl } from './url'; import https from 'https'; import path from 'path'; +import { logger } from '../../logger'; export async function createChtRecord(patientId: string) { const record = { @@ -22,6 +23,60 @@ export async function createChtRecord(patientId: string) { return await axios.post(chtApiUrl, record, options); } +export async function createChtPatient(fhirPatient: fhir4.Patient) { + const options = { + httpsAgent: new https.Agent({ + rejectUnauthorized: false, + }), + }; + + const name = fhirPatient.name?.[0] + const given = name?.given ? name?.given : '' + const tc = fhirPatient.telecom?.[0] + const record = { + _meta: { + form: "N" + }, + age_in_years: 22, + patient_phone: tc?.value, + patient_name: `${given} ${name?.family}`, + gender: fhirPatient.gender, + location_id: "65985" + } + + const chtApiUrl = generateChtRecordsApiUrl(CHT.url, CHT.username, CHT.password); + + return await axios.post(chtApiUrl, record, options); +} + +export async function chtRecordFromObservations(patient_id: string, observations: any) { + const options = { + httpsAgent: new https.Agent({ + rejectUnauthorized: false, + }), + }; + + const record: any = { + _meta: { + form: "openmrs_anc" + }, + patient_id: patient_id + } + + for (const entry of observations.entry) { + const code:string = entry.resource.code.coding[0].code; + if (entry.resource.valueCodeableConcept) { + record[code] = entry.resource.valueCodeableConcept.text; + } else if (entry.resource.valueDateTime) { + record[code] = entry.resource.valueDateTime; + } + } + + const chtApiUrl = generateChtRecordsApiUrl(CHT.url, CHT.username, CHT.password); + + return await axios.post(chtApiUrl, record, options); +} + export const generateChtRecordsApiUrl = (chtUrl: string, username: string, password: string) => { const endpoint = generateBasicAuthUrl(chtUrl, username, password); return path.join(endpoint, '/api/v2/records'); diff --git a/mediator/src/utils/openmrs.ts b/mediator/src/utils/openmrs.ts index f6a5e518..333da8e7 100644 --- a/mediator/src/utils/openmrs.ts +++ b/mediator/src/utils/openmrs.ts @@ -16,6 +16,123 @@ const chtIdentifierType: fhir4.CodeableConcept = { text: 'CHT ID' } +const medicIdentifierType: fhir4.CodeableConcept = { + text: 'Medic ID' +} + +const noteEncounterType: fhir4.CodeableConcept = { + text: "Visit Note", + coding: [{ + system: "http://fhir.openmrs.org/code-system/encounter-type", + code: "d7151f82-c1f3-4152-a605-2f9ea7414a79", + display: "Visit Note" + }] +} + +const chwEncounterType: fhir4.CodeableConcept = { + text: "Community Health Worker Visit", + coding: [{ + system: "http://fhir.openmrs.org/code-system/visit-type", + code: "479a14c9-fd05-4399-8e3d-fed3f8c654c", + display: "Community Health Worker Visit", + }] +} + +const homeEncounterType: fhir4.CodeableConcept = { + text: "Home Visit", + coding: [{ + system: "http://fhir.openmrs.org/code-system/visit-type", + code: "d66e9fe0-7d51-4801-a550-5d462ad1c944", + display: "Home Visit", + }] +} + +const homeHealthEncounterClass: fhir4.CodeableConcept = { + text: 'HH', + coding : [{ + system: "http://terminology.hl7.org/CodeSystem/v3-ActCode", + code: "HH" + }] +} + +export function buildOpenMRSVisit(patient_id: string, reported_date: number) : fhir4.Encounter { + const visit = buildOpenMRSEncounter(patient_id, reported_date, homeEncounterType); + return visit; +} + +export function buildOpenMRSVisitNote(patient_id: string, reported_date: number, visit_id: string): fhir4.Encounter { + const visitNote = buildOpenMRSEncounter(patient_id, reported_date, noteEncounterType); + const visitRef: fhir4.Reference = { + reference: `Encounter/${visit_id}`, + type: "Encounter" + }; + visitNote.partOf = visitRef; + return visitNote; +} + +export function buildOpenMRSEncounter(patient_id: string, reported_date: number, visitType: fhir4.CodeableConcept): fhir4.Encounter { + const patientRef: fhir4.Reference = { + reference: `Patient/${patient_id}`, + type: "Patient" + }; + + const openMRSEncounter: fhir4.Encounter = { + resourceType: 'Encounter', + id: randomUUID(), + status: 'unknown', + class: homeHealthEncounterClass, + type: [visitType], + subject: patientRef, + period: { + start: new Date(reported_date).toISOString(), + end: new Date(reported_date + 1000*60*10).toISOString() + } + } + + return openMRSEncounter +} + + +export function buildOpenMRSObservation(patient_id: string, encounter_id: string, entry: any): fhir4.Observation { + const patientRef: fhir4.Reference = { + reference: `Patient/${patient_id}`, + type: "Patient" + }; + + const encounterRef: fhir4.Reference = { + reference: `Encounter/${encounter_id}`, + type: "Encounter" + }; + + const observation: fhir4.Observation = { + resourceType: "Observation", + subject: patientRef, + encounter: encounterRef, + status: "final", + code: { + coding: [{ + code: entry.code, + }], + }, + effectiveDateTime: "2024-03-31T12:26:27+00:00", + issued: "2024-03-31T12:26:28.000+00:00", + }; + + if ('valueCode' in entry){ + observation.valueCodeableConcept = { + coding: [{ + code: entry['valueCode'] + }] + }; + } else if ('valueDateTime' in entry){ + observation.valueDateTime = entry['valueDateTime']; + } else if ('valueString' in entry){ + observation.valueString = entry['valueString']; + } + + return observation; +} + export function buildOpenMRSPatient(chtPatient: Record): fhir4.Patient { const nameParts = chtPatient.name.split(" "); const familyName = nameParts.pop() || ""; @@ -34,10 +151,18 @@ export function buildOpenMRSPatient(chtPatient: Record): fhir4.Pati use: 'usual' }; + const medicIdentifier: OpenMRSIdentifier = { + id: randomUUID(), + type: medicIdentifierType, + value: chtPatient.patient_id, + system: 'cht', + use: 'official' + }; + const chtIdentifier: OpenMRSIdentifier = { id: randomUUID(), type: chtIdentifierType, - value: chtPatient.id, + value: chtPatient._id, system: 'cht', use: 'official' }; @@ -46,8 +171,8 @@ export function buildOpenMRSPatient(chtPatient: Record): fhir4.Pati resourceType: 'Patient', name: [name], birthDate: chtPatient.birthDate, - id: chtPatient.id, - identifier: [phoneIdentifier, chtIdentifier], + id: chtPatient._id, + identifier: [phoneIdentifier, chtIdentifier, medicIdentifier], gender: chtPatient.gender }; From 534b1fdd4cc40cbce5532289f0a7f813e1c0125c Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Mon, 8 Apr 2024 12:25:13 +0300 Subject: [PATCH 03/67] feat(#114): add missing routes --- mediator/src/routes/cht.ts | 27 +++++++++++++++ mediator/src/routes/openmrs.ts | 61 ++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 mediator/src/routes/cht.ts create mode 100644 mediator/src/routes/openmrs.ts diff --git a/mediator/src/routes/cht.ts b/mediator/src/routes/cht.ts new file mode 100644 index 00000000..51be1528 --- /dev/null +++ b/mediator/src/routes/cht.ts @@ -0,0 +1,27 @@ +import { Router } from 'express'; +import { PatientSchema } from '../middlewares/schemas/patient'; +import { requestHandler } from '../utils/request'; +import { createChtPatient, chtRecordFromObservations } from '../utils/cht'; +import { logger } from '../../logger'; + +const router = Router(); + +const resourceType = 'Patient'; + +router.post( + '/patient', + requestHandler(async (req) => { + logger.info(JSON.stringify(req.body)); + const response = await createChtPatient(req.body); + return { status: response.status, data: response.data } + }) +); + +router.post( + '/observations', + requestHandler(async (req) => { + const response = await chtRecordFromObservations('a60dec895aa93569df4e1513210009b8', req.body) + return { status: response.status, data: response.data } + }) +); +export default router; diff --git a/mediator/src/routes/openmrs.ts b/mediator/src/routes/openmrs.ts new file mode 100644 index 00000000..85eaf5d0 --- /dev/null +++ b/mediator/src/routes/openmrs.ts @@ -0,0 +1,61 @@ +import { Router } from 'express'; +import { validateBodyAgainst } from '../middlewares'; +import { EncounterSchema } from '../middlewares/schemas/encounter'; +import { createFhirResource, validateFhirResource, getFHIRPatientResource } from '../utils/fhir'; +import { requestHandler } from '../utils/request'; +import { buildOpenMRSVisit, buildOpenMRSVisitNote, buildOpenMRSObservation, buildOpenMRSPatient } from '../utils/openmrs'; +import { logger } from '../../logger'; + +const router = Router(); + +router.post( + '/encounter', + requestHandler(async (req) => { + const patient_response = await getFHIRPatientResource(req.body.patient_uuid); + + if (patient_response.status != 200) { + return { status: 500, data: { error: 'Error getting patient' } }; + } else if (!patient_response.data?.entry || patient_response.data.entry.length === 0) { + return { status: 400, data: { error: 'Patient not found' } }; + } + + const patient_id = patient_response.data.entry[0].resource?.id; + + if (!patient_id) { + return { status: 400, data: { error: 'Patient ID is null or undefined' } }; + } + + const openMRSVisit = buildOpenMRSVisit(patient_id, req.body.reported_date); + var enc_response = await createFhirResource({ ...openMRSVisit, resourceType: 'Encounter' }); + + if (enc_response.status != 200 && enc_response.status != 201) { + return { status: 400, data: { error: 'Error saving Visit Encounter' } }; + } + + const openMRSVisitNote = buildOpenMRSVisitNote(patient_id, req.body.reported_date, enc_response.data.id); + enc_response = await createFhirResource({ ...openMRSVisitNote, resourceType: 'Encounter' }); + + if (enc_response.status != 200 && enc_response.status != 201) { + return { status: 400, data: { error: 'Error saving Visit Note' } }; + } + + const encounter_id = enc_response.data.id; + for (const entry of req.body.observations) { + if (entry.valueCode || entry.valueString || entry.valueDateTime) { + const openMRSObservation = buildOpenMRSObservation(patient_id, encounter_id, entry); + const obsResponse = createFhirResource({ ...openMRSObservation, resourceType: 'Observation' }); + } + } + return { status: 201, data: {}}; + }) +); + +router.post( + '/patient', + requestHandler(async (req) => { + const openMRSPatient = buildOpenMRSPatient(req.body); + return createFhirResource({ ...openMRSPatient, resourceType: 'Patient' }); + }) +); + +export default router; From 50a3f3fb002a19bad761754628bbf77d90aa9c2f Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Tue, 16 Apr 2024 11:39:26 +0300 Subject: [PATCH 04/67] feat(#114): add openmrs poller --- docker/docker-compose.openmrs-poller.yml | 15 ++++ openmrs-poller/Dockerfile | 9 +++ openmrs-poller/poll_openmrs.py | 99 ++++++++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 docker/docker-compose.openmrs-poller.yml create mode 100644 openmrs-poller/Dockerfile create mode 100644 openmrs-poller/poll_openmrs.py diff --git a/docker/docker-compose.openmrs-poller.yml b/docker/docker-compose.openmrs-poller.yml new file mode 100644 index 00000000..0eec108c --- /dev/null +++ b/docker/docker-compose.openmrs-poller.yml @@ -0,0 +1,15 @@ +version: '3.9' + +services: + openmrs-poller: + build: ../openmrs-poller + container_name: openmrs-poller + networks: + - cht-net + environment: + - OPENMRS_URL=${OPENMRS_URL} + - OPENMRS_USER=${OPENMRS_USER} + - OPENMRS_PASSWORD=${OPENMRS_PASSWORD} + - OPENHIM_URL=${OPENHIM_URL} + - OPENHIM_USER=${OPENHIM_USER} + - OPENHIM_PASSWORD=${OPENHIM_PASSWORD} diff --git a/openmrs-poller/Dockerfile b/openmrs-poller/Dockerfile new file mode 100644 index 00000000..6a2b1ae8 --- /dev/null +++ b/openmrs-poller/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3 + +WORKDIR /app + +COPY . /app + +RUN pip3 install --no-cache-dir schedule requests + +CMD ["python", "poll_openmrs.py"] diff --git a/openmrs-poller/poll_openmrs.py b/openmrs-poller/poll_openmrs.py new file mode 100644 index 00000000..0e64dea7 --- /dev/null +++ b/openmrs-poller/poll_openmrs.py @@ -0,0 +1,99 @@ +import requests +import json +import schedule +import time +import datetime +import os + +openmrs_url = os.getenv('OPENMRS_URL') +openmrs_username = os.getenv('OPENMRS_USER') +openmrs_password = os.getenv('OPENMRS_PASSWORD') + +openhim_url = os.getenv('OPENHIM_URL') +openhim_username = os.getenv('OPENHIM_USER') +openhim_password = os.getenv('OPENHIM_PASSWORD') + + +last_updated = datetime.datetime.now(datetime.UTC).isoformat() +patients_already_sent = [] +encounters_already_sent = [] + +def get_patient_count(): + count_url = f"{openmrs_url}?_summary=count" + response = requests.get(count_url, auth=(openmrs_username, openmrs_password), verify=False) + count = response.json()['total'] + return count + +def fetch_and_post_by_id(patient_id): + patient_url = f"{openmrs_url}Patient/{patient_id}" + response = requests.get(patient_url, auth=(openmrs_username, openmrs_password)) + return post_data_to_openhim(response.json(), 'patient') + +def fetch_and_post_observations(patient_id): + patient_url = f"{openmrs_url}Observation/?subject={patient_id}" + response = requests.get(patient_url, auth=(openmrs_username, openmrs_password)) + return post_data_to_openhim(response.json(), 'observations') + +def fetch_new_patient_data(): + try: + print('Fetching new patients') + patient_url = f"{openmrs_url}Patient/?_lastUpdated=gt{last_updated}" + response = requests.get(patient_url, auth=(openmrs_username, openmrs_password)) + if response.status_code == 200 and response.json()['total'] > 0: + patients = response.json()['entry'] + for patient in patients: + if patient['resource']['id'] not in patients_already_sent: + print("Sending new patient") + print(patient) + response = post_data_to_openhim(patient['resource'], 'patient') + if response.status_code == 200 or response.status_code == 201: + patients_already_sent.append(patient['resource']['id']) + else: + print(f"Failed to fetch patient data from OpenMRS. Status code: {response.status_code}") + except requests.exceptions.RequestException as e: + print(f"Error fetching patient data: {e}") + +def fetch_new_observations(): + print('Fetching new observations') + try: + encounter_url = f"{openmrs_url}Encounter/?_lastUpdated=gt{last_updated}" + response = requests.get(encounter_url, auth=(openmrs_username, openmrs_password)) + if response.status_code == 200 and response.json()['total'] > 0: + encounters = response.json()['entry'] + for encounter in encounters: + if (encounter['resource']['id'] not in encounters_already_sent) and ('partOf' in encounter['resource']): + print("Sending new encounter") + print(encounter) + patient_id = encounter['resource']['subject']['reference'].split('/')[-1] + patient_url = f"{openmrs_url}Observation/?subject={patient_id}" + response = requests.get(patient_url, auth=(openmrs_username, openmrs_password)) + if response.status_code == 200: + response = post_data_to_openhim(response.json(), 'observations') + if response.status_code == 200 or response.status_code == 201: + encounters_already_sent.append(encounter['resource']['id']) + else: + print(f"Failed to fetch patient data from OpenMRS. Status code: {response.status_code}") + except requests.exceptions.RequestException as e: + print(f"Error fetching patient data: {e}") + +def post_data_to_openhim(data, suffix): + response = requests.post( + f"{openhim_url}/{suffix}", + json=data, + auth=(openhim_username, openhim_password), + verify=False, + headers={'Content-Type': 'application/json'} + ) + if response.status_code == 201: + print("Patient data posted to OpenHIM successfully.") + else: + print(f"Failed to post patient data to OpenHIM. Status code: {response.status_code}") + print(response.text) + return response + +schedule.every(1).minutes.do(fetch_new_patient_data) +schedule.every(1).minutes.do(fetch_new_observations) + +while True: + schedule.run_pending() + time.sleep(1) From 494ae3d31d2a1328ea60a87aed8096bb7adaf7ad Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Mon, 22 Apr 2024 13:03:05 +0300 Subject: [PATCH 05/67] feat(#114): id exchange and sms forms --- docker/docker-compose.mediator.yml | 3 + mediator/config/index.ts | 5 + mediator/src/routes/cht.ts | 35 +++++-- mediator/src/routes/openmrs.ts | 96 +++++++++++++++-- mediator/src/utils/cht.ts | 162 ++++++++++++++++++++++------- mediator/src/utils/fhir.ts | 44 ++++++++ mediator/src/utils/openmrs.ts | 89 ++++++++++++++-- 7 files changed, 368 insertions(+), 66 deletions(-) diff --git a/docker/docker-compose.mediator.yml b/docker/docker-compose.mediator.yml index 6ff4d360..762a1fde 100644 --- a/docker/docker-compose.mediator.yml +++ b/docker/docker-compose.mediator.yml @@ -16,6 +16,9 @@ services: - "FHIR_URL=${FHIR_URL:-http://openhim-core:5001/fhir}" - "FHIR_USERNAME=${FHIR_USERNAME:-interop-client}" - "FHIR_PASSWORD=${FHIR_PASSWORD:-interop-password}" + - "OPENMRS_URL=${OPENMRS_URL:-http://openhim-core:5001/openmrs}" + - "OPENMRS_USERNAME=${OPENMRS_USERNAME:-interop-client}" + - "OPENMRS_PASSWORD=${OPENMRS_PASSWORD:-interop-password}" - "CHT_URL=${CHT_URL:-https://nginx}" - "CHT_USERNAME=${CHT_USERNAME:-admin}" - "CHT_PASSWORD=${CHT_PASSWORD:-password}" diff --git a/mediator/config/index.ts b/mediator/config/index.ts index 7ca72253..6f5a3960 100644 --- a/mediator/config/index.ts +++ b/mediator/config/index.ts @@ -22,6 +22,11 @@ export const CHT = { password: getEnvironmentVariable('CHT_PASSWORD', 'password'), }; +export const OPENMRS = { + url: getEnvironmentVariable('OPENMRS_URL', 'http://openhim-core:5001/openmrs'), + username: getEnvironmentVariable('OPENMRS_USERNAME', 'interop-client'), + password: getEnvironmentVariable('OPENMRS_PASSWORD', 'interop-password'), +}; function getEnvironmentVariable(env: string, def: string) { if (process.env.NODE_ENV === 'test') { diff --git a/mediator/src/routes/cht.ts b/mediator/src/routes/cht.ts index 51be1528..a507c9bf 100644 --- a/mediator/src/routes/cht.ts +++ b/mediator/src/routes/cht.ts @@ -1,8 +1,9 @@ import { Router } from 'express'; import { PatientSchema } from '../middlewares/schemas/patient'; import { requestHandler } from '../utils/request'; -import { createChtPatient, chtRecordFromObservations } from '../utils/cht'; -import { logger } from '../../logger'; +import { createFhirResource, updateFhirResource, copyIdToNamedIdentifier, getIdType } from '../utils/fhir'; +import { createChtPatient, chtRecordFromObservations, chtIdentifierType } from '../utils/cht'; +import { openMRSIdentifierType } from '../utils/openmrs'; const router = Router(); @@ -11,17 +12,35 @@ const resourceType = 'Patient'; router.post( '/patient', requestHandler(async (req) => { - logger.info(JSON.stringify(req.body)); - const response = await createChtPatient(req.body); - return { status: response.status, data: response.data } + const openMRSPatient = req.body; + + copyIdToNamedIdentifier(openMRSPatient, openMRSPatient, openMRSIdentifierType); + var response = await createFhirResource({ ...openMRSPatient, resourceType: 'Patient' }); + + if (response.status != 200 && response.status != 201){ + return { status: 500, data: { message: `FHIR responded with ${response.status}`} }; + } + + // TODO: move this to some kind of observer thing + const fhirPatient = response.data; + response = await createChtPatient(openMRSPatient); + if (response.status != 200 && response.status != 201){ + return { status: 500, data: { message: `FHIR responded with ${response.status}`} }; + } + + return response; }) ); router.post( - '/observations', + '/encounter', requestHandler(async (req) => { - const response = await chtRecordFromObservations('a60dec895aa93569df4e1513210009b8', req.body) - return { status: response.status, data: response.data } + // request should include a patient + const bundle = req.body; + const openMRSPatient = bundle.entry.find((entry: any) => entry.resource.resourceType === 'Patient')?.resource; + const cht_id = getIdType(openMRSPatient, chtIdentifierType); + return chtRecordFromObservations(cht_id, req.body) }) ); + export default router; diff --git a/mediator/src/routes/openmrs.ts b/mediator/src/routes/openmrs.ts index 85eaf5d0..118efb3b 100644 --- a/mediator/src/routes/openmrs.ts +++ b/mediator/src/routes/openmrs.ts @@ -1,17 +1,19 @@ import { Router } from 'express'; -import { validateBodyAgainst } from '../middlewares'; -import { EncounterSchema } from '../middlewares/schemas/encounter'; -import { createFhirResource, validateFhirResource, getFHIRPatientResource } from '../utils/fhir'; +import { createFhirResource, updateFhirResource, getFHIRPatientResource } from '../utils/fhir'; +import { copyIdToNamedIdentifier, getIdType, addId } from '../utils/fhir'; +import { buildChtPatientFromFhir, getPatientUUIDFromSourceId } from '../utils/cht'; +import { medicIdentifierType, chtIdentifierType } from '../utils/cht'; import { requestHandler } from '../utils/request'; import { buildOpenMRSVisit, buildOpenMRSVisitNote, buildOpenMRSObservation, buildOpenMRSPatient } from '../utils/openmrs'; -import { logger } from '../../logger'; +import { createOpenMRSResource, updateOpenMRSResource, getOpenMRSPatientResource } from '../utils/openmrs'; +import { openMRSIdentifierType } from '../utils/openmrs'; const router = Router(); router.post( '/encounter', requestHandler(async (req) => { - const patient_response = await getFHIRPatientResource(req.body.patient_uuid); + const patient_response = await getOpenMRSPatientResource(req.body.patient_uuid); if (patient_response.status != 200) { return { status: 500, data: { error: 'Error getting patient' } }; @@ -26,14 +28,14 @@ router.post( } const openMRSVisit = buildOpenMRSVisit(patient_id, req.body.reported_date); - var enc_response = await createFhirResource({ ...openMRSVisit, resourceType: 'Encounter' }); + var enc_response = await createOpenMRSResource({ ...openMRSVisit, resourceType: 'Encounter' }); if (enc_response.status != 200 && enc_response.status != 201) { return { status: 400, data: { error: 'Error saving Visit Encounter' } }; } const openMRSVisitNote = buildOpenMRSVisitNote(patient_id, req.body.reported_date, enc_response.data.id); - enc_response = await createFhirResource({ ...openMRSVisitNote, resourceType: 'Encounter' }); + enc_response = await createOpenMRSResource({ ...openMRSVisitNote, resourceType: 'Encounter' }); if (enc_response.status != 200 && enc_response.status != 201) { return { status: 400, data: { error: 'Error saving Visit Note' } }; @@ -43,18 +45,92 @@ router.post( for (const entry of req.body.observations) { if (entry.valueCode || entry.valueString || entry.valueDateTime) { const openMRSObservation = buildOpenMRSObservation(patient_id, encounter_id, entry); - const obsResponse = createFhirResource({ ...openMRSObservation, resourceType: 'Observation' }); + const obsResponse = createOpenMRSResource({ ...openMRSObservation, resourceType: 'Observation' }); } } return { status: 201, data: {}}; }) ); +async function createOrUpdateOpenMRS(fhirPatient: fhir4.Patient) { + if (getIdType(fhirPatient, openMRSIdentifierType)) { + return { status: 201, data: { message: `Updates are not supported`} }; + //return updateOpenMRSResource({ ...fhirPatient, resourceType: 'Patient' }); + } else { + var response = await createOpenMRSResource({ ...fhirPatient, resourceType: 'Patient' }); + if (response.status != 200 && response.status != 201) { + return { status: 500, data: { message: `OpenMRS responded with ${response.status}`} }; + } + + // its in openMRS, merge IDs and push it back to the fhir Server + copyIdToNamedIdentifier(response.data, fhirPatient, openMRSIdentifierType); + return updateFhirResource({ ...fhirPatient, resourceType: 'Patient' }); + } +} + router.post( '/patient', requestHandler(async (req) => { - const openMRSPatient = buildOpenMRSPatient(req.body); - return createFhirResource({ ...openMRSPatient, resourceType: 'Patient' }); + // hack for sms forms: if source_id but not _id, + // first get patient id from source + if (req.body.doc.source_id){ + req.body.doc._id = await getPatientUUIDFromSourceId(req.body.source_id); + } + + // TODO: build FhirPatient here, openmrsify later! + var fhirPatient = buildOpenMRSPatient(req.body.doc); + const chtPatientDoc = req.body.doc; + + const response = await getFHIRPatientResource(chtPatientDoc._id); + if (response.status != 200) { + return { status: 500, data: { message: `FHIR responded with ${response.status}`} }; + } + + if (response.data.total == 0) { + // if we don't have the fhir resource yet, create it here + // note it might already exist without doc id + const response2 = await createFhirResource({ ...fhirPatient, resourceType: 'Patient' }); + if (response2.status != 200 && response2.status != 201) { + return { status: 500, data: { message: `FHIR responded with ${response2.status}`} }; + } + fhirPatient = response2.data; + } else { + return { status: 201, data: { message: `Updates not supported`} }; + } + + return createOrUpdateOpenMRS(fhirPatient) + }) +); + +router.post( + '/patient_ids', + requestHandler(async (req) => { + const chtFormDoc = req.body.doc; + + // first, get the existing patient from fhir server + var response = await getFHIRPatientResource(chtFormDoc.openmrs_patient_uuid); + + 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`} }; + } + + const fhirPatient = response.data.entry[0].resource; + addId(fhirPatient, medicIdentifierType, chtFormDoc.patient_id); + + // now, we need to get the actual patient doc from cht... + const patient_uuid = await getPatientUUIDFromSourceId(chtFormDoc._id); + addId(fhirPatient, chtIdentifierType, patient_uuid); + + const update_response = await updateFhirResource({ ...fhirPatient, resourceType: 'Patient' }); + if (update_response.status != 200 && update_response.status != 201) { + return { status: 500, data: { message: `FHIR responded with ${update_response.status}`} }; + } + + //change id to openmrs for update. maybe should fetch from openmrs again? + fhirPatient.id = getIdType(fhirPatient, openMRSIdentifierType); + return updateOpenMRSResource({ ...fhirPatient, resourceType: 'Patient' }); }) ); diff --git a/mediator/src/utils/cht.ts b/mediator/src/utils/cht.ts index 680ed8b9..dbe2d912 100644 --- a/mediator/src/utils/cht.ts +++ b/mediator/src/utils/cht.ts @@ -4,6 +4,30 @@ import { generateBasicAuthUrl } from './url'; import https from 'https'; import path from 'path'; import { logger } from '../../logger'; +import { openMRSIdentifierType } from './openmrs'; +import { getIdType } from './fhir'; + +type CouchDBQuery = { + selector: Record; + fields?: string[]; +}; + +export const chtIdentifierType: fhir4.CodeableConcept = { + text: 'CHT ID' +} + +export const medicIdentifierType: fhir4.CodeableConcept = { + text: 'Medic ID' +} + +function getOptions(){ + const options = { + httpsAgent: new https.Agent({ + rejectUnauthorized: false, + }), + }; + return options; +} export async function createChtRecord(patientId: string) { const record = { @@ -12,50 +36,65 @@ export async function createChtRecord(patientId: string) { }, patient_uuid: patientId, }; - const options = { - httpsAgent: new https.Agent({ - rejectUnauthorized: false, - }), - }; const chtApiUrl = generateChtRecordsApiUrl(CHT.url, CHT.username, CHT.password); - return await axios.post(chtApiUrl, record, options); + return await axios.post(chtApiUrl, record, getOptions()); } -export async function createChtPatient(fhirPatient: fhir4.Patient) { - const options = { - httpsAgent: new https.Agent({ - rejectUnauthorized: false, - }), - }; +async function getLocationByName(fhirPatient: fhir4.Patient) { + const query: CouchDBQuery = { + selector: { + type: "contact" + } + } - const name = fhirPatient.name?.[0] - const given = name?.given ? name?.given : '' - const tc = fhirPatient.telecom?.[0] - const record = { - _meta: { - form: "N" + const addresses = fhirPatient.address?.[0]?.extension?.[0]?.extension; + + var addressKey = "http://fhir.openmrs.org/ext/address#address4" + var addressValue = addresses?.find((ext: any) => ext.url === addressKey)?.valueString; + + if (addressValue) { + query.fields = ['place_id']; + } else { + addressKey = "http://fhir.openmrs.org/ext/address#address5" + addressValue = addresses?.find((ext: any) => ext.url === addressKey)?.valueString; + //TODO: support getting area + //query.fields = ['default_place_id']; + query.fields = ['place_id']; + } + + query.selector.name = addressValue + + const location = await queryCht(query); + return location.data.docs[0].place_id; +} + +export async function getPatientUUIDFromSourceId(source_id: string) { + const query: CouchDBQuery = { + selector: { + source_id: source_id, + type: "person" }, - age_in_years: 22, - patient_phone: tc?.value, - patient_name: `${given} ${name?.family}`, - gender: fhirPatient.gender, - location_id: "65985" + fields: [ "_id" ] } - const chtApiUrl = generateChtRecordsApiUrl(CHT.url, CHT.username, CHT.password); + const patient = await queryCht(query); + return patient.data.docs[0]._id; +} + +export async function createChtPatient(fhirPatient: fhir4.Patient) { + const cht_patient = buildChtPatientFromFhir(fhirPatient); + + cht_patient._meta = { form: "openmrs_patient" } + + const location_id = await getLocationByName(fhirPatient); + cht_patient.location_id = location_id; - return await axios.post(chtApiUrl, record, options); + return chtRecordsApi(cht_patient); } export async function chtRecordFromObservations(patient_id: string, observations: any) { - const options = { - httpsAgent: new https.Agent({ - rejectUnauthorized: false, - }), - }; - const record: any = { _meta: { form: "openmrs_anc" @@ -64,20 +103,69 @@ export async function chtRecordFromObservations(patient_id: string, observations } for (const entry of observations.entry) { - const code:string = entry.resource.code.coding[0].code; - if (entry.resource.valueCodeableConcept) { - record[code] = entry.resource.valueCodeableConcept.text; - } else if (entry.resource.valueDateTime) { - record[code] = entry.resource.valueDateTime; + if (entry.resource.resourceType == 'Observation') { + const code:string = entry.resource.code.coding[0].code; + if (entry.resource.valueCodeableConcept) { + record[code.toLowerCase()] = entry.resource.valueCodeableConcept.text; + } else if (entry.resource.valueDateTime) { + record[code.toLowerCase()] = entry.resource.valueDateTime.split('T')[0]; + } } } + return chtRecordsApi(record); +} + +export function buildChtPatientFromFhir(fhirPatient: fhir4.Patient): any { + const name = fhirPatient.name?.[0]; + const given = name?.given ? name?.given : ''; + + const tc = fhirPatient.telecom?.[0]; + + const now = new Date().getTime(); + const birthDate = Date.parse(fhirPatient.birthDate || ''); + const age_in_days = Math.floor((now - birthDate) / (1000 * 60 * 60 * 24)); + + const updateObject = { + patient_name: `${given} ${name?.family}`, + phone_number: tc?.value, + sex: fhirPatient.gender, + age_in_days: age_in_days, + //TODO: decouple from openmrs + openmrs_patient_uuid: fhirPatient.id, + openmrs_id: getIdType(fhirPatient, openMRSIdentifierType) + }; + + return updateObject; +} + +export async function updateChtDocument(doc: any, update_object: any) { + const chtApiUrl = generateChtDBUrl(CHT.url, CHT.username, CHT.password); + const updated_doc = { ...doc, ...update_object } + return await axios.put(path.join(chtApiUrl, doc._id), updated_doc, getOptions()); +} + +export async function chtRecordsApi(doc: any) { const chtApiUrl = generateChtRecordsApiUrl(CHT.url, CHT.username, CHT.password); + return await axios.post(chtApiUrl, doc, getOptions()); +} + +export async function getChtDocumentById(doc_id: string) { + const chtApiUrl = generateChtDBUrl(CHT.url, CHT.username, CHT.password); + return await axios.get(path.join(chtApiUrl, doc_id), getOptions()); +} - return await axios.post(chtApiUrl, record, options); +export async function queryCht(query: any) { + const chtApiUrl = generateChtDBUrl(CHT.url, CHT.username, CHT.password); + return await axios.post(path.join(chtApiUrl, '_find'), query, getOptions()); } export const generateChtRecordsApiUrl = (chtUrl: string, username: string, password: string) => { const endpoint = generateBasicAuthUrl(chtUrl, username, password); return path.join(endpoint, '/api/v2/records'); }; + +export const generateChtDBUrl = (chtUrl: string, username: string, password: string) => { + const endpoint = generateBasicAuthUrl(chtUrl, username, password); + return path.join(endpoint, '/medic/'); +}; diff --git a/mediator/src/utils/fhir.ts b/mediator/src/utils/fhir.ts index 82e817bf..ba2c8f90 100644 --- a/mediator/src/utils/fhir.ts +++ b/mediator/src/utils/fhir.ts @@ -107,6 +107,34 @@ export async function getFHIRPatientResource(patientId: string) { ); } +export function copyIdToNamedIdentifier(resourceFrom: any, resourceTo: fhir4.Patient, fromIDType: fhir4.CodeableConcept){ + const identifier: fhir4.Identifier = { + type: fromIDType, + value: resourceFrom.id || resourceFrom._id, + }; + resourceTo.identifier?.push(identifier); + return resourceTo; +} + +export function getIdType(resource: fhir4.Patient, 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){ + const identifier: fhir4.Identifier = { + type: idType, + value: value + }; + resource.identifier?.push(identifier); + return resource; +} + +export async function getFHIRLocation(locationId: string) { + return await axios.get( + `${FHIR.url}/Patient/?identifier=${locationId}`, + axiosOptions + ); +} export async function deleteFhirSubscription(id?: string) { return await axios.delete(`${FHIR.url}/Subscription/${id}`, axiosOptions); } @@ -126,3 +154,19 @@ export async function createFhirResource(doc: fhir4.Resource) { return { status: error.status, data: error.data }; } } + +export async function updateFhirResource(doc: fhir4.Resource) { + try { + const res = await axios.put(`${FHIR.url}/${doc.resourceType}/${doc.id}`, doc, { + auth: { + username: FHIR.username, + password: FHIR.password, + }, + }); + + 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.ts b/mediator/src/utils/openmrs.ts index 333da8e7..801fdbcf 100644 --- a/mediator/src/utils/openmrs.ts +++ b/mediator/src/utils/openmrs.ts @@ -1,4 +1,14 @@ import { randomUUID } from 'crypto'; +import { OPENMRS } from '../../config'; +import axios from 'axios'; +import { logger } from '../../logger'; + +const axiosOptions = { + auth: { + username: OPENMRS.username, + password: OPENMRS.password, + }, +}; interface OpenMRSIdentifier extends fhir4.Identifier { id: string //uuid @@ -20,6 +30,10 @@ const medicIdentifierType: fhir4.CodeableConcept = { text: 'Medic ID' } +export const openMRSIdentifierType: fhir4.CodeableConcept = { + text: 'OpenMRS Patient UUID' +} + const noteEncounterType: fhir4.CodeableConcept = { text: "Visit Note", coding: [{ @@ -104,6 +118,8 @@ export function buildOpenMRSObservation(patient_id: string, encounter_id: string type: "Encounter" }; + const now = new Date().toISOString(); + const observation: fhir4.Observation = { resourceType: "Observation", subject: patientRef, @@ -114,8 +130,8 @@ export function buildOpenMRSObservation(patient_id: string, encounter_id: string code: entry.code, }], }, - effectiveDateTime: "2024-03-31T12:26:27+00:00", - issued: "2024-03-31T12:26:28.000+00:00", + effectiveDateTime: now, + issued: now }; if ('valueCode' in entry){ @@ -151,6 +167,21 @@ export function buildOpenMRSPatient(chtPatient: Record): fhir4.Pati use: 'usual' }; + const patient: fhir4.Patient = { + resourceType: 'Patient', + name: [name], + birthDate: chtPatient.date_of_birth, + id: chtPatient._id, + identifier: [phoneIdentifier], + gender: chtPatient.sex + }; + + mergeIds(patient, chtPatient); + + return patient; +} + +export async function mergeIds(doc: fhir4.Patient, chtPatient: any) { const medicIdentifier: OpenMRSIdentifier = { id: randomUUID(), type: medicIdentifierType, @@ -167,14 +198,50 @@ export function buildOpenMRSPatient(chtPatient: Record): fhir4.Pati use: 'official' }; - const patient: fhir4.Patient = { - resourceType: 'Patient', - name: [name], - birthDate: chtPatient.birthDate, - id: chtPatient._id, - identifier: [phoneIdentifier, chtIdentifier, medicIdentifier], - gender: chtPatient.gender - }; + if (!doc.identifier) { + doc.identifier = []; + } + doc.identifier.push(medicIdentifier); + doc.identifier.push(chtIdentifier); - return patient; + return doc; +} + +export async function getOpenMRSPatientResource(patientId: string) { + return await axios.get( + `${OPENMRS.url}/Patient/?identifier=${patientId}`, + axiosOptions + ); +} + +export async function createOpenMRSResource(doc: fhir4.Resource) { + try { + const res = await axios.post(`${OPENMRS.url}/${doc.resourceType}`, doc, { + auth: { + username: OPENMRS.username, + password: OPENMRS.password, + }, + }); + + return { status: res.status, data: res.data }; + } catch (error: any) { + logger.error(error); + return { status: error.status, data: error.data }; + } +} + +export async function updateOpenMRSResource(doc: fhir4.Resource) { + try { + const res = await axios.put(`${OPENMRS.url}/${doc.resourceType}/${doc.id}`, doc, { + auth: { + username: OPENMRS.username, + password: OPENMRS.password, + }, + }); + + return { status: res.status, data: res.data }; + } catch (error: any) { + logger.error(error); + return { status: error.status, data: error.data }; + } } From 8c3c4fe5f23bcbd4934ef5bbfa534cdacdd565e8 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Tue, 23 Apr 2024 10:44:59 +0300 Subject: [PATCH 06/67] feat(#114): add patient to bundle from openmrs --- openmrs-poller/poll_openmrs.py | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/openmrs-poller/poll_openmrs.py b/openmrs-poller/poll_openmrs.py index 0e64dea7..0370d608 100644 --- a/openmrs-poller/poll_openmrs.py +++ b/openmrs-poller/poll_openmrs.py @@ -14,26 +14,10 @@ openhim_password = os.getenv('OPENHIM_PASSWORD') -last_updated = datetime.datetime.now(datetime.UTC).isoformat() +last_updated = datetime.datetime.utcnow().isoformat() patients_already_sent = [] encounters_already_sent = [] -def get_patient_count(): - count_url = f"{openmrs_url}?_summary=count" - response = requests.get(count_url, auth=(openmrs_username, openmrs_password), verify=False) - count = response.json()['total'] - return count - -def fetch_and_post_by_id(patient_id): - patient_url = f"{openmrs_url}Patient/{patient_id}" - response = requests.get(patient_url, auth=(openmrs_username, openmrs_password)) - return post_data_to_openhim(response.json(), 'patient') - -def fetch_and_post_observations(patient_id): - patient_url = f"{openmrs_url}Observation/?subject={patient_id}" - response = requests.get(patient_url, auth=(openmrs_username, openmrs_password)) - return post_data_to_openhim(response.json(), 'observations') - def fetch_new_patient_data(): try: print('Fetching new patients') @@ -68,9 +52,15 @@ def fetch_new_observations(): patient_url = f"{openmrs_url}Observation/?subject={patient_id}" response = requests.get(patient_url, auth=(openmrs_username, openmrs_password)) if response.status_code == 200: - response = post_data_to_openhim(response.json(), 'observations') + bundle = response.json() + patient_url = f"{openmrs_url}Patient/{patient_id}" + response = requests.get(patient_url, auth=(openmrs_username, openmrs_password)) if response.status_code == 200 or response.status_code == 201: - encounters_already_sent.append(encounter['resource']['id']) + # add patient to the bundle to send to openhim + bundle['entry'].append({'resource': response.json()}) + response = post_data_to_openhim(bundle, 'encounter') + if response.status_code == 200 or response.status_code == 201: + encounters_already_sent.append(encounter['resource']['id']) else: print(f"Failed to fetch patient data from OpenMRS. Status code: {response.status_code}") except requests.exceptions.RequestException as e: From 6ed106d2c68c4c8618f2b5986de0ed285c929467 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Mon, 29 Apr 2024 12:49:16 +0300 Subject: [PATCH 07/67] feat(#114): allow palce id in name --- mediator/src/utils/cht.ts | 57 +++++++++++++++++++++++------------ mediator/src/utils/openmrs.ts | 2 -- 2 files changed, 37 insertions(+), 22 deletions(-) diff --git a/mediator/src/utils/cht.ts b/mediator/src/utils/cht.ts index dbe2d912..270556e6 100644 --- a/mediator/src/utils/cht.ts +++ b/mediator/src/utils/cht.ts @@ -42,32 +42,49 @@ export async function createChtRecord(patientId: string) { return await axios.post(chtApiUrl, record, getOptions()); } -async function getLocationByName(fhirPatient: fhir4.Patient) { - const query: CouchDBQuery = { - selector: { - type: "contact" - } - } - +async function getLocation(fhirPatient: fhir4.Patient) { + // first, extract address value; is fchv area available? const addresses = fhirPatient.address?.[0]?.extension?.[0]?.extension; - var addressKey = "http://fhir.openmrs.org/ext/address#address4" var addressValue = addresses?.find((ext: any) => ext.url === addressKey)?.valueString; - if (addressValue) { - query.fields = ['place_id']; - } else { + if (!addressValue) { + // no fchv area, use next highest address addressKey = "http://fhir.openmrs.org/ext/address#address5" addressValue = addresses?.find((ext: any) => ext.url === addressKey)?.valueString; - //TODO: support getting area - //query.fields = ['default_place_id']; - query.fields = ['place_id']; - } - query.selector.name = addressValue - - const location = await queryCht(query); - return location.data.docs[0].place_id; + // still no... return nothing + if (!addressValue) { + return ''; + } + } + + // does the name have a place id included? + const regex = /\[(\d+)\]/; + const match = addressValue.match(regex); + + // if so, return it and we're done + if (match) { + return match[1]; + } else { + // if not, query by name + const query: CouchDBQuery = { + selector: { + type: "contact", + name: addressValue + }, + fields: ['place_id'] + } + const location = await queryCht(query); + + // edge cases can result in more than one location, get first matching + // if not found by name, no more we can do, give up + if (!location.data?.docs || location.data.docs.length == 0){ + return ''; + } else { + return location.data.docs[0].place_id; + } + } } export async function getPatientUUIDFromSourceId(source_id: string) { @@ -88,7 +105,7 @@ export async function createChtPatient(fhirPatient: fhir4.Patient) { cht_patient._meta = { form: "openmrs_patient" } - const location_id = await getLocationByName(fhirPatient); + const location_id = await getLocation(fhirPatient); cht_patient.location_id = location_id; return chtRecordsApi(cht_patient); diff --git a/mediator/src/utils/openmrs.ts b/mediator/src/utils/openmrs.ts index 801fdbcf..6d3848ee 100644 --- a/mediator/src/utils/openmrs.ts +++ b/mediator/src/utils/openmrs.ts @@ -186,7 +186,6 @@ export async function mergeIds(doc: fhir4.Patient, chtPatient: any) { id: randomUUID(), type: medicIdentifierType, value: chtPatient.patient_id, - system: 'cht', use: 'official' }; @@ -194,7 +193,6 @@ export async function mergeIds(doc: fhir4.Patient, chtPatient: any) { id: randomUUID(), type: chtIdentifierType, value: chtPatient._id, - system: 'cht', use: 'official' }; From 76e90127fb78f1e83ff02795d1d1da8ad25d66a9 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Mon, 20 May 2024 12:16:09 +0300 Subject: [PATCH 08/67] feat(#124): remove openmrs endpoints, add mappers for cht, openmrs --- mediator/index.ts | 3 - mediator/src/mappers/cht.ts | 144 ++++++++++++++++++++++ mediator/src/mappers/openmrs.ts | 87 ++++++++++++++ mediator/src/routes/cht.ts | 71 +++++++---- mediator/src/routes/openmrs.ts | 137 --------------------- mediator/src/routes/patient.ts | 10 +- mediator/src/utils/cht.ts | 38 +----- mediator/src/utils/fhir.ts | 22 +++- mediator/src/utils/openmrs.ts | 203 ++------------------------------ 9 files changed, 313 insertions(+), 402 deletions(-) create mode 100644 mediator/src/mappers/cht.ts create mode 100644 mediator/src/mappers/openmrs.ts delete mode 100644 mediator/src/routes/openmrs.ts diff --git a/mediator/index.ts b/mediator/index.ts index 46cdb7d8..a27d7e2e 100644 --- a/mediator/index.ts +++ b/mediator/index.ts @@ -9,7 +9,6 @@ import encounterRoutes from './src/routes/encounter'; import organizationRoutes from './src/routes/organization'; import endpointRoutes from './src/routes/endpoint'; import chtRoutes from './src/routes/cht'; -import openmrsRoutes from './src/routes/openmrs'; import { registerMediatorCallback } from './src/utils/openhim'; import os from 'os'; @@ -35,8 +34,6 @@ app.use('/endpoint', endpointRoutes); // routes for cht docs app.use('/cht', chtRoutes); -// routes for openmrs -app.use('/openmrs', openmrsRoutes); if (process.env.NODE_ENV !== 'test') { app.listen(PORT, () => logger.info(`Server listening on port ${PORT}`)); diff --git a/mediator/src/mappers/cht.ts b/mediator/src/mappers/cht.ts new file mode 100644 index 00000000..de327141 --- /dev/null +++ b/mediator/src/mappers/cht.ts @@ -0,0 +1,144 @@ +import { getIdType, copyIdToNamedIdentifier } from '../utils/fhir'; +import { openMRSIdentifierType } from './openmrs'; + +export const chtDocumentIdentifierType: fhir4.CodeableConcept = { + text: 'CHT Document ID' +} + +export const chtPatientIdentifierType: fhir4.CodeableConcept = { + text: 'CHT Patient ID' +} + +const chwVisitType: fhir4.CodeableConcept = { + text: "Communtiy Health Worker Visit", +} + +const homeHealthEncounterClass: fhir4.CodeableConcept = { + text: 'HH', + coding : [{ + system: "http://terminology.hl7.org/CodeSystem/v3-ActCode", + code: "HH" + }] +} + +export function buildChtPatientFromFhir(fhirPatient: fhir4.Patient): any { + const name = fhirPatient.name?.[0]; + const given = name?.given ? name?.given : ''; + + const tc = fhirPatient.telecom?.[0]; + + const now = new Date().getTime(); + const birthDate = Date.parse(fhirPatient.birthDate || ''); + const age_in_days = Math.floor((now - birthDate) / (1000 * 60 * 60 * 24)); + + const updateObject = { + patient_name: `${given} ${name?.family}`, + phone_number: tc?.value, + sex: fhirPatient.gender, + age_in_days: age_in_days, + //TODO: decouple from openmrs + openmrs_patient_uuid: fhirPatient.id, + openmrs_id: getIdType(fhirPatient, openMRSIdentifierType) + }; + + return updateObject; +} + +export function buildFhirPatientFromCht(chtPatient: any): fhir4.Patient { + const nameParts = chtPatient.name.split(" "); + const familyName = nameParts.pop() || ""; + const givenNames = nameParts; + + const name: fhir4.HumanName = { + family: familyName, + given: givenNames, + }; + + const chtPatientId: fhir4.Identifier = { + type: chtPatientIdentifierType, + value: chtPatient.patient_id, + use: 'official' + }; + + const phone: fhir4.ContactPoint = { + value: chtPatient.phone + }; + + const patient: fhir4.Patient = { + resourceType: 'Patient', + name: [name], + birthDate: chtPatient.date_of_birth, + id: chtPatient._id, + identifier: [chtPatientId], + gender: chtPatient.sex, + telecom: [phone] + }; + + copyIdToNamedIdentifier(patient, chtDocumentIdentifierType); + + return patient; +} + +export function buildFhirEncounterFromCht(chtReport: any): fhir4.Encounter { + const patientRef: fhir4.Reference = { + reference: `Patient/${chtReport.patient_id}`, + type: "Patient" + }; + + const encounter: fhir4.Encounter = { + resourceType: 'Encounter', + id: chtReport.id, + status: 'unknown', + type: [chwVisitType], + class: homeHealthEncounterClass, + subject: patientRef, + period: { + start: new Date(chtReport.reported_date).toISOString(), + end: new Date(chtReport.reported_date + 1000*60*10).toISOString() + } + } + + return encounter +} + +export function buildFhirObservationFromCht(patient_id: string, encounter: fhir4.Encounter, entry: any): fhir4.Observation { + const patientRef: fhir4.Reference = { + reference: `Patient/${patient_id}`, + type: "Patient" + }; + + const encounterRef: fhir4.Reference = { + reference: `Encounter/${encounter.id}`, + type: "Encounter" + }; + + const now = new Date().toISOString(); + + const observation: fhir4.Observation = { + resourceType: "Observation", + subject: patientRef, + encounter: encounterRef, + status: "final", + code: { + coding: [{ + code: entry.code, + }], + }, + effectiveDateTime: encounter.period?.start, + issued: now + }; + + if ('valueCode' in entry){ + observation.valueCodeableConcept = { + coding: [{ + code: entry['valueCode'] + }] + }; + } else if ('valueDateTime' in entry){ + observation.valueDateTime = entry['valueDateTime']; + } else if ('valueString' in entry){ + observation.valueString = entry['valueString']; + } + + return observation; +} diff --git a/mediator/src/mappers/openmrs.ts b/mediator/src/mappers/openmrs.ts new file mode 100644 index 00000000..9b2f09b2 --- /dev/null +++ b/mediator/src/mappers/openmrs.ts @@ -0,0 +1,87 @@ +import { randomUUID } from 'crypto'; + +interface OpenMRSIdentifier extends fhir4.Identifier { + id: string //uuid +} + +interface OpenMRSHumanName extends fhir4.HumanName { + id: string //uuid +} + +export const openMRSIdentifierType: fhir4.CodeableConcept = { + text: 'OpenMRS Patient UUID' +} + +const visitNoteType: fhir4.CodeableConcept = { + text: "Visit Note", + coding: [{ + system: "http://fhir.openmrs.org/code-system/encounter-type", + code: "d7151f82-c1f3-4152-a605-2f9ea7414a79", + display: "Visit Note" + }] +} + +const visitType: fhir4.CodeableConcept = { + text: "Home Visit", + coding: [{ + system: "http://fhir.openmrs.org/code-system/visit-type", + code: "d66e9fe0-7d51-4801-a550-5d462ad1c944", + display: "Home Visit", + }] +} + +/* +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[] { + const openMRSVisit = fhirEncounter; + openMRSVisit.type = [visitType] + //openMRSVisit.subject.reference = `Patient/${patient_id}` + + const visitRef: fhir4.Reference = { + reference: `Encounter/${openMRSVisit.id}`, + type: "Encounter" + }; + const openMRSVisitNote: fhir4.Encounter = { + ...openMRSVisit, + id: randomUUID(), + type: [visitNoteType], + partOf: visitRef + } + + return [openMRSVisit, openMRSVisitNote]; +} + +/* +Build an observation that opnemrs will accept from a FHIR Observation +This means swapping refreneces, which may not be the same in both servers +*/ +export function buildOpenMRSObservation(fhirObservation: fhir4.Observation, patientId: string, encounterId: string) : fhir4.Observation { + if (fhirObservation.subject) { // to satisfy type checker, subject is not optional + fhirObservation.subject.reference = `Patient/${patientId}` + } + if (fhirObservation.encounter) { // to satisfy type checker, encounter is not optional + fhirObservation.encounter.reference = `Encounter/${encounterId}` + } + return fhirObservation; +} + +/* +Build a patient that OpenMRS will accept from a FHIR Patient +The only difference is that name and identifiers need uuids +*/ +export function buildOpenMRSPatient(fhirPatient: fhir4.Patient): fhir4.Patient { + function addId(resource: any) { + if ( resource.id ){ + return resource; + } else { + return { ...resource, id: randomUUID() }; + } + } + fhirPatient.name = fhirPatient.name?.map(addId); + fhirPatient.identifier = fhirPatient.identifier?.map(addId); + return fhirPatient; +} + diff --git a/mediator/src/routes/cht.ts b/mediator/src/routes/cht.ts index a507c9bf..8d5c1617 100644 --- a/mediator/src/routes/cht.ts +++ b/mediator/src/routes/cht.ts @@ -1,9 +1,10 @@ import { Router } from 'express'; -import { PatientSchema } from '../middlewares/schemas/patient'; import { requestHandler } from '../utils/request'; -import { createFhirResource, updateFhirResource, copyIdToNamedIdentifier, getIdType } from '../utils/fhir'; -import { createChtPatient, chtRecordFromObservations, chtIdentifierType } from '../utils/cht'; -import { openMRSIdentifierType } from '../utils/openmrs'; +import { createFhirResource, updateFhirResource, getFHIRPatientResource } from '../utils/fhir'; +import { addId } from '../utils/fhir'; +import { getPatientUUIDFromSourceId } from '../utils/cht'; +import { buildFhirObservationFromCht, buildFhirEncounterFromCht, buildFhirPatientFromCht } from '../mappers/cht'; +import { chtPatientIdentifierType, chtDocumentIdentifierType } from '../mappers/cht'; const router = Router(); @@ -12,35 +13,63 @@ const resourceType = 'Patient'; router.post( '/patient', requestHandler(async (req) => { - const openMRSPatient = req.body; + // hack for sms forms: if source_id but not _id, + // first get patient id from source + if (req.body.doc.source_id){ + req.body.doc._id = await getPatientUUIDFromSourceId(req.body.source_id); + } - copyIdToNamedIdentifier(openMRSPatient, openMRSPatient, openMRSIdentifierType); - var response = await createFhirResource({ ...openMRSPatient, resourceType: 'Patient' }); + const fhirPatient = buildFhirPatientFromCht(req.body.doc); + // create or update in the FHIR Server + // note that either way, its a PUT with the id from the patient doc + return updateFhirResource({ ...fhirPatient, resourceType: 'Patient' }); + }) +); - if (response.status != 200 && response.status != 201){ - return { status: 500, data: { message: `FHIR responded with ${response.status}`} }; - } +router.post( + '/patient_ids', + requestHandler(async (req) => { + const chtFormDoc = req.body.doc; + + // first, get the existing patient from fhir server + var response = await getFHIRPatientResource(chtFormDoc.openmrs_patient_uuid); - // TODO: move this to some kind of observer thing - const fhirPatient = response.data; - response = await createChtPatient(openMRSPatient); - if (response.status != 200 && response.status != 201){ + 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`} }; } - return response; + const fhirPatient = response.data.entry[0].resource; + addId(fhirPatient, chtPatientIdentifierType, chtFormDoc.patient_id); + + // 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' }); }) ); router.post( '/encounter', requestHandler(async (req) => { - // request should include a patient - const bundle = req.body; - const openMRSPatient = bundle.entry.find((entry: any) => entry.resource.resourceType === 'Patient')?.resource; - const cht_id = getIdType(openMRSPatient, chtIdentifierType); - return chtRecordFromObservations(cht_id, req.body) + const chtReport = req.body; + const fhirEncounter = buildFhirEncounterFromCht(chtReport); + + const bundle: fhir4.Bundle = { + resourceType: 'Bundle', + type: 'collection', + entry: [fhirEncounter] + } + + for (const entry of req.body.observations) { + if (entry.valueCode || entry.valueString || entry.valueDateTime) { + const observation = buildFhirObservationFromCht(chtReport, fhirEncounter, entry); + bundle.entry?.push(observation); + } + } + return createFhirResource(bundle); }) ); - export default router; diff --git a/mediator/src/routes/openmrs.ts b/mediator/src/routes/openmrs.ts deleted file mode 100644 index 118efb3b..00000000 --- a/mediator/src/routes/openmrs.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { Router } from 'express'; -import { createFhirResource, updateFhirResource, getFHIRPatientResource } from '../utils/fhir'; -import { copyIdToNamedIdentifier, getIdType, addId } from '../utils/fhir'; -import { buildChtPatientFromFhir, getPatientUUIDFromSourceId } from '../utils/cht'; -import { medicIdentifierType, chtIdentifierType } from '../utils/cht'; -import { requestHandler } from '../utils/request'; -import { buildOpenMRSVisit, buildOpenMRSVisitNote, buildOpenMRSObservation, buildOpenMRSPatient } from '../utils/openmrs'; -import { createOpenMRSResource, updateOpenMRSResource, getOpenMRSPatientResource } from '../utils/openmrs'; -import { openMRSIdentifierType } from '../utils/openmrs'; - -const router = Router(); - -router.post( - '/encounter', - requestHandler(async (req) => { - const patient_response = await getOpenMRSPatientResource(req.body.patient_uuid); - - if (patient_response.status != 200) { - return { status: 500, data: { error: 'Error getting patient' } }; - } else if (!patient_response.data?.entry || patient_response.data.entry.length === 0) { - return { status: 400, data: { error: 'Patient not found' } }; - } - - const patient_id = patient_response.data.entry[0].resource?.id; - - if (!patient_id) { - return { status: 400, data: { error: 'Patient ID is null or undefined' } }; - } - - const openMRSVisit = buildOpenMRSVisit(patient_id, req.body.reported_date); - var enc_response = await createOpenMRSResource({ ...openMRSVisit, resourceType: 'Encounter' }); - - if (enc_response.status != 200 && enc_response.status != 201) { - return { status: 400, data: { error: 'Error saving Visit Encounter' } }; - } - - const openMRSVisitNote = buildOpenMRSVisitNote(patient_id, req.body.reported_date, enc_response.data.id); - enc_response = await createOpenMRSResource({ ...openMRSVisitNote, resourceType: 'Encounter' }); - - if (enc_response.status != 200 && enc_response.status != 201) { - return { status: 400, data: { error: 'Error saving Visit Note' } }; - } - - const encounter_id = enc_response.data.id; - for (const entry of req.body.observations) { - if (entry.valueCode || entry.valueString || entry.valueDateTime) { - const openMRSObservation = buildOpenMRSObservation(patient_id, encounter_id, entry); - const obsResponse = createOpenMRSResource({ ...openMRSObservation, resourceType: 'Observation' }); - } - } - return { status: 201, data: {}}; - }) -); - -async function createOrUpdateOpenMRS(fhirPatient: fhir4.Patient) { - if (getIdType(fhirPatient, openMRSIdentifierType)) { - return { status: 201, data: { message: `Updates are not supported`} }; - //return updateOpenMRSResource({ ...fhirPatient, resourceType: 'Patient' }); - } else { - var response = await createOpenMRSResource({ ...fhirPatient, resourceType: 'Patient' }); - if (response.status != 200 && response.status != 201) { - return { status: 500, data: { message: `OpenMRS responded with ${response.status}`} }; - } - - // its in openMRS, merge IDs and push it back to the fhir Server - copyIdToNamedIdentifier(response.data, fhirPatient, openMRSIdentifierType); - return updateFhirResource({ ...fhirPatient, resourceType: 'Patient' }); - } -} - -router.post( - '/patient', - requestHandler(async (req) => { - // hack for sms forms: if source_id but not _id, - // first get patient id from source - if (req.body.doc.source_id){ - req.body.doc._id = await getPatientUUIDFromSourceId(req.body.source_id); - } - - // TODO: build FhirPatient here, openmrsify later! - var fhirPatient = buildOpenMRSPatient(req.body.doc); - const chtPatientDoc = req.body.doc; - - const response = await getFHIRPatientResource(chtPatientDoc._id); - if (response.status != 200) { - return { status: 500, data: { message: `FHIR responded with ${response.status}`} }; - } - - if (response.data.total == 0) { - // if we don't have the fhir resource yet, create it here - // note it might already exist without doc id - const response2 = await createFhirResource({ ...fhirPatient, resourceType: 'Patient' }); - if (response2.status != 200 && response2.status != 201) { - return { status: 500, data: { message: `FHIR responded with ${response2.status}`} }; - } - fhirPatient = response2.data; - } else { - return { status: 201, data: { message: `Updates not supported`} }; - } - - return createOrUpdateOpenMRS(fhirPatient) - }) -); - -router.post( - '/patient_ids', - requestHandler(async (req) => { - const chtFormDoc = req.body.doc; - - // first, get the existing patient from fhir server - var response = await getFHIRPatientResource(chtFormDoc.openmrs_patient_uuid); - - 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`} }; - } - - const fhirPatient = response.data.entry[0].resource; - addId(fhirPatient, medicIdentifierType, chtFormDoc.patient_id); - - // now, we need to get the actual patient doc from cht... - const patient_uuid = await getPatientUUIDFromSourceId(chtFormDoc._id); - addId(fhirPatient, chtIdentifierType, patient_uuid); - - const update_response = await updateFhirResource({ ...fhirPatient, resourceType: 'Patient' }); - if (update_response.status != 200 && update_response.status != 201) { - return { status: 500, data: { message: `FHIR responded with ${update_response.status}`} }; - } - - //change id to openmrs for update. maybe should fetch from openmrs again? - fhirPatient.id = getIdType(fhirPatient, openMRSIdentifierType); - return updateOpenMRSResource({ ...fhirPatient, resourceType: 'Patient' }); - }) -); - -export default router; diff --git a/mediator/src/routes/patient.ts b/mediator/src/routes/patient.ts index 76df1dfc..a65b07a8 100644 --- a/mediator/src/routes/patient.ts +++ b/mediator/src/routes/patient.ts @@ -1,7 +1,6 @@ import { Router } from 'express'; import { validateBodyAgainst } from '../middlewares'; -import { createFhirResource } from '../utils/fhir'; -import { buildOpenMRSPatient } from '../utils/openmrs'; +import { createFhirResource, validateFhirResource } from '../utils/fhir'; import { PatientSchema } from '../middlewares/schemas/patient'; import { requestHandler } from '../utils/request'; @@ -11,11 +10,8 @@ const resourceType = 'Patient'; router.post( '/', - validateBodyAgainst(PatientSchema), - requestHandler((req) => { - const openMRSPatient = buildOpenMRSPatient(req.body); - return createFhirResource({ ...openMRSPatient, resourceType }); - }) + validateBodyAgainst(validateFhirResource(resourceType), PatientSchema), + requestHandler((req) => createFhirResource({ ...req.body, resourceType })) ); export default router; diff --git a/mediator/src/utils/cht.ts b/mediator/src/utils/cht.ts index 270556e6..09e896d5 100644 --- a/mediator/src/utils/cht.ts +++ b/mediator/src/utils/cht.ts @@ -4,22 +4,15 @@ import { generateBasicAuthUrl } from './url'; import https from 'https'; import path from 'path'; import { logger } from '../../logger'; -import { openMRSIdentifierType } from './openmrs'; -import { getIdType } from './fhir'; +import { openMRSIdentifierType } from '../mappers/openmrs'; +import { getIdType, copyIdToNamedIdentifier } from './fhir'; +import { buildChtPatientFromFhir } from '../mappers/cht'; type CouchDBQuery = { selector: Record; fields?: string[]; }; -export const chtIdentifierType: fhir4.CodeableConcept = { - text: 'CHT ID' -} - -export const medicIdentifierType: fhir4.CodeableConcept = { - text: 'Medic ID' -} - function getOptions(){ const options = { httpsAgent: new https.Agent({ @@ -58,7 +51,7 @@ async function getLocation(fhirPatient: fhir4.Patient) { return ''; } } - + // does the name have a place id included? const regex = /\[(\d+)\]/; const match = addressValue.match(regex); @@ -133,29 +126,6 @@ export async function chtRecordFromObservations(patient_id: string, observations return chtRecordsApi(record); } -export function buildChtPatientFromFhir(fhirPatient: fhir4.Patient): any { - const name = fhirPatient.name?.[0]; - const given = name?.given ? name?.given : ''; - - const tc = fhirPatient.telecom?.[0]; - - const now = new Date().getTime(); - const birthDate = Date.parse(fhirPatient.birthDate || ''); - const age_in_days = Math.floor((now - birthDate) / (1000 * 60 * 60 * 24)); - - const updateObject = { - patient_name: `${given} ${name?.family}`, - phone_number: tc?.value, - sex: fhirPatient.gender, - age_in_days: age_in_days, - //TODO: decouple from openmrs - openmrs_patient_uuid: fhirPatient.id, - openmrs_id: getIdType(fhirPatient, openMRSIdentifierType) - }; - - return updateObject; -} - export async function updateChtDocument(doc: any, update_object: any) { const chtApiUrl = generateChtDBUrl(CHT.url, CHT.username, CHT.password); const updated_doc = { ...doc, ...update_object } diff --git a/mediator/src/utils/fhir.ts b/mediator/src/utils/fhir.ts index ba2c8f90..987fae31 100644 --- a/mediator/src/utils/fhir.ts +++ b/mediator/src/utils/fhir.ts @@ -107,13 +107,20 @@ export async function getFHIRPatientResource(patientId: string) { ); } -export function copyIdToNamedIdentifier(resourceFrom: any, resourceTo: fhir4.Patient, fromIDType: fhir4.CodeableConcept){ +export async function getFHIRPatients(lastUpdated: Date) { + return await axios.get( + `${FHIR.url}/Patient/?_lastUpdated=gt${lastUpdated.toISOString()}`, + axiosOptions + ); +} + +export function copyIdToNamedIdentifier(resource: fhir4.Patient, fromIDType: fhir4.CodeableConcept){ const identifier: fhir4.Identifier = { type: fromIDType, - value: resourceFrom.id || resourceFrom._id, + value: resource.id }; - resourceTo.identifier?.push(identifier); - return resourceTo; + resource.identifier?.push(identifier); + return resource; } export function getIdType(resource: fhir4.Patient, idType: fhir4.CodeableConcept): string{ @@ -170,3 +177,10 @@ export async function updateFhirResource(doc: fhir4.Resource) { return { status: error.status, data: error.data }; } } + +export async function getFhirResourcesSince(lastUpdated: Date, resourceType: string) { + return await axios.get( + `${FHIR.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`, + axiosOptions + ); +} diff --git a/mediator/src/utils/openmrs.ts b/mediator/src/utils/openmrs.ts index 6d3848ee..b09fee22 100644 --- a/mediator/src/utils/openmrs.ts +++ b/mediator/src/utils/openmrs.ts @@ -1,4 +1,3 @@ -import { randomUUID } from 'crypto'; import { OPENMRS } from '../../config'; import axios from 'axios'; import { logger } from '../../logger'; @@ -10,201 +9,6 @@ const axiosOptions = { }, }; -interface OpenMRSIdentifier extends fhir4.Identifier { - id: string //uuid -} - -interface OpenMRSHumanName extends fhir4.HumanName { - id: string //uuid -} - -const phoneIdentifierType: fhir4.CodeableConcept = { - text: 'Phone Number' -} - -const chtIdentifierType: fhir4.CodeableConcept = { - text: 'CHT ID' -} - -const medicIdentifierType: fhir4.CodeableConcept = { - text: 'Medic ID' -} - -export const openMRSIdentifierType: fhir4.CodeableConcept = { - text: 'OpenMRS Patient UUID' -} - -const noteEncounterType: fhir4.CodeableConcept = { - text: "Visit Note", - coding: [{ - system: "http://fhir.openmrs.org/code-system/encounter-type", - code: "d7151f82-c1f3-4152-a605-2f9ea7414a79", - display: "Visit Note" - }] -} - -const chwEncounterType: fhir4.CodeableConcept = { - text: "Community Health Worker Visit", - coding: [{ - system: "http://fhir.openmrs.org/code-system/visit-type", - code: "479a14c9-fd05-4399-8e3d-fed3f8c654c", - display: "Community Health Worker Visit", - }] -} - -const homeEncounterType: fhir4.CodeableConcept = { - text: "Home Visit", - coding: [{ - system: "http://fhir.openmrs.org/code-system/visit-type", - code: "d66e9fe0-7d51-4801-a550-5d462ad1c944", - display: "Home Visit", - }] -} - -const homeHealthEncounterClass: fhir4.CodeableConcept = { - text: 'HH', - coding : [{ - system: "http://terminology.hl7.org/CodeSystem/v3-ActCode", - code: "HH" - }] -} - -export function buildOpenMRSVisit(patient_id: string, reported_date: number) : fhir4.Encounter { - const visit = buildOpenMRSEncounter(patient_id, reported_date, homeEncounterType); - return visit; -} - -export function buildOpenMRSVisitNote(patient_id: string, reported_date: number, visit_id: string): fhir4.Encounter { - const visitNote = buildOpenMRSEncounter(patient_id, reported_date, noteEncounterType); - const visitRef: fhir4.Reference = { - reference: `Encounter/${visit_id}`, - type: "Encounter" - }; - visitNote.partOf = visitRef; - return visitNote; -} - -export function buildOpenMRSEncounter(patient_id: string, reported_date: number, visitType: fhir4.CodeableConcept): fhir4.Encounter { - const patientRef: fhir4.Reference = { - reference: `Patient/${patient_id}`, - type: "Patient" - }; - - const openMRSEncounter: fhir4.Encounter = { - resourceType: 'Encounter', - id: randomUUID(), - status: 'unknown', - class: homeHealthEncounterClass, - type: [visitType], - subject: patientRef, - period: { - start: new Date(reported_date).toISOString(), - end: new Date(reported_date + 1000*60*10).toISOString() - } - } - - return openMRSEncounter -} - - -export function buildOpenMRSObservation(patient_id: string, encounter_id: string, entry: any): fhir4.Observation { - const patientRef: fhir4.Reference = { - reference: `Patient/${patient_id}`, - type: "Patient" - }; - - const encounterRef: fhir4.Reference = { - reference: `Encounter/${encounter_id}`, - type: "Encounter" - }; - - const now = new Date().toISOString(); - - const observation: fhir4.Observation = { - resourceType: "Observation", - subject: patientRef, - encounter: encounterRef, - status: "final", - code: { - coding: [{ - code: entry.code, - }], - }, - effectiveDateTime: now, - issued: now - }; - - if ('valueCode' in entry){ - observation.valueCodeableConcept = { - coding: [{ - code: entry['valueCode'] - }] - }; - } else if ('valueDateTime' in entry){ - observation.valueDateTime = entry['valueDateTime']; - } else if ('valueString' in entry){ - observation.valueString = entry['valueString']; - } - - return observation; -} - -export function buildOpenMRSPatient(chtPatient: Record): fhir4.Patient { - const nameParts = chtPatient.name.split(" "); - const familyName = nameParts.pop() || ""; - const givenNames = nameParts; - - const name: OpenMRSHumanName = { - family: familyName, - given: givenNames, - id: randomUUID(), - }; - - const phoneIdentifier: OpenMRSIdentifier = { - id: randomUUID(), - type: phoneIdentifierType, - value: chtPatient.phone, - use: 'usual' - }; - - const patient: fhir4.Patient = { - resourceType: 'Patient', - name: [name], - birthDate: chtPatient.date_of_birth, - id: chtPatient._id, - identifier: [phoneIdentifier], - gender: chtPatient.sex - }; - - mergeIds(patient, chtPatient); - - return patient; -} - -export async function mergeIds(doc: fhir4.Patient, chtPatient: any) { - const medicIdentifier: OpenMRSIdentifier = { - id: randomUUID(), - type: medicIdentifierType, - value: chtPatient.patient_id, - use: 'official' - }; - - const chtIdentifier: OpenMRSIdentifier = { - id: randomUUID(), - type: chtIdentifierType, - value: chtPatient._id, - use: 'official' - }; - - if (!doc.identifier) { - doc.identifier = []; - } - doc.identifier.push(medicIdentifier); - doc.identifier.push(chtIdentifier); - - return doc; -} - export async function getOpenMRSPatientResource(patientId: string) { return await axios.get( `${OPENMRS.url}/Patient/?identifier=${patientId}`, @@ -212,6 +16,13 @@ export async function getOpenMRSPatientResource(patientId: string) { ); } +export async function getOpenMRSResourcesSince(lastUpdated: Date, resourceType: string) { + return await axios.get( + `${OPENMRS.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`, + axiosOptions + ); +} + export async function createOpenMRSResource(doc: fhir4.Resource) { try { const res = await axios.post(`${OPENMRS.url}/${doc.resourceType}`, doc, { From ffaa7e0618af8298771dac624b49f0d9827d3ef8 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Mon, 20 May 2024 12:46:34 +0300 Subject: [PATCH 09/67] feat(#124): add openmrs sync --- mediator/src/utils/openmrs_sync.ts | 131 +++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 mediator/src/utils/openmrs_sync.ts diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts new file mode 100644 index 00000000..9686a332 --- /dev/null +++ b/mediator/src/utils/openmrs_sync.ts @@ -0,0 +1,131 @@ +import { getFhirResourcesSince, updateFhirResource, getIdType } from './fhir' +import { getOpenMRSResourcesSince, createOpenMRSResource } from './openmrs' +import { buildOpenMRSPatient, buildOpenMRSVisit, buildOpenMRSObservation } from '../mappers/openmrs' +import { openMRSIdentifierType } from '../mappers/openmrs' +import { createChtPatient, chtRecordFromObservations } from './cht' + +interface ComparisonResult { + fhirResources: fhir4.Resource[], + openMRSResources: fhir4.Resource[] +} + +async function getResources(resourceType: string): Promise { + var lastUpdated = new Date(); + lastUpdated.setDate(lastUpdated.getDate() - 1); + + const fhirResponse = await getFhirResourcesSince(lastUpdated, resourceType); + const fhirResources: fhir4.Resource[] = fhirResponse.data.entry || []; + + const openMRSResponse = await getOpenMRSResourcesSince(lastUpdated, resourceType); + const openMRSResources: fhir4.Resource[] = openMRSResponse.data.entry || []; + + return { fhirResources: fhirResources, openMRSResources: openMRSResources }; +} + +interface SyncResults { + toupdate: fhir4.Resource[], + incoming: fhir4.Resource[], + outgoing: fhir4.Resource[] +} + +async function sync( + getKey: (resource: any) => string, + resourceType: string +): Promise { + const results: SyncResults = { + toupdate: [], + incoming: [], + outgoing: [] + } + + const comparison = await getResources(resourceType); + // get the key for each resource and create a Map + const fhirIds = new Map(comparison.fhirResources.map(resource => [getKey(resource), resource])); + + comparison.openMRSResources.forEach((openMRSResource) => { + if (fhirIds.has(getKey(openMRSResource))) { + // ok so the fhir server already has it + results.toupdate.push(openMRSResource); + fhirIds.delete(getKey(openMRSResource)); + } else { + results.incoming.push(openMRSResource); + } + }); + + fhirIds.forEach((resource, key) => { + results.outgoing.push(resource); + }); + + return results; +} + +export async function syncPatients(){ + const getKey = (fhirPatient: any) => { return getIdType(fhirPatient, openMRSIdentifierType) || fhirPatient.id }; + const results: SyncResults = await sync(getKey, 'Patient'); + + results.incoming.forEach(async (openMRSResource) => { + const response = await updateFhirResource(openMRSResource); + const response2 = await createChtPatient( response.data ); + }); + + /* + results.toupdate.forEach(async (openMRSResource) => { + const chtDocId = openMRSPatient.getIdType(chtIdentifierType) + if (! chtDocId ){ + const response = await updateOpenMRSResource({ ...openMRSResource, resourceType: 'Patient' }); + } + }); + */ + + results.outgoing.forEach(async (openMRSResource) => { + const patient = openMRSResource as fhir4.Patient; + const openMRSPatient = buildOpenMRSPatient(patient); + const response = await createOpenMRSResource(openMRSPatient); + }); +} + +export async function syncEncountersAndObservations(){ + const getEncounterKey = (fhirEncounter: any) => { return JSON.stringify(fhirEncounter.period); }; + const encounters: SyncResults = await sync(getEncounterKey, 'Encounter'); + const getObservationKey = (fhirObservation: any) => { + return fhirObservation.effectiveDateTime + fhirObservation.code.coding[0].code; + }; + const observations: SyncResults = await sync(getObservationKey, 'Observation'); + + const encountersToCht = new Map(); + // create encounters and observations in the fhir server + encounters.incoming.forEach(async (openMRSResource) => { + const response = await updateFhirResource(openMRSResource); + + // save to map to push to cht later + encountersToCht.set(openMRSResource.id, { + observations: [], + encounter: openMRSResource + }); + }); + + 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; + encountersToCht.get(getEncounter(observation)).observations.push(observation); + 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]); + }); + observations.outgoing.forEach(async (openMRSResource) => { + const response = await createOpenMRSResource(openMRSResource); + }); + + encountersToCht.forEach(async (key: string, value: any) => { + const patientId = value.encounter.subject.reference.split('/')[1]; + const response = await chtRecordFromObservations(patientId, value.observations); + }); +} From 9b7978d0005fcd6f6dded6586055a10108e1f558 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Tue, 21 May 2024 14:21:46 +0300 Subject: [PATCH 10/67] feat(#114): add sequence diagrams for document endpoints --- docs/sequence-diagram/cht-form-submission.png | Bin 0 -> 155172 bytes docs/sequence-diagram/cht-form-submission.txt | 31 ++++++++++++++++++ .../sequence-diagram/cht-patient-creation.png | Bin 0 -> 124019 bytes .../sequence-diagram/cht-patient-creation.txt | 29 ++++++++++++++++ 4 files changed, 60 insertions(+) create mode 100644 docs/sequence-diagram/cht-form-submission.png create mode 100644 docs/sequence-diagram/cht-form-submission.txt create mode 100644 docs/sequence-diagram/cht-patient-creation.png create mode 100644 docs/sequence-diagram/cht-patient-creation.txt diff --git a/docs/sequence-diagram/cht-form-submission.png b/docs/sequence-diagram/cht-form-submission.png new file mode 100644 index 0000000000000000000000000000000000000000..8faf098adf7a3363f3966e7bbe7d5c747bae5826 GIT binary patch literal 155172 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 z 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
0QVCo_gQl6C5@a*HITL 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 O)j{f0N+#S3x@hoHGOM}w0ItMs_l$EK-9^_5i<5 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≧̸qKSRuy}V@`v6`tg!?H!X$jB?mga+Hxe={4e|jy}RO}GH>khj&YOQ z`)(O(7$xUle}mL@7x>PkZ*OinD*GzM{$|;yFF0lE7p!mFith>uo*i>AF=G_@o>9Jn z@u@O0xX3`8*M3R8+fm1)bL-}JG}N{fzV+`e)vRkV!4)0mCV!ZF^P^7_`;{1P?YoW( z!zbE2A57}~0A8&*kPWDUnaLy&BwWNB_`j&{{WMS#H+3ZGE~wUB^*D$(7Ru)%2*>_7 zbP5Tp3X03NE>96YHoWE%CrD%qJr=t9JY{M)Rxjxc%p*dY;%ZkCM|@f03=?a~QDKX0 zJbs8d$RYh}ql#nMf)*-bvrG%fpPXhb#D)z%q60E!{dpFQU4W z@#P;T##!DGzu=ntsL`NfDEJEdTJ}(QOb@H39j9|ev0gSTU8@pjt+B!o6k}z_hjm#* zH2KfHzM8FWdXB+M+&Fi<@bf|@m#U}DPyRKHwXeI@Y>M=FkgVG(zQ-me5AfvDn<`Qd zC29Dl)lb3YX(}qV)`m9j#rG!*L0J!!Hj0IENl1d)V#5F5J}vI*y~U+n<}8|bUcEIg zJ{G^-ob(phT`lCBiXGMOxBrI=@I67|?cOf>gHy3;FG`DyX&(BEH2v|I0J=ENwjp2+ zlrki|u=RRCIR1bb$8+b%OQ8i9$`B+!Wl!$!9iofGGFRdQx7g|~-B9!TIIgzuJ}?H& zT?iopf{$dgBOZVjs76VziHsk}8m9a0ez3X2yZ4^SRrcQ5msU3w@8nPq>0J05TDvu# zPyVw1YnL(YQFo2>ndX)w9XHz^8LYbXikd38e|>^cCQfb^S=rRzL2`Q(`e5j+Rnka# zp<>0)nG0+CMN?vg9#EYhXj*VE58wlFMf@TTntq(>1$Y`%HuIG+Zv&ww3JKHHS6jj%u4vNRi5LrWk}}I47W19X zIbG@!qW*(8TJgw?8*h;!?ODDr#!JDcRK}D9Q7P1S4>m;Ml*`~jI>dua(}8AdIrE!Pwo z&uOIB8@lOrf$2PgJQPv!$qq}k4~}E^4WZEudJeBiE#YkBPsk)HrBdx3j+9r0GuOmRo-($26YpDu6|frlZ!_a@W26m zXxBX|&_4z7!3#Rmfp5dL_ok1hS3iw|M!A6eJ_? z#1KtVzmR+lf;slrXOMqb?JD|F&z@lzYO!BC^AWT@{JC7rXQo$#_1-qxVxKE5?!F|6 z>a}$hr3Fm8fs6N3~0TRkDkf+-+uW2$5 zWbWJp0{xGThXGAUgHa+tdqLXf32%^HA*ARKd#W|+ZpEFQO?PDDn~_kdY>#g^sqL{K zshFH+O(%|-0pdB7$3=?#t7{9phxvSA>5sqhQ3c&ARQs7oyuG+tWuvHEU%gCLsfUg~ zTxgeJ$Qy|dw`f}JDVj(;yBaiQSu*wQp*!#j0@?+Sb~qkfz3{7x!8|_S^@+IF(M|~o zGe)YEO(fCK&H&4Y>%^E6d$@|FxEYzK;(J-;B+OXlc(!m$nPah?W(KU!=sUYZK8^f( z|MuCaj z-EpAHaxBpE=?+=)H%<9z-i>F#V?K-7vDr?3u8&wnn=N ztek-YX1svLoJv1+yjuaeQ)bs~S-q=J_%NK=9bO)6+MlY{B}KN=iMLWM-tkfEb(( zQ$A%X1!1>yo0Aa%6onkdV3_yGC`XRX8M-FR*e9eCIx~u$ko#S5qL#k7Ua>2sdhczt z#r?E{2IEy2MCQqI62~@pvw7$`UEO&q7Yrx&EwKK60UtaAgigvHGy=Sqkzxv^B9N0` zD*QNs>K>Z$MNs@y_P!R}Z&JmJp99)t**M>0fe}J%E4Pw!zWierY*fnDms{E*%~QgLZvc0OPcz=c zTSoN}HpM?cTVA%C4WDRx$IDFmi^S%IEtWK+iAONi@Ms@0W`IgCRs6z=O{X^L5fZ95 zS@mJEtpsgO#O^nP^7o0@#R+SYD=|9X^|nEO2y? zf?Z9>%9djI9CsN|wOSDs;SIErq7mW8CD;A^65ltF5kKp#+aQr@6!PVD{5>a7|2F7h z9d6N4RJ1br8{{k@eNEz(6n!8o44GO=bRoOE{E5bYi%h`qaA&v}O39AB1EG$hM@-as z$FFChp8<)?{TWSC1prBA`5!?cUc^ittq}H@zJoysUY6fCJOTn$N|J4N$_&1hWhEjejE9$Q1mkwV*F%I))rAx}0e#Io$#JAMvW6kW z7wZ7L1e;yP-}e>j8}xdDw3*#`XBL|=cau0+Ooe!;YyS+r@!D#;#b9NWfsW!eW*9y8 zJbhbQe6C4)>4o!76b*_6@vR~V;>LNe2$xS#>w#r%<7sj&rLZMF*V8ZevW{l8|E4l= z?i&JjVhME0+}>AbN*FHf*n_;@Y?(~(vEO4 z)|`e2D)G{`>LBkoHvWWb*LDEx(%|m1Tz_ev35ehOMpt1L+0KPwjpVcDPjkin{pxLt zo7!gQ664Eyh8t>oA zZ6rI<>*>E6KPt(krPs8fmr^Y;VyyHRxK8t}#i&dtw;cCLm zwkj1qqO%83c(-ihteV6T6u8@1Cs_|M&y}#YxGyY!4eR5-3@fxm&7OxwfKB#Z{19Jv zIyM?kA&se_;*#T3rR&3+1l{i4#FrE2#*G6XWIUZ z7+9YB%!Kn`yHaecCUJs?@W|L?4GwcldmJTz z#goOmCN?D4H+_>^u!cD(0HnpIo6^{OF9ue<+N6>EL?#I4W8d(V2zS2E{F{);mj!&@ z0k!mUAd;ZmLlG_iV9|BcB@ix*5_=^bF#$+TlGyQkL`*PTh>th7e0S^gSs&V|WyNXC z81*W6iT&l+O71@EyD565KIIlRhH@j?E2`>E;mN8c{6kCvEWG#rdA{zvpcCT9St@@N zh-dXiSFJ$*rgv>rfJn{XMe*TqP%F^^A-;&-w(+AR>ACA`E4xobP92n>`m+bHNVXY( zNSyu2{bPUn%XPmkFM`k|Ny#bZWr;j7b`2lVz>~(03Ix=n?w<)-$ z^(W1>BzXL`!=F6064m)`yZ$GNu0y#3{6!U8Z(hBa7!J*8#;PFMjX&A}f28f1JGh7E z|LJw)(SxL5TV0v@2cwd~lZjmYfCL8Yy)is38f$(ZS_2HEh7B+}d^&PRiP1au5McOF6yU*?{GM!^vEA#nIpRS%mllT-8v^q05=l-5|e z3UD4~)lUr71d&lPj0y>qZ30SLdYwt^;fB@AgQa2m-yxul@Vn0+x~|*oic^;U0@+T| zy>sL|1~RYetSMla)L*C)$wPFqdYd_ejT-nC7Iwx+x_{O6jh%wNx-&)i{Wf>l<9fHf z24tQV@&pULfqTWo-HfWSL;)3}^MC&w1xL;}q{?(e9cQC=1mtkuAvw(8`H9cRL*OX& zKw^pdKS;VWy`KAJJPSthHtk3%||0S!(^NOucpE69&$b>>r36ZDl@94`slo`qSKAMmN3gR0Yb*>8w14&Mq+ zuv}{;kS~eq$OL&ZdPXqDNVU(UJRTmXJ?@C8^LD+bfSOVW6RxVEi+-IsT7g7QjspjU zrl7`r4sAR8)wTp2Zsh_r8A4+{&nwSh`(Pw91q?}9+TVkl9N7ZufFj`5se*w2!Wv?W zuRB+w+Bu27DCGC|0B9V`l5PeTnC!D-Z`Pq3d5jsH@hv zd-(aye&s<6&_Nx6|8Z1WW^2%vw|_)a+~59uomjty8S6!5z-tj8P_5S^@O1(b5Vvjo zwm8fQsKk*$2J0^lgD3^51K?j0ne2l~oJ`2b%wWKRS7aTHpWlRhpn7lbcQo6Z$fn9= zqye^W=I%zb-$AC-%#vc=;Y8Wp@d`>tsV0;7Z9L^)XSC{UJFCXapLc>n0AN zLZV`_*6E3qc$OGtBw^YsFrTcB)pshWK(^JaPJWCbEavlgsKRLJFep*Bb{$;Lz(()j zDw{u>S!EJT@0=?Ym+gp6YLrsGo_RJMa%^*G=w^6xIMkfmP5cer$6j6I{?R6t^DLu4 zmUspyG;b+%!1Zj&A7aFC^n9BD>XS>K!e0YMhX}Q|?`}mEzsmUo44I=Kl+X$2P}sR! zf+b}?uVg>v1RbKmsK7dOZibmqlzRV|uJY37ePQnfw(hz7LgC4XCaKw;(J$#HiE0#9 z4OgW#m#(t&UEs#0L98CT5qo(`u*ohMQ4~M@+2EPsIKCd|pkX3&7e~C%eGQ%=PE?Yni*c?imbZwxI=|e5U1&0;b=t^_w8xZxVcL_FZ~u0LOIf z@*#f_sbM+HU>!4oj;r`@WV8LnF?Uk4AtSbyl{M;k&vWs6Q4byCp@wQ`En9GDIX(mY z@l?nFHn|>x`sp&H4<)^fkja_*u`>#4nE$*|U(6fX+a!^N3&2quQ5_ba&hCIUTmg2# z5^-zPE&hyzQSNVXEVjh9d;=QT0$r24^QK9Hu%&WtDx-d@RnFe*WyAP{7Ft zmMc2B<$jwc33N;1x#s5d?inbLdI7OrF8B~2ugFsRxUknZ*yv`2aEka?i*b79 zuli%DawBH&vzyp(k>h=(8PA@?9VmBG0MN<elRk_ZxS~2$WB-LU{5*i*N?Y^bUf|EFGUVJZ0 z$L)CC``*i-sa@I>3nHdAN8XPQ zPVK1*#NV6C^VKY2J9bI`&)43*uD$>o&Dn8+p;a5BQT3hri{WQtjks3881P+bN>j?C zH+#Q4{{N7z-9IY-zA=@zP|*!Ke@RJ+Q}rubSi^6S3u&DCO<3 z4zb_m7xvVMMlk0Mi<&T{(Wm>_Lj3%c0gYKnuO+6c@XWtZt%e#CtsFy#xj9O*otBtb z4l5k}4lu9X5F3%E4^XCD)-rIK_N~T{+CuYQ!B+4sj6Kb1tveSLZ^!B2S9yEuDyHix zGY=)ce={KoU!Y!6cGQ*W7yY>)d?3CFA3dkkE+}y9d*P?1>9Rji;=VlkOlSkE_OOt6 zf6+Yg;ZgISprGXLC5@aE&0U{!-vu2wFkAS3;s9)g90{@tXWh8pNaD`8 z>E24=mV3+{z>!^&=Uw?y@-Rl~(68yY8HvV3DYvJ)lkFSwFC}$K#M?0UXiG%X5q8gg zB86btO4uTjw2W&ie)aFxESYMu5h5xNifH;7oM>h;qtt9kc@*^R;BzwDvCZeZQ|dE$ z*OGdG=XFD4v+%C-4W|m!L@sV&%j6~g{0pv{@zN%otNu85lfIuW;XWk%!6AROg4Y9@ z^hQ>%_WzXCZ|swOg^KNw#V#Gr#FK>ka{0p5pXfAtf4r(K>(uM8U~z9Ca0$kW)czGF z;}6s2?}Coob_8L?PY}J7G`3x|NQmy`IFl5eRjw6wV>_N^PLfOou2i#Zx+S zOpELaU`EGuL!x`vCYk;Yi253e4UgM5iWOLU*%{%-&RawwQ>x**U#Y#jItQwW; zN%V(~BQJ9bc+*10Ynoje4Cn}a!t;OjxGE)Pd)5G%#x#DJ`u`=@c^NL~>U-TN@4~{*j2DAcHwrCL*bGDd4ue~HW@X?Pr@C}@ zpEw8~^kYW-FV~D|!SzZj32L*Ry1DHeC4(@+L+8e-j)QF%JW?#%k8|J`yf}2&n@A_# z8{Ypa-EJDtu{BBQ@;k;C6bt>3U*$D0(2h8P>lFdydbTi)M$iDG$qH7iGt?^yn1?ZHk zR?jXgWLrS}-+63J6%OjLPmdZ=$LHhu!Kd>_g+Mv1zT$-(=r&Qk^fq?SJ^V0V z2{d)w5IG59)s8zww!^P}N$$O6B?^*{wL{x<>`U!_O-Y0hF=CRhZx}M5F4O7hL7SeN zmQ&T;YioW_KFH&3=YXxvT6o2|S%Qo#TO+3Dl(2drvRLW-lGrG=qv$)N8B&m{-c0-q zd36TNp)s!Npt7!o9HBJ{94ZNy1t@5*a7&tYfI#DR(2TT+WRrHV-C6y2E7M-tlI-;Q z7uru>`~eIxZcbg&7S{kd>2?IbcT}|btRNA^c+>AdUqQYdejMU@!FE9Lk_Y^b3aR+i z<$8f9P+Nt1wl*Gwa^eeguzrUrL5iFT3_j8=*|9kY+j(9l8|!igj{r->Rka?j_o3Ic z0!^V1lSC72&T4b?rI%G=n=T@Mn#Y%{P{){5Drfj<-Wm5>0K~x=vlK(XXiwk8w)-A&R z%kXUv!?nsJBOB|&f1OdX8_=)Dpwyd-xya%ax&}jtJrB>Ga;`sJa-cET{&o}K1FZUs z#@#gJ2IloRij}qoT-n2Xdcx&V!f7%G@khEjrM^LJssPG;mmEJpbCXXgw7D9Z^FafQ zQhq=*W;)Ik!MNydTZRimoJE*~B!PaL!oQy5A}tpfb}(^mx+-GJ7D%a%VA?#+-W{KL z7n+K)#}Zj~6G{1uQ)`f0Z-=DazPpt|=-0vFoQEL42>nk=XX@Y(Cb$6x# z8eF2VU5|rX7F)gnT`L{TOp0sRonp~g?b;>EB#alDf$vX6#UG^gVSahL;EYR4aC?^X z{gc<~9q?9Vb|nO*hS^ADQBHfy685+uMhVPDEyensK4Hmk=S8 zTQ3%~&FStD%X7#E<3&VGbWYS*L}KJ@OB0Npa}&o5RrP@#Ztl>Pz0(?PUf+F5y&ZqIRIl->*?%Fc;1Gaqkf<{yNFo-MdGHtST!Pe0!mPCxS&jZzLO0k*tnh;>V7t37Tt8lyee(gl_gH)*>Y>o&r70@}mjFJJB`p>^qD~b1wwrT)2=hS0ODQAcg7Ev7$t=*fw#0XB^ z7t(i&og;n;k0!A@h=tADNbd$EpwG)H`Qv_KgtPc40<|6+HF3p3Vq3R(XdYu)Jw>P; zHzq;gs*Th)AQ&b4XyN^M?Z-bU#d+{={8)!pc#Rw7pN@bGhfX&e}%?&eb{0lrFjfRw>o~8||Fk{VwUcga~eKM3!9&7&5|seSXE-Bnk~@8$OKX zq9EooVU|}utJiW`fVf~wlZmJ90!hO4LM9A>N)u(#yZ;wWCtQaWRnsxO9GCau2+a9g zm{hj`7%6wZ$Uy?mUtICNvFzxY2n%-UOo>mkF6ynf9k5#QXCwlk6U|5Mjo#anA7CRM zg(Wy~zohU$yGw!)zr)GU>nkwtr+DqLag-j*@(DrSLeW;BCVu6)&E~-$ydJTt89rA3?;%dkJjOL=lNNeb)H0MB9utZMTfoSCU&68vFQ>(db zNy0fJIp*8L44a5w0CiM#Bd1K&^<$~GtDnRgOs zMT+Bs;-!nY!g0`b+u3V+EWJKXk>PL3zY{Re4fS|_CI7N`bapRj{FIWI);hSBf9L2) z(a&I;8Dv{8I%j_fiWS*yj_JoXtH-^+ajd7X`X+sBFLV{Q3E4VYTe)_q6>fSR56ok2 z%^%H1e&p}(oy}_9nIZ+Kl_`p-g~?B5`}`AbsqN28LcoWNPgEdtAFlG)@ygi(eToce zPX{U89h#r^)L)0d>Iv@{D^rS@vZiIupY-NeT*bH@ie3C=-GsHG<-ejdn4GuEVLry8 zRequ_Mx;EGH_5q5MW+8kPQssvMPs7!4`E5^2nVcxqLz`HYp7oeU>?%Hu+i}x)qCd= z3Bg#^<2IAI>)5wGLI?gw%Cdm)iDOJaJY`e=nm4|mOL#|rsPdzstlihC?FRBd`LQd} zRVzuDF@EOhh7YIyqO*Cu2*sQi{mr^E)`-sQ(2 zzm`)ET%qQeQ$w*FN6cOIP<(G$^qIt^v}5iZkath0k%0c~1xv&4gn;In0}X zaG%8ag^kv?yn2n>QKZ<is(0U(h@bdk|8qTZ*zaYBRQe(3`FD znX-E16(O^+Qhq0!dENgIxP23rT@D94}S zTX+D~w?CJrUXswu)^RBDef^?-KdXisasH&NS*0V*W1k`Pt3$RbxBcYQmtehT;465m4A!I>i(fLMhK;`&^{bckX|ptaF{IR z&J8tQLh~IL9unyBet0(wL&kRjPZcc|}FM=uGW5)~7mxr*mg@ZD&buXgSLj0fAPZP&OO8Pp%|pcF7|L^qld zmURE#RRN76hCRd{!Ut9_Sz8(R6_~Z*xJ97 zTCc51G?0TdGczr8GgxAKp;uThLsl^j9ghEn|3KZ!{~(&7NU?&Ka%9Z1%^%oURG=yr zci7#|6f8;5ep0RC`7(Bdjg_-I*>ZX|kFk9}I z18&;a832-8FVs~~_X&%0N|@t#1lwMaTzi?T6Q;!f6wT>OLNADRk%Pr;Y`JX+M&z_= zSLpv8rA%uSa{efoHkq1#nAmYNK`LbnRNQ8H?SW`C1-KAzH@FZkJD-$aMfZaSV>)p^ zi3@?l;{kseQ&%e={2kCCY1(ewtZyN@&dB^+-dcM7$xM^v7B+wd8kL`m6r@3IZ%flY z)_A896?!QMn5f{|_^SZ`H^?8-QM=X&UNZTb_L=uZ>v#gt3$+mFzy&bK!f;_=2m0IL zGw`63*AFFMV$4CK4O)S1xxpLr!aE(bcY~TUbV1VV-7*g+Q_(xtMf3wt-mcH7;YkSW z2nrZfw_X<&UKo4)^#<(E14xxP^;| z2h}u@f1|%Z>{i4vKt14!@QcGTgIqjyNTtd4%D_4)yADlT?>~|{x&h1rpd$qTRfTjH z-dVEhjQPWx1r();Q4{%w;?QDrdj+gZ_!?lApqtY?34Fdg7uL?B3U>Q}y1mc?9*H^R z51c-23>?`E(DNr9IFM5aW(nlc(Qs1pF-8EpWakP`|SE_{QvS6+MCjH6{rE^5ONYSOo&ETO2Ts_dSV0$Jqe3B(wh*fe z2hqlxY6;IwG#>^N+f=;qrp`>20x7^mdfuGVg=^f&ID2*n6mq#uGJq{u=IT0w1i>NI zFo)d$2>Lug?SfKCMh}+qF=zeFyNCV7M;N<_UE`<-RZZ+-7)o&l`$$LP&c71pn zWChIPc%)yH=aOLuW)g1zw!wqLZG{pkt+!-fww-(#4DXQSBEV7H49ep|==r9?{Ai)W z=;F@?zsj*ivk^Lrt*=3sL<_XrEwa?i04VbaUM){yoF;`+>(Auy{racUAVsgFTfCR{ z10z9y6v!Rqc9{PA2+H0eAr+1AOxNsRQfWY}190n7Alm*rU&=gPH>MJk7>-671zg*` zIJ^1a2)L>wi9{u=pX+*Wtw33B*hooO0j{%XB;V0Ok{OhCV3HH5hOc9k&EMgVzUdoJ zUmsZiqu>)S0~^*riR4OIO9K3=QFBUU2ihK*nXp$bZoTBXmHrAk4R50YG?QVh0^~^- zOh3B~g>CE8Q*T-yKrFq>>IY<=HvT3ldRTt^R>=QXCmEvkT{;6|6H{u zbpuJvC|c|T^lFmfHR%PCLX zINCm#(7n}J%2UXUdHtWCDhQZpo%=O-7X9Eg>?p@w@?vlwfZ*lMfF9iV_jCEZXI}ZrDr$Wh1w9dmo&hOx+H1(=aZv6$F%3>{L+MW7!~%bNcYs{pTM{+Q?UHbt4L6Yn%0 zfUY}`?x`LFZ9iY+wZTz(&bNLzrG{lOZb5q=KxGej$Q}qU(typUfl7&{Px7EM&+O(1 z5C}Dcd&}vmeoK?SyRbQ1FB^d1Lm`mudm2N|ngMF%tHb-GAA-jg;|AZMn~cca732mo zpkHHldAqak(4I31j4xpO9#^#P|2J2xvJr(`8YoVB5b)soN~M&?Fj}CgSK&h3_Y4ac z)$(=|bYj%5ip2l#lqDVP?Bk1@iz-ANgq963pW!sc9Bq{(No2vpw~-4Td>p*|pH@Ql z1jM)`&ZGC%Lzt914;P7ox9DUvj$dqyU96jRPju=i+PWvKo0}C40ZlmV$Y{^A-bc;uJcG9N#eu9vyKZs z$E#qc$=%^6dmn}tR1XmI9{7EZT;eBW!}MpV*9v0cxu#4muF2V#B1KDNB{LU8?+^18 z<*pAMHn9sx2y6K64I_Fjz!nd~>@O4V{y;hX7kXLiKH!i-#$G!r$t=Mmoa1LHW1ah+ zWS^`*AphGuIalh;$I}>?NT7oLVTn7-w^ON;NmWKXRf4d**_JF1H+&pZG?98%G{n~w zgLxKcWSO!)gznjMfE&2)efAqsufjYucO_mPNs5DFK>opIK_PP2 zL8IU^5Tx_Y!4zI0k~XC5C_LHB914v%yVdaZJ)4n;IFQ=EQM<9bn_?02IT~jt3h-Zk z3U}WUba^%t;%#0*$A^)-9t)S0Ow})d@yQ-AM?1GN_tf5Q$CyI@GdKkqW5AdRqs->2 z9*&Ry@dyUw3c(_MT4;p6opsi&Q`5MA{Z4BIB1ThD&K}|$chgYD#T$>?)_>D2NrxW* zr!zj+c#7+7iKDmqYva6a6jXQGecVr6bbAmWZTJ*0mX;q2gd&Ee8z!`v{CyBZs zY3|RgB?-k&={rA#?gPF3q?FgZ&*29yB$q9~H37d5^B|uhNWA6NfE@ADJ{Z$sJtiB| zz_ERT_Xk}Qj3oVl3Bu*`iLzgzb!lOht6TEumLu&E@Y@1tz1u)QiTH+J&W_Y5jr5CddV_4LEw2 z!Vp;5-XlRBI8iy^X^Kt?Me=n_=!Y!B^Jbki!)OxIO%n@dFrF64KPlzJnkVeoL{38K{q@MJ6ov z1hXbOfSB(6wZ%>H^;aI8NvoH4C{ICia)yB}QVPj1EpY}@!e+?k&yHu0_YOhy7F~s zdkI@V=}=1gJR5c@%fKX_xqNr%tAk?3HKqizdK?T;?%D%{zfiIs_|XJRyvAqT#P`-; z%IN)uyj>uTcTLOFxEDA|7SfbadM|(OkeF6`2g~qp&W}#b?=T(TUK(NC(D90P9tTq; z0Etb|hrpCPJU*aUVqbRIKsH_0=CR3+j=Yrc5Eg^eZEbBOmKE=}ryx&4x2I1{U6Jv5 zDsHrGW}9zHv>g)?7UmU}Tg9Ar0CH3<{>p_owN)bzn`RfFbL}p)nNXq=B?*r$+xYfkO|qQ|6_X85D@3&-&sKA$6+Hv5lQ7uC zcVSixBE0#jy#=NmhITVB*Cqmts4>IuZ|~mn9jr23T{Q@19(&NH3&f zQu*33$(B4=$RX~1qjCbLPa*oS*X%EMQt8{3s6Irk?<=)W1DR$s;C$O}X>?40`&xtc zeUY=?7#IiODJ;3FcFZ=iNqx)|a}v_fss5{9>fp9bYA!-c<^iEhn|Zn>d4S>!KYf9S z8Eh_Sgv^Nhj|0_*{@5XjnJl$q1f*1@6nmmLsb?><1_Q{aIqNQ3rPOx_vPb*@C8cdt zAtxR}x;omjV>?sy4aw8AGDhD_|%nz1-BQsBy_SI2pR$=FnTU zA7UQsHQBX|{PK@zTCWSB0O@uby001`tNoyJU&I+%%pDNj2t>&QRJsGAF7_SIfX0}` zrsu-fM}9f!mCRg$4gln(%XE_DQ$H6TH>TFg$JOef5|sC7QkVuRzGMw^*Chp|u=c24 zP%);B*n2l@1_H+cq|B8iNqjAR@2$1n*3=|wI2)>_-FdVa9U;4gYPv6S+?@c4y)d_; z*fbBkEyb%O&h@d425`^YId@ebe44Ds!8%0xV++*q56m^Yyx~ui_>bK)#w@L41+@L! zbe|=m&!$->vQHD9IpatLM%{o z5yV#E@>;}#2u~vUxb31Va_*Dn?L35Tx#5m1b-+0rz0n+*bfRjT<=WrXzMDIMp2?=& z2+xR{{!!6+!1q9!Q(o`FGyBK>rjYNStRdYM<#`SIqE~DkKbe2FISGKkRJ-$+%*XKN zId)TaZ4{_geDpibK7eS)=73L|c17`;ly$Cd3m(MS(&!mBb-*$5f>*5{ zFNRDIO^~=g{UHIB$1Ecje_eVHKhTmNA{c6g6)F!>vn{4U=G188xt}+uKQ``<=U0$} z6sk5|OMWZ;L#*bxABDpguc5o!>WL$GdL@X~3y-%4u4@Sn6EG*K^``9+BsU5&8d@Zf zxvPaMbU_~qe8Cd@V%L{dn>A5H%T~^@VmojN(t-app_>p3*p!orI}GC^30kt{O@JFq zkN6&(fk6s%`=^zGy2ia3B^?sx2-XR5clcqh?Ergb`4bgbbkQC5(;LG*bSwhYyfXxE zFOjTXSC9!a=p}r^*ebsOLIk#GOR4f|Bci=P`lz#PCVn$yYb2A?!PE zGbgw0XUsg`vj6AHZZv-IJYzkfK$Yytb9+@(lL5Q3l+Yl(`7yOA?IEDaF0|=#1{O6? zTQg|_w#PYbx!3gj+7yGM3_zKZaX`AF6L?EOUcIcb$Hv5-+f_Ei1cBYiO zAid;ofy?B0z75()CX(VfT?nw;di*i}_m=%^9Y-&Ww6Y3<&pDE9hsG@df^8b0H7;6u z<=N|xhJYo}&Y1Y#v$bx@H+TI<5{Qw=-8_=F@p9E$hc7epNshjjCo_CegmB^LxuN+3 zsfnKwdMDqD9#hvb?qzgFPg!XZ&}Z>er@yaTEFieM(AIH37RKIF;3i!k9Omcu?_&&; zF{a;D8oH84*SGXmRJHc_R<9%Z2AGPs-ETV>9BqpfuW-Rpdcc@6A_Ws0sVbhPQ2$?j zka1T7HZp}Z2gvz7x2$LW9PjDSGi`l+4#PSa)eJ$AiJl$4UIDMUcZH7An^>IyP#bW0 zRK6jy((JXdO5iJ)hT|)fjF93fwkt_(ZEG`w5fa2~Gb@k2g>X}o8_?iKW!NM*d=y${ z_m}&KI$nZZ^OWPVs7%C~{{sp9LUVhSf80-@9Td*^7^_S${>C`ap$XWkTH18N6?om? zbHB9C=&ZkP?P$H%^?Wq{s-5#T{y(q?AaMveGyhO^9DcK%AZCEEK_C?03*RWl)9WB; zU!zmdjsK4BiNXOU8;syLHpDNyyVT=8{Q~8CajZoZ*N&uy&HZnLO^~fWaAkVmY1m|@ zu{-uxBcr4BX*mX)PQQHhtQiW&Xp1w)7-MhO1P+|`W(;ha++9{m2%v5nlQ zyDXx!jtGOkGXS{^R}>yjqtQXdYfhZctKmIuBa1!^dh7omEQb??;AS(=E9)82^YsTb zHvH8(gZQ(x1EEzr;Q?U9S=Rgze%Gk;+f59FeZbrg{2^dHm^Uuf@EtGID2I#R9=vH#f4$3M zK>Y8P+ATkomB1w$!uN~WB_@K~=<%}L(OUTk5FjjW2z&A2=zyR{Q-dkpfRu)U1<>CIV#H20K=9G! zNWOShEdq+!4wwm%0-|{8c4yVym!hw2rAIl>Lh6YB#=E&E@t==S+h*&vm1OWS?6+$P zL=2eyyC_`buZM_xpylZRjiIe*I5c3OtPK{{@v8s(uB}L3S?lcgoEq>(LJ-Fay+5PW z4d^abaFTR4Tz;GN|GTWeoG(qs-DfqJXy1rN0=K8W6*YIii$V}-`fC)2ZWSfNGGtpw zKQzQ}^G*P3SOf^NfW=6m`N_r5Tmy_1+O@6Dp4ZnG>dAkfUF;EEzUFCNfg~8XysYS2 zDwYZ+p#L{Wt??gBrtAF*d@l9YQF}2x?e1J~#~=c7kusd==yuKodk*K6d*Sc7KR~-2 z1M&V;P#Z%DCJ?g~KOO>5Ee-1W=O9Ax1t1re;O&GZAmZQb+5=45F`#RKZjIEF7>+b3 zg=41g_Mfa`XlAy^aX}DEW#g}`a6L9wO<;+C|0cL2;L_(Bt)myr3_$k$dF}t%bop%MV+IH6MyrBYDzc*LVH42}+6%fN@+jd!-G?O8wE9n>Ka*1P|02olfYl|k z7MYel^X^}Z;5mR9Gs|vJ21YM?!)KIyCTFXj0pneW-InXITkBu!uCG)|$xg`{Twx2?IT&NCxhZwvAPt!SYzBQz zeiFQ8yWSa~@rwR(BDs=ALBf_QdC^Wbnk&xdptdxm^)nExZtlHpRm&o|&H~XGZy4Z(BH(G8+-B)) zK$y_rnH@M?*Z!Na48RoT)W~QY)j29rgsx1pz5;aRJaavIRu$;y0mdV7{U<0A{{oM@B?VqrkXNS3|a--K*vk~FEV#K5QJjR&2&6GGkupbJ_zX(lFO?5H16adCWyjLVoRbxEAKhepOK>q+f^Tfk&PUDpj@w393QoA(Wz%WmOLP84j zX&*J+L2%v0c%o_h00zm~nukgdWoVd*`p~YjmJZ!;ECG4WAw*~r)SCgWI2q~iPMS9s z>B$2AteH1?G#-(X!B3q;g10=@juwERMR}e>Cs6^PLwVNEeLSF5N(#cnmBu%E_1$PB z(}AorFeZ5RJ!_FVv(JK@x-ksG1pU${oh)#q#x9{tuMyAP;8v>YNwp^A$mX+_QC2*F z0!DfQoG1A+#t}z~xJ4N|Gtz#1#5n@4MZ;qWro@LM;vk4{9JiVHal=o4|I?x#r)r?0 zrU5b9oW2~~Ambdh+jUFLY&zOL0y`$R)`L@+I za5HV?4&xUu`EVqI_V zNZe(%DyAb;lM6D{{7eT)h2?P#5Vi5#rw1`xZSDI2@PU@ zs`GXTSN(&%eart?j?1GCYT$$HZ zY*XuVMBA+wPP=e}`XJhdOND`XHnmkyUt;P?!Bohf62JrA1tMhSy{}6$EE-Sla`C}U z+P4+4T~%&F`Mf@5YWr{wU~t1rFfTjkKQm?z+|&sfC|xM zI9J6GiREq3JF~&Co&u@NWIZxy224W#0vNZ*-#k1si^o^|sgxs> zG=q@3Zp2`N;93Hab#^qV5e7G9-THFY7AhYrg?o`2iOBAT+wcaVcr!ziHiR=Tx~B63 zWIo74nAsB4I_w<|xF#xG%)hz3JD%`2y$;C-^;QBA&xFHk;gbTiAg_+9xN$Mogn*+% z3@L&?K)j0c-0d)Im@fYi=l#0?u#F8;a=^8lfOZew9Zjx}itS@Rzb1RKE8qyvItSOx zs4H0s6Zn;<&oz4}Wsq!9Y=I~T_10m(gXas=};5N+SPJ4*ycx8POrr)dhj0+_$F=Rij{POWB+D}@>0K$&U1Z%Wbd}!wEmi?>s1u$x%j}$k_RuUw&TOJz zV#ja4{pWm0thnrhCH`a3dnza$uwsU8^V5m7eWh_)|DIEBp*;$5hI3*L&?>BZD8vLw z`zp2?vSo)9>%~8hvm1a69N8RiRa-9*89hL*2-?<;VJN zX(w+9)SyA6+Yo6eP^`^!bxH2)llH?Oz5PlUbcS>}lGRl7ZB=+-)Ez!Mlw{R81koy+^?sES8x#74d2S~VhsFiEAeNB@ zj>~ODZ)eFzqkX!d6AHqO!l36bxv384ERwrO=2Y?3{B#<;L$qxSK9+^Ukel=WdvTg4J{1pLvqB7~n4mI| zN5s6CJ-hf;)0qiQjiW0Ny<9|yQ7Io^9k)ct2=)zyJ1|nisXvFaSMOdIzThmd!3LrV z500XqQp2KS6f-k9z-;s^?OnGIbf~jdfWu?Cvf&XzOb66KPG~M+1%4=Ue8;r2ob{pS zI|4+($UW4`M6psZTfHR85A;x^#>~Bmf?i*tfm@~@L~Y`1NMLC-YJ{jyLel*n!)mK9 z{LmwF{x8CgAojP5m{y%>!AQ-5c#tHVh62Ici8XM@q<}%s=a~Ht=#en9o{j(8W{BnL zn0)q9_aR-SHmAZ=%8_!|m6-k=iQFJ)fch6uBPbzkriG#am||a0%SqkQKT%p+~XcpiT| z;!3V(vOa6+=tafxg1`_QizDf!M|)sBR6dZlCIUEEsf0h)rjfiUU#Ed27!g>7bc8rk zOAX{fw%o`Z@J40;-YE#b9r5=QA$`E#K(6~N6m#8%1m})q5dz*@ktCb$5S%QI{tnk% zhSu%h^JA$0Bc&^5!h{p_*Z=ciQxE&sK&Ys#=uJRmRiD~Y_VZy&LE@>H{DdX3xfIee z=l6T)DU$5jK_~2x(&P21KktwT&U3(+2F@_`QSO+Vp+A3DbBBg*r%vb+LMHc51O#_y zzL9m{M=7(57bahk=>ql+iWG@}iRTiPqGkbeAvj$8edoEOM0VHp2|#rEOT6ajsVxwC z6K`o@3KB5tH0=8`43{1z7lB|MYDD@80>u4@f+{TIqrQr907pdt=2d*y?jL&%0&+n} z0H?rFZ#*~P?%T%~jl3;%O7~xH>koYC@_xX+h#w>JPxh8E)D?gEq6}M}8)*i&9`uj3 z^)IkQ+5hBqCBRyb4P%NEARUK!UXlJYhpR}tvwDRyafLuMb5U`q+5zb8c>l~}Bu$iQ zLJ%p8f?{;Z1gnFTv-@`dv_0bzd^90w6M5h6$NTt@skLfP6-Qnl;JZ417UNimy(zyP zvYbeLl#_|p*qrO&|G6pQhiB=geE)xJIk>5jDNqCGV`mxtzcU2^XsfD=A_(OQM4Y~i zspl2YFwpUmeuP&@(D<_*|0m`z>21Cu2nC#lLvbeec*bGztDii=q2s0)LRVJ+1Se!1 z*(3UNISc z$AnoZcNag;Yn*5OSCRc?$Q}UMI{W%=L}TYv^(D3UWZ*#r1t2@b;vYNYUvS;3|FA>o zg*GLBa{>M<}y3-l0K+QS} zWnIRQw~nU*pZx+trHk2jNDuRQ*PB|0mOU#g!057$;_Q6~XWy%+UY+cPf5690X70NA z_->^=FE&R#_(u;SPjUX;KbG}^#)pg}lECA-14qMPNho5Tki!|3TliO}G{ryWX}0Ii zN0*+g1-LRBxEN~T4%%4@(2@vk^g>w9eK-ji0BwO=-?@VI_n4KQmpR+Y4OldTW*5I% zPqB>{8O82+S~l5SFGM&2bPy^K5rz9#K&kAdSPJNTGoLy-a}*5eWcHoYw;|`5=YYAO z3_1=-4s17@tbPE<`Tqo>PIu@q+{k2aJ9HB<1-?~aJ;>KPtP6wksDw^KzK&>i!Er|yy29a#IO_9OdT@gG1P& zvDkT(EAK01%B69tZVH0hcEfcj8$xZ3qM71UR#TlRj0O=7*%8WF2>EuMl_B79w?O$Q z8&?2Q(+-TACYh?F-p2vO@GpG1Tb<4GIyC(H24%TA(8Yn*{$>?mmj=Q7M*y zgf-AU7mXaK58RG_2NVT+rZ3Fu*%5uvPXu3Rd9pk7@*Yex<_T~VjK0xhuYh)js3=Rh z9ORGnbP$r5ZXbd+t@3E_l&}H%x_(E@TtYyc-YCX|F1|hfUT=x*on$fd{JpyX4Kg#n z3T$!^NoVjrjEgjKgkR2qNKXC&bgvN6-SzXX%JnPB%_v)dQI{%X*Fom&j}W)6U4&lI z9;ktP*c>b^&_mIjCgXVg+QPHxz4zdpLp1aRzjS3M0_Ueg$Xhxvl`iX3(EgUb299?| zhPKV`5ZI{uQ!nw^4J(P%oUW>85^^euK`CgY6MXOzsE{wpd@#C>ePuVhx)zl zHAIDb>q&eNATj16R#Lw$GJ+=9{Wk~u*fTPXN&Jme%?g2CS?pmBx!-`H6aGH0AbVfX zWi2&PidNHKZ27A1;0!j?s^MishY9hI@x>|QviNrKS|w1@iEvbeR8V7*W&(4czi@xP zbQAjmQ!X6;5c5912&tPN(NKx61)t2Q{D4L9sZW3;%|UYl*VLQ1NV`){gawYJm|$66 zNtXsuPAmdl>k;m1CCV~3i6L-vB)fd8DT)}?{x zQ1Q2*e5RSw?F znQRHqao+ict<8;Wgdp$(xF4R)-qpB2ObLf8qvpi91_DdYyUq-g9PRgBCy}Io&v~5$DWJKGNWYQLj~b{EcV;e1Wuku z1Xx8`ybkpKesg`&-Op2^SD{>~ZUG|jX=f2C)W$w2_B1%5E}?z^rz)tidK^dv*@slq z!U4l>13K*lC^DEG39a633gQGhful$Eyl~BQeMcRppeZZ+RKu4{a~M?Rcx_PM763eM z(>WCUJ$?XX^g+kasP>BMh;$krk<`rRGd>SS@DVh}Q-!D_XG@F!;MCq{J_)BoZ=kW@ z1s1sbv-};vSX4MAtV=zG!hyQ_-ql$F(6#xO+OyO@!OWP})F{D$$2*v&WQnhS4uE)0 zdX+5uAbCGPuS|z6v+PyfOf|ndf1yK(Y4VG7aoNcnDjMD9xxYX;~Nj7e=ViLLje*AvD*M+!YS0b*NUIv$2bU2d9kjZC)lv^vH|UmV-uOFy|g za+gR9ipLu0>OS;?m~naJfQ@`?i-a_W2SHqKYqJxV@p8AVT}0(~AcbmI1o)J$A^y%i zuYY);eVPbWnBackaO&Crf`aom&XgFA6$1{mwdQFzNQQ};+OVJUI=`>pw8bISt z0>VaK5)EjJnCK@s!>S>AEIlEAN2zc^{_o;$i2_kGV`i4JCDw@vI2S^V{XyQu#0o`q z^f_l_gjxDWklI07(fd!U&e@O4Xb$tniObq*L{R{L?x2(=m+bdf%@E)mCpd>DayxzJ}bV(24Vtq z4G~e=jZzcNP(fo-pP(p-gSE+_nihCnY|2iUT z7dm$|jM3P|ZnsZt%D+8byva2_Hf&wHa4p-(-V8Z6AP5tp=5{5S*pIV2L{Hd@0XAbV zl)anhT)w->Pr9N;^39FU=2({2;K4;;BgZV8v~aisDCiM-E?jqX8`iw1$iEh%?VvR? z$u(*3nhJ)Y*uuOd*YLh#_sb{kP-*vvfU*WKMz>tcKcd_J7D=KK@=h25{P`JyrncXO z{Mz9WNOdzfhQbs4F4tY3LB6u((?7i3{}%pvn6&`Dg8u>LX9vk>vWp{W5}oWh zu&h)HtiMlBKi}SN|KzJId*@Xu8BL$vyWl357KdNwixdia4-#5#+9I(?+p&U4JZ*R2 z)8a2WC1*;CzUZ{fR=9BYaZ!nt1n*)L%}uXC&iTfxAiKgQeE?_#w}2TmoBPGy(Eyq&EZRM&#maL^q(ek8-f%0*FX?0_D-iTU8r+ zy;_%A``bb8F&WA+q+hNrb+FIxeF^2;SehB`AAqKKA)93UNr z90n$}=P^OfDfW4N}>HoJxeVfy(&vr;aFLZ{!#dU=9>qdNYe0KcE7Wpj2HwK(ksVZ9i z6D89r8O$S#+AS={w^isNtb2(Tx=eq9g+nZ}b)fA;}9<=DFA18VnkYcA? zvBiB2QxUmhC;a$oSiH%jJPRrDj6Xi_?rDdj-n7wuEI_Eg44Ju?hk{lV&b$gk_o@OA zLY`-bDj|KYwFp~_znTJ`P7t^qhL>pVIMhFopC*H7EdhE^!R;15_qvq4cpR1)cnx^z zh&-R+;&l&LN1`Ytxb@cz0C#j?hkM(QA&CHYBlKx93nidd^!n4P#>;_s0(Yh>TI8`F zJrv{t#m6eh@9c}(N8$I8N|1V{^`*@$`4J>iYIiYyEjW;})NN@^aPcciA3{i>E+#E4 z1_&m^g~aPS;MPsG6qi&+ubzY|3oLuw)|uEbcg$h^UB7BWG9KCf7;T+LxLSk#m;4qG zqTuke$yRl+`;&y7c=^)a9gs0RaGgAY-u2|ch?2%#UIazwyeCigaFbZ>QlNdv!NM^g zwD2SvQBU}hxeH+cH$(*X(#j@~0^qZV6}r`JgI;_nd-zuq<@I1L_8ONgc3SFRN5B;+ zrudH;FEsG|zc3>_613#$Njfv2xK-l&Eq5-$>R^#9pRRMf5W=~I%Gtl@AUmDTA(0Up;&`^|$Lh!;*=+8s1hK5EBV=931# zv!LuA4ZH+eQkF~oArzDvJ32x#V?c>PI4^3J&bI1|1zOW1IyVT{Z1a*zJDxi?*9jjv z%a1iWqwg6*!T2arsS^XNaLrGTnh&K-urT8rA60*iiVBI~t;U#`oc*3*yX4;W-PiYV zj_uX<0(0B@uhqC#2tyk~)+Gf2Ha6s>R3IUwMIRy4EKCUggeLwfu1;d~RIBv+3FI@3 zaEhvxps2?$VNLw|MH}FT+g$F{TIi7A5PYnWD2xdrL35LkQf1vyO_$d8%^PKfU!kMB zD&D|We~Ai(2~y3c&N_#~w!}?E)bx)PuMTyS9@tBznt2etoUIGZr3#@;2};cix(@1z z!PT8;CW%u{U_y4+88)*z?4`|MX=(ck3JRVO z2n438$CI2~jxn8{X5Y_JHS$S8+hLH4d|&{c545t|S0cS|Ge`)H@m8VUdtMzTB&fj| zb>VJq;}V(ConeyTvoHt)ZT3#QWzmO z2IbM%!Os&lPKk57%^NAs!=MU^F6%!YXu=;x)abrDb}@f9{9)UrDUmn4LAmz|ee7t7 zvwKjD`xZ+Yf3?RixR)`1nZQ~23!bA~BZ)dBM$ATXk=G&(l6iu1tyw=P3s}Nf6B+3W zgNbiOb4bt&UZ<|6Nsy`*jK&NtLl><260V{5ra8_e`uG+J&W)fRr9lqQAx?rdMc$?= zw*q>s@RGqT=A~5%?SJ!@!Gds`G#)~i0eAdv80VV^GBZ_f4P>BqhA|TGfR`M&%BOc8 zfCuj8Lwtzd*A-0@Ng$79aKX;wtu=8Wwg?90va#{dB3Jdk&OCDfSzz(h+yq|2 z&GhXb5B4Lsh@$LKCEnol{~tF19is{WjJj?e+43pK&(}-wupkR2q2A4@+_y(z0@Eu5 zONophhIz5W;}i7ZaYwo-+64TZ?19rVLY12ehNu?CDug_2RyYYx0_Wyi&T8l!}@Y+6H4~+8W7I({VUb zhtfM6Tnzrcd@S2z=eI+_75XM)KZ0|u357Tn;=cu{`cgHrI%I{T9v~J$=Lr14atjL` z@p+JyPIJiE5jwGD z%A+80@B~Crs*fOsVUoH-$h?L71IweyM-k*0;ad%m0MK`sh+q3Caf|)xPkmSi(=%t3 zkaa-v=c`B3iA1cfdIqUw!r%hAV!|4u0u@4kW!o8e8Q8@V)2wT0iof zP!49c_3K0ktAjuq$6SeSX}$jd4A;tjyHpxv4_yAe&E=L)2W{)}bU;O0K~Yg9WL~Yn zni#)(wTB9Q%&sAb)a>L*2XAk0GaDO@{o(QsF?J6>SFfP~a&m+y;t5g<0_fk1HA+hE z6I3vF>cU2pFfAl#tOmC@L$Z7TH$hO4Lz@2*tdlSLK^k-_c<8U_0`2FIDRStSnMc$V zgt9rtB#bRLR3?GZFt`@vQKAMH^HUJ#+ehp7*Z3xvv)r>s=`tczo{QidTnU{xvOdFS z4XvPETR5&>4lI7LI^eqkT?aUTj&f({%Al*D$4R7XYkB?FO3CBElbwNq+Z!XcUbX;J z7D6%d6+&Ll08co3ms2A&lAyT<{W6ApXN%mlOaU);1@PkP{QZFSFOjV&dXY8IWTQgd zvb=HWVqxRw`&5~X2T?{>56!`p?7A6!R1MYz#`{=QVzV+Bt6AMlDF)&+bR9B^l=-ZV z&z4i=W<}qYAUyE;pdbs00^icAK{utRy-p&ULkQGdw^HQZ55AK#RPf*5cry!08E4p> z@7OYt?TKiW-jnyhMP_IYL#=!N=Cg_0$J+K;yioFvKYlWNhTjw~e)Fnjuwbc&=FsKX zWNfN^YLXpHBlVs=kCS7;4)%9+NfBo>DO8nRx`KUm@8Eg%BN*@K><|7JVe#Os=$hVB zUa=fr@1$uJZFU)51?OrZQ$88uj2adFcKPw$iK(AXbk-I?3B4=`1t`rHKG{dx3{ov_ zjt{OqR)I?7g$G{))@oO4M>anc@tR!&vWo%351|J>uZr}NI6%I(EMs%d5za#~^Xc-p z5>J5ZJ0ej7nogr3G;`oMS0cKsRe%paV{8M~OBFDZhblo1HgMy6#`Z({&Y)i3Yv zS9a;ye>KC5F9Z5K_COCH#K=I>u81@A$mSQr1>cIZW(3)$4ZxzSfci7&R`W#=!90sV zq;NGc<5#1{8>xgviTdsoGhO!=D?P{Kd!4@kfxU@;{cZ7)U!UpR&Q%e9eY;a%0TU7s zYdPlMCX&+!V;ncz=UzKsbp_H31JFFP2jA&@srwp8{?2D?|H>J^)<}P}57dL7Ynw1D zO}>K8TNB3%&37~AkkKi3RGy4DVq^w8BoZvt!2#)?N2&Nz;Z=r&hF&nplA=~{6pPJ zk99ls4XkWscW9#{bp*)tJq4}V#yv`NQr3_dxSzWLNVGzN@tVt_vH^S~A@Jv0+f#$U zCwt1-Hqea)M1~Q;ne;&4iEHtbYLLGB@U(xw#p7{XI?eK89wRdAa zM5?Qh^cbWTN*_g=m)(_ixH;~-xzalbHN5v(HkUsd4#MVGm}`rusafON{t@o_Y%;~o zw2}p33Rgk9OvRDqHE3ly8D?9A?-?;a<#9vAO=+$)w1CgZ+#~Ts9TX(WFP$p8?IdQs zHyk)m@*B(Bz`Q~h;(Nu*Gj@=e^Kd;lX{e!aGq*NP5001bBVnIN|2Rjua@hD{Z|~*o zoCo@UEbDo!hZ0&(llu$K4R1i5Xl~=3Tw9o2gVp|?tiFoxmfa(Y{c6I$sC%k={(NO zf}T@KU(J`t_i9|6kB$|aJ=mI(tH02p9~p^oAYc5Py61KyPfwewr`B34BuCSZ`f(>O zCpJ8AV{oLVyY?<`=ShX{keaS#eEFWyn{B+vLi91T<2wB$&9XgtrjYUV!0;G!)xN@; z-{-RaXYsRc2%-PYpiPPIw--r5LzW8U$VZm!YzPk!-9Rh zi1X23{FuPSmy}hV+HCzm@teOsvT^#B_>CVQPb^}*%}++kG&_}^yhM-U1P^V6TPM}R z6Ks~iQN2*&fqvrn@5y|c&Z<3#5l&a_(i)n1P=m!n`N0?VB$Dbkg7+pOZd)1Ej09{j zUOtg#=HWa@S~Q?MU-2hPKc?=W~Ao&V!Z*8JqJc) zI&A~##r@RNygeayK=gAXgJA`{3eKeeuTg zM@|jeBYNMrLc=T~YoZdXlH!9N^c(Z?5u8_wJ(W&28io7>cD%jZr%Ry>`Op=;Mtjjr z#kDfim8oJVBudj}GK1w@l-W_2x8$xx4{=3m6{cZx9b83J!{_h0eyV32Gt>y8-qf)L70$KA#UF}VyG2c? zgI$U$Q>CeC$|jj-Rrte8OdEY91?af;%7>P&H2vNFrmaLyWC9X7(T_n z+_dh3OIS> z2T$qXKkS{1ZuIMa6gpbr-3Xk9IWukMF+;qe=1{jL_;YC=uR{M6ek8UtK3JM6NUr>{RXx1fl=B39$%pybRmaDd*XRk2~cpUl50oaokb ztSk}d3|I{1>Pt31eV6%06<Rbg*YP$hsnuW3VF zkn_NokS17>W*$;()-PMN8M1opgOr#F{(aR+u~HeFPuUhXp;&OAV~mm0!U;N*E*u&6 zDkAI*1&~-N`SO!MQ$jI2pngh;IaZzkDAFkv%q4Tsm#4O(VhTBwT#bZqy zSFy}_R7LV@&{F!e>tp@qyt~^<@xqJcu46+yF5@k6SI4}75i!(*MhXn9Z3=no1s*$R z=g;*xJ9I~<_~G1=p3e1FMoZT9xjxD4u0TqZl(G!0O`Vje%Q|^3U>^vKIiIVjGV2(A z%?j)1vXkWL`TGS$(iN`|49r8U%(R?N@1q&3qVPQ6LG22{MP6&iA}cc*439D zDoGj4f4M7a@blw|kPFefEAzy8_t{&v1*A+=!GgLy$6TeBDUBNVru9{cBQmw{)p4LT!k>-vtpPbCS!8cLhH}*^z3f zvc&VnSBH*l^m3M1b>?5MI8XsC99$L>O2x{I#M~?6iuTxkWTG87X@cs~XfH6bngZ!gm*4x2&^80{7`j{mk6x7LuX-D)89B ztRo(AxYOP|;yz)F^8NbdMJMFZ-m>qv?4s;oL1Q71mJ3#U$xn>!S`B~44qY0=&_5H8 zK7d0~4fj=#FCnPT&HB?0*9NGZ2H7|!Qa8_84)`^4Q8fxcxO<0tZ6~7B`V)plJ!Yp1 z1(S(EBQau_!Uq;UmA#9NS5b}`{#6^^FV!eHCIX%XSI=J8EURY+3-VSiSr&;SRWmRC zdD;D4(KnHQLmz{YsyvL7Y1W}5S6;dg*^Okbfia!yc`jmiHTEbpUhOUALJ1&V-hrfp zJK$CFA1_ZV;es+5%76h$UYnYmTSK^VuUMZJ?lgx!;BUOaE_5Mdwp@wx8WIg>yG~xi zv>xTQ#=pmRp6+M$Sc78A;;RL#yKJ9%W_v@+xS1_O?HgJ!phBNL1;begv1t39w{x5I-$CDn-EYe)>=P|;p!8bLB z^v4yc3<|CYED}iclHW?yzC#+DD8b2G2r-1flhB*o@d((XduvbCj_N!M{8G)vQRr>&tv6qaHS7fKJ=t558zk;SsB{VwC z*_5pN@d|2L(@xrw`4<((Fm-Q(B$_9|rO8}FOuGKpw__hOoQyVa3Nkobw?{Db^K#*| z>Ii3lIwh3Y?-f^EvebX1HOJN_U1t6cr&Tr&7~Ti(CNl+k@iYsOaGch&$EIGLI8)iS zPA8+7BM>O1&$cPtKRXx=)n?x4f@@#Lrvx|!>P_Y7j zKiFI4s@}*kiLQEa4^V!Kb^R851O5_z@LM1QQ_Z@t@5$N5A&|{`2gtG`yg_m2i}w@z zP6Y)~!dh3M!V2ynh}%?ITKMk91@O$<*{mXOAObb>rLXtUMoD7(+yJ~`?=*e&76PpY zGW{PLy&y__im2BO%N$zr1Gb@SqJeDQRhyNyJ_ow%kTNIO^z#3NQkM$Xr$bf4-alnP z$?rNuQ=WtoaOM=oo}k}Cukufvz_#;U5wJK7i^6Ti;fm2F1Y*Pd=N*|1F2V?m!Q+1o zU)8wET>s;S4MZslD?o?y>ovQQ(2)09zM_q<)`_=}k;_@k3aJ9T@{*hxUt9d<1{!Jp zQ8Mh^5sA-eXT*` zA?Gs)jY~}7=U&L&^#yFq-#HnWbN@;?WR#}yf#3()96!rUoeLSUG9-o`_KsA76*G-H z2NC8I5E7G_uF;GD$Y=j_jpsV3!@vDGrskgoA*li`n^3T+xX9_an7br^4!*tJl=u(O=N9%# zyJ=w5RK>up)8<^&Q)w=ztUK4sZ8qi8K0>8m?fpxys1^L}U_#oLj`^#4bY2&zg(ZAz z9lO6)_;gh5FkB5M&6$2Jg`(%qGtmbp&m3IH2SUWx>E~kIQ*iTp-`%>sBh@;umKBTT zK!$WEUGgKIQWmI->5_MnJ`zegB@N+!Bj9Paej)Sa(%i@(%tp5$4FH95z5-yf_A|+5 zSs`=nBP4KazSekJmBrK@N&4u!{Y3`GQa5}j%Ik`e{|g3aQ1!tVl(v1j3XYtMu8Df_ zO?JJ`#Q(+DM_yF!=^T-F+b%=CRP8x%A& zQt>J-pnG>;Np70PekB;3ttmgttizcqxJR~BqB&Rl&oMii!*fxG5Z-pCBf?g1zRuy= zgCYLzg^_O}$U$xkF~pZMhm~2ToA52SY2e}CnFAneK;}qLsG-A&Gr>aeipAeV3s%x>`Dd4?1BORr6y*tMpSq2S;-o#>B^zt zsN(&m{suERu_zPUF|W%_aRhe&0u=1S2**xs{1}g${rPwudkE_6VQ&g1LYVu3Uh+U< zJFVO&shm2&80?wyXu`3x{eVGx5ekYIB6~)h9>sb#()aW6Xvu1Ux!PJk&hlO&U()u` znB)2Gd={_+0 zo|0KUTU^U@svhgF>My@ebJI9r&kV<287c`FxR6s08DzfU1e!+|jm*`p>e@-E z``SLvTsIkeR;L+Nmm_dcyQw+G=_mN=6Zl@on6M8{8|%g*qUZD5Td(-1z_RV7F)~v5 z?t+dq?_6K^)VDo6vso;8RAB1Mcl9|othuS7E%(vr#?f4-5=A<jg(Tj)l#XgzcM^ zS^uW6UclwQXJVFYjA1j`z`=pX{~>m>JBBblOBUt6Y%ZRkQ9?KKc6Ek%IX~kIS!a5x zjN>hTKm!yM2pU|%CU_(bU|m#Tabbhpj4*ZifOIOq;kksmp{?sjxva>A)-rObSldy)|zevP`_HG320%T zy=Msdp`Y)n$%t62RlF({L$bOVp~{ZR=OCJUaT^w)E;$u{Gzo5j;1ecgre`*F#z28; zg2B7;oR0Rp_gexiI5!2H>OEZ{h@FEa zkp7W)achhft;}<&Lyhw8diFZfW=y55X8HV0DKk-(LsrZ88_1bY*&(Ht24GQMhT{F! z=r(vdFfiP2zo)W6v#H?Hnf=7q0%K(qY_Q0C)o_g+{vntNCVEhP^xDp+dyfW1>Un&s zrXjur_*jZc%4`?k2Onw#LN&U>P7H@hOQ@ubmPAjnAZZd}^nZg|$}C^%5+^0%$lriD ztHStj?tI!rZ-+BxmsNIJDe)x%TEnvtFcu=57~y z6ew_10oB0*PWiM z9>2s2FFL3N#-Xx@^RWi^Ir!-$(}_<@50ETn@tST7nHIq0;`=z{eE)1Hm_!Ur=}i`I zzniKD;3q*D2@6JK^FnI(30FJ4_nW^jfH@zPhL7Sx!@pp3SYfzH%_9%A|#W?u_WE4Q)44$J)S3*a4aGEiyZ{UOuk zn&H1%27ak^#$kWuTP!eNFVL%>ybKY)`67txLGeuOvSO4YW-+Xx^vvtI%+NjM(B-Zt zzo6h>_N=Aj{c}KGs)H^R{UG5wR;$!U7aRibmnke#kGNi-nG38JB#mng@hg&Xy8>wt z3rNb{4&hRQSFj%?H*`7C6};do01vEzifTtjN>w2hZ0+`}3Fua{>t~^O#JVpn<+>jn z0x6H)CMN2QLd8ra`1GH^FP217+4*<-$b)YqD^KM3zMT!?$p9PH?NtTcUzN>ZBx?TV0PX7NX%v zx<|s0?8)%hf5&8yp#8uMz4@KUeFe*i=|fc7&a;ys#=xxj)CHu<0Q!yfL%sKXl5eT- zrJB?r|I;`>*fQ)tL4m%lrs{7T&Qt{fDl0{0JmbTdWGgDkI0NBeAfpWctE3Px9)w*I zW)*1(89W$#*SD-g#32aF1Y-n66@^_tzq)bdOZJ@sU7`DDL54tB#xxBcHHt^-LKi;} z6*K0c#SnXv*>2T^VAV)!gu`~_lgCkTMKc+$YKZg1B&)}Y2TTiHf_y?BlAJtr=n!Or zX+^2fu+ilrq{jIXyR(e!WSl7lXqs0baR-pNKj{ub!khaVnt!IJr)QwA%)zqg-t$E} zqaZY>KaHaAgPqCLx{qnrBYi9u253kha1 z6b>~mk5_G%U@-#2B5~elA3#0Fj{jdiA0uiwDd|4)#2J=eE zEu%4p-0{39$@*XAPyq3|$2QR41(4P#<=2ehUXFstROa1d*|PEmu$zr9n$(4R&J~)V zOb)_%1RO4eF8Hho1MEcI1jlMr@Gp4cH+zUXZ83_3fwAP(jk4cn!Xy>U#4fUF59A-L zB49#jf~R*rJ;6>YDB9#PF7ew;yy9-K5Q&i=7eK5Cn100HAVELp{db%i?xe^zAd&1J z7uyJ7yd(CBJlK;63l4s-0S{s_VDCKOnBs$OWyxn){&Ou@J(~(@j(kJeXaN{z(0s8 z6yn?oycnHb!Jt%Y0SSxl?(S!%%MVk)raWwBs{hWw0M|IF26sFnl0X$%kE)Q)?U5esQwe$wTP$}5`P9Z1Rs910~S3sZn zN1CeZ0EMou4!T=IU;Dm-Cw9&zVXEh-LC90`*rxORc^P>+qWxY>= z&Pe?02fweuAW5{+@wheOqyEP~CSDWd2%AiWhGmL~D*mS%z%~D03?g`&%pFg5toYjd R6D07TuGVqQVs*Qa{{@Jl@eBX} literal 0 HcmV?d00001 diff --git a/docs/sequence-diagram/cht-form-submission.txt b/docs/sequence-diagram/cht-form-submission.txt new file mode 100644 index 00000000..71c35a7e --- /dev/null +++ b/docs/sequence-diagram/cht-form-submission.txt @@ -0,0 +1,31 @@ +title CHT Form Submission + +participant CHT + +participantgroup +participant OpenHIM +participant Mediator +participant FHIR Server +end + +participant Requesting System +autoactivation on +box over CHT: Form submission in CHT +box over CHT: Outbound push\nrecognizes a\ntracked form was submitted +CHT->OpenHIM: Outbound push POSTS Data Record\nDocument to OpenHim +OpenHIM->Mediator: +box over Mediator: Mediator creates Encounter FHIR +box over Mediator: Mediator creates Observation FHIR Resources\nfrom the fields in the data record document +box over Mediator: Mediator creates Bundle\nfrom Encounter and Observations +Mediator->FHIR Server: POST Encounter Bundle +box over FHIR Server: FHIR Server saves\nthe resources in the Bundle +FHIR Server-->Mediator: FHIR Bundle Response +Mediator-->OpenHIM: 200 Response (no body) +OpenHIM-->CHT: 200 Response (no body) +destroysilent CHT +box over Requesting System: Any FHIR Compliant Server\nthat has been set up as a client\nin OpenHIM can now request\nEncounter or Observation records from CHT +Requesting System->OpenHIM: GET FHIR Encounter +OpenHIM->FHIR Server: GET FHIR Encounter +FHIR Server-->OpenHIM: FHIR Searchset response +OpenHIM-->Requesting System: FHIR Searchset Response +autoactivation off diff --git a/docs/sequence-diagram/cht-patient-creation.png b/docs/sequence-diagram/cht-patient-creation.png new file mode 100644 index 0000000000000000000000000000000000000000..1436a37e54a630848fcf0a3e4e9eb6de303f3eaa GIT binary patch literal 124019 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 literal 0 HcmV?d00001 diff --git a/docs/sequence-diagram/cht-patient-creation.txt b/docs/sequence-diagram/cht-patient-creation.txt new file mode 100644 index 00000000..bca1021d --- /dev/null +++ b/docs/sequence-diagram/cht-patient-creation.txt @@ -0,0 +1,29 @@ +title CHT Patient Creation + +participant CHT + +participantgroup +participant OpenHIM +participant Mediator +participant FHIR Server +end + +participant Requesting System +autoactivation on +box over CHT: Patient is created in CHT +box over CHT: Outbound push\nrecognizes changes +CHT->OpenHIM: Outbound push POSTS Patient\nDocument to OpenHim +OpenHIM->Mediator: +box over Mediator: Mediator converts Patient\nDocument to FHIR Patient +Mediator->FHIR Server: POST Patient FHIR +box over FHIR Server: FHIR Server saves\nthe Patient FHIR +FHIR Server-->Mediator: FHIR Patient Response +Mediator-->OpenHIM: 200 Response (no body) +OpenHIM-->CHT: 200 Response (no body) +destroysilent CHT +box over Requesting System: Any FHIR Compliant Server\nthat has been set up as a client\nin OpenHIM can now request\nPatient records from CHT +Requesting System->OpenHIM: GET FHIR Patient +OpenHIM->FHIR Server: GET FHIR Patient +FHIR Server-->OpenHIM: FHIR Searchset response +OpenHIM-->Requesting System: FHIR Searchset Response +autoactivation off From ac83b030c8e26d37c97e7b77de48745468bed29d Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Tue, 21 May 2024 15:32:45 +0300 Subject: [PATCH 11/67] feat(#125): sequence diagrams for outgoing patients --- .../cht-outgoing-patients.png | Bin 0 -> 85794 bytes .../cht-outgoing-patients.txt | 25 ++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 docs/sequence-diagram/cht-outgoing-patients.png create mode 100644 docs/sequence-diagram/cht-outgoing-patients.txt diff --git a/docs/sequence-diagram/cht-outgoing-patients.png b/docs/sequence-diagram/cht-outgoing-patients.png new file mode 100644 index 0000000000000000000000000000000000000000..7bbd521a1b2dedd8e4ebd4ee055fd2f7421aa2cd GIT binary patch literal 85794 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 System: Mediator GETs FHIR Patients +activate Mediator +Requesting System-->Mediator: FHIR Searchset Response +Mediator->FHIR Server: Mediator GETs FHIR Patients +FHIR Server-->Mediator: FHIR Searchset Response +box over Mediator: Mediator compares Resources from\nFHIR Server and Requesting System +box over Mediator: Patients found in FHIR Serverbut\nnot in Requesting System are sent to Requesting System +Mediator->Requesting System: Mediator POSTS new Patients +Requesting System-->Mediator: FHIR Patient Response\n(w/ Requesting System ids) +Mediator->FHIR Server: Mediator PUTS Patient\nw/ updated ids from requesting System +FHIR Server-->Mediator: +deactivate Mediator +autoactivation off From 1fb13517a8fe9e14dad23a7fb4ee0be693cc5fdf Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Wed, 22 May 2024 16:17:28 +0300 Subject: [PATCH 12/67] feat(#114): fix uit tests --- mediator/src/middlewares/schemas/encounter.ts | 4 + mediator/src/middlewares/schemas/patient.ts | 27 +++++- .../schemas/tests/cht-request-factories.ts | 63 +++++++++++++ .../schemas/tests/fhir-resource-factories.ts | 12 ++- mediator/src/routes/tests/cht.spec.ts | 49 +++++++++++ mediator/src/routes/tests/patient.spec.ts | 7 +- mediator/src/utils/fhir.ts | 14 ++- mediator/src/utils/openmrs.ts | 14 ++- mediator/src/utils/openmrs_sync.ts | 10 +-- mediator/src/utils/tests/openmrs_sync.spec.ts | 88 +++++++++++++++++++ 10 files changed, 266 insertions(+), 22 deletions(-) create mode 100644 mediator/src/middlewares/schemas/tests/cht-request-factories.ts create mode 100644 mediator/src/routes/tests/cht.spec.ts create mode 100644 mediator/src/utils/tests/openmrs_sync.spec.ts diff --git a/mediator/src/middlewares/schemas/encounter.ts b/mediator/src/middlewares/schemas/encounter.ts index d8641068..f43f2fbf 100644 --- a/mediator/src/middlewares/schemas/encounter.ts +++ b/mediator/src/middlewares/schemas/encounter.ts @@ -1,10 +1,14 @@ import joi from 'joi'; export const EncounterSchema = joi.object({ + id: joi.string().uuid(), identifier: joi .array() .items( joi.object({ + type: joi.object({ + text: joi.string() + }), system: joi.string().valid('cht').required(), value: joi.string().uuid().required(), }) diff --git a/mediator/src/middlewares/schemas/patient.ts b/mediator/src/middlewares/schemas/patient.ts index 4bffd3d4..35a4a1b0 100644 --- a/mediator/src/middlewares/schemas/patient.ts +++ b/mediator/src/middlewares/schemas/patient.ts @@ -1,9 +1,30 @@ import joi from 'joi'; export const PatientSchema = joi.object({ - id: joi.string().uuid().required(), - name: joi.string().required(), + id: joi.string().uuid(), + identifier: joi + .array() + .items( + joi.object({ + type: joi.object({ + text: joi.string() + }), + system: joi.string().valid('cht').required(), + value: joi.string().uuid().required(), + }) + ) + .min(1) + .required(), + name: joi + .array() + .items( + joi.object({ + family: joi.string().required(), + given: joi.array().length(1).required(), + }) + ) + .min(1) + .required(), gender: joi.string().required(), birthDate: joi.string().required(), - phone: joi.string().required(), }); diff --git a/mediator/src/middlewares/schemas/tests/cht-request-factories.ts b/mediator/src/middlewares/schemas/tests/cht-request-factories.ts new file mode 100644 index 00000000..c45dd7f1 --- /dev/null +++ b/mediator/src/middlewares/schemas/tests/cht-request-factories.ts @@ -0,0 +1,63 @@ +import { randomUUID } from 'crypto'; +import { Factory } from 'rosie'; + +export const ChtPatientFactory = Factory.define('chtPatient') + .attr('doc', () => ChtPatientDoc.build()) + +export const ChtPatientDoc = Factory.define('chtPatientDoc') + .attr('_id', randomUUID()) + .attr('name', 'John Doe') + .attr('phone', '+9770000000') + .attr('date_of_birth', '2000-01-01') + .attr('sex', 'female') + .attr('patient_id', randomUUID()); + +export const ChtPregnancyForm = Factory.define('chtPregnancyDoc') + .attr('patient_uuid', randomUUID()) + .attr('reported_date', Date.now()) + .attr('observations', [ + { + "code": "17a57368-5f59-42c8-aaab-f2774d21501e", + "valueCode": "43221561-0600-410e-8932-945665533510" + }, + { + "code": "17a57368-5f59-42c8-aaab-f2774d21501e", + "valueCode": "070dca86-c275-4369-b405-868904d78156" + }, + { + "code": "17a57368-5f59-42c8-aaab-f2774d21501e", + "valueCode": "117399AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + { + "code": "17a57368-5f59-42c8-aaab-f2774d21501e", + "valueCode": "ea6a020e-05cd-4fea-b618-abd7494ac571" + }, + { + "code": "17a57368-5f59-42c8-aaab-f2774d21501e", + "valueCode": false + }, + { + "code": "17a57368-5f59-42c8-aaab-f2774d21501e", + "valueCode": "0d9e45d6-9288-494e-841c-80f3f9b8e126" + }, + { + "code": "17a57368-5f59-42c8-aaab-f2774d21501e", + "valueCode": "73f56d98-207e-4e91-9a41-bc744e933cbd" + }, + { + "code": "17a57368-5f59-42c8-aaab-f2774d21501e", + "valueCode": "121629AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + { + "code": "1427AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "valueDateTime": "2023-11-20" + }, + { + "code": "5596AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "valueDateTime": "2024-08-26" + }, + { + "code": "13179cce-a424-43d7-9ad1-dce7861946e8", + "valueString": "" + } + ]); diff --git a/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts b/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts index fdc2f81e..c978b590 100644 --- a/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts +++ b/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts @@ -4,6 +4,9 @@ import { VALID_CODE, VALID_SYSTEM } from '../endpoint'; const identifier = [ { + type: { + text: 'CHT Document Identifier' + }, system: 'cht', value: randomUUID(), }, @@ -14,13 +17,14 @@ export const HumanNameFactory = Factory.define('humanName') .attr('given', ['John']); export const PatientFactory = Factory.define('patient') - .attr('id', randomUUID) - .attr('name', 'Patient Zero') + .attr('id', randomUUID()) + .attr('identifier', identifier) + .attr('name', () => [HumanNameFactory.build()]) .attr('gender', 'male') - .attr('birthDate', '2000-01-01') - .attr('phone', '+97723423411'); + .attr('birthDate', '2000-01-01'); export const EncounterFactory = Factory.define('encounter') + .attr('id', randomUUID()) .attr('identifier', identifier) .attr('status', 'planned') .attr('class', 'outpatient') diff --git a/mediator/src/routes/tests/cht.spec.ts b/mediator/src/routes/tests/cht.spec.ts new file mode 100644 index 00000000..c9a6e5ab --- /dev/null +++ b/mediator/src/routes/tests/cht.spec.ts @@ -0,0 +1,49 @@ +import request from 'supertest'; +import app from '../../..'; +import { ChtPatientFactory, ChtPregnancyForm } from '../../middlewares/schemas/tests/cht-request-factories'; +import * as fhir from '../../utils/fhir'; + +describe('POST /cht/patient', () => { + it('accepts incoming request with valid patient resource', async () => { + jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ + data: {}, + status: 200, + }); + + const data = ChtPatientFactory.build(); + + const res = await request(app).post('/cht/patient').send(data); + + expect(res.status).toBe(200); + expect(res.body).toEqual({}); + + /* + expect(fhir.createFhirResource).toHaveBeenCalledWith({ + ...data, + resourceType: 'Patient', + }); + */ + expect(fhir.updateFhirResource).toHaveBeenCalled(); + }); + + it('accepts incoming request with valid form', async () => { + jest.spyOn(fhir, 'createFhirResource').mockResolvedValueOnce({ + data: {}, + status: 200, + }); + + const data = ChtPregnancyForm.build(); + + const res = await request(app).post('/cht/encounter').send(data); + + expect(res.status).toBe(200); + expect(res.body).toEqual({}); + /* + expect(fhir.createFhirResource).toHaveBeenCalledWith({ + ...data, + resourceType: 'Patient', + }); + */ + expect(fhir.createFhirResource).toHaveBeenCalled(); + }); +}); diff --git a/mediator/src/routes/tests/patient.spec.ts b/mediator/src/routes/tests/patient.spec.ts index 4d509c83..25f2c8fd 100644 --- a/mediator/src/routes/tests/patient.spec.ts +++ b/mediator/src/routes/tests/patient.spec.ts @@ -16,11 +16,14 @@ describe('POST /patient', () => { expect(res.status).toBe(201); expect(res.body).toEqual({}); + expect(fhir.createFhirResource).toHaveBeenCalledWith({ + ...data, + resourceType: 'Patient', + }); expect(fhir.createFhirResource).toHaveBeenCalled(); }); - // TODO: reenable when validating fhir resource after mapping - it.skip('doesn\'t accept incoming request with invalid patient resource', async () => { + it('doesn\'t accept incoming request with invalid patient resource', async () => { const data = PatientFactory.build({ birthDate: 'INVALID_BIRTH_DATE' }); const res = await request(app).post('/patient').send(data); diff --git a/mediator/src/utils/fhir.ts b/mediator/src/utils/fhir.ts index 987fae31..a8545feb 100644 --- a/mediator/src/utils/fhir.ts +++ b/mediator/src/utils/fhir.ts @@ -179,8 +179,14 @@ export async function updateFhirResource(doc: fhir4.Resource) { } export async function getFhirResourcesSince(lastUpdated: Date, resourceType: string) { - return await axios.get( - `${FHIR.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`, - axiosOptions - ); + try { + const res = await axios.get( + `${FHIR.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`, + 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.ts b/mediator/src/utils/openmrs.ts index b09fee22..5c33211a 100644 --- a/mediator/src/utils/openmrs.ts +++ b/mediator/src/utils/openmrs.ts @@ -17,10 +17,16 @@ export async function getOpenMRSPatientResource(patientId: string) { } export async function getOpenMRSResourcesSince(lastUpdated: Date, resourceType: string) { - return await axios.get( - `${OPENMRS.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`, - axiosOptions - ); + try { + const res = await axios.get( + `${OPENMRS.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`, + axiosOptions + ); + return { status: res.status, data: res.data }; + } catch (error: any) { + logger.error(error); + return { status: error.status, data: error.data }; + } } export async function createOpenMRSResource(doc: fhir4.Resource) { diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index 9686a332..b793813d 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -28,7 +28,7 @@ interface SyncResults { outgoing: fhir4.Resource[] } -async function sync( +export async function compare( getKey: (resource: any) => string, resourceType: string ): Promise { @@ -61,11 +61,11 @@ async function sync( export async function syncPatients(){ const getKey = (fhirPatient: any) => { return getIdType(fhirPatient, openMRSIdentifierType) || fhirPatient.id }; - const results: SyncResults = await sync(getKey, 'Patient'); + const results: SyncResults = await compare(getKey, 'Patient'); results.incoming.forEach(async (openMRSResource) => { const response = await updateFhirResource(openMRSResource); - const response2 = await createChtPatient( response.data ); + const response2 = await createChtPatient(response.data); }); /* @@ -86,11 +86,11 @@ export async function syncPatients(){ export async function syncEncountersAndObservations(){ const getEncounterKey = (fhirEncounter: any) => { return JSON.stringify(fhirEncounter.period); }; - const encounters: SyncResults = await sync(getEncounterKey, 'Encounter'); + const encounters: SyncResults = await compare(getEncounterKey, 'Encounter'); const getObservationKey = (fhirObservation: any) => { return fhirObservation.effectiveDateTime + fhirObservation.code.coding[0].code; }; - const observations: SyncResults = await sync(getObservationKey, 'Observation'); + const observations: SyncResults = await compare(getObservationKey, 'Observation'); const encountersToCht = new Map(); // create encounters and observations in the fhir server diff --git a/mediator/src/utils/tests/openmrs_sync.spec.ts b/mediator/src/utils/tests/openmrs_sync.spec.ts new file mode 100644 index 00000000..900c7d6b --- /dev/null +++ b/mediator/src/utils/tests/openmrs_sync.spec.ts @@ -0,0 +1,88 @@ +import { compare, syncPatients, syncEncountersAndObservations } from '../openmrs_sync'; +import * as fhir from '../fhir'; +import * as openmrs from '../openmrs'; +import * as cht from '../cht'; + +import { PatientFactory } from '../../middlewares/schemas/tests/fhir-resource-factories'; + +describe('OpenMRS Sync', () => { + it('compares resources with the gvien key', async () => { + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: { entry: [ + {id: 'outgoing'}, + {id: 'toupdate'} + ] }, + status: 200, + }); + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: { entry: [ + {id: 'incoming'}, + {id: 'toupdate'} + ] }, + status: 200, + }); + + const getKey = (obj: any) => { return obj.id }; + const comparison = await compare(getKey, 'Patient') + + expect(comparison.incoming).toEqual([{id: 'incoming'}]); + expect(comparison.outgoing).toEqual([{id: 'outgoing'}]); + expect(comparison.toupdate).toEqual([{id: 'toupdate'}]); + + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); + }); + + it('sends incoming Patients to FHIR and CHT', async () => { + const openMRSPatient = PatientFactory.build(); + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: { entry: [] }, + status: 200, + }); + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: { entry: [ openMRSPatient ] }, + status: 200, + }); + jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ + data: openMRSPatient, + status: 200 + }); + jest.spyOn(cht, 'createChtPatient') + + const getKey = (obj: any) => { return obj.id }; + const comparison = await syncPatients(); + + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); + + expect(fhir.updateFhirResource).toHaveBeenCalledWith(openMRSPatient); + expect(cht.createChtPatient).toHaveBeenCalledWith(openMRSPatient); + }); + + it('sends outgoing Patients to OpenMRS', async () => { + const fhirPatient = PatientFactory.build(); + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: { entry: [ fhirPatient ] }, + status: 200, + }); + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: { entry: [] }, + status: 200, + }); + jest.spyOn(openmrs, 'createOpenMRSResource').mockResolvedValueOnce({ + data: fhirPatient, + status: 200 + }); + //jest.spyOn(fhir, 'updateFhirResource') + + const getKey = (obj: any) => { return obj.id }; + const comparison = await syncPatients(); + + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); + + expect(openmrs.createOpenMRSResource).toHaveBeenCalledWith(fhirPatient); + // updating with openmrs id + //expect(fhir.updateFhirResource).toHaveBeenCalledWith(fhirPatient); + }); +}); From d9e1c13a452644141afe0e7226a851d85c223720 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Thu, 23 May 2024 18:07:49 +0300 Subject: [PATCH 13/67] feat(#125): sequence diagrams for incoming patients and forms --- docs/sequence-diagram/cht-incoming-forms.png | Bin 0 -> 136855 bytes docs/sequence-diagram/cht-incoming-forms.txt | 32 +++++++++++++++ .../cht-incoming-patients.png | Bin 0 -> 167340 bytes .../cht-incoming-patients.txt | 38 ++++++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 docs/sequence-diagram/cht-incoming-forms.png create mode 100644 docs/sequence-diagram/cht-incoming-forms.txt create mode 100644 docs/sequence-diagram/cht-incoming-patients.png create mode 100644 docs/sequence-diagram/cht-incoming-patients.txt diff --git a/docs/sequence-diagram/cht-incoming-forms.png b/docs/sequence-diagram/cht-incoming-forms.png new file mode 100644 index 0000000000000000000000000000000000000000..94df2d871904d406fa8cf48f35b1c11cc36e1836 GIT binary patch literal 136855 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 aVjm)fn3S0 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

|~|!iA@oEV-m>OXx?kmF`V*nwx4r9 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 R4v_H2!FG$y_067f{{`0{OXdIo literal 0 HcmV?d00001 diff --git a/docs/sequence-diagram/cht-incoming-forms.txt b/docs/sequence-diagram/cht-incoming-forms.txt new file mode 100644 index 00000000..22242f2a --- /dev/null +++ b/docs/sequence-diagram/cht-incoming-forms.txt @@ -0,0 +1,32 @@ +title CHT Incoming Forms + +participant Requesting System + +participantgroup +participant OpenHIM +participant Mediator +participant FHIR Server +end + +participant CHT + +autoactivation on +box over Requesting System: A FHIR Encounter\nwith Observations is created +activate Mediator +Mediator->Requesting System: Mediator GETs FHIR Encounters +Requesting System-->Mediator: FHIR Searchset Response +Mediator->Requesting System: Mediator GETs FHIR Observations +Requesting System-->Mediator: FHIR Searchset Response +Mediator->FHIR Server: Mediator GETs FHIR Encounters +FHIR Server-->Mediator: FHIR Searchset Response +Mediator->FHIR Server: Mediator GETs FHIR Observations +FHIR Server-->Mediator: FHIR Searchset Response +box over Mediator: Mediator compares Resources from\nFHIR Server and Requesting System +box over Mediator: Encounters found in Requesting System but\nnot in Requesting System are sent to CHT +box over Mediator: Mediator converts FHIR Observations\n for each Encounter into form fields +Mediator->CHT: Mediator POSTS to the records API +box over CHT: CHT saves a data_record\nwith the data from the patient\ncreation form +CHT-->Mediator: CHT Responds with source record id. +deactivate Mediator +autoactivation off + diff --git a/docs/sequence-diagram/cht-incoming-patients.png b/docs/sequence-diagram/cht-incoming-patients.png new file mode 100644 index 0000000000000000000000000000000000000000..6df50795f8818c9169598a75e11313bc0a75fbaf GIT binary patch literal 167340 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

V* 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�UhFUO~sG^9q{veF*ZBL;K(o`e*>9d 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 Nn5Requesting System: Mediator GETs FHIR Patients +activate Mediator +Requesting System-->Mediator: FHIR Searchset Response +Mediator->FHIR Server: Mediator GETs FHIR Patients +FHIR Server-->Mediator: FHIR Searchset Response +box over Mediator: Mediator compares Resources from\nFHIR Server and Requesting System +box over Mediator: Patients found in FHIR Serverbut\nnot in Requesting System are sent to Requesting System +Mediator->CHT: Mediator POSTS to the records API +box over CHT: CHT saves a data_record\nwith the data from the patient\ncreation form +CHT-->Mediator: CHT Responds with source record id. +deactivate Mediator +box over CHT: CHT creates a patient document +box over CHT: CHT adds parent using place_id +box over CHT: CHT adds patient_id and other fields with transitions +CHT->OpenHIM: Outbound push recognizes new patient and sends +OpenHIM->Mediator: +Mediator->FHIR Server: GET FHIR Patient\nusing external_id +FHIR Server-->Mediator: FHIR Patient +box over Mediator: Mediator adds CHT and Medic ID +Mediator->FHIR Server: PUT FHIR Patient\nwith updated ids +FHIR Server-->Mediator: +Mediator-->OpenHIM: +OpenHIM-->CHT: +autoactivation off From 449de725b9847f3307697aba1e33e352dcbf2035 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Fri, 24 May 2024 12:48:31 +0300 Subject: [PATCH 14/67] feat(#114): sonar fixes --- mediator/src/controllers/cht.ts | 65 ++++++++++++++ mediator/src/mappers/cht.ts | 6 +- mediator/src/routes/cht.ts | 61 ++----------- mediator/src/utils/cht.ts | 7 +- mediator/src/utils/openmrs_sync.ts | 5 +- mediator/src/utils/tests/openmrs_sync.spec.ts | 3 + openmrs-poller/Dockerfile | 9 -- openmrs-poller/poll_openmrs.py | 89 ------------------- 8 files changed, 79 insertions(+), 166 deletions(-) create mode 100644 mediator/src/controllers/cht.ts delete mode 100644 openmrs-poller/Dockerfile delete mode 100644 openmrs-poller/poll_openmrs.py diff --git a/mediator/src/controllers/cht.ts b/mediator/src/controllers/cht.ts new file mode 100644 index 00000000..7b07e191 --- /dev/null +++ b/mediator/src/controllers/cht.ts @@ -0,0 +1,65 @@ +import { + createFhirResource, + updateFhirResource, + getFHIRPatientResource, + addId +} from '../utils/fhir'; +import { + buildFhirObservationFromCht, + buildFhirEncounterFromCht, + buildFhirPatientFromCht, + chtPatientIdentifierType, + chtDocumentIdentifierType +} from '../mappers/cht'; +import { getPatientUUIDFromSourceId } from '../utils/cht'; + +export async function createPatient(chtPatientDoc: any) { + // hack for sms forms: if source_id but not _id, + // first get patient id from source + if (chtPatientDoc.doc.source_id){ + chtPatientDoc.doc._id = await getPatientUUIDFromSourceId(chtPatientDoc.source_id); + } + + 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 + 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); + + 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`} }; + } + + const fhirPatient = response.data.entry[0].resource; + addId(fhirPatient, chtPatientIdentifierType, chtFormDoc.patient_id); + + // 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' }); +} + +export async function createEncounter(chtReport: any) { + const fhirEncounter = buildFhirEncounterFromCht(chtReport); + + const bundle: fhir4.Bundle = { + resourceType: 'Bundle', + type: 'collection', + entry: [fhirEncounter] + } + + 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); + } + } + return createFhirResource(bundle); +} diff --git a/mediator/src/mappers/cht.ts b/mediator/src/mappers/cht.ts index de327141..2a4b13a8 100644 --- a/mediator/src/mappers/cht.ts +++ b/mediator/src/mappers/cht.ts @@ -36,9 +36,7 @@ export function buildChtPatientFromFhir(fhirPatient: fhir4.Patient): any { phone_number: tc?.value, sex: fhirPatient.gender, age_in_days: age_in_days, - //TODO: decouple from openmrs - openmrs_patient_uuid: fhirPatient.id, - openmrs_id: getIdType(fhirPatient, openMRSIdentifierType) + external_id: fhirPatient.id }; return updateObject; @@ -81,7 +79,7 @@ export function buildFhirPatientFromCht(chtPatient: any): fhir4.Patient { export function buildFhirEncounterFromCht(chtReport: any): fhir4.Encounter { const patientRef: fhir4.Reference = { - reference: `Patient/${chtReport.patient_id}`, + reference: `Patient/${chtReport.patient_uuid}`, type: "Patient" }; diff --git a/mediator/src/routes/cht.ts b/mediator/src/routes/cht.ts index 8d5c1617..4444d10b 100644 --- a/mediator/src/routes/cht.ts +++ b/mediator/src/routes/cht.ts @@ -1,10 +1,6 @@ import { Router } from 'express'; import { requestHandler } from '../utils/request'; -import { createFhirResource, updateFhirResource, getFHIRPatientResource } from '../utils/fhir'; -import { addId } from '../utils/fhir'; -import { getPatientUUIDFromSourceId } from '../utils/cht'; -import { buildFhirObservationFromCht, buildFhirEncounterFromCht, buildFhirPatientFromCht } from '../mappers/cht'; -import { chtPatientIdentifierType, chtDocumentIdentifierType } from '../mappers/cht'; +import { createPatient, updatePatientIds, createEncounter } from '../controllers/cht' const router = Router(); @@ -12,64 +8,17 @@ const resourceType = 'Patient'; router.post( '/patient', - requestHandler(async (req) => { - // hack for sms forms: if source_id but not _id, - // first get patient id from source - if (req.body.doc.source_id){ - req.body.doc._id = await getPatientUUIDFromSourceId(req.body.source_id); - } - - const fhirPatient = buildFhirPatientFromCht(req.body.doc); - // create or update in the FHIR Server - // note that either way, its a PUT with the id from the patient doc - return updateFhirResource({ ...fhirPatient, resourceType: 'Patient' }); - }) + requestHandler((req) => createPatient(req.body)) ); router.post( '/patient_ids', - requestHandler(async (req) => { - const chtFormDoc = req.body.doc; - - // first, get the existing patient from fhir server - var response = await getFHIRPatientResource(chtFormDoc.openmrs_patient_uuid); - - 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`} }; - } - - const fhirPatient = response.data.entry[0].resource; - addId(fhirPatient, chtPatientIdentifierType, chtFormDoc.patient_id); - - // 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' }); - }) + requestHandler((req) => updatePatientIds(req.body.doc)) ); router.post( '/encounter', - requestHandler(async (req) => { - const chtReport = req.body; - const fhirEncounter = buildFhirEncounterFromCht(chtReport); - - const bundle: fhir4.Bundle = { - resourceType: 'Bundle', - type: 'collection', - entry: [fhirEncounter] - } - - for (const entry of req.body.observations) { - if (entry.valueCode || entry.valueString || entry.valueDateTime) { - const observation = buildFhirObservationFromCht(chtReport, fhirEncounter, entry); - bundle.entry?.push(observation); - } - } - return createFhirResource(bundle); - }) + requestHandler((req) => createEncounter(req.body)) ); + export default router; diff --git a/mediator/src/utils/cht.ts b/mediator/src/utils/cht.ts index 09e896d5..09a7fba4 100644 --- a/mediator/src/utils/cht.ts +++ b/mediator/src/utils/cht.ts @@ -3,9 +3,6 @@ import { CHT } from '../../config'; import { generateBasicAuthUrl } from './url'; import https from 'https'; import path from 'path'; -import { logger } from '../../logger'; -import { openMRSIdentifierType } from '../mappers/openmrs'; -import { getIdType, copyIdToNamedIdentifier } from './fhir'; import { buildChtPatientFromFhir } from '../mappers/cht'; type CouchDBQuery = { @@ -38,8 +35,8 @@ export async function createChtRecord(patientId: string) { async function getLocation(fhirPatient: fhir4.Patient) { // first, extract address value; is fchv area available? const addresses = fhirPatient.address?.[0]?.extension?.[0]?.extension; - var addressKey = "http://fhir.openmrs.org/ext/address#address4" - var addressValue = addresses?.find((ext: any) => ext.url === addressKey)?.valueString; + let addressKey = "http://fhir.openmrs.org/ext/address#address4" + let addressValue = addresses?.find((ext: any) => ext.url === addressKey)?.valueString; if (!addressValue) { // no fchv area, use next highest address diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index b793813d..b72b99fc 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -1,7 +1,6 @@ import { getFhirResourcesSince, updateFhirResource, getIdType } from './fhir' import { getOpenMRSResourcesSince, createOpenMRSResource } from './openmrs' -import { buildOpenMRSPatient, buildOpenMRSVisit, buildOpenMRSObservation } from '../mappers/openmrs' -import { openMRSIdentifierType } from '../mappers/openmrs' +import { buildOpenMRSPatient, buildOpenMRSVisit, openMRSIdentifierType } from '../mappers/openmrs' import { createChtPatient, chtRecordFromObservations } from './cht' interface ComparisonResult { @@ -10,7 +9,7 @@ interface ComparisonResult { } async function getResources(resourceType: string): Promise { - var lastUpdated = new Date(); + const lastUpdated = new Date(); lastUpdated.setDate(lastUpdated.getDate() - 1); const fhirResponse = await getFhirResourcesSince(lastUpdated, resourceType); diff --git a/mediator/src/utils/tests/openmrs_sync.spec.ts b/mediator/src/utils/tests/openmrs_sync.spec.ts index 900c7d6b..a3501a33 100644 --- a/mediator/src/utils/tests/openmrs_sync.spec.ts +++ b/mediator/src/utils/tests/openmrs_sync.spec.ts @@ -5,6 +5,9 @@ import * as cht from '../cht'; import { PatientFactory } from '../../middlewares/schemas/tests/fhir-resource-factories'; +import axios from 'axios'; +jest.mock('axios'); + describe('OpenMRS Sync', () => { it('compares resources with the gvien key', async () => { jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ diff --git a/openmrs-poller/Dockerfile b/openmrs-poller/Dockerfile deleted file mode 100644 index 6a2b1ae8..00000000 --- a/openmrs-poller/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM python:3 - -WORKDIR /app - -COPY . /app - -RUN pip3 install --no-cache-dir schedule requests - -CMD ["python", "poll_openmrs.py"] diff --git a/openmrs-poller/poll_openmrs.py b/openmrs-poller/poll_openmrs.py deleted file mode 100644 index 0370d608..00000000 --- a/openmrs-poller/poll_openmrs.py +++ /dev/null @@ -1,89 +0,0 @@ -import requests -import json -import schedule -import time -import datetime -import os - -openmrs_url = os.getenv('OPENMRS_URL') -openmrs_username = os.getenv('OPENMRS_USER') -openmrs_password = os.getenv('OPENMRS_PASSWORD') - -openhim_url = os.getenv('OPENHIM_URL') -openhim_username = os.getenv('OPENHIM_USER') -openhim_password = os.getenv('OPENHIM_PASSWORD') - - -last_updated = datetime.datetime.utcnow().isoformat() -patients_already_sent = [] -encounters_already_sent = [] - -def fetch_new_patient_data(): - try: - print('Fetching new patients') - patient_url = f"{openmrs_url}Patient/?_lastUpdated=gt{last_updated}" - response = requests.get(patient_url, auth=(openmrs_username, openmrs_password)) - if response.status_code == 200 and response.json()['total'] > 0: - patients = response.json()['entry'] - for patient in patients: - if patient['resource']['id'] not in patients_already_sent: - print("Sending new patient") - print(patient) - response = post_data_to_openhim(patient['resource'], 'patient') - if response.status_code == 200 or response.status_code == 201: - patients_already_sent.append(patient['resource']['id']) - else: - print(f"Failed to fetch patient data from OpenMRS. Status code: {response.status_code}") - except requests.exceptions.RequestException as e: - print(f"Error fetching patient data: {e}") - -def fetch_new_observations(): - print('Fetching new observations') - try: - encounter_url = f"{openmrs_url}Encounter/?_lastUpdated=gt{last_updated}" - response = requests.get(encounter_url, auth=(openmrs_username, openmrs_password)) - if response.status_code == 200 and response.json()['total'] > 0: - encounters = response.json()['entry'] - for encounter in encounters: - if (encounter['resource']['id'] not in encounters_already_sent) and ('partOf' in encounter['resource']): - print("Sending new encounter") - print(encounter) - patient_id = encounter['resource']['subject']['reference'].split('/')[-1] - patient_url = f"{openmrs_url}Observation/?subject={patient_id}" - response = requests.get(patient_url, auth=(openmrs_username, openmrs_password)) - if response.status_code == 200: - bundle = response.json() - patient_url = f"{openmrs_url}Patient/{patient_id}" - response = requests.get(patient_url, auth=(openmrs_username, openmrs_password)) - if response.status_code == 200 or response.status_code == 201: - # add patient to the bundle to send to openhim - bundle['entry'].append({'resource': response.json()}) - response = post_data_to_openhim(bundle, 'encounter') - if response.status_code == 200 or response.status_code == 201: - encounters_already_sent.append(encounter['resource']['id']) - else: - print(f"Failed to fetch patient data from OpenMRS. Status code: {response.status_code}") - except requests.exceptions.RequestException as e: - print(f"Error fetching patient data: {e}") - -def post_data_to_openhim(data, suffix): - response = requests.post( - f"{openhim_url}/{suffix}", - json=data, - auth=(openhim_username, openhim_password), - verify=False, - headers={'Content-Type': 'application/json'} - ) - if response.status_code == 201: - print("Patient data posted to OpenHIM successfully.") - else: - print(f"Failed to post patient data to OpenHIM. Status code: {response.status_code}") - print(response.text) - return response - -schedule.every(1).minutes.do(fetch_new_patient_data) -schedule.every(1).minutes.do(fetch_new_observations) - -while True: - schedule.run_pending() - time.sleep(1) From 613bf6b064eafde72ce68e001e3532a60a955aca Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Fri, 24 May 2024 14:13:18 +0300 Subject: [PATCH 15/67] feat(#114): add openmrs channel to configurator --- configurator/Dockerfile | 11 ++++-- configurator/config/index.js | 16 +++++++- configurator/index.js | 4 +- configurator/libs/generators.js | 63 ++++++++++++++++++++++++++++++- docker/docker-compose.openmrs.yml | 49 ++++++++++++++++++++++++ 5 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 docker/docker-compose.openmrs.yml diff --git a/configurator/Dockerfile b/configurator/Dockerfile index 55a051a3..0e23ce33 100644 --- a/configurator/Dockerfile +++ b/configurator/Dockerfile @@ -1,10 +1,9 @@ FROM node:16-alpine -RUN apk add g++ make py3-pip git curl chromium - WORKDIR /scripts/configurator -COPY ./configurator ./ +COPY ./configurator/package.json ./package.json +COPY ./configurator/package-lock.json ./package-lock.json RUN npm install @@ -12,7 +11,11 @@ WORKDIR /scripts/cht-config COPY ../cht-config ./ -RUN npm install && npm install -g cht-conf && python -m pip install git+https://github.com/medic/pyxform.git@medic-conf-1.17#egg=pyxform-medic +RUN npm install && npm install -g cht-conf + +WORKDIR /scripts/configurator + +COPY ./configurator ./ WORKDIR /scripts diff --git a/configurator/config/index.js b/configurator/config/index.js index 68dce649..164db124 100644 --- a/configurator/config/index.js +++ b/configurator/config/index.js @@ -9,11 +9,25 @@ const OPENHIM_API_USERNAME = const OPENHIM_CLIENT_PASSWORD = process.env.OPENHIM_CLIENT_PASSWORD || 'interop-password'; const OPENHIM_USER_PASSWORD = process.env.OPENHIM_USER_PASSWORD || 'interop-password'; +const OPENMRS_HOST = process.env.OPENMRS || 'openmrs'; +const OPENMRS_PORT = process.env.OPENMRS_PORT || 8090; +const OPENMRS_USERNAME = + process.env.OPENMRS_USERNAME || 'admin'; +const OPENMRS_PASSWORD = + process.env.OPENMRS_PASSWORD || 'Admin123'; +const OPENMRS_PROTOCOL = process.env.OPENMRS_PROTOCOL || 'http' + module.exports = { OPENHIM_API_HOSTNAME, OPENHIM_API_PASSWORD, OPENHIM_API_PORT, OPENHIM_API_USERNAME, OPENHIM_CLIENT_PASSWORD, - OPENHIM_USER_PASSWORD + OPENHIM_USER_PASSWORD, + + OPENMRS_HOST, + OPENMRS_PORT, + OPENMRS_USERNAME, + OPENMRS_PASSWORD, + OPENMRS_PROTOCOL }; diff --git a/configurator/index.js b/configurator/index.js index 9262294e..c303c54c 100644 --- a/configurator/index.js +++ b/configurator/index.js @@ -1,6 +1,7 @@ const {OPENHIM_USER_PASSWORD, OPENHIM_CLIENT_PASSWORD} = require('./config'); +const {OPENMRS_USERNAME, OPENMRS_PASSWORD, OPENMRS_HOST, OPENMRS_PORT, OPENMRS_PROTOCOL} = require('./config'); const {generateApiOptions, generateAuthHeaders} = require('./libs/authentication'); -const {generateUser, generateClient, generateHapiFihrChannel} = require('./libs/generators'); +const {generateUser, generateClient, generateHapiFihrChannel, generateOpenMRSChannel} = require('./libs/generators'); const {fetch} = require('./utils'); const logger = require('./logger'); @@ -16,6 +17,7 @@ async function handleConfiguration () { metadata.Users.push(await generateUser(OPENHIM_USER_PASSWORD)); metadata.Clients.push(await generateClient(OPENHIM_CLIENT_PASSWORD)); metadata.Channels.push(await generateHapiFihrChannel()); + metadata.Channels.push(await generateOpenMRSChannel(OPENMRS_HOST, OPENMRS_PORT, OPENMRS_USERNAME, OPENMRS_PASSWORD, OPENMRS_PROTOCOL)); const data = JSON.stringify(metadata); const apiOptions = generateApiOptions('/metadata'); diff --git a/configurator/libs/generators.js b/configurator/libs/generators.js index 87178838..c56c4ec7 100644 --- a/configurator/libs/generators.js +++ b/configurator/libs/generators.js @@ -94,8 +94,69 @@ async function generateHapiFihrChannel () { }; } +async function generateOpenMRSChannel (host, port, username, password, type) { + return { + methods: [ + 'GET', + 'POST', + 'DELETE', + 'PUT', + 'OPTIONS', + 'HEAD', + 'TRACE', + 'CONNECT', + 'PATCH' + ], + type: type, + allow: CLIENT_ROLES, + whitelist: [], + authType: 'private', + matchContentTypes: [], + properties: [], + txViewAcl: [], + txViewFullAcl: [], + txRerunAcl: [], + status: 'enabled', + rewriteUrls: false, + addAutoRewriteRules: true, + autoRetryEnabled: false, + autoRetryPeriodMinutes: 60, + routes: [ + { + type: type, + status: 'enabled', + forwardAuthHeader: false, + name: 'OpenMRS', + secured: false, + host: host, + port: port, + path: '', + pathTransform: 's/openmrs/openmrs\/ws\/fhir2\/R4/g', + primary: true, + username: username, + password: password + } + ], + requestBody: true, + responseBody: true, + rewriteUrlsConfig: [], + name: 'OpenMRS', + description: 'OpenMRS', + urlPattern: '^/openmrs/.*$', + priority: 1, + matchContentRegex: null, + matchContentXpath: null, + matchContentValue: null, + matchContentJson: null, + pollingSchedule: null, + tcpHost: null, + tcpPort: null, + alerts: [] + }; +} module.exports = { generateClient, generateUser, - generateHapiFihrChannel + generateHapiFihrChannel, + generateOpenMRSChannel }; diff --git a/docker/docker-compose.openmrs.yml b/docker/docker-compose.openmrs.yml new file mode 100644 index 00000000..67063c74 --- /dev/null +++ b/docker/docker-compose.openmrs.yml @@ -0,0 +1,49 @@ +version: '2.1' + +services: + openmrs-mysql: + restart: "always" + image: mysql:5.6 + command: "mysqld --character-set-server=utf8 --collation-server=utf8_general_ci" + environment: + MYSQL_DATABASE: ${MYSQL_DB:-openmrs} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-Admin123} + MYSQL_USER: ${MYSQL_USER:-openmrs} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-Admin123} + ports: + - "3306:3306" + healthcheck: + test: "exit 0" + volumes: + - openmrs-referenceapplication-mysql-data:/var/lib/mysql + networks: + - cht-net + + openmrs: + restart: "always" + image: openmrs/openmrs-reference-application-distro:demo + depends_on: + - openmrs-mysql + ports: + - "8090:8080" + environment: + DB_DATABASE: ${MYSQL_DB:-openmrs} + DB_HOST: openmrs-referenceapplication-mysql + DB_USERNAME: ${MYSQL_USER:-openmrs} + DB_PASSWORD: ${MYSQL_PASSWORD:-Admin123} + DB_CREATE_TABLES: 'true' + DB_AUTO_UPDATE: 'true' + MODULE_WEB_ADMIN: 'true' + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/openmrs/"] + timeout: 20s + volumes: + - openmrs-referenceapplication-data:/usr/local/tomcat/.OpenMRS/ + - /usr/local/tomcat/.OpenMRS/modules/ # do not store modules in data + - /usr/local/tomcat/.OpenMRS/owa/ # do not store owa in data + networks: + - cht-net + +volumes: + openmrs-referenceapplication-mysql-data: + openmrs-referenceapplication-data: From 6d03a4f463933a267960f423da30246df28cfaf8 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Mon, 27 May 2024 11:12:25 +0300 Subject: [PATCH 16/67] 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({ From aba7c95f210812714f719ef603c80f4c83826c55 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Fri, 31 May 2024 18:06:25 +0300 Subject: [PATCH 17/67] feat(#114): get observations and patients together with encounters --- mediator/src/controllers/cht.ts | 2 +- mediator/src/mappers/cht.ts | 24 +++ mediator/src/middlewares/schemas/encounter.ts | 1 + mediator/src/middlewares/schemas/patient.ts | 1 + .../schemas/tests/fhir-resource-factories.ts | 2 + mediator/src/routes/cht.ts | 10 +- mediator/src/utils/cht.ts | 62 +++---- mediator/src/utils/fhir.ts | 41 +++-- mediator/src/utils/openmrs.ts | 26 +-- mediator/src/utils/openmrs_sync.ts | 157 +++++++++++------- mediator/src/utils/tests/openmrs_sync.spec.ts | 41 ++++- 11 files changed, 229 insertions(+), 138 deletions(-) diff --git a/mediator/src/controllers/cht.ts b/mediator/src/controllers/cht.ts index a49768e6..274f8c3c 100644 --- a/mediator/src/controllers/cht.ts +++ b/mediator/src/controllers/cht.ts @@ -63,7 +63,7 @@ export async function createEncounter(chtReport: any) { for (const entry of chtReport.observations) { if (entry.valueCode || entry.valueString || entry.valueDateTime) { const observation = buildFhirObservationFromCht(chtReport.patient_uuid, fhirEncounter, entry); - updateFhirResource(observation); + createFhirResource(observation); } } diff --git a/mediator/src/mappers/cht.ts b/mediator/src/mappers/cht.ts index 2014e877..8f4be9b8 100644 --- a/mediator/src/mappers/cht.ts +++ b/mediator/src/mappers/cht.ts @@ -140,3 +140,27 @@ export function buildFhirObservationFromCht(patient_id: string, encounter: fhir4 return observation; } + +export async function buildChtRecordFromObservations(patient: fhir4.Patient, observations: fhir4.Observation[]) { + const patientId = getIdType(patient, chtDocumentIdentifierType); + + const record: any = { + _meta: { + form: "openmrs_anc" + }, + patient_id: patientId + } + + observations.forEach((observation: fhir4.Observation) => { + if ( observation?.code?.coding && observation.code.coding.length > 0){ + const code = observation.code.coding[0].code?.toLowerCase() || ''; + if (observation.valueCodeableConcept) { + record[code] = observation.valueCodeableConcept.text; + } else if (observation.valueDateTime) { + record[code] = observation.valueDateTime.split('T')[0]; + } + } + }); + + return record; +} diff --git a/mediator/src/middlewares/schemas/encounter.ts b/mediator/src/middlewares/schemas/encounter.ts index f43f2fbf..b6d3d50c 100644 --- a/mediator/src/middlewares/schemas/encounter.ts +++ b/mediator/src/middlewares/schemas/encounter.ts @@ -1,6 +1,7 @@ import joi from 'joi'; export const EncounterSchema = joi.object({ + resourceType: joi.string(), id: joi.string().uuid(), identifier: joi .array() diff --git a/mediator/src/middlewares/schemas/patient.ts b/mediator/src/middlewares/schemas/patient.ts index 35a4a1b0..0b05b6cc 100644 --- a/mediator/src/middlewares/schemas/patient.ts +++ b/mediator/src/middlewares/schemas/patient.ts @@ -1,6 +1,7 @@ import joi from 'joi'; export const PatientSchema = joi.object({ + resourceType: joi.string(), id: joi.string().uuid(), identifier: joi .array() diff --git a/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts b/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts index c978b590..a489694d 100644 --- a/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts +++ b/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts @@ -17,6 +17,7 @@ export const HumanNameFactory = Factory.define('humanName') .attr('given', ['John']); export const PatientFactory = Factory.define('patient') + .attr('resourceType', 'Patient') .attr('id', randomUUID()) .attr('identifier', identifier) .attr('name', () => [HumanNameFactory.build()]) @@ -24,6 +25,7 @@ export const PatientFactory = Factory.define('patient') .attr('birthDate', '2000-01-01'); export const EncounterFactory = Factory.define('encounter') + .attr('resourceType', 'Encounter') .attr('id', randomUUID()) .attr('identifier', identifier) .attr('status', 'planned') diff --git a/mediator/src/routes/cht.ts b/mediator/src/routes/cht.ts index 9e1d3de4..91f9a029 100644 --- a/mediator/src/routes/cht.ts +++ b/mediator/src/routes/cht.ts @@ -1,7 +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' +import { syncPatients, syncEncounters } from '../utils/openmrs_sync' const router = Router(); @@ -25,8 +25,12 @@ router.post( router.post( '/sync', requestHandler(async (req) => { - await syncPatients(); - syncEncountersAndObservations(); + async function syncAll() { + await syncPatients(); + await syncEncounters(); + } + // dont await, return immediately + syncAll(); return { status: 200, data: {}}; }) ); diff --git a/mediator/src/utils/cht.ts b/mediator/src/utils/cht.ts index b83e4f35..f951c9b1 100644 --- a/mediator/src/utils/cht.ts +++ b/mediator/src/utils/cht.ts @@ -3,7 +3,8 @@ import { CHT } from '../../config'; import { generateBasicAuthUrl } from './url'; import https from 'https'; import path from 'path'; -import { buildChtPatientFromFhir } from '../mappers/cht'; +import { buildChtPatientFromFhir, buildChtRecordFromObservations } from '../mappers/cht'; +import { logger } from '../../logger'; type CouchDBQuery = { selector: Record; @@ -29,7 +30,13 @@ export async function createChtRecord(patientId: string) { const chtApiUrl = generateChtRecordsApiUrl(CHT.url, CHT.username, CHT.password); - return await axios.post(chtApiUrl, record, getOptions()); + try { + const res = await axios.post(chtApiUrl, record, getOptions()); + return { status: res?.status, data: res?.data }; + } catch (error: any) { + logger.error(error); + return { status: error.status, data: error.data }; + } } async function getLocation(fhirPatient: fhir4.Patient) { @@ -105,47 +112,42 @@ export async function createChtPatient(fhirPatient: fhir4.Patient) { return chtRecordsApi(cht_patient); } -export async function chtRecordFromObservations(patient_id: string, observations: any) { - const record: any = { - _meta: { - form: "openmrs_anc" - }, - patient_id: patient_id - } - - for (const entry of observations.entry) { - if (entry.resource.resourceType == 'Observation') { - const code:string = entry.resource.code.coding[0].code; - if (entry.resource.valueCodeableConcept) { - record[code.toLowerCase()] = entry.resource.valueCodeableConcept.text; - } else if (entry.resource.valueDateTime) { - record[code.toLowerCase()] = entry.resource.valueDateTime.split('T')[0]; - } - } - } - +export async function chtRecordFromObservations(patient: fhir4.Patient, observations: fhir4.Observation[]) { + const record = buildChtRecordFromObservations(patient, observations); return chtRecordsApi(record); } -export async function updateChtDocument(doc: any, update_object: any) { - const chtApiUrl = generateChtDBUrl(CHT.url, CHT.username, CHT.password); - const updated_doc = { ...doc, ...update_object } - return await axios.put(path.join(chtApiUrl, doc._id), updated_doc, getOptions()); -} - export async function chtRecordsApi(doc: any) { const chtApiUrl = generateChtRecordsApiUrl(CHT.url, CHT.username, CHT.password); - return await axios.post(chtApiUrl, doc, getOptions()); + try { + const res = await axios.post(chtApiUrl, doc, getOptions()); + return { status: res?.status, data: res?.data }; + } catch (error: any) { + logger.error(error); + return { status: error.status, data: error.data }; + } } export async function getChtDocumentById(doc_id: string) { const chtApiUrl = generateChtDBUrl(CHT.url, CHT.username, CHT.password); - return await axios.get(path.join(chtApiUrl, doc_id), getOptions()); + try { + const res = await axios.get(path.join(chtApiUrl, doc_id), getOptions()); + return { status: res?.status, data: res?.data }; + } catch (error: any) { + logger.error(error); + return { status: error.status, data: error.data }; + } } export async function queryCht(query: any) { const chtApiUrl = generateChtDBUrl(CHT.url, CHT.username, CHT.password); - return await axios.post(path.join(chtApiUrl, '_find'), query, getOptions()); + try { + const res = await axios.post(path.join(chtApiUrl, '_find'), query, getOptions()); + return { status: res?.status, data: res?.data }; + } catch (error: any) { + logger.error(error); + return { status: error.status, data: error.data }; + } } export const generateChtRecordsApiUrl = (chtUrl: string, username: string, password: string) => { diff --git a/mediator/src/utils/fhir.ts b/mediator/src/utils/fhir.ts index 4a6f4df9..87d40393 100644 --- a/mediator/src/utils/fhir.ts +++ b/mediator/src/utils/fhir.ts @@ -114,13 +114,13 @@ export async function getFHIRPatients(lastUpdated: Date) { ); } -export function copyIdToNamedIdentifier(fromResource: any, toResource: fhir4.Patient | fhir4.Encounter, fromIDType: fhir4.CodeableConcept){ +export function copyIdToNamedIdentifier(fromResource: any, toResource: fhir4.Patient | fhir4.Encounter, fromIdType: fhir4.CodeableConcept){ const identifier: fhir4.Identifier = { - type: fromIDType, + type: fromIdType, value: fromResource.id, use: "secondary" }; - const sameIdType = (id: any) => (id.type.text === fromIDType.text) + const sameIdType = (id: any) => (id.type.text === fromIdType.text) if (!toResource.identifier?.some(sameIdType)) { toResource.identifier?.push(identifier); } @@ -128,7 +128,7 @@ export function copyIdToNamedIdentifier(fromResource: any, toResource: fhir4.Pat } export function getIdType(resource: fhir4.Patient | fhir4.Encounter, idType: fhir4.CodeableConcept): string{ - return resource.identifier?.find((id: any) => id?.type.text == idType.text)?.value || ''; + return resource?.identifier?.find((id: any) => id?.type.text == idType.text)?.value || ''; } export function addId(resource: fhir4.Patient | fhir4.Encounter, idType: fhir4.CodeableConcept, value: string){ @@ -140,6 +140,14 @@ export function addId(resource: fhir4.Patient | fhir4.Encounter, idType: fhir4.C return resource; } +export function replaceReference(resource: any, referenceKey: string, referred: fhir4.Resource) { + const newReference: fhir4.Reference = { + reference: `${referred.resourceType}/${referred.id}`, + type: referred.resourceType + } + resource[referenceKey] = newReference; +} + export async function getFHIRLocation(locationId: string) { return await axios.get( `${FHIR.url}/Patient/?identifier=${locationId}`, @@ -152,13 +160,7 @@ export async function deleteFhirSubscription(id?: string) { export async function createFhirResource(doc: fhir4.Resource) { try { - const res = await axios.post(`${FHIR.url}/${doc.resourceType}`, doc, { - auth: { - username: FHIR.username, - password: FHIR.password, - }, - }); - + const res = await axios.post(`${FHIR.url}/${doc.resourceType}`, doc, axiosOptions); return { status: res.status, data: res.data }; } catch (error: any) { logger.error(error); @@ -168,12 +170,7 @@ export async function createFhirResource(doc: fhir4.Resource) { export async function updateFhirResource(doc: fhir4.Resource) { try { - const res = await axios.put(`${FHIR.url}/${doc.resourceType}/${doc.id}`, doc, { - auth: { - username: FHIR.username, - password: FHIR.password, - }, - }); + const res = await axios.put(`${FHIR.url}/${doc.resourceType}/${doc.id}`, doc, axiosOptions); return { status: res?.status, data: res?.data }; } catch (error: any) { @@ -184,10 +181,12 @@ export async function updateFhirResource(doc: fhir4.Resource) { export async function getFhirResourcesSince(lastUpdated: Date, resourceType: string) { try { - const res = await axios.get( - `${FHIR.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`, - axiosOptions - ); + let url = `${FHIR.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`; + // for encounters, include related resources + if (resourceType === 'Encounter') { + url = url + '&_revinclude=Observation:encounter&_include=Encounter:patient'; + } + const res = await axios.get(url, axiosOptions); return { status: res?.status, data: res?.data }; } catch (error: any) { logger.error(error); diff --git a/mediator/src/utils/openmrs.ts b/mediator/src/utils/openmrs.ts index 5c33211a..a43ce6a7 100644 --- a/mediator/src/utils/openmrs.ts +++ b/mediator/src/utils/openmrs.ts @@ -18,10 +18,12 @@ export async function getOpenMRSPatientResource(patientId: string) { export async function getOpenMRSResourcesSince(lastUpdated: Date, resourceType: string) { try { - const res = await axios.get( - `${OPENMRS.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`, - axiosOptions - ); + let url = `${OPENMRS.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`; + // for encounters, include related resources + if (resourceType === 'Encounter') { + url = url + '&_revinclude=Observation:encounter&_include=Encounter:patient'; + } + const res = await axios.get(url, axiosOptions); return { status: res.status, data: res.data }; } catch (error: any) { logger.error(error); @@ -31,13 +33,7 @@ export async function getOpenMRSResourcesSince(lastUpdated: Date, resourceType: export async function createOpenMRSResource(doc: fhir4.Resource) { try { - const res = await axios.post(`${OPENMRS.url}/${doc.resourceType}`, doc, { - auth: { - username: OPENMRS.username, - password: OPENMRS.password, - }, - }); - + const res = await axios.post(`${OPENMRS.url}/${doc.resourceType}`, doc, axiosOptions); return { status: res.status, data: res.data }; } catch (error: any) { logger.error(error); @@ -47,13 +43,7 @@ export async function createOpenMRSResource(doc: fhir4.Resource) { export async function updateOpenMRSResource(doc: fhir4.Resource) { try { - const res = await axios.put(`${OPENMRS.url}/${doc.resourceType}/${doc.id}`, doc, { - auth: { - username: OPENMRS.username, - password: OPENMRS.password, - }, - }); - + const res = await axios.post(`${OPENMRS.url}/${doc.resourceType}/${doc.id}`, doc, axiosOptions); return { status: res.status, data: res.data }; } catch (error: any) { logger.error(error); diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index 62000c8f..3def3a19 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -1,11 +1,21 @@ -import { getFhirResourcesSince, updateFhirResource, getIdType, copyIdToNamedIdentifier, getFhirResource } from './fhir' +import { + getFhirResourcesSince, + updateFhirResource, + createFhirResource, + getIdType, + copyIdToNamedIdentifier, + getFhirResource, + replaceReference, + getFHIRPatientResource +} from './fhir' import { getOpenMRSResourcesSince, createOpenMRSResource } from './openmrs' -import { buildOpenMRSPatient, buildOpenMRSVisit, openMRSIdentifierType } from '../mappers/openmrs' +import { buildOpenMRSPatient, buildOpenMRSVisit, buildOpenMRSObservation, openMRSIdentifierType } from '../mappers/openmrs' import { createChtPatient, chtRecordFromObservations } from './cht' interface ComparisonResources { fhirResources: fhir4.Resource[], - openMRSResources: fhir4.Resource[] + openMRSResources: fhir4.Resource[], + references: fhir4.Resource[] } /* @@ -15,19 +25,29 @@ async function getResources(resourceType: string): Promise const lastUpdated = new Date(); lastUpdated.setDate(lastUpdated.getDate() - 1); + function onlyType(resource: fhir4.Resource) { + return resource.resourceType === resourceType; + } + let references: fhir4.Resource[] = [] + const fhirResponse = await getFhirResourcesSince(lastUpdated, resourceType); - const fhirResources: fhir4.Resource[] = fhirResponse.data.entry?.map((entry: any) => entry.resource) || []; + let fhirResources: fhir4.Resource[] = fhirResponse.data.entry?.map((entry: any) => entry.resource) || []; + references = references.concat(fhirResources.filter((resource) => !onlyType(resource))); + fhirResources = fhirResources.filter(onlyType); const openMRSResponse = await getOpenMRSResourcesSince(lastUpdated, resourceType); - const openMRSResources: fhir4.Resource[] = openMRSResponse.data.entry?.map((entry: any) => entry.resource) || []; + let openMRSResources: fhir4.Resource[] = openMRSResponse.data.entry?.map((entry: any) => entry.resource) || []; + references = references.concat(openMRSResources.filter((resource) => !onlyType(resource))); + openMRSResources = openMRSResources.filter(onlyType); - return { fhirResources: fhirResources, openMRSResources: openMRSResources }; + return { fhirResources: fhirResources, openMRSResources: openMRSResources, references: references }; } interface ComparisonResult { toupdate: fhir4.Resource[], incoming: fhir4.Resource[], - outgoing: fhir4.Resource[] + outgoing: fhir4.Resource[], + references: fhir4.Resource[] } /* @@ -44,13 +64,15 @@ export async function compare( getKey: (resource: any) => string, resourceType: string ): Promise { + const comparison = await getResources(resourceType); + const results: ComparisonResult = { toupdate: [], incoming: [], - outgoing: [] + outgoing: [], + references: comparison.references } - const comparison = await getResources(resourceType); // get the key for each resource and create a Map const fhirIds = new Map(comparison.fhirResources.map(resource => [getKey(resource), resource])); @@ -112,6 +134,45 @@ export async function syncPatients(){ }); } +const getEncounterKey = (encounter: any) => { return getIdType(encounter, openMRSIdentifierType) || encounter.id }; + +async function sendEncounterToOpenMRS( + encounter: fhir4.Encounter, + patient: fhir4.Patient, + observations: fhir4.Observation[] +) { + const patientId = getIdType(patient, openMRSIdentifierType); + const openMRSVisit = buildOpenMRSVisit(patientId, encounter); + const visitResponse = await createOpenMRSResource(openMRSVisit[0]); + if (visitResponse.status == 200 || visitResponse.status == 201) { + const visitNoteResponse = await createOpenMRSResource(openMRSVisit[1]); + if (visitNoteResponse.status == 200 || visitNoteResponse.status == 201) { + const visitNote = visitNoteResponse.data as fhir4.Encounter; + // save openmrs id on orignal encounter + copyIdToNamedIdentifier(visitNote, encounter, openMRSIdentifierType); + updateFhirResource(encounter); + observations.forEach((observation) => { + const openMRSObservation = buildOpenMRSObservation(observation, patientId, visitNote.id || ''); + createOpenMRSResource(openMRSObservation); + }); + } + } +} + +async function sendEncounterToFhir( + encounter: fhir4.Encounter, + patient: fhir4.Patient, + observations: fhir4.Observation[] +) { + copyIdToNamedIdentifier(encounter, encounter, openMRSIdentifierType); + replaceReference(encounter, 'subject', patient); + const response = await updateFhirResource(encounter); + observations.forEach((observation) => { + replaceReference(observation, 'subject', patient); + createFhirResource(observation); + }); +} + /* Sync Encounters and Observations For incoming encounters, saves them to FHIR Server, then gathers related Observations @@ -119,62 +180,44 @@ export async function syncPatients(){ For outgoing, converts to OpenMRS format and sends to OpenMRS Updates to Observations and Encounters are not allowed */ -export async function syncEncountersAndObservations(){ - const getEncounterKey = (encounter: any) => { return getIdType(encounter, openMRSIdentifierType) || encounter.id }; +export async function syncEncounters(){ 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: ComparisonResult = await compare(getObservationKey, 'Observation'); + function getPatient(encounter: fhir4.Encounter): fhir4.Patient { + return encounters.references.filter((resource) => { + return resource.resourceType === 'Patient' && `Patient/${resource.id}` === encounter.subject?.reference + })[0] as fhir4.Patient; + } - const encountersToCht = new Map(); + function getObservations(encounter: fhir4.Encounter): fhir4.Observation[] { + return encounters.references.filter((resource) => { + if (resource.resourceType === 'Observation') { + const observation = resource as fhir4.Observation; + return observation.encounter?.reference === `Encounter/${encounter.id}` + } else { + return false; + } + }) as fhir4.Observation[]; + } - // 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 encounter = openMRSResource as fhir4.Encounter; - copyIdToNamedIdentifier(encounter, encounter, openMRSIdentifierType); - const response = await updateFhirResource(encounter); - - // save to map to push to cht below - encountersToCht.set(getEncounterKey(encounter), { - observations: [], - 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; - encountersToCht.get(getEncounter(observation)).observations.push(observation); - const response = await updateFhirResource(openMRSResource); - }); - - // 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]); + const patient = getPatient(encounter); + if (patient && patient.id) { + const observations = getObservations(encounter); + const patientResponse = await getFHIRPatientResource(patient.id); + if (patientResponse.status == 200 || patientResponse.status == 201) { + const existingPatient = patientResponse.data?.entry[0]; + sendEncounterToFhir(encounter, existingPatient, observations); + chtRecordFromObservations(existingPatient.id, observations); + } } }); - observations.outgoing.forEach(async (resource) => { - const response = await createOpenMRSResource(resource); - }); - - encountersToCht.forEach(async (key: string, value: any) => { - const patientId = value.encounter.subject.reference.split('/')[1]; - const response = await chtRecordFromObservations(patientId, value.observations); + encounters.outgoing.forEach(async (openMRSResource) => { + const encounter = openMRSResource as fhir4.Encounter; + const patient = getPatient(encounter); + const observations = getObservations(encounter); + sendEncounterToOpenMRS(encounter, patient, observations); }); } diff --git a/mediator/src/utils/tests/openmrs_sync.spec.ts b/mediator/src/utils/tests/openmrs_sync.spec.ts index 513d5e5b..974b7b41 100644 --- a/mediator/src/utils/tests/openmrs_sync.spec.ts +++ b/mediator/src/utils/tests/openmrs_sync.spec.ts @@ -1,4 +1,4 @@ -import { compare, syncPatients, syncEncountersAndObservations } from '../openmrs_sync'; +import { compare, syncPatients, syncEncounters } from '../openmrs_sync'; import * as fhir from '../fhir'; import * as openmrs from '../openmrs'; import * as cht from '../cht'; @@ -12,15 +12,15 @@ describe('OpenMRS Sync', () => { it('compares resources with the gvien key', async () => { jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ data: { entry: [ - { resource: {id: 'outgoing'}}, - { resource: {id: 'toupdate'}} + { resource: {id: 'outgoing', resourceType: 'Patient'}}, + { resource: {id: 'toupdate', resourceType: 'Patient'}} ] }, status: 200, }); jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ data: { entry: [ - { resource: {id: 'incoming'}}, - { resource: {id: 'toupdate'}} + { resource: {id: 'incoming', resourceType: 'Patient'}}, + { resource: {id: 'toupdate', resourceType: 'Patient'}} ] }, status: 200, }); @@ -28,9 +28,34 @@ describe('OpenMRS Sync', () => { const getKey = (obj: any) => { return obj.id }; const comparison = await compare(getKey, 'Patient') - expect(comparison.incoming).toEqual([{id: 'incoming'}]); - expect(comparison.outgoing).toEqual([{id: 'outgoing'}]); - expect(comparison.toupdate).toEqual([{id: 'toupdate'}]); + expect(comparison.incoming).toEqual([{id: 'incoming', resourceType: 'Patient'}]); + expect(comparison.outgoing).toEqual([{id: 'outgoing', resourceType: 'Patient'}]); + expect(comparison.toupdate).toEqual([{id: 'toupdate', resourceType: 'Patient'}]); + + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); + }); + + it('loads references for related resources', async () => { + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: { entry: [ + { resource: {id: 'resource0', resourceType: 'Encounter'}}, + { resource: {id: 'reference0', resourceType: 'Patient'}} + ] }, + status: 200, + }); + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: { entry: [ + { resource: {id: 'resource0', resourceType: 'Encounter'}}, + ] }, + status: 200, + }); + + const getKey = (obj: any) => { return obj.id }; + const comparison = await compare(getKey, 'Encounter') + + expect(comparison.references).toEqual([{id: 'reference0', resourceType: 'Patient'}]); + expect(comparison.toupdate).toEqual([{id: 'resource0', resourceType: 'Encounter'}]); expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); From fda229f3954ddccd641eccee50d67f11b261fee9 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Fri, 31 May 2024 18:22:48 +0300 Subject: [PATCH 18/67] feat(#114): configurator changes --- cht-config/app_settings.json | 134 ++++++++++++++++++++--- configurator/Dockerfile | 2 +- configurator/config/index.js | 2 +- configurator/libs/generators.js | 2 +- docker/docker-compose.openmrs-poller.yml | 15 --- 5 files changed, 120 insertions(+), 35 deletions(-) delete mode 100644 docker/docker-compose.openmrs-poller.yml diff --git a/cht-config/app_settings.json b/cht-config/app_settings.json index 80522ce3..7a9a319a 100644 --- a/cht-config/app_settings.json +++ b/cht-config/app_settings.json @@ -442,11 +442,11 @@ "mark_for_outbound": true }, "outbound": { - "FHIR_patient": { - "relevant_to": "doc.type === 'person' && doc.role == 'patient'", + "patient": { + "relevant_to": "doc.type === 'person' && doc.patient_id && doc.role === 'patient'", "destination": { "base_url": "http://openhim-core:5001", - "path": "/mediator/patient", + "path": "/mediator/cht/patient", "auth": { "type": "basic", "username": "interop-client", @@ -454,19 +454,29 @@ } }, "mapping": { - "resourceType": { - "expr": "'Patient'" - }, - "identifier": { - "expr": "[{ \"system\": \"cht\", \"use\": \"official\", \"value\": doc._id }]", - "optional": false - }, - "name": { - "expr": "[ { \"use\":\"official\", \"family\": doc.name , \"given\": [ doc.short_name ] } ]", - "optional": false - }, - "gender": "doc.sex", - "birthDate": "doc.date_of_birth" + "doc._id": "doc._id", + "doc.name": "doc.name", + "doc.phone": "doc.phone", + "doc.date_of_birth": "doc.date_of_birth", + "doc.sex": "doc.sex", + "doc.patient_id": "doc.patient_id" + } + }, + "patient_id": { + "relevant_to": "doc.type === 'data_record' && doc.form === 'OPENMRS_PATIENT'", + "destination": { + "base_url": "http://openhim-core:5001", + "path": "/mediator/cht/patient_ids", + "auth": { + "type": "basic", + "username": "interop-client", + "password_key": "openhim1" + } + }, + "mapping": { + "doc._id": "doc._id", + "doc.patient_id": "doc.patient_id", + "doc.external_id": "doc.fields.external_id" } }, "FHIR_practitioner": { @@ -570,6 +580,96 @@ }, "set_task": true } + }, + "OPENMRS_PATIENT": { + "meta": { + "code": "openmrs_patient", + "translation_key": "forms.n.title", + "icon": "medic-person" + }, + "fields": { + "age_in_days": { + "labels": { + "tiny": { + "en": "Age in Days" + }, + "short": { + "en": "Age in Days" + } + }, + "position": 0, + "type": "integer", + "required": true + }, + "patient_name": { + "labels": { + "tiny": { + "en": "patient_name" + }, + "short": { + "en": "Patient Name" + } + }, + "position": 1, + "type": "string", + "length": [ + 1, + 40 + ], + "required": true + }, + "phone_number": { + "labels": { + "tiny": { + "en": "patient phone" + }, + "short": { + "en": "patient Phone" + } + }, + "position": 2, + "flags":{ + "allow_duplicate": false + }, + "type": "phone_number", + "required": true + }, + "location_id": { + "labels": { + "tiny": { + "en": "location_id" + }, + "short": { + "en": "location_id" + } + }, + "position": 3, + "type": "string", + "length": [ + 1, + 60 + ], + "required": true + }, + "external_id": { + "labels": { + "tiny": { + "en": "OpenMRS ID" + }, + "short": { + "en": "OpenMRS ID" + } + }, + "position": 4, + "type": "string", + "length": [ + 1, + 60 + ], + "required": true + } + }, + "public_form": true } } -} \ No newline at end of file +} diff --git a/configurator/Dockerfile b/configurator/Dockerfile index 0e23ce33..65f8af85 100644 --- a/configurator/Dockerfile +++ b/configurator/Dockerfile @@ -11,7 +11,7 @@ WORKDIR /scripts/cht-config COPY ../cht-config ./ -RUN npm install && npm install -g cht-conf +RUN npm install && npm install -g cht-conf && python -m pip install git+https://github.com/medic/pyxform.git@medic-conf-1.17#egg=pyxform-medic WORKDIR /scripts/configurator diff --git a/configurator/config/index.js b/configurator/config/index.js index 164db124..7967c821 100644 --- a/configurator/config/index.js +++ b/configurator/config/index.js @@ -10,7 +10,7 @@ const OPENHIM_CLIENT_PASSWORD = process.env.OPENHIM_CLIENT_PASSWORD || 'interop- const OPENHIM_USER_PASSWORD = process.env.OPENHIM_USER_PASSWORD || 'interop-password'; const OPENMRS_HOST = process.env.OPENMRS || 'openmrs'; -const OPENMRS_PORT = process.env.OPENMRS_PORT || 8090; +const OPENMRS_PORT = process.env.OPENMRS_PORT || 8080; const OPENMRS_USERNAME = process.env.OPENMRS_USERNAME || 'admin'; const OPENMRS_PASSWORD = diff --git a/configurator/libs/generators.js b/configurator/libs/generators.js index c56c4ec7..3e272c4f 100644 --- a/configurator/libs/generators.js +++ b/configurator/libs/generators.js @@ -131,7 +131,7 @@ async function generateOpenMRSChannel (host, port, username, password, type) { host: host, port: port, path: '', - pathTransform: 's/openmrs/openmrs\/ws\/fhir2\/R4/g', + pathTransform: 's/openmrs/openmrs\\/ws\\/fhir2\\/R4/g', primary: true, username: username, password: password diff --git a/docker/docker-compose.openmrs-poller.yml b/docker/docker-compose.openmrs-poller.yml deleted file mode 100644 index 0eec108c..00000000 --- a/docker/docker-compose.openmrs-poller.yml +++ /dev/null @@ -1,15 +0,0 @@ -version: '3.9' - -services: - openmrs-poller: - build: ../openmrs-poller - container_name: openmrs-poller - networks: - - cht-net - environment: - - OPENMRS_URL=${OPENMRS_URL} - - OPENMRS_USER=${OPENMRS_USER} - - OPENMRS_PASSWORD=${OPENMRS_PASSWORD} - - OPENHIM_URL=${OPENHIM_URL} - - OPENHIM_USER=${OPENHIM_USER} - - OPENHIM_PASSWORD=${OPENHIM_PASSWORD} From f7a834d0a593aba851089731af5d17f97351b297 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Fri, 31 May 2024 18:32:28 +0300 Subject: [PATCH 19/67] feat(#114): add openmrs to startup --- docker/docker-compose.openmrs.yml | 4 ++-- startup.sh | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docker/docker-compose.openmrs.yml b/docker/docker-compose.openmrs.yml index 67063c74..7b9ee060 100644 --- a/docker/docker-compose.openmrs.yml +++ b/docker/docker-compose.openmrs.yml @@ -1,7 +1,7 @@ version: '2.1' services: - openmrs-mysql: + openmrs-referenceapplication-mysql: restart: "always" image: mysql:5.6 command: "mysqld --character-set-server=utf8 --collation-server=utf8_general_ci" @@ -23,7 +23,7 @@ services: restart: "always" image: openmrs/openmrs-reference-application-distro:demo depends_on: - - openmrs-mysql + - openmrs-referenceapplication-mysql ports: - "8090:8080" environment: diff --git a/startup.sh b/startup.sh index 6e7dcf30..97856bc7 100755 --- a/startup.sh +++ b/startup.sh @@ -2,15 +2,15 @@ if [ "$1" == "init" ]; then # start up docker containers - docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.cht-couchdb.yml -f ./docker/docker-compose.mediator.yml up -d --build + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.cht-couchdb.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.openmrs.yml up -d --build elif [ "$1" == "up" ]; then - docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.cht-couchdb.yml up -d + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.cht-couchdb.yml -f ./docker/docker-compose.openmrs.yml up -d elif [ "$1" == "up-dev" ]; then - docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.cht-couchdb.yml -f ./docker/docker-compose.mediator.yml up -d --build + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.cht-couchdb.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.openmrs.yml up -d --build elif [ "$1" == "down" ]; then - docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.cht-couchdb.yml stop + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.cht-couchdb.yml -f ./docker/docker-compose.openmrs.yml stop elif [ "$1" == "destroy" ]; then - docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.cht-couchdb.yml down -v + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.cht-couchdb.yml -f ./docker/docker-compose.openmrs.yml down -v else echo "Invalid option $1 From dfe749d42eaa5386c62a4016d917dee7221d76e6 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Tue, 4 Jun 2024 15:48:14 +0300 Subject: [PATCH 20/67] feat(#114): add timeout, fix timing issues --- mediator/config/index.ts | 3 + mediator/index.ts | 11 + mediator/src/mappers/cht.ts | 4 +- mediator/src/mappers/openmrs.ts | 2 +- mediator/src/utils/cht.ts | 1 + mediator/src/utils/fhir.ts | 29 +-- mediator/src/utils/openmrs.ts | 1 + mediator/src/utils/openmrs_sync.ts | 224 ++++++++++++------ mediator/src/utils/tests/openmrs_sync.spec.ts | 2 +- 9 files changed, 182 insertions(+), 95 deletions(-) diff --git a/mediator/config/index.ts b/mediator/config/index.ts index 6f5a3960..e29dc703 100644 --- a/mediator/config/index.ts +++ b/mediator/config/index.ts @@ -14,18 +14,21 @@ export const FHIR = { url: getEnvironmentVariable('FHIR_URL', 'http://openhim-core:5001/fhir'), username: getEnvironmentVariable('FHIR_USERNAME', 'interop-client'), password: getEnvironmentVariable('FHIR_PASSWORD', 'interop-password'), + timeout: Number(getEnvironmentVariable('REQUEST_TIMEOUT', '5000')) }; export const CHT = { url: getEnvironmentVariable('CHT_URL', 'https://nginx'), username: getEnvironmentVariable('CHT_USERNAME', 'admin'), password: getEnvironmentVariable('CHT_PASSWORD', 'password'), + timeout: Number(getEnvironmentVariable('REQUEST_TIMEOUT', '5000')) }; export const OPENMRS = { url: getEnvironmentVariable('OPENMRS_URL', 'http://openhim-core:5001/openmrs'), username: getEnvironmentVariable('OPENMRS_USERNAME', 'interop-client'), password: getEnvironmentVariable('OPENMRS_PASSWORD', 'interop-password'), + timeout: Number(getEnvironmentVariable('REQUEST_TIMEOUT', '5000')) }; function getEnvironmentVariable(env: string, def: string) { diff --git a/mediator/index.ts b/mediator/index.ts index a27d7e2e..4f1236e4 100644 --- a/mediator/index.ts +++ b/mediator/index.ts @@ -10,6 +10,7 @@ import organizationRoutes from './src/routes/organization'; import endpointRoutes from './src/routes/endpoint'; import chtRoutes from './src/routes/cht'; import { registerMediatorCallback } from './src/utils/openhim'; +import { syncPatients, syncEncounters } from './src/utils/openmrs_sync' import os from 'os'; const {registerMediator} = require('openhim-mediator-utils'); @@ -40,6 +41,16 @@ if (process.env.NODE_ENV !== 'test') { // TODO => inject the 'port' and 'http scheme' into 'mediatorConfig' registerMediator(OPENHIM, mediatorConfig, registerMediatorCallback); + + // start patient and ecnounter sync in the background + setInterval(async () => { + try { + await syncPatients(); + await syncEncounters(); + } catch (error: any) { + logger.error(error); + } + }, Number(process.env.SYNC_INTERVAL || (1000 * 60))); } export default app; diff --git a/mediator/src/mappers/cht.ts b/mediator/src/mappers/cht.ts index 8f4be9b8..f921cca5 100644 --- a/mediator/src/mappers/cht.ts +++ b/mediator/src/mappers/cht.ts @@ -96,6 +96,8 @@ export function buildFhirEncounterFromCht(chtReport: any): fhir4.Encounter { } } + copyIdToNamedIdentifier(encounter, encounter, chtDocumentIdentifierType); + return encounter } @@ -141,7 +143,7 @@ export function buildFhirObservationFromCht(patient_id: string, encounter: fhir4 return observation; } -export async function buildChtRecordFromObservations(patient: fhir4.Patient, observations: fhir4.Observation[]) { +export function buildChtRecordFromObservations(patient: fhir4.Patient, observations: fhir4.Observation[]) { const patientId = getIdType(patient, chtDocumentIdentifierType); const record: any = { diff --git a/mediator/src/mappers/openmrs.ts b/mediator/src/mappers/openmrs.ts index a649fb1f..366e8f39 100644 --- a/mediator/src/mappers/openmrs.ts +++ b/mediator/src/mappers/openmrs.ts @@ -36,7 +36,7 @@ From a fhir Encounter One CHT encounter will become 2 OpenMRS Encounters */ export function buildOpenMRSVisit(patientId: string, fhirEncounter: fhir4.Encounter): fhir4.Encounter[] { - const openMRSVisit = fhirEncounter; + const openMRSVisit = { ...fhirEncounter }; openMRSVisit.type = [visitType] const subjectRef: fhir4.Reference = { diff --git a/mediator/src/utils/cht.ts b/mediator/src/utils/cht.ts index f951c9b1..5cc86da3 100644 --- a/mediator/src/utils/cht.ts +++ b/mediator/src/utils/cht.ts @@ -16,6 +16,7 @@ function getOptions(){ httpsAgent: new https.Agent({ rejectUnauthorized: false, }), + timeout: CHT.timeout }; return options; } diff --git a/mediator/src/utils/fhir.ts b/mediator/src/utils/fhir.ts index 87d40393..cb58c3c2 100644 --- a/mediator/src/utils/fhir.ts +++ b/mediator/src/utils/fhir.ts @@ -10,6 +10,7 @@ const axiosOptions = { username: FHIR.username, password: FHIR.password, }, + timeout: FHIR.timeout }; const fhir = new Fhir(); @@ -101,17 +102,16 @@ export async function getFHIROrgEndpointResource(id: string) { } export async function getFHIRPatientResource(patientId: string) { - return await axios.get( - `${FHIR.url}/Patient/?identifier=${patientId}`, - axiosOptions - ); -} - -export async function getFHIRPatients(lastUpdated: Date) { - return await axios.get( - `${FHIR.url}/Patient/?_lastUpdated=gt${lastUpdated.toISOString()}`, - axiosOptions - ); + try { + const res = await axios.get( + `${FHIR.url}/Patient/?identifier=${patientId}`, + axiosOptions + ); + return { status: res?.status, data: res?.data }; + } catch (error: any) { + logger.error(error); + return { status: error.status, data: error.data }; + } } export function copyIdToNamedIdentifier(fromResource: any, toResource: fhir4.Patient | fhir4.Encounter, fromIdType: fhir4.CodeableConcept){ @@ -120,6 +120,7 @@ export function copyIdToNamedIdentifier(fromResource: any, toResource: fhir4.Pat value: fromResource.id, use: "secondary" }; + toResource.identifier = toResource.identifier || []; const sameIdType = (id: any) => (id.type.text === fromIdType.text) if (!toResource.identifier?.some(sameIdType)) { toResource.identifier?.push(identifier); @@ -148,12 +149,6 @@ export function replaceReference(resource: any, referenceKey: string, referred: resource[referenceKey] = newReference; } -export async function getFHIRLocation(locationId: string) { - return await axios.get( - `${FHIR.url}/Patient/?identifier=${locationId}`, - axiosOptions - ); -} export async function deleteFhirSubscription(id?: string) { return await axios.delete(`${FHIR.url}/Subscription/${id}`, axiosOptions); } diff --git a/mediator/src/utils/openmrs.ts b/mediator/src/utils/openmrs.ts index a43ce6a7..14041e8b 100644 --- a/mediator/src/utils/openmrs.ts +++ b/mediator/src/utils/openmrs.ts @@ -7,6 +7,7 @@ const axiosOptions = { username: OPENMRS.username, password: OPENMRS.password, }, + timeout: OPENMRS.timeout }; export async function getOpenMRSPatientResource(patientId: string) { diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index 3def3a19..a7b2954e 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -3,6 +3,7 @@ import { updateFhirResource, createFhirResource, getIdType, + addId, copyIdToNamedIdentifier, getFhirResource, replaceReference, @@ -10,7 +11,9 @@ import { } from './fhir' import { getOpenMRSResourcesSince, createOpenMRSResource } from './openmrs' import { buildOpenMRSPatient, buildOpenMRSVisit, buildOpenMRSObservation, openMRSIdentifierType } from '../mappers/openmrs' +import { chtDocumentIdentifierType } from '../mappers/cht' import { createChtPatient, chtRecordFromObservations } from './cht' +import { logger } from '../../logger'; interface ComparisonResources { fhirResources: fhir4.Resource[], @@ -23,7 +26,8 @@ interface ComparisonResources { */ async function getResources(resourceType: string): Promise { const lastUpdated = new Date(); - lastUpdated.setDate(lastUpdated.getDate() - 1); + //lastUpdated.setDate(lastUpdated.getDate() - 1); + lastUpdated.setHours(lastUpdated.getHours() - 1); function onlyType(resource: fhir4.Resource) { return resource.resourceType === resourceType; @@ -31,13 +35,19 @@ async function getResources(resourceType: string): Promise let references: fhir4.Resource[] = [] const fhirResponse = await getFhirResourcesSince(lastUpdated, resourceType); + if (fhirResponse.status != 200 || !fhirResponse.data.entry) { + throw new Error(`Error ${fhirResponse.status} when requesting FHIR resources`); + } let fhirResources: fhir4.Resource[] = fhirResponse.data.entry?.map((entry: any) => entry.resource) || []; - references = references.concat(fhirResources.filter((resource) => !onlyType(resource))); + references = references.concat(fhirResources); fhirResources = fhirResources.filter(onlyType); const openMRSResponse = await getOpenMRSResourcesSince(lastUpdated, resourceType); + if (openMRSResponse.status != 200 || !openMRSResponse.data.entry) { + throw new Error(`Error ${openMRSResponse.status} when requesting OpenMRS resources`); + } let openMRSResources: fhir4.Resource[] = openMRSResponse.data.entry?.map((entry: any) => entry.resource) || []; - references = references.concat(openMRSResources.filter((resource) => !onlyType(resource))); + references = references.concat(openMRSResources); openMRSResources = openMRSResources.filter(onlyType); return { fhirResources: fhirResources, openMRSResources: openMRSResources, references: references }; @@ -91,9 +101,41 @@ export async function compare( results.outgoing.push(resource); }); + logger.info(`Comparing ${resourceType}`); + logger.info(`Incoming: ${results.incoming.map(r => r.id)}`); + logger.info(`Outgoing: ${results.outgoing.map(r => r.id)}`); return results; } +/* + Send a patient from OpenMRS in the FHIR server + And forward to CHT if successful +*/ +async function sendPatientToFhir(patient: fhir4.Patient) { + logger.info(`Sending Patient ${patient.id} to FHIR`); + copyIdToNamedIdentifier(patient, patient, openMRSIdentifierType); + const response = await updateFhirResource(patient); + if (response.status == 200 || response.status == 201) { + logger.info(`Sending Patient ${patient.id} to CHT`); + createChtPatient(response.data); + } +} + +/* + Send a patient from CHT to OpenMRS + And update OpenMRS Id if successful +*/ +async function sendPatientToOpenMRS(patient: fhir4.Patient) { + logger.info(`Sending Patient ${patient.id} to OpenMRS`); + 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); + logger.info(`Updating Patient ${patient.id} with openMRSId ${response.data.id}`); + await updateFhirResource(patient); + } +} /* Sync Patients between OpenMRS and FHIR compare patient resources @@ -104,43 +146,55 @@ export async function syncPatients(){ const getKey = (fhirPatient: any) => { return getIdType(fhirPatient, openMRSIdentifierType) || fhirPatient.id }; const results: ComparisonResult = await compare(getKey, 'Patient'); - results.incoming.forEach(async (openMRSResource) => { - 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); - } - }); - - /* - results.toupdate.forEach(async (openMRSResource) => { - const chtDocId = openMRSPatient.getIdType(chtIdentifierType) - if (! chtDocId ){ - const response = await updateOpenMRSResource({ ...openMRSResource, resourceType: 'Patient' }); - } + const incomingPromises = results.incoming.map(async (resource) => { + const patient = resource as fhir4.Patient; + return sendPatientToFhir(patient); }); - */ - - results.outgoing.forEach(async (resource) => { + const outgoingPromises = results.outgoing.map(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); - } + return sendPatientToOpenMRS(patient); }); + + await Promise.all([...incomingPromises, ...outgoingPromises]); } -const getEncounterKey = (encounter: any) => { return getIdType(encounter, openMRSIdentifierType) || encounter.id }; +/* + Get a patient from a list of resources, by an encounters subject reference +*/ +function getPatient(encounter: fhir4.Encounter, references: fhir4.Resource[]): fhir4.Patient { + return references.filter((resource) => { + return resource.resourceType === 'Patient' && `Patient/${resource.id}` === encounter.subject?.reference + })[0] as fhir4.Patient; +} +/* + Get a list of observations from a list of resources + where the observations encounter reference is the encounter +*/ +function getObservations(encounter: fhir4.Encounter, references: fhir4.Resource[]): fhir4.Observation[] { + return references.filter((resource) => { + if (resource.resourceType === 'Observation') { + const observation = resource as fhir4.Observation; + return observation.encounter?.reference === `Encounter/${encounter.id}` + } else { + return false; + } + }) as fhir4.Observation[]; +} + +/* + Send an encounter from CHT to OpenMRS + Saves both a Visit and VisitNote Encounter + Updates the OpenMRS Id on the CHT encounter to the VisitNote + Sends Observations for the visitNote Encounter +*/ async function sendEncounterToOpenMRS( encounter: fhir4.Encounter, - patient: fhir4.Patient, - observations: fhir4.Observation[] + references: fhir4.Resource[] ) { + logger.info(`Sending Encounter ${encounter.id} to OpenMRS`); + const patient = getPatient(encounter, references); + const observations = getObservations(encounter, references); const patientId = getIdType(patient, openMRSIdentifierType); const openMRSVisit = buildOpenMRSVisit(patientId, encounter); const visitResponse = await createOpenMRSResource(openMRSVisit[0]); @@ -149,9 +203,11 @@ async function sendEncounterToOpenMRS( if (visitNoteResponse.status == 200 || visitNoteResponse.status == 201) { const visitNote = visitNoteResponse.data as fhir4.Encounter; // save openmrs id on orignal encounter + logger.info(`Updating Encounter ${patient.id} with openMRSId ${visitNote.id}`); copyIdToNamedIdentifier(visitNote, encounter, openMRSIdentifierType); - updateFhirResource(encounter); + await updateFhirResource(encounter); observations.forEach((observation) => { + logger.info(`Sending Observation ${observation.code!.coding![0]!.code} to OpenMRS`); const openMRSObservation = buildOpenMRSObservation(observation, patientId, visitNote.id || ''); createOpenMRSResource(openMRSObservation); }); @@ -159,18 +215,60 @@ async function sendEncounterToOpenMRS( } } +/* + Send Observation from OpenMRS to FHIR + Replacing the subject reference +*/ +async function sendObservationToFhir(observation: fhir4.Observation, patient: fhir4.Patient) { + logger.info(`Sending Observation ${observation.code!.coding![0]!.code} to FHIR`); + replaceReference(observation, 'subject', patient); + createFhirResource(observation); +} + +/* + Send an Encounter from OpenMRS to FHIR + Replaces the subject reference with an existing patient id from FHIR + If there are any observations, sends them to FHIR + If this encounter matches a CHT form, gathers observations + and sends them to CHT +*/ async function sendEncounterToFhir( encounter: fhir4.Encounter, - patient: fhir4.Patient, - observations: fhir4.Observation[] + references: fhir4.Resource[] ) { - copyIdToNamedIdentifier(encounter, encounter, openMRSIdentifierType); - replaceReference(encounter, 'subject', patient); - const response = await updateFhirResource(encounter); - observations.forEach((observation) => { - replaceReference(observation, 'subject', patient); - createFhirResource(observation); - }); + logger.info(`Sending Encounter ${encounter.id} to FHIR`); + const patient = getPatient(encounter, references); + const observations = getObservations(encounter, references); + if (patient && patient.id) { + // get patient from FHIR to resolve reference + const patientResponse = await getFHIRPatientResource(patient.id); + if (patientResponse.status == 200 || patientResponse.status == 201) { + const existingPatient = patientResponse.data?.entry[0].resource; + copyIdToNamedIdentifier(encounter, encounter, openMRSIdentifierType); + + logger.info(`Replacing ${encounter.subject!.reference} with ${patient.id} for ${encounter.id}`); + replaceReference(encounter, 'subject', existingPatient); + + // remove unused references + delete encounter.participant; + delete encounter.location; + + const response = await updateFhirResource(encounter); + if (response.status == 200 || response.status == 201) { + observations.forEach(o => sendObservationToFhir(o, existingPatient)); + + logger.info(`Sending Encounter ${encounter.id} to CHT`); + const chtResponse = await chtRecordFromObservations(existingPatient.id, observations); + if (chtResponse.status == 200) { + const chtId = chtResponse.data.id; + addId(encounter, chtDocumentIdentifierType, chtId) + await updateFhirResource(encounter); + } + } + } + } else { + logger.error(`Patient ${encounter.subject!.reference} not found for ${encounter.id}`); + } } /* @@ -181,43 +279,19 @@ async function sendEncounterToFhir( Updates to Observations and Encounters are not allowed */ export async function syncEncounters(){ + const getEncounterKey = (encounter: any) => { return getIdType(encounter, openMRSIdentifierType) || encounter.id }; const encounters: ComparisonResult = await compare(getEncounterKey, 'Encounter'); - function getPatient(encounter: fhir4.Encounter): fhir4.Patient { - return encounters.references.filter((resource) => { - return resource.resourceType === 'Patient' && `Patient/${resource.id}` === encounter.subject?.reference - })[0] as fhir4.Patient; - } + // for incoming encounters, save them in order so that references are saved before + for (const resource of encounters.incoming) { + const encounter = resource as fhir4.Encounter; + await sendEncounterToFhir(encounter, encounters.references); + }; - function getObservations(encounter: fhir4.Encounter): fhir4.Observation[] { - return encounters.references.filter((resource) => { - if (resource.resourceType === 'Observation') { - const observation = resource as fhir4.Observation; - return observation.encounter?.reference === `Encounter/${encounter.id}` - } else { - return false; - } - }) as fhir4.Observation[]; - } - - encounters.incoming.forEach(async (openMRSResource) => { - const encounter = openMRSResource as fhir4.Encounter; - const patient = getPatient(encounter); - if (patient && patient.id) { - const observations = getObservations(encounter); - const patientResponse = await getFHIRPatientResource(patient.id); - if (patientResponse.status == 200 || patientResponse.status == 201) { - const existingPatient = patientResponse.data?.entry[0]; - sendEncounterToFhir(encounter, existingPatient, observations); - chtRecordFromObservations(existingPatient.id, observations); - } - } + const outgoingPromises = encounters.outgoing.map(async (resource) => { + const encounter = resource as fhir4.Encounter; + return sendEncounterToOpenMRS(encounter, encounters.references) }); - encounters.outgoing.forEach(async (openMRSResource) => { - const encounter = openMRSResource as fhir4.Encounter; - const patient = getPatient(encounter); - const observations = getObservations(encounter); - sendEncounterToOpenMRS(encounter, patient, observations); - }); + await Promise.all(outgoingPromises); } diff --git a/mediator/src/utils/tests/openmrs_sync.spec.ts b/mediator/src/utils/tests/openmrs_sync.spec.ts index 974b7b41..baaaa132 100644 --- a/mediator/src/utils/tests/openmrs_sync.spec.ts +++ b/mediator/src/utils/tests/openmrs_sync.spec.ts @@ -54,7 +54,7 @@ describe('OpenMRS Sync', () => { const getKey = (obj: any) => { return obj.id }; const comparison = await compare(getKey, 'Encounter') - expect(comparison.references).toEqual([{id: 'reference0', resourceType: 'Patient'}]); + expect(comparison.references).toContainEqual({id: 'reference0', resourceType: 'Patient'}); expect(comparison.toupdate).toEqual([{id: 'resource0', resourceType: 'Encounter'}]); expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); From 6c3ff2715247aa880a79e9f7084b22ac3f8371cc Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Thu, 6 Jun 2024 12:33:28 +0300 Subject: [PATCH 21/67] feat(#114): pagination for openmrs sync --- mediator/src/utils/fhir.ts | 36 ++++++++++++++++--- mediator/src/utils/openmrs.ts | 23 +++++++++--- mediator/src/utils/openmrs_sync.ts | 14 ++++---- mediator/src/utils/tests/openmrs_sync.spec.ts | 36 +++++++++---------- 4 files changed, 74 insertions(+), 35 deletions(-) diff --git a/mediator/src/utils/fhir.ts b/mediator/src/utils/fhir.ts index cb58c3c2..51145e4a 100644 --- a/mediator/src/utils/fhir.ts +++ b/mediator/src/utils/fhir.ts @@ -176,13 +176,28 @@ export async function updateFhirResource(doc: fhir4.Resource) { export async function getFhirResourcesSince(lastUpdated: Date, resourceType: string) { try { - let url = `${FHIR.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`; + let nextUrl = `${FHIR.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`; + let results: fhir4.Resource[] = []; // for encounters, include related resources if (resourceType === 'Encounter') { - url = url + '&_revinclude=Observation:encounter&_include=Encounter:patient'; + nextUrl = nextUrl + '&_revinclude=Observation:encounter&_include=Encounter:patient'; } - const res = await axios.get(url, axiosOptions); - return { status: res?.status, data: res?.data }; + + while (nextUrl) { + const res = await axios.get(nextUrl, axiosOptions); + + if (res.data.entry){ + results = results.concat(res.data.entry.map((entry: any) => entry.resource)); + } + + const nextLink = res.data.link && res.data.link.find((link: any) => link.relation === 'next'); + nextUrl = nextLink ? nextLink.url : null; + if (nextUrl) { + const qs = nextUrl.split('?')[1]; + nextUrl = `${FHIR.url}/?${qs}`; + } + } + return { status: 200, data: results }; } catch (error: any) { logger.error(error); return { status: error.status, data: error.data }; @@ -201,3 +216,16 @@ export async function getFhirResource(id: string, resourceType: string) { return { status: error.status, data: error.data }; } } + +export async function getQuestionnaire(name: string){ + try { + const res = await axios.get( + `${FHIR.url}/Questionnaire`, + 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.ts b/mediator/src/utils/openmrs.ts index 14041e8b..c49887bf 100644 --- a/mediator/src/utils/openmrs.ts +++ b/mediator/src/utils/openmrs.ts @@ -19,13 +19,28 @@ export async function getOpenMRSPatientResource(patientId: string) { export async function getOpenMRSResourcesSince(lastUpdated: Date, resourceType: string) { try { - let url = `${OPENMRS.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`; + let nextUrl = `${OPENMRS.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`; + let results: fhir4.Resource[] = []; // for encounters, include related resources if (resourceType === 'Encounter') { - url = url + '&_revinclude=Observation:encounter&_include=Encounter:patient'; + nextUrl = nextUrl + '&_revinclude=Observation:encounter&_include=Encounter:patient'; } - const res = await axios.get(url, axiosOptions); - return { status: res.status, data: res.data }; + + while (nextUrl) { + const res = await axios.get(nextUrl, axiosOptions); + + if (res.data.entry){ + results = results.concat(res.data.entry.map((entry: any) => entry.resource)); + } + + const nextLink = res.data.link && res.data.link.find((link: any) => link.relation === 'next'); + nextUrl = nextLink ? nextLink.url : null; + if (nextUrl) { + const qs = nextUrl.split('?')[1]; + nextUrl = `${OPENMRS.url}/?${qs}`; + } + } + return { status: 200, data: results }; } 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 a7b2954e..dd69082e 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -35,20 +35,18 @@ async function getResources(resourceType: string): Promise let references: fhir4.Resource[] = [] const fhirResponse = await getFhirResourcesSince(lastUpdated, resourceType); - if (fhirResponse.status != 200 || !fhirResponse.data.entry) { + if (fhirResponse.status != 200) { throw new Error(`Error ${fhirResponse.status} when requesting FHIR resources`); } - let fhirResources: fhir4.Resource[] = fhirResponse.data.entry?.map((entry: any) => entry.resource) || []; - references = references.concat(fhirResources); - fhirResources = fhirResources.filter(onlyType); + const fhirResources = fhirResponse.data.filter(onlyType); + references = references.concat(fhirResponse.data); const openMRSResponse = await getOpenMRSResourcesSince(lastUpdated, resourceType); - if (openMRSResponse.status != 200 || !openMRSResponse.data.entry) { + if (openMRSResponse.status != 200) { throw new Error(`Error ${openMRSResponse.status} when requesting OpenMRS resources`); } - let openMRSResources: fhir4.Resource[] = openMRSResponse.data.entry?.map((entry: any) => entry.resource) || []; - references = references.concat(openMRSResources); - openMRSResources = openMRSResources.filter(onlyType); + const openMRSResources = openMRSResponse.data.filter(onlyType); + references = references.concat(openMRSResponse.data); return { fhirResources: fhirResources, openMRSResources: openMRSResources, references: references }; } diff --git a/mediator/src/utils/tests/openmrs_sync.spec.ts b/mediator/src/utils/tests/openmrs_sync.spec.ts index baaaa132..cb7460d0 100644 --- a/mediator/src/utils/tests/openmrs_sync.spec.ts +++ b/mediator/src/utils/tests/openmrs_sync.spec.ts @@ -11,17 +11,17 @@ jest.mock('axios'); describe('OpenMRS Sync', () => { it('compares resources with the gvien key', async () => { jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ - data: { entry: [ - { resource: {id: 'outgoing', resourceType: 'Patient'}}, - { resource: {id: 'toupdate', resourceType: 'Patient'}} - ] }, + data: [ + { id: 'outgoing', resourceType: 'Patient'}, + { id: 'toupdate', resourceType: 'Patient'} + ], status: 200, }); jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ - data: { entry: [ - { resource: {id: 'incoming', resourceType: 'Patient'}}, - { resource: {id: 'toupdate', resourceType: 'Patient'}} - ] }, + data: [ + { id: 'incoming', resourceType: 'Patient'}, + { id: 'toupdate', resourceType: 'Patient'} + ], status: 200, }); @@ -38,16 +38,14 @@ describe('OpenMRS Sync', () => { it('loads references for related resources', async () => { jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ - data: { entry: [ - { resource: {id: 'resource0', resourceType: 'Encounter'}}, - { resource: {id: 'reference0', resourceType: 'Patient'}} - ] }, + data: [ + { id: 'resource0', resourceType: 'Encounter'}, + { id: 'reference0', resourceType: 'Patient'} + ], status: 200, }); jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ - data: { entry: [ - { resource: {id: 'resource0', resourceType: 'Encounter'}}, - ] }, + data: [ {id: 'resource0', resourceType: 'Encounter'} ], status: 200, }); @@ -64,11 +62,11 @@ describe('OpenMRS Sync', () => { it('sends incoming Patients to FHIR and CHT', async () => { const openMRSPatient = PatientFactory.build(); jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ - data: { entry: [] }, + data: [], status: 200, }); jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ - data: { entry: [ { resource: openMRSPatient } ] }, + data: [openMRSPatient], status: 200, }); jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ @@ -90,11 +88,11 @@ describe('OpenMRS Sync', () => { it('sends outgoing Patients to OpenMRS', async () => { const fhirPatient = PatientFactory.build(); jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ - data: { entry: [ { resource: fhirPatient } ] }, + data: [fhirPatient], status: 200, }); jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ - data: { entry: [] }, + data: [], status: 200, }); jest.spyOn(openmrs, 'createOpenMRSResource').mockResolvedValueOnce({ From 3eab817a85acc7b70337ddc0ab5ce6ad3e8b3b35 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Mon, 10 Jun 2024 11:29:41 +0300 Subject: [PATCH 22/67] feat(#114): fix reference for encounters and don't allow updates --- mediator/src/controllers/cht.ts | 28 ++++++++++++++++++++++++--- mediator/src/routes/tests/cht.spec.ts | 13 ++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/mediator/src/controllers/cht.ts b/mediator/src/controllers/cht.ts index 274f8c3c..8c852b80 100644 --- a/mediator/src/controllers/cht.ts +++ b/mediator/src/controllers/cht.ts @@ -2,6 +2,7 @@ import { createFhirResource, updateFhirResource, getFHIRPatientResource, + replaceReference, addId } from '../utils/fhir'; import { @@ -21,9 +22,18 @@ export async function createPatient(chtPatientDoc: any) { } const fhirPatient = buildFhirPatientFromCht(chtPatientDoc.doc); - // create or update in the FHIR Server - // even for create, sends a PUT request - return updateFhirResource({ ...fhirPatient, resourceType: 'Patient' }); + const patientResponse = await getFHIRPatientResource(fhirPatient.id || ''); + if (patientResponse.status != 200){ + // any error, just return it to caller + return patientResponse; + } else if (patientResponse.data.total > 0) { + // updates not currently supported + return patientResponse; + } else { + // create or update in the FHIR Server + // even for create, sends a PUT request + return updateFhirResource({ ...fhirPatient, resourceType: 'Patient' }); + } } export async function updatePatientIds(chtFormDoc: any) { @@ -53,6 +63,18 @@ export async function updatePatientIds(chtFormDoc: any) { export async function createEncounter(chtReport: any) { const fhirEncounter = buildFhirEncounterFromCht(chtReport); + + const patientResponse = await getFHIRPatientResource(chtReport.patient_uuid); + if (patientResponse.status != 200){ + // any error, just return it to caller + return patientResponse; + } else if (patientResponse.data.total == 0) { + // in case the patient is not found, return 200 to prevent retries + return { status: 200, data: { message: `Patient not found`} }; + } + + const patient = patientResponse.data.entry[0].resource as fhir4.Patient; + replaceReference(fhirEncounter, 'subject', patient); const response = await updateFhirResource(fhirEncounter); if (response.status != 200 && response.status != 201){ diff --git a/mediator/src/routes/tests/cht.spec.ts b/mediator/src/routes/tests/cht.spec.ts index c796cadd..2b0ee95f 100644 --- a/mediator/src/routes/tests/cht.spec.ts +++ b/mediator/src/routes/tests/cht.spec.ts @@ -1,6 +1,8 @@ import request from 'supertest'; import app from '../../..'; -import { ChtPatientFactory, ChtPregnancyForm } from '../../middlewares/schemas/tests/cht-request-factories'; +import { ChtPatientFactory, ChtPregnancyForm, ChtSampleForm } from '../../middlewares/schemas/tests/cht-request-factories'; +import { PatientFactory, EncounterFactory, QuestionnaireFactory, QuestionnaireResponseFactory, CodedObservationFactory, DateTimeObservationFactory } from '../../middlewares/schemas/tests/fhir-resource-factories'; +import { buildQuestionnaireResponse, extractObservations } from '../../mappers/cht'; import * as fhir from '../../utils/fhir'; import axios from 'axios'; @@ -8,6 +10,10 @@ jest.mock('axios'); describe('POST /cht/patient', () => { it('accepts incoming request with valid patient resource', async () => { + jest.spyOn(fhir, 'getFHIRPatientResource').mockResolvedValueOnce({ + data: {}, + status: 200, + }); jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ data: {}, status: 200, @@ -30,6 +36,10 @@ describe('POST /cht/patient', () => { }); it('accepts incoming request with valid form', async () => { + jest.spyOn(fhir, 'getFHIRPatientResource').mockResolvedValueOnce({ + data: { total: 1, entry: [ { resource: PatientFactory.build() } ] }, + status: 200, + }); jest.spyOn(fhir, 'createFhirResource').mockResolvedValueOnce({ data: {}, status: 200, @@ -49,4 +59,5 @@ describe('POST /cht/patient', () => { */ expect(fhir.updateFhirResource).toHaveBeenCalled(); }); + }); From 0e38c83a6d5da17c225cfd8a1cc41e1f86a4b0ac Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Mon, 10 Jun 2024 11:38:40 +0300 Subject: [PATCH 23/67] feat(#114): fix unit tests --- mediator/src/routes/tests/cht.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mediator/src/routes/tests/cht.spec.ts b/mediator/src/routes/tests/cht.spec.ts index 2b0ee95f..e8c9d522 100644 --- a/mediator/src/routes/tests/cht.spec.ts +++ b/mediator/src/routes/tests/cht.spec.ts @@ -1,8 +1,7 @@ import request from 'supertest'; import app from '../../..'; import { ChtPatientFactory, ChtPregnancyForm, ChtSampleForm } from '../../middlewares/schemas/tests/cht-request-factories'; -import { PatientFactory, EncounterFactory, QuestionnaireFactory, QuestionnaireResponseFactory, CodedObservationFactory, DateTimeObservationFactory } from '../../middlewares/schemas/tests/fhir-resource-factories'; -import { buildQuestionnaireResponse, extractObservations } from '../../mappers/cht'; +import { PatientFactory, EncounterFactory } from '../../middlewares/schemas/tests/fhir-resource-factories'; import * as fhir from '../../utils/fhir'; import axios from 'axios'; From d05968e02fa0f0016807b3cc3843130cb2b176ee Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Mon, 10 Jun 2024 11:40:43 +0300 Subject: [PATCH 24/67] feat(#114): fix unit tests --- mediator/src/routes/tests/cht.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mediator/src/routes/tests/cht.spec.ts b/mediator/src/routes/tests/cht.spec.ts index e8c9d522..22ac0fa3 100644 --- a/mediator/src/routes/tests/cht.spec.ts +++ b/mediator/src/routes/tests/cht.spec.ts @@ -1,6 +1,6 @@ import request from 'supertest'; import app from '../../..'; -import { ChtPatientFactory, ChtPregnancyForm, ChtSampleForm } from '../../middlewares/schemas/tests/cht-request-factories'; +import { ChtPatientFactory, ChtPregnancyForm } from '../../middlewares/schemas/tests/cht-request-factories'; import { PatientFactory, EncounterFactory } from '../../middlewares/schemas/tests/fhir-resource-factories'; import * as fhir from '../../utils/fhir'; import axios from 'axios'; From 70aee078d6bfe0429224bc8636ccff10046a0004 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Tue, 11 Jun 2024 13:19:50 +0300 Subject: [PATCH 25/67] feat(#114): remove cht from main startup script --- cht-config/Dockerfile | 19 +++++++++++++++ configurator/Dockerfile | 24 ++++--------------- docker/docker-compose.cht-core.yml | 34 +++++++++++++++++++++++++++ docker/docker-compose.cht-couchdb.yml | 30 ----------------------- docker/docker-compose.mediator.yml | 6 ++--- mediator/test/e2e-test.sh | 4 ++-- startup.sh | 12 ++++++---- 7 files changed, 69 insertions(+), 60 deletions(-) create mode 100644 cht-config/Dockerfile delete mode 100644 docker/docker-compose.cht-couchdb.yml diff --git a/cht-config/Dockerfile b/cht-config/Dockerfile new file mode 100644 index 00000000..69cd2f4f --- /dev/null +++ b/cht-config/Dockerfile @@ -0,0 +1,19 @@ +FROM node:16-slim + +RUN apt update \ + && apt install --no-install-recommends -y \ + git \ + python3-pip \ + python3-setuptools \ + python3-wheel + +RUN python3 -m pip install git+https://github.com/medic/pyxform.git@medic-conf-1.17#egg=pyxform-medic + +WORKDIR /scripts/cht-config + +COPY ./ ./ + +RUN npm install +RUN npm install -g cht-conf + +CMD npm run deploy diff --git a/configurator/Dockerfile b/configurator/Dockerfile index 65f8af85..bc5da0f1 100644 --- a/configurator/Dockerfile +++ b/configurator/Dockerfile @@ -1,26 +1,12 @@ -FROM node:16-alpine +FROM node:22-alpine WORKDIR /scripts/configurator -COPY ./configurator/package.json ./package.json -COPY ./configurator/package-lock.json ./package-lock.json +COPY ./package.json ./package.json +COPY ./package-lock.json ./package-lock.json RUN npm install -WORKDIR /scripts/cht-config +COPY ./ ./ -COPY ../cht-config ./ - -RUN npm install && npm install -g cht-conf && python -m pip install git+https://github.com/medic/pyxform.git@medic-conf-1.17#egg=pyxform-medic - -WORKDIR /scripts/configurator - -COPY ./configurator ./ - -WORKDIR /scripts - -RUN echo "cd /scripts/configurator && npm run configure && cd /scripts/cht-config && npm run deploy && exit 0" > ./startup.sh - -RUN chmod +x ./startup.sh - -CMD ["sh", "./startup.sh"] +CMD npm run configure diff --git a/docker/docker-compose.cht-core.yml b/docker/docker-compose.cht-core.yml index d13c53cc..795f99dc 100644 --- a/docker/docker-compose.cht-core.yml +++ b/docker/docker-compose.cht-core.yml @@ -109,9 +109,43 @@ services: networks: - cht-net + cht-configurator: + build: + context: ../cht-config + dockerfile: ./Dockerfile + environment: + - "COUCHDB_USER=${COUCHDB_USER:-admin}" + - "COUCHDB_PASSWORD=${COUCHDB_PASSWORD:-password}" + depends_on: + - couchdb + networks: + - cht-net + + couchdb: + image: public.ecr.aws/medic/cht-couchdb:4.1.0-alpha + volumes: + - ${COUCHDB_DATA:-./srv}:/opt/couchdb/data + - cht-credentials:/opt/couchdb/etc/local.d/ + environment: + - "COUCHDB_USER=${COUCHDB_USER:-admin}" + - "COUCHDB_PASSWORD=${COUCHDB_PASSWORD:-password}" + - "COUCHDB_SECRET=${COUCHDB_SECRET:-secret}" + - "COUCHDB_UUID=${COUCHDB_UUID:-CC0C3BA1-88EE-4AE3-BFD3-6E0EE56ED534}" + - "SVC_NAME=${SVC_NAME:-couchdb}" + - "COUCHDB_LOG_LEVEL=${COUCHDB_LOG_LEVEL:-error}" + restart: always + logging: + driver: "local" + options: + max-size: "${LOG_MAX_SIZE:-50m}" + max-file: "${LOG_MAX_FILES:-20}" + networks: + cht-net: + networks: cht-net: name: ${CHT_NETWORK:-cht-net} volumes: cht-ssl: + cht-credentials: diff --git a/docker/docker-compose.cht-couchdb.yml b/docker/docker-compose.cht-couchdb.yml deleted file mode 100644 index 6bc06056..00000000 --- a/docker/docker-compose.cht-couchdb.yml +++ /dev/null @@ -1,30 +0,0 @@ -version: '3.9' - -services: - couchdb: - image: public.ecr.aws/medic/cht-couchdb:4.1.0-alpha - volumes: - - ${COUCHDB_DATA:-./srv}:/opt/couchdb/data - - cht-credentials:/opt/couchdb/etc/local.d/ - environment: - - "COUCHDB_USER=${COUCHDB_USER:-admin}" - - "COUCHDB_PASSWORD=${COUCHDB_PASSWORD:-password}" - - "COUCHDB_SECRET=${COUCHDB_SECRET:-secret}" - - "COUCHDB_UUID=${COUCHDB_UUID:-CC0C3BA1-88EE-4AE3-BFD3-6E0EE56ED534}" - - "SVC_NAME=${SVC_NAME:-couchdb}" - - "COUCHDB_LOG_LEVEL=${COUCHDB_LOG_LEVEL:-error}" - restart: always - logging: - driver: "local" - options: - max-size: "${LOG_MAX_SIZE:-50m}" - max-file: "${LOG_MAX_FILES:-20}" - networks: - cht-net: - -volumes: - cht-credentials: - -networks: - cht-net: - name: ${CHT_NETWORK:-cht-net} diff --git a/docker/docker-compose.mediator.yml b/docker/docker-compose.mediator.yml index 762a1fde..3b4ce6b0 100644 --- a/docker/docker-compose.mediator.yml +++ b/docker/docker-compose.mediator.yml @@ -31,11 +31,9 @@ services: configurator: build: - context: ../ - dockerfile: ./configurator/Dockerfile + context: ../configurator + dockerfile: ./Dockerfile environment: - - "COUCHDB_USER=${COUCHDB_USER:-admin}" - - "COUCHDB_PASSWORD=${COUCHDB_PASSWORD:-password}" - "OPENHIM_API_HOSTNAME=${OPENHIM_API_HOSTNAME:-openhim-core}" - "OPENHIM_API_PORT=${OPENHIM_API_PORT:-8080}" - "OPENHIM_PASSWORD=${OPENHIM_PASSWORD:-openhim-password}" diff --git a/mediator/test/e2e-test.sh b/mediator/test/e2e-test.sh index d6a95399..2657ea04 100755 --- a/mediator/test/e2e-test.sh +++ b/mediator/test/e2e-test.sh @@ -12,11 +12,11 @@ cd $BASEDIR # Starting the interoperability containers cd $BASEDIR -./startup.sh init +./startup.sh up-test # Waiting for configurator to finish echo 'Waiting for configurator to finish...' -docker container wait chis-interop-configurator-1 +docker container wait chis-interop-cht-configurator-1 # Executing mediator e2e tests cd $MEDIATORDIR diff --git a/startup.sh b/startup.sh index 97856bc7..492c79d5 100755 --- a/startup.sh +++ b/startup.sh @@ -2,15 +2,17 @@ if [ "$1" == "init" ]; then # start up docker containers - docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.cht-couchdb.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.openmrs.yml up -d --build + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.openmrs.yml up -d --build elif [ "$1" == "up" ]; then - docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.cht-couchdb.yml -f ./docker/docker-compose.openmrs.yml up -d + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.openmrs.yml up -d elif [ "$1" == "up-dev" ]; then - docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.cht-couchdb.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.openmrs.yml up -d --build + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.openmrs.yml up -d --build elif [ "$1" == "down" ]; then - docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.cht-couchdb.yml -f ./docker/docker-compose.openmrs.yml stop + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.openmrs.yml stop elif [ "$1" == "destroy" ]; then - docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.cht-couchdb.yml -f ./docker/docker-compose.openmrs.yml down -v + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.openmrs.yml down -v +elif [ "$1" == "up-test" ]; then + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.openmrs.yml up -d --build else echo "Invalid option $1 From 6141ce832d111463fb183361bf12d2f9a3e7b6c9 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Fri, 21 Jun 2024 08:39:07 -0700 Subject: [PATCH 26/67] feat(#114): adding source ot prevent infinite loops --- mediator/src/mappers/cht.ts | 4 +++- mediator/src/mappers/openmrs.ts | 2 ++ mediator/src/utils/fhir.ts | 5 +++++ mediator/src/utils/openmrs_sync.ts | 19 ++++++++++++++++--- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/mediator/src/mappers/cht.ts b/mediator/src/mappers/cht.ts index f921cca5..bccfddcd 100644 --- a/mediator/src/mappers/cht.ts +++ b/mediator/src/mappers/cht.ts @@ -1,4 +1,4 @@ -import { getIdType, copyIdToNamedIdentifier } from '../utils/fhir'; +import { getIdType, copyIdToNamedIdentifier, addSourceMeta } from '../utils/fhir'; import { openMRSIdentifierType } from './openmrs'; export const chtDocumentIdentifierType: fhir4.CodeableConcept = { @@ -9,6 +9,8 @@ export const chtPatientIdentifierType: fhir4.CodeableConcept = { text: 'CHT Patient ID' } +export const chtSource = 'cht'; + const chwVisitType: fhir4.CodeableConcept = { text: "Communtiy Health Worker Visit", } diff --git a/mediator/src/mappers/openmrs.ts b/mediator/src/mappers/openmrs.ts index 366e8f39..99f9a5fe 100644 --- a/mediator/src/mappers/openmrs.ts +++ b/mediator/src/mappers/openmrs.ts @@ -12,6 +12,8 @@ export const openMRSIdentifierType: fhir4.CodeableConcept = { text: 'OpenMRS Patient UUID' } +export const openMRSSource = 'openmrs'; + const visitNoteType: fhir4.CodeableConcept = { text: "Visit Note", coding: [{ diff --git a/mediator/src/utils/fhir.ts b/mediator/src/utils/fhir.ts index 51145e4a..31e637a8 100644 --- a/mediator/src/utils/fhir.ts +++ b/mediator/src/utils/fhir.ts @@ -114,6 +114,11 @@ export async function getFHIRPatientResource(patientId: string) { } } +export function addSourceMeta(resource: fhir4.Resource, source: string) { + resource.meta = resource.meta || {}; + resource.meta['source'] = source; +} + export function copyIdToNamedIdentifier(fromResource: any, toResource: fhir4.Patient | fhir4.Encounter, fromIdType: fhir4.CodeableConcept){ const identifier: fhir4.Identifier = { type: fromIdType, diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index dd69082e..4fd5cb92 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -7,11 +7,12 @@ import { copyIdToNamedIdentifier, getFhirResource, replaceReference, - getFHIRPatientResource + getFHIRPatientResource, + addSourceMeta } from './fhir' import { getOpenMRSResourcesSince, createOpenMRSResource } from './openmrs' -import { buildOpenMRSPatient, buildOpenMRSVisit, buildOpenMRSObservation, openMRSIdentifierType } from '../mappers/openmrs' -import { chtDocumentIdentifierType } from '../mappers/cht' +import { buildOpenMRSPatient, buildOpenMRSVisit, buildOpenMRSObservation, openMRSIdentifierType, openMRSSource } from '../mappers/openmrs' +import { chtDocumentIdentifierType, chtSource } from '../mappers/cht' import { createChtPatient, chtRecordFromObservations } from './cht' import { logger } from '../../logger'; @@ -112,6 +113,7 @@ export async function compare( async function sendPatientToFhir(patient: fhir4.Patient) { logger.info(`Sending Patient ${patient.id} to FHIR`); copyIdToNamedIdentifier(patient, patient, openMRSIdentifierType); + addSourceMeta(patient, openMRSSource); const response = await updateFhirResource(patient); if (response.status == 200 || response.status == 201) { logger.info(`Sending Patient ${patient.id} to CHT`); @@ -126,6 +128,7 @@ async function sendPatientToFhir(patient: fhir4.Patient) { async function sendPatientToOpenMRS(patient: fhir4.Patient) { logger.info(`Sending Patient ${patient.id} to OpenMRS`); const openMRSPatient = buildOpenMRSPatient(patient); + addSourceMeta(openMRSPatient, chtSource); const response = await createOpenMRSResource(openMRSPatient); // copy openmrs identifier if successful if (response.status == 200 || response.status == 201) { @@ -190,6 +193,10 @@ async function sendEncounterToOpenMRS( encounter: fhir4.Encounter, references: fhir4.Resource[] ) { + if (encounter.meta?.source == openMRSSource) { + logger.error(`Not re-sending encounter from openMRS ${encounter.id}`); + return + } logger.info(`Sending Encounter ${encounter.id} to OpenMRS`); const patient = getPatient(encounter, references); const observations = getObservations(encounter, references); @@ -203,6 +210,7 @@ async function sendEncounterToOpenMRS( // save openmrs id on orignal encounter logger.info(`Updating Encounter ${patient.id} with openMRSId ${visitNote.id}`); copyIdToNamedIdentifier(visitNote, encounter, openMRSIdentifierType); + addSourceMeta(visitNote, chtSource); await updateFhirResource(encounter); observations.forEach((observation) => { logger.info(`Sending Observation ${observation.code!.coding![0]!.code} to OpenMRS`); @@ -234,6 +242,10 @@ async function sendEncounterToFhir( encounter: fhir4.Encounter, references: fhir4.Resource[] ) { + if (encounter.meta?.source == chtSource) { + logger.error(`Not re-sending encounter from cht ${encounter.id}`); + return + } logger.info(`Sending Encounter ${encounter.id} to FHIR`); const patient = getPatient(encounter, references); const observations = getObservations(encounter, references); @@ -243,6 +255,7 @@ async function sendEncounterToFhir( if (patientResponse.status == 200 || patientResponse.status == 201) { const existingPatient = patientResponse.data?.entry[0].resource; copyIdToNamedIdentifier(encounter, encounter, openMRSIdentifierType); + addSourceMeta(encounter, openMRSSource); logger.info(`Replacing ${encounter.subject!.reference} with ${patient.id} for ${encounter.id}`); replaceReference(encounter, 'subject', existingPatient); From 5bb2eae9c82d7a5ac53b8f701d5682303721e56f Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Fri, 21 Jun 2024 08:59:27 -0700 Subject: [PATCH 27/67] feat(#114): don't save encounters until complete --- mediator/src/utils/openmrs_sync.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index 4fd5cb92..51bbf1f5 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -246,6 +246,10 @@ async function sendEncounterToFhir( logger.error(`Not re-sending encounter from cht ${encounter.id}`); return } + if (!encounter.period?.end) { + logger.error(`Not sending encounter which is incomplete ${encounter.id}`); + return + } logger.info(`Sending Encounter ${encounter.id} to FHIR`); const patient = getPatient(encounter, references); const observations = getObservations(encounter, references); From 12e2a535aa5f4e202e90eef051b6bfe7724a58c4 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Mon, 24 Jun 2024 07:26:44 -0700 Subject: [PATCH 28/67] feat(#114): boundary conditions for sync --- mediator/src/utils/openmrs_sync.ts | 46 +++++++++++++++++++----------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index 51bbf1f5..81c2d715 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -10,6 +10,7 @@ import { getFHIRPatientResource, addSourceMeta } from './fhir' +import { SYNC_INTERVAL } from '../../config' import { getOpenMRSResourcesSince, createOpenMRSResource } from './openmrs' import { buildOpenMRSPatient, buildOpenMRSVisit, buildOpenMRSObservation, openMRSIdentifierType, openMRSSource } from '../mappers/openmrs' import { chtDocumentIdentifierType, chtSource } from '../mappers/cht' @@ -25,24 +26,20 @@ interface ComparisonResources { /* 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); - lastUpdated.setHours(lastUpdated.getHours() - 1); - +async function getResources(resourceType: string, startTime: Date): Promise { function onlyType(resource: fhir4.Resource) { return resource.resourceType === resourceType; } let references: fhir4.Resource[] = [] - const fhirResponse = await getFhirResourcesSince(lastUpdated, resourceType); + const fhirResponse = await getFhirResourcesSince(startTime, resourceType); if (fhirResponse.status != 200) { throw new Error(`Error ${fhirResponse.status} when requesting FHIR resources`); } const fhirResources = fhirResponse.data.filter(onlyType); references = references.concat(fhirResponse.data); - const openMRSResponse = await getOpenMRSResourcesSince(lastUpdated, resourceType); + const openMRSResponse = await getOpenMRSResourcesSince(startTime, resourceType); if (openMRSResponse.status != 200) { throw new Error(`Error ${openMRSResponse.status} when requesting OpenMRS resources`); } @@ -71,16 +68,17 @@ interface ComparisonResult { */ export async function compare( getKey: (resource: any) => string, - resourceType: string + resourceType: string, + startTime: Date, ): Promise { - const comparison = await getResources(resourceType); + const comparison = await getResources(resourceType, startTime); const results: ComparisonResult = { toupdate: [], incoming: [], outgoing: [], references: comparison.references - } + }; // get the key for each resource and create a Map const fhirIds = new Map(comparison.fhirResources.map(resource => [getKey(resource), resource])); @@ -92,12 +90,26 @@ export async function compare( results.toupdate.push(openMRSResource); fhirIds.delete(key); } else { - results.incoming.push(openMRSResource); + const lastUpdated = new Date(openMRSResource.meta?.lastUpdated!); + if (isNaN(lastUpdated.getTime()) || isNaN(startTime.getTime())) { + throw new Error("Invalid date format"); + } + const diff = lastUpdated.getTime() - startTime.getTime(); + if (diff > (Number(SYNC_INTERVAL) * 2)){ + results.incoming.push(openMRSResource); + } } }); fhirIds.forEach((resource, key) => { - results.outgoing.push(resource); + const lastUpdated = new Date(resource.meta?.lastUpdated || ''); + if (isNaN(lastUpdated.getTime()) || isNaN(startTime.getTime())) { + throw new Error("Invalid date format"); + } + const diff = lastUpdated.getTime() - startTime.getTime(); + if (diff > (Number(SYNC_INTERVAL) * 2)){ + results.outgoing.push(resource); + } }); logger.info(`Comparing ${resourceType}`); @@ -143,9 +155,9 @@ async function sendPatientToOpenMRS(patient: fhir4.Patient) { 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(){ +export async function syncPatients(startTime: Date){ const getKey = (fhirPatient: any) => { return getIdType(fhirPatient, openMRSIdentifierType) || fhirPatient.id }; - const results: ComparisonResult = await compare(getKey, 'Patient'); + const results: ComparisonResult = await compare(getKey, 'Patient', startTime); const incomingPromises = results.incoming.map(async (resource) => { const patient = resource as fhir4.Patient; @@ -197,6 +209,7 @@ async function sendEncounterToOpenMRS( logger.error(`Not re-sending encounter from openMRS ${encounter.id}`); return } + logger.info(`Sending Encounter ${encounter.id} to OpenMRS`); const patient = getPatient(encounter, references); const observations = getObservations(encounter, references); @@ -250,6 +263,7 @@ async function sendEncounterToFhir( logger.error(`Not sending encounter which is incomplete ${encounter.id}`); return } + logger.info(`Sending Encounter ${encounter.id} to FHIR`); const patient = getPatient(encounter, references); const observations = getObservations(encounter, references); @@ -293,9 +307,9 @@ async function sendEncounterToFhir( For outgoing, converts to OpenMRS format and sends to OpenMRS Updates to Observations and Encounters are not allowed */ -export async function syncEncounters(){ +export async function syncEncounters(startTime: Date){ const getEncounterKey = (encounter: any) => { return getIdType(encounter, openMRSIdentifierType) || encounter.id }; - const encounters: ComparisonResult = await compare(getEncounterKey, 'Encounter'); + const encounters: ComparisonResult = await compare(getEncounterKey, 'Encounter', startTime); // for incoming encounters, save them in order so that references are saved before for (const resource of encounters.incoming) { From 88f1014cf864c30f8413330ed0e1107a37c1337c Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Mon, 24 Jun 2024 07:31:19 -0700 Subject: [PATCH 29/67] feat(#114): add sync_interval to index --- mediator/config/index.ts | 2 ++ mediator/index.ts | 10 ++++++---- mediator/src/utils/tests/openmrs_sync.spec.ts | 16 ++++++++++++---- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/mediator/config/index.ts b/mediator/config/index.ts index e29dc703..51944773 100644 --- a/mediator/config/index.ts +++ b/mediator/config/index.ts @@ -31,6 +31,8 @@ export const OPENMRS = { timeout: Number(getEnvironmentVariable('REQUEST_TIMEOUT', '5000')) }; +export const SYNC_INTERVAL = getEnvironmentVariable('SYNC_INTERVAL', '6000'); + function getEnvironmentVariable(env: string, def: string) { if (process.env.NODE_ENV === 'test') { return def; diff --git a/mediator/index.ts b/mediator/index.ts index 4f1236e4..da347981 100644 --- a/mediator/index.ts +++ b/mediator/index.ts @@ -2,7 +2,7 @@ import express, {Request, Response} from 'express'; import { mediatorConfig } from './config/mediator'; import { logger } from './logger'; import bodyParser from 'body-parser'; -import {PORT, OPENHIM} from './config'; +import {PORT, OPENHIM, SYNC_INTERVAL} from './config'; import patientRoutes from './src/routes/patient'; import serviceRequestRoutes from './src/routes/service-request'; import encounterRoutes from './src/routes/encounter'; @@ -45,12 +45,14 @@ if (process.env.NODE_ENV !== 'test') { // start patient and ecnounter sync in the background setInterval(async () => { try { - await syncPatients(); - await syncEncounters(); + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + await syncPatients(startTime); + await syncEncounters(startTime); } catch (error: any) { logger.error(error); } - }, Number(process.env.SYNC_INTERVAL || (1000 * 60))); + }, Number(SYNC_INTERVAL)); } export default app; diff --git a/mediator/src/utils/tests/openmrs_sync.spec.ts b/mediator/src/utils/tests/openmrs_sync.spec.ts index cb7460d0..fed5d829 100644 --- a/mediator/src/utils/tests/openmrs_sync.spec.ts +++ b/mediator/src/utils/tests/openmrs_sync.spec.ts @@ -26,7 +26,9 @@ describe('OpenMRS Sync', () => { }); const getKey = (obj: any) => { return obj.id }; - const comparison = await compare(getKey, 'Patient') + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await compare(getKey, 'Patient', startTime) expect(comparison.incoming).toEqual([{id: 'incoming', resourceType: 'Patient'}]); expect(comparison.outgoing).toEqual([{id: 'outgoing', resourceType: 'Patient'}]); @@ -50,7 +52,9 @@ describe('OpenMRS Sync', () => { }); const getKey = (obj: any) => { return obj.id }; - const comparison = await compare(getKey, 'Encounter') + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await compare(getKey, 'Encounter', startTime) expect(comparison.references).toContainEqual({id: 'reference0', resourceType: 'Patient'}); expect(comparison.toupdate).toEqual([{id: 'resource0', resourceType: 'Encounter'}]); @@ -76,7 +80,9 @@ describe('OpenMRS Sync', () => { jest.spyOn(cht, 'createChtPatient') const getKey = (obj: any) => { return obj.id }; - const comparison = await syncPatients(); + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await syncPatients(startTime); expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); @@ -102,7 +108,9 @@ describe('OpenMRS Sync', () => { //jest.spyOn(fhir, 'updateFhirResource') const getKey = (obj: any) => { return obj.id }; - const comparison = await syncPatients(); + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await syncPatients(startTime); expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); From 7a5243c242ac4f83b4293c638714a910ec372b61 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Mon, 24 Jun 2024 07:34:13 -0700 Subject: [PATCH 30/67] feat(#114): fix tests --- mediator/src/routes/cht.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/mediator/src/routes/cht.ts b/mediator/src/routes/cht.ts index 91f9a029..4444d10b 100644 --- a/mediator/src/routes/cht.ts +++ b/mediator/src/routes/cht.ts @@ -1,7 +1,6 @@ import { Router } from 'express'; import { requestHandler } from '../utils/request'; import { createPatient, updatePatientIds, createEncounter } from '../controllers/cht' -import { syncPatients, syncEncounters } from '../utils/openmrs_sync' const router = Router(); @@ -22,17 +21,4 @@ router.post( requestHandler((req) => createEncounter(req.body)) ); -router.post( - '/sync', - requestHandler(async (req) => { - async function syncAll() { - await syncPatients(); - await syncEncounters(); - } - // dont await, return immediately - syncAll(); - return { status: 200, data: {}}; - }) -); - export default router; From 3db5945be6c0a20b9a58340ff6bb7898ed3e973c Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Mon, 24 Jun 2024 09:15:46 -0700 Subject: [PATCH 31/67] feat(#114): fix defualt sync_interval --- mediator/config/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mediator/config/index.ts b/mediator/config/index.ts index 51944773..67c5a23a 100644 --- a/mediator/config/index.ts +++ b/mediator/config/index.ts @@ -31,7 +31,7 @@ export const OPENMRS = { timeout: Number(getEnvironmentVariable('REQUEST_TIMEOUT', '5000')) }; -export const SYNC_INTERVAL = getEnvironmentVariable('SYNC_INTERVAL', '6000'); +export const SYNC_INTERVAL = getEnvironmentVariable('SYNC_INTERVAL', '60000'); function getEnvironmentVariable(env: string, def: string) { if (process.env.NODE_ENV === 'test') { From 82e29ec73f9f7fa939af34c9a4439c645f2d6d16 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Mon, 9 Sep 2024 13:45:20 +0300 Subject: [PATCH 32/67] feat(#114): fix unit tests for default sync_interval --- mediator/src/utils/tests/openmrs_sync.spec.ts | 55 ++++++++++++++----- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/mediator/src/utils/tests/openmrs_sync.spec.ts b/mediator/src/utils/tests/openmrs_sync.spec.ts index fed5d829..765c68b4 100644 --- a/mediator/src/utils/tests/openmrs_sync.spec.ts +++ b/mediator/src/utils/tests/openmrs_sync.spec.ts @@ -10,17 +10,25 @@ jest.mock('axios'); describe('OpenMRS Sync', () => { it('compares resources with the gvien key', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); + + const constants = { + resourceType: 'Patient', + meta: { lastUpdated: lastUpdated } + } + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ data: [ - { id: 'outgoing', resourceType: 'Patient'}, - { id: 'toupdate', resourceType: 'Patient'} + { id: 'outgoing', ...constants }, + { id: 'toupdate', ...constants } ], status: 200, }); jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ data: [ - { id: 'incoming', resourceType: 'Patient'}, - { id: 'toupdate', resourceType: 'Patient'} + { id: 'incoming', ...constants }, + { id: 'toupdate', ...constants } ], status: 200, }); @@ -30,24 +38,34 @@ describe('OpenMRS Sync', () => { startTime.setHours(startTime.getHours() - 1); const comparison = await compare(getKey, 'Patient', startTime) - expect(comparison.incoming).toEqual([{id: 'incoming', resourceType: 'Patient'}]); - expect(comparison.outgoing).toEqual([{id: 'outgoing', resourceType: 'Patient'}]); - expect(comparison.toupdate).toEqual([{id: 'toupdate', resourceType: 'Patient'}]); + expect(comparison.incoming).toEqual([{id: 'incoming', ...constants }]); + expect(comparison.outgoing).toEqual([{id: 'outgoing', ...constants }]); + expect(comparison.toupdate).toEqual([{id: 'toupdate', ...constants }]); expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); }); it('loads references for related resources', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); + const reference = { + id: 'reference0', + resourceType: 'Patient', + meta: { lastUpdated: lastUpdated } + }; + const resource = { + id: 'resource0', + resourceType: 'Encounter', + meta: { lastUpdated: lastUpdated } + }; + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ - data: [ - { id: 'resource0', resourceType: 'Encounter'}, - { id: 'reference0', resourceType: 'Patient'} - ], + data: [ resource, reference ], status: 200, }); jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ - data: [ {id: 'resource0', resourceType: 'Encounter'} ], + data: [ resource ], status: 200, }); @@ -56,15 +74,19 @@ describe('OpenMRS Sync', () => { startTime.setHours(startTime.getHours() - 1); const comparison = await compare(getKey, 'Encounter', startTime) - expect(comparison.references).toContainEqual({id: 'reference0', resourceType: 'Patient'}); - expect(comparison.toupdate).toEqual([{id: 'resource0', resourceType: 'Encounter'}]); + expect(comparison.references).toContainEqual(reference); + expect(comparison.toupdate).toEqual([resource]); expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); }); it('sends incoming Patients to FHIR and CHT', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); + const openMRSPatient = PatientFactory.build(); + openMRSPatient.meta = { lastUpdated: lastUpdated }; jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ data: [], status: 200, @@ -92,7 +114,12 @@ describe('OpenMRS Sync', () => { }); it('sends outgoing Patients to OpenMRS', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); + const fhirPatient = PatientFactory.build(); + fhirPatient.meta = { lastUpdated: lastUpdated }; + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ data: [fhirPatient], status: 200, From ee8e8dd4a2fc8ab25da7d007e08ba8073b34d057 Mon Sep 17 00:00:00 2001 From: njuguna-n <141340177+njuguna-n@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:52:28 +0300 Subject: [PATCH 33/67] feat: add platform to failing containers --- docker/docker-compose.openmrs.yml | 1 + docker/docker-compose.yml | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docker/docker-compose.openmrs.yml b/docker/docker-compose.openmrs.yml index 7b9ee060..1bd628cf 100644 --- a/docker/docker-compose.openmrs.yml +++ b/docker/docker-compose.openmrs.yml @@ -3,6 +3,7 @@ version: '2.1' services: openmrs-referenceapplication-mysql: restart: "always" + platform: linux/x86_64 image: mysql:5.6 command: "mysqld --character-set-server=utf8 --collation-server=utf8_general_ci" environment: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index e881eb68..5d8bba03 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -11,13 +11,14 @@ services: openhim-core: container_name: openhim-core + platform: linux/x86_64 image: jembi/openhim-core:7 environment: - mongo_url=mongodb://mongo/openhim - mongo_atnaUrl=mongodb://mongo/openhim ports: - "8080:8080" - - "5000:5000" + - "5002:5000" - "5001:5001" - "5050:5050" - "5051:5051" @@ -30,6 +31,7 @@ services: openhim-console: container_name: openhim-console + platform: linux/x86_64 image: jembi/openhim-console:1.14.4 ports: - "9000:80" From 6d2c7d2b611274088027af63ddac92462fcfd97d Mon Sep 17 00:00:00 2001 From: mrjones-plip Date: Wed, 18 Sep 2024 09:37:41 -0700 Subject: [PATCH 34/67] no service line in compose files, pin to cht core 4.10, improve startup.sh, add missing curl to config --- cht-config/Dockerfile | 3 ++- docker/docker-compose.cht-core.yml | 33 +++++++++++++++--------------- docker/docker-compose.mediator.yml | 2 -- docker/docker-compose.openmrs.yml | 2 -- docker/docker-compose.yml | 2 -- startup.sh | 12 +++++++++++ 6 files changed, 31 insertions(+), 23 deletions(-) diff --git a/cht-config/Dockerfile b/cht-config/Dockerfile index 69cd2f4f..9584778f 100644 --- a/cht-config/Dockerfile +++ b/cht-config/Dockerfile @@ -5,7 +5,8 @@ RUN apt update \ git \ python3-pip \ python3-setuptools \ - python3-wheel + python3-wheel \ + curl RUN python3 -m pip install git+https://github.com/medic/pyxform.git@medic-conf-1.17#egg=pyxform-medic diff --git a/docker/docker-compose.cht-core.yml b/docker/docker-compose.cht-core.yml index 795f99dc..521abeba 100644 --- a/docker/docker-compose.cht-core.yml +++ b/docker/docker-compose.cht-core.yml @@ -1,8 +1,7 @@ -version: '3.9' services: haproxy: - image: public.ecr.aws/medic/cht-haproxy:4.1.0-alpha + image: public.ecr.aws/medic/cht-haproxy:4.10.0 restart: always hostname: haproxy environment: @@ -12,6 +11,7 @@ services: - "COUCHDB_SERVERS=${COUCHDB_SERVERS:-couchdb}" - "HAPROXY_PORT=${HAPROXY_PORT:-5984}" - "HEALTHCHECK_ADDR=${HEALTHCHECK_ADDR:-healthcheck}" + - "DOCKER_DNS_RESOLVER=true" logging: driver: "local" options: @@ -19,14 +19,15 @@ services: max-file: "${LOG_MAX_FILES:-20}" networks: - cht-net + deploy: + resources: + limits: + memory: 1G expose: - ${HAPROXY_PORT:-5984} - ports: - - "5984:5984" - healthcheck: - image: public.ecr.aws/medic/cht-haproxy-healthcheck:4.1.0-alpha + image: public.ecr.aws/medic/cht-haproxy-healthcheck:4.10.0 restart: always environment: - "COUCHDB_SERVERS=${COUCHDB_SERVERS:-couchdb}" @@ -41,14 +42,12 @@ services: - cht-net api: - image: public.ecr.aws/medic/cht-api:4.1.0-alpha + image: public.ecr.aws/medic/cht-api:4.10.0 restart: always depends_on: - haproxy expose: - "${API_PORT:-5988}" - ports: - - "5988:5988" environment: - COUCH_URL=http://${COUCHDB_USER:-admin}:${COUCHDB_PASSWORD:-password}@haproxy:${HAPROXY_PORT:-5984}/medic - BUILDS_URL=${MARKET_URL_READ:-https://staging.dev.medicmobile.org}/${BUILDS_SERVER:-_couch/builds_4} @@ -62,7 +61,7 @@ services: - cht-net sentinel: - image: public.ecr.aws/medic/cht-sentinel:4.1.0-alpha + image: public.ecr.aws/medic/cht-sentinel:4.10.0 restart: always depends_on: - haproxy @@ -78,14 +77,14 @@ services: - cht-net nginx: - image: public.ecr.aws/medic/cht-nginx:4.1.0-alpha + image: public.ecr.aws/medic/cht-nginx:4.10.0 restart: always depends_on: - api - haproxy ports: - - "${NGINX_HTTP_PORT:-80}:80" - - "${NGINX_HTTPS_PORT:-443}:443" + - "${NGINX_HTTP_PORT:-8880}:80" + - "${NGINX_HTTPS_PORT:-8843}:443" volumes: - cht-ssl:${SSL_VOLUME_MOUNT_PATH:-/etc/nginx/private/} environment: @@ -121,8 +120,9 @@ services: networks: - cht-net + couchdb: - image: public.ecr.aws/medic/cht-couchdb:4.1.0-alpha + image: public.ecr.aws/medic/cht-couchdb:4.10.0 volumes: - ${COUCHDB_DATA:-./srv}:/opt/couchdb/data - cht-credentials:/opt/couchdb/etc/local.d/ @@ -130,7 +130,7 @@ services: - "COUCHDB_USER=${COUCHDB_USER:-admin}" - "COUCHDB_PASSWORD=${COUCHDB_PASSWORD:-password}" - "COUCHDB_SECRET=${COUCHDB_SECRET:-secret}" - - "COUCHDB_UUID=${COUCHDB_UUID:-CC0C3BA1-88EE-4AE3-BFD3-6E0EE56ED534}" + - "COUCHDB_UUID=${COUCHDB_UUID:-secret}" - "SVC_NAME=${SVC_NAME:-couchdb}" - "COUCHDB_LOG_LEVEL=${COUCHDB_LOG_LEVEL:-error}" restart: always @@ -140,7 +140,8 @@ services: max-size: "${LOG_MAX_SIZE:-50m}" max-file: "${LOG_MAX_FILES:-20}" networks: - cht-net: + - cht-net + networks: cht-net: diff --git a/docker/docker-compose.mediator.yml b/docker/docker-compose.mediator.yml index 3b4ce6b0..17126f76 100644 --- a/docker/docker-compose.mediator.yml +++ b/docker/docker-compose.mediator.yml @@ -1,5 +1,3 @@ -version: '3' - services: mediator: build: diff --git a/docker/docker-compose.openmrs.yml b/docker/docker-compose.openmrs.yml index 1bd628cf..7ed25f40 100644 --- a/docker/docker-compose.openmrs.yml +++ b/docker/docker-compose.openmrs.yml @@ -1,5 +1,3 @@ -version: '2.1' - services: openmrs-referenceapplication-mysql: restart: "always" diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 5d8bba03..659d8e06 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.9' - services: mongo: image: mongo:4.2 diff --git a/startup.sh b/startup.sh index 492c79d5..f097e84b 100755 --- a/startup.sh +++ b/startup.sh @@ -21,7 +21,19 @@ else init starts the docker containers and configures OpenHIM up starts the docker containers up-dev starts the docker containers with updated files. + up-test starts the docker containers with updated files, including CHT Core down stops the docker containers destroy shutdown the docker containers and deletes volumes " fi + +echo " + + Possible URLs after startup: + ----------- + OpenMRS http://localhost:8090/ + OpenMRS MySQL localhost:3306 + CHT Core https://localhost:8843/ + OpenHIM Console http://localhost:9000/ + +" From e91be25978efd81ea49abd4744b9ccbe6d4cf3e9 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Wed, 2 Oct 2024 16:49:56 +0300 Subject: [PATCH 35/67] fix(#138): change ltfu mediator to cht mediator and add openmrs mediator --- configurator/config/index.js | 10 +++---- configurator/index.js | 8 +++--- configurator/libs/generators.js | 4 +-- docker/docker-compose.mediator.yml | 9 ++++--- mediator/config/index.ts | 6 ++--- mediator/config/openmrs_mediator.ts | 16 +++++++++++ mediator/index.ts | 30 ++++++++++++--------- mediator/src/controllers/service-request.ts | 4 +-- mediator/src/controllers/tests/utils.ts | 6 ++--- mediator/src/utils/cht.ts | 2 +- mediator/src/utils/tests/cht.spec.ts | 6 ++--- 11 files changed, 63 insertions(+), 38 deletions(-) create mode 100644 mediator/config/openmrs_mediator.ts diff --git a/configurator/config/index.js b/configurator/config/index.js index 7967c821..579d7fec 100644 --- a/configurator/config/index.js +++ b/configurator/config/index.js @@ -9,13 +9,11 @@ const OPENHIM_API_USERNAME = const OPENHIM_CLIENT_PASSWORD = process.env.OPENHIM_CLIENT_PASSWORD || 'interop-password'; const OPENHIM_USER_PASSWORD = process.env.OPENHIM_USER_PASSWORD || 'interop-password'; -const OPENMRS_HOST = process.env.OPENMRS || 'openmrs'; +const OPENMRS_HOST = process.env.OPENMRS_HOST; const OPENMRS_PORT = process.env.OPENMRS_PORT || 8080; -const OPENMRS_USERNAME = - process.env.OPENMRS_USERNAME || 'admin'; -const OPENMRS_PASSWORD = - process.env.OPENMRS_PASSWORD || 'Admin123'; -const OPENMRS_PROTOCOL = process.env.OPENMRS_PROTOCOL || 'http' +const OPENMRS_USERNAME = process.env.OPENMRS_USERNAME; +const OPENMRS_PASSWORD = process.env.OPENMRS_PASSWORD; +const OPENMRS_PROTOCOL = process.env.OPENMRS_PROTOCOL || 'http'; module.exports = { OPENHIM_API_HOSTNAME, diff --git a/configurator/index.js b/configurator/index.js index c303c54c..3204e060 100644 --- a/configurator/index.js +++ b/configurator/index.js @@ -1,7 +1,7 @@ const {OPENHIM_USER_PASSWORD, OPENHIM_CLIENT_PASSWORD} = require('./config'); const {OPENMRS_USERNAME, OPENMRS_PASSWORD, OPENMRS_HOST, OPENMRS_PORT, OPENMRS_PROTOCOL} = require('./config'); const {generateApiOptions, generateAuthHeaders} = require('./libs/authentication'); -const {generateUser, generateClient, generateHapiFihrChannel, generateOpenMRSChannel} = require('./libs/generators'); +const {generateUser, generateClient, generateHapiFhirChannel, generateOpenMRSChannel} = require('./libs/generators'); const {fetch} = require('./utils'); const logger = require('./logger'); @@ -16,8 +16,10 @@ async function handleConfiguration () { metadata.Users.push(await generateUser(OPENHIM_USER_PASSWORD)); metadata.Clients.push(await generateClient(OPENHIM_CLIENT_PASSWORD)); - metadata.Channels.push(await generateHapiFihrChannel()); - metadata.Channels.push(await generateOpenMRSChannel(OPENMRS_HOST, OPENMRS_PORT, OPENMRS_USERNAME, OPENMRS_PASSWORD, OPENMRS_PROTOCOL)); + metadata.Channels.push(await generateHapiFhirChannel()); + if (OPENMRS_HOST) { + metadata.Channels.push(await generateOpenMRSChannel(OPENMRS_HOST, OPENMRS_PORT, OPENMRS_USERNAME, OPENMRS_PASSWORD, OPENMRS_PROTOCOL)); + } const data = JSON.stringify(metadata); const apiOptions = generateApiOptions('/metadata'); diff --git a/configurator/libs/generators.js b/configurator/libs/generators.js index 3e272c4f..43be3980 100644 --- a/configurator/libs/generators.js +++ b/configurator/libs/generators.js @@ -33,7 +33,7 @@ async function generateUser (password) { }; } -async function generateHapiFihrChannel () { +async function generateHapiFhirChannel () { return { methods: [ 'GET', @@ -157,6 +157,6 @@ async function generateOpenMRSChannel (host, port, username, password, type) { module.exports = { generateClient, generateUser, - generateHapiFihrChannel, + generateHapiFhirChannel, generateOpenMRSChannel }; diff --git a/docker/docker-compose.mediator.yml b/docker/docker-compose.mediator.yml index 17126f76..1d481dd8 100644 --- a/docker/docker-compose.mediator.yml +++ b/docker/docker-compose.mediator.yml @@ -14,9 +14,9 @@ services: - "FHIR_URL=${FHIR_URL:-http://openhim-core:5001/fhir}" - "FHIR_USERNAME=${FHIR_USERNAME:-interop-client}" - "FHIR_PASSWORD=${FHIR_PASSWORD:-interop-password}" - - "OPENMRS_URL=${OPENMRS_URL:-http://openhim-core:5001/openmrs}" - - "OPENMRS_USERNAME=${OPENMRS_USERNAME:-interop-client}" - - "OPENMRS_PASSWORD=${OPENMRS_PASSWORD:-interop-password}" + - "OPENMRS_URL=${OPENMRS_URL}" + - "OPENMRS_USERNAME=${OPENMRS_USERNAME}" + - "OPENMRS_PASSWORD=${OPENMRS_PASSWORD}" - "CHT_URL=${CHT_URL:-https://nginx}" - "CHT_USERNAME=${CHT_USERNAME:-admin}" - "CHT_PASSWORD=${CHT_PASSWORD:-password}" @@ -38,6 +38,9 @@ services: - "OPENHIM_USERNAME=${OPENHIM_USERNAME:-root@openhim.org}" - "OPENHIM_CLIENT_PASSWORD=${OPENHIM_CLIENT_PASSWORD:-interop-password}" - "OPENHIM_USER_PASSWORD=${OPENHIM_USER_PASSWORD:-interop-password}" + - "OPENMRS_HOST=${OPENMRS_HOST}" + - "OPENMRS_USERNAME=${OPENMRS_USERNAME}" + - "OPENMRS_PASSWORD=${OPENMRS_PASSWORD}" networks: - cht-net diff --git a/mediator/config/index.ts b/mediator/config/index.ts index 67c5a23a..fdeec60a 100644 --- a/mediator/config/index.ts +++ b/mediator/config/index.ts @@ -25,9 +25,9 @@ export const CHT = { }; export const OPENMRS = { - url: getEnvironmentVariable('OPENMRS_URL', 'http://openhim-core:5001/openmrs'), - username: getEnvironmentVariable('OPENMRS_USERNAME', 'interop-client'), - password: getEnvironmentVariable('OPENMRS_PASSWORD', 'interop-password'), + url: getEnvironmentVariable('OPENMRS_URL', ''), + username: getEnvironmentVariable('OPENMRS_USERNAME', ''), + password: getEnvironmentVariable('OPENMRS_PASSWORD', ''), timeout: Number(getEnvironmentVariable('REQUEST_TIMEOUT', '5000')) }; diff --git a/mediator/config/openmrs_mediator.ts b/mediator/config/openmrs_mediator.ts new file mode 100644 index 00000000..968cced7 --- /dev/null +++ b/mediator/config/openmrs_mediator.ts @@ -0,0 +1,16 @@ +export const openMRSMediatorConfig = { + urn: 'urn:mediator:openmrs-mediator', + version: '1.0.0', + name: 'OpenMRS Mediator', + description: 'A mediator to sync CHT data with OpenMRS', + endpoints: [ + { + name: 'OpenMRS Mediator', + host: 'mediator', + path: '/', + port: '6000', + primary: true, + type: 'http', + }, + ], +}; diff --git a/mediator/index.ts b/mediator/index.ts index da347981..864582fc 100644 --- a/mediator/index.ts +++ b/mediator/index.ts @@ -1,8 +1,9 @@ import express, {Request, Response} from 'express'; import { mediatorConfig } from './config/mediator'; +import { openMRSMediatorConfig } from './config/openmrs_mediator'; import { logger } from './logger'; import bodyParser from 'body-parser'; -import {PORT, OPENHIM, SYNC_INTERVAL} from './config'; +import {PORT, OPENHIM, SYNC_INTERVAL, OPENMRS} from './config'; import patientRoutes from './src/routes/patient'; import serviceRequestRoutes from './src/routes/service-request'; import encounterRoutes from './src/routes/encounter'; @@ -42,17 +43,22 @@ if (process.env.NODE_ENV !== 'test') { // TODO => inject the 'port' and 'http scheme' into 'mediatorConfig' registerMediator(OPENHIM, mediatorConfig, registerMediatorCallback); - // start patient and ecnounter sync in the background - setInterval(async () => { - try { - const startTime = new Date(); - startTime.setHours(startTime.getHours() - 1); - await syncPatients(startTime); - await syncEncounters(startTime); - } catch (error: any) { - logger.error(error); - } - }, Number(SYNC_INTERVAL)); + // if OPENMRS is specified, register its mediator + // and start the sync background task + if (OPENMRS.url) { + registerMediator(OPENHIM, openMRSMediatorConfig, registerMediatorCallback); + // start patient and ecnounter sync in the background + setInterval(async () => { + try { + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + await syncPatients(startTime); + await syncEncounters(startTime); + } catch (error: any) { + logger.error(error); + } + }, Number(SYNC_INTERVAL)); + } } export default app; diff --git a/mediator/src/controllers/service-request.ts b/mediator/src/controllers/service-request.ts index 1437b7d1..243f762a 100644 --- a/mediator/src/controllers/service-request.ts +++ b/mediator/src/controllers/service-request.ts @@ -1,5 +1,5 @@ import { logger } from '../../logger'; -import { createChtRecord } from '../utils/cht'; +import { createChtFollowUpRecord } from '../utils/cht'; import { getFHIRPatientResource, getFHIROrgEndpointResource, @@ -21,7 +21,7 @@ export async function createServiceRequest(request: fhir4.ServiceRequest) { const url = endpointRes.data.address; const subscriptionRes = await createFHIRSubscriptionResource(patientId, url); - const recordRes = await createChtRecord(patientId); + const recordRes = await createChtFollowUpRecord(patientId); if (recordRes.data.success !== true) { await deleteFhirSubscription(subscriptionRes.data.id); diff --git a/mediator/src/controllers/tests/utils.ts b/mediator/src/controllers/tests/utils.ts index 26b923cc..5fe3e797 100644 --- a/mediator/src/controllers/tests/utils.ts +++ b/mediator/src/controllers/tests/utils.ts @@ -1,4 +1,4 @@ -import { createChtRecord } from '../../utils/cht'; +import { createChtFollowUpRecord } from '../../utils/cht'; import { getFHIROrgEndpointResource, getFHIRPatientResource, @@ -21,6 +21,6 @@ export const mockCreateFHIRSubscriptionResource = createFHIRSubscriptionResource as jest.MockedFn< typeof createFHIRSubscriptionResource >; -export const mockCreateChtRecord = createChtRecord as jest.MockedFn< - typeof createChtRecord +export const mockCreateChtRecord = createChtFollowUpRecord as jest.MockedFn< + typeof createChtFollowUpRecord >; diff --git a/mediator/src/utils/cht.ts b/mediator/src/utils/cht.ts index 5cc86da3..32be635b 100644 --- a/mediator/src/utils/cht.ts +++ b/mediator/src/utils/cht.ts @@ -21,7 +21,7 @@ function getOptions(){ return options; } -export async function createChtRecord(patientId: string) { +export async function createChtFollowUpRecord(patientId: string) { const record = { _meta: { form: 'interop_follow_up', diff --git a/mediator/src/utils/tests/cht.spec.ts b/mediator/src/utils/tests/cht.spec.ts index 7402aaf8..7f99b3a5 100644 --- a/mediator/src/utils/tests/cht.spec.ts +++ b/mediator/src/utils/tests/cht.spec.ts @@ -1,4 +1,4 @@ -import { createChtRecord, generateChtRecordsApiUrl } from '../cht'; +import { createChtFollowUpRecord, generateChtRecordsApiUrl } from '../cht'; import axios from 'axios'; jest.mock('axios'); @@ -6,14 +6,14 @@ jest.mock('axios'); const mockAxios = axios as jest.Mocked; describe('CHT Utils', () => { - describe('createChtRecord', () => { + describe('createChtFollowUpRecord', () => { it('creates a new cht record', async () => { const patientId = 'PATIENT_ID'; const data = { status: 201, data: {} }; mockAxios.post.mockResolvedValueOnce(data); - const res = await createChtRecord(patientId); + const res = await createChtFollowUpRecord(patientId); expect(res.status).toBe(data.status); expect(res.data).toStrictEqual(data.data); From c1cc023ddfa27b340c616289a21a56b0087b12e2 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Thu, 3 Oct 2024 12:05:43 +0300 Subject: [PATCH 36/67] fix(#138): env.template changes and small fixes --- configurator/env.template | 4 ++++ docker/docker-compose.cht-core.yml | 3 ++- docker/docker-compose.mediator.yml | 10 +++++----- mediator/config/mediator.ts | 6 +++--- mediator/env.template | 9 +++++++-- startup.sh | 6 +++--- 6 files changed, 24 insertions(+), 14 deletions(-) diff --git a/configurator/env.template b/configurator/env.template index 8c172f48..fb0c5d99 100644 --- a/configurator/env.template +++ b/configurator/env.template @@ -4,3 +4,7 @@ OPENHIM_PASSWORD = 'openhim-password'; OPENHIM_USERNAME = 'root@openhim.org'; OPENHIM_CLIENT_PASSWORD = 'interop-password'; OPENHIM_USER_PASSWORD = 'interop-password'; + +OPENMRS_HOST='openmrs' +OPENMRS_PASSWORD='Admin123' +OPENMRS_USERNAME='admin' diff --git a/docker/docker-compose.cht-core.yml b/docker/docker-compose.cht-core.yml index 521abeba..f590310d 100644 --- a/docker/docker-compose.cht-core.yml +++ b/docker/docker-compose.cht-core.yml @@ -124,7 +124,7 @@ services: couchdb: image: public.ecr.aws/medic/cht-couchdb:4.10.0 volumes: - - ${COUCHDB_DATA:-./srv}:/opt/couchdb/data + - couchdb-data:/opt/couchdb/data - cht-credentials:/opt/couchdb/etc/local.d/ environment: - "COUCHDB_USER=${COUCHDB_USER:-admin}" @@ -150,3 +150,4 @@ networks: volumes: cht-ssl: cht-credentials: + couchdb-data: diff --git a/docker/docker-compose.mediator.yml b/docker/docker-compose.mediator.yml index 1d481dd8..4ca4e6e3 100644 --- a/docker/docker-compose.mediator.yml +++ b/docker/docker-compose.mediator.yml @@ -12,11 +12,11 @@ services: - "OPENHIM_API_URL=${OPENHIM_API_URL:-https://openhim-core:8080}" - "PORT=${PORT:-6000}" - "FHIR_URL=${FHIR_URL:-http://openhim-core:5001/fhir}" - - "FHIR_USERNAME=${FHIR_USERNAME:-interop-client}" - - "FHIR_PASSWORD=${FHIR_PASSWORD:-interop-password}" - - "OPENMRS_URL=${OPENMRS_URL}" - - "OPENMRS_USERNAME=${OPENMRS_USERNAME}" - - "OPENMRS_PASSWORD=${OPENMRS_PASSWORD}" + - "FHIR_USERNAME=${MEDIATORS_USERNAME:-interop-client}" + - "FHIR_PASSWORD=${MEDIATORS_PASSWORD:-interop-password}" + - "OPENMRS_URL=${OPENMRS_CHANNEL_URL}" + - "OPENMRS_USERNAME=${MEDIATORS_USERNAME}" + - "OPENMRS_PASSWORD=${MEDIATORS_PASSWORD}" - "CHT_URL=${CHT_URL:-https://nginx}" - "CHT_USERNAME=${CHT_USERNAME:-admin}" - "CHT_PASSWORD=${CHT_PASSWORD:-password}" diff --git a/mediator/config/mediator.ts b/mediator/config/mediator.ts index 0cfc3793..edea2c24 100644 --- a/mediator/config/mediator.ts +++ b/mediator/config/mediator.ts @@ -1,8 +1,8 @@ export const mediatorConfig = { - urn: 'urn:mediator:ltfu-mediator', + urn: 'urn:mediator:cht-mediator', version: '1.0.0', - name: 'Loss to Follow Up Mediator', - description: 'A loss to follow up mediator for mediator for CHIS.', + name: 'CHT Mediator', + description: 'The default mediator for CHT applications', defaultChannelConfig: [ { name: 'Mediator', diff --git a/mediator/env.template b/mediator/env.template index dd41c65a..213dc1f6 100644 --- a/mediator/env.template +++ b/mediator/env.template @@ -1,10 +1,15 @@ OPENHIM_USERNAME = "interop@openhim.org" OPENHIM_PASSWORD = "password" OPENHIM_API_URL = "https://openhim-core:8080" + PORT = 6000 + FHIR_URL = http://openhim-core:5001/fhir -FHIR_USERNAME = interop-client -FHIR_PASSWORD = interop-password +OPENMRS_URL = http://openhim-core:5001/openmrs + +MEDIATORS_USERNAME = interop-client +MEDIATORS_PASSWORD = interop-password + CHT_URL = http://nginx CHT_USERNAME = admin CHT_PASSWORD = password diff --git a/startup.sh b/startup.sh index f097e84b..337484df 100755 --- a/startup.sh +++ b/startup.sh @@ -2,11 +2,11 @@ if [ "$1" == "init" ]; then # start up docker containers - docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.openmrs.yml up -d --build + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -d --build elif [ "$1" == "up" ]; then - docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.openmrs.yml up -d + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -d elif [ "$1" == "up-dev" ]; then - docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.openmrs.yml up -d --build + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -d --build elif [ "$1" == "down" ]; then docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.openmrs.yml stop elif [ "$1" == "destroy" ]; then From f325eddc739af8b5b43e53431b7560958041f325 Mon Sep 17 00:00:00 2001 From: Lore Date: Fri, 4 Oct 2024 06:25:31 -0400 Subject: [PATCH 37/67] chore (#123): openmrs mediator e2e test (#128) Co-authored-by: Maria Lorena Rodriguez Viruel Co-authored-by: Maria Lorena Rodriguez Viruel Co-authored-by: Tom Wier --- mediator/test/e2e-test.sh | 8 +- mediator/test/ltfu-flow.spec.ts | 180 ---------------------- mediator/test/workflows.spec.ts | 259 ++++++++++++++++++++++++++++++++ 3 files changed, 266 insertions(+), 181 deletions(-) delete mode 100644 mediator/test/ltfu-flow.spec.ts create mode 100644 mediator/test/workflows.spec.ts diff --git a/mediator/test/e2e-test.sh b/mediator/test/e2e-test.sh index 2657ea04..479ed2e3 100755 --- a/mediator/test/e2e-test.sh +++ b/mediator/test/e2e-test.sh @@ -29,7 +29,10 @@ export FHIR_USERNAME='interop-client' export FHIR_PASSWORD='interop-password' export CHT_USERNAME='admin' export CHT_PASSWORD='password' -npm test ltfu-flow.spec.ts +export OPENMRS_URL='http://openhim-core:5001/openmrs' +export OPENMRS_USERNAME='interop-client' +export OPENMRS_PASSWORD='interop-password' +npm run test -t workflows.spec.ts # Cleanup unset NODE_ENV @@ -43,6 +46,9 @@ unset FHIR_USERNAME unset FHIR_PASSWORD unset CHT_USERNAME unset CHT_PASSWORD +unset OPENMRS_URL +unset OPENMRS_USERNAME +unset OPENMRS_PASSWORD cd $BASEDIR ./startup.sh destroy diff --git a/mediator/test/ltfu-flow.spec.ts b/mediator/test/ltfu-flow.spec.ts deleted file mode 100644 index 8e7d8478..00000000 --- a/mediator/test/ltfu-flow.spec.ts +++ /dev/null @@ -1,180 +0,0 @@ -import request from 'supertest'; -import { OPENHIM, CHT, FHIR } from '../config'; -import { - UserFactory, PatientFactory, TaskReportFactory -} from './cht-resource-factories'; -import { - EndpointFactory as EndpointFactoryBase, - OrganizationFactory as OrganizationFactoryBase, - ServiceRequestFactory as ServiceRequestFactoryBase -} from '../src/middlewares/schemas/tests/fhir-resource-factories'; -const { generateAuthHeaders } = require('../../configurator/libs/authentication'); - -jest.setTimeout(10000); - -const EndpointFactory = EndpointFactoryBase.attr('status', 'active') - .attr('address', 'https://interop.free.beeceptor.com/callback') - .attr('payloadType', [{ text: 'application/json' }]); - -const endpointIdentifier = 'test-endpoint'; -const organizationIdentifier = 'test-org'; -const OrganizationFactory = OrganizationFactoryBase.attr('identifier', [{ system: 'official', value: organizationIdentifier }]); - -const ServiceRequestFactory = ServiceRequestFactoryBase.attr('status', 'active'); - -const installMediatorConfiguration = async () => { - const authHeaders = await generateAuthHeaders({ - apiURL: OPENHIM.apiURL, - username: OPENHIM.username, - password: OPENHIM.password, - rejectUnauthorized: false, - }); - try { - const res = await request(OPENHIM.apiURL) - .post('/mediators/urn:mediator:ltfu-mediator/channels') - .send(['Mediator']) - .set('auth-username', authHeaders['auth-username']) - .set('auth-ts', authHeaders['auth-ts']) - .set('auth-salt', authHeaders['auth-salt']) - .set('auth-token', authHeaders['auth-token']); - - if (res.status !== 201) { - throw new Error(`Mediator channel installation failed: Reason ${res.status}`); - } - } catch (error) { - throw new Error(`Mediator channel installation failed ${error}`); - } -}; -let placeId: string; -let chwUserName: string; -let chwPassword: string; -let contactId: string; - -const configureCHT = async () => { - const createPlaceResponse = await request(CHT.url) - .post('/api/v1/places') - .auth(CHT.username, CHT.password) - .send({ 'name': 'CHP Branch Two', 'type': 'district_hospital' }); - - if (createPlaceResponse.status === 200 && createPlaceResponse.body.ok === true) { - placeId = createPlaceResponse.body.id; - } else { - throw new Error(`CHT place creation failed: Reason ${createPlaceResponse.status}`); - } - - const user = UserFactory.build({}, { placeId: placeId }); - - chwUserName = user.username; - chwPassword = user.password; - - const createUserResponse = await request(CHT.url) - .post('/api/v2/users') - .auth(CHT.username, CHT.password) - .send(user); - if (createUserResponse.status === 200) { - contactId = createUserResponse.body.contact.id; - } else { - throw new Error(`CHT user creation failed: Reason ${createUserResponse.status}`); - } -}; - -describe('Steps to follow the Loss To Follow-Up (LTFU) workflow', () => { - let patientId: string; - let encounterUrl: string; - let endpointId: string; - - beforeAll(async () => { - await installMediatorConfiguration(); - await configureCHT(); - }); - - it('Should follow the LTFU workflow', async () => { - const checkMediatorResponse = await request(FHIR.url) - .get('/mediator/') - .auth(FHIR.username, FHIR.password); - - expect(checkMediatorResponse.status).toBe(200); - expect(checkMediatorResponse.body.status).toBe('success'); - - const identifier = [{ system: 'official', value: endpointIdentifier }]; - const endpoint = EndpointFactory.build({ identifier: identifier }); - const createMediatorEndpointResponse = await request(FHIR.url) - .post('/mediator/endpoint') - .auth(FHIR.username, FHIR.password) - .send(endpoint); - - expect(createMediatorEndpointResponse.status).toBe(201); - endpointId = createMediatorEndpointResponse.body.id; - - const retrieveEndpointResponse = await request(FHIR.url) - .get('/fhir/Endpoint/?identifier=' + endpointIdentifier) - .auth(FHIR.username, FHIR.password); - - expect(retrieveEndpointResponse.status).toBe(200); - expect(retrieveEndpointResponse.body.total).toBe(1); - - const organization = OrganizationFactory.build(); - organization.endpoint[0].reference = `Endpoint/${endpointId}`; - - const createMediatorOrganizationResponse = await request(FHIR.url) - .post('/mediator/organization') - .auth(FHIR.username, FHIR.password) - .send(organization); - - expect(createMediatorOrganizationResponse.status).toBe(201); - - const retrieveOrganizationResponse = await request(FHIR.url) - .get('/fhir/Organization/?identifier=' + organizationIdentifier) - .auth(FHIR.username, FHIR.password); - - expect(retrieveOrganizationResponse.status).toBe(200); - expect(retrieveOrganizationResponse.body.total).toBe(1); - - const patient = PatientFactory.build({}, { placeId: placeId }); - - const createPatientResponse = await request(CHT.url) - .post('/api/v1/people') - .auth(chwUserName, chwPassword) - .send(patient); - - expect(createPatientResponse.status).toBe(200); - expect(createPatientResponse.body.ok).toEqual(true); - patientId = createPatientResponse.body.id; - - const retrieveFhirPatientIdResponse = await request(FHIR.url) - .get('/fhir/Patient/?identifier=' + patientId) - .auth(FHIR.username, FHIR.password); - - expect(retrieveFhirPatientIdResponse.status).toBe(200); - - const serviceRequest = ServiceRequestFactory.build(); - serviceRequest.subject.reference = `Patient/${patientId}`; - serviceRequest.requester.reference = `Organization/${organizationIdentifier}`; - - const sendMediatorServiceRequestResponse = await request(FHIR.url) - .post('/mediator/service-request') - .auth(FHIR.username, FHIR.password) - .send(serviceRequest); - expect(sendMediatorServiceRequestResponse.status).toBe(201); - encounterUrl = sendMediatorServiceRequestResponse.body.criteria; - - const taskReport = TaskReportFactory.build({}, { placeId, contactId, patientId }); - - const submitChtTaskResponse = await request(CHT.url) - .post('/medic/_bulk_docs') - .auth(chwUserName, chwPassword) - .send(taskReport); - - expect(submitChtTaskResponse.status).toBe(201); - - await new Promise((r) => setTimeout(r, 2000)); - - const retrieveFhirDbEncounter = await request(FHIR.url) - .get('/fhir/' + encounterUrl) - .auth(FHIR.username, FHIR.password); - - expect(retrieveFhirDbEncounter.status).toBe(200); - expect(retrieveFhirDbEncounter.body.total).toBe(1); - }); -}); - diff --git a/mediator/test/workflows.spec.ts b/mediator/test/workflows.spec.ts new file mode 100644 index 00000000..d3c299de --- /dev/null +++ b/mediator/test/workflows.spec.ts @@ -0,0 +1,259 @@ +import request from 'supertest'; +import { OPENHIM, CHT, FHIR, OPENMRS } from '../config'; +import { + UserFactory, PatientFactory, TaskReportFactory +} from './cht-resource-factories'; +import { + EndpointFactory as EndpointFactoryBase, + OrganizationFactory as OrganizationFactoryBase, + ServiceRequestFactory as ServiceRequestFactoryBase +} from '../src/middlewares/schemas/tests/fhir-resource-factories'; +const { generateAuthHeaders } = require('../../configurator/libs/authentication'); + +jest.setTimeout(50000); + +const EndpointFactory = EndpointFactoryBase.attr('status', 'active') + .attr('address', 'https://interop.free.beeceptor.com/callback') + .attr('payloadType', [{ text: 'application/json' }]); + +const endpointIdentifier = 'test-endpoint'; +const organizationIdentifier = 'test-org'; +const OrganizationFactory = OrganizationFactoryBase.attr('identifier', [{ system: 'official', value: organizationIdentifier }]); + +const ServiceRequestFactory = ServiceRequestFactoryBase.attr('status', 'active'); + +const installMediatorConfiguration = async () => { + const authHeaders = await generateAuthHeaders({ + apiURL: OPENHIM.apiURL, + username: OPENHIM.username, + password: OPENHIM.password, + rejectUnauthorized: false, + }); + try { + const res = await request(OPENHIM.apiURL) + .post('/mediators/urn:mediator:ltfu-mediator/channels') + .send(['Mediator']) + .set('auth-username', authHeaders['auth-username']) + .set('auth-ts', authHeaders['auth-ts']) + .set('auth-salt', authHeaders['auth-salt']) + .set('auth-token', authHeaders['auth-token']); + + if (res.status !== 201) { + throw new Error(`Mediator channel installation failed: Reason ${res.status}`); + } + } catch (error) { + throw new Error(`Mediator channel installation failed ${error}`); + } +}; + +let placeId: string; +let chwUserName: string; +let chwPassword: string; +let contactId: string; +let patientId: string; + +const configureCHT = async () => { + const createPlaceResponse = await request(CHT.url) + .post('/api/v1/places') + .auth(CHT.username, CHT.password) + .send({ 'name': 'CHP Branch Two', 'type': 'district_hospital' }); + + if (createPlaceResponse.status === 200 && createPlaceResponse.body.ok === true) { + placeId = createPlaceResponse.body.id; + } else { + throw new Error(`CHT place creation failed: Reason ${createPlaceResponse.status}`); + } + + const user = UserFactory.build({}, { placeId: placeId }); + + chwUserName = user.username; + chwPassword = user.password; + + const createUserResponse = await request(CHT.url) + .post('/api/v2/users') + .auth(CHT.username, CHT.password) + .send(user); + if (createUserResponse.status === 200) { + contactId = createUserResponse.body.contact.id; + } else { + throw new Error(`CHT user creation failed: Reason ${createUserResponse.status}`); + } +}; + +describe('Workflows', () => { + + beforeAll(async () => { + await installMediatorConfiguration(); + await configureCHT(); + }); + + describe('Loss To Follow-Up (LTFU) workflow', () => { + let encounterUrl: string; + let endpointId: string; + + it('Should follow the LTFU workflow', async () => { + const checkMediatorResponse = await request(FHIR.url) + .get('/mediator/') + .auth(FHIR.username, FHIR.password); + + expect(checkMediatorResponse.status).toBe(200); + expect(checkMediatorResponse.body.status).toBe('success'); + + const identifier = [{ system: 'official', value: endpointIdentifier }]; + const endpoint = EndpointFactory.build({ identifier: identifier }); + const createMediatorEndpointResponse = await request(FHIR.url) + .post('/mediator/endpoint') + .auth(FHIR.username, FHIR.password) + .send(endpoint); + + expect(createMediatorEndpointResponse.status).toBe(201); + endpointId = createMediatorEndpointResponse.body.id; + + const retrieveEndpointResponse = await request(FHIR.url) + .get('/fhir/Endpoint/?identifier=' + endpointIdentifier) + .auth(FHIR.username, FHIR.password); + + expect(retrieveEndpointResponse.status).toBe(200); + expect(retrieveEndpointResponse.body.total).toBe(1); + + const organization = OrganizationFactory.build(); + organization.endpoint[0].reference = `Endpoint/${endpointId}`; + + const createMediatorOrganizationResponse = await request(FHIR.url) + .post('/mediator/organization') + .auth(FHIR.username, FHIR.password) + .send(organization); + + expect(createMediatorOrganizationResponse.status).toBe(201); + + const retrieveOrganizationResponse = await request(FHIR.url) + .get('/fhir/Organization/?identifier=' + organizationIdentifier) + .auth(FHIR.username, FHIR.password); + + expect(retrieveOrganizationResponse.status).toBe(200); + expect(retrieveOrganizationResponse.body.total).toBe(1); + + const patient = PatientFactory.build({}, { name: 'LTFU patient', placeId: placeId }); + + const createPatientResponse = await request(CHT.url) + .post('/api/v1/people') + .auth(chwUserName, chwPassword) + .send(patient); + + expect(createPatientResponse.status).toBe(200); + expect(createPatientResponse.body.ok).toEqual(true); + patientId = createPatientResponse.body.id; + + await new Promise((r) => setTimeout(r, 3000)); + + const retrieveFhirPatientIdResponse = await request(FHIR.url) + .get('/fhir/Patient/?identifier=' + patientId) + .auth(FHIR.username, FHIR.password); + + expect(retrieveFhirPatientIdResponse.status).toBe(200); + expect(retrieveFhirPatientIdResponse.body.total).toBe(1); + + const serviceRequest = ServiceRequestFactory.build(); + serviceRequest.subject.reference = `Patient/${patientId}`; + serviceRequest.requester.reference = `Organization/${organizationIdentifier}`; + + const sendMediatorServiceRequestResponse = await request(FHIR.url) + .post('/mediator/service-request') + .auth(FHIR.username, FHIR.password) + .send(serviceRequest); + expect(sendMediatorServiceRequestResponse.status).toBe(201); + encounterUrl = sendMediatorServiceRequestResponse.body.criteria; + + const taskReport = TaskReportFactory.build({}, { placeId, contactId, patientId }); + + const submitChtTaskResponse = await request(CHT.url) + .post('/medic/_bulk_docs') + .auth(chwUserName, chwPassword) + .send(taskReport); + + expect(submitChtTaskResponse.status).toBe(201); + + await new Promise((r) => setTimeout(r, 2000)); + + const retrieveFhirDbEncounter = await request(FHIR.url) + .get('/fhir/' + encounterUrl) + .auth(FHIR.username, FHIR.password); + + expect(retrieveFhirDbEncounter.status).toBe(200); + expect(retrieveFhirDbEncounter.body.total).toBe(1); + }); + }); + + describe('OpenMRS workflow', () => { + it('Should follow the CHT Patient to OpenMRS workflow', async () => { + const checkMediatorResponse = await request(FHIR.url) + .get('/mediator/') + .auth(FHIR.username, FHIR.password); + + expect(checkMediatorResponse.status).toBe(200); + expect(checkMediatorResponse.body.status).toBe('success'); + + const patient = PatientFactory.build({}, { name: 'OpenMRS patient', placeId: placeId }); + + const createPatientResponse = await request(CHT.url) + .post('/api/v1/people') + .auth(chwUserName, chwPassword) + .send(patient); + + expect(createPatientResponse.status).toBe(200); + expect(createPatientResponse.body.ok).toEqual(true); + patientId = createPatientResponse.body.id; + + await new Promise((r) => setTimeout(r, 3000)); + + const retrieveFhirPatientIdResponse = await request(FHIR.url) + .get('/fhir/Patient/?identifier=' + patientId) + .auth(FHIR.username, FHIR.password); + + expect(retrieveFhirPatientIdResponse.status).toBe(200); + expect(retrieveFhirPatientIdResponse.body.total).toBe(1); + + const triggerOpenMrsSyncPatientResponse = await request(FHIR.url) + .post('/mediator/cht/sync') + .auth(FHIR.username, FHIR.password) + .send(); + + expect(triggerOpenMrsSyncPatientResponse.status).toBe(200); + + await new Promise((r) => setTimeout(r, 30000)); + + const retrieveOpenMrsPatientIdResponse = await request(OPENMRS.url) + .get('/Patient/?identifier=' + patientId) + .auth(OPENMRS.username, OPENMRS.password); + + expect(retrieveOpenMrsPatientIdResponse.status).toBe(200); + //this should work after fixing openmrs to have latest fhir omod and cht identifier defined. + //expect(retrieveOpenMrsPatientIdResponse.body.total).toBe(1); + + //Validate HAPI updated ids + + }); + + it('Should follow the OpenMRS Patient to CHT workflow', async () => { + const checkMediatorResponse = await request(FHIR.url) + .get('/mediator/') + .auth(FHIR.username, FHIR.password); + + expect(checkMediatorResponse.status).toBe(200); + expect(checkMediatorResponse.body.status).toBe('success'); + + //Create a patient using openMRS api + + /*const retrieveFhirPatientIdResponse = await request(FHIR.url) + .get('/fhir/Patient/?identifier=' + patientId) + .auth(FHIR.username, FHIR.password); + + expect(retrieveFhirPatientIdResponse.status).toBe(200); + expect(retrieveFhirPatientIdResponse.body.total).toBe(1);*/ + + //retrieve and validate patient from CHT api + //trigger openmrs sync + //validate id + }); + }); +}); From 74703598f1e2c2053e01e648be341eb190356a16 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Tue, 8 Oct 2024 15:37:09 +0300 Subject: [PATCH 38/67] fix(#123): fixing tests --- cht-config/package.json | 2 +- docker/docker-compose.cht-core.yml | 3 + docker/docker-compose.mediator.yml | 10 +- mediator/config/index.ts | 6 +- mediator/env.template | 8 +- mediator/src/controllers/cht.ts | 13 +-- mediator/src/utils/fhir.ts | 2 +- mediator/src/utils/openmrs.ts | 4 + mediator/test/e2e-test.sh | 23 ++-- mediator/test/workflows.spec.ts | 171 ++++++++++++++++------------- 10 files changed, 136 insertions(+), 106 deletions(-) diff --git a/cht-config/package.json b/cht-config/package.json index d84a3a85..2cebce4e 100644 --- a/cht-config/package.json +++ b/cht-config/package.json @@ -13,7 +13,7 @@ "test-targets": "npm run eslint && TZ=Africa/Nairobi mocha --reporter progress test/targets/*.spec.js --timeout 10000", "test-contact-summary": "npm run eslint && TZ=Africa/Nairobi mocha --reporter progress test/contact-summary/*.spec.js --timeout 10000", "test-unit": "TZ=Africa/Nairobi mocha --recursive --reporter spec test --timeout 20000", - "deploy": "wait-on http://api:5988/ && sleep 100 && sh ./script.sh" + "deploy": "wait-on http://api:5988/ && sh ./script.sh" }, "devDependencies": { "chai": "^4.2.0", diff --git a/docker/docker-compose.cht-core.yml b/docker/docker-compose.cht-core.yml index f590310d..4f585af1 100644 --- a/docker/docker-compose.cht-core.yml +++ b/docker/docker-compose.cht-core.yml @@ -52,6 +52,8 @@ services: - COUCH_URL=http://${COUCHDB_USER:-admin}:${COUCHDB_PASSWORD:-password}@haproxy:${HAPROXY_PORT:-5984}/medic - BUILDS_URL=${MARKET_URL_READ:-https://staging.dev.medicmobile.org}/${BUILDS_SERVER:-_couch/builds_4} - UPGRADE_SERVICE_URL=${UPGRADE_SERVICE_URL:-http://localhost:5100} + ports: + - "5988:5988" logging: driver: "local" options: @@ -117,6 +119,7 @@ services: - "COUCHDB_PASSWORD=${COUCHDB_PASSWORD:-password}" depends_on: - couchdb + - api networks: - cht-net diff --git a/docker/docker-compose.mediator.yml b/docker/docker-compose.mediator.yml index 4ca4e6e3..ee795858 100644 --- a/docker/docker-compose.mediator.yml +++ b/docker/docker-compose.mediator.yml @@ -12,11 +12,11 @@ services: - "OPENHIM_API_URL=${OPENHIM_API_URL:-https://openhim-core:8080}" - "PORT=${PORT:-6000}" - "FHIR_URL=${FHIR_URL:-http://openhim-core:5001/fhir}" - - "FHIR_USERNAME=${MEDIATORS_USERNAME:-interop-client}" - - "FHIR_PASSWORD=${MEDIATORS_PASSWORD:-interop-password}" - - "OPENMRS_URL=${OPENMRS_CHANNEL_URL}" - - "OPENMRS_USERNAME=${MEDIATORS_USERNAME}" - - "OPENMRS_PASSWORD=${MEDIATORS_PASSWORD}" + - "FHIR_USERNAME=${FHIR_USERNAME:-interop-client}" + - "FHIR_PASSWORD=${FHIR_PASSWORD:-interop-password}" + - "OPENMRS_CHANNEL_URL=${OPENMRS_CHANNEL_URL:-http://openhim-core:5001/openmrs}" + - "OPENMRS_CHANNEL_USERNAME=${OPENMRS_CHANNEL_USERNAME:-interop-client}" + - "OPENMRS_CHANNEL_PASSWORD=${OPENMRS_CHANNEL_PASSWORD:-interop-password}" - "CHT_URL=${CHT_URL:-https://nginx}" - "CHT_USERNAME=${CHT_USERNAME:-admin}" - "CHT_PASSWORD=${CHT_PASSWORD:-password}" diff --git a/mediator/config/index.ts b/mediator/config/index.ts index fdeec60a..892e710c 100644 --- a/mediator/config/index.ts +++ b/mediator/config/index.ts @@ -25,9 +25,9 @@ export const CHT = { }; export const OPENMRS = { - url: getEnvironmentVariable('OPENMRS_URL', ''), - username: getEnvironmentVariable('OPENMRS_USERNAME', ''), - password: getEnvironmentVariable('OPENMRS_PASSWORD', ''), + url: getEnvironmentVariable('OPENMRS_CHANNEL_URL', 'http://openhim-core:5001/openmrs'), + username: getEnvironmentVariable('OPENMRS_CHANNEL_USERNAME', 'interop-client'), + password: getEnvironmentVariable('OPENMRS_CHANNEL_PASSWORD', 'interop-password'), timeout: Number(getEnvironmentVariable('REQUEST_TIMEOUT', '5000')) }; diff --git a/mediator/env.template b/mediator/env.template index 213dc1f6..03edd2d6 100644 --- a/mediator/env.template +++ b/mediator/env.template @@ -5,10 +5,12 @@ OPENHIM_API_URL = "https://openhim-core:8080" PORT = 6000 FHIR_URL = http://openhim-core:5001/fhir -OPENMRS_URL = http://openhim-core:5001/openmrs +FHIR_USERNAME = interop-client +FHIR_PASSWORD = interop-password -MEDIATORS_USERNAME = interop-client -MEDIATORS_PASSWORD = interop-password +OPENMRS_CHANNEL_URL = http://openhim-core:5001/openmrs +OPENMRS_CHANNEL_USERNAME = interop-client +OPENMRS_CHANNEL_PASSWORD = interop-password CHT_URL = http://nginx CHT_USERNAME = admin diff --git a/mediator/src/controllers/cht.ts b/mediator/src/controllers/cht.ts index 8c852b80..960ab638 100644 --- a/mediator/src/controllers/cht.ts +++ b/mediator/src/controllers/cht.ts @@ -22,18 +22,7 @@ export async function createPatient(chtPatientDoc: any) { } const fhirPatient = buildFhirPatientFromCht(chtPatientDoc.doc); - const patientResponse = await getFHIRPatientResource(fhirPatient.id || ''); - if (patientResponse.status != 200){ - // any error, just return it to caller - return patientResponse; - } else if (patientResponse.data.total > 0) { - // updates not currently supported - return patientResponse; - } else { - // create or update in the FHIR Server - // even for create, sends a PUT request - return updateFhirResource({ ...fhirPatient, resourceType: 'Patient' }); - } + return updateFhirResource({ ...fhirPatient, resourceType: 'Patient' }); } export async function updatePatientIds(chtFormDoc: any) { diff --git a/mediator/src/utils/fhir.ts b/mediator/src/utils/fhir.ts index 31e637a8..8a7547f3 100644 --- a/mediator/src/utils/fhir.ts +++ b/mediator/src/utils/fhir.ts @@ -134,7 +134,7 @@ export function copyIdToNamedIdentifier(fromResource: any, toResource: fhir4.Pat } export function getIdType(resource: fhir4.Patient | fhir4.Encounter, idType: fhir4.CodeableConcept): string{ - return resource?.identifier?.find((id: any) => id?.type.text == idType.text)?.value || ''; + return resource?.identifier?.find((id: any) => id?.type?.text == idType.text)?.value || ''; } export function addId(resource: fhir4.Patient | fhir4.Encounter, idType: fhir4.CodeableConcept, value: string){ diff --git a/mediator/src/utils/openmrs.ts b/mediator/src/utils/openmrs.ts index c49887bf..6b404901 100644 --- a/mediator/src/utils/openmrs.ts +++ b/mediator/src/utils/openmrs.ts @@ -1,12 +1,16 @@ import { OPENMRS } from '../../config'; import axios from 'axios'; import { logger } from '../../logger'; +import https from 'https'; const axiosOptions = { auth: { username: OPENMRS.username, password: OPENMRS.password, }, + httpsAgent: new https.Agent({ + rejectUnauthorized: false, + }), timeout: OPENMRS.timeout }; diff --git a/mediator/test/e2e-test.sh b/mediator/test/e2e-test.sh index 479ed2e3..1809dad4 100755 --- a/mediator/test/e2e-test.sh +++ b/mediator/test/e2e-test.sh @@ -6,6 +6,10 @@ MEDIATORDIR="${BASEDIR}/mediator" export NODE_ENV=integration export NODE_TLS_REJECT_UNAUTHORIZED=0 +export OPENMRS_HOST=openmrs +export OPENMRS_USERNAME=admin +export OPENMRS_PASSWORD=Admin123 + # Cleanup from last test, in case of interruptions cd $BASEDIR ./startup.sh destroy @@ -29,14 +33,20 @@ export FHIR_USERNAME='interop-client' export FHIR_PASSWORD='interop-password' export CHT_USERNAME='admin' export CHT_PASSWORD='password' -export OPENMRS_URL='http://openhim-core:5001/openmrs' -export OPENMRS_USERNAME='interop-client' -export OPENMRS_PASSWORD='interop-password' +export OPENMRS_CHANNEL_URL='http://localhost:5001/openmrs' +export OPENMRS_CHANNEL_USERNAME='interop-client' +export OPENMRS_CHANNEL_PASSWORD='interop-password' + +echo 'Waiting for OpenMRS to be ready' +sleep 180 npm run test -t workflows.spec.ts # Cleanup unset NODE_ENV unset NODE_TLS_REJECT_UNAUTHORIZED +unset OPENMRS_HOST +unset OPENMRS_USERNAME +unset OPENMRS_PASSWORD unset OPENHIM_API_URL unset FHIR_URL unset CHT_URL @@ -46,9 +56,8 @@ unset FHIR_USERNAME unset FHIR_PASSWORD unset CHT_USERNAME unset CHT_PASSWORD -unset OPENMRS_URL -unset OPENMRS_USERNAME -unset OPENMRS_PASSWORD +unset OPENMRS_CHANNEL_URL +unset OPENMRS_CHANNEL_USERNAME +unset OPENMRS_CHANNEL_PASSWORD cd $BASEDIR ./startup.sh destroy - diff --git a/mediator/test/workflows.spec.ts b/mediator/test/workflows.spec.ts index d3c299de..c060ab33 100644 --- a/mediator/test/workflows.spec.ts +++ b/mediator/test/workflows.spec.ts @@ -31,7 +31,7 @@ const installMediatorConfiguration = async () => { }); try { const res = await request(OPENHIM.apiURL) - .post('/mediators/urn:mediator:ltfu-mediator/channels') + .post('/mediators/urn:mediator:cht-mediator/channels') .send(['Mediator']) .set('auth-username', authHeaders['auth-username']) .set('auth-ts', authHeaders['auth-ts']) @@ -46,6 +46,27 @@ const installMediatorConfiguration = async () => { } }; +const createOpenMRSIdType = async (name: string) => { + const patientIdType = { + name: name, + description: "CHT Patient ID", + required: false, + locationBehavior: "NOT_USED", + uniquenessBehavior: "Unique" + } + try { + const res = await request("http://localhost:8090") + .post('/openmrs/ws/rest/v1/patientidentifiertype') + .auth('admin', 'Admin123') + .send(patientIdType) + if (res.status !== 201) { + throw new Error(`Mediator channel installation failed: Reason ${res.status}`); + } + } catch (error) { + throw new Error(`Mediator channel installation failed ${error}`); + } +}; + let placeId: string; let chwUserName: string; let chwPassword: string; @@ -85,6 +106,81 @@ describe('Workflows', () => { beforeAll(async () => { await installMediatorConfiguration(); await configureCHT(); + await createOpenMRSIdType('CHT Patient ID'); + await createOpenMRSIdType('CHT Document ID'); + }); + + describe('OpenMRS workflow', () => { + it('Should follow the CHT Patient to OpenMRS workflow', async () => { + const checkMediatorResponse = await request(FHIR.url) + .get('/mediator/') + .auth(FHIR.username, FHIR.password); + + expect(checkMediatorResponse.status).toBe(200); + expect(checkMediatorResponse.body.status).toBe('success'); + + const patient = PatientFactory.build({}, { name: 'OpenMRS patient', placeId: placeId }); + + const createPatientResponse = await request(CHT.url) + .post('/api/v1/people') + .auth(chwUserName, chwPassword) + .send(patient); + + expect(createPatientResponse.status).toBe(200); + expect(createPatientResponse.body.ok).toEqual(true); + patientId = createPatientResponse.body.id; + + await new Promise((r) => setTimeout(r, 3000)); + + const retrieveFhirPatientIdResponse = await request(FHIR.url) + .get('/fhir/Patient/?identifier=' + patientId) + .auth(FHIR.username, FHIR.password); + + expect(retrieveFhirPatientIdResponse.status).toBe(200); + expect(retrieveFhirPatientIdResponse.body.total).toBe(1); + + const triggerOpenMrsSyncPatientResponse = await request(FHIR.url) + .get('/mediator/openmrs/sync') + .auth(FHIR.username, FHIR.password) + .send(); + + expect(triggerOpenMrsSyncPatientResponse.status).toBe(200); + + await new Promise((r) => setTimeout(r, 3000)); + + const retrieveOpenMrsPatientIdResponse = await request(OPENMRS.url) + .get('/Patient/?identifier=' + patientId) + .auth(OPENMRS.username, OPENMRS.password); + + expect(retrieveOpenMrsPatientIdResponse.status).toBe(200); + //this should work after fixing openmrs to have latest fhir omod and cht identifier defined. + //expect(retrieveOpenMrsPatientIdResponse.body.total).toBe(1); + + //Validate HAPI updated ids + + }); + + it('Should follow the OpenMRS Patient to CHT workflow', async () => { + const checkMediatorResponse = await request(FHIR.url) + .get('/mediator/') + .auth(FHIR.username, FHIR.password); + + expect(checkMediatorResponse.status).toBe(200); + expect(checkMediatorResponse.body.status).toBe('success'); + + //Create a patient using openMRS api + + /*const retrieveFhirPatientIdResponse = await request(FHIR.url) + .get('/fhir/Patient/?identifier=' + patientId) + .auth(FHIR.username, FHIR.password); + + expect(retrieveFhirPatientIdResponse.status).toBe(200); + expect(retrieveFhirPatientIdResponse.body.total).toBe(1);*/ + + //retrieve and validate patient from CHT api + //trigger openmrs sync + //validate id + }); }); describe('Loss To Follow-Up (LTFU) workflow', () => { @@ -183,77 +279,4 @@ describe('Workflows', () => { expect(retrieveFhirDbEncounter.body.total).toBe(1); }); }); - - describe('OpenMRS workflow', () => { - it('Should follow the CHT Patient to OpenMRS workflow', async () => { - const checkMediatorResponse = await request(FHIR.url) - .get('/mediator/') - .auth(FHIR.username, FHIR.password); - - expect(checkMediatorResponse.status).toBe(200); - expect(checkMediatorResponse.body.status).toBe('success'); - - const patient = PatientFactory.build({}, { name: 'OpenMRS patient', placeId: placeId }); - - const createPatientResponse = await request(CHT.url) - .post('/api/v1/people') - .auth(chwUserName, chwPassword) - .send(patient); - - expect(createPatientResponse.status).toBe(200); - expect(createPatientResponse.body.ok).toEqual(true); - patientId = createPatientResponse.body.id; - - await new Promise((r) => setTimeout(r, 3000)); - - const retrieveFhirPatientIdResponse = await request(FHIR.url) - .get('/fhir/Patient/?identifier=' + patientId) - .auth(FHIR.username, FHIR.password); - - expect(retrieveFhirPatientIdResponse.status).toBe(200); - expect(retrieveFhirPatientIdResponse.body.total).toBe(1); - - const triggerOpenMrsSyncPatientResponse = await request(FHIR.url) - .post('/mediator/cht/sync') - .auth(FHIR.username, FHIR.password) - .send(); - - expect(triggerOpenMrsSyncPatientResponse.status).toBe(200); - - await new Promise((r) => setTimeout(r, 30000)); - - const retrieveOpenMrsPatientIdResponse = await request(OPENMRS.url) - .get('/Patient/?identifier=' + patientId) - .auth(OPENMRS.username, OPENMRS.password); - - expect(retrieveOpenMrsPatientIdResponse.status).toBe(200); - //this should work after fixing openmrs to have latest fhir omod and cht identifier defined. - //expect(retrieveOpenMrsPatientIdResponse.body.total).toBe(1); - - //Validate HAPI updated ids - - }); - - it('Should follow the OpenMRS Patient to CHT workflow', async () => { - const checkMediatorResponse = await request(FHIR.url) - .get('/mediator/') - .auth(FHIR.username, FHIR.password); - - expect(checkMediatorResponse.status).toBe(200); - expect(checkMediatorResponse.body.status).toBe('success'); - - //Create a patient using openMRS api - - /*const retrieveFhirPatientIdResponse = await request(FHIR.url) - .get('/fhir/Patient/?identifier=' + patientId) - .auth(FHIR.username, FHIR.password); - - expect(retrieveFhirPatientIdResponse.status).toBe(200); - expect(retrieveFhirPatientIdResponse.body.total).toBe(1);*/ - - //retrieve and validate patient from CHT api - //trigger openmrs sync - //validate id - }); - }); }); From a0fc96b40b128dea90b11d8efab23a4fb50f5376 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Tue, 8 Oct 2024 15:49:36 +0300 Subject: [PATCH 39/67] fix(#123): fixing tests --- mediator/src/routes/tests/cht.spec.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mediator/src/routes/tests/cht.spec.ts b/mediator/src/routes/tests/cht.spec.ts index 22ac0fa3..c45fd724 100644 --- a/mediator/src/routes/tests/cht.spec.ts +++ b/mediator/src/routes/tests/cht.spec.ts @@ -9,10 +9,6 @@ jest.mock('axios'); describe('POST /cht/patient', () => { it('accepts incoming request with valid patient resource', async () => { - jest.spyOn(fhir, 'getFHIRPatientResource').mockResolvedValueOnce({ - data: {}, - status: 200, - }); jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ data: {}, status: 200, From 86bdc31d7a8453aed3eb5ca9b22d9a2dc538478f Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Fri, 18 Oct 2024 15:08:02 +0300 Subject: [PATCH 40/67] chore(#142): adding tests to increase coverage --- mediator/src/controllers/cht.ts | 6 +- mediator/src/controllers/tests/cht.spec.ts | 178 +++++++++ mediator/src/controllers/tests/utils.ts | 4 + mediator/src/mappers/openmrs.ts | 4 +- mediator/src/middlewares/schemas/encounter.ts | 4 + .../schemas/tests/cht-request-factories.ts | 18 + .../schemas/tests/fhir-resource-factories.ts | 24 +- .../tests/openmrs-resource-factories.ts | 32 ++ mediator/src/routes/cht.ts | 2 +- mediator/src/routes/tests/cht.spec.ts | 58 --- mediator/src/utils/cht.ts | 12 +- mediator/src/utils/fhir.ts | 34 +- mediator/src/utils/openmrs.ts | 40 +- mediator/src/utils/openmrs_sync.ts | 13 +- mediator/src/utils/tests/cht.spec.ts | 92 ++++- mediator/src/utils/tests/fhir.spec.ts | 134 ++++++- mediator/src/utils/tests/openmrs.spec.ts | 82 +++++ mediator/src/utils/tests/openmrs_sync.spec.ts | 342 ++++++++++++------ 18 files changed, 820 insertions(+), 259 deletions(-) create mode 100644 mediator/src/controllers/tests/cht.spec.ts create mode 100644 mediator/src/middlewares/schemas/tests/openmrs-resource-factories.ts delete mode 100644 mediator/src/routes/tests/cht.spec.ts create mode 100644 mediator/src/utils/tests/openmrs.spec.ts diff --git a/mediator/src/controllers/cht.ts b/mediator/src/controllers/cht.ts index 960ab638..2cb4d4db 100644 --- a/mediator/src/controllers/cht.ts +++ b/mediator/src/controllers/cht.ts @@ -27,7 +27,7 @@ export async function createPatient(chtPatientDoc: any) { export async function updatePatientIds(chtFormDoc: any) { // first, get the existing patient from fhir server - const response = await getFHIRPatientResource(chtFormDoc.external_id); + const response = await getFHIRPatientResource(chtFormDoc.doc.external_id); if (response.status != 200) { return { status: 500, data: { message: `FHIR responded with ${response.status}`} }; @@ -37,10 +37,10 @@ export async function updatePatientIds(chtFormDoc: any) { } const fhirPatient = response.data.entry[0].resource; - addId(fhirPatient, chtPatientIdentifierType, chtFormDoc.patient_id); + addId(fhirPatient, chtPatientIdentifierType, chtFormDoc.doc.patient_id); // now, we need to get the actual patient doc from cht... - const patient_uuid = await getPatientUUIDFromSourceId(chtFormDoc._id); + const patient_uuid = await getPatientUUIDFromSourceId(chtFormDoc.doc._id); if (patient_uuid){ addId(fhirPatient, chtDocumentIdentifierType, patient_uuid); return updateFhirResource({ ...fhirPatient, resourceType: 'Patient' }); diff --git a/mediator/src/controllers/tests/cht.spec.ts b/mediator/src/controllers/tests/cht.spec.ts new file mode 100644 index 00000000..446fcdd6 --- /dev/null +++ b/mediator/src/controllers/tests/cht.spec.ts @@ -0,0 +1,178 @@ +import { + createPatient, + updatePatientIds, + createEncounter +} from '../cht' +import { + ChtPatientFactory, + ChtSMSPatientFactory, + ChtPatientIdsFactory, + ChtPregnancyForm +} from '../../middlewares/schemas/tests/cht-request-factories'; +import { + PatientFactory, + EncounterFactory, + ObservationFactory +} from '../../middlewares/schemas/tests/fhir-resource-factories'; +import { + chtDocumentIdentifierType, + chtPatientIdentifierType +} from '../../mappers/cht'; + +import * as fhir from '../../utils/fhir'; +import * as cht from '../../utils/cht'; + +import axios from 'axios'; +import { randomUUID } from 'crypto'; + +jest.mock('axios'); + +describe('CHT outgoing document controllers', () => { + describe('createPatient', () => { + it('creates a FHIR Patient from CHT patient doc', async () => { + jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ + data: {}, + status: 200, + }); + + const data = ChtPatientFactory.build(); + + const res = await createPatient(data); + + expect(res.status).toBe(200); + + // assert that the create resource has the right identifier and type + expect(fhir.updateFhirResource).toHaveBeenCalledWith( + expect.objectContaining({ + resourceType: 'Patient', + identifier: expect.arrayContaining([ + expect.objectContaining({ + type: chtDocumentIdentifierType, + value: data.doc._id + }) + ]), + }) + ); + }); + + it('creates a FHIR Patient from an SMS form using source id', async () => { + jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ + data: {}, + status: 200, + }); + + let sourceId = randomUUID(); + jest.spyOn(cht, 'getPatientUUIDFromSourceId').mockResolvedValueOnce(sourceId); + + const data = ChtSMSPatientFactory.build(); + + const res = await createPatient(data); + + expect(res.status).toBe(200); + + // assert that the createid resource has the right identifier and type + expect(fhir.updateFhirResource).toHaveBeenCalledWith( + expect.objectContaining({ + resourceType: 'Patient', + identifier: expect.arrayContaining([ + expect.objectContaining({ + type: chtDocumentIdentifierType, + value: sourceId + }) + ]), + }) + ); + }); + }); + + describe('updatePatientIds', () => { + it('updates patient ids', async () => { + const existingPatient = PatientFactory.build(); + jest.spyOn(fhir, 'getFHIRPatientResource').mockResolvedValue({ + data: { total: 1, entry: [ { resource: existingPatient } ] }, + status: 200, + }); + jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ + data: {}, + status: 200, + }); + + let sourceId = randomUUID(); + jest.spyOn(cht, 'getPatientUUIDFromSourceId').mockResolvedValueOnce(sourceId); + + const data = ChtPatientIdsFactory.build(); + + const res = await updatePatientIds(data); + + expect(res.status).toBe(200); + + // assert that the created resource has the right identifier and type + expect(fhir.updateFhirResource).toHaveBeenCalledWith( + expect.objectContaining({ + id: existingPatient.id, + identifier: expect.arrayContaining([ + expect.objectContaining({ + type: chtDocumentIdentifierType, + value: sourceId + }) + ]), + }) + ); + }); + }); + + describe('createEncounter', () => { + it('creates FHIR Encounter from CHT form', async () => { + jest.spyOn(fhir, 'getFHIRPatientResource').mockResolvedValueOnce({ + data: { total: 1, entry: [ { resource: PatientFactory.build() } ] }, + status: 200, + }); + // observations use createFhirResource + jest.spyOn(fhir, 'createFhirResource').mockResolvedValueOnce({ + data: {}, + status: 200, + }); + // encounter uses updatedFhirResource + jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ + data: {}, + status: 200, + }); + + const data = ChtPregnancyForm.build(); + + const res = await createEncounter(data); + + expect(res.status).toBe(200); + + // assert that the encounter was created + expect(fhir.updateFhirResource).toHaveBeenCalledWith( + expect.objectContaining({ + resourceType: 'Encounter', + identifier: expect.arrayContaining([ + expect.objectContaining({ + type: chtDocumentIdentifierType, + value: data.id + }) + ]), + }) + ); + + // assert that at least one observation was created with the right codes + expect(fhir.createFhirResource).toHaveBeenCalledWith( + expect.objectContaining({ + resourceType: 'Observation', + code: { + coding: expect.arrayContaining([{ + code: data.observations[0].code + }]) + }, + valueCodeableConcept: { + coding: expect.arrayContaining([{ + code: data.observations[0].valueCode + }]) + } + }) + ); + }); + }); +}); diff --git a/mediator/src/controllers/tests/utils.ts b/mediator/src/controllers/tests/utils.ts index 5fe3e797..b582c66f 100644 --- a/mediator/src/controllers/tests/utils.ts +++ b/mediator/src/controllers/tests/utils.ts @@ -5,6 +5,7 @@ import { deleteFhirSubscription, createFHIRSubscriptionResource, } from '../../utils/fhir'; +import { queryCht } from '../../utils/cht'; jest.mock('../../utils/fhir'); jest.mock('../../utils/cht'); @@ -24,3 +25,6 @@ export const mockCreateFHIRSubscriptionResource = export const mockCreateChtRecord = createChtFollowUpRecord as jest.MockedFn< typeof createChtFollowUpRecord >; +export const mockQueryCht = queryCht as jest.MockedFn< + typeof queryCht +>; diff --git a/mediator/src/mappers/openmrs.ts b/mediator/src/mappers/openmrs.ts index 99f9a5fe..c1cf0424 100644 --- a/mediator/src/mappers/openmrs.ts +++ b/mediator/src/mappers/openmrs.ts @@ -14,7 +14,7 @@ export const openMRSIdentifierType: fhir4.CodeableConcept = { export const openMRSSource = 'openmrs'; -const visitNoteType: fhir4.CodeableConcept = { +export const visitNoteType: fhir4.CodeableConcept = { text: "Visit Note", coding: [{ system: "http://fhir.openmrs.org/code-system/encounter-type", @@ -23,7 +23,7 @@ const visitNoteType: fhir4.CodeableConcept = { }] } -const visitType: fhir4.CodeableConcept = { +export const visitType: fhir4.CodeableConcept = { text: "Home Visit", coding: [{ system: "http://fhir.openmrs.org/code-system/visit-type", diff --git a/mediator/src/middlewares/schemas/encounter.ts b/mediator/src/middlewares/schemas/encounter.ts index b6d3d50c..0665720a 100644 --- a/mediator/src/middlewares/schemas/encounter.ts +++ b/mediator/src/middlewares/schemas/encounter.ts @@ -21,4 +21,8 @@ export const EncounterSchema = joi.object({ type: joi.array().length(1).required(), subject: joi.required(), participant: joi.array().length(1).required(), + period: joi.object({ + start: joi.string(), + end: joi.string() + }) }); diff --git a/mediator/src/middlewares/schemas/tests/cht-request-factories.ts b/mediator/src/middlewares/schemas/tests/cht-request-factories.ts index c45dd7f1..337c1504 100644 --- a/mediator/src/middlewares/schemas/tests/cht-request-factories.ts +++ b/mediator/src/middlewares/schemas/tests/cht-request-factories.ts @@ -12,6 +12,24 @@ export const ChtPatientDoc = Factory.define('chtPatientDoc') .attr('sex', 'female') .attr('patient_id', randomUUID()); +export const ChtSMSPatientFactory = Factory.define('chtPatient') + .attr('doc', () => ChtSMSPatientDoc.build()) + +export const ChtSMSPatientDoc = Factory.define('chtPatientDoc') + .attr('_id', randomUUID()) + .attr('name', 'John Doe') + .attr('phone', '+9770000000') + .attr('date_of_birth', '2000-01-01') + .attr('sex', 'female') + .attr('source_id', randomUUID()); + +export const ChtPatientIdsFactory = Factory.define('chtPatientIds') + .attr('doc', () => ChtPatientIdsDoc.build()) + +export const ChtPatientIdsDoc = Factory.define('chtPatientIds') + .attr('external_id', randomUUID()) + .attr('patient_uuid', randomUUID()); + export const ChtPregnancyForm = Factory.define('chtPregnancyDoc') .attr('patient_uuid', randomUUID()) .attr('reported_date', Date.now()) diff --git a/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts b/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts index a489694d..34a6befa 100644 --- a/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts +++ b/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts @@ -1,12 +1,11 @@ import { randomUUID } from 'crypto'; import { Factory } from 'rosie'; import { VALID_CODE, VALID_SYSTEM } from '../endpoint'; +import { chtDocumentIdentifierType } from '../../../mappers/cht'; const identifier = [ { - type: { - text: 'CHT Document Identifier' - }, + type: chtDocumentIdentifierType, system: 'cht', value: randomUUID(), }, @@ -28,11 +27,15 @@ export const EncounterFactory = Factory.define('encounter') .attr('resourceType', 'Encounter') .attr('id', randomUUID()) .attr('identifier', identifier) - .attr('status', 'planned') + .attr('status', 'finished') .attr('class', 'outpatient') .attr('type', [{ text: 'Community health worker visit' }]) .attr('subject', { reference: 'Patient/3' }) - .attr('participant', [{ type: [{ text: 'Community health worker' }] }]); + .attr('participant', [{ type: [{ text: 'Community health worker' }] }]) + .attr('period', { + start: new Date(new Date().getTime() - 60 * 60 * 1000).toISOString(), + end: new Date(new Date().getTime() - 50 * 60 * 1000).toISOString() + }) export const EndpointFactory = Factory.define('endpoint') .attr('connectionType', { system: VALID_SYSTEM, code: VALID_CODE }) @@ -56,3 +59,14 @@ export const ServiceRequestFactory = Factory.define('serviceRequest') .attr('intent', 'order') .attr('subject', SubjectFactory.build()) .attr('requester', RequesterFactory.build()); + +export const ObservationFactory = Factory.define('Observation') + .attr('resourceType', 'Observation') + .attr('id', () => randomUUID()) + .attr('encounter', () => { reference: 'Encounter/' + randomUUID() }) + .attr('code', { + coding: [{ code: 'DANGER_SIGNS' }], + }) + .attr('valueCodeableConcept', { + coding: [{ code: 'HIGH_BLOOD_PRESSURE' }] + }); diff --git a/mediator/src/middlewares/schemas/tests/openmrs-resource-factories.ts b/mediator/src/middlewares/schemas/tests/openmrs-resource-factories.ts new file mode 100644 index 00000000..48907540 --- /dev/null +++ b/mediator/src/middlewares/schemas/tests/openmrs-resource-factories.ts @@ -0,0 +1,32 @@ +import { randomUUID } from 'crypto'; +import { Factory } from 'rosie'; +import { visitNoteType, visitType } from '../../../mappers/openmrs'; + +// creates an openmrs patient with the special address extension +export const OpenMRSPatientFactory = Factory.define('openMRSFhirPatient') + .attr('resourceType', 'Patient') + .attr('id', () => randomUUID()) // Assign a random UUID for the patient + .attr('address', ['addressKey', 'addressValue'], (addressKey, addressValue) => [ + { + extension: [{ + extension: [ + { + url: `http://fhir.openmrs.org/ext/address#${addressKey}`, + valueString: addressValue + } + ] + }] + } + ]); + +// creates an openmrs encounter with visit type +export const OpenMRSVisitFactory = Factory.define('openMRSVisit') + .attr('resourceType', 'Encounter') + .attr('id', () => randomUUID()) // Assign a random UUID for the patient + .attr('type', visitType); + +// creates an openmrs encounter with visit note type +export const OpenMRSVisitNoteFactory = Factory.define('openMRSVisit') + .attr('resourceType', 'Encounter') + .attr('id', () => randomUUID()) // Assign a random UUID for the patient + .attr('type', visitNoteType); diff --git a/mediator/src/routes/cht.ts b/mediator/src/routes/cht.ts index 4444d10b..1bebab29 100644 --- a/mediator/src/routes/cht.ts +++ b/mediator/src/routes/cht.ts @@ -13,7 +13,7 @@ router.post( router.post( '/patient_ids', - requestHandler((req) => updatePatientIds(req.body.doc)) + requestHandler((req) => updatePatientIds(req.body)) ); router.post( diff --git a/mediator/src/routes/tests/cht.spec.ts b/mediator/src/routes/tests/cht.spec.ts deleted file mode 100644 index c45fd724..00000000 --- a/mediator/src/routes/tests/cht.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import request from 'supertest'; -import app from '../../..'; -import { ChtPatientFactory, ChtPregnancyForm } from '../../middlewares/schemas/tests/cht-request-factories'; -import { PatientFactory, EncounterFactory } from '../../middlewares/schemas/tests/fhir-resource-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 () => { - jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ - data: {}, - status: 200, - }); - - const data = ChtPatientFactory.build(); - - const res = await request(app).post('/cht/patient').send(data); - - expect(res.status).toBe(200); - expect(res.body).toEqual({}); - - /* - expect(fhir.createFhirResource).toHaveBeenCalledWith({ - ...data, - resourceType: 'Patient', - }); - */ - expect(fhir.updateFhirResource).toHaveBeenCalled(); - }); - - it('accepts incoming request with valid form', async () => { - jest.spyOn(fhir, 'getFHIRPatientResource').mockResolvedValueOnce({ - data: { total: 1, entry: [ { resource: PatientFactory.build() } ] }, - status: 200, - }); - jest.spyOn(fhir, 'createFhirResource').mockResolvedValueOnce({ - data: {}, - status: 200, - }); - - const data = ChtPregnancyForm.build(); - - const res = await request(app).post('/cht/encounter').send(data); - - expect(res.status).toBe(200); - expect(res.body).toEqual({}); - /* - expect(fhir.createFhirResource).toHaveBeenCalledWith({ - ...data, - resourceType: 'Patient', - }); - */ - expect(fhir.updateFhirResource).toHaveBeenCalled(); - }); - -}); diff --git a/mediator/src/utils/cht.ts b/mediator/src/utils/cht.ts index 32be635b..e59545cf 100644 --- a/mediator/src/utils/cht.ts +++ b/mediator/src/utils/cht.ts @@ -36,11 +36,11 @@ export async function createChtFollowUpRecord(patientId: string) { return { status: res?.status, data: res?.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } -async function getLocation(fhirPatient: fhir4.Patient) { +export async function getLocationFromOpenMRSPatient(fhirPatient: fhir4.Patient) { // first, extract address value; is fchv area available? const addresses = fhirPatient.address?.[0]?.extension?.[0]?.extension; let addressKey = "http://fhir.openmrs.org/ext/address#address4" @@ -107,7 +107,7 @@ export async function createChtPatient(fhirPatient: fhir4.Patient) { cht_patient._meta = { form: "openmrs_patient" } - const location_id = await getLocation(fhirPatient); + const location_id = await getLocationFromOpenMRSPatient(fhirPatient); cht_patient.location_id = location_id; return chtRecordsApi(cht_patient); @@ -125,7 +125,7 @@ export async function chtRecordsApi(doc: any) { return { status: res?.status, data: res?.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } @@ -136,7 +136,7 @@ export async function getChtDocumentById(doc_id: string) { return { status: res?.status, data: res?.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } @@ -147,7 +147,7 @@ export async function queryCht(query: any) { return { status: res?.status, data: res?.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } diff --git a/mediator/src/utils/fhir.ts b/mediator/src/utils/fhir.ts index 8a7547f3..33ce7a88 100644 --- a/mediator/src/utils/fhir.ts +++ b/mediator/src/utils/fhir.ts @@ -110,7 +110,7 @@ export async function getFHIRPatientResource(patientId: string) { return { status: res?.status, data: res?.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } @@ -161,27 +161,30 @@ export async function deleteFhirSubscription(id?: string) { export async function createFhirResource(doc: fhir4.Resource) { try { const res = await axios.post(`${FHIR.url}/${doc.resourceType}`, doc, 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 }; + return { status: error.response?.status, data: error.response?.data }; } } export async function updateFhirResource(doc: fhir4.Resource) { try { const res = await axios.put(`${FHIR.url}/${doc.resourceType}/${doc.id}`, doc, axiosOptions); - return { status: res?.status, data: res?.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } export async function getFhirResourcesSince(lastUpdated: Date, resourceType: string) { + return getResourcesSince(FHIR.url, lastUpdated, resourceType); +} + +export async function getResourcesSince(url: string, lastUpdated: Date, resourceType: string) { try { - let nextUrl = `${FHIR.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`; + let nextUrl = `${url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`; let results: fhir4.Resource[] = []; // for encounters, include related resources if (resourceType === 'Encounter') { @@ -199,13 +202,13 @@ export async function getFhirResourcesSince(lastUpdated: Date, resourceType: str nextUrl = nextLink ? nextLink.url : null; if (nextUrl) { const qs = nextUrl.split('?')[1]; - nextUrl = `${FHIR.url}/?${qs}`; + nextUrl = `${url}/?${qs}`; } } return { status: 200, data: results }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } @@ -218,19 +221,6 @@ export async function getFhirResource(id: string, resourceType: string) { return { status: res?.status, data: res?.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; - } -} - -export async function getQuestionnaire(name: string){ - try { - const res = await axios.get( - `${FHIR.url}/Questionnaire`, - axiosOptions - ); - return { status: res?.status, data: res?.data }; - } catch (error: any) { - logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } diff --git a/mediator/src/utils/openmrs.ts b/mediator/src/utils/openmrs.ts index 6b404901..5bc87bc3 100644 --- a/mediator/src/utils/openmrs.ts +++ b/mediator/src/utils/openmrs.ts @@ -2,6 +2,7 @@ import { OPENMRS } from '../../config'; import axios from 'axios'; import { logger } from '../../logger'; import https from 'https'; +import { getResourcesSince } from './fhir'; const axiosOptions = { auth: { @@ -14,41 +15,8 @@ const axiosOptions = { timeout: OPENMRS.timeout }; -export async function getOpenMRSPatientResource(patientId: string) { - return await axios.get( - `${OPENMRS.url}/Patient/?identifier=${patientId}`, - axiosOptions - ); -} - export async function getOpenMRSResourcesSince(lastUpdated: Date, resourceType: string) { - try { - let nextUrl = `${OPENMRS.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`; - let results: fhir4.Resource[] = []; - // for encounters, include related resources - if (resourceType === 'Encounter') { - nextUrl = nextUrl + '&_revinclude=Observation:encounter&_include=Encounter:patient'; - } - - while (nextUrl) { - const res = await axios.get(nextUrl, axiosOptions); - - if (res.data.entry){ - results = results.concat(res.data.entry.map((entry: any) => entry.resource)); - } - - const nextLink = res.data.link && res.data.link.find((link: any) => link.relation === 'next'); - nextUrl = nextLink ? nextLink.url : null; - if (nextUrl) { - const qs = nextUrl.split('?')[1]; - nextUrl = `${OPENMRS.url}/?${qs}`; - } - } - return { status: 200, data: results }; - } catch (error: any) { - logger.error(error); - return { status: error.status, data: error.data }; - } + return getResourcesSince(OPENMRS.url, lastUpdated, resourceType); } export async function createOpenMRSResource(doc: fhir4.Resource) { @@ -57,7 +25,7 @@ export async function createOpenMRSResource(doc: fhir4.Resource) { return { status: res.status, data: res.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } @@ -67,6 +35,6 @@ export async function updateOpenMRSResource(doc: fhir4.Resource) { return { status: res.status, data: res.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index 81c2d715..509ed183 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -174,7 +174,7 @@ export async function syncPatients(startTime: Date){ /* Get a patient from a list of resources, by an encounters subject reference */ -function getPatient(encounter: fhir4.Encounter, references: fhir4.Resource[]): fhir4.Patient { +export function getPatient(encounter: fhir4.Encounter, references: fhir4.Resource[]): fhir4.Patient { return references.filter((resource) => { return resource.resourceType === 'Patient' && `Patient/${resource.id}` === encounter.subject?.reference })[0] as fhir4.Patient; @@ -184,7 +184,7 @@ function getPatient(encounter: fhir4.Encounter, references: fhir4.Resource[]): f Get a list of observations from a list of resources where the observations encounter reference is the encounter */ -function getObservations(encounter: fhir4.Encounter, references: fhir4.Resource[]): fhir4.Observation[] { +export function getObservations(encounter: fhir4.Encounter, references: fhir4.Resource[]): fhir4.Observation[] { return references.filter((resource) => { if (resource.resourceType === 'Observation') { const observation = resource as fhir4.Observation; @@ -201,7 +201,7 @@ function getObservations(encounter: fhir4.Encounter, references: fhir4.Resource[ Updates the OpenMRS Id on the CHT encounter to the VisitNote Sends Observations for the visitNote Encounter */ -async function sendEncounterToOpenMRS( +export async function sendEncounterToOpenMRS( encounter: fhir4.Encounter, references: fhir4.Resource[] ) { @@ -221,7 +221,7 @@ async function sendEncounterToOpenMRS( if (visitNoteResponse.status == 200 || visitNoteResponse.status == 201) { const visitNote = visitNoteResponse.data as fhir4.Encounter; // save openmrs id on orignal encounter - logger.info(`Updating Encounter ${patient.id} with openMRSId ${visitNote.id}`); + logger.info(`Updating Encounter ${encounter.id} with openMRSId ${visitNote.id}`); copyIdToNamedIdentifier(visitNote, encounter, openMRSIdentifierType); addSourceMeta(visitNote, chtSource); await updateFhirResource(encounter); @@ -238,7 +238,7 @@ async function sendEncounterToOpenMRS( Send Observation from OpenMRS to FHIR Replacing the subject reference */ -async function sendObservationToFhir(observation: fhir4.Observation, patient: fhir4.Patient) { +export async function sendObservationToFhir(observation: fhir4.Observation, patient: fhir4.Patient) { logger.info(`Sending Observation ${observation.code!.coding![0]!.code} to FHIR`); replaceReference(observation, 'subject', patient); createFhirResource(observation); @@ -251,7 +251,7 @@ async function sendObservationToFhir(observation: fhir4.Observation, patient: fh If this encounter matches a CHT form, gathers observations and sends them to CHT */ -async function sendEncounterToFhir( +export async function sendEncounterToFhir( encounter: fhir4.Encounter, references: fhir4.Resource[] ) { @@ -263,7 +263,6 @@ async function sendEncounterToFhir( logger.error(`Not sending encounter which is incomplete ${encounter.id}`); return } - logger.info(`Sending Encounter ${encounter.id} to FHIR`); const patient = getPatient(encounter, references); const observations = getObservations(encounter, references); diff --git a/mediator/src/utils/tests/cht.spec.ts b/mediator/src/utils/tests/cht.spec.ts index 7f99b3a5..205fbdbe 100644 --- a/mediator/src/utils/tests/cht.spec.ts +++ b/mediator/src/utils/tests/cht.spec.ts @@ -1,7 +1,15 @@ -import { createChtFollowUpRecord, generateChtRecordsApiUrl } from '../cht'; +import { + createChtFollowUpRecord, + generateChtRecordsApiUrl, + getLocationFromOpenMRSPatient, + queryCht } from '../cht'; import axios from 'axios'; +import { logger } from '../../../logger'; +import { OpenMRSPatientFactory } from '../../middlewares/schemas/tests/openmrs-resource-factories'; +import { mockQueryCht } from '../../controllers/tests/utils'; jest.mock('axios'); +jest.mock('../../../logger'); const mockAxios = axios as jest.Mocked; @@ -36,4 +44,86 @@ describe('CHT Utils', () => { expect(res).toContain(`${username}:${password}`); }); }); + + describe('getLocationFromOpenMRSPatient', () => { + it('should return place ID if address contains place ID', async () => { + const fhirPatient = OpenMRSPatientFactory.build({}, { + addressKey: 'address4', + addressValue: 'FCHV Area [12345]' + }); + + const result = await getLocationFromOpenMRSPatient(fhirPatient as any); + + expect(result).toBe('12345'); + }); + + it('should return an empty string if no address or place ID is found', async () => { + const fhirPatient = OpenMRSPatientFactory.build({}, { + addressKey: 'address4', + addressValue: 'Unknown Area' + }); + + mockQueryCht.mockResolvedValue({ status: 200, data: { docs: [] } }); // Simulating no result from the query + + const result = await getLocationFromOpenMRSPatient(fhirPatient as any); + + expect(result).toBe(''); + }); + + it('should return address5 if address4 is not available', async () => { + const fhirPatient = OpenMRSPatientFactory.build({}, { + addressKey: 'address5', + addressValue: 'Health Center [54321]' + }); + + const result = await getLocationFromOpenMRSPatient(fhirPatient as any); + + expect(result).toBe('54321'); + }); + + it('should handle error cases by returning an empty string when query fails', async () => { + const fhirPatient = OpenMRSPatientFactory.build({}, { + addressKey: 'address4', + addressValue: 'Unknown Location' + }); + + mockQueryCht.mockRejectedValue(new Error('Database query failed')); + + const result = await getLocationFromOpenMRSPatient(fhirPatient as any); + + expect(result).toBe(''); + }); + }); + + describe('queryCHT', () => { + it('should return data when the query is successful', async () => { + const mockQuery = { selector: { type: 'contact' } }; + const mockResponse = { status: 200, data: { docs: [{ place_id: '12345' }] } }; + + mockAxios.post.mockResolvedValue(mockResponse); // Simulate a successful response + + const result = await queryCht(mockQuery); + + expect(mockAxios.post).toHaveBeenCalledWith(expect.stringContaining('_find'), mockQuery, expect.anything()); + expect(result).toEqual(mockResponse); + }); + + it('should log an error and return error.response.data when the query fails', async () => { + const mockQuery = { selector: { type: 'contact' } }; + const mockError = { + response: { status: 500, data: 'Internal Server Error' } + } + + mockAxios.post.mockRejectedValue(mockError); // Simulate an error response + const loggerErrorSpy = jest.spyOn(logger, 'error'); // Spy on the logger's error method + + const result = await queryCht(mockQuery); + + expect(loggerErrorSpy).toHaveBeenCalledWith(mockError); + expect(result).toEqual({ + status: mockError.response.status, + data: mockError.response.data + }); + }); + }); }); diff --git a/mediator/src/utils/tests/fhir.spec.ts b/mediator/src/utils/tests/fhir.spec.ts index 63854743..a619327b 100644 --- a/mediator/src/utils/tests/fhir.spec.ts +++ b/mediator/src/utils/tests/fhir.spec.ts @@ -1,5 +1,8 @@ import { logger } from '../../../logger'; -import { EncounterFactory } from '../../middlewares/schemas/tests/fhir-resource-factories'; +import { + EncounterFactory, + PatientFactory +} from '../../middlewares/schemas/tests/fhir-resource-factories'; import { createFHIRSubscriptionResource, createFhirResource, @@ -7,8 +10,11 @@ import { generateFHIRSubscriptionResource, getFHIROrgEndpointResource, getFHIRPatientResource, + getFhirResourcesSince, + addId } from '../fhir'; import axios from 'axios'; +import { FHIR } from '../../../config'; jest.mock('axios'); jest.mock('../../../logger'); @@ -201,7 +207,9 @@ describe('FHIR Utils', () => { }); it('should return an error if the FHIR server returns an error', async () => { - const data = { status: 400, data: { message: 'Bad request' } }; + const data = { + response: { status: 400, data: { message: 'Bad request' } } + }; mockAxios.post = jest.fn().mockRejectedValue(data); @@ -211,8 +219,128 @@ describe('FHIR Utils', () => { expect(mockAxios.post.mock.calls[0][0]).toContain(resourceType); expect(mockAxios.post.mock.calls[0][1]).toEqual({...encounter, resourceType}); expect(res.status).toEqual(400); - expect(res.data).toEqual(data.data); + expect(res.data).toEqual(data.response.data); expect(logger.error).toBeCalledTimes(1); }); }); + + describe('addIds', () => { + it('should add ids to a fhir patient', () => { + const patient = PatientFactory.build(); + const idType = { coding: [{ code: 'OpenMRS ID' }] }; + const value = '12345'; + + const result = addId(patient, idType, value); + + expect(result.identifier).toBeDefined(); + // patient has one idenditifer already, so afterwards, should be 2 + expect(result.identifier?.length).toBe(2); + // and the one we are checking is the second one + expect(result.identifier?.[1]).toEqual({ + type: idType, + value: value + }); + }); + }); + + describe('getFhirResourcesSince', () => { + it('should fetch FHIR resources successfully', async () => { + const lastUpdated = new Date('2023-01-01T00:00:00Z'); + const resourceType = 'Patient'; + const mockResponse = { + data: { + entry: [ + { resource: { id: '123', resourceType: 'Patient' } } + ], + link: [] + } + }; + mockAxios.get.mockResolvedValue(mockResponse); + + const result = await getFhirResourcesSince(lastUpdated, resourceType); + + expect(mockAxios.get).toHaveBeenCalledWith( + `${FHIR.url}/Patient/?_lastUpdated=gt2023-01-01T00:00:00.000Z`, + expect.anything() // axiosOptions + ); + expect(result.status).toBe(200); + expect(result.data).toEqual([{ id: '123', resourceType: 'Patient' }]); + }); + + it('should include related resources for encounters', async () => { + const lastUpdated = new Date('2023-01-01T00:00:00Z'); + const resourceType = 'Encounter'; + const mockResponse = { + data: { + entry: [ + { resource: { id: 'enc-123', resourceType: 'Encounter' } } + ], + link: [] + } + }; + mockAxios.get.mockResolvedValue(mockResponse); + + const result = await getFhirResourcesSince(lastUpdated, resourceType); + + expect(mockAxios.get).toHaveBeenCalledWith( + `${FHIR.url}/Encounter/?_lastUpdated=gt2023-01-01T00:00:00.000Z&_revinclude=Observation:encounter&_include=Encounter:patient`, + expect.anything() // axiosOptions + ); + expect(result.status).toBe(200); + expect(result.data).toEqual([{ id: 'enc-123', resourceType: 'Encounter' }]); + }); + + it('should handle pagination', async () => { + const lastUpdated = new Date('2023-01-01T00:00:00Z'); + const resourceType = 'Patient'; + const mockFirstPageResponse = { + data: { + entry: [ + { resource: { id: '123', resourceType: 'Patient' } } + ], + link: [ + { relation: 'next', url: `${FHIR.url}/Patient/?page=2` } + ] + } + }; + const mockSecondPageResponse = { + data: { + entry: [ + { resource: { id: '124', resourceType: 'Patient' } } + ], + link: [] + } + }; + mockAxios.get + .mockResolvedValueOnce(mockFirstPageResponse) + .mockResolvedValueOnce(mockSecondPageResponse); + + const result = await getFhirResourcesSince(lastUpdated, resourceType); + + expect(mockAxios.get).toHaveBeenCalledTimes(2); + expect(result.status).toBe(200); + expect(result.data).toEqual([ + { id: '123', resourceType: 'Patient' }, + { id: '124', resourceType: 'Patient' } + ]); + }); + + it('should return an error if the request fails', async () => { + const lastUpdated = new Date('2023-01-01T00:00:00Z'); + const resourceType = 'Patient'; + const mockError = { + response: { + status: 500, + data: 'Internal Server Error' + } + }; + mockAxios.get.mockRejectedValue(mockError); + + const result = await getFhirResourcesSince(lastUpdated, resourceType); + + expect(logger.error).toHaveBeenCalledWith(mockError); + expect(result.status).toBe(500); + expect(result.data).toBe('Internal Server Error'); + }); + }); }); diff --git a/mediator/src/utils/tests/openmrs.spec.ts b/mediator/src/utils/tests/openmrs.spec.ts new file mode 100644 index 00000000..1a40447c --- /dev/null +++ b/mediator/src/utils/tests/openmrs.spec.ts @@ -0,0 +1,82 @@ +import axios from 'axios'; +import { createOpenMRSResource, updateOpenMRSResource, getOpenMRSResourcesSince } from '../openmrs'; +import { logger } from '../../../logger'; +import { OPENMRS } from '../../../config'; + +jest.mock('axios'); +jest.mock('../../../logger'); + +describe('OpenMRS utility functions', () => { + const mockAxiosGet = axios.get as jest.Mock; + const mockAxiosPost = axios.post as jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createOpenMRSResource', () => { + it('should create a new OpenMRS resource', async () => { + const mockResource = { id: '456', resourceType: 'Patient' }; + const mockResponse = { status: 201, data: mockResource }; + mockAxiosPost.mockResolvedValue(mockResponse); + + const result = await createOpenMRSResource(mockResource); + + expect(mockAxiosPost).toHaveBeenCalledWith( + `${OPENMRS.url}/Patient`, + mockResource, + expect.anything() // axiosOptions + ); + expect(result).toEqual({ status: 201, data: mockResource }); + }); + + it('should handle errors when creating a resource', async () => { + const mockResource = { id: '456', resourceType: 'Patient' }; + const mockError = { + response: { status: 500, data: 'Internal Server Error' } + }; + mockAxiosPost.mockRejectedValue(mockError); + + const result = await createOpenMRSResource(mockResource); + + expect(logger.error).toHaveBeenCalledWith(mockError); + expect(result).toEqual({ + status: mockError.response.status, + data: mockError.response.data + }); + }); + }); + + describe('updateOpenMRSResource', () => { + it('should update an existing OpenMRS resource', async () => { + const mockResource = { id: '456', resourceType: 'Patient' }; + const mockResponse = { status: 200, data: mockResource }; + mockAxiosPost.mockResolvedValue(mockResponse); + + const result = await updateOpenMRSResource(mockResource); + + expect(mockAxiosPost).toHaveBeenCalledWith( + `${OPENMRS.url}/Patient/456`, + mockResource, + expect.anything() // axiosOptions + ); + expect(result).toEqual({ status: 200, data: mockResource }); + }); + + it('should handle errors when updating a resource', async () => { + const mockResource = { id: '456', resourceType: 'Patient' }; + const mockError = { + response: { status: 500, data: 'Internal Server Error' } + }; + mockAxiosPost.mockRejectedValue(mockError); + + const result = await updateOpenMRSResource(mockResource); + + expect(logger.error).toHaveBeenCalledWith(mockError); + expect(result).toEqual({ + status: mockError.response.status, + data: mockError.response.data + }); + }); + }); +}); diff --git a/mediator/src/utils/tests/openmrs_sync.spec.ts b/mediator/src/utils/tests/openmrs_sync.spec.ts index 765c68b4..6a0c0933 100644 --- a/mediator/src/utils/tests/openmrs_sync.spec.ts +++ b/mediator/src/utils/tests/openmrs_sync.spec.ts @@ -1,149 +1,261 @@ -import { compare, syncPatients, syncEncounters } from '../openmrs_sync'; +import { + compare, + syncPatients, + syncEncounters, + getPatient +} from '../openmrs_sync'; import * as fhir from '../fhir'; import * as openmrs from '../openmrs'; import * as cht from '../cht'; -import { PatientFactory } from '../../middlewares/schemas/tests/fhir-resource-factories'; +import { PatientFactory, EncounterFactory, ObservationFactory } from '../../middlewares/schemas/tests/fhir-resource-factories'; +import { visitType, visitNoteType } from '../../mappers/openmrs'; +import { chtDocumentIdentifierType, chtPatientIdentifierType } from '../../mappers/cht'; +import { getIdType } from '../../utils/fhir'; import axios from 'axios'; +import { logger } from '../../../logger'; jest.mock('axios'); +jest.mock('../../../logger'); describe('OpenMRS Sync', () => { - it('compares resources with the gvien key', async () => { - const lastUpdated = new Date(); - lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); - - const constants = { - resourceType: 'Patient', - meta: { lastUpdated: lastUpdated } - } - - jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ - data: [ - { id: 'outgoing', ...constants }, - { id: 'toupdate', ...constants } - ], - status: 200, - }); - jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ - data: [ - { id: 'incoming', ...constants }, - { id: 'toupdate', ...constants } - ], - status: 200, - }); + describe('compare', () => { + it('compares resources with the gvien key', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); - const getKey = (obj: any) => { return obj.id }; - const startTime = new Date(); - startTime.setHours(startTime.getHours() - 1); - const comparison = await compare(getKey, 'Patient', startTime) + const constants = { + resourceType: 'Patient', + meta: { lastUpdated: lastUpdated } + } - expect(comparison.incoming).toEqual([{id: 'incoming', ...constants }]); - expect(comparison.outgoing).toEqual([{id: 'outgoing', ...constants }]); - expect(comparison.toupdate).toEqual([{id: 'toupdate', ...constants }]); + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: [ + { id: 'outgoing', ...constants }, + { id: 'toupdate', ...constants } + ], + status: 200, + }); + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: [ + { id: 'incoming', ...constants }, + { id: 'toupdate', ...constants } + ], + status: 200, + }); - expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); - expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); - }); + const getKey = (obj: any) => { return obj.id }; + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await compare(getKey, 'Patient', startTime) - it('loads references for related resources', async () => { - const lastUpdated = new Date(); - lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); - const reference = { - id: 'reference0', - resourceType: 'Patient', - meta: { lastUpdated: lastUpdated } - }; - const resource = { - id: 'resource0', - resourceType: 'Encounter', - meta: { lastUpdated: lastUpdated } - }; - - jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ - data: [ resource, reference ], - status: 200, - }); - jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ - data: [ resource ], - status: 200, + expect(comparison.incoming).toEqual([{id: 'incoming', ...constants }]); + expect(comparison.outgoing).toEqual([{id: 'outgoing', ...constants }]); + expect(comparison.toupdate).toEqual([{id: 'toupdate', ...constants }]); + + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); }); - const getKey = (obj: any) => { return obj.id }; - const startTime = new Date(); - startTime.setHours(startTime.getHours() - 1); - const comparison = await compare(getKey, 'Encounter', startTime) + it('loads references for related resources', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); + const reference = { + id: 'reference0', + resourceType: 'Patient', + meta: { lastUpdated: lastUpdated } + }; + const resource = { + id: 'resource0', + resourceType: 'Encounter', + meta: { lastUpdated: lastUpdated } + }; - expect(comparison.references).toContainEqual(reference); - expect(comparison.toupdate).toEqual([resource]); + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: [ resource, reference ], + status: 200, + }); + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: [ resource ], + status: 200, + }); - expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); - expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); - }); + const getKey = (obj: any) => { return obj.id }; + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await compare(getKey, 'Encounter', startTime) - it('sends incoming Patients to FHIR and CHT', async () => { - const lastUpdated = new Date(); - lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); + expect(comparison.references).toContainEqual(reference); + expect(comparison.toupdate).toEqual([resource]); - const openMRSPatient = PatientFactory.build(); - openMRSPatient.meta = { lastUpdated: lastUpdated }; - jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ - data: [], - status: 200, - }); - jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ - data: [openMRSPatient], - status: 200, + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); }); - jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ - data: openMRSPatient, - status: 200 + }); + + describe('syncPatients', () => { + it('sends incoming Patients to FHIR and CHT', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); + + const openMRSPatient = PatientFactory.build(); + openMRSPatient.meta = { lastUpdated: lastUpdated }; + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: [], + status: 200, + }); + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: [openMRSPatient], + status: 200, + }); + jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ + data: openMRSPatient, + status: 200 + }); + jest.spyOn(cht, 'createChtPatient') + + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await syncPatients(startTime); + + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); + + expect(fhir.updateFhirResource).toHaveBeenCalledWith(openMRSPatient); + expect(cht.createChtPatient).toHaveBeenCalledWith(openMRSPatient); }); - jest.spyOn(cht, 'createChtPatient') - const getKey = (obj: any) => { return obj.id }; - const startTime = new Date(); - startTime.setHours(startTime.getHours() - 1); - const comparison = await syncPatients(startTime); + it('sends outgoing Patients to OpenMRS', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); - expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); - expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); + const fhirPatient = PatientFactory.build(); + fhirPatient.meta = { lastUpdated: lastUpdated }; - expect(fhir.updateFhirResource).toHaveBeenCalledWith(openMRSPatient); - expect(cht.createChtPatient).toHaveBeenCalledWith(openMRSPatient); - }); + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: [fhirPatient], + status: 200, + }); + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: [], + status: 200, + }); + jest.spyOn(openmrs, 'createOpenMRSResource').mockResolvedValueOnce({ + data: fhirPatient, + status: 200 + }); + jest.spyOn(fhir, 'updateFhirResource') - it('sends outgoing Patients to OpenMRS', async () => { - const lastUpdated = new Date(); - lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await syncPatients(startTime); - const fhirPatient = PatientFactory.build(); - fhirPatient.meta = { lastUpdated: lastUpdated }; + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); - jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ - data: [fhirPatient], - status: 200, - }); - jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ - data: [], - status: 200, + expect(openmrs.createOpenMRSResource).toHaveBeenCalledWith(fhirPatient); + // updating with openmrs id + expect(fhir.updateFhirResource).toHaveBeenCalledWith(fhirPatient); }); - jest.spyOn(openmrs, 'createOpenMRSResource').mockResolvedValueOnce({ - data: fhirPatient, - status: 200 + }); + describe('syncEncounters', () => { + it('sends incoming Encounters to FHIR and CHT', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); + + const openMRSPatient = PatientFactory.build(); + openMRSPatient.meta = { lastUpdated: lastUpdated }; + const openMRSEncounter = EncounterFactory.build(); + openMRSEncounter.meta = { lastUpdated: lastUpdated }; + openMRSEncounter.subject = { + reference: `Patient/${openMRSPatient.id}` + }; + const openMRSObservation = ObservationFactory.build(); + openMRSObservation.encounter = { reference: 'Encounter/' + openMRSEncounter.id } + + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: [], + status: 200, + }); + + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: [openMRSEncounter, openMRSPatient, openMRSObservation], + status: 200, + }); + + jest.spyOn(fhir, 'getFHIRPatientResource').mockResolvedValueOnce({ + data: { entry: [{ resource: openMRSPatient }] }, + status: 200, + }); + + jest.spyOn(fhir, 'updateFhirResource').mockResolvedValue({ + data: [], + status: 200, + }); + + jest.spyOn(fhir, 'createFhirResource') + + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await syncEncounters(startTime); + + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); + + expect(fhir.updateFhirResource).toHaveBeenCalledWith(openMRSEncounter); + expect(fhir.createFhirResource).toHaveBeenCalledWith(openMRSObservation); }); - //jest.spyOn(fhir, 'updateFhirResource') - const getKey = (obj: any) => { return obj.id }; - const startTime = new Date(); - startTime.setHours(startTime.getHours() - 1); - const comparison = await syncPatients(startTime); + it('sends outgoing Encounters to OpenMRS', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); - expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); - expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); + const fhirEncounter = EncounterFactory.build(); + fhirEncounter.meta = { lastUpdated: lastUpdated }; + const fhirObservation = ObservationFactory.build(); + fhirObservation.encounter = { reference: 'Encounter/' + fhirEncounter.id } + const chtDocId = { + system: "cht", + type: chtDocumentIdentifierType, + value: getIdType(fhirEncounter, chtDocumentIdentifierType) + } - expect(openmrs.createOpenMRSResource).toHaveBeenCalledWith(fhirPatient); - // updating with openmrs id - //expect(fhir.updateFhirResource).toHaveBeenCalledWith(fhirPatient); + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: [fhirEncounter, fhirObservation], + status: 200, + }); + + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: [], + status: 200, + }); + + jest.spyOn(openmrs, 'createOpenMRSResource').mockResolvedValue({ + data: [], + status: 200, + }); + + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await syncEncounters(startTime); + + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); + + expect(openmrs.createOpenMRSResource).toHaveBeenCalledWith( + expect.objectContaining({ + "type": expect.arrayContaining([visitType]), + "identifier": expect.arrayContaining([chtDocId]) + }) + ); + + expect(openmrs.createOpenMRSResource).toHaveBeenCalledWith( + expect.objectContaining({ + "type": expect.arrayContaining([visitNoteType]), + }) + ); + + expect(openmrs.createOpenMRSResource).toHaveBeenCalledWith(fhirObservation); + }); }); }); From 1a5be52f81ec78567f61ad53c661a3b74334102d Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Tue, 22 Oct 2024 12:22:17 +0300 Subject: [PATCH 41/67] chore(#142): sonar fixes --- mediator/src/utils/cht.ts | 68 ++++++---- mediator/src/utils/fhir.ts | 46 +++++-- mediator/src/utils/openmrs_sync.ts | 117 +++++++++++------- mediator/src/utils/tests/openmrs_sync.spec.ts | 8 +- 4 files changed, 158 insertions(+), 81 deletions(-) diff --git a/mediator/src/utils/cht.ts b/mediator/src/utils/cht.ts index e59545cf..22fdc1d8 100644 --- a/mediator/src/utils/cht.ts +++ b/mediator/src/utils/cht.ts @@ -40,7 +40,11 @@ export async function createChtFollowUpRecord(patientId: string) { } } -export async function getLocationFromOpenMRSPatient(fhirPatient: fhir4.Patient) { +/* + Get the address field from an OpenMRS Patient + Assuming it is stored at a specific path in the fhir Patient +*/ +function getAddressFromOpenMRSPatient(fhirPatient: fhir4.Patient) { // first, extract address value; is fchv area available? const addresses = fhirPatient.address?.[0]?.extension?.[0]?.extension; let addressKey = "http://fhir.openmrs.org/ext/address#address4" @@ -51,10 +55,47 @@ export async function getLocationFromOpenMRSPatient(fhirPatient: fhir4.Patient) addressKey = "http://fhir.openmrs.org/ext/address#address5" addressValue = addresses?.find((ext: any) => ext.url === addressKey)?.valueString; - // still no... return nothing - if (!addressValue) { - return ''; - } + } + return addressValue; +} + +/* + * Query CouchDB to get a place_id from a name + * This is a workaround for patients not having an place_id + * in the address field (as described above) + * Because it relies on names matching excatly, and qurying a + * CHT couchdb directly, it is not intended for general use +*/ +async function getPlaceIdFromCouch(addressValue: string) { + const query: CouchDBQuery = { + selector: { + type: "contact", + name: addressValue + }, + fields: ['place_id'] + } + const location = await queryCht(query); + + // edge cases can result in more than one location, get first matching + // if not found by name, no more we can do, give up + if (!location.data?.docs || location.data.docs.length == 0){ + return ''; + } else { + return location.data.docs[0].place_id; + } +} + +/* + * get a CHT place_id from an OpenMRS patient + * assumes that either the patient has an address containing the palce id + * (see above), or the name matches the contact name in CHT + * It is to support a specific workflow and is not intended for general use. +*/ +export async function getLocationFromOpenMRSPatient(fhirPatient: fhir4.Patient) { + // if no address found, return empty string + const addressValue = getAddressFromOpenMRSPatient(fhirPatient); + if (!addressValue) { + return ''; } // does the name have a place id included? @@ -66,22 +107,7 @@ export async function getLocationFromOpenMRSPatient(fhirPatient: fhir4.Patient) return match[1]; } else { // if not, query by name - const query: CouchDBQuery = { - selector: { - type: "contact", - name: addressValue - }, - fields: ['place_id'] - } - const location = await queryCht(query); - - // edge cases can result in more than one location, get first matching - // if not found by name, no more we can do, give up - if (!location.data?.docs || location.data.docs.length == 0){ - return ''; - } else { - return location.data.docs[0].place_id; - } + return getPlaceIdFromCouch(addressValue); } } diff --git a/mediator/src/utils/fhir.ts b/mediator/src/utils/fhir.ts index 33ce7a88..f56260f2 100644 --- a/mediator/src/utils/fhir.ts +++ b/mediator/src/utils/fhir.ts @@ -182,14 +182,43 @@ export async function getFhirResourcesSince(lastUpdated: Date, resourceType: str return getResourcesSince(FHIR.url, lastUpdated, resourceType); } +/* + * get the "next" url from a fhir paginated response and a base url +*/ +function getNextUrl(url: string, pagination: any) { + let nextUrl = ''; + const nextLink = pagination.link && pagination.link.find((link: any) => link.relation === 'next'); + if (nextLink?.url) { + const qs = nextLink.url.split('?')[1]; + nextUrl = `${url}/?${qs}`; + } + return nextUrl; +} + +/* + * Gets the full url for a resource type, given base url + * For some resource types, it is usefult o get related resources + * This function returns the full url including include clauses + * currently it is only for encounters, to include observations + * and the subject patient +*/ +function getResourceUrl(baseUrl: string, lastUpdated: Date, resourceType: string) { + let url = `${baseUrl}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`; + // for encounters, include related resources + if (resourceType === 'Encounter') { + url = url + '&_revinclude=Observation:encounter&_include=Encounter:patient'; + } + return url +} + +/* + * get resources of a given type from url, where lastUpdated is > the given data + * if results are paginated, goes through all pages +*/ export async function getResourcesSince(url: string, lastUpdated: Date, resourceType: string) { try { - let nextUrl = `${url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`; let results: fhir4.Resource[] = []; - // for encounters, include related resources - if (resourceType === 'Encounter') { - nextUrl = nextUrl + '&_revinclude=Observation:encounter&_include=Encounter:patient'; - } + let nextUrl = getResourceUrl(url, lastUpdated, resourceType); while (nextUrl) { const res = await axios.get(nextUrl, axiosOptions); @@ -198,12 +227,7 @@ export async function getResourcesSince(url: string, lastUpdated: Date, resource results = results.concat(res.data.entry.map((entry: any) => entry.resource)); } - const nextLink = res.data.link && res.data.link.find((link: any) => link.relation === 'next'); - nextUrl = nextLink ? nextLink.url : null; - if (nextUrl) { - const qs = nextUrl.split('?')[1]; - nextUrl = `${url}/?${qs}`; - } + nextUrl = getNextUrl(url, res.data); } return { status: 200, data: results }; } catch (error: any) { diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index 509ed183..0836adfe 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -211,27 +211,39 @@ export async function sendEncounterToOpenMRS( } logger.info(`Sending Encounter ${encounter.id} to OpenMRS`); + const patient = getPatient(encounter, references); const observations = getObservations(encounter, references); const patientId = getIdType(patient, openMRSIdentifierType); const openMRSVisit = buildOpenMRSVisit(patientId, encounter); + const visitResponse = await createOpenMRSResource(openMRSVisit[0]); - if (visitResponse.status == 200 || visitResponse.status == 201) { - const visitNoteResponse = await createOpenMRSResource(openMRSVisit[1]); - if (visitNoteResponse.status == 200 || visitNoteResponse.status == 201) { - const visitNote = visitNoteResponse.data as fhir4.Encounter; - // save openmrs id on orignal encounter - logger.info(`Updating Encounter ${encounter.id} with openMRSId ${visitNote.id}`); - copyIdToNamedIdentifier(visitNote, encounter, openMRSIdentifierType); - addSourceMeta(visitNote, chtSource); - await updateFhirResource(encounter); - observations.forEach((observation) => { - logger.info(`Sending Observation ${observation.code!.coding![0]!.code} to OpenMRS`); - const openMRSObservation = buildOpenMRSObservation(observation, patientId, visitNote.id || ''); - createOpenMRSResource(openMRSObservation); - }); - } + if (visitResponse.status != 201) { + logger.error(`Error saving visit to OpenMRS ${encounter.id}: ${visitResponse.status}`); + return + } + + const visitNoteResponse = await createOpenMRSResource(openMRSVisit[1]); + if (visitNoteResponse.status != 201) { + logger.error(`Error saving visit note to OpenMRS ${encounter.id}: ${visitNoteResponse.status}`); + return } + + const visitNote = visitNoteResponse.data as fhir4.Encounter; + + logger.info(`Updating Encounter ${encounter.id} with openMRSId ${visitNote.id}`); + + // save openmrs id on orignal encounter + copyIdToNamedIdentifier(visitNote, encounter, openMRSIdentifierType); + addSourceMeta(visitNote, chtSource); + + await updateFhirResource(encounter); + + observations.forEach((observation) => { + logger.info(`Sending Observation ${observation.code!.coding![0]!.code} to OpenMRS`); + const openMRSObservation = buildOpenMRSObservation(observation, patientId, visitNote.id || ''); + createOpenMRSResource(openMRSObservation); + }); } /* @@ -259,44 +271,59 @@ export async function sendEncounterToFhir( logger.error(`Not re-sending encounter from cht ${encounter.id}`); return } + if (!encounter.period?.end) { logger.error(`Not sending encounter which is incomplete ${encounter.id}`); return } + logger.info(`Sending Encounter ${encounter.id} to FHIR`); - const patient = getPatient(encounter, references); + const observations = getObservations(encounter, references); - if (patient && patient.id) { - // get patient from FHIR to resolve reference - const patientResponse = await getFHIRPatientResource(patient.id); - if (patientResponse.status == 200 || patientResponse.status == 201) { - const existingPatient = patientResponse.data?.entry[0].resource; - copyIdToNamedIdentifier(encounter, encounter, openMRSIdentifierType); - addSourceMeta(encounter, openMRSSource); - - logger.info(`Replacing ${encounter.subject!.reference} with ${patient.id} for ${encounter.id}`); - replaceReference(encounter, 'subject', existingPatient); - - // remove unused references - delete encounter.participant; - delete encounter.location; - - const response = await updateFhirResource(encounter); - if (response.status == 200 || response.status == 201) { - observations.forEach(o => sendObservationToFhir(o, existingPatient)); - - logger.info(`Sending Encounter ${encounter.id} to CHT`); - const chtResponse = await chtRecordFromObservations(existingPatient.id, observations); - if (chtResponse.status == 200) { - const chtId = chtResponse.data.id; - addId(encounter, chtDocumentIdentifierType, chtId) - await updateFhirResource(encounter); - } - } - } - } else { + + const patient = getPatient(encounter, references); + if (!patient?.id) { logger.error(`Patient ${encounter.subject!.reference} not found for ${encounter.id}`); + return + } + + // get patient from FHIR to resolve reference + const patientResponse = await getFHIRPatientResource(patient.id); + if (patientResponse.status != 200) { + logger.error(`Error getting Patient ${patient.id}: ${patientResponse.status}`); + return + } + + const existingPatient = patientResponse.data?.entry[0].resource; + copyIdToNamedIdentifier(encounter, encounter, openMRSIdentifierType); + addSourceMeta(encounter, openMRSSource); + + logger.info(`Replacing ${encounter.subject!.reference} with ${patient.id} for ${encounter.id}`); + replaceReference(encounter, 'subject', existingPatient); + + // remove unused references + delete encounter.participant; + delete encounter.location; + + const response = await updateFhirResource(encounter); + if (response.status != 201) { + logger.error(`Error saving encounter to fhir ${encounter.id}: ${response.status}`); + return } + + observations.forEach(o => sendObservationToFhir(o, existingPatient)); + + logger.info(`Sending Encounter ${encounter.id} to CHT`); + const chtResponse = await chtRecordFromObservations(existingPatient.id, observations); + if (chtResponse.status != 200) { + logger.error(`Error saving encounter to cht ${encounter.id}: ${chtResponse.status}`); + return + } + + const chtId = chtResponse.data.id; + addId(encounter, chtDocumentIdentifierType, chtId) + + await updateFhirResource(encounter); } /* diff --git a/mediator/src/utils/tests/openmrs_sync.spec.ts b/mediator/src/utils/tests/openmrs_sync.spec.ts index 6a0c0933..8f76eeae 100644 --- a/mediator/src/utils/tests/openmrs_sync.spec.ts +++ b/mediator/src/utils/tests/openmrs_sync.spec.ts @@ -110,7 +110,7 @@ describe('OpenMRS Sync', () => { }); jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ data: openMRSPatient, - status: 200 + status: 201 }); jest.spyOn(cht, 'createChtPatient') @@ -142,7 +142,7 @@ describe('OpenMRS Sync', () => { }); jest.spyOn(openmrs, 'createOpenMRSResource').mockResolvedValueOnce({ data: fhirPatient, - status: 200 + status: 201 }); jest.spyOn(fhir, 'updateFhirResource') @@ -190,7 +190,7 @@ describe('OpenMRS Sync', () => { jest.spyOn(fhir, 'updateFhirResource').mockResolvedValue({ data: [], - status: 200, + status: 201, }); jest.spyOn(fhir, 'createFhirResource') @@ -232,7 +232,7 @@ describe('OpenMRS Sync', () => { jest.spyOn(openmrs, 'createOpenMRSResource').mockResolvedValue({ data: [], - status: 200, + status: 201, }); const startTime = new Date(); From d6db1d6dcd7490a88e302fb14b7b274536e561e6 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Tue, 22 Oct 2024 12:42:21 +0300 Subject: [PATCH 42/67] chore(#142): sonar fixes --- mediator/src/utils/fhir.ts | 2 +- mediator/src/utils/openmrs_sync.ts | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/mediator/src/utils/fhir.ts b/mediator/src/utils/fhir.ts index f56260f2..28fad9b6 100644 --- a/mediator/src/utils/fhir.ts +++ b/mediator/src/utils/fhir.ts @@ -187,7 +187,7 @@ export async function getFhirResourcesSince(lastUpdated: Date, resourceType: str */ function getNextUrl(url: string, pagination: any) { let nextUrl = ''; - const nextLink = pagination.link && pagination.link.find((link: any) => link.relation === 'next'); + const nextLink = pagination.link?.find((link: any) => link.relation === 'next'); if (nextLink?.url) { const qs = nextLink.url.split('?')[1]; nextUrl = `${url}/?${qs}`; diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index 0836adfe..585b3e03 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -313,16 +313,26 @@ export async function sendEncounterToFhir( observations.forEach(o => sendObservationToFhir(o, existingPatient)); + sendEncounterToCht(encounter, existingPatient, observations); +} + +/* + Send an Encounter from OpenMRS to CHT +*/ +export async function sendEncounterToCht( + encounter: fhir4.Encounter, + patient: fhir4.Patient, + observations: fhir4.Observation[] +) { logger.info(`Sending Encounter ${encounter.id} to CHT`); - const chtResponse = await chtRecordFromObservations(existingPatient.id, observations); + const chtResponse = await chtRecordFromObservations(patient, observations); if (chtResponse.status != 200) { logger.error(`Error saving encounter to cht ${encounter.id}: ${chtResponse.status}`); return } const chtId = chtResponse.data.id; - addId(encounter, chtDocumentIdentifierType, chtId) - + addId(encounter, chtDocumentIdentifierType, chtId); await updateFhirResource(encounter); } From 7d279c18bc436616403fd8192fe75f6e814a195c Mon Sep 17 00:00:00 2001 From: witash Date: Wed, 23 Oct 2024 13:32:17 +0300 Subject: [PATCH 43/67] chore(#142): adding tests to increase coverage (#143) --- mediator/src/controllers/cht.ts | 6 +- mediator/src/controllers/tests/cht.spec.ts | 178 +++++++++ mediator/src/controllers/tests/utils.ts | 4 + mediator/src/mappers/openmrs.ts | 4 +- mediator/src/middlewares/schemas/encounter.ts | 4 + .../schemas/tests/cht-request-factories.ts | 18 + .../schemas/tests/fhir-resource-factories.ts | 24 +- .../tests/openmrs-resource-factories.ts | 32 ++ mediator/src/routes/cht.ts | 2 +- mediator/src/routes/tests/cht.spec.ts | 58 --- mediator/src/utils/cht.ts | 78 ++-- mediator/src/utils/fhir.ts | 76 ++-- mediator/src/utils/openmrs.ts | 40 +- mediator/src/utils/openmrs_sync.ts | 138 ++++--- mediator/src/utils/tests/cht.spec.ts | 92 ++++- mediator/src/utils/tests/fhir.spec.ts | 134 ++++++- mediator/src/utils/tests/openmrs.spec.ts | 82 +++++ mediator/src/utils/tests/openmrs_sync.spec.ts | 342 ++++++++++++------ 18 files changed, 980 insertions(+), 332 deletions(-) create mode 100644 mediator/src/controllers/tests/cht.spec.ts create mode 100644 mediator/src/middlewares/schemas/tests/openmrs-resource-factories.ts delete mode 100644 mediator/src/routes/tests/cht.spec.ts create mode 100644 mediator/src/utils/tests/openmrs.spec.ts diff --git a/mediator/src/controllers/cht.ts b/mediator/src/controllers/cht.ts index 960ab638..2cb4d4db 100644 --- a/mediator/src/controllers/cht.ts +++ b/mediator/src/controllers/cht.ts @@ -27,7 +27,7 @@ export async function createPatient(chtPatientDoc: any) { export async function updatePatientIds(chtFormDoc: any) { // first, get the existing patient from fhir server - const response = await getFHIRPatientResource(chtFormDoc.external_id); + const response = await getFHIRPatientResource(chtFormDoc.doc.external_id); if (response.status != 200) { return { status: 500, data: { message: `FHIR responded with ${response.status}`} }; @@ -37,10 +37,10 @@ export async function updatePatientIds(chtFormDoc: any) { } const fhirPatient = response.data.entry[0].resource; - addId(fhirPatient, chtPatientIdentifierType, chtFormDoc.patient_id); + addId(fhirPatient, chtPatientIdentifierType, chtFormDoc.doc.patient_id); // now, we need to get the actual patient doc from cht... - const patient_uuid = await getPatientUUIDFromSourceId(chtFormDoc._id); + const patient_uuid = await getPatientUUIDFromSourceId(chtFormDoc.doc._id); if (patient_uuid){ addId(fhirPatient, chtDocumentIdentifierType, patient_uuid); return updateFhirResource({ ...fhirPatient, resourceType: 'Patient' }); diff --git a/mediator/src/controllers/tests/cht.spec.ts b/mediator/src/controllers/tests/cht.spec.ts new file mode 100644 index 00000000..446fcdd6 --- /dev/null +++ b/mediator/src/controllers/tests/cht.spec.ts @@ -0,0 +1,178 @@ +import { + createPatient, + updatePatientIds, + createEncounter +} from '../cht' +import { + ChtPatientFactory, + ChtSMSPatientFactory, + ChtPatientIdsFactory, + ChtPregnancyForm +} from '../../middlewares/schemas/tests/cht-request-factories'; +import { + PatientFactory, + EncounterFactory, + ObservationFactory +} from '../../middlewares/schemas/tests/fhir-resource-factories'; +import { + chtDocumentIdentifierType, + chtPatientIdentifierType +} from '../../mappers/cht'; + +import * as fhir from '../../utils/fhir'; +import * as cht from '../../utils/cht'; + +import axios from 'axios'; +import { randomUUID } from 'crypto'; + +jest.mock('axios'); + +describe('CHT outgoing document controllers', () => { + describe('createPatient', () => { + it('creates a FHIR Patient from CHT patient doc', async () => { + jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ + data: {}, + status: 200, + }); + + const data = ChtPatientFactory.build(); + + const res = await createPatient(data); + + expect(res.status).toBe(200); + + // assert that the create resource has the right identifier and type + expect(fhir.updateFhirResource).toHaveBeenCalledWith( + expect.objectContaining({ + resourceType: 'Patient', + identifier: expect.arrayContaining([ + expect.objectContaining({ + type: chtDocumentIdentifierType, + value: data.doc._id + }) + ]), + }) + ); + }); + + it('creates a FHIR Patient from an SMS form using source id', async () => { + jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ + data: {}, + status: 200, + }); + + let sourceId = randomUUID(); + jest.spyOn(cht, 'getPatientUUIDFromSourceId').mockResolvedValueOnce(sourceId); + + const data = ChtSMSPatientFactory.build(); + + const res = await createPatient(data); + + expect(res.status).toBe(200); + + // assert that the createid resource has the right identifier and type + expect(fhir.updateFhirResource).toHaveBeenCalledWith( + expect.objectContaining({ + resourceType: 'Patient', + identifier: expect.arrayContaining([ + expect.objectContaining({ + type: chtDocumentIdentifierType, + value: sourceId + }) + ]), + }) + ); + }); + }); + + describe('updatePatientIds', () => { + it('updates patient ids', async () => { + const existingPatient = PatientFactory.build(); + jest.spyOn(fhir, 'getFHIRPatientResource').mockResolvedValue({ + data: { total: 1, entry: [ { resource: existingPatient } ] }, + status: 200, + }); + jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ + data: {}, + status: 200, + }); + + let sourceId = randomUUID(); + jest.spyOn(cht, 'getPatientUUIDFromSourceId').mockResolvedValueOnce(sourceId); + + const data = ChtPatientIdsFactory.build(); + + const res = await updatePatientIds(data); + + expect(res.status).toBe(200); + + // assert that the created resource has the right identifier and type + expect(fhir.updateFhirResource).toHaveBeenCalledWith( + expect.objectContaining({ + id: existingPatient.id, + identifier: expect.arrayContaining([ + expect.objectContaining({ + type: chtDocumentIdentifierType, + value: sourceId + }) + ]), + }) + ); + }); + }); + + describe('createEncounter', () => { + it('creates FHIR Encounter from CHT form', async () => { + jest.spyOn(fhir, 'getFHIRPatientResource').mockResolvedValueOnce({ + data: { total: 1, entry: [ { resource: PatientFactory.build() } ] }, + status: 200, + }); + // observations use createFhirResource + jest.spyOn(fhir, 'createFhirResource').mockResolvedValueOnce({ + data: {}, + status: 200, + }); + // encounter uses updatedFhirResource + jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ + data: {}, + status: 200, + }); + + const data = ChtPregnancyForm.build(); + + const res = await createEncounter(data); + + expect(res.status).toBe(200); + + // assert that the encounter was created + expect(fhir.updateFhirResource).toHaveBeenCalledWith( + expect.objectContaining({ + resourceType: 'Encounter', + identifier: expect.arrayContaining([ + expect.objectContaining({ + type: chtDocumentIdentifierType, + value: data.id + }) + ]), + }) + ); + + // assert that at least one observation was created with the right codes + expect(fhir.createFhirResource).toHaveBeenCalledWith( + expect.objectContaining({ + resourceType: 'Observation', + code: { + coding: expect.arrayContaining([{ + code: data.observations[0].code + }]) + }, + valueCodeableConcept: { + coding: expect.arrayContaining([{ + code: data.observations[0].valueCode + }]) + } + }) + ); + }); + }); +}); diff --git a/mediator/src/controllers/tests/utils.ts b/mediator/src/controllers/tests/utils.ts index 5fe3e797..b582c66f 100644 --- a/mediator/src/controllers/tests/utils.ts +++ b/mediator/src/controllers/tests/utils.ts @@ -5,6 +5,7 @@ import { deleteFhirSubscription, createFHIRSubscriptionResource, } from '../../utils/fhir'; +import { queryCht } from '../../utils/cht'; jest.mock('../../utils/fhir'); jest.mock('../../utils/cht'); @@ -24,3 +25,6 @@ export const mockCreateFHIRSubscriptionResource = export const mockCreateChtRecord = createChtFollowUpRecord as jest.MockedFn< typeof createChtFollowUpRecord >; +export const mockQueryCht = queryCht as jest.MockedFn< + typeof queryCht +>; diff --git a/mediator/src/mappers/openmrs.ts b/mediator/src/mappers/openmrs.ts index 99f9a5fe..c1cf0424 100644 --- a/mediator/src/mappers/openmrs.ts +++ b/mediator/src/mappers/openmrs.ts @@ -14,7 +14,7 @@ export const openMRSIdentifierType: fhir4.CodeableConcept = { export const openMRSSource = 'openmrs'; -const visitNoteType: fhir4.CodeableConcept = { +export const visitNoteType: fhir4.CodeableConcept = { text: "Visit Note", coding: [{ system: "http://fhir.openmrs.org/code-system/encounter-type", @@ -23,7 +23,7 @@ const visitNoteType: fhir4.CodeableConcept = { }] } -const visitType: fhir4.CodeableConcept = { +export const visitType: fhir4.CodeableConcept = { text: "Home Visit", coding: [{ system: "http://fhir.openmrs.org/code-system/visit-type", diff --git a/mediator/src/middlewares/schemas/encounter.ts b/mediator/src/middlewares/schemas/encounter.ts index b6d3d50c..0665720a 100644 --- a/mediator/src/middlewares/schemas/encounter.ts +++ b/mediator/src/middlewares/schemas/encounter.ts @@ -21,4 +21,8 @@ export const EncounterSchema = joi.object({ type: joi.array().length(1).required(), subject: joi.required(), participant: joi.array().length(1).required(), + period: joi.object({ + start: joi.string(), + end: joi.string() + }) }); diff --git a/mediator/src/middlewares/schemas/tests/cht-request-factories.ts b/mediator/src/middlewares/schemas/tests/cht-request-factories.ts index c45dd7f1..337c1504 100644 --- a/mediator/src/middlewares/schemas/tests/cht-request-factories.ts +++ b/mediator/src/middlewares/schemas/tests/cht-request-factories.ts @@ -12,6 +12,24 @@ export const ChtPatientDoc = Factory.define('chtPatientDoc') .attr('sex', 'female') .attr('patient_id', randomUUID()); +export const ChtSMSPatientFactory = Factory.define('chtPatient') + .attr('doc', () => ChtSMSPatientDoc.build()) + +export const ChtSMSPatientDoc = Factory.define('chtPatientDoc') + .attr('_id', randomUUID()) + .attr('name', 'John Doe') + .attr('phone', '+9770000000') + .attr('date_of_birth', '2000-01-01') + .attr('sex', 'female') + .attr('source_id', randomUUID()); + +export const ChtPatientIdsFactory = Factory.define('chtPatientIds') + .attr('doc', () => ChtPatientIdsDoc.build()) + +export const ChtPatientIdsDoc = Factory.define('chtPatientIds') + .attr('external_id', randomUUID()) + .attr('patient_uuid', randomUUID()); + export const ChtPregnancyForm = Factory.define('chtPregnancyDoc') .attr('patient_uuid', randomUUID()) .attr('reported_date', Date.now()) diff --git a/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts b/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts index a489694d..34a6befa 100644 --- a/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts +++ b/mediator/src/middlewares/schemas/tests/fhir-resource-factories.ts @@ -1,12 +1,11 @@ import { randomUUID } from 'crypto'; import { Factory } from 'rosie'; import { VALID_CODE, VALID_SYSTEM } from '../endpoint'; +import { chtDocumentIdentifierType } from '../../../mappers/cht'; const identifier = [ { - type: { - text: 'CHT Document Identifier' - }, + type: chtDocumentIdentifierType, system: 'cht', value: randomUUID(), }, @@ -28,11 +27,15 @@ export const EncounterFactory = Factory.define('encounter') .attr('resourceType', 'Encounter') .attr('id', randomUUID()) .attr('identifier', identifier) - .attr('status', 'planned') + .attr('status', 'finished') .attr('class', 'outpatient') .attr('type', [{ text: 'Community health worker visit' }]) .attr('subject', { reference: 'Patient/3' }) - .attr('participant', [{ type: [{ text: 'Community health worker' }] }]); + .attr('participant', [{ type: [{ text: 'Community health worker' }] }]) + .attr('period', { + start: new Date(new Date().getTime() - 60 * 60 * 1000).toISOString(), + end: new Date(new Date().getTime() - 50 * 60 * 1000).toISOString() + }) export const EndpointFactory = Factory.define('endpoint') .attr('connectionType', { system: VALID_SYSTEM, code: VALID_CODE }) @@ -56,3 +59,14 @@ export const ServiceRequestFactory = Factory.define('serviceRequest') .attr('intent', 'order') .attr('subject', SubjectFactory.build()) .attr('requester', RequesterFactory.build()); + +export const ObservationFactory = Factory.define('Observation') + .attr('resourceType', 'Observation') + .attr('id', () => randomUUID()) + .attr('encounter', () => { reference: 'Encounter/' + randomUUID() }) + .attr('code', { + coding: [{ code: 'DANGER_SIGNS' }], + }) + .attr('valueCodeableConcept', { + coding: [{ code: 'HIGH_BLOOD_PRESSURE' }] + }); diff --git a/mediator/src/middlewares/schemas/tests/openmrs-resource-factories.ts b/mediator/src/middlewares/schemas/tests/openmrs-resource-factories.ts new file mode 100644 index 00000000..48907540 --- /dev/null +++ b/mediator/src/middlewares/schemas/tests/openmrs-resource-factories.ts @@ -0,0 +1,32 @@ +import { randomUUID } from 'crypto'; +import { Factory } from 'rosie'; +import { visitNoteType, visitType } from '../../../mappers/openmrs'; + +// creates an openmrs patient with the special address extension +export const OpenMRSPatientFactory = Factory.define('openMRSFhirPatient') + .attr('resourceType', 'Patient') + .attr('id', () => randomUUID()) // Assign a random UUID for the patient + .attr('address', ['addressKey', 'addressValue'], (addressKey, addressValue) => [ + { + extension: [{ + extension: [ + { + url: `http://fhir.openmrs.org/ext/address#${addressKey}`, + valueString: addressValue + } + ] + }] + } + ]); + +// creates an openmrs encounter with visit type +export const OpenMRSVisitFactory = Factory.define('openMRSVisit') + .attr('resourceType', 'Encounter') + .attr('id', () => randomUUID()) // Assign a random UUID for the patient + .attr('type', visitType); + +// creates an openmrs encounter with visit note type +export const OpenMRSVisitNoteFactory = Factory.define('openMRSVisit') + .attr('resourceType', 'Encounter') + .attr('id', () => randomUUID()) // Assign a random UUID for the patient + .attr('type', visitNoteType); diff --git a/mediator/src/routes/cht.ts b/mediator/src/routes/cht.ts index 4444d10b..1bebab29 100644 --- a/mediator/src/routes/cht.ts +++ b/mediator/src/routes/cht.ts @@ -13,7 +13,7 @@ router.post( router.post( '/patient_ids', - requestHandler((req) => updatePatientIds(req.body.doc)) + requestHandler((req) => updatePatientIds(req.body)) ); router.post( diff --git a/mediator/src/routes/tests/cht.spec.ts b/mediator/src/routes/tests/cht.spec.ts deleted file mode 100644 index c45fd724..00000000 --- a/mediator/src/routes/tests/cht.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import request from 'supertest'; -import app from '../../..'; -import { ChtPatientFactory, ChtPregnancyForm } from '../../middlewares/schemas/tests/cht-request-factories'; -import { PatientFactory, EncounterFactory } from '../../middlewares/schemas/tests/fhir-resource-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 () => { - jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ - data: {}, - status: 200, - }); - - const data = ChtPatientFactory.build(); - - const res = await request(app).post('/cht/patient').send(data); - - expect(res.status).toBe(200); - expect(res.body).toEqual({}); - - /* - expect(fhir.createFhirResource).toHaveBeenCalledWith({ - ...data, - resourceType: 'Patient', - }); - */ - expect(fhir.updateFhirResource).toHaveBeenCalled(); - }); - - it('accepts incoming request with valid form', async () => { - jest.spyOn(fhir, 'getFHIRPatientResource').mockResolvedValueOnce({ - data: { total: 1, entry: [ { resource: PatientFactory.build() } ] }, - status: 200, - }); - jest.spyOn(fhir, 'createFhirResource').mockResolvedValueOnce({ - data: {}, - status: 200, - }); - - const data = ChtPregnancyForm.build(); - - const res = await request(app).post('/cht/encounter').send(data); - - expect(res.status).toBe(200); - expect(res.body).toEqual({}); - /* - expect(fhir.createFhirResource).toHaveBeenCalledWith({ - ...data, - resourceType: 'Patient', - }); - */ - expect(fhir.updateFhirResource).toHaveBeenCalled(); - }); - -}); diff --git a/mediator/src/utils/cht.ts b/mediator/src/utils/cht.ts index 32be635b..22fdc1d8 100644 --- a/mediator/src/utils/cht.ts +++ b/mediator/src/utils/cht.ts @@ -36,11 +36,15 @@ export async function createChtFollowUpRecord(patientId: string) { return { status: res?.status, data: res?.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } -async function getLocation(fhirPatient: fhir4.Patient) { +/* + Get the address field from an OpenMRS Patient + Assuming it is stored at a specific path in the fhir Patient +*/ +function getAddressFromOpenMRSPatient(fhirPatient: fhir4.Patient) { // first, extract address value; is fchv area available? const addresses = fhirPatient.address?.[0]?.extension?.[0]?.extension; let addressKey = "http://fhir.openmrs.org/ext/address#address4" @@ -51,10 +55,47 @@ async function getLocation(fhirPatient: fhir4.Patient) { addressKey = "http://fhir.openmrs.org/ext/address#address5" addressValue = addresses?.find((ext: any) => ext.url === addressKey)?.valueString; - // still no... return nothing - if (!addressValue) { - return ''; - } + } + return addressValue; +} + +/* + * Query CouchDB to get a place_id from a name + * This is a workaround for patients not having an place_id + * in the address field (as described above) + * Because it relies on names matching excatly, and qurying a + * CHT couchdb directly, it is not intended for general use +*/ +async function getPlaceIdFromCouch(addressValue: string) { + const query: CouchDBQuery = { + selector: { + type: "contact", + name: addressValue + }, + fields: ['place_id'] + } + const location = await queryCht(query); + + // edge cases can result in more than one location, get first matching + // if not found by name, no more we can do, give up + if (!location.data?.docs || location.data.docs.length == 0){ + return ''; + } else { + return location.data.docs[0].place_id; + } +} + +/* + * get a CHT place_id from an OpenMRS patient + * assumes that either the patient has an address containing the palce id + * (see above), or the name matches the contact name in CHT + * It is to support a specific workflow and is not intended for general use. +*/ +export async function getLocationFromOpenMRSPatient(fhirPatient: fhir4.Patient) { + // if no address found, return empty string + const addressValue = getAddressFromOpenMRSPatient(fhirPatient); + if (!addressValue) { + return ''; } // does the name have a place id included? @@ -66,22 +107,7 @@ async function getLocation(fhirPatient: fhir4.Patient) { return match[1]; } else { // if not, query by name - const query: CouchDBQuery = { - selector: { - type: "contact", - name: addressValue - }, - fields: ['place_id'] - } - const location = await queryCht(query); - - // edge cases can result in more than one location, get first matching - // if not found by name, no more we can do, give up - if (!location.data?.docs || location.data.docs.length == 0){ - return ''; - } else { - return location.data.docs[0].place_id; - } + return getPlaceIdFromCouch(addressValue); } } @@ -107,7 +133,7 @@ export async function createChtPatient(fhirPatient: fhir4.Patient) { cht_patient._meta = { form: "openmrs_patient" } - const location_id = await getLocation(fhirPatient); + const location_id = await getLocationFromOpenMRSPatient(fhirPatient); cht_patient.location_id = location_id; return chtRecordsApi(cht_patient); @@ -125,7 +151,7 @@ export async function chtRecordsApi(doc: any) { return { status: res?.status, data: res?.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } @@ -136,7 +162,7 @@ export async function getChtDocumentById(doc_id: string) { return { status: res?.status, data: res?.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } @@ -147,7 +173,7 @@ export async function queryCht(query: any) { return { status: res?.status, data: res?.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } diff --git a/mediator/src/utils/fhir.ts b/mediator/src/utils/fhir.ts index 8a7547f3..28fad9b6 100644 --- a/mediator/src/utils/fhir.ts +++ b/mediator/src/utils/fhir.ts @@ -110,7 +110,7 @@ export async function getFHIRPatientResource(patientId: string) { return { status: res?.status, data: res?.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } @@ -161,32 +161,64 @@ export async function deleteFhirSubscription(id?: string) { export async function createFhirResource(doc: fhir4.Resource) { try { const res = await axios.post(`${FHIR.url}/${doc.resourceType}`, doc, 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 }; + return { status: error.response?.status, data: error.response?.data }; } } export async function updateFhirResource(doc: fhir4.Resource) { try { const res = await axios.put(`${FHIR.url}/${doc.resourceType}/${doc.id}`, doc, axiosOptions); - return { status: res?.status, data: res?.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } export async function getFhirResourcesSince(lastUpdated: Date, resourceType: string) { + return getResourcesSince(FHIR.url, lastUpdated, resourceType); +} + +/* + * get the "next" url from a fhir paginated response and a base url +*/ +function getNextUrl(url: string, pagination: any) { + let nextUrl = ''; + const nextLink = pagination.link?.find((link: any) => link.relation === 'next'); + if (nextLink?.url) { + const qs = nextLink.url.split('?')[1]; + nextUrl = `${url}/?${qs}`; + } + return nextUrl; +} + +/* + * Gets the full url for a resource type, given base url + * For some resource types, it is usefult o get related resources + * This function returns the full url including include clauses + * currently it is only for encounters, to include observations + * and the subject patient +*/ +function getResourceUrl(baseUrl: string, lastUpdated: Date, resourceType: string) { + let url = `${baseUrl}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`; + // for encounters, include related resources + if (resourceType === 'Encounter') { + url = url + '&_revinclude=Observation:encounter&_include=Encounter:patient'; + } + return url +} + +/* + * get resources of a given type from url, where lastUpdated is > the given data + * if results are paginated, goes through all pages +*/ +export async function getResourcesSince(url: string, lastUpdated: Date, resourceType: string) { try { - let nextUrl = `${FHIR.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`; let results: fhir4.Resource[] = []; - // for encounters, include related resources - if (resourceType === 'Encounter') { - nextUrl = nextUrl + '&_revinclude=Observation:encounter&_include=Encounter:patient'; - } + let nextUrl = getResourceUrl(url, lastUpdated, resourceType); while (nextUrl) { const res = await axios.get(nextUrl, axiosOptions); @@ -195,17 +227,12 @@ export async function getFhirResourcesSince(lastUpdated: Date, resourceType: str results = results.concat(res.data.entry.map((entry: any) => entry.resource)); } - const nextLink = res.data.link && res.data.link.find((link: any) => link.relation === 'next'); - nextUrl = nextLink ? nextLink.url : null; - if (nextUrl) { - const qs = nextUrl.split('?')[1]; - nextUrl = `${FHIR.url}/?${qs}`; - } + nextUrl = getNextUrl(url, res.data); } return { status: 200, data: results }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } @@ -218,19 +245,6 @@ export async function getFhirResource(id: string, resourceType: string) { return { status: res?.status, data: res?.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; - } -} - -export async function getQuestionnaire(name: string){ - try { - const res = await axios.get( - `${FHIR.url}/Questionnaire`, - axiosOptions - ); - return { status: res?.status, data: res?.data }; - } catch (error: any) { - logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } diff --git a/mediator/src/utils/openmrs.ts b/mediator/src/utils/openmrs.ts index 6b404901..5bc87bc3 100644 --- a/mediator/src/utils/openmrs.ts +++ b/mediator/src/utils/openmrs.ts @@ -2,6 +2,7 @@ import { OPENMRS } from '../../config'; import axios from 'axios'; import { logger } from '../../logger'; import https from 'https'; +import { getResourcesSince } from './fhir'; const axiosOptions = { auth: { @@ -14,41 +15,8 @@ const axiosOptions = { timeout: OPENMRS.timeout }; -export async function getOpenMRSPatientResource(patientId: string) { - return await axios.get( - `${OPENMRS.url}/Patient/?identifier=${patientId}`, - axiosOptions - ); -} - export async function getOpenMRSResourcesSince(lastUpdated: Date, resourceType: string) { - try { - let nextUrl = `${OPENMRS.url}/${resourceType}/?_lastUpdated=gt${lastUpdated.toISOString()}`; - let results: fhir4.Resource[] = []; - // for encounters, include related resources - if (resourceType === 'Encounter') { - nextUrl = nextUrl + '&_revinclude=Observation:encounter&_include=Encounter:patient'; - } - - while (nextUrl) { - const res = await axios.get(nextUrl, axiosOptions); - - if (res.data.entry){ - results = results.concat(res.data.entry.map((entry: any) => entry.resource)); - } - - const nextLink = res.data.link && res.data.link.find((link: any) => link.relation === 'next'); - nextUrl = nextLink ? nextLink.url : null; - if (nextUrl) { - const qs = nextUrl.split('?')[1]; - nextUrl = `${OPENMRS.url}/?${qs}`; - } - } - return { status: 200, data: results }; - } catch (error: any) { - logger.error(error); - return { status: error.status, data: error.data }; - } + return getResourcesSince(OPENMRS.url, lastUpdated, resourceType); } export async function createOpenMRSResource(doc: fhir4.Resource) { @@ -57,7 +25,7 @@ export async function createOpenMRSResource(doc: fhir4.Resource) { return { status: res.status, data: res.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } @@ -67,6 +35,6 @@ export async function updateOpenMRSResource(doc: fhir4.Resource) { return { status: res.status, data: res.data }; } catch (error: any) { logger.error(error); - return { status: error.status, data: error.data }; + return { status: error.response?.status, data: error.response?.data }; } } diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index 81c2d715..585b3e03 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -174,7 +174,7 @@ export async function syncPatients(startTime: Date){ /* Get a patient from a list of resources, by an encounters subject reference */ -function getPatient(encounter: fhir4.Encounter, references: fhir4.Resource[]): fhir4.Patient { +export function getPatient(encounter: fhir4.Encounter, references: fhir4.Resource[]): fhir4.Patient { return references.filter((resource) => { return resource.resourceType === 'Patient' && `Patient/${resource.id}` === encounter.subject?.reference })[0] as fhir4.Patient; @@ -184,7 +184,7 @@ function getPatient(encounter: fhir4.Encounter, references: fhir4.Resource[]): f Get a list of observations from a list of resources where the observations encounter reference is the encounter */ -function getObservations(encounter: fhir4.Encounter, references: fhir4.Resource[]): fhir4.Observation[] { +export function getObservations(encounter: fhir4.Encounter, references: fhir4.Resource[]): fhir4.Observation[] { return references.filter((resource) => { if (resource.resourceType === 'Observation') { const observation = resource as fhir4.Observation; @@ -201,7 +201,7 @@ function getObservations(encounter: fhir4.Encounter, references: fhir4.Resource[ Updates the OpenMRS Id on the CHT encounter to the VisitNote Sends Observations for the visitNote Encounter */ -async function sendEncounterToOpenMRS( +export async function sendEncounterToOpenMRS( encounter: fhir4.Encounter, references: fhir4.Resource[] ) { @@ -211,34 +211,46 @@ async function sendEncounterToOpenMRS( } logger.info(`Sending Encounter ${encounter.id} to OpenMRS`); + const patient = getPatient(encounter, references); const observations = getObservations(encounter, references); const patientId = getIdType(patient, openMRSIdentifierType); const openMRSVisit = buildOpenMRSVisit(patientId, encounter); + const visitResponse = await createOpenMRSResource(openMRSVisit[0]); - if (visitResponse.status == 200 || visitResponse.status == 201) { - const visitNoteResponse = await createOpenMRSResource(openMRSVisit[1]); - if (visitNoteResponse.status == 200 || visitNoteResponse.status == 201) { - const visitNote = visitNoteResponse.data as fhir4.Encounter; - // save openmrs id on orignal encounter - logger.info(`Updating Encounter ${patient.id} with openMRSId ${visitNote.id}`); - copyIdToNamedIdentifier(visitNote, encounter, openMRSIdentifierType); - addSourceMeta(visitNote, chtSource); - await updateFhirResource(encounter); - observations.forEach((observation) => { - logger.info(`Sending Observation ${observation.code!.coding![0]!.code} to OpenMRS`); - const openMRSObservation = buildOpenMRSObservation(observation, patientId, visitNote.id || ''); - createOpenMRSResource(openMRSObservation); - }); - } + if (visitResponse.status != 201) { + logger.error(`Error saving visit to OpenMRS ${encounter.id}: ${visitResponse.status}`); + return } + + const visitNoteResponse = await createOpenMRSResource(openMRSVisit[1]); + if (visitNoteResponse.status != 201) { + logger.error(`Error saving visit note to OpenMRS ${encounter.id}: ${visitNoteResponse.status}`); + return + } + + const visitNote = visitNoteResponse.data as fhir4.Encounter; + + logger.info(`Updating Encounter ${encounter.id} with openMRSId ${visitNote.id}`); + + // save openmrs id on orignal encounter + copyIdToNamedIdentifier(visitNote, encounter, openMRSIdentifierType); + addSourceMeta(visitNote, chtSource); + + await updateFhirResource(encounter); + + observations.forEach((observation) => { + logger.info(`Sending Observation ${observation.code!.coding![0]!.code} to OpenMRS`); + const openMRSObservation = buildOpenMRSObservation(observation, patientId, visitNote.id || ''); + createOpenMRSResource(openMRSObservation); + }); } /* Send Observation from OpenMRS to FHIR Replacing the subject reference */ -async function sendObservationToFhir(observation: fhir4.Observation, patient: fhir4.Patient) { +export async function sendObservationToFhir(observation: fhir4.Observation, patient: fhir4.Patient) { logger.info(`Sending Observation ${observation.code!.coding![0]!.code} to FHIR`); replaceReference(observation, 'subject', patient); createFhirResource(observation); @@ -251,7 +263,7 @@ async function sendObservationToFhir(observation: fhir4.Observation, patient: fh If this encounter matches a CHT form, gathers observations and sends them to CHT */ -async function sendEncounterToFhir( +export async function sendEncounterToFhir( encounter: fhir4.Encounter, references: fhir4.Resource[] ) { @@ -259,45 +271,69 @@ async function sendEncounterToFhir( logger.error(`Not re-sending encounter from cht ${encounter.id}`); return } + if (!encounter.period?.end) { logger.error(`Not sending encounter which is incomplete ${encounter.id}`); return } - + logger.info(`Sending Encounter ${encounter.id} to FHIR`); - const patient = getPatient(encounter, references); + const observations = getObservations(encounter, references); - if (patient && patient.id) { - // get patient from FHIR to resolve reference - const patientResponse = await getFHIRPatientResource(patient.id); - if (patientResponse.status == 200 || patientResponse.status == 201) { - const existingPatient = patientResponse.data?.entry[0].resource; - copyIdToNamedIdentifier(encounter, encounter, openMRSIdentifierType); - addSourceMeta(encounter, openMRSSource); - - logger.info(`Replacing ${encounter.subject!.reference} with ${patient.id} for ${encounter.id}`); - replaceReference(encounter, 'subject', existingPatient); - - // remove unused references - delete encounter.participant; - delete encounter.location; - - const response = await updateFhirResource(encounter); - if (response.status == 200 || response.status == 201) { - observations.forEach(o => sendObservationToFhir(o, existingPatient)); - - logger.info(`Sending Encounter ${encounter.id} to CHT`); - const chtResponse = await chtRecordFromObservations(existingPatient.id, observations); - if (chtResponse.status == 200) { - const chtId = chtResponse.data.id; - addId(encounter, chtDocumentIdentifierType, chtId) - await updateFhirResource(encounter); - } - } - } - } else { + + const patient = getPatient(encounter, references); + if (!patient?.id) { logger.error(`Patient ${encounter.subject!.reference} not found for ${encounter.id}`); + return + } + + // get patient from FHIR to resolve reference + const patientResponse = await getFHIRPatientResource(patient.id); + if (patientResponse.status != 200) { + logger.error(`Error getting Patient ${patient.id}: ${patientResponse.status}`); + return } + + const existingPatient = patientResponse.data?.entry[0].resource; + copyIdToNamedIdentifier(encounter, encounter, openMRSIdentifierType); + addSourceMeta(encounter, openMRSSource); + + logger.info(`Replacing ${encounter.subject!.reference} with ${patient.id} for ${encounter.id}`); + replaceReference(encounter, 'subject', existingPatient); + + // remove unused references + delete encounter.participant; + delete encounter.location; + + const response = await updateFhirResource(encounter); + if (response.status != 201) { + logger.error(`Error saving encounter to fhir ${encounter.id}: ${response.status}`); + return + } + + observations.forEach(o => sendObservationToFhir(o, existingPatient)); + + sendEncounterToCht(encounter, existingPatient, observations); +} + +/* + Send an Encounter from OpenMRS to CHT +*/ +export async function sendEncounterToCht( + encounter: fhir4.Encounter, + patient: fhir4.Patient, + observations: fhir4.Observation[] +) { + logger.info(`Sending Encounter ${encounter.id} to CHT`); + const chtResponse = await chtRecordFromObservations(patient, observations); + if (chtResponse.status != 200) { + logger.error(`Error saving encounter to cht ${encounter.id}: ${chtResponse.status}`); + return + } + + const chtId = chtResponse.data.id; + addId(encounter, chtDocumentIdentifierType, chtId); + await updateFhirResource(encounter); } /* diff --git a/mediator/src/utils/tests/cht.spec.ts b/mediator/src/utils/tests/cht.spec.ts index 7f99b3a5..205fbdbe 100644 --- a/mediator/src/utils/tests/cht.spec.ts +++ b/mediator/src/utils/tests/cht.spec.ts @@ -1,7 +1,15 @@ -import { createChtFollowUpRecord, generateChtRecordsApiUrl } from '../cht'; +import { + createChtFollowUpRecord, + generateChtRecordsApiUrl, + getLocationFromOpenMRSPatient, + queryCht } from '../cht'; import axios from 'axios'; +import { logger } from '../../../logger'; +import { OpenMRSPatientFactory } from '../../middlewares/schemas/tests/openmrs-resource-factories'; +import { mockQueryCht } from '../../controllers/tests/utils'; jest.mock('axios'); +jest.mock('../../../logger'); const mockAxios = axios as jest.Mocked; @@ -36,4 +44,86 @@ describe('CHT Utils', () => { expect(res).toContain(`${username}:${password}`); }); }); + + describe('getLocationFromOpenMRSPatient', () => { + it('should return place ID if address contains place ID', async () => { + const fhirPatient = OpenMRSPatientFactory.build({}, { + addressKey: 'address4', + addressValue: 'FCHV Area [12345]' + }); + + const result = await getLocationFromOpenMRSPatient(fhirPatient as any); + + expect(result).toBe('12345'); + }); + + it('should return an empty string if no address or place ID is found', async () => { + const fhirPatient = OpenMRSPatientFactory.build({}, { + addressKey: 'address4', + addressValue: 'Unknown Area' + }); + + mockQueryCht.mockResolvedValue({ status: 200, data: { docs: [] } }); // Simulating no result from the query + + const result = await getLocationFromOpenMRSPatient(fhirPatient as any); + + expect(result).toBe(''); + }); + + it('should return address5 if address4 is not available', async () => { + const fhirPatient = OpenMRSPatientFactory.build({}, { + addressKey: 'address5', + addressValue: 'Health Center [54321]' + }); + + const result = await getLocationFromOpenMRSPatient(fhirPatient as any); + + expect(result).toBe('54321'); + }); + + it('should handle error cases by returning an empty string when query fails', async () => { + const fhirPatient = OpenMRSPatientFactory.build({}, { + addressKey: 'address4', + addressValue: 'Unknown Location' + }); + + mockQueryCht.mockRejectedValue(new Error('Database query failed')); + + const result = await getLocationFromOpenMRSPatient(fhirPatient as any); + + expect(result).toBe(''); + }); + }); + + describe('queryCHT', () => { + it('should return data when the query is successful', async () => { + const mockQuery = { selector: { type: 'contact' } }; + const mockResponse = { status: 200, data: { docs: [{ place_id: '12345' }] } }; + + mockAxios.post.mockResolvedValue(mockResponse); // Simulate a successful response + + const result = await queryCht(mockQuery); + + expect(mockAxios.post).toHaveBeenCalledWith(expect.stringContaining('_find'), mockQuery, expect.anything()); + expect(result).toEqual(mockResponse); + }); + + it('should log an error and return error.response.data when the query fails', async () => { + const mockQuery = { selector: { type: 'contact' } }; + const mockError = { + response: { status: 500, data: 'Internal Server Error' } + } + + mockAxios.post.mockRejectedValue(mockError); // Simulate an error response + const loggerErrorSpy = jest.spyOn(logger, 'error'); // Spy on the logger's error method + + const result = await queryCht(mockQuery); + + expect(loggerErrorSpy).toHaveBeenCalledWith(mockError); + expect(result).toEqual({ + status: mockError.response.status, + data: mockError.response.data + }); + }); + }); }); diff --git a/mediator/src/utils/tests/fhir.spec.ts b/mediator/src/utils/tests/fhir.spec.ts index 63854743..a619327b 100644 --- a/mediator/src/utils/tests/fhir.spec.ts +++ b/mediator/src/utils/tests/fhir.spec.ts @@ -1,5 +1,8 @@ import { logger } from '../../../logger'; -import { EncounterFactory } from '../../middlewares/schemas/tests/fhir-resource-factories'; +import { + EncounterFactory, + PatientFactory +} from '../../middlewares/schemas/tests/fhir-resource-factories'; import { createFHIRSubscriptionResource, createFhirResource, @@ -7,8 +10,11 @@ import { generateFHIRSubscriptionResource, getFHIROrgEndpointResource, getFHIRPatientResource, + getFhirResourcesSince, + addId } from '../fhir'; import axios from 'axios'; +import { FHIR } from '../../../config'; jest.mock('axios'); jest.mock('../../../logger'); @@ -201,7 +207,9 @@ describe('FHIR Utils', () => { }); it('should return an error if the FHIR server returns an error', async () => { - const data = { status: 400, data: { message: 'Bad request' } }; + const data = { + response: { status: 400, data: { message: 'Bad request' } } + }; mockAxios.post = jest.fn().mockRejectedValue(data); @@ -211,8 +219,128 @@ describe('FHIR Utils', () => { expect(mockAxios.post.mock.calls[0][0]).toContain(resourceType); expect(mockAxios.post.mock.calls[0][1]).toEqual({...encounter, resourceType}); expect(res.status).toEqual(400); - expect(res.data).toEqual(data.data); + expect(res.data).toEqual(data.response.data); expect(logger.error).toBeCalledTimes(1); }); }); + + describe('addIds', () => { + it('should add ids to a fhir patient', () => { + const patient = PatientFactory.build(); + const idType = { coding: [{ code: 'OpenMRS ID' }] }; + const value = '12345'; + + const result = addId(patient, idType, value); + + expect(result.identifier).toBeDefined(); + // patient has one idenditifer already, so afterwards, should be 2 + expect(result.identifier?.length).toBe(2); + // and the one we are checking is the second one + expect(result.identifier?.[1]).toEqual({ + type: idType, + value: value + }); + }); + }); + + describe('getFhirResourcesSince', () => { + it('should fetch FHIR resources successfully', async () => { + const lastUpdated = new Date('2023-01-01T00:00:00Z'); + const resourceType = 'Patient'; + const mockResponse = { + data: { + entry: [ + { resource: { id: '123', resourceType: 'Patient' } } + ], + link: [] + } + }; + mockAxios.get.mockResolvedValue(mockResponse); + + const result = await getFhirResourcesSince(lastUpdated, resourceType); + + expect(mockAxios.get).toHaveBeenCalledWith( + `${FHIR.url}/Patient/?_lastUpdated=gt2023-01-01T00:00:00.000Z`, + expect.anything() // axiosOptions + ); + expect(result.status).toBe(200); + expect(result.data).toEqual([{ id: '123', resourceType: 'Patient' }]); + }); + + it('should include related resources for encounters', async () => { + const lastUpdated = new Date('2023-01-01T00:00:00Z'); + const resourceType = 'Encounter'; + const mockResponse = { + data: { + entry: [ + { resource: { id: 'enc-123', resourceType: 'Encounter' } } + ], + link: [] + } + }; + mockAxios.get.mockResolvedValue(mockResponse); + + const result = await getFhirResourcesSince(lastUpdated, resourceType); + + expect(mockAxios.get).toHaveBeenCalledWith( + `${FHIR.url}/Encounter/?_lastUpdated=gt2023-01-01T00:00:00.000Z&_revinclude=Observation:encounter&_include=Encounter:patient`, + expect.anything() // axiosOptions + ); + expect(result.status).toBe(200); + expect(result.data).toEqual([{ id: 'enc-123', resourceType: 'Encounter' }]); + }); + + it('should handle pagination', async () => { + const lastUpdated = new Date('2023-01-01T00:00:00Z'); + const resourceType = 'Patient'; + const mockFirstPageResponse = { + data: { + entry: [ + { resource: { id: '123', resourceType: 'Patient' } } + ], + link: [ + { relation: 'next', url: `${FHIR.url}/Patient/?page=2` } + ] + } + }; + const mockSecondPageResponse = { + data: { + entry: [ + { resource: { id: '124', resourceType: 'Patient' } } + ], + link: [] + } + }; + mockAxios.get + .mockResolvedValueOnce(mockFirstPageResponse) + .mockResolvedValueOnce(mockSecondPageResponse); + + const result = await getFhirResourcesSince(lastUpdated, resourceType); + + expect(mockAxios.get).toHaveBeenCalledTimes(2); + expect(result.status).toBe(200); + expect(result.data).toEqual([ + { id: '123', resourceType: 'Patient' }, + { id: '124', resourceType: 'Patient' } + ]); + }); + + it('should return an error if the request fails', async () => { + const lastUpdated = new Date('2023-01-01T00:00:00Z'); + const resourceType = 'Patient'; + const mockError = { + response: { + status: 500, + data: 'Internal Server Error' + } + }; + mockAxios.get.mockRejectedValue(mockError); + + const result = await getFhirResourcesSince(lastUpdated, resourceType); + + expect(logger.error).toHaveBeenCalledWith(mockError); + expect(result.status).toBe(500); + expect(result.data).toBe('Internal Server Error'); + }); + }); }); diff --git a/mediator/src/utils/tests/openmrs.spec.ts b/mediator/src/utils/tests/openmrs.spec.ts new file mode 100644 index 00000000..1a40447c --- /dev/null +++ b/mediator/src/utils/tests/openmrs.spec.ts @@ -0,0 +1,82 @@ +import axios from 'axios'; +import { createOpenMRSResource, updateOpenMRSResource, getOpenMRSResourcesSince } from '../openmrs'; +import { logger } from '../../../logger'; +import { OPENMRS } from '../../../config'; + +jest.mock('axios'); +jest.mock('../../../logger'); + +describe('OpenMRS utility functions', () => { + const mockAxiosGet = axios.get as jest.Mock; + const mockAxiosPost = axios.post as jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createOpenMRSResource', () => { + it('should create a new OpenMRS resource', async () => { + const mockResource = { id: '456', resourceType: 'Patient' }; + const mockResponse = { status: 201, data: mockResource }; + mockAxiosPost.mockResolvedValue(mockResponse); + + const result = await createOpenMRSResource(mockResource); + + expect(mockAxiosPost).toHaveBeenCalledWith( + `${OPENMRS.url}/Patient`, + mockResource, + expect.anything() // axiosOptions + ); + expect(result).toEqual({ status: 201, data: mockResource }); + }); + + it('should handle errors when creating a resource', async () => { + const mockResource = { id: '456', resourceType: 'Patient' }; + const mockError = { + response: { status: 500, data: 'Internal Server Error' } + }; + mockAxiosPost.mockRejectedValue(mockError); + + const result = await createOpenMRSResource(mockResource); + + expect(logger.error).toHaveBeenCalledWith(mockError); + expect(result).toEqual({ + status: mockError.response.status, + data: mockError.response.data + }); + }); + }); + + describe('updateOpenMRSResource', () => { + it('should update an existing OpenMRS resource', async () => { + const mockResource = { id: '456', resourceType: 'Patient' }; + const mockResponse = { status: 200, data: mockResource }; + mockAxiosPost.mockResolvedValue(mockResponse); + + const result = await updateOpenMRSResource(mockResource); + + expect(mockAxiosPost).toHaveBeenCalledWith( + `${OPENMRS.url}/Patient/456`, + mockResource, + expect.anything() // axiosOptions + ); + expect(result).toEqual({ status: 200, data: mockResource }); + }); + + it('should handle errors when updating a resource', async () => { + const mockResource = { id: '456', resourceType: 'Patient' }; + const mockError = { + response: { status: 500, data: 'Internal Server Error' } + }; + mockAxiosPost.mockRejectedValue(mockError); + + const result = await updateOpenMRSResource(mockResource); + + expect(logger.error).toHaveBeenCalledWith(mockError); + expect(result).toEqual({ + status: mockError.response.status, + data: mockError.response.data + }); + }); + }); +}); diff --git a/mediator/src/utils/tests/openmrs_sync.spec.ts b/mediator/src/utils/tests/openmrs_sync.spec.ts index 765c68b4..8f76eeae 100644 --- a/mediator/src/utils/tests/openmrs_sync.spec.ts +++ b/mediator/src/utils/tests/openmrs_sync.spec.ts @@ -1,149 +1,261 @@ -import { compare, syncPatients, syncEncounters } from '../openmrs_sync'; +import { + compare, + syncPatients, + syncEncounters, + getPatient +} from '../openmrs_sync'; import * as fhir from '../fhir'; import * as openmrs from '../openmrs'; import * as cht from '../cht'; -import { PatientFactory } from '../../middlewares/schemas/tests/fhir-resource-factories'; +import { PatientFactory, EncounterFactory, ObservationFactory } from '../../middlewares/schemas/tests/fhir-resource-factories'; +import { visitType, visitNoteType } from '../../mappers/openmrs'; +import { chtDocumentIdentifierType, chtPatientIdentifierType } from '../../mappers/cht'; +import { getIdType } from '../../utils/fhir'; import axios from 'axios'; +import { logger } from '../../../logger'; jest.mock('axios'); +jest.mock('../../../logger'); describe('OpenMRS Sync', () => { - it('compares resources with the gvien key', async () => { - const lastUpdated = new Date(); - lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); - - const constants = { - resourceType: 'Patient', - meta: { lastUpdated: lastUpdated } - } - - jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ - data: [ - { id: 'outgoing', ...constants }, - { id: 'toupdate', ...constants } - ], - status: 200, - }); - jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ - data: [ - { id: 'incoming', ...constants }, - { id: 'toupdate', ...constants } - ], - status: 200, - }); + describe('compare', () => { + it('compares resources with the gvien key', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); - const getKey = (obj: any) => { return obj.id }; - const startTime = new Date(); - startTime.setHours(startTime.getHours() - 1); - const comparison = await compare(getKey, 'Patient', startTime) + const constants = { + resourceType: 'Patient', + meta: { lastUpdated: lastUpdated } + } - expect(comparison.incoming).toEqual([{id: 'incoming', ...constants }]); - expect(comparison.outgoing).toEqual([{id: 'outgoing', ...constants }]); - expect(comparison.toupdate).toEqual([{id: 'toupdate', ...constants }]); + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: [ + { id: 'outgoing', ...constants }, + { id: 'toupdate', ...constants } + ], + status: 200, + }); + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: [ + { id: 'incoming', ...constants }, + { id: 'toupdate', ...constants } + ], + status: 200, + }); - expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); - expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); - }); + const getKey = (obj: any) => { return obj.id }; + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await compare(getKey, 'Patient', startTime) - it('loads references for related resources', async () => { - const lastUpdated = new Date(); - lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); - const reference = { - id: 'reference0', - resourceType: 'Patient', - meta: { lastUpdated: lastUpdated } - }; - const resource = { - id: 'resource0', - resourceType: 'Encounter', - meta: { lastUpdated: lastUpdated } - }; - - jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ - data: [ resource, reference ], - status: 200, - }); - jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ - data: [ resource ], - status: 200, + expect(comparison.incoming).toEqual([{id: 'incoming', ...constants }]); + expect(comparison.outgoing).toEqual([{id: 'outgoing', ...constants }]); + expect(comparison.toupdate).toEqual([{id: 'toupdate', ...constants }]); + + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); }); - const getKey = (obj: any) => { return obj.id }; - const startTime = new Date(); - startTime.setHours(startTime.getHours() - 1); - const comparison = await compare(getKey, 'Encounter', startTime) + it('loads references for related resources', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); + const reference = { + id: 'reference0', + resourceType: 'Patient', + meta: { lastUpdated: lastUpdated } + }; + const resource = { + id: 'resource0', + resourceType: 'Encounter', + meta: { lastUpdated: lastUpdated } + }; - expect(comparison.references).toContainEqual(reference); - expect(comparison.toupdate).toEqual([resource]); + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: [ resource, reference ], + status: 200, + }); + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: [ resource ], + status: 200, + }); - expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); - expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); - }); + const getKey = (obj: any) => { return obj.id }; + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await compare(getKey, 'Encounter', startTime) - it('sends incoming Patients to FHIR and CHT', async () => { - const lastUpdated = new Date(); - lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); + expect(comparison.references).toContainEqual(reference); + expect(comparison.toupdate).toEqual([resource]); - const openMRSPatient = PatientFactory.build(); - openMRSPatient.meta = { lastUpdated: lastUpdated }; - jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ - data: [], - status: 200, - }); - jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ - data: [openMRSPatient], - status: 200, + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); }); - jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ - data: openMRSPatient, - status: 200 + }); + + describe('syncPatients', () => { + it('sends incoming Patients to FHIR and CHT', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); + + const openMRSPatient = PatientFactory.build(); + openMRSPatient.meta = { lastUpdated: lastUpdated }; + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: [], + status: 200, + }); + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: [openMRSPatient], + status: 200, + }); + jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ + data: openMRSPatient, + status: 201 + }); + jest.spyOn(cht, 'createChtPatient') + + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await syncPatients(startTime); + + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); + + expect(fhir.updateFhirResource).toHaveBeenCalledWith(openMRSPatient); + expect(cht.createChtPatient).toHaveBeenCalledWith(openMRSPatient); }); - jest.spyOn(cht, 'createChtPatient') - const getKey = (obj: any) => { return obj.id }; - const startTime = new Date(); - startTime.setHours(startTime.getHours() - 1); - const comparison = await syncPatients(startTime); + it('sends outgoing Patients to OpenMRS', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); - expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); - expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); + const fhirPatient = PatientFactory.build(); + fhirPatient.meta = { lastUpdated: lastUpdated }; - expect(fhir.updateFhirResource).toHaveBeenCalledWith(openMRSPatient); - expect(cht.createChtPatient).toHaveBeenCalledWith(openMRSPatient); - }); + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: [fhirPatient], + status: 200, + }); + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: [], + status: 200, + }); + jest.spyOn(openmrs, 'createOpenMRSResource').mockResolvedValueOnce({ + data: fhirPatient, + status: 201 + }); + jest.spyOn(fhir, 'updateFhirResource') - it('sends outgoing Patients to OpenMRS', async () => { - const lastUpdated = new Date(); - lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await syncPatients(startTime); - const fhirPatient = PatientFactory.build(); - fhirPatient.meta = { lastUpdated: lastUpdated }; + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); - jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ - data: [fhirPatient], - status: 200, - }); - jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ - data: [], - status: 200, + expect(openmrs.createOpenMRSResource).toHaveBeenCalledWith(fhirPatient); + // updating with openmrs id + expect(fhir.updateFhirResource).toHaveBeenCalledWith(fhirPatient); }); - jest.spyOn(openmrs, 'createOpenMRSResource').mockResolvedValueOnce({ - data: fhirPatient, - status: 200 + }); + describe('syncEncounters', () => { + it('sends incoming Encounters to FHIR and CHT', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); + + const openMRSPatient = PatientFactory.build(); + openMRSPatient.meta = { lastUpdated: lastUpdated }; + const openMRSEncounter = EncounterFactory.build(); + openMRSEncounter.meta = { lastUpdated: lastUpdated }; + openMRSEncounter.subject = { + reference: `Patient/${openMRSPatient.id}` + }; + const openMRSObservation = ObservationFactory.build(); + openMRSObservation.encounter = { reference: 'Encounter/' + openMRSEncounter.id } + + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: [], + status: 200, + }); + + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: [openMRSEncounter, openMRSPatient, openMRSObservation], + status: 200, + }); + + jest.spyOn(fhir, 'getFHIRPatientResource').mockResolvedValueOnce({ + data: { entry: [{ resource: openMRSPatient }] }, + status: 200, + }); + + jest.spyOn(fhir, 'updateFhirResource').mockResolvedValue({ + data: [], + status: 201, + }); + + jest.spyOn(fhir, 'createFhirResource') + + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await syncEncounters(startTime); + + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); + + expect(fhir.updateFhirResource).toHaveBeenCalledWith(openMRSEncounter); + expect(fhir.createFhirResource).toHaveBeenCalledWith(openMRSObservation); }); - //jest.spyOn(fhir, 'updateFhirResource') - const getKey = (obj: any) => { return obj.id }; - const startTime = new Date(); - startTime.setHours(startTime.getHours() - 1); - const comparison = await syncPatients(startTime); + it('sends outgoing Encounters to OpenMRS', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); - expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); - expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); + const fhirEncounter = EncounterFactory.build(); + fhirEncounter.meta = { lastUpdated: lastUpdated }; + const fhirObservation = ObservationFactory.build(); + fhirObservation.encounter = { reference: 'Encounter/' + fhirEncounter.id } + const chtDocId = { + system: "cht", + type: chtDocumentIdentifierType, + value: getIdType(fhirEncounter, chtDocumentIdentifierType) + } - expect(openmrs.createOpenMRSResource).toHaveBeenCalledWith(fhirPatient); - // updating with openmrs id - //expect(fhir.updateFhirResource).toHaveBeenCalledWith(fhirPatient); + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: [fhirEncounter, fhirObservation], + status: 200, + }); + + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: [], + status: 200, + }); + + jest.spyOn(openmrs, 'createOpenMRSResource').mockResolvedValue({ + data: [], + status: 201, + }); + + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await syncEncounters(startTime); + + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); + + expect(openmrs.createOpenMRSResource).toHaveBeenCalledWith( + expect.objectContaining({ + "type": expect.arrayContaining([visitType]), + "identifier": expect.arrayContaining([chtDocId]) + }) + ); + + expect(openmrs.createOpenMRSResource).toHaveBeenCalledWith( + expect.objectContaining({ + "type": expect.arrayContaining([visitNoteType]), + }) + ); + + expect(openmrs.createOpenMRSResource).toHaveBeenCalledWith(fhirObservation); + }); }); }); From d5b02529904ea80b18e152cafb6c839eab58b18b Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Wed, 23 Oct 2024 14:00:34 +0300 Subject: [PATCH 44/67] fix: fixing startup script --- startup.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/startup.sh b/startup.sh index 337484df..f7a3f417 100755 --- a/startup.sh +++ b/startup.sh @@ -2,11 +2,11 @@ if [ "$1" == "init" ]; then # start up docker containers - docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -d --build + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml up -d --build elif [ "$1" == "up" ]; then - docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -d + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml up -d elif [ "$1" == "up-dev" ]; then - docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -d --build + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml up -d --build elif [ "$1" == "down" ]; then docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.openmrs.yml stop elif [ "$1" == "destroy" ]; then From ef558a2a06edf42571c28429dc8e513230924c15 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Wed, 23 Oct 2024 14:02:57 +0300 Subject: [PATCH 45/67] chore(#136): skipping e2e-tests until rate limiting is fixed --- .github/workflows/test.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7e249cbf..eac0b1f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,6 @@ name: Test -on: [push, pull_request] +on: [push] jobs: unit: @@ -20,6 +20,7 @@ jobs: e2e-tests: name: E2E Tests runs-on: ubuntu-latest + if: false steps: - name: Login to Docker Hub From d8cb295a49c830b238862efa58f7436cdc8c787f Mon Sep 17 00:00:00 2001 From: witash Date: Fri, 25 Oct 2024 15:25:00 +0300 Subject: [PATCH 46/67] feat(#138): move polling to openhim channel config (#139) --- mediator/config/index.ts | 16 ++++++---- mediator/config/openmrs_mediator.ts | 21 ++++++++++++- mediator/index.ts | 23 +++++--------- mediator/src/controllers/openmrs.ts | 18 +++++++++++ mediator/src/routes/openmrs.ts | 12 ++++++++ mediator/src/routes/tests/openmrs.spec.ts | 37 +++++++++++++++++++++++ mediator/src/utils/openmrs_sync.ts | 35 +++++++++++---------- mediator/test/workflows.spec.ts | 9 +++--- 8 files changed, 127 insertions(+), 44 deletions(-) create mode 100644 mediator/src/controllers/openmrs.ts create mode 100644 mediator/src/routes/openmrs.ts create mode 100644 mediator/src/routes/tests/openmrs.spec.ts diff --git a/mediator/config/index.ts b/mediator/config/index.ts index 892e710c..a3c6d1ea 100644 --- a/mediator/config/index.ts +++ b/mediator/config/index.ts @@ -2,6 +2,7 @@ import * as dotenv from 'dotenv'; dotenv.config(); export const PORT = process.env.PORT || 6000; +const REQUEST_TIMEOUT = Number(getEnvironmentVariable('REQUEST_TIMEOUT', '5000')); export const OPENHIM = { username: getEnvironmentVariable('OPENHIM_USERNAME', 'interop@openhim.org'), @@ -11,27 +12,30 @@ export const OPENHIM = { }; export const FHIR = { - url: getEnvironmentVariable('FHIR_URL', 'http://openhim-core:5001/fhir'), + url: getEnvironmentVariable('FHIR_URL', 'https://openhim-core:5001/fhir'), username: getEnvironmentVariable('FHIR_USERNAME', 'interop-client'), password: getEnvironmentVariable('FHIR_PASSWORD', 'interop-password'), - timeout: Number(getEnvironmentVariable('REQUEST_TIMEOUT', '5000')) + timeout: REQUEST_TIMEOUT }; export const CHT = { url: getEnvironmentVariable('CHT_URL', 'https://nginx'), username: getEnvironmentVariable('CHT_USERNAME', 'admin'), password: getEnvironmentVariable('CHT_PASSWORD', 'password'), - timeout: Number(getEnvironmentVariable('REQUEST_TIMEOUT', '5000')) + timeout: REQUEST_TIMEOUT }; export const OPENMRS = { - url: getEnvironmentVariable('OPENMRS_CHANNEL_URL', 'http://openhim-core:5001/openmrs'), + url: getEnvironmentVariable('OPENMRS_CHANNEL_URL', 'https://openhim-core:5001/openmrs'), username: getEnvironmentVariable('OPENMRS_CHANNEL_USERNAME', 'interop-client'), password: getEnvironmentVariable('OPENMRS_CHANNEL_PASSWORD', 'interop-password'), - timeout: Number(getEnvironmentVariable('REQUEST_TIMEOUT', '5000')) + timeout: REQUEST_TIMEOUT }; -export const SYNC_INTERVAL = getEnvironmentVariable('SYNC_INTERVAL', '60000'); +// how often in seconds the sync should run. hardcoded to 1 minute +export const SYNC_INTERVAL = '60'; +// how far back should the sync look for new resources. Defaults to one hour +export const SYNC_PERIOD = getEnvironmentVariable('SYNC_PERIOD', '3600'); function getEnvironmentVariable(env: string, def: string) { if (process.env.NODE_ENV === 'test') { diff --git a/mediator/config/openmrs_mediator.ts b/mediator/config/openmrs_mediator.ts index 968cced7..829878b3 100644 --- a/mediator/config/openmrs_mediator.ts +++ b/mediator/config/openmrs_mediator.ts @@ -3,11 +3,30 @@ export const openMRSMediatorConfig = { version: '1.0.0', name: 'OpenMRS Mediator', description: 'A mediator to sync CHT data with OpenMRS', + defaultChannelConfig: [ + { + name: 'OpenMRS Sync', + urlPattern: '^/trigger$', + routes: [ + { + name: 'OpenMRS polling Mediator', + host: 'mediator', + path: '/openmrs/sync', + port: 6000, + primary: true, + type: 'http', + }, + ], + allow: ['interop'], + type: 'polling', + pollingSchedule: '1 minute' + }, + ], endpoints: [ { name: 'OpenMRS Mediator', host: 'mediator', - path: '/', + path: '/openmrs/sync', port: '6000', primary: true, type: 'http', diff --git a/mediator/index.ts b/mediator/index.ts index 864582fc..4f9eb578 100644 --- a/mediator/index.ts +++ b/mediator/index.ts @@ -3,15 +3,15 @@ import { mediatorConfig } from './config/mediator'; import { openMRSMediatorConfig } from './config/openmrs_mediator'; import { logger } from './logger'; import bodyParser from 'body-parser'; -import {PORT, OPENHIM, SYNC_INTERVAL, OPENMRS} from './config'; +import {PORT, OPENHIM, OPENMRS} from './config'; import patientRoutes from './src/routes/patient'; import serviceRequestRoutes from './src/routes/service-request'; import encounterRoutes from './src/routes/encounter'; import organizationRoutes from './src/routes/organization'; import endpointRoutes from './src/routes/endpoint'; import chtRoutes from './src/routes/cht'; +import openMRSRoutes from './src/routes/openmrs'; import { registerMediatorCallback } from './src/utils/openhim'; -import { syncPatients, syncEncounters } from './src/utils/openmrs_sync' import os from 'os'; const {registerMediator} = require('openhim-mediator-utils'); @@ -21,7 +21,7 @@ const app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({extended: true})); -app.get('*', (_: Request, res: Response) => { +app.get('/', (_: Request, res: Response) => { const osUptime = os.uptime(); const processUptime = process.uptime(); res.send({status: 'success', osuptime: osUptime, processuptime: processUptime}); @@ -34,9 +34,12 @@ app.use('/encounter', encounterRoutes); app.use('/organization', organizationRoutes); app.use('/endpoint', endpointRoutes); -// routes for cht docs +// routes for CHT docs app.use('/cht', chtRoutes); +// routes for OpenMRS +app.use('/openmrs', openMRSRoutes); + if (process.env.NODE_ENV !== 'test') { app.listen(PORT, () => logger.info(`Server listening on port ${PORT}`)); @@ -44,20 +47,8 @@ if (process.env.NODE_ENV !== 'test') { registerMediator(OPENHIM, mediatorConfig, registerMediatorCallback); // if OPENMRS is specified, register its mediator - // and start the sync background task if (OPENMRS.url) { registerMediator(OPENHIM, openMRSMediatorConfig, registerMediatorCallback); - // start patient and ecnounter sync in the background - setInterval(async () => { - try { - const startTime = new Date(); - startTime.setHours(startTime.getHours() - 1); - await syncPatients(startTime); - await syncEncounters(startTime); - } catch (error: any) { - logger.error(error); - } - }, Number(SYNC_INTERVAL)); } } diff --git a/mediator/src/controllers/openmrs.ts b/mediator/src/controllers/openmrs.ts new file mode 100644 index 00000000..9a05b831 --- /dev/null +++ b/mediator/src/controllers/openmrs.ts @@ -0,0 +1,18 @@ +import { logger } from '../../logger'; +import { syncPatients, syncEncounters } from '../utils/openmrs_sync' +import { SYNC_PERIOD } from '../../config' + +export async function sync() { + try { + let now = Date.now(); + let syncPeriod = parseInt(SYNC_PERIOD, 10); + let startTime = new Date(now - syncPeriod); + + await syncPatients(startTime); + await syncEncounters(startTime); + return { status: 200, data: { message: `OpenMRS sync completed successfully`} }; + } catch(error: any) { + logger.error(error); + return { status: 500, data: { message: `Error during OpenMRS Sync`} }; + } +} diff --git a/mediator/src/routes/openmrs.ts b/mediator/src/routes/openmrs.ts new file mode 100644 index 00000000..ac487654 --- /dev/null +++ b/mediator/src/routes/openmrs.ts @@ -0,0 +1,12 @@ +import { Router } from 'express'; +import { requestHandler } from '../utils/request'; +import { sync } from '../controllers/openmrs' + +const router = Router(); + +router.get( + '/sync', + requestHandler((req) => sync()) +); + +export default router; diff --git a/mediator/src/routes/tests/openmrs.spec.ts b/mediator/src/routes/tests/openmrs.spec.ts new file mode 100644 index 00000000..ee7e8b89 --- /dev/null +++ b/mediator/src/routes/tests/openmrs.spec.ts @@ -0,0 +1,37 @@ +import request from 'supertest'; +import app from '../../..'; +import * as openmrs_sync from '../../utils/openmrs_sync'; +import axios from 'axios'; +import { logger } from '../../../logger'; + +jest.mock('axios'); +jest.mock('../../../logger'); + +describe('GET /openmrs/sync', () => { + it('calls syncPatients and syncEncouners', async () => { + jest.spyOn(openmrs_sync, 'syncPatients').mockImplementation(async (startTime) => { + }); + + jest.spyOn(openmrs_sync, 'syncEncounters').mockImplementation(async (startTime) => { + }); + + const res = await request(app).get('/openmrs/sync').send(); + + expect(res.status).toBe(200); + + expect(openmrs_sync.syncPatients).toHaveBeenCalled(); + expect(openmrs_sync.syncEncounters).toHaveBeenCalled(); + }); + + it('returns 500 if syncPatients throws an error', async () => { + jest.spyOn(openmrs_sync, 'syncPatients').mockImplementation(async (startTime) => { + throw new Error('Sync Failed'); + }); + + const res = await request(app).get('/openmrs/sync').send(); + + expect(res.status).toBe(500); + + expect(openmrs_sync.syncPatients).toHaveBeenCalled(); + }); +}); diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index 585b3e03..af8efd6e 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -83,31 +83,34 @@ export async function compare( // get the key for each resource and create a Map const fhirIds = new Map(comparison.fhirResources.map(resource => [getKey(resource), resource])); + function isValidDate(resource: fhir4.Resource) { + // if lastUpdated is missing or invalid, cannot proceed, throw an error + if (!resource.meta?.lastUpdated) { + throw new Error("Last updated missing"); + } + const lastUpdated = new Date(resource.meta.lastUpdated); + if (isNaN(lastUpdated.getTime()) || isNaN(startTime.getTime())) { + throw new Error("Invalid date format"); + } + + // don't sync resources created with 2 * SYNC_INTERVAL of start time + const syncWindow = (Number(SYNC_INTERVAL) * 1000) * 2 + const diff = lastUpdated.getTime() - startTime.getTime(); + return diff > syncWindow; + } + comparison.openMRSResources.forEach((openMRSResource) => { const key = getKey(openMRSResource); if (fhirIds.has(key)) { - // ok so the fhir server already has it results.toupdate.push(openMRSResource); fhirIds.delete(key); - } else { - const lastUpdated = new Date(openMRSResource.meta?.lastUpdated!); - if (isNaN(lastUpdated.getTime()) || isNaN(startTime.getTime())) { - throw new Error("Invalid date format"); - } - const diff = lastUpdated.getTime() - startTime.getTime(); - if (diff > (Number(SYNC_INTERVAL) * 2)){ - results.incoming.push(openMRSResource); - } + } else if (isValidDate(openMRSResource)){ + results.incoming.push(openMRSResource); } }); fhirIds.forEach((resource, key) => { - const lastUpdated = new Date(resource.meta?.lastUpdated || ''); - if (isNaN(lastUpdated.getTime()) || isNaN(startTime.getTime())) { - throw new Error("Invalid date format"); - } - const diff = lastUpdated.getTime() - startTime.getTime(); - if (diff > (Number(SYNC_INTERVAL) * 2)){ + if (isValidDate(resource)) { results.outgoing.push(resource); } }); diff --git a/mediator/test/workflows.spec.ts b/mediator/test/workflows.spec.ts index c060ab33..c76d1600 100644 --- a/mediator/test/workflows.spec.ts +++ b/mediator/test/workflows.spec.ts @@ -166,18 +166,17 @@ describe('Workflows', () => { .auth(FHIR.username, FHIR.password); expect(checkMediatorResponse.status).toBe(200); - expect(checkMediatorResponse.body.status).toBe('success'); - //Create a patient using openMRS api + //TODO: Create a patient using openMRS api - /*const retrieveFhirPatientIdResponse = await request(FHIR.url) + const retrieveFhirPatientIdResponse = await request(FHIR.url) .get('/fhir/Patient/?identifier=' + patientId) .auth(FHIR.username, FHIR.password); expect(retrieveFhirPatientIdResponse.status).toBe(200); - expect(retrieveFhirPatientIdResponse.body.total).toBe(1);*/ + expect(retrieveFhirPatientIdResponse.body.total).toBe(1); - //retrieve and validate patient from CHT api + //TODO: retrieve and validate patient from CHT api //trigger openmrs sync //validate id }); From e3f1a4c3c65578f1cc8ee778be2eeecad78743ad Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Mon, 28 Oct 2024 17:10:00 +0300 Subject: [PATCH 47/67] chore(#142): add tests and small fixes --- mediator/src/controllers/tests/cht.spec.ts | 28 +++---- mediator/src/controllers/tests/utils.ts | 3 - mediator/src/utils/cht.ts | 2 +- mediator/src/utils/tests/cht.spec.ts | 75 +++++++++++++++++-- mediator/src/utils/tests/openmrs_sync.spec.ts | 2 +- 5 files changed, 78 insertions(+), 32 deletions(-) diff --git a/mediator/src/controllers/tests/cht.spec.ts b/mediator/src/controllers/tests/cht.spec.ts index 446fcdd6..1865149f 100644 --- a/mediator/src/controllers/tests/cht.spec.ts +++ b/mediator/src/controllers/tests/cht.spec.ts @@ -28,13 +28,17 @@ import { randomUUID } from 'crypto'; jest.mock('axios'); describe('CHT outgoing document controllers', () => { + beforeEach(async () => { + // All of these tests call updateFhirResource, either to create the + // resource directly, or to update its id + jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ + data: {}, + status: 200, + }); + }); + describe('createPatient', () => { it('creates a FHIR Patient from CHT patient doc', async () => { - jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ - data: {}, - status: 200, - }); - const data = ChtPatientFactory.build(); const res = await createPatient(data); @@ -56,11 +60,6 @@ describe('CHT outgoing document controllers', () => { }); it('creates a FHIR Patient from an SMS form using source id', async () => { - jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ - data: {}, - status: 200, - }); - let sourceId = randomUUID(); jest.spyOn(cht, 'getPatientUUIDFromSourceId').mockResolvedValueOnce(sourceId); @@ -92,10 +91,6 @@ describe('CHT outgoing document controllers', () => { data: { total: 1, entry: [ { resource: existingPatient } ] }, status: 200, }); - jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ - data: {}, - status: 200, - }); let sourceId = randomUUID(); jest.spyOn(cht, 'getPatientUUIDFromSourceId').mockResolvedValueOnce(sourceId); @@ -132,11 +127,6 @@ describe('CHT outgoing document controllers', () => { data: {}, status: 200, }); - // encounter uses updatedFhirResource - jest.spyOn(fhir, 'updateFhirResource').mockResolvedValueOnce({ - data: {}, - status: 200, - }); const data = ChtPregnancyForm.build(); diff --git a/mediator/src/controllers/tests/utils.ts b/mediator/src/controllers/tests/utils.ts index b582c66f..648877f3 100644 --- a/mediator/src/controllers/tests/utils.ts +++ b/mediator/src/controllers/tests/utils.ts @@ -25,6 +25,3 @@ export const mockCreateFHIRSubscriptionResource = export const mockCreateChtRecord = createChtFollowUpRecord as jest.MockedFn< typeof createChtFollowUpRecord >; -export const mockQueryCht = queryCht as jest.MockedFn< - typeof queryCht ->; diff --git a/mediator/src/utils/cht.ts b/mediator/src/utils/cht.ts index 22fdc1d8..ecdd7be8 100644 --- a/mediator/src/utils/cht.ts +++ b/mediator/src/utils/cht.ts @@ -121,7 +121,7 @@ export async function getPatientUUIDFromSourceId(source_id: string) { } const patient = await queryCht(query); - if ( patient.data.docs && patient.data.docs.length > 0 ){ + if ( patient?.data?.docs && patient.data.docs.length > 0 ){ return patient.data.docs[0]._id; } else { return '' diff --git a/mediator/src/utils/tests/cht.spec.ts b/mediator/src/utils/tests/cht.spec.ts index 205fbdbe..23f4b91a 100644 --- a/mediator/src/utils/tests/cht.spec.ts +++ b/mediator/src/utils/tests/cht.spec.ts @@ -2,11 +2,12 @@ import { createChtFollowUpRecord, generateChtRecordsApiUrl, getLocationFromOpenMRSPatient, - queryCht } from '../cht'; + getPatientUUIDFromSourceId, + queryCht +} from '../cht'; import axios from 'axios'; import { logger } from '../../../logger'; import { OpenMRSPatientFactory } from '../../middlewares/schemas/tests/openmrs-resource-factories'; -import { mockQueryCht } from '../../controllers/tests/utils'; jest.mock('axios'); jest.mock('../../../logger'); @@ -52,7 +53,7 @@ describe('CHT Utils', () => { addressValue: 'FCHV Area [12345]' }); - const result = await getLocationFromOpenMRSPatient(fhirPatient as any); + const result = await getLocationFromOpenMRSPatient(fhirPatient); expect(result).toBe('12345'); }); @@ -63,9 +64,10 @@ describe('CHT Utils', () => { addressValue: 'Unknown Area' }); - mockQueryCht.mockResolvedValue({ status: 200, data: { docs: [] } }); // Simulating no result from the query + const data = { status: 200, data: { docs: [] } }; + mockAxios.post.mockResolvedValue(data); - const result = await getLocationFromOpenMRSPatient(fhirPatient as any); + const result = await getLocationFromOpenMRSPatient(fhirPatient); expect(result).toBe(''); }); @@ -76,7 +78,7 @@ describe('CHT Utils', () => { addressValue: 'Health Center [54321]' }); - const result = await getLocationFromOpenMRSPatient(fhirPatient as any); + const result = await getLocationFromOpenMRSPatient(fhirPatient); expect(result).toBe('54321'); }); @@ -87,9 +89,66 @@ describe('CHT Utils', () => { addressValue: 'Unknown Location' }); - mockQueryCht.mockRejectedValue(new Error('Database query failed')); + mockAxios.post.mockRejectedValue(new Error('Database query failed')); - const result = await getLocationFromOpenMRSPatient(fhirPatient as any); + const result = await getLocationFromOpenMRSPatient(fhirPatient); + + expect(result).toBe(''); + }); + + it('should return location by name if no address4 or address5', async () => { + const fhirPatient = OpenMRSPatientFactory.build({}, { + addressKey: 'address4', + addressValue: 'Area1' + }); + + const data = { + status: 200, + data: { docs: [ { place_id: 12345 } ] } }; + mockAxios.post.mockResolvedValue(data); + + const result = await getLocationFromOpenMRSPatient(fhirPatient); + + expect(result).toBe(12345); + }); + }); + + describe('getPatientUUIDFromSourceId', () => { + it('should return patient UUID if patient is found', async () => { + const sourceId = '12345'; + const mockUUID = 'abcdef-123456'; + + const data = { + status: 200, + data: { docs: [{ _id: mockUUID }] } + }; + mockAxios.post.mockResolvedValue(data); + + const result = await getPatientUUIDFromSourceId(sourceId); + + expect(result).toBe(mockUUID); + }); + + it('should return an empty string if no patient is found', async () => { + const sourceId = 'not_found_id'; + + const data = { + status: 200, + data: { docs: [] } + }; + mockAxios.post.mockResolvedValue(data); + + const result = await getPatientUUIDFromSourceId(sourceId); + + expect(result).toBe(''); + }); + + it('should handle error cases by returning an empty string when query fails', async () => { + const sourceId = 'error_id'; + + mockAxios.post.mockRejectedValue(new Error('Database query failed')); + + const result = await getPatientUUIDFromSourceId(sourceId); expect(result).toBe(''); }); diff --git a/mediator/src/utils/tests/openmrs_sync.spec.ts b/mediator/src/utils/tests/openmrs_sync.spec.ts index 8f76eeae..f8fc45b9 100644 --- a/mediator/src/utils/tests/openmrs_sync.spec.ts +++ b/mediator/src/utils/tests/openmrs_sync.spec.ts @@ -20,7 +20,7 @@ jest.mock('../../../logger'); describe('OpenMRS Sync', () => { describe('compare', () => { - it('compares resources with the gvien key', async () => { + it('correctly identifies incoming, outgoing, and to-be-updated resources based on the given key', async () => { const lastUpdated = new Date(); lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); From 59bf49435580d83a588dedc11ed6cfe965c30408 Mon Sep 17 00:00:00 2001 From: Maria Lorena Rodriguez Viruel Date: Thu, 31 Oct 2024 16:37:03 -0300 Subject: [PATCH 48/67] fix(#123): use up-test command in retry_startup --- mediator/test/e2e-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mediator/test/e2e-test.sh b/mediator/test/e2e-test.sh index 34540188..020203eb 100755 --- a/mediator/test/e2e-test.sh +++ b/mediator/test/e2e-test.sh @@ -14,7 +14,7 @@ export OPENMRS_PASSWORD=Admin123 retry_startup() { max_attempts=5 count=0 - until ./startup.sh init || [ $count -eq $max_attempts ]; do + until ./startup.sh up-test || [ $count -eq $max_attempts ]; do echo "Attempt $((count+1)) of $max_attempts to start containers failed, retrying in 30 seconds..." count=$((count+1)) sleep 30 From 0f7ef52c6ffc5b68fb03ab63eb1f2a65514f617b Mon Sep 17 00:00:00 2001 From: Maria Lorena Rodriguez Viruel Date: Thu, 31 Oct 2024 16:43:20 -0300 Subject: [PATCH 49/67] fix(#123): remove conditional to enable E2E tests in CI --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eac0b1f9..5cbc68b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,6 @@ jobs: e2e-tests: name: E2E Tests runs-on: ubuntu-latest - if: false steps: - name: Login to Docker Hub From 61ccd62c45f6257d72afe844155cc7a4b6ca1fff Mon Sep 17 00:00:00 2001 From: Maria Lorena Rodriguez Viruel Date: Thu, 31 Oct 2024 16:43:39 -0300 Subject: [PATCH 50/67] fix(#123): remove conditional to enable E2E tests in CI --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5cbc68b0..eac0b1f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,7 @@ jobs: e2e-tests: name: E2E Tests runs-on: ubuntu-latest + if: false steps: - name: Login to Docker Hub From 5be2b00339c1c047882d67dcd7bc86d856447e7e Mon Sep 17 00:00:00 2001 From: njuguna-n <141340177+njuguna-n@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:56:26 +0300 Subject: [PATCH 51/67] fix: convert sync period to milliseconds before subtracting from the current time --- mediator/src/controllers/openmrs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mediator/src/controllers/openmrs.ts b/mediator/src/controllers/openmrs.ts index 9a05b831..a0436de3 100644 --- a/mediator/src/controllers/openmrs.ts +++ b/mediator/src/controllers/openmrs.ts @@ -5,7 +5,7 @@ import { SYNC_PERIOD } from '../../config' export async function sync() { try { let now = Date.now(); - let syncPeriod = parseInt(SYNC_PERIOD, 10); + let syncPeriod = parseInt(SYNC_PERIOD, 10) * 1000; // Convert seconds to milliseconds let startTime = new Date(now - syncPeriod); await syncPatients(startTime); From 8abfa1068eb3369abb77e8285e8c30916b82f74e Mon Sep 17 00:00:00 2001 From: Maria Lorena Rodriguez Viruel Date: Fri, 1 Nov 2024 10:02:45 -0300 Subject: [PATCH 52/67] fix(#123): remove conditional to enable E2E tests in CI --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eac0b1f9..5cbc68b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,6 @@ jobs: e2e-tests: name: E2E Tests runs-on: ubuntu-latest - if: false steps: - name: Login to Docker Hub From 3cc6a556127b796c90ebf669e39cc80203e85a6b Mon Sep 17 00:00:00 2001 From: Maria Lorena Rodriguez Viruel Date: Fri, 1 Nov 2024 10:08:48 -0300 Subject: [PATCH 53/67] fix(#123): revert remove conditional to enable E2E tests in CI --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5cbc68b0..eac0b1f9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,6 +20,7 @@ jobs: e2e-tests: name: E2E Tests runs-on: ubuntu-latest + if: false steps: - name: Login to Docker Hub From 3216ce7bef3c01a2de35b5b82dba54b19587cdf4 Mon Sep 17 00:00:00 2001 From: Maria Lorena Rodriguez Viruel Date: Wed, 6 Nov 2024 16:15:05 -0300 Subject: [PATCH 54/67] fix(#123): add wait time before asserting fhir response --- mediator/test/e2e-test.sh | 4 ++-- mediator/test/workflows.spec.ts | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/mediator/test/e2e-test.sh b/mediator/test/e2e-test.sh index 020203eb..7bdf4f33 100755 --- a/mediator/test/e2e-test.sh +++ b/mediator/test/e2e-test.sh @@ -37,7 +37,6 @@ retry_startup echo 'Waiting for configurator to finish...' docker container wait chis-interop-cht-configurator-1 -echo 'Executing mediator e2e tests...' cd $MEDIATORDIR export OPENHIM_API_URL='https://localhost:8080' export FHIR_URL='http://localhost:5001' @@ -53,7 +52,8 @@ export OPENMRS_CHANNEL_USERNAME='interop-client' export OPENMRS_CHANNEL_PASSWORD='interop-password' echo 'Waiting for OpenMRS to be ready' -sleep 180 +sleep 280 +echo 'Executing mediator e2e tests...' npm run test -t workflows.spec.ts echo 'Cleanup after test...' diff --git a/mediator/test/workflows.spec.ts b/mediator/test/workflows.spec.ts index c76d1600..baba0aa7 100644 --- a/mediator/test/workflows.spec.ts +++ b/mediator/test/workflows.spec.ts @@ -49,7 +49,7 @@ const installMediatorConfiguration = async () => { const createOpenMRSIdType = async (name: string) => { const patientIdType = { name: name, - description: "CHT Patient ID", + description: name, required: false, locationBehavior: "NOT_USED", uniquenessBehavior: "Unique" @@ -60,10 +60,11 @@ const createOpenMRSIdType = async (name: string) => { .auth('admin', 'Admin123') .send(patientIdType) if (res.status !== 201) { - throw new Error(`Mediator channel installation failed: Reason ${res.status}`); + console.error('Response:', res); + throw new Error(`create OpenMRS Id Type failed: Reason ${JSON.stringify(res.body || res)}`); } } catch (error) { - throw new Error(`Mediator channel installation failed ${error}`); + throw new Error(`create OpenMRS Id Type failed ${error}`); } }; @@ -106,6 +107,7 @@ describe('Workflows', () => { beforeAll(async () => { await installMediatorConfiguration(); await configureCHT(); + await new Promise((r) => setTimeout(r, 3000)); await createOpenMRSIdType('CHT Patient ID'); await createOpenMRSIdType('CHT Document ID'); }); @@ -130,7 +132,7 @@ describe('Workflows', () => { expect(createPatientResponse.body.ok).toEqual(true); patientId = createPatientResponse.body.id; - await new Promise((r) => setTimeout(r, 3000)); + await new Promise((r) => setTimeout(r, 5000)); const retrieveFhirPatientIdResponse = await request(FHIR.url) .get('/fhir/Patient/?identifier=' + patientId) @@ -146,7 +148,7 @@ describe('Workflows', () => { expect(triggerOpenMrsSyncPatientResponse.status).toBe(200); - await new Promise((r) => setTimeout(r, 3000)); + await new Promise((r) => setTimeout(r, 5000)); const retrieveOpenMrsPatientIdResponse = await request(OPENMRS.url) .get('/Patient/?identifier=' + patientId) @@ -154,13 +156,14 @@ describe('Workflows', () => { expect(retrieveOpenMrsPatientIdResponse.status).toBe(200); //this should work after fixing openmrs to have latest fhir omod and cht identifier defined. - //expect(retrieveOpenMrsPatientIdResponse.body.total).toBe(1); + expect(retrieveOpenMrsPatientIdResponse.body.total).toBe(1); //Validate HAPI updated ids }); - it('Should follow the OpenMRS Patient to CHT workflow', async () => { + //skipping this test because is incomplete. + it.skip('Should follow the OpenMRS Patient to CHT workflow', async () => { const checkMediatorResponse = await request(FHIR.url) .get('/mediator/') .auth(FHIR.username, FHIR.password); @@ -174,7 +177,7 @@ describe('Workflows', () => { .auth(FHIR.username, FHIR.password); expect(retrieveFhirPatientIdResponse.status).toBe(200); - expect(retrieveFhirPatientIdResponse.body.total).toBe(1); + //expect(retrieveFhirPatientIdResponse.body.total).toBe(1); //TODO: retrieve and validate patient from CHT api //trigger openmrs sync From 71df8ded3cd247931b49d2e9d565640d65fd210c Mon Sep 17 00:00:00 2001 From: Maria Lorena Rodriguez Viruel Date: Wed, 6 Nov 2024 17:32:17 -0300 Subject: [PATCH 55/67] fix(#123): add logic to retry image pulls --- mediator/test/e2e-test.sh | 48 +++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/mediator/test/e2e-test.sh b/mediator/test/e2e-test.sh index 7bdf4f33..c9e33435 100755 --- a/mediator/test/e2e-test.sh +++ b/mediator/test/e2e-test.sh @@ -10,29 +10,43 @@ export OPENMRS_HOST=openmrs export OPENMRS_USERNAME=admin export OPENMRS_PASSWORD=Admin123 -# Cleanup from last test, in case of interruptions -retry_startup() { - max_attempts=5 - count=0 - until ./startup.sh up-test || [ $count -eq $max_attempts ]; do - echo "Attempt $((count+1)) of $max_attempts to start containers failed, retrying in 30 seconds..." - count=$((count+1)) - sleep 30 - done - - if [ $count -eq $max_attempts ]; then - echo "Failed to start containers after $max_attempts attempts." - exit 1 - fi -} - echo 'Cleanup from last test, in case of interruptions...' cd $BASEDIR ./startup.sh destroy +echo 'Pulling Docker images with retry mechanism...' +services=("haproxy" "healthcheck" "api" "sentinel" "nginx" "couchdb") +max_retries=3 +retry_delay=10 # seconds + +# Retry pulling the images +for service in "${services[@]}"; do + attempt=1 + success=false + while [[ $attempt -le $max_retries ]]; do + echo "Pulling service: $service (Attempt $attempt of $max_retries)" + + # Attempt to pull the image for the specific service + if docker compose -f ./docker/docker-compose.cht-core.yml pull $service; then + echo "$service pulled successfully!" + success=true + break + else + echo "Failed to pull $service. Retrying in $retry_delay seconds..." + sleep $retry_delay + fi + attempt=$(( attempt + 1 )) + done + + # Check if we exhausted all retries without success + if [[ $success == false ]]; then + echo "ERROR: Failed to pull $service after $max_retries attempts." + exit 1 # Exit the script if pulling the image fails after retries + fi +done + echo 'Starting the interoperability containers...' ./startup.sh up-test -retry_startup echo 'Waiting for configurator to finish...' docker container wait chis-interop-cht-configurator-1 From 1ec51816880fd01230d0ec691400abf8bb7bfd4e Mon Sep 17 00:00:00 2001 From: Maria Lorena Rodriguez Viruel Date: Wed, 6 Nov 2024 17:32:57 -0300 Subject: [PATCH 56/67] fix(#123): enable e2e test in ci --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eac0b1f9..5cbc68b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,6 @@ jobs: e2e-tests: name: E2E Tests runs-on: ubuntu-latest - if: false steps: - name: Login to Docker Hub From 670f3a5bdca9862a6c66fac5ffc9e1d8f56298e3 Mon Sep 17 00:00:00 2001 From: njuguna-n <141340177+njuguna-n@users.noreply.github.com> Date: Fri, 8 Nov 2024 13:29:11 +0300 Subject: [PATCH 57/67] feat: make encounter requests idempotent by using the identifier --- mediator/src/utils/fhir.ts | 13 +++ mediator/src/utils/openmrs_sync.ts | 90 +++++++++++++----- mediator/src/utils/tests/openmrs_sync.spec.ts | 94 +++++++++++++++++++ 3 files changed, 174 insertions(+), 23 deletions(-) diff --git a/mediator/src/utils/fhir.ts b/mediator/src/utils/fhir.ts index 28fad9b6..68de7409 100644 --- a/mediator/src/utils/fhir.ts +++ b/mediator/src/utils/fhir.ts @@ -248,3 +248,16 @@ export async function getFhirResource(id: string, resourceType: string) { return { status: error.response?.status, data: error.response?.data }; } } + +export async function getFhirResourceByIdentifier(identifierValue: string, resourceType: string) { + try { + const res = await axios.get( + `${FHIR.url}/${resourceType}/?identifier=${identifierValue}`, + axiosOptions + ); + return { status: res?.status, data: res?.data }; + } catch (error: any) { + logger.error(error); + return { status: error.response?.status, data: error.response?.data }; + } +} diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index af8efd6e..8fe9ccbe 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -1,14 +1,14 @@ import { - getFhirResourcesSince, - updateFhirResource, - createFhirResource, - getIdType, addId, + addSourceMeta, copyIdToNamedIdentifier, - getFhirResource, - replaceReference, + createFhirResource, getFHIRPatientResource, - addSourceMeta + getFhirResourceByIdentifier, + getFhirResourcesSince, + getIdType, + replaceReference, + updateFhirResource } from './fhir' import { SYNC_INTERVAL } from '../../config' import { getOpenMRSResourcesSince, createOpenMRSResource } from './openmrs' @@ -213,6 +213,14 @@ export async function sendEncounterToOpenMRS( return } + // don't send if identifier already exists + const identifier = getIdType(encounter, chtDocumentIdentifierType); + const existingEncounter = await getFhirResourceByIdentifier(identifier, 'Encounter'); + if (existingEncounter?.data?.total > 0) { + logger.error(`Not re-sending encounter from cht ${encounter.id}`); + return + } + logger.info(`Sending Encounter ${encounter.id} to OpenMRS`); const patient = getPatient(encounter, references); @@ -270,53 +278,89 @@ export async function sendEncounterToFhir( encounter: fhir4.Encounter, references: fhir4.Resource[] ) { + if (isEncounterFromCht(encounter) || isEncounterIncomplete(encounter) || await isEncounterAlreadySent(encounter)) { + return; + } + + logger.info(`Sending Encounter ${encounter.id} to FHIR`); + + const observations = getObservations(encounter, references); + const patient = getPatient(encounter, references); + + if (!await isPatientValid(patient, encounter)) { + return; + } + + prepareEncounterForFhir(encounter, patient); + + if (!await saveEncounterToFhir(encounter)) { + return; + } + + observations.forEach(o => sendObservationToFhir(o, patient)); + sendEncounterToCht(encounter, patient, observations); +} + +function isEncounterFromCht(encounter: fhir4.Encounter): boolean { if (encounter.meta?.source == chtSource) { logger.error(`Not re-sending encounter from cht ${encounter.id}`); - return + return true; } + return false; +} +function isEncounterIncomplete(encounter: fhir4.Encounter): boolean { if (!encounter.period?.end) { logger.error(`Not sending encounter which is incomplete ${encounter.id}`); - return + return true; } + return false; +} - logger.info(`Sending Encounter ${encounter.id} to FHIR`); - - const observations = getObservations(encounter, references); +async function isEncounterAlreadySent(encounter: fhir4.Encounter): Promise { + const identifier = getIdType(encounter, openMRSIdentifierType); + const existingEncounter = await getFhirResourceByIdentifier(identifier, 'Encounter'); + if (existingEncounter?.data?.total > 0) { + logger.error(`Not re-sending encounter from openMRS ${encounter.id}`); + return true; + } + return false; +} - const patient = getPatient(encounter, references); +async function isPatientValid(patient: fhir4.Patient | undefined, encounter: fhir4.Encounter): Promise { if (!patient?.id) { logger.error(`Patient ${encounter.subject!.reference} not found for ${encounter.id}`); - return + return false; } - // get patient from FHIR to resolve reference const patientResponse = await getFHIRPatientResource(patient.id); if (patientResponse.status != 200) { logger.error(`Error getting Patient ${patient.id}: ${patientResponse.status}`); - return + return false; } - const existingPatient = patientResponse.data?.entry[0].resource; + return true; +} + +function prepareEncounterForFhir(encounter: fhir4.Encounter, patient: fhir4.Patient) { + const existingPatient = patient; copyIdToNamedIdentifier(encounter, encounter, openMRSIdentifierType); addSourceMeta(encounter, openMRSSource); logger.info(`Replacing ${encounter.subject!.reference} with ${patient.id} for ${encounter.id}`); replaceReference(encounter, 'subject', existingPatient); - // remove unused references delete encounter.participant; delete encounter.location; +} +async function saveEncounterToFhir(encounter: fhir4.Encounter): Promise { const response = await updateFhirResource(encounter); if (response.status != 201) { logger.error(`Error saving encounter to fhir ${encounter.id}: ${response.status}`); - return + return false; } - - observations.forEach(o => sendObservationToFhir(o, existingPatient)); - - sendEncounterToCht(encounter, existingPatient, observations); + return true; } /* diff --git a/mediator/src/utils/tests/openmrs_sync.spec.ts b/mediator/src/utils/tests/openmrs_sync.spec.ts index f8fc45b9..2598f165 100644 --- a/mediator/src/utils/tests/openmrs_sync.spec.ts +++ b/mediator/src/utils/tests/openmrs_sync.spec.ts @@ -257,5 +257,99 @@ describe('OpenMRS Sync', () => { expect(openmrs.createOpenMRSResource).toHaveBeenCalledWith(fhirObservation); }); + it('does not send incoming Encounters to FHIR and CHT if OpenMRS identifier exists', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); + + const openMRSPatient = PatientFactory.build(); + openMRSPatient.meta = { lastUpdated: lastUpdated }; + const openMRSEncounter = EncounterFactory.build(); + openMRSEncounter.meta = { lastUpdated: lastUpdated }; + openMRSEncounter.subject = { + reference: `Patient/${openMRSPatient.id}` + }; + const openMRSObservation = ObservationFactory.build(); + openMRSObservation.encounter = { reference: 'Encounter/' + openMRSEncounter.id } + + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: [], + status: 200, + }); + + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: [openMRSEncounter, openMRSPatient, openMRSObservation], + status: 200, + }); + + jest.spyOn(fhir, 'getFHIRPatientResource').mockResolvedValueOnce({ + data: { entry: [{ resource: openMRSPatient }] }, + status: 200, + }); + + jest.spyOn(fhir, 'getFhirResourceByIdentifier').mockResolvedValue({ + data: { total: 1, entry: [ { resource: openMRSPatient } ] }, + status: 200, + }); + + jest.spyOn(fhir, 'updateFhirResource').mockResolvedValue({ + data: [], + status: 201, + }); + + jest.spyOn(fhir, 'createFhirResource') + + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await syncEncounters(startTime); + + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); + + expect(fhir.updateFhirResource).not.toHaveBeenCalled(); + }); + + it('does not send outgoing Encounters to OpenMRS if identifier exists in FHIR', async () => { + const lastUpdated = new Date(); + lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); + + const fhirEncounter = EncounterFactory.build(); + fhirEncounter.meta = { lastUpdated: lastUpdated }; + const fhirObservation = ObservationFactory.build(); + fhirObservation.encounter = { reference: 'Encounter/' + fhirEncounter.id } + const chtDocId = { + system: "cht", + type: chtDocumentIdentifierType, + value: getIdType(fhirEncounter, chtDocumentIdentifierType) + } + + jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ + data: [fhirEncounter, fhirObservation], + status: 200, + }); + + jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ + data: [], + status: 200, + }); + + jest.spyOn(fhir, 'getFhirResourceByIdentifier').mockResolvedValue({ + data: { total: 1, entry: [ { resource: fhirEncounter } ] }, + status: 200, + }); + + jest.spyOn(openmrs, 'createOpenMRSResource').mockResolvedValue({ + data: [], + status: 201, + }); + + const startTime = new Date(); + startTime.setHours(startTime.getHours() - 1); + const comparison = await syncEncounters(startTime); + + expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); + expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); + + expect(openmrs.createOpenMRSResource).not.toHaveBeenCalled(); + }); }); }); From 0bca7aec6a0b953ee439ca23ee3895980d270e02 Mon Sep 17 00:00:00 2001 From: njuguna-n <141340177+njuguna-n@users.noreply.github.com> Date: Mon, 11 Nov 2024 18:03:06 +0300 Subject: [PATCH 58/67] feat: get fhir resource by identifier --- mediator/src/controllers/cht.ts | 11 +++++++++-- mediator/src/controllers/tests/cht.spec.ts | 19 +++++++++++++++++++ mediator/src/utils/openmrs_sync.ts | 8 +++++++- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/mediator/src/controllers/cht.ts b/mediator/src/controllers/cht.ts index 2cb4d4db..7388e173 100644 --- a/mediator/src/controllers/cht.ts +++ b/mediator/src/controllers/cht.ts @@ -1,9 +1,10 @@ import { + addId, createFhirResource, - updateFhirResource, + getFhirResourceByIdentifier, getFHIRPatientResource, replaceReference, - addId + updateFhirResource } from '../utils/fhir'; import { buildFhirObservationFromCht, @@ -21,6 +22,12 @@ export async function createPatient(chtPatientDoc: any) { chtPatientDoc.doc._id = await getPatientUUIDFromSourceId(chtPatientDoc.source_id); } + //check if patient already exists + const patient = await getFhirResourceByIdentifier(chtPatientDoc.doc.patient_id, 'Patient'); + if (patient?.data?.total > 0){ + return { status: 200, data: { message: `Patient with the same patient_id already exists`} }; + } + const fhirPatient = buildFhirPatientFromCht(chtPatientDoc.doc); return updateFhirResource({ ...fhirPatient, resourceType: 'Patient' }); } diff --git a/mediator/src/controllers/tests/cht.spec.ts b/mediator/src/controllers/tests/cht.spec.ts index 1865149f..56336f0b 100644 --- a/mediator/src/controllers/tests/cht.spec.ts +++ b/mediator/src/controllers/tests/cht.spec.ts @@ -40,6 +40,10 @@ describe('CHT outgoing document controllers', () => { describe('createPatient', () => { it('creates a FHIR Patient from CHT patient doc', async () => { const data = ChtPatientFactory.build(); + jest.spyOn(fhir, 'getFhirResourceByIdentifier').mockResolvedValue({ + data: { total: 0 }, + status: 200, + }); const res = await createPatient(data); @@ -82,6 +86,21 @@ describe('CHT outgoing document controllers', () => { }) ); }); + + it('does not create a patient if one with the same patient_id already exists', async () => { + const existingPatient = PatientFactory.build(); + jest.spyOn(fhir, 'getFhirResourceByIdentifier').mockResolvedValue({ + data: { total: 1, entry: [ { resource: existingPatient } ] }, + status: 200, + }); + + const data = ChtPatientFactory.build(); + + const res = await createPatient(data); + + expect(res.status).toBe(200); + expect(fhir.updateFhirResource).not.toHaveBeenCalled(); + }); }); describe('updatePatientIds', () => { diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index 8fe9ccbe..83020bc0 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -126,11 +126,17 @@ export async function compare( And forward to CHT if successful */ async function sendPatientToFhir(patient: fhir4.Patient) { + // check if patient already exists + const openMRSPatient = await getFhirResourceByIdentifier(getIdType(patient, openMRSIdentifierType), 'Patient'); + if (openMRSPatient.data?.total > 0) { + logger.error(`Patient with the same patient_id already exists`); + return { status: 200, data: { message: `Patient with the same ${openMRSIdentifierType} already exists`} }; + } logger.info(`Sending Patient ${patient.id} to FHIR`); copyIdToNamedIdentifier(patient, patient, openMRSIdentifierType); addSourceMeta(patient, openMRSSource); const response = await updateFhirResource(patient); - if (response.status == 200 || response.status == 201) { + if (response.status == 201) { logger.info(`Sending Patient ${patient.id} to CHT`); createChtPatient(response.data); } From dcb5e61668c5a77ac696e99bc855b8dc89205426 Mon Sep 17 00:00:00 2001 From: witash Date: Wed, 13 Nov 2024 12:49:49 +0300 Subject: [PATCH 59/67] feat(#147): adding value types (#149) --- mediator/src/controllers/cht.ts | 6 +-- mediator/src/mappers/cht.ts | 45 ++++++++++++---- mediator/src/middlewares/schemas/cht.ts | 41 ++++++++++++++ .../schemas/tests/cht-request-factories.ts | 20 ++++--- mediator/src/routes/cht.ts | 5 ++ mediator/src/routes/tests/cht.spec.ts | 53 +++++++++++++++++++ 6 files changed, 149 insertions(+), 21 deletions(-) create mode 100644 mediator/src/middlewares/schemas/cht.ts create mode 100644 mediator/src/routes/tests/cht.spec.ts diff --git a/mediator/src/controllers/cht.ts b/mediator/src/controllers/cht.ts index 7388e173..6707333b 100644 --- a/mediator/src/controllers/cht.ts +++ b/mediator/src/controllers/cht.ts @@ -79,10 +79,8 @@ export async function createEncounter(chtReport: any) { } for (const entry of chtReport.observations) { - if (entry.valueCode || entry.valueString || entry.valueDateTime) { - const observation = buildFhirObservationFromCht(chtReport.patient_uuid, fhirEncounter, entry); - createFhirResource(observation); - } + const observation = buildFhirObservationFromCht(chtReport.patient_uuid, fhirEncounter, entry); + createFhirResource(observation); } return { status: 200, data: {} }; diff --git a/mediator/src/mappers/cht.ts b/mediator/src/mappers/cht.ts index bccfddcd..3805bede 100644 --- a/mediator/src/mappers/cht.ts +++ b/mediator/src/mappers/cht.ts @@ -103,6 +103,39 @@ export function buildFhirEncounterFromCht(chtReport: any): fhir4.Encounter { return encounter } +/* + * Copy the value from the form mapping to observation + * value are expected to already have been validated + * for types which are native to JSON (Integer, String, Boolean) just copy them exactly + * copy Time and DateTime as strings (they have been validated already) + * valueQuantity will be an object, but also just copy it directly + */ +function copyObservationValue(observation: any, entry: any) { + const copyValueTypes = [ + 'valueString', + 'valueDateTime', + 'valueTime', + 'valueBoolean', + 'valueInteger', + 'valueQuantity' + ]; + copyValueTypes.forEach((name) => { + if (entry.hasOwnProperty(name)) { + observation[name] = entry[name]; + } + }); + + // valueCode needs a little bit of conversion; we are not requiring cht forms to + // map to codeableConcept, just to give the string + if ('valueCode' in entry){ + observation.valueCodeableConcept = { + coding: [{ + code: entry['valueCode'] + }] + }; + } +} + export function buildFhirObservationFromCht(patient_id: string, encounter: fhir4.Encounter, entry: any): fhir4.Observation { const patientRef: fhir4.Reference = { reference: `Patient/${patient_id}`, @@ -130,17 +163,7 @@ export function buildFhirObservationFromCht(patient_id: string, encounter: fhir4 issued: now }; - if ('valueCode' in entry){ - observation.valueCodeableConcept = { - coding: [{ - code: entry['valueCode'] - }] - }; - } else if ('valueDateTime' in entry){ - observation.valueDateTime = entry['valueDateTime']; - } else if ('valueString' in entry){ - observation.valueString = entry['valueString']; - } + copyObservationValue(observation, entry); return observation; } diff --git a/mediator/src/middlewares/schemas/cht.ts b/mediator/src/middlewares/schemas/cht.ts new file mode 100644 index 00000000..5bde9cf2 --- /dev/null +++ b/mediator/src/middlewares/schemas/cht.ts @@ -0,0 +1,41 @@ +import joi from 'joi'; + +export const ChtPatientSchema = joi.object({ + doc: joi.object({ + _id: joi.string().uuid().required(), + name: joi.string().required(), + phone: joi.string().required(), + date_of_birth: joi.string().required(), + sex: joi.string().required(), + patient_id: joi.string().required() + }).required() +}); + +export const ChtPatientIdsSchema = joi.object({ + doc: joi.object({ + patient_id: joi.string().required(), + external_id: joi.string().required() + }) +}); + +export const ValueTypeSchema = joi.object({ + code: joi.string().required(), + valueString: joi.string(), + valueCode: joi.string(), + valueBoolean: joi.boolean(), + valueInteger: joi.number().integer(), + valueDateTime: joi.string().isoDate(), + valueTime: joi.string().regex(/^([01]\d|2[0-3]):([0-5]\d)(:[0-5]\d(\.\d{1,3})?)?$/), // matches HH:mm[:ss[.SSS]] + valueQuantity: joi.object({ + value: joi.number().required(), + unit: joi.string().required(), + system: joi.string().uri().optional(), + code: joi.string().optional() + }) +}).or('valueString', 'valueCode', 'valueBoolean', 'valueInteger', 'valueDateTime', 'valueTime', 'valueQuantity'); + +export const ChtEncounterFormSchema = joi.object({ + patient_uuid: joi.string().required(), + reported_date: joi.number().required(), //timestamp + observations: joi.array().items(ValueTypeSchema).optional() +}); diff --git a/mediator/src/middlewares/schemas/tests/cht-request-factories.ts b/mediator/src/middlewares/schemas/tests/cht-request-factories.ts index 337c1504..9efe2b2f 100644 --- a/mediator/src/middlewares/schemas/tests/cht-request-factories.ts +++ b/mediator/src/middlewares/schemas/tests/cht-request-factories.ts @@ -50,10 +50,6 @@ export const ChtPregnancyForm = Factory.define('chtPregnancyDoc') "code": "17a57368-5f59-42c8-aaab-f2774d21501e", "valueCode": "ea6a020e-05cd-4fea-b618-abd7494ac571" }, - { - "code": "17a57368-5f59-42c8-aaab-f2774d21501e", - "valueCode": false - }, { "code": "17a57368-5f59-42c8-aaab-f2774d21501e", "valueCode": "0d9e45d6-9288-494e-841c-80f3f9b8e126" @@ -75,7 +71,19 @@ export const ChtPregnancyForm = Factory.define('chtPregnancyDoc') "valueDateTime": "2024-08-26" }, { - "code": "13179cce-a424-43d7-9ad1-dce7861946e8", - "valueString": "" + "code": "73179cce-a424-43d7-9ad1-dce7861946e8", + "valueString": "String" + }, + { + "code": "53179cce-a424-43d7-9ad1-dce7861946e8", + "valueQuantity": { "value": 160, "unit": "kg" } + }, + { + "code": "37a57368-5f59-42c8-aaab-f2774d21501e", + "valueBoolean": false + }, + { + "code": "47a57368-5f54-42c8-aaab-f2774d21501e", + "valueInteger": 12 } ]); diff --git a/mediator/src/routes/cht.ts b/mediator/src/routes/cht.ts index 1bebab29..7a8e0cbb 100644 --- a/mediator/src/routes/cht.ts +++ b/mediator/src/routes/cht.ts @@ -1,5 +1,7 @@ import { Router } from 'express'; import { requestHandler } from '../utils/request'; +import { validateBodyAgainst } from '../middlewares'; +import { ChtPatientSchema, ChtPatientIdsSchema, ChtEncounterFormSchema } from '../middlewares/schemas/cht'; import { createPatient, updatePatientIds, createEncounter } from '../controllers/cht' const router = Router(); @@ -8,16 +10,19 @@ const resourceType = 'Patient'; router.post( '/patient', + validateBodyAgainst(ChtPatientSchema), requestHandler((req) => createPatient(req.body)) ); router.post( '/patient_ids', + validateBodyAgainst(ChtPatientIdsSchema), requestHandler((req) => updatePatientIds(req.body)) ); router.post( '/encounter', + validateBodyAgainst(ChtEncounterFormSchema), requestHandler((req) => createEncounter(req.body)) ); diff --git a/mediator/src/routes/tests/cht.spec.ts b/mediator/src/routes/tests/cht.spec.ts new file mode 100644 index 00000000..2d733e84 --- /dev/null +++ b/mediator/src/routes/tests/cht.spec.ts @@ -0,0 +1,53 @@ +import request from 'supertest'; +import app from '../../..'; +import { ChtPatientFactory, ChtPatientIdsFactory, ChtPregnancyForm } from '../../middlewares/schemas/tests/cht-request-factories'; + +describe('POST /cht/patient', () => { + it('doesn\'t accept incoming request with invalid patient resource', async () => { + const data = ChtPatientFactory.build(); + delete data.doc._id; + + const res = await request(app).post('/cht/patient').send(data); + + expect(res.status).toBe(400); + expect(res.body.valid).toBe(false); + expect(res.body.message).toMatchInlineSnapshot( + `""doc._id" is required"` + ); + }); +}); + +describe('POST /cht/patient_ids', () => { + it('doesn\'t accept incoming request with invalid patient resource', async () => { + const data = ChtPatientIdsFactory.build(); + delete data.doc.patient_id; + + const res = await request(app).post('/cht/patient_ids').send(data); + + expect(res.status).toBe(400); + expect(res.body.valid).toBe(false); + expect(res.body.message).toMatchInlineSnapshot( + `""doc.patient_id" is required"` + ); + }); +}); + +describe('POST /cht/encounter', () => { + it('doesn\'t accept incoming request with invalid form', async () => { + const data = ChtPregnancyForm.build(); + + // push an invalid observation + data.observations.push({ + "code": "17a57368-5f59-42c8-aaab-f2774d21501e", + "valueDateTime": "This is not a valid date" + }) + + const res = await request(app).post('/cht/encounter').send(data); + + expect(res.status).toBe(400); + expect(res.body.valid).toBe(false); + expect(res.body.message).toMatchInlineSnapshot( + `""observations[13].valueDateTime" must be in iso format"` + ); + }); +}); From 40716e870ff9a0896b8526f85c9cf552407abfb7 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Tue, 19 Nov 2024 12:25:44 +0300 Subject: [PATCH 60/67] chore: adding sample forms --- cht-config/app_settings.json | 86 ++++++++++++++++++++++++++++++++++++ mediator/src/mappers/cht.ts | 3 +- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/cht-config/app_settings.json b/cht-config/app_settings.json index 7a9a319a..9c8a5423 100644 --- a/cht-config/app_settings.json +++ b/cht-config/app_settings.json @@ -551,6 +551,32 @@ "expr": "[ { \"type\": [ { \"text\": \"Community health worker\" } ] } ]" } } + }, + "openmrs_height_weight": { + "relevant_to": "doc.type === 'data_record' && doc.form === 'height_weight'", + "destination": { + "base_url": "http://openhim-core:5001", + "path": "/mediator/cht/encounter", + "auth": { + "type": "basic", + "username": "interop-client", + "password_key": "openhim1" + } + }, + "mapping": { + "observations.0.valueQuantity": { + "expr": "{ \"quantity\": doc.fields.height, \"unit\": \"cm\" }" + }, + "observations.0.code": { + "expr": "\"5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"" + }, + "observations.1.valueQuantity": { + "expr": "{ \"quantity\": doc.fields.weight, \"unit\": \"kg\" }" + }, + "observations.1.code": { + "expr": "\"5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"" + } + } } }, "forms": { @@ -581,6 +607,66 @@ "set_task": true } }, + "height_weight": { + "meta": { + "code": "height_weight", + "translation_key": "forms.openmrs_height_weight.title", + "icon": "medic-person" + }, + "fields": { + "height": { + "labels": { + "short": { + "en": "Heght in cm" + } + }, + "position": 0, + "type": "integer", + "required": true + }, + "weight": { + "labels": { + "short": { + "en": "Weight in kg" + } + }, + "position": 1, + "type": "integer", + "required": true + } + }, + "public_form": true + }, + "OPENMRS_INCOMING": { + "meta": { + "code": "openmrs_incoming", + "translation_key": "forms.openmrs_height_weight.title", + "icon": "medic-person" + }, + "fields": { + "5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": { + "labels": { + "short": { + "en": "Heght in cm" + } + }, + "position": 0, + "type": "integer", + "required": true + }, + "5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": { + "labels": { + "short": { + "en": "Weight in kg" + } + }, + "position": 1, + "type": "integer", + "required": true + } + }, + "public_form": true + }, "OPENMRS_PATIENT": { "meta": { "code": "openmrs_patient", diff --git a/mediator/src/mappers/cht.ts b/mediator/src/mappers/cht.ts index 3805bede..51aba899 100644 --- a/mediator/src/mappers/cht.ts +++ b/mediator/src/mappers/cht.ts @@ -171,9 +171,10 @@ export function buildFhirObservationFromCht(patient_id: string, encounter: fhir4 export function buildChtRecordFromObservations(patient: fhir4.Patient, observations: fhir4.Observation[]) { const patientId = getIdType(patient, chtDocumentIdentifierType); + // TODO: remove reference to openmrs const record: any = { _meta: { - form: "openmrs_anc" + form: "openmrs_incoming" }, patient_id: patientId } From 1de75e48db41181e3b53022778bdb062d6902473 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Tue, 19 Nov 2024 12:59:15 +0300 Subject: [PATCH 61/67] fix: removing date check --- mediator/src/utils/openmrs_sync.ts | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index 83020bc0..b1a1f97f 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -83,36 +83,18 @@ export async function compare( // get the key for each resource and create a Map const fhirIds = new Map(comparison.fhirResources.map(resource => [getKey(resource), resource])); - function isValidDate(resource: fhir4.Resource) { - // if lastUpdated is missing or invalid, cannot proceed, throw an error - if (!resource.meta?.lastUpdated) { - throw new Error("Last updated missing"); - } - const lastUpdated = new Date(resource.meta.lastUpdated); - if (isNaN(lastUpdated.getTime()) || isNaN(startTime.getTime())) { - throw new Error("Invalid date format"); - } - - // don't sync resources created with 2 * SYNC_INTERVAL of start time - const syncWindow = (Number(SYNC_INTERVAL) * 1000) * 2 - const diff = lastUpdated.getTime() - startTime.getTime(); - return diff > syncWindow; - } - comparison.openMRSResources.forEach((openMRSResource) => { const key = getKey(openMRSResource); if (fhirIds.has(key)) { results.toupdate.push(openMRSResource); fhirIds.delete(key); - } else if (isValidDate(openMRSResource)){ + } else { results.incoming.push(openMRSResource); } }); fhirIds.forEach((resource, key) => { - if (isValidDate(resource)) { - results.outgoing.push(resource); - } + results.outgoing.push(resource); }); logger.info(`Comparing ${resourceType}`); From c9354ff6ca51de263bcf4ebcce9b60ef0d6c77f9 Mon Sep 17 00:00:00 2001 From: njuguna-n <141340177+njuguna-n@users.noreply.github.com> Date: Mon, 25 Nov 2024 11:37:42 +0300 Subject: [PATCH 62/67] chore: address sonarlint issues --- cht-config/Dockerfile | 12 ++++++------ configurator/Dockerfile | 2 +- configurator/index.js | 10 +++++++++- configurator/libs/generators.js | 3 ++- mediator/src/mappers/cht.ts | 3 +-- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/cht-config/Dockerfile b/cht-config/Dockerfile index 9584778f..3ba4aee1 100644 --- a/cht-config/Dockerfile +++ b/cht-config/Dockerfile @@ -2,19 +2,19 @@ FROM node:16-slim RUN apt update \ && apt install --no-install-recommends -y \ + curl \ git \ python3-pip \ python3-setuptools \ python3-wheel \ - curl - -RUN python3 -m pip install git+https://github.com/medic/pyxform.git@medic-conf-1.17#egg=pyxform-medic + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + && python3 -m pip install git+https://github.com/medic/pyxform.git@medic-conf-1.17#egg=pyxform-medic WORKDIR /scripts/cht-config COPY ./ ./ -RUN npm install -RUN npm install -g cht-conf +RUN npm install --ignore-scripts && npm install -g --ignore-scripts cht-conf -CMD npm run deploy +CMD ["npm", "run", "deploy"] diff --git a/configurator/Dockerfile b/configurator/Dockerfile index bc5da0f1..3ca72577 100644 --- a/configurator/Dockerfile +++ b/configurator/Dockerfile @@ -9,4 +9,4 @@ RUN npm install COPY ./ ./ -CMD npm run configure +CMD ["npm", "run", "configure"] diff --git a/configurator/index.js b/configurator/index.js index 3204e060..f9202ec7 100644 --- a/configurator/index.js +++ b/configurator/index.js @@ -14,11 +14,19 @@ async function handleConfiguration () { ContactGroups: [] }; + const openMRSConfig = { + host: OPENMRS_HOST, + port: OPENMRS_PORT, + username: OPENMRS_USERNAME, + password: OPENMRS_PASSWORD, + protocol: OPENMRS_PROTOCOL + } + metadata.Users.push(await generateUser(OPENHIM_USER_PASSWORD)); metadata.Clients.push(await generateClient(OPENHIM_CLIENT_PASSWORD)); metadata.Channels.push(await generateHapiFhirChannel()); if (OPENMRS_HOST) { - metadata.Channels.push(await generateOpenMRSChannel(OPENMRS_HOST, OPENMRS_PORT, OPENMRS_USERNAME, OPENMRS_PASSWORD, OPENMRS_PROTOCOL)); + metadata.Channels.push(await generateOpenMRSChannel(openMRSConfig)); } const data = JSON.stringify(metadata); diff --git a/configurator/libs/generators.js b/configurator/libs/generators.js index 43be3980..dc9edd61 100644 --- a/configurator/libs/generators.js +++ b/configurator/libs/generators.js @@ -94,7 +94,8 @@ async function generateHapiFhirChannel () { }; } -async function generateOpenMRSChannel (host, port, username, password, type) { +async function generateOpenMRSChannel (config) { + const { host, port, username, password, type } = config; return { methods: [ 'GET', diff --git a/mediator/src/mappers/cht.ts b/mediator/src/mappers/cht.ts index 51aba899..01b61aa0 100644 --- a/mediator/src/mappers/cht.ts +++ b/mediator/src/mappers/cht.ts @@ -1,5 +1,4 @@ -import { getIdType, copyIdToNamedIdentifier, addSourceMeta } from '../utils/fhir'; -import { openMRSIdentifierType } from './openmrs'; +import { getIdType, copyIdToNamedIdentifier } from '../utils/fhir'; export const chtDocumentIdentifierType: fhir4.CodeableConcept = { text: 'CHT Document ID' From 7b8978894e3a53e1d3f1cc4fb195f323fccf40a8 Mon Sep 17 00:00:00 2001 From: Andra Blaj Date: Thu, 28 Nov 2024 11:05:16 +0200 Subject: [PATCH 63/67] chore: remove unused variable --- mediator/src/utils/openmrs_sync.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index b1a1f97f..339816ec 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -10,7 +10,6 @@ import { replaceReference, updateFhirResource } from './fhir' -import { SYNC_INTERVAL } from '../../config' import { getOpenMRSResourcesSince, createOpenMRSResource } from './openmrs' import { buildOpenMRSPatient, buildOpenMRSVisit, buildOpenMRSObservation, openMRSIdentifierType, openMRSSource } from '../mappers/openmrs' import { chtDocumentIdentifierType, chtSource } from '../mappers/cht' From f361ca4485daafe391241106a622775355b82b3c Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Thu, 9 Jan 2025 12:34:02 +0300 Subject: [PATCH 64/67] fix: adding postman collection --- .../OpenMRS Interop.postman_collection.json | 862 ++++++++++++++++++ docs/local-test/dev.json | 110 ++- 2 files changed, 923 insertions(+), 49 deletions(-) create mode 100644 docs/local-test/OpenMRS Interop.postman_collection.json diff --git a/docs/local-test/OpenMRS Interop.postman_collection.json b/docs/local-test/OpenMRS Interop.postman_collection.json new file mode 100644 index 00000000..91977758 --- /dev/null +++ b/docs/local-test/OpenMRS Interop.postman_collection.json @@ -0,0 +1,862 @@ +{ + "info": { + "_postman_id": "961b51d8-e267-46d0-9dcc-0ae3349817e1", + "name": "OpenMRS Interop", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "39404962", + "_collection_link": "https://cht-ecosystem.postman.co/workspace/CHT-Ecosystem-Workspace~2c294298-272a-4a31-8c13-568b5d0706a4/collection/39404962-961b51d8-e267-46d0-9dcc-0ae3349817e1?action=share&source=collection_link&creator=39404962" + }, + "item": [ + { + "name": "Mediator Status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", () => {", + " pm.expect(pm.response.code).to.eql(200);", + "});", + "", + "pm.test(\"Mediator response is success\", () => {", + " const responseJson = pm.response.json();", + " pm.expect(responseJson.status).to.eql('success');", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{OPENHIM_PASSWORD}}", + "type": "string" + }, + { + "key": "username", + "value": "{{OPENHIM_USER}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:5001/mediator/", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5001", + "path": [ + "mediator", + "" + ] + } + }, + "response": [] + }, + { + "name": "OpenMRS Api Status", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", () => {", + " pm.expect(pm.response.code).to.eql(200);", + "});", + "", + "pm.test(\"Mediator response is success\", () => {", + " const responseJson = pm.response.json();", + " pm.expect(responseJson.status).to.eql('active');", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8090/openmrs/ws/fhir2/R4/metadata", + "host": [ + "localhost" + ], + "port": "8090", + "path": [ + "openmrs", + "ws", + "fhir2", + "R4", + "metadata" + ] + } + }, + "response": [] + }, + { + "name": "createPatientIdentiferType", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", () => {", + " pm.expect(pm.response.code).to.eql(201);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{OPENMRS_PASSWORD}}", + "type": "string" + }, + { + "key": "username", + "value": "{{OPENMRS_USER}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"CHT Patient ID\",\n \"description\": \"CHT Patient ID\",\n \"required\": false,\n \"locationBehavior\": \"NOT_USED\",\n \"uniquenessBehavior\": \"Unique\"\n}\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8090/openmrs/ws/rest/v1/patientidentifiertype", + "host": [ + "localhost" + ], + "port": "8090", + "path": [ + "openmrs", + "ws", + "rest", + "v1", + "patientidentifiertype" + ] + } + }, + "response": [] + }, + { + "name": "createDocumentIdentiferType", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", () => {", + " pm.expect(pm.response.code).to.eql(201);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{OPENMRS_PASSWORD}}", + "type": "string" + }, + { + "key": "username", + "value": "{{OPENMRS_USER}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"CHT Document ID\",\n \"description\": \"CHT Document ID\",\n \"required\": false,\n \"locationBehavior\": \"NOT_USED\",\n \"uniquenessBehavior\": \"Unique\"\n}\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "localhost:8090/openmrs/ws/rest/v1/patientidentifiertype", + "host": [ + "localhost" + ], + "port": "8090", + "path": [ + "openmrs", + "ws", + "rest", + "v1", + "patientidentifiertype" + ] + } + }, + "response": [] + }, + { + "name": "CHT Create Patient", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "let response = pm.response.json();", + "", + "pm.collectionVariables.set(\"cht_patient_id\", response.id);", + "", + "pm.test(\"Status code is 200\", () => {", + " pm.expect(pm.response.code).to.eql(200);", + "});", + "", + "pm.test(\"CHT Patient creation is successful\", () => {", + " pm.expect(response.ok).to.eql(true);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{CHT_ADMIN_PASSWORD}}", + "type": "string" + }, + { + "key": "username", + "value": "{{CHT_ADMIN_USER}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"John Test\",\n \"phone\": \"+2548277217095\",\n \"date_of_birth\":\"1980-06-06\",\n \"sex\":\"male\",\n \"type\": \"person\",\n \"role\": \"patient\",\n \"contact_type\": \"patient\",\n \"place\": \"\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:5988/api/v1/people", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5988", + "path": [ + "api", + "v1", + "people" + ] + } + }, + "response": [] + }, + { + "name": "GET FHIR Patient", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "let response = pm.response.json();", + "", + "pm.test(\"Status code is 200\", () => {", + " pm.expect(pm.response.code).to.eql(200);", + "});", + "", + "pm.test(\"FHIR patient creation is successful\", () => {", + " pm.expect(response.total).to.eql(1);", + "});", + "", + "//should be the same as cht_patient_id", + "pm.collectionVariables.set(\"fhir_patient_id\", response.entry[0].resource.id);" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{OPENHIM_PASSWORD}}", + "type": "string" + }, + { + "key": "username", + "value": "{{OPENHIM_USER}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "localhost:5001/fhir/Patient/?identifier={{cht_patient_id}}", + "host": [ + "localhost" + ], + "port": "5001", + "path": [ + "fhir", + "Patient", + "" + ], + "query": [ + { + "key": "identifier", + "value": "{{cht_patient_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "OpenMRS FHIR Sync Patient", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{OPENHIM_PASSWORD}}", + "type": "string" + }, + { + "key": "username", + "value": "{{OPENHIM_USER}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "localhost:5001/mediator/openmrs/sync", + "host": [ + "localhost" + ], + "port": "5001", + "path": [ + "mediator", + "openmrs", + "sync" + ] + } + }, + "response": [] + }, + { + "name": "GET OpenMRS Patient", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "let response = pm.response.json();", + "", + "pm.collectionVariables.set(\"openmrs_patient_id\", response.entry[0].resource.id);", + "", + "pm.test(\"Status code is 200\", () => {", + " pm.expect(pm.response.code).to.eql(200);", + "});", + "", + "pm.test(\"OpenMRS patient creation is successful\", () => {", + " pm.expect(response.total).to.eql(1);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{OPENMRS_PASSWORD}}", + "type": "string" + }, + { + "key": "username", + "value": "{{OPENMRS_USER}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8090/openmrs/ws/fhir2/R4/Patient/?identifier={{cht_patient_id}}", + "host": [ + "localhost" + ], + "port": "8090", + "path": [ + "openmrs", + "ws", + "fhir2", + "R4", + "Patient", + "" + ], + "query": [ + { + "key": "identifier", + "value": "{{cht_patient_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "CHT Create Report", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "let response = pm.response.json();", + "", + "pm.test(\"Status code is 200\", () => {", + " pm.expect(pm.response.code).to.eql(200);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{CHT_ADMIN_PASSWORD}}", + "type": "string" + }, + { + "key": "username", + "value": "{{CHT_ADMIN_USER}}", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"_meta\": {\n \"form\": \"HEIGHT_WEIGHT\"\n },\n \"patient_uuid\": \"{{cht_patient_id}}\",\n \"height\": 172,\n \"weight\": 65\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:5988/api/v2/records", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "5988", + "path": [ + "api", + "v2", + "records" + ] + } + }, + "response": [] + }, + { + "name": "GET FHIR Encounter", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "let response = pm.response.json();", + "", + "pm.test(\"Status code is 200\", () => {", + " pm.expect(pm.response.code).to.eql(200);", + "});", + "", + "pm.test(\"FHIR encounter creation is successful\", () => {", + " pm.expect(response.total).to.eql(1);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{OPENHIM_PASSWORD}}", + "type": "string" + }, + { + "key": "username", + "value": "{{OPENHIM_USER}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "localhost:5001/fhir/Encounter/?subject=Patient/{{cht_patient_id}}", + "host": [ + "localhost" + ], + "port": "5001", + "path": [ + "fhir", + "Encounter", + "" + ], + "query": [ + { + "key": "subject", + "value": "Patient/{{cht_patient_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "GET FHIR Observations", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "let response = pm.response.json();", + "", + "pm.test(\"Status code is 200\", () => {", + " pm.expect(pm.response.code).to.eql(200);", + "});", + "", + "pm.test(\"FHIR observation creation is successful\", () => {", + " // height weight form has 2 observations (height and weight)", + " pm.expect(response.total).to.eql(2);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{OPENHIM_PASSWORD}}", + "type": "string" + }, + { + "key": "username", + "value": "{{OPENHIM_USER}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "localhost:5001/fhir/Observation/?subject=Patient/{{cht_patient_id}}", + "host": [ + "localhost" + ], + "port": "5001", + "path": [ + "fhir", + "Observation", + "" + ], + "query": [ + { + "key": "subject", + "value": "Patient/{{cht_patient_id}}" + } + ] + } + }, + "response": [] + }, + { + "name": "OpenMRS FHIR Sync Encounter", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{OPENHIM_PASSWORD}}", + "type": "string" + }, + { + "key": "username", + "value": "{{OPENHIM_USER}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "localhost:5001/mediator/openmrs/sync", + "host": [ + "localhost" + ], + "port": "5001", + "path": [ + "mediator", + "openmrs", + "sync" + ] + } + }, + "response": [] + }, + { + "name": "GET OpenMRS Encounter", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "let response = pm.response.json();", + "", + "pm.test(\"Status code is 200\", () => {", + " pm.expect(pm.response.code).to.eql(200);", + "});", + "", + "pm.test(\"OpenMRS encounter creation is successful\", () => {", + " pm.expect(response.total).to.eql(1);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{OPENMRS_PASSWORD}}", + "type": "string" + }, + { + "key": "username", + "value": "{{OPENMRS_USER}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8090/openmrs/ws/fhir2/R4/Encounter", + "host": [ + "localhost" + ], + "port": "8090", + "path": [ + "openmrs", + "ws", + "fhir2", + "R4", + "Encounter" + ] + } + }, + "response": [] + }, + { + "name": "GET OpenMRS Observations", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "let response = pm.response.json();", + "", + "pm.test(\"Status code is 200\", () => {", + " pm.expect(pm.response.code).to.eql(200);", + "});", + "", + "pm.test(\"OpenMRS Observation creation is successful\", () => {", + " // height weight form has 2 observations (height and weight)", + " pm.expect(response.total).to.eql(2);", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "{{OPENMRS_PASSWORD}}", + "type": "string" + }, + { + "key": "username", + "value": "{{OPENMRS_USER}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "localhost:8090/openmrs/ws/fhir2/R4/Observation?subject={{openmrs_patient_id}}", + "host": [ + "localhost" + ], + "port": "8090", + "path": [ + "openmrs", + "ws", + "fhir2", + "R4", + "Observation" + ], + "query": [ + { + "key": "subject", + "value": "{{openmrs_patient_id}}" + } + ] + } + }, + "response": [] + } + ], + "auth": { + "type": "basic" + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "patient", + "value": "" + }, + { + "key": "place", + "value": "" + }, + { + "key": "cht_patient_id", + "value": "" + }, + { + "key": "fhir_patient_id", + "value": "" + }, + { + "key": "openmrs_patient_id", + "value": "" + }, + { + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/local-test/dev.json b/docs/local-test/dev.json index 4b8f8167..dbe5cc2a 100644 --- a/docs/local-test/dev.json +++ b/docs/local-test/dev.json @@ -1,51 +1,63 @@ { - "id": "2e553c09-22c1-4ca2-a1b2-1ecd6d711bf4", - "name": "dev", - "values": [ - { - "key": "OPENHIM_USER", - "value": "interop-client", - "type": "default", - "enabled": true - }, - { - "key": "OPENHIM_PASSWORD", - "value": "interop-password", - "type": "default", - "enabled": true - }, - { - "key": "CHT_ADMIN_USER", - "value": "admin", - "type": "default", - "enabled": true - }, - { - "key": "CHT_ADMIN_PASSWORD", - "value": "password", - "type": "default", - "enabled": true - }, - { - "key": "CALLBACK_URL", - "value": "https://interop.free.beeceptor.com/callback", - "type": "default", - "enabled": true - }, - { - "key": "ORGANIZATION_IDENTIFIER", - "value": "test-org", - "type": "default", - "enabled": true - }, - { - "key": "ENDPOINT_IDENTIFIER", - "value": "test-endpoint", - "type": "default", - "enabled": true - } - ], - "_postman_variable_scope": "environment", - "_postman_exported_at": "2023-04-25T11:48:41.589Z", - "_postman_exported_using": "Postman/10.13.4" + "id": "6ac40f9a-94ed-44a4-abf3-cf1078dc7768", + "name": "dev", + "values": [ + { + "key": "OPENHIM_USER", + "value": "interop-client", + "type": "default", + "enabled": true + }, + { + "key": "OPENHIM_PASSWORD", + "value": "interop-password", + "type": "default", + "enabled": true + }, + { + "key": "CHT_ADMIN_USER", + "value": "admin", + "type": "default", + "enabled": true + }, + { + "key": "CHT_ADMIN_PASSWORD", + "value": "password", + "type": "default", + "enabled": true + }, + { + "key": "CALLBACK_URL", + "value": "https://interop.free.beeceptor.com/callback", + "type": "default", + "enabled": true + }, + { + "key": "ORGANIZATION_IDENTIFIER", + "value": "test-org", + "type": "default", + "enabled": true + }, + { + "key": "ENDPOINT_IDENTIFIER", + "value": "test-endpoint", + "type": "default", + "enabled": true + }, + { + "key": "OPENMRS_USER", + "value": "admin", + "type": "default", + "enabled": true + }, + { + "key": "OPENMRS_PASSWORD", + "value": "Admin123", + "type": "default", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2025-01-09T09:27:08.965Z", + "_postman_exported_using": "Postman/11.23.3" } From 9290c0f2514d763fcc78d5052b52f82ccc2b7113 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Fri, 10 Jan 2025 11:54:56 +0300 Subject: [PATCH 65/67] fix: app setting changes for testing --- cht-config/app_settings.json | 38 ++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/cht-config/app_settings.json b/cht-config/app_settings.json index 9c8a5423..8da0d58d 100644 --- a/cht-config/app_settings.json +++ b/cht-config/app_settings.json @@ -553,7 +553,7 @@ } }, "openmrs_height_weight": { - "relevant_to": "doc.type === 'data_record' && doc.form === 'height_weight'", + "relevant_to": "doc.type === 'data_record' && doc.form === 'HEIGHT_WEIGHT'", "destination": { "base_url": "http://openhim-core:5001", "path": "/mediator/cht/encounter", @@ -564,14 +564,19 @@ } }, "mapping": { - "observations.0.valueQuantity": { - "expr": "{ \"quantity\": doc.fields.height, \"unit\": \"cm\" }" + "id": "doc._id", + "patient_uuid": "doc.fields.patient_uuid", + "reported_date": "doc.reported_date", + "observations.0.valueQuantity.value": "doc.fields.height", + "observations.0.valueQuantity.unit": { + "expr": "\"cm\"" }, "observations.0.code": { "expr": "\"5090AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"" }, - "observations.1.valueQuantity": { - "expr": "{ \"quantity\": doc.fields.weight, \"unit\": \"kg\" }" + "observations.1.valueQuantity.value": "doc.fields.weight", + "observations.1.valueQuantity.unit": { + "expr": "\"kg\"" }, "observations.1.code": { "expr": "\"5089AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"" @@ -607,20 +612,37 @@ "set_task": true } }, - "height_weight": { + "HEIGHT_WEIGHT": { "meta": { "code": "height_weight", "translation_key": "forms.openmrs_height_weight.title", "icon": "medic-person" }, "fields": { + "patient_uuid": { + "labels": { + "tiny": { + "en": "ID" + }, + "short": { + "translation_key": "patient_id" + } + }, + "position": 0, + "type": "string", + "length": [ + 1, + 13 + ], + "required": true + }, "height": { "labels": { "short": { "en": "Heght in cm" } }, - "position": 0, + "position": 1, "type": "integer", "required": true }, @@ -630,7 +652,7 @@ "en": "Weight in kg" } }, - "position": 1, + "position": 2, "type": "integer", "required": true } From 539f03e94ee682dc0487afd201fe8117e58c0421 Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Fri, 10 Jan 2025 12:03:31 +0300 Subject: [PATCH 66/67] feat: separating test and openmrs job --- startup.sh | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/startup.sh b/startup.sh index f7a3f417..4669b2c7 100755 --- a/startup.sh +++ b/startup.sh @@ -12,18 +12,21 @@ elif [ "$1" == "down" ]; then elif [ "$1" == "destroy" ]; then docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.openmrs.yml down -v elif [ "$1" == "up-test" ]; then + docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.cht-core.yml -d --build +elif [ "$1" == "up-openmrs" ]; then docker compose -p chis-interop -f ./docker/docker-compose.yml -f ./docker/docker-compose.mediator.yml -f ./docker/docker-compose.cht-core.yml -f ./docker/docker-compose.openmrs.yml up -d --build else echo "Invalid option $1 Help: - init starts the docker containers and configures OpenHIM - up starts the docker containers - up-dev starts the docker containers with updated files. - up-test starts the docker containers with updated files, including CHT Core - down stops the docker containers - destroy shutdown the docker containers and deletes volumes + init starts the docker containers and configures OpenHIM + up starts the docker containers + up-dev starts the docker containers with updated files. + up-test starts the docker containers with updated files, including CHT Core + up-openmrs starts the docker containers with updated files, including CHT Core and OpenMRS + down stops the docker containers + destroy shutdown the docker containers and deletes volumes " fi From 07ca30dde7f3e377f2894bf6cfd487c53de13cbd Mon Sep 17 00:00:00 2001 From: Tom Wier Date: Tue, 28 Jan 2025 11:08:45 +0300 Subject: [PATCH 67/67] fix: removing unecessarry test --- mediator/src/utils/openmrs_sync.ts | 8 ---- mediator/src/utils/tests/openmrs_sync.spec.ts | 44 ------------------- 2 files changed, 52 deletions(-) diff --git a/mediator/src/utils/openmrs_sync.ts b/mediator/src/utils/openmrs_sync.ts index 339816ec..ad3788b2 100644 --- a/mediator/src/utils/openmrs_sync.ts +++ b/mediator/src/utils/openmrs_sync.ts @@ -200,14 +200,6 @@ export async function sendEncounterToOpenMRS( return } - // don't send if identifier already exists - const identifier = getIdType(encounter, chtDocumentIdentifierType); - const existingEncounter = await getFhirResourceByIdentifier(identifier, 'Encounter'); - if (existingEncounter?.data?.total > 0) { - logger.error(`Not re-sending encounter from cht ${encounter.id}`); - return - } - logger.info(`Sending Encounter ${encounter.id} to OpenMRS`); const patient = getPatient(encounter, references); diff --git a/mediator/src/utils/tests/openmrs_sync.spec.ts b/mediator/src/utils/tests/openmrs_sync.spec.ts index 2598f165..b82db5fb 100644 --- a/mediator/src/utils/tests/openmrs_sync.spec.ts +++ b/mediator/src/utils/tests/openmrs_sync.spec.ts @@ -307,49 +307,5 @@ describe('OpenMRS Sync', () => { expect(fhir.updateFhirResource).not.toHaveBeenCalled(); }); - - it('does not send outgoing Encounters to OpenMRS if identifier exists in FHIR', async () => { - const lastUpdated = new Date(); - lastUpdated.setMinutes(lastUpdated.getMinutes() - 30); - - const fhirEncounter = EncounterFactory.build(); - fhirEncounter.meta = { lastUpdated: lastUpdated }; - const fhirObservation = ObservationFactory.build(); - fhirObservation.encounter = { reference: 'Encounter/' + fhirEncounter.id } - const chtDocId = { - system: "cht", - type: chtDocumentIdentifierType, - value: getIdType(fhirEncounter, chtDocumentIdentifierType) - } - - jest.spyOn(fhir, 'getFhirResourcesSince').mockResolvedValueOnce({ - data: [fhirEncounter, fhirObservation], - status: 200, - }); - - jest.spyOn(openmrs, 'getOpenMRSResourcesSince').mockResolvedValueOnce({ - data: [], - status: 200, - }); - - jest.spyOn(fhir, 'getFhirResourceByIdentifier').mockResolvedValue({ - data: { total: 1, entry: [ { resource: fhirEncounter } ] }, - status: 200, - }); - - jest.spyOn(openmrs, 'createOpenMRSResource').mockResolvedValue({ - data: [], - status: 201, - }); - - const startTime = new Date(); - startTime.setHours(startTime.getHours() - 1); - const comparison = await syncEncounters(startTime); - - expect(fhir.getFhirResourcesSince).toHaveBeenCalled(); - expect(openmrs.getOpenMRSResourcesSince).toHaveBeenCalled(); - - expect(openmrs.createOpenMRSResource).not.toHaveBeenCalled(); - }); }); });