diff --git a/operations/app/terraform/modules/chatops/main.tf b/operations/app/terraform/modules/chatops/main.tf index 722ee471762..99e8705d0e7 100644 --- a/operations/app/terraform/modules/chatops/main.tf +++ b/operations/app/terraform/modules/chatops/main.tf @@ -48,3 +48,4 @@ resource "azurerm_container_group" "chatops" { var.storage_account ] } + diff --git a/prime-router/docs/authz/authz-deprecated-implementation.md b/prime-router/docs/authz/authz-deprecated-implementation.md new file mode 100644 index 00000000000..4beaa25b869 --- /dev/null +++ b/prime-router/docs/authz/authz-deprecated-implementation.md @@ -0,0 +1,151 @@ +# Authorization Deep Dive + +## Current Design + +To authorize a user for an endpoint, we compare the scopes that are present on the access token versus what scope is +required. + +### Scope Design + +- A client can have multiple scopes +- Scopes are in the format of `{organization}.{senderOrReceiver}.{role}` +- `organization` and `senderOrReceiver` can be omitted with a wildcard `*` or `default` + +There are four possible roles. + +| Role | Definition | +|------------|---------------------------------------------------------------------------| +| primeadmin | system administrator with full access to all endpoints | +| admin | administrator with access to endpoints that manage their own organization | +| user | user with read access to endpoints scoped to their organization | +| report | able to submit a report as an organization | + + +Here are some examples given an organization of md-phd (Maryland Public Health Department) + +| Scope | Access | +|-------------------------|------------------------------------------------------------------------| +| `*.*.primeadmin` | System Administrator | +| `md-phd.*.admin` | Organization Administrator for md-phd | +| `md-phd.*.user` | Organization User for md-phd | +| `md-phd.*.report` | Organization Report Submitter for md-phd | +| `md-phd.default.admin` | Organization Administrator for md-phd | +| `md-phd.default.user` | Organization User for md-phd | +| `md-phd.default.report` | Organization Report Submitter for md-phd | +| `md-phd.abc.admin` | Organization Administrator for md-phd scoped to abc sender or receiver | +| `md-phd.abc.user` | Organization User for md-phd scoped to abc sender or receiver | + + + +We currently have three methods of authenticating and authorizing a user or machine for accessing +ReportStream endpoints. + +### Server2Server + +This is ReportStream code that handles the client credentials OAuth 2.0 flow. This is most often used in machine-to-machine +communication such as senders submitting reports. The important bit for authorization happens based on the scope requested +in the call to the `/api/token` endpoint. A member of the engagement team sets up an organization's public key under a +specific scope in the organization settings. When a request for an access token comes in with a requested scope, we check +the JWT assertion was signed with the public key under that scope in settings. + +If the request is successful, an access token is granted with a scope matching the pattern above. + +### Okta + +Users are all set up under our Okta instance. We add users to appropriate groups in Okta that correspond to what they +should be allowed to access. Those groups are in a custom string array claim called `organization` in the access token +which we then map to scopes that our system can handle. Scopes internal to okta are ignored. + +Here is the Okta group to scope mapping strategy given an organization of md-phd (Maryland Public Health Department) + +| Okta Group | ReportStream Scope | +|-------------------------|--------------------| +| `DHPrimeAdmins` | `*.*.primeadmin` | +| `DHmd-phdAdmins` | `md-phd.*.admin` | +| `DHSender_md-phdAdmins` | `md-phd.*.admin` | +| `DHSender_md-phd` | `md-phd.*.user` | +| `DHmd-phd` | `md-phd.*.user` | +* Note: There is no group that maps to the `report` role + + +### Azure Function Keys + +This is the default authentication process built into the Azure functions library. It is a simple shared secret that is +stored in Azure and shared with the client. It is still used on some endpoints for its simplicity of setup. It includes +no authorization check at all and should be deprecated for that reason. + +### Current system pros +- Flexible for onboarding senders/receivers to allow them to use the system they prefer +- The scopes are fine-grained allowing us to be specific about what resources a client should be able to access + +### Current system cons +- x-function-key authentication is almost a backdoor into our system given that it does not do authorization +- Scopes are difficult to understand and are often tailored to a single client rather than generic permissions +- Authorization code is spread widely across the codebase with a lot of duplication +- Okta group to scope mapping is clunky and prone to errors based on group naming in the Okta admin portal +- Difficult to keep track of which client is using what authentication system + + +## Authorization requirements by endpoint + +| Location | Function Name | Verb | URL | Auth Strategy | Role requirements | Restrictions | +|-------------------------------------------------------------------------------------------|-----------------------------|------------------|-------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|------------------------------------------------------------------|----------------------------------| +| [SenderFilesFunction](../../src/main/kotlin/azure/SenderFilesFunction.kt) | getSenderFiles | GET | /api/sender-files | [OktaAuthentication](../../src/main/kotlin/tokens/OktaAuthentication.kt) | System Admin | None | +| [AdminApiFunctions](../../src/main/kotlin/azure/AdminApiFunctions.kt) | getSendFailures | GET | /api/adm/getsendfailures | [OktaAuthentication](../../src/main/kotlin/tokens/OktaAuthentication.kt) | System Admin | None | +| [AdminApiFunctions](../../src/main/kotlin/azure/AdminApiFunctions.kt) | listreceiversconnstatus | GET | /api/adm/listreceiversconnstatus | [OktaAuthentication](../../src/main/kotlin/tokens/OktaAuthentication.kt) | System Admin | None | +| [AdminApiFunctions](../../src/main/kotlin/azure/AdminApiFunctions.kt) | getresend | GET | /api/adm/getresend | [OktaAuthentication](../../src/main/kotlin/tokens/OktaAuthentication.kt) | System Admin | None | +| [ApiKeysFunctions](../../src/main/kotlin/azure/ApiKeysFunctions.kt) | getApiKeys | GET | /api/settings/organizations/{organizationName}/public-keys | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, or org user | Organization | +| [ApiKeysFunctions](../../src/main/kotlin/azure/ApiKeysFunctions.kt) | getApiKeysV1 | GET | /api/v1/settings/organizations/{organizationName}/public-keys | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, or org user | Organization | +| [ApiKeysFunctions](../../src/main/kotlin/azure/ApiKeysFunctions.kt) | postApiKey | POST | /api/settings/organizations/{organizationName}/public-keys | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin or org admin | Organization | +| [ApiKeysFunctions](../../src/main/kotlin/azure/ApiKeysFunctions.kt) | deleteApiKey | DELETE | settings/organizations/{organizationName}/public-keys/{scope}/{kid} | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin or org admin | Organization | +| [CheckFunction](../../src/main/kotlin/azure/CheckFunction.kt) | check | GET,POST | /api/check | x-functions-key | None | None | +| [CheckFunction](../../src/main/kotlin/azure/CheckFunction.kt) | checkreceiver | POST | /api/checkreceiver/org/{orgName}/receiver/{receiverName} | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin or org admin | Organization | +| [CovidResultMetadataFunction](../../src/main/kotlin/azure/CovidResultMetaDataFunction.kt) | save-covid-result-metadata | GET | /api/saveTestData | x-functions-key | None | None | +| [EmailEngineFunction](../../src/main/kotlin/azure/EmailEngineFunction.kt) | createEmailSchedule | POST | /api/email-schedule | EmailEngineFunction.validateUser (Custom Okta) | System Admin | None | +| [EmailEngineFunction](../../src/main/kotlin/azure/EmailEngineFunction.kt) | deleteEmailSchedule | DELETE | /api/email-schedule/{scheduleId} | EmailEngineFunction.validateUser (Custom Okta) | System Admin | None | +| [EmailSenderFunction](../../src/main/kotlin/azure/EmailSenderFunction.kt) | emailRegisteredOrganization | POST | /api/email-registered | None | None | None | +| [SettingsFunctions](../../src/main/kotlin/azure/SettingsFunctions.kt) | getOrganizations | GET,HEAD | /api/settings/organizations | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | HEAD: System Admin
GET: System Admin, org admin, or org user | Head: None
GET: Organization | +| [SettingsFunctions](../../src/main/kotlin/azure/SettingsFunctions.kt) | getOneOrganization | GET | /api/settings/organizations/{organizationName} | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, or org user | Organization | +| [SettingsFunctions](../../src/main/kotlin/azure/SettingsFunctions.kt) | getSenders | GET | /api/settings/organizations/{organizationName}/senders | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, or org user | Organization | +| [SettingsFunctions](../../src/main/kotlin/azure/SettingsFunctions.kt) | updateOneOrganization | DELETE,PUT | /api/settings/organizations/{organizationName} | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin | None | +| [SettingsFunctions](../../src/main/kotlin/azure/SettingsFunctions.kt) | getOneSender | GET | /api/settings/organizations/{organizationName}/senders/{senderName} | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, or org user | Organization | +| [SettingsFunctions](../../src/main/kotlin/azure/SettingsFunctions.kt) | updateOneSender | DELETE,PUT | /api/settings/organizations/{organizationName}/senders/{senderName} | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin | None | +| [SettingsFunctions](../../src/main/kotlin/azure/SettingsFunctions.kt) | getReceivers | GET | /api/settings/organizations/{organizationName}/receivers | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, or org user | Organization | +| [SettingsFunctions](../../src/main/kotlin/azure/SettingsFunctions.kt) | getOneReceiver | GET | /api/settings/organizations/{organizationName}/receivers/{receiverName} | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, or org user | Organization | +| [SettingsFunctions](../../src/main/kotlin/azure/SettingsFunctions.kt) | updateOneReceiver | DELETE,PUT | /api/settings/organizations/{organizationName}/receivers/{receiverName} | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin | None | +| [SettingsFunctions](../../src/main/kotlin/azure/SettingsFunctions.kt) | getSettingRevisionHistory | GET | /api/waters/org/{organizationName}/settings/revs/{settingSelector} | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, or org user | Organization | +| [RequeueFunction](../../src/main/kotlin/azure/RequeueFunction.kt) | requeue | POST | /api/requeue/send | x-functions-key | None | None | +| [RequeueFunction](../../src/main/kotlin/azure/RequeueFunction.kt) | doResendFunction | POST | /api/adm/resend | [OktaAuthentication](../../src/main/kotlin/tokens/OktaAuthentication.kt) | System Admin | None | +| [TokenFunction](../../src/main/kotlin/azure/TokenFunction.kt) | token | POST | /api/token | [Server2ServerAuthentication](../../src/main/kotlin/tokens/Server2ServerAuthentication.kt) | Requires properly signed client assertion JWT | None | +| [HistoryFunctions](../../src/main/kotlin/azure/HistoryFunctions.kt) | getReports | GET,HEAD,OPTIONS | /api/history/report | HistoryFunctions.checkAuthenticated (Custom) | System Admin or org user (no org Admin) | Organization | +| [HistoryFunctions](../../src/main/kotlin/azure/HistoryFunctions.kt) | searchReports | POST | /api/v1/reports/search | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin | None | +| [HistoryFunctions](../../src/main/kotlin/azure/HistoryFunctions.kt) | getReportById | GET | /api/history/report/{reportId} | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, org user, or report | Organization | +| [HistoryFunctions](../../src/main/kotlin/azure/HistoryFunctions.kt) | getFacilitiesByReportId | GET | /api/history/report/{reportId}/facilities | HistoryFunctions.checkAuthenticated (Custom) | System Admin or org user (no org Admin) | Organization | +| [LookupTableFunctions](../../src/main/kotlin/azure/LookupTableFunctions.kt) | getLookupTableList | GET,HEAD | /api/lookuptables/list | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | None | None | +| [LookupTableFunctions](../../src/main/kotlin/azure/LookupTableFunctions.kt) | getLookupTableData | GET | /api/lookuptables/{tableName}/{tableVersion}/content | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | None | None | +| [LookupTableFunctions](../../src/main/kotlin/azure/LookupTableFunctions.kt) | getActiveLookupTableData | GET | /api/lookuptables/{tableName}/content | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | None | None | +| [LookupTableFunctions](../../src/main/kotlin/azure/LookupTableFunctions.kt) | getLookupTableInfo | GET | /api/lookuptables/{tableName}/{tableVersion}/info | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | None | None | +| [LookupTableFunctions](../../src/main/kotlin/azure/LookupTableFunctions.kt) | createLookupTable | POST | /api/lookuptables/{tableName} | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin | None | +| [LookupTableFunctions](../../src/main/kotlin/azure/LookupTableFunctions.kt) | activateLookupTable | PUT | /api/lookuptables/{tableName}/{tableVersion}/activate | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin | None | +| [MessagesFunctions](../../src/main/kotlin/azure/MessagesFunctions.kt) | messageSearch | GET | /api/messages | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin | None | +| [MessagesFunctions](../../src/main/kotlin/azure/MessagesFunctions.kt) | messageDetails | GET | /api/message/{id} | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin | None | +| [MetaDataFunction](../../src/main/kotlin/azure/MetaDataFunction.kt) | getLivdData | GET | /api/metadata/livd | None | None | None | +| [ReportFunction](../../src/main/kotlin/azure/ReportFunction.kt) | reports | POST | /api/reports | x-functions-key | None | None | +| [ReportFunction](../../src/main/kotlin/azure/ReportFunction.kt) | getMessagesFromTestBank | POST | /api/reports/testing | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin | None | +| [ReportFunction](../../src/main/kotlin/azure/ReportFunction.kt) | processFhirDataRequest | POST | /api/reports/testing/test | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin | None | +| [ReportFunction](../../src/main/kotlin/azure/ReportFunction.kt) | downloadReport | GET | /api/reports/download | x-functions-key | None | None | +| [ReportFunction](../../src/main/kotlin/azure/ReportFunction.kt) | waters | POST | /api/waters | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, org user, or report | Organization and Sender | +| [ValidateFunction](../../src/main/kotlin/azure/ValidateFunction.kt) | validate | POST | /api/validate | None | None | None | +| [DeliveryFunction](../../src/main/kotlin/history/azure/DeliveryFunction.kt) | getDeliveriesV1 | POST | /api/v1/receivers/{receiverName}/deliveries | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, org user, or report | Organization | +| [DeliveryFunction](../../src/main/kotlin/history/azure/DeliveryFunction.kt) | getDeliveriesHistory | POST | /api/v1/waters/org/{organization}/deliveries | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, org user, or report | Organization | +| [DeliveryFunction](../../src/main/kotlin/history/azure/DeliveryFunction.kt) | getDeliveries | GET | /api/waters/org/{organization}/deliveries | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, org user, or report | Organization | +| [DeliveryFunction](../../src/main/kotlin/history/azure/DeliveryFunction.kt) | getDeliveryDetails | GET | /api/waters/report/{id}/delivery | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, org user, or report | Organization | +| [DeliveryFunction](../../src/main/kotlin/history/azure/DeliveryFunction.kt) | getEtorMetadataForDelivery | GET | /api/waters/report/{reportId}/delivery/etorMetadata | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, org user, or report | Organization | +| [DeliveryFunction](../../src/main/kotlin/history/azure/DeliveryFunction.kt) | getDeliveryFacilities | GET | /api/waters/report/{id}/facilities | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, org user, or report | Organization | +| [DeliveryFunction](../../src/main/kotlin/history/azure/DeliveryFunction.kt) | getReportItemsV1 | GET | /api/v1/report/{reportId}/items | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin | None | +| [DeliveryFunction](../../src/main/kotlin/history/azure/DeliveryFunction.kt) | getSubmittersV1 | POST | /api/v1/receivers/{receiverName}/deliveries/submitters/search | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, org user, or report | Organization | +| [SubmissionFunction](../../src/main/kotlin/history/azure/SubmissionFunction.kt) | getOrgSubmissionsList | GET | /api/waters/org/{organization}/submissions | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, org user, or report | Organization | +| [SubmissionFunction](../../src/main/kotlin/history/azure/SubmissionFunction.kt) | getReportDetailedHistory | GET | /api/waters/report/{id}/history | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, org user, or report | Organization | +| [SubmissionFunction](../../src/main/kotlin/history/azure/SubmissionFunction.kt) | getEtorMetadataForHistory | GET | /api/waters/report/{reportId}/history/etorMetadata | [AuthenticatedClaims](../../src/main/kotlin/tokens/AuthenticatedClaims.kt) | System Admin, org admin, org user, or report | Organization | + +*This is valid as of 12/16/24 diff --git a/prime-router/docs/design/design/RS-Extention-Registry.yml b/prime-router/docs/design/design/RS-Extention-Registry.yml index 8b3bb667f28..247103ae682 100644 --- a/prime-router/docs/design/design/RS-Extention-Registry.yml +++ b/prime-router/docs/design/design/RS-Extention-Registry.yml @@ -3418,6 +3418,99 @@ valueType: String hl7Use: XTN.12 +- Resource: Any + FHIR-Type: Extension + HL7-Type: + CP: + Example: + { + "fullUrl": "Coverage/1733795578717198000.8888487d-55f4-4d1d-963d-f8692fc77e9c", + "resource": { + "resourceType": "Coverage", + "id": "1733795578717198000.8888487d-55f4-4d1d-963d-f8692fc77e9c", + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/composite-price", + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/hl7v2Field", + "valueString": "IN1.40" + }, + { + "url": "CP.1.1", + "valueDecimal": 33.33 + }, + { + "url": "CP.1.2", + "valueString": "MXN" + }, + { + "url": "CP.2", + "valueString": "P3" + }, + { + "url": "CP.3", + "valueDecimal": 3.003 + }, + { + "url": "CP.4", + "valueDecimal": -3 + }, + { + "url": "CP.5", + "valueCodeableConcept": { + # CWE to CodeableConcept + } + }, + { + "url": "CP.6", + "valueString": "P" + } + ] + } + ] + } + } + Extension: + - id: composite-price + url: "https://reportstream.cdc.gov/fhir/StructureDefinition/composite-price" + valueType: Extension + hl7Use: NA + extension: + - id: hl7v2Field + url: "https://reportstream.cdc.gov/fhir/StructureDefinition/hl7v2Field" + valueType: String + hl7Use: NA + description: Identifies the HL7 field of the data. + - id: cp-price-quantity + url: "CP.1.1" + valueType: Decimal + hl7use: CP.1.1 + - id: cp-price-denomination + url: "CP.1.2" + valueType: String + hl7use: CP.1.2 + - id: cp-price-type + url: "CP.2" + valueType: String + hl7use: CP.2 + - id: cp-from-value + url: "CP.3" + valueType: Decimal + hl7use: CP.3 + - id: cp-to-value + url: "CP.4" + valueType: Decimal + hl7use: CP.4 + - id: cp-range-units + url: "CP.5" + valueType: CodeableConcept + hl7use: CP.5 + - id: cp-range-type + url: "CP.6" + valueType: String + hl7use: CP.6 + - Resource: Any FHIR-Type: MessageHeader.destination HL7-Type: diff --git a/prime-router/metadata/HL7/catchall/hl7/codesystem/ExtensionUrlMapping.yml b/prime-router/metadata/HL7/catchall/hl7/codesystem/ExtensionUrlMapping.yml index 2643e984fed..cc27f956220 100644 --- a/prime-router/metadata/HL7/catchall/hl7/codesystem/ExtensionUrlMapping.yml +++ b/prime-router/metadata/HL7/catchall/hl7/codesystem/ExtensionUrlMapping.yml @@ -65,6 +65,10 @@ - id: "xon-organization" url: "https://reportstream.cdc.gov/fhir/StructureDefinition/xon-organization" +# CP -> Extension +- id: "composite-price" + url: "https://reportstream.cdc.gov/fhir/StructureDefinition/composite-price" + # CX -> Identifier - id: "assigning-facility" url: "https://reportstream.cdc.gov/fhir/StructureDefinition/assigning-facility" diff --git a/prime-router/metadata/HL7/catchall/hl7/datatypes/CP/CPExtension.yml b/prime-router/metadata/HL7/catchall/hl7/datatypes/CP/CPExtension.yml new file mode 100644 index 00000000000..ad352c02b56 --- /dev/null +++ b/prime-router/metadata/HL7/catchall/hl7/datatypes/CP/CPExtension.yml @@ -0,0 +1,103 @@ +# $schema: ./../../../../../json_schema/fhir/hl7-to-fhir-mapping-resource-template.json + +url: + type: SYSTEM_URL + value: composite-price + +extension: + expressionType: nested + generateList: true + expressions: + - expressionType: nested + expressionsMap: + url: + type: SYSTEM_URL + value: hl7v2Field + valueString: + type: STRING + expressionType: HL7Spec + valueOf: $cpExtensionName + - expressionType: nested + vars: + cp11: STRING, CP.1 + condition: $cp11 NOT_NULL + expressionsMap: + url: + type: STRING + value: CP.1.1 + valueDecimal: + type: STRING + expressionType: HL7Spec + valueOf: CP.1.1 + - expressionType: nested + vars: + cp12: STRING, CP.1.2 + condition: $cp12 NOT_NULL + expressionsMap: + url: + type: STRING + value: CP.1.2 + valueString: + type: STRING + expressionType: HL7Spec + valueOf: CP.1.2 + - expressionType: nested + vars: + cp2: STRING, CP.2 + condition: $cp2 NOT_NULL + expressionsMap: + url: + type: STRING + valueOf: CP.2 + valueString: + type: STRING + expressionType: HL7Spec + valueOf: CP.2 + - expressionType: nested + vars: + cp3: STRING, CP.3 + condition: $cp3 NOT_NULL + expressionsMap: + url: + type: STRING + valueOf: CP.3 + valueDecimal: + type: STRING + expressionType: HL7Spec + valueOf: CP.3 + - expressionType: nested + vars: + cp4: STRING, CP.4 + condition: $cp4 NOT_NULL + expressionsMap: + url: + type: STRING + valueOf: CP.4 + valueDecimal: + type: STRING + expressionType: HL7Spec + valueOf: CP.4 + - expressionType: nested + vars: + cp5: STRING_ALL, CP.5 + condition: $cp5 NOT_NULL + expressionsMap: + url: + type: STRING + value: CP.5 + valueCodeableConcept: + valueOf: datatypes/CWE/CodeableConcept + specs: CP.5 + expressionType: resource + - expressionType: nested + vars: + cp6: STRING, CP.6 + condition: $cp6 NOT_NULL + expressionsMap: + url: + type: STRING + valueOf: CP.6 + valueString: + type: STRING + expressionType: HL7Spec + valueOf: CP.6 diff --git a/prime-router/metadata/HL7/catchall/hl7/datatypes/MOC/Extension.yml b/prime-router/metadata/HL7/catchall/hl7/datatypes/MOC/MOCExtension.yml similarity index 100% rename from prime-router/metadata/HL7/catchall/hl7/datatypes/MOC/Extension.yml rename to prime-router/metadata/HL7/catchall/hl7/datatypes/MOC/MOCExtension.yml diff --git a/prime-router/metadata/HL7/catchall/hl7/segments/IN1/Coverage.yml b/prime-router/metadata/HL7/catchall/hl7/segments/IN1/Coverage.yml index 8479f85f8bc..c4bfd5d0c1b 100644 --- a/prime-router/metadata/HL7/catchall/hl7/segments/IN1/Coverage.yml +++ b/prime-router/metadata/HL7/catchall/hl7/segments/IN1/Coverage.yml @@ -13,4 +13,36 @@ extension: expressions: - expressionType: resource valueOf: datatypes/AUI/AUIExtension - specs: IN1.14 \ No newline at end of file + specs: IN1.14 + - expressionType: resource + vars: + in137: STRING, IN1.37 + condition: $in137 NOT_NULL + valueOf: datatypes/CP/CPExtension + constants: + cpExtensionName: IN1.37 + specs: IN1.37 + - expressionType: resource + vars: + in138: STRING, IN1.38 + condition: $in138 NOT_NULL + valueOf: datatypes/CP/CPExtension + constants: + cpExtensionName: IN1.38 + specs: IN1.38 + - expressionType: resource + vars: + in140: STRING, IN1.40 + condition: $in140 NOT_NULL + valueOf: datatypes/CP/CPExtension + constants: + cpExtensionName: IN1.40 + specs: IN1.40 + - expressionType: resource + vars: + in141: STRING, IN1.41 + condition: $in141 NOT_NULL + valueOf: datatypes/CP/CPExtension + constants: + cpExtensionName: IN1.41 + specs: IN1.41 diff --git a/prime-router/metadata/HL7/catchall/hl7/segments/ORC/OBRExtension.yml b/prime-router/metadata/HL7/catchall/hl7/segments/ORC/OBRExtension.yml index c2509802ecf..78c9142b244 100644 --- a/prime-router/metadata/HL7/catchall/hl7/segments/ORC/OBRExtension.yml +++ b/prime-router/metadata/HL7/catchall/hl7/segments/ORC/OBRExtension.yml @@ -335,7 +335,7 @@ extension: expressionType: HL7Spec valueOf: OBR.22 - expressionType: resource - valueOf: datatypes/MOC/Extension + valueOf: datatypes/MOC/MOCExtension vars: obr23: STRING_ALL, OBR.23 condition: $obr23 NOT_NULL diff --git a/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt b/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt index e5cd8908412..87bff24f1e9 100644 --- a/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt +++ b/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt @@ -44,6 +44,7 @@ import gov.cdc.prime.router.azure.observability.event.AzureEventServiceImpl import gov.cdc.prime.router.azure.observability.event.IReportStreamEventService import gov.cdc.prime.router.azure.observability.event.ReportStreamEventName import gov.cdc.prime.router.azure.observability.event.ReportStreamEventProperties +import gov.cdc.prime.router.common.BaseEngine import gov.cdc.prime.router.fhirengine.translation.HL7toFhirTranslator import gov.cdc.prime.router.fhirengine.translation.hl7.FhirTransformer import gov.cdc.prime.router.fhirengine.translation.hl7.utils.CustomContext @@ -52,6 +53,8 @@ import gov.cdc.prime.router.fhirengine.utils.FhirTranscoder import gov.cdc.prime.router.fhirengine.utils.HL7Reader import gov.cdc.prime.router.fhirengine.utils.HL7Reader.Companion.parseHL7Message import gov.cdc.prime.router.fhirengine.utils.getObservations +import gov.cdc.prime.router.fhirengine.utils.getRSMessageType +import gov.cdc.prime.router.fhirengine.utils.isElr import gov.cdc.prime.router.logging.LogMeasuredTime import gov.cdc.prime.router.report.ReportService import gov.cdc.prime.router.validation.IItemValidator @@ -111,6 +114,7 @@ class FHIRConverter( companion object { private val clientIdHeader = "client_id" + private val payloadNameHeader = "payloadname" /** * Converts a [FhirConvertQueueMessage] into the input to the convert processing @@ -157,6 +161,7 @@ class FHIRConverter( val blobSubFolderName = message.blobSubFolderName val clientId = message.headers[clientIdHeader] + val payloadName = message.headers[payloadNameHeader] val sender = clientId?.takeIf { it.isNotBlank() }?.let { settings.findSender(it) } if (sender == null) { throw SubmissionSenderNotFound(clientId ?: "", reportId, blobUrl) @@ -178,7 +183,8 @@ class FHIRConverter( // is properly recorded in the report file table with the correct sender actionHistory.trackExternalInputReport( report, - BlobAccess.BlobInfo(format, blobUrl, blobDigest.toByteArray()) + BlobAccess.BlobInfo(format, blobUrl, blobDigest.toByteArray()), + payloadName ) actionHistory.trackActionSenderInfo(sender.fullName) @@ -258,7 +264,7 @@ class FHIRConverter( // TODO: https://github.com/CDCgov/prime-reportstream/issues/14287 FhirPathUtils - val processedItems = process(format, input.blobURL, input.blobDigest, input.topic, actionLogger) + val processedItems = process(format, input, actionLogger) // processedItems can be empty in three scenarios: // - the blob had no contents, i.e. an empty file was submitted @@ -336,6 +342,12 @@ class FHIRConverter( nextAction = TaskAction.destination_filter ) + logger.info( + "Applied transform - parentReportId=[${input.reportId}]" + + ", childReportId=[${report.id}], schemaName=[${input.schemaName}]" + + ", trackingId=[${processedItem.getTrackingId()}]" + ) + // create route event val routeEvent = ProcessEvent( Event.EventAction.DESTINATION_FILTER, @@ -382,7 +394,8 @@ class FHIRConverter( mapOf( ReportStreamEventProperties.BUNDLE_DIGEST to bundleDigestExtractor.generateDigest(processedItem.bundle!!), - ReportStreamEventProperties.ITEM_FORMAT to format + ReportStreamEventProperties.ITEM_FORMAT to format, + ReportStreamEventProperties.ENRICHMENTS to input.schemaName ) ) } @@ -450,14 +463,12 @@ class FHIRConverter( */ internal fun process( format: MimeFormat, - blobURL: String, - blobDigest: String, - topic: Topic, + input: FHIRConvertInput, actionLogger: ActionLogger, routeReportWithInvalidItems: Boolean = true, ): List> { - val validator = topic.validator - val rawReport = BlobAccess.downloadBlob(blobURL, blobDigest) + val validator = input.topic.validator + val rawReport = BlobAccess.downloadBlob(input.blobURL, input.blobDigest) return if (rawReport.isBlank()) { actionLogger.error(InvalidReportMessage("Provided raw data is empty.")) emptyList() @@ -471,7 +482,7 @@ class FHIRConverter( "format" to format.name ) ) { - getBundlesFromRawHL7(rawReport, validator, topic.hl7ParseConfiguration) + getBundlesFromRawHL7(rawReport, validator, input.topic.hl7ParseConfiguration) } } catch (ex: ParseFailureError) { actionLogger.error( @@ -508,21 +519,24 @@ class FHIRConverter( } // 'stamp' observations with their condition code if (item.bundle != null) { + val isElr = item.bundle!!.getRSMessageType() == RSMessageType.LAB_RESULT item.bundle!!.getObservations().forEach { observation -> - val result = stamper.stampObservation(observation) - if (!result.success) { - val logger = actionLogger.getItemLogger(item.index + 1, observation.id) - if (result.failures.isEmpty()) { - logger.warn(UnmappableConditionMessage()) - } else { - logger.warn( - result.failures.map { - UnmappableConditionMessage( - it.failures.map { it.code }, - it.source + if (isElr) { + val result = stamper.stampObservation(observation) + if (!result.success) { + val logger = actionLogger.getItemLogger(item.index + 1, observation.id) + if (result.failures.isEmpty()) { + logger.warn(UnmappableConditionMessage()) + } else { + logger.warn( + result.failures.map { + UnmappableConditionMessage( + it.failures.map { it.code }, + it.source + ) + } ) } - ) } } } diff --git a/prime-router/src/main/kotlin/fhirengine/engine/RSMessageType.kt b/prime-router/src/main/kotlin/fhirengine/engine/RSMessageType.kt new file mode 100644 index 00000000000..d1db9dd9d12 --- /dev/null +++ b/prime-router/src/main/kotlin/fhirengine/engine/RSMessageType.kt @@ -0,0 +1,11 @@ +package gov.cdc.prime.router.fhirengine.engine + +/** + * This class represents a way to group message types from an RS perspective. As we add additional logical + * groupings, FHIRBundleHelpers.getRSMessageType will need to be updated. + * + */ +enum class RSMessageType { + LAB_RESULT, + UNKNOWN, +} \ No newline at end of file diff --git a/prime-router/src/main/kotlin/fhirengine/utils/FHIRBundleHelpers.kt b/prime-router/src/main/kotlin/fhirengine/utils/FHIRBundleHelpers.kt index 564d0d5e0e7..dc12c4a9e8c 100644 --- a/prime-router/src/main/kotlin/fhirengine/utils/FHIRBundleHelpers.kt +++ b/prime-router/src/main/kotlin/fhirengine/utils/FHIRBundleHelpers.kt @@ -9,12 +9,14 @@ import gov.cdc.prime.router.azure.ConditionStamper.Companion.BUNDLE_CODE_IDENTIF import gov.cdc.prime.router.azure.ConditionStamper.Companion.BUNDLE_VALUE_IDENTIFIER import gov.cdc.prime.router.azure.ConditionStamper.Companion.conditionCodeExtensionURL import gov.cdc.prime.router.codes +import gov.cdc.prime.router.fhirengine.engine.RSMessageType import gov.cdc.prime.router.fhirengine.translation.hl7.utils.CustomContext import gov.cdc.prime.router.fhirengine.translation.hl7.utils.FhirPathUtils import gov.cdc.prime.router.fhirengine.utils.FHIRBundleHelpers.Companion.getChildProperties import io.github.linuxforhealth.hl7.data.Hl7RelatedGeneralUtils import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.Bundle +import org.hl7.fhir.r4.model.CodeType import org.hl7.fhir.r4.model.CodeableConcept import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.DateTimeType @@ -116,6 +118,36 @@ fun Bundle.addProvenanceReference() { } } +/** + * Return true if Bundle contains an ELR in the MessageHeader. + * + * @return true if has a MesssageHeader that contains an R01 or ORU_R01, otherwise false. + */ +fun Bundle.isElr(): Boolean { + val code = FhirPathUtils.evaluate( + null, + this, + this, + "Bundle.entry.resource.ofType(MessageHeader).event.code" + ) + .filterIsInstance() + .firstOrNull() + ?.code + return ((code == "R01") || (code == "ORU_R01")) +} + +/** + * Return RSMessageType based on grouping logic. + * + * @return RSMessageType of this Bundle. + */ +fun Bundle.getRSMessageType(): RSMessageType { + return when { + isElr() -> RSMessageType.LAB_RESULT + else -> RSMessageType.UNKNOWN + } +} + /** * Gets all properties for a [Base] resource recursively and filters only its references * diff --git a/prime-router/src/main/kotlin/history/db/ReportGraph.kt b/prime-router/src/main/kotlin/history/db/ReportGraph.kt index 103b9831eb3..0b44dcfe271 100644 --- a/prime-router/src/main/kotlin/history/db/ReportGraph.kt +++ b/prime-router/src/main/kotlin/history/db/ReportGraph.kt @@ -123,11 +123,10 @@ class ReportGraph( } /** - * Recursively goes up the report_linage table from any report until it reaches - * a report with an action type of "receive" (the root report) + * Recursively goes up the report_lineage table from any report until it reaches + * a report that does not appear in report_lineage as a child report (the root report) * - * This will return null if no report with action type "receive" is present or if - * the root is passed in + * This will return null if the root is passed in */ fun getRootReport(childReportId: UUID): ReportFile? { return db.transactReturning { txn -> @@ -172,19 +171,19 @@ class ReportGraph( .from(cte) .join(REPORT_FILE) .on(REPORT_FILE.REPORT_ID.eq(ItemGraphTable.ITEM_GRAPH.PARENT_REPORT_ID)) - .join(ACTION) - .on(ACTION.ACTION_ID.eq(REPORT_FILE.ACTION_ID)) - .where(ACTION.ACTION_NAME.eq(TaskAction.receive)) + .leftJoin(REPORT_LINEAGE) + .on(REPORT_FILE.REPORT_ID.eq(REPORT_LINEAGE.CHILD_REPORT_ID)) + .where(REPORT_LINEAGE.PARENT_REPORT_ID.isNull()) + .orderBy(REPORT_FILE.ACTION_ID.asc()) .fetchOneInto(Item::class.java) return rootItem } /** - * Recursively goes up the report_linage table from any report until it reaches - * all reports with an action type of "receive" (the root report) + * Recursively goes up the report_lineage table from any report until it reaches + * all reports that do not appear in report_lineage as a child report (the root report) * - * This will return null if no report with action type "receive" is present or if - * the root is passed in + * This will return null if the root is passed in * * If the passed in report ID has multiple root reports, they will all be returned */ @@ -476,9 +475,10 @@ class ReportGraph( .from(cte) .join(REPORT_FILE) .on(REPORT_FILE.REPORT_ID.eq(cte.field(0, UUID::class.java))) - .join(ACTION) - .on(ACTION.ACTION_ID.eq(REPORT_FILE.ACTION_ID)) - .where(ACTION.ACTION_NAME.eq(TaskAction.receive)) + .leftJoin(REPORT_LINEAGE) + .on(REPORT_FILE.REPORT_ID.eq(REPORT_LINEAGE.CHILD_REPORT_ID)) + .where(REPORT_LINEAGE.PARENT_REPORT_ID.isNull()) + .orderBy(REPORT_FILE.ACTION_ID.asc()) /** * Accepts a list of ids and walks down the report lineage graph diff --git a/prime-router/src/main/resources/metadata/hl7_mapping/OML_O21/OML_O21-base.yml b/prime-router/src/main/resources/metadata/hl7_mapping/OML_O21/OML_O21-base.yml index 305e890b694..5038b8227fa 100644 --- a/prime-router/src/main/resources/metadata/hl7_mapping/OML_O21/OML_O21-base.yml +++ b/prime-router/src/main/resources/metadata/hl7_mapping/OML_O21/OML_O21-base.yml @@ -1,6 +1,6 @@ # $schema: ./../../../../../../metadata/json_schema/fhir/fhir-to-hl7-mapping.json -hl7Class: ca.uhn.hl7v2.model.v27.message.OML_O21 +hl7Class: fhirengine.translation.hl7.structures.fhirinventory.message.OML_O21 constants: # Prefix for RS custom extension URLs diff --git a/prime-router/src/main/resources/metadata/hl7_mapping/OML_O21/OML_O21-test.yml b/prime-router/src/main/resources/metadata/hl7_mapping/OML_O21/OML_O21-test.yml index d9c02000ec8..5dda094bd39 100644 --- a/prime-router/src/main/resources/metadata/hl7_mapping/OML_O21/OML_O21-test.yml +++ b/prime-router/src/main/resources/metadata/hl7_mapping/OML_O21/OML_O21-test.yml @@ -1,6 +1,6 @@ # $schema: ./../../../../../../metadata/json_schema/fhir/fhir-to-hl7-mapping.json -hl7Class: ca.uhn.hl7v2.model.v27.message.OML_O21 +hl7Class: fhirengine.translation.hl7.structures.fhirinventory.message.OML_O21 extends: classpath:/metadata/hl7_mapping/OML_O21/OML_O21-base.yml elements: diff --git a/prime-router/src/main/resources/metadata/hl7_mapping/datatypes/extensionCP/CP.yml b/prime-router/src/main/resources/metadata/hl7_mapping/datatypes/extensionCP/CP.yml new file mode 100644 index 00000000000..12df1c0bab0 --- /dev/null +++ b/prime-router/src/main/resources/metadata/hl7_mapping/datatypes/extensionCP/CP.yml @@ -0,0 +1,33 @@ +# $schema: ./../../../../../../../metadata/json_schema/fhir/fhir-to-hl7-mapping.json + +elements: + + - name: cp-price-quantity + value: [ '%resource.extension.where(url = "CP.1.1").value' ] + hl7Spec: [ '%{cpField}-1-1' ] + + - name: cp-price-denomination + value: [ '%resource.extension.where(url = "CP.1.2").value' ] + hl7Spec: [ '%{cpField}-1-2' ] + + - name: cp-price-type + value: [ '%resource.extension.where(url = "CP.2").value' ] + hl7Spec: [ '%{cpField}-2' ] + + - name: cp-from-value + value: [ '%resource.extension.where(url = "CP.3").value' ] + hl7Spec: [ '%{cpField}-3' ] + + - name: cp-to-value + value: [ '%resource.extension.where(url = "CP.4").value' ] + hl7Spec: [ '%{cpField}-4' ] + + - name: cp-range-units + resource: '%resource.extension.where(url = "CP.5").value' + schema: classpath:/metadata/hl7_mapping/datatypes/codeableConcept/CWE.yml + constants: + cweField: '%{cpField}-5' + + - name: cp-range-type + value: [ '%resource.extension.where(url = "CP.6").value' ] + hl7Spec: [ '%{cpField}-6' ] diff --git a/prime-router/src/main/resources/metadata/hl7_mapping/resources/Coverage/IN1.yml b/prime-router/src/main/resources/metadata/hl7_mapping/resources/Coverage/IN1.yml index fe437d13fce..b412907a538 100644 --- a/prime-router/src/main/resources/metadata/hl7_mapping/resources/Coverage/IN1.yml +++ b/prime-router/src/main/resources/metadata/hl7_mapping/resources/Coverage/IN1.yml @@ -5,8 +5,32 @@ constants: elements: - - name: aui-extension + - name: in1-authorization-information resource: '%resource.extension.where(url = "IN1.14")' schema: classpath:/metadata/hl7_mapping/datatypes/extensionAUI/AUI.yml constants: - auiField: '%{hl7IN1Field}-14' \ No newline at end of file + auiField: '%{hl7IN1Field}-14' + + - name: in1-policy-deductable + resource: '%resource.extension(%`rsext-composite-price`).where(extension(%`rsext-hl7v2Field`).value = "IN1.37")' + schema: classpath:/metadata/hl7_mapping/datatypes/extensionCP/CP.yml + constants: + cpField: '%{hl7IN1Field}-37' + + - name: in1-policy-limit-amount + resource: '%resource.extension(%`rsext-composite-price`).where(extension(%`rsext-hl7v2Field`).value = "IN1.38")' + schema: classpath:/metadata/hl7_mapping/datatypes/extensionCP/CP.yml + constants: + cpField: '%{hl7IN1Field}-38' + + - name: in1-room-rate-semi-private + resource: '%resource.extension(%`rsext-composite-price`).where(extension(%`rsext-hl7v2Field`).value = "IN1.40")' + schema: classpath:/metadata/hl7_mapping/datatypes/extensionCP/CP.yml + constants: + cpField: '%{hl7IN1Field}-40' + + - name: in1-room-rate-private + resource: '%resource.extension(%`rsext-composite-price`).where(extension(%`rsext-hl7v2Field`).value = "IN1.41")' + schema: classpath:/metadata/hl7_mapping/datatypes/extensionCP/CP.yml + constants: + cpField: '%{hl7IN1Field}-41' diff --git a/prime-router/src/main/resources/metadata/hl7_validation/v251/radxmars/onboarding/constraints.xml b/prime-router/src/main/resources/metadata/hl7_validation/v251/radxmars/onboarding/constraints.xml index 75e5166f1dc..ec78da25b9d 100644 --- a/prime-router/src/main/resources/metadata/hl7_validation/v251/radxmars/onboarding/constraints.xml +++ b/prime-router/src/main/resources/metadata/hl7_validation/v251/radxmars/onboarding/constraints.xml @@ -584,27 +584,12 @@ Text="ISO"/> - - MSH-5.2 (Universal ID) shall contain one of the values in the list: ['2.16.840.1.114222.4.3.15.2','2.16.840.1.114222.4.3.15.1']. - - - - MSH-6.3 (Universal ID Type) shall contain the value 'ISO'. </Assertion> </Constraint> - <Constraint ID="MSH-6.2" Strength="SHALL"> - <Description> MSH-6.2 (Universal ID) shall contain the value '2.16.840.1.114222.4.1.217446'. </Description> - <Assertion> - <PlainText IgnoreCase="false" NotPresentBehavior="PASS" Path="6[1].2[1]" - Text="2.16.840.1.114222.4.1.217446"/> - </Assertion> - </Constraint> <Constraint ID="MSH-7" Strength="SHALL"> <Description> MSH-7.1 (Time) shall match the regular expression '^(\d{14}\.\d{1,4}|\d{14})((\x2D|\x2B)\d{4})$'. </Description> <Assertion> @@ -612,13 +597,6 @@ Regex="^(\d{14}\.\d{1,4}|\d{14})((\x2D|\x2B)\d{4})$"/> </Assertion> </Constraint> - <Constraint ID="MSH-6.1" Strength="SHALL"> - <Description> MSH-6.1 (Namespace ID) shall contain the value 'AIMS.PLATFORM'. </Description> - <Assertion> - <PlainText IgnoreCase="false" NotPresentBehavior="PASS" Path="6[1].1[1]" - Text="AIMS.PLATFORM"/> - </Assertion> - </Constraint> <Constraint ID="MSH-1" Strength="SHALL"> <Description> MSH-1 (Field Separator) shall contain the value '|'. </Description> <Assertion> @@ -679,14 +657,6 @@ <PlainText IgnoreCase="false" NotPresentBehavior="PASS" Path="15[1]" Text="NE"/> </Assertion> </Constraint> - <Constraint ID="MSH-5.1" Strength="SHALL"> - <Description> MSH-5.1 (Namespace ID) shall contain one of the values in the list: ['AIMS.INTEGRATION.STG','AIMS.INTEGRATION.PRD']. </Description> - <Assertion> - <StringList CSV="AIMS.INTEGRATION.STG,AIMS.INTEGRATION.PRD" IgnoreCase="false" - NotPresentBehavior="PASS" - Path="5[1].1[1]"/> - </Assertion> - </Constraint> </ByID> <ByID ID="OBX_NIH_2-8-1"> <Constraint ID="OBX-19" Strength="SHALL"> diff --git a/prime-router/src/main/resources/metadata/hl7_validation/v251/radxmars/production/constraints.xml b/prime-router/src/main/resources/metadata/hl7_validation/v251/radxmars/production/constraints.xml index 6be6551b1dc..f96e980e3f9 100644 --- a/prime-router/src/main/resources/metadata/hl7_validation/v251/radxmars/production/constraints.xml +++ b/prime-router/src/main/resources/metadata/hl7_validation/v251/radxmars/production/constraints.xml @@ -684,27 +684,12 @@ Text="ISO"/> </Assertion> </Constraint> - <Constraint ID="MSH-5.2" Strength="SHALL"> - <Description> MSH-5.2 (Universal ID) shall contain one of the values in the list: ['2.16.840.1.114222.4.3.15.2','2.16.840.1.114222.4.3.15.1']. </Description> - <Assertion> - <StringList CSV="2.16.840.1.114222.4.3.15.2,2.16.840.1.114222.4.3.15.1" IgnoreCase="false" - NotPresentBehavior="PASS" - Path="5[1].2[1]"/> - </Assertion> - </Constraint> <Constraint ID="MSH-6.3" Strength="SHALL"> <Description> MSH-6.3 (Universal ID Type) shall contain the value 'ISO'. </Description> <Assertion> <PlainText IgnoreCase="false" NotPresentBehavior="PASS" Path="6[1].3[1]" Text="ISO"/> </Assertion> </Constraint> - <Constraint ID="MSH-6.2" Strength="SHALL"> - <Description> MSH-6.2 (Universal ID) shall contain the value '2.16.840.1.114222.4.1.217446'. </Description> - <Assertion> - <PlainText IgnoreCase="false" NotPresentBehavior="PASS" Path="6[1].2[1]" - Text="2.16.840.1.114222.4.1.217446"/> - </Assertion> - </Constraint> <Constraint ID="MSH-7" Strength="SHALL"> <Description> MSH-7.1 (Time) shall match the regular expression '^(\d{14}\.\d{1,4}|\d{14})((\x2D|\x2B)\d{4})$'. </Description> <Assertion> @@ -712,13 +697,6 @@ Regex="^(\d{14}\.\d{1,4}|\d{14})((\x2D|\x2B)\d{4})$"/> </Assertion> </Constraint> - <Constraint ID="MSH-6.1" Strength="SHALL"> - <Description> MSH-6.1 (Namespace ID) shall contain the value 'AIMS.PLATFORM'. </Description> - <Assertion> - <PlainText IgnoreCase="false" NotPresentBehavior="PASS" Path="6[1].1[1]" - Text="AIMS.PLATFORM"/> - </Assertion> - </Constraint> <Constraint ID="MSH-1" Strength="SHALL"> <Description> MSH-1 (Field Separator) shall contain the value '|'. </Description> <Assertion> @@ -779,14 +757,6 @@ <PlainText IgnoreCase="false" NotPresentBehavior="PASS" Path="15[1]" Text="NE"/> </Assertion> </Constraint> - <Constraint ID="MSH-5.1" Strength="SHALL"> - <Description> MSH-5.1 (Namespace ID) shall contain one of the values in the list: ['AIMS.INTEGRATION.STG','AIMS.INTEGRATION.PRD']. </Description> - <Assertion> - <StringList CSV="AIMS.INTEGRATION.STG,AIMS.INTEGRATION.PRD" IgnoreCase="false" - NotPresentBehavior="PASS" - Path="5[1].1[1]"/> - </Assertion> - </Constraint> </ByID> <ByID ID="OBR_NIH"> <Constraint ID="OBR-22.1" Strength="SHOULD"> diff --git a/prime-router/src/test/kotlin/common/UniversalPipelineTestUtils.kt b/prime-router/src/test/kotlin/common/UniversalPipelineTestUtils.kt index 9a3aaf1c8b8..96ac1340b5d 100644 --- a/prime-router/src/test/kotlin/common/UniversalPipelineTestUtils.kt +++ b/prime-router/src/test/kotlin/common/UniversalPipelineTestUtils.kt @@ -40,16 +40,16 @@ import java.time.OffsetDateTime @Suppress("ktlint:standard:max-line-length") const val validFHIRRecord1 = - """{"resourceType":"Bundle","id":"1667861767830636000.7db38d22-b713-49fc-abfa-2edba9c12347","meta":{"lastUpdated":"2022-11-07T22:56:07.832+00:00"},"identifier":{"value":"1234d1d1-95fe-462c-8ac6-46728dba581c"},"type":"message","timestamp":"2021-08-03T13:15:11.015+00:00","entry":[{"fullUrl":"Observation/d683b42a-bf50-45e8-9fce-6c0531994f09","resource":{"resourceType":"Observation","id":"d683b42a-bf50-45e8-9fce-6c0531994f09","status":"final","code":{"coding":[{"system":"http://loinc.org","code":"80382-5"}],"text":"Flu A"},"subject":{"reference":"Patient/9473889b-b2b9-45ac-a8d8-191f27132912"},"performer":[{"reference":"Organization/1a0139b9-fc23-450b-9b6c-cd081e5cea9d"}],"valueCodeableConcept":{"coding":[{"system":"http://snomed.info/sct","code":"260373001","display":"Detected"}]},"interpretation":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0078","code":"A","display":"Abnormal"}]}],"method":{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/testkit-name-id","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/equipment-uid","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}}],"coding":[{"display":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B*"}]},"specimen":{"reference":"Specimen/52a582e4-d389-42d0-b738-bee51cf5244d"},"device":{"reference":"Device/78dc4d98-2958-43a3-a445-76ceef8c0698"}}}]}""" + """{"resourceType":"Bundle","id":"1667861767830636000.7db38d22-b713-49fc-abfa-2edba9c12347","meta":{"lastUpdated":"2022-11-07T22:56:07.832+00:00"},"identifier":{"value":"1234d1d1-95fe-462c-8ac6-46728dba581c"},"type":"message","timestamp":"2021-08-03T13:15:11.015+00:00","entry":[{"fullUrl":"MessageHeader/0993dd0b-6ce5-3caf-a177-0b81cc780c18","resource":{"resourceType":"MessageHeader","id":"0993dd0b-6ce5-3caf-a177-0b81cc780c18","extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/encoding-characters","valueString":"^~\\&#"},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/character-set","valueString":"UNICODE UTF-8"},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/msh-message-header","extension":[{"url":"MSH.7","valueString":"20230501102531-0400"}]}],"eventCoding":{"system":"http://terminology.hl7.org/CodeSystem/v2-0003","code":"R01","display":"ORU^R01^ORU_R01"},"sender":{"reference":"Organization/1710886092467181000.213628f7-9569-4400-a95d-621c3bfbf121"}}},{"fullUrl":"Observation/d683b42a-bf50-45e8-9fce-6c0531994f09","resource":{"resourceType":"Observation","id":"d683b42a-bf50-45e8-9fce-6c0531994f09","status":"final","code":{"coding":[{"system":"http://loinc.org","code":"80382-5"}],"text":"Flu A"},"subject":{"reference":"Patient/9473889b-b2b9-45ac-a8d8-191f27132912"},"performer":[{"reference":"Organization/1a0139b9-fc23-450b-9b6c-cd081e5cea9d"}],"valueCodeableConcept":{"coding":[{"system":"http://snomed.info/sct","code":"260373001","display":"Detected"}]},"interpretation":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0078","code":"A","display":"Abnormal"}]}],"method":{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/testkit-name-id","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/equipment-uid","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}}],"coding":[{"display":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B*"}]},"specimen":{"reference":"Specimen/52a582e4-d389-42d0-b738-bee51cf5244d"},"device":{"reference":"Device/78dc4d98-2958-43a3-a445-76ceef8c0698"}}}]}""" const val validFHIRRecord1Identifier = "1234d1d1-95fe-462c-8ac6-46728dba581c" @Suppress("ktlint:standard:max-line-length") const val conditionCodedValidFHIRRecord1 = - """{"resourceType":"Bundle","id":"1667861767830636000.7db38d22-b713-49fc-abfa-2edba9c12347","meta":{"lastUpdated":"2022-11-07T22:56:07.832+00:00"},"identifier":{"value":"1234d1d1-95fe-462c-8ac6-46728dba581c"},"type":"message","timestamp":"2021-08-03T13:15:11.015+00:00","entry":[{"fullUrl":"Observation/d683b42a-bf50-45e8-9fce-6c0531994f09","resource":{"resourceType":"Observation","id":"d683b42a-bf50-45e8-9fce-6c0531994f09","status":"final","code":{"coding":[{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code","valueCoding":{"system":"SNOMEDCT","code":"6142004","display":"Influenza (disorder)"}}],"system":"http://loinc.org","code":"80382-5"}],"text":"Flu A"},"subject":{"reference":"Patient/9473889b-b2b9-45ac-a8d8-191f27132912"},"performer":[{"reference":"Organization/1a0139b9-fc23-450b-9b6c-cd081e5cea9d"}],"valueCodeableConcept":{"coding":[{"system":"http://snomed.info/sct","code":"260373001","display":"Detected"}]},"interpretation":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0078","code":"A","display":"Abnormal"}]}],"method":{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/testkit-name-id","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/equipment-uid","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}}],"coding":[{"display":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B*"}]},"specimen":{"reference":"Specimen/52a582e4-d389-42d0-b738-bee51cf5244d"},"device":{"reference":"Device/78dc4d98-2958-43a3-a445-76ceef8c0698"}}}]}""" + """{"resourceType":"Bundle","id":"1667861767830636000.7db38d22-b713-49fc-abfa-2edba9c12347","meta":{"lastUpdated":"2022-11-07T22:56:07.832+00:00"},"identifier":{"value":"1234d1d1-95fe-462c-8ac6-46728dba581c"},"type":"message","timestamp":"2021-08-03T13:15:11.015+00:00","entry":[{"fullUrl":"MessageHeader/0993dd0b-6ce5-3caf-a177-0b81cc780c18","resource":{"resourceType":"MessageHeader","id":"0993dd0b-6ce5-3caf-a177-0b81cc780c18","extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/encoding-characters","valueString":"^~\\&#"},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/character-set","valueString":"UNICODE UTF-8"},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/msh-message-header","extension":[{"url":"MSH.7","valueString":"20230501102531-0400"}]}],"eventCoding":{"system":"http://terminology.hl7.org/CodeSystem/v2-0003","code":"R01","display":"ORU^R01^ORU_R01"},"sender":{"reference":"Organization/1710886092467181000.213628f7-9569-4400-a95d-621c3bfbf121"}}},{"fullUrl":"Observation/d683b42a-bf50-45e8-9fce-6c0531994f09","resource":{"resourceType":"Observation","id":"d683b42a-bf50-45e8-9fce-6c0531994f09","status":"final","code":{"coding":[{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code","valueCoding":{"system":"SNOMEDCT","code":"6142004","display":"Influenza (disorder)"}}],"system":"http://loinc.org","code":"80382-5"}],"text":"Flu A"},"subject":{"reference":"Patient/9473889b-b2b9-45ac-a8d8-191f27132912"},"performer":[{"reference":"Organization/1a0139b9-fc23-450b-9b6c-cd081e5cea9d"}],"valueCodeableConcept":{"coding":[{"system":"http://snomed.info/sct","code":"260373001","display":"Detected"}]},"interpretation":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0078","code":"A","display":"Abnormal"}]}],"method":{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/testkit-name-id","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/equipment-uid","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}}],"coding":[{"display":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B*"}]},"specimen":{"reference":"Specimen/52a582e4-d389-42d0-b738-bee51cf5244d"},"device":{"reference":"Device/78dc4d98-2958-43a3-a445-76ceef8c0698"}}}]}""" @Suppress("ktlint:standard:max-line-length") const val validFHIRRecord2 = - """{"resourceType":"Bundle","id":"1667861767830636000.7db38d22-b713-49fc-abfa-2edba9c09876","meta":{"lastUpdated":"2022-11-07T22:56:07.832+00:00"},"identifier":{"value":"1234d1d1-95fe-462c-8ac6-46728dbau8cd"},"type":"message","timestamp":"2021-08-03T13:15:11.015+00:00","entry":[{"fullUrl":"Observation/d683b42a-bf50-45e8-9fce-6c0531994f09","resource":{"resourceType":"Observation","id":"d683b42a-bf50-45e8-9fce-6c0531994f09","status":"final","code":{"coding":[{"system":"http://loinc.org","code":"41458-1"}],"text":"SARS "},"subject":{"reference":"Patient/9473889b-b2b9-45ac-a8d8-191f27132912"},"performer":[{"reference":"Organization/1a0139b9-fc23-450b-9b6c-cd081e5cea9d"}],"valueCodeableConcept":{"coding":[{"system":"http://snomed.info/sct","code":"260373001","display":"Detected"}]},"interpretation":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0078","code":"A","display":"Abnormal"}]}],"method":{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/testkit-name-id","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/equipment-uid","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}}],"coding":[{"display":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B*"}]},"specimen":{"reference":"Specimen/52a582e4-d389-42d0-b738-bee51cf5244d"},"device":{"reference":"Device/78dc4d98-2958-43a3-a445-76ceef8c0698"}}}]}""" + """{"resourceType":"Bundle","id":"1667861767830636000.7db38d22-b713-49fc-abfa-2edba9c09876","meta":{"lastUpdated":"2022-11-07T22:56:07.832+00:00"},"identifier":{"value":"1234d1d1-95fe-462c-8ac6-46728dbau8cd"},"type":"message","timestamp":"2021-08-03T13:15:11.015+00:00","entry":[{"fullUrl":"MessageHeader/0993dd0b-6ce5-3caf-a177-0b81cc780c18","resource":{"resourceType":"MessageHeader","id":"0993dd0b-6ce5-3caf-a177-0b81cc780c18","extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/encoding-characters","valueString":"^~\\&#"},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/character-set","valueString":"UNICODE UTF-8"},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/msh-message-header","extension":[{"url":"MSH.7","valueString":"20230501102531-0400"}]}],"eventCoding":{"system":"http://terminology.hl7.org/CodeSystem/v2-0003","code":"R01","display":"ORU^R01^ORU_R01"},"sender":{"reference":"Organization/1710886092467181000.213628f7-9569-4400-a95d-621c3bfbf121"}}},{"fullUrl":"Observation/d683b42a-bf50-45e8-9fce-6c0531994f09","resource":{"resourceType":"Observation","id":"d683b42a-bf50-45e8-9fce-6c0531994f09","status":"final","code":{"coding":[{"system":"http://loinc.org","code":"41458-1"}],"text":"SARS "},"subject":{"reference":"Patient/9473889b-b2b9-45ac-a8d8-191f27132912"},"performer":[{"reference":"Organization/1a0139b9-fc23-450b-9b6c-cd081e5cea9d"}],"valueCodeableConcept":{"coding":[{"system":"http://snomed.info/sct","code":"260373001","display":"Detected"}]},"interpretation":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0078","code":"A","display":"Abnormal"}]}],"method":{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/testkit-name-id","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/equipment-uid","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}}],"coding":[{"display":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B*"}]},"specimen":{"reference":"Specimen/52a582e4-d389-42d0-b738-bee51cf5244d"},"device":{"reference":"Device/78dc4d98-2958-43a3-a445-76ceef8c0698"}}}]}""" const val invalidEmptyFHIRRecord = "{}" const val invalidMalformedFHIRRecord = """{"resourceType":"Bund}""" @@ -441,7 +441,7 @@ object UniversalPipelineTestUtils { fileName: String = "mr_fhir_face.fhir", ): Report { val blobUrl = BlobAccess.uploadBlob( - "${TaskAction.receive.literal}/$fileName", + "${previousAction.literal}/$fileName", reportContents.toByteArray(), getBlobContainerMetadata(azuriteContainer) ) diff --git a/prime-router/src/test/kotlin/fhirengine/azure/FHIRConverterIntegrationTests.kt b/prime-router/src/test/kotlin/fhirengine/azure/FHIRConverterIntegrationTests.kt index e95fb3d5806..b534f994a12 100644 --- a/prime-router/src/test/kotlin/fhirengine/azure/FHIRConverterIntegrationTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/azure/FHIRConverterIntegrationTests.kt @@ -508,7 +508,8 @@ class FHIRConverterIntegrationTests { orderingFacilityState = listOf("FL"), performerState = emptyList(), eventType = "ORU^R01^ORU_R01" - ) + ), + ReportStreamEventProperties.ENRICHMENTS to "" ) ) } @@ -663,7 +664,8 @@ class FHIRConverterIntegrationTests { orderingFacilityState = listOf("FL"), performerState = emptyList(), eventType = "ORU^R01^ORU_R01" - ) + ), + ReportStreamEventProperties.ENRICHMENTS to "" ) ) } @@ -798,7 +800,7 @@ class FHIRConverterIntegrationTests { ) assertThat(azureEventService.reportStreamEvents[ReportStreamEventName.ITEM_ACCEPTED]!!).hasSize(2) - val event = azureEventService + var event = azureEventService .reportStreamEvents[ReportStreamEventName.ITEM_ACCEPTED]!!.last() as ReportStreamItemEvent assertThat(event.reportEventData).isEqualToIgnoringGivenProperties( ReportEventData( @@ -835,8 +837,9 @@ class FHIRConverterIntegrationTests { patientState = emptyList(), orderingFacilityState = emptyList(), performerState = emptyList(), - eventType = "" - ) + eventType = "ORU^R01^ORU_R01" + ), + ReportStreamEventProperties.ENRICHMENTS to "" ) ) } diff --git a/prime-router/src/test/kotlin/fhirengine/azure/FHIRReceiverFilterIntegrationTests.kt b/prime-router/src/test/kotlin/fhirengine/azure/FHIRReceiverFilterIntegrationTests.kt index 4acfecbf826..d8bae65c3eb 100644 --- a/prime-router/src/test/kotlin/fhirengine/azure/FHIRReceiverFilterIntegrationTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/azure/FHIRReceiverFilterIntegrationTests.kt @@ -920,7 +920,7 @@ class FHIRReceiverFilterIntegrationTests : Logging { ReportStreamEventProperties.FILTER_TYPE to ReportStreamFilterType.QUALITY_FILTER, ReportStreamEventProperties.BUNDLE_DIGEST to BundleDigestLabResult( observationSummaries = AzureEventUtils.getObservationSummaries(bundle), - eventType = "", + eventType = "ORU^R01^ORU_R01", patientState = emptyList(), performerState = emptyList(), orderingFacilityState = emptyList() diff --git a/prime-router/src/test/kotlin/fhirengine/azure/FHIRTranslatorIntegrationTests.kt b/prime-router/src/test/kotlin/fhirengine/azure/FHIRTranslatorIntegrationTests.kt index 99eada67991..d8a9adeb1dd 100644 --- a/prime-router/src/test/kotlin/fhirengine/azure/FHIRTranslatorIntegrationTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/azure/FHIRTranslatorIntegrationTests.kt @@ -640,7 +640,7 @@ class FHIRTranslatorIntegrationTests : Logging { } @Test - fun `successfully translate HL7 for FHIR receiver when isSendOriginal is true`() { + fun `successfully translate HL7 for FHIR receiver when isSendOriginal is true from receive step`() { // set up val receiverSetupData = listOf( UniversalPipelineTestUtils.ReceiverSetupData( @@ -720,7 +720,7 @@ class FHIRTranslatorIntegrationTests : Logging { } @Test - fun `successfully translate for FHIR receiver when isSendOriginal is true`() { + fun `successfully translate for FHIR receiver when isSendOriginal is true from receive step`() { // set up val receiverSetupData = listOf( UniversalPipelineTestUtils.ReceiverSetupData( @@ -798,4 +798,166 @@ class FHIRTranslatorIntegrationTests : Logging { assertThat(translatedValue).isEqualTo(reportContents.toByteArray()) } } + + @Test + fun `successfully translate HL7 for FHIR receiver when isSendOriginal is true from convert step`() { + // set up + val receiverSetupData = listOf( + UniversalPipelineTestUtils.ReceiverSetupData( + "x", + jurisdictionalFilter = listOf("true"), + qualityFilter = listOf("true"), + routingFilter = listOf("true"), + conditionFilter = listOf("true"), + format = MimeFormat.FHIR + ) + ) + val receivers = UniversalPipelineTestUtils.createReceivers(receiverSetupData) + val org = UniversalPipelineTestUtils.createOrganizationWithReceivers(receivers) + val translator = createFHIRTranslator(azureEventService, org) + val reportContents = File(HL7_WITH_BIRTH_TIME).readText() + val convertReport = UniversalPipelineTestUtils.createReport( + reportContents, + TaskAction.convert, + Event.EventAction.CONVERT, + azuriteContainer, + TaskAction.convert, + fileName = "originalhl7.hl7" + ) + val queueMessage = generateQueueMessage( + convertReport, + reportContents, + UniversalPipelineTestUtils.hl7SenderWithSendOriginal, + "phd.x" + ) + val fhirFunctions = UniversalPipelineTestUtils.createFHIRFunctionsInstance() + + // execute + fhirFunctions.process(queueMessage, 1, translator, ActionHistory(TaskAction.translate)) + + // check that send queue was updated + verify(exactly = 1) { + QueueAccess.sendMessage(QueueMessage.elrSendQueueName, any()) + } + + // check action table + UniversalPipelineTestUtils.checkActionTable(listOf(TaskAction.convert, TaskAction.translate)) + + // verify task and report_file tables were updated correctly in the Translate function (new task and new + // record file created) + ReportStreamTestDatabaseContainer.testDatabaseAccess.transact { txn -> + val report = fetchChildReports(convertReport, txn, 1).single() + assertThat(report.nextAction).isEqualTo(TaskAction.send) + assertThat(report.receivingOrg).isEqualTo("phd") + assertThat(report.receivingOrgSvc).isEqualTo("x") + assertThat(report.schemaName).isEqualTo("None") + assertThat(report.schemaTopic).isEqualTo(Topic.ELR_ELIMS) + assertThat(report.bodyFormat).isEqualTo("HL7") + + val batchTask = DSL.using(txn).select(Task.TASK.asterisk()).from(Task.TASK) + .where(Task.TASK.NEXT_ACTION.eq(TaskAction.batch)) + .fetchOneInto(Task.TASK) + // verify batch queue task does not exist + assertThat(batchTask).isNull() + + val sendTask = DSL.using(txn).select(Task.TASK.asterisk()).from(Task.TASK) + .where(Task.TASK.NEXT_ACTION.eq(TaskAction.send)) + .fetchOneInto(Task.TASK) + // verify send queue task exists + assertThat(sendTask).isNotNull() + assertThat(sendTask!!.reportId).isEqualTo(report.reportId) + + // verify message format is HL7 and is for the expected receiver + assertThat(sendTask.receiverName).isEqualTo("phd.x") + assertThat(sendTask.bodyFormat).isEqualTo("HL7") + + // verify message matches the original HL7 input + val translatedValue = BlobAccess.downloadBlobAsByteArray( + report.bodyUrl, + UniversalPipelineTestUtils.getBlobContainerMetadata(azuriteContainer) + ) + assertThat(translatedValue).isEqualTo(reportContents.toByteArray()) + } + } + + @Test + fun `successfully translate for FHIR receiver when isSendOriginal is true from convert step`() { + // set up + val receiverSetupData = listOf( + UniversalPipelineTestUtils.ReceiverSetupData( + "x", + jurisdictionalFilter = listOf("true"), + qualityFilter = listOf("true"), + routingFilter = listOf("true"), + conditionFilter = listOf("true"), + format = MimeFormat.FHIR + ) + ) + val receivers = UniversalPipelineTestUtils.createReceivers(receiverSetupData) + val org = UniversalPipelineTestUtils.createOrganizationWithReceivers(receivers) + val translator = createFHIRTranslator(azureEventService, org) + val reportContents = File(MULTIPLE_TARGETS_FHIR_PATH).readText() + val convertReport = UniversalPipelineTestUtils.createReport( + reportContents, + TaskAction.convert, + Event.EventAction.CONVERT, + azuriteContainer, + TaskAction.convert + ) + + val queueMessage = generateQueueMessage( + convertReport, + reportContents, + UniversalPipelineTestUtils.fhirSenderWithSendOriginal, + "phd.x" + ) + val fhirFunctions = UniversalPipelineTestUtils.createFHIRFunctionsInstance() + + // execute + fhirFunctions.process(queueMessage, 1, translator, ActionHistory(TaskAction.translate)) + + // check that send queue was updated + verify(exactly = 1) { + QueueAccess.sendMessage(QueueMessage.elrSendQueueName, any()) + } + + // check action table + UniversalPipelineTestUtils.checkActionTable(listOf(TaskAction.convert, TaskAction.translate)) + + // verify task and report_file tables were updated correctly in the Translate function (new task and new + // record file created) + ReportStreamTestDatabaseContainer.testDatabaseAccess.transact { txn -> + val report = fetchChildReports(convertReport, txn, 1).single() + assertThat(report.nextAction).isEqualTo(TaskAction.send) + assertThat(report.receivingOrg).isEqualTo("phd") + assertThat(report.receivingOrgSvc).isEqualTo("x") + assertThat(report.schemaName).isEqualTo("None") + assertThat(report.schemaTopic).isEqualTo(Topic.ELR_ELIMS) + assertThat(report.bodyFormat).isEqualTo("FHIR") + + val batchTask = DSL.using(txn).select(Task.TASK.asterisk()).from(Task.TASK) + .where(Task.TASK.NEXT_ACTION.eq(TaskAction.batch)) + .fetchOneInto(Task.TASK) + // verify batch queue task does not exist + assertThat(batchTask).isNull() + + val sendTask = DSL.using(txn).select(Task.TASK.asterisk()).from(Task.TASK) + .where(Task.TASK.NEXT_ACTION.eq(TaskAction.send)) + .fetchOneInto(Task.TASK) + // verify send queue task exists + assertThat(sendTask).isNotNull() + assertThat(sendTask!!.reportId).isEqualTo(report.reportId) + + // verify message format is FHIR and is for the expected receiver + assertThat(sendTask.receiverName).isEqualTo("phd.x") + assertThat(sendTask.bodyFormat).isEqualTo("FHIR") + + // verify message matches the original FHIR input + val translatedValue = BlobAccess.downloadBlobAsByteArray( + report.bodyUrl, + UniversalPipelineTestUtils.getBlobContainerMetadata(azuriteContainer) + ) + assertThat(translatedValue).isEqualTo(reportContents.toByteArray()) + } + } } \ 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 33731c0e5af..e23ef1b6e35 100644 --- a/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt @@ -344,6 +344,106 @@ class FhirConverterTests { @Test fun `test condition code stamping`() { + @Suppress("ktlint:standard:max-line-length") + val fhirRecord = + """{"resourceType":"Bundle","id":"1667861767830636000.7db38d22-b713-49fc-abfa-2edba9c12347","meta":{"lastUpdated":"2022-11-07T22:56:07.832+00:00"},"identifier":{"value":"1234d1d1-95fe-462c-8ac6-46728dba581c"},"type":"message","timestamp":"2021-08-03T13:15:11.015+00:00","entry":[{"fullUrl" : "MessageHeader/0993dd0b-6ce5-3caf-a177-0b81cc780c18","resource" : {"resourceType" : "MessageHeader","id" : "0993dd0b-6ce5-3caf-a177-0b81cc780c18","extension" : [ {"url" : "https://reportstream.cdc.gov/fhir/StructureDefinition/encoding-characters","valueString" : "^~\\&#"}, {"url" : "https://reportstream.cdc.gov/fhir/StructureDefinition/character-set","valueString" : "UNICODE UTF-8"}, {"url" : "https://reportstream.cdc.gov/fhir/StructureDefinition/msh-message-header","extension" : [ {"url" : "MSH.7","valueString" : "20230501102531-0400"} ]} ],"eventCoding" : {"system" : "http://terminology.hl7.org/CodeSystem/v2-0003","code" : "R01","display" : "ORU^R01^ORU_R01"},"sender" : {"reference" : "Organization/1710886092467181000.213628f7-9569-4400-a95d-621c3bfbf121"}}},{"fullUrl":"Observation/d683b42a-bf50-45e8-9fce-6c0531994f09","resource":{"resourceType":"Observation","id":"d683b42a-bf50-45e8-9fce-6c0531994f09","status":"final","code":{"coding":[{"system":"http://loinc.org","code":"80382-5"}],"text":"Flu A"},"subject":{"reference":"Patient/9473889b-b2b9-45ac-a8d8-191f27132912"},"performer":[{"reference":"Organization/1a0139b9-fc23-450b-9b6c-cd081e5cea9d"}],"valueCodeableConcept":{"coding":[{"system":"http://snomed.info/sct","code":"260373001","display":"Detected"}]},"interpretation":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0078","code":"A","display":"Abnormal"}]}],"method":{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/testkit-name-id","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/equipment-uid","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}}],"coding":[{"display":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B*"}]},"specimen":{"reference":"Specimen/52a582e4-d389-42d0-b738-bee51cf5244d"},"device":{"reference":"Device/78dc4d98-2958-43a3-a445-76ceef8c0698"}}}]}""" + + val conditionCodeExtensionURL = "https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code" + mockkObject(BlobAccess) + mockkObject(Report) + metadata.lookupTableStore += mapOf( + "observation-mapping" to LookupTable( + "observation-mapping", + listOf( + listOf( + ObservationMappingConstants.TEST_CODE_KEY, + ObservationMappingConstants.CONDITION_CODE_KEY, + ObservationMappingConstants.CONDITION_CODE_SYSTEM_KEY, + ObservationMappingConstants.CONDITION_NAME_KEY + ), + listOf( + "80382-5", + "6142004", + "SNOMEDCT", + "Influenza (disorder)" + ), + listOf( + "260373001", + "Some Condition Code", + "Condition Code System", + "Condition Name" + ) + ) + ) + ) + + // set up + val actionHistory = mockk<ActionHistory>() + val actionLogger = mockk<ActionLogger>() + val transformer = mockk<FhirTransformer>() + + val engine = spyk(makeFhirEngine(metadata, settings, TaskAction.process) as FHIRConverter) + val message = spyk( + FhirConvertQueueMessage( + UUID.randomUUID(), + BLOB_FHIR_URL, + "test", + BLOB_SUB_FOLDER_NAME, + Topic.FULL_ELR, + SCHEMA_NAME + ) + ) + + val bodyFormat = MimeFormat.FHIR + val bodyUrl = "https://anyblob.com" + + every { actionLogger.hasErrors() } returns false + every { actionLogger.getItemLogger(any(), any()) } returns actionLogger + every { actionLogger.warn(any<List<ActionLogDetail>>()) } just runs + every { actionLogger.setReportId(any()) } returns actionLogger + every { BlobAccess.downloadBlob(any(), any()) } returns (fhirRecord) + every { Report.getFormatFromBlobURL(message.blobURL) } returns MimeFormat.FHIR + every { BlobAccess.Companion.uploadBlob(any(), any()) } returns "test" + every { accessSpy.insertTask(any(), bodyFormat.toString(), bodyUrl, any()) }.returns(Unit) + every { actionHistory.trackCreatedReport(any(), any(), blobInfo = any()) }.returns(Unit) + every { actionHistory.trackExistingInputReport(any()) }.returns(Unit) + val action = Action() + action.actionName = TaskAction.convert + every { actionHistory.action } returns action + every { engine.getTransformerFromSchema(SCHEMA_NAME) }.returns(transformer) + every { transformer.process(any()) } returnsArgument (0) + + // act + accessSpy.transact { txn -> + engine.run(message, actionLogger, actionHistory, txn) + } + + val bundle = FhirContext.forR4().newJsonParser().parseResource(Bundle::class.java, fhirRecord) + bundle.entry.filter { it.resource is Observation }.forEach { + val observation = (it.resource as Observation) + observation.code.coding[0].addExtension( + conditionCodeExtensionURL, + Coding("SNOMEDCT", "6142004", "Influenza (disorder)") + ) + observation.valueCodeableConcept.coding[0].addExtension( + conditionCodeExtensionURL, + Coding("Condition Code System", "Some Condition Code", "Condition Name") + ) + } + + // assert + verify(exactly = 1) { + // TODO clean up assertions + // engine.getContentFromFHIR(any(), any()) + actionHistory.trackExistingInputReport(any()) + transformer.process(any()) + actionHistory.trackCreatedReport(any(), any(), blobInfo = any()) + BlobAccess.Companion.uploadBlob(any(), FhirTranscoder.encode(bundle).toByteArray(), any()) + } + } + + @Test + fun `test condition code stamping without message header`() { @Suppress("ktlint:standard:max-line-length") val fhirRecord = """{"resourceType":"Bundle","id":"1667861767830636000.7db38d22-b713-49fc-abfa-2edba9c12347","meta":{"lastUpdated":"2022-11-07T22:56:07.832+00:00"},"identifier":{"value":"1234d1d1-95fe-462c-8ac6-46728dba581c"},"type":"message","timestamp":"2021-08-03T13:15:11.015+00:00","entry":[{"fullUrl":"Observation/d683b42a-bf50-45e8-9fce-6c0531994f09","resource":{"resourceType":"Observation","id":"d683b42a-bf50-45e8-9fce-6c0531994f09","status":"final","code":{"coding":[{"system":"http://loinc.org","code":"80382-5"}],"text":"Flu A"},"subject":{"reference":"Patient/9473889b-b2b9-45ac-a8d8-191f27132912"},"performer":[{"reference":"Organization/1a0139b9-fc23-450b-9b6c-cd081e5cea9d"}],"valueCodeableConcept":{"coding":[{"system":"http://snomed.info/sct","code":"260373001","display":"Detected"}]},"interpretation":[{"coding":[{"system":"http://terminology.hl7.org/CodeSystem/v2-0078","code":"A","display":"Abnormal"}]}],"method":{"extension":[{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/testkit-name-id","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}},{"url":"https://reportstream.cdc.gov/fhir/StructureDefinition/equipment-uid","valueCoding":{"code":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B_Becton, Dickinson and Company (BD)"}}],"coding":[{"display":"BD Veritor System for Rapid Detection of SARS-CoV-2 & Flu A+B*"}]},"specimen":{"reference":"Specimen/52a582e4-d389-42d0-b738-bee51cf5244d"},"device":{"reference":"Device/78dc4d98-2958-43a3-a445-76ceef8c0698"}}}]}""" @@ -438,6 +538,8 @@ class FhirConverterTests { actionHistory.trackExistingInputReport(any()) transformer.process(any()) actionHistory.trackCreatedReport(any(), any(), blobInfo = any()) + } + verify(exactly = 0) { BlobAccess.Companion.uploadBlob(any(), FhirTranscoder.encode(bundle).toByteArray(), any()) } } @@ -560,12 +662,11 @@ class FhirConverterTests { @Test fun `should log an error and return no bundles if the message is empty`() { mockkObject(BlobAccess) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") val engine = spyk(makeFhirEngine(metadata, settings, TaskAction.process) as FHIRConverter) val actionLogger = ActionLogger() every { BlobAccess.downloadBlob(any(), any()) } returns "" - val bundles = engine.process( - MimeFormat.FHIR, "", "", Topic.FULL_ELR, actionLogger - ) + val bundles = engine.process(MimeFormat.FHIR, input, actionLogger) assertThat(bundles).isEmpty() assertThat(actionLogger.errors.map { it.detail.message }).contains("Provided raw data is empty.") } @@ -587,9 +688,8 @@ class FhirConverterTests { every { mockMessage.topic } returns Topic.FULL_ELR every { mockMessage.reportId } returns UUID.randomUUID() every { BlobAccess.downloadBlob(any(), any()) } returns simpleHL7 - val bundles = engine.process( - MimeFormat.HL7, "", "", Topic.FULL_ELR, actionLogger - ) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val bundles = engine.process(MimeFormat.HL7, input, actionLogger) assertThat(bundles).isEmpty() assertThat( actionLogger.errors.map { @@ -604,9 +704,8 @@ class FhirConverterTests { val engine = spyk(makeFhirEngine(metadata, settings, TaskAction.process) as FHIRConverter) val actionLogger = ActionLogger() every { BlobAccess.downloadBlob(any(), any()) } returns "test,1,2" - val bundles = engine.process( - MimeFormat.CSV, "", "", Topic.FULL_ELR, actionLogger - ) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val bundles = engine.process(MimeFormat.CSV, input, actionLogger) assertThat(bundles).isEmpty() assertThat(actionLogger.errors.map { it.detail.message }) .contains("Received unsupported report format: CSV") @@ -618,7 +717,8 @@ class FhirConverterTests { val engine = spyk(makeFhirEngine(metadata, settings, TaskAction.process) as FHIRConverter) val actionLogger = ActionLogger() every { BlobAccess.downloadBlob(any(), any()) } returns "{\"id\":}" - val processedItems = engine.process(MimeFormat.FHIR, "", "", Topic.FULL_ELR, actionLogger) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val processedItems = engine.process(MimeFormat.FHIR, input, actionLogger) assertThat(processedItems).hasSize(1) assertThat(processedItems.first().bundle).isNull() assertThat(actionLogger.errors.map { it.detail.message }).contains( @@ -648,9 +748,8 @@ class FhirConverterTests { every { mockMessage.topic } returns Topic.FULL_ELR every { mockMessage.reportId } returns UUID.randomUUID() every { BlobAccess.downloadBlob(any(), any()) } returns "{\"id\":\"1\", \"resourceType\":\"Bundle\"}" - val processedItems = engine.process( - MimeFormat.FHIR, "", "", Topic.FULL_ELR, actionLogger - ) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val processedItems = engine.process(MimeFormat.FHIR, input, actionLogger) assertThat(processedItems).hasSize(1) assertThat(processedItems.first().bundle).isNull() assertThat(actionLogger.errors.map { it.detail.message }).contains( @@ -669,7 +768,8 @@ class FhirConverterTests { every { BlobAccess.downloadBlob(any(), any()) } returns unparseableHL7 - val processedItems = engine.process(MimeFormat.HL7, "", "", Topic.FULL_ELR, actionLogger) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val processedItems = engine.process(MimeFormat.HL7, input, actionLogger) assertThat(processedItems).hasSize(1) assertThat(processedItems.first().bundle).isNull() assertThat( @@ -704,7 +804,8 @@ class FhirConverterTests { every { BlobAccess.downloadBlob(any(), any()) } returns simpleHL7 - val processedItems = engine.process(MimeFormat.HL7, "", "", Topic.FULL_ELR, actionLogger) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val processedItems = engine.process(MimeFormat.HL7, input, actionLogger) assertThat(processedItems).hasSize(1) assertThat(processedItems.first().bundle).isNull() @Suppress("ktlint:standard:max-line-length") @@ -733,7 +834,8 @@ class FhirConverterTests { every { BlobAccess.downloadBlob(any(), any()) } returns simpleHL7 - val processedItems = engine.process(MimeFormat.HL7, "", "", Topic.FULL_ELR, actionLogger) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val processedItems = engine.process(MimeFormat.HL7, input, actionLogger) assertThat(processedItems).hasSize(1) assertThat(processedItems.first().bundle).isNull() assertThat( @@ -759,14 +861,15 @@ class FhirConverterTests { } returns """{\"id\":} {"id":"1", "resourceType":"Bundle"} """.trimMargin() - val processedItems = engine.process(MimeFormat.FHIR, "", "", Topic.FULL_ELR, actionLogger) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val processedItems = engine.process(MimeFormat.FHIR, input, actionLogger) assertThat(processedItems).hasSize(2) assertThat(actionLogger.errors.map { it.detail.message }).contains( @Suppress("ktlint:standard:max-line-length") "Item 1 in the report was not parseable. Reason: exception while parsing FHIR: HAPI-1861: Failed to parse JSON encoded FHIR content: Unexpected character ('\\' (code 92)): was expecting double-quote to start field name\n at [line: 1, column: 2]" ) - val bundles2 = engine.process(MimeFormat.FHIR, "", "", Topic.FULL_ELR, actionLogger, false) + val bundles2 = engine.process(MimeFormat.FHIR, input, actionLogger, false) assertThat(bundles2).hasSize(0) assertThat(actionLogger.errors.map { it.detail.message }).contains( @Suppress("ktlint:standard:max-line-length") @@ -786,7 +889,8 @@ class FhirConverterTests { every { BlobAccess.downloadBlob(any(), any()) } returns simpleHL7 - val bundles = engine.process(MimeFormat.HL7, "", "", Topic.FULL_ELR, actionLogger) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val bundles = engine.process(MimeFormat.HL7, input, actionLogger) assertThat(bundles).hasSize(1) assertThat(actionLogger.errors).isEmpty() } @@ -806,7 +910,8 @@ class FhirConverterTests { every { BlobAccess.downloadBlob(any(), any()) } returns simpleHL7 + "\n" + simpleHL7 + "\n" + simpleHL7 - val bundles = engine.process(MimeFormat.HL7, "", "", Topic.FULL_ELR, actionLogger) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val bundles = engine.process(MimeFormat.HL7, input, actionLogger) assertThat(bundles).hasSize(3) assertThat(actionLogger.errors).isEmpty() @@ -837,7 +942,8 @@ class FhirConverterTests { every { BlobAccess.downloadBlob(any(), any()) } returns simpleHL7 - val bundles = engine.process(MimeFormat.HL7, "", "", Topic.FULL_ELR, actionLogger) + val input = FHIRConverter.FHIRConvertInput(UUID.randomUUID(), Topic.FULL_ELR, "", "", "", "") + val bundles = engine.process(MimeFormat.HL7, input, actionLogger) assertThat(bundles).hasSize(1) assertThat(actionLogger.errors).isEmpty() } diff --git a/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt b/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt index a9d4a282cf6..5abc630dc42 100644 --- a/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt @@ -31,6 +31,7 @@ import gov.cdc.prime.router.azure.ConditionStamper.Companion.conditionCodeExtens import gov.cdc.prime.router.azure.DatabaseAccess import gov.cdc.prime.router.azure.LookupTableConditionMapper import gov.cdc.prime.router.azure.QueueAccess +import gov.cdc.prime.router.fhirengine.engine.RSMessageType import gov.cdc.prime.router.fhirengine.translation.hl7.utils.CustomContext import gov.cdc.prime.router.fhirengine.translation.hl7.utils.FhirPathUtils import gov.cdc.prime.router.metadata.LookupTable @@ -45,6 +46,7 @@ import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.DiagnosticReport import org.hl7.fhir.r4.model.Endpoint import org.hl7.fhir.r4.model.Extension +import org.hl7.fhir.r4.model.MessageHeader import org.hl7.fhir.r4.model.Observation import org.hl7.fhir.r4.model.Patient import org.hl7.fhir.r4.model.PractitionerRole @@ -195,6 +197,91 @@ class FHIRBundleHelpersTests { assertThat(diagnosticReport.getResourceProperties()).isNotEmpty() } + @Test + fun `Test if ELR when bundle is empty`() { + val fhirBundle = Bundle() + assertThat(fhirBundle.isElr()).isFalse() + } + + @Test + fun `Test if ELR when bundle is not BundleType MESSAGE`() { + val fhirBundle = Bundle() + fhirBundle.type = Bundle.BundleType.DOCUMENT + assertThat(fhirBundle.isElr()).isFalse() + } + + @Test + fun `Test if ELR when bundle has no entries`() { + val fhirBundle = Bundle() + fhirBundle.type = Bundle.BundleType.MESSAGE + assertThat(fhirBundle.isElr()).isFalse() + } + + @Test + fun `Test if ELR when bundle has no MessageHeader`() { + val fhirBundle = Bundle() + fhirBundle.type = Bundle.BundleType.MESSAGE + val entry = Bundle.BundleEntryComponent() + fhirBundle.entry.add(0, entry) + assertThat(fhirBundle.isElr()).isFalse() + } + + @Test + fun `Test if ELR when bundle has MessageHeader but no Coding event`() { + val fhirBundle = Bundle() + fhirBundle.type = Bundle.BundleType.MESSAGE + val entry = Bundle.BundleEntryComponent() + entry.resource = MessageHeader() + fhirBundle.entry.add(0, entry) + assertThat(fhirBundle.isElr()).isFalse() + } + + @Test + fun `Test if ELR when bundle has MessageHeader but Coding event not R01`() { + val fhirBundle = Bundle() + fhirBundle.type = Bundle.BundleType.MESSAGE + val entry = Bundle.BundleEntryComponent() + val messageHeader = MessageHeader() + val event = Coding() + messageHeader.event = event + entry.resource = messageHeader + fhirBundle.entry.add(0, entry) + assertThat(fhirBundle.isElr()).isFalse() + } + + @Test + fun `Test if ELR when bundle is happy path`() { + val fhirBundle = Bundle() + fhirBundle.type = Bundle.BundleType.MESSAGE + val entry = Bundle.BundleEntryComponent() + val messageHeader = MessageHeader() + var event = Coding() + event.code = "R01" + messageHeader.event = event + entry.resource = messageHeader + fhirBundle.entry.add(0, entry) + assertThat(fhirBundle.isElr()).isTrue() + event.code = "ORU_R01" + assertThat(fhirBundle.isElr()).isTrue() + event.code = "R21" + assertThat(fhirBundle.isElr()).isFalse() + } + + @Test + fun `Test current values for rs message type`() { + val fhirBundle = Bundle() + assertThat(fhirBundle.getRSMessageType()).isEqualTo(RSMessageType.UNKNOWN) + fhirBundle.type = Bundle.BundleType.MESSAGE + val entry = Bundle.BundleEntryComponent() + val messageHeader = MessageHeader() + val event = Coding() + event.code = "R01" + messageHeader.event = event + entry.resource = messageHeader + fhirBundle.entry.add(0, entry) + assertThat(fhirBundle.getRSMessageType()).isEqualTo(RSMessageType.LAB_RESULT) + } + @Test fun `Test find Diagnostic report no observation`() { val actionLogger = ActionLogger() diff --git a/prime-router/src/test/kotlin/tokens/ScopeTests.kt b/prime-router/src/test/kotlin/tokens/ScopeTests.kt index 8e3e1ad53b2..c0d0caf36da 100644 --- a/prime-router/src/test/kotlin/tokens/ScopeTests.kt +++ b/prime-router/src/test/kotlin/tokens/ScopeTests.kt @@ -144,4 +144,23 @@ class ScopeTests { assertThat(Scope.scopeListContainsScope("x x", "")).isFalse() assertThat(Scope.scopeListContainsScope("x x", " ")).isFalse() } + + @Test + fun `test mapped scopes`() { + val groupMemberships = listOf( + "DHPrimeAdmins", + "DHmd-phdAdmins", + "DHSender_md-phd", + "DHmd-phd" + ) + + val mappedScopes = Scope.mapOktaGroupsToScopes(groupMemberships) + + val expected = setOf( + "*.*.primeadmin", + "md-phd.*.admin", + "md-phd.*.user" + ) + assertThat(mappedScopes).isEqualTo(expected) + } } \ No newline at end of file diff --git a/prime-router/src/test/kotlin/validation/MarsOtcElrValidatorTests.kt b/prime-router/src/test/kotlin/validation/MarsOtcElrValidatorTests.kt index 8bcf227d3b4..16b35f45c91 100644 --- a/prime-router/src/test/kotlin/validation/MarsOtcElrValidatorTests.kt +++ b/prime-router/src/test/kotlin/validation/MarsOtcElrValidatorTests.kt @@ -32,4 +32,15 @@ class MarsOtcElrValidatorTests { val report = validator.validate(messages[0]) assertThat(report.isValid()).isTrue() } + + @Test + fun `test valid RADxMARS message with NIST invalid MSH-5-1, MSH-5-2, MSH-6-1, MSH-6-2 `() { + val sampleMessageInputStream = + this.javaClass.classLoader.getResourceAsStream("validation/marsotcelr/valid_altered_msh.hl7") + + val sampleMessage = sampleMessageInputStream!!.bufferedReader().use { it.readText() } + val messages = HL7Reader(ActionLogger()).getMessages(sampleMessage) + val report = validator.validate(messages[0]) + assertThat(report.isValid()).isTrue() + } } \ No newline at end of file diff --git a/prime-router/src/test/resources/validation/marsotcelr/valid_altered_msh.hl7 b/prime-router/src/test/resources/validation/marsotcelr/valid_altered_msh.hl7 new file mode 100644 index 00000000000..dbdea1b3ef7 --- /dev/null +++ b/prime-router/src/test/resources/validation/marsotcelr/valid_altered_msh.hl7 @@ -0,0 +1,9 @@ +MSH|^~\&|AbbottInformatics^2.16.840.1.113883.3.8589.4.1.22^ISO|AbbottInformatics^00Z0000002^CLIA|CDC PRIME^2.16.840.1.114222.4.1.237821^ISO|CDC PRIME^2.16.840.1.114222.4.1.237821^ISO|20241204094313+0100||ORU^R01^ORU_R01|20241204094313+0100_Your Test Kit ID|T|2.5.1|||NE|NE|||||PHLabReport-NoAck^ELR251R1_Rcvr_Prof^2.16.840.1.113883.9.11^ISO +SFT|Meadows Design, LLC|1.0.0|RADx MARS Hub API|1.0.0|| +PID|1||c2sense079177^^^&2.16.840.1.113883.3.8589.4.1.22&ISO^PI||Smith^Rebecca^P||20241204|F||2076-8^Native Hawaiian or Other Pacific Islander^HL70005^^^^2.5.1|^^^^60013^^^^||^^PH^^^111^1111111|||||||||H^Hispanic or Latino^HL70189^^^^2.5.1 +ORC|RE||Your Test Kit ID^^2.16.840.1.113883.3.8589.4.1.22^ISO|||||||||^^SA.Proctor|||||||||SA.Proctor|^^^^60013^^^^|^^PH^^^111^1111111| +OBR|1||Your Test Kit ID^^2.16.840.1.113883.3.8589.4.1.22^ISO|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by Rapid immunoassay^LN^^^^2.71|||20241204094313+0100|||||||||^^SA.Proctor||||||20241204094313+0100|||F +OBX|1|CWE|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by Rapid immunoassay^LN^^^^2.71||260415000^Not Detected^SCT^^^^20210301|||N^Normal^HL70078^^^^2.5.1|||F||||00Z0000016||10811877011337_DIT^^99ELR^^^^Vunknown||20241204094313+0100||||SA.Proctor^^^^^&2.16.840.1.113883.3.8589.4.1.152&ISO^XX^^^00Z0000016|13 Fake AtHome Test Street^^Fake City| +NTE|1|L|10811877011337_DIT +OBX|2|NM|35659-2^Age at specimen collection^LN^^^^2.71||28|a^year^UCUM^^^^2.1|||||F||||00Z0000016||||||||SA.Proctor^^^^^&2.16.840.1.113883.3.8589.4.1.152&ISO^XX^^^00Z0000016|13 Fake AtHome Test Street^^Fake City|||||QST +SPM|1|^Your Test Kit ID&&2.16.840.1.113883.3.8589.4.1.22&ISO||697989009^Anterior nares swab (specimen)^SCT^^^^20210301|||||||||||||20241204094313+0100|20241204094313+0100 \ No newline at end of file diff --git a/prime-router/src/testIntegration/kotlin/datatests/mappinginventory/catchall/cp/CPTests.kt b/prime-router/src/testIntegration/kotlin/datatests/mappinginventory/catchall/cp/CPTests.kt new file mode 100644 index 00000000000..6fa650b9945 --- /dev/null +++ b/prime-router/src/testIntegration/kotlin/datatests/mappinginventory/catchall/cp/CPTests.kt @@ -0,0 +1,16 @@ +package gov.cdc.prime.router.datatests.mappinginventory.cp + +import gov.cdc.prime.router.datatests.mappinginventory.verifyHL7ToFHIRToHL7Mapping +import org.junit.jupiter.api.Test + +class CPTests { + @Test + fun `test CP mapped to CPExtension`() { + assert( + verifyHL7ToFHIRToHL7Mapping( + "catchall/cp/cp-to-extension", + outputSchema = "classpath:/metadata/hl7_mapping/OML_O21/OML_O21-test.yml" + ).passed + ) + } +} \ No newline at end of file diff --git a/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/cp/cp-to-extension.fhir b/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/cp/cp-to-extension.fhir new file mode 100644 index 00000000000..86c2f6a5a65 --- /dev/null +++ b/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/cp/cp-to-extension.fhir @@ -0,0 +1,195 @@ +{ + "resourceType": "Bundle", + "id": "1733797768446514000.f1548dec-5abc-4302-9b8d-59f755ec9b2f", + "meta": { + "lastUpdated": "2024-12-09T18:29:28.450-08:00" + }, + "identifier": { + "system": "https://reportstream.cdc.gov/prime-router", + "value": "MSG00001" + }, + "type": "message", + "entry": [ + { + "fullUrl": "MessageHeader/1733797768476500000.9c68bbf7-cd15-490d-a357-ac0d515575f8", + "resource": { + "resourceType": "MessageHeader", + "id": "1733797768476500000.9c68bbf7-cd15-490d-a357-ac0d515575f8", + "meta": { + "tag": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0103", + "code": "P" + } + ] + }, + "eventCoding": { + "system": "http://terminology.hl7.org/CodeSystem/v2-0003", + "code": "O21", + "display": "OML^O21^OML_O21" + } + } + }, + { + "fullUrl": "Provenance/1733797768760500000.23e4e2bd-4532-468d-89ec-e012d5702477", + "resource": { + "resourceType": "Provenance", + "id": "1733797768760500000.23e4e2bd-4532-468d-89ec-e012d5702477", + "activity": { + "coding": [ + { + "display": "OML^O21^OML_O21" + } + ] + } + } + }, + { + "fullUrl": "Provenance/1733797768767441000.19ac0704-100d-4d66-8cb9-3094fba92b95", + "resource": { + "resourceType": "Provenance", + "id": "1733797768767441000.19ac0704-100d-4d66-8cb9-3094fba92b95", + "recorded": "2024-12-09T18:29:28Z", + "policy": [ + "http://hl7.org/fhir/uv/v2mappings/message-oru-r01-to-bundle" + ], + "activity": { + "coding": [ + { + "code": "v2-FHIR transformation" + } + ] + }, + "agent": [ + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/provenance-participant-type", + "code": "assembler" + } + ] + }, + "who": { + "reference": "Organization/1733797768766853000.2f23f653-749b-4b43-b159-d1d955375f3a" + } + } + ] + } + }, + { + "fullUrl": "Organization/1733797768766853000.2f23f653-749b-4b43-b159-d1d955375f3a", + "resource": { + "resourceType": "Organization", + "id": "1733797768766853000.2f23f653-749b-4b43-b159-d1d955375f3a", + "identifier": [ + { + "value": "CDC PRIME - Atlanta" + }, + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0301" + } + ] + }, + "system": "urn:ietf:rfc:3986", + "value": "2.16.840.1.114222.4.1.237821" + } + ] + } + }, + { + "fullUrl": "Patient/1733797768779870000.bb1ddb84-5fa1-484f-96a4-4616988212fc", + "resource": { + "resourceType": "Patient", + "id": "1733797768779870000.bb1ddb84-5fa1-484f-96a4-4616988212fc" + } + }, + { + "fullUrl": "Provenance/1733797768780413000.e0ff5861-fd54-4149-8ee0-415a1dcc9123", + "resource": { + "resourceType": "Provenance", + "id": "1733797768780413000.e0ff5861-fd54-4149-8ee0-415a1dcc9123", + "target": [ + { + "reference": "Patient/1733797768779870000.bb1ddb84-5fa1-484f-96a4-4616988212fc" + } + ], + "recorded": "2024-12-09T18:29:28Z", + "activity": { + "coding": [ + { + "system": "https://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "UPDATE" + } + ] + } + } + }, + { + "fullUrl": "Coverage/1733797768783117000.2931a2e0-097c-481f-9adf-46e114f827b2", + "resource": { + "resourceType": "Coverage", + "id": "1733797768783117000.2931a2e0-097c-481f-9adf-46e114f827b2", + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/composite-price", + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/hl7v2Field", + "valueString": "IN1.40" + }, + { + "url": "CP.1.1", + "valueDecimal": 33.33 + }, + { + "url": "CP.1.2", + "valueString": "MXN" + }, + { + "url": "CP.2", + "valueString": "P3" + }, + { + "url": "CP.3", + "valueDecimal": 3.003 + }, + { + "url": "CP.4", + "valueDecimal": -3 + }, + { + "url": "CP.5", + "valueCodeableConcept": { + "coding": [ + { + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding", + "valueString": "coding" + }, + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding-system", + "valueString": "HL79999" + } + ], + "code": "OR", + "display": "Oven Range" + } + ] + } + }, + { + "url": "CP.6", + "valueString": "P" + } + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/cp/cp-to-extension.hl7 b/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/cp/cp-to-extension.hl7 new file mode 100644 index 00000000000..be3735df492 --- /dev/null +++ b/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/cp/cp-to-extension.hl7 @@ -0,0 +1,3 @@ +MSH|^~\&|||||||OML^O21^OML_O21|MSG00001|P|2.5.1 +PID|1 +IN1||||||||||||||||||||||||||||||||||||||||33.33&MXN^P3^3.003^-3^OR&Oven Range&HL79999^P \ No newline at end of file diff --git a/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/in1/IN1-to-Coverage.fhir b/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/in1/IN1-to-Coverage.fhir index bac350c4bb3..c341f40f512 100644 --- a/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/in1/IN1-to-Coverage.fhir +++ b/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/in1/IN1-to-Coverage.fhir @@ -1,8 +1,8 @@ { "resourceType": "Bundle", - "id": "1732324230716018000.38edba19-1d39-4040-85af-9f5f6248813c", + "id": "1733797806213402000.63933238-a67a-4568-8544-178612c57351", "meta": { - "lastUpdated": "2024-11-22T17:10:30.720-08:00" + "lastUpdated": "2024-12-09T18:30:06.217-08:00" }, "identifier": { "system": "https://reportstream.cdc.gov/prime-router", @@ -11,10 +11,10 @@ "type": "message", "entry": [ { - "fullUrl": "MessageHeader/1732324230752828000.fc7cea9d-d7ca-4378-91d4-ea97c86b4787", + "fullUrl": "MessageHeader/1733797806248346000.554ce0a6-e3fb-4b4e-b8e2-c111f8944cd5", "resource": { "resourceType": "MessageHeader", - "id": "1732324230752828000.fc7cea9d-d7ca-4378-91d4-ea97c86b4787", + "id": "1733797806248346000.554ce0a6-e3fb-4b4e-b8e2-c111f8944cd5", "meta": { "tag": [ { @@ -31,17 +31,108 @@ } }, { - "fullUrl": "Patient/1732324231018814000.533db9a5-5375-4746-be23-f7140d4a3280", + "fullUrl": "Provenance/1733797806498618000.239e218a-0960-4528-939e-5ee68bfbba1d", + "resource": { + "resourceType": "Provenance", + "id": "1733797806498618000.239e218a-0960-4528-939e-5ee68bfbba1d", + "activity": { + "coding": [ + { + "display": "OML^O21^OML_O21" + } + ] + } + } + }, + { + "fullUrl": "Provenance/1733797806506657000.35b16b0c-d80a-4017-8cad-868a79863b64", + "resource": { + "resourceType": "Provenance", + "id": "1733797806506657000.35b16b0c-d80a-4017-8cad-868a79863b64", + "recorded": "2024-12-09T18:30:06Z", + "policy": [ + "http://hl7.org/fhir/uv/v2mappings/message-oru-r01-to-bundle" + ], + "activity": { + "coding": [ + { + "code": "v2-FHIR transformation" + } + ] + }, + "agent": [ + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/provenance-participant-type", + "code": "assembler" + } + ] + }, + "who": { + "reference": "Organization/1733797806506092000.a6f8b4f5-e971-4ee2-8891-8b67af7df93d" + } + } + ] + } + }, + { + "fullUrl": "Organization/1733797806506092000.a6f8b4f5-e971-4ee2-8891-8b67af7df93d", + "resource": { + "resourceType": "Organization", + "id": "1733797806506092000.a6f8b4f5-e971-4ee2-8891-8b67af7df93d", + "identifier": [ + { + "value": "CDC PRIME - Atlanta" + }, + { + "type": { + "coding": [ + { + "system": "http://terminology.hl7.org/CodeSystem/v2-0301" + } + ] + }, + "system": "urn:ietf:rfc:3986", + "value": "2.16.840.1.114222.4.1.237821" + } + ] + } + }, + { + "fullUrl": "Patient/1733797806517233000.6ddc53a3-0587-4db5-8c73-5b6634eb1369", "resource": { "resourceType": "Patient", - "id": "1732324231018814000.533db9a5-5375-4746-be23-f7140d4a3280" + "id": "1733797806517233000.6ddc53a3-0587-4db5-8c73-5b6634eb1369" } }, { - "fullUrl": "Coverage/1732324231019741000.a36368bf-6c63-4970-8948-034a92248964", + "fullUrl": "Provenance/1733797806517793000.28f02504-824d-4c22-a6a1-e234b3affd5f", + "resource": { + "resourceType": "Provenance", + "id": "1733797806517793000.28f02504-824d-4c22-a6a1-e234b3affd5f", + "target": [ + { + "reference": "Patient/1733797806517233000.6ddc53a3-0587-4db5-8c73-5b6634eb1369" + } + ], + "recorded": "2024-12-09T18:30:06Z", + "activity": { + "coding": [ + { + "system": "https://terminology.hl7.org/CodeSystem/v3-DataOperation", + "code": "UPDATE" + } + ] + } + } + }, + { + "fullUrl": "Coverage/1733797806523309000.c5403041-1e4b-4f81-bc46-f16237041fec", "resource": { "resourceType": "Coverage", - "id": "1732324231019741000.a36368bf-6c63-4970-8948-034a92248964", + "id": "1733797806523309000.c5403041-1e4b-4f81-bc46-f16237041fec", "extension": [ { "url": "IN1.14", @@ -59,6 +150,222 @@ "valueString": "DR TEETH AND THE ELECTRIC MAYHEM" } ] + }, + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/composite-price", + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/hl7v2Field", + "valueString": "IN1.37" + }, + { + "url": "CP.1.1", + "valueDecimal": 11.11 + }, + { + "url": "CP.1.2", + "valueString": "USD" + }, + { + "url": "CP.2", + "valueString": "P1" + }, + { + "url": "CP.3", + "valueDecimal": 1.001 + }, + { + "url": "CP.4", + "valueDecimal": -1 + }, + { + "url": "CP.5", + "valueCodeableConcept": { + "coding": [ + { + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding", + "valueString": "coding" + }, + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding-system", + "valueString": "HL79999" + } + ], + "code": "R", + "display": "Range Units" + } + ] + } + }, + { + "url": "CP.6", + "valueString": "F" + } + ] + }, + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/composite-price", + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/hl7v2Field", + "valueString": "IN1.38" + }, + { + "url": "CP.1.1", + "valueDecimal": 22.22 + }, + { + "url": "CP.1.2", + "valueString": "RUB" + }, + { + "url": "CP.2", + "valueString": "P2" + }, + { + "url": "CP.3", + "valueDecimal": 2.002 + }, + { + "url": "CP.4", + "valueDecimal": -2 + }, + { + "url": "CP.5", + "valueCodeableConcept": { + "coding": [ + { + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding", + "valueString": "coding" + }, + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding-system", + "valueString": "HL79999" + } + ], + "code": "RR", + "display": "Russian Rubble" + } + ] + } + }, + { + "url": "CP.6", + "valueString": "F" + } + ] + }, + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/composite-price", + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/hl7v2Field", + "valueString": "IN1.40" + }, + { + "url": "CP.1.1", + "valueDecimal": 33.33 + }, + { + "url": "CP.1.2", + "valueString": "MXN" + }, + { + "url": "CP.2", + "valueString": "P3" + }, + { + "url": "CP.3", + "valueDecimal": 3.003 + }, + { + "url": "CP.4", + "valueDecimal": -3 + }, + { + "url": "CP.5", + "valueCodeableConcept": { + "coding": [ + { + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding", + "valueString": "coding" + }, + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding-system", + "valueString": "HL79999" + } + ], + "code": "OR", + "display": "Oven Range" + } + ] + } + }, + { + "url": "CP.6", + "valueString": "P" + } + ] + }, + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/composite-price", + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/hl7v2Field", + "valueString": "IN1.41" + }, + { + "url": "CP.1.1", + "valueDecimal": 44.44 + }, + { + "url": "CP.1.2", + "valueString": "CAD" + }, + { + "url": "CP.2", + "valueString": "P4" + }, + { + "url": "CP.3", + "valueDecimal": 4.004 + }, + { + "url": "CP.4", + "valueDecimal": -4 + }, + { + "url": "CP.5", + "valueCodeableConcept": { + "coding": [ + { + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding", + "valueString": "coding" + }, + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding-system", + "valueString": "HL79999" + } + ], + "code": "HHR", + "display": "Home Home Range" + } + ] + } + }, + { + "url": "CP.6", + "valueString": "P" + } + ] } ] } diff --git a/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/in1/IN1-to-Coverage.hl7 b/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/in1/IN1-to-Coverage.hl7 index 6d424a80fa2..0279bf1db76 100644 --- a/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/in1/IN1-to-Coverage.hl7 +++ b/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/in1/IN1-to-Coverage.hl7 @@ -1,3 +1,3 @@ MSH|^~\&|||||||OML^O21^OML_O21|MSG00001|P|2.5.1 PID|1 -IN1||||||||||||||1701^19700101^DR TEETH AND THE ELECTRIC MAYHEM \ No newline at end of file +IN1||||||||||||||1701^19700101^DR TEETH AND THE ELECTRIC MAYHEM|||||||||||||||||||||||11.11&USD^P1^1.001^-1^R&Range Units&HL79999^F|22.22&RUB^P2^2.002^-2^RR&Russian Rubble&HL79999^F||33.33&MXN^P3^3.003^-3^OR&Oven Range&HL79999^P|44.44&CAD^P4^4.004^-4^HHR&Home Home Range&HL79999^P \ No newline at end of file diff --git a/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/omlo21/oml_o21-full.fhir b/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/omlo21/oml_o21-full.fhir index 214a988f494..d24dfe4dad2 100644 --- a/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/omlo21/oml_o21-full.fhir +++ b/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/omlo21/oml_o21-full.fhir @@ -13262,6 +13262,222 @@ "valueString": "DR TEETH AND THE ELECTRIC MAYHEM" } ] + }, + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/composite-price", + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/hl7v2Field", + "valueString": "IN1.37" + }, + { + "url": "CP.1.1", + "valueDecimal": 11.11 + }, + { + "url": "CP.1.2", + "valueString": "USD" + }, + { + "url": "CP.2", + "valueString": "P1" + }, + { + "url": "CP.3", + "valueDecimal": 1.001 + }, + { + "url": "CP.4", + "valueDecimal": -1 + }, + { + "url": "CP.5", + "valueCodeableConcept": { + "coding": [ + { + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding", + "valueString": "coding" + }, + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding-system", + "valueString": "HL79999" + } + ], + "code": "R", + "display": "Range Units" + } + ] + } + }, + { + "url": "CP.6", + "valueString": "F" + } + ] + }, + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/composite-price", + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/hl7v2Field", + "valueString": "IN1.38" + }, + { + "url": "CP.1.1", + "valueDecimal": 22.22 + }, + { + "url": "CP.1.2", + "valueString": "RUB" + }, + { + "url": "CP.2", + "valueString": "P2" + }, + { + "url": "CP.3", + "valueDecimal": 2.002 + }, + { + "url": "CP.4", + "valueDecimal": -2 + }, + { + "url": "CP.5", + "valueCodeableConcept": { + "coding": [ + { + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding", + "valueString": "coding" + }, + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding-system", + "valueString": "HL79999" + } + ], + "code": "RR", + "display": "Russian Rubble" + } + ] + } + }, + { + "url": "CP.6", + "valueString": "F" + } + ] + }, + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/composite-price", + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/hl7v2Field", + "valueString": "IN1.40" + }, + { + "url": "CP.1.1", + "valueDecimal": 33.33 + }, + { + "url": "CP.1.2", + "valueString": "MXN" + }, + { + "url": "CP.2", + "valueString": "P3" + }, + { + "url": "CP.3", + "valueDecimal": 3.003 + }, + { + "url": "CP.4", + "valueDecimal": -3 + }, + { + "url": "CP.5", + "valueCodeableConcept": { + "coding": [ + { + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding", + "valueString": "coding" + }, + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding-system", + "valueString": "HL79999" + } + ], + "code": "OR", + "display": "Oven Range" + } + ] + } + }, + { + "url": "CP.6", + "valueString": "P" + } + ] + }, + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/composite-price", + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/hl7v2Field", + "valueString": "IN1.41" + }, + { + "url": "CP.1.1", + "valueDecimal": 44.44 + }, + { + "url": "CP.1.2", + "valueString": "CAD" + }, + { + "url": "CP.2", + "valueString": "P4" + }, + { + "url": "CP.3", + "valueDecimal": 4.004 + }, + { + "url": "CP.4", + "valueDecimal": -4 + }, + { + "url": "CP.5", + "valueCodeableConcept": { + "coding": [ + { + "extension": [ + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding", + "valueString": "coding" + }, + { + "url": "https://reportstream.cdc.gov/fhir/StructureDefinition/cwe-coding-system", + "valueString": "HL79999" + } + ], + "code": "HHR", + "display": "Home Home Range" + } + ] + } + }, + { + "url": "CP.6", + "valueString": "P" + } + ] } ] } diff --git a/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/omlo21/oml_o21-full.hl7 b/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/omlo21/oml_o21-full.hl7 index 23863a51bb8..baa36fced05 100644 --- a/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/omlo21/oml_o21-full.hl7 +++ b/prime-router/src/testIntegration/resources/datatests/mappinginventory/catchall/omlo21/oml_o21-full.hl7 @@ -8,7 +8,7 @@ NK1|1|SURYAN&Prefix&Own&SpousePrefix&Spouse^GENARO^GR^JR^Sir^Md^L^I^CON&Context NK1|2|SUPERMAN&Prefix&Own&SpousePrefix&Spouse^GENARO^GR^JR^Sir^Md^L^I^CON&Context the namee&HL70448^2000&2030^G^20000501102531^2030501102531^Dr|OTH^Other^HL70063^OT^OTHER RELATIONSHIP^L|4861 20TH AVE^^THUNDER MOUNTAIN^IG^99999^USA^H|^PRN^PH^example2@exmaple.com^1^720^5553954^2^any^^^+1 720 555 3955|^WPN^PH^^1^555^4672293^^^^^+1 555 467 2294|F^Federal Agency^HL70131|20220501102531-0400|20230501102531-0400|||052479^^^^^^20160822|HospitalsRUs^^112233^^^^^^^HRU||N^Not Applicable^HL70001|19860505||||E^English^HL70296|||||||||||^WPN^PH^^1^720^5553954^^^^^+1 720 555 3955|4861 20TH AVE^#B^AURORA^IG^99999^USA^H^World^King^12^8^2017&2025^2020^2021|052479^^^^^^^20210428|||||||^VHN^SAT^^1^314^5553131^^^^^+1 314 555 3132|^AWN^FX^^1^281^5558181^^^^^+1 281 555 8182 PV1|1|O|A&Point OF Care&C^1&Room&3^B&Bed&A^Hospital Assigned&2.4.4.4&ISO^^R^&Building^&Floor^Totally A Real Location^Comprehensive&&UID4This^AA&AssigningAUTH&ISO|R^Routine^HL70007|232323|^^^Hospital Prio&2.4.4.4&ISO^active^location type^^^Description^Entity ID&NAME&UNI&ISO^ASSIGNEE&222.1111.22222&UUID|1^BEETHOVEN^LUDWIG^B^2ND^DR^MD^^Namespace&AssigningSystem&UUID^B^^^DL^^^^^^^^MD~1^BEETHOVEN2^LUDWIG^B^2ND^DR^MD^^Namespace&AssigningSystem&UUID^B^^^DL^^^^^^^^MD|1^MOZART~1^MOZARTJR|1^CHOPIN~1^CHOPINSR|URO^Urology Service^HL70069|^^^^^^^^Its Temporary|P^Passed^HL70087|R^Re-admission^HL70092|RL^Real Life^HL70023||VIP^Very Interesting Person^HL70099|1^BACH~1^BACHtheSecond|H^Human Patient^HL70018|22|||||||||||20020101|C^Collectors^HL70021|1|0|Y^Yes^HL70111|20080101|H^Happy^HL70112|^202305061200|F^Fed^HL70114|H^A Hospital Of Course^HL70115||A^Active^HL70117|^^^^^^^^Pending Location|^^^^^^^^Prior Location|20240801102531-0400|20240801102531-0400|100|199|142|130|alternate visit|A^Account Level^HL70326||Service Description|episode identifier PV2|^^^Hospital PriorPending&2.4.4.4&ISO^active^location type^^^Description^Entity ID&NAME&UNI&ISO^ASSIGNEE&222.1111.22222&UUID||1^AD||||413^V~423^X|20230601102531-0400|20230701102531-0400|5|12|Description|1^BEETHOVEN&VAN&Referral Source Code1&VAL&ROGER^LUDWIG^B^2ND^DR^MD^SRC^&AssigningSystem&ISO^B^A^NPI^DL^^A^NameContext^^G^20220501102531-0400^20230501102531-0400^MD^AssignJ^AssignA~1^BEETHOVEN&VAN&Referral Source Code2&VAL&ROGER^LUDWIG^B^2ND^DR^MD^SRC^&AssigningSystem&ISO^B^A^NPI^DL^^A^NameContext^^G^20220501102531-0400^20230501102531-0400^MD^AssignJ^AssignA||EMP_ILL||||||100^PublicCode|SEC|Org1^1234-5&TestText&LN&1234-5&TestAltText&LN&1&2&OriginalText^123^Check Digit^C1^Assigning Authority&2.1.4.1&ISO^MD^Hospital A&2.16.840.1.113883.9.11&ISO~Org2^1234-5&TestText&LN&1234-5&TestAltText&LN&1&2&OriginalText^123^Check Digit^C1^Assigning Authority&2.1.4.1&ISO^MD^Hospital A&2.16.840.1.113883.9.11&ISO||3^Elective^HL70217|20230501102531-0400|||20220501102531-0400|||||||||444^MODE||123^CARELEVEL1 -IN1||||||||||||||1701^19700101^DR TEETH AND THE ELECTRIC MAYHEM +IN1||||||||||||||1701^19700101^DR TEETH AND THE ELECTRIC MAYHEM|||||||||||||||||||||||11.11&USD^P1^1.001^-1^R&Range Units&HL79999^F|22.22&RUB^P2^2.002^-2^RR&Russian Rubble&HL79999^F||33.33&MXN^P3^3.003^-3^OR&Oven Range&HL79999^P|44.44&CAD^P4^4.004^-4^HHR&Home Home Range&HL79999^P ORC|RE|Specimen123^SPHL-000048^2.16.840.1.114222.4.1.10765^ISO|Specimen12311^SPHL-000048^2.16.840.1.114222.4.1.10765^ISO|Specimen12322^SPHL-000048^2.16.840.1.114222.4.1.10765^ISO|CM|E||Specimen12333&SPHL-000048&2.16.840.1.114222.4.1.10765&ISO^Specimen12333454&SPHL-000048&2.16.840.1.114222.4.1.10765&ISO|20230725|71^^ORC.10Name~71^^ORC.10Name2|82^^ORC.11Name~82^^ORC.11Name2|93^^ORC.12Name~93^^ORC.12Name2|12.12.12&enter location id&L|123^^^+123~1234^^^+1234|202101021000||EO^entering org text^HL79999|ED^entering device text^HL79999|60^^ORC.19Name~60^^ORC.19Name2||CDPH, Viral and Rickettsial Disease Laboratory^^^^^STARLIMS.CDC.Stag&2.16.840.1.114222.4.3.3.2.1.2&ISO^XX^STARLINKS.CDC.Stag&2.16.840.1.114222.8.7.6.5.4.1&ISO^A^SPHL-000048~CDPH, Viral and Rickettsial Disease Laboratory part two^^^^^STARLIMS.CDC.Stag&2.16.840.1.114222.4.3.3.2.1.2&ISO|111 Street St^^Streetsville^ST~112 Street St^^Streetsville^ST|456^^^+456~4567^^^+4567|111 Road Rd^^Roadsville^RD~112 Road Rd^^Roadsville^RD|OS^order status^HL79999||20260404|EMP^Employee^HL70177|I^Inpatient Order^HL70482|EL^Electronic^HL70483|UMM^Universal Modifier^HL70000|20250403|33~332 OBR|1|Placer Identifier^Placer Identifier Namespace^Placer Universal ID^ISO|Filler Identifier^Filler Identifier Namespace^Filler Universal ID^ISO|123^Universal service identifier||202202021022||20240220|1771|1^Collector&VAN&Identifier&VAL&ROGER^LUDWIG^B^2ND^DR^MD^SRC^&AssigningSystem&ISO^B^A^NPI^DL^^A^NameContext~1^Collectorx2&VAN&Identifier&VAL&ROGER^LUDWIG^B^2ND^DR^MD^SRC^&AssigningSystem&ISO^B^A^NPI^DL^^A^NameContext|G|512^Danger code|R^relevent info^ISO|202102021000|ID^BOUIN&Bouin's solution&HL70371^Collection Method^LN&Left Naris&HL70163^^CMMC&Collection Method Modifer Code&HL7|1^Ordering&VAN&Provider&VAL&JollyROGER~2^Ordering&VAN&Provider&VAL&JollyROGER|^WPN^BP^^1^260^7595016^^^^^+1 260 759 5016~^WPN^Internet^order.callback@email.com~^WPN^BP^^1^260^7595016^^^^^+1 260 759 5016~^WPN^Internet^order.callback2@email.com|placer1|placer2|filler1|filler2||100&$^16&code|OTH|F|444&ParentId^888^ParentOBSdescriptor||1^result&VAN&copiesto&VAL&NotSoJollyROGER~2^results&VAN&copiesto&VAL&NotSoJollyROGER|adb4a5cc-50ec-4f1e-95d7-0c1f77cacee1&CSV&11D1111111&CLIA^f34b0f57-1601-4480-ae8a-d4006e50f38d&Other CSV&22D2222222&CLIA2||3216^ReasonForStudy~3216^ReasonForStudy2||123&Assistant&Results Interpreter&S&ESQ&DR&MD&&Assigning Authority&2.1.4.1&ISO^20230401102531-0400^20230501102531-0400^Point of Care^Room 101^Bed A^Hospital A&2.16.840.1.113883.9.11&ISO^active^^Building 123^Floor A~123&Assistant&Results Interpreter&S&ESQ&DR&MD&&Assigning Authority&2.1.4.1&ISO^20230401102531-0400^20230501102531-0400^Point of Care^Room 101^Bed A^Hospital A&2.16.840.1.113883.9.11&ISO^active^^Building 123^Floor A|||20230806123359-0500|||4438^Collectors Comment~4438^Collectors Comment2|||||5019^Procedure Code|887766^Procedure Code Modifier~887766^Procedure Code Modifier2|7461^Placer Supplemental~7461^Placer Supplemental2|8811^Fillter Supplemental~8811^Fillter Supplemental2|71435^Medically Necessary Duplicate Procedure|N|443331^Parent Universal Service Identifier|||Alt^Placer Order~Alt^Placer Order2|adb4a5cc-50ec-4f1e-95d7-0c1f77cacee1&CSV&11D1111111&CLIA^f34b0f57-1601-4480-ae8a-d4006e50f38d&Other CSV&22D2222222&CLIA2 NTE|1|L|Note about the observation request~second~third~fourth|||20230207|20230208|20230209|CC^Coded Request note