From e14d22ed63c0a6e99b19d80debcf7dc13bba7e24 Mon Sep 17 00:00:00 2001 From: mherman22 Date: Fri, 3 Jul 2026 18:26:14 +0000 Subject: [PATCH 1/2] refactor(xds-sender): route CCD retrieval through IPS, drop duplicate path Follow-up to #112. Instead of a parallel downloadAndSaveIps/retrieveIps/ IpsRetrievalController surface, the existing downloadAndSaveCcd now retrieves the consolidated IPS when xdssender.ipsEndpoint is configured, falling back to the legacy client-side CCD aggregation otherwise. Backwards compatible: - ShrImportServiceImpl.retrieveCCD: IPS-first (via ShrRetriever.fetchIps) when ipsEndpoint is set; else / on empty result, legacy sendRetrieveCCD. - Removes CcdService.downloadAndSaveIps + impl and ShrImportServiceImpl.retrieveIps. - Removes IpsRetrievalController (the UI trigger lives in registrationcore). - getIpsEndpoint()/config.xml: opt-in, empty default so existing installs are unchanged. Co-Authored-By: Claude Opus 4.8 --- .../module/xdssender/XdsSenderConfig.java | 6 +- .../xdssender/api/service/CcdService.java | 3 - .../api/service/impl/CcdServiceImpl.java | 14 ----- .../service/impl/ShrImportServiceImpl.java | 45 ++++++--------- .../controller/IpsRetrievalController.java | 56 ------------------- omod/src/main/resources/config.xml | 3 +- 6 files changed, 20 insertions(+), 107 deletions(-) delete mode 100644 omod/src/main/java/org/openmrs/module/xdssender/web/controller/IpsRetrievalController.java diff --git a/api/src/main/java/org/openmrs/module/xdssender/XdsSenderConfig.java b/api/src/main/java/org/openmrs/module/xdssender/XdsSenderConfig.java index 4d6c7524..1d0f90f1 100644 --- a/api/src/main/java/org/openmrs/module/xdssender/XdsSenderConfig.java +++ b/api/src/main/java/org/openmrs/module/xdssender/XdsSenderConfig.java @@ -183,10 +183,10 @@ public String getExportCcdEndpoint() { return getProperty(XDSSENDER_EXPORT_CCD_ENDPOINT); } - // Base URL of the consolidated IPS mediator (e.g. https:///SHR/ips). Used to pull a - // patient's cross-facility summary by CRUID for continuity of care. + // Opt-in: empty by default so existing deployments keep the legacy CCD retrieval. Set this to + // the SHR IPS mediator base (e.g. https:///SHR/ips) to enable IPS-based retrieval. public String getIpsEndpoint() { - return getProperty(XDSSENDER_IPS_ENDPOINT); + return getProperty(XDSSENDER_IPS_ENDPOINT, ""); } public String getLocalPatientIdRoot() { return getProperty("mpi-client.pid.local"); } diff --git a/api/src/main/java/org/openmrs/module/xdssender/api/service/CcdService.java b/api/src/main/java/org/openmrs/module/xdssender/api/service/CcdService.java index 5a663bce..81f908ca 100644 --- a/api/src/main/java/org/openmrs/module/xdssender/api/service/CcdService.java +++ b/api/src/main/java/org/openmrs/module/xdssender/api/service/CcdService.java @@ -21,9 +21,6 @@ public interface CcdService { @Transactional Ccd downloadAndSaveCcd(Patient patient) throws XDSException; - @Transactional - Ccd downloadAndSaveIps(Patient patient); - @Transactional(readOnly = true) void downloadCcdAsPDF(OutputStream stream, Patient patient); } diff --git a/api/src/main/java/org/openmrs/module/xdssender/api/service/impl/CcdServiceImpl.java b/api/src/main/java/org/openmrs/module/xdssender/api/service/impl/CcdServiceImpl.java index fc0938a1..80cb4072 100644 --- a/api/src/main/java/org/openmrs/module/xdssender/api/service/impl/CcdServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/xdssender/api/service/impl/CcdServiceImpl.java @@ -129,20 +129,6 @@ public Ccd downloadAndSaveCcd(Patient patient) throws XDSException { return ccd; } - @Override - public Ccd downloadAndSaveIps(Patient patient) { - Ccd ccd; - - shrImportService.setFhirContext(fhirContext); - ccd = shrImportService.retrieveIps(patient); - - if (ccd != null) { - ccd = ccdDao.saveOrUpdate(ccd); - } - - return ccd; - } - @Override public void downloadCcdAsPDF(OutputStream stream, Patient patient) { LOGGER.info("CCD PDF is being downloaded."); diff --git a/api/src/main/java/org/openmrs/module/xdssender/api/service/impl/ShrImportServiceImpl.java b/api/src/main/java/org/openmrs/module/xdssender/api/service/impl/ShrImportServiceImpl.java index 69ce02ad..8168fe47 100644 --- a/api/src/main/java/org/openmrs/module/xdssender/api/service/impl/ShrImportServiceImpl.java +++ b/api/src/main/java/org/openmrs/module/xdssender/api/service/impl/ShrImportServiceImpl.java @@ -42,41 +42,28 @@ public void setFhirContext(FhirContext fhirContext) { @Override public Ccd retrieveCCD(Patient patient) { Ccd ccd = null; - Bundle result; + Bundle result = null; try { shrRetriever.setFhirContext(this.getFhirContext()); - result = shrRetriever.sendRetrieveCCD(patient); + // Prefer the consolidated IPS (server-side aggregation via the SHR IPS mediator) when an + // IPS endpoint is configured; otherwise, or if it yields nothing, fall back to the legacy + // client-side CCD aggregation so existing deployments keep working unchanged. + String ipsEndpoint = config.getIpsEndpoint(); + if (ipsEndpoint != null && !ipsEndpoint.isEmpty()) { + result = shrRetriever.fetchIps(patient); + } + if (result == null || !result.hasEntry() || result.getEntry().isEmpty()) { + result = shrRetriever.sendRetrieveCCD(patient); + } } catch (Exception e) { - LOGGER.error("Unable to load CCD content", e); + LOGGER.error("Unable to load CCD/IPS content", e); return null; } - if(result != null && result.hasTotal() && result.getTotal() > 0) { - ccd = new Ccd(); - ccd.setPatient(patient); - ccd.setDocument(fhirContext.newJsonParser().encodeResourceToString(result)); - } - - return ccd; - } - - /** - * Retrieve the consolidated IPS document for a patient from the SHR IPS mediator and wrap it as - * a Ccd (raw FHIR JSON), mirroring {@link #retrieveCCD(Patient)}. IPS document bundles carry no - * total, so presence is judged by entries. - */ - public Ccd retrieveIps(Patient patient) { - Ccd ccd = null; - Bundle result; - try { - shrRetriever.setFhirContext(this.getFhirContext()); - result = shrRetriever.fetchIps(patient); - } catch (Exception e) { - LOGGER.error("Unable to load IPS content", e); - return null; - } - - if (result != null && result.hasEntry() && !result.getEntry().isEmpty()) { + // Legacy CCD search bundles carry a total; IPS document bundles do not, so accept either. + boolean hasData = result != null && ((result.hasTotal() && result.getTotal() > 0) + || (result.hasEntry() && !result.getEntry().isEmpty())); + if (hasData) { ccd = new Ccd(); ccd.setPatient(patient); ccd.setDocument(fhirContext.newJsonParser().encodeResourceToString(result)); diff --git a/omod/src/main/java/org/openmrs/module/xdssender/web/controller/IpsRetrievalController.java b/omod/src/main/java/org/openmrs/module/xdssender/web/controller/IpsRetrievalController.java deleted file mode 100644 index 1d38a7e1..00000000 --- a/omod/src/main/java/org/openmrs/module/xdssender/web/controller/IpsRetrievalController.java +++ /dev/null @@ -1,56 +0,0 @@ -/** - * 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/. OpenMRS is also distributed under - * the terms of the Healthcare Disclaimer located at http://openmrs.org/license. - * - * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS - * graphic logo is a trademark of OpenMRS Inc. - */ -package org.openmrs.module.xdssender.web.controller; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.openmrs.Patient; -import org.openmrs.api.PatientService; -import org.openmrs.module.xdssender.api.service.CcdService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; - -/** - * Continuity-of-care retrieval. Pulls a patient's consolidated IPS from the SHR IPS mediator - * (OpenCR resolves the patient to the golden record; OpenHIM aggregates the SHR clinical data) and - * returns it rendered for viewing. Read-only: it does not write discrete clinical data into the - * local record. - * - *

GET {@code module/xdssender/retrieveIps.form?patientId=} - */ -@Controller("xdssender.IpsRetrievalController") -@RequestMapping(value = "module/xdssender/retrieveIps.form") -public class IpsRetrievalController { - - protected final Log log = LogFactory.getLog(getClass()); - - @Autowired - private PatientService patientService; - - @Autowired - private CcdService ccdService; - - @RequestMapping(method = RequestMethod.GET) - @ResponseBody - public String retrieveIps(@RequestParam("patientId") String patientUuid) { - Patient patient = patientService.getPatientByUuid(patientUuid); - if (patient == null) { - log.warn("IPS retrieval requested for unknown patient uuid: " + patientUuid); - return "Patient not found: " + patientUuid; - } - - ccdService.downloadAndSaveIps(patient); - return ccdService.getHtmlParsedLocallyStoredCcd(patient); - } -} diff --git a/omod/src/main/resources/config.xml b/omod/src/main/resources/config.xml index 59138b32..741a5b61 100644 --- a/omod/src/main/resources/config.xml +++ b/omod/src/main/resources/config.xml @@ -92,8 +92,7 @@ xdssender.ipsEndpoint - https://openhimcore.sedishtest.live/SHR/ips - Base URL of the consolidated IPS mediator used to pull a patient's cross-facility summary by CRUID (continuity of care) + Base URL of the consolidated IPS mediator (e.g. https://<openhim>/SHR/ips). When set, patient retrieval (downloadAndSaveCcd) pulls the consolidated IPS by CRUID for continuity of care; when blank, the legacy client-side CCD aggregation is used. From 259848b6887e8d43e61a9e14774c8a19af08ca6b Mon Sep 17 00:00:00 2001 From: mherman22 Date: Sun, 12 Jul 2026 05:59:18 +0000 Subject: [PATCH 2/2] feat(ips): fetch consolidated IPS by iSantePlus ID via the mediator (drop client-side OpenCR/CRUID resolution) --- .../xdssender/api/xds/ShrRetriever.java | 41 ++++--------------- omod/src/main/resources/config.xml | 2 +- 2 files changed, 10 insertions(+), 33 deletions(-) diff --git a/api/src/main/java/org/openmrs/module/xdssender/api/xds/ShrRetriever.java b/api/src/main/java/org/openmrs/module/xdssender/api/xds/ShrRetriever.java index 7e86e176..3dc7e9c8 100644 --- a/api/src/main/java/org/openmrs/module/xdssender/api/xds/ShrRetriever.java +++ b/api/src/main/java/org/openmrs/module/xdssender/api/xds/ShrRetriever.java @@ -27,7 +27,6 @@ public class ShrRetriever { private static final String ECID_NAME = "ECID"; - private static final String GOLDEN_RECORD_TAG = "5c827da5-4858-4f3d-a50c-62ece001efea"; private static final String ISANTEPLUS_ID_TYPE = "iSantePlus ID"; @@ -149,45 +148,23 @@ public Bundle sendRetrieveCCD(Patient patient) { /** * Pull the consolidated IPS for this patient from the SHR IPS mediator (continuity of care). - * Resolves the patient's golden-record id (CRUID) from the MPI by iSantePlus ID, then GETs - * {@code /Patient/cruid/}. Returns null if no golden record resolves. + * The EMR sends only its own iSantePlus ID; the mediator resolves it to the golden record in the + * CR and assembles the IPS from the SHR — so the EMR does not talk to OpenCR directly. GETs + * {@code /Patient/isanteplus/}. Returns null if the patient has no + * iSantePlus ID or the mediator returns nothing. */ public Bundle fetchIps(Patient patient) { try { - IGenericClient mpiClient = getAuthenticatedClient(config.getMpiEndpoint(), config.getOshrUsername(), - config.getOshrPassword()); - - PatientIdentifier mpiIdentifier = patient.getPatientIdentifier(ISANTEPLUS_ID_TYPE); - if (mpiIdentifier == null) { - LOGGER.warn("Patient has no iSantePlus ID; cannot resolve CRUID for IPS retrieval"); - return null; - } - String systemUrl = patientIdentifierSystemService.getUrlByPatientIdentifierType(mpiIdentifier.getIdentifierType()); - - Bundle linkedPatientBundle = mpiClient.search() - .byUrl("/Patient?identifier=" + systemUrl + "|" + mpiIdentifier.getIdentifier() + "&_include=Patient:link") - .returnBundle(Bundle.class).execute(); - - String cruid = null; - for (Bundle.BundleEntryComponent entry : linkedPatientBundle.getEntry()) { - if (entry.hasResource() && entry.getResource() instanceof org.hl7.fhir.r4.model.Patient) { - org.hl7.fhir.r4.model.Patient candidate = (org.hl7.fhir.r4.model.Patient) entry.getResource(); - if (candidate.hasMeta() && candidate.getMeta().hasTag() && candidate.getMeta().getTagFirstRep().hasCode() - && GOLDEN_RECORD_TAG.equals(candidate.getMeta().getTagFirstRep().getCode())) { - cruid = candidate.getIdElement().getIdPart(); - break; - } - } - } - - if (cruid == null) { - LOGGER.warn("No golden record (CRUID) resolved from MPI; cannot fetch IPS"); + PatientIdentifier isantePlusIdentifier = patient.getPatientIdentifier(ISANTEPLUS_ID_TYPE); + if (isantePlusIdentifier == null) { + LOGGER.warn("Patient has no iSantePlus ID; cannot fetch IPS"); return null; } IGenericClient ipsClient = getAuthenticatedClient(config.getIpsEndpoint(), config.getOshrUsername(), config.getOshrPassword()); - return ipsClient.fetchResourceFromUrl(Bundle.class, config.getIpsEndpoint() + "/Patient/cruid/" + cruid); + return ipsClient.fetchResourceFromUrl(Bundle.class, + config.getIpsEndpoint() + "/Patient/isanteplus/" + isantePlusIdentifier.getIdentifier()); } catch (Exception ex) { LOGGER.error("Error fetching IPS", ex); diff --git a/omod/src/main/resources/config.xml b/omod/src/main/resources/config.xml index 741a5b61..bc0c4b98 100644 --- a/omod/src/main/resources/config.xml +++ b/omod/src/main/resources/config.xml @@ -92,7 +92,7 @@ xdssender.ipsEndpoint - Base URL of the consolidated IPS mediator (e.g. https://<openhim>/SHR/ips). When set, patient retrieval (downloadAndSaveCcd) pulls the consolidated IPS by CRUID for continuity of care; when blank, the legacy client-side CCD aggregation is used. + Base URL of the consolidated IPS mediator (e.g. https://<openhim>/SHR/fhir/ips). When set, patient retrieval (downloadAndSaveCcd) sends the patient's iSantePlus ID to the mediator, which resolves the golden record in the CR and returns the consolidated IPS; when blank, the legacy client-side CCD aggregation is used.