diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleExecutionException.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleExecutionException.java index 4a88f1bb7..7f562738b 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleExecutionException.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/RuleExecutionException.java @@ -4,4 +4,8 @@ public class RuleExecutionException extends Exception { public RuleExecutionException(String message, Throwable cause) { super(message, cause); } + + public RuleExecutionException(String message) { + super(message); + } } diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdatePatientIdentifierListAssigningAuthority.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdatePatientIdentifierListAssigningAuthority.java new file mode 100644 index 000000000..64e9c6fb0 --- /dev/null +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/UpdatePatientIdentifierListAssigningAuthority.java @@ -0,0 +1,30 @@ +package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.custom; + +import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; +import gov.hhs.cdc.trustedintermediary.etor.ruleengine.RuleExecutionException; +import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation; +import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper; +import java.util.Map; +import org.hl7.fhir.r4.model.Bundle; + +public class UpdatePatientIdentifierListAssigningAuthority implements CustomFhirTransformation { + + @Override + public void transform(FhirResource resource, Map args) + throws RuleExecutionException { + + if (!(resource.getUnderlyingResource() instanceof Bundle)) { + throw new RuleExecutionException("Resource provided is not a Bundle"); + } + + try { + Bundle bundle = (Bundle) resource.getUnderlyingResource(); + String newValue = args.get("newValue"); + + HapiHelper.updateOrganizationIdentifierValue(bundle, newValue); + + } catch (Exception e) { + throw new RuleExecutionException("Unexpected error during transformation", e); + } + } +} diff --git a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java index 1ab275c6e..86d20eb41 100644 --- a/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java +++ b/shared/src/main/java/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelper.java @@ -1,10 +1,18 @@ package gov.hhs.cdc.trustedintermediary.external.hapi; +import gov.hhs.cdc.trustedintermediary.context.ApplicationContext; +import gov.hhs.cdc.trustedintermediary.wrappers.Logger; +import java.util.AbstractMap; +import java.util.Optional; import java.util.stream.Stream; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.MessageHeader; import org.hl7.fhir.r4.model.Meta; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Resource; /** Helper class that works on HapiFHIR constructs. */ @@ -12,6 +20,8 @@ public class HapiHelper { private HapiHelper() {} + private static final Logger LOGGER = ApplicationContext.getImplementation(Logger.class); + public static final Coding OML_CODING = new Coding( "http://terminology.hl7.org/CodeSystem/v2-0003", @@ -65,4 +75,106 @@ public static MessageHeader findOrInitializeMessageHeader(Bundle bundle) { } return (MessageHeader) messageHeader; } + + /** + * Updates the value of an identifier for an Organization within a FHIR Bundle based on specific + * criteria. This method processes each Patient resource within the bundle, identifies linked + * Organizations via the assigner field of Patient identifiers, and checks each Organization's + * identifier for specific extension criteria. If the criteria are met (specifically an + * extension URL and a valueString that match predefined values), the identifier's value is + * updated to a new specified value. + * + *

The following HL7 segment is changed: PID 3.4 - Assigning Authority + * + * @param bundle the FHIR Bundle containing Patient resources whose identifiers need to be + * updated. + * @param newValue the new value to which the identifier type code should be set. + */ + public static void updateOrganizationIdentifierValue(Bundle bundle, String newValue) { + bundle.getEntry().stream() + .map(Bundle.BundleEntryComponent::getResource) + .filter(Patient.class::isInstance) + .map(Patient.class::cast) + .flatMap(patient -> patient.getIdentifier().stream()) + .map( + identifier -> + new AbstractMap.SimpleEntry<>( + identifier, + getOrganizationFromAssigner( + bundle, identifier.getAssigner()))) + .filter(entry -> entry.getValue().isPresent()) + .flatMap( + entry -> + entry.getValue().get().getIdentifier().stream() + .filter( + orgIdentifier -> + hasRequiredExtension( + orgIdentifier, + "https://reportstream.cdc.gov/fhir/StructureDefinition/hl7v2Field", + "HD.1")) + .peek( + orgIdentifier -> + LOGGER.logInfo( + "Updating Organization identifier from: " + + orgIdentifier.getValue())) + .map(orgIdentifier -> orgIdentifier.setValue(newValue))) + .forEach( + orgIdentifier -> + LOGGER.logInfo( + "Updated Organization identifier to: " + + orgIdentifier.getValue())); + } + + /** + * Fetches the Organization referenced by a Patient identifier assigner. + * + * @param bundle The FHIR Bundle to search. + * @param assigner The assigner reference. + * @return Optional if found, otherwise Optionale.empty(). + */ + private static Optional getOrganizationFromAssigner( + Bundle bundle, Reference assigner) { + if (assigner == null || assigner.getReference() == null) { + LOGGER.logInfo("Assigner or assigner reference is null."); + return Optional.empty(); + } + + LOGGER.logInfo( + "Starting search for Organization with reference: " + assigner.getReference()); + + return bundle.getEntry().stream() + .map(Bundle.BundleEntryComponent::getResource) + .filter(Organization.class::isInstance) + .map(Organization.class::cast) + .peek(org -> LOGGER.logInfo("Checking organization with ID: " + org.getId())) + .filter( + org -> { + boolean matches = + ("Organization/" + org.getId()).equals(assigner.getReference()); + LOGGER.logInfo( + "Checking organization with ID: " + + org.getId() + + " for match: " + + matches); + return matches; + }) + .findFirst(); + } + + /** + * Checks if an identifier has the required extension with specific url and valueString. + * + * @param identifier The identifier to check. + * @param url The extension url to match. + * @param valueString The extension valueString to match. + * @return true if the extension exists and matches, false otherwise. + */ + private static boolean hasRequiredExtension( + Identifier identifier, String url, String valueString) { + return identifier.getExtension().stream() + .anyMatch( + extension -> + url.equals(extension.getUrl()) + && valueString.equals(extension.getValue().toString())); + } } diff --git a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy index 4c5180c8e..c52ea2d6e 100644 --- a/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy +++ b/shared/src/test/groovy/gov/hhs/cdc/trustedintermediary/external/hapi/HapiHelperTest.groovy @@ -1,17 +1,29 @@ package gov.hhs.cdc.trustedintermediary.external.hapi +import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext import org.hl7.fhir.r4.model.Bundle +import org.hl7.fhir.r4.model.Organization import org.hl7.fhir.r4.model.Coding +import org.hl7.fhir.r4.model.Identifier +import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.MessageHeader import org.hl7.fhir.r4.model.Meta import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.Provenance +import org.hl7.fhir.r4.model.Reference import org.hl7.fhir.r4.model.ResourceType import org.hl7.fhir.r4.model.ServiceRequest +import org.hl7.fhir.r4.model.StringType import spock.lang.Specification class HapiHelperTest extends Specification { + def setup() { + TestApplicationContext.reset() + TestApplicationContext.init() + TestApplicationContext.injectRegisteredImplementations() + } + def "resourcesInBundle return a stream of a specific type of resources in a FHIR Bundle"() { given: def patients = [ @@ -181,4 +193,76 @@ class HapiHelperTest extends Specification { convertedMessageHeader.getEventCoding().getCode() == expectedCode convertedMessageHeader.getEventCoding().getDisplay() == expectedDisplay } + + def "updateOrganizationIdentifierValue updates the correct identifier value"() { + given: + def id = "1" + def presentValue = "initial Assigning Authority" + def newValue = "Updated Assigning Authority" + def identifierExtension = new Extension("https://reportstream.cdc.gov/fhir/StructureDefinition/hl7v2Field", new StringType("HD.1")) + + def organization = new Organization() + organization.setId(id) + organization.addIdentifier(new Identifier().setValue(presentValue).addExtension(identifierExtension) as Identifier) + + def patient = new Patient() + patient.addIdentifier(new Identifier().setAssigner(new Reference("Organization/" + id) as Reference)) + + def bundle = new Bundle() + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(patient)) + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(organization).setFullUrl("Organization/1")) + + when: + HapiHelper.updateOrganizationIdentifierValue(bundle, newValue) + + then: + organization.getIdentifier().any { + it.hasExtension("https://reportstream.cdc.gov/fhir/StructureDefinition/hl7v2Field") && + it.getExtensionByUrl("https://reportstream.cdc.gov/fhir/StructureDefinition/hl7v2Field").getValue().toString() == "HD.1" && + it.getValue() == newValue + } + } + + def "handle empty bundle gracefully"() { + given: + Bundle emptyBundle = new Bundle() + + when: + HapiHelper.updateOrganizationIdentifierValue(emptyBundle, "newValue") + + then: + noExceptionThrown() + } + + def "should not perform updates if no organizations are present in the bundle"() { + given: + def id = "1" + def patient = new Patient() + patient.addIdentifier(new Identifier().setAssigner(new Reference("Organization/" + id) as Reference)) + + def bundle = new Bundle() + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(patient)) + Bundle bundleWithNoOrganizations = bundle + + when: + HapiHelper.updateOrganizationIdentifierValue(bundleWithNoOrganizations, "newValue") + + then: + noExceptionThrown() + } + + def "should handle patients without assigners"() { + given: + def patient = new Patient() + patient.addIdentifier(new Identifier()) + + def bundle = new Bundle() + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(patient)) + + when: + HapiHelper.updateOrganizationIdentifierValue(bundle, "newValue") + + then: + noExceptionThrown() + } }