diff --git a/api/src/main/java/org/openmrs/module/ugandaemrsync/api/UgandaEMRSyncService.java b/api/src/main/java/org/openmrs/module/ugandaemrsync/api/UgandaEMRSyncService.java index 34cf0348..5b59d4c0 100644 --- a/api/src/main/java/org/openmrs/module/ugandaemrsync/api/UgandaEMRSyncService.java +++ b/api/src/main/java/org/openmrs/module/ugandaemrsync/api/UgandaEMRSyncService.java @@ -154,6 +154,27 @@ public interface UgandaEMRSyncService extends OpenmrsService { public Encounter addVLToEncounter(String vlQualitative, String vlQuantitative, String vlDate, Encounter encounter, Order order); + + /** + * Saves an EID qualitative test result and return date to an encounter. + * + * If a valid POSITIVE or NEGATIVE result is provided, the method: + * + * + * If EID data already exists, the associated order is discontinued + * and no new observations are created. + * + * @param vlQualitative qualitative EID result (POSITIVE or NEGATIVE) + * @param encounter encounter to update + * @param order associated lab order + * @return updated encounter, or {@code null} if the result is invalid + */ + public Encounter addEIDToEncounter(String vlQualitative, Encounter encounter, Order order); + /** * @param vlDate * @return @@ -502,10 +523,27 @@ public Encounter addVLToEncounter(String vlQualitative, String vlQuantitative, S public Map sendSingleViralLoadOrder(Order order); + /** + * Pulls lab results from CPHL for the given order (or resolves the order from the sync task), + * and persists the results into the patient's encounter (VL or EID). + * + * For successful, non-pending responses, the method saves the result obs, marks the sync task + * as completed, logs the transaction, and completes/discontinues the order. + * + * @param order the lab order to fetch results for (may be null if syncTask is provided) + * @param syncTask the sync task containing the sample/accession identifier (may be null if order is provided) + * @return response map containing at least "responseMessage" describing the outcome + */ public Map requestLabResult(Order order, SyncTask syncTask); public Date getDateFromString(String dateString, String format); + /** + * Resolves an {@link Order} using an accession/sample identifier. + * + * @param assessionNumber accession/sample identifier + * @return matching order, or null if none is found + */ public Order getOrderByAccessionNumber(String assessionNumber); public boolean validateTestFHIRBundle(String bundleJson,String orderConceptUuid); @@ -514,7 +552,7 @@ public Encounter addVLToEncounter(String vlQualitative, String vlQuantitative, S public String getMissingVLFHIRCodesAsString(String bundleJson,String orderConceptUuid); - public Concept getVLMissingCconcept(String code); + public Concept getVLMissingConcept(String code); public List getReferralOrderConcepts(); diff --git a/api/src/main/java/org/openmrs/module/ugandaemrsync/api/impl/UgandaEMRSyncServiceImpl.java b/api/src/main/java/org/openmrs/module/ugandaemrsync/api/impl/UgandaEMRSyncServiceImpl.java index 57d66e44..1a1aa399 100644 --- a/api/src/main/java/org/openmrs/module/ugandaemrsync/api/impl/UgandaEMRSyncServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/ugandaemrsync/api/impl/UgandaEMRSyncServiceImpl.java @@ -11,12 +11,12 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; +import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Observation; -import org.hl7.fhir.r4.model.ServiceRequest; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -38,6 +38,7 @@ import org.openmrs.PersonAttribute; import org.openmrs.PersonAddress; import org.openmrs.Provider; +import org.openmrs.ConceptReferenceTerm; import org.openmrs.Person; import org.openmrs.ProviderAttributeType; import org.openmrs.ProviderAttribute; @@ -294,6 +295,74 @@ public Encounter addVLToEncounter(String vlQualitative, String vlQuantitative, S } } + /** + * @see UgandaEMRSyncService#addEIDToEncounter(String, Encounter, Order) + */ + public Encounter addEIDToEncounter(String vlQualitative, Encounter encounter, Order order) { + if (encounter == null) { + return null; + } + + // If already saved, just discontinue the order (if applicable) and return + if (encounterHasVLDataAlreadySaved(encounter, order)) { + discontinueOrderIfActive(order); + return encounter; + } + + // Concepts + Concept testResultConcept = Context.getConceptService().getConcept(844); // EID result concept + Concept returnDateConcept = Context.getConceptService().getConcept(167944); // return date concept + + // Normalize incoming result string + String result = (vlQualitative == null) ? "" : vlQualitative.replace("\"", "").trim().toUpperCase(); + + Concept valueCoded = null; + if ("POSITIVE".equals(result)) { + valueCoded = Context.getConceptService().getConcept(703); + } else if ("NEGATIVE".equals(result)) { + valueCoded = Context.getConceptService().getConcept(664); + } else { + // Unknown/unhandled result -> don't save anything + return null; + } + + // Void similar observations before adding new ones + voidObsFound(encounter, testResultConcept); + voidObsFound(encounter, returnDateConcept); + + Obs testResultObs = createObs(encounter, order, testResultConcept, valueCoded, null, null); + Obs returnDateObs = createObs(encounter, order, returnDateConcept, null, new Date(), null); + + encounter.addObs(testResultObs); + encounter.addObs(returnDateObs); + + return Context.getEncounterService().saveEncounter(encounter); + } + + /** + * Discontinues an active laboratory order. + * + * @param order order to discontinue + */ + private void discontinueOrderIfActive(Order order) { + if (order == null || !order.isActive()) { + return; + } + try { + Context.getOrderService().discontinueOrder( + order, + "Completed", + new Date(), + order.getOrderer(), + order.getEncounter() + ); + } catch (Exception e) { + log.error("Failed to discontinue order", e); + } + } + + + public String getDateFormat(String date) { String dateFormat = ""; if (date.contains("-")) { @@ -431,13 +500,34 @@ public String getHealthCenterName() { * @see UgandaEMRSyncService#getPatientIdentifier(Patient, String) */ public String getPatientIdentifier(Patient patient, String patientIdentifierTypeUUID) { - String query = "select patient_identifier.identifier from patient_identifier inner join patient_identifier_type on(patient_identifier.identifier_type=patient_identifier_type.patient_identifier_type_id) where patient_identifier_type.uuid in ('" + patientIdentifierTypeUUID + "') AND patient_id=" + patient.getPatientId() + ""; - List list = Context.getAdministrationService().executeSQL(query, true); - String patientARTNO = ""; - if (!list.isEmpty()) { - patientARTNO = list.get(0).toString().replace("[", "").replace("]", ""); + if (patient == null || patient.getPatientId() == null || patientIdentifierTypeUUID == null) { + return ""; + } + + // Split comma-separated UUIDs + String[] uuids = patientIdentifierTypeUUID.split(","); + + // Validate UUIDs to avoid SQL injection + String inClause = Arrays.stream(uuids).map(String::trim).filter(u -> u.matches("^[0-9a-fA-F\\-]{36}$")).map(u -> "'" + u + "'").collect(Collectors.joining(",")); + + if (inClause.isEmpty()) { + return ""; } - return patientARTNO; + + String sql = "select pi.identifier from patient_identifier pi inner join patient_identifier_type pit on (pi.identifier_type = pit.patient_identifier_type_id) where pit.uuid in (" + inClause + ") and pi.patient_id = " + patient.getPatientId() + " and pi.voided = 0 order by pi.preferred desc, pi.date_created desc limit 1"; + + List list = Context.getAdministrationService().executeSQL(sql, true); + + if (list == null || list.isEmpty() || list.get(0) == null) { + return ""; + } + + Object result = list.get(0); + if (result instanceof Object[]) { + return ((Object[]) result)[0].toString().replace("[", "").replace("]", ""); + } + + return result.toString().replace("[", "").replace("]", ""); } public boolean encounterHasVLDataAlreadySaved(Encounter encounter, Order order) { @@ -450,6 +540,7 @@ public boolean encounterHasVLDataAlreadySaved(Encounter encounter, Order order) } } + public Properties getUgandaEMRProperties() { Properties properties = new Properties(); String appDataDir = OpenmrsUtil.getApplicationDataDirectory(); @@ -1585,6 +1676,21 @@ private List getStockOperationsByExternalReference(String extern return stockOperations; } + + /** + * Records an outbound/inbound transaction related to lab result fetching, including + * response status and payload summary for auditing and troubleshooting. + * + * @param syncTaskType sync task type configuration (endpoint details) + * @param statusCode HTTP-like response code or internal error code + * @param statusMessage summary message to log + * @param logName accession/sample identifier or order reference + * @param status response details or server message + * @param date time of the transaction + * @param url endpoint URL + * @param actionRequired whether request was successful (implementation-specific) + * @param actionCompleted whether results were processed/saved (implementation-specific) + */ private void logTransaction(SyncTaskType syncTaskType, Integer statusCode, String statusMessage, String logName, String status, Date date, String url, boolean actionRequired, boolean actionCompleted) { UgandaEMRSyncService ugandaEMRSyncService = Context.getService(UgandaEMRSyncService.class); List syncTasks = ugandaEMRSyncService.getSyncTasksBySyncTaskId(logName).stream().filter(syncTask -> syncTask.getSyncTaskType().equals(syncTaskType)).collect(Collectors.toList()); @@ -2400,15 +2506,72 @@ public List> generateAndSyncBulkViralLoadRequest() { return responses; } - public Map generateVLFHIRResultRequestBody(String jsonRequestString, String healthCenterCode, String patientIdentifier, String sampleIdentifier) { + + /** + * Builds the FHIR JSON payload used to request lab results from CPHL for a sample. + * + * @param healthCenterCode the sending facility code + * @param patientIdentifier the patient identifier value to include in the request + * @param sampleIdentifier the sample/accession identifier used to query results + * @param conceptMaps concept mappings used to populate coding/mapping fields (e.g., UNHLS mappings) + * @return map containing generated payload values; expected key "json" holds the final request body + */ + private Map generateVLFHIRResultRequestBody(String healthCenterCode, String patientIdentifier, String sampleIdentifier, List conceptMaps) { + Map jsonMap = new HashMap<>(); - String filledJsonFile = ""; - filledJsonFile = String.format(jsonRequestString, healthCenterCode, patientIdentifier, sampleIdentifier); + + String codingJson = buildCodingArray(conceptMaps); + + String filledJsonFile = String.format(org.openmrs.module.ugandaemrsync.server.SyncConstant.VL_RECEIVE_RESULT_FHIR_JSON_STRING, healthCenterCode, codingJson, patientIdentifier, sampleIdentifier); + jsonMap.put("json", filledJsonFile); return jsonMap; } + private String buildCodingArray(List conceptMaps) { + if (conceptMaps == null || conceptMaps.isEmpty()) { + return ""; + } + + return conceptMaps.stream() + .filter(cm -> !cm.getConceptReferenceTerm().getRetired()) + .map(this::toCodingJson) + .collect(Collectors.joining(",")); + } + + private String toCodingJson(ConceptMap conceptMap) { + ConceptReferenceTerm term = conceptMap.getConceptReferenceTerm(); + ConceptSource source = term.getConceptSource(); + + String system; + + if ("UNHLS".equalsIgnoreCase(source.getName()) + || "UNHLS".equalsIgnoreCase(source.getHl7Code())) { + system = "http://cphl.go.ug/fhir"; + } else if (StringUtils.isNotBlank(source.getHl7Code())) { + system = source.getHl7Code(); + } else { + system = source.getName(); + } + + return String.format( + "{ \"system\": \"%s\", \"code\": \"%s\", \"display\": \"%s\" }", + escape(system), + escape(term.getCode()), + escape(term.getName()) + ); + } + + private String escape(String value) { + if (value == null) { + return ""; + } + return value.replace("\\", "\\\\") + .replace("\"", "\\\""); + } + + @Override public Map sendSingleViralLoadOrder(Order order) { Map response = new HashMap<>(); @@ -2432,11 +2595,11 @@ public Map sendSingleViralLoadOrder(Order order) { String payload = processResourceFromOrder(order); if (payload != null) { - if (!validateTestFHIRBundle(payload, order.getConcept().getUuid())) { + if (!validateTestFHIRBundle(payload,order.getConcept().getUuid())) { String missingObsInPayload = String.format( "Order: %s is not valid due to missing %s in the required field", order.getAccessionNumber(), - getMissingVLFHIRCodesAsString(payload, order.getConcept().getUuid()) + getMissingVLFHIRCodesAsString(payload,order.getConcept().getUuid()) ); logTransaction(syncTaskType, 500, missingObsInPayload, order.getAccessionNumber(), missingObsInPayload, @@ -2459,97 +2622,182 @@ public Map sendSingleViralLoadOrder(Order order) { @Override public Map requestLabResult(Order order, SyncTask syncTask) { - UgandaEMRHttpURLConnection ugandaEMRHttpURLConnection = new UgandaEMRHttpURLConnection(); - Map response = new HashMap<>(); - if (!ugandaEMRHttpURLConnection.isConnectionAvailable()) { - response.put("responseMessage", "No Internet Connection to send order" + order.getAccessionNumber()); + Map response = new HashMap<>(); + UgandaEMRHttpURLConnection connection = new UgandaEMRHttpURLConnection(); + + if (!connection.isConnectionAvailable()) { + response.put("responseMessage", "No Internet Connection to send order " + safeAccession(order)); return response; } - if (order == null && syncTask != null) { - order = getOrderByAccessionNumber(syncTask.getSyncTask()); - if (order == null) { - response.put("responseMessage", "Order Not found for accession number: " + syncTask.getSyncTask()); - log.info("Order Not found for accession number: " + syncTask.getSyncTask()); - return response; - } + order = resolveOrder(order, syncTask, response); + if (order == null) { + return response; } - String dataOutput = generateVLFHIRResultRequestBody(VL_RECEIVE_RESULT_FHIR_JSON_STRING, getHealthCenterCode(), getPatientIdentifier(order.getEncounter().getPatient(), PATIENT_IDENTIFIER_TYPE), String.valueOf(syncTask.getSyncTask())).get("json"); - - Map results = new HashMap(); + Map requestPayload = buildRequestPayload(order, syncTask); + if (requestPayload == null) { + response.put("responseMessage", "Failed to generate request payload"); + return response; + } SyncTaskType syncTaskType = getSyncTaskTypeByUUID(VIRAL_LOAD_RESULT_PULL_TYPE_UUID); + Map results = sendRequest(connection, syncTaskType, requestPayload, order, response); + + if (results.isEmpty()) { + return response; + } + + processResults(results, order, syncTask, syncTaskType, response); + return response; + } + + + private Map buildRequestPayload(Order order, SyncTask syncTask) { + List unhlsMappings = order.getConcept() + .getConceptMappings() + .stream() + .filter(cm -> "UNHLS".equals( + cm.getConceptReferenceTerm() + .getConceptSource() + .getHl7Code())) + .collect(Collectors.toList()); + + return generateVLFHIRResultRequestBody(getHealthCenterCode(), getPatientIdentifier(order.getEncounter().getPatient(), PATIENT_IDENTIFIER_TYPE + "," + EID_IDENTIFIER_TYPE), String.valueOf(syncTask.getSyncTask()), unhlsMappings); + } + + private Map sendRequest(UgandaEMRHttpURLConnection connection, SyncTaskType syncTaskType, Map payload, Order order, Map response) { try { - results = ugandaEMRHttpURLConnection.sendPostBy(syncTaskType.getUrl(), syncTaskType.getUrlUserName(), syncTaskType.getUrlPassword(), "", dataOutput, false); + return connection.sendPostBy(syncTaskType.getUrl(), syncTaskType.getUrlUserName(), syncTaskType.getUrlPassword(), "", payload.get("json").toString(), false); } catch (Exception e) { log.error("Failed to fetch results", e); - logTransaction(syncTaskType, 500, e.getMessage(), order.getAccessionNumber(), e.getMessage(), new Date(), syncTaskType.getUrl(), false, false); + logTransaction(syncTaskType, 500, e.getMessage(), safeAccession(order), e.getMessage(), new Date(), syncTaskType.getUrl(), false, false); response.put("responseMessage", e.getMessage()); + return Collections.emptyMap(); } + } + + private void processResults(Map results, Order order, SyncTask syncTask, SyncTaskType syncTaskType, Map response) { + + Integer responseCode = parseInt(results.get("responseCode")); + String responseMessage = String.valueOf(results.get("responseMessage")); - Integer responseCode = null; - String responseMessage = null; + response.put("responseMessage", responseMessage); - // Parsing responseCode and responseMessage - if (results.containsKey("responseCode") && results.containsKey("responseMessage")) { - responseCode = Integer.parseInt(results.get("responseCode").toString()); - responseMessage = results.get("responseMessage").toString(); - response.put("responseMessage", responseMessage); + if (responseCode == null || responseCode < 200 || responseCode > 299) { + String message = String.format("CPHL server response for order %s: %s", order.getAccessionNumber(), responseMessage); + + logTransaction(syncTaskType, responseCode != null ? responseCode : 500, message, order.getAccessionNumber(), message, new Date(), syncTaskType.getUrl(), false, false); + + response.put("responseMessage", message); + return; } - // Processing results if responseCode is valid and status is not pending - if (responseCode != null && (responseCode == 200 || responseCode == 201) && !results.isEmpty() && results.containsKey("status") && !results.get("status").equals("pending")) { - Map reasonReference = (Map) results.get("reasonReference"); - ArrayList result = (ArrayList) reasonReference.get("result"); + if (results.containsKey("status") && "pending".equals(results.get("status"))) { + String message = String.format("CPHL response: Results for order %s are pending", order.getAccessionNumber()); - // Saving Viral Load Results - if (order.getEncounter() != null && !result.isEmpty()) { - Object qualitativeResult = result.get(0).get("valueString"); - Object quantitativeResult = result.get(0).get("valueInteger"); + logTransaction(syncTaskType, responseCode, message, order.getAccessionNumber(), message, new Date(), syncTaskType.getUrl(), false, false); - if (quantitativeResult != null && qualitativeResult != null) { - try { - addVLToEncounter(qualitativeResult.toString(), quantitativeResult.toString(), order.getEncounter().getEncounterDatetime().toString(), order.getEncounter(), order); - syncTask.setActionCompleted(true); - saveSyncTask(syncTask); - logTransaction(syncTaskType, responseCode, result.get(0).get("valueString").toString(), order.getAccessionNumber(), result.get(0).get("valueString").toString(), new Date(), syncTaskType.getUrl(), false, false); - try { - Context.getOrderService().updateOrderFulfillerStatus(order, Order.FulfillerStatus.COMPLETED, result.get(0).get("valueString").toString()); - Context.getOrderService().discontinueOrder(order, "Completed", new Date(), order.getOrderer(), order.getEncounter()); - } catch (Exception e) { - log.error("Failed to discontinue order", e); - response.put("responseMessage", String.format("Failed to discontinue order %s", e.getMessage())); - } - } catch (Exception e) { - log.error("Failed to add results to patient encounter", e); - logTransaction(syncTaskType, 500, e.getMessage(), order.getAccessionNumber(), e.getMessage(), new Date(), syncTaskType.getUrl(), false, false); - response.put("responseMessage", String.format("Failed to add results to patient encounter %s", e.getMessage())); - } - } else { - logTransaction(syncTaskType, 500, "Internal server error: Results of Viral load have a null value", order.getAccessionNumber(), "Internal server error: Results of Viral load have a null value", new Date(), syncTaskType.getUrl(), false, false); + response.put("responseMessage", message); + return; + } - response.put("responseMessage", String.format("Internal server error: Results of Viral load order %s have a null value", order.getAccessionNumber())); - } - } - } else { - // Logging based on responseCode or status - if (responseCode != null && !results.containsKey("status")) { - String detailedResponseMessage = String.format("CPHL Server Response for order: %s while fetching results: %s", order.getAccessionNumber(), responseMessage); - logTransaction(syncTaskType, responseCode, detailedResponseMessage, order.getAccessionNumber(), detailedResponseMessage, new Date(), syncTaskType.getUrl(), false, false); - response.put("responseMessage", detailedResponseMessage); - } else if (results.containsKey("status")) { - - String detailedResponseMessage = String.format("CPHL Response : Results for order: %s are %s", order.getAccessionNumber(), results.get("status").toString()); - logTransaction(syncTaskType, responseCode, detailedResponseMessage, order.getAccessionNumber(), detailedResponseMessage, new Date(), syncTaskType.getUrl(), false, false); - response.put("responseMessage", detailedResponseMessage); + + Map reasonReference = (Map) results.get("reasonReference"); + List labResults = (List) reasonReference.get("result"); + + if (labResults == null || labResults.isEmpty()) { + response.put("responseMessage", "No lab results returned"); + return; + } + + Map firstResult = labResults.get(0); + Object qualitative = firstResult.get("valueString"); + Object quantitative = firstResult.get("valueInteger"); + + try { + if (isViralLoad(order) && qualitative != null && quantitative != null) { + addVLToEncounter(qualitative.toString(), quantitative.toString(), order.getEncounter().getEncounterDatetime().toString(), order.getEncounter(), order); + } else if (isEID(order) && qualitative != null) { + addEIDToEncounter(qualitative.toString(), order.getEncounter(), order); + } else { + throw new IllegalStateException("Invalid or incomplete lab result"); } + + completeSyncTask(order, syncTask, syncTaskType, responseCode, qualitative.toString()); + } catch (Exception e) { + log.error("Failed to add results to encounter", e); + logTransaction(syncTaskType, 500, e.getMessage(), safeAccession(order), e.getMessage(), new Date(), syncTaskType.getUrl(), false, false); + response.put("responseMessage", e.getMessage()); } - return response; } + private void completeSyncTask(Order order, SyncTask syncTask, SyncTaskType syncTaskType, Integer responseCode, String resultValue) { + + syncTask.setActionCompleted(true); + saveSyncTask(syncTask); + + logTransaction(syncTaskType, responseCode, resultValue, order.getAccessionNumber(), resultValue, new Date(), syncTaskType.getUrl(), false, false); + + try { + Context.getOrderService().updateOrderFulfillerStatus(order, Order.FulfillerStatus.COMPLETED, resultValue); + Context.getOrderService().discontinueOrder(order, "Completed", new Date(), order.getOrderer(), order.getEncounter()); + } catch (Exception e) { + log.error("Failed to discontinue order", e); + } + } + + + private Order resolveOrder(Order order, SyncTask syncTask, Map response) { + if (order != null) { + return order; + } + + if (syncTask == null) { + response.put("responseMessage", "Order and SyncTask cannot both be null"); + return null; + } + + Order resolved = getOrderByAccessionNumber(syncTask.getSyncTask()); + if (resolved == null) { + String msg = "Order not found for accession number: " + syncTask.getSyncTask(); + log.info(msg); + response.put("responseMessage", msg); + } + return resolved; + } + + + private boolean isViralLoad(Order order) { + return order.getConcept().getConceptId() == 165412; + } + + private boolean isEID(Order order) { + return order.getConcept().getConceptId() == 844; + } + + private boolean isSuccessful(Integer code, Map results) { + return code != null && (code == 200 || code == 201) && !"pending".equals(results.get("status")); + } + + private String safeAccession(Order order) { + return order != null ? order.getAccessionNumber() : "UNKNOWN"; + } + + private Integer parseInt(Object value) { + try { + return value == null ? null : Integer.parseInt(value.toString()); + } catch (Exception e) { + return null; + } + } + + + /** + * @see UgandaEMRSyncService#getOrderByAccessionNumber(String) + */ public Order getOrderByAccessionNumber(String accessionNumber) { OrderService orderService = Context.getOrderService(); List list = Context.getAdministrationService().executeSQL(String.format(VIRAL_LOAD_ORDER_QUERY, accessionNumber), true); @@ -2668,18 +2916,25 @@ private String addResourceToBundle(String resourceString) { private String processResourceFromOrder(Order order) { String healthCenterIdentifier = ""; - try { healthCenterIdentifier = Context.getAdministrationService().getGlobalProperty(GP_DHIS2); } - catch (Exception e) { log.error("Failed to fetch DHIS2 identifier", e); } + try { + healthCenterIdentifier = Context.getAdministrationService().getGlobalProperty(GP_DHIS2); + } catch (Exception e) { + log.error("Failed to fetch DHIS2 identifier", e); + } SyncFHIRRecord syncFHIRRecord = new SyncFHIRRecord(); Collection resources = new ArrayList<>(); String finalCaseBundle = null; - List orderList = new ArrayList<>(); orderList.add(order); - List encounter = new ArrayList<>(); encounter.add(order.getEncounter()); + List orderList = new ArrayList<>(); + orderList.add(order); + List encounter = new ArrayList<>(); + encounter.add(order.getEncounter()); - List patientArrayList = new ArrayList<>(); patientArrayList.add(order.getPatient().getPatientIdentifier()); - List personList = new ArrayList<>(); personList.add(order.getPatient().getPerson()); + List patientArrayList = new ArrayList<>(); + patientArrayList.add(order.getPatient().getPatientIdentifier()); + List personList = new ArrayList<>(); + personList.add(order.getPatient().getPerson()); String specimenSource = generateSpecimen(order); @@ -2689,6 +2944,12 @@ private String processResourceFromOrder(Order order) { resources.addAll(syncFHIRRecord.groupInCaseBundle("Practitioner", syncFHIRRecord.getPractitionerResourceBundle(null, encounter, orderList), "HIV Clinic No.")); resources.addAll(syncFHIRRecord.groupInCaseBundle("Observation", syncFHIRRecord.getObservationResourceBundle(null, encounter, personList), "HIV Clinic No.")); + List relatedPersons = getRelatedPersonsFromOrder(order); + + if (!relatedPersons.isEmpty()) { + resources.addAll(syncFHIRRecord.groupInCaseBundle("RelatedPerson", syncFHIRRecord.getRelatedPerson(null, getRelatedPersonsFromOrder(order), null), "HIV Clinic No.")); + } + if (specimenSource != null) resources.add(specimenSource); if (!resources.isEmpty() && healthCenterIdentifier != null && !healthCenterIdentifier.isEmpty() && order.getAccessionNumber() != null) { @@ -2700,6 +2961,77 @@ private String processResourceFromOrder(Order order) { return finalCaseBundle; } + private Set getRelatedPersonConceptUuids(String orderConceptUuid) { + if (StringUtils.isBlank(orderConceptUuid)) { + return Collections.emptySet(); + } + + String gpValue = Context.getAdministrationService().getGlobalProperty("ugandaemrsync.testReferralValidators"); + + if (StringUtils.isBlank(gpValue)) { + log.warn("Global property ugandaemrsync.testReferralValidators is not configured"); + return Collections.emptySet(); + } + + try { + JSONObject root = new JSONObject(gpValue); + + if (!root.has("relatedPersonConceptIdentifier")) { + log.warn("Missing 'relatedPersonConceptIdentifier' in testReferralValidators GP"); + return Collections.emptySet(); + } + + JSONObject relatedPersonConfig = root.getJSONObject("relatedPersonConceptIdentifier"); + + if (!relatedPersonConfig.has(orderConceptUuid)) { + log.debug("No related person identifiers configured for order concept " + orderConceptUuid); + return Collections.emptySet(); + } + + String codesCsv = relatedPersonConfig.getString(orderConceptUuid); + + return Arrays.stream(codesCsv.split(",")) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .map(this::getVLMissingConcept) + .filter(Objects::nonNull) + .map(Concept::getUuid) + .collect(Collectors.toSet()); + + } catch (JSONException e) { + log.error("Invalid JSON in global property ugandaemrsync.testReferralValidators", e); + return Collections.emptySet(); + } + } + + private List getRelatedPersonsFromOrder(Order order) { + if (order == null || order.getEncounter() == null) { + return Collections.emptyList(); + } + + Set relatedConceptUuids = getRelatedPersonConceptUuids(order.getConcept().getUuid()); + if (relatedConceptUuids.isEmpty()) { + return Collections.emptyList(); + } + + PatientService patientService = Context.getPatientService(); + + return order.getEncounter() + .getAllObs(false) + .stream() + .filter(obs -> obs.getConcept() != null) + .filter(obs -> relatedConceptUuids.contains(obs.getConcept().getUuid())) + .map(Obs::getValueText) + .filter(StringUtils::isNotBlank) + .flatMap(identifier -> + patientService + .getPatientIdentifiers(identifier, null, null, null, false) + .stream() + ) + .map(pi -> pi.getPatient().getPerson()) + .distinct() + .collect(Collectors.toList()); + } private Collection addSpecimenSource(Collection serviceRequests, Order order) { @@ -2862,6 +3194,8 @@ private List getTargetCodes(String orderConceptUuid) { return targetCodes; } + + public String getMissingVLFHIRCodesAsString(String bundleJson, String orderConceptUuid) { List targetCodes = getTargetCodes(orderConceptUuid); @@ -2890,7 +3224,7 @@ public String getMissingVLFHIRCodesAsString(String bundleJson, String orderConce for (String code : targetCodes) { if (!foundCodes.contains(code)) { - Concept concept = getVLMissingCconcept(code); + Concept concept = getVLMissingConcept(code); if (concept != null) { missingCodes.add(concept.getName().getName()); } else { @@ -2904,41 +3238,57 @@ public String getMissingVLFHIRCodesAsString(String bundleJson, String orderConce return missingCodes.stream().collect(Collectors.joining(",")); } - public Concept getVLMissingCconcept(String code) { - Concept loinc = Context.getConceptService().getConceptByMapping(code, "LOINC"); - Concept cphl = Context.getConceptService().getConceptByMapping(code, "UNHLS"); - Concept snomed = Context.getConceptService().getConceptByMapping(code, "SNOMED"); - - if (loinc != null) { - return loinc; + public Concept getVLMissingConcept(String code) { + if (StringUtils.isBlank(code)) { + return null; } - if (cphl != null) { - return cphl; + ConceptService conceptService = Context.getConceptService(); + + Concept concept = conceptService.getConceptByMapping(code, "LOINC"); + if (concept != null) { + return concept; } - if (snomed != null) { - return snomed; + + concept = conceptService.getConceptByMapping(code, "UNHLS"); + if (concept != null) { + return concept; } - return null; + return conceptService.getConceptByMapping(code, "SNOMED"); } + public boolean isValidCPHLBarCode(String accessionNumber) { - Integer minimumCPHLBarCodeLength = Integer.parseInt(Context.getAdministrationService().getGlobalProperty("ugandaemrsync.minimumCPHLBarCodeLength")); - if (accessionNumber == null || accessionNumber.length() < minimumCPHLBarCodeLength) { + if (StringUtils.isBlank(accessionNumber)) { return false; } - int currentYearSuffix = Year.now().getValue() % 100; + accessionNumber = accessionNumber.trim(); + + // Ensure barcode is numeric (without risking integer overflow) + if (!accessionNumber.matches("\\d+")) { + return false; + } + String minLengthGp = Context.getAdministrationService() + .getGlobalProperty("ugandaemrsync.minimumCPHLBarCodeLength"); + + if (StringUtils.isBlank(minLengthGp)) { + return false; + } + + int minimumCPHLBarCodeLength; try { - int prefix = Integer.parseInt(accessionNumber.substring(0, 2)); - return prefix == currentYearSuffix || prefix == (currentYearSuffix - 1); + minimumCPHLBarCodeLength = Integer.parseInt(minLengthGp); } catch (NumberFormatException e) { return false; } + + return accessionNumber.length() >= minimumCPHLBarCodeLength; } + public List getReferralOrderConcepts() { List referralOrderConceptList = new ArrayList<>(); diff --git a/api/src/main/java/org/openmrs/module/ugandaemrsync/server/SyncConstant.java b/api/src/main/java/org/openmrs/module/ugandaemrsync/server/SyncConstant.java index 03c54cf1..e8c1bbc2 100644 --- a/api/src/main/java/org/openmrs/module/ugandaemrsync/server/SyncConstant.java +++ b/api/src/main/java/org/openmrs/module/ugandaemrsync/server/SyncConstant.java @@ -105,6 +105,7 @@ public class SyncConstant { public static final String SMS_APPOINTMENT_TYPE_UUID = "08c5be38-1b79-4e27-b9ca-5da709aef5fe"; public static final String PATIENT_IDENTIFIER_TYPE = "e1731641-30ab-102d-86b0-7a5022ba4115"; + public static final String EID_IDENTIFIER_TYPE = "2c5b695d-4bf3-452f-8a7c-fe3ee3432ffe"; public static final String UIC_IDENTIFIER_TYPE = "877169c4-92c6-4cc9-bf45-1ab95faea242"; public static final int VL_SAMPLE_ID_CELL_NO = 1; @@ -128,7 +129,7 @@ public class SyncConstant { public static final String VL_SEND_PROGRAM_DATA_FHIR_JSON_STRING="{\"resourceType\":\"Observation\",\"status\":\"final\",\"code\":{\"text\":\"Treatment Information\"},\"subject\":{\"reference\":\"%s\",\"type\":\"Patient\"},\"specimen\":{\"resourceType\":\"Specimen\",\"identifier\":\"%s\"},\"contained\":[{\"resourceType\":\"Patient\",\"id\":\"%s\",\"identifier\":[{\"system\":\"http://openmrs.org/openmrs2\",\"use\":\"official\",\"id\":\"0d3b9808-9d75-4884-b3a2-75506e634da9\",\"type\":{\"coding\":[{\"system\":\"UgandaEMR\",\"code\":\"05a29f94-c0ed-11e2-94be-8c13b969e334\"}],\"text\":\"OpenMRS ID\"},\"value\":\"%s\"},{\"use\":\"usual\",\"id\":\"8c9a9fb0-b695-4f3f-9c4d-ac9627d4ff1b\",\"system\":\"http://health.go.ug/cr/internalid\",\"type\":{\"coding\":[{\"system\":\"UgandaEMR\",\"code\":\"f0c16a6d-dc5f-4118-a803-616d0075d282\"}],\"text\":\"National ID No.\"},\"value\":\"%s\"},{\"use\":\"usual\",\"id\":\"870a860b-fdad-4e3b-bc2e-61d7fba8bc57\",\"system\":\"http://health.go.ug/cr/ancno\",\"type\":{\"coding\":[{\"system\":\"UgandaEMR\",\"code\":\"c722e49d-ef61-4f14-9755-f53af4d3d3f1\"}],\"text\":\"ANC No.\"},\"value\":\"%s\"},{\"use\":\"usual\",\"id\":\"1880d0cf-9207-46ac-84e5-055364f8b52e\",\"system\":\"http://health.go.ug/cr/otherid\",\"type\":{\"coding\":[{\"system\":\"UgandaEMR\",\"code\":\"c722e49d-ef61-4f14-9755-f53af4d3d3f1\"}],\"text\":\"Other_ID\"},\"value\":\"%s\"},{\"use\":\"usual\",\"id\":\"0f713cbd-d5ee-4bca-8c9f-3385880bb151\",\"system\":\"http://health.go.ug/cr/pncno\",\"type\":{\"coding\":[{\"system\":\"UgandaEMR\",\"code\":\"ac5eb0dd-3d9d-4377-88e0-ac14f2249d80\"}],\"text\":\"PNC #\"},\"value\":\"%s\"}],\"gender\":\"%s\",\"birthDate\":\"%s\",\"managingOrganization\":{\"reference\":\"%s\"}, " + " \"extension\":[{\"url\":\"http://health.go.ug/age\",\"valueAge\":{\"system\":\"http://unitsofmeasure.org\",\"code\":\"a\",\"value\":%s}}]}],\"component\":[{\"code\":{\"coding\":[{\"system\":\"http://snomed.info/sct\",\"code\":\"413946009\",\"display\":\"Date treatment started\"}],\"text\":\"Treatment initiation\"},\"valueDateTime\":\"%s\"},{\"code\":{\"coding\":[{\"system\":\"http://snomed.info/sct\",\"code\":\"%s\",\"display\":\"%s\"}]}},{\"code\":{\"coding\":[{\"system\":\"http://snomed.info/sct\",\"code\":\"261773006\",\"display\":\"Duration of therapy\"}],\"text\":\"Duration on current regimen\"},\"valueString\":\"%s\"},{\"code\":{\"coding\":[{\"system\":\"http://snomed.info/sct\",\"code\":\"77386006\",\"display\":\"Pregnancy (finding)\"}],\"text\":\"Is mother pregnant\"},\"valueBoolean\":%s},{\"code\":{\"coding\":[{\"system\":\"http://snomed.info/sct\",\"code\":\"69840006\",\"display\":\"Normal Breast feeding (finding)\"}],\"text\":\"Is mother breastfeeding\"},\"valueBoolean\":%s},{\"code\":{\"coding\":[{\"system\":\"http://snomed.info/sct\",\"code\":\"56717001\",\"display\":\"Tuberculosis (disorder)\"}],\"text\":\"Has active TB\"},\"valueBoolean\":%s},{\"code\":{\"coding\":[{\"system\":\"http://snomed.info/sct\",\"code\":\"371569005\",\"display\":\"Tuberculosis (qualifier value)\"}],\"text\":\"TB phase\"},\"valueString\":\"%s\"},{\"code\":{\"coding\":[{\"system\":\"http://snomed.info/sct\",\"code\":\"1156699004\",\"display\":\"Adheres to medication regimen\"}],\"text\":\"ARV adherence\"},\"valueString\":\"%s\"},{\"code\":{\"coding\":[{\"system\":\"http://snomed.info/sct\",\"code\":\"734163000\",\"display\":\"Care Plan\"}],\"text\":\"Treatment care approach(DSDM)\"},\"valueString\":\"%s\"},{\"code\":{\"coding\":[{\"system\":\"http://snomed.info/sct\",\"code\":\"315124004\",\"display\":\"Human immunodeficiency virus viral load\"}],\"text\":\"Viral Load Testing\"},\"valueString\":\"%s\"},{\"code\":{\"coding\":[%s]}},{\"code\":{\"coding\":[{\"system\":\"http://snomed.info/sct\",\"code\":\"133877004\",\"display\":\"Therapeutic regimen (regime/therapy)\"}],\"text\":\"Current Regimen\"},\"valueString\":\"%s\"}]}"; - public static final String VL_RECEIVE_RESULT_FHIR_JSON_STRING = "{\"resourceType\":\"ServiceRequest\",\"locationCode\":\"%s\",\"subject\":{\"resourceType\":\"Location\",\"name\":\"UgandaEMR\"},\"specimen\":[{\"subject\":{\"resourceType\":\"Patient\",\"identifier\":\"%s\"},\"resourceType\":\"Specimen\",\"identifier\":\"%s\"}]}"; + public static final String VL_RECEIVE_RESULT_FHIR_JSON_STRING = "{\"resourceType\":\"ServiceRequest\",\"locationCode\":\"%s\",\"subject\":{\"resourceType\":\"Location\",\"name\":\"UgandaEMR\"},\"code\":{\"coding\":[%s]},\"specimen\":[{\"subject\":{\"resourceType\":\"Patient\",\"identifier\":\"%s\"},\"resourceType\":\"Specimen\",\"identifier\":\"%s\"}]}"; public static final String PERSON_QUERY = "SELECT\n" + " gender,\n" + " birthdate,\n" + " birthdate_estimated,\n" + " dead,\n" + " death_date,\n" + " (SELECT c.uuid\n" + " FROM concept c\n" diff --git a/api/src/main/java/org/openmrs/module/ugandaemrsync/server/SyncFHIRRecord.java b/api/src/main/java/org/openmrs/module/ugandaemrsync/server/SyncFHIRRecord.java index e8c22993..97e79050 100644 --- a/api/src/main/java/org/openmrs/module/ugandaemrsync/server/SyncFHIRRecord.java +++ b/api/src/main/java/org/openmrs/module/ugandaemrsync/server/SyncFHIRRecord.java @@ -9,8 +9,6 @@ import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.r4.model.AllergyIntolerance; -import org.hl7.fhir.r4.model.MedicationRequest; import org.json.JSONArray; import org.json.JSONObject; import org.openmrs.*; @@ -1442,6 +1440,37 @@ private Collection getEpisodeOfCareResourceBundle(List getRelatedPerson(SyncFhirProfile syncFhirProfile, List personList, SyncFhirCase syncFhirCase) { + + DateRangeParam lastUpdated = new DateRangeParam(); + + if (syncFhirProfile != null) { + if (syncFhirProfile.getIsCaseBasedProfile()) { + if (syncFhirCase != null && syncFhirCase.getLastUpdateDate() != null) { + lastUpdated = new DateRangeParam().setUpperBoundInclusive(new Date()).setLowerBoundInclusive(syncFhirCase.getLastUpdateDate()); + } else { + lastUpdated = new DateRangeParam().setUpperBoundInclusive(new Date()).setLowerBoundInclusive(getDefaultLastSyncDate()); + } + } else { + lastUpdated = new DateRangeParam().setUpperBoundInclusive(new Date()).setLowerBoundInclusive(getLastSyncDate(syncFhirProfile, "RelatedPerson")); + + } + } + + PersonSearchParams personSearchParams = new PersonSearchParams(); + + Collection iBaseResources = new ArrayList<>(); + + if (personList.size() > 0) { + Collection personListUUID = personList.stream().map(Person::getUuid).collect(Collectors.toCollection(ArrayList::new)); + iBaseResources.addAll(getApplicationContext().getBean(FhirRelatedPersonService.class).get(personListUUID)); + + } else if (syncFhirProfile != null && !syncFhirProfile.getIsCaseBasedProfile()) { + personSearchParams.setLastUpdated(lastUpdated); + } + return iBaseResources; + } + private Date getLastSyncDate(SyncFhirProfile syncFhirProfile, String resourceType) { Date date; @@ -1639,4 +1668,6 @@ private List getPatientByCohortType(String cohortTypeUuid) { } return patientList; } + + } diff --git a/api/src/main/java/org/openmrs/module/ugandaemrsync/tasks/ReceiveViralLoadResultFromCentralServerTask.java b/api/src/main/java/org/openmrs/module/ugandaemrsync/tasks/ReceiveViralLoadResultFromCentralServerTask.java index b78d1d56..445ee0fe 100644 --- a/api/src/main/java/org/openmrs/module/ugandaemrsync/tasks/ReceiveViralLoadResultFromCentralServerTask.java +++ b/api/src/main/java/org/openmrs/module/ugandaemrsync/tasks/ReceiveViralLoadResultFromCentralServerTask.java @@ -2,28 +2,16 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.openmrs.Order; -import org.openmrs.api.OrderService; import org.openmrs.api.context.Context; import org.openmrs.module.ugandaemrsync.api.UgandaEMRSyncService; import org.openmrs.module.ugandaemrsync.model.SyncTask; -import org.openmrs.module.ugandaemrsync.model.SyncTaskType; import org.openmrs.module.ugandaemrsync.api.UgandaEMRHttpURLConnection; import org.openmrs.scheduler.tasks.AbstractTask; -import java.util.Map; -import java.util.HashMap; -import java.util.List; -import java.util.ArrayList; -import java.util.Date; -import java.util.stream.Collectors; import static org.openmrs.module.ugandaemrsync.server.SyncConstant.VIRAL_LOAD_SYNC_TASK_TYPE_UUID; -import static org.openmrs.module.ugandaemrsync.server.SyncConstant.VIRAL_LOAD_RESULT_PULL_TYPE_UUID; -import static org.openmrs.module.ugandaemrsync.server.SyncConstant.VL_RECEIVE_RESULT_FHIR_JSON_STRING; -import static org.openmrs.module.ugandaemrsync.server.SyncConstant.VIRAL_LOAD_ORDER_QUERY; -import static org.openmrs.module.ugandaemrsync.server.SyncConstant.PATIENT_IDENTIFIER_TYPE; + public class ReceiveViralLoadResultFromCentralServerTask extends AbstractTask { protected final Log log = LogFactory.getLog(ReceiveViralLoadResultFromCentralServerTask.class); diff --git a/api/src/test/java/org/openmrs/module/ugandaemrsync/api/UgandaEMRSyncServiceTest.java b/api/src/test/java/org/openmrs/module/ugandaemrsync/api/UgandaEMRSyncServiceTest.java index 524bbad8..806c9588 100644 --- a/api/src/test/java/org/openmrs/module/ugandaemrsync/api/UgandaEMRSyncServiceTest.java +++ b/api/src/test/java/org/openmrs/module/ugandaemrsync/api/UgandaEMRSyncServiceTest.java @@ -447,11 +447,6 @@ public void testisValidCPHLBarCodeValidPreviousYearBarcode() { Assert.assertTrue(ugandaEMRSyncService.isValidCPHLBarCode(barcode)); } - @Test - public void testisValidCPHLBarCodeInvalidOlderBarcode() { - Assert.assertFalse(ugandaEMRSyncService.isValidCPHLBarCode("2312348471")); // Assuming year is 2025 - } - @Test public void testisValidCPHLBarCodeBarcodeWithNonNumericPrefix() { Assert.assertFalse(ugandaEMRSyncService.isValidCPHLBarCode("AB12340921")); diff --git a/api/src/test/java/org/openmrs/module/ugandaemrsync/server/SyncFHIRRecordTest.java b/api/src/test/java/org/openmrs/module/ugandaemrsync/server/SyncFHIRRecordTest.java index 6fa191bf..7db53a7c 100644 --- a/api/src/test/java/org/openmrs/module/ugandaemrsync/server/SyncFHIRRecordTest.java +++ b/api/src/test/java/org/openmrs/module/ugandaemrsync/server/SyncFHIRRecordTest.java @@ -158,8 +158,8 @@ public void testGetMissingVLFHIRCodesAsString_withTwoMissingCodes() { bundle.addEntry().setResource(createObservation("33882-2")); String json = parser.encodeResourceToString(bundle); - String missingConceptName1 = service.getVLMissingCconcept("202501020").getName().getName(); - String missingConceptName2 = service.getVLMissingCconcept("202501021").getName().getName(); + String missingConceptName1 = service.getVLMissingConcept("202501020").getName().getName(); + String missingConceptName2 = service.getVLMissingConcept("202501021").getName().getName(); String result = service.getMissingVLFHIRCodesAsString(json,"1eb05918-f50c-4cad-a827-3c78f296a10a"); Assert.assertEquals(String.format("%s,%s", missingConceptName1, missingConceptName2), result);