From b5986d2171204acd54dfebfcf55b95fc3cf29b82 Mon Sep 17 00:00:00 2001 From: Patrick LaRocque Date: Tue, 5 Mar 2024 23:33:29 -0500 Subject: [PATCH 1/4] Add support for Addyi, a medication that does not have ETASU and only has a medication guide. --- src/fhir/utilities.ts | 7 +++++ src/hooks/hookResources.ts | 56 ++++++++++++++++++++++++++++------- src/hooks/rems.patientview.ts | 1 + src/lib/etasu.ts | 43 ++++++++++++++++++++++++--- 4 files changed, 92 insertions(+), 15 deletions(-) diff --git a/src/fhir/utilities.ts b/src/fhir/utilities.ts index aceb3703..b191a1f8 100644 --- a/src/fhir/utilities.ts +++ b/src/fhir/utilities.ts @@ -265,6 +265,13 @@ export class FhirUtilities { requiredToDispense: true } ] + }, + { + name: 'Addyi', + codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', + code: '1666386', + requirements: [ + ] } ]; diff --git a/src/hooks/hookResources.ts b/src/hooks/hookResources.ts index 2bea88c7..53601b91 100644 --- a/src/hooks/hookResources.ts +++ b/src/hooks/hookResources.ts @@ -39,6 +39,7 @@ export interface CardRule { links: Link[]; summary?: string; stakeholderType?: string; + cardDetails?: string; } export const CARD_DETAILS = 'Documentation Required, please complete form via Smart App link.'; // TODO: this codemap should be replaced with a system similar to original CRD's questionnaire package operation @@ -70,7 +71,8 @@ export const codeMap: { [key: string]: CardRule[] } = { } ], stakeholderType: 'patient', - summary: 'Turalio REMS Patient Requirements' + summary: 'Turalio REMS Patient Requirements', + cardDetails: CARD_DETAILS }, { links: [ @@ -97,7 +99,8 @@ export const codeMap: { [key: string]: CardRule[] } = { } ], stakeholderType: 'prescriber', - summary: 'Turalio REMS Prescriber Requirements' + summary: 'Turalio REMS Prescriber Requirements', + cardDetails: CARD_DETAILS } ], '6064': [ @@ -133,7 +136,8 @@ export const codeMap: { [key: string]: CardRule[] } = { } ], stakeholderType: 'patient', - summary: 'iPledge/Isotretinoin REMS Patient Requirements' + summary: 'iPledge/Isotretinoin REMS Patient Requirements', + cardDetails: CARD_DETAILS }, { links: [ @@ -153,7 +157,8 @@ export const codeMap: { [key: string]: CardRule[] } = { } ], stakeholderType: 'prescriber', - summary: 'iPledge/Isotretinoin REMS Provider Requirements' + summary: 'iPledge/Isotretinoin REMS Provider Requirements', + cardDetails: CARD_DETAILS } ], '1237051': [ @@ -182,7 +187,8 @@ export const codeMap: { [key: string]: CardRule[] } = { } ], stakeholderType: 'patient', - summary: 'TIRF REMS Patient Requirements' + summary: 'TIRF REMS Patient Requirements', + cardDetails: CARD_DETAILS }, { links: [ @@ -202,7 +208,24 @@ export const codeMap: { [key: string]: CardRule[] } = { } ], stakeholderType: 'prescriber', - summary: 'TIRF REMS Prescriber Requirements' + summary: 'TIRF REMS Prescriber Requirements', + cardDetails: CARD_DETAILS + } + ], + '1666386': [ + { + links: [ + { + label: 'Medication Guide', + type: 'absolute', + url: new URL( + 'https://www.accessdata.fda.gov/drugsatfda_docs/rems/Addyi_2019_10_09_Medication_Guide.pdf' + ) + } + ], + stakeholderType: '', + summary: 'Addyi REMS Patient Information', + cardDetails: 'Please review safety documentation' } ] }; @@ -219,6 +242,10 @@ export const validCodes: Coding[] = [ { code: '6064', // iPledge system: 'http://www.nlm.nih.gov/research/umls/rxnorm' + }, + { + code: '1666386', // Addyi + system: 'http://www.nlm.nih.gov/research/umls/rxnorm' } ]; const source = { @@ -335,6 +362,8 @@ export async function handleCardOrder( }) .exec(); + // count the total requirement for each type + // 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; @@ -354,7 +383,7 @@ export async function handleCardOrder( for (const rule of codeRule) { const card = new Card( rule.summary || medicationCode.display || 'Rems', - CARD_DETAILS, + rule.cardDetails || CARD_DETAILS, source, 'info' ); @@ -365,6 +394,7 @@ export async function handleCardOrder( } }); + let smartLinkCountAdded = 0; let smartLinkCount = 0; // process the smart links from the medicationCollection @@ -372,6 +402,9 @@ export async function handleCardOrder( if (drug) { for (const requirement of drug.requirements) { if (requirement.stakeholderType == rule.stakeholderType) { + + smartLinkCount++; + // only add the link if the form has not already been processed / received if (etasu) { let found = false; @@ -385,7 +418,7 @@ export async function handleCardOrder( if (patient && patient.resourceType === 'Patient') { createQuestionnaireSuggestion(card, requirement, patient); } - smartLinkCount++; + smartLinkCountAdded++; } } } @@ -396,7 +429,7 @@ export async function handleCardOrder( if (patient && patient.resourceType === 'Patient') { createQuestionnaireSuggestion(card, requirement, patient); } - smartLinkCount++; + smartLinkCountAdded++; } } else { // add all the required to dispense links if no etasu to check @@ -407,7 +440,7 @@ export async function handleCardOrder( if (patient && patient.resourceType === 'Patient') { createQuestionnaireSuggestion(card, requirement, patient); } - smartLinkCount++; + smartLinkCountAdded++; } } } @@ -415,7 +448,8 @@ export async function handleCardOrder( } // only add the card if there are smart links to needed forms - if (smartLinkCount > 0) { + // allow information only cards to be returned as well + if ((smartLinkCountAdded > 0) || (smartLinkCount == 0)) { cardArray.push(card); } } diff --git a/src/hooks/rems.patientview.ts b/src/hooks/rems.patientview.ts index da123210..3cb71f8f 100644 --- a/src/hooks/rems.patientview.ts +++ b/src/hooks/rems.patientview.ts @@ -50,6 +50,7 @@ function buildErrorCard(reason: string) { } const handler = (req: TypedRequestBody, res: any) => { + console.log('REMS patient-view hook'); // process the MedicationRequests to add the Medication into contained resources function processMedicationRequests(medicationRequestsBundle: Bundle) { medicationRequestsBundle?.entry?.forEach(entry => { diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index cd6d043c..c3bf1003 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -24,6 +24,35 @@ router.get('/met/:caseId', async (req: Request, res: Response) => { res.send(await remsCaseCollection.findOne({ case_number: req.params.caseId })); }); +const getCaseInfo = async (remsCaseSearchDict: any, medicationSearchDict: any/*, + patientFirstName: string, patientLastName: string, patientDOB: string*/) => { + let ret = await remsCaseCollection.findOne(remsCaseSearchDict); + // if there are no requirements, then return 'Approved' + if (!ret) { + // look for the medication by name in the medications list + const drug = await medicationCollection.findOne(medicationSearchDict).exec(); + + // iterate through each requirement of the drug + if (drug?.requirements.length == 0) { + // create simple rems request to return + const remsRequest: any = { + //case_number: case_number, + status: 'Approved', + drugName: drug?.name, + drugCode: drug?.code, + patientFirstName: remsCaseSearchDict.patientFirstName, + patientLastName: remsCaseSearchDict.patientLastName, + patientDOB: remsCaseSearchDict.patientDOB, + metRequirements: [] + }; + ret = remsRequest; + } + } + + // not a supported medication or requirements / record not created yet will return null + return ret; +}; + router.get( '/met/patient/:patientFirstName/:patientLastName/:patientDOB/drugCode/:drugCode', async (req: Request, res: Response) => { @@ -37,14 +66,17 @@ router.get( ' - ' + req.params.drugCode ); - const searchDict = { + const remsCaseSearchDict = { patientFirstName: req.params.patientFirstName, patientLastName: req.params.patientLastName, patientDOB: req.params.patientDOB, drugCode: req.params.drugCode }; + const medicationSearchDict = { + code: req.params.drugCode + } - res.send(await remsCaseCollection.findOne(searchDict)); + res.send(await getCaseInfo(remsCaseSearchDict, medicationSearchDict)); } ); @@ -61,14 +93,17 @@ router.get( ' - ' + req.params.drugName ); - const searchDict = { + const remsCaseSearchDict = { patientFirstName: req.params.patientFirstName, patientLastName: req.params.patientLastName, patientDOB: req.params.patientDOB, drugName: req.params.drugName }; + const medicationSearchDict = { + name: req.params.drugName + } - res.send(await remsCaseCollection.findOne(searchDict)); + res.send(await getCaseInfo(remsCaseSearchDict, medicationSearchDict)); } ); From 02ccc29227026bb832fff928d8a3c7c29eab59e4 Mon Sep 17 00:00:00 2001 From: Patrick LaRocque Date: Wed, 6 Mar 2024 13:54:31 -0500 Subject: [PATCH 2/4] run prettier --- src/fhir/utilities.ts | 3 +-- src/hooks/hookResources.ts | 5 ++--- src/lib/etasu.ts | 7 +++---- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/fhir/utilities.ts b/src/fhir/utilities.ts index b191a1f8..ac5cb92a 100644 --- a/src/fhir/utilities.ts +++ b/src/fhir/utilities.ts @@ -270,8 +270,7 @@ export class FhirUtilities { name: 'Addyi', codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '1666386', - requirements: [ - ] + requirements: [] } ]; diff --git a/src/hooks/hookResources.ts b/src/hooks/hookResources.ts index 53601b91..86cb167f 100644 --- a/src/hooks/hookResources.ts +++ b/src/hooks/hookResources.ts @@ -402,8 +402,7 @@ export async function handleCardOrder( if (drug) { for (const requirement of drug.requirements) { if (requirement.stakeholderType == rule.stakeholderType) { - - smartLinkCount++; + smartLinkCount++; // only add the link if the form has not already been processed / received if (etasu) { @@ -449,7 +448,7 @@ export async function handleCardOrder( // only add the card if there are smart links to needed forms // allow information only cards to be returned as well - if ((smartLinkCountAdded > 0) || (smartLinkCount == 0)) { + if (smartLinkCountAdded > 0 || smartLinkCount == 0) { cardArray.push(card); } } diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index c3bf1003..81726b79 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -24,8 +24,7 @@ router.get('/met/:caseId', async (req: Request, res: Response) => { res.send(await remsCaseCollection.findOne({ case_number: req.params.caseId })); }); -const getCaseInfo = async (remsCaseSearchDict: any, medicationSearchDict: any/*, - patientFirstName: string, patientLastName: string, patientDOB: string*/) => { +const getCaseInfo = async (remsCaseSearchDict: any, medicationSearchDict: any) => { let ret = await remsCaseCollection.findOne(remsCaseSearchDict); // if there are no requirements, then return 'Approved' if (!ret) { @@ -74,7 +73,7 @@ router.get( }; const medicationSearchDict = { code: req.params.drugCode - } + }; res.send(await getCaseInfo(remsCaseSearchDict, medicationSearchDict)); } @@ -101,7 +100,7 @@ router.get( }; const medicationSearchDict = { name: req.params.drugName - } + }; res.send(await getCaseInfo(remsCaseSearchDict, medicationSearchDict)); } From 09e60f1bca1b5cb1c2125df0de131e093b494cf9 Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Thu, 7 Mar 2024 18:15:59 -0500 Subject: [PATCH 3/4] Tweak Mongoose schema types so there are defined types for Intellisense, remove unused imports, and change double equals to triple equals --- src/fhir/models.ts | 114 ++++++++++++---------- src/fhir/utilities.ts | 43 ++++++--- src/hooks/hookResources.ts | 37 ++----- src/hooks/rems.patientview.ts | 8 +- src/lib/etasu.ts | 177 +++++++++++++++++++--------------- 5 files changed, 202 insertions(+), 177 deletions(-) diff --git a/src/fhir/models.ts b/src/fhir/models.ts index 934e09f9..d49f33ce 100644 --- a/src/fhir/models.ts +++ b/src/fhir/models.ts @@ -1,23 +1,40 @@ -import { Document, Schema, model } from 'mongoose'; +import { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; +import { Schema, model } from 'mongoose'; -export interface Medication extends Document { +interface MongoItemWithInternalId { + _id: string; +} + +export interface Requirement { + name: string; + description: string; + questionnaire: Questionnaire | null; + stakeholderType: 'patient' | 'prescriber' | 'pharmacist' | string; // From fhir4.Parameters.parameter.name + createNewCase: boolean; + resourceId: string; + requiredToDispense: boolean; + appContext: string | null; +} + +export interface Medication { name: string; codeSystem: string; code: string; - requirements: any; + requirements: Requirement[]; } -interface MetRequirements extends Document { +export interface MetRequirements extends MongoItemWithInternalId { completed: boolean; - completedQuestionnaire: any; + completedQuestionnaire: QuestionnaireResponse | null; requirementName: string; requirementDescription: string; drugName: string; stakeholderId: string; - case_numbers: any; + case_numbers: string[]; + metRequirementId: string; } -interface RemsCase extends Document { +export interface RemsCase { case_number: string; status: string; drugName: string; @@ -25,29 +42,25 @@ interface RemsCase extends Document { patientFirstName: string; patientLastName: string; patientDOB: string; - metRequirements: any; + metRequirements: Partial[]; } const medicationCollectionSchema = new Schema({ - name: { type: 'String' }, - codeSystem: { type: 'string' }, - code: { type: 'string' }, - requirements: { - type: 'array', - items: { - type: 'object', - properties: { - name: { type: 'string' }, - description: { type: 'string' }, - questionnaire: { type: 'object' }, - stakeholderType: { type: 'string' }, - createNewCase: { type: 'boolean' }, - resourceId: { type: 'string' }, - requiredToDispense: { type: 'boolean' }, - appContext: { type: 'string' } - } + name: { type: String }, + codeSystem: { type: String }, + code: { type: String }, + requirements: [ + { + name: { type: String }, + description: { type: String }, + questionnaire: { type: Schema.Types.Mixed, default: null }, + stakeholderType: { type: String }, + createNewCase: { type: Boolean }, + resourceId: { type: String }, + requiredToDispense: { type: Boolean }, + appContext: { type: String, default: null } } - } + ] }); medicationCollectionSchema.index({ name: 1 }, { unique: true }); @@ -58,14 +71,15 @@ export const medicationCollection = model( ); const metRequirementsSchema = new Schema({ - completed: { type: 'boolean' }, - completedQuestionnaire: { type: 'object' }, - requirementName: { type: 'string' }, - requirementDescription: { type: 'string' }, - drugName: { type: 'string' }, - stakeholderId: { type: 'string' }, - case_numbers: { type: 'array', items: { type: 'string' } } + completed: { type: Boolean }, + completedQuestionnaire: { type: Schema.Types.Mixed, default: null }, + requirementName: { type: String }, + requirementDescription: { type: String }, + drugName: { type: String }, + stakeholderId: { type: String }, + case_numbers: [{ type: String }] }); + metRequirementsSchema.index( { drugName: 1, requirementName: 1, stakeholderId: 1 }, { unique: true } @@ -77,26 +91,22 @@ export const metRequirementsCollection = model( ); const remsCaseCollectionSchema = new Schema({ - case_number: { type: 'string' }, - status: { type: 'string' }, - drugName: { type: 'string' }, - patientFirstName: { type: 'string' }, - patientLastName: { type: 'string' }, - patientDOB: { type: 'string' }, - drugCode: { type: 'string' }, - metRequirements: { - type: 'array', - items: { - type: 'object', - properties: { - metRequirementId: { type: 'number' }, - completed: { type: 'boolean' }, - stakeholderId: { type: 'string' }, - requirementName: { type: 'string' }, - requirementDescription: { type: 'string' } - } + case_number: { type: String }, + status: { type: String }, + drugName: { type: String }, + patientFirstName: { type: String }, + patientLastName: { type: String }, + patientDOB: { type: String }, + drugCode: { type: String }, + metRequirements: [ + { + metRequirementId: { type: String }, + completed: { type: Boolean }, + stakeholderId: { type: String }, + requirementName: { type: String }, + requirementDescription: { type: String } } - } + ] }); export const remsCaseCollection = model('RemsCaseCollection', remsCaseCollectionSchema); diff --git a/src/fhir/utilities.ts b/src/fhir/utilities.ts index ac5cb92a..f572c505 100644 --- a/src/fhir/utilities.ts +++ b/src/fhir/utilities.ts @@ -136,7 +136,8 @@ export class FhirUtilities { resourceId: 'TuralioRemsPatientEnrollment', requiredToDispense: true, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TuralioRemsPatientEnrollment' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TuralioRemsPatientEnrollment', + questionnaire: null }, { name: 'Prescriber Enrollment', @@ -146,7 +147,8 @@ export class FhirUtilities { resourceId: 'TuralioPrescriberEnrollmentForm', requiredToDispense: true, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TuralioPrescriberEnrollmentForm' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TuralioPrescriberEnrollmentForm', + questionnaire: null }, { name: 'Prescriber Knowledge Assessment', @@ -156,7 +158,8 @@ export class FhirUtilities { resourceId: 'TuralioPrescriberKnowledgeAssessment', requiredToDispense: true, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TuralioPrescriberKnowledgeAssessment' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TuralioPrescriberKnowledgeAssessment', + questionnaire: null }, { name: 'Pharmacist Enrollment', @@ -164,7 +167,9 @@ export class FhirUtilities { stakeholderType: 'pharmacist', createNewCase: false, resourceId: 'TuralioPharmacistEnrollment', - requiredToDispense: true + requiredToDispense: true, + appContext: null, + questionnaire: null }, { name: 'Patient Status Update', @@ -174,7 +179,8 @@ export class FhirUtilities { resourceId: 'TuralioRemsPatientStatus', requiredToDispense: false, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TuralioRemsPatientStatus' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TuralioRemsPatientStatus', + questionnaire: null } ] }, @@ -191,7 +197,8 @@ export class FhirUtilities { resourceId: 'TIRFRemsPatientEnrollment', requiredToDispense: true, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TIRFRemsPatientEnrollment' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TIRFRemsPatientEnrollment', + questionnaire: null }, { name: 'Prescriber Enrollment', @@ -201,7 +208,8 @@ export class FhirUtilities { resourceId: 'TIRFPrescriberEnrollmentForm', requiredToDispense: true, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TIRFPrescriberEnrollmentForm' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TIRFPrescriberEnrollmentForm', + questionnaire: null }, { name: 'Prescriber Knowledge Assessment', @@ -211,7 +219,8 @@ export class FhirUtilities { resourceId: 'TIRFPrescriberKnowledgeAssessment', requiredToDispense: true, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TIRFPrescriberKnowledgeAssessment' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/TIRFPrescriberKnowledgeAssessment', + questionnaire: null }, { name: 'Pharmacist Enrollment', @@ -219,7 +228,9 @@ export class FhirUtilities { stakeholderType: 'pharmacist', createNewCase: false, resourceId: 'TIRFPharmacistEnrollmentForm', - requiredToDispense: true + requiredToDispense: true, + appContext: null, + questionnaire: null }, { name: 'Pharmacist Knowledge Assessment', @@ -227,7 +238,9 @@ export class FhirUtilities { stakeholderType: 'pharmacist', createNewCase: false, resourceId: 'TIRFPharmacistKnowledgeAssessment', - requiredToDispense: true + requiredToDispense: true, + appContext: null, + questionnaire: null } ] }, @@ -244,7 +257,8 @@ export class FhirUtilities { resourceId: 'IPledgeRemsPatientEnrollment', requiredToDispense: true, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/IPledgeRemsPatientEnrollment' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/IPledgeRemsPatientEnrollment', + questionnaire: null }, { name: 'Prescriber Enrollment', @@ -254,7 +268,8 @@ export class FhirUtilities { resourceId: 'IPledgeRemsPrescriberEnrollmentForm', requiredToDispense: true, appContext: - 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/IPledgeRemsPrescriberEnrollmentForm' + 'questionnaire=http://localhost:8090/4_0_0/Questionnaire/IPledgeRemsPrescriberEnrollmentForm', + questionnaire: null }, { name: 'Pharmacist Enrollment', @@ -262,7 +277,9 @@ export class FhirUtilities { stakeholderType: 'pharmacist', createNewCase: false, resourceId: 'IPledgeRemsPharmacistEnrollmentForm', - requiredToDispense: true + requiredToDispense: true, + appContext: null, + questionnaire: null } ] }, diff --git a/src/hooks/hookResources.ts b/src/hooks/hookResources.ts index 86cb167f..86f35867 100644 --- a/src/hooks/hookResources.ts +++ b/src/hooks/hookResources.ts @@ -1,20 +1,8 @@ -import { - MedicationRequest, - Coding, - FhirResource, - Identifier, - Task, - Questionnaire, - Patient -} from 'fhir/r4'; +import { MedicationRequest, Coding, FhirResource, Task, Patient } from 'fhir/r4'; import Card, { Link, Suggestion, Action } from '../cards/Card'; -import { - HookPrefetch, - OrderSignPrefetch, - TypedRequestBody -} from '../rems-cds-hooks/resources/HookTypes'; +import { HookPrefetch, TypedRequestBody } from '../rems-cds-hooks/resources/HookTypes'; import config from '../config'; -import { medicationCollection, remsCaseCollection } from '../fhir/models'; +import { Requirement, medicationCollection, remsCaseCollection } from '../fhir/models'; import axios from 'axios'; import { ServicePrefetch } from '../rems-cds-hooks/resources/CdsService'; @@ -26,15 +14,6 @@ type HandleCallback = ( patient: FhirResource | undefined ) => Promise; -interface Requirement { - name: string; - description: string; - stakeholderType: string; - createNewCase: boolean; - resourceId: string; - requiredToDispense: boolean; - appContext?: string; -} export interface CardRule { links: Link[]; summary?: string; @@ -294,7 +273,7 @@ export function getFhirResource(token: string, req: TypedRequestBody) { } export function createSmartLink( requirementName: string, - appContext: string, + appContext: string | null, request: MedicationRequest | undefined ) { const newLink: Link = { @@ -388,7 +367,7 @@ export async function handleCardOrder( 'info' ); rule.links.forEach(function (e) { - if (e.type == 'absolute') { + if (e.type === 'absolute') { // no construction needed card.addLink(e); } @@ -401,14 +380,14 @@ export async function handleCardOrder( // 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) { + if (requirement.stakeholderType === rule.stakeholderType) { smartLinkCount++; // 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) { + if (metRequirement.requirementName === requirement.name) { found = true; if (!metRequirement.completed) { card.addLink( @@ -448,7 +427,7 @@ export async function handleCardOrder( // only add the card if there are smart links to needed forms // allow information only cards to be returned as well - if (smartLinkCountAdded > 0 || smartLinkCount == 0) { + if (smartLinkCountAdded > 0 || smartLinkCount === 0) { cardArray.push(card); } } diff --git a/src/hooks/rems.patientview.ts b/src/hooks/rems.patientview.ts index 3cb71f8f..da33871e 100644 --- a/src/hooks/rems.patientview.ts +++ b/src/hooks/rems.patientview.ts @@ -2,15 +2,12 @@ import Card from '../cards/Card'; import { PatientViewHook, SupportedHooks, - 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, 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, @@ -18,7 +15,6 @@ import { createSmartLink, handleHook } from './hookResources'; -import axios from 'axios'; interface TypedRequestBody extends Express.Request { body: PatientViewHook; @@ -163,7 +159,7 @@ const handler = (req: TypedRequestBody, res: any) => { } // loop through all of the ETASU requirements for this drug - const requirements = drug?.requirements; + const requirements = drug?.requirements || []; for (const requirement of requirements) { // find all of the matching patient forms if (requirement?.stakeholderType === 'patient') { diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index 81726b79..c5bb0911 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -4,10 +4,22 @@ import { medicationCollection, metRequirementsCollection, remsCaseCollection, - Medication + Medication, + RemsCase, + Requirement, + MetRequirements } from '../fhir/models'; import { getDrugCodeFromMedicationRequest } from '../hooks/hookResources'; import { uid } from 'uid'; +import { + Bundle, + Coding, + MedicationRequest, + MessageHeader, + Parameters, + Patient, + QuestionnaireResponse +} from 'fhir/r4'; const router = Router(); // const medicationCollection = db.collection('medication-requirements'); @@ -24,32 +36,35 @@ router.get('/met/:caseId', async (req: Request, res: Response) => { res.send(await remsCaseCollection.findOne({ case_number: req.params.caseId })); }); -const getCaseInfo = async (remsCaseSearchDict: any, medicationSearchDict: any) => { - let ret = await remsCaseCollection.findOne(remsCaseSearchDict); +const getCaseInfo = async ( + remsCaseSearchDict: Partial, + medicationSearchDict: Partial +): Promise | null> => { + const foundRequirements = await remsCaseCollection.findOne(remsCaseSearchDict); + // if there are no requirements, then return 'Approved' - if (!ret) { + if (!foundRequirements) { // look for the medication by name in the medications list const drug = await medicationCollection.findOne(medicationSearchDict).exec(); // iterate through each requirement of the drug - if (drug?.requirements.length == 0) { + if (drug?.requirements.length === 0) { // create simple rems request to return - const remsRequest: any = { - //case_number: case_number, + const remsRequest: Omit = { status: 'Approved', drugName: drug?.name, drugCode: drug?.code, - patientFirstName: remsCaseSearchDict.patientFirstName, - patientLastName: remsCaseSearchDict.patientLastName, - patientDOB: remsCaseSearchDict.patientDOB, + patientFirstName: remsCaseSearchDict.patientFirstName || '', + patientLastName: remsCaseSearchDict.patientLastName || '', + patientDOB: remsCaseSearchDict.patientDOB || '', metRequirements: [] }; - ret = remsRequest; + return remsRequest; } } // not a supported medication or requirements / record not created yet will return null - return ret; + return foundRequirements; }; router.get( @@ -116,7 +131,7 @@ router.post('/reset', async (req: Request, res: Response) => { res.send('reset etasu database collections'); }); -const pushMetRequirements = (matchedMetReq: any, remsRequest: any) => { +const pushMetRequirements = (matchedMetReq: MetRequirements, remsRequest: RemsCase) => { remsRequest.metRequirements.push({ stakeholderId: matchedMetReq?.stakeholderId, completed: matchedMetReq?.completed, @@ -126,11 +141,14 @@ const pushMetRequirements = (matchedMetReq: any, remsRequest: any) => { }); }; -const createMetRequirements = async (metReq: any) => { +const createMetRequirements = async (metReq: Partial) => { return await metRequirementsCollection.create(metReq); }; -const createAndPushMetRequirements = async (metReq: any, remsRequest: any) => { +const createAndPushMetRequirements = async ( + metReq: Partial, + remsRequest: RemsCase +) => { try { const matchedMetReq = await createMetRequirements(metReq); pushMetRequirements(matchedMetReq, remsRequest); @@ -143,25 +161,25 @@ const createAndPushMetRequirements = async (metReq: any, remsRequest: any) => { }; const createMetRequirementAndNewCase = async ( - patient: any, + patient: Patient, drug: Medication, - requirement: any, - questionnaireResponse: any, + requirement: Requirement, + questionnaireResponse: QuestionnaireResponse, res: Response, - reqStakeholderReference: any, + reqStakeholderReference: string, practitionerReference: string, pharmacistReference: string, patientReference: string ) => { - const patientFirstName = patient.name[0].given[0]; - const patientLastName = patient.name[0].family; - const patientDOB = patient.birthDate; + const patientFirstName = patient.name?.[0].given?.[0] || ''; + const patientLastName = patient.name?.[0].family || ''; + const patientDOB = patient.birthDate || ''; let message = ''; const case_number = uid(); // create new rems request and add the created metReq to it let remsRequestCompletedStatus = 'Approved'; - const remsRequest: any = { + const remsRequest: RemsCase = { case_number: case_number, status: remsRequestCompletedStatus, drugName: drug?.name, @@ -224,7 +242,6 @@ const createMetRequirementAndNewCase = async ( // create the metReq that was submitted const newMetReq = { completed: false, - completedQuestionnaire: null, requirementName: requirement2.name, requirementDescription: requirement2.description, drugName: drug?.name, @@ -252,12 +269,12 @@ const createMetRequirementAndNewCase = async ( const createMetRequirementAndUpdateCase = async ( drug: Medication, - requirement: any, - questionnaireResponse: any, + requirement: Requirement, + questionnaireResponse: QuestionnaireResponse, res: Response, - reqStakeholderReference: any + reqStakeholderReference: string ) => { - let returnedMetReqDoc: any; + let returnedMetReqDoc; const matchedMetReq = await metRequirementsCollection .findOne({ @@ -282,7 +299,7 @@ const createMetRequirementAndUpdateCase = async ( }) .exec(); - for (const case_number of returnedMetReqDoc.case_numbers) { + for (const case_number of returnedMetReqDoc?.case_numbers || []) { // get the rems case to update, search by the case_number const remsRequestToUpdate = await remsCaseCollection .findOne({ @@ -291,12 +308,12 @@ const createMetRequirementAndUpdateCase = async ( .exec(); let foundUncompleted = false; - const metReqArray = remsRequestToUpdate?.metRequirements; + const metReqArray = remsRequestToUpdate?.metRequirements || []; // Check to see if there are any uncompleted requirements, if all have been completed then set status to approved - for (let i = 0; i < remsRequestToUpdate?.metRequirements.length; i++) { + for (let i = 0; i < metReqArray.length; i++) { const req4 = remsRequestToUpdate?.metRequirements[i]; // _id comparison would not work for some reason - if (req4.requirementName === matchedMetReq.requirementName) { + if (req4?.requirementName === matchedMetReq.requirementName) { metReqArray[i].completed = true; req4.completed = true; await remsCaseCollection.updateOne( @@ -304,7 +321,7 @@ const createMetRequirementAndUpdateCase = async ( { $set: { metRequirements: metReqArray } } ); } - if (!req4.completed) { + if (!req4?.completed) { foundUncompleted = true; } } @@ -316,13 +333,13 @@ const createMetRequirementAndUpdateCase = async ( } } } else { - // submitting the requirment but there is no case, create new met requirment + // submitting the requirement but there is no case, create new met requirement // create the metReq that was submitted const newMetReq = { completed: true, completedQuestionnaire: questionnaireResponse, requirementName: requirement.name, - requirementDescription: requirement.requirementDescription, + requirementDescription: requirement.description, drugName: drug?.name, stakeholderId: reqStakeholderReference, case_numbers: [] @@ -337,22 +354,21 @@ const createMetRequirementAndUpdateCase = async ( }; const createMetRequirementAndUpdateCaseNotRequiredToDispense = async ( - patient: any, + patient: Patient, drug: Medication, - requirement: any, - questionnaireResponse: any, + requirement: Requirement, + questionnaireResponse: QuestionnaireResponse, res: Response, - reqStakeholderReference: any + reqStakeholderReference: string ) => { // Find the specific case associated with an individual patient for the patient status form // Is it possible for there to be multiple cases for this patient and the same drug? - let returnedRemsRequestDoc: any; let returnRemsRequest = false; let message = ''; - const patientFirstName = patient.name[0].given[0]; - const patientLastName = patient.name[0].family; - const patientDOB = patient.birthDate; + const patientFirstName = patient.name?.[0].given?.[0] || ''; + const patientLastName = patient.name?.[0].family || ''; + const patientDOB = patient.birthDate || ''; const remsRequestToUpdate = await remsCaseCollection .findOne({ @@ -362,6 +378,7 @@ const createMetRequirementAndUpdateCaseNotRequiredToDispense = async ( drugCode: drug?.code }) .exec(); + // If you found a case for the patient status form to update if (remsRequestToUpdate) { if (remsRequestToUpdate.status === 'Approved') { @@ -383,7 +400,6 @@ const createMetRequirementAndUpdateCaseNotRequiredToDispense = async ( try { await remsRequestToUpdate.save(); returnRemsRequest = true; - returnedRemsRequestDoc = remsRequestToUpdate; } catch (e) { console.log(e); message = 'ERROR: failed to update rems case with requirement not needed to dispense'; @@ -404,7 +420,7 @@ const createMetRequirementAndUpdateCaseNotRequiredToDispense = async ( res.status(201); if (returnRemsRequest) { - res.send(returnedRemsRequestDoc); + res.send(remsRequestToUpdate); } else { res.send(message); } @@ -413,37 +429,40 @@ const createMetRequirementAndUpdateCaseNotRequiredToDispense = async ( router.post('/met', async (req: Request, res: Response) => { try { - const requestBody = req.body; + const requestBody = req.body as Bundle; // extract params and questionnaire response identifier - const params = getResource(requestBody, requestBody.entry[0].resource.focus?.[0]?.reference); - const questionnaireResponse = getQuestionnaireResponse(requestBody); - const questionnaireStringArray = questionnaireResponse.questionnaire.split('/'); - const requirementId = questionnaireStringArray[questionnaireStringArray.length - 1]; + const params = getResource( + requestBody, + (requestBody.entry?.[0]?.resource as MessageHeader)?.focus?.[0]?.reference || '' + ) as Parameters; + const questionnaireResponse = getQuestionnaireResponse(requestBody) as QuestionnaireResponse; + const questionnaireStringArray = questionnaireResponse?.questionnaire?.split('/'); + const requirementId = questionnaireStringArray?.[questionnaireStringArray.length - 1]; // stakeholder and medication references let prescriptionReference = ''; let practitionerReference = ''; let pharmacistReference = ''; let patientReference = ''; - for (const param of params.parameter) { + for (const param of params.parameter || []) { if (param.name === 'prescription') { - prescriptionReference = param.valueReference.reference; + prescriptionReference = param.valueReference?.reference || ''; } else if (param.name === 'prescriber') { - practitionerReference = param.valueReference.reference; + practitionerReference = param.valueReference?.reference || ''; } else if (param.name === 'pharmacy') { - pharmacistReference = param.valueReference.reference; + pharmacistReference = param.valueReference?.reference || ''; } else if (param.name === 'source-patient') { - patientReference = param.valueReference.reference; + patientReference = param.valueReference?.reference || ''; } } // obtain drug information from database - const prescription = getResource(requestBody, prescriptionReference); - const medicationCode = getDrugCodeFromMedicationRequest(prescription); + const prescription = getResource(requestBody, prescriptionReference) as MedicationRequest; + const medicationCode = getDrugCodeFromMedicationRequest(prescription) as Coding; const prescriptionSystem = medicationCode?.system; const prescriptionCode = medicationCode?.code; - const patient = getResource(requestBody, patientReference); + const patient = getResource(requestBody, patientReference) as Patient; const drug = await medicationCollection .findOne({ @@ -455,11 +474,11 @@ router.post('/met', async (req: Request, res: Response) => { if (drug) { for (const requirement of drug.requirements) { // figure out which stakeholder the req corresponds to - const reqStakeholder = requirement.stakeholderType; - const reqStakeholderReference = - reqStakeholder === 'prescriber' + const stakeholder = requirement.stakeholderType; + const stakeholderReference = + stakeholder === 'prescriber' ? practitionerReference - : reqStakeholder === 'pharmacist' + : stakeholder === 'pharmacist' ? pharmacistReference : patientReference; @@ -473,7 +492,7 @@ router.post('/met', async (req: Request, res: Response) => { requirement, questionnaireResponse, res, - reqStakeholderReference, + stakeholderReference, practitionerReference, pharmacistReference, patientReference @@ -481,14 +500,14 @@ router.post('/met', async (req: Request, res: Response) => { return; } else { - // If its not the patient status requirement + // If it's not the patient status requirement if (requirement.requiredToDispense) { await createMetRequirementAndUpdateCase( drug, requirement, questionnaireResponse, res, - reqStakeholderReference + stakeholderReference ); return; } else { @@ -498,7 +517,7 @@ router.post('/met', async (req: Request, res: Response) => { requirement, questionnaireResponse, res, - reqStakeholderReference + stakeholderReference ); return; } @@ -513,28 +532,32 @@ router.post('/met', async (req: Request, res: Response) => { } }); -const getResource = (bundle: { entry: any[] }, resourceReference: string) => { +const getResource = (bundle: Bundle, resourceReference: string) => { const temp = resourceReference.split('/'); const _resourceType = temp[0]; const _id = temp[1]; - for (let i = 0; i < bundle.entry.length; i++) { - if ( - bundle.entry[i].resource.resourceType === _resourceType && - bundle.entry[i].resource.id === _id - ) { - return bundle.entry[i].resource; + if (bundle.entry) { + for (let i = 0; i < bundle.entry.length; i++) { + if ( + bundle.entry[i].resource?.resourceType === _resourceType && + bundle.entry[i].resource?.id === _id + ) { + return bundle.entry[i].resource; + } } } return null; }; -const getQuestionnaireResponse = (bundle: { entry: any[] }) => { +const getQuestionnaireResponse = (bundle: Bundle) => { const _resourceType = 'QuestionnaireResponse'; - for (let i = 0; i < bundle.entry.length; i++) { - if (bundle.entry[i].resource.resourceType === _resourceType) { - return bundle.entry[i].resource; + if (bundle.entry) { + for (let i = 0; i < bundle.entry.length; i++) { + if (bundle.entry[i].resource?.resourceType === _resourceType) { + return bundle.entry[i].resource as QuestionnaireResponse; + } } } return null; From bb70e7fb2efe95af9497e0822a98cddf207d76ac Mon Sep 17 00:00:00 2001 From: Joyce Quach Date: Fri, 8 Mar 2024 00:49:07 -0500 Subject: [PATCH 4/4] Re-extend interfaces to use mongoose.Document and access Document._id --- src/fhir/models.ts | 12 ++++-------- src/lib/etasu.ts | 46 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/fhir/models.ts b/src/fhir/models.ts index d49f33ce..b1015386 100644 --- a/src/fhir/models.ts +++ b/src/fhir/models.ts @@ -1,9 +1,5 @@ import { Questionnaire, QuestionnaireResponse } from 'fhir/r4'; -import { Schema, model } from 'mongoose'; - -interface MongoItemWithInternalId { - _id: string; -} +import { Schema, model, Document } from 'mongoose'; export interface Requirement { name: string; @@ -16,14 +12,14 @@ export interface Requirement { appContext: string | null; } -export interface Medication { +export interface Medication extends Document { name: string; codeSystem: string; code: string; requirements: Requirement[]; } -export interface MetRequirements extends MongoItemWithInternalId { +export interface MetRequirements extends Document { completed: boolean; completedQuestionnaire: QuestionnaireResponse | null; requirementName: string; @@ -34,7 +30,7 @@ export interface MetRequirements extends MongoItemWithInternalId { metRequirementId: string; } -export interface RemsCase { +export interface RemsCase extends Document { case_number: string; status: string; drugName: string; diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index c5bb0911..149b27f1 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -20,6 +20,7 @@ import { Patient, QuestionnaireResponse } from 'fhir/r4'; +import { FilterQuery } from 'mongoose'; const router = Router(); // const medicationCollection = db.collection('medication-requirements'); @@ -37,9 +38,18 @@ router.get('/met/:caseId', async (req: Request, res: Response) => { }); const getCaseInfo = async ( - remsCaseSearchDict: Partial, - medicationSearchDict: Partial -): Promise | null> => { + remsCaseSearchDict: FilterQuery, + medicationSearchDict: FilterQuery +): Promise | null> => { const foundRequirements = await remsCaseCollection.findOne(remsCaseSearchDict); // if there are no requirements, then return 'Approved' @@ -50,7 +60,16 @@ const getCaseInfo = async ( // iterate through each requirement of the drug if (drug?.requirements.length === 0) { // create simple rems request to return - const remsRequest: Omit = { + const remsRequest: Pick< + RemsCase, + | 'status' + | 'drugName' + | 'drugCode' + | 'patientFirstName' + | 'patientLastName' + | 'patientDOB' + | 'metRequirements' + > = { status: 'Approved', drugName: drug?.name, drugCode: drug?.code, @@ -131,7 +150,10 @@ router.post('/reset', async (req: Request, res: Response) => { res.send('reset etasu database collections'); }); -const pushMetRequirements = (matchedMetReq: MetRequirements, remsRequest: RemsCase) => { +const pushMetRequirements = ( + matchedMetReq: MetRequirements, + remsRequest: Pick +) => { remsRequest.metRequirements.push({ stakeholderId: matchedMetReq?.stakeholderId, completed: matchedMetReq?.completed, @@ -147,7 +169,7 @@ const createMetRequirements = async (metReq: Partial) => { const createAndPushMetRequirements = async ( metReq: Partial, - remsRequest: RemsCase + remsRequest: Pick ) => { try { const matchedMetReq = await createMetRequirements(metReq); @@ -179,7 +201,17 @@ const createMetRequirementAndNewCase = async ( // create new rems request and add the created metReq to it let remsRequestCompletedStatus = 'Approved'; - const remsRequest: RemsCase = { + const remsRequest: Pick< + RemsCase, + | 'case_number' + | 'status' + | 'drugName' + | 'drugCode' + | 'patientFirstName' + | 'patientLastName' + | 'patientDOB' + | 'metRequirements' + > = { case_number: case_number, status: remsRequestCompletedStatus, drugName: drug?.name,