diff --git a/.env b/.env index 2fecc8d5..c80dcf6a 100644 --- a/.env +++ b/.env @@ -1,19 +1,12 @@ -# Development URLS - -MONGO_URL = mongodb://rems-user:pass@127.0.0.1:27017 -MONGO_DB_NAME = remsadmin -WHITELIST = http://localhost, http://localhost:3005 +AUTH_SERVER_URI = http://localhost:8090 +HTTPS_CERT_PATH = server.cert +HTTPS_KEY_PATH = server.key LOGGING_LEVEL = debug +MONGO_DB_NAME = remsadmin +MONGO_URL = mongodb://rems-user:pass@127.0.0.1:27017 PORT = 8090 RESOURCE_SERVER = http://localhost:8090 -AUTH_SERVER_URI = http://localhost:8090 -VSAC_API_KEY = changeMe SMART_ENDPOINT = http://localhost:4040/launch -HTTPS_KEY_PATH = server.key -HTTPS_CERT_PATH = server.cert USE_HTTPS = false - -# To Override start command: -# REACT_APP_REMS_HOOKS_PATH=http://example.com PORT=6000 npm start -# Note: .env values can only be accessed by react app starting with 'REACT_APP_' -# Note: .env key and value will always be string. Non-string type must be parsed or cast \ No newline at end of file +VSAC_API_KEY = changeMe +WHITELIST = http://localhost, http://localhost:3005 diff --git a/NOTICE.md b/NOTICE.md index 22e6f9c7..ee959a64 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1,5 +1,5 @@ REMS Administrator -Copyright 2022 The MITRE Corporation +Copyright 2023 The MITRE Corporation -This project includes software developed at -Asymmetrik, a BlueHalo Company (https://bluehalo.com) \ No newline at end of file +This project includes software developed at +Asymmetrik, a BlueHalo Company (https://bluehalo.com) diff --git a/README.md b/README.md index 8cd996d3..c3d138df 100644 --- a/README.md +++ b/README.md @@ -1,75 +1,91 @@ # rems-admin ## Running only the REMS server project locally -1. Clone the REMS repositories from Github: + +1. Clone the REMS repositories from GitHub: ```bash - git clone https://github.com/mcode/rems-admin.git rems-admin + git clone https://github.com/mcode/rems-admin.git rems-admin ``` -2. Run dockerRunner.sh script +2. Run dockerRunner.sh script ```bash npm run start ``` ### How To Override Defaults -The .env file contains the default URI paths, these can be overwritten from the start command as follows: - `MONGO_URL=http://example.com SERVER_PORT=6000 npm start` - -Following are a list of modifiable paths: - -| URI Name | Default | -| ----------- | ----------- | -| MONGO_URL | `mongodb://rems-user:pass@127.0.0.1:27017` | -| MONGO_DB_NAME | `remsadmin` | -| WHITELIST | `http://localhost, http://localhost:3005` | -| LOGGING_LEVEL | `debug` | -| PORT | `8090` | -| RESOURCE_SERVER | `http://localhost:8090` | -| AUTH_SERVER_URI | `http://localhost:8090` | -| VSAC_API_KEY | `changeMe` | -| SMART_ENDPOINT | `http://localhost:3005/launch` | -| HTTPS_KEY_PATH | `server.key` | -| HTTPS_CERT_PATH | `server.cert` | -| USE_HTTPS | `false`| - -## Running the Mongo DB instance -1. On the first run use the following command to create a docker mongo instance: - ```bash - docker run --name rems_local_pims_remsadmin_mongo --expose 27017 -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME='rems-admin-pims-root' -e MONGO_INITDB_ROOT_PASSWORD='rems-admin-pims-password' -v rems_local_pims_remsadmin_mongo:/data/db -v "$(pwd)"/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js mongo - ``` - To stop the running container, simply use ctrl + c +The .env file contains the default URI paths, which can be overwritten from the start command as follows: +a) `REACT_APP_LAUNCH_URL=http://example.com PORT=6000 npm start` or b) by specifying the environment variables and desired values in a `.env.local`. + +> **Bug**: Do note that the `SMART_ENDPOINT` environment variable cannot be overwritten in a `.env.local`, it must be done in the `.env`. + +Following are a list of modifiable paths: + +| URI Name | Default | +| --------------- | ------------------------------------------ | +| AUTH_SERVER_URI | `http://localhost:8090` | +| HTTPS_CERT_PATH | `server.cert` | +| HTTPS_KEY_PATH | `server.key` | +| LOGGING_LEVEL | `debug` | +| MONGO_DB_NAME | `remsadmin` | +| MONGO_URL | `mongodb://rems-user:pass@127.0.0.1:27017` | +| PORT | `8090` | +| RESOURCE_SERVER | `http://localhost:8090` | +| SMART_ENDPOINT | `http://localhost:4040/launch` | +| USE_HTTPS | `false` | +| VSAC_API_KEY | `changeMe` | +| WHITELIST | `http://localhost, http://localhost:3005` | + +## Running the Mongo DB instance + +1. On the first run use the following command to create a Docker MongoDB instance: + + ```bash + docker run --name rems_local_pims_remsadmin_mongo --expose 27017 -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME='rems-admin-pims-root' -e MONGO_INITDB_ROOT_PASSWORD='rems-admin-pims-password' -v rems_local_pims_remsadmin_mongo:/data/db -v "$(pwd)"/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js mongo + ``` + + To stop the running container, simply use Ctrl + C. + +2. On subsequent runs use the following command to start the existing mongo container: + ```bash + docker start rems_local_pims_remsadmin_mongo + ``` + To stop the running container, simply run the below command + ```bash + docker stop rems_local_pims_remsadmin_mongo + ``` -2. On subsequent runs use the following command to start the existing mongo container: - ```bash - docker start rems_local_pims_remsadmin_mongo - ``` - To stop the running container, simply run the below command - ```bash - docker stop rems_local_pims_remsadmin_mongo - ``` # REMS Administrator + NOTE: The REMS Administrator is a work in progress. -## Running the REMS Adminstrator +## Running the REMS Administrator #### Initialization + After cloning the repository, the submodules must be initialized. To do this you can run: ``` git submodule update --init ``` + #### Setup + ``` npm install ``` + #### Run Tests + ``` npm test ``` + #### Run Application + ``` npm start ``` + Application will be running on port 8090. To reach the CDS Services discovery information: diff --git a/prototype-images/layout.png b/prototype-images/layout.png deleted file mode 100644 index 1d39a5c1..00000000 Binary files a/prototype-images/layout.png and /dev/null differ diff --git a/setup-images/ClosingLaunchChromeTask.png b/setup-images/ClosingLaunchChromeTask.png deleted file mode 100644 index ce0cb60b..00000000 Binary files a/setup-images/ClosingLaunchChromeTask.png and /dev/null differ diff --git a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-AssessmentAndPlan.json b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-AssessmentAndPlan.json index 5597281d..1724d07a 100644 --- a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-AssessmentAndPlan.json +++ b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-AssessmentAndPlan.json @@ -2,6 +2,7 @@ "resourceType": "Questionnaire", "id": "assessment-and-plan", "name": "Assessment and Plan Module", + "url": "http://hl7.org/fhir/Questionnaire/assessment-and-plan", "meta": { "profile": [ "http://hl7.org/fhir/StructureDefinition/cqf-questionnaire", diff --git a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-Encounter.json b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-Encounter.json index 19031025..1afac0cd 100644 --- a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-Encounter.json +++ b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-Encounter.json @@ -2,6 +2,7 @@ "resourceType": "Questionnaire", "id": "encounter", "name": "Encounter Module", + "url": "http://hl7.org/fhir/Questionnaire/encounter", "meta": { "profile": [ "http://hl7.org/fhir/StructureDefinition/cqf-questionnaire", diff --git a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PatientInfo.json b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PatientInfo.json index a7a6eb24..64793164 100644 --- a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PatientInfo.json +++ b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PatientInfo.json @@ -2,6 +2,7 @@ "resourceType": "Questionnaire", "id": "patient-info", "name": "Patient Module", + "url": "http://hl7.org/fhir/Questionnaire/patient-info", "meta": { "profile": [ "http://hl7.org/fhir/StructureDefinition/cqf-questionnaire", diff --git a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PatientInfoBase.json b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PatientInfoBase.json index 1fb44220..b8a3d55f 100644 --- a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PatientInfoBase.json +++ b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PatientInfoBase.json @@ -2,6 +2,7 @@ "resourceType": "Questionnaire", "id": "patient-info-base", "name": "Base Patient Module", + "url": "http://hl7.org/fhir/Questionnaire/patient-info-base", "meta": { "profile": [ "http://hl7.org/fhir/StructureDefinition/cqf-questionnaire", diff --git a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PatientSignature.json b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PatientSignature.json index bf456917..839bebe0 100644 --- a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PatientSignature.json +++ b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PatientSignature.json @@ -2,6 +2,7 @@ "resourceType": "Questionnaire", "id": "patient-signature", "name": "Patient Signature", + "url": "http://hl7.org/fhir/Questionnaire/patient-signature", "meta": { "profile": [ "http://hl7.org/fhir/StructureDefinition/cqf-questionnaire", diff --git a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PhysicalExam.json b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PhysicalExam.json index 3a87200c..8a0ae2e1 100644 --- a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PhysicalExam.json +++ b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PhysicalExam.json @@ -2,6 +2,7 @@ "resourceType": "Questionnaire", "id": "physical-exam", "name": "Physican Exam Module", + "url": "http://hl7.org/fhir/Questionnaire/physical-exam", "meta": { "profile": [ "http://hl7.org/fhir/StructureDefinition/cqf-questionnaire", diff --git a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PractitionerInfo.json b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PractitionerInfo.json index e930b5d5..39805ca4 100644 --- a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PractitionerInfo.json +++ b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PractitionerInfo.json @@ -2,6 +2,7 @@ "resourceType": "Questionnaire", "id": "practitioner-info", "name": "Practitioner Module", + "url": "http://hl7.org/fhir/Questionnaire/practitioner-info", "meta": { "profile": [ "http://hl7.org/fhir/StructureDefinition/cqf-questionnaire", diff --git a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PractitionerInfoBase.json b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PractitionerInfoBase.json index f0ee8a7b..0ff1eea0 100644 --- a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PractitionerInfoBase.json +++ b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-PractitionerInfoBase.json @@ -2,6 +2,7 @@ "resourceType": "Questionnaire", "id": "practitioner-info-base", "name": "Base Practitioner Module", + "url": "http://hl7.org/fhir/Questionnaire/practitioner-info-base", "meta": { "profile": [ "http://hl7.org/fhir/StructureDefinition/cqf-questionnaire", diff --git a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-ProviderSignature.json b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-ProviderSignature.json index 4a8a92f9..8a6f8c28 100644 --- a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-ProviderSignature.json +++ b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-ProviderSignature.json @@ -2,6 +2,8 @@ "resourceType": "Questionnaire", "id": "provider-signature", "name": "Provider Signature", + "url": "http://hl7.org/fhir/Questionnaire/provider-signature", + "meta": { "profile": [ "http://hl7.org/fhir/StructureDefinition/cqf-questionnaire", diff --git a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-ReviewOfSystem.json b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-ReviewOfSystem.json index 338e199b..e186971c 100644 --- a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-ReviewOfSystem.json +++ b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-ReviewOfSystem.json @@ -1,6 +1,7 @@ { "resourceType": "Questionnaire", "id": "review-of-system", + "url": "http://hl7.org/fhir/Questionnaire/review-of-system", "meta": { "profile": [ "http://hl7.org/fhir/StructureDefinition/cqf-questionnaire", diff --git a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-Subjective.json b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-Subjective.json index cc1328ab..6493ef6c 100644 --- a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-Subjective.json +++ b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-Subjective.json @@ -2,6 +2,8 @@ "resourceType": "Questionnaire", "id": "subjective", "name": "Subjective Module", + "url": "http://hl7.org/fhir/Questionnaire/subjective", + "meta": { "profile": [ "http://hl7.org/fhir/StructureDefinition/cqf-questionnaire", diff --git a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-VitalSigns.json b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-VitalSigns.json index efa8402a..dd99ee6f 100644 --- a/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-VitalSigns.json +++ b/src/cds-library/CRD-DTR/Shared/R4/resources/Questionnaire-R4-VitalSigns.json @@ -1,6 +1,8 @@ { "resourceType": "Questionnaire", "id": "vital-signs", + "url": "http://hl7.org/fhir/Questionnaire/vital-signs", + "meta": { "profile": [ "http://hl7.org/fhir/StructureDefinition/cqf-questionnaire", diff --git a/src/cds-library/CRD-DTR/TIRF/R4/resources/Questionnaire-R4-Prescriber-Knowledge-Assessment-TIRF.json b/src/cds-library/CRD-DTR/TIRF/R4/resources/Questionnaire-R4-Prescriber-Knowledge-Assessment-TIRF.json index 600bca2e..21661c92 100644 --- a/src/cds-library/CRD-DTR/TIRF/R4/resources/Questionnaire-R4-Prescriber-Knowledge-Assessment-TIRF.json +++ b/src/cds-library/CRD-DTR/TIRF/R4/resources/Questionnaire-R4-Prescriber-Knowledge-Assessment-TIRF.json @@ -25,12 +25,12 @@ "linkId": "1.1", "extension": [ { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire", - "valueCanonical": "questionnaire/practitioner-info-base" + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire", + "valueCanonical": "http://hl7.org/fhir/Questionnaire/practitioner-info-base" }, { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire-expand", - "valueBoolean": true + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-collapsible", + "valueCode": "default-open" } ], "type": "display" @@ -366,8 +366,8 @@ "linkId": "2.2", "extension": [ { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire", - "valueCanonical": "questionnaire/provider-signature" + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire", + "valueCanonical": "http://hl7.org/fhir/Questionnaire/provider-signature" } ], "type": "display" diff --git a/src/cds-library/CRD-DTR/TIRF/R4/resources/Questionnaire-R4-PrescriberEnrollment-TIRF.json b/src/cds-library/CRD-DTR/TIRF/R4/resources/Questionnaire-R4-PrescriberEnrollment-TIRF.json index 5bd192ca..be8a4684 100644 --- a/src/cds-library/CRD-DTR/TIRF/R4/resources/Questionnaire-R4-PrescriberEnrollment-TIRF.json +++ b/src/cds-library/CRD-DTR/TIRF/R4/resources/Questionnaire-R4-PrescriberEnrollment-TIRF.json @@ -25,12 +25,12 @@ "linkId": "1.1", "extension": [ { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire", - "valueCanonical": "questionnaire/practitioner-info-base" + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire", + "valueCanonical": "http://hl7.org/fhir/Questionnaire/practitioner-info-base" }, { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire-expand", - "valueBoolean": true + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-collapsible", + "valueCode": "default-open" } ], "type": "display" @@ -413,8 +413,8 @@ "linkId": "4.2", "extension": [ { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire", - "valueCanonical": "questionnaire/provider-signature" + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire", + "valueCanonical": "http://hl7.org/fhir/Questionnaire/provider-signature" } ], "type": "display" diff --git a/src/cds-library/CRD-DTR/TIRF/R4/resources/Questionnaire-R4-TIRF.json b/src/cds-library/CRD-DTR/TIRF/R4/resources/Questionnaire-R4-TIRF.json index b30144cd..1c8ec8ab 100644 --- a/src/cds-library/CRD-DTR/TIRF/R4/resources/Questionnaire-R4-TIRF.json +++ b/src/cds-library/CRD-DTR/TIRF/R4/resources/Questionnaire-R4-TIRF.json @@ -25,12 +25,12 @@ "linkId": "1.1", "extension": [ { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire", - "valueCanonical": "questionnaire/patient-info-base" + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire", + "valueCanonical": "http://hl7.org/fhir/Questionnaire/patient-info-base" }, { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire-expand", - "valueBoolean": true + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-collapsible", + "valueCode": "default-open" } ], "type": "display" @@ -39,7 +39,7 @@ "linkId": "1.2", "text": "Race", "type": "open-choice", - "required": false, + "required": true, "repeats": true, "answerOption": [ { @@ -78,7 +78,6 @@ "linkId": "1.3", "text": "Ethnicity", "type": "group", - "required": true, "item": [ { "linkId": "1.3.1", @@ -216,7 +215,7 @@ "linkId": "1.12", "text": "Is there a child in the home or are you a caregiver of small children?", "type": "boolean", - "required": true + "required": false }, { "linkId": "1.13", @@ -278,8 +277,8 @@ "linkId": "3.2", "extension": [ { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire", - "valueCanonical": "questionnaire/patient-signature" + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire", + "valueCanonical": "http://hl7.org/fhir/Questionnaire/patient-signature" } ], "type": "display" @@ -452,31 +451,59 @@ "linkId": "5.1", "text": "Prior TIRF Use within the last 6 months", "type": "boolean", - "required": true + "required": false }, { "linkId": "5.2", "text": "TIRF Product Name", "type": "string", - "required": true + "required": true, + "enableWhen": [ + { + "question": "5.1", + "operator": "=", + "answerBoolean": true + } + ] }, { "linkId": "5.3", "text": "Strength", "type": "string", - "required": true + "required": true, + "enableWhen": [ + { + "question": "5.1", + "operator": "=", + "answerBoolean": true + } + ] }, { "linkId": "5.4", "text": "Dose", "type": "string", - "required": true + "required": true, + "enableWhen": [ + { + "question": "5.1", + "operator": "=", + "answerBoolean": true + } + ] }, { "linkId": "5.5", "text": "Frequency", "type": "string", - "required": true + "required": true, + "enableWhen": [ + { + "question": "5.1", + "operator": "=", + "answerBoolean": true + } + ] }, { "linkId": "5.6", @@ -496,6 +523,13 @@ "display": "Non-cancer Pain" } } + ], + "enableWhen": [ + { + "question": "5.1", + "operator": "=", + "answerBoolean": true + } ] } ] @@ -504,7 +538,7 @@ "linkId": "6", "type": "group", "text": "Verify Opioid Tolerance", - "required": true, + "required": false, "item": [ { "linkId": "6.1", @@ -808,8 +842,8 @@ "linkId": "7.2", "extension": [ { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire", - "valueCanonical": "questionnaire/provider-signature" + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire", + "valueCanonical": "http://hl7.org/fhir/Questionnaire/provider-signature" } ], "type": "display" diff --git a/src/cds-library/CRD-DTR/Turalio/R4/resources/Questionnaire-R4-PrescriberEnrollment.json b/src/cds-library/CRD-DTR/Turalio/R4/resources/Questionnaire-R4-PrescriberEnrollment.json index d86fa4ba..2e082a9b 100644 --- a/src/cds-library/CRD-DTR/Turalio/R4/resources/Questionnaire-R4-PrescriberEnrollment.json +++ b/src/cds-library/CRD-DTR/Turalio/R4/resources/Questionnaire-R4-PrescriberEnrollment.json @@ -504,8 +504,8 @@ "linkId": "4.2", "extension": [ { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire", - "valueCanonical": "questionnaire/provider-signature" + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire", + "valueCanonical": "http://hl7.org/fhir/Questionnaire/provider-signature" } ], "type": "display" diff --git a/src/cds-library/CRD-DTR/Turalio/R4/resources/Questionnaire-R4-Turalio.json b/src/cds-library/CRD-DTR/Turalio/R4/resources/Questionnaire-R4-Turalio.json index 6efc860c..7142bd5c 100644 --- a/src/cds-library/CRD-DTR/Turalio/R4/resources/Questionnaire-R4-Turalio.json +++ b/src/cds-library/CRD-DTR/Turalio/R4/resources/Questionnaire-R4-Turalio.json @@ -25,12 +25,12 @@ "linkId": "1.1", "extension": [ { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire", - "valueCanonical": "questionnaire/patient-info-base" + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire", + "valueCanonical": "http://hl7.org/fhir/Questionnaire/patient-info-base" }, { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire-expand", - "valueBoolean": true + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-collapsible", + "valueCode": "default-open" } ], "type": "display" @@ -325,12 +325,12 @@ "linkId": "2.1", "extension": [ { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire", - "valueCanonical": "questionnaire/practitioner-info-base" + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire", + "valueCanonical": "http://hl7.org/fhir/Questionnaire/practitioner-info-base" }, { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire-expand", - "valueBoolean": true + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-collapsible", + "valueCode": "default-open" } ], "type": "display" @@ -1137,8 +1137,8 @@ "linkId": "6.2", "extension": [ { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire", - "valueCanonical": "questionnaire/provider-signature" + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire", + "valueCanonical": "http://hl7.org/fhir/Questionnaire/provider-signature" } ], "type": "display" @@ -1160,8 +1160,8 @@ "linkId": "7.2", "extension": [ { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire", - "valueCanonical": "questionnaire/patient-signature" + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire", + "valueCanonical": "http://hl7.org/fhir/Questionnaire/patient-signature" } ], "type": "display" diff --git a/src/cds-library/CRD-DTR/Turalio/R4/resources/Questionnaire-R4-TuralioProgressNote.json b/src/cds-library/CRD-DTR/Turalio/R4/resources/Questionnaire-R4-TuralioProgressNote.json index fd80e731..23fd20fb 100644 --- a/src/cds-library/CRD-DTR/Turalio/R4/resources/Questionnaire-R4-TuralioProgressNote.json +++ b/src/cds-library/CRD-DTR/Turalio/R4/resources/Questionnaire-R4-TuralioProgressNote.json @@ -26,8 +26,8 @@ "linkId": "1", "extension": [ { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire", - "valueCanonical": "questionnaire/patient-info" + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire", + "valueCanonical": "http://hl7.org/fhir/Questionnaire/patient-info" } ], "type": "display", @@ -37,12 +37,12 @@ "linkId": "2", "extension": [ { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire", - "valueCanonical": "questionnaire/practitioner-info-base" + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire", + "valueCanonical": "http://hl7.org/fhir/Questionnaire/practitioner-info-base" }, { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire-expand", - "valueBoolean": true + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-collapsible", + "valueCode": "default-open" } ], "type": "display", @@ -504,8 +504,8 @@ "linkId": "5", "extension": [ { - "url": "http://hl7.org/fhir/StructureDefinition/sub-questionnaire", - "valueCanonical": "questionnaire/provider-signature" + "url": "http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire", + "valueCanonical": "http://hl7.org/fhir/Questionnaire/provider-signature" } ], "type": "display", diff --git a/src/fhir/questionnaireUtilities.ts b/src/fhir/questionnaireUtilities.ts index 9e7c54a2..64b30207 100644 --- a/src/fhir/questionnaireUtilities.ts +++ b/src/fhir/questionnaireUtilities.ts @@ -104,8 +104,9 @@ export class QuestionnaireUtilities { const returnValue = this.vsacCache.cacheLibrary(library); return returnValue; } - static async findQuestionnaire(id: string): Promise { - return await QuestionnaireModel.findOne({ id: id.toString() }); + + static async findQuestionnaireByUrl(url: string): Promise { + return await QuestionnaireModel.findOne({ url: url.toString() }); } static async findLibraryByUrl(url: string): Promise { return await LibraryModel.findOne({ url: url.toString() }); @@ -167,26 +168,22 @@ export class QuestionnaireUtilities { ) { const ext = this.getExtension( item, - 'http://hl7.org/fhir/StructureDefinition/sub-questionnaire' + 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-subQuestionnaire' ); if (ext) { const subQ = ext.valueCanonical; + console.log(subQ); if (subQ) { - // not undefind - let id = subQ; - const parts = subQ.split('/'); - if (id.length > 1) { - id = parts[1]; - } + // not undefined let expandRootItem = false; const expandExt = this.getExtension( item, - 'http://hl7.org/fhir/StructureDefinition/sub-questionnaire-expand' + 'http://hl7.org/fhir/uv/sdc/StructureDefinition/sdc-questionnaire-collapsible' ); if (expandExt && expandExt.valueBoolean) { - expandRootItem = expandExt.valueBoolean; + expandRootItem = expandExt.valueCode === 'default-open'; } - const subQuestionnaire = await this.findQuestionnaire(id); + const subQuestionnaire = await this.findQuestionnaireByUrl(subQ); if (subQuestionnaire) { const subExtensions = subQuestionnaire.extension || []; subExtensions.forEach(ext => { diff --git a/src/hooks/hookResources.ts b/src/hooks/hookResources.ts index 29f6fdda..ff7588f6 100644 --- a/src/hooks/hookResources.ts +++ b/src/hooks/hookResources.ts @@ -1,8 +1,22 @@ -import { MedicationRequest, Coding } from 'fhir/r4'; -import { Link } from '../cards/Card'; -import { TypedRequestBody } from '../rems-cds-hooks/resources/HookTypes'; -import axios from 'axios'; +import { MedicationRequest, Coding, FhirResource, Identifier } from 'fhir/r4'; +import Card, { Link } from '../cards/Card'; +import { + HookPrefetch, + OrderSignPrefetch, + TypedRequestBody +} from '../rems-cds-hooks/resources/HookTypes'; +import config from '../config'; +import { medicationCollection, remsCaseCollection } from '../fhir/models'; +import axios from 'axios'; +import { ServicePrefetch } from '../rems-cds-hooks/resources/CdsService'; +import { hydrate } from '../rems-cds-hooks/prefetch/PrefetchHydrator'; +type HandleCallback = ( + res: any, + hydratedPrefetch: HookPrefetch | undefined, + contextRequest: FhirResource | undefined, + patient: FhirResource | undefined +) => Promise; export interface CardRule { links: Link[]; summary?: string; @@ -189,21 +203,10 @@ export const validCodes: Coding[] = [ system: 'http://www.nlm.nih.gov/research/umls/rxnorm' } ]; - -export function getFhirResource(token: string, req: TypedRequestBody) { - const ehrUrl = `${req.body.fhirServer}/${token}`; - const access_token = req.body.fhirAuthorization?.access_token; - const options = { - method: 'GET', - headers: { - Authorization: `Bearer ${access_token}` - } - }; - const response = axios(ehrUrl, options); - return response.then(e => { - return e.data; - }); -} +const source = { + label: 'MCODE REMS Administrator Prototype', + url: new URL('https://github.com/mcode/rems-admin') +}; /* * Retrieve the coding for the medication from the medicationCodeableConcept if available. @@ -230,3 +233,233 @@ export function getDrugCodeFromMedicationRequest(medicationRequest: MedicationRe } return null; } +export function getFhirResource(token: string, req: TypedRequestBody) { + const ehrUrl = `${req.body.fhirServer}/${token}`; + const access_token = req.body.fhirAuthorization?.access_token; + const options = { + method: 'GET', + headers: { + Authorization: `Bearer ${access_token}` + } + }; + const response = axios(ehrUrl, options); + return response.then(e => { + return e.data; + }); +} +export function createSmartLink( + requirementName: string, + appContext: string, + request: MedicationRequest | undefined +) { + const newLink: Link = { + label: requirementName + ' Form', + url: new URL(config.smart.endpoint), + type: 'smart', + appContext: `${appContext}&order=${JSON.stringify(request)}&coverage=${ + request?.insurance?.[0].reference + }` + }; + return newLink; +} +export function buildErrorCard(reason: string) { + const errorCard = new Card('Bad Request', reason, source, 'warning'); + const cards = { + cards: [errorCard.card] + }; + return cards; +} + +// handles order-sign and order-select currently +export async function handleCardOrder( + res: any, + hydratedPrefetch: HookPrefetch | undefined, + contextRequest: FhirResource | undefined, + patient: FhirResource | undefined +) { + const prefetchRequest = hydratedPrefetch?.request; + console.log(' MedicationRequest: ' + prefetchRequest?.id); + // verify there is a contextRequest + if (!contextRequest) { + res.json(buildErrorCard('DraftOrders does not contain a request')); + return; + } + + // verify a MedicationRequest was sent + if (contextRequest && contextRequest.resourceType !== 'MedicationRequest') { + res.json(buildErrorCard('DraftOrders does not contain a MedicationRequest')); + return; + } + if ( + prefetchRequest?.id && + contextRequest && + contextRequest.id && + prefetchRequest.id.replace('MedicationRequest/', '') !== + contextRequest.id.replace('MedicationRequest/', '') + ) { + res.json(buildErrorCard('Context draftOrder does not match prefetch MedicationRequest ID')); + return; + } + + const medicationCode = + contextRequest && + contextRequest.resourceType === 'MedicationRequest' && + getDrugCodeFromMedicationRequest(contextRequest); + if (!medicationCode) { + return; + } + if (medicationCode && medicationCode?.code) { + // find the drug in the medicationCollection to get the smart links + const drug = await medicationCollection + .findOne({ + code: medicationCode.code, + codeSystem: medicationCode.system + }) + .exec(); + + // find a matching rems case for the patient and this drug to only return needed results + const patientName = patient?.resourceType === 'Patient' ? patient?.name?.[0] : undefined; + const patientBirth = patient?.resourceType === 'Patient' ? patient?.birthDate : undefined; + const etasu = await remsCaseCollection.findOne({ + patientFirstName: patientName?.given?.[0], + patientLastName: patientName?.family, + patientDOB: patientBirth, + drugCode: medicationCode?.code + }); + + const returnCard = validCodes.some(e => { + return e.code === medicationCode.code && e.system === medicationCode.system; + }); + if (returnCard) { + const cardArray: Card[] = []; + const codeRule = codeMap[medicationCode.code]; + for (const rule of codeRule) { + const card = new Card( + rule.summary || medicationCode.display || 'Rems', + CARD_DETAILS, + source, + 'info' + ); + rule.links.forEach(function (e) { + if (e.type == 'absolute') { + // no construction needed + card.addLink(e); + } + }); + + let smartLinkCount = 0; + + // process the smart links from the medicationCollection + // TODO: smart links should be built with discovered questionnaires, not hard coded ones + if (drug) { + for (const requirement of drug.requirements) { + if (requirement.stakeholderType == rule.stakeholderType) { + // only add the link if the form has not already been processed / received + if (etasu) { + let found = false; + for (const metRequirement of etasu.metRequirements) { + if (metRequirement.requirementName == requirement.name) { + found = true; + if (!metRequirement.completed) { + card.addLink( + createSmartLink(requirement.name, requirement.appContext, contextRequest) + ); + smartLinkCount++; + } + } + } + if (!found) { + card.addLink( + createSmartLink(requirement.name, requirement.appContext, contextRequest) + ); + smartLinkCount++; + } + } else { + // add all the required to dispense links if no etasu to check + if (requirement.requiredToDispense) { + card.addLink( + createSmartLink(requirement.name, requirement.appContext, contextRequest) + ); + smartLinkCount++; + } + } + } + } + } + + // only add the card if there are smart links to needed forms + if (smartLinkCount > 0) { + cardArray.push(card); + } + } + res.json({ + cards: cardArray + }); + } else { + res.json(buildErrorCard('Unsupported code')); + } + } else { + res.json(buildErrorCard('MedicationRequest does not contain a code')); + } +} + +// handles preliminary card creation. ALL hooks should go through this function. +// make sure code here is applicable to all supported hooks. +export async function handleCard( + req: TypedRequestBody, + res: any, + hydratedPrefetch: HookPrefetch, + contextRequest: FhirResource | undefined, + callback: HandleCallback +) { + const context = req.body.context; + const patient = hydratedPrefetch?.patient; + const practitioner = hydratedPrefetch?.practitioner; + + console.log(' Patient: ' + patient?.id); + + // verify ids + if ( + patient?.id && + patient.id.replace('Patient/', '') !== context.patientId.replace('Patient/', '') + ) { + res.json(buildErrorCard('Context patientId does not match prefetch Patient ID')); + return; + } + if ( + practitioner?.id && + practitioner.id.replace('Practitioner/', '') !== context.userId.replace('Practitioner/', '') + ) { + res.json(buildErrorCard('Context userId does not match prefetch Practitioner ID')); + return; + } + return callback(res, hydratedPrefetch, contextRequest, patient); +} + +// handles all hooks, any supported hook should pass through this function +export function handleHook( + req: TypedRequestBody, + res: any, + hookPrefetch: ServicePrefetch, + contextRequest: FhirResource | undefined, + callback: HandleCallback +) { + try { + const fhirUrl = req.body.fhirServer; + const fhirAuth = req.body.fhirAuthorization; + if (fhirUrl && fhirAuth && fhirAuth.access_token) { + hydrate(getFhirResource, hookPrefetch, req.body).then(hydratedPrefetch => { + handleCard(req, res, hydratedPrefetch, contextRequest, callback); + }); + } else { + if (req.body.prefetch) { + handleCard(req, res, req.body.prefetch, contextRequest, callback); + } else { + handleCard(req, res, {}, contextRequest, callback); + } + } + } catch (error) { + console.log(error); + res.json(buildErrorCard('Unknown Error')); + } +} diff --git a/src/hooks/rems.orderselect.ts b/src/hooks/rems.orderselect.ts index 636a4aee..9300c1bd 100644 --- a/src/hooks/rems.orderselect.ts +++ b/src/hooks/rems.orderselect.ts @@ -1,22 +1,6 @@ -import Card from '../cards/Card'; -import { - SupportedHooks, - OrderSelectPrefetch, - OrderSelectHook -} from '../rems-cds-hooks/resources/HookTypes'; -import { medicationCollection, remsCaseCollection } from '../fhir/models'; +import { SupportedHooks, OrderSelectHook } from '../rems-cds-hooks/resources/HookTypes'; import { ServicePrefetch, CdsService } from '../rems-cds-hooks/resources/CdsService'; -import { MedicationRequest } from 'fhir/r4'; -import { Link } from '../cards/Card'; -import config from '../config'; -import { hydrate } from '../rems-cds-hooks/prefetch/PrefetchHydrator'; -import { - validCodes, - codeMap, - CARD_DETAILS, - getDrugCodeFromMedicationRequest -} from './hookResources'; -import axios from 'axios'; +import { handleCardOrder, handleHook } from './hookResources'; interface TypedRequestBody extends Express.Request { body: OrderSelectHook; @@ -33,223 +17,17 @@ const definition: CdsService = { description: 'REMS Requirement Lookup', prefetch: hookPrefetch }; -const source = { - label: 'MCODE REMS Administrator Prototype', - url: new URL('https://github.com/mcode/rems-admin') -}; -function buildErrorCard(reason: string) { - const errorCard = new Card('Bad Request', reason, source, 'warning'); - const cards = { - cards: [errorCard.card] - }; - return cards; -} const handler = (req: TypedRequestBody, res: any) => { - function getFhirResource(token: string) { - const ehrUrl = `${req.body.fhirServer}/${token}`; - const access_token = req.body.fhirAuthorization?.access_token; - const options = { - method: 'GET', - headers: { - Authorization: `Bearer ${access_token}` - } - }; - const response = axios(ehrUrl, options); - return response.then(e => { - return e.data; - }); - } - - function createSmartLink( - requirementName: string, - appContext: string, - request: MedicationRequest - ) { - const newLink: Link = { - label: requirementName + ' Form', - url: new URL(config.smart.endpoint), - type: 'smart', - appContext: `${appContext}&order=${JSON.stringify(request)}&coverage=${ - request.insurance?.[0].reference - }` - }; - return newLink; - } - - async function handleCard(hydratedPrefetch: OrderSelectPrefetch) { - console.log(hydratedPrefetch); - const context = req.body.context; - // const contextRequest = context.draftOrders?.entry?.[0].resource; - const selection = context.selections?.[0]; - const contextRequest = context.draftOrders?.entry?.filter(entry => { - if (entry.resource) { - return selection === `${entry.resource.resourceType}/${entry.resource.id}`; - } - })[0].resource; - const patient = hydratedPrefetch?.patient; - const prefetchRequest = context.draftOrders?.entry?.[0].resource; - const practitioner = hydratedPrefetch?.practitioner; - const npi = practitioner?.identifier; - - console.log(' MedicationRequest: ' + prefetchRequest?.id); - console.log(' Practitioner: ' + practitioner?.id + ' NPI: ' + npi); - console.log(' Patient: ' + patient?.id); - - // verify there is a contextRequest - if (!contextRequest) { - res.json(buildErrorCard('DraftOrders does not contain a request')); - return; - } - - // verify a MedicationRequest was sent - if (contextRequest && contextRequest.resourceType !== 'MedicationRequest') { - res.json(buildErrorCard('DraftOrders does not contain a MedicationRequest')); - return; - } - - // verify ids - if ( - patient?.id && - patient.id.replace('Patient/', '') !== context.patientId.replace('Patient/', '') - ) { - res.json(buildErrorCard('Context patientId does not match prefetch Patient ID')); - return; - } - if ( - practitioner?.id && - practitioner.id.replace('Practitioner/', '') !== context.userId.replace('Practitioner/', '') - ) { - res.json(buildErrorCard('Context userId does not match prefetch Practitioner ID')); - return; - } - if ( - prefetchRequest?.id && - contextRequest && - contextRequest.id && - prefetchRequest.id.replace('MedicationRequest/', '') !== - contextRequest.id.replace('MedicationRequest/', '') - ) { - res.json(buildErrorCard('Context draftOrder does not match prefetch MedicationRequest ID')); - return; - } - - const medicationCode = getDrugCodeFromMedicationRequest(contextRequest); - if (medicationCode && medicationCode.code) { - // find the drug in the medicationCollection to get the smart links - const drug = await medicationCollection - .findOne({ - code: medicationCode.code, - codeSystem: medicationCode.system - }) - .exec(); - - // find a matching rems case for the patient and this drug to only return needed results - const patientName = patient?.name?.[0]; - const etasu = await remsCaseCollection.findOne({ - patientFirstName: patientName?.given?.[0], - patientLastName: patientName?.family, - patientDOB: patient?.birthDate, - drugCode: medicationCode?.code - }); - - const returnCard = validCodes.some(e => { - return e.code === medicationCode.code && e.system === medicationCode.system; - }); - if (returnCard) { - const cardArray: Card[] = []; - const codeRule = codeMap[medicationCode.code]; - for (const rule of codeRule) { - const card = new Card( - rule.summary || medicationCode.display || 'Rems', - CARD_DETAILS, - source, - 'info' - ); - rule.links.forEach(function (e) { - if (e.type == 'absolute') { - // no construction needed - card.addLink(e); - } - }); - - let smartLinkCount = 0; - - // process the smart links from the medicationCollection - // TODO: smart links should be built with discovered questionnaires, not hard coded ones - if (drug) { - for (const requirement of drug.requirements) { - if (requirement.stakeholderType == rule.stakeholderType) { - // only add the link if the form has not already been processed / received - if (etasu) { - let found = false; - for (const metRequirement of etasu.metRequirements) { - if (metRequirement.requirementName == requirement.name) { - found = true; - if (!metRequirement.completed) { - card.addLink( - createSmartLink(requirement.name, requirement.appContext, contextRequest) - ); - smartLinkCount++; - } - } - } - if (!found) { - card.addLink( - createSmartLink(requirement.name, requirement.appContext, contextRequest) - ); - smartLinkCount++; - } - } else { - // add all the required to dispense links if no etasu to check - if (requirement.requiredToDispense) { - card.addLink( - createSmartLink(requirement.name, requirement.appContext, contextRequest) - ); - smartLinkCount++; - } - } - } - } - } - - // only add the card if there are smart links to needed forms - if (smartLinkCount > 0) { - cardArray.push(card); - } - } - res.json({ - cards: cardArray - }); - } else { - res.json(buildErrorCard('Unsupported code')); - } - } else { - res.json(buildErrorCard('MedicationRequest does not contain a code')); - } - } - console.log('REMS order-select hook'); - try { - const fhirUrl = req.body.fhirServer; - const fhirAuth = req.body.fhirAuthorization; - if (fhirUrl && fhirAuth && fhirAuth.access_token) { - hydrate(getFhirResource, hookPrefetch, req.body).then( - (hydratedPrefetch: OrderSelectPrefetch) => { - handleCard(hydratedPrefetch); - } - ); - } else { - if (req.body.prefetch) { - handleCard(req.body.prefetch); - } else { - handleCard({}); - } + const context = req.body.context; + const selection = context.selections?.[0]; + const contextRequest = context.draftOrders?.entry?.filter(entry => { + if (entry.resource) { + return selection === `${entry.resource.resourceType}/${entry.resource.id}`; } - } catch (error) { - console.log(error); - res.json(buildErrorCard('Unknown Error')); - } + })[0].resource; + handleHook(req, res, hookPrefetch, contextRequest, handleCardOrder); }; export default { definition, handler }; diff --git a/src/hooks/rems.ordersign.ts b/src/hooks/rems.ordersign.ts index 2e621aea..3e0fedc0 100644 --- a/src/hooks/rems.ordersign.ts +++ b/src/hooks/rems.ordersign.ts @@ -1,22 +1,6 @@ -import Card from '../cards/Card'; -import { - OrderSignHook, - SupportedHooks, - OrderSignPrefetch -} from '../rems-cds-hooks/resources/HookTypes'; -import { medicationCollection, remsCaseCollection } from '../fhir/models'; +import { OrderSignHook, SupportedHooks } from '../rems-cds-hooks/resources/HookTypes'; import { ServicePrefetch, CdsService } from '../rems-cds-hooks/resources/CdsService'; -import { MedicationRequest } from 'fhir/r4'; -import { Link } from '../cards/Card'; -import config from '../config'; -import { hydrate } from '../rems-cds-hooks/prefetch/PrefetchHydrator'; -import { - validCodes, - codeMap, - CARD_DETAILS, - getDrugCodeFromMedicationRequest -} from './hookResources'; -import axios from 'axios'; +import { handleCardOrder, handleHook } from './hookResources'; interface TypedRequestBody extends Express.Request { body: OrderSignHook; @@ -33,225 +17,11 @@ const definition: CdsService = { description: 'REMS Requirement Lookup', prefetch: hookPrefetch }; -const source = { - label: 'MCODE REMS Administrator Prototype', - url: new URL('https://github.com/mcode/rems-admin') -}; -function buildErrorCard(reason: string) { - const errorCard = new Card('Bad Request', reason, source, 'warning'); - const cards = { - cards: [errorCard.card] - }; - return cards; -} const handler = (req: TypedRequestBody, res: any) => { - async function getFhirResource(token: string) { - const ehrUrl = `${req.body.fhirServer}/${token}`; - const access_token = req.body.fhirAuthorization?.access_token; - const options = { - method: 'GET', - headers: { - Authorization: `Bearer ${access_token}` - } - }; - // application errors out here if you can't reach out to the EHR and results in server stopping and subsequent requests failing - let response = { data: {} }; - try { - response = await axios(ehrUrl, options); - } catch (error: any) { - console.warn('Could not connect to EHR Server: ' + error); - response = error; - } - return response?.data; - } - - function createSmartLink( - requirementName: string, - appContext: string, - request: MedicationRequest - ) { - const newLink: Link = { - label: requirementName + ' Form', - url: new URL(config.smart.endpoint), - type: 'smart', - appContext: `${appContext}&order=${JSON.stringify(request)}&coverage=${ - request.insurance?.[0].reference - }` - }; - return newLink; - } - - async function handleCard(hydratedPrefetch: OrderSignPrefetch) { - console.log(hydratedPrefetch); - const context = req.body.context; - const contextRequest = context.draftOrders?.entry?.[0].resource; - const patient = hydratedPrefetch?.patient; - const prefetchRequest = hydratedPrefetch?.request; - const practitioner = hydratedPrefetch?.practitioner; - const npi = practitioner?.identifier; - - console.log(' MedicationRequest: ' + prefetchRequest?.id); - console.log(' Practitioner: ' + practitioner?.id + ' NPI: ' + npi); - console.log(' Patient: ' + patient?.id); - - // verify there is a contextRequest - if (!contextRequest) { - res.json(buildErrorCard('DraftOrders does not contain a request')); - return; - } - - // verify a MedicationRequest was sent - if (contextRequest && contextRequest.resourceType !== 'MedicationRequest') { - res.json(buildErrorCard('DraftOrders does not contain a MedicationRequest')); - return; - } - - // verify ids - if ( - patient?.id && - patient.id.replace('Patient/', '') !== context.patientId.replace('Patient/', '') - ) { - res.json(buildErrorCard('Context patientId does not match prefetch Patient ID')); - return; - } - if ( - practitioner?.id && - practitioner.id.replace('Practitioner/', '') !== context.userId.replace('Practitioner/', '') - ) { - res.json(buildErrorCard('Context userId does not match prefetch Practitioner ID')); - return; - } - if ( - prefetchRequest?.id && - contextRequest && - contextRequest.id && - prefetchRequest.id.replace('MedicationRequest/', '') !== - contextRequest.id.replace('MedicationRequest/', '') - ) { - res.json(buildErrorCard('Context draftOrder does not match prefetch MedicationRequest ID')); - return; - } - - const medicationCode = getDrugCodeFromMedicationRequest(contextRequest); - if (!medicationCode) { - return; - } - if (medicationCode && medicationCode?.code) { - // find the drug in the medicationCollection to get the smart links - const drug = await medicationCollection - .findOne({ - code: medicationCode.code, - codeSystem: medicationCode.system - }) - .exec(); - - // find a matching rems case for the patient and this drug to only return needed results - const patientName = patient?.name?.[0]; - const etasu = await remsCaseCollection.findOne({ - patientFirstName: patientName?.given?.[0], - patientLastName: patientName?.family, - patientDOB: patient?.birthDate, - drugCode: medicationCode?.code - }); - - const returnCard = validCodes.some(e => { - return e.code === medicationCode.code && e.system === medicationCode.system; - }); - if (returnCard) { - const cardArray: Card[] = []; - const codeRule = codeMap[medicationCode.code]; - for (const rule of codeRule) { - const card = new Card( - rule.summary || medicationCode.display || 'Rems', - CARD_DETAILS, - source, - 'info' - ); - rule.links.forEach(function (e) { - if (e.type == 'absolute') { - // no construction needed - card.addLink(e); - } - }); - - let smartLinkCount = 0; - - // process the smart links from the medicationCollection - // TODO: smart links should be built with discovered questionnaires, not hard coded ones - if (drug) { - for (const requirement of drug.requirements) { - if (requirement.stakeholderType == rule.stakeholderType) { - // only add the link if the form has not already been processed / received - if (etasu) { - let found = false; - for (const metRequirement of etasu.metRequirements) { - if (metRequirement.requirementName == requirement.name) { - found = true; - if (!metRequirement.completed) { - card.addLink( - createSmartLink(requirement.name, requirement.appContext, contextRequest) - ); - smartLinkCount++; - } - } - } - if (!found) { - card.addLink( - createSmartLink(requirement.name, requirement.appContext, contextRequest) - ); - smartLinkCount++; - } - } else { - // add all the required to dispense links if no etasu to check - if (requirement.requiredToDispense) { - card.addLink( - createSmartLink(requirement.name, requirement.appContext, contextRequest) - ); - smartLinkCount++; - } - } - } - } - } - - // only add the card if there are smart links to needed forms - if (smartLinkCount > 0) { - cardArray.push(card); - } - } - res.json({ - cards: cardArray - }); - } else { - res.json(buildErrorCard('Unsupported code')); - } - } else { - res.json(buildErrorCard('MedicationRequest does not contain a code')); - } - } - console.log('REMS order-sign hook'); - try { - const fhirUrl = req.body.fhirServer; - const fhirAuth = req.body.fhirAuthorization; - if (fhirUrl && fhirAuth && fhirAuth.access_token) { - hydrate(getFhirResource, hookPrefetch, req.body).then( - (hydratedPrefetch: OrderSignPrefetch) => { - handleCard(hydratedPrefetch); - } - ); - } else { - if (req.body.prefetch) { - handleCard(req.body.prefetch); - } else { - handleCard({}); - } - } - } catch (error) { - console.log(error); - res.json(buildErrorCard('Unknown Error')); - } + const contextRequest = req.body.context.draftOrders?.entry?.[0]?.resource; + handleHook(req, res, hookPrefetch, contextRequest, handleCardOrder); }; export default { definition, handler }; diff --git a/src/hooks/rems.patientview.ts b/src/hooks/rems.patientview.ts index b84e6dbb..da123210 100644 --- a/src/hooks/rems.patientview.ts +++ b/src/hooks/rems.patientview.ts @@ -2,15 +2,22 @@ import Card from '../cards/Card'; import { PatientViewHook, SupportedHooks, - PatientViewPrefetch + PatientViewPrefetch, + HookPrefetch } from '../rems-cds-hooks/resources/HookTypes'; import { medicationCollection, remsCaseCollection } from '../fhir/models'; import { ServicePrefetch, CdsService } from '../rems-cds-hooks/resources/CdsService'; -import { Bundle, MedicationRequest } from 'fhir/r4'; +import { Bundle, FhirResource, MedicationRequest } from 'fhir/r4'; import { Link } from '../cards/Card'; import config from '../config'; import { hydrate } from '../rems-cds-hooks/prefetch/PrefetchHydrator'; -import { codeMap, CARD_DETAILS, getDrugCodeFromMedicationRequest } from './hookResources'; +import { + codeMap, + CARD_DETAILS, + getDrugCodeFromMedicationRequest, + createSmartLink, + handleHook +} from './hookResources'; import axios from 'axios'; interface TypedRequestBody extends Express.Request { @@ -43,37 +50,6 @@ function buildErrorCard(reason: string) { } const handler = (req: TypedRequestBody, res: any) => { - function getFhirResource(token: string) { - const ehrUrl = `${req.body.fhirServer}/${token}`; - const access_token = req.body.fhirAuthorization?.access_token; - const options = { - method: 'GET', - headers: { - Authorization: `Bearer ${access_token}` - } - }; - const response = axios(ehrUrl, options); - return response.then(e => { - return e.data; - }); - } - - function createSmartLink( - requirementName: string, - appContext: string, - request: MedicationRequest | undefined - ) { - const newLink: Link = { - label: requirementName + ' Form', - url: new URL(config.smart.endpoint), - type: 'smart', - appContext: `${appContext}&order=${JSON.stringify(request)}&coverage=${ - request?.insurance?.[0].reference - }` - }; - return newLink; - } - // process the MedicationRequests to add the Medication into contained resources function processMedicationRequests(medicationRequestsBundle: Bundle) { medicationRequestsBundle?.entry?.forEach(entry => { @@ -117,44 +93,28 @@ const handler = (req: TypedRequestBody, res: any) => { }); } - async function handleCard(hydratedPrefetch: PatientViewPrefetch) { - console.log(hydratedPrefetch); - const context = req.body.context; - const patient = hydratedPrefetch?.patient; - const practitioner = hydratedPrefetch?.practitioner; - const medicationRequestsBundle = hydratedPrefetch?.medicationRequests; - const npi = practitioner?.identifier; - - console.log(' Practitioner: ' + practitioner?.id + ' NPI: ' + npi); - console.log(' Patient: ' + patient?.id); - - // verify ids - if ( - patient?.id && - patient.id.replace('Patient/', '') !== context.patientId.replace('Patient/', '') - ) { - res.json(buildErrorCard('Context patientId does not match prefetch Patient ID')); - return; - } - if ( - practitioner?.id && - practitioner.id.replace('Practitioner/', '') !== context.userId.replace('Practitioner/', '') - ) { - res.json(buildErrorCard('Context userId does not match prefetch Practitioner ID')); - return; - } - + async function handleCard( + res: any, + hookPrefetch: HookPrefetch | undefined, + contextRequest: FhirResource | undefined, + patient: FhirResource | undefined + ) { //TODO: should we add the other pdf information links to the card, or just have the smart links? + const medResource = hookPrefetch?.medicationRequests; + const medicationRequestsBundle = + medResource?.resourceType === 'Bundle' ? medResource : undefined; + // create empty card array const cardArray: Card[] = []; // find all matching rems cases for the patient - const patientName = patient?.name?.[0]; + const patientName = patient?.resourceType === 'Patient' ? patient?.name?.[0] : undefined; + const patientBirth = patient?.resourceType === 'Patient' ? patient?.birthDate : undefined; const remsCaseList = await remsCaseCollection.find({ patientFirstName: patientName?.given?.[0], patientLastName: patientName?.family, - patientDOB: patient?.birthDate + patientDOB: patientBirth }); // loop through all the rems cases in the list @@ -238,27 +198,8 @@ const handler = (req: TypedRequestBody, res: any) => { }); } - console.log('REMS patient-view hook'); - try { - const fhirUrl = req.body.fhirServer; - const fhirAuth = req.body.fhirAuthorization; - if (fhirUrl && fhirAuth && fhirAuth.access_token) { - hydrate(getFhirResource, hookPrefetch, req.body).then( - (hydratedPrefetch: PatientViewPrefetch) => { - handleCard(hydratedPrefetch); - } - ); - } else { - if (req.body.prefetch) { - handleCard(req.body.prefetch); - } else { - handleCard({}); - } - } - } catch (error) { - console.log(error); - res.json(buildErrorCard('Unknown Error')); - } + // contextRequest is undefined + handleHook(req, res, hookPrefetch, undefined, handleCard); }; export default { definition, handler }; diff --git a/src/rems-cds-hooks b/src/rems-cds-hooks index da1a46d2..94a78e8c 160000 --- a/src/rems-cds-hooks +++ b/src/rems-cds-hooks @@ -1 +1 @@ -Subproject commit da1a46d22b1d5ded3afe1be73dde06d20d080b53 +Subproject commit 94a78e8cd27734938ec41858f8d0ca4028da5f21