From 204ddee8f65f0ccf5febba4eb6cb838955b02120 Mon Sep 17 00:00:00 2001 From: JessicaWNava <119880261+JessicaWNava@users.noreply.github.com> Date: Wed, 16 Oct 2024 12:39:26 -0400 Subject: [PATCH] Engagement/jessica/15544 fhirdata api (#15926) Creates an API that runs a message through specified enrichments, transforms, and filters and provides warnings and errors as a response. --- prime-router/docs/api/reports.yml | 45 ++ .../src/main/kotlin/azure/ReportFunction.kt | 112 ++++- .../main/kotlin/cli/ProcessFhirCommands.kt | 429 +++++++++++++----- .../translation/hl7/ConfigSchemaProcessor.kt | 6 +- .../translation/hl7/FhirToHl7Converter.kt | 29 +- .../translation/hl7/FhirTransformer.kt | 22 +- .../test/kotlin/azure/ReportFunctionTests.kt | 117 +++++ .../engine/LookupTableValueSetTests.kt | 2 +- .../hl7/FhirToHl7ConverterTests.kt | 224 ++++++--- .../FHIR_to_HL7/sample_ME_20240806-0001.hl7 | 8 +- 10 files changed, 824 insertions(+), 170 deletions(-) diff --git a/prime-router/docs/api/reports.yml b/prime-router/docs/api/reports.yml index 2a860227aa8..3212c7f4e96 100644 --- a/prime-router/docs/api/reports.yml +++ b/prime-router/docs/api/reports.yml @@ -118,6 +118,51 @@ paths: $ref: '#/components/schemas/Report' '500': description: Internal Server Error + /reports/testing/test: + post: + summary: Evaluates a message based off of the receiver settings specified. Returns any errors, filtering, or the message. + security: + - OAuth2: [ system_admin ] + parameters: + - in: query + name: receiverName + description: The name of the receiver to look for in the current environment's settings + schema: + type: string + required: true + example: full-elr + - in: query + name: organizationName + description: The name of the organization to look for the receiver in the current environment's settings + required: true + schema: + type: string + example: me-phd + - in: query + name: senderSchema + description: The path to the sender schema + required: false + schema: + type: string + example: classpath:/metadata/fhir_transforms/senders/SimpleReport/simple-report-sender-transform.yml + requestBody: + description: The message to process + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Report' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Report' + '400': + description: Error with one or more filters or finding the receiver. + '500': + description: Internal Server Error /reports/download: get: summary: Downloads a message based on the report id diff --git a/prime-router/src/main/kotlin/azure/ReportFunction.kt b/prime-router/src/main/kotlin/azure/ReportFunction.kt index 65c5aaed0aa..9a97ed96533 100644 --- a/prime-router/src/main/kotlin/azure/ReportFunction.kt +++ b/prime-router/src/main/kotlin/azure/ReportFunction.kt @@ -1,8 +1,10 @@ package gov.cdc.prime.router.azure import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.github.ajalt.clikt.core.CliktError import com.google.common.net.HttpHeaders import com.microsoft.azure.functions.HttpMethod import com.microsoft.azure.functions.HttpRequestMessage @@ -23,7 +25,6 @@ import gov.cdc.prime.router.Sender import gov.cdc.prime.router.Sender.ProcessingType import gov.cdc.prime.router.SubmissionReceiver import gov.cdc.prime.router.UniversalPipelineReceiver -import gov.cdc.prime.router.azure.BlobAccess.Companion.defaultBlobMetadata import gov.cdc.prime.router.azure.BlobAccess.Companion.getBlobContainer import gov.cdc.prime.router.azure.db.enums.TaskAction import gov.cdc.prime.router.azure.db.tables.pojos.ReportFile @@ -32,6 +33,7 @@ import gov.cdc.prime.router.azure.observability.event.ReportStreamEventName import gov.cdc.prime.router.azure.observability.event.ReportStreamEventProperties import gov.cdc.prime.router.azure.observability.event.ReportStreamEventService import gov.cdc.prime.router.cli.PIIRemovalCommands +import gov.cdc.prime.router.cli.ProcessFhirCommands import gov.cdc.prime.router.common.AzureHttpUtils.getSenderIP import gov.cdc.prime.router.common.Environment import gov.cdc.prime.router.common.JacksonMapperUtilities @@ -43,6 +45,7 @@ import gov.cdc.prime.router.tokens.authenticationFailure import gov.cdc.prime.router.tokens.authorizationFailure import kotlinx.serialization.json.Json import org.apache.logging.log4j.kotlin.Logging +import java.io.File import java.nio.charset.StandardCharsets import java.util.UUID @@ -120,6 +123,113 @@ class ReportFunction( return HttpUtilities.unauthorizedResponse(request) } + /** + * Run a message through the fhirdata cli + * + * @see ../../../docs/api/reports.yml + */ + @FunctionName("processFhirDataRequest") + fun processFhirDataRequest( + @HttpTrigger( + name = "processFhirDataRequest", + methods = [HttpMethod.POST], + authLevel = AuthorizationLevel.ANONYMOUS, + route = "reports/testing/test" + ) request: HttpRequestMessage, + ): HttpResponseMessage { + val claims = AuthenticatedClaims.authenticate(request) + if (claims != null && claims.authorized(setOf(Scope.primeAdminScope))) { + val receiverName = request.queryParameters["receiverName"] + val organizationName = request.queryParameters["organizationName"] + val senderSchema = request.queryParameters["senderSchema"] + if (receiverName.isNullOrBlank()) { + return HttpUtilities.badRequestResponse( + request, + "The receiver name is required" + ) + } + if (organizationName.isNullOrBlank()) { + return HttpUtilities.badRequestResponse( + request, + "The organization name is required" + ) + } + if (request.body.isNullOrBlank()) { + return HttpUtilities.badRequestResponse( + request, + "A message to process must be included in the body" + ) + } + val file = File("filename.fhir") + file.createNewFile() + file.bufferedWriter().use { out -> + out.write(request.body) + } + + try { + val result = ProcessFhirCommands().processFhirDataRequest( + file, + Environment.get().envName, + receiverName, + organizationName, + senderSchema, + false + ) + file.delete() + val message = if (result.message != null) { + result.message.toString() + } else { + null + } + val bundle = if (result.bundle != null) { + result.bundle.toString() + } else { + null + } + return HttpUtilities.okResponse( + request, + ObjectMapper().configure(SerializationFeature.FAIL_ON_SELF_REFERENCES, false).writeValueAsString( + MessageOrBundleStringified( + message, + bundle, + result.senderTransformPassed, + result.senderTransformErrors, + result.senderTransformWarnings, + result.enrichmentSchemaPassed, + result.enrichmentSchemaErrors, + result.senderTransformWarnings, + result.receiverTransformPassed, + result.receiverTransformErrors, + result.receiverTransformWarnings, + result.filterErrors, + result.filtersPassed + ) + ) + ) + } catch (exception: CliktError) { + file.delete() + return HttpUtilities.badRequestResponse(request, "${exception.message}") + } + } + return HttpUtilities.unauthorizedResponse(request) + } + + class MessageOrBundleStringified( + var message: String? = null, + var bundle: String? = null, + override var senderTransformPassed: Boolean = true, + override var senderTransformErrors: MutableList = mutableListOf(), + override var senderTransformWarnings: MutableList = mutableListOf(), + override var enrichmentSchemaPassed: Boolean = true, + override var enrichmentSchemaErrors: MutableList = mutableListOf(), + override var enrichmentSchemaWarnings: MutableList = mutableListOf(), + override var receiverTransformPassed: Boolean = true, + override var receiverTransformErrors: MutableList = mutableListOf(), + override var receiverTransformWarnings: MutableList = mutableListOf(), + override var filterErrors: MutableList = mutableListOf(), + override var filtersPassed: Boolean = true, + ) : ProcessFhirCommands.MessageOrBundleParent() + /** * Moved the logic to a separate function for testing purposes */ diff --git a/prime-router/src/main/kotlin/cli/ProcessFhirCommands.kt b/prime-router/src/main/kotlin/cli/ProcessFhirCommands.kt index 43bfa4ab2e7..dbe6d5bf212 100644 --- a/prime-router/src/main/kotlin/cli/ProcessFhirCommands.kt +++ b/prime-router/src/main/kotlin/cli/ProcessFhirCommands.kt @@ -31,7 +31,6 @@ import gov.cdc.prime.router.common.Environment import gov.cdc.prime.router.common.JacksonMapperUtilities import gov.cdc.prime.router.config.validation.OrganizationValidation import gov.cdc.prime.router.fhirengine.config.HL7TranslationConfig -import gov.cdc.prime.router.fhirengine.engine.FHIRConverter import gov.cdc.prime.router.fhirengine.engine.FHIRReceiverFilter import gov.cdc.prime.router.fhirengine.engine.FHIRReceiverFilter.ReceiverFilterEvaluationResult import gov.cdc.prime.router.fhirengine.engine.encodePreserveEncodingChars @@ -49,6 +48,7 @@ import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.Bundle import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.Reference +import java.io.File import java.util.UUID /** @@ -100,28 +100,28 @@ class ProcessFhirCommands : CliktCommand( /** * Name of the receiver settings to use */ - private val receiverName by option( + private val receiverNameParam by option( "--receiver-name", help = "Name of the receiver settings to use" ) /** * Name of the org settings to use */ - private val orgName by option( + private val orgNameParam by option( "--org", help = "Name of the org settings to use" ) /** * Environment that specifies where to get the receiver settings */ - private val environment by option( + private val environmentParam by option( "--receiver-setting-env", help = "Environment that specifies where to get the receiver settings" ) /** * Sender schema location */ - private val senderSchema by option("-s", "--sender-schema", help = "Sender schema location") + private val senderSchemaParam by option("-s", "--sender-schema", help = "Sender schema location") private val inputSchema by option( "--input-schema", help = "Mapping schema for input file" @@ -130,35 +130,67 @@ class ProcessFhirCommands : CliktCommand( private val hl7DiffHelper = HL7DiffHelper() override fun run() { + val messageOrBundle = + processFhirDataRequest( + inputFile, + environmentParam, + receiverNameParam, + orgNameParam, + senderSchemaParam, + true + ) + if (messageOrBundle.message != null) { + outputResult(messageOrBundle.message!!) + } else if (messageOrBundle.bundle != null) { + outputResult(fhirResult = messageOrBundle.bundle!!, ActionLogger()) + } else { + throw CliktError("No result returned.") + } + } + + fun processFhirDataRequest( + inputFile: File, + environment: String?, + receiverName: String?, + orgName: String?, + senderSchema: String?, + isCli: Boolean, + ): MessageOrBundle { // Read the contents of the file val contents = inputFile.inputStream().readBytes().toString(Charsets.UTF_8) if (contents.isBlank()) throw CliktError("File ${inputFile.absolutePath} is empty.") - val actionLogger = ActionLogger() // Check on the extension of the file for supported operations val inputFileType = inputFile.extension.uppercase() - val receiver = getReceiver() + val receiver = if (!isCli) { + getReceiver(environment, receiverName, orgName, GetMultipleSettings(), isCli) + } else { + null + } // If there is a receiver, check the filters var bundle = FhirTranscoder.decode(contents) if (receiver != null) { - val reportStreamFilters = mutableListOf() - reportStreamFilters.add(receiver.jurisdictionalFilter) - reportStreamFilters.add(receiver.qualityFilter) - reportStreamFilters.add(receiver.routingFilter) - reportStreamFilters.add(receiver.processingModeFilter) + val reportStreamFilters = mutableListOf>() + reportStreamFilters.add(Pair("Jurisdictional Filter", receiver.jurisdictionalFilter)) + reportStreamFilters.add(Pair("Quality Filter", receiver.qualityFilter)) + reportStreamFilters.add(Pair("Routing Filter", receiver.routingFilter)) + reportStreamFilters.add(Pair("Processing Mode Filter", receiver.processingModeFilter)) val validationErrors = mutableListOf() reportStreamFilters.forEach { reportStreamFilter -> - reportStreamFilter.forEach { filter -> + reportStreamFilter.second.forEach { filter -> val validation = OrganizationValidation.validateFilter(filter) if (!validation) { - validationErrors.add("Filter '$filter' is not valid.") + validationErrors.add( + "Filter of type ${reportStreamFilter.first} is not valid. " + + "Value: '$filter'" + ) } else { val result = FhirPathUtils.evaluate( CustomContext( bundle, bundle, - FHIRConverter().loadFhirPathShorthandLookupTable(), + mutableMapOf(), CustomFhirPathFunctions() ), bundle, @@ -168,94 +200,190 @@ class ProcessFhirCommands : CliktCommand( if (result.isEmpty() || (result[0].isBooleanPrimitive && result[0].primitiveValue() == "false") ) { - throw CliktError("Filter '$filter' filtered out everything, nothing to return.") + return MessageOrBundle( + filterErrors = + mutableListOf("Filter '$filter' filtered out everything, nothing to return."), + filtersPassed = false + ) } } } } if (validationErrors.isNotEmpty()) { - throw CliktError(validationErrors.joinToString("\n")) + return MessageOrBundle( + filterErrors = mutableListOf(validationErrors.joinToString("\n")), + filtersPassed = false + ) } receiver.conditionFilter.forEach { conditionFilter -> val validation = OrganizationValidation.validateFilter(conditionFilter) if (!validation) { - throw CliktError("Condition filter '$conditionFilter' is not valid.") + return MessageOrBundle( + filterErrors = + mutableListOf("Condition filter '$conditionFilter' is not valid."), + filtersPassed = false + ) } } } + var messageOrBundle = MessageOrBundle() when { // HL7 to FHIR conversion inputFileType == "HL7" && ( - outputFormat == MimeFormat.FHIR.toString() || - (receiver != null && receiver.format == MimeFormat.FHIR) - ) -> { - var fhirMessage = convertHl7ToFhir(contents, receiver).first - fhirMessage = applyEnrichmentSchemas(fhirMessage) + (isCli && outputFormat == MimeFormat.FHIR.toString()) || + (receiver != null && receiver.format == MimeFormat.FHIR) + ) -> { + val fhirMessage = convertHl7ToFhir(contents, receiver).first + val enrichmentSchemaInfo = applyEnrichmentSchemas(fhirMessage, isCli) + setEnrichmentSchemaFields(messageOrBundle, enrichmentSchemaInfo) + if (receiver != null && receiver.enrichmentSchemaNames.isNotEmpty()) { receiver.enrichmentSchemaNames.forEach { currentSchema -> - fhirMessage = FhirTransformer(currentSchema).process(fhirMessage) + val transfromer = FhirTransformer(currentSchema) + val returnedBundle = + transfromer.process(messageOrBundle.bundle!!) + setEnrichmentSchemaFields( + messageOrBundle, + transfromer.warnings, + transfromer.errors, + returnedBundle + ) } } - outputResult( - handleSenderAndReceiverTransforms(fhirMessage), actionLogger - ) + return handleSenderAndReceiverTransforms(messageOrBundle, senderSchema, isCli) } // FHIR to HL7 conversion (inputFileType == "FHIR" || inputFileType == "JSON") && ( - outputFormat == MimeFormat.HL7.toString() || - (receiver != null && (receiver.format == MimeFormat.HL7 || receiver.format == MimeFormat.HL7_BATCH)) - ) -> { - if (receiver == null) { - return outputResult(convertFhirToHl7(contents)) - } + (isCli && outputFormat == MimeFormat.HL7.toString()) || + (receiver != null && (receiver.format == MimeFormat.HL7 || receiver.format == MimeFormat.HL7_BATCH)) + ) -> { + if (receiver == null) { + return convertFhirToHl7( + jsonString = contents, + senderSchema = senderSchema, + isCli = isCli + ) + } - bundle = FhirTranscoder.decode(contents) - if (receiver.enrichmentSchemaNames.isNotEmpty()) { - receiver.enrichmentSchemaNames.forEach { currentSchema -> - bundle = FhirTransformer(currentSchema).process(bundle) - } - } - outputResult( - convertFhirToHl7( - FhirTranscoder.encode(bundle), - receiver.translation as Hl7Configuration, - receiver + bundle = FhirTranscoder.decode(contents) + messageOrBundle.bundle = bundle + if (receiver.enrichmentSchemaNames.isNotEmpty()) { + receiver.enrichmentSchemaNames.forEach { currentSchema -> + val transformer = FhirTransformer(currentSchema) + val returnedBundle = + transformer.process(bundle) + setEnrichmentSchemaFields( + messageOrBundle, + transformer.warnings, + transformer.errors, + returnedBundle ) + } + } + + messageOrBundle = convertFhirToHl7( + FhirTranscoder.encode(messageOrBundle.bundle!!), + receiver.translation as Hl7Configuration, + receiver, + senderSchema, + isCli ) } // FHIR to FHIR conversion (inputFileType == "FHIR" || inputFileType == "JSON") && ( - outputFormat == MimeFormat.FHIR.toString() || - (receiver != null && receiver.format == MimeFormat.FHIR) - ) -> { - outputResult(convertFhirToFhir(FhirTranscoder.encode(bundle), receiver), actionLogger) + (isCli && outputFormat == MimeFormat.FHIR.toString()) || + (receiver != null && receiver.format == MimeFormat.FHIR) + ) -> { + return convertFhirToFhir(FhirTranscoder.encode(bundle), receiver, senderSchema, isCli) } // HL7 to FHIR to HL7 conversion inputFileType == "HL7" && ( - outputFormat == MimeFormat.HL7.toString() || - (receiver != null && (receiver.format == MimeFormat.HL7 || receiver.format == MimeFormat.HL7_BATCH)) - ) -> { + (isCli && outputFormat == MimeFormat.HL7.toString()) || + ( + receiver != null && + (receiver.format == MimeFormat.HL7 || receiver.format == MimeFormat.HL7_BATCH) + ) + ) -> { val (bundle2, inputMessage) = convertHl7ToFhir(contents, receiver) - val output = convertFhirToHl7(FhirTranscoder.encode(bundle2)) - outputResult(output) - if (diffHl7Output != null) { - val differences = hl7DiffHelper.diffHl7(output, inputMessage) + val output = convertFhirToHl7( + jsonString = FhirTranscoder.encode(bundle2), + senderSchema = senderSchema, + isCli = isCli + ) + if (diffHl7Output != null && isCli) { + val differences = hl7DiffHelper.diffHl7(output.message!!, inputMessage) echo("-------diff output") echo("There were ${differences.size} differences between the input and output") differences.forEach { echo(it.toString()) } } + return output } else -> throw CliktError("File extension ${inputFile.extension} is not supported.") } + return messageOrBundle } + private fun setEnrichmentSchemaFields( + messageOrBundle: MessageOrBundle, + enrichmentSchemaFields: FhirTransformer.BundleWithMessages, + ): MessageOrBundle { + messageOrBundle.enrichmentSchemaWarnings.addAll(enrichmentSchemaFields.warnings) + messageOrBundle.enrichmentSchemaErrors.addAll(enrichmentSchemaFields.errors) + messageOrBundle.enrichmentSchemaPassed = enrichmentSchemaFields.errors.isEmpty() + messageOrBundle.bundle = enrichmentSchemaFields.bundle + return messageOrBundle + } + + private fun setEnrichmentSchemaFields( + messageOrBundle: MessageOrBundle, + warnings: MutableList, + errors: MutableList, + bundle: Bundle, + ): MessageOrBundle { + messageOrBundle.enrichmentSchemaWarnings.addAll(warnings) + messageOrBundle.enrichmentSchemaErrors.addAll(errors) + messageOrBundle.enrichmentSchemaPassed = errors.isEmpty() + messageOrBundle.bundle = bundle + return messageOrBundle + } + + abstract class MessageOrBundleParent( + open var senderTransformPassed: Boolean = true, + open var senderTransformErrors: MutableList = mutableListOf(), + open var senderTransformWarnings: MutableList = mutableListOf(), + open var enrichmentSchemaPassed: Boolean = true, + open var enrichmentSchemaErrors: MutableList = mutableListOf(), + open var enrichmentSchemaWarnings: MutableList = mutableListOf(), + open var receiverTransformPassed: Boolean = true, + open var receiverTransformErrors: MutableList = mutableListOf(), + open var receiverTransformWarnings: MutableList = mutableListOf(), + open var filterErrors: MutableList = mutableListOf(), + open var filtersPassed: Boolean = true, + ) + + class MessageOrBundle( + var message: Message? = null, + var bundle: Bundle? = null, + override var senderTransformPassed: Boolean = true, + override var senderTransformErrors: MutableList = mutableListOf(), + override var senderTransformWarnings: MutableList = mutableListOf(), + override var enrichmentSchemaPassed: Boolean = true, + override var enrichmentSchemaErrors: MutableList = mutableListOf(), + override var enrichmentSchemaWarnings: MutableList = mutableListOf(), + override var receiverTransformPassed: Boolean = true, + override var receiverTransformErrors: MutableList = mutableListOf(), + override var receiverTransformWarnings: MutableList = mutableListOf(), + override var filterErrors: MutableList = mutableListOf(), + override var filtersPassed: Boolean = true, + ) : MessageOrBundleParent() + private fun applyConditionFilter(receiver: Receiver, bundle: Bundle): Bundle { val trackingId = if (bundle.id != null) { bundle.id @@ -276,32 +404,41 @@ class ProcessFhirCommands : CliktCommand( } } - private fun getReceiver(): Receiver? { + fun getReceiver( + environment: String?, + receiverName: String?, + orgName: String?, + getMultipleSettings: GetMultipleSettings = GetMultipleSettings(), + isCli: Boolean, + ): Receiver? { if (!environment.isNullOrBlank() && !receiverName.isNullOrBlank() && !orgName.isNullOrBlank()) { - if (!outputFormat.isNullOrBlank()) { + if (isCli && !outputFormat.isNullOrBlank()) { throw CliktError( "Please specify either a receiver OR an output format. Not both." ) } - val foundEnvironment = Environment.get(environment!!) + val foundEnvironment = Environment.get(environment) val accessToken = OktaCommand.fetchAccessToken(foundEnvironment.oktaApp) ?: abort( "Invalid access token. " + "Run ./prime login to fetch/refresh your access " + "token for the $foundEnvironment environment." ) - val organizations = GetMultipleSettings().getAll( + val organizations = getMultipleSettings.getAll( environment = foundEnvironment, accessToken = accessToken, specificOrg = orgName, exactMatch = true ) + if (organizations.isEmpty()) { + return null + } val receivers = organizations[0].receivers.filter { receiver -> receiver.name == receiverName } if (receivers.isNotEmpty()) { return receivers[0] } - } else if (outputFormat.isNullOrBlank()) { + } else if (isCli && outputFormat.isNullOrBlank()) { throw CliktError( "Output format is required if the environment, receiver, and org " + "are not specified. " @@ -327,25 +464,30 @@ class ProcessFhirCommands : CliktCommand( jsonString: String, hl7Configuration: Hl7Configuration = defaultHL7Configuration, receiver: Receiver? = null, - ): Message { - var fhirMessage = FhirTranscoder.decode(jsonString) - fhirMessage = applyEnrichmentSchemas(fhirMessage) + senderSchema: String?, + isCli: Boolean, + ): MessageOrBundle { + val fhirMessage = FhirTranscoder.decode(jsonString) + val enrichmentSchemaMessages = applyEnrichmentSchemas(fhirMessage, isCli) + val errors: MutableList = mutableListOf() + val warnings: MutableList = mutableListOf() return when { - receiverSchema == null && (receiver == null || receiver.schemaName.isBlank()) -> + (isCli && receiverSchema == null) && (receiver == null || (isCli && receiver.schemaName.isBlank())) -> // Receiver schema required because if it's coming out as HL7, it would be getting any transform info // for that from a receiver schema. throw CliktError("You must specify a receiver schema using --receiver-schema.") - receiverSchema != null -> { - var bundle = applySenderTransforms(fhirMessage) + isCli && receiverSchema != null -> { + val senderTransformMessages = applySenderTransforms(enrichmentSchemaMessages.bundle, senderSchema) val stamper = ConditionStamper(LookupTableConditionMapper(Metadata.getInstance())) - bundle.getObservations().forEach { observation -> + senderTransformMessages.bundle.getObservations().forEach { observation -> stamper.stampObservation(observation) } if (receiver != null) { - bundle = applyConditionFilter(receiver, bundle) + senderTransformMessages.bundle = applyConditionFilter(receiver, senderTransformMessages.bundle) } - FhirToHl7Converter( + + val message = FhirToHl7Converter( receiverSchema!!, BlobAccess.BlobContainerMetadata.build("metadata", Environment.get().storageEnvVar), context = FhirToHl7Context( @@ -355,13 +497,24 @@ class ProcessFhirCommands : CliktCommand( receiver ), translationFunctions = CustomTranslationFunctions(), - ) - ).process(bundle) + ), + warnings = warnings, + errors = errors + ).process(senderTransformMessages.bundle) + val messageOrBundle = MessageOrBundle() + messageOrBundle.senderTransformPassed = senderTransformMessages.errors.isEmpty() + messageOrBundle.senderTransformWarnings = senderTransformMessages.warnings + messageOrBundle.senderTransformErrors = senderTransformMessages.errors + messageOrBundle.receiverTransformPassed = errors.isEmpty() + messageOrBundle.receiverTransformErrors = errors + messageOrBundle.receiverTransformWarnings = warnings + messageOrBundle.message = message + messageOrBundle } receiver != null && receiver.schemaName.isNotBlank() -> { - var bundle = applySenderTransforms(fhirMessage) - bundle = applyConditionFilter(receiver, bundle) - FhirToHl7Converter( + val senderTransformMessages = applySenderTransforms(fhirMessage, senderSchema) + val bundle = applyConditionFilter(receiver, senderTransformMessages.bundle) + val message = FhirToHl7Converter( receiver.schemaName, BlobAccess.BlobContainerMetadata.build("metadata", Environment.get().storageEnvVar), context = FhirToHl7Context( @@ -371,11 +524,32 @@ class ProcessFhirCommands : CliktCommand( receiver ), translationFunctions = CustomTranslationFunctions(), - ) + ), + warnings = warnings, + errors = errors ).process(bundle) + val messageOrBundle = MessageOrBundle() + messageOrBundle.senderTransformPassed = senderTransformMessages.errors.isEmpty() + messageOrBundle.senderTransformWarnings = senderTransformMessages.warnings + messageOrBundle.senderTransformErrors = senderTransformMessages.errors + messageOrBundle.receiverTransformPassed = errors.isEmpty() + messageOrBundle.receiverTransformErrors = errors + messageOrBundle.receiverTransformWarnings = warnings + messageOrBundle.message = message + messageOrBundle } else -> { - throw CliktError("Error state reached when trying to apply the transforms.") + if (isCli) { + throw CliktError("Error state reached when trying to apply the transforms.") + } else { + MessageOrBundle( + senderTransformErrors = + mutableListOf("Error state reached when trying to apply the transforms."), + receiverTransformErrors = mutableListOf( + "Error state reached when trying to apply the transforms." + ) + ) + } } } } @@ -383,27 +557,51 @@ class ProcessFhirCommands : CliktCommand( /** * convert an FHIR message to FHIR message */ - private fun convertFhirToFhir(jsonString: String, receiver: Receiver?): Bundle { + private fun convertFhirToFhir( + jsonString: String, + receiver: Receiver?, + senderSchema: String?, + isCli: Boolean, + ): MessageOrBundle { var fhirMessage = FhirTranscoder.decode(jsonString) val stamper = ConditionStamper(LookupTableConditionMapper(Metadata.getInstance())) fhirMessage.getObservations().forEach { observation -> stamper.stampObservation(observation) } + + val messageOrBundle = MessageOrBundle() if (receiver != null) { fhirMessage = applyConditionFilter(receiver, fhirMessage) if (receiver.enrichmentSchemaNames.isNotEmpty()) { receiver.enrichmentSchemaNames.forEach { currentSchema -> - fhirMessage = FhirTransformer(currentSchema).process(fhirMessage) + val transformer = FhirTransformer(currentSchema) + val bundle = transformer.process(fhirMessage) + setEnrichmentSchemaFields( + messageOrBundle, + transformer.warnings, + transformer.errors, + bundle + ) } } } - fhirMessage = applyEnrichmentSchemas(fhirMessage) - if (receiverSchema == null && senderSchema == null) { + setEnrichmentSchemaFields(messageOrBundle, applyEnrichmentSchemas(fhirMessage, isCli)) + if (( + (isCli && receiverSchema == null) || + (!isCli && (receiver == null || receiver.schemaName.isBlank())) + ) && senderSchema == null + ) { // Must have at least one schema or else why are you doing this throw CliktError("You must specify a schema.") } else { - return handleSenderAndReceiverTransforms(fhirMessage) + handleSenderAndReceiverTransforms( + messageOrBundle = messageOrBundle, + senderSchema = senderSchema, + isCli = isCli + ) } + + return messageOrBundle } /** @@ -455,13 +653,15 @@ class ProcessFhirCommands : CliktCommand( * @throws CliktError if senderSchema is present, but unable to be read. * @return If senderSchema is present, apply it, otherwise just return the input bundle. */ - private fun applySenderTransforms(bundle: Bundle): Bundle { + private fun applySenderTransforms(bundle: Bundle, senderSchema: String?): FhirTransformer.BundleWithMessages { return when { senderSchema != null -> { - FhirTransformer(senderSchema!!).process(bundle) + val transformer = FhirTransformer(senderSchema) + val returnedBundle = transformer.process(bundle) + FhirTransformer.BundleWithMessages(returnedBundle, transformer.warnings, transformer.errors) } - else -> bundle + else -> FhirTransformer.BundleWithMessages(bundle = bundle, mutableListOf(), mutableListOf()) } } @@ -470,38 +670,65 @@ class ProcessFhirCommands : CliktCommand( * @throws CliktError if enrichmentSchemaName is present, but unable to be read. * @return If receiverSchema is present, apply it, otherwise just return the input bundle. */ - private fun applyReceiverEnrichmentAndTransforms(bundle: Bundle): Bundle { - val enrichedBundle = applyEnrichmentSchemas(bundle) - - return when { - receiverSchema != null -> { - FhirTransformer( - receiverSchema!! - ).process(enrichedBundle) - } - - else -> enrichedBundle + private fun applyReceiverEnrichmentAndTransforms(bundle: Bundle, isCli: Boolean): MessageOrBundle { + val messageOrBundle = MessageOrBundle() + setEnrichmentSchemaFields(messageOrBundle, applyEnrichmentSchemas(bundle, isCli)) + + if (isCli && receiverSchema != null) { + val transformer = FhirTransformer(receiverSchema!!) + val returnedBundle = transformer.process(messageOrBundle.bundle!!) + messageOrBundle.receiverTransformWarnings.addAll(transformer.warnings) + messageOrBundle.receiverTransformErrors.addAll(transformer.errors) + messageOrBundle.receiverTransformPassed = transformer.errors.isEmpty() + messageOrBundle.bundle = returnedBundle } + + return messageOrBundle } /** * Applies the enrichment schema to the bundle. */ - private fun applyEnrichmentSchemas(bundle: Bundle): Bundle { - if (!enrichmentSchemaNames.isNullOrEmpty()) { + private fun applyEnrichmentSchemas(bundle: Bundle, isCli: Boolean): FhirTransformer.BundleWithMessages { + var enrichedbundle = bundle + val warnings = mutableListOf() + val errors = mutableListOf() + if (isCli && !enrichmentSchemaNames.isNullOrEmpty()) { enrichmentSchemaNames!!.split(",").forEach { currentEnrichmentSchemaName -> - FhirTransformer(currentEnrichmentSchemaName).process(bundle) + val transformer = FhirTransformer(currentEnrichmentSchemaName) + val returnedBundle = transformer.process( + enrichedbundle + ) + errors.addAll(transformer.errors) + warnings.addAll(transformer.warnings) + enrichedbundle = returnedBundle } } - return bundle + return FhirTransformer.BundleWithMessages(enrichedbundle, warnings, errors) } /** * Apply both sender and receiver schemas if present. * @return the FHIR bundle after having sender and/or receiver schemas applied to it. */ - private fun handleSenderAndReceiverTransforms(bundle: Bundle): Bundle { - return applyReceiverEnrichmentAndTransforms(applySenderTransforms(bundle)) + private fun handleSenderAndReceiverTransforms( + messageOrBundle: MessageOrBundle, + senderSchema: String?, + isCli: Boolean, + ): MessageOrBundle { + val senderTransformInfo = applySenderTransforms(messageOrBundle.bundle!!, senderSchema) + val receiverTransformInfo = applyReceiverEnrichmentAndTransforms(senderTransformInfo.bundle, isCli) + messageOrBundle.bundle = receiverTransformInfo.bundle + messageOrBundle.senderTransformWarnings.addAll(senderTransformInfo.warnings) + messageOrBundle.senderTransformErrors.addAll(senderTransformInfo.errors) + messageOrBundle.senderTransformPassed = senderTransformInfo.errors.isEmpty() + messageOrBundle.receiverTransformErrors.addAll(receiverTransformInfo.receiverTransformErrors) + messageOrBundle.receiverTransformWarnings.addAll(receiverTransformInfo.receiverTransformWarnings) + messageOrBundle.receiverTransformPassed = receiverTransformInfo.receiverTransformPassed && + messageOrBundle.receiverTransformPassed + messageOrBundle.enrichmentSchemaPassed + + return messageOrBundle } /** diff --git a/prime-router/src/main/kotlin/fhirengine/translation/hl7/ConfigSchemaProcessor.kt b/prime-router/src/main/kotlin/fhirengine/translation/hl7/ConfigSchemaProcessor.kt index 0aed4333f42..1d744d91adf 100644 --- a/prime-router/src/main/kotlin/fhirengine/translation/hl7/ConfigSchemaProcessor.kt +++ b/prime-router/src/main/kotlin/fhirengine/translation/hl7/ConfigSchemaProcessor.kt @@ -16,6 +16,8 @@ abstract class ConfigSchemaProcessor< SchemaElement : ConfigSchemaElement, >( val schema: Schema, + val errors: MutableList, + val warnings: MutableList, ) : Logging { @@ -40,7 +42,9 @@ abstract class ConfigSchemaProcessor< * @property input the value to apply the schema to * @return The value after applying the schema to [input] */ - abstract fun process(input: Original): Converted + abstract fun process( + input: Original, + ): Converted /** * Get the first valid value from the list of values specified in the schema for a given [element] using diff --git a/prime-router/src/main/kotlin/fhirengine/translation/hl7/FhirToHl7Converter.kt b/prime-router/src/main/kotlin/fhirengine/translation/hl7/FhirToHl7Converter.kt index 56c10594e2f..8ddb085e4c5 100644 --- a/prime-router/src/main/kotlin/fhirengine/translation/hl7/FhirToHl7Converter.kt +++ b/prime-router/src/main/kotlin/fhirengine/translation/hl7/FhirToHl7Converter.kt @@ -38,7 +38,13 @@ class FhirToHl7Converter( // the constant substitutor is not thread safe, so we need one instance per converter instead of using a shared copy private val constantSubstitutor: ConstantSubstitutor = ConstantSubstitutor(), private val context: FhirToHl7Context? = null, -) : ConfigSchemaProcessor(schemaRef), Logging { + warnings: MutableList, + errors: MutableList, +) : ConfigSchemaProcessor( + schemaRef, + warnings, errors +), +Logging { /** * Convert a FHIR bundle to an HL7 message using the [schema] in the [schemaFolder] location to perform the conversion. * The converter will error out if [strict] is set to true and there is an error during the conversion. If [strict] @@ -53,11 +59,15 @@ class FhirToHl7Converter( terser: Terser? = null, context: FhirToHl7Context? = null, blobConnectionInfo: BlobAccess.BlobContainerMetadata, + warnings: MutableList = mutableListOf(), + errors: MutableList = mutableListOf(), ) : this( schemaRef = converterSchemaFromFile(schema, blobConnectionInfo), strict = strict, terser = terser, - context = context + context = context, + warnings = warnings, + errors = errors ) constructor( @@ -66,6 +76,8 @@ class FhirToHl7Converter( strict: Boolean = false, terser: Terser? = null, context: FhirToHl7Context? = null, + warnings: MutableList = mutableListOf(), + errors: MutableList = mutableListOf(), ) : this( ConfigSchemaReader.fromFile( schemaUri, @@ -74,7 +86,10 @@ class FhirToHl7Converter( ), strict = strict, terser = terser, - context = context + context = context, + warnings = warnings, + errors = errors + ) /** @@ -87,7 +102,13 @@ class FhirToHl7Converter( val message = HL7Utils.getMessageInstance(schemaRef.hl7Class!!) terser = Terser(message) - processSchema(schemaRef, input, input) + try { + processSchema(schemaRef, input, input) + } catch (e: Exception) { + if (e.message != null) { + errors.add(e.message!!) + } + } return message } diff --git a/prime-router/src/main/kotlin/fhirengine/translation/hl7/FhirTransformer.kt b/prime-router/src/main/kotlin/fhirengine/translation/hl7/FhirTransformer.kt index af338c1b338..a206b0262c4 100644 --- a/prime-router/src/main/kotlin/fhirengine/translation/hl7/FhirTransformer.kt +++ b/prime-router/src/main/kotlin/fhirengine/translation/hl7/FhirTransformer.kt @@ -24,7 +24,14 @@ import org.hl7.fhir.r4.model.Property */ class FhirTransformer( private val schemaRef: FhirTransformSchema, -) : ConfigSchemaProcessor(schemaRef) { + errors: MutableList = mutableListOf(), + warnings: MutableList = mutableListOf(), +) : ConfigSchemaProcessor< + Bundle, + Bundle, + FhirTransformSchema, + FhirTransformSchemaElement + >(schemaRef, errors, warnings) { private val extensionRegex = """^extension\(["'](?[^'"]+)["']\)""".toRegex() private val valueXRegex = Regex("""value[A-Z][a-z]*""") private val indexRegex = Regex("""(?.*)\[%?(?[0-9A-Za-z]*)\]""") @@ -40,6 +47,8 @@ class FhirTransformer( ), ) : this( schemaRef = fhirTransformSchemaFromFile(schema, blobConnectionInfo), + mutableListOf(), + mutableListOf(), ) /** @@ -51,6 +60,12 @@ class FhirTransformer( return input } + class BundleWithMessages( + var bundle: Bundle, + val warnings: MutableList, + val errors: MutableList, + ) + override fun checkForEquality(converted: Bundle, expectedOutput: Bundle): Boolean { return converted.equalsDeep(expectedOutput) } @@ -107,6 +122,7 @@ class FhirTransformer( debugMsg += "resource: NONE" } + val warnings = mutableListOf() val eligibleFocusResources = focusResources.filter { canEvaluate(element, bundle, it, focusResource, elementContext) } when (element.action) { @@ -141,6 +157,10 @@ class FhirTransformer( "Element ${element.name} is updating a bundle property," + " but did not specify a value or function" ) + warnings.add( + "Element ${element.name} is updating a bundle property, " + + "but did not specify a value" + ) } debugMsg += "condition: true, resourceType: ${singleFocusResource.fhirType()}, " + "value: $value" diff --git a/prime-router/src/test/kotlin/azure/ReportFunctionTests.kt b/prime-router/src/test/kotlin/azure/ReportFunctionTests.kt index c431198cd5f..68b98a739c9 100644 --- a/prime-router/src/test/kotlin/azure/ReportFunctionTests.kt +++ b/prime-router/src/test/kotlin/azure/ReportFunctionTests.kt @@ -11,6 +11,7 @@ import com.azure.storage.blob.models.BlobItemProperties import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.github.ajalt.clikt.core.CliktError import com.google.common.net.HttpHeaders import com.microsoft.azure.functions.HttpStatus import gov.cdc.prime.router.ActionLog @@ -20,11 +21,14 @@ import gov.cdc.prime.router.CustomerStatus import gov.cdc.prime.router.DeepOrganization import gov.cdc.prime.router.Element import gov.cdc.prime.router.FileSettings +import gov.cdc.prime.router.Hl7Configuration import gov.cdc.prime.router.Metadata import gov.cdc.prime.router.MimeFormat import gov.cdc.prime.router.Organization import gov.cdc.prime.router.Receiver import gov.cdc.prime.router.Report +import gov.cdc.prime.router.ReportStreamFilter +import gov.cdc.prime.router.ReportStreamFilters import gov.cdc.prime.router.Schema import gov.cdc.prime.router.Sender import gov.cdc.prime.router.SettingsProvider @@ -35,7 +39,9 @@ import gov.cdc.prime.router.UniversalPipelineSender import gov.cdc.prime.router.azure.BlobAccess.BlobContainerMetadata import gov.cdc.prime.router.azure.db.enums.TaskAction import gov.cdc.prime.router.azure.db.tables.pojos.ReportFile +import gov.cdc.prime.router.cli.GetMultipleSettings import gov.cdc.prime.router.cli.PIIRemovalCommands +import gov.cdc.prime.router.cli.ProcessFhirCommands import gov.cdc.prime.router.history.DetailedSubmissionHistory import gov.cdc.prime.router.history.azure.SubmissionsFacade import gov.cdc.prime.router.serializers.Hl7Serializer @@ -57,6 +63,7 @@ import org.jooq.tools.jdbc.MockResult import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import java.io.File import java.time.OffsetDateTime import java.util.UUID @@ -979,4 +986,114 @@ class ReportFunctionTests { assert(result.status.equals(HttpStatus.BAD_REQUEST)) } + + @Test + fun `processFhirDataRequest blank file`() { + val file = File("filename.txt") + file.createNewFile() + assertThrows { + ProcessFhirCommands().processFhirDataRequest( + file, + "local", + "full-elr", + "me-phd", + "classpath:/metadata/fhir_transforms/senders/SimpleReport/simple-report-sender-transform.yml", + false + ) + } + file.delete() + } + + @Test + fun `processFhirDataRequest no environment, receiver name, or org name and output format blank`() { + val file = File("filename.txt") + file.createNewFile() + assertThrows { + ProcessFhirCommands().processFhirDataRequest( + file, + "", + "", + "", + "classpath:/metadata/fhir_transforms/senders/SimpleReport/simple-report-sender-transform.yml", + false + ) + } + file.delete() + } + + @Suppress("ktlint:standard:max-line-length") + val jurisdictionalFilter: ReportStreamFilter = + listOf("(Bundle.entry.resource.ofType(ServiceRequest)[0].requester.resolve().organization.resolve().address.state = 'ME') or (Bundle.entry.resource.ofType(Patient).address.state = 'ME')") + val qualityFilter: ReportStreamFilter = listOf("Bundle.entry.resource.ofType(MessageHeader).id.exists()") + + @Suppress("ktlint:standard:max-line-length") + val conditionFilter: ReportStreamFilter = + listOf("%resource.where(interpretation.coding.code = 'A').code.coding.extension('https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code').value.where(code in ('840539006'|'895448002')).exists()") + val filters = listOf(jurisdictionalFilter, qualityFilter, conditionFilter) + val organization = Organization( + "me-phd", + "This is my description", + Organization.Jurisdiction.STATE, + "ME", + "Cumberland", + listOf( + ReportStreamFilters( + Topic.FULL_ELR, + jurisdictionalFilter, + qualityFilter, + null, + null, + conditionFilter, + null + ) + ) + ) + val sender = UniversalPipelineSender( + "full-elr", + "me-phd", + MimeFormat.HL7, + CustomerStatus.ACTIVE, + "classpath:/metadata/hl7_mapping/receivers/STLTs/ME/ME-receiver-transform.yml", + Sender.ProcessingType.async, + true, + Sender.SenderType.facility, + Sender.PrimarySubmissionMethod.manual, + Topic.FULL_ELR + ) + val receiver = Receiver( + "full-elr", + "me-phd", + Topic.FULL_ELR, + CustomerStatus.ACTIVE, + Hl7Configuration( + schemaName = "classpath:/metadata/hl7_mapping/receivers/STLTs/ME/ME-receiver-transform.yml", + useTestProcessingMode = true, + useBatchHeaders = true, + messageProfileId = "", + receivingApplicationName = "", + receivingFacilityOID = "", + receivingFacilityName = "", + receivingApplicationOID = "", + receivingOrganization = "" + ), + jurisdictionalFilter, + qualityFilter + ) + + @Test + fun getReceiver() { + val file = File("filename.txt") + file.createNewFile() + val getMultipleSettings = mockkClass(GetMultipleSettings::class) + every { getMultipleSettings.getAll(any(), any(), "me-phd", true) } returns + listOf(DeepOrganization(organization, listOf(sender), listOf(receiver))) + val receiverReturned = ProcessFhirCommands().getReceiver( + "local", + "full-elr", + "me-phd", + getMultipleSettings, + false + ) + assert(receiverReturned!!.name == receiver.name) + } } \ No newline at end of file diff --git a/prime-router/src/test/kotlin/fhirengine/engine/LookupTableValueSetTests.kt b/prime-router/src/test/kotlin/fhirengine/engine/LookupTableValueSetTests.kt index a5451867670..0ef5b5c71e4 100644 --- a/prime-router/src/test/kotlin/fhirengine/engine/LookupTableValueSetTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/engine/LookupTableValueSetTests.kt @@ -125,7 +125,7 @@ class LookupTableValueSetTests { val schema = FhirTransformSchema(elements = mutableListOf(elemA)) - FhirTransformer(schema).process(bundle) + FhirTransformer(schema, mutableListOf(), mutableListOf()).process(bundle) assertThat(resource.name[0].text).isEqualTo("ghi789") assertThat(resource.telecom[0].value).isEqualTo("ijk012") diff --git a/prime-router/src/test/kotlin/fhirengine/translation/hl7/FhirToHl7ConverterTests.kt b/prime-router/src/test/kotlin/fhirengine/translation/hl7/FhirToHl7ConverterTests.kt index d435d94b0ce..4700b330438 100644 --- a/prime-router/src/test/kotlin/fhirengine/translation/hl7/FhirToHl7ConverterTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/translation/hl7/FhirToHl7ConverterTests.kt @@ -19,7 +19,6 @@ import fhirengine.engine.CustomTranslationFunctions import gov.cdc.prime.router.Metadata import gov.cdc.prime.router.azure.BlobAccess import gov.cdc.prime.router.fhirengine.config.HL7TranslationConfig -import gov.cdc.prime.router.fhirengine.translation.hl7.schema.ConfigSchemaElementProcessingException import gov.cdc.prime.router.fhirengine.translation.hl7.schema.ConfigSchemaReader import gov.cdc.prime.router.fhirengine.translation.hl7.schema.converter.ConverterSchemaElement import gov.cdc.prime.router.fhirengine.translation.hl7.schema.converter.HL7ConverterSchema @@ -43,7 +42,6 @@ import org.junit.jupiter.api.Nested import java.io.File import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertFailsWith class FhirToHl7ConverterTests { @Test @@ -54,7 +52,12 @@ class FhirToHl7ConverterTests { bundle.id = "abc123" val customContext = CustomContext(bundle, bundle) - val converter = FhirToHl7Converter(mockSchema, terser = mockTerser) + val converter = FhirToHl7Converter( + mockSchema, + terser = mockTerser, + warnings = mutableListOf(), + errors = mutableListOf() + ) var element = ConverterSchemaElement("name") assertThat(converter.canEvaluate(element, bundle, bundle, bundle, customContext)).isTrue() @@ -81,7 +84,7 @@ class FhirToHl7ConverterTests { bundle.addEntry().resource = resource val customContext = CustomContext(bundle, bundle) - val converter = FhirToHl7Converter(mockSchema) + val converter = FhirToHl7Converter(mockSchema, warnings = mutableListOf(), errors = mutableListOf()) var element = ConverterSchemaElement("name") var focusResources = converter.getFocusResources(element.resource, bundle, bundle, customContext) @@ -131,7 +134,7 @@ class FhirToHl7ConverterTests { val bundle = Bundle() bundle.id = "abc123" val customContext = CustomContext(bundle, bundle) - val converter = FhirToHl7Converter(mockSchema) + val converter = FhirToHl7Converter(mockSchema, warnings = mutableListOf(), errors = mutableListOf()) var element = ConverterSchemaElement("name", value = listOf("Bundle.id", "Bundle.timestamp")) assertThat(converter.getValueAsString(element, bundle, bundle, customContext)).isEqualTo(bundle.id) @@ -149,7 +152,7 @@ class FhirToHl7ConverterTests { val bundle = Bundle() bundle.id = "stagnatious" val customContext = CustomContext(bundle, bundle) - val converter = FhirToHl7Converter(mockSchema) + val converter = FhirToHl7Converter(mockSchema, warnings = mutableListOf(), errors = mutableListOf()) val valueSet = InlineValueSet( sortedMapOf( @@ -188,7 +191,7 @@ class FhirToHl7ConverterTests { resource.destination[0].name = "a destination" bundle.addEntry().resource = resource val customContext = CustomContext(bundle, bundle) - val converter = FhirToHl7Converter(mockSchema) + val converter = FhirToHl7Converter(mockSchema, warnings = mutableListOf(), errors = mutableListOf()) // Non-primitive values should return an empty string and log an error val element = ConverterSchemaElement("name", value = listOf("Bundle.entry")) @@ -201,7 +204,8 @@ class FhirToHl7ConverterTests { val mockTerser = mockk() val mockSchema = mockk() // Just a dummy schema to pass around var element = ConverterSchemaElement("name", required = true, hl7Spec = listOf("MSH-10")) - var converter = FhirToHl7Converter(mockSchema, terser = mockTerser) + var converter = + FhirToHl7Converter(mockSchema, terser = mockTerser, warnings = mutableListOf(), errors = mutableListOf()) val customContext = CustomContext(Bundle(), Bundle()) // Required element @@ -230,7 +234,13 @@ class FhirToHl7ConverterTests { clearAllMocks() // Strict errors - converter = FhirToHl7Converter(mockSchema, true, mockTerser) + converter = FhirToHl7Converter( + mockSchema, + true, + mockTerser, + warnings = mutableListOf(), + errors = mutableListOf() + ) every { mockTerser.set(element.hl7Spec[0], any()) } throws HL7Exception("some text") assertFailure { converter.setHl7Value(element, fieldValue, customContext) } .hasClass(HL7ConversionException::class.java) @@ -251,7 +261,12 @@ class FhirToHl7ConverterTests { val bundle = Bundle() bundle.id = "abc123" val customContext = CustomContext(bundle, bundle) - val converter = FhirToHl7Converter(mockSchema, terser = mockTerser) + val converter = FhirToHl7Converter( + mockSchema, + terser = mockTerser, + warnings = mutableListOf(), + errors = mutableListOf() + ) val pathWithValue = "Bundle.id" val pathNoValue = "Bundle.timestamp" val conditionTrue = "Bundle.id.exists()" @@ -337,7 +352,8 @@ class FhirToHl7ConverterTests { bundle.addEntry().resource = servRequest1 bundle.addEntry().resource = servRequest2 - val converter = FhirToHl7Converter(mockSchema, terser = mockTerser) + val converter = + FhirToHl7Converter(mockSchema, terser = mockTerser, warnings = mutableListOf(), errors = mutableListOf()) val childElement = ConverterSchemaElement( "childElement", @@ -383,7 +399,7 @@ class FhirToHl7ConverterTests { hl7Class = "ca.uhn.hl7v2.model.v27.message.ORU_R01", elements = listOf(element).toMutableList() ) - val converter = FhirToHl7Converter(schema) + val converter = FhirToHl7Converter(schema, warnings = mutableListOf(), errors = mutableListOf()) val message = converter.process(bundle) assertThat(Terser(message).get("MSH-3-1")).isEqualTo("Epic") } @@ -407,7 +423,7 @@ class FhirToHl7ConverterTests { hl7Class = "ca.uhn.hl7v2.model.v251.message.ORU_R01", elements = mutableListOf(element) ) - val message = FhirToHl7Converter(schema).process(bundle) + val message = FhirToHl7Converter(schema, warnings = mutableListOf(), errors = mutableListOf()).process(bundle) assertThat(message.isEmpty).isFalse() assertThat(Terser(message).get(element.hl7Spec[0])).isEqualTo(bundle.id) @@ -419,16 +435,23 @@ class FhirToHl7ConverterTests { ) schema = HL7ConverterSchema(elements = mutableListOf(element)) - assertFailure { FhirToHl7Converter(schema).process(bundle) } - - // Use a file based schema which will fail as we do not have enough data in the bundle - val missingDataEx = assertFailsWith { + assertFailure { FhirToHl7Converter( - "classpath:/fhirengine/translation/hl7/schema/schema-read-test-01/ORU_R01.yml", - mockk() - ).process(bundle) + schema, + warnings = mutableListOf(), + errors = mutableListOf() + ).process(bundle) } - assertThat(missingDataEx.message).isEqualTo( + + // Use a file based schema which will fail as we do not have enough data in the bundle + val transformer = FhirToHl7Converter( + "classpath:/fhirengine/translation/hl7/schema/schema-read-test-01/ORU_R01.yml", + mockk(), warnings = mutableListOf(), errors = mutableListOf() + ) + + transformer.process(bundle) + + assertThat(transformer.errors[0]).isEqualTo( "Error encountered while applying: message-headers in" + " /fhirengine/translation/hl7/schema/schema-read-test-01/ORU_R01.yml to FHIR bundle. \n" + "Error was: Required element message-headers conditional was false or value was empty." @@ -462,7 +485,8 @@ class FhirToHl7ConverterTests { CustomFhirPathFunctions(), null, CustomTranslationFunctions() - ) + ), + warnings = mutableListOf(), errors = mutableListOf() ).process(bundle) assertThat(message.isEmpty).isFalse() assertThat(Terser(message).get(element.hl7Spec[0])).isEqualTo(loincCode) @@ -491,7 +515,8 @@ class FhirToHl7ConverterTests { val message = FhirToHl7Converter( schema, - context = FhirToHl7Context(CustomFhirPathFunctions(), null, CustomTranslationFunctions()) + context = FhirToHl7Context(CustomFhirPathFunctions(), null, CustomTranslationFunctions()), + warnings = mutableListOf(), errors = mutableListOf() ).process(bundle) assertThat(message.isEmpty).isFalse() assertThat(Terser(message).get(element.hl7Spec[0])).isEqualTo(expectedDate) @@ -513,7 +538,11 @@ class FhirToHl7ConverterTests { elements = mutableListOf(elemA) ) - val message = FhirToHl7Converter(rootSchema).process(bundle) + val message = FhirToHl7Converter( + rootSchema, + warnings = mutableListOf(), + errors = mutableListOf() + ).process(bundle) assertThat(Terser(message).get("MSH-11")).isEqualTo("654321") assertThat(Terser(message).get("MSH-12")).isEqualTo("fedcba") } @@ -540,7 +569,11 @@ class FhirToHl7ConverterTests { val overrideSchema = HL7ConverterSchema(elements = mutableListOf(elemBOverride)) rootSchema.override(overrideSchema) - val message = FhirToHl7Converter(rootSchema).process(bundle) + val message = FhirToHl7Converter( + rootSchema, + warnings = mutableListOf(), + errors = mutableListOf() + ).process(bundle) assertThat(Terser(message).get("MSH-11")).isEqualTo("overrideVal") assertThat(Terser(message).get("MSH-12")).isEqualTo("overrideVal") } @@ -578,7 +611,7 @@ class FhirToHl7ConverterTests { val converter = FhirToHl7Converter( mockSchema, terser = terser, - context = contextWithConfig + context = contextWithConfig, warnings = mutableListOf(), errors = mutableListOf() ) // should truncate to 194 @@ -628,7 +661,7 @@ class FhirToHl7ConverterTests { val converter = FhirToHl7Converter( mockSchema, terser = terser, - context = contextWithConfig + context = contextWithConfig, warnings = mutableListOf(), errors = mutableListOf() ) // should truncate to 199 @@ -690,8 +723,14 @@ class FhirToHl7ConverterTests { val bundle = Bundle() bundle.id = "abc123" - val baseMessage = FhirToHl7Converter(baseSchema).process(bundle) - val message = FhirToHl7Converter(extendedSchema).process(bundle) + val baseMessage = FhirToHl7Converter( + baseSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) + val message = FhirToHl7Converter( + extendedSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) assertThat(Terser(baseMessage).get("MSH-11")).isEqualTo("abc123") // Confirms that we can override an element that exists in the base @@ -702,8 +741,14 @@ class FhirToHl7ConverterTests { fun `test override uses a constant`() { val bundle = Bundle() - val baseMessage = FhirToHl7Converter(baseSchema).process(bundle) - val message = FhirToHl7Converter(extendedSchema).process(bundle) + val baseMessage = FhirToHl7Converter( + baseSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) + val message = FhirToHl7Converter( + extendedSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) assertThat(Terser(baseMessage).get("MSH-10")).isEqualTo("10") // Assert that we can create an override element that uses a constant from the base schema @@ -714,8 +759,14 @@ class FhirToHl7ConverterTests { fun `test override overrides a constant`() { val bundle = Bundle() - val baseMessage = FhirToHl7Converter(baseSchema).process(bundle) - val message = FhirToHl7Converter(extendedSchema).process(bundle) + val baseMessage = FhirToHl7Converter( + baseSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) + val message = FhirToHl7Converter( + extendedSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) assertThat(Terser(baseMessage).get("MSH-8")).isEqualTo("otherValue") // Assert that a constant can get overridden @@ -726,8 +777,14 @@ class FhirToHl7ConverterTests { fun `test the overriding schema takes priority when setting the same HL7 field`() { val bundle = Bundle() - val baseMessage = FhirToHl7Converter(baseSchema).process(bundle) - val message = FhirToHl7Converter(extendedSchema).process(bundle) + val baseMessage = FhirToHl7Converter( + baseSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) + val message = FhirToHl7Converter( + extendedSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) assertThat(Terser(baseMessage).get("MSH-14")).isEqualTo("14") // A new element in the overriding schema sets MSH-14 as well @@ -742,8 +799,14 @@ class FhirToHl7ConverterTests { messageHeader.definition = "definition" bundle.addEntry().resource = messageHeader - val baseMessage = FhirToHl7Converter(baseSchema).process(bundle) - val message = FhirToHl7Converter(extendedSchema).process(bundle) + val baseMessage = FhirToHl7Converter( + baseSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) + val message = FhirToHl7Converter( + extendedSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) assertThat(Terser(baseMessage).get("SFT-2")).isEqualTo("1") // Assert that an element can be overridden in a nested schema @@ -759,8 +822,14 @@ class FhirToHl7ConverterTests { messageHeader.id = "idSft" bundle.addEntry().resource = messageHeader - val baseMessage = FhirToHl7Converter(baseSchema).process(bundle) - val message = FhirToHl7Converter(extendedSchema).process(bundle) + val baseMessage = FhirToHl7Converter( + baseSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) + val message = FhirToHl7Converter( + extendedSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) assertThat(Terser(baseMessage).get("SFT-3")).isEqualTo("definition") @@ -779,8 +848,14 @@ class FhirToHl7ConverterTests { messageHeader.event = Coding("system", "noEvent", "displayCode") bundle.addEntry().resource = messageHeader - val baseMessage = FhirToHl7Converter(baseSchema).process(bundle) - val message = FhirToHl7Converter(extendedSchema).process(bundle) + val baseMessage = FhirToHl7Converter( + baseSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) + val message = FhirToHl7Converter( + extendedSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) assertThat(Terser(baseMessage).get("SFT-5")).isEqualTo("idSft") // Asserts that the base extending schema can provide for a nested schema element @@ -797,8 +872,14 @@ class FhirToHl7ConverterTests { messageHeader.event = Coding("system", "code", "displayCode") bundle.addEntry().resource = messageHeader - val baseMessage = FhirToHl7Converter(baseSchema).process(bundle) - val message = FhirToHl7Converter(extendedSchema).process(bundle) + val baseMessage = FhirToHl7Converter( + baseSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) + val message = FhirToHl7Converter( + extendedSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) assertThat(Terser(baseMessage).get("SFT-1-1")).isEqualTo("system") // Assert that a deeply nested schema element can be overridden, using a @@ -817,7 +898,12 @@ class FhirToHl7ConverterTests { // Assert that a new element that would be "part" of an existing schema cannot // reference a constant from the nested schema - assertFailure { FhirToHl7Converter(extendedSchema).process(bundle) } + val transformer = FhirToHl7Converter( + extendedSchema, + warnings = mutableListOf(), errors = mutableListOf() + ) + transformer.process(bundle) + assertThat(transformer.errors.size > 0) } @Test @@ -829,8 +915,14 @@ class FhirToHl7ConverterTests { messageHeader.event = Coding("system", "xon3", "displayCode") bundle.addEntry().resource = messageHeader - val baseMessage = FhirToHl7Converter(baseSchema).process(bundle) - val message = FhirToHl7Converter(extendedSchemaOverridesSoftware).process(bundle) + val baseMessage = FhirToHl7Converter( + baseSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) + val message = FhirToHl7Converter( + extendedSchemaOverridesSoftware, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) assertThat(Terser(baseMessage).get("SFT-5")).isEqualTo("idSft") // Assert that the override sets a different schema for software that @@ -847,8 +939,14 @@ class FhirToHl7ConverterTests { messageHeader.event = Coding("system", "aCode", "displayCode") bundle.addEntry().resource = messageHeader - val baseMessage = FhirToHl7Converter(baseSchema).process(bundle) - val message = FhirToHl7Converter(extendedSchemaOverridesXon).process(bundle) + val baseMessage = FhirToHl7Converter( + baseSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) + val message = FhirToHl7Converter( + extendedSchemaOverridesXon, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) assertThat(Terser(baseMessage).get("SFT-1-1")).isEqualTo("system") // Assert that an extending schema can override the schema for a nested element @@ -864,8 +962,14 @@ class FhirToHl7ConverterTests { messageHeader.event = Coding("system", "aCode", "displayCode") bundle.addEntry().resource = messageHeader - val baseMessage = FhirToHl7Converter(baseSchema).process(bundle) - val message = FhirToHl7Converter(extendedSchemaOverridesXon).process(bundle) + val baseMessage = FhirToHl7Converter( + baseSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) + val message = FhirToHl7Converter( + extendedSchemaOverridesXon, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) assertThat(Terser(baseMessage).get("SFT-1-10")).isNull() // Assert that a new element can get added when overriding a nested schema @@ -877,8 +981,14 @@ class FhirToHl7ConverterTests { val bundle = Bundle() bundle.id = "abc123" - val baseMessage = FhirToHl7Converter(baseSchema).process(bundle) - val message = FhirToHl7Converter(extendedExtendedSchema).process(bundle) + val baseMessage = FhirToHl7Converter( + baseSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) + val message = FhirToHl7Converter( + extendedExtendedSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) assertThat(Terser(baseMessage).get("MSH-11")).isEqualTo("abc123") // Assert that a schema that extends a schema overriding MSH-11 can further @@ -895,8 +1005,14 @@ class FhirToHl7ConverterTests { messageHeader.event = Coding("system", "aCode", "displayCode") bundle.addEntry().resource = messageHeader - val baseMessage = FhirToHl7Converter(baseSchema).process(bundle) - val message = FhirToHl7Converter(extendedExtendedSchema).process(bundle) + val baseMessage = FhirToHl7Converter( + baseSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) + val message = FhirToHl7Converter( + extendedExtendedSchema, + warnings = mutableListOf(), errors = mutableListOf() + ).process(bundle) assertThat(Terser(baseMessage).get("SFT-1-1")).isEqualTo("system") // Asserts that a nested schema cannot use an extends clause diff --git a/prime-router/src/testIntegration/resources/datatests/FHIR_to_HL7/sample_ME_20240806-0001.hl7 b/prime-router/src/testIntegration/resources/datatests/FHIR_to_HL7/sample_ME_20240806-0001.hl7 index c94288fb486..e3505bb4288 100644 --- a/prime-router/src/testIntegration/resources/datatests/FHIR_to_HL7/sample_ME_20240806-0001.hl7 +++ b/prime-router/src/testIntegration/resources/datatests/FHIR_to_HL7/sample_ME_20240806-0001.hl7 @@ -1,7 +1 @@ -MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|CDC-ReportStream^11D2030855^CLIA|ME-DOH|ME-DOH|20240806184343+0000||ORU^R01^ORU_R01|4168584e-1921-40e7-ba70-76e298ec37be|P|2.5.1|||NE|NE|USA|UNICODE UTF-8|ENG^English^ISO||PHLabReport-NoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO -SFT|Centers for Disease Control and Prevention|0.2-SNAPSHOT|PRIME ReportStream|0.2-SNAPSHOT||20240805221133+0000 -PID|1||98796b2b-f07e-4750-9141-3007811d2b33^^^Testing Lab&12D4567890&CLIA^PI^Testing Lab&12D4567890&CLIA||Test^Patientseven^^^^^L||19880423|M||2028-9^asian^HL70005^^^^2.5.1^^asian|345 Simple St^^Portland^ME^04019^USA^^^Cumberland||(340) 555 5555^PRS^CP^^1^340^5555555^^^^^(340) 555 5555|||||||||N^Not Hispanic or Latino^HL70189^^^^2.9^^Not Hispanic or Latino||||||||N -ORC|RE|4168584e-1921-40e7-ba70-76e298ec37be^Testing Lab^12D4567890^CLIA|4168584e-1921-40e7-ba70-76e298ec37be^Testing Lab^12D4567890^CLIA|||||||||1245319599^McTester^Phil^^^^^^NPI&2.16.840.1.113883.4.6&ISO^L^^^NPI||(530) 867 5309^WPN^PH^^1^530^8675309^^^^^(530) 867 5309|20240806184340+0000||||||Testing Lab^L^^^^CLIA&2.16.840.1.113883.4.7&ISO^XX^^^12D4567890|123 Beach Way^^Denver^CO^80210^USA|(530) 867 5309^WPN^PH^^1^530^8675309^^^^^(530) 867 5309|321 Ocean Drive^^Denver^CO^80210^USA -OBR|1|4168584e-1921-40e7-ba70-76e298ec37be^Testing Lab^12D4567890^CLIA|4168584e-1921-40e7-ba70-76e298ec37be^Testing Lab^12D4567890^CLIA|95941-1^Influenza virus A and B and SARS-CoV-2 (COVID-19) and Respiratory syncytial virus RNA panel - Respiratory system specimen by NAA with probe detection^LN|||20240806182840+0000|||||||||1245319599^McTester^Phil^^^^^^NPI&2.16.840.1.113883.4.6&ISO^L^^^NPI|(530) 867 5309^WPN^PH^^1^530^8675309^^^^^(530) 867 5309|||||20240806184343+0000|||F -OBX|1|CWE|94500-6^SARS-CoV-2 (COVID-19) RNA [Presence] in Respiratory system specimen by NAA with probe detection^LN^^^^^^COVID-19||260373001^Detected^SCT|||A^Abnormal^HL70078^^^^2.7|||F|||20240806182840+0000|12D4567890^Testing Lab^CLIA||Xpert Xpress CoV-2/Flu/RSV plus_Cepheid^Xpert Xpress CoV-2/Flu/RSV plus^^^^^^^Xpert Xpress CoV-2/Flu/RSV plus_Cepheid|^^MNI|20240806184340+0000||||Testing Lab^L^^^^CLIA&2.16.840.1.113883.4.7&ISO^XX^^^12D4567890|123 Beach Way^^Denver^CO^80210^USA -SPM|1|4168584e-1921-40e7-ba70-76e298ec37be&Testing Lab&12D4567890&CLIA^4168584e-1921-40e7-ba70-76e298ec37be&Testing Lab&12D4567890&CLIA||258500001^Nasopharyngeal swab^SCT^^^^2.67^^Nasopharyngeal swab||||87100004^Topography unknown (body structure)^SCT^^^^^^Topography unknown (body structure)|||||||||20240806182840+0000|20240806182840+0000 \ No newline at end of file +MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|CDC-ReportStream^11D2030855^CLIA|ME-DOH|ME-DOH|20240806184343+0000||ORU^R01^ORU_R01|4168584e-1921-40e7-ba70-76e298ec37be|P|2.5.1|||NE|NE|USA|UNICODE UTF-8|ENG^English^ISO||PHLabReport-NoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.2-SNAPSHOT|PRIME ReportStream|0.2-SNAPSHOT||20240805221133+0000 PID|1||98796b2b-f07e-4750-9141-3007811d2b33^^^Testing Lab&12D4567890&CLIA^PI^Testing Lab&12D4567890&CLIA||Test^Patientseven^^^^^L||19880423|M||2028-9^asian^HL70005^^^^2.5.1^^asian|345 Simple St^^Portland^ME^04019^USA^^^Cumberland||(340) 555 5555^PRS^CP^^1^340^5555555^^^^^(340) 555 5555|||||||||N^Not Hispanic or Latino^HL70189^^^^2.9^^Not Hispanic or Latino||||||||N ORC|RE|4168584e-1921-40e7-ba70-76e298ec37be^Testing Lab^12D4567890^CLIA|4168584e-1921-40e7-ba70-76e298ec37be^Testing Lab^12D4567890^CLIA|||||||||1245319599^McTester^Phil^^^^^^NPI&2.16.840.1.113883.4.6&ISO^L^^^NPI||(530) 867 5309^WPN^PH^^1^530^8675309^^^^^(530) 867 5309|20240806184340+0000||||||Testing Lab^L^^^^CLIA&2.16.840.1.113883.4.7&ISO^XX^^^12D4567890|123 Beach Way^^Denver^CO^80210^USA|(530) 867 5309^WPN^PH^^1^530^8675309^^^^^(530) 867 5309|321 Ocean Drive^^Denver^CO^80210^USA OBR|1|4168584e-1921-40e7-ba70-76e298ec37be^Testing Lab^12D4567890^CLIA|4168584e-1921-40e7-ba70-76e298ec37be^Testing Lab^12D4567890^CLIA|95941-1^Influenza virus A and B and SARS-CoV-2 (COVID-19) and Respiratory syncytial virus RNA panel - Respiratory system specimen by NAA with probe detection^LN|||20240806182840+0000|||||||||1245319599^McTester^Phil^^^^^^NPI&2.16.840.1.113883.4.6&ISO^L^^^NPI|(530) 867 5309^WPN^PH^^1^530^8675309^^^^^(530) 867 5309|||||20240806184343+0000|||F OBX|1|CWE|94500-6^SARS-CoV-2 (COVID-19) RNA [Presence] in Respiratory system specimen by NAA with probe detection^LN^^^^^^COVID-19||260373001^Detected^SCT|||A^Abnormal^HL70078^^^^2.7|||F|||20240806182840+0000|12D4567890^Testing Lab^CLIA||Xpert Xpress CoV-2/Flu/RSV plus_Cepheid^Xpert Xpress CoV-2/Flu/RSV plus^^^^^^^Xpert Xpress CoV-2/Flu/RSV plus_Cepheid|^^MNI|20240806184340+0000||||Testing Lab^L^^^^CLIA&2.16.840.1.113883.4.7&ISO^XX^^^12D4567890|123 Beach Way^^Denver^CO^80210^USA SPM|1|4168584e-1921-40e7-ba70-76e298ec37be&Testing Lab&12D4567890&CLIA^4168584e-1921-40e7-ba70-76e298ec37be&Testing Lab&12D4567890&CLIA||258500001^Nasopharyngeal swab^SCT^^^^2.67^^Nasopharyngeal swab||||87100004^Topography unknown (body structure)^SCT^^^^^^Topography unknown (body structure)|||||||||20240806182840+0000|20240806182840+0000 \ No newline at end of file