diff --git a/src/fhir/models.ts b/src/fhir/models.ts index b94727b..4873016 100644 --- a/src/fhir/models.ts +++ b/src/fhir/models.ts @@ -39,6 +39,8 @@ export interface RemsCase extends Document { patientFirstName: string; patientLastName: string; patientDOB: string; + medicationRequestReference?: string; + originatingFhirServer?: string; metRequirements: Partial[]; } @@ -96,6 +98,8 @@ const remsCaseCollectionSchema = new Schema({ patientLastName: { type: String }, patientDOB: { type: String }, drugCode: { type: String }, + medicationRequestReference: { type: String }, + originatingFhirServer: { type: String }, metRequirements: [ { metRequirementId: { type: String }, @@ -107,4 +111,4 @@ const remsCaseCollectionSchema = new Schema({ ] }); -export const remsCaseCollection = model('RemsCaseCollection', remsCaseCollectionSchema); +export const remsCaseCollection = model('RemsCaseCollection', remsCaseCollectionSchema); \ No newline at end of file diff --git a/src/hooks/hookResources.ts b/src/hooks/hookResources.ts index 7646a44..0787b5d 100644 --- a/src/hooks/hookResources.ts +++ b/src/hooks/hookResources.ts @@ -24,12 +24,14 @@ import { import axios from 'axios'; import { ServicePrefetch } from '../rems-cds-hooks/resources/CdsService'; import { hydrate } from '../rems-cds-hooks/prefetch/PrefetchHydrator'; +import { createNewRemsCaseFromCDSHook } from '../lib/etasu'; type HandleCallback = ( res: any, hydratedPrefetch: HookPrefetch | undefined, contextRequest: FhirResource | undefined, - patient: FhirResource | undefined + patient: FhirResource | undefined, + fhirServer?: string ) => Promise; export interface CardRule { @@ -366,7 +368,8 @@ export const handleCardOrder = async ( res: any, hydratedPrefetch: HookPrefetch | undefined, contextRequest: FhirResource | undefined, - resource: FhirResource | undefined + resource: FhirResource | undefined, + fhirServer?: string ): Promise => { const patient = resource?.resourceType === 'Patient' ? resource : undefined; @@ -396,13 +399,43 @@ export const handleCardOrder = async ( // find a matching REMS case for the patient and this drug to only return needed results const patientName = patient?.name?.[0]; const patientBirth = patient?.birthDate; - const remsCase = await remsCaseCollection.findOne({ + let remsCase = await remsCaseCollection.findOne({ patientFirstName: patientName?.given?.[0], patientLastName: patientName?.family, patientDOB: patientBirth, drugCode: code }); + // If no REMS case exists and drug has requirements, create case with all requirements unmet + if (!remsCase && drug && patient && request) { + const requiresCase = drug.requirements.some(req => req.requiredToDispense); + + if (requiresCase && fhirServer) { + try { + const patientReference = `Patient/${patient.id}`; + const medicationRequestReference = `${request.resourceType}/${request.id}`; + const practitionerReference = request.requester?.reference || ''; + const pharmacistReference = pharmacy?.id ? `HealthcareService/${pharmacy.id}` : ''; + + const newCase = await createNewRemsCaseFromCDSHook( + patient, + drug, + practitionerReference, + pharmacistReference, + patientReference, + medicationRequestReference, + fhirServer + ); + + remsCase = newCase; + + console.log(`Created REMS case from CDS Hook with originating server: ${fhirServer}`); + } catch (error) { + console.error('Failed to create REMS case from CDS Hook:', error); + } + } + } + const codeRule = (code && codeMap[code]) || []; const cardPromises = codeRule.map( @@ -594,6 +627,7 @@ export async function handleCard( const context = req.body.context; const patient = hydratedPrefetch?.patient; const practitioner = hydratedPrefetch?.practitioner; + const fhirServer = req.body.fhirServer; console.log(' Patient: ' + patient?.id); @@ -612,7 +646,7 @@ export async function handleCard( res.json(buildErrorCard('Context userId does not match prefetch Practitioner ID')); return; } - return callback(res, hydratedPrefetch, contextRequest, patient); + return callback(res, hydratedPrefetch, contextRequest, patient, fhirServer); } // handles all hooks, any supported hook should pass through this function @@ -836,7 +870,8 @@ export const handleCardEncounter = async ( res: any, hookPrefetch: HookPrefetch | undefined, _contextRequest: FhirResource | undefined, - resource: FhirResource | undefined + resource: FhirResource | undefined, + fhirServer?: string ): Promise => { const patient = resource?.resourceType === 'Patient' ? resource : undefined; const medResource = hookPrefetch?.medicationRequests; @@ -962,4 +997,4 @@ export function createQuestionnaireCompletionTask( ] }; return taskResource; -} +} \ No newline at end of file diff --git a/src/lib/communication.ts b/src/lib/communication.ts new file mode 100644 index 0000000..55c4c4f --- /dev/null +++ b/src/lib/communication.ts @@ -0,0 +1,172 @@ +import { Communication, Task, Patient, MedicationRequest } from 'fhir/r4'; +import axios from 'axios'; +import config from '../config'; +import { uid } from 'uid'; +import container from './winston'; +import { createQuestionnaireCompletionTask } from '../hooks/hookResources'; +import { Requirement } from '../fhir/models'; + +const logger = container.get('application'); + + +export async function sendCommunicationToEHR( + remsCase: any, + medication: any, + outstandingRequirements: any[] +): Promise { + try { + logger.info(`Creating Communication for case ${remsCase.case_number}`); + + // Create patient object from REMS case + const patient: Patient = { + resourceType: 'Patient', + id: `${remsCase.patientFirstName}-${remsCase.patientLastName}`.replace(/\s+/g, '-'), + name: [ + { + given: [remsCase.patientFirstName], + family: remsCase.patientLastName + } + ], + birthDate: remsCase.patientDOB + }; + + // Get the stored MedicationRequest reference + const medicationRequestRef = remsCase.medicationRequestReference; + + // Create a minimal MedicationRequest for task context + const medicationRequest: MedicationRequest = { + resourceType: 'MedicationRequest', + status: 'active', + intent: 'order', + medicationCodeableConcept: { + coding: [ + { + system: 'http://www.nlm.nih.gov/research/umls/rxnorm', + code: remsCase.drugCode, + display: remsCase.drugName + } + ] + }, + subject: { + reference: `Patient/${patient.id}` + }, + requester: { + reference: remsCase.metRequirements.find((mr: any) => + mr.requirementName?.toLowerCase().includes('prescriber') + )?.stakeholderId + } + }; + + // Create Tasks for each outstanding requirement + const tasks: Task[] = []; + for (const outstandingReq of outstandingRequirements) { + const requirement = outstandingReq.requirement || + medication.requirements.find((r: Requirement) => r.name === outstandingReq.name); + + if (requirement && requirement.appContext) { + const questionnaireUrl = requirement.appContext; + const task = createQuestionnaireCompletionTask( + requirement, + patient, + questionnaireUrl, + medicationRequest + ); + task.id = `task-${uid()}`; + tasks.push(task); + } + } + + // Create Communication resource + const communication: Communication = { + resourceType: 'Communication', + id: `comm-${uid()}`, + status: 'completed', + category: [ + { + coding: [ + { + system: 'http://terminology.hl7.org/CodeSystem/communication-category', + code: 'notification', + display: 'Notification' + } + ] + } + ], + priority: 'urgent', + subject: { + reference: `Patient/${patient.id}`, + display: `${remsCase.patientFirstName} ${remsCase.patientLastName}` + }, + topic: { + coding: [ + { + system: 'http://terminology.hl7.org/CodeSystem/communication-topic', + code: 'progress-update', + display: 'Progress Update' + } + ], + text: 'Outstanding REMS Requirements for Medication Dispensing' + }, + sent: new Date().toISOString(), + recipient: [ + { + reference: medicationRequest.requester?.reference || '' + } + ], + sender: { + reference: 'Organization/rems-admin', + display: config.server?.name || 'REMS Administrator' + }, + payload: [ + { + contentString: `Medication dispensing authorization DENIED for ${remsCase.drugName}.\n\n` + + `The following REMS requirements must be completed:\n\n` + + outstandingRequirements + .map((req, idx) => `${idx + 1}. ${req.name} (${req.stakeholder})`) + .join('\n') + + `\n\nCase Number: ${remsCase.case_number}\n` + + `Patient: ${remsCase.patientFirstName} ${remsCase.patientLastName} (DOB: ${remsCase.patientDOB})` + } + ], + contained: tasks, + about: [ + { + reference: medicationRequestRef, + display: `Prescription for ${remsCase.drugName}` + }, + ...tasks.map(task => ({ + reference: `#${task.id}`, + display: task.description + })) + ] + }; + + // Determine EHR endpoint: use originatingFhirServer if available, otherwise default + const ehrEndpoint = remsCase.originatingFhirServer || + config.fhirServerConfig?.auth?.resourceServer; + + if (!ehrEndpoint) { + logger.warn('No EHR endpoint configured, Communication not sent'); + return; + } + + // Send Communication to EHR + logger.info(`Sending Communication to EHR: ${ehrEndpoint}`); + + const response = await axios.post(`${ehrEndpoint}/Communication`, communication, { + headers: { + 'Content-Type': 'application/fhir+json' + } + }); + + if (response.status === 200 || response.status === 201) { + logger.info(`Communication successfully sent to EHR for case ${remsCase.case_number}`); + } else { + logger.warn(`Unexpected response status from EHR: ${response.status}`); + } + + } catch (error: any) { + logger.error(`Failed to send Communication to EHR: ${error.message}`); + throw error; + } +} \ No newline at end of file diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index 63ef37b..3c6338e 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -183,6 +183,113 @@ const pushMetRequirements = ( }); }; +export const createNewRemsCaseFromCDSHook = async ( + patient: Patient, + drug: Medication, + practitionerReference: string, + pharmacistReference: string, + patientReference: string, + medicationRequestReference: string, + originatingFhirServer?: string +) => { + const patientFirstName = patient.name?.[0].given?.[0] || ''; + const patientLastName = patient.name?.[0].family || ''; + const patientDOB = patient.birthDate || ''; + const case_number = uid(); + + // Check if case already exists + const existingCase = await remsCaseCollection.findOne({ + patientFirstName: patientFirstName, + patientLastName: patientLastName, + patientDOB: patientDOB, + drugCode: drug?.code + }); + + if (existingCase) { + console.log(`Case already exists for patient ${patientFirstName} ${patientLastName} and drug ${drug?.name}`); + return existingCase; + } + + // Create new case with all requirements pending + const remsRequest: Pick< + RemsCase, + | 'case_number' + | 'status' + | 'dispenseStatus' + | 'drugName' + | 'drugCode' + | 'patientFirstName' + | 'patientLastName' + | 'patientDOB' + | 'medicationRequestReference' + | 'metRequirements' + > & { originatingFhirServer?: string } = { + case_number: case_number, + status: 'Pending', // All requirements unmet, so status is Pending + dispenseStatus: 'Pending', + drugName: drug?.name, + drugCode: drug?.code, + patientFirstName: patientFirstName, + patientLastName: patientLastName, + patientDOB: patientDOB, + medicationRequestReference: medicationRequestReference, + originatingFhirServer: originatingFhirServer, + metRequirements: [] + }; + + // Iterate through ALL requirements and create as unmet (or link to existing if already completed) + for (const requirement of drug.requirements) { + // Only process requirements that are required to dispense + if (requirement.requiredToDispense) { + // Figure out which stakeholder the requirement corresponds to + const stakeholderType = requirement.stakeholderType; + const stakeholderReference = + stakeholderType === 'prescriber' + ? practitionerReference + : stakeholderType === 'pharmacist' + ? pharmacistReference + : patientReference; + + // Check if this stakeholder has already completed this requirement + const existingMetReq = await metRequirementsCollection + .findOne({ + stakeholderId: stakeholderReference, + requirementName: requirement.name, + drugName: drug?.name + }) + .exec(); + + if (existingMetReq) { + // Requirement already exists (e.g., prescriber or pharmacist enrolled previously) + pushMetRequirements(existingMetReq, remsRequest); + existingMetReq.case_numbers.push(case_number); + await existingMetReq.save(); + } else { + // Create new unmet requirement + const newMetReq = { + completed: false, + requirementName: requirement.name, + requirementDescription: requirement.description, + drugName: drug?.name, + stakeholderId: stakeholderReference, + case_numbers: [case_number] + }; + + if (!(await createAndPushMetRequirements(newMetReq, remsRequest))) { + console.log('ERROR: failed to create unmet requirement for new case'); + } + } + } + } + + // Save the new case + remsRequest.status = remsRequest.metRequirements.every(req => req.completed) ? 'Approved' : 'Pending'; + const newCase = await remsCaseCollection.create(remsRequest); + + console.log(`Created new REMS case ${case_number} with all requirements unmet (or linked to existing)`); + return newCase; +}; + const createMetRequirements = async (metReq: Partial) => { return await metRequirementsCollection.create(metReq); }; @@ -210,7 +317,9 @@ const createMetRequirementAndNewCase = async ( reqStakeholderReference: string, practitionerReference: string, pharmacistReference: string, - patientReference: string + patientReference: string, + medicationRequestReference: string, + originatingFhirServer?: string ) => { const patientFirstName = patient.name?.[0].given?.[0] || ''; const patientLastName = patient.name?.[0].family || ''; @@ -231,8 +340,9 @@ const createMetRequirementAndNewCase = async ( | 'patientFirstName' | 'patientLastName' | 'patientDOB' + | 'medicationRequestReference' | 'metRequirements' - > = { + > & { originatingFhirServer?: string } = { case_number: case_number, status: remsRequestCompletedStatus, dispenseStatus: dispenseStatusDefault, @@ -241,6 +351,8 @@ const createMetRequirementAndNewCase = async ( patientFirstName: patientFirstName, patientLastName: patientLastName, patientDOB: patientDOB, + medicationRequestReference: medicationRequestReference, + originatingFhirServer: originatingFhirServer, metRequirements: [] }; @@ -575,7 +687,8 @@ export const processQuestionnaireResponseSubmission = async (requestBody: Bundle stakeholderReference, practitionerReference, pharmacistReference, - patientReference + patientReference, + prescriptionReference ); } else { // If it's not the patient status requirement @@ -605,4 +718,4 @@ export const processQuestionnaireResponseSubmission = async (requestBody: Bundle export { getResource, getQuestionnaireResponse }; -export default router; +export default router; \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index b5ad26b..f2970f2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -133,7 +133,7 @@ class REMSServer extends Server { } }) ); - this.app.use('/', Ncpdp); + this.app.use('/ncpdp', Ncpdp); return this; } @@ -163,4 +163,4 @@ class REMSServer extends Server { // Start the application -export { REMSServer, initialize }; +export { REMSServer, initialize }; \ No newline at end of file