diff --git a/frontend-react/__mocks__/focus-trap-react.tsx b/frontend-react/__mocks__/focus-trap-react.tsx index 3fef86165be..1abc45ef15b 100644 --- a/frontend-react/__mocks__/focus-trap-react.tsx +++ b/frontend-react/__mocks__/focus-trap-react.tsx @@ -1,17 +1,13 @@ -import type * as FocusTrapType from "focus-trap-react"; +import { FocusTrapProps } from "focus-trap-react"; import React from "react"; import { vi } from "vitest"; -const FocusTrap = (await vi.importActual("focus-trap-react")) - .default as React.ComponentType; +const FocusTrap = (await vi.importActual("focus-trap-react")).default as React.ComponentType; /** * Override displayCheck for testing. See: https://github.com/focus-trap/tabbable#testing-in-jsdom */ -const FixedComponent = ({ - focusTrapOptions, - ...props -}: FocusTrapType.Props) => { +const FixedComponent = ({ focusTrapOptions, ...props }: FocusTrapProps) => { const fixedOptions = { ...focusTrapOptions }; fixedOptions.tabbableOptions = { ...fixedOptions.tabbableOptions, diff --git a/frontend-react/package.json b/frontend-react/package.json index cbb39055b49..79667c7bdc8 100644 --- a/frontend-react/package.json +++ b/frontend-react/package.json @@ -8,10 +8,10 @@ "@microsoft/applicationinsights-react-js": "^17.3.4", "@microsoft/applicationinsights-web": "^3.3.4", "@okta/okta-react": "^6.9.0", - "@okta/okta-signin-widget": "^7.26.1", + "@okta/okta-signin-widget": "^7.27.1", "@rest-hooks/rest": "^3.0.3", - "@tanstack/react-query": "^5.62.11", - "@tanstack/react-query-devtools": "^5.62.11", + "@tanstack/react-query": "^5.62.16", + "@tanstack/react-query-devtools": "^5.62.16", "@trussworks/react-uswds": "^9.1.0", "@uswds/uswds": "3.7.1", "axios": "^1.7.9", @@ -20,7 +20,7 @@ "date-fns-tz": "^3.2.0", "dompurify": "^3.2.3", "export-to-csv-fix-source-map": "^0.2.1", - "focus-trap-react": "^10.3.1", + "focus-trap-react": "^11.0.2", "history": "^5.3.0", "html-to-text": "^9.0.5", "lodash": "^4.17.21", @@ -30,19 +30,19 @@ "react-helmet-async": "^2.0.5", "react-idle-timer": "^5.7.2", "react-loader-spinner": "^6.1.6", - "react-markdown": "^9.0.1", + "react-markdown": "^9.0.3", "react-query-kit": "^3.3.1", "react-router": "^6.28.0", "react-router-dom": "^6.28.0", "react-scroll-sync": "^0.11.2", - "react-toastify": "^10.0.6", + "react-toastify": "^11.0.2", "rehype-raw": "^7.0.0", "rehype-slug": "^5.1.0", "rest-hooks": "^6.1.7", "sanitize-html": "^2.14.0", "tsx": "^4.19.2", "use-deep-compare-effect": "^1.8.1", - "uuid": "^11.0.3", + "uuid": "^11.0.4", "web-vitals": "^3.4.0" }, "scripts": { @@ -137,7 +137,7 @@ "@types/eslint__js": "^8.42.3", "@types/github-slugger": "^2.0.0", "@types/html-to-text": "^9.0.4", - "@types/lodash": "^4.17.13", + "@types/lodash": "^4.17.14", "@types/mdx": "^2.0.13", "@types/node": "^20.12.5", "@types/react": "^18.3.11", @@ -151,7 +151,7 @@ "autoprefixer": "^10.4.20", "browserslist": "^4.24.3", "browserslist-useragent-regexp": "^4.1.3", - "chromatic": "^11.20.2", + "chromatic": "^11.22.0", "cross-env": "^7.0.3", "dotenv-flow": "^4.1.0", "eslint": "^9.17.0", @@ -164,7 +164,7 @@ "eslint-plugin-react": "^7.37.3", "eslint-plugin-react-hooks": "^5.1.0", "eslint-plugin-react-refresh": "^0.4.16", - "eslint-plugin-storybook": "^0.11.1", + "eslint-plugin-storybook": "^0.11.2", "eslint-plugin-testing-library": "^7.1.1", "eslint-plugin-vitest": "^0.5.4", "globals": "^15.14.0", @@ -178,11 +178,11 @@ "patch-package": "^8.0.0", "postcss": "^8.4.49", "prettier": "^3.4.2", - "react-error-boundary": "^4.1.2", + "react-error-boundary": "^5.0.0", "remark-frontmatter": "^5.0.0", "remark-mdx-frontmatter": "^5.0.0", "remark-mdx-toc": "^0.3.1", - "sass": "^1.83.0", + "sass": "^1.83.1", "storybook": "^8.4.7", "storybook-addon-remix-react-router": "^3.0.2", "ts-node": "^10.9.2", @@ -190,7 +190,7 @@ "typescript": "^5.7.2", "typescript-eslint": "^8.19.0", "undici": "^6.20.1", - "vite": "^6.0.6", + "vite": "^6.0.7", "vite-plugin-checker": "^0.8.0", "vite-plugin-svgr": "^4.3.0", "vitest": "^2.1.8" diff --git a/frontend-react/src/components/App/App.tsx b/frontend-react/src/components/App/App.tsx index 4d84e0b8731..3f09079c12a 100644 --- a/frontend-react/src/components/App/App.tsx +++ b/frontend-react/src/components/App/App.tsx @@ -5,11 +5,7 @@ import { QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { Suspense, useCallback, useMemo } from "react"; import { HelmetProvider } from "react-helmet-async"; -import { - createBrowserRouter, - RouteObject, - RouterProvider, -} from "react-router-dom"; +import { createBrowserRouter, RouteObject, RouterProvider } from "react-router-dom"; import { CacheProvider, NetworkErrorBoundary } from "rest-hooks"; import AuthStateGate from "./AuthStateGate"; @@ -25,7 +21,6 @@ import { RSConsole } from "../../utils/rsConsole/rsConsole"; import { createTelemetryService } from "../../utils/TelemetryService/TelemetryService"; import { PERMISSIONS } from "../../utils/UsefulTypes"; -import "react-toastify/dist/ReactToastify.css"; import RSErrorBoundary from "../RSErrorBoundary/RSErrorBoundary"; export interface AppProps { @@ -48,10 +43,7 @@ function App({ config, routes }: AppProps) { () => new RSConsole({ ai: aiReactPlugin, ...config.RSCONSOLE }), [aiReactPlugin, config.RSCONSOLE], ); - const oktaAuth = useMemo( - () => new OktaAuth(config.OKTA_AUTH), - [config.OKTA_AUTH], - ); + const oktaAuth = useMemo(() => new OktaAuth(config.OKTA_AUTH), [config.OKTA_AUTH]); const router = useMemo(() => createBrowserRouter(routes), [routes]); const Fallback = useCallback(() => , []); @@ -67,25 +59,13 @@ function App({ config, routes }: AppProps) { let url = originalUri; if (originalUri === "/") { /* PERMISSIONS REFACTOR: Redirect URL should be determined by active membership type */ - if ( - authState?.accessToken && - permissionCheck( - PERMISSIONS.PRIME_ADMIN, - authState.accessToken, - ) - ) { + if (authState?.accessToken && permissionCheck(PERMISSIONS.PRIME_ADMIN, authState.accessToken)) { url = "/admin/settings"; } - if ( - authState?.accessToken && - permissionCheck(PERMISSIONS.SENDER, authState.accessToken) - ) { + if (authState?.accessToken && permissionCheck(PERMISSIONS.SENDER, authState.accessToken)) { url = "/submissions"; } - if ( - authState?.accessToken && - permissionCheck(PERMISSIONS.RECEIVER, authState.accessToken) - ) { + if (authState?.accessToken && permissionCheck(PERMISSIONS.RECEIVER, authState.accessToken)) { url = "/daily-data"; } } @@ -101,36 +81,20 @@ function App({ config, routes }: AppProps) { return ( - + - + - + - + - + - + diff --git a/frontend-react/yarn.lock b/frontend-react/yarn.lock index 4c930a6b876..0027b2b3293 100644 --- a/frontend-react/yarn.lock +++ b/frontend-react/yarn.lock @@ -1407,9 +1407,9 @@ __metadata: languageName: node linkType: hard -"@okta/okta-signin-widget@npm:^7.26.1": - version: 7.26.1 - resolution: "@okta/okta-signin-widget@npm:7.26.1" +"@okta/okta-signin-widget@npm:^7.27.1": + version: 7.27.1 + resolution: "@okta/okta-signin-widget@npm:7.27.1" dependencies: "@okta/okta-auth-js": ^7.9.0 "@sindresorhus/to-milliseconds": ^1.0.0 @@ -1433,7 +1433,7 @@ __metadata: dependenciesMeta: fsevents: optional: true - checksum: bac77bb6cda8c34a5155859f11e4c1a50ff62a523fe48e2bbfdb58527ffa1265e7eabbacdc67c104b43b32b3f60b0a82704f075a65f7008090dc32a80b549433 + checksum: 704c599fcab1009ea29a8fdab59c3b37b26942e4b19436d09a544cee00289c00ca425b00440f29c01e7ccfd6d522b7e7dbeb7697b063fab3b0afb5b1eff92bab languageName: node linkType: hard @@ -2459,40 +2459,40 @@ __metadata: languageName: node linkType: hard -"@tanstack/query-core@npm:5.62.9": - version: 5.62.9 - resolution: "@tanstack/query-core@npm:5.62.9" - checksum: 3dc3ca1654a3e3a3f92e569d2bf9e8c09b6bb670562090af357db7e95175ad0397a01dfd2c2eda34bdfe0666e7d6fb841d0c435223119681b1ba1f28d4fcd6b1 +"@tanstack/query-core@npm:5.62.16": + version: 5.62.16 + resolution: "@tanstack/query-core@npm:5.62.16" + checksum: 96e712dba70b9234884108ecac2fa05ae588f7a1758377d80aa30d4e830f00cbef95309d2806828ca224d1db51e3c8364857a6f3581edb989f2f16cb98ad0f26 languageName: node linkType: hard -"@tanstack/query-devtools@npm:5.62.9": - version: 5.62.9 - resolution: "@tanstack/query-devtools@npm:5.62.9" - checksum: 08415a0bc3661f088cecc44cecbd782eb54aef8e6ed05f6d7d56e92e96f2933f52a73d227e5925f141f46068ea68e6521a01f7773a96057ef96523c738b26e36 +"@tanstack/query-devtools@npm:5.62.16": + version: 5.62.16 + resolution: "@tanstack/query-devtools@npm:5.62.16" + checksum: 1b6554334ef00818ae4ba3cea1a369c70ce2d9605e7c44de3dd802cce355a8d559108f93a68cd5df5de7e5eeee4e89259577cf83611a43f62f67f27fc9cdb61a languageName: node linkType: hard -"@tanstack/react-query-devtools@npm:^5.62.11": - version: 5.62.11 - resolution: "@tanstack/react-query-devtools@npm:5.62.11" +"@tanstack/react-query-devtools@npm:^5.62.16": + version: 5.62.16 + resolution: "@tanstack/react-query-devtools@npm:5.62.16" dependencies: - "@tanstack/query-devtools": 5.62.9 + "@tanstack/query-devtools": 5.62.16 peerDependencies: - "@tanstack/react-query": ^5.62.11 + "@tanstack/react-query": ^5.62.16 react: ^18 || ^19 - checksum: c16732d639a91060dfaa68dd9e41c57b13baf626523034f944759196293e861c37596d2ecddf9d5de6d85981a98513b18ef4a2d5cb6d470d4260cd12039e1df1 + checksum: 1acd07c82cd37a73731e180b4511025d33143a5820c09450143b81bde24871a7f7bccb44d6d488583dc319ae3ed22779bd38157836d37e2673a1c9efa004a3b6 languageName: node linkType: hard -"@tanstack/react-query@npm:^5.62.11": - version: 5.62.11 - resolution: "@tanstack/react-query@npm:5.62.11" +"@tanstack/react-query@npm:^5.62.16": + version: 5.62.16 + resolution: "@tanstack/react-query@npm:5.62.16" dependencies: - "@tanstack/query-core": 5.62.9 + "@tanstack/query-core": 5.62.16 peerDependencies: react: ^18 || ^19 - checksum: dcd0fc21eead400f385299f3afd230a98884a8eda5fe55e1803bf0b2657ddf109509afcb12f3c23ef143ec8debcbece908c99c59729c3ab911068619015c0eb7 + checksum: dbf9cf549799d96ecefea237617e15234d9fa446460efe5286e128840b1bcfff5d7ff8a9eb9208aefe469800b50fa2ad31d93e631be3e0a58baac61a3094fb33 languageName: node linkType: hard @@ -2880,10 +2880,10 @@ __metadata: languageName: node linkType: hard -"@types/lodash@npm:^4.17.13": - version: 4.17.13 - resolution: "@types/lodash@npm:4.17.13" - checksum: d0bf8fbd950be71946e0076b30fd40d492293baea75f05931b6b5b906fd62583708c6229abdb95b30205ad24ce1ed2f48bc9d419364f682320edd03405cc0c7e +"@types/lodash@npm:^4.17.14": + version: 4.17.14 + resolution: "@types/lodash@npm:4.17.14" + checksum: 2dbeaff92b31cb523f6bc4bb99a3d8c88fbb001d54f2367a888add85784fb213744a9b1600e1e98b6790ab191fdb6ec839eb0e3d63fcf6fb6cf1ebe4c3d21149 languageName: node linkType: hard @@ -4230,9 +4230,9 @@ __metadata: languageName: node linkType: hard -"chromatic@npm:^11.20.2": - version: 11.20.2 - resolution: "chromatic@npm:11.20.2" +"chromatic@npm:^11.22.0": + version: 11.22.0 + resolution: "chromatic@npm:11.22.0" peerDependencies: "@chromatic-com/cypress": ^0.*.* || ^1.0.0 "@chromatic-com/playwright": ^0.*.* || ^1.0.0 @@ -4245,7 +4245,7 @@ __metadata: chroma: dist/bin.js chromatic: dist/bin.js chromatic-cli: dist/bin.js - checksum: fb1732aa78b42ee18934a40b53c7e09a701f3a6a75cb4323a1c449a5488178da0774e7063a281349d09cfa1cf57e275c9363e3d58af850cd93f1388caf9461ad + checksum: 7ec1d7000f30356f99210b086b04ad1041ab254fe06bb27294e762b57816ce998bf29666d4641a6555fdac46ecb474fde024e4fbc4af101ee666dac33fde6a87 languageName: node linkType: hard @@ -4339,10 +4339,10 @@ __metadata: languageName: node linkType: hard -"clsx@npm:^2.1.0": - version: 2.1.0 - resolution: "clsx@npm:2.1.0" - checksum: 43fefc29b6b49c9476fbce4f8b1cc75c27b67747738e598e6651dd40d63692135dc60b18fa1c5b78a2a9ba8ae6fd2055a068924b94e20b42039bd53b78b98e1d +"clsx@npm:^2.1.1": + version: 2.1.1 + resolution: "clsx@npm:2.1.1" + checksum: acd3e1ab9d8a433ecb3cc2f6a05ab95fe50b4a3cfc5ba47abb6cbf3754585fcb87b84e90c822a1f256c4198e3b41c7f6c391577ffc8678ad587fc0976b24fd57 languageName: node linkType: hard @@ -5702,16 +5702,16 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-storybook@npm:^0.11.1": - version: 0.11.1 - resolution: "eslint-plugin-storybook@npm:0.11.1" +"eslint-plugin-storybook@npm:^0.11.2": + version: 0.11.2 + resolution: "eslint-plugin-storybook@npm:0.11.2" dependencies: "@storybook/csf": ^0.1.11 "@typescript-eslint/utils": ^8.8.1 ts-dedent: ^2.2.0 peerDependencies: - eslint: ">=6" - checksum: 8b8eb30b598f3c44c2bbf921e318215338f1159c1fba2d2f6cd5bc0b2ec14515655cf1760b5e11355baddabc2ac8446d4dc18ee6df6d2252555b126ff649a421 + eslint: ">=8" + checksum: 04dab47b676db57aa6c2325d4ab66283f5970133a2615cc68a97e42cb4ae1c79317e6db73bcfc24468e146bdff4c9ffe90a368bf2bcb67593954485f058b33ca languageName: node linkType: hard @@ -6162,21 +6162,22 @@ __metadata: languageName: node linkType: hard -"focus-trap-react@npm:^10.3.1": - version: 10.3.1 - resolution: "focus-trap-react@npm:10.3.1" +"focus-trap-react@npm:^11.0.2": + version: 11.0.2 + resolution: "focus-trap-react@npm:11.0.2" dependencies: - focus-trap: ^7.6.1 + focus-trap: ^7.6.2 tabbable: ^6.2.0 peerDependencies: - prop-types: ^15.8.1 - react: ">=16.3.0" - react-dom: ">=16.3.0" - checksum: 7992402b86a2ebada9232f36388fe7997e395365f1ca89927114caf84c53c546940c066b0a60417b01b4b366d97d0ac313d7c65ef69b74d188fc0863f850f480 + "@types/react": ^18.0.0 || ^19.0.0 + "@types/react-dom": ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + checksum: 992b6330101ff71abba01c0ea0c85104a0bbf3bf91f335ca004776e77c7d700d8f1e8a12425cb7a1bc8a041169c2b0d4c25c9ac0db5b5bdf8d17c21d66085ab8 languageName: node linkType: hard -"focus-trap@npm:^7.6.1": +"focus-trap@npm:^7.6.2": version: 7.6.2 resolution: "focus-trap@npm:7.6.2" dependencies: @@ -9781,14 +9782,14 @@ __metadata: languageName: node linkType: hard -"react-error-boundary@npm:^4.1.2": - version: 4.1.2 - resolution: "react-error-boundary@npm:4.1.2" +"react-error-boundary@npm:^5.0.0": + version: 5.0.0 + resolution: "react-error-boundary@npm:5.0.0" dependencies: "@babel/runtime": ^7.12.5 peerDependencies: react: ">=16.13.1" - checksum: afe692f1bbbfb5998b49e1001d7682a3cbfdc623dca1318b408e738606f3450d925c28fbbfa5dc84d2cf285d17c2e7f079d59386a27da354dea9c902a935149b + checksum: 4fa78890bb254fe1f0ee1eed893ac161a27482c4567f7667ef83a8339432eb99e323ee69757f01f4864e0037b01a9b6822735ea122f02e749d0bf7a781d9ea53 languageName: node linkType: hard @@ -9810,7 +9811,7 @@ __metadata: "@microsoft/applicationinsights-react-js": ^17.3.4 "@microsoft/applicationinsights-web": ^3.3.4 "@okta/okta-react": ^6.9.0 - "@okta/okta-signin-widget": ^7.26.1 + "@okta/okta-signin-widget": ^7.27.1 "@playwright/test": ^1.49.1 "@rest-hooks/rest": ^3.0.3 "@rest-hooks/test": ^7.3.1 @@ -9827,8 +9828,8 @@ __metadata: "@storybook/react-vite": ^8.4.7 "@storybook/testing-library": ^0.2.2 "@storybook/theming": ^8.4.7 - "@tanstack/react-query": ^5.62.11 - "@tanstack/react-query-devtools": ^5.62.11 + "@tanstack/react-query": ^5.62.16 + "@tanstack/react-query-devtools": ^5.62.16 "@testing-library/dom": ^10.4.0 "@testing-library/jest-dom": ^6.6.3 "@testing-library/react": ^16.1.0 @@ -9839,7 +9840,7 @@ __metadata: "@types/eslint__js": ^8.42.3 "@types/github-slugger": ^2.0.0 "@types/html-to-text": ^9.0.4 - "@types/lodash": ^4.17.13 + "@types/lodash": ^4.17.14 "@types/mdx": ^2.0.13 "@types/node": ^20.12.5 "@types/react": ^18.3.11 @@ -9855,7 +9856,7 @@ __metadata: axios: ^1.7.9 browserslist: ^4.24.3 browserslist-useragent-regexp: ^4.1.3 - chromatic: ^11.20.2 + chromatic: ^11.22.0 classnames: ^2.5.1 cross-env: ^7.0.3 date-fns: ^4.1.0 @@ -9872,11 +9873,11 @@ __metadata: eslint-plugin-react: ^7.37.3 eslint-plugin-react-hooks: ^5.1.0 eslint-plugin-react-refresh: ^0.4.16 - eslint-plugin-storybook: ^0.11.1 + eslint-plugin-storybook: ^0.11.2 eslint-plugin-testing-library: ^7.1.1 eslint-plugin-vitest: ^0.5.4 export-to-csv-fix-source-map: ^0.2.1 - focus-trap-react: ^10.3.1 + focus-trap-react: ^11.0.2 globals: ^15.14.0 history: ^5.3.0 html-to-text: ^9.0.5 @@ -9894,16 +9895,16 @@ __metadata: prettier: ^3.4.2 react: ^18.3.1 react-dom: ^18.3.1 - react-error-boundary: ^4.1.2 + react-error-boundary: ^5.0.0 react-helmet-async: ^2.0.5 react-idle-timer: ^5.7.2 react-loader-spinner: ^6.1.6 - react-markdown: ^9.0.1 + react-markdown: ^9.0.3 react-query-kit: ^3.3.1 react-router: ^6.28.0 react-router-dom: ^6.28.0 react-scroll-sync: ^0.11.2 - react-toastify: ^10.0.6 + react-toastify: ^11.0.2 rehype-raw: ^7.0.0 rehype-slug: ^5.1.0 remark-frontmatter: ^5.0.0 @@ -9911,7 +9912,7 @@ __metadata: remark-mdx-toc: ^0.3.1 rest-hooks: ^6.1.7 sanitize-html: ^2.14.0 - sass: ^1.83.0 + sass: ^1.83.1 storybook: ^8.4.7 storybook-addon-remix-react-router: ^3.0.2 ts-node: ^10.9.2 @@ -9921,8 +9922,8 @@ __metadata: typescript-eslint: ^8.19.0 undici: ^6.20.1 use-deep-compare-effect: ^1.8.1 - uuid: ^11.0.3 - vite: ^6.0.6 + uuid: ^11.0.4 + vite: ^6.0.7 vite-plugin-checker: ^0.8.0 vite-plugin-svgr: ^4.3.0 vitest: ^2.1.8 @@ -9996,9 +9997,9 @@ __metadata: languageName: node linkType: hard -"react-markdown@npm:^9.0.1": - version: 9.0.1 - resolution: "react-markdown@npm:9.0.1" +"react-markdown@npm:^9.0.3": + version: 9.0.3 + resolution: "react-markdown@npm:9.0.3" dependencies: "@types/hast": ^3.0.0 devlop: ^1.0.0 @@ -10013,7 +10014,7 @@ __metadata: peerDependencies: "@types/react": ">=18" react: ">=18" - checksum: ca1daa650d48b84a5a9771683cdb3f3d2d418247ce0faf73ede3207c65f2a21cdebb9df37afda67f6fc8f0f0a7b9ce00eb239781954a4d6c7ad88ea4df068add + checksum: 7ebb01b295f7c9acddcd305308a8531c58c582c24fb8d6a4897f16b21ba0bd7e9e20ddae4a9024767e910310d22db0003489b61478cdb491a3d802343cf3a931 languageName: node linkType: hard @@ -10070,15 +10071,15 @@ __metadata: languageName: node linkType: hard -"react-toastify@npm:^10.0.6": - version: 10.0.6 - resolution: "react-toastify@npm:10.0.6" +"react-toastify@npm:^11.0.2": + version: 11.0.2 + resolution: "react-toastify@npm:11.0.2" dependencies: - clsx: ^2.1.0 + clsx: ^2.1.1 peerDependencies: - react: ">=18" - react-dom: ">=18" - checksum: 89fa24718eba0800e2bc1f88121a6e119efd87df26344d5b3c86442d4a13fbd65b67932e01bd6442758f4fe3f5eca6c8fc80c1dfb51c8dc2a4ec06bdaf9762da + react: ^18 || ^19 + react-dom: ^18 || ^19 + checksum: b951638b517e110f09a60f8164d759d29d480132832d574a57b5724ed6887ec728401f6fe9bf00d4a70ec5edb5a7871fc45f18ddeffdecf677b1dbbdddc55b2c languageName: node linkType: hard @@ -10609,9 +10610,9 @@ __metadata: languageName: node linkType: hard -"sass@npm:^1.83.0": - version: 1.83.0 - resolution: "sass@npm:1.83.0" +"sass@npm:^1.83.1": + version: 1.83.1 + resolution: "sass@npm:1.83.1" dependencies: "@parcel/watcher": ^2.4.1 chokidar: ^4.0.0 @@ -10622,7 +10623,7 @@ __metadata: optional: true bin: sass: sass.js - checksum: ab7ba7829b3a53f4cc209dba4b515e48b67fbabfa7afc1195275991a18511e04259c1e1b19bf93d77e6f5c39556e022f2ea2ce68117668f59f6679cd8c58ddd9 + checksum: 367a9f270c74a9ad2851955e1cf5b2a05e57d27aec4bf054be1da48eb49858076467b65ec180d8c4392b5c55c0f4d4ba644855f985652ffca15d68a20641d5e0 languageName: node linkType: hard @@ -12120,12 +12121,12 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^11.0.3": - version: 11.0.3 - resolution: "uuid@npm:11.0.3" +"uuid@npm:^11.0.4": + version: 11.0.4 + resolution: "uuid@npm:11.0.4" bin: uuid: dist/esm/bin/uuid - checksum: 646181c77e8b8df9bd07254faa703943e1c4d5ccde7d080312edf12f443f6c5750801fd9b27bf2e628594182165e6b1b880c0382538f7eca00b26622203741dc + checksum: 88860d20dafa648642581b2e7bb364588149df50d41c812100839aec095593fcc8548fde6929605efd42f556455cf5683e3bad9e9e311f4b62d7efbb68598d3e languageName: node linkType: hard @@ -12331,9 +12332,9 @@ __metadata: languageName: node linkType: hard -"vite@npm:^6.0.6": - version: 6.0.6 - resolution: "vite@npm:6.0.6" +"vite@npm:^6.0.7": + version: 6.0.7 + resolution: "vite@npm:6.0.7" dependencies: esbuild: ^0.24.2 fsevents: ~2.3.3 @@ -12379,7 +12380,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 6786a42805dc7f277462ace1890cdf07562fefd79ef00e15e5e7eca64b3ef90589b768cf44622f4b6fa336ef167e6f7611d0a3cc81fe25798a1eb7e1b3d659b6 + checksum: 0a948bcd33cf1891a1079ccea6d7e2d1e030ed5d1ad86c9de41a5cb2935da50f77ef699a57ea12fa4a332d9def5494e80d75ea6504758d9d1d9a139f3c1c7fbe languageName: node linkType: hard diff --git a/prime-router/metadata/file_name_templates/file-name-templates.yml b/prime-router/metadata/file_name_templates/file-name-templates.yml index d449942f837..8388d9bc672 100644 --- a/prime-router/metadata/file_name_templates/file-name-templates.yml +++ b/prime-router/metadata/file_name_templates/file-name-templates.yml @@ -122,4 +122,8 @@ elements: - GOLDEN-COPY - "-" + - uuid() + +- name: uuid + elements: - uuid() \ No newline at end of file diff --git a/prime-router/settings/STLTs/WA/wa-phd.yml b/prime-router/settings/STLTs/WA/wa-phd.yml index b4c3e2d43b7..5c7ae4ba5f4 100644 --- a/prime-router/settings/STLTs/WA/wa-phd.yml +++ b/prime-router/settings/STLTs/WA/wa-phd.yml @@ -90,6 +90,7 @@ reverseTheQualityFilter: false conditionFilter: #Accept COVID only, "matches(abnormal_flag, A)" + #Verified that Covid only, "matches(abnormal_flag, A)" Abi Philip Jan 6 2025 - "%resource.interpretation.coding.code = 'A' and (%resource.code.coding.extension('https://reportstream.cdc.gov/fhir/StructureDefinition/condition-code').value.where(code in ('840539006')).exists())" mappedConditionFilter: [] deidentify: false diff --git a/prime-router/src/main/kotlin/SettingsProvider.kt b/prime-router/src/main/kotlin/SettingsProvider.kt index ba98f8a168f..7eaa32d3dee 100644 --- a/prime-router/src/main/kotlin/SettingsProvider.kt +++ b/prime-router/src/main/kotlin/SettingsProvider.kt @@ -5,7 +5,6 @@ import com.fasterxml.jackson.annotation.JsonValue import gov.cdc.prime.router.CustomerStatus.ACTIVE import gov.cdc.prime.router.CustomerStatus.INACTIVE import gov.cdc.prime.router.CustomerStatus.TESTING -import gov.cdc.prime.router.fhirengine.utils.HL7Reader import gov.cdc.prime.router.validation.IItemValidator import gov.cdc.prime.router.validation.MarsOtcElrOnboardingValidator import gov.cdc.prime.router.validation.MarsOtcElrValidator @@ -55,7 +54,6 @@ enum class Topic( val isUniversalPipeline: Boolean = true, val isSendOriginal: Boolean = false, val validator: IItemValidator = NoopItemValidator(), - val hl7ParseConfiguration: HL7Reader.Companion.HL7MessageParseAndConvertConfiguration? = null, ) { FULL_ELR("full-elr", true, false), ETOR_TI("etor-ti", true, false), diff --git a/prime-router/src/main/kotlin/SubmissionReceiver.kt b/prime-router/src/main/kotlin/SubmissionReceiver.kt index d9cc88a1e5b..fe819504005 100644 --- a/prime-router/src/main/kotlin/SubmissionReceiver.kt +++ b/prime-router/src/main/kotlin/SubmissionReceiver.kt @@ -9,8 +9,8 @@ import gov.cdc.prime.router.azure.ReportWriter import gov.cdc.prime.router.azure.WorkflowEngine import gov.cdc.prime.router.azure.db.enums.TaskAction import gov.cdc.prime.router.fhirengine.engine.FhirConvertQueueMessage -import gov.cdc.prime.router.fhirengine.engine.MessageType import gov.cdc.prime.router.fhirengine.utils.FhirTranscoder +import gov.cdc.prime.router.fhirengine.utils.HL7MessageHelpers import gov.cdc.prime.router.fhirengine.utils.HL7Reader /** @@ -268,14 +268,14 @@ class UniversalPipelineReceiver : SubmissionReceiver { when (sender.format) { MimeFormat.HL7 -> { - val messages = HL7Reader(actionLogs).getMessages(content) - val isBatch = HL7Reader(actionLogs).isBatch(content, messages.size) - // create a Report for this incoming HL7 message to use for tracking in the database + val messageCount = HL7MessageHelpers.messageCount(content) + val isBatch = HL7Reader.isBatch(content, messageCount) + // create a Report for this incoming HL7 message to use for tracking in the database report = Report( if (isBatch) MimeFormat.HL7_BATCH else MimeFormat.HL7, sources, - messages.size, + messageCount, metadata = metadata, nextAction = TaskAction.convert, topic = sender.topic, @@ -290,11 +290,8 @@ class UniversalPipelineReceiver : SubmissionReceiver { // actionLogs // ) // } - - // check for valid message type - messages.forEachIndexed { - idx, element -> - MessageType.validateMessageType(element, actionLogs, idx + 1) + if (messageCount == 0 && !actionLogs.hasErrors()) { + actionLogs.error(InvalidReportMessage("Unable to find HL7 messages in provided data.")) } } diff --git a/prime-router/src/main/kotlin/azure/BlobAccess.kt b/prime-router/src/main/kotlin/azure/BlobAccess.kt index 8b88fbcf017..8cbe6f1b68e 100644 --- a/prime-router/src/main/kotlin/azure/BlobAccess.kt +++ b/prime-router/src/main/kotlin/azure/BlobAccess.kt @@ -401,19 +401,6 @@ class BlobAccess() : Logging { return binaryData } - /** - * Copy a blob at [fromBlobUrl] to a blob in [blobConnInfo] - */ - fun copyBlob(fromBlobUrl: String, blobConnInfo: BlobContainerMetadata): String { - val fromBytes = downloadBlobAsByteArray(fromBlobUrl) - logger.info("Ready to copy ${fromBytes.size} bytes from $fromBlobUrl") - val toFilename = BlobInfo.getBlobFilename(fromBlobUrl) - logger.info("New blob filename will be $toFilename") - val toBlobUrl = uploadBlob(toFilename, fromBytes, blobConnInfo) - logger.info("New blob URL is $toBlobUrl") - return toBlobUrl - } - /** * Accepts a [BlobItemAndPreviousVersions] and grabs the most recent previous version and updates * the blob to it. diff --git a/prime-router/src/main/kotlin/azure/service/SubmissionResponseBuilder.kt b/prime-router/src/main/kotlin/azure/service/SubmissionResponseBuilder.kt index c01f4a76fbc..eff8dcd02da 100644 --- a/prime-router/src/main/kotlin/azure/service/SubmissionResponseBuilder.kt +++ b/prime-router/src/main/kotlin/azure/service/SubmissionResponseBuilder.kt @@ -5,12 +5,12 @@ import com.google.common.net.HttpHeaders import com.microsoft.azure.functions.HttpRequestMessage import com.microsoft.azure.functions.HttpResponseMessage import com.microsoft.azure.functions.HttpStatus -import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.Sender import gov.cdc.prime.router.azure.HttpUtilities import gov.cdc.prime.router.azure.HttpUtilities.Companion.isSuccessful import gov.cdc.prime.router.common.JacksonMapperUtilities import gov.cdc.prime.router.fhirengine.translation.hl7.utils.HL7ACKUtils +import gov.cdc.prime.router.fhirengine.utils.HL7MessageHelpers import gov.cdc.prime.router.fhirengine.utils.HL7Reader import gov.cdc.prime.router.history.DetailedSubmissionHistory import org.apache.logging.log4j.kotlin.Logging @@ -106,12 +106,11 @@ class SubmissionResponseBuilder( contentType == HttpUtilities.hl7V2MediaType && requestBody != null ) { - val hl7Reader = HL7Reader(ActionLogger()) - val messages = hl7Reader.getMessages(requestBody) - val isBatch = hl7Reader.isBatch(requestBody, messages.size) + val messageCount = HL7MessageHelpers.messageCount(requestBody) + val isBatch = HL7Reader.isBatch(requestBody, messageCount) - if (!isBatch && messages.size == 1) { - val message = messages.first() + if (!isBatch && messageCount == 1) { + val message = HL7Reader.parseHL7Message(requestBody) val acceptAcknowledgementType = HL7Reader.getAcceptAcknowledgmentType(message) val ackResponseRequired = acceptAcknowledgmentTypeRespondValues.contains(acceptAcknowledgementType) if (ackResponseRequired) { diff --git a/prime-router/src/main/kotlin/cli/ProcessFhirCommands.kt b/prime-router/src/main/kotlin/cli/ProcessFhirCommands.kt index c1a91a09b92..d3ecb48bf78 100644 --- a/prime-router/src/main/kotlin/cli/ProcessFhirCommands.kt +++ b/prime-router/src/main/kotlin/cli/ProcessFhirCommands.kt @@ -223,8 +223,8 @@ class ProcessFhirCommands : CliktCommand( (isCli && outputFormat == MimeFormat.HL7.toString()) || ( receiver != null && - (receiver.format == MimeFormat.HL7 || receiver.format == MimeFormat.HL7_BATCH) - ) + (receiver.format == MimeFormat.HL7 || receiver.format == MimeFormat.HL7_BATCH) + ) ) -> { val (bundle2, inputMessage) = convertHl7ToFhir(contents, receiver) @@ -297,7 +297,7 @@ class ProcessFhirCommands : CliktCommand( } } - private fun evaluateReceiverFilters(receiver: Receiver?, messageOrBundle: MessageOrBundle, isCli: Boolean) { + private fun evaluateReceiverFilters(receiver: Receiver?, messageOrBundle: MessageOrBundle, isCli: Boolean) { if (receiver != null && messageOrBundle.bundle != null) { val reportStreamFilters = mutableListOf>() reportStreamFilters.add(Pair("Jurisdictional Filter", receiver.jurisdictionalFilter)) @@ -522,10 +522,8 @@ class ProcessFhirCommands : CliktCommand( // However, the library used to encode the HL7 message throws an error it there are more than 4 encoding // characters, so this work around exists for that scenario val stringToEncode = hl7String.replace("MSH|^~\\&#|", "MSH|^~\\&|") - val hl7message = HL7Reader.parseHL7Message( - stringToEncode, - null - ) + val hl7message = HL7Reader.parseHL7Message(stringToEncode) + // if a hl7 parsing failure happens, throw error and show the message if (hl7message.toString().lowercase().contains("failed")) { throw CliktError("HL7 parser failure. $hl7message") @@ -534,12 +532,8 @@ class ProcessFhirCommands : CliktCommand( val msh = hl7message.get("MSH") as Segment Terser.set(msh, 2, 0, 1, 1, "^~\\&#") } - val hl7profile = HL7Reader.getMessageProfile(hl7message.toString()) // search hl7 profile map and create translator with config path if found - var fhirMessage = when (val configPath = HL7Reader.profileDirectoryMap[hl7profile]) { - null -> HL7toFhirTranslator(inputSchema).translate(hl7message) - else -> HL7toFhirTranslator(configPath).translate(hl7message) - } + var fhirMessage = HL7toFhirTranslator(inputSchema).translate(hl7message) val stamper = ConditionStamper(LookupTableConditionMapper(Metadata.getInstance())) fhirMessage.getObservations().forEach { observation -> diff --git a/prime-router/src/main/kotlin/cli/ProcessHl7Commands.kt b/prime-router/src/main/kotlin/cli/ProcessHl7Commands.kt index 1f89270217f..9c23ab1c9ec 100644 --- a/prime-router/src/main/kotlin/cli/ProcessHl7Commands.kt +++ b/prime-router/src/main/kotlin/cli/ProcessHl7Commands.kt @@ -1,11 +1,11 @@ package gov.cdc.prime.router.cli +import ca.uhn.hl7v2.util.Hl7InputStreamMessageStringIterator import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.CliktError import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.options.required import com.github.ajalt.clikt.parameters.types.file -import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.cli.helpers.HL7DiffHelper import gov.cdc.prime.router.fhirengine.utils.HL7Reader @@ -45,9 +45,14 @@ class ProcessHl7Commands : CliktCommand( val comparisonFile = comparisonFile.inputStream().readBytes().toString(Charsets.UTF_8) if (comparisonFile.isBlank()) throw CliktError("File ${this.comparisonFile.absolutePath} is empty.") - val actionLogger = ActionLogger() - val starterMessages = HL7Reader(actionLogger).getMessages(starterFile) - val comparisonMessages = HL7Reader(actionLogger).getMessages(comparisonFile) + val starterMessages = Hl7InputStreamMessageStringIterator(starterFile.byteInputStream()).asSequence() + .map { rawItem -> + HL7Reader.parseHL7Message(rawItem) + }.toList() + val comparisonMessages = Hl7InputStreamMessageStringIterator(comparisonFile.byteInputStream()).asSequence() + .map { rawItem -> + HL7Reader.parseHL7Message(rawItem) + }.toList() starterMessages.forEachIndexed { counter, message -> val differences = hl7DiffHelper.diffHl7(message, comparisonMessages[counter]) diff --git a/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt b/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt index 53a4242dfc4..0ce0eb45ab7 100644 --- a/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt +++ b/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt @@ -50,11 +50,9 @@ import gov.cdc.prime.router.fhirengine.translation.hl7.FhirTransformer 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.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 @@ -397,7 +395,7 @@ class FHIRConverter( ) } - FHIREngineRunResult( + FHIREngineRunResult( routeEvent, report, blobInfo.blobUrl, @@ -427,7 +425,7 @@ class FHIRConverter( report, TaskAction.convert, "Submitted report was either empty or could not be parsed into HL7" - ) { + ) { parentReportId(input.reportId) params( mapOf( @@ -479,7 +477,7 @@ class FHIRConverter( "format" to format.name ) ) { - getBundlesFromRawHL7(rawReport, validator, input.topic.hl7ParseConfiguration) + getBundlesFromRawHL7(rawReport, validator) } } catch (ex: ParseFailureError) { actionLogger.error( @@ -571,7 +569,6 @@ class FHIRConverter( private fun getBundlesFromRawHL7( rawReport: String, validator: IItemValidator, - hL7MessageParseAndConvertConfiguration: HL7Reader.Companion.HL7MessageParseAndConvertConfiguration?, ): List> { val itemStream = Hl7InputStreamMessageStringIterator(rawReport.byteInputStream()).asSequence() @@ -580,17 +577,16 @@ class FHIRConverter( }.toList() return maybeParallelize(itemStream.size, itemStream.stream(), "Generating FHIR bundles in").map { item -> - parseHL7Item(item, hL7MessageParseAndConvertConfiguration) + parseHL7Item(item) }.map { item -> - validateAndConvertHL7Item(item, validator, hL7MessageParseAndConvertConfiguration) + validateAndConvertHL7Item(item, validator) }.collect(Collectors.toList()) } private fun parseHL7Item( item: ProcessedHL7Item, - hL7MessageParseAndConvertConfiguration: HL7Reader.Companion.HL7MessageParseAndConvertConfiguration?, ) = try { - val message = parseHL7Message(item.rawItem, hL7MessageParseAndConvertConfiguration) + val message = parseHL7Message(item.rawItem) item.updateParsed(message) } catch (e: HL7Exception) { item.updateParsed( @@ -605,20 +601,11 @@ class FHIRConverter( private fun validateAndConvertHL7Item( item: ProcessedHL7Item, validator: IItemValidator, - hL7MessageParseAndConvertConfiguration: HL7Reader.Companion.HL7MessageParseAndConvertConfiguration?, ): ProcessedHL7Item = if (item.parsedItem != null) { val validationResult = validator.validate(item.parsedItem) if (validationResult.isValid()) { try { - val bundle = when (hL7MessageParseAndConvertConfiguration) { - null -> HL7toFhirTranslator.getHL7ToFhirTranslatorInstance().translate(item.parsedItem) - else -> - HL7toFhirTranslator - .getHL7ToFhirTranslatorInstance( - hL7MessageParseAndConvertConfiguration.hl7toFHIRMappingLocation - ) - .translate(item.parsedItem) - } + val bundle = HL7toFhirTranslator.getHL7ToFhirTranslatorInstance().translate(item.parsedItem) item.setBundle(bundle) } catch (ex: Exception) { item.setConversionError( @@ -759,13 +746,13 @@ class FHIRConverter( * transformer in tests. */ fun getTransformerFromSchema(schemaName: String): FhirTransformer? = if (schemaName.isNotBlank()) { - withLoggingContext(mapOf("schemaName" to schemaName)) { - logger.info("Apply a sender transform to the items in the report") - } - FhirTransformer(schemaName) - } else { - null + withLoggingContext(mapOf("schemaName" to schemaName)) { + logger.info("Apply a sender transform to the items in the report") } + FhirTransformer(schemaName) + } else { + null + } } /** diff --git a/prime-router/src/main/kotlin/fhirengine/translation/TranslationSchemaManager.kt b/prime-router/src/main/kotlin/fhirengine/translation/TranslationSchemaManager.kt index 8890f198b63..b5c97da7463 100644 --- a/prime-router/src/main/kotlin/fhirengine/translation/TranslationSchemaManager.kt +++ b/prime-router/src/main/kotlin/fhirengine/translation/TranslationSchemaManager.kt @@ -1,9 +1,9 @@ package gov.cdc.prime.router.fhirengine.translation +import ca.uhn.hl7v2.util.Hl7InputStreamMessageStringIterator import com.azure.storage.blob.models.BlobItem import fhirengine.engine.CustomFhirPathFunctions import fhirengine.engine.CustomTranslationFunctions -import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.Hl7Configuration import gov.cdc.prime.router.azure.BlobAccess import gov.cdc.prime.router.fhirengine.config.HL7TranslationConfig @@ -41,7 +41,6 @@ class TranslationSchemaManager : Logging { Regex("/$previousValidBlobName-$timestampRegex") private val previousPreviousValidBlobNameRegex = Regex("/$previousPreviousValidBlobName-$timestampRegex") - private val hL7Reader = HL7Reader(ActionLogger()) /** * Container class that holds the current state for a schema type in a particular azure store. @@ -440,7 +439,11 @@ class TranslationSchemaManager : Logging { ) ).validate( inputBundle, - hL7Reader.getMessages(rawValidationInput.output)[0] + HL7Reader.parseHL7Message( + Hl7InputStreamMessageStringIterator(rawValidationInput.output.byteInputStream()) + .asSequence() + .first() + ), ) } } diff --git a/prime-router/src/main/kotlin/fhirengine/utils/HL7MessageHelpers.kt b/prime-router/src/main/kotlin/fhirengine/utils/HL7MessageHelpers.kt index 6b05e746c36..cbd8b1b4700 100644 --- a/prime-router/src/main/kotlin/fhirengine/utils/HL7MessageHelpers.kt +++ b/prime-router/src/main/kotlin/fhirengine/utils/HL7MessageHelpers.kt @@ -1,6 +1,8 @@ package gov.cdc.prime.router.fhirengine.utils +import ca.uhn.hl7v2.AbstractHL7Exception import ca.uhn.hl7v2.model.v251.datatype.DTM +import ca.uhn.hl7v2.util.Hl7InputStreamMessageStringIterator import ca.uhn.hl7v2.util.Terser import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.Hl7Configuration @@ -24,6 +26,8 @@ object HL7MessageHelpers : Logging { */ const val hl7SegmentDelimiter = "\r" + val actionLogger = ActionLogger() + /** * Generate a HL7 Batch file from the list of [hl7RawMsgs] for the given [receiver]. The [hl7RawMsgs] are expected * to be real HL7 messages at this point, so we will not validate their contents here for performance reasons. @@ -34,12 +38,17 @@ object HL7MessageHelpers : Logging { val useBatchHeaders = receiver.translation.useBatchHeaders // Grab the first message to extract some data if not set in the settings val firstMessage = if (hl7RawMsgs.isNotEmpty()) { - val messages = HL7Reader(ActionLogger()).getMessages(hl7RawMsgs[0]) - if (messages.isEmpty()) { + try { + val message = HL7Reader.parseHL7Message(hl7RawMsgs[0]) + Terser(message) + } catch (exception: Hl7InputStreamMessageStringIterator.ParseFailureError) { + logger.warn("Unable to extract batch header values from HL7: ${hl7RawMsgs[0].take(80)} ...") + HL7Reader.logHL7ParseFailure(exception, actionLogger) + null + } catch (exception: AbstractHL7Exception) { logger.warn("Unable to extract batch header values from HL7: ${hl7RawMsgs[0].take(80)} ...") + HL7Reader.recordError(exception, actionLogger) null - } else { - Terser(messages[0]) } } else { null @@ -94,4 +103,8 @@ object HL7MessageHelpers : Logging { return builder.toString() } + + fun messageCount(rawHl7: String): Int { + return Hl7InputStreamMessageStringIterator(rawHl7.byteInputStream()).asSequence().count() + } } \ No newline at end of file diff --git a/prime-router/src/main/kotlin/fhirengine/utils/HL7Reader.kt b/prime-router/src/main/kotlin/fhirengine/utils/HL7Reader.kt index aa48c011d8f..698b41c7697 100644 --- a/prime-router/src/main/kotlin/fhirengine/utils/HL7Reader.kt +++ b/prime-router/src/main/kotlin/fhirengine/utils/HL7Reader.kt @@ -5,10 +5,8 @@ import ca.uhn.hl7v2.DefaultHapiContext import ca.uhn.hl7v2.ErrorCode import ca.uhn.hl7v2.HL7Exception import ca.uhn.hl7v2.HapiContext -import ca.uhn.hl7v2.model.AbstractMessage import ca.uhn.hl7v2.model.Message import ca.uhn.hl7v2.parser.ParserConfiguration -import ca.uhn.hl7v2.util.Hl7InputStreamMessageIterator import ca.uhn.hl7v2.util.Hl7InputStreamMessageStringIterator import ca.uhn.hl7v2.util.Terser import ca.uhn.hl7v2.validation.ValidationException @@ -21,13 +19,10 @@ import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.InvalidReportMessage import org.apache.commons.lang3.exception.ExceptionUtils import org.apache.logging.log4j.Level -import org.apache.logging.log4j.kotlin.Logging +import org.apache.logging.log4j.kotlin.logger import java.util.Date -import ca.uhn.hl7v2.model.v251.message.ORU_R01 as v251_ORU_R01 import ca.uhn.hl7v2.model.v251.segment.MSH as v251_MSH -import ca.uhn.hl7v2.model.v27.message.ORU_R01 as v27_ORU_R01 import ca.uhn.hl7v2.model.v27.segment.MSH as v27_MSH -import fhirengine.translation.hl7.structures.nistelr251.message.ORU_R01 as NIST_ELR_ORU_R01 import fhirengine.translation.hl7.structures.nistelr251.segment.MSH as NIST_MSH private const val MSH_SEGMENT_NAME = "MSH" @@ -35,184 +30,12 @@ private const val MSH_SEGMENT_NAME = "MSH" /** * Converts raw HL7 data (message or batch) to HL7 message objects. */ -class HL7Reader(private val actionLogger: ActionLogger) : Logging { - - /** - * Returns one or more messages read from the raw HL7 data. - * - * This function takes a couple of different approaches to transforming the raw string into messages. - * - * First, it will read the message type from MSH.9 and attempt to find the list of mapped MessageModels. - * See [getMessageModelClasses]. These mappings will typically consist of the v27 structure and the v25 structure - * for that message type. If models are found, the code will iterate over the models and attempt to parse the - * message. If messages are parsed, loop short circuits. - * - * The reason we need to use multiple message models is due to inconsistencies of the specs across different - * organizations. For example, the NIST profile for v251 includes fields that are only available in the v27 - * standard spec. To get around this fact, we take advantage that the specs are mostly backwards compatible; - * a NIST v251 can be parsed using the v271 structure successfully and will now also include the data from the - * fields only available in the standard v27. The only caveat to this approach is that the HAPI library itself - * is not 100% backwards compatible. A common error is that a v251 message will specify a component is a CE, but - * the v27 spec says it must be a CWE; though these two data types are compatible from a field standpoint, the HAPI - * library will throw a type error along the lines of "a CWE field cannot be set to a CE type". To get around this - * issue, if the message cannot be parsed to v27 we fall back to parsing it as a v251 message. - * - * - * If no message models are returned by [getMessageModelClasses], the string is parsed using the default behavior - * of [Hl7InputStreamMessageIterator]. - * - * - * @return one or more HL7 messages - * @throws IllegalArgumentException if the raw data cannot be parsed or no messages were read - */ - fun getMessages(rawMessage: String): List { - val messageModelsToTry = getMessageModelClasses(rawMessage) - val messages: MutableList = mutableListOf() - if (rawMessage.isBlank()) { - actionLogger.error(InvalidReportMessage("Provided raw data is empty.")) - } else if (messageModelsToTry.isEmpty()) { - try { - val iterator = Hl7InputStreamMessageIterator(rawMessage.byteInputStream()) - while (iterator.hasNext()) { - messages.add(iterator.next()) - } - } catch (e: Hl7InputStreamMessageStringIterator.ParseFailureError) { - logHL7ParseFailure(e) - } - } else { - val validationContext = ValidationContextFactory.noValidation() - val parseError = mutableListOf() - run modelLoop@{ - messageModelsToTry.forEach { model -> - val context = DefaultHapiContext(ReportStreamCanonicalModelClassFactory(model)) - context.validationContext = validationContext - try { - val iterator = Hl7InputStreamMessageIterator(rawMessage.byteInputStream(), context) - while (iterator.hasNext()) { - messages.add(iterator.next()) - } - } catch (e: Hl7InputStreamMessageStringIterator.ParseFailureError) { - messages.clear() - parseError.add(e) - } - - if (messages.isNotEmpty()) { - // Don't try other message models if we were able to parse - return@modelLoop - } - } - } - - // if it was able to parse the message through one of the models, then we do not want to log it as an error - val parseLogLevel = if (parseError.size == messageModelsToTry.size) Level.ERROR else Level.WARN - parseError.forEach { currentError -> - logHL7ParseFailure(currentError, messages.isEmpty(), parseLogLevel) - } - } - - if (messages.isEmpty() && !actionLogger.hasErrors()) { - actionLogger.error(InvalidReportMessage("Unable to find HL7 messages in provided data.")) - } - - return messages - } - - /** - * Extracts the message type from the MSH segment and returns the list of message models to use to - * try to parse the messages. - * - * This function assumes all the message types will be the same if this is a HL7 batch. - */ - private fun getMessageModelClasses(rawMessage: String): List> { - try { - val messageProfile = getMessageProfile(rawMessage) - if (messageProfile != null) { - when (messageProfile.typeID) { - "ORU" -> { - return when (messageProfile.profileID) { - // TODO: NIST ELR conformance profile to be enabled in a future PR (rename to "NIST_ELR") - "NIST_ELR_TEST" -> listOf( - NIST_ELR_ORU_R01::class.java - ) - else -> listOf( - v27_ORU_R01::class.java, - v251_ORU_R01::class.java - ) - } - } - else -> { - logger.warn( - "${messageProfile.typeID} did not have any mapped message model classes, " + - "using default behavior" - ) - return emptyList() - } - } - } - } catch (ex: Hl7InputStreamMessageStringIterator.ParseFailureError) { - logHL7ParseFailure(ex) - return emptyList() - } - actionLogger.error(InvalidReportMessage("String did not contain any HL7 messages")) - return emptyList() - } - - /** - * Takes a [rawMessage] and the number of messages [numMessages] in the rawMessage and determines if it is a batch - * or singular HL7 message. It will qualify as a batch message if it follows the HL7 standards and have the Hl7 - * batch headers which start with "FHS" or if they left off the batch headers and just sent multiple messages - */ - fun isBatch(rawMessage: String, numMessages: Int): Boolean { - return rawMessage.startsWith("FHS") || numMessages > 1 - } - - /** - * Takes an [exception] thrown by the HL7 HAPI library, gets the root cause and logs the error into [actionLogger]. - * Sample error messages returned by the HAPI library are: - * Error Code = DATA_TYPE_ERROR-102: 'test' in record 3 is invalid for version 2.5.1 - * Error Code = REQUIRED_FIELD_MISSING-101: Can't find version ID - MSH.12 is null - * This functions only logs messages that contain meaningful data. - * - */ - private fun logHL7ParseFailure( - exception: Hl7InputStreamMessageStringIterator.ParseFailureError, - isError: Boolean = true, - logLevel: Level = Level.ERROR, - ) { - logger.log(logLevel, "Failed to parse message: ${exception.message}") - - // Get the exception root cause and log it accordingly - when (val rootCause = ExceptionUtils.getRootCause(exception)) { - is AbstractHL7Exception -> recordError(rootCause, isError) - else -> throw rootCause - } - } - - private fun recordError(exception: AbstractHL7Exception, isError: Boolean) { - val errorMessage: String = when (exception) { - is ValidationException -> "Validation Failed: ${exception.message}" - - is HL7Exception -> { - when (exception.errorCode) { - ErrorCode.REQUIRED_FIELD_MISSING.code -> "Required field missing: ${exception.message}" - ErrorCode.DATA_TYPE_ERROR.code -> "Data type error: ${exception.message}" - else -> "Failed to parse message" - } - } - - else -> "Failed to parse message" - } - if (isError) { - actionLogger.error(InvalidReportMessage(errorMessage)) - } else { - actionLogger.warn(InvalidReportMessage(errorMessage)) - } - } - +class HL7Reader { companion object { // This regex is used to replace \n with \r while not replacing \r\n val newLineRegex = Regex("(?, - val hl7toFHIRMappingLocation: String, - ) - - /** - * Map of configured message types to their configuration - */ - val messageToConfigMap = mapOf( - HL7MessageType( - "ORU_R01", - "2.5.1", - "2.16.840.1.113883.9.10" - ) to HL7MessageParseAndConvertConfiguration( - ORU_R01::class.java, - "./metadata/HL7/catchall" - ), - HL7MessageType( - "ORU_R01", - "2.5.1", - "2.16.840.1.113883.9.11" - ) to HL7MessageParseAndConvertConfiguration( - ORU_R01::class.java, - "./metadata/HL7/catchall" - ) - ) - - // TODO: https://github.com/CDCgov/prime-reportstream/issues/14116 /** * Accepts a raw HL7 string and uses the MSH segment to detect the [HL7MessageType] which is then used - * to parse the string into an instance of [Message]. If the type is not one that is configured in - * [messageToConfigMap] the default HAPI parsing logic is used + * to parse the string into an instance of [Message]. If the type is not one that is supported in + * [getHL7ParsingContext] the default HAPI parsing logic is used * * @param rawHL7 the HL7 string to convert into a [Message] * - * @return a [Pair] with parsed message and optional type + * @return a [Message] with parsed message and optional type */ fun parseHL7Message( rawHL7: String, - parseConfiguration: HL7MessageParseAndConvertConfiguration?, ): Message { // A carriage return is the official segment delimiter; a newline is not recognized so we replace // them val carriageReturnFixedHL7 = rawHL7.replace(newLineRegex, "\r") val hl7MessageType = getMessageType(carriageReturnFixedHL7) - return getHL7ParsingContext(hl7MessageType, parseConfiguration).pipeParser.parse(carriageReturnFixedHL7) + return getHL7ParsingContext(hl7MessageType).pipeParser.parse(carriageReturnFixedHL7) } /** * Creates a HAPI context that can be used to parse an HL7 string. If no configuration is passed, the function * will return a context with the HAPI defaults which will defer to that library to determine the kind of message * - * @param hl7MessageParseAndConvertConfiguration optional configuration to use when creating a context */ private fun getHL7ParsingContext( hl7MessageType: HL7MessageType?, - hl7MessageParseAndConvertConfiguration: HL7MessageParseAndConvertConfiguration?, ): HapiContext { - return if (hl7MessageParseAndConvertConfiguration == null) { - if (hl7MessageType?.msh93 == "ORU_R01") { + return when (hl7MessageType?.msh93) { + "ORU_R01" -> { DefaultHapiContext( ParserConfiguration(), ValidationContextFactory.noValidation(), ReportStreamCanonicalModelClassFactory(ORU_R01::class.java), ) - } else if (hl7MessageType?.msh93 == "OML_O21") { + } + "OML_O21" -> { DefaultHapiContext( ParserConfiguration(), ValidationContextFactory.noValidation(), ReportStreamCanonicalModelClassFactory(OML_O21::class.java), ) - } else if (hl7MessageType?.msh93 == "ORM_O01") { + } + "ORM_O01" -> { DefaultHapiContext( ParserConfiguration(), ValidationContextFactory.noValidation(), ReportStreamCanonicalModelClassFactory(ORM_O01::class.java), ) - } else { + } + else -> { DefaultHapiContext(ValidationContextFactory.noValidation()) } - } else { - DefaultHapiContext( - ParserConfiguration(), - ValidationContextFactory.noValidation(), - ReportStreamCanonicalModelClassFactory(hl7MessageParseAndConvertConfiguration.messageModelClass), - ) } } @@ -335,7 +117,7 @@ class HL7Reader(private val actionLogger: ActionLogger) : Logging { */ @Throws(HL7Exception::class) internal fun getMessageType(rawHL7: String): HL7MessageType { - val message = getHL7ParsingContext(null, null) + val message = getHL7ParsingContext(null) .pipeParser // In order to determine the message configuration, only parse the MSH segment since the type of message // is required in order to accurately parse the message in its entirety @@ -349,19 +131,6 @@ class HL7Reader(private val actionLogger: ActionLogger) : Logging { ) } - // map of HL7 message profiles: maps profile to configuration directory path - val profileDirectoryMap: Map = mapOf( - // TODO: https://github.com/CDCgov/prime-reportstream/issues/14124 - // Pair(MessageProfile("ORU", "NIST_ELR"), "./metadata/HL7/v251-elr"), - ) - - // map of HL7 OIDs to supported conformance profiles - // list of OIDs for NIST ELR retrieved from https://oidref.com/2.16.840.1.113883.9 - private val oidProfileMap: Map = mapOf( - Pair("2.16.840.1.113883.9.10", "NIST_ELR"), - Pair("2.16.840.1.113883.9.11", "NIST_ELR") - ) - // data class to uniquely identify a message profile data class MessageProfile(val typeID: String, val profileID: String) @@ -391,21 +160,6 @@ class HL7Reader(private val actionLogger: ActionLogger) : Logging { } } - /** - * Get the profile of the [rawmessage] - * If there are multiple HL7 messages the first message's data will be returned - * @param rawmessage string representative of hl7 messages - * @return the message profile, or null if there is no message - */ - fun getMessageProfile(rawmessage: String): MessageProfile? { - val iterator = Hl7InputStreamMessageIterator(rawmessage.byteInputStream()) - if (!iterator.hasNext()) return null - val hl7message = iterator.next() - val msh9 = Terser(hl7message).get("MSH-9") - val profileID = oidProfileMap[Terser(hl7message).get("MSH-21-3")] ?: "" - return MessageProfile(msh9 ?: "", profileID) - } - /** * Get the birthTime from the [message] * @return the birthTime, if available or blank if not @@ -480,5 +234,53 @@ class HL7Reader(private val actionLogger: ActionLogger) : Logging { else -> null } } + + /** + * Takes a [rawMessage] and the number of messages [numMessages] in the rawMessage and determines if it is a batch + * or singular HL7 message. It will qualify as a batch message if it follows the HL7 standards and have the Hl7 + * batch headers which start with "FHS" or if they left off the batch headers and just sent multiple messages + */ + fun isBatch(rawMessage: String, numMessages: Int): Boolean { + return rawMessage.startsWith("FHS") || numMessages > 1 + } + + /** + * Takes an [exception] thrown by the HL7 HAPI library, gets the root cause and logs the error into [actionLogger]. + * Sample error messages returned by the HAPI library are: + * Error Code = DATA_TYPE_ERROR-102: 'test' in record 3 is invalid for version 2.5.1 + * Error Code = REQUIRED_FIELD_MISSING-101: Can't find version ID - MSH.12 is null + * This functions only logs messages that contain meaningful data. + * + */ + fun logHL7ParseFailure( + exception: Hl7InputStreamMessageStringIterator.ParseFailureError, + actionLogger: ActionLogger, + logLevel: Level = Level.ERROR, + ) { + logger.log(logLevel, "Failed to parse message: ${exception.message}") + + // Get the exception root cause and log it accordingly + when (val rootCause = ExceptionUtils.getRootCause(exception)) { + is AbstractHL7Exception -> recordError(rootCause, actionLogger) + else -> throw rootCause + } + } + + fun recordError(exception: AbstractHL7Exception, actionLogger: ActionLogger) { + val errorMessage: String = when (exception) { + is ValidationException -> "Validation Failed: ${exception.message}" + + is HL7Exception -> { + when (exception.errorCode) { + ErrorCode.REQUIRED_FIELD_MISSING.code -> "Required field missing: ${exception.message}" + ErrorCode.DATA_TYPE_ERROR.code -> "Data type error: ${exception.message}" + else -> "Failed to parse message" + } + } + + else -> "Failed to parse message" + } + actionLogger.error(InvalidReportMessage(errorMessage)) + } } } \ No newline at end of file diff --git a/prime-router/src/main/kotlin/transport/BlobStoreTransport.kt b/prime-router/src/main/kotlin/transport/BlobStoreTransport.kt index ea372653d59..2f3dfe9e6dd 100644 --- a/prime-router/src/main/kotlin/transport/BlobStoreTransport.kt +++ b/prime-router/src/main/kotlin/transport/BlobStoreTransport.kt @@ -31,7 +31,11 @@ class BlobStoreTransport : ITransport { val receiver = header.receiver ?: error("No receiver defined for report ${header.reportFile.reportId}") val bodyUrl = header.reportFile.bodyUrl ?: error("Report ${header.reportFile.reportId} has no blob to copy") context.logger.info("About to copy $bodyUrl to $envVar:$storageName") - val newUrl = BlobAccess.copyBlob(bodyUrl, BlobAccess.BlobContainerMetadata.build(transportType)) + val blobConnection = BlobAccess.BlobContainerMetadata.build(transportType) + val blobFile = BlobAccess.downloadBlobAsByteArray(bodyUrl) + context.logger.info("New blob filename will be $externalFileName") + val newUrl = BlobAccess.uploadBlob(externalFileName, blobFile, blobConnection) + context.logger.info("New blob URL is $newUrl") val msg = "Successfully copied $bodyUrl to $newUrl" context.logger.info(msg) actionHistory.trackActionResult(msg) diff --git a/prime-router/src/main/resources/metadata/hl7_mapping/receivers/STLTs/WA/WA-receiver-transform.yml b/prime-router/src/main/resources/metadata/hl7_mapping/receivers/STLTs/WA/WA-receiver-transform.yml index 8d233b16b8d..c4fc5487234 100644 --- a/prime-router/src/main/resources/metadata/hl7_mapping/receivers/STLTs/WA/WA-receiver-transform.yml +++ b/prime-router/src/main/resources/metadata/hl7_mapping/receivers/STLTs/WA/WA-receiver-transform.yml @@ -42,6 +42,18 @@ elements: value: [ '"ISO"' ] hl7Spec: [ '%{MSH}-3-3' ] + - name: wa-sending-facility-namespace-id + value: [ '"7uycso49"' ] + hl7Spec: [ '%{MSH}-4-1' ] + + - name: wa-sending-facility-universal-id + value: [ '"1.3.6.1.4.1.38630.2.1.1.519"' ] + hl7Spec: [ '%{MSH}-4-2' ] + + - name: wa-sending-facility-universal-id-type + value: [ '"ISO"' ] + hl7Spec: [ '%{MSH}-4-3' ] + - name: wa-receiving-application-namespace-id value: [ '"WADOHPHRED"' ] hl7Spec: [ '%{MSH}-5-1' ] diff --git a/prime-router/src/test/kotlin/azure/BlobAccessTests.kt b/prime-router/src/test/kotlin/azure/BlobAccessTests.kt index bdda5a8abdd..11a3a4d52b2 100644 --- a/prime-router/src/test/kotlin/azure/BlobAccessTests.kt +++ b/prime-router/src/test/kotlin/azure/BlobAccessTests.kt @@ -704,38 +704,6 @@ class BlobAccessTests { assertThat(resultBinaryData.toString()).isEqualTo(expectedResult) } - @Test - fun `copy blob`() { - mockkClass(BlobAccess::class) - mockkObject(BlobAccess.Companion) - every { BlobAccess.Companion.getBlobConnection(any()) } returns "testconnection" - - val testUrl = "http://testurl/testfile" - val testFile = BlobAccess.BlobInfo.getBlobFilename(testUrl) - val testBlobMetadata = BlobAccess.BlobContainerMetadata.build("testcontainer", "testenvvar") - - every { BlobAccess.Companion.downloadBlobAsByteArray(testUrl) }.returns("testblob".toByteArray()) - every { - BlobAccess.Companion.uploadBlob( - testFile, - "testblob".toByteArray(), - testBlobMetadata - ) - }.returns("http://testurl2") - - val result = BlobAccess.copyBlob(testUrl, testBlobMetadata) - - verify(exactly = 1) { BlobAccess.Companion.downloadBlobAsByteArray(testUrl, any(), any()) } - verify(exactly = 1) { - BlobAccess.Companion.uploadBlob( - testFile, - "testblob".toByteArray(), - testBlobMetadata - ) - } - assertThat(result).isEqualTo("http://testurl2") - } - @Test fun `delete blob`() { val testUrl = "http://deleteblob" diff --git a/prime-router/src/test/kotlin/cli/helpers/HL7DiffHelperTests.kt b/prime-router/src/test/kotlin/cli/helpers/HL7DiffHelperTests.kt index f17ab0631d3..07e2b485888 100644 --- a/prime-router/src/test/kotlin/cli/helpers/HL7DiffHelperTests.kt +++ b/prime-router/src/test/kotlin/cli/helpers/HL7DiffHelperTests.kt @@ -11,13 +11,12 @@ import ca.uhn.hl7v2.model.Varies import ca.uhn.hl7v2.model.v27.datatype.ID import ca.uhn.hl7v2.model.v27.datatype.NM import ca.uhn.hl7v2.model.v27.datatype.ST -import ca.uhn.hl7v2.model.v27.message.ORU_R01 -import gov.cdc.prime.router.ActionLogger +import fhirengine.translation.hl7.structures.fhirinventory.message.ORU_R01 import gov.cdc.prime.router.fhirengine.utils.HL7Reader import kotlin.test.Test class HL7DiffHelperTests { - private val hL7DiffHelper = HL7DiffHelper() + private val hl7DiffHelper = HL7DiffHelper() private val originalMessage = "MSH|^~\\&#|STARLIMS.CDC.Stag^2.16.840.1.114222.4.3.3.2.1.2^ISO|CDC Atlanta2^" + "11D0668319^CLIA|MEDSS-ELR ^2.16.840.1.114222.4.3.3.6.2.1^ISO|MNDOH^2.16.840.1.114222.4.1.3661^ISO|" + "20230501102531-0400||ORU^R01^ORU_R01|3003786103_4988249_33033|T|2.5.1|||NE|NE|USA||||" + @@ -107,25 +106,21 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- @Test fun `diff hl7`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val inputMessage = hl7Reader.getMessages(originalMessage) - val outputMessage = hl7Reader.getMessages(comparisonMessage) - val differences = hL7DiffHelper.diffHl7(inputMessage[0], outputMessage[0]) + val inputMessage = HL7Reader.parseHL7Message(originalMessage) + val outputMessage = HL7Reader.parseHL7Message(comparisonMessage) + val differences = hl7DiffHelper.diffHl7(inputMessage, outputMessage) assertThat(differences.size).isEqualTo(15) - val differences2 = hL7DiffHelper.diffHl7(outputMessage[0], inputMessage[0]) + val differences2 = hl7DiffHelper.diffHl7(outputMessage, inputMessage) assertThat(differences2.size).isEqualTo(15) } @Test fun `test index structure`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val outputMessage = hl7Reader.getMessages(comparisonMessage) - val outputNames = outputMessage[0].names + val outputMessage = HL7Reader.parseHL7Message(comparisonMessage) + val outputNames = outputMessage.names val outputMap: MutableMap = mutableMapOf() - hL7DiffHelper.filterNames(outputMessage[0], outputNames, outputMap) + hl7DiffHelper.filterNames(outputMessage, outputNames, outputMap) assertThat(outputMap.size).isEqualTo(9) assertThat( @@ -138,16 +133,14 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- @Test fun `test compareHl7Type primitive`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val inputMessage = hl7Reader.getMessages(originalMessage) - val outputMessage = hl7Reader.getMessages(comparisonMessage) - val inputVal = ST(inputMessage[0]) + val inputMessage = HL7Reader.parseHL7Message(originalMessage) + val outputMessage = HL7Reader.parseHL7Message(comparisonMessage) + val inputVal = ST(inputMessage) inputVal.value = "blah" - val outputVal = ST(outputMessage[0]) + val outputVal = ST(outputMessage) outputVal.value = "blah" - val samePrimitive = hL7DiffHelper.compareHl7Type( + val samePrimitive = hl7DiffHelper.compareHl7Type( "", inputVal, outputVal, @@ -160,7 +153,7 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- assertThat(samePrimitive).isEmpty() outputVal.value = "test" - val differentPrimitive = hL7DiffHelper.compareHl7Type( + val differentPrimitive = hl7DiffHelper.compareHl7Type( "", inputVal, outputVal, @@ -175,21 +168,19 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- @Test fun `test compareHl7Type varies`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val inputMessage = hl7Reader.getMessages(originalMessage) - val outputMessage = hl7Reader.getMessages(comparisonMessage) - val inputType = ST(inputMessage[0]) + val inputMessage = HL7Reader.parseHL7Message(originalMessage) + val outputMessage = HL7Reader.parseHL7Message(comparisonMessage) + val inputType = ST(inputMessage) inputType.value = "blah" - val outputType = ST(outputMessage[0]) + val outputType = ST(outputMessage) outputType.value = "blah" - val inputVal = Varies(inputMessage[0]) + val inputVal = Varies(inputMessage) inputVal.data = inputType - val outputVal = Varies(outputMessage[0]) + val outputVal = Varies(outputMessage) outputVal.data = outputType - val sameVaries = hL7DiffHelper.compareHl7Type( + val sameVaries = hl7DiffHelper.compareHl7Type( "", inputVal, outputVal, @@ -202,7 +193,7 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- assertThat(sameVaries).isEmpty() outputType.value = "test" - val differentVaries = hL7DiffHelper.compareHl7Type( + val differentVaries = hl7DiffHelper.compareHl7Type( "", inputVal, outputVal, @@ -217,17 +208,15 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- @Test fun `test compareHl7Type composite`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val inputMessage = hl7Reader.getMessages(originalMessage) - val id = ID(inputMessage[0]) + val inputMessage = HL7Reader.parseHL7Message(originalMessage) + val id = ID(inputMessage) id.value = "blah" - val nm = NM(inputMessage[0]) + val nm = NM(inputMessage) nm.value = "blah2" - val sameComposite = hL7DiffHelper.compareHl7Type( + val sameComposite = hl7DiffHelper.compareHl7Type( "", - (inputMessage[0] as ORU_R01).msh.getField(4)[0], - (inputMessage[0] as ORU_R01).msh.getField(4)[0], + (inputMessage as ORU_R01).msh.getField(4)[0], + inputMessage.msh.getField(4)[0], "", 0, 0, @@ -235,11 +224,11 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- ) assertThat(sameComposite).isEmpty() - val outputMessage = hl7Reader.getMessages(comparisonMessage) - val differentComposite = hL7DiffHelper.compareHl7Type( + val outputMessage = HL7Reader.parseHL7Message(comparisonMessage) + val differentComposite = hl7DiffHelper.compareHl7Type( "", - (inputMessage[0] as ORU_R01).msh.getField(4)[0], - (outputMessage[0] as ORU_R01).msh.getField(4)[0], + inputMessage.msh.getField(4)[0], + (outputMessage as ORU_R01).msh.getField(4)[0], "", 0, 0, @@ -250,19 +239,17 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- @Test fun `test compareHl7Type different types`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val inputMessage = hl7Reader.getMessages(originalMessage) - val outputMessage = hl7Reader.getMessages(comparisonMessage) - val inputType = ST(inputMessage[0]) + val inputMessage = HL7Reader.parseHL7Message(originalMessage) + val outputMessage = HL7Reader.parseHL7Message(comparisonMessage) + val inputType = ST(inputMessage) inputType.value = "blah" - val outputType = ST(outputMessage[0]) + val outputType = ST(outputMessage) outputType.value = "blah" - val inputVal = Varies(inputMessage[0]) + val inputVal = Varies(inputMessage) inputVal.data = inputType - val differentVaries = hL7DiffHelper.compareHl7Type( + val differentVaries = hl7DiffHelper.compareHl7Type( "", inputVal, outputType, @@ -277,29 +264,25 @@ OBR|1||232270000212^ProPhase Diagnostics^2.16.840.1.114222.4.1.238646^ISO|55454- @Test fun `expect no diff messages have blank vs empty MSH 8 (ST), OBR 49 (CWE) respectively`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val inputMessage = hl7Reader.getMessages(msgMSH8OBR49Blank) - val outputMessage = hl7Reader.getMessages(msgMSH8OBR49Empty) - val differences = hL7DiffHelper.diffHl7(inputMessage[0], outputMessage[0]) + val inputMessage = HL7Reader.parseHL7Message(msgMSH8OBR49Blank) + val outputMessage = HL7Reader.parseHL7Message(msgMSH8OBR49Empty) + val differences = hl7DiffHelper.diffHl7(inputMessage, outputMessage) assertThat(differences.size).isEqualTo(0) - val differences2 = hL7DiffHelper.diffHl7(outputMessage[0], inputMessage[0]) + val differences2 = hl7DiffHelper.diffHl7(outputMessage, inputMessage) assertThat(differences2.size).isEqualTo(0) } @Test fun `diff output, input missing segments`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) val msg = originalMessage.split("\n").toMutableList() msg.removeAt(1) - val inputMessage = hl7Reader.getMessages(msg.joinToString("\n")) - val outputMessage = hl7Reader.getMessages(originalMessage) - val differences = hL7DiffHelper.diffHl7(inputMessage[0], outputMessage[0]) + val inputMessage = HL7Reader.parseHL7Message(msg.joinToString("\n")) + val outputMessage = HL7Reader.parseHL7Message(originalMessage) + val differences = hl7DiffHelper.diffHl7(inputMessage, outputMessage) // input missing seg SFT assertThat(differences.size).isEqualTo(1) assertThat(differences[0].toString().contains("Input missing segment SFT")) - val differences2 = hL7DiffHelper.diffHl7(outputMessage[0], inputMessage[0]) + val differences2 = hl7DiffHelper.diffHl7(outputMessage, inputMessage) // output missing seg SFT assertThat(differences2.size).isEqualTo(1) assertThat(differences[0].toString().contains("Output missing segment SFT")) diff --git a/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt b/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt index e23ef1b6e35..3a762bd29ae 100644 --- a/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/engine/FhirConverterTests.kt @@ -11,7 +11,6 @@ import ca.uhn.fhir.validation.ResultSeverityEnum import ca.uhn.fhir.validation.SingleValidationMessage import ca.uhn.fhir.validation.ValidationResult import ca.uhn.hl7v2.util.Hl7InputStreamMessageStringIterator -import fhirengine.translation.hl7.structures.nistelr251.message.ORU_R01 import gov.cdc.prime.router.ActionLogDetail import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.CustomerStatus @@ -922,16 +921,6 @@ class FhirConverterTests { fun `should process an HL7 message with a registered profile`() { mockkObject(BlobAccess) mockkObject(HL7Reader.Companion) - every { HL7Reader.Companion.messageToConfigMap } returns mapOf( - HL7Reader.Companion.HL7MessageType( - "ORU_R01", - "2.5.1", - "2.16.840.1.113883.9.11" - ) to HL7Reader.Companion.HL7MessageParseAndConvertConfiguration( - ORU_R01::class.java, - "./metadata/test_fhir_mapping" - ) - ) val engine = spyk(makeFhirEngine(metadata, settings, TaskAction.process) as FHIRConverter) val actionLogger = ActionLogger() diff --git a/prime-router/src/test/kotlin/fhirengine/translation/HL7toFhirTranslatorTests.kt b/prime-router/src/test/kotlin/fhirengine/translation/HL7toFhirTranslatorTests.kt index 533c1386ea2..a7d8e1838a4 100644 --- a/prime-router/src/test/kotlin/fhirengine/translation/HL7toFhirTranslatorTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/translation/HL7toFhirTranslatorTests.kt @@ -6,8 +6,8 @@ import assertk.assertions.isEqualTo import assertk.assertions.isNotEmpty import assertk.assertions.isNotNull import assertk.assertions.isNull -import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.fhirengine.translation.HL7toFhirTranslator +import gov.cdc.prime.router.fhirengine.utils.HL7MessageHelpers import gov.cdc.prime.router.fhirengine.utils.HL7Reader import io.github.linuxforhealth.hl7.data.Hl7RelatedGeneralUtils import org.hl7.fhir.r4.model.Bundle @@ -48,16 +48,20 @@ OBX|1|CWE|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by @Test fun `test get message template`() { - val message = HL7Reader(ActionLogger()).getMessages(supportedHL7) - assertThat(message.size).isEqualTo(1) - assertThat(translator.getMessageTemplateType(message[0])).isEqualTo("ORU_R01") + val message = HL7Reader.parseHL7Message(supportedHL7) + assertThat( + HL7MessageHelpers.messageCount(supportedHL7) + ).isEqualTo(1) + assertThat(translator.getMessageTemplateType(message)).isEqualTo("ORU_R01") } @Test fun `test get message model`() { - var message = HL7Reader(ActionLogger()).getMessages(supportedHL7) - assertThat(message.size).isEqualTo(1) - val model = translator.getHL7MessageModel(message[0]) + val supportedMessage = HL7Reader.parseHL7Message(supportedHL7) + assertThat( + HL7MessageHelpers.messageCount(supportedHL7) + ).isEqualTo(1) + val model = translator.getHL7MessageModel(supportedMessage) assertThat(model).isNotNull() assertThat(model.messageName).isEqualTo("ORU_R01") @@ -70,17 +74,21 @@ ORC|NW|ORD448811^NIST EHR|||||||20120628070100|||5742200012^Radon^Nicholas^^^^^^ OBR|1|ORD448811^NIST EHR||1000^Hepatitis A B C Panel^99USL|||20120628070100|||||||||5742200012^Radon^Nicholas^^^^^^NPI^L^^^NPI DG1|1||F11.129^Opioid abuse with intoxication,unspecified^I10C|||W|||||||||1 """.trimIndent() - message = HL7Reader(ActionLogger()).getMessages(unsupportedHL7) - assertThat(message.size).isEqualTo(1) - assertFailure { translator.getHL7MessageModel(message[0]) } + val unsupportedMessage = HL7Reader.parseHL7Message(unsupportedHL7) + assertThat( + HL7MessageHelpers.messageCount(unsupportedHL7) + ).isEqualTo(1) + assertFailure { translator.getHL7MessageModel(unsupportedMessage) } } @Test fun `test a quick translation to FHIR`() { // Note that FHIR content will be tested as an integration test - val message = HL7Reader(ActionLogger()).getMessages(supportedHL7) - assertThat(message.size).isEqualTo(1) - val bundle = translator.translate(message[0]) + val message = HL7Reader.parseHL7Message(supportedHL7) + assertThat( + HL7MessageHelpers.messageCount(supportedHL7) + ).isEqualTo(1) + val bundle = translator.translate(message) assertThat(bundle).isNotNull() assertThat(bundle.type).isEqualTo(Bundle.BundleType.MESSAGE) assertThat(bundle.id).isNotEmpty() @@ -88,9 +96,11 @@ DG1|1||F11.129^Opioid abuse with intoxication,unspecified^I10C|||W|||||||||1 @Test fun `test birth date extension addition`() { - val message = HL7Reader(ActionLogger()).getMessages(supportedHL7ORMWithBirthDateTime) - assertThat(message.size).isEqualTo(1) - val bundle = translator.translate(message[0]) + val message = HL7Reader.parseHL7Message(supportedHL7ORMWithBirthDateTime) + assertThat( + HL7MessageHelpers.messageCount(supportedHL7ORMWithBirthDateTime) + ).isEqualTo(1) + val bundle = translator.translate(message) assertThat(bundle).isNotNull() assertThat(bundle.type).isEqualTo(Bundle.BundleType.MESSAGE) assertThat(bundle.id).isNotEmpty() @@ -111,9 +121,11 @@ DG1|1||F11.129^Opioid abuse with intoxication,unspecified^I10C|||W|||||||||1 @Test fun `test birth date extension is missing when birthdate is only date`() { - val message = HL7Reader(ActionLogger()).getMessages(supportedHL7ORMWithBirthDate) - assertThat(message.size).isEqualTo(1) - val bundle = translator.translate(message[0]) + val message = HL7Reader.parseHL7Message(supportedHL7ORMWithBirthDate) + assertThat( + HL7MessageHelpers.messageCount(supportedHL7ORMWithBirthDateTime) + ).isEqualTo(1) + val bundle = translator.translate(message) assertThat(bundle).isNotNull() assertThat(bundle.type).isEqualTo(Bundle.BundleType.MESSAGE) assertThat(bundle.id).isNotEmpty() diff --git a/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/HL7ACKUtilsTest.kt b/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/HL7ACKUtilsTest.kt index a519bcf396e..8fb1d4308ee 100644 --- a/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/HL7ACKUtilsTest.kt +++ b/prime-router/src/test/kotlin/fhirengine/translation/hl7/utils/HL7ACKUtilsTest.kt @@ -2,7 +2,6 @@ package gov.cdc.prime.router.fhirengine.translation.hl7.utils import assertk.assertThat import assertk.assertions.isEqualTo -import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.cli.helpers.HL7DiffHelper import gov.cdc.prime.router.fhirengine.utils.HL7Reader import io.mockk.every @@ -19,7 +18,6 @@ import kotlin.test.Test class HL7ACKUtilsTest { inner class Fixture { - val hl7Reader = HL7Reader(ActionLogger()) val hl7DiffHelper = HL7DiffHelper() private val clock = Clock.fixed( @@ -45,17 +43,16 @@ class HL7ACKUtilsTest { val incomingMessage = """ MSH|^~\&|Epic|Hospital|LIMS|StatePHL|20241003000000||ORM^O01^ORM_O01|4AFA57FE-D41D-4631-9500-286AAAF797E4|T|2.5.1|||AL|NE """.trimIndent() - val parsedIncomingMessage = f.hl7Reader.getMessages(incomingMessage).first() + val expectedMessage = """ + MSH|^~\&|ReportStream|CDC|Epic|Hospital|20240921000000+0000||ACK|$id|T|2.5.1|||NE|NE + MSA|CA|4AFA57FE-D41D-4631-9500-286AAAF797E4 + """.trimIndent() + val parsedIncomingMessage = HL7Reader.parseHL7Message(incomingMessage) val ack = f.utils.generateOutgoingACKMessage(parsedIncomingMessage) - val expected = f.hl7Reader.getMessages( - """ - MSH|^~\&|ReportStream|CDC|Epic|Hospital|20240921000000+0000||ACK|$id|T|2.5.1|||NE|NE - MSA|CA|4AFA57FE-D41D-4631-9500-286AAAF797E4 - """ - ).first() - val actual = f.hl7Reader.getMessages(ack).first() + val expected = HL7Reader.parseHL7Message(expectedMessage) + val actual = HL7Reader.parseHL7Message(ack) val diffs = f.hl7DiffHelper.diffHl7(expected, actual) if (diffs.isNotEmpty()) { diff --git a/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt b/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt index 5abc630dc42..52fdafd144b 100644 --- a/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/utils/FHIRBundleHelpersTests.kt @@ -16,6 +16,7 @@ import assertk.assertions.isNull import assertk.assertions.isTrue import ca.uhn.fhir.context.FhirContext import ca.uhn.hl7v2.model.v251.segment.MSH +import ca.uhn.hl7v2.util.Hl7InputStreamMessageIterator import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.CodeStringConditionFilter import gov.cdc.prime.router.CustomerStatus @@ -30,7 +31,6 @@ import gov.cdc.prime.router.azure.ConditionStamper import gov.cdc.prime.router.azure.ConditionStamper.Companion.conditionCodeExtensionURL 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 @@ -77,7 +77,6 @@ class FHIRBundleHelpersTests { val connection = MockConnection(dataProvider) val accessSpy = spyk(DatabaseAccess(connection)) val blobMock = mockkClass(BlobAccess::class) - val queueMock = mockkClass(QueueAccess::class) val metadata = Metadata(schema = Schema(name = "None", topic = Topic.FULL_ELR, elements = emptyList())) private val shorthandLookupTable = emptyMap().toMutableMap() @@ -666,12 +665,11 @@ class FHIRBundleHelpersTests { val bundle = messages[0] assertThat(bundle).isNotNull() - // create the hl7 reader - val hl7Reader = HL7Reader(actionLogger) + // create the hl7 message val hl7Message = File("src/test/resources/fhirengine/engine/hl7_with_birth_time.hl7").readText() - val hl7messages = hl7Reader.getMessages(hl7Message) + val parsedHl7Message = Hl7InputStreamMessageIterator(hl7Message.byteInputStream()).next() - bundle.handleBirthTime(hl7messages[0]) + bundle.handleBirthTime(parsedHl7Message) val patient = FhirPathUtils.evaluate( CustomContext(bundle, bundle), @@ -697,12 +695,11 @@ class FHIRBundleHelpersTests { val bundle = messages[0] assertThat(bundle).isNotNull() - // create the hl7 reader - val hl7Reader = HL7Reader(actionLogger) + // create the hl7 message val hl7Message = File("src/test/resources/fhirengine/engine/hl7_with_birth_time.hl7").readText() - val hl7messages = hl7Reader.getMessages(hl7Message) + val parsedHl7Message = Hl7InputStreamMessageIterator(hl7Message.byteInputStream()).next() - bundle.handleBirthTime(hl7messages[0]) + bundle.handleBirthTime(parsedHl7Message) val patient = FhirPathUtils.evaluate( CustomContext(bundle, bundle), @@ -728,14 +725,13 @@ class FHIRBundleHelpersTests { val bundle = messages[0] assertThat(bundle).isNotNull() - // create the hl7 reader - val hl7Reader = HL7Reader(actionLogger) + // create the hl7 message val hl7Message = File("src/test/resources/fhirengine/engine/hl7_with_birth_time.hl7").readText() - val hl7Messages = hl7Reader.getMessages(hl7Message) + val parsedHl7Message = Hl7InputStreamMessageIterator(hl7Message.byteInputStream()).next() - assertThat(hl7Messages[0]["MSH"] is MSH).isTrue() + assertThat(parsedHl7Message["MSH"] is MSH).isTrue() - bundle.enhanceBundleMetadata(hl7Messages[0]) + bundle.enhanceBundleMetadata(parsedHl7Message) val expectedDate = Date(1612994857000) // Wednesday, February 10, 2021 10:07:37 PM GMT assertThat(bundle.timestamp).isEqualTo(expectedDate) @@ -753,14 +749,13 @@ class FHIRBundleHelpersTests { val bundle = messages[0] assertThat(bundle).isNotNull() - // create the hl7 reader - val hl7Reader = HL7Reader(actionLogger) + // create the hl7 message val hl7Message = File("src/test/resources/fhirengine/engine/hl7_2.7.hl7").readText() - val hl7Messages = hl7Reader.getMessages(hl7Message) + val parsedHl7Message = Hl7InputStreamMessageIterator(hl7Message.byteInputStream()).next() - assertThat(hl7Messages[0]["MSH"] is ca.uhn.hl7v2.model.v27.segment.MSH).isTrue() + assertThat(parsedHl7Message["MSH"] is ca.uhn.hl7v2.model.v27.segment.MSH).isTrue() - bundle.enhanceBundleMetadata(hl7Messages[0]) + bundle.enhanceBundleMetadata(parsedHl7Message) val expectedDate = Date(1612994857000) // Wednesday, February 10, 2021 10:07:37 PM GMT assertThat(bundle.timestamp).isEqualTo(expectedDate) @@ -778,15 +773,14 @@ class FHIRBundleHelpersTests { val bundle = messages[0] assertThat(bundle).isNotNull() - // create the hl7 reader - val hl7Reader = HL7Reader(actionLogger) + // create the hl7 message val hl7Message = File("src/test/resources/fhirengine/engine/hl7_2.6.hl7").readText() - val hl7Messages = hl7Reader.getMessages(hl7Message) + val parsedHl7Message = Hl7InputStreamMessageIterator(hl7Message.byteInputStream()).next() - assertThat(hl7Messages[0]["MSH"] is MSH).isFalse() - assertThat(hl7Messages[0]["MSH"] is ca.uhn.hl7v2.model.v27.segment.MSH).isFalse() + assertThat(parsedHl7Message["MSH"] is MSH).isFalse() + assertThat(parsedHl7Message["MSH"] is ca.uhn.hl7v2.model.v27.segment.MSH).isFalse() - bundle.enhanceBundleMetadata(hl7Messages[0]) + bundle.enhanceBundleMetadata(parsedHl7Message) assertThat(bundle.timestamp).isNull() assertThat(bundle.identifier.value).isNull() diff --git a/prime-router/src/test/kotlin/fhirengine/utils/HL7QueueMessageHelpersTests.kt b/prime-router/src/test/kotlin/fhirengine/utils/HL7QueueMessageHelpersTests.kt index 21e56122939..6f08e4cc95d 100644 --- a/prime-router/src/test/kotlin/fhirengine/utils/HL7QueueMessageHelpersTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/utils/HL7QueueMessageHelpersTests.kt @@ -7,8 +7,8 @@ import assertk.assertions.isNotEmpty import assertk.assertions.isNotNull import assertk.assertions.isTrue import assertk.assertions.startsWith +import ca.uhn.hl7v2.util.Hl7InputStreamMessageStringIterator import ca.uhn.hl7v2.util.Terser -import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.Hl7Configuration import gov.cdc.prime.router.Receiver import gov.cdc.prime.router.Topic @@ -135,7 +135,10 @@ OBX|1|ST|MLI-4000.15^TEMPERATURE||97.7|deg f|||||R|||19980601184619 """.trimIndent() val batchFile = HL7MessageHelpers.batchMessages(listOf(hl7Message, hl7Message), receiver) - val messages = HL7Reader(ActionLogger()).getMessages(batchFile) + val messages = Hl7InputStreamMessageStringIterator(batchFile.byteInputStream()).asSequence() + .map { rawItem -> + HL7Reader.parseHL7Message(rawItem) + }.toList() assertThat(messages).isNotEmpty() assertThat(messages.size).isEqualTo(2) val a = Terser(messages[0]) diff --git a/prime-router/src/test/kotlin/fhirengine/utils/HL7ReaderTests.kt b/prime-router/src/test/kotlin/fhirengine/utils/HL7ReaderTests.kt index aef41a860bf..286074b1230 100644 --- a/prime-router/src/test/kotlin/fhirengine/utils/HL7ReaderTests.kt +++ b/prime-router/src/test/kotlin/fhirengine/utils/HL7ReaderTests.kt @@ -2,23 +2,17 @@ package gov.cdc.prime.router.fhirengine.utils import assertk.assertThat import assertk.assertions.contains -import assertk.assertions.hasSize import assertk.assertions.isEqualTo import assertk.assertions.isFalse -import assertk.assertions.isInstanceOf import assertk.assertions.isNotNull import assertk.assertions.isNull import assertk.assertions.isTrue +import ca.uhn.hl7v2.ErrorCode +import ca.uhn.hl7v2.HL7Exception import ca.uhn.hl7v2.model.Message -import ca.uhn.hl7v2.model.v27.datatype.CWE -import ca.uhn.hl7v2.model.v27.segment.OBX -import ca.uhn.hl7v2.util.Terser -import gov.cdc.prime.router.ActionLogger -import io.mockk.every -import io.mockk.mockk -import io.mockk.spyk -import io.mockk.verify -import org.apache.logging.log4j.kotlin.KotlinLogger +import ca.uhn.hl7v2.parser.EncodingNotSupportedException +import org.apache.commons.lang3.exception.ExceptionUtils +import java.lang.Exception import java.text.SimpleDateFormat import kotlin.test.Test @@ -44,36 +38,39 @@ class HL7ReaderTests { @Test fun `test decoding of bad HL7 messages`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - // Empty data val badData1 = "" - hl7Reader.getMessages(badData1) - assertThat(actionLogger.hasErrors()).isTrue() - actionLogger.logs.clear() + try { + HL7Reader.parseHL7Message(badData1) + } catch (e: Exception) { + assertThat(e is HL7Exception).isTrue() + assertThat(ExceptionUtils.getRootCause(e) is EncodingNotSupportedException).isTrue() + } // Some CSV was sent val badData2 = """ a,b,c 1,2,3 """.trimIndent() - hl7Reader.getMessages(badData2) - assertThat(actionLogger.hasErrors()).isTrue() - actionLogger.logs.clear() + try { + HL7Reader.parseHL7Message(badData2) + } catch (e: Exception) { + assertThat(e is HL7Exception).isTrue() + assertThat(ExceptionUtils.getRootCause(e) is EncodingNotSupportedException).isTrue() + } // Some truncated HL7 val badData3 = "MSH|^~\\&#|MEDITECH^2.16.840.1.114222.4.3.2.2.1.321.111^ISO|COCAA^1.2." - hl7Reader.getMessages(badData3) - assertThat(actionLogger.hasErrors()).isTrue() - actionLogger.logs.clear() + try { + HL7Reader.parseHL7Message(badData3) + } catch (e: Exception) { + assertThat(e is HL7Exception).isTrue() + assertThat(ExceptionUtils.getRootCause(e) is EncodingNotSupportedException).isTrue() + } } @Test fun `test decoding of good HL7 messages`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val goodData1 = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 @@ -89,49 +86,12 @@ OBX|6|CWE|82810-3^Pregnant^LN^^^^2.69||N^No^HL70136||||||F|||202102090000-0600|| NTE|1|L|This is a note|RE SPM|1|b518ef23-1d9a-40c1-ac4b-ed7b438dfc4b||258500001^Nasopharyngeal swab^SCT||||71836000^Nasopharyngeal structure (body structure)^SCT^^^^2020-09-01|||||||||202102090000-0600|202102090000-0600 """.trimIndent() - var messages = hl7Reader.getMessages(goodData1) - assertThat(actionLogger.hasErrors()).isFalse() - assertThat(messages.size).isEqualTo(1) - actionLogger.logs.clear() - - val goodData2 = """ -FHS|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|||202102101707-0500 -BHS|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|||202102101707-0500 -MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO -SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 -PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||19580810|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N -ORC|RE|73a6e9bd-aaec-418e-813a-0ad33366ca85|73a6e9bd-aaec-418e-813a-0ad33366ca85|||||||||1629082607^Eddin^Husam^^^^^^CMS&2.16.840.1.113883.3.249&ISO^^^^NPI||^WPN^^^1^386^6825220|20210209||||||Avante at Ormond Beach|170 North King Road^^Ormond Beach^FL^32174^^^^12127|^WPN^^jbrush@avantecenters.com^1^407^7397506|^^^^32174 -OBR|1|73a6e9bd-aaec-418e-813a-0ad33366ca85|0cba76f5-35e0-4a28-803a-2f31308aae9b|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by Rapid immunoassay^LN|||202102090000-0600|202102090000-0600||||||||1629082607^Eddin^Husam^^^^^^CMS&2.16.840.1.113883.3.249&ISO^^^^NPI|^WPN^^^1^386^6825220|||||202102090000-0600|||F -OBX|1|CWE|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by Rapid immunoassay^LN||260415000^Not detected^SCT|||N^Normal (applies to non-numeric results)^HL70078|||F|||202102090000-0600|||CareStart COVID-19 Antigen test_Access Bio, Inc._EUA^^99ELR||202102090000-0600||||Avante at Ormond Beach^^^^^CLIA&2.16.840.1.113883.4.7&ISO^^^^10D0876999^CLIA|170 North King Road^^Ormond Beach^FL^32174^^^^12127 -OBX|2|CWE|95418-0^Whether patient is employed in a healthcare setting^LN^^^^2.69||Y^Yes^HL70136||||||F|||202102090000-0600|||||||||||||||QST -OBX|3|CWE|95417-2^First test for condition of interest^LN^^^^2.69||Y^Yes^HL70136||||||F|||202102090000-0600|||||||||||||||QST -OBX|4|CWE|95421-4^Resides in a congregate care setting^LN^^^^2.69||N^No^HL70136||||||F|||202102090000-0600|||||||||||||||QST -OBX|5|CWE|95419-8^Has symptoms related to condition of interest^LN^^^^2.69||N^No^HL70136||||||F|||202102090000-0600|||||||||||||||QST -SPM|1|0cba76f5-35e0-4a28-803a-2f31308aae9b||258500001^Nasopharyngeal swab^SCT||||71836000^Nasopharyngeal structure (body structure)^SCT^^^^2020-09-01|||||||||202102090000-0600|202102090000-0600 -MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|612092|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO -SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 -PID|1||a40ea680-51bc-4a05-bf23-786dd08e64f2^^^Avante at Ormond Beach^PI||Keeling^Tyson^Chuck^^^^L||19550206|M||2106-3^White^HL70005^^^^2.5.1|97065 Mohr Island^Street^North Taylor^TX^69622^^^^48077||7275555555:1:^PRN^^kenton.wilderman@email.com^1^281^2498561|||||||||U^Unknown^HL70189||||||||N -ORC|RE|4ba0f2c4-5f39-4716-9daa-d450573e7019|4ba0f2c4-5f39-4716-9daa-d450573e7019|||||||||1629082607^Eddin^Husam^^^^^^CMS&2.16.840.1.113883.3.249&ISO^^^^NPI||^WPN^^^1^386^6825220|20210209||||||Avante at Ormond Beach|170 North King Road^^Ormond Beach^FL^32174^^^^12127|^WPN^^jbrush@avantecenters.com^1^407^7397506|^^^^32174 -OBR|1|4ba0f2c4-5f39-4716-9daa-d450573e7019|b518ef23-1d9a-40c1-ac4b-ed7b438dfc4b|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by Rapid immunoassay^LN|||202102090000-0600|202102090000-0600||||||||1629082607^Eddin^Husam^^^^^^CMS&2.16.840.1.113883.3.249&ISO^^^^NPI|^WPN^^^1^386^6825220|||||202102090000-0600|||F -OBX|1|CWE|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by Rapid immunoassay^LN||260415000^Not detected^SCT|||N^Normal (applies to non-numeric results)^HL70078|||F|||202102090000-0600|||CareStart COVID-19 Antigen test_Access Bio, Inc._EUA^^99ELR||202102090000-0600||||Avante at Ormond Beach^^^^^CLIA&2.16.840.1.113883.4.7&ISO^^^^10D0876999^CLIA|170 North King Road^^Ormond Beach^FL^32174^^^^12127 -OBX|2|CWE|95418-0^Whether patient is employed in a healthcare setting^LN^^^^2.69||Y^Yes^HL70136||||||F|||202102090000-0600|||||||||||||||QST -OBX|3|CWE|95417-2^First test for condition of interest^LN^^^^2.69||Y^Yes^HL70136||||||F|||202102090000-0600|||||||||||||||QST -OBX|4|CWE|95421-4^Resides in a congregate care setting^LN^^^^2.69||N^No^HL70136||||||F|||202102090000-0600|||||||||||||||QST -OBX|5|CWE|95419-8^Has symptoms related to condition of interest^LN^^^^2.69||N^No^HL70136||||||F|||202102090000-0600|||||||||||||||QST -SPM|1|b518ef23-1d9a-40c1-ac4b-ed7b438dfc4b||258500001^Nasopharyngeal swab^SCT||||71836000^Nasopharyngeal structure (body structure)^SCT^^^^2020-09-01|||||||||202102090000-0600|202102090000-0600 -BTS|2 -FTS|1 - """.trimIndent() - messages = hl7Reader.getMessages(goodData2) - assertThat(actionLogger.hasErrors()).isFalse() - assertThat(messages.size).isEqualTo(2) - actionLogger.logs.clear() + val message = HL7Reader.parseHL7Message(goodData1) + assertThat(message.isEmpty).isFalse() } @Test fun `test get message time stamp`() { - val hl7Reader = HL7Reader(ActionLogger()) - fun getTestMessage(timestampStr: String): Message { val rawData = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|$timestampStr||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO @@ -141,9 +101,9 @@ ORC|RE|73a6e9bd-aaec-418e-813a-0ad33366ca85|73a6e9bd-aaec-418e-813a-0ad33366ca85 OBR|1|73a6e9bd-aaec-418e-813a-0ad33366ca85|b518ef23-1d9a-40c1-ac4b-ed7b438dfc4b|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by Rapid immunoassay^LN|||202102090000-0600|202102090000-0600||||||||1629082607^Eddin^Husam^^^^^^CMS&2.16.840.1.113883.3.249&ISO^^^^NPI|^WPN^^^1^386^6825220|||||202102090000-0600|||F OBX|1|CWE|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by Rapid immunoassay^LN||260415000^Not detected^SCT|||N^Normal (applies to non-numeric results)^HL70078|||F|||202102090000-0600|||CareStart COVID-19 Antigen test_Access Bio, Inc._EUA^^99ELR||202102090000-0600||||Avante at Ormond Beach^^^^^CLIA&2.16.840.1.113883.4.7&ISO^^^^10D0876999^CLIA|170 North King Road^^Ormond Beach^FL^32174^^^^12127 """.trimIndent() - val message = hl7Reader.getMessages(rawData) - assertThat(message.size).isEqualTo(1) - return message[0] + val message = HL7Reader.parseHL7Message(rawData) + assertThat(message.isEmpty).isFalse() + return message } var timestampStr = "" @@ -163,38 +123,32 @@ OBX|1|CWE|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by @Test fun `test decoding of bad HL7 message - MSH Version ID missing`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val goodData1 = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P||||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO """.trimIndent() - hl7Reader.getMessages(goodData1) - assertThat(actionLogger.hasErrors()).isTrue() - assertThat(actionLogger.logs[0].detail.message).contains("Required field missing") - actionLogger.logs.clear() + try { + HL7Reader.parseHL7Message(goodData1) + } catch (e: HL7Exception) { + assertThat(e.errorCode).isEqualTo(ErrorCode.REQUIRED_FIELD_MISSING.code) + assertThat(ExceptionUtils.getMessage(e)).contains("Can't find version ID - MSH.12 is null") + } } @Test fun `test decoding of bad HL7 message - OBX Wrong data type`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val goodData1 = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO OBX|1|test|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen by Rapid immunoassay^LN||260415000^Not detected^SCT|||N^Normal (applies to non-numeric results)^HL70078|||F|||202102090000-0600|||CareStart COVID-19 Antigen test_Access Bio, Inc._EUA^^99ELR||202102090000-0600||||Avante at Ormond Beach^^^^^CLIA&2.16.840.1.113883.4.7&ISO^^^^10D0876999^CLIA|170 North King Road^^Ormond Beach^FL^32174^^^^12127 """.trimIndent() - hl7Reader.getMessages(goodData1) - assertThat(actionLogger.hasErrors()).isTrue() - assertThat(actionLogger.logs[0].detail.message).contains("Data type error") - actionLogger.logs.clear() + try { + HL7Reader.parseHL7Message(goodData1) + } catch (e: HL7Exception) { + assertThat(ExceptionUtils.getMessage(e)).contains("trying to set data type of OBX-5") + } } @Test fun `test isBatch with FSH Header`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val goodData1 = """ FHS|^~\&|||0.0.0.0.1|0.0.0.0.1|202106221314-0400 BHS|^~\&|||0.0.0.0.1|0.0.0.0.1|202106221314-0400 @@ -214,16 +168,15 @@ OBX|1|test|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen b BTS|2 FTS|1 """.trimIndent() - val messages = hl7Reader.getMessages(goodData1) - val isBatch = hl7Reader.isBatch(goodData1, messages.size) + val isBatch = HL7Reader.isBatch( + goodData1, + HL7MessageHelpers.messageCount(goodData1) + ) assertThat(isBatch).isTrue() } @Test fun `test isBatch without FSH Header`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val goodData1 = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 @@ -252,16 +205,15 @@ OBX|1|test|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen b NTE|1|L|This is a note|RE SPM|1|b518ef23-1d9a-40c1-ac4b-ed7b438dfc4b||258500001^Nasopharyngeal swab^SCT||||71836000^Nasopharyngeal structure (body structure)^SCT^^^^2020-09-01|||||||||202102090000-0600|202102090000-0600 """.trimIndent() - val messages = hl7Reader.getMessages(goodData1) - val isBatch = hl7Reader.isBatch(goodData1, messages.size) + val isBatch = HL7Reader.isBatch( + goodData1, + HL7MessageHelpers.messageCount(goodData1) + ) assertThat(isBatch).isTrue() } @Test fun `test isBatch singular message`() { - val actionLogger = ActionLogger() - val hl7Reader = HL7Reader(actionLogger) - val goodData1 = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 @@ -277,207 +229,101 @@ OBX|1|test|94558-4^SARS-CoV-2 (COVID-19) Ag [Presence] in Respiratory specimen b NTE|1|L|This is a note|RE SPM|1|b518ef23-1d9a-40c1-ac4b-ed7b438dfc4b||258500001^Nasopharyngeal swab^SCT||||71836000^Nasopharyngeal structure (body structure)^SCT^^^^2020-09-01|||||||||202102090000-0600|202102090000-0600 """.trimIndent() - val messages = hl7Reader.getMessages(goodData1) - val isBatch = hl7Reader.isBatch(goodData1, messages.size) + val isBatch = HL7Reader.isBatch( + goodData1, + HL7MessageHelpers.messageCount(goodData1) + ) assertThat(isBatch).isFalse() } @Test fun `test getMessageType`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) val justMSH = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO """.trimIndent() - val messages = hL7Reader.getMessages(justMSH) - val type = HL7Reader.getMessageType(messages[0]) + val message = HL7Reader.parseHL7Message(justMSH) + val type = HL7Reader.getMessageType(message) assertThat(type).isEqualTo("ORU") } - @Test - fun `test getMessageProfile`() { - val justMSH = """ - MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO - """.trimIndent() - val messages = HL7Reader.getMessageProfile(justMSH) - assertThat(messages).isEqualTo( - HL7Reader.Companion.MessageProfile( - "ORU", - "NIST_ELR" - ) - ) - - val noProfile = """ - MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA - """.trimIndent() - val messages2 = HL7Reader.getMessageProfile(noProfile) - assertThat(messages2).isEqualTo( - HL7Reader.Companion.MessageProfile( - "ORU", - "" - ) - ) - } - @Test fun `test getBirthTime_DateTime`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) val birthTimeMessage = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORM^O01^ORM_O01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||195808100102034|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N """.trimIndent() - val messages = hL7Reader.getMessages(birthTimeMessage) - val type = HL7Reader.getBirthTime(messages[0]) + val message = HL7Reader.parseHL7Message(birthTimeMessage) + val type = HL7Reader.getBirthTime(message) assertThat(type).isEqualTo("195808100102034") } @Test fun `test getBirthTime_Date`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) val birthDateMessage = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORM^O01^ORM_O01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||19580810|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N """.trimIndent() - val messages = hL7Reader.getMessages(birthDateMessage) - val type = HL7Reader.getBirthTime(messages[0]) + val message = HL7Reader.parseHL7Message(birthDateMessage) + val type = HL7Reader.getBirthTime(message) assertThat(type).isEqualTo("19580810") } @Test fun `test getPatientPath_ORM`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) val birthDateMessage = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORM^O01^ORM_O01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||19580810|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N """.trimIndent() - val messages = hL7Reader.getMessages(birthDateMessage) - val type = HL7Reader.getPatientPath(messages[0]) + val message = HL7Reader.parseHL7Message(birthDateMessage) + val type = HL7Reader.getPatientPath(message) assertThat(type).isEqualTo("PATIENT") } @Test fun `test getPatientPath_OML`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) val birthDateMessage = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||OML^O21^OML_O21|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||19580810|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N """.trimIndent() - val messages = hL7Reader.getMessages(birthDateMessage) - val type = HL7Reader.getPatientPath(messages[0]) + val message = HL7Reader.parseHL7Message(birthDateMessage) + val type = HL7Reader.getPatientPath(message) assertThat(type).isEqualTo("PATIENT") } @Test fun `test getPatientPath_ORU`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) val birthDateMessage = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||19580810|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N """.trimIndent() - val messages = hL7Reader.getMessages(birthDateMessage) - val type = HL7Reader.getPatientPath(messages[0]) + val message = HL7Reader.parseHL7Message(birthDateMessage) + val type = HL7Reader.getPatientPath(message) assertThat(type).isEqualTo("PATIENT_RESULT/PATIENT") } @Test fun `test getPatientPath_Other`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) val birthDateMessage = """ MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||TEST^O01^TEST_O01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||19580810|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N """.trimIndent() - val messages = hL7Reader.getMessages(birthDateMessage) - val type = HL7Reader.getPatientPath(messages[0]) + val message = HL7Reader.parseHL7Message(birthDateMessage) + val type = HL7Reader.getPatientPath(message) assertThat(type).isEqualTo(null) } - @Test - fun `get getMessages no mapped models`() { - val actionLogger = ActionLogger() - val hl7Reader = spyk(HL7Reader(actionLogger), recordPrivateCalls = true) - val mockedLogger = mockk() - - every { hl7Reader.logger } returns mockedLogger - every { mockedLogger.warn(any()) } returns Unit - - val goodData1 = """ - MSH|^~\&|ADT1|GOOD HEALTH HOSPITAL|GHH LAB, INC.|GOOD HEALTH HOSPITAL|198808181126|SECURITY|ADT^A01^ADT_A01|MSG00001|P|2.5.1|| - EVN|A01|200708181123|| - PID|1||PATID1234^5^M11^ADT1^MR^GOOD HEALTH HOSPITAL~123456789^^^USSSA^SS||EVERYMAN^ADAM^A^III||19610615|M||C|2222 HOME STREET^^GREENSBORO^NC^27401-1020|GL|(555) 555-2004|(555)555-2004||S||PATID12345001^2^M10^ADT1^AN^A|444333333|987654^NC| - NK1|1|NUCLEAR^NELDA^W|SPO^SPOUSE||||NK^NEXT OF KIN - PV1|1|I|2000^2012^01||||004777^ATTEND^AARON^A|||SUR||||ADM|A0| - """.trimIndent() - val messages = hl7Reader.getMessages(goodData1) - assertThat(messages).hasSize(1) - verify { - mockedLogger.warn("ADT did not have any mapped message model classes, using default behavior") - } - } - - @Test - fun `get getMessages v27 succeeds`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) - - val data = """ - MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA||||PHLabReportNoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO - SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 - PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||19580810|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N - ORC|RE|73a6e9bd-aaec-418e-813a-0ad33366ca85|73a6e9bd-aaec-418e-813a-0ad33366ca85|||||||||1629082607^Eddin^Husam^^^^^^CMS&2.16.840.1.113883.3.249&ISO^^^^NPI||^WPN^^^1^386^6825220|20210209||||||Avante at Ormond Beach|170 North King Road^^Ormond Beach^FL^32174^^^^12127|^WPN^^jbrush@avantecenters.com^1^407^7397506|^^^^32174 - OBR|1|sphlspid^SPHL-000048^2.16.840.1.114222.4.1.10765^ISO|3015894676_04608646^STARLIMS.CDC.Stag^2.16.840.1.114222.4.3.3.2.1.2^ISO|68991-9^Epidemiologically Important Information^LN^^^^2.69^^^CDC-10516^Poxvirus Serology^L^^2.16.840.1.113883.6.1|||202302101135|||||||||SPHL-000148^CA-Veterans Affairs Palo Alto Healthcare System^^^^^^^STARLIMS.CDC.Stag&2.16.840.1.114222.4.3.3.2.1.2&ISO^^^^XX|^NET^Internet^mark.holodniy@va.gov|||||202302131116-0500|||F - OBX|1|CWE|68993-5^Human RNase P RNA XXX Ql NAA+probe^LN^3844^RNaseP human DNA^L^2.69^v_unknown^RNaseP human DNA|ZZYGNASR-1|260385009^Negative^SCT^^^^09012018^^Negative (No human DNA present)||||||C|||202302101135|11D0668319^Centers for Disease Control and Prevention^CLIA^47^Poxvirus Laboratory/Poxvirus and Rabies Branch^L|UIE8@CDC.GOV^Perkins ^Kayla|||20230213102132||||Centers for Disease Control and Prevention^L^^^^CLIA&2.16.840.1.113883.4.7&ISO^XX^^^11D0668319|1600 Clifton Rd^^Atlanta^GA^30329^USA^B - NTE|1|L|This is a note|RE - SPM|1|b518ef23-1d9a-40c1-ac4b-ed7b438dfc4b||258500001^Nasopharyngeal swab^SCT||||71836000^Nasopharyngeal structure (body structure)^SCT^^^^2020-09-01|||||||||202102090000-0600|202102090000-0600 - """.trimIndent() - - val messages = hL7Reader.getMessages(data) - assertThat(messages).hasSize(1) - assertThat(actionLogger.hasErrors()).isFalse() - } - - @Test - fun `get getMessages can parse a message that uses the deprecated CE type in OBX2`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) - - val data = """ - MSH|^~\&#|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Avante at Ormond Beach^10D0876999^CLIA|PRIME_DOH|Prime ReportStream|20210210170737||ORU^R01^ORU_R01|371784|P|2.5.1|||NE|NE|USA - SFT|Centers for Disease Control and Prevention|0.1-SNAPSHOT|PRIME ReportStream|0.1-SNAPSHOT||20210210 - PID|1||2a14112c-ece1-4f82-915c-7b3a8d152eda^^^Avante at Ormond Beach^PI||Buckridge^Kareem^Millie^^^^L||19580810|F||2106-3^White^HL70005^^^^2.5.1|688 Leighann Inlet^^South Rodneychester^TX^67071^^^^48077||7275555555:1:^PRN^^roscoe.wilkinson@email.com^1^211^2240784|||||||||U^Unknown^HL70189||||||||N - ORC|RE|73a6e9bd-aaec-418e-813a-0ad33366ca85|73a6e9bd-aaec-418e-813a-0ad33366ca85|||||||||1629082607^Eddin^Husam^^^^^^CMS&2.16.840.1.113883.3.249&ISO^^^^NPI||^WPN^^^1^386^6825220|20210209||||||Avante at Ormond Beach|170 North King Road^^Ormond Beach^FL^32174^^^^12127|^WPN^^jbrush@avantecenters.com^1^407^7397506|^^^^32174 - OBR|1|sphlspid^SPHL-000048^2.16.840.1.114222.4.1.10765^ISO|3015894676_04608646^STARLIMS.CDC.Stag^2.16.840.1.114222.4.3.3.2.1.2^ISO|68991-9^Epidemiologically Important Information^LN^^^^2.69^^^CDC-10516^Poxvirus Serology^L^^2.16.840.1.113883.6.1|||202302101135|||||||||SPHL-000148^CA-Veterans Affairs Palo Alto Healthcare System^^^^^^^STARLIMS.CDC.Stag&2.16.840.1.114222.4.3.3.2.1.2&ISO^^^^XX|^NET^Internet^mark.holodniy@va.gov|||||202302131116-0500|||F - OBX|1|CE|68993-5^Human RNase P RNA XXX Ql NAA+probe^LN^3844^RNaseP human DNA^L^2.69^v_unknown^RNaseP human DNA|ZZYGNASR-1|260385009^Negative^SCT^^^^09012018^^Negative (No human DNA present)||||||C|||202302101135|11D0668319^Centers for Disease Control and Prevention^CLIA^47^Poxvirus Laboratory/Poxvirus and Rabies Branch^L|UIE8@CDC.GOV^Perkins ^Kayla|||20230213102132||||Centers for Disease Control and Prevention^L^^^^CLIA&2.16.840.1.113883.4.7&ISO^XX^^^11D0668319|1600 Clifton Rd^^Atlanta^GA^30329^USA^B - NTE|1|L|This is a note|RE - SPM|1|b518ef23-1d9a-40c1-ac4b-ed7b438dfc4b||258500001^Nasopharyngeal swab^SCT||||71836000^Nasopharyngeal structure (body structure)^SCT^^^^2020-09-01|||||||||202102090000-0600|202102090000-0600 - """.trimIndent() - - val messages = hL7Reader.getMessages(data) - assertThat(messages).hasSize(1) - assertThat(actionLogger.hasWarnings()).isFalse() - val obxSegment = Terser(messages[0]).getSegment("/PATIENT_RESULT/ORDER_OBSERVATION/OBSERVATION/OBX") as OBX - assertThat(obxSegment.getObservationValue(0).data).isInstanceOf(CWE::class) - } - @Test fun `extract MSH segment values`() { - val actionLogger = ActionLogger() - val hL7Reader = HL7Reader(actionLogger) - @Suppress("ktlint:standard:max-line-length") - val rawMessage = "MSH|^~\\&|Epic|Hospital|LIMS|StatePHL|20241003000000||ORM^O01^ORM_O01|4AFA57FE-D41D-4631-9500-286AAAF797E4|T|2.5.1|||AL|NE" - val message = hL7Reader.getMessages(rawMessage).first() + val rawMessage = + "MSH|^~\\&|Epic|Hospital|LIMS|StatePHL|20241003000000||ORM^O01^ORM_O01|4AFA57FE-D41D-4631-9500-286AAAF797E4|T|2.5.1|||AL|NE" + val message = HL7Reader.parseHL7Message(rawMessage) assertThat( HL7Reader.getSendingApplication(message) diff --git a/prime-router/src/test/kotlin/validation/MarsOtcElrOnboardingValidatorTests.kt b/prime-router/src/test/kotlin/validation/MarsOtcElrOnboardingValidatorTests.kt index b7c5e9ce64c..851576300b1 100644 --- a/prime-router/src/test/kotlin/validation/MarsOtcElrOnboardingValidatorTests.kt +++ b/prime-router/src/test/kotlin/validation/MarsOtcElrOnboardingValidatorTests.kt @@ -3,7 +3,6 @@ package gov.cdc.prime.router.validation import assertk.assertThat import assertk.assertions.isFalse import assertk.assertions.isTrue -import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.fhirengine.utils.HL7Reader import org.junit.jupiter.api.Test @@ -17,8 +16,8 @@ class MarsOtcElrOnboardingValidatorTests { this.javaClass.classLoader.getResourceAsStream("validation/marsotcelr/fail_onboarding_pass_prod.hl7") val sampleMessage = sampleMessageInputStream!!.bufferedReader().use { it.readText() } - val messages = HL7Reader(ActionLogger()).getMessages(sampleMessage) - val report = validator.validate(messages[0]) + val message = HL7Reader.parseHL7Message(sampleMessage) + val report = validator.validate(message) assertThat(report.isValid()).isFalse() } @@ -28,8 +27,8 @@ class MarsOtcElrOnboardingValidatorTests { this.javaClass.classLoader.getResourceAsStream("validation/marsotcelr/valid.hl7") val sampleMessage = sampleMessageInputStream!!.bufferedReader().use { it.readText() } - val messages = HL7Reader(ActionLogger()).getMessages(sampleMessage) - val report = validator.validate(messages[0]) + val message = HL7Reader.parseHL7Message(sampleMessage) + val report = validator.validate(message) assertThat(report.isValid()).isTrue() } } \ 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 16b35f45c91..ee5ceb366a0 100644 --- a/prime-router/src/test/kotlin/validation/MarsOtcElrValidatorTests.kt +++ b/prime-router/src/test/kotlin/validation/MarsOtcElrValidatorTests.kt @@ -3,7 +3,6 @@ package gov.cdc.prime.router.validation import assertk.assertThat import assertk.assertions.isFalse import assertk.assertions.isTrue -import gov.cdc.prime.router.ActionLogger import gov.cdc.prime.router.fhirengine.utils.HL7Reader import org.junit.jupiter.api.Test @@ -17,8 +16,8 @@ class MarsOtcElrValidatorTests { this.javaClass.classLoader.getResourceAsStream("validation/marsotcelr/sample_2.hl7") val sampleMessage = sampleMessageInputStream!!.bufferedReader().use { it.readText() } - val messages = HL7Reader(ActionLogger()).getMessages(sampleMessage) - val report = validator.validate(messages[0]) + val message = HL7Reader.parseHL7Message(sampleMessage) + val report = validator.validate(message) assertThat(report.isValid()).isFalse() } @@ -28,8 +27,8 @@ class MarsOtcElrValidatorTests { this.javaClass.classLoader.getResourceAsStream("validation/marsotcelr/valid.hl7") val sampleMessage = sampleMessageInputStream!!.bufferedReader().use { it.readText() } - val messages = HL7Reader(ActionLogger()).getMessages(sampleMessage) - val report = validator.validate(messages[0]) + val message = HL7Reader.parseHL7Message(sampleMessage) + val report = validator.validate(message) assertThat(report.isValid()).isTrue() } @@ -39,8 +38,8 @@ class MarsOtcElrValidatorTests { 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]) + val message = HL7Reader.parseHL7Message(sampleMessage) + val report = validator.validate(message) assertThat(report.isValid()).isTrue() } } \ No newline at end of file diff --git a/prime-router/src/testIntegration/kotlin/datatests/TranslationTests.kt b/prime-router/src/testIntegration/kotlin/datatests/TranslationTests.kt index cfca5b5c76a..f18c57cfbdc 100644 --- a/prime-router/src/testIntegration/kotlin/datatests/TranslationTests.kt +++ b/prime-router/src/testIntegration/kotlin/datatests/TranslationTests.kt @@ -453,10 +453,7 @@ class TranslationTests { * @return a FHIR bundle as a JSON input stream */ private fun translateToFhir(hl7: String, profile: String? = null): InputStream { - val hl7message = HL7Reader.parseHL7Message( - hl7, - null - ) + val hl7message = HL7Reader.parseHL7Message(hl7) val fhirBundle = if (profile == null) { HL7toFhirTranslator().translate(hl7message) } else { diff --git a/prime-router/src/testIntegration/resources/datatests/FHIR_to_HL7/sample_WA_20240719-0001.hl7 b/prime-router/src/testIntegration/resources/datatests/FHIR_to_HL7/sample_WA_20240719-0001.hl7 index 94859fda75e..5c68836448b 100644 --- a/prime-router/src/testIntegration/resources/datatests/FHIR_to_HL7/sample_WA_20240719-0001.hl7 +++ b/prime-router/src/testIntegration/resources/datatests/FHIR_to_HL7/sample_WA_20240719-0001.hl7 @@ -1,4 +1,4 @@ -MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|Testing Lab^12D4567890^CLIA|WADOHPHRED^2.16.840.1.113883.3.237.4.2^ISO|dn1fro00^1.3.6.1.4.1.38630.2.1.1.19^ISO|20240815054718||ORU^R01^ORU_R01|12256b9a-0ca9-47ed-95b4-0d4ca60c4324|P|2.5.1|||NE|NE|USA|UNICODE UTF-8|ENG^English^ISO||PHLabReport-NoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO +MSH|^~\&|CDC PRIME - Atlanta, Georgia (Dekalb)^2.16.840.1.114222.4.1.237821^ISO|7uycso49^1.3.6.1.4.1.38630.2.1.1.519^ISO|WADOHPHRED^2.16.840.1.113883.3.237.4.2^ISO|dn1fro00^1.3.6.1.4.1.38630.2.1.1.19^ISO|20240815054718||ORU^R01^ORU_R01|12256b9a-0ca9-47ed-95b4-0d4ca60c4324|P|2.5.1|||NE|NE|USA|UNICODE UTF-8|ENG^English^ISO||PHLabReport-NoAck^ELR_Receiver^2.16.840.1.113883.9.11^ISO SFT|Centers for Disease Control and Prevention|0.2-SNAPSHOT|PRIME ReportStream|0.2-SNAPSHOT||20240812210736 PID|1||10083d1d-dc8b-4ea0-91fa-8744cf0f013b^^^Testing Lab&12D4567890&CLIA^PI^Testing Lab&12D4567890&CLIA||Wolf^Karolann^^^^^L||19700201|F||1002-5^native^HL70005^^^^2.5.1^^native|123 Main St^^Tamuning^ND^55987^USA||8002324636^PRS^CP^^1^800^2324636^^^^^8002324636~^NET^Internet^wolf@test.com||333^Unknown^99WA_LANG^^^^1|||||||H^Hispanic or Latino^HL70189^^^^2.9^^Hispanic or Latino||||||||N ORC|RE|12256b9a-0ca9-47ed-95b4-0d4ca60c4324^Testing Lab^12D4567890^CLIA|12256b9a-0ca9-47ed-95b4-0d4ca60c4324^Testing Lab^12D4567890^CLIA|||||||||1245319599^McTester^Phil^^^^^^NPI&2.16.840.1.113883.4.6&ISO^L^^^NPI||^WPN^PH^^1^530^8675309|20240815054718||||||Testing Lab^L^^^^CLIA&2.16.840.1.113883.4.7&ISO^XX^^^12D4567890|123 Beach Way^^Denver^CO^80210^USA|^WPN^PH^^1^530^8675309|321 Ocean Drive^^Denver^CO^80210^USA