diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/MapLocalObservationCodes.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/MapLocalObservationCodes.java index 5468561be..4749f3779 100644 --- a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/MapLocalObservationCodes.java +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/MapLocalObservationCodes.java @@ -8,7 +8,6 @@ import gov.hhs.cdc.trustedintermediary.wrappers.Logger; import java.util.HashMap; import java.util.Map; -import java.util.Objects; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Observation; @@ -43,7 +42,8 @@ public void transform(FhirResource resource, Map args) { } var coding = codingList.get(0); - if (!hasLocalCodeInAlternateCoding(coding)) { + if (!HapiHelper.hasDefinedCoding( + coding, HapiHelper.EXTENSION_ALT_CODING, HapiHelper.LOCAL_CODE)) { continue; } @@ -60,24 +60,6 @@ public void transform(FhirResource resource, Map args) { } } - private Boolean hasLocalCodeInAlternateCoding(Coding coding) { - if (!HapiHelper.hasCodingExtensionWithUrl(coding, HapiHelper.EXTENSION_CWE_CODING)) { - return false; - } - - if (!HapiHelper.hasCodingSystem(coding)) { - return false; - } - - var cwe = - HapiHelper.getCodingExtensionByUrl(coding, HapiHelper.EXTENSION_CWE_CODING) - .getValue() - .toString(); - var codingSystem = HapiHelper.getCodingSystem(coding); - - return Objects.equals(cwe, "alt-coding") && HapiHelper.LOCAL_CODE_URL.equals(codingSystem); - } - private void logUnmappedLocalCode(Bundle bundle, Coding coding) { var msh41Identifier = HapiHelper.getMSH4_1Identifier(bundle); var msh41Value = msh41Identifier != null ? msh41Identifier.getValue() : null; @@ -103,6 +85,12 @@ private Coding getMappedCoding(IdentifierCode identifierCode) { return mappedCoding; } + /** + * Initializes the local-to-LOINC/PLT hash map, customized for CDPH and UCSD. Currently, the + * mapping is hardcoded for simplicity. If expanded to support additional entities, the + * implementation may be updated to allow dynamic configuration via + * transformation_definitions.json or a database-driven mapping. + */ private void initMap() { this.codingMap = new HashMap<>(); // ALD diff --git a/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationByCode.java b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationByCode.java new file mode 100644 index 000000000..c3cb989ce --- /dev/null +++ b/etor/src/main/java/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationByCode.java @@ -0,0 +1,41 @@ +package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.custom; + +import gov.hhs.cdc.trustedintermediary.etor.ruleengine.FhirResource; +import gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.CustomFhirTransformation; +import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Resource; + +public class RemoveObservationByCode implements CustomFhirTransformation { + public static final String CODE_NAME = "code"; + public static final String CODING_SYSTEM_NAME = "codingSystemExtension"; + public static final String CODING_NAME = "codingExtension"; + + @Override + public void transform(FhirResource resource, Map args) { + var bundle = (Bundle) resource.getUnderlyingResource(); + Set resourcesToRemove = new HashSet<>(); + + for (Bundle.BundleEntryComponent entry : bundle.getEntry()) { + Resource resourceEntry = entry.getResource(); + + if (!(resourceEntry instanceof Observation observation)) { + continue; + } + + if (HapiHelper.hasMatchingCoding( + observation, + args.get(CODE_NAME).toString(), + args.get(CODING_NAME).toString(), + args.get(CODING_SYSTEM_NAME).toString())) { + resourcesToRemove.add(resourceEntry); + } + } + + bundle.getEntry().removeIf(entry -> resourcesToRemove.contains(entry.getResource())); + } +} diff --git a/etor/src/main/resources/transformation_definitions.json b/etor/src/main/resources/transformation_definitions.json index 0daf8a52d..427cf0138 100644 --- a/etor/src/main/resources/transformation_definitions.json +++ b/etor/src/main/resources/transformation_definitions.json @@ -216,6 +216,25 @@ "args": {} } ] + }, + { + "name": "ucsdOruRemoveAccessionNumberObservation", + "description": "Remove Observations for UCSD ORUs when their OBX-3.4 value is '99717-5' and OBX-3.6 is 'L'", + "message": "", + "conditions": [ + "Bundle.entry.resource.ofType(MessageHeader).destination.receiver.resolve().identifier.where(extension.value = 'HD.1').value in ('R797' | 'R508')", + "Bundle.entry.resource.ofType(MessageHeader).event.code = 'R01'" + ], + "rules": [ + { + "name": "RemoveObservationByCode", + "args": { + "code": "99717-5", + "codingSystemExtension": "L", + "codingExtension": "alt-coding" + } + } + ] } ] } diff --git a/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationByCodeTest.groovy b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationByCodeTest.groovy new file mode 100644 index 000000000..d06e9bf94 --- /dev/null +++ b/etor/src/test/groovy/gov/hhs/cdc/trustedintermediary/etor/ruleengine/transformation/custom/RemoveObservationByCodeTest.groovy @@ -0,0 +1,193 @@ +package gov.hhs.cdc.trustedintermediary.etor.ruleengine.transformation.custom + +import gov.hhs.cdc.trustedintermediary.ExamplesHelper +import gov.hhs.cdc.trustedintermediary.context.TestApplicationContext +import gov.hhs.cdc.trustedintermediary.external.hapi.HapiFhirHelper +import gov.hhs.cdc.trustedintermediary.external.hapi.HapiFhirResource +import gov.hhs.cdc.trustedintermediary.external.hapi.HapiHelper +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.StringType +import spock.lang.Specification + +class RemoveObservationByCodeTest extends Specification { + def transformClass + + def setup() { + TestApplicationContext.reset() + TestApplicationContext.init() + TestApplicationContext.injectRegisteredImplementations() + + transformClass = new RemoveObservationByCode() + } + + def "When an observation has the desired coding, it should be removed"() { + given: + def bundle = HapiFhirHelper.createMessageBundle(messageTypeCode: 'ORU_R01') + def observation = new Observation() + addCodingToObservation(observation, code, codingSystemExt, codingExt) + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(observation)) + + def args = getArgs(code, codingSystemExt, codingExt) + + expect: + HapiHelper.resourcesInBundle(bundle, Observation.class).count() == 1 + + when: + transformClass.transform(new HapiFhirResource(bundle), args) + + then: + HapiHelper.resourcesInBundle(bundle, Observation.class).count() == 0 + + where: + code | codingSystemExt | codingExt + "99717-5" | "L" | "alt-coding" + "my_code" | "MY_SYS" | "coding" + } + + def "When an observation with >1 coding has the desired coding, it should be removed"() { + given: + final String MATCHING_CODE = "99717-5" + final String MATCHING_CODING_SYSTEM_EXT = "L" + final String MATCHING_CODING_EXT = "alt-coding" + + def bundle = HapiFhirHelper.createMessageBundle(messageTypeCode: 'ORU_R01') + def observation = new Observation() + + addCodingToObservation(observation, "ANOTHER_CODE", "ANOTHER_SYSTEM", "coding") + addCodingToObservation(observation, MATCHING_CODE, MATCHING_CODING_SYSTEM_EXT, MATCHING_CODING_EXT) + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(observation)) + + def args = getArgs(MATCHING_CODE, MATCHING_CODING_SYSTEM_EXT, MATCHING_CODING_EXT) + + expect: + HapiHelper.resourcesInBundle(bundle, Observation.class).count() == 1 + + when: + transformClass.transform(new HapiFhirResource(bundle), args) + + then: + HapiHelper.resourcesInBundle(bundle, Observation.class).count() == 0 + } + + def "When an observation has coding that's only a partial match, it should NOT be removed"() { + given: + final String MATCHING_CODE = "99717-5" + final String MATCHING_CODING_SYSTEM_EXT = "L" + final String MATCHING_CODING_EXT = "alt-coding" + + def bundle = HapiFhirHelper.createMessageBundle(messageTypeCode: 'ORU_R01') + def observation = new Observation() + addCodingToObservation(observation, code, codingSystemExt, codingExt) + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(observation)) + + def args = getArgs(MATCHING_CODE, MATCHING_CODING_SYSTEM_EXT, MATCHING_CODING_EXT) + + expect: + HapiHelper.resourcesInBundle(bundle, Observation.class).count() == 1 + + when: + transformClass.transform(new HapiFhirResource(bundle), args) + + then: + HapiHelper.resourcesInBundle(bundle, Observation.class).count() == 1 + + where: + code | codingSystemExt | codingExt + "11111-1" | "L" | "alt-coding" + "99717-5" | "DIFFERENT_SYS" | "alt-coding" + "99717-5" | "L" | "coding" + } + + def "When an observation has no identifier OBX-3, it should NOT be removed"() { + given: + def bundle = HapiFhirHelper.createMessageBundle(messageTypeCode: 'ORU_R01') + + // Add an observation with an observation value and a status, but no observation identifier + def observation = new Observation() + observation.status = Observation.ObservationStatus.FINAL + def valueCoding = new Coding() + valueCoding.code = "123456" + observation.valueCodeableConcept.coding.add(valueCoding) + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(observation)) + + def args = getArgs("55555-5", "LN", "coding") + + expect: + HapiHelper.resourcesInBundle(bundle, Observation.class).count() == 1 + + when: + transformClass.transform(new HapiFhirResource(bundle), args) + + then: + HapiHelper.resourcesInBundle(bundle, Observation.class).count() == 1 + } + + def "When there is >1 matching observation, all matching observations should be removed"() { + given: + final String MATCHING_CODE = "99717-5" + final String MATCHING_CODING_SYSTEM_EXT = "L" + final String MATCHING_CODING_EXT = "alt-coding" + + def bundle = HapiFhirHelper.createMessageBundle(messageTypeCode: 'ORU_R01') + + def observation1 = new Observation() + def observation2 = new Observation() + + addCodingToObservation(observation1, MATCHING_CODE, MATCHING_CODING_SYSTEM_EXT, MATCHING_CODING_EXT) + addCodingToObservation(observation2, MATCHING_CODE, MATCHING_CODING_SYSTEM_EXT, MATCHING_CODING_EXT) + + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(observation1)) + bundle.addEntry(new Bundle.BundleEntryComponent().setResource(observation2)) + + def args = getArgs(MATCHING_CODE, MATCHING_CODING_SYSTEM_EXT, MATCHING_CODING_EXT) + + expect: + HapiHelper.resourcesInBundle(bundle, Observation.class).count() == 2 + + when: + transformClass.transform(new HapiFhirResource(bundle), args) + + then: + HapiHelper.resourcesInBundle(bundle, Observation.class).count() == 0 + } + + def "When message has multiple observations with only 1 matching, only 1 is removed"() { + given: + final String MATCHING_CODE = "99717-5" + final String MATCHING_CODING_SYSTEM_EXT = "L" + final String MATCHING_CODING_EXT = "alt-coding" + + final String FHIR_ORU_PATH = "../CA/020_CA_ORU_R01_CDPH_OBX_to_LOINC_1_hl7_translation.fhir" + def fhirResource = ExamplesHelper.getExampleFhirResource(FHIR_ORU_PATH) + def bundle = fhirResource.getUnderlyingResource() as Bundle + + def args = getArgs(MATCHING_CODE, MATCHING_CODING_SYSTEM_EXT, MATCHING_CODING_EXT) + + expect: + HapiHelper.resourcesInBundle(bundle, Observation.class).count() == 114 + + when: + transformClass.transform(new HapiFhirResource(bundle), args) + + then: + HapiHelper.resourcesInBundle(bundle, Observation.class).count() == 113 + } + + void addCodingToObservation(Observation observation, String code, String codingSystemExtension, String codingExtension) { + def coding = new Coding() + + coding.code = code + coding.addExtension(HapiHelper.EXTENSION_CODING_SYSTEM, new StringType(codingSystemExtension)) + coding.addExtension(HapiHelper.EXTENSION_CWE_CODING, new StringType(codingExtension)) + observation.code.addCoding(coding) + } + + Map getArgs(String code, String codingSystem, String coding) { + return [ + "code" : code, + "codingSystemExtension" : codingSystem, + codingExtension : coding] + } +} 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 8614567c5..8ff7cb02f 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,6 +1,7 @@ package gov.hhs.cdc.trustedintermediary.external.hapi; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import java.util.stream.Stream; import org.hl7.fhir.r4.model.Bundle; @@ -12,6 +13,7 @@ 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.Observation; import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Practitioner; @@ -88,7 +90,7 @@ private HapiHelper() {} */ public static Stream resourcesInBundle( Bundle bundle, Class resourceType) { - if (bundle == null || bundle.getEntry() == null) { + if (bundle == null || bundle.getEntry().isEmpty()) { return Stream.empty(); } return bundle.getEntry().stream() @@ -681,4 +683,57 @@ public static String urlForCodeType(String code) { default -> HapiHelper.LOCAL_CODE_URL; }; } + + /** + * Check if a given Coding resource has a coding extension and coding system extension with the + * specified type. + * + * @param coding the resource to check. Expected to be converted from an HL7 CWE format field. + * @param codingExt Name of coding extension (e.g. "coding", "alt-coding") + * @param codingSystemExt Name of coding system to look for (e.g. Local code "L", LOINC "LN"...) + * @return True if the Coding is formatted correctly and has the expected code type, else false + */ + public static boolean hasDefinedCoding( + Coding coding, String codingExt, String codingSystemExt) { + var codingExtMatch = + hasMatchingCodingExtension(coding, HapiHelper.EXTENSION_CWE_CODING, codingExt); + var codingSystemExtMatch = + hasMatchingCodingExtension( + coding, HapiHelper.EXTENSION_CODING_SYSTEM, codingSystemExt); + return codingExtMatch && codingSystemExtMatch; + } + + private static boolean hasMatchingCodingExtension( + Coding coding, String extensionUrl, String valueToMatch) { + if (!HapiHelper.hasCodingExtensionWithUrl(coding, extensionUrl)) { + return false; + } + + var extensionValue = + HapiHelper.getCodingExtensionByUrl(coding, extensionUrl).getValue().toString(); + return Objects.equals(valueToMatch, extensionValue); + } + + /** + * Check if an observation has a Coding resource with the given code, coding, and coding system + * + * @param codeToMatch The code to look for. + * @param codingExtToMatch Name of coding extension (e.g. "coding", "alt-coding") + * @param codingSystemToMatch Name of coding system to look for (e.g. Local code "L", LOINC + * "LN"...) + * @return True if the Coding is present in the observation, else false + */ + public static boolean hasMatchingCoding( + Observation observation, + String codeToMatch, + String codingExtToMatch, + String codingSystemToMatch) { + for (Coding coding : observation.getCode().getCoding()) { + if (Objects.equals(coding.getCode(), codeToMatch) + && hasDefinedCoding(coding, codingExtToMatch, codingSystemToMatch)) { + return true; + } + } + return false; + } } 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 e7a6d8dc9..b8ff46c6b 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,6 +1,5 @@ package gov.hhs.cdc.trustedintermediary.external.hapi -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.Extension @@ -909,4 +908,47 @@ class HapiHelperTest extends Specification { "L" || HapiHelper.LOCAL_CODE_URL "PLT" || null } + + def "check getPractitioner gets the correct resource"() { + given: + def bundle = new Bundle() + def dr = HapiFhirHelper.createDiagnosticReport(bundle) + def sr = HapiFhirHelper.createBasedOnServiceRequest(dr) + + def role = HapiFhirHelper.createPractitionerRole() + Reference requesterReference = HapiFhirHelper.createPractitionerRoleReference(role) + sr.setRequester(requesterReference) + + def practitioner = new Practitioner() + practitioner.setId(UUID.randomUUID().toString()) + + String organizationId = practitioner.getId() + Reference organizationReference = new Reference("Practitioner/" + organizationId) + organizationReference.setResource(practitioner) + role.setPractitioner(organizationReference) + + expect: + def pr = HapiHelper.getPractitioner(role) + pr.id == practitioner.id + } + + def "hasDefinedCoding returns the correct result"() { + given: + def coding = new Coding() + coding.code = "SOME_CODE" + coding.addExtension(HapiHelper.EXTENSION_CWE_CODING, new StringType("coding")) + coding.addExtension(HapiHelper.EXTENSION_CODING_SYSTEM, new StringType("L")) + + when: + def actualResult = HapiHelper.hasDefinedCoding(coding, codingExt, codingSystemExt) + + then: + actualResult == expectedResult + + where: + codingExt | codingSystemExt || expectedResult + "coding" | "L" || true + "alt-coding" | "L" || false + "coding" | "LN" || false + } }