diff --git a/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt b/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt index 0d573d55b7f..87bff24f1e9 100644 --- a/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt +++ b/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt @@ -44,6 +44,7 @@ import gov.cdc.prime.router.azure.observability.event.AzureEventServiceImpl import gov.cdc.prime.router.azure.observability.event.IReportStreamEventService import gov.cdc.prime.router.azure.observability.event.ReportStreamEventName import gov.cdc.prime.router.azure.observability.event.ReportStreamEventProperties +import gov.cdc.prime.router.common.BaseEngine import gov.cdc.prime.router.fhirengine.translation.HL7toFhirTranslator import gov.cdc.prime.router.fhirengine.translation.hl7.FhirTransformer import gov.cdc.prime.router.fhirengine.translation.hl7.utils.CustomContext @@ -52,6 +53,8 @@ import gov.cdc.prime.router.fhirengine.utils.FhirTranscoder import gov.cdc.prime.router.fhirengine.utils.HL7Reader import gov.cdc.prime.router.fhirengine.utils.HL7Reader.Companion.parseHL7Message import gov.cdc.prime.router.fhirengine.utils.getObservations +import gov.cdc.prime.router.fhirengine.utils.getRSMessageType +import gov.cdc.prime.router.fhirengine.utils.isElr import gov.cdc.prime.router.logging.LogMeasuredTime import gov.cdc.prime.router.report.ReportService import gov.cdc.prime.router.validation.IItemValidator @@ -261,7 +264,7 @@ class FHIRConverter( // TODO: https://github.com/CDCgov/prime-reportstream/issues/14287 FhirPathUtils - val processedItems = process(format, input.blobURL, input.blobDigest, input.topic, actionLogger) + val processedItems = process(format, input, actionLogger) // processedItems can be empty in three scenarios: // - the blob had no contents, i.e. an empty file was submitted @@ -339,6 +342,12 @@ class FHIRConverter( nextAction = TaskAction.destination_filter ) + logger.info( + "Applied transform - parentReportId=[${input.reportId}]" + + ", childReportId=[${report.id}], schemaName=[${input.schemaName}]" + + ", trackingId=[${processedItem.getTrackingId()}]" + ) + // create route event val routeEvent = ProcessEvent( Event.EventAction.DESTINATION_FILTER, @@ -385,7 +394,8 @@ class FHIRConverter( mapOf( ReportStreamEventProperties.BUNDLE_DIGEST to bundleDigestExtractor.generateDigest(processedItem.bundle!!), - ReportStreamEventProperties.ITEM_FORMAT to format + ReportStreamEventProperties.ITEM_FORMAT to format, + ReportStreamEventProperties.ENRICHMENTS to input.schemaName ) ) } @@ -453,14 +463,12 @@ class FHIRConverter( */ internal fun process( format: MimeFormat, - blobURL: String, - blobDigest: String, - topic: Topic, + input: FHIRConvertInput, actionLogger: ActionLogger, routeReportWithInvalidItems: Boolean = true, ): List> { - val validator = topic.validator - val rawReport = BlobAccess.downloadBlob(blobURL, blobDigest) + val validator = input.topic.validator + val rawReport = BlobAccess.downloadBlob(input.blobURL, input.blobDigest) return if (rawReport.isBlank()) { actionLogger.error(InvalidReportMessage("Provided raw data is empty.")) emptyList() @@ -474,7 +482,7 @@ class FHIRConverter( "format" to format.name ) ) { - getBundlesFromRawHL7(rawReport, validator, topic.hl7ParseConfiguration) + getBundlesFromRawHL7(rawReport, validator, input.topic.hl7ParseConfiguration) } } catch (ex: ParseFailureError) { actionLogger.error( @@ -511,21 +519,24 @@ class FHIRConverter( } // 'stamp' observations with their condition code if (item.bundle != null) { + val isElr = item.bundle!!.getRSMessageType() == RSMessageType.LAB_RESULT item.bundle!!.getObservations().forEach { observation -> - val result = stamper.stampObservation(observation) - if (!result.success) { - val logger = actionLogger.getItemLogger(item.index + 1, observation.id) - if (result.failures.isEmpty()) { - logger.warn(UnmappableConditionMessage()) - } else { - logger.warn( - result.failures.map { - UnmappableConditionMessage( - it.failures.map { it.code }, - it.source + if (isElr) { + val result = stamper.stampObservation(observation) + if (!result.success) { + val logger = actionLogger.getItemLogger(item.index + 1, observation.id) + if (result.failures.isEmpty()) { + logger.warn(UnmappableConditionMessage()) + } else { + logger.warn( + result.failures.map { + UnmappableConditionMessage( + it.failures.map { it.code }, + it.source + ) + } ) } - ) } } } diff --git a/prime-router/src/main/kotlin/fhirengine/engine/RSMessageType.kt b/prime-router/src/main/kotlin/fhirengine/engine/RSMessageType.kt new file mode 100644 index 00000000000..d1db9dd9d12 --- /dev/null +++ b/prime-router/src/main/kotlin/fhirengine/engine/RSMessageType.kt @@ -0,0 +1,11 @@ +package gov.cdc.prime.router.fhirengine.engine + +/** + * This class represents a way to group message types from an RS perspective. As we add additional logical + * groupings, FHIRBundleHelpers.getRSMessageType will need to be updated. + * + */ +enum class RSMessageType { + LAB_RESULT, + UNKNOWN, +} \ No newline at end of file diff --git a/prime-router/src/main/kotlin/fhirengine/utils/FHIRBundleHelpers.kt b/prime-router/src/main/kotlin/fhirengine/utils/FHIRBundleHelpers.kt index 564d0d5e0e7..dc12c4a9e8c 100644 --- a/prime-router/src/main/kotlin/fhirengine/utils/FHIRBundleHelpers.kt +++ b/prime-router/src/main/kotlin/fhirengine/utils/FHIRBundleHelpers.kt @@ -9,12 +9,14 @@ import gov.cdc.prime.router.azure.ConditionStamper.Companion.BUNDLE_CODE_IDENTIF import gov.cdc.prime.router.azure.ConditionStamper.Companion.BUNDLE_VALUE_IDENTIFIER import gov.cdc.prime.router.azure.ConditionStamper.Companion.conditionCodeExtensionURL import gov.cdc.prime.router.codes +import gov.cdc.prime.router.fhirengine.engine.RSMessageType import gov.cdc.prime.router.fhirengine.translation.hl7.utils.CustomContext import gov.cdc.prime.router.fhirengine.translation.hl7.utils.FhirPathUtils import gov.cdc.prime.router.fhirengine.utils.FHIRBundleHelpers.Companion.getChildProperties import io.github.linuxforhealth.hl7.data.Hl7RelatedGeneralUtils import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.Bundle +import org.hl7.fhir.r4.model.CodeType import org.hl7.fhir.r4.model.CodeableConcept import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.DateTimeType @@ -116,6 +118,36 @@ fun Bundle.addProvenanceReference() { } } +/** + * Return true if Bundle contains an ELR in the MessageHeader. + * + * @return true if has a MesssageHeader that contains an R01 or ORU_R01, otherwise false. + */ +fun Bundle.isElr(): Boolean { + val code = FhirPathUtils.evaluate( + null, + this, + this, + "Bundle.entry.resource.ofType(MessageHeader).event.code" + ) + .filterIsInstance() + .firstOrNull() + ?.code + return ((code == "R01") || (code == "ORU_R01")) +} + +/** + * Return RSMessageType based on grouping logic. + * + * @return RSMessageType of this Bundle. + */ +fun Bundle.getRSMessageType(): RSMessageType { + return when { + isElr() -> RSMessageType.LAB_RESULT + else -> RSMessageType.UNKNOWN + } +} + /** * Gets all properties for a [Base] resource recursively and filters only its references * diff --git a/prime-router/src/test/kotlin/common/UniversalPipelineTestUtils.kt b/prime-router/src/test/kotlin/common/UniversalPipelineTestUtils.kt index b647a93f997..96ac1340b5d 100644 --- a/prime-router/src/test/kotlin/common/UniversalPipelineTestUtils.kt +++ b/prime-router/src/test/kotlin/common/UniversalPipelineTestUtils.kt @@ -40,16 +40,16 @@ import java.time.OffsetDateTime @Suppress("ktlint:standard:max-line-length") const val validFHIRRecord1 = - """{"resourceType":"Bundle","id":"1667861767830636000.7db38d22-b713-49fc-abfa-2edba9c12347","meta":{"lastUpdated":"2022-11-07T22:56:07.832+00:00"},"identifier":{"value":"1234d1d1-95fe-462c-8ac6-46728dba581c"},"type":"message","timestamp":"2021-08-03T13:15:11.015+00:00","entry":[{"fullUrl":"Observation/d683b42a-bf50-45e8-9fce-6c0531994f09","resource":{"resourceType":"Observation","id":"d683b42a-bf50-45e8-9fce-6c0531994f09","status":"final","code":{"coding":[{"system":"http://loinc.org","code":"80382-5"}],"text":"Flu A"},"subject":{"reference":"Patient/9473889b-b2b9-45ac-a8d8-191f27132912"},"performer":[{"reference":"Organization/1a0139b9-fc23-450b-9b6c-cd081e5cea9d"}],"valueCodeableConcept":{"coding":[{"system":"http://snomed.info/sct","code":"260373001","display":"Detected"}]},"interpretation":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0078","code":"A","display":"Abnormal"}]}],"method":{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/testkit-name-id","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/equipment-uid","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}}],"coding":[{"display":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B*"}]},"specimen":{"reference":"Specimen/52a582e4-d389-42d0-b738-bee51cf5244d"},"device":{"reference":"Device/78dc4d98-2958-43a3-a445-76ceef8c0698"}}}]}""" + """{"resourceType":"Bundle","id":"1667861767830636000.7db38d22-b713-49fc-abfa-2edba9c12347","meta":{"lastUpdated":"2022-11-07T22:56:07.832+00:00"},"identifier":{"value":"1234d1d1-95fe-462c-8ac6-46728dba581c"},"type":"message","timestamp":"2021-08-03T13:15:11.015+00:00","entry":[{"fullUrl":"MessageHeader/0993dd0b-6ce5-3caf-a177-0b81cc780c18","resource":{"resourceType":"MessageHeader","id":"0993dd0b-6ce5-3caf-a177-0b81cc780c18","extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/encoding-characters","valueString":"^~\\&#"},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/character-set","valueString":"UNICODE UTF-8"},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/msh-message-header","extension":[{"url":"MSH.7","valueString":"20230501102531-0400"}]}],"eventCoding":{"system":"http://terminology.hl7.org/CodeSystem/v2-0003","code":"R01","display":"ORU^R01^ORU_R01"},"sender":{"reference":"Organization/1710886092467181000.213628f7-9569-4400-a95d-621c3bfbf121"}}},{"fullUrl":"Observation/d683b42a-bf50-45e8-9fce-6c0531994f09","resource":{"resourceType":"Observation","id":"d683b42a-bf50-45e8-9fce-6c0531994f09","status":"final","code":{"coding":[{"system":"http://loinc.org","code":"80382-5"}],"text":"Flu A"},"subject":{"reference":"Patient/9473889b-b2b9-45ac-a8d8-191f27132912"},"performer":[{"reference":"Organization/1a0139b9-fc23-450b-9b6c-cd081e5cea9d"}],"valueCodeableConcept":{"coding":[{"system":"http://snomed.info/sct","code":"260373001","display":"Detected"}]},"interpretation":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0078","code":"A","display":"Abnormal"}]}],"method":{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/testkit-name-id","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/equipment-uid","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}}],"coding":[{"display":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B*"}]},"specimen":{"reference":"Specimen/52a582e4-d389-42d0-b738-bee51cf5244d"},"device":{"reference":"Device/78dc4d98-2958-43a3-a445-76ceef8c0698"}}}]}""" const val validFHIRRecord1Identifier = "1234d1d1-95fe-462c-8ac6-46728dba581c" @Suppress("ktlint:standard:max-line-length") const val conditionCodedValidFHIRRecord1 = - """{"resourceType":"Bundle","id":"1667861767830636000.7db38d22-b713-49fc-abfa-2edba9c12347","meta":{"lastUpdated":"2022-11-07T22:56:07.832+00:00"},"identifier":{"value":"1234d1d1-95fe-462c-8ac6-46728dba581c"},"type":"message","timestamp":"2021-08-03T13:15:11.015+00:00","entry":[{"fullUrl":"Observation/d683b42a-bf50-45e8-9fce-6c0531994f09","resource":{"resourceType":"Observation","id":"d683b42a-bf50-45e8-9fce-6c0531994f09","status":"final","code":{"coding":[{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code","valueCoding":{"system":"SNOMEDCT","code":"6142004","display":"Influenza (disorder)"}}],"system":"http://loinc.org","code":"80382-5"}],"text":"Flu A"},"subject":{"reference":"Patient/9473889b-b2b9-45ac-a8d8-191f27132912"},"performer":[{"reference":"Organization/1a0139b9-fc23-450b-9b6c-cd081e5cea9d"}],"valueCodeableConcept":{"coding":[{"system":"http://snomed.info/sct","code":"260373001","display":"Detected"}]},"interpretation":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0078","code":"A","display":"Abnormal"}]}],"method":{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/testkit-name-id","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/equipment-uid","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}}],"coding":[{"display":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B*"}]},"specimen":{"reference":"Specimen/52a582e4-d389-42d0-b738-bee51cf5244d"},"device":{"reference":"Device/78dc4d98-2958-43a3-a445-76ceef8c0698"}}}]}""" + """{"resourceType":"Bundle","id":"1667861767830636000.7db38d22-b713-49fc-abfa-2edba9c12347","meta":{"lastUpdated":"2022-11-07T22:56:07.832+00:00"},"identifier":{"value":"1234d1d1-95fe-462c-8ac6-46728dba581c"},"type":"message","timestamp":"2021-08-03T13:15:11.015+00:00","entry":[{"fullUrl":"MessageHeader/0993dd0b-6ce5-3caf-a177-0b81cc780c18","resource":{"resourceType":"MessageHeader","id":"0993dd0b-6ce5-3caf-a177-0b81cc780c18","extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/encoding-characters","valueString":"^~\\&#"},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/character-set","valueString":"UNICODE UTF-8"},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/msh-message-header","extension":[{"url":"MSH.7","valueString":"20230501102531-0400"}]}],"eventCoding":{"system":"http://terminology.hl7.org/CodeSystem/v2-0003","code":"R01","display":"ORU^R01^ORU_R01"},"sender":{"reference":"Organization/1710886092467181000.213628f7-9569-4400-a95d-621c3bfbf121"}}},{"fullUrl":"Observation/d683b42a-bf50-45e8-9fce-6c0531994f09","resource":{"resourceType":"Observation","id":"d683b42a-bf50-45e8-9fce-6c0531994f09","status":"final","code":{"coding":[{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code","valueCoding":{"system":"SNOMEDCT","code":"6142004","display":"Influenza (disorder)"}}],"system":"http://loinc.org","code":"80382-5"}],"text":"Flu A"},"subject":{"reference":"Patient/9473889b-b2b9-45ac-a8d8-191f27132912"},"performer":[{"reference":"Organization/1a0139b9-fc23-450b-9b6c-cd081e5cea9d"}],"valueCodeableConcept":{"coding":[{"system":"http://snomed.info/sct","code":"260373001","display":"Detected"}]},"interpretation":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0078","code":"A","display":"Abnormal"}]}],"method":{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/testkit-name-id","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/equipment-uid","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}}],"coding":[{"display":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B*"}]},"specimen":{"reference":"Specimen/52a582e4-d389-42d0-b738-bee51cf5244d"},"device":{"reference":"Device/78dc4d98-2958-43a3-a445-76ceef8c0698"}}}]}""" @Suppress("ktlint:standard:max-line-length") const val validFHIRRecord2 = - """{"resourceType":"Bundle","id":"1667861767830636000.7db38d22-b713-49fc-abfa-2edba9c09876","meta":{"lastUpdated":"2022-11-07T22:56:07.832+00:00"},"identifier":{"value":"1234d1d1-95fe-462c-8ac6-46728dbau8cd"},"type":"message","timestamp":"2021-08-03T13:15:11.015+00:00","entry":[{"fullUrl":"Observation/d683b42a-bf50-45e8-9fce-6c0531994f09","resource":{"resourceType":"Observation","id":"d683b42a-bf50-45e8-9fce-6c0531994f09","status":"final","code":{"coding":[{"system":"http://loinc.org","code":"41458-1"}],"text":"SARS "},"subject":{"reference":"Patient/9473889b-b2b9-45ac-a8d8-191f27132912"},"performer":[{"reference":"Organization/1a0139b9-fc23-450b-9b6c-cd081e5cea9d"}],"valueCodeableConcept":{"coding":[{"system":"http://snomed.info/sct","code":"260373001","display":"Detected"}]},"interpretation":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0078","code":"A","display":"Abnormal"}]}],"method":{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/testkit-name-id","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/equipment-uid","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}}],"coding":[{"display":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B*"}]},"specimen":{"reference":"Specimen/52a582e4-d389-42d0-b738-bee51cf5244d"},"device":{"reference":"Device/78dc4d98-2958-43a3-a445-76ceef8c0698"}}}]}""" + """{"resourceType":"Bundle","id":"1667861767830636000.7db38d22-b713-49fc-abfa-2edba9c09876","meta":{"lastUpdated":"2022-11-07T22:56:07.832+00:00"},"identifier":{"value":"1234d1d1-95fe-462c-8ac6-46728dbau8cd"},"type":"message","timestamp":"2021-08-03T13:15:11.015+00:00","entry":[{"fullUrl":"MessageHeader/0993dd0b-6ce5-3caf-a177-0b81cc780c18","resource":{"resourceType":"MessageHeader","id":"0993dd0b-6ce5-3caf-a177-0b81cc780c18","extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/encoding-characters","valueString":"^~\\&#"},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/character-set","valueString":"UNICODE UTF-8"},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/msh-message-header","extension":[{"url":"MSH.7","valueString":"20230501102531-0400"}]}],"eventCoding":{"system":"http://terminology.hl7.org/CodeSystem/v2-0003","code":"R01","display":"ORU^R01^ORU_R01"},"sender":{"reference":"Organization/1710886092467181000.213628f7-9569-4400-a95d-621c3bfbf121"}}},{"fullUrl":"Observation/d683b42a-bf50-45e8-9fce-6c0531994f09","resource":{"resourceType":"Observation","id":"d683b42a-bf50-45e8-9fce-6c0531994f09","status":"final","code":{"coding":[{"system":"http://loinc.org","code":"41458-1"}],"text":"SARS "},"subject":{"reference":"Patient/9473889b-b2b9-45ac-a8d8-191f27132912"},"performer":[{"reference":"Organization/1a0139b9-fc23-450b-9b6c-cd081e5cea9d"}],"valueCodeableConcept":{"coding":[{"system":"http://snomed.info/sct","code":"260373001","display":"Detected"}]},"interpretation":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0078","code":"A","display":"Abnormal"}]}],"method":{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/testkit-name-id","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/equipment-uid","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}}],"coding":[{"display":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B*"}]},"specimen":{"reference":"Specimen/52a582e4-d389-42d0-b738-bee51cf5244d"},"device":{"reference":"Device/78dc4d98-2958-43a3-a445-76ceef8c0698"}}}]}""" const val invalidEmptyFHIRRecord = "{}" const val invalidMalformedFHIRRecord = """{"resourceType":"Bund}""" diff --git a/prime-router/src/test/kotlin/fhirengine/azure/FHIRConverterIntegrationTests.kt b/prime-router/src/test/kotlin/fhirengine/azure/FHIRConverterIntegrationTests.kt index e95fb3d5806..b534f994a12 100644 --- a/prime-router/src/test/kotlin/fhirengine/azure/FHIRConverterIntegrationTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/azure/FHIRConverterIntegrationTests.kt @@ -508,7 +508,8 @@ class FHIRConverterIntegrationTests { orderingFacilityState = listOf("FL"), performerState = emptyList(), eventType = "ORU^R01^ORU_R01" - ) + ), + ReportStreamEventProperties.ENRICHMENTS to "" ) ) } @@ -663,7 +664,8 @@ class FHIRConverterIntegrationTests { orderingFacilityState = listOf("FL"), performerState = emptyList(), eventType = "ORU^R01^ORU_R01" - ) + ), + ReportStreamEventProperties.ENRICHMENTS to "" ) ) } @@ -798,7 +800,7 @@ class FHIRConverterIntegrationTests { ) assertThat(azureEventService.reportStreamEvents[ReportStreamEventName.ITEM_ACCEPTED]!!).hasSize(2) - val event = azureEventService + var event = azureEventService .reportStreamEvents[ReportStreamEventName.ITEM_ACCEPTED]!!.last() as ReportStreamItemEvent assertThat(event.reportEventData).isEqualToIgnoringGivenProperties( ReportEventData( @@ -835,8 +837,9 @@ class FHIRConverterIntegrationTests { patientState = emptyList(), orderingFacilityState = emptyList(), performerState = emptyList(), - eventType = "" - ) + eventType = "ORU^R01^ORU_R01" + ), + ReportStreamEventProperties.ENRICHMENTS to "" ) ) } diff --git a/prime-router/src/test/kotlin/fhirengine/azure/FHIRReceiverFilterIntegrationTests.kt b/prime-router/src/test/kotlin/fhirengine/azure/FHIRReceiverFilterIntegrationTests.kt index 4acfecbf826..d8bae65c3eb 100644 --- a/prime-router/src/test/kotlin/fhirengine/azure/FHIRReceiverFilterIntegrationTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/azure/FHIRReceiverFilterIntegrationTests.kt @@ -920,7 +920,7 @@ class FHIRReceiverFilterIntegrationTests : Logging { ReportStreamEventProperties.FILTER_TYPE to ReportStreamFilterType.QUALITY_FILTER, ReportStreamEventProperties.BUNDLE_DIGEST to BundleDigestLabResult( observationSummaries = AzureEventUtils.getObservationSummaries(bundle), - eventType = "", + eventType = "ORU^R01^ORU_R01", patientState = emptyList(), performerState = emptyList(), orderingFacilityState = emptyList() diff --git a/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt b/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt index 33731c0e5af..e23ef1b6e35 100644 --- a/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt @@ -344,6 +344,106 @@ class FhirConverterTests { @Test fun `test condition code stamping`() { + @Suppress("ktlint:standard:max-line-length") + val fhirRecord = + """{"resourceType":"Bundle","id":"1667861767830636000.7db38d22-b713-49fc-abfa-2edba9c12347","meta":{"lastUpdated":"2022-11-07T22:56:07.832+00:00"},"identifier":{"value":"1234d1d1-95fe-462c-8ac6-46728dba581c"},"type":"message","timestamp":"2021-08-03T13:15:11.015+00:00","entry":[{"fullUrl" : "MessageHeader/0993dd0b-6ce5-3caf-a177-0b81cc780c18","resource" : {"resourceType" : "MessageHeader","id" : "0993dd0b-6ce5-3caf-a177-0b81cc780c18","extension" : [ {"url" : "https://reportstream.cdc.gov/fhir/StructureDefinition/encoding-characters","valueString" : "^~\\&#"}, {"url" : "https://reportstream.cdc.gov/fhir/StructureDefinition/character-set","valueString" : "UNICODE UTF-8"}, {"url" : "https://reportstream.cdc.gov/fhir/StructureDefinition/msh-message-header","extension" : [ {"url" : "MSH.7","valueString" : "20230501102531-0400"} ]} ],"eventCoding" : {"system" : "http://terminology.hl7.org/CodeSystem/v2-0003","code" : "R01","display" : "ORU^R01^ORU_R01"},"sender" : {"reference" : "Organization/1710886092467181000.213628f7-9569-4400-a95d-621c3bfbf121"}}},{"fullUrl":"Observation/d683b42a-bf50-45e8-9fce-6c0531994f09","resource":{"resourceType":"Observation","id":"d683b42a-bf50-45e8-9fce-6c0531994f09","status":"final","code":{"coding":[{"system":"http://loinc.org","code":"80382-5"}],"text":"Flu A"},"subject":{"reference":"Patient/9473889b-b2b9-45ac-a8d8-191f27132912"},"performer":[{"reference":"Organization/1a0139b9-fc23-450b-9b6c-cd081e5cea9d"}],"valueCodeableConcept":{"coding":[{"system":"http://snomed.info/sct","code":"260373001","display":"Detected"}]},"interpretation":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0078","code":"A","display":"Abnormal"}]}],"method":{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/testkit-name-id","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/equipment-uid","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}}],"coding":[{"display":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B*"}]},"specimen":{"reference":"Specimen/52a582e4-d389-42d0-b738-bee51cf5244d"},"device":{"reference":"Device/78dc4d98-2958-43a3-a445-76ceef8c0698"}}}]}""" + + val conditionCodeExtensionURL = "https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code" + mockkObject(BlobAccess) + mockkObject(Report) + metadata.lookupTableStore += mapOf( + "observation-mapping" to LookupTable( + "observation-mapping", + listOf( + listOf( + ObservationMappingConstants.TEST_CODE_KEY, + ObservationMappingConstants.CONDITION_CODE_KEY, + ObservationMappingConstants.CONDITION_CODE_SYSTEM_KEY, + ObservationMappingConstants.CONDITION_NAME_KEY + ), + listOf( + "80382-5", + "6142004", + "SNOMEDCT", + "Influenza (disorder)" + ), + listOf( + "260373001", + "Some Condition Code", + "Condition Code System", + "Condition Name" + ) + ) + ) + ) + + // set up + val actionHistory = mockk() + val actionLogger = mockk() + val transformer = mockk() + + val engine = spyk(makeFhirEngine(metadata, settings, TaskAction.process) as FHIRConverter) + val message = spyk( + FhirConvertQueueMessage( + UUID.randomUUID(), + BLOB_FHIR_URL, + "test", + BLOB_SUB_FOLDER_NAME, + Topic.FULL_ELR, + SCHEMA_NAME + ) + ) + + val bodyFormat = MimeFormat.FHIR + val bodyUrl = "https://anyblob.com" + + every { actionLogger.hasErrors() } returns false + every { actionLogger.getItemLogger(any(), any()) } returns actionLogger + every { actionLogger.warn(any>()) } just runs + every { actionLogger.setReportId(any()) } returns actionLogger + every { BlobAccess.downloadBlob(any(), any()) } returns (fhirRecord) + every { Report.getFormatFromBlobURL(message.blobURL) } returns MimeFormat.FHIR + every { BlobAccess.Companion.uploadBlob(any(), any()) } returns "test" + every { accessSpy.insertTask(any(), bodyFormat.toString(), bodyUrl, any()) }.returns(Unit) + every { actionHistory.trackCreatedReport(any(), any(), blobInfo = any()) }.returns(Unit) + every { actionHistory.trackExistingInputReport(any()) }.returns(Unit) + val action = Action() + action.actionName = TaskAction.convert + every { actionHistory.action } returns action + every { engine.getTransformerFromSchema(SCHEMA_NAME) }.returns(transformer) + every { transformer.process(any()) } returnsArgument (0) + + // act + accessSpy.transact { txn -> + engine.run(message, actionLogger, actionHistory, txn) + } + + val bundle = FhirContext.forR4().newJsonParser().parseResource(Bundle::class.java, fhirRecord) + bundle.entry.filter { it.resource is Observation }.forEach { + val observation = (it.resource as Observation) + observation.code.coding[0].addExtension( + conditionCodeExtensionURL, + Coding("SNOMEDCT", "6142004", "Influenza (disorder)") + ) + observation.valueCodeableConcept.coding[0].addExtension( + conditionCodeExtensionURL, + Coding("Condition Code System", "Some Condition Code", "Condition Name") + ) + } + + // assert + verify(exactly = 1) { + // TODO clean up assertions + // engine.getContentFromFHIR(any(), any()) + actionHistory.trackExistingInputReport(any()) + transformer.process(any()) + actionHistory.trackCreatedReport(any(), any(), blobInfo = any()) + BlobAccess.Companion.uploadBlob(any(), FhirTranscoder.encode(bundle).toByteArray(), any()) + } + } + + @Test + fun `test condition code stamping without message header`() { @Suppress("ktlint:standard:max-line-length") val fhirRecord = """{"resourceType":"Bundle","id":"1667861767830636000.7db38d22-b713-49fc-abfa-2edba9c12347","meta":{"lastUpdated":"2022-11-07T22:56:07.832+00:00"},"identifier":{"value":"1234d1d1-95fe-462c-8ac6-46728dba581c"},"type":"message","timestamp":"2021-08-03T13:15:11.015+00:00","entry":[{"fullUrl":"Observation/d683b42a-bf50-45e8-9fce-6c0531994f09","resource":{"resourceType":"Observation","id":"d683b42a-bf50-45e8-9fce-6c0531994f09","status":"final","code":{"coding":[{"system":"http://loinc.org","code":"80382-5"}],"text":"Flu A"},"subject":{"reference":"Patient/9473889b-b2b9-45ac-a8d8-191f27132912"},"performer":[{"reference":"Organization/1a0139b9-fc23-450b-9b6c-cd081e5cea9d"}],"valueCodeableConcept":{"coding":[{"system":"http://snomed.info/sct","code":"260373001","display":"Detected"}]},"interpretation":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0078","code":"A","display":"Abnormal"}]}],"method":{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/testkit-name-id","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/equipment-uid","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}}],"coding":[{"display":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B*"}]},"specimen":{"reference":"Specimen/52a582e4-d389-42d0-b738-bee51cf5244d"},"device":{"reference":"Device/78dc4d98-2958-43a3-a445-76ceef8c0698"}}}]}""" @@ -438,6 +538,8 @@ class FhirConverterTests { actionHistory.trackExistingInputReport(any()) transformer.process(any()) actionHistory.trackCreatedReport(any(), any(), blobInfo = any()) + } + verify(exactly = 0) { BlobAccess.Companion.uploadBlob(any(), FhirTranscoder.encode(bundle).toByteArray(), any()) } } @@ -560,12 +662,11 @@ class FhirConverterTests { @Test fun `should log an error and return no bundles if the message is empty`() { mockkObject(BlobAccess) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") val engine = spyk(makeFhirEngine(metadata, settings, TaskAction.process) as FHIRConverter) val actionLogger = ActionLogger() every { BlobAccess.downloadBlob(any(), any()) } returns "" - val bundles = engine.process( - MimeFormat.FHIR, "", "", Topic.FULL_ELR, actionLogger - ) + val bundles = engine.process(MimeFormat.FHIR, input, actionLogger) assertThat(bundles).isEmpty() assertThat(actionLogger.errors.map { it.detail.message }).contains("Provided raw data is empty.") } @@ -587,9 +688,8 @@ class FhirConverterTests { every { mockMessage.topic } returns Topic.FULL_ELR every { mockMessage.reportId } returns UUID.randomUUID() every { BlobAccess.downloadBlob(any(), any()) } returns simpleHL7 - val bundles = engine.process( - MimeFormat.HL7, "", "", Topic.FULL_ELR, actionLogger - ) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val bundles = engine.process(MimeFormat.HL7, input, actionLogger) assertThat(bundles).isEmpty() assertThat( actionLogger.errors.map { @@ -604,9 +704,8 @@ class FhirConverterTests { val engine = spyk(makeFhirEngine(metadata, settings, TaskAction.process) as FHIRConverter) val actionLogger = ActionLogger() every { BlobAccess.downloadBlob(any(), any()) } returns "test,1,2" - val bundles = engine.process( - MimeFormat.CSV, "", "", Topic.FULL_ELR, actionLogger - ) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val bundles = engine.process(MimeFormat.CSV, input, actionLogger) assertThat(bundles).isEmpty() assertThat(actionLogger.errors.map { it.detail.message }) .contains("Received unsupported report format: CSV") @@ -618,7 +717,8 @@ class FhirConverterTests { val engine = spyk(makeFhirEngine(metadata, settings, TaskAction.process) as FHIRConverter) val actionLogger = ActionLogger() every { BlobAccess.downloadBlob(any(), any()) } returns "{\"id\":}" - val processedItems = engine.process(MimeFormat.FHIR, "", "", Topic.FULL_ELR, actionLogger) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val processedItems = engine.process(MimeFormat.FHIR, input, actionLogger) assertThat(processedItems).hasSize(1) assertThat(processedItems.first().bundle).isNull() assertThat(actionLogger.errors.map { it.detail.message }).contains( @@ -648,9 +748,8 @@ class FhirConverterTests { every { mockMessage.topic } returns Topic.FULL_ELR every { mockMessage.reportId } returns UUID.randomUUID() every { BlobAccess.downloadBlob(any(), any()) } returns "{\"id\":\"1\", \"resourceType\":\"Bundle\"}" - val processedItems = engine.process( - MimeFormat.FHIR, "", "", Topic.FULL_ELR, actionLogger - ) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val processedItems = engine.process(MimeFormat.FHIR, input, actionLogger) assertThat(processedItems).hasSize(1) assertThat(processedItems.first().bundle).isNull() assertThat(actionLogger.errors.map { it.detail.message }).contains( @@ -669,7 +768,8 @@ class FhirConverterTests { every { BlobAccess.downloadBlob(any(), any()) } returns unparseableHL7 - val processedItems = engine.process(MimeFormat.HL7, "", "", Topic.FULL_ELR, actionLogger) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val processedItems = engine.process(MimeFormat.HL7, input, actionLogger) assertThat(processedItems).hasSize(1) assertThat(processedItems.first().bundle).isNull() assertThat( @@ -704,7 +804,8 @@ class FhirConverterTests { every { BlobAccess.downloadBlob(any(), any()) } returns simpleHL7 - val processedItems = engine.process(MimeFormat.HL7, "", "", Topic.FULL_ELR, actionLogger) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val processedItems = engine.process(MimeFormat.HL7, input, actionLogger) assertThat(processedItems).hasSize(1) assertThat(processedItems.first().bundle).isNull() @Suppress("ktlint:standard:max-line-length") @@ -733,7 +834,8 @@ class FhirConverterTests { every { BlobAccess.downloadBlob(any(), any()) } returns simpleHL7 - val processedItems = engine.process(MimeFormat.HL7, "", "", Topic.FULL_ELR, actionLogger) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val processedItems = engine.process(MimeFormat.HL7, input, actionLogger) assertThat(processedItems).hasSize(1) assertThat(processedItems.first().bundle).isNull() assertThat( @@ -759,14 +861,15 @@ class FhirConverterTests { } returns """{\"id\":} {"id":"1", "resourceType":"Bundle"} """.trimMargin() - val processedItems = engine.process(MimeFormat.FHIR, "", "", Topic.FULL_ELR, actionLogger) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val processedItems = engine.process(MimeFormat.FHIR, input, actionLogger) assertThat(processedItems).hasSize(2) assertThat(actionLogger.errors.map { it.detail.message }).contains( @Suppress("ktlint:standard:max-line-length") "Item 1 in the report was not parseable. Reason: exception while parsing FHIR: HAPI-1861: Failed to parse JSON encoded FHIR content: Unexpected character ('\\' (code 92)): was expecting double-quote to start field name\n at [line: 1, column: 2]" ) - val bundles2 = engine.process(MimeFormat.FHIR, "", "", Topic.FULL_ELR, actionLogger, false) + val bundles2 = engine.process(MimeFormat.FHIR, input, actionLogger, false) assertThat(bundles2).hasSize(0) assertThat(actionLogger.errors.map { it.detail.message }).contains( @Suppress("ktlint:standard:max-line-length") @@ -786,7 +889,8 @@ class FhirConverterTests { every { BlobAccess.downloadBlob(any(), any()) } returns simpleHL7 - val bundles = engine.process(MimeFormat.HL7, "", "", Topic.FULL_ELR, actionLogger) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val bundles = engine.process(MimeFormat.HL7, input, actionLogger) assertThat(bundles).hasSize(1) assertThat(actionLogger.errors).isEmpty() } @@ -806,7 +910,8 @@ class FhirConverterTests { every { BlobAccess.downloadBlob(any(), any()) } returns simpleHL7 + "\n" + simpleHL7 + "\n" + simpleHL7 - val bundles = engine.process(MimeFormat.HL7, "", "", Topic.FULL_ELR, actionLogger) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val bundles = engine.process(MimeFormat.HL7, input, actionLogger) assertThat(bundles).hasSize(3) assertThat(actionLogger.errors).isEmpty() @@ -837,7 +942,8 @@ class FhirConverterTests { every { BlobAccess.downloadBlob(any(), any()) } returns simpleHL7 - val bundles = engine.process(MimeFormat.HL7, "", "", Topic.FULL_ELR, actionLogger) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val bundles = engine.process(MimeFormat.HL7, input, actionLogger) assertThat(bundles).hasSize(1) assertThat(actionLogger.errors).isEmpty() } diff --git a/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt b/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt index a9d4a282cf6..5abc630dc42 100644 --- a/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt @@ -31,6 +31,7 @@ import gov.cdc.prime.router.azure.ConditionStamper.Companion.conditionCodeExtens import gov.cdc.prime.router.azure.DatabaseAccess import gov.cdc.prime.router.azure.LookupTableConditionMapper import gov.cdc.prime.router.azure.QueueAccess +import gov.cdc.prime.router.fhirengine.engine.RSMessageType import gov.cdc.prime.router.fhirengine.translation.hl7.utils.CustomContext import gov.cdc.prime.router.fhirengine.translation.hl7.utils.FhirPathUtils import gov.cdc.prime.router.metadata.LookupTable @@ -45,6 +46,7 @@ import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.DiagnosticReport import org.hl7.fhir.r4.model.Endpoint import org.hl7.fhir.r4.model.Extension +import org.hl7.fhir.r4.model.MessageHeader import org.hl7.fhir.r4.model.Observation import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.PractitionerRole @@ -195,6 +197,91 @@ class FHIRBundleHelpersTests { assertThat(diagnosticReport.getResourceProperties()).isNotEmpty() } + @Test + fun `Test if ELR when bundle is empty`() { + val fhirBundle = Bundle() + assertThat(fhirBundle.isElr()).isFalse() + } + + @Test + fun `Test if ELR when bundle is not BundleType MESSAGE`() { + val fhirBundle = Bundle() + fhirBundle.type = Bundle.BundleType.DOCUMENT + assertThat(fhirBundle.isElr()).isFalse() + } + + @Test + fun `Test if ELR when bundle has no entries`() { + val fhirBundle = Bundle() + fhirBundle.type = Bundle.BundleType.MESSAGE + assertThat(fhirBundle.isElr()).isFalse() + } + + @Test + fun `Test if ELR when bundle has no MessageHeader`() { + val fhirBundle = Bundle() + fhirBundle.type = Bundle.BundleType.MESSAGE + val entry = Bundle.BundleEntryComponent() + fhirBundle.entry.add(0, entry) + assertThat(fhirBundle.isElr()).isFalse() + } + + @Test + fun `Test if ELR when bundle has MessageHeader but no Coding event`() { + val fhirBundle = Bundle() + fhirBundle.type = Bundle.BundleType.MESSAGE + val entry = Bundle.BundleEntryComponent() + entry.resource = MessageHeader() + fhirBundle.entry.add(0, entry) + assertThat(fhirBundle.isElr()).isFalse() + } + + @Test + fun `Test if ELR when bundle has MessageHeader but Coding event not R01`() { + val fhirBundle = Bundle() + fhirBundle.type = Bundle.BundleType.MESSAGE + val entry = Bundle.BundleEntryComponent() + val messageHeader = MessageHeader() + val event = Coding() + messageHeader.event = event + entry.resource = messageHeader + fhirBundle.entry.add(0, entry) + assertThat(fhirBundle.isElr()).isFalse() + } + + @Test + fun `Test if ELR when bundle is happy path`() { + val fhirBundle = Bundle() + fhirBundle.type = Bundle.BundleType.MESSAGE + val entry = Bundle.BundleEntryComponent() + val messageHeader = MessageHeader() + var event = Coding() + event.code = "R01" + messageHeader.event = event + entry.resource = messageHeader + fhirBundle.entry.add(0, entry) + assertThat(fhirBundle.isElr()).isTrue() + event.code = "ORU_R01" + assertThat(fhirBundle.isElr()).isTrue() + event.code = "R21" + assertThat(fhirBundle.isElr()).isFalse() + } + + @Test + fun `Test current values for rs message type`() { + val fhirBundle = Bundle() + assertThat(fhirBundle.getRSMessageType()).isEqualTo(RSMessageType.UNKNOWN) + fhirBundle.type = Bundle.BundleType.MESSAGE + val entry = Bundle.BundleEntryComponent() + val messageHeader = MessageHeader() + val event = Coding() + event.code = "R01" + messageHeader.event = event + entry.resource = messageHeader + fhirBundle.entry.add(0, entry) + assertThat(fhirBundle.getRSMessageType()).isEqualTo(RSMessageType.LAB_RESULT) + } + @Test fun `Test find Diagnostic report no observation`() { val actionLogger = ActionLogger()