From cffc7939993d79716460ef83d19737e1bc8e5882 Mon Sep 17 00:00:00 2001 From: kant Date: Wed, 20 Nov 2024 11:15:06 -0800 Subject: [PATCH 01/10] Added member oid extension for each observation --- .../src/main/kotlin/azure/ConditionMapper.kt | 46 ++++++++++++- .../azure/observability/event/TestSummary.kt | 4 +- .../fhirengine/utils/FHIRBundleHelpers.kt | 4 +- ...rPathBundleDigestExtractorStrategyTests.kt | 4 +- .../fhirengine/engine/FhirConverterTests.kt | 68 ++++++++++++++++++- .../engine/FhirReceiverFilterTests.kt | 12 ++-- .../utils/FHIRBundleHelpersTests.kt | 12 ++-- 7 files changed, 128 insertions(+), 22 deletions(-) diff --git a/prime-router/src/main/kotlin/azure/ConditionMapper.kt b/prime-router/src/main/kotlin/azure/ConditionMapper.kt index e14ba24276d..2ab18dc3b0c 100644 --- a/prime-router/src/main/kotlin/azure/ConditionMapper.kt +++ b/prime-router/src/main/kotlin/azure/ConditionMapper.kt @@ -4,7 +4,9 @@ import gov.cdc.prime.router.Metadata import gov.cdc.prime.router.fhirengine.utils.getCodeSourcesMap import gov.cdc.prime.router.metadata.ObservationMappingConstants import org.hl7.fhir.r4.model.Coding +import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.Observation +import org.hl7.fhir.r4.model.StringType interface IConditionMapper { /** @@ -12,6 +14,12 @@ interface IConditionMapper { * @return a map associating test [codings] to their diagnostic conditions as Coding's */ fun lookupConditions(codings: List): Map> + + /** + * Lookup test code to Member OID mappings for the given [codings]. + * @return a map associating test codes to their Member OIDs + */ + fun lookupMemberOid(codings: List): Map } class LookupTableConditionMapper(metadata: Metadata) : IConditionMapper { @@ -34,16 +42,32 @@ class LookupTableConditionMapper(metadata: Metadata) : IConditionMapper { acc } } + + override fun lookupMemberOid(codings: List): Map { + return mappingTable.FilterBuilder() + .isIn(ObservationMappingConstants.TEST_CODE_KEY, codings.map { it.code }) + .filter().caseSensitiveDataRowsMap.fold(mutableMapOf()) { acc, condition -> + val testCode = condition[ObservationMappingConstants.TEST_CODE_KEY] ?: "" + val memberOid = condition[ObservationMappingConstants.TEST_OID_KEY] ?: "" + if (testCode.isNotEmpty() && memberOid.isNotEmpty()) { + acc[testCode] = memberOid + } + acc + } + } } class ConditionStamper(private val conditionMapper: IConditionMapper) { companion object { - const val conditionCodeExtensionURL = "https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code" + const val CONDITION_CODE_EXTENSION_URL = "https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code" + const val MEMBER_OID_EXTENSION_URL = + "https://reportstream.cdc.gov/fhir/StructureDefinition/test-performed-member-oid" const val BUNDLE_CODE_IDENTIFIER = "observation.code.coding.code" const val BUNDLE_VALUE_IDENTIFIER = "observation.valueCodeableConcept.coding.code" const val MAPPING_CODES_IDENTIFIER = "observation.{code|valueCodeableConcept}.coding.code" } + data class ObservationMappingFailure(val source: String, val failures: List) data class ObservationStampingResult( @@ -52,7 +76,7 @@ class ConditionStamper(private val conditionMapper: IConditionMapper) { ) /** - * Lookup condition codes for an [observation] and add them as custom extensions + * Lookup condition codes and member OIDs for an [observation] and add them as custom extensions * @param observation the observation that will be stamped * @return a [ObservationStampingResult] including stamping success and any mapping failures */ @@ -60,16 +84,20 @@ class ConditionStamper(private val conditionMapper: IConditionMapper) { val codeSourcesMap = observation.getCodeSourcesMap().filterValues { it.isNotEmpty() } if (codeSourcesMap.values.flatten().isEmpty()) return ObservationStampingResult(false) + // Lookup conditions and Member OIDs val conditionsToCode = conditionMapper.lookupConditions(codeSourcesMap.values.flatten()) + val memberOidMap = conditionMapper.lookupMemberOid(codeSourcesMap.values.flatten()) + var mappedSomething = false + // Process condition mappings val failures = codeSourcesMap.mapNotNull { codes -> val unnmapped = codes.value.mapNotNull { code -> val conditions = conditionsToCode.getOrDefault(code, emptyList()) if (conditions.isEmpty()) { code } else { - conditions.forEach { code.addExtension(conditionCodeExtensionURL, it) } + conditions.forEach { code.addExtension(CONDITION_CODE_EXTENSION_URL, it) } mappedSomething = true null } @@ -77,6 +105,18 @@ class ConditionStamper(private val conditionMapper: IConditionMapper) { if (unnmapped.isEmpty()) null else ObservationMappingFailure(codes.key, unnmapped) } + // Add the Member OID extension to the observation, based on the lookup + observation.code.coding.forEach { coding -> + val testCode = coding.code + val memberOid = memberOidMap[testCode] + if (memberOid != null) { + val memberOidExtension = Extension(MEMBER_OID_EXTENSION_URL) + memberOidExtension.setValue(StringType(memberOid)) + observation.addExtension(memberOidExtension) + mappedSomething = true + } + } + return ObservationStampingResult(mappedSomething, failures) } } \ No newline at end of file diff --git a/prime-router/src/main/kotlin/azure/observability/event/TestSummary.kt b/prime-router/src/main/kotlin/azure/observability/event/TestSummary.kt index c1486968977..6b333f45083 100644 --- a/prime-router/src/main/kotlin/azure/observability/event/TestSummary.kt +++ b/prime-router/src/main/kotlin/azure/observability/event/TestSummary.kt @@ -1,6 +1,6 @@ package gov.cdc.prime.router.azure.observability.event -import gov.cdc.prime.router.azure.ConditionStamper.Companion.conditionCodeExtensionURL +import gov.cdc.prime.router.azure.ConditionStamper.Companion.CONDITION_CODE_EXTENSION_URL import org.hl7.fhir.r4.model.Coding data class TestSummary( @@ -17,7 +17,7 @@ data class TestSummary( */ fun fromCoding(coding: Coding): TestSummary { val conditions = coding.extension - .filter { it.url == conditionCodeExtensionURL } + .filter { it.url == CONDITION_CODE_EXTENSION_URL } .map { it.castToCoding(it.value) } .map(CodeSummary::fromCoding) return TestSummary( diff --git a/prime-router/src/main/kotlin/fhirengine/utils/FHIRBundleHelpers.kt b/prime-router/src/main/kotlin/fhirengine/utils/FHIRBundleHelpers.kt index 564d0d5e0e7..e1774c682c5 100644 --- a/prime-router/src/main/kotlin/fhirengine/utils/FHIRBundleHelpers.kt +++ b/prime-router/src/main/kotlin/fhirengine/utils/FHIRBundleHelpers.kt @@ -7,7 +7,7 @@ import gov.cdc.prime.router.ReportStreamConditionFilter import gov.cdc.prime.router.ReportStreamFilter import gov.cdc.prime.router.azure.ConditionStamper.Companion.BUNDLE_CODE_IDENTIFIER 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.azure.ConditionStamper.Companion.CONDITION_CODE_EXTENSION_URL import gov.cdc.prime.router.codes import gov.cdc.prime.router.fhirengine.translation.hl7.utils.CustomContext import gov.cdc.prime.router.fhirengine.translation.hl7.utils.FhirPathUtils @@ -63,7 +63,7 @@ fun Observation.getMappedConditionExtensions(): List { return this.getCodeSourcesMap() .flatMap { it.value } .flatMap { it.extension } - .filter { it.url == conditionCodeExtensionURL } + .filter { it.url == CONDITION_CODE_EXTENSION_URL } } /** diff --git a/prime-router/src/test/kotlin/azure/observability/bundleDigest/FhirPathBundleDigestExtractorStrategyTests.kt b/prime-router/src/test/kotlin/azure/observability/bundleDigest/FhirPathBundleDigestExtractorStrategyTests.kt index 34b84489383..826cd4146f0 100644 --- a/prime-router/src/test/kotlin/azure/observability/bundleDigest/FhirPathBundleDigestExtractorStrategyTests.kt +++ b/prime-router/src/test/kotlin/azure/observability/bundleDigest/FhirPathBundleDigestExtractorStrategyTests.kt @@ -3,7 +3,7 @@ package gov.cdc.prime.router.azure.observability.bundleDigest import assertk.assertThat import assertk.assertions.isDataClassEqualTo import fhirengine.engine.CustomFhirPathFunctions -import gov.cdc.prime.router.azure.ConditionStamper.Companion.conditionCodeExtensionURL +import gov.cdc.prime.router.azure.ConditionStamper.Companion.CONDITION_CODE_EXTENSION_URL import gov.cdc.prime.router.azure.observability.event.CodeSummary import gov.cdc.prime.router.azure.observability.event.ObservationSummary import gov.cdc.prime.router.azure.observability.event.TestSummary @@ -152,7 +152,7 @@ class FhirPathBundleDigestExtractorStrategyTests { val observation = Observation() val coding = Coding() val extension = Extension() - extension.url = conditionCodeExtensionURL + extension.url = CONDITION_CODE_EXTENSION_URL extension.setValue(Coding()) coding.extension = listOf(extension) observation.code.coding = listOf(coding) diff --git a/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt b/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt index 33731c0e5af..1e6182b91c7 100644 --- a/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt @@ -27,7 +27,9 @@ import gov.cdc.prime.router.SettingsProvider import gov.cdc.prime.router.Topic import gov.cdc.prime.router.azure.ActionHistory import gov.cdc.prime.router.azure.BlobAccess +import gov.cdc.prime.router.azure.ConditionStamper import gov.cdc.prime.router.azure.DatabaseAccess +import gov.cdc.prime.router.azure.LookupTableConditionMapper import gov.cdc.prime.router.azure.SubmissionTableService import gov.cdc.prime.router.azure.db.enums.TaskAction import gov.cdc.prime.router.azure.db.tables.pojos.Action @@ -60,9 +62,12 @@ import io.mockk.verify 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 org.jooq.tools.jdbc.MockConnection import org.jooq.tools.jdbc.MockDataProvider import org.jooq.tools.jdbc.MockResult +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested @@ -359,7 +364,7 @@ class FhirConverterTests { ObservationMappingConstants.TEST_CODE_KEY, ObservationMappingConstants.CONDITION_CODE_KEY, ObservationMappingConstants.CONDITION_CODE_SYSTEM_KEY, - ObservationMappingConstants.CONDITION_NAME_KEY + ObservationMappingConstants.CONDITION_NAME_KEY, ), listOf( "80382-5", @@ -442,6 +447,67 @@ class FhirConverterTests { } } + @Test + fun `test condition code and member OID 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":"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 memberOidExtensionURL = "https://reportstream.cdc.gov/fhir/StructureDefinition/test-performed-member-oid" + + 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, + ObservationMappingConstants.TEST_OID_KEY + ), + listOf( + "80382-5", + "6142004", + "SNOMEDCT", + "Influenza (disorder)", + "OID12345" + ), + listOf( + "260373001", + "Some Condition Code", + "Condition Code System", + "Condition Name", + "OID67890" + ) + ) + ) + ) + + val bundle = FhirContext.forR4().newJsonParser().parseResource(Bundle::class.java, fhirRecord) + + bundle.entry.filter { it.resource is Observation }.forEach { + val observation = (it.resource as Observation) + + // Add Condition and Member OID extensions + ConditionStamper(LookupTableConditionMapper(metadata)).stampObservation(observation) + + // Assert condition extensions + val conditionExtension = observation.code.coding[0].extension.find { + it.url == "https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code" + } + assertNotNull(conditionExtension) + assertEquals("6142004", (conditionExtension!!.value as Coding).code) + + // Assert Member OID extension + val memberOidExtension = observation.extension.find { + it.url == memberOidExtensionURL + } + assertNotNull(memberOidExtension) + assertEquals("OID12345", (memberOidExtension!!.value as StringType).value) + } + } + @Test fun `test fully unmapped condition code stamping logs errors`() { val fhirData = File(BATCH_VALID_DATA_URL).readText() diff --git a/prime-router/src/test/kotlin/fhirengine/engine/FhirReceiverFilterTests.kt b/prime-router/src/test/kotlin/fhirengine/engine/FhirReceiverFilterTests.kt index a6c989e9a66..d36694d5274 100644 --- a/prime-router/src/test/kotlin/fhirengine/engine/FhirReceiverFilterTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/engine/FhirReceiverFilterTests.kt @@ -27,7 +27,7 @@ import gov.cdc.prime.router.TestSource import gov.cdc.prime.router.Topic import gov.cdc.prime.router.azure.ActionHistory import gov.cdc.prime.router.azure.BlobAccess -import gov.cdc.prime.router.azure.ConditionStamper.Companion.conditionCodeExtensionURL +import gov.cdc.prime.router.azure.ConditionStamper.Companion.CONDITION_CODE_EXTENSION_URL import gov.cdc.prime.router.azure.DatabaseAccess import gov.cdc.prime.router.azure.db.enums.TaskAction import gov.cdc.prime.router.azure.db.tables.pojos.ReportFile @@ -508,7 +508,7 @@ class FhirReceiverFilterTests { val coding = it.code.coding.first() if (coding.extension.isEmpty()) { coding.addExtension( - conditionCodeExtensionURL, + CONDITION_CODE_EXTENSION_URL, Coding( "system", "AOE", "name" ) @@ -569,11 +569,11 @@ class FhirReceiverFilterTests { bundle.entry.filter { it.resource is Observation }.forEach { val observation = (it.resource as Observation) observation.code.coding[0].addExtension( - conditionCodeExtensionURL, + CONDITION_CODE_EXTENSION_URL, Coding("SNOMEDCT", "6142004", "Influenza (disorder)") ) observation.valueCodeableConcept.coding[0].addExtension( - conditionCodeExtensionURL, + CONDITION_CODE_EXTENSION_URL, Coding("Condition Code System", "foobar", "Condition Name") ) } @@ -681,11 +681,11 @@ class FhirReceiverFilterTests { val bundle = FhirContext.forR4().newJsonParser().parseResource(Bundle::class.java, fhirRecord) bundle.getObservations().forEach { observation -> observation.code.coding[0].addExtension( - conditionCodeExtensionURL, + CONDITION_CODE_EXTENSION_URL, Coding("SNOMEDCT", "6142004", "Influenza (disorder)") ) observation.valueCodeableConcept.coding[0].addExtension( - conditionCodeExtensionURL, + CONDITION_CODE_EXTENSION_URL, Coding("Condition Code System", "Some Condition Code", "Condition Name") ) } diff --git a/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt b/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt index a9d4a282cf6..3e4f694f6d8 100644 --- a/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt @@ -27,7 +27,7 @@ import gov.cdc.prime.router.Schema import gov.cdc.prime.router.Topic import gov.cdc.prime.router.azure.BlobAccess import gov.cdc.prime.router.azure.ConditionStamper -import gov.cdc.prime.router.azure.ConditionStamper.Companion.conditionCodeExtensionURL +import gov.cdc.prime.router.azure.ConditionStamper.Companion.CONDITION_CODE_EXTENSION_URL import gov.cdc.prime.router.azure.DatabaseAccess import gov.cdc.prime.router.azure.LookupTableConditionMapper import gov.cdc.prime.router.azure.QueueAccess @@ -497,7 +497,7 @@ class FHIRBundleHelpersTests { val fhirRecord = File(VALID_ROUTING_DATA_URL).readText() val bundle = FhirContext.forR4().newJsonParser().parseResource(Bundle::class.java, fhirRecord) bundle.getObservations()[0].code.coding[0].addExtension( - conditionCodeExtensionURL, Coding("SOMESYSTEM", "840539006", "SOMECONDITION") + CONDITION_CODE_EXTENSION_URL, Coding("SOMESYSTEM", "840539006", "SOMECONDITION") ) val filteredBundle = bundle.filterMappedObservations( @@ -800,7 +800,7 @@ class FHIRBundleHelpersTests { assertThat(failure.failures.first().code).isEqualTo("some-unmapped-code") val extension = code.coding.first().extension.first() - assertThat(extension.url).isEqualTo(conditionCodeExtensionURL) + assertThat(extension.url).isEqualTo(CONDITION_CODE_EXTENSION_URL) assertThat((extension.value as? Coding)?.code).isEqualTo("6142004") } @@ -853,7 +853,7 @@ class FHIRBundleHelpersTests { val extensions = entry.getMappedConditionExtensions() assertThat(extensions) .extracting { it.url } - .each { it.isEqualTo(conditionCodeExtensionURL) } + .each { it.isEqualTo(CONDITION_CODE_EXTENSION_URL) } } @Test @@ -899,7 +899,7 @@ class FHIRBundleHelpersTests { assertThat(result.failures).isEmpty() val extension = code.coding.first().extension.first() - assertThat(extension.url).isEqualTo(conditionCodeExtensionURL) + assertThat(extension.url).isEqualTo(CONDITION_CODE_EXTENSION_URL) assertThat(extension.value) .isInstanceOf() .transform { it.code } @@ -947,7 +947,7 @@ class FHIRBundleHelpersTests { assertThat(result.failures).isEmpty() val extension = code.coding.first().extension.first() - assertThat(extension.url).isEqualTo(conditionCodeExtensionURL) + assertThat(extension.url).isEqualTo(CONDITION_CODE_EXTENSION_URL) assertThat(extension.value) .isInstanceOf() .transform { it.code } From d14dae0dc2272e406861b07d7a2f32b949f2bdf7 Mon Sep 17 00:00:00 2001 From: kant Date: Tue, 3 Dec 2024 02:25:49 -0800 Subject: [PATCH 02/10] stamping oid for each condition rather than for each observation --- .../src/main/kotlin/azure/ConditionMapper.kt | 69 +++++++++++-------- .../fhirengine/engine/FhirConverterTests.kt | 31 ++++----- 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/prime-router/src/main/kotlin/azure/ConditionMapper.kt b/prime-router/src/main/kotlin/azure/ConditionMapper.kt index 2ab18dc3b0c..df3c0b9c12e 100644 --- a/prime-router/src/main/kotlin/azure/ConditionMapper.kt +++ b/prime-router/src/main/kotlin/azure/ConditionMapper.kt @@ -44,16 +44,23 @@ class LookupTableConditionMapper(metadata: Metadata) : IConditionMapper { } override fun lookupMemberOid(codings: List): Map { - return mappingTable.FilterBuilder() - .isIn(ObservationMappingConstants.TEST_CODE_KEY, codings.map { it.code }) - .filter().caseSensitiveDataRowsMap.fold(mutableMapOf()) { acc, condition -> - val testCode = condition[ObservationMappingConstants.TEST_CODE_KEY] ?: "" - val memberOid = condition[ObservationMappingConstants.TEST_OID_KEY] ?: "" - if (testCode.isNotEmpty() && memberOid.isNotEmpty()) { - acc[testCode] = memberOid - } - acc + // Extract condition codes using the mapping table, not directly from codings + val testCodes = codings.mapNotNull { it.code } // These are the input test codes + + // Filter rows related to condition mappings based on test codes + val filteredRows = mappingTable.FilterBuilder() + .isIn(ObservationMappingConstants.TEST_CODE_KEY, testCodes) // Map test codes to conditions + .filter().caseSensitiveDataRowsMap + + // Create a map of condition codes to member OIDs + return filteredRows.fold(mutableMapOf()) { acc, condition -> + val conditionCode = condition[ObservationMappingConstants.CONDITION_CODE_KEY] ?: "" + val memberOid = condition[ObservationMappingConstants.TEST_OID_KEY] ?: "" + if (conditionCode.isNotEmpty() && memberOid.isNotEmpty()) { + acc[conditionCode] = memberOid } + acc + } } } @@ -81,42 +88,48 @@ class ConditionStamper(private val conditionMapper: IConditionMapper) { * @return a [ObservationStampingResult] including stamping success and any mapping failures */ fun stampObservation(observation: Observation): ObservationStampingResult { + // Extract codes and filter out empty values val codeSourcesMap = observation.getCodeSourcesMap().filterValues { it.isNotEmpty() } if (codeSourcesMap.values.flatten().isEmpty()) return ObservationStampingResult(false) - // Lookup conditions and Member OIDs + // Lookup conditions mapped to codes val conditionsToCode = conditionMapper.lookupConditions(codeSourcesMap.values.flatten()) + + // Map test codes to member OIDs val memberOidMap = conditionMapper.lookupMemberOid(codeSourcesMap.values.flatten()) var mappedSomething = false - // Process condition mappings + // Process condition mappings for each code val failures = codeSourcesMap.mapNotNull { codes -> - val unnmapped = codes.value.mapNotNull { code -> + val unmapped = codes.value.mapNotNull { code -> val conditions = conditionsToCode.getOrDefault(code, emptyList()) if (conditions.isEmpty()) { + // If no conditions are mapped, add this code to failures code } else { - conditions.forEach { code.addExtension(CONDITION_CODE_EXTENSION_URL, it) } - mappedSomething = true + conditions.forEach { conditionCoding -> + // Create a condition-code extension + val conditionCodeExtension = Extension(CONDITION_CODE_EXTENSION_URL) + conditionCodeExtension.setValue(conditionCoding) + + // Retrieve and add the member OID as a sub-extension + val memberOid = memberOidMap[conditionCoding.code] + if (memberOid != null) { + val memberOidExtension = Extension(MEMBER_OID_EXTENSION_URL) + memberOidExtension.setValue(StringType(memberOid)) + conditionCodeExtension.addExtension(memberOidExtension) + } + + // Attach the condition-code extension to the coding + code.addExtension(conditionCodeExtension) + mappedSomething = true + } null } } - if (unnmapped.isEmpty()) null else ObservationMappingFailure(codes.key, unnmapped) + if (unmapped.isEmpty()) null else ObservationMappingFailure(codes.key, unmapped) } - - // Add the Member OID extension to the observation, based on the lookup - observation.code.coding.forEach { coding -> - val testCode = coding.code - val memberOid = memberOidMap[testCode] - if (memberOid != null) { - val memberOidExtension = Extension(MEMBER_OID_EXTENSION_URL) - memberOidExtension.setValue(StringType(memberOid)) - observation.addExtension(memberOidExtension) - mappedSomething = true - } - } - return ObservationStampingResult(mappedSomething, failures) } } \ No newline at end of file diff --git a/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt b/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt index 1e6182b91c7..43cdb669938 100644 --- a/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt @@ -454,6 +454,7 @@ class FhirConverterTests { """{"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"}}}]}""" val memberOidExtensionURL = "https://reportstream.cdc.gov/fhir/StructureDefinition/test-performed-member-oid" + val conditionCodeExtensionURL = "https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code" metadata.lookupTableStore += mapOf( "observation-mapping" to LookupTable( @@ -467,18 +468,11 @@ class FhirConverterTests { ObservationMappingConstants.TEST_OID_KEY ), listOf( - "80382-5", - "6142004", - "SNOMEDCT", + "80382-5", // Test Code + "6142004", // Condition Code + "SNOMEDCT", // System "Influenza (disorder)", - "OID12345" - ), - listOf( - "260373001", - "Some Condition Code", - "Condition Code System", - "Condition Name", - "OID67890" + "OID12345" // OID ) ) ) @@ -492,19 +486,18 @@ class FhirConverterTests { // Add Condition and Member OID extensions ConditionStamper(LookupTableConditionMapper(metadata)).stampObservation(observation) - // Assert condition extensions + // Assert condition-code extension exists val conditionExtension = observation.code.coding[0].extension.find { - it.url == "https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code" + it.url == conditionCodeExtensionURL } - assertNotNull(conditionExtension) - assertEquals("6142004", (conditionExtension!!.value as Coding).code) + assertNotNull("Condition-code extension not found.", conditionExtension) - // Assert Member OID extension - val memberOidExtension = observation.extension.find { + // Assert member OID sub-extension exists within condition-code extension + val oidSubExtension = conditionExtension!!.extension.find { it.url == memberOidExtensionURL } - assertNotNull(memberOidExtension) - assertEquals("OID12345", (memberOidExtension!!.value as StringType).value) + assertNotNull("Member OID sub-extension not found in condition-code extension.", oidSubExtension) + assertEquals("Member OID value does not match", (oidSubExtension!!.value as StringType).value, "OID12345") } } From eb27d14b6415e76366f8093be68b410ed6fbb488 Mon Sep 17 00:00:00 2001 From: kant Date: Thu, 12 Dec 2024 10:14:36 -0800 Subject: [PATCH 03/10] fix: extension in the FHIR resource cannot have both a value and nested extension elements simultaneously --- prime-router/src/main/kotlin/azure/ConditionMapper.kt | 4 ++-- .../kotlin/fhirengine/engine/FhirConverterTests.kt | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/prime-router/src/main/kotlin/azure/ConditionMapper.kt b/prime-router/src/main/kotlin/azure/ConditionMapper.kt index df3c0b9c12e..2118f80df50 100644 --- a/prime-router/src/main/kotlin/azure/ConditionMapper.kt +++ b/prime-router/src/main/kotlin/azure/ConditionMapper.kt @@ -111,13 +111,13 @@ class ConditionStamper(private val conditionMapper: IConditionMapper) { conditions.forEach { conditionCoding -> // Create a condition-code extension val conditionCodeExtension = Extension(CONDITION_CODE_EXTENSION_URL) - conditionCodeExtension.setValue(conditionCoding) + conditionCodeExtension.addExtension(Extension(CONDITION_CODE_EXTENSION_URL, conditionCoding)) // Retrieve and add the member OID as a sub-extension val memberOid = memberOidMap[conditionCoding.code] if (memberOid != null) { val memberOidExtension = Extension(MEMBER_OID_EXTENSION_URL) - memberOidExtension.setValue(StringType(memberOid)) + memberOidExtension.addExtension(Extension(MEMBER_OID_EXTENSION_URL, StringType(memberOid))) conditionCodeExtension.addExtension(memberOidExtension) } diff --git a/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt b/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt index 43cdb669938..23348264fd2 100644 --- a/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt @@ -492,12 +492,18 @@ class FhirConverterTests { } assertNotNull("Condition-code extension not found.", conditionExtension) - // Assert member OID sub-extension exists within condition-code extension + // Navigate into the nested extensions to find member OID val oidSubExtension = conditionExtension!!.extension.find { it.url == memberOidExtensionURL } assertNotNull("Member OID sub-extension not found in condition-code extension.", oidSubExtension) - assertEquals("Member OID value does not match", (oidSubExtension!!.value as StringType).value, "OID12345") + + // Assert that the member OID value matches expected + val memberOidValue = oidSubExtension!!.extension.find { + it.url == memberOidExtensionURL + }?.value as? StringType + assertNotNull("Member OID value not found.", memberOidValue) + assertEquals("OID12345", memberOidValue!!.value) } } From e5d0264799e35e1e56804579da722d6c18d5024a Mon Sep 17 00:00:00 2001 From: kant Date: Mon, 16 Dec 2024 08:40:09 -0800 Subject: [PATCH 04/10] Avoiding npe using null safe operator for code summary --- .../main/kotlin/azure/observability/event/CodeSummary.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/prime-router/src/main/kotlin/azure/observability/event/CodeSummary.kt b/prime-router/src/main/kotlin/azure/observability/event/CodeSummary.kt index 7365ecca5d2..5ac3146463d 100644 --- a/prime-router/src/main/kotlin/azure/observability/event/CodeSummary.kt +++ b/prime-router/src/main/kotlin/azure/observability/event/CodeSummary.kt @@ -14,10 +14,10 @@ data class CodeSummary( /** * Create an instance of [CodeSummary] from a [Coding] */ - fun fromCoding(coding: Coding) = CodeSummary( - coding.system ?: UNKNOWN, - coding.code ?: UNKNOWN, - coding.display ?: UNKNOWN, + fun fromCoding(coding: Coding?) = CodeSummary( + coding?.system ?: UNKNOWN, + coding?.code ?: UNKNOWN, + coding?.display ?: UNKNOWN, ) } } \ No newline at end of file From 597d371374d8f3b5cba5f272c17eb50fc52c1fc0 Mon Sep 17 00:00:00 2001 From: kant Date: Thu, 2 Jan 2025 09:42:09 -0800 Subject: [PATCH 05/10] refactoring to not mutate the arguments --- .../src/main/kotlin/azure/ConditionMapper.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/prime-router/src/main/kotlin/azure/ConditionMapper.kt b/prime-router/src/main/kotlin/azure/ConditionMapper.kt index 2118f80df50..00faddabd71 100644 --- a/prime-router/src/main/kotlin/azure/ConditionMapper.kt +++ b/prime-router/src/main/kotlin/azure/ConditionMapper.kt @@ -53,14 +53,17 @@ class LookupTableConditionMapper(metadata: Metadata) : IConditionMapper { .filter().caseSensitiveDataRowsMap // Create a map of condition codes to member OIDs - return filteredRows.fold(mutableMapOf()) { acc, condition -> - val conditionCode = condition[ObservationMappingConstants.CONDITION_CODE_KEY] ?: "" - val memberOid = condition[ObservationMappingConstants.TEST_OID_KEY] ?: "" - if (conditionCode.isNotEmpty() && memberOid.isNotEmpty()) { - acc[conditionCode] = memberOid + return filteredRows + .mapNotNull { condition -> + val conditionCode = condition[ObservationMappingConstants.CONDITION_CODE_KEY] + val memberOid = condition[ObservationMappingConstants.TEST_OID_KEY] + if (!conditionCode.isNullOrEmpty() && !memberOid.isNullOrEmpty()) { + conditionCode to memberOid + } else { + null + } } - acc - } + .toMap() } } From 2ae58567b7d2798eb0573eda8c768d8c373c271f Mon Sep 17 00:00:00 2001 From: kant Date: Tue, 7 Jan 2025 09:51:56 -0800 Subject: [PATCH 06/10] fixing the PR comments, satisfying the new format requirements --- .../src/main/kotlin/azure/ConditionMapper.kt | 57 +++++++-------- .../fhirengine/engine/FhirConverterTests.kt | 71 ++++++++++--------- 2 files changed, 65 insertions(+), 63 deletions(-) diff --git a/prime-router/src/main/kotlin/azure/ConditionMapper.kt b/prime-router/src/main/kotlin/azure/ConditionMapper.kt index 00faddabd71..60c028587a3 100644 --- a/prime-router/src/main/kotlin/azure/ConditionMapper.kt +++ b/prime-router/src/main/kotlin/azure/ConditionMapper.kt @@ -91,48 +91,43 @@ class ConditionStamper(private val conditionMapper: IConditionMapper) { * @return a [ObservationStampingResult] including stamping success and any mapping failures */ fun stampObservation(observation: Observation): ObservationStampingResult { - // Extract codes and filter out empty values val codeSourcesMap = observation.getCodeSourcesMap().filterValues { it.isNotEmpty() } - if (codeSourcesMap.values.flatten().isEmpty()) return ObservationStampingResult(false) + if (codeSourcesMap.values.flatten().isEmpty()) { + return ObservationStampingResult(false) + } - // Lookup conditions mapped to codes val conditionsToCode = conditionMapper.lookupConditions(codeSourcesMap.values.flatten()) - - // Map test codes to member OIDs val memberOidMap = conditionMapper.lookupMemberOid(codeSourcesMap.values.flatten()) var mappedSomething = false - // Process condition mappings for each code - val failures = codeSourcesMap.mapNotNull { codes -> - val unmapped = codes.value.mapNotNull { code -> - val conditions = conditionsToCode.getOrDefault(code, emptyList()) - if (conditions.isEmpty()) { - // If no conditions are mapped, add this code to failures - code - } else { - conditions.forEach { conditionCoding -> - // Create a condition-code extension - val conditionCodeExtension = Extension(CONDITION_CODE_EXTENSION_URL) - conditionCodeExtension.addExtension(Extension(CONDITION_CODE_EXTENSION_URL, conditionCoding)) - - // Retrieve and add the member OID as a sub-extension - val memberOid = memberOidMap[conditionCoding.code] - if (memberOid != null) { - val memberOidExtension = Extension(MEMBER_OID_EXTENSION_URL) - memberOidExtension.addExtension(Extension(MEMBER_OID_EXTENSION_URL, StringType(memberOid))) - conditionCodeExtension.addExtension(memberOidExtension) + codeSourcesMap.forEach { (_, codings) -> + codings.forEach { originalCoding -> + val mappedConditions = conditionsToCode[originalCoding].orEmpty() + mappedConditions.forEach { conditionCoding -> + val snomedCoding = Coding().apply { + system = conditionCoding.system + code = conditionCoding.code + display = conditionCoding.display + } + // If we have an OID, add it as a sub-extension on this SNOMED coding + memberOidMap[conditionCoding.code]?.let { memberOid -> + val memberOidExtension = Extension(MEMBER_OID_EXTENSION_URL).apply { + setValue(StringType(memberOid)) } - - // Attach the condition-code extension to the coding - code.addExtension(conditionCodeExtension) - mappedSomething = true + snomedCoding.addExtension(memberOidExtension) } - null + // Create the top-level condition-code extension + val conditionExtension = Extension(CONDITION_CODE_EXTENSION_URL, snomedCoding) + observation.code.coding + .firstOrNull() + ?.addExtension(conditionExtension) + + mappedSomething = true } } - if (unmapped.isEmpty()) null else ObservationMappingFailure(codes.key, unmapped) } - return ObservationStampingResult(mappedSomething, failures) + + return ObservationStampingResult(mappedSomething) } } \ No newline at end of file diff --git a/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt b/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt index 23348264fd2..b93c95e3b3b 100644 --- a/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt @@ -449,13 +449,14 @@ class FhirConverterTests { @Test fun `test condition code and member OID 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":"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 memberOidExtensionURL = "https://reportstream.cdc.gov/fhir/StructureDefinition/test-performed-member-oid" - val conditionCodeExtensionURL = "https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code" - + 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"}}}]} +""".trimMargin() + + // Setup metadata (already present in your code) metadata.lookupTableStore += mapOf( "observation-mapping" to LookupTable( "observation-mapping", @@ -468,9 +469,9 @@ class FhirConverterTests { ObservationMappingConstants.TEST_OID_KEY ), listOf( - "80382-5", // Test Code - "6142004", // Condition Code - "SNOMEDCT", // System + "80382-5", // LOINC + "6142004", // SNOMED code + "SNOMEDCT", // SNOMED system "Influenza (disorder)", "OID12345" // OID ) @@ -480,31 +481,37 @@ class FhirConverterTests { val bundle = FhirContext.forR4().newJsonParser().parseResource(Bundle::class.java, fhirRecord) - bundle.entry.filter { it.resource is Observation }.forEach { - val observation = (it.resource as Observation) - - // Add Condition and Member OID extensions - ConditionStamper(LookupTableConditionMapper(metadata)).stampObservation(observation) + bundle.entry + .filter { it.resource is Observation } + .forEach { entry -> + val observation = entry.resource as Observation - // Assert condition-code extension exists - val conditionExtension = observation.code.coding[0].extension.find { - it.url == conditionCodeExtensionURL - } - assertNotNull("Condition-code extension not found.", conditionExtension) + // Stamp it + ConditionStamper(LookupTableConditionMapper(metadata)).stampObservation(observation) - // Navigate into the nested extensions to find member OID - val oidSubExtension = conditionExtension!!.extension.find { - it.url == memberOidExtensionURL + // Find the "condition-code" extension on the main LOINC coding + val coding = observation.code.coding.first() // The LOINC coding + val conditionCodeExt = coding.extension.firstOrNull { + it.url == ConditionStamper.CONDITION_CODE_EXTENSION_URL + } + assertNotNull("Condition-code extension not found.", conditionCodeExt) + + // Check that the extension's "valueCoding" is the SNOMED code + val snomedCoding = conditionCodeExt!!.value as? Coding + assertNotNull("Condition-code extension does not contain a valid Coding.", snomedCoding) + assertEquals("SNOMEDCT", snomedCoding!!.system) + assertEquals("6142004", snomedCoding.code) + assertEquals("Influenza (disorder)", snomedCoding.display) + + // Nested sub-extension for the OID + val oidSubExtension = snomedCoding.extension.find { + it.url == ConditionStamper.MEMBER_OID_EXTENSION_URL + } + assertNotNull("Member OID sub-extension not found.", oidSubExtension) + val oidValue = oidSubExtension!!.value as? StringType + assertNotNull("Member OID value not found.", oidValue) + assertEquals("OID12345", oidValue!!.value) } - assertNotNull("Member OID sub-extension not found in condition-code extension.", oidSubExtension) - - // Assert that the member OID value matches expected - val memberOidValue = oidSubExtension!!.extension.find { - it.url == memberOidExtensionURL - }?.value as? StringType - assertNotNull("Member OID value not found.", memberOidValue) - assertEquals("OID12345", memberOidValue!!.value) - } } @Test From c6460a1264e481944c7263207cee66289d6f5002 Mon Sep 17 00:00:00 2001 From: kant Date: Mon, 13 Jan 2025 08:28:08 -0800 Subject: [PATCH 07/10] adding back the unmapped codes --- .../src/main/kotlin/azure/ConditionMapper.kt | 59 ++++++++++++------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/prime-router/src/main/kotlin/azure/ConditionMapper.kt b/prime-router/src/main/kotlin/azure/ConditionMapper.kt index 60c028587a3..ab957c9a2d6 100644 --- a/prime-router/src/main/kotlin/azure/ConditionMapper.kt +++ b/prime-router/src/main/kotlin/azure/ConditionMapper.kt @@ -100,34 +100,51 @@ class ConditionStamper(private val conditionMapper: IConditionMapper) { val memberOidMap = conditionMapper.lookupMemberOid(codeSourcesMap.values.flatten()) var mappedSomething = false + val failures = mutableListOf() + + codeSourcesMap.forEach { (key, codings) -> + // keep track of codes that have no mapped conditions + val unmappedCodings = mutableListOf() - codeSourcesMap.forEach { (_, codings) -> codings.forEach { originalCoding -> val mappedConditions = conditionsToCode[originalCoding].orEmpty() - mappedConditions.forEach { conditionCoding -> - val snomedCoding = Coding().apply { - system = conditionCoding.system - code = conditionCoding.code - display = conditionCoding.display - } - // If we have an OID, add it as a sub-extension on this SNOMED coding - memberOidMap[conditionCoding.code]?.let { memberOid -> - val memberOidExtension = Extension(MEMBER_OID_EXTENSION_URL).apply { - setValue(StringType(memberOid)) + if (mappedConditions.isEmpty()) { + // No mapped conditions => unmapped + unmappedCodings.add(originalCoding) + } else { + // We do have mapped conditions => build all the SNOMED codings + mappedConditions.forEach { conditionCoding -> + val snomedCoding = Coding().apply { + system = conditionCoding.system + code = conditionCoding.code + display = conditionCoding.display + } + + // If we have an OID for this code, add as sub-extension + memberOidMap[conditionCoding.code]?.let { memberOid -> + val memberOidExtension = Extension(MEMBER_OID_EXTENSION_URL).apply { + setValue(StringType(memberOid)) + } + snomedCoding.addExtension(memberOidExtension) } - snomedCoding.addExtension(memberOidExtension) - } - // Create the top-level condition-code extension - val conditionExtension = Extension(CONDITION_CODE_EXTENSION_URL, snomedCoding) - observation.code.coding - .firstOrNull() - ?.addExtension(conditionExtension) - mappedSomething = true + // Create the top-level condition-code extension + val conditionExtension = Extension(CONDITION_CODE_EXTENSION_URL, snomedCoding) + observation.code + .coding + .firstOrNull() + ?.addExtension(conditionExtension) + + mappedSomething = true + } } } - } - return ObservationStampingResult(mappedSomething) + // If there's any unmapped codes for this key, record them + if (unmappedCodings.isNotEmpty()) { + failures.add(ObservationMappingFailure(key, unmappedCodings)) + } + } + return ObservationStampingResult(mappedSomething, failures) } } \ No newline at end of file From e77881fd4467b84c0b378f3d781046234aa0b145 Mon Sep 17 00:00:00 2001 From: kant Date: Mon, 13 Jan 2025 09:14:48 -0800 Subject: [PATCH 08/10] fixing the incorrect test --- .../src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt b/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt index 3e4f694f6d8..f0488cc78c0 100644 --- a/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt @@ -940,7 +940,7 @@ class FHIRBundleHelpersTests { val entry = Observation() val code = CodeableConcept() code.addCoding(Coding("system", "80382-5", "display")) - entry.setValue(code) + entry.setCode(code) val result = stamper.stampObservation(entry) assertThat(result.success).isTrue() From e730708aa3a0492983221e1dacbcf00b2690b0dd Mon Sep 17 00:00:00 2001 From: kant Date: Mon, 13 Jan 2025 14:25:01 -0800 Subject: [PATCH 09/10] Altering condition code mapping logic to take sources map key into account before adding the extension --- .../src/main/kotlin/azure/ConditionMapper.kt | 18 +++++++----------- .../fhirengine/utils/FHIRBundleHelpersTests.kt | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/prime-router/src/main/kotlin/azure/ConditionMapper.kt b/prime-router/src/main/kotlin/azure/ConditionMapper.kt index ab957c9a2d6..9c999ff29ef 100644 --- a/prime-router/src/main/kotlin/azure/ConditionMapper.kt +++ b/prime-router/src/main/kotlin/azure/ConditionMapper.kt @@ -91,11 +91,13 @@ class ConditionStamper(private val conditionMapper: IConditionMapper) { * @return a [ObservationStampingResult] including stamping success and any mapping failures */ fun stampObservation(observation: Observation): ObservationStampingResult { + // Retrieve only the code sources that are non-empty val codeSourcesMap = observation.getCodeSourcesMap().filterValues { it.isNotEmpty() } if (codeSourcesMap.values.flatten().isEmpty()) { return ObservationStampingResult(false) } + // Look up mapped SNOMED conditions and member OIDs val conditionsToCode = conditionMapper.lookupConditions(codeSourcesMap.values.flatten()) val memberOidMap = conditionMapper.lookupMemberOid(codeSourcesMap.values.flatten()) @@ -103,16 +105,13 @@ class ConditionStamper(private val conditionMapper: IConditionMapper) { val failures = mutableListOf() codeSourcesMap.forEach { (key, codings) -> - // keep track of codes that have no mapped conditions val unmappedCodings = mutableListOf() - codings.forEach { originalCoding -> val mappedConditions = conditionsToCode[originalCoding].orEmpty() if (mappedConditions.isEmpty()) { - // No mapped conditions => unmapped + // If no mapped conditions, record as unmapped unmappedCodings.add(originalCoding) } else { - // We do have mapped conditions => build all the SNOMED codings mappedConditions.forEach { conditionCoding -> val snomedCoding = Coding().apply { system = conditionCoding.system @@ -120,7 +119,7 @@ class ConditionStamper(private val conditionMapper: IConditionMapper) { display = conditionCoding.display } - // If we have an OID for this code, add as sub-extension + // If we have an OID for this code, add it as a sub-extension memberOidMap[conditionCoding.code]?.let { memberOid -> val memberOidExtension = Extension(MEMBER_OID_EXTENSION_URL).apply { setValue(StringType(memberOid)) @@ -130,21 +129,18 @@ class ConditionStamper(private val conditionMapper: IConditionMapper) { // Create the top-level condition-code extension val conditionExtension = Extension(CONDITION_CODE_EXTENSION_URL, snomedCoding) - observation.code - .coding - .firstOrNull() - ?.addExtension(conditionExtension) - + originalCoding.addExtension(conditionExtension) mappedSomething = true } } } - // If there's any unmapped codes for this key, record them + // If there's any unmapped codes, record them as failures if (unmappedCodings.isNotEmpty()) { failures.add(ObservationMappingFailure(key, unmappedCodings)) } } + return ObservationStampingResult(mappedSomething, failures) } } \ No newline at end of file diff --git a/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt b/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt index f0488cc78c0..3e4f694f6d8 100644 --- a/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt @@ -940,7 +940,7 @@ class FHIRBundleHelpersTests { val entry = Observation() val code = CodeableConcept() code.addCoding(Coding("system", "80382-5", "display")) - entry.setCode(code) + entry.setValue(code) val result = stamper.stampObservation(entry) assertThat(result.success).isTrue() From 94a4472c6f57cd19756ee79a0e08d7476138f1af Mon Sep 17 00:00:00 2001 From: kant Date: Wed, 15 Jan 2025 12:09:12 -0800 Subject: [PATCH 10/10] fixing mars otc fhir smoke tests --- .../smoketest/Expected_HL7_to_FHIR_MARSOTC.fhir | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/prime-router/src/test/resources/fhirengine/smoketest/Expected_HL7_to_FHIR_MARSOTC.fhir b/prime-router/src/test/resources/fhirengine/smoketest/Expected_HL7_to_FHIR_MARSOTC.fhir index 1e2c444e1d7..885a92bed7c 100644 --- a/prime-router/src/test/resources/fhirengine/smoketest/Expected_HL7_to_FHIR_MARSOTC.fhir +++ b/prime-router/src/test/resources/fhirengine/smoketest/Expected_HL7_to_FHIR_MARSOTC.fhir @@ -571,7 +571,13 @@ "valueCoding": { "system": "SNOMEDCT", "code": "840539006", - "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)" + "display": "Disease caused by severe acute respiratory syndrome coronavirus 2 (disorder)", + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/test-performed-member-oid", + "valueString": "2.16.840.1.113762.1.4.1146.1158" + } + ] } } ],