From f4a0b6ed6d79b8a39f56f3a7c56a00b9f2e667e2 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Fri, 14 Nov 2025 13:07:26 -0500 Subject: [PATCH 1/9] send communication resource to EHR --- src/fhir/models.ts | 4 +- src/lib/dispense_authorization.ts | 241 ++++++++++++++++++++++++++++++ src/lib/etasu.ts | 10 +- src/server.ts | 9 +- 4 files changed, 259 insertions(+), 5 deletions(-) create mode 100644 src/lib/dispense_authorization.ts diff --git a/src/fhir/models.ts b/src/fhir/models.ts index b94727b..c380fc6 100644 --- a/src/fhir/models.ts +++ b/src/fhir/models.ts @@ -39,6 +39,7 @@ export interface RemsCase extends Document { patientFirstName: string; patientLastName: string; patientDOB: string; + medicationRequestReference?: string; metRequirements: Partial[]; } @@ -96,6 +97,7 @@ const remsCaseCollectionSchema = new Schema({ patientLastName: { type: String }, patientDOB: { type: String }, drugCode: { type: String }, + medicationRequestReference: { type: String }, metRequirements: [ { metRequirementId: { type: String }, @@ -107,4 +109,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/lib/dispense_authorization.ts b/src/lib/dispense_authorization.ts new file mode 100644 index 0000000..cb90bcd --- /dev/null +++ b/src/lib/dispense_authorization.ts @@ -0,0 +1,241 @@ +import { Router, Response, Request } from 'express'; +import { + medicationCollection, + remsCaseCollection, + Requirement +} from '../fhir/models'; +import { Communication, Task, Patient, MedicationRequest } from 'fhir/r4'; +import axios from 'axios'; +import config from '../config'; +import { uid } from 'uid'; +import container from '../lib/winston'; +import { createQuestionnaireCompletionTask } from '../hooks/hookResources'; + +const router = Router(); +const logger = container.get('application'); + +router.post('/authorize', async (req: Request, res: Response) => { + try { + const { caseNumber } = req.body; + + if (!caseNumber) { + return res.status(400).json({ error: 'caseNumber is required' }); + } + + logger.info(`Dispense authorization check for case: ${caseNumber}`); + + // Find the REMS case + const remsCase = await remsCaseCollection.findOne({ case_number: caseNumber }); + + if (!remsCase) { + logger.warn(`REMS case not found: ${caseNumber}`); + return res.status(404).json({ + approved: false, + error: 'Case not found' + }); + } + + // Get the medication to check requirements + const medication = await medicationCollection.findOne({ + code: remsCase.drugCode, + name: remsCase.drugName + }); + + if (!medication) { + logger.error(`Medication not found: ${remsCase.drugCode}`); + return res.status(500).json({ + approved: false, + error: 'Medication not found' + }); + } + + // Check which requirements are required for dispensing and not completed + const outstandingRequirements: Requirement[] = []; + + for (const requirement of medication.requirements) { + if (requirement.requiredToDispense) { + const metRequirement = remsCase.metRequirements.find( + metReq => metReq.requirementName === requirement.name + ); + + if (!metRequirement || !metRequirement.completed) { + outstandingRequirements.push(requirement); + } + } + } + + // If all required requirements are met, approve + if (outstandingRequirements.length === 0) { + logger.info(`All requirements met for case ${caseNumber}. Approving.`); + + // Update dispense status + remsCase.dispenseStatus = 'Approved'; + await remsCase.save(); + + return res.status(200).json({ approved: true }); + } + + // Outstanding requirements - deny and send Communication + logger.info( + `Outstanding requirements for case ${caseNumber}: ${outstandingRequirements + .map(r => r.name) + .join(', ')}` + ); + + // 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 or create a minimal one for Task context + const medicationRequestRef = remsCase.medicationRequestReference || + `MedicationRequest/${remsCase.case_number}`; + + // Create a minimal MedicationRequest for task context if needed + 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 => + mr.requirementName?.toLowerCase().includes('prescriber') + )?.stakeholderId + } + }; + + // Create Tasks using the existing function + const tasks: Task[] = []; + for (const requirement of outstandingRequirements) { + if (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.stakeholderType})`) + .join('\n') + + `\n\nCase Number: ${remsCase.case_number}\n` + + `Patient: ${remsCase.patientFirstName} ${remsCase.patientLastName} (DOB: ${remsCase.patientDOB})` + } + ], + contained: tasks, + about: [ + // Reference the actual MedicationRequest + { + reference: medicationRequestRef, + display: `Prescription for ${remsCase.drugName}` + }, + // Reference the contained Tasks + ...tasks.map(task => ({ + reference: `#${task.id}`, + display: task.description + })) + ] + }; + + + let ehrEndpoint = config.fhirServerConfig?.auth?.resourceServer; + + // Send Communication to EHR + if (ehrEndpoint) { + try { + const response = await axios.post(`${ehrEndpoint}/Communication`, communication, { + headers: { + 'Content-Type': 'application/fhir+json' + } + }); + + if (response.status === 200 || response.status === 201) { + logger.info(`Communication sent to EHR: ${ehrEndpoint}`); + } + } catch (error: any) { + logger.error(`Failed to send Communication to EHR: ${error.message}`); + } + } else { + logger.warn('No EHR endpoint configured, Communication not sent'); + } + + return res.status(200).json({ approved: false }); + } catch (error: any) { + logger.error(`Error in dispense authorization: ${error.message}`); + return res.status(500).json({ + approved: false, + error: 'Internal server error' + }); + } +}); + +export default router; \ No newline at end of file diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index 63ef37b..85ba3f6 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -210,7 +210,8 @@ const createMetRequirementAndNewCase = async ( reqStakeholderReference: string, practitionerReference: string, pharmacistReference: string, - patientReference: string + patientReference: string, + medicationRequestReference: string ) => { const patientFirstName = patient.name?.[0].given?.[0] || ''; const patientLastName = patient.name?.[0].family || ''; @@ -231,6 +232,7 @@ const createMetRequirementAndNewCase = async ( | 'patientFirstName' | 'patientLastName' | 'patientDOB' + | 'medicationRequestReference' | 'metRequirements' > = { case_number: case_number, @@ -241,6 +243,7 @@ const createMetRequirementAndNewCase = async ( patientFirstName: patientFirstName, patientLastName: patientLastName, patientDOB: patientDOB, + medicationRequestReference: medicationRequestReference, metRequirements: [] }; @@ -575,7 +578,8 @@ export const processQuestionnaireResponseSubmission = async (requestBody: Bundle stakeholderReference, practitionerReference, pharmacistReference, - patientReference + patientReference, + prescriptionReference ); } else { // If it's not the patient status requirement @@ -605,4 +609,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..9184ee4 100644 --- a/src/server.ts +++ b/src/server.ts @@ -11,6 +11,7 @@ import { Server } from '@projecttacoma/node-fhir-server-core'; import Etasu from './lib/etasu'; import Ncpdp from './ncpdp/script'; import Api from './lib/api_routes'; +import DispenseAuth from './lib/dispense_authorization'; import env from 'env-var'; import https from 'https'; import fs from 'fs'; @@ -31,6 +32,7 @@ const initialize = (config: any) => { .configureEtasuEndpoints() .configureNCPDPEndpoints() .configureUIEndpoints() + .configureDispenseAuthEndpoints() .setErrorRoutes(); }; @@ -142,6 +144,11 @@ class REMSServer extends Server { return this; } + configureDispenseAuthEndpoints() { + this.app.use('/dispense', DispenseAuth); + return this; + } + /** * @method listen * @description Start listening on the configured port @@ -163,4 +170,4 @@ class REMSServer extends Server { // Start the application -export { REMSServer, initialize }; +export { REMSServer, initialize }; \ No newline at end of file From 1536e690024c15ac74f6918abbf911259348fd44 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra <88040167+smalho01@users.noreply.github.com> Date: Fri, 14 Nov 2025 13:26:44 -0500 Subject: [PATCH 2/9] Potential fix for code scanning alert no. 32: Database query built from user-controlled sources Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- src/lib/dispense_authorization.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/dispense_authorization.ts b/src/lib/dispense_authorization.ts index cb90bcd..786dee1 100644 --- a/src/lib/dispense_authorization.ts +++ b/src/lib/dispense_authorization.ts @@ -25,7 +25,7 @@ router.post('/authorize', async (req: Request, res: Response) => { logger.info(`Dispense authorization check for case: ${caseNumber}`); // Find the REMS case - const remsCase = await remsCaseCollection.findOne({ case_number: caseNumber }); + const remsCase = await remsCaseCollection.findOne({ case_number: { $eq: caseNumber } }); if (!remsCase) { logger.warn(`REMS case not found: ${caseNumber}`); From e56aad932d91e98589ab64e8649803f56e0daeca Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Fri, 5 Dec 2025 10:09:32 -0500 Subject: [PATCH 3/9] create case on cds hook --- src/fhir/models.ts | 2 + src/hooks/hookResources.ts | 47 +++++++++++++-- src/lib/etasu.ts | 113 ++++++++++++++++++++++++++++++++++++- src/server.ts | 2 +- 4 files changed, 155 insertions(+), 9 deletions(-) diff --git a/src/fhir/models.ts b/src/fhir/models.ts index c380fc6..4873016 100644 --- a/src/fhir/models.ts +++ b/src/fhir/models.ts @@ -40,6 +40,7 @@ export interface RemsCase extends Document { patientLastName: string; patientDOB: string; medicationRequestReference?: string; + originatingFhirServer?: string; metRequirements: Partial[]; } @@ -98,6 +99,7 @@ const remsCaseCollectionSchema = new Schema({ patientDOB: { type: String }, drugCode: { type: String }, medicationRequestReference: { type: String }, + originatingFhirServer: { type: String }, metRequirements: [ { metRequirementId: { type: String }, 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/etasu.ts b/src/lib/etasu.ts index 85ba3f6..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); }; @@ -211,7 +318,8 @@ const createMetRequirementAndNewCase = async ( practitionerReference: string, pharmacistReference: string, patientReference: string, - medicationRequestReference: string + medicationRequestReference: string, + originatingFhirServer?: string ) => { const patientFirstName = patient.name?.[0].given?.[0] || ''; const patientLastName = patient.name?.[0].family || ''; @@ -234,7 +342,7 @@ const createMetRequirementAndNewCase = async ( | 'patientDOB' | 'medicationRequestReference' | 'metRequirements' - > = { + > & { originatingFhirServer?: string } = { case_number: case_number, status: remsRequestCompletedStatus, dispenseStatus: dispenseStatusDefault, @@ -244,6 +352,7 @@ const createMetRequirementAndNewCase = async ( patientLastName: patientLastName, patientDOB: patientDOB, medicationRequestReference: medicationRequestReference, + originatingFhirServer: originatingFhirServer, metRequirements: [] }; diff --git a/src/server.ts b/src/server.ts index 9184ee4..6f65dff 100644 --- a/src/server.ts +++ b/src/server.ts @@ -135,7 +135,7 @@ class REMSServer extends Server { } }) ); - this.app.use('/', Ncpdp); + this.app.use('/ncpdp', Ncpdp); return this; } From 25babbe02e5b2eb6c6176bc8e7d66a3f3b59a4d4 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Fri, 5 Dec 2025 12:25:09 -0500 Subject: [PATCH 4/9] update dispense auth --- src/lib/dispense_authorization.ts | 159 +++++++++--------------------- 1 file changed, 45 insertions(+), 114 deletions(-) diff --git a/src/lib/dispense_authorization.ts b/src/lib/dispense_authorization.ts index 786dee1..60dba37 100644 --- a/src/lib/dispense_authorization.ts +++ b/src/lib/dispense_authorization.ts @@ -1,87 +1,22 @@ -import { Router, Response, Request } from 'express'; -import { - medicationCollection, - remsCaseCollection, - Requirement -} from '../fhir/models'; import { Communication, Task, Patient, MedicationRequest } from 'fhir/r4'; import axios from 'axios'; import config from '../config'; import { uid } from 'uid'; import container from '../lib/winston'; import { createQuestionnaireCompletionTask } from '../hooks/hookResources'; +import { Requirement } from '../fhir/models'; -const router = Router(); const logger = container.get('application'); -router.post('/authorize', async (req: Request, res: Response) => { - try { - const { caseNumber } = req.body; - - if (!caseNumber) { - return res.status(400).json({ error: 'caseNumber is required' }); - } - - logger.info(`Dispense authorization check for case: ${caseNumber}`); - - // Find the REMS case - const remsCase = await remsCaseCollection.findOne({ case_number: { $eq: caseNumber } }); - - if (!remsCase) { - logger.warn(`REMS case not found: ${caseNumber}`); - return res.status(404).json({ - approved: false, - error: 'Case not found' - }); - } - - // Get the medication to check requirements - const medication = await medicationCollection.findOne({ - code: remsCase.drugCode, - name: remsCase.drugName - }); - - if (!medication) { - logger.error(`Medication not found: ${remsCase.drugCode}`); - return res.status(500).json({ - approved: false, - error: 'Medication not found' - }); - } - - // Check which requirements are required for dispensing and not completed - const outstandingRequirements: Requirement[] = []; - - for (const requirement of medication.requirements) { - if (requirement.requiredToDispense) { - const metRequirement = remsCase.metRequirements.find( - metReq => metReq.requirementName === requirement.name - ); - - if (!metRequirement || !metRequirement.completed) { - outstandingRequirements.push(requirement); - } - } - } - - // If all required requirements are met, approve - if (outstandingRequirements.length === 0) { - logger.info(`All requirements met for case ${caseNumber}. Approving.`); - - // Update dispense status - remsCase.dispenseStatus = 'Approved'; - await remsCase.save(); - - return res.status(200).json({ approved: true }); - } - - // Outstanding requirements - deny and send Communication - logger.info( - `Outstanding requirements for case ${caseNumber}: ${outstandingRequirements - .map(r => r.name) - .join(', ')}` - ); +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', @@ -95,11 +30,10 @@ router.post('/authorize', async (req: Request, res: Response) => { birthDate: remsCase.patientDOB }; - // Get the stored MedicationRequest reference or create a minimal one for Task context - const medicationRequestRef = remsCase.medicationRequestReference || - `MedicationRequest/${remsCase.case_number}`; + // Get the stored MedicationRequest reference + const medicationRequestRef = remsCase.medicationRequestReference; - // Create a minimal MedicationRequest for task context if needed + // Create a minimal MedicationRequest for task context const medicationRequest: MedicationRequest = { resourceType: 'MedicationRequest', status: 'active', @@ -117,16 +51,19 @@ router.post('/authorize', async (req: Request, res: Response) => { reference: `Patient/${patient.id}` }, requester: { - reference: remsCase.metRequirements.find(mr => + reference: remsCase.metRequirements.find((mr: any) => mr.requirementName?.toLowerCase().includes('prescriber') )?.stakeholderId } }; - // Create Tasks using the existing function + // Create Tasks for each outstanding requirement const tasks: Task[] = []; - for (const requirement of outstandingRequirements) { - if (requirement.appContext) { + 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, @@ -143,7 +80,7 @@ router.post('/authorize', async (req: Request, res: Response) => { const communication: Communication = { resourceType: 'Communication', id: `comm-${uid()}`, - status: 'completed', + status: 'completed', category: [ { coding: [ @@ -155,7 +92,7 @@ router.post('/authorize', async (req: Request, res: Response) => { ] } ], - priority: 'urgent', + priority: 'urgent', subject: { reference: `Patient/${patient.id}`, display: `${remsCase.patientFirstName} ${remsCase.patientLastName}` @@ -170,7 +107,7 @@ router.post('/authorize', async (req: Request, res: Response) => { ], text: 'Outstanding REMS Requirements for Medication Dispensing' }, - sent: new Date().toISOString(), + sent: new Date().toISOString(), recipient: [ { reference: medicationRequest.requester?.reference || '' @@ -185,7 +122,7 @@ router.post('/authorize', async (req: Request, res: Response) => { 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.stakeholderType})`) + .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})` @@ -193,12 +130,10 @@ router.post('/authorize', async (req: Request, res: Response) => { ], contained: tasks, about: [ - // Reference the actual MedicationRequest { reference: medicationRequestRef, display: `Prescription for ${remsCase.drugName}` }, - // Reference the contained Tasks ...tasks.map(task => ({ reference: `#${task.id}`, display: task.description @@ -206,36 +141,32 @@ router.post('/authorize', async (req: Request, res: Response) => { ] }; - - let ehrEndpoint = config.fhirServerConfig?.auth?.resourceServer; + // Determine EHR endpoint: use originatingFhirServer if available, otherwise default + const ehrEndpoint = remsCase.originatingFhirServer || + config.fhirServerConfig?.auth?.resourceServer; - // Send Communication to EHR - if (ehrEndpoint) { - try { - const response = await axios.post(`${ehrEndpoint}/Communication`, communication, { - headers: { - 'Content-Type': 'application/fhir+json' - } - }); + if (!ehrEndpoint) { + logger.warn('No EHR endpoint configured, Communication not sent'); + return; + } - if (response.status === 200 || response.status === 201) { - logger.info(`Communication sent to EHR: ${ehrEndpoint}`); - } - } catch (error: any) { - logger.error(`Failed to send Communication to EHR: ${error.message}`); + // 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('No EHR endpoint configured, Communication not sent'); + logger.warn(`Unexpected response status from EHR: ${response.status}`); } - - return res.status(200).json({ approved: false }); + } catch (error: any) { - logger.error(`Error in dispense authorization: ${error.message}`); - return res.status(500).json({ - approved: false, - error: 'Internal server error' - }); + logger.error(`Failed to send Communication to EHR: ${error.message}`); + throw error; } -}); - -export default router; \ No newline at end of file +} \ No newline at end of file From 89bdb150e54be76485f61fd7d1f65e1501754614 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Fri, 5 Dec 2025 12:26:21 -0500 Subject: [PATCH 5/9] remove dispense auth endpoint - replacing with ncpdp --- src/server.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/server.ts b/src/server.ts index 6f65dff..f2970f2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -11,7 +11,6 @@ import { Server } from '@projecttacoma/node-fhir-server-core'; import Etasu from './lib/etasu'; import Ncpdp from './ncpdp/script'; import Api from './lib/api_routes'; -import DispenseAuth from './lib/dispense_authorization'; import env from 'env-var'; import https from 'https'; import fs from 'fs'; @@ -32,7 +31,6 @@ const initialize = (config: any) => { .configureEtasuEndpoints() .configureNCPDPEndpoints() .configureUIEndpoints() - .configureDispenseAuthEndpoints() .setErrorRoutes(); }; @@ -144,11 +142,6 @@ class REMSServer extends Server { return this; } - configureDispenseAuthEndpoints() { - this.app.use('/dispense', DispenseAuth); - return this; - } - /** * @method listen * @description Start listening on the configured port From 01e7447acf3c112d355fa3a8e711434cbe695708 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Tue, 9 Dec 2025 10:21:12 -0500 Subject: [PATCH 6/9] rename dispense to communcation resource --- src/lib/{dispense_authorization.ts => communication.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/lib/{dispense_authorization.ts => communication.ts} (99%) diff --git a/src/lib/dispense_authorization.ts b/src/lib/communication.ts similarity index 99% rename from src/lib/dispense_authorization.ts rename to src/lib/communication.ts index 60dba37..55c4c4f 100644 --- a/src/lib/dispense_authorization.ts +++ b/src/lib/communication.ts @@ -2,7 +2,7 @@ import { Communication, Task, Patient, MedicationRequest } from 'fhir/r4'; import axios from 'axios'; import config from '../config'; import { uid } from 'uid'; -import container from '../lib/winston'; +import container from './winston'; import { createQuestionnaireCompletionTask } from '../hooks/hookResources'; import { Requirement } from '../fhir/models'; From 6fcbad3ed85559d41d84240304fab5da00675d83 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 7 Jan 2026 12:45:25 -0500 Subject: [PATCH 7/9] update create new case to check for existing case --- src/lib/etasu.ts | 66 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index 3c6338e..305c4cc 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -325,8 +325,72 @@ const createMetRequirementAndNewCase = async ( const patientLastName = patient.name?.[0].family || ''; const patientDOB = patient.birthDate || ''; let message = ''; - 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) { + // Case already exists - update the existing requirement instead of creating new case + console.log(`Case ${existingCase.case_number} already exists, updating requirement ${requirement.name}`); + + // Find and update the existing MetRequirement + const matchedMetReq = await metRequirementsCollection + .findOne({ + stakeholderId: reqStakeholderReference, + requirementName: requirement.name, + drugName: drug?.name + }) + .exec(); + + if (matchedMetReq) { + // Update existing MetRequirement + matchedMetReq.completed = true; + matchedMetReq.completedQuestionnaire = questionnaireResponse; + await matchedMetReq.save(); + + // Update the case's metRequirements array + const metReqArray = existingCase.metRequirements || []; + let foundUncompleted = false; + + for (let i = 0; i < metReqArray.length; i++) { + const req = existingCase.metRequirements[i]; + if (req?.requirementName === matchedMetReq.requirementName) { + metReqArray[i].completed = true; + req!.completed = true; + await remsCaseCollection.updateOne( + { _id: existingCase._id }, + { $set: { metRequirements: metReqArray } } + ); + } + if (!req?.completed) { + foundUncompleted = true; + } + } + + // Update case status if all requirements are now complete + if (!foundUncompleted && existingCase.status === 'Pending') { + existingCase.status = 'Approved'; + await existingCase.save(); + } + + return { + returnedRemsRequestDoc: existingCase + }; + } else { + message = 'ERROR: MetRequirement not found for existing case'; + console.log(message); + throw new Error(message); + } + } + + // No existing case - create new one + const case_number = uid(); + // create new rems request and add the created metReq to it let remsRequestCompletedStatus = 'Approved'; const dispenseStatusDefault = 'Pending'; From 0009f6ca7ad6e08d42fdceb48158df84ea12e92e Mon Sep 17 00:00:00 2001 From: Sahil Malhotra <88040167+smalho01@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:12:04 -0500 Subject: [PATCH 8/9] PACIO - added extended case history and prescriber/pharmacy change detection (#183) * added advanced case history and prescriber/pharmacy change detection * run lint / prettier * ncpdp endpoint implimentation * update routing * add package ndc code tracking for ncpdp messages * fix ndc logic * updated models for patient id tracking * include response type in ncpdp messages * use saved medication on rems case creation to get ndc code from rxnorm * don't show patient status until patient enrolled * rxfill * proper reason code handling * singular denial reason * single denial reason --- .env | 1 + README.md | 2 + src/config.ts | 3 +- src/fhir/models.ts | 50 ++- src/fhir/utilities.ts | 4 + src/hooks/hookResources.ts | 55 ++- src/lib/communication.ts | 37 +- src/lib/etasu.ts | 279 +++++++++++++-- src/lib/winston.ts | 16 +- src/ncpdp/script.ts | 675 +++++++++++++++++++++++++++++++++++-- src/server.ts | 4 +- 11 files changed, 1042 insertions(+), 84 deletions(-) diff --git a/.env b/.env index 3ae3aff..34a5e1b 100644 --- a/.env +++ b/.env @@ -12,6 +12,7 @@ VSAC_API_KEY = changeMe WHITELIST = * SERVER_NAME = CodeX REMS Administrator Prototype FULL_RESOURCE_IN_APP_CONTEXT = false +DOCKERED_EHR_CONTAINER_NAME = false #Frontend Vars FRONTEND_PORT=9090 diff --git a/README.md b/README.md index 22421b3..c4219f8 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,8 @@ Following are a list of modifiable paths: | WHITELIST | `http://localhost, http://localhost:3005` | List of valid URLs for CORS. Should include any URLs the server accesses for resources. | | SERVER_NAME | `CodeX REMS Administrator Prototype` | Name of the server that is returned in the card source. | | FULL_RESOURCE_IN_APP_CONTEXT | 'false' | If true, the entire order resource will be included in the appContext, otherwise only a reference will be. | +| DOCKERED_EHR_CONTAINER_NAME | '' | String of the EHR container name for local docker networking communication | + | FRONTEND_PORT | `9080` | Port that the frontend server should run on, change if there are conflicts with port usage. | | VITE_REALM | `ClientFhirServer` | Keycloak realm for frontend authentication. | | VITE_AUTH | `http://localhost:8180` | Keycloak authentication server URL for frontend. | diff --git a/src/config.ts b/src/config.ts index de555c5..eedc332 100644 --- a/src/config.ts +++ b/src/config.ts @@ -41,7 +41,8 @@ export default { fhirServerConfig: { auth: { // This server's URI - resourceServer: env.get('RESOURCE_SERVER').required().asUrlString() + resourceServer: env.get('RESOURCE_SERVER').required().asUrlString(), + dockered_ehr_container_name: env.get('DOCKERED_EHR_CONTAINER_NAME').asString() // // if you use this strategy, you need to add the corresponding env vars to docker-compose // diff --git a/src/fhir/models.ts b/src/fhir/models.ts index 4873016..d997b57 100644 --- a/src/fhir/models.ts +++ b/src/fhir/models.ts @@ -5,7 +5,7 @@ export interface Requirement { name: string; description: string; questionnaire: Questionnaire | null; - stakeholderType: 'patient' | 'prescriber' | 'pharmacist' | string; // From fhir4.Parameters.parameter.name + stakeholderType: 'patient' | 'prescriber' | 'pharmacist' | string; createNewCase: boolean; resourceId: string; requiredToDispense: boolean; @@ -15,7 +15,8 @@ export interface Requirement { export interface Medication extends Document { name: string; codeSystem: string; - code: string; + code: string; // RxNorm code (used for CDS Hooks) + ndcCode: string; // NDC code (used for NCPDP SCRIPT) requirements: Requirement[]; } @@ -30,15 +31,31 @@ export interface MetRequirements extends Document { metRequirementId: any; } +export interface PrescriptionEvent { + medicationRequestReference: string; + prescriberId: string; + pharmacyId?: string; + timestamp: Date; + originatingFhirServer?: string; + caseStatusAtTime: string; +} + export interface RemsCase extends Document { case_number: string; + remsPatientId?: string; status: string; dispenseStatus: string; drugName: string; drugCode: string; + drugNdcCode?: string; patientFirstName: string; patientLastName: string; patientDOB: string; + currentPrescriberId?: string; + currentPharmacyId?: string; + prescriberHistory: string[]; + pharmacyHistory: string[]; + prescriptionEvents: PrescriptionEvent[]; medicationRequestReference?: string; originatingFhirServer?: string; metRequirements: Partial[]; @@ -48,6 +65,7 @@ const medicationCollectionSchema = new Schema({ name: { type: String }, codeSystem: { type: String }, code: { type: String }, + ndcCode: { type: String }, requirements: [ { name: { type: String }, @@ -63,6 +81,8 @@ const medicationCollectionSchema = new Schema({ }); medicationCollectionSchema.index({ name: 1 }, { unique: true }); +medicationCollectionSchema.index({ code: 1 }); +medicationCollectionSchema.index({ ndcCode: 1 }); export const medicationCollection = model( 'medicationCollection', @@ -91,13 +111,29 @@ export const metRequirementsCollection = model( const remsCaseCollectionSchema = new Schema({ case_number: { type: String }, + remsPatientId: { type: String }, status: { type: String }, dispenseStatus: { type: String }, drugName: { type: String }, patientFirstName: { type: String }, patientLastName: { type: String }, patientDOB: { type: String }, - drugCode: { type: String }, + drugCode: { type: String }, + drugNdcCode: { type: String }, + currentPrescriberId: { type: String }, + currentPharmacyId: { type: String }, + prescriberHistory: [{ type: String }], + pharmacyHistory: [{ type: String }], + prescriptionEvents: [ + { + medicationRequestReference: { type: String }, + prescriberId: { type: String }, + pharmacyId: { type: String }, + timestamp: { type: Date }, + originatingFhirServer: { type: String }, + caseStatusAtTime: { type: String } + } + ], medicationRequestReference: { type: String }, originatingFhirServer: { type: String }, metRequirements: [ @@ -111,4 +147,12 @@ const remsCaseCollectionSchema = new Schema({ ] }); +remsCaseCollectionSchema.index( + { patientFirstName: 1, patientLastName: 1, patientDOB: 1, drugNdcCode: 1 } +); + +remsCaseCollectionSchema.index( + { patientFirstName: 1, patientLastName: 1, patientDOB: 1, drugCode: 1 } +); + export const remsCaseCollection = model('RemsCaseCollection', remsCaseCollectionSchema); \ No newline at end of file diff --git a/src/fhir/utilities.ts b/src/fhir/utilities.ts index 72e66b3..eba7e77 100644 --- a/src/fhir/utilities.ts +++ b/src/fhir/utilities.ts @@ -127,6 +127,7 @@ export class FhirUtilities { name: 'Turalio', codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '2183126', + ndcCode: '65597-407-20', requirements: [ { name: 'Patient Enrollment', @@ -196,6 +197,7 @@ export class FhirUtilities { name: 'TIRF', codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '1237051', + ndcCode: '63459-502-30', requirements: [ { name: 'Patient Enrollment', @@ -262,6 +264,7 @@ export class FhirUtilities { name: 'Isotretinoin', codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '6064', + ndcCode: '0245-0571-01', requirements: [ { name: 'Patient Enrollment', @@ -305,6 +308,7 @@ export class FhirUtilities { name: 'Addyi', codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '1666386', + ndcCode: '58604-214-30', requirements: [] } ]; diff --git a/src/hooks/hookResources.ts b/src/hooks/hookResources.ts index 0787b5d..677afc0 100644 --- a/src/hooks/hookResources.ts +++ b/src/hooks/hookResources.ts @@ -24,7 +24,7 @@ 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'; +import { createNewRemsCaseFromCDSHook, handleStakeholderChangesAndRecordEvent } from '../lib/etasu'; type HandleCallback = ( res: any, @@ -406,11 +406,50 @@ export const handleCardOrder = async ( drugCode: code }); + // If case exists, check for stakeholder changes and record prescription event + if (remsCase && drug && fhirServer) { + const practitionerReference = request.requester?.reference || ''; + const pharmacistReference = pharmacy?.id ? `HealthcareService/${pharmacy.id}` : ''; + const medicationRequestReference = `${request.resourceType}/${request.id}`; + + const prescriberChanged = remsCase.currentPrescriberId !== practitionerReference; + const pharmacyChanged = + pharmacistReference && remsCase.currentPharmacyId !== pharmacistReference; + + if (prescriberChanged || pharmacyChanged) { + try { + const updatedCase = await handleStakeholderChangesAndRecordEvent( + remsCase, + drug, + practitionerReference, + pharmacistReference, + medicationRequestReference, + fhirServer + ); + console.log(`Updated case ${updatedCase?.case_number} with stakeholder changes`); + } catch (error) { + console.error('Failed to handle stakeholder changes:', error); + } + } else { + // Record prescription event even if no stakeholder change + remsCase.prescriptionEvents.push({ + medicationRequestReference: medicationRequestReference, + prescriberId: practitionerReference, + pharmacyId: pharmacistReference, + timestamp: new Date(), + originatingFhirServer: fhirServer, + caseStatusAtTime: remsCase.status + }); + remsCase.medicationRequestReference = medicationRequestReference; + await remsCase.save(); + } + } + // 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) { + if (requiresCase && fhirServer) { try { const patientReference = `Patient/${patient.id}`; const medicationRequestReference = `${request.resourceType}/${request.id}`; @@ -527,6 +566,11 @@ const getCardOrEmptyArrayFromRules = const notFound = remsCase && !metRequirement; const noEtasuToCheckAndRequiredToDispense = !remsCase && requirement.requiredToDispense; + // Only show forms that are not required to dispense (like patient status) if case is approved + if (!requirement.requiredToDispense && remsCase && remsCase.status !== 'Approved') { + return false; + } + return formNotProcessed || notFound || noEtasuToCheckAndRequiredToDispense; }; @@ -786,7 +830,7 @@ const containsMatchingMedicationRequest = const getCardOrEmptyArrayFromCases = (entries: BundleEntry[] | undefined) => - async ({ drugCode, drugName, metRequirements }: RemsCase): Promise => { + async ({ drugCode, drugName, metRequirements, status }: RemsCase): Promise => { // find the drug in the medicationCollection that matches the REMS case to get the smart links const drug = await medicationCollection .findOne({ @@ -828,6 +872,11 @@ const getCardOrEmptyArrayFromCases = const formNotProcessed = metRequirement && !metRequirement.completed; const notFound = !metRequirement; + // Only show forms that are not required to dispense (like patient status) if case is approved + if (!requirement.requiredToDispense && status !== 'Approved') { + return false; + } + return formNotProcessed || notFound; }; diff --git a/src/lib/communication.ts b/src/lib/communication.ts index 55c4c4f..3039fb6 100644 --- a/src/lib/communication.ts +++ b/src/lib/communication.ts @@ -8,7 +8,6 @@ import { Requirement } from '../fhir/models'; const logger = container.get('application'); - export async function sendCommunicationToEHR( remsCase: any, medication: any, @@ -16,7 +15,7 @@ export async function sendCommunicationToEHR( ): Promise { try { logger.info(`Creating Communication for case ${remsCase.case_number}`); - + // Create patient object from REMS case const patient: Patient = { resourceType: 'Patient', @@ -60,9 +59,10 @@ export async function sendCommunicationToEHR( // Create Tasks for each outstanding requirement const tasks: Task[] = []; for (const outstandingReq of outstandingRequirements) { - const requirement = outstandingReq.requirement || + const requirement = + outstandingReq.requirement || medication.requirements.find((r: Requirement) => r.name === outstandingReq.name); - + if (requirement && requirement.appContext) { const questionnaireUrl = requirement.appContext; const task = createQuestionnaireCompletionTask( @@ -80,7 +80,7 @@ export async function sendCommunicationToEHR( const communication: Communication = { resourceType: 'Communication', id: `comm-${uid()}`, - status: 'completed', + status: 'completed', category: [ { coding: [ @@ -92,7 +92,7 @@ export async function sendCommunicationToEHR( ] } ], - priority: 'urgent', + priority: 'urgent', subject: { reference: `Patient/${patient.id}`, display: `${remsCase.patientFirstName} ${remsCase.patientLastName}` @@ -107,7 +107,7 @@ export async function sendCommunicationToEHR( ], text: 'Outstanding REMS Requirements for Medication Dispensing' }, - sent: new Date().toISOString(), + sent: new Date().toISOString(), recipient: [ { reference: medicationRequest.requester?.reference || '' @@ -119,8 +119,9 @@ export async function sendCommunicationToEHR( }, payload: [ { - contentString: `Medication dispensing authorization DENIED for ${remsCase.drugName}.\n\n` + - `The following REMS requirements must be completed:\n\n` + + 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') + @@ -142,17 +143,24 @@ export async function sendCommunicationToEHR( }; // Determine EHR endpoint: use originatingFhirServer if available, otherwise default - const ehrEndpoint = remsCase.originatingFhirServer || - config.fhirServerConfig?.auth?.resourceServer; + let ehrEndpoint = + remsCase.originatingFhirServer || config.fhirServerConfig?.auth?.resourceServer; if (!ehrEndpoint) { logger.warn('No EHR endpoint configured, Communication not sent'); return; } + if (config.fhirServerConfig.auth.dockered_ehr_container_name) { + const originalEhrEndpoint = ehrEndpoint; + ehrEndpoint = originalEhrEndpoint.replace(/localhost/g, config.fhirServerConfig.auth.dockered_ehr_container_name) + .replace(/127\.0\.0\.1/g, config.fhirServerConfig.auth.dockered_ehr_container_name); + logger.info(`Running locally in Docker, converting EHR url from ${originalEhrEndpoint} to ${ehrEndpoint}`); + } + // 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' @@ -164,9 +172,8 @@ export async function sendCommunicationToEHR( } 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; + throw error; } -} \ No newline at end of file +} diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index 305c4cc..41d92e5 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -197,16 +197,25 @@ export const createNewRemsCaseFromCDSHook = async ( const patientDOB = patient.birthDate || ''; const case_number = uid(); + // Fetch the full medication from database to get NDC code + const fullMedication = await medicationCollection.findOne({ + code: drug?.code + }).exec(); + + const medicationData = fullMedication || drug; + // Check if case already exists const existingCase = await remsCaseCollection.findOne({ patientFirstName: patientFirstName, patientLastName: patientLastName, patientDOB: patientDOB, - drugCode: drug?.code + drugCode: medicationData?.code }); if (existingCase) { - console.log(`Case already exists for patient ${patientFirstName} ${patientLastName} and drug ${drug?.name}`); + console.log( + `Case already exists for patient ${patientFirstName} ${patientLastName} and drug ${medicationData?.name}` + ); return existingCase; } @@ -218,27 +227,48 @@ export const createNewRemsCaseFromCDSHook = async ( | 'dispenseStatus' | 'drugName' | 'drugCode' + | 'drugNdcCode' | 'patientFirstName' | 'patientLastName' | 'patientDOB' | 'medicationRequestReference' + | 'currentPrescriberId' + | 'currentPharmacyId' + | 'prescriberHistory' + | 'pharmacyHistory' + | 'prescriptionEvents' | 'metRequirements' > & { originatingFhirServer?: string } = { case_number: case_number, - status: 'Pending', // All requirements unmet, so status is Pending + status: 'Pending', dispenseStatus: 'Pending', - drugName: drug?.name, - drugCode: drug?.code, + drugName: medicationData?.name, + drugCode: medicationData?.code, + drugNdcCode: medicationData?.ndcCode, patientFirstName: patientFirstName, patientLastName: patientLastName, patientDOB: patientDOB, medicationRequestReference: medicationRequestReference, + currentPrescriberId: practitionerReference, + currentPharmacyId: pharmacistReference, + prescriberHistory: [practitionerReference], + pharmacyHistory: pharmacistReference ? [pharmacistReference] : [], + prescriptionEvents: [ + { + medicationRequestReference: medicationRequestReference, + prescriberId: practitionerReference, + pharmacyId: pharmacistReference, + timestamp: new Date(), + originatingFhirServer: originatingFhirServer, + caseStatusAtTime: 'Pending' + } + ], originatingFhirServer: originatingFhirServer, metRequirements: [] }; // Iterate through ALL requirements and create as unmet (or link to existing if already completed) - for (const requirement of drug.requirements) { + for (const requirement of medicationData.requirements) { // Only process requirements that are required to dispense if (requirement.requiredToDispense) { // Figure out which stakeholder the requirement corresponds to @@ -255,7 +285,7 @@ export const createNewRemsCaseFromCDSHook = async ( .findOne({ stakeholderId: stakeholderReference, requirementName: requirement.name, - drugName: drug?.name + drugName: medicationData?.name }) .exec(); @@ -270,7 +300,7 @@ export const createNewRemsCaseFromCDSHook = async ( completed: false, requirementName: requirement.name, requirementDescription: requirement.description, - drugName: drug?.name, + drugName: medicationData?.name, stakeholderId: stakeholderReference, case_numbers: [case_number] }; @@ -283,13 +313,161 @@ export const createNewRemsCaseFromCDSHook = async ( } // Save the new case - remsRequest.status = remsRequest.metRequirements.every(req => req.completed) ? 'Approved' : 'Pending'; + 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)`); + + console.log( + `Created new REMS case ${case_number} with all requirements unmet (or linked to existing)` + ); return newCase; }; +export const handleStakeholderChangesAndRecordEvent = async ( + remsCase: RemsCase, + drug: Medication, + practitionerReference: string, + pharmacistReference: string, + medicationRequestReference: string, + originatingFhirServer?: string +) => { + let stakeholdersChanged = false; + + // Record prescription event + const prescriptionEvent = { + medicationRequestReference: medicationRequestReference, + prescriberId: practitionerReference, + pharmacyId: pharmacistReference, + timestamp: new Date(), + originatingFhirServer: originatingFhirServer, + caseStatusAtTime: remsCase.status + }; + remsCase.prescriptionEvents.push(prescriptionEvent); + remsCase.medicationRequestReference = medicationRequestReference; + if (originatingFhirServer) { + remsCase.originatingFhirServer = originatingFhirServer; + } + + // Check if prescriber changed + if (remsCase.currentPrescriberId !== practitionerReference) { + console.log( + `Prescriber changed from ${remsCase.currentPrescriberId} to ${practitionerReference}` + ); + stakeholdersChanged = true; + + // Remove old prescriber requirements + remsCase.metRequirements = remsCase.metRequirements.filter( + req => + req.stakeholderId !== remsCase.currentPrescriberId || + !drug.requirements.some( + r => r.name === req.requirementName && r.stakeholderType === 'prescriber' + ) + ); + + // Add new prescriber requirements + const prescriberRequirements = drug.requirements.filter( + r => r.stakeholderType === 'prescriber' + ); + for (const requirement of prescriberRequirements) { + if (requirement.requiredToDispense) { + const existingMetReq = await metRequirementsCollection + .findOne({ + stakeholderId: practitionerReference, + requirementName: requirement.name, + drugName: drug?.name + }) + .exec(); + + if (existingMetReq) { + pushMetRequirements(existingMetReq, remsCase); + if (!existingMetReq.case_numbers.includes(remsCase.case_number)) { + existingMetReq.case_numbers.push(remsCase.case_number); + await existingMetReq.save(); + } + } else { + const newMetReq = { + completed: false, + requirementName: requirement.name, + requirementDescription: requirement.description, + drugName: drug?.name, + stakeholderId: practitionerReference, + case_numbers: [remsCase.case_number] + }; + await createAndPushMetRequirements(newMetReq, remsCase); + } + } + } + + // Update prescriber tracking + remsCase.currentPrescriberId = practitionerReference; + if (!remsCase.prescriberHistory.includes(practitionerReference)) { + remsCase.prescriberHistory.push(practitionerReference); + } + } + + // Check if pharmacy changed + if (pharmacistReference && remsCase.currentPharmacyId !== pharmacistReference) { + console.log(`Pharmacy changed from ${remsCase.currentPharmacyId} to ${pharmacistReference}`); + stakeholdersChanged = true; + + // Remove old pharmacy requirements + remsCase.metRequirements = remsCase.metRequirements.filter( + req => + req.stakeholderId !== remsCase.currentPharmacyId || + !drug.requirements.some( + r => r.name === req.requirementName && r.stakeholderType === 'pharmacist' + ) + ); + + // Add new pharmacy requirements + const pharmacyRequirements = drug.requirements.filter(r => r.stakeholderType === 'pharmacist'); + for (const requirement of pharmacyRequirements) { + if (requirement.requiredToDispense) { + const existingMetReq = await metRequirementsCollection + .findOne({ + stakeholderId: pharmacistReference, + requirementName: requirement.name, + drugName: drug?.name + }) + .exec(); + + if (existingMetReq) { + pushMetRequirements(existingMetReq, remsCase); + if (!existingMetReq.case_numbers.includes(remsCase.case_number)) { + existingMetReq.case_numbers.push(remsCase.case_number); + await existingMetReq.save(); + } + } else { + const newMetReq = { + completed: false, + requirementName: requirement.name, + requirementDescription: requirement.description, + drugName: drug?.name, + stakeholderId: pharmacistReference, + case_numbers: [remsCase.case_number] + }; + await createAndPushMetRequirements(newMetReq, remsCase); + } + } + } + + // Update pharmacy tracking + remsCase.currentPharmacyId = pharmacistReference; + if (!remsCase.pharmacyHistory.includes(pharmacistReference)) { + remsCase.pharmacyHistory.push(pharmacistReference); + } + } + + // Recalculate status if stakeholders changed + if (stakeholdersChanged) { + remsCase.status = remsCase.metRequirements.every(req => req.completed) ? 'Approved' : 'Pending'; + } + + await remsCase.save(); + return remsCase; +}; + const createMetRequirements = async (metReq: Partial) => { return await metRequirementsCollection.create(metReq); }; @@ -326,24 +504,49 @@ const createMetRequirementAndNewCase = async ( const patientDOB = patient.birthDate || ''; let message = ''; - // Check if case already exists + // Fetch the full medication from database to get NDC code + const fullMedication = await medicationCollection.findOne({ + code: drug?.code + }).exec(); + + const medicationData = fullMedication || drug; + + // Check if case already exists const existingCase = await remsCaseCollection.findOne({ patientFirstName: patientFirstName, patientLastName: patientLastName, patientDOB: patientDOB, - drugCode: drug?.code + drugCode: medicationData?.code }); if (existingCase) { - // Case already exists - update the existing requirement instead of creating new case - console.log(`Case ${existingCase.case_number} already exists, updating requirement ${requirement.name}`); - + // Case already exists - check for stakeholder changes before updating requirement + console.log( + `Case ${existingCase.case_number} already exists, checking for stakeholder changes` + ); + + // Check if prescriber or pharmacy changed and handle accordingly + const prescriberChanged = existingCase.currentPrescriberId !== practitionerReference; + const pharmacyChanged = + pharmacistReference && existingCase.currentPharmacyId !== pharmacistReference; + + if (prescriberChanged || pharmacyChanged) { + await handleStakeholderChangesAndRecordEvent( + existingCase, + medicationData, + practitionerReference, + pharmacistReference, + medicationRequestReference, + originatingFhirServer + ); + } + // Find and update the existing MetRequirement const matchedMetReq = await metRequirementsCollection .findOne({ stakeholderId: reqStakeholderReference, requirementName: requirement.name, - drugName: drug?.name + drugName: medicationData?.name }) .exec(); @@ -356,10 +559,13 @@ const createMetRequirementAndNewCase = async ( // Update the case's metRequirements array const metReqArray = existingCase.metRequirements || []; let foundUncompleted = false; - + for (let i = 0; i < metReqArray.length; i++) { const req = existingCase.metRequirements[i]; - if (req?.requirementName === matchedMetReq.requirementName) { + if ( + req?.requirementName === matchedMetReq.requirementName && + req?.stakeholderId === matchedMetReq.stakeholderId + ) { metReqArray[i].completed = true; req!.completed = true; await remsCaseCollection.updateOne( @@ -390,7 +596,7 @@ const createMetRequirementAndNewCase = async ( // No existing case - create new one const case_number = uid(); - + // create new rems request and add the created metReq to it let remsRequestCompletedStatus = 'Approved'; const dispenseStatusDefault = 'Pending'; @@ -401,21 +607,42 @@ const createMetRequirementAndNewCase = async ( | 'dispenseStatus' | 'drugName' | 'drugCode' + | 'drugNdcCode' | 'patientFirstName' | 'patientLastName' | 'patientDOB' | 'medicationRequestReference' + | 'currentPrescriberId' + | 'currentPharmacyId' + | 'prescriberHistory' + | 'pharmacyHistory' + | 'prescriptionEvents' | 'metRequirements' > & { originatingFhirServer?: string } = { case_number: case_number, status: remsRequestCompletedStatus, dispenseStatus: dispenseStatusDefault, - drugName: drug?.name, - drugCode: drug?.code, + drugName: medicationData?.name, + drugCode: medicationData?.code, + drugNdcCode: medicationData?.ndcCode, patientFirstName: patientFirstName, patientLastName: patientLastName, patientDOB: patientDOB, medicationRequestReference: medicationRequestReference, + currentPrescriberId: practitionerReference, + currentPharmacyId: pharmacistReference, + prescriberHistory: [practitionerReference], + pharmacyHistory: pharmacistReference ? [pharmacistReference] : [], + prescriptionEvents: [ + { + medicationRequestReference: medicationRequestReference, + prescriberId: practitionerReference, + pharmacyId: pharmacistReference, + timestamp: new Date(), + originatingFhirServer: originatingFhirServer, + caseStatusAtTime: remsRequestCompletedStatus + } + ], originatingFhirServer: originatingFhirServer, metRequirements: [] }; @@ -426,7 +653,7 @@ const createMetRequirementAndNewCase = async ( completedQuestionnaire: questionnaireResponse, requirementName: requirement.name, requirementDescription: requirement.description, - drugName: drug?.name, + drugName: medicationData?.name, stakeholderId: reqStakeholderReference, case_numbers: [case_number] }; @@ -438,7 +665,7 @@ const createMetRequirementAndNewCase = async ( } // iterate through all other requirements again to create corresponding false metRequirements / assign to existing - for (const requirement2 of drug.requirements) { + for (const requirement2 of medicationData.requirements) { // skip if the req found is the same as in the outer loop and has already been processed // && If the requirement is not the patient Status Form (when requiredToDispense == false) if (!(requirement2.resourceId === requirement.resourceId) && requirement2.requiredToDispense) { @@ -455,7 +682,7 @@ const createMetRequirementAndNewCase = async ( .findOne({ stakeholderId: reqStakeholder2Reference, requirementName: requirement2.name, - drugName: drug?.name + drugName: medicationData?.name }) .exec(); if (matchedMetReq2) { @@ -472,7 +699,7 @@ const createMetRequirementAndNewCase = async ( completed: false, requirementName: requirement2.name, requirementDescription: requirement2.description, - drugName: drug?.name, + drugName: medicationData?.name, stakeholderId: reqStakeholder2Reference, case_numbers: [case_number] }; diff --git a/src/lib/winston.ts b/src/lib/winston.ts index 975e568..411f1b1 100644 --- a/src/lib/winston.ts +++ b/src/lib/winston.ts @@ -16,26 +16,30 @@ const applicationTransports = []; const transportConsole = new transports.Console({ level: logConfig.level, format: winston.format.combine( - winston.format.prettyPrint(), - winston.format.json(), - winston.format.splat() + winston.format.timestamp(), + winston.format.json() ) }); applicationTransports.push(transportConsole); + if (logConfig.directory) { const transportDailyFile = new transports.DailyRotateFile({ filename: path.join(logConfig.directory, 'application-%DATE%.log'), datePattern: 'YYYY-MM-DD-HH', level: logging.level, zippedArchive: true, - maxSize: '20m' + maxSize: '20m', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ) }); applicationTransports.push(transportDailyFile); } + // Add a default application logger container.add('application', { - format: format.combine(format.timestamp(), format.logstash()), transports: applicationTransports }); @@ -44,4 +48,4 @@ container.add('application', { * @static * @summary Logging container for the application */ -export default container; +export default container; \ No newline at end of file diff --git a/src/ncpdp/script.ts b/src/ncpdp/script.ts index df4c210..872d7ed 100644 --- a/src/ncpdp/script.ts +++ b/src/ncpdp/script.ts @@ -1,40 +1,659 @@ -import { Router, Request } from 'express'; -import { remsCaseCollection } from '../fhir/models'; +import { Router, Request, Response } from 'express'; +import { remsCaseCollection, medicationCollection } from '../fhir/models'; +import container from '../lib/winston'; +import { Builder } from 'xml2js'; +import { sendCommunicationToEHR } from '../lib/communication'; +import { v4 as uuidv4 } from 'uuid'; + const router = Router(); +const logger = container.get('application'); -router.post('/ncpdp/script', async (req: Request) => { +router.post('/', async (req: Request, res: Response) => { try { - const requestBody = req.body; - if (requestBody.message?.body?.rxfill) { - // call to handle rxfill - handleRxFill(requestBody); + const parsedMessage = req.body; + + logger.info('=== NCPDP Request Received ==='); + + const message = parsedMessage.message; + const body = message?.body; + + const messageInfo = { + hasRemsRequest: !!body?.remsrequest, + hasRemsInitiation: !!body?.remsinitiationrequest, + hasRxFill: !!body?.rxfill, + bodyKeys: body ? Object.keys(body).join(', ') : 'no body' + }; + logger.info(`Message type check: ${JSON.stringify(messageInfo)}`); + + if (body?.remsrequest) { + logger.info('Routing to handleRemsRequest'); + await handleRemsRequest(message, res); + } else if (body?.remsinitiationrequest) { + logger.info('Routing to handleRemsInitiation'); + await handleRemsInitiation(message, res); + } else if (body?.rxfill) { + logger.info('Routing to handleRxFill'); + await handleRxFill(message, res); } else { - // do nothing for now + logger.error('Unknown NCPDP message type'); + res.type('application/xml'); + res.status(400).send(buildErrorResponse('Unknown message type')); } - } catch (error) { - console.log(error); - throw error; + } catch (error: any) { + logger.error(`ERROR processing NCPDP message: ${error.message}`); + logger.error(`Stack: ${error.stack}`); + res.type('application/xml'); + res.status(500).send(buildErrorResponse('Internal server error')); } }); -const handleRxFill = async (bundle: any) => { - const rxfill = bundle.message?.body?.rxfill; - const fillStatus = rxfill?.fillstatus?.dispensed?.note; - const patient = rxfill?.patient; - const code = rxfill?.medicationprescribed?.drugcoded?.drugdbcode?.code; +const handleRemsRequest = async (message: any, res: Response) => { + try { + logger.info('--- handleRemsRequest started ---'); + + const header = message.header; + const remsRequest = message.body.remsrequest; + const caseId = remsRequest.request?.solicitedmodel?.remscaseid; + + logger.info(`Extracted case ID: ${caseId}`); + + + if (!caseId) { + logger.error('Case ID not provided in request'); + res.type('application/xml'); + return res.status(200).send(buildDeniedResponse(header, remsRequest, 'EC', 'Case ID not provided')); + } + + logger.info(`Looking up case: ${caseId}`); + + const remsCase = await remsCaseCollection.findOne({ case_number: caseId }); + + if (!remsCase) { + logger.error(`Case not found: ${caseId}`); + res.type('application/xml'); + return res.status(200).send(buildDeniedResponse(header, remsRequest, 'EC', 'Case not found')); + } + + const caseInfo = { + case_number: remsCase.case_number, + status: remsCase.status, + drugName: remsCase.drugName, + drugNdcCode: remsCase.drugNdcCode, + numRequirements: remsCase.metRequirements?.length + }; + logger.info(`Case found: ${JSON.stringify(caseInfo)}`); + + const medication = await medicationCollection.findOne({ + ndcCode: remsCase.drugNdcCode + }); + + if (!medication) { + logger.error(`Medication configuration not found for NDC: ${remsCase.drugNdcCode}`); + res.type('application/xml'); + return res.status(200).send(buildDeniedResponse(header, remsRequest, 'ER', 'Medication configuration error')); + } + + const medInfo = { + name: medication.name, + ndcCode: medication.ndcCode, + totalRequirements: medication.requirements.length, + requiredToDispense: medication.requirements.filter((r: any) => r.requiredToDispense).length + }; + logger.info(`Medication found: ${JSON.stringify(medInfo)}`); + + // Check if all requiredToDispense requirements are met + const requiredRequirements = medication.requirements.filter((req: any) => req.requiredToDispense); + const outstandingRequirements: any[] = []; + + logger.info('Checking requirements...'); + + for (const requirement of requiredRequirements) { + const matchingMetReq = remsCase.metRequirements?.find( + (mr: any) => mr.requirementName === requirement.name + ); + + const isComplete = matchingMetReq && matchingMetReq.completed; + logger.info(` Requirement: ${requirement.name} - ${isComplete ? 'COMPLETE' : 'INCOMPLETE'}`); + + if (!matchingMetReq || !matchingMetReq.completed) { + outstandingRequirements.push({ + name: requirement.name, + stakeholder: requirement.stakeholderType, + requirement: requirement + }); + } + } + + // If all requirements met, approve + if (outstandingRequirements.length === 0) { + logger.info('All requirements met - APPROVING'); + const authNumber = `RDA${Math.floor(Math.random() * 10000000)}`; + const today = new Date(); + const expirationDate = new Date(today); + expirationDate.setDate(expirationDate.getDate() + 7); + + const authDetails = { + authNumber, + effectiveDate: today.toISOString().split('T')[0], + expirationDate: expirationDate.toISOString().split('T')[0] + }; + logger.info(`Authorization details: ${JSON.stringify(authDetails)}`); + + res.type('application/xml'); + return res.status(200).send(buildApprovedResponse( + header, + remsRequest, + caseId, + authNumber, + today.toISOString().split('T')[0], + expirationDate.toISOString().split('T')[0] + )); + } + + // Requirements not met - denial with reason code + const reasonCode = determineReasonCode(outstandingRequirements); + const reasonText = buildReasonText(reasonCode); + + const denialDetails = { + reasonCode: reasonCode, + reasonText, + outstandingCount: outstandingRequirements.length + }; + logger.info(`Denial details: ${JSON.stringify(denialDetails)}`); + + // Send Communication to EHR with outstanding requirements + logger.info('Attempting to send Communication to EHR...'); + try { + await sendCommunicationToEHR(remsCase, medication, outstandingRequirements); + const ehrEndpoint = remsCase.originatingFhirServer || 'default server'; + logger.info(`Communication sent successfully to: ${ehrEndpoint}`); + } catch (commError: any) { + logger.error(`Failed to send Communication: ${commError.message}`); + } + + logger.info('Sending DENIED response'); + res.type('application/xml'); + return res.status(200).send(buildDeniedResponse(header, remsRequest, reasonCode, reasonText)); + + } catch (error: any) { + logger.error(`ERROR in handleRemsRequest: ${error.message}`); + logger.error(`Stack trace: ${error.stack}`); + res.type('application/xml'); + return res.status(500).send(buildErrorResponse(error.message)); + } +}; + + +const handleRemsInitiation = async (message: any, res: Response) => { + try { + logger.info('--- handleRemsInitiation started ---'); + const header = message.header; + const initRequest = message.body.remsinitiationrequest; + const patient = initRequest.patient?.humanpatient; + const prescriber = initRequest.prescriber?.nonveterinarian; + const pharmacy = initRequest.pharmacy; + const drugNdcCode = initRequest.medicationprescribed?.product?.drugcoded?.ndc; + + const requestInfo = { + patientName: `${patient?.names?.name?.firstname} ${patient?.names?.name?.lastname}`, + drugNdcCode: drugNdcCode + }; + logger.info(`REMS Initiation request: ${JSON.stringify(requestInfo)}`); + + const remsCase = await remsCaseCollection.findOne({ + patientFirstName: patient?.names?.name?.firstname, + patientLastName: patient?.names?.name?.lastname, + patientDOB: patient?.dateofbirth?.date, + drugNdcCode: drugNdcCode + }); + + if (!remsCase) { + logger.info('No case exists - patient must enroll'); + res.type('application/xml'); + return res.status(200).send(buildInitiationClosedResponse( + header, + initRequest, + 'EM', + 'Patient must enroll/certify' + )); + } + + // Case exists - check requirements + const medication = await medicationCollection.findOne({ + ndcCode: drugNdcCode + }); + + if (!medication) { + logger.error(`Medication not found for NDC: ${drugNdcCode}`); + res.type('application/xml'); + return res.status(200).send(buildInitiationClosedResponse( + header, + initRequest, + 'ER', + 'Medication configuration error' + )); + } + + // Check for outstanding requirements + const requiredRequirements = medication.requirements.filter(req => req.requiredToDispense); + const outstandingRequirements: any[] = []; + + for (const requirement of requiredRequirements) { + const matchingMetReq = remsCase.metRequirements?.find( + mr => mr.requirementName === requirement.name + ); + + if (!matchingMetReq || !matchingMetReq.completed) { + outstandingRequirements.push({ + name: requirement.name, + stakeholder: requirement.stakeholderType + }); + } + } + + if (outstandingRequirements.length > 0) { + const reasonCode = determineReasonCode(outstandingRequirements); + const reasonText = buildReasonText(reasonCode); + + logger.info(`Requirements not met - closing with: ${reasonCode}`); + res.type('application/xml'); + return res.status(200).send(buildInitiationClosedResponse( + header, + initRequest, + reasonCode, + reasonText + )); + } + + // All requirements met - return success with patient ID + logger.info('All requirements met - returning success'); + res.type('application/xml'); + return res.status(200).send(buildInitiationSuccessResponse(header, initRequest, remsCase)); + + } catch (error: any) { + logger.error(`ERROR in handleRemsInitiation: ${error.message}`); + res.type('application/xml'); + return res.status(500).send(buildErrorResponse(error.message)); + } +}; + + +const handleRxFill = async (message: any, res: Response) => { + try { + logger.info('--- handleRxFill started ---'); + const header = message.header; + const rxFill = message.body.rxfill; + const patient = rxFill.patient?.humanpatient; + + const medicationDispensed = rxFill.medicationdispensed; + + if (!medicationDispensed) { + logger.error('MedicationDispensed not found in RxFill message'); + logger.error(`Available RxFill fields: ${JSON.stringify(Object.keys(rxFill))}`); + } + + let drugNdcCode = medicationDispensed?.drugcoded?.productcode?.code + + const patientInfo = { + firstName: patient?.name?.firstname || patient?.names?.name?.firstname, + lastName: patient?.name?.lastname || patient?.names?.name?.lastname, + dob: patient?.dateofbirth?.date, + ndc: drugNdcCode, + }; + + logger.info(`RxFill received for: ${JSON.stringify(patientInfo)}`); + + // Try to find case - if NDC not available, try by patient + drug description + let remsCase = null; + + if (drugNdcCode) { + remsCase = await remsCaseCollection.findOne({ + patientFirstName: patientInfo.firstName, + patientLastName: patientInfo.lastName, + patientDOB: patientInfo.dob, + drugNdcCode: drugNdcCode + }); + } + + if (remsCase) { + remsCase.dispenseStatus = 'Dispensed'; + await remsCase.save(); + + logger.info(`Updated case ${remsCase.case_number} dispense status to 'Dispensed'`); + logger.info(` Patient: ${remsCase.patientFirstName} ${remsCase.patientLastName}`); + logger.info(` Drug: ${remsCase.drugName} (NDC: ${remsCase.drugNdcCode})`); + logger.info(` Case Status: ${remsCase.status}`); + } else { + logger.warn(`Case not found for RxFill notification`); + logger.warn(` Searched for: ${JSON.stringify(patientInfo)}`); + } + + // Return success status per NCPDP + res.type('application/xml'); + return res.status(200).send(buildRxFillResponse(header, rxFill)); + } catch (error: any) { + logger.error(`ERROR in handleRxFill: ${error.message}`); + logger.error(`Stack trace: ${error.stack}`); + res.type('application/xml'); + return res.status(500).send(buildErrorResponse(error.message)); + } +}; + + +const determineReasonCode = (outstandingRequirements: any[]): string => { + let hasPatientReq = false; + let hasPrescriberReq = false; + let hasPharmacyReq = false; + + for (const req of outstandingRequirements) { + const stakeholder = req.stakeholder?.toLowerCase(); + if (stakeholder === 'patient') { + hasPatientReq = true; + } else if (stakeholder === 'prescriber') { + hasPrescriberReq = true; + } else if (stakeholder === 'pharmacist' || stakeholder === 'pharmacy') { + hasPharmacyReq = true; + } + } + + // Return only the highest priority requirement + if (hasPatientReq) { + return 'EM'; + } else if (hasPrescriberReq) { + return 'ES'; + } else if (hasPharmacyReq) { + return 'EO'; + } + + // Fallback - should not reach here + return 'EC'; +}; + + +const buildReasonText = (reasonCode: string): string => { + const reasonCodeNotes: { [key: string]: string } = { + 'EM': 'Patient enrollment/certification required', + 'ES': 'Prescriber enrollment/certification required', + 'EO': 'Pharmacy enrollment/certification required', + 'EC': 'Case information incomplete or invalid', + 'ER': 'REMS program error', + 'EX': 'Prescriber deactivated/decertified', + 'EY': 'Pharmacy deactivated/decertified', + 'EZ': 'Patient deactivated/decertified' + }; + + return reasonCodeNotes[reasonCode] || 'REMS requirement not met'; +}; + + +const buildApprovedResponse = ( + header: any, + request: any, + caseId: string, + authNumber: string, + effectiveDate: string, + expirationDate: string +): string => { + const builder = new Builder({ headless: false }); + + const patient = request.patient; + const pharmacy = request.pharmacy; + const prescriber = request.prescriber; + const medicationPrescribed = request.medicationprescribed; + const remsReferenceID = request.remsreferenceid; + + const response = { + Message: { + $: { + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + DatatypesVersion: 'V2024071', + TransportVersion: 'V2024071', + TransactionDomain: 'SCRIPT', + TransactionVersion: 'V2024071', + StructuresVersion: 'V2024071', + ECLVersion: 'V2024071' + }, + Header: header, + Body: { + REMSResponse: { + REMSReferenceID: remsReferenceID, + Patient: patient, + Pharmacy: pharmacy, + Prescriber: prescriber, + MedicationPrescribed: medicationPrescribed, + Response: { + ResponseStatus: { + Approved: { + REMSCaseID: caseId, + REMSAuthorizationNumber: authNumber, + AuthorizationPeriod: { + EffectiveDate: { Date: effectiveDate }, + ExpirationDate: { Date: expirationDate } + } + } + } + } + } + } + } + }; + + return builder.buildObject(response); +}; + - await remsCaseCollection.findOneAndUpdate( - { - patientFirstName: patient?.humanpatient?.name?.firstname, - patientLastName: patient?.humanpatient?.name?.lastname, - patientDOB: patient?.humanpatient?.dateofbirth?.date, - drugCode: code - }, - { dispenseStatus: fillStatus }, - { new: true } - ); - return fillStatus; +const buildDeniedResponse = ( + header: any, + request: any, + reasonCode: string, + note: string +): string => { + const builder = new Builder({ headless: false }); + + const patient = request.patient; + const pharmacy = request.pharmacy; + const prescriber = request.prescriber; + const medicationPrescribed = request.medicationprescribed; + const remsReferenceID = request.remsreferenceid; + const solicitedModel = request.request?.solicitedmodel; + const caseId = solicitedModel?.remscaseid; + + const response = { + Message: { + $: { + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + DatatypesVersion: 'V2024071', + TransportVersion: 'V2024071', + TransactionDomain: 'SCRIPT', + TransactionVersion: 'V2024071', + StructuresVersion: 'V2024071', + ECLVersion: 'V2024071' + }, + Header: header, + Body: { + REMSResponse: { + REMSReferenceID: remsReferenceID, + Patient: patient, + Pharmacy: pharmacy, + Prescriber: prescriber, + MedicationPrescribed: medicationPrescribed, + Response: { + ResponseStatus: { + Denied: { + REMSCaseID: caseId, + DeniedReasonCode: reasonCode, + REMSNote: note + } + } + } + } + } + } + }; + + return builder.buildObject(response); +}; + + +const buildInitiationClosedResponse = ( + header: any, + request: any, + reasonCode: string, + note: string +): string => { + const builder = new Builder({ headless: false }); + + const patient = request.patient; + const pharmacy = request.pharmacy; + const prescriber = request.prescriber; + const medicationPrescribed = request.medicationprescribed; + const remsReferenceID = request.remsreferenceid; + + const response = { + Message: { + $: { + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + DatatypesVersion: 'V2024071', + TransportVersion: 'V2024071', + TransactionDomain: 'SCRIPT', + TransactionVersion: 'V2024071', + StructuresVersion: 'V2024071', + ECLVersion: 'V2024071' + }, + Header: header, + Body: { + REMSInitiationResponse: { + REMSReferenceID: remsReferenceID, + Patient: patient, + Pharmacy: pharmacy, + Prescriber: prescriber, + MedicationPrescribed: medicationPrescribed, + Response: { + ResponseStatus: { + Closed: { + ReasonCode: reasonCode, + REMSNote: note + } + } + } + } + } + } + }; + + return builder.buildObject(response); +}; + + +const buildInitiationSuccessResponse = (header: any, request: any, remsCase: any): string => { + const builder = new Builder({ headless: false }); + + const patient = request.patient; + const humanPatient = patient?.humanpatient; + const pharmacy = request.pharmacy; + const prescriber = request.prescriber; + const medicationPrescribed = request.medicationprescribed; + const remsReferenceID = request.remsreferenceid; + + const response = { + Message: { + $: { + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + DatatypesVersion: 'V2024071', + TransportVersion: 'V2024071', + TransactionDomain: 'SCRIPT', + TransactionVersion: 'V2024071', + StructuresVersion: 'V2024071', + ECLVersion: 'V2024071' + }, + Header: header, + Body: { + REMSInitiationResponse: { + REMSReferenceID: remsReferenceID, + Patient: { + HumanPatient: { + $: { + 'xsi:type': 'PatientMandatoryAddress' + }, + Identification: { + REMSPatientID: remsCase.remsPatientId || remsCase.case_number + }, + Names: humanPatient?.names, + GenderAndSex: humanPatient?.genderandsex, + DateOfBirth: humanPatient?.dateofbirth, + Address: { + $: { + 'xsi:type': 'MandatoryAddressType' + }, + ...humanPatient?.address + } + } + }, + Pharmacy: pharmacy, + Prescriber: prescriber, + MedicationPrescribed: medicationPrescribed + } + } + } + }; + + return builder.buildObject(response); +}; + + +const buildRxFillResponse = (header: any, rxFill: any): string => { + const builder = new Builder({ headless: false }); + + const response = { + Message: { + $: { + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + DatatypesVersion: 'V2024071', + TransportVersion: 'V2024071', + TransactionDomain: 'SCRIPT', + TransactionVersion: 'V2024071', + StructuresVersion: 'V2024071', + ECLVersion: 'V2024071' + }, + Header: header, + Body: { + Status: { + Code: '000', + Description: 'Dispense notification received and processed' + } + } + } + }; + + return builder.buildObject(response); +}; + + +const buildErrorResponse = (errorMessage: string): string => { + const builder = new Builder({ headless: false }); + + const response = { + Message: { + $: { + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + DatatypesVersion: 'V2024071', + TransportVersion: 'V2024071', + TransactionDomain: 'SCRIPT', + TransactionVersion: 'V2024071', + StructuresVersion: 'V2024071', + ECLVersion: 'V2024071' + }, + Body: { + Error: { + Code: 'ER', + Description: errorMessage + } + } + } + }; + + return builder.buildObject(response); }; -export default router; +export default router; \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index f2970f2..d75e5c6 100644 --- a/src/server.ts +++ b/src/server.ts @@ -133,7 +133,7 @@ class REMSServer extends Server { } }) ); - this.app.use('/ncpdp', Ncpdp); + this.app.use('/ncpdp/script', Ncpdp); return this; } @@ -163,4 +163,4 @@ class REMSServer extends Server { // Start the application -export { REMSServer, initialize }; \ No newline at end of file +export { REMSServer, initialize }; From f55517b4bb8539423e3923666ba0ae62e9b99a1b Mon Sep 17 00:00:00 2001 From: Patrick LaRocque Date: Wed, 4 Feb 2026 19:33:19 -0500 Subject: [PATCH 9/9] fix prettier and lint issues --- src/fhir/models.ts | 26 ++-- src/hooks/hookResources.ts | 2 +- src/lib/communication.ts | 9 +- src/lib/etasu.ts | 22 ++-- src/lib/winston.ts | 14 +-- src/ncpdp/script.ts | 238 ++++++++++++++++++------------------- 6 files changed, 154 insertions(+), 157 deletions(-) diff --git a/src/fhir/models.ts b/src/fhir/models.ts index d997b57..218052e 100644 --- a/src/fhir/models.ts +++ b/src/fhir/models.ts @@ -15,8 +15,8 @@ export interface Requirement { export interface Medication extends Document { name: string; codeSystem: string; - code: string; // RxNorm code (used for CDS Hooks) - ndcCode: string; // NDC code (used for NCPDP SCRIPT) + code: string; // RxNorm code (used for CDS Hooks) + ndcCode: string; // NDC code (used for NCPDP SCRIPT) requirements: Requirement[]; } @@ -118,7 +118,7 @@ const remsCaseCollectionSchema = new Schema({ patientFirstName: { type: String }, patientLastName: { type: String }, patientDOB: { type: String }, - drugCode: { type: String }, + drugCode: { type: String }, drugNdcCode: { type: String }, currentPrescriberId: { type: String }, currentPharmacyId: { type: String }, @@ -147,12 +147,18 @@ const remsCaseCollectionSchema = new Schema({ ] }); -remsCaseCollectionSchema.index( - { patientFirstName: 1, patientLastName: 1, patientDOB: 1, drugNdcCode: 1 } -); +remsCaseCollectionSchema.index({ + patientFirstName: 1, + patientLastName: 1, + patientDOB: 1, + drugNdcCode: 1 +}); -remsCaseCollectionSchema.index( - { patientFirstName: 1, patientLastName: 1, patientDOB: 1, drugCode: 1 } -); +remsCaseCollectionSchema.index({ + patientFirstName: 1, + patientLastName: 1, + patientDOB: 1, + drugCode: 1 +}); -export const remsCaseCollection = model('RemsCaseCollection', remsCaseCollectionSchema); \ No newline at end of file +export const remsCaseCollection = model('RemsCaseCollection', remsCaseCollectionSchema); diff --git a/src/hooks/hookResources.ts b/src/hooks/hookResources.ts index 677afc0..23269ad 100644 --- a/src/hooks/hookResources.ts +++ b/src/hooks/hookResources.ts @@ -1046,4 +1046,4 @@ export function createQuestionnaireCompletionTask( ] }; return taskResource; -} \ No newline at end of file +} diff --git a/src/lib/communication.ts b/src/lib/communication.ts index 3039fb6..2f7a62c 100644 --- a/src/lib/communication.ts +++ b/src/lib/communication.ts @@ -153,9 +153,12 @@ export async function sendCommunicationToEHR( if (config.fhirServerConfig.auth.dockered_ehr_container_name) { const originalEhrEndpoint = ehrEndpoint; - ehrEndpoint = originalEhrEndpoint.replace(/localhost/g, config.fhirServerConfig.auth.dockered_ehr_container_name) - .replace(/127\.0\.0\.1/g, config.fhirServerConfig.auth.dockered_ehr_container_name); - logger.info(`Running locally in Docker, converting EHR url from ${originalEhrEndpoint} to ${ehrEndpoint}`); + ehrEndpoint = originalEhrEndpoint + .replace(/localhost/g, config.fhirServerConfig.auth.dockered_ehr_container_name) + .replace(/127\.0\.0\.1/g, config.fhirServerConfig.auth.dockered_ehr_container_name); + logger.info( + `Running locally in Docker, converting EHR url from ${originalEhrEndpoint} to ${ehrEndpoint}` + ); } // Send Communication to EHR diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index 41d92e5..341d91e 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -198,9 +198,11 @@ export const createNewRemsCaseFromCDSHook = async ( const case_number = uid(); // Fetch the full medication from database to get NDC code - const fullMedication = await medicationCollection.findOne({ - code: drug?.code - }).exec(); + const fullMedication = await medicationCollection + .findOne({ + code: drug?.code + }) + .exec(); const medicationData = fullMedication || drug; @@ -505,9 +507,11 @@ const createMetRequirementAndNewCase = async ( let message = ''; // Fetch the full medication from database to get NDC code - const fullMedication = await medicationCollection.findOne({ - code: drug?.code - }).exec(); + const fullMedication = await medicationCollection + .findOne({ + code: drug?.code + }) + .exec(); const medicationData = fullMedication || drug; @@ -567,7 +571,7 @@ const createMetRequirementAndNewCase = async ( req?.stakeholderId === matchedMetReq.stakeholderId ) { metReqArray[i].completed = true; - req!.completed = true; + req.completed = true; await remsCaseCollection.updateOne( { _id: existingCase._id }, { $set: { metRequirements: metReqArray } } @@ -769,7 +773,7 @@ const createMetRequirementAndUpdateCase = async ( // _id comparison would not work for some reason if (req4?.requirementName === matchedMetReq.requirementName) { metReqArray[i].completed = true; - req4!.completed = true; + req4.completed = true; await remsCaseCollection.updateOne( { _id: remsRequestToUpdate?._id }, { $set: { metRequirements: metReqArray } } @@ -1009,4 +1013,4 @@ export const processQuestionnaireResponseSubmission = async (requestBody: Bundle export { getResource, getQuestionnaireResponse }; -export default router; \ No newline at end of file +export default router; diff --git a/src/lib/winston.ts b/src/lib/winston.ts index 411f1b1..6a2b3fd 100644 --- a/src/lib/winston.ts +++ b/src/lib/winston.ts @@ -1,4 +1,4 @@ -import winston, { Container, transports, format } from 'winston'; +import winston, { Container, transports } from 'winston'; import config from '../config'; import 'winston-daily-rotate-file'; import path from 'path'; @@ -15,10 +15,7 @@ const applicationTransports = []; // Create a console transport const transportConsole = new transports.Console({ level: logConfig.level, - format: winston.format.combine( - winston.format.timestamp(), - winston.format.json() - ) + format: winston.format.combine(winston.format.timestamp(), winston.format.json()) }); applicationTransports.push(transportConsole); @@ -30,10 +27,7 @@ if (logConfig.directory) { level: logging.level, zippedArchive: true, maxSize: '20m', - format: winston.format.combine( - winston.format.timestamp(), - winston.format.json() - ) + format: winston.format.combine(winston.format.timestamp(), winston.format.json()) }); applicationTransports.push(transportDailyFile); } @@ -48,4 +42,4 @@ container.add('application', { * @static * @summary Logging container for the application */ -export default container; \ No newline at end of file +export default container; diff --git a/src/ncpdp/script.ts b/src/ncpdp/script.ts index 872d7ed..7bfbdad 100644 --- a/src/ncpdp/script.ts +++ b/src/ncpdp/script.ts @@ -3,7 +3,6 @@ import { remsCaseCollection, medicationCollection } from '../fhir/models'; import container from '../lib/winston'; import { Builder } from 'xml2js'; import { sendCommunicationToEHR } from '../lib/communication'; -import { v4 as uuidv4 } from 'uuid'; const router = Router(); const logger = container.get('application'); @@ -11,12 +10,12 @@ const logger = container.get('application'); router.post('/', async (req: Request, res: Response) => { try { const parsedMessage = req.body; - + logger.info('=== NCPDP Request Received ==='); - + const message = parsedMessage.message; const body = message?.body; - + const messageInfo = { hasRemsRequest: !!body?.remsrequest, hasRemsInitiation: !!body?.remsinitiationrequest, @@ -24,7 +23,7 @@ router.post('/', async (req: Request, res: Response) => { bodyKeys: body ? Object.keys(body).join(', ') : 'no body' }; logger.info(`Message type check: ${JSON.stringify(messageInfo)}`); - + if (body?.remsrequest) { logger.info('Routing to handleRemsRequest'); await handleRemsRequest(message, res); @@ -47,34 +46,34 @@ router.post('/', async (req: Request, res: Response) => { } }); - const handleRemsRequest = async (message: any, res: Response) => { try { logger.info('--- handleRemsRequest started ---'); - + const header = message.header; const remsRequest = message.body.remsrequest; const caseId = remsRequest.request?.solicitedmodel?.remscaseid; logger.info(`Extracted case ID: ${caseId}`); - if (!caseId) { logger.error('Case ID not provided in request'); res.type('application/xml'); - return res.status(200).send(buildDeniedResponse(header, remsRequest, 'EC', 'Case ID not provided')); + return res + .status(200) + .send(buildDeniedResponse(header, remsRequest, 'EC', 'Case ID not provided')); } - + logger.info(`Looking up case: ${caseId}`); - + const remsCase = await remsCaseCollection.findOne({ case_number: caseId }); - + if (!remsCase) { logger.error(`Case not found: ${caseId}`); res.type('application/xml'); return res.status(200).send(buildDeniedResponse(header, remsRequest, 'EC', 'Case not found')); } - + const caseInfo = { case_number: remsCase.case_number, status: remsCase.status, @@ -83,17 +82,19 @@ const handleRemsRequest = async (message: any, res: Response) => { numRequirements: remsCase.metRequirements?.length }; logger.info(`Case found: ${JSON.stringify(caseInfo)}`); - + const medication = await medicationCollection.findOne({ ndcCode: remsCase.drugNdcCode }); - + if (!medication) { logger.error(`Medication configuration not found for NDC: ${remsCase.drugNdcCode}`); res.type('application/xml'); - return res.status(200).send(buildDeniedResponse(header, remsRequest, 'ER', 'Medication configuration error')); + return res + .status(200) + .send(buildDeniedResponse(header, remsRequest, 'ER', 'Medication configuration error')); } - + const medInfo = { name: medication.name, ndcCode: medication.ndcCode, @@ -101,30 +102,32 @@ const handleRemsRequest = async (message: any, res: Response) => { requiredToDispense: medication.requirements.filter((r: any) => r.requiredToDispense).length }; logger.info(`Medication found: ${JSON.stringify(medInfo)}`); - + // Check if all requiredToDispense requirements are met - const requiredRequirements = medication.requirements.filter((req: any) => req.requiredToDispense); + const requiredRequirements = medication.requirements.filter( + (req: any) => req.requiredToDispense + ); const outstandingRequirements: any[] = []; - + logger.info('Checking requirements...'); - + for (const requirement of requiredRequirements) { const matchingMetReq = remsCase.metRequirements?.find( (mr: any) => mr.requirementName === requirement.name ); - + const isComplete = matchingMetReq && matchingMetReq.completed; logger.info(` Requirement: ${requirement.name} - ${isComplete ? 'COMPLETE' : 'INCOMPLETE'}`); - + if (!matchingMetReq || !matchingMetReq.completed) { outstandingRequirements.push({ name: requirement.name, stakeholder: requirement.stakeholderType, - requirement: requirement + requirement: requirement }); } } - + // If all requirements met, approve if (outstandingRequirements.length === 0) { logger.info('All requirements met - APPROVING'); @@ -132,36 +135,40 @@ const handleRemsRequest = async (message: any, res: Response) => { const today = new Date(); const expirationDate = new Date(today); expirationDate.setDate(expirationDate.getDate() + 7); - + const authDetails = { authNumber, effectiveDate: today.toISOString().split('T')[0], expirationDate: expirationDate.toISOString().split('T')[0] }; logger.info(`Authorization details: ${JSON.stringify(authDetails)}`); - + res.type('application/xml'); - return res.status(200).send(buildApprovedResponse( - header, - remsRequest, - caseId, - authNumber, - today.toISOString().split('T')[0], - expirationDate.toISOString().split('T')[0] - )); + return res + .status(200) + .send( + buildApprovedResponse( + header, + remsRequest, + caseId, + authNumber, + today.toISOString().split('T')[0], + expirationDate.toISOString().split('T')[0] + ) + ); } - + // Requirements not met - denial with reason code const reasonCode = determineReasonCode(outstandingRequirements); const reasonText = buildReasonText(reasonCode); - + const denialDetails = { reasonCode: reasonCode, reasonText, outstandingCount: outstandingRequirements.length }; logger.info(`Denial details: ${JSON.stringify(denialDetails)}`); - + // Send Communication to EHR with outstanding requirements logger.info('Attempting to send Communication to EHR...'); try { @@ -171,11 +178,10 @@ const handleRemsRequest = async (message: any, res: Response) => { } catch (commError: any) { logger.error(`Failed to send Communication: ${commError.message}`); } - + logger.info('Sending DENIED response'); res.type('application/xml'); return res.status(200).send(buildDeniedResponse(header, remsRequest, reasonCode, reasonText)); - } catch (error: any) { logger.error(`ERROR in handleRemsRequest: ${error.message}`); logger.error(`Stack trace: ${error.stack}`); @@ -184,66 +190,63 @@ const handleRemsRequest = async (message: any, res: Response) => { } }; - const handleRemsInitiation = async (message: any, res: Response) => { try { logger.info('--- handleRemsInitiation started ---'); const header = message.header; const initRequest = message.body.remsinitiationrequest; const patient = initRequest.patient?.humanpatient; - const prescriber = initRequest.prescriber?.nonveterinarian; - const pharmacy = initRequest.pharmacy; + //const prescriber = initRequest.prescriber?.nonveterinarian; + //const pharmacy = initRequest.pharmacy; const drugNdcCode = initRequest.medicationprescribed?.product?.drugcoded?.ndc; - + const requestInfo = { patientName: `${patient?.names?.name?.firstname} ${patient?.names?.name?.lastname}`, drugNdcCode: drugNdcCode }; logger.info(`REMS Initiation request: ${JSON.stringify(requestInfo)}`); - + const remsCase = await remsCaseCollection.findOne({ patientFirstName: patient?.names?.name?.firstname, patientLastName: patient?.names?.name?.lastname, patientDOB: patient?.dateofbirth?.date, drugNdcCode: drugNdcCode }); - + if (!remsCase) { logger.info('No case exists - patient must enroll'); res.type('application/xml'); - return res.status(200).send(buildInitiationClosedResponse( - header, - initRequest, - 'EM', - 'Patient must enroll/certify' - )); + return res + .status(200) + .send( + buildInitiationClosedResponse(header, initRequest, 'EM', 'Patient must enroll/certify') + ); } - + // Case exists - check requirements - const medication = await medicationCollection.findOne({ + const medication = await medicationCollection.findOne({ ndcCode: drugNdcCode }); - + if (!medication) { logger.error(`Medication not found for NDC: ${drugNdcCode}`); res.type('application/xml'); - return res.status(200).send(buildInitiationClosedResponse( - header, - initRequest, - 'ER', - 'Medication configuration error' - )); + return res + .status(200) + .send( + buildInitiationClosedResponse(header, initRequest, 'ER', 'Medication configuration error') + ); } - + // Check for outstanding requirements const requiredRequirements = medication.requirements.filter(req => req.requiredToDispense); const outstandingRequirements: any[] = []; - + for (const requirement of requiredRequirements) { const matchingMetReq = remsCase.metRequirements?.find( mr => mr.requirementName === requirement.name ); - + if (!matchingMetReq || !matchingMetReq.completed) { outstandingRequirements.push({ name: requirement.name, @@ -251,26 +254,22 @@ const handleRemsInitiation = async (message: any, res: Response) => { }); } } - + if (outstandingRequirements.length > 0) { const reasonCode = determineReasonCode(outstandingRequirements); const reasonText = buildReasonText(reasonCode); - + logger.info(`Requirements not met - closing with: ${reasonCode}`); res.type('application/xml'); - return res.status(200).send(buildInitiationClosedResponse( - header, - initRequest, - reasonCode, - reasonText - )); + return res + .status(200) + .send(buildInitiationClosedResponse(header, initRequest, reasonCode, reasonText)); } - + // All requirements met - return success with patient ID logger.info('All requirements met - returning success'); res.type('application/xml'); return res.status(200).send(buildInitiationSuccessResponse(header, initRequest, remsCase)); - } catch (error: any) { logger.error(`ERROR in handleRemsInitiation: ${error.message}`); res.type('application/xml'); @@ -278,7 +277,6 @@ const handleRemsInitiation = async (message: any, res: Response) => { } }; - const handleRxFill = async (message: any, res: Response) => { try { logger.info('--- handleRxFill started ---'); @@ -287,26 +285,26 @@ const handleRxFill = async (message: any, res: Response) => { const patient = rxFill.patient?.humanpatient; const medicationDispensed = rxFill.medicationdispensed; - + if (!medicationDispensed) { logger.error('MedicationDispensed not found in RxFill message'); logger.error(`Available RxFill fields: ${JSON.stringify(Object.keys(rxFill))}`); } - - let drugNdcCode = medicationDispensed?.drugcoded?.productcode?.code - + + const drugNdcCode = medicationDispensed?.drugcoded?.productcode?.code; + const patientInfo = { firstName: patient?.name?.firstname || patient?.names?.name?.firstname, lastName: patient?.name?.lastname || patient?.names?.name?.lastname, dob: patient?.dateofbirth?.date, - ndc: drugNdcCode, + ndc: drugNdcCode }; - + logger.info(`RxFill received for: ${JSON.stringify(patientInfo)}`); - + // Try to find case - if NDC not available, try by patient + drug description let remsCase = null; - + if (drugNdcCode) { remsCase = await remsCaseCollection.findOne({ patientFirstName: patientInfo.firstName, @@ -315,20 +313,20 @@ const handleRxFill = async (message: any, res: Response) => { drugNdcCode: drugNdcCode }); } - + if (remsCase) { remsCase.dispenseStatus = 'Dispensed'; await remsCase.save(); - + logger.info(`Updated case ${remsCase.case_number} dispense status to 'Dispensed'`); logger.info(` Patient: ${remsCase.patientFirstName} ${remsCase.patientLastName}`); logger.info(` Drug: ${remsCase.drugName} (NDC: ${remsCase.drugNdcCode})`); logger.info(` Case Status: ${remsCase.status}`); } else { - logger.warn(`Case not found for RxFill notification`); + logger.warn('Case not found for RxFill notification'); logger.warn(` Searched for: ${JSON.stringify(patientInfo)}`); } - + // Return success status per NCPDP res.type('application/xml'); return res.status(200).send(buildRxFillResponse(header, rxFill)); @@ -340,12 +338,11 @@ const handleRxFill = async (message: any, res: Response) => { } }; - const determineReasonCode = (outstandingRequirements: any[]): string => { let hasPatientReq = false; let hasPrescriberReq = false; let hasPharmacyReq = false; - + for (const req of outstandingRequirements) { const stakeholder = req.stakeholder?.toLowerCase(); if (stakeholder === 'patient') { @@ -356,7 +353,7 @@ const determineReasonCode = (outstandingRequirements: any[]): string => { hasPharmacyReq = true; } } - + // Return only the highest priority requirement if (hasPatientReq) { return 'EM'; @@ -365,28 +362,26 @@ const determineReasonCode = (outstandingRequirements: any[]): string => { } else if (hasPharmacyReq) { return 'EO'; } - + // Fallback - should not reach here return 'EC'; }; - const buildReasonText = (reasonCode: string): string => { const reasonCodeNotes: { [key: string]: string } = { - 'EM': 'Patient enrollment/certification required', - 'ES': 'Prescriber enrollment/certification required', - 'EO': 'Pharmacy enrollment/certification required', - 'EC': 'Case information incomplete or invalid', - 'ER': 'REMS program error', - 'EX': 'Prescriber deactivated/decertified', - 'EY': 'Pharmacy deactivated/decertified', - 'EZ': 'Patient deactivated/decertified' + EM: 'Patient enrollment/certification required', + ES: 'Prescriber enrollment/certification required', + EO: 'Pharmacy enrollment/certification required', + EC: 'Case information incomplete or invalid', + ER: 'REMS program error', + EX: 'Prescriber deactivated/decertified', + EY: 'Pharmacy deactivated/decertified', + EZ: 'Patient deactivated/decertified' }; - + return reasonCodeNotes[reasonCode] || 'REMS requirement not met'; }; - const buildApprovedResponse = ( header: any, request: any, @@ -396,13 +391,13 @@ const buildApprovedResponse = ( expirationDate: string ): string => { const builder = new Builder({ headless: false }); - + const patient = request.patient; const pharmacy = request.pharmacy; const prescriber = request.prescriber; const medicationPrescribed = request.medicationprescribed; const remsReferenceID = request.remsreferenceid; - + const response = { Message: { $: { @@ -438,11 +433,10 @@ const buildApprovedResponse = ( } } }; - + return builder.buildObject(response); }; - const buildDeniedResponse = ( header: any, request: any, @@ -450,7 +444,7 @@ const buildDeniedResponse = ( note: string ): string => { const builder = new Builder({ headless: false }); - + const patient = request.patient; const pharmacy = request.pharmacy; const prescriber = request.prescriber; @@ -458,7 +452,7 @@ const buildDeniedResponse = ( const remsReferenceID = request.remsreferenceid; const solicitedModel = request.request?.solicitedmodel; const caseId = solicitedModel?.remscaseid; - + const response = { Message: { $: { @@ -491,11 +485,10 @@ const buildDeniedResponse = ( } } }; - + return builder.buildObject(response); }; - const buildInitiationClosedResponse = ( header: any, request: any, @@ -503,13 +496,13 @@ const buildInitiationClosedResponse = ( note: string ): string => { const builder = new Builder({ headless: false }); - + const patient = request.patient; const pharmacy = request.pharmacy; const prescriber = request.prescriber; const medicationPrescribed = request.medicationprescribed; const remsReferenceID = request.remsreferenceid; - + const response = { Message: { $: { @@ -541,21 +534,20 @@ const buildInitiationClosedResponse = ( } } }; - + return builder.buildObject(response); }; - const buildInitiationSuccessResponse = (header: any, request: any, remsCase: any): string => { const builder = new Builder({ headless: false }); - + const patient = request.patient; const humanPatient = patient?.humanpatient; const pharmacy = request.pharmacy; const prescriber = request.prescriber; const medicationPrescribed = request.medicationprescribed; const remsReferenceID = request.remsreferenceid; - + const response = { Message: { $: { @@ -597,14 +589,13 @@ const buildInitiationSuccessResponse = (header: any, request: any, remsCase: any } } }; - + return builder.buildObject(response); }; - const buildRxFillResponse = (header: any, rxFill: any): string => { const builder = new Builder({ headless: false }); - + const response = { Message: { $: { @@ -625,14 +616,13 @@ const buildRxFillResponse = (header: any, rxFill: any): string => { } } }; - + return builder.buildObject(response); }; - const buildErrorResponse = (errorMessage: string): string => { const builder = new Builder({ headless: false }); - + const response = { Message: { $: { @@ -652,8 +642,8 @@ const buildErrorResponse = (errorMessage: string): string => { } } }; - + return builder.buildObject(response); }; -export default router; \ No newline at end of file +export default router;