Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OZ-757: Added Bahmni specific result creation logic. #46

Open
wants to merge 49 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
d1e2989
OZ-757: EIP OpenMRS SENAITE routes to support Lab workflows for Bahmni
Ruhanga Jan 7, 2025
0101116
Minor fixes
Ruhanga Jan 7, 2025
bd13121
Minor fixes
Ruhanga Jan 7, 2025
019bd7e
Debugging
Ruhanga Jan 7, 2025
c78f0b8
Debugging
Ruhanga Jan 7, 2025
32d8f59
Debugging
Ruhanga Jan 7, 2025
cec2a02
Debugging
Ruhanga Jan 7, 2025
c361bd7
Use OpenMRS REST API to save Bahmni Results.
Ruhanga Jan 8, 2025
570766b
Use OpenMRS REST API to save Bahmni Results.
Ruhanga Jan 8, 2025
981dd3d
Use OpenMRS REST API to save Bahmni Results.
Ruhanga Jan 8, 2025
e6a663e
Use OpenMRS REST API to save Bahmni Results.
Ruhanga Jan 8, 2025
3fab5c4
Use OpenMRS REST API to save Bahmni Results.
Ruhanga Jan 8, 2025
702a0ee
Use OpenMRS REST API to save Bahmni Results.
Ruhanga Jan 8, 2025
bab1de7
Use OpenMRS REST API to save Bahmni Results.
Ruhanga Jan 8, 2025
6aa559a
Use OpenMRS REST API to save Bahmni Results.
Ruhanga Jan 8, 2025
21ea820
Use OpenMRS REST API to save Bahmni Results.
Ruhanga Jan 8, 2025
861a1c8
Added some logging
Ruhanga Jan 26, 2025
e48ed31
Associate order to encounter
Ruhanga Jan 27, 2025
288a9bd
Some logging
Ruhanga Jan 27, 2025
e484975
Associate proper panel to obs
Ruhanga Jan 27, 2025
2994ccd
Associate proper panel to obs
Ruhanga Jan 27, 2025
2f73acd
Associate proper panel to obs
Ruhanga Jan 27, 2025
d05267b
Properly add obsgroup members
Ruhanga Jan 27, 2025
e89d9d2
Properly add obsgroup members
Ruhanga Jan 27, 2025
97fcbbd
Properly add obsgroup members
Ruhanga Jan 27, 2025
8bcc8c6
Properly add obsgroup members date
Ruhanga Jan 27, 2025
f852bfd
Properly add obsgroup members date
Ruhanga Jan 27, 2025
3af241b
Properly add obsgroup members date
Ruhanga Jan 27, 2025
2c7ee40
Added option to switch from Bahmni to OpenMRS EMRs.
Ruhanga Jan 27, 2025
f75680a
Reverting previous implemetation
Ruhanga Jan 27, 2025
2ef8ff2
Code cleanup
Ruhanga Jan 27, 2025
0970674
Fixed failing test and code formatting
Ruhanga Jan 27, 2025
dac4e22
Added auto creation of tests
Ruhanga Jan 27, 2025
e37d136
Added auto creation of tests
Ruhanga Jan 27, 2025
777980c
Added auto creation of tests
Ruhanga Jan 27, 2025
8d34c1a
Added auto creation of tests
Ruhanga Jan 27, 2025
9bdc1c0
Added auto creation of tests
Ruhanga Jan 27, 2025
b2c3504
Added auto creation of tests
Ruhanga Jan 27, 2025
0964ceb
Added auto creation of tests
Ruhanga Jan 27, 2025
5ce0d0d
Proper event parsing
Ruhanga Jan 27, 2025
a080499
Proper event parsing
Ruhanga Jan 27, 2025
5e408fe
Proper event parsing
Ruhanga Jan 27, 2025
5b4cb7f
Proper event parsing
Ruhanga Jan 27, 2025
56511cd
Code dlean up
Ruhanga Jan 29, 2025
9374c48
Fix compilation errors
Ruhanga Jan 30, 2025
cc8c3b2
OZ-757: Added Unit tests.
Ruhanga Feb 3, 2025
221eb98
OZ-757: Added code formatting changes.
Ruhanga Feb 3, 2025
03ee415
Code clean up
Ruhanga Feb 6, 2025
b15e9d7
Update pom.xml
Ruhanga Feb 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
<h3 align="center">
<a href="https://docs.ozone-his.com/">Docs</a>&nbsp;&nbsp;&nbsp;&nbsp;•&nbsp;&nbsp;&nbsp;&nbsp;<a href="https://talk.openmrs.org/c/software/ozone-his/70">Forum</a>&nbsp;&nbsp;&nbsp;&nbsp;•&nbsp;&nbsp;&nbsp;&nbsp;<a href="https://openmrs.slack.com/archives/C02PYQD5D0A">Chat Room</a>
</h3>

6 changes: 6 additions & 0 deletions senaite-openmrs/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@
<artifactId>camel-openmrs-fhir</artifactId>
<version>${camel.openmrs.fhir.version}</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.9.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/*
* Copyright © 2021, Ozone HIS <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.ozonehis.eip.openmrs.senaite.handlers.bahmni;

import ca.uhn.fhir.rest.client.api.IGenericClient;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ozonehis.eip.openmrs.senaite.handlers.openmrs.ObservationHandler;
import com.ozonehis.eip.openmrs.senaite.model.analyses.AnalysesDTO;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.camel.ProducerTemplate;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.Encounter;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.ServiceRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Slf4j
@Setter
@Component
public class BahmniResultsHandler {

private static final String UUID_REGEX =
"^[0-9a-fA-F]{36}$|^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$";

private static final Pattern UUID_PATTERN = Pattern.compile(UUID_REGEX);

@Value("${openmrs.baseUrl}")
protected String openmrsBaseUrl;

@Value("${openmrs.username}")
protected String openmrsUsername;

@Value("${openmrs.password}")
protected String openmrsPassword;

@Autowired
private IGenericClient openmrsFhirClient;

@Autowired
private ObservationHandler observationHandler;

public Observation buildAndSendBahmniResultObservation(
ProducerTemplate producerTemplate,
Encounter savedResultEncounter,
ServiceRequest serviceRequest,
ArrayList<AnalysesDTO> analysesDTOs,
String datePublished) {

String panelConceptUuid = serviceRequest.getIdPart();

// Create the top-level map
Map<String, Object> resultMap = new HashMap<>();

resultMap.put("encounter", savedResultEncounter.getIdPart());
resultMap.put("concept", getServiceRequestCodingIdentifier(serviceRequest));
resultMap.put("order", panelConceptUuid);
resultMap.put("person", savedResultEncounter.getSubject().getReference().substring("Patient/".length()));
resultMap.put("obsDatetime", datePublished);

// Create the groupMembers list for the first level
List<Map<String, Object>> groupMembersLevel1 = new ArrayList<>();

for (AnalysesDTO resultAnalysesDTO : analysesDTOs) {

String analysesDescription = resultAnalysesDTO.getDescription();

String testConceptUuid = analysesDescription.substring(
analysesDescription.lastIndexOf("(") + 1, analysesDescription.lastIndexOf(")"));
String analysesResult = resultAnalysesDTO.getResult();
String analysesResultCaptureDate = resultAnalysesDTO.getResultCaptureDate();

// Create a nested map for the first group member
Map<String, Object> groupMember1 = new HashMap<>();
groupMember1.put("concept", testConceptUuid);
groupMember1.put("order", serviceRequest.getIdPart());
groupMember1.put(
"person", savedResultEncounter.getSubject().getReference().substring("Patient/".length()));
groupMember1.put("obsDatetime", analysesResultCaptureDate);

// Create the groupMembers list for the second level (nested inside groupMember1)
List<Map<String, Object>> groupMembersLevel2 = new ArrayList<>();

// Create a nested map for the second group member
Map<String, Object> groupMember2 = new HashMap<>();
groupMember2.put("value", analysesResult);
groupMember2.put("order", serviceRequest.getIdPart());
groupMember2.put(
"person", savedResultEncounter.getSubject().getReference().substring("Patient/".length()));
groupMember2.put("obsDatetime", analysesResultCaptureDate);
groupMember2.put("concept", testConceptUuid);

groupMembersLevel2.add(groupMember2);

groupMember1.put("groupMembers", groupMembersLevel2);

groupMembersLevel1.add(groupMember1);
}

resultMap.put("groupMembers", groupMembersLevel1);

String jsonString = null;
try {
ObjectMapper objectMapper = new ObjectMapper();
jsonString = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(resultMap);
} catch (Exception e) {
throw new RuntimeException("Could not generate Bahmni ENR results payload : ", e);
}

String payload = jsonString;

Map<String, Object> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put("Authorization", "Basic " + encodeBasicAuth(openmrsUsername, openmrsPassword));

String obsEndpointUrl = openmrsBaseUrl + "/ws/rest/v1/obs";
headers.put("obsEndpointUrl", obsEndpointUrl);

String response = producerTemplate.requestBodyAndHeaders(
"direct:create-bahmni-lab-results-route", payload, headers, String.class);

String observationUuid = null;
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(response);
observationUuid = rootNode.get("uuid").asText();
} catch (Exception e) {
throw new RuntimeException("Could not extract Bahmni EMR results observation uuid : ", e);
}

Bundle bundle = openmrsFhirClient
.search()
.forResource(Observation.class)
.where(Observation.RES_ID.exactly().identifier(observationUuid))
.returnBundle(Bundle.class)
.execute();

return bundle.getEntry().stream()
.map(Bundle.BundleEntryComponent::getResource)
.filter(Observation.class::isInstance)
.map(Observation.class::cast)
.findFirst()
.orElse(null);
}

private String encodeBasicAuth(String username, String password) {
String credentials = username + ":" + password;
return new String(java.util.Base64.getEncoder().encode(credentials.getBytes()));
}

public String getServiceRequestCodingIdentifier(ServiceRequest serviceRequest) {
List<Coding> codings = serviceRequest.getCode().getCoding();
for (Coding coding : codings) {
String code = coding.getCode();

// check if the code is a valid UUID
if (isValidUUID(code)) {
return code;
}
// check if it's a LOINC code
if ("http://loinc.org".equals(coding.getSystem())) {
return "LOINC:" + code;
}
// check if it's a CIEL code
if ("https://cielterminology.org".equals(coding.getSystem())) {
return "CIEL:" + code;
}
// check if it's a SNOMED code
if ("http://snomed.info/sct/".equals(coding.getSystem())) {
return "SNOMED CT:" + code;
}
}
return null;
}

// Helper method to check if the code is a valid UUID
private boolean isValidUUID(String code) {
return UUID_PATTERN.matcher(code).matches();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public AnalysisRequestDTO getAnalysisRequestByClientIDAndClientSampleID(
headers.put(Constants.HEADER_CLIENT_SAMPLE_ID, clientSampleID);
String response = producerTemplate.requestBodyAndHeaders(
"direct:senaite-get-analysis-request-route", null, headers, String.class);

TypeReference<SenaiteResponseWrapper<AnalysisRequestItem>> typeReference = new TypeReference<>() {};
SenaiteResponseWrapper<AnalysisRequestItem> responseWrapper = objectMapper.readValue(response, typeReference);
return AnalysisRequestMapper.map(responseWrapper);
Expand All @@ -61,6 +62,7 @@ public AnalysisRequestDTO getAnalysisRequestByClientSampleID(
headers.put(Constants.HEADER_CLIENT_SAMPLE_ID, clientSampleID);
String response = producerTemplate.requestBodyAndHeaders(
"direct:senaite-get-analysis-request-by-client-sample-id-route", null, headers, String.class);

TypeReference<SenaiteResponseWrapper<AnalysisRequestItem>> typeReference = new TypeReference<>() {};
SenaiteResponseWrapper<AnalysisRequestItem> responseWrapper = objectMapper.readValue(response, typeReference);
return AnalysisRequestMapper.map(responseWrapper);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public class AnalysisRequestDTO {

private String dateSampled;

private String datePublished;

private String template; // Template UUID

private String profiles; // Profiles UUID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public static AnalysisRequestDTO map(SenaiteResponseWrapper<AnalysisRequestItem>
analysisRequestDTO.setAnalyses(analysisRequestItem.getAnalyses());
analysisRequestDTO.setClientSampleID(analysisRequestItem.getClientSampleID());
analysisRequestDTO.setReviewState(analysisRequestItem.getReviewState());
analysisRequestDTO.setDatePublished(analysisRequestItem.getDatePublished());
return analysisRequestDTO;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public class AnalysisRequestItem implements SenaiteResource {
@JsonProperty("DateSampled")
private String dateSampled;

@JsonProperty("getDatePublished")
private String datePublished;

@JsonProperty("TemplateUID")
private String templateUid;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package com.ozonehis.eip.openmrs.senaite.processors;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.ozonehis.eip.openmrs.senaite.handlers.bahmni.BahmniResultsHandler;
import com.ozonehis.eip.openmrs.senaite.handlers.openmrs.DiagnosticReportHandler;
import com.ozonehis.eip.openmrs.senaite.handlers.openmrs.EncounterHandler;
import com.ozonehis.eip.openmrs.senaite.handlers.openmrs.ObservationHandler;
Expand Down Expand Up @@ -48,6 +49,9 @@ public class TaskProcessor implements Processor {
@Value("${results.encounterType.uuid}")
private String resultEncounterTypeUUID;

@Value("${run.with.bahmni.emr}")
private String runWithBahmniEmr;

@Autowired
private ServiceRequestHandler serviceRequestHandler;

Expand All @@ -69,6 +73,9 @@ public class TaskProcessor implements Processor {
@Autowired
private DiagnosticReportHandler diagnosticReportHandler;

@Autowired
private BahmniResultsHandler bahmniResultsHandler;

@Override
public void process(Exchange exchange) {
try (ProducerTemplate producerTemplate = exchange.getContext().createProducerTemplate()) {
Expand Down Expand Up @@ -102,7 +109,8 @@ public void process(Exchange exchange) {
getTaskStatusCorrespondingToAnalysisRequestStatus(analysisRequestDTO);
if (analysisRequestTaskStatus != null
&& analysisRequestTaskStatus.equalsIgnoreCase("completed")) {
createResultsInOpenMRS(producerTemplate, serviceRequest, analyses);
createResultsInOpenMRS(
producerTemplate, serviceRequest, analyses, analysisRequestDTO.getDatePublished());
} else {
log.debug(
"TaskProcessor: Nothing to update for task {} with status {}",
Expand Down Expand Up @@ -142,7 +150,7 @@ private String getTaskStatusCorrespondingToAnalysisRequestStatus(AnalysisRequest
}

private void createResultsInOpenMRS(
ProducerTemplate producerTemplate, ServiceRequest serviceRequest, Analyses[] analyses)
ProducerTemplate producerTemplate, ServiceRequest serviceRequest, Analyses[] analyses, String datePublished)
throws JsonProcessingException {
Encounter resultEncounter = encounterHandler.getEncounterByTypeAndSubject(
resultEncounterTypeUUID,
Expand All @@ -151,43 +159,71 @@ private void createResultsInOpenMRS(
&& resultEncounter.getPeriod().getStart().getTime()
== serviceRequest.getOccurrencePeriod().getStart().getTime()) {
// Result Encounter exists
saveObservationAndDiagnosticReport(producerTemplate, serviceRequest, analyses, resultEncounter);
saveObservationAndDiagnosticReport(
producerTemplate, serviceRequest, analyses, resultEncounter, datePublished);
} else {
String encounterID = serviceRequest.getEncounter().getReference().split("/")[1];
Encounter orderEncounter = encounterHandler.getEncounterByEncounterID(encounterID);
Encounter savedResultEncounter =
encounterHandler.sendEncounter(encounterHandler.buildLabResultEncounter(orderEncounter));
saveObservationAndDiagnosticReport(producerTemplate, serviceRequest, analyses, savedResultEncounter);
saveObservationAndDiagnosticReport(
producerTemplate, serviceRequest, analyses, savedResultEncounter, datePublished);
}
}

private void saveObservationAndDiagnosticReport(
ProducerTemplate producerTemplate,
ServiceRequest serviceRequest,
Analyses[] analyses,
Encounter savedResultEncounter)
Encounter savedResultEncounter,
String datePublished)
throws JsonProcessingException {
String subjectID = serviceRequest.getSubject().getReference().split("/")[1];
ArrayList<String> observationUuids = new ArrayList<>();

ArrayList<AnalysesDTO> analysesDTOs = new ArrayList<>();
for (Analyses analysis : analyses) {
AnalysesDTO resultAnalysesDTO =
analysesHandler.getAnalysesByAnalysesApiUrl(producerTemplate, analysis.getAnalysesApiUrl());
String analysesDescription = resultAnalysesDTO.getDescription();
String conceptUuid = analysesDescription.substring(
analysesDescription.lastIndexOf("(") + 1, analysesDescription.lastIndexOf(")"));
analysesDTOs.add(resultAnalysesDTO);
}

if (Boolean.parseBoolean(runWithBahmniEmr)) {
Observation savedObservation = observationHandler.getObservationByCodeSubjectEncounterAndDate(
conceptUuid, subjectID, savedResultEncounter.getIdPart(), resultAnalysesDTO.getResultCaptureDate());
bahmniResultsHandler.getServiceRequestCodingIdentifier(serviceRequest),
subjectID,
savedResultEncounter.getIdPart(),
datePublished);
if (!observationHandler.doesObservationExists(savedObservation)) {
// Create result Observation
savedObservation = observationHandler.sendObservation(observationHandler.buildResultObservation(
savedResultEncounter,
conceptUuid,
resultAnalysesDTO.getResult(),
resultAnalysesDTO.getResultCaptureDate()));
// Create Bahmni result Observation
savedObservation = bahmniResultsHandler.buildAndSendBahmniResultObservation(
producerTemplate, savedResultEncounter, serviceRequest, analysesDTOs, datePublished);
}
observationUuids.add(savedObservation.getIdPart());
} else {
for (AnalysesDTO resultAnalysesDTO : analysesDTOs) {

String analysesDescription = resultAnalysesDTO.getDescription();
String conceptUuid = analysesDescription.substring(
analysesDescription.lastIndexOf("(") + 1, analysesDescription.lastIndexOf(")"));

Observation savedObservation = observationHandler.getObservationByCodeSubjectEncounterAndDate(
conceptUuid,
subjectID,
savedResultEncounter.getIdPart(),
resultAnalysesDTO.getResultCaptureDate());
if (!observationHandler.doesObservationExists(savedObservation)) {
// Create result Observation
savedObservation = observationHandler.sendObservation(observationHandler.buildResultObservation(
savedResultEncounter,
conceptUuid,
resultAnalysesDTO.getResult(),
resultAnalysesDTO.getResultCaptureDate()));
}
observationUuids.add(savedObservation.getIdPart());
}
}

diagnosticReportHandler.sendDiagnosticReport(diagnosticReportHandler.buildDiagnosticReport(
observationUuids, serviceRequest, savedResultEncounter.getIdPart()));
}
Expand Down
Loading
Loading