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

Update PID 3.4 #1095

Closed
wants to merge 13 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ public class RuleExecutionException extends Exception {
public RuleExecutionException(String message, Throwable cause) {
super(message, cause);
}

public RuleExecutionException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> args)
throws RuleExecutionException {

if (!(resource.getUnderlyingResource() instanceof Bundle)) {
throw new RuleExecutionException("Resource provided is not a Bundle");
}

try {
Bundle bundle = (Bundle) resource.getUnderlyingResource();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be the only check that would trigger an exception as the HapiHelper::updatePatientIdentifierValue does not throw any exceptions. So, I will add the ability to throw exceptions if the bundle is missing the patient resource or the organization/id can't be found.

String newValue = args.get("newValue");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should not be a new value for this transformation. This is the description as it's in the spreadsheet: "Strip content from Patient Identifier (PID-3). Remove PID-3.4 and PID-3.5"

Copy link
Contributor Author

@jorg3lopez jorg3lopez May 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the value can be: newValue = " "


HapiHelper.updateOrganizationIdentifierValue(bundle, newValue);

} catch (Exception e) {
throw new RuleExecutionException("Unexpected error during transformation", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
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. */
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",
Expand Down Expand Up @@ -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.
*
* <p>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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I will end up moving this logic into the transform method of the transformation class. This will only leave helper methods in the HapiHelper class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will use FHIR path for checking, if it exists, then I will proceed to update the value

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<Organization> if found, otherwise Optionale.empty().
*/
private static Optional<Organization> 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(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried using LOGGER.logDebug() here but it was not working

"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()));
}
}
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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"() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test will change once exceptions are thrown

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"() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with this unit test, it will change once exceptions are thrown

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"() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same. It will change once exceptions are thrown

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()
}
}