From 9d30bc7bdd211f2750b72b743b7e228e2f75f8b7 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 7 Jan 2026 15:24:59 -0500 Subject: [PATCH 01/14] added advanced case history and prescriber/pharmacy change detection --- src/fhir/models.ts | 28 ++++++ src/hooks/hookResources.ts | 40 +++++++- src/lib/etasu.ts | 201 ++++++++++++++++++++++++++++++++++++- 3 files changed, 264 insertions(+), 5 deletions(-) diff --git a/src/fhir/models.ts b/src/fhir/models.ts index 4873016..ddc53fe 100644 --- a/src/fhir/models.ts +++ b/src/fhir/models.ts @@ -30,6 +30,15 @@ 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; status: string; @@ -39,6 +48,11 @@ export interface RemsCase extends Document { patientFirstName: string; patientLastName: string; patientDOB: string; + currentPrescriberId?: string; + currentPharmacyId?: string; + prescriberHistory: string[]; + pharmacyHistory: string[]; + prescriptionEvents: PrescriptionEvent[]; medicationRequestReference?: string; originatingFhirServer?: string; metRequirements: Partial[]; @@ -98,6 +112,20 @@ const remsCaseCollectionSchema = new Schema({ patientLastName: { type: String }, patientDOB: { type: String }, drugCode: { 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: [ diff --git a/src/hooks/hookResources.ts b/src/hooks/hookResources.ts index 0787b5d..317ccf4 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,6 +406,44 @@ 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); diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index 305c4cc..246d3a6 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -222,10 +222,15 @@ export const createNewRemsCaseFromCDSHook = async ( | '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, @@ -233,6 +238,20 @@ export const createNewRemsCaseFromCDSHook = async ( 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: [] }; @@ -290,6 +309,146 @@ export const createNewRemsCaseFromCDSHook = async ( 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); }; @@ -335,8 +494,23 @@ const createMetRequirementAndNewCase = async ( }); 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, + drug, + practitionerReference, + pharmacistReference, + medicationRequestReference, + originatingFhirServer + ); + } // Find and update the existing MetRequirement const matchedMetReq = await metRequirementsCollection @@ -359,7 +533,7 @@ const createMetRequirementAndNewCase = async ( 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( @@ -405,6 +579,11 @@ const createMetRequirementAndNewCase = async ( | 'patientLastName' | 'patientDOB' | 'medicationRequestReference' + | 'currentPrescriberId' + | 'currentPharmacyId' + | 'prescriberHistory' + | 'pharmacyHistory' + | 'prescriptionEvents' | 'metRequirements' > & { originatingFhirServer?: string } = { case_number: case_number, @@ -416,6 +595,20 @@ const createMetRequirementAndNewCase = async ( 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: [] }; From 3293b19e73f8cb188b262145f1edce9d3d2e8af9 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 7 Jan 2026 15:29:24 -0500 Subject: [PATCH 02/14] run lint / prettier --- src/fhir/models.ts | 2 +- src/hooks/hookResources.ts | 7 +++--- src/lib/communication.ts | 30 ++++++++++++------------ src/lib/etasu.ts | 48 +++++++++++++++++++++++++------------- src/server.ts | 2 +- 5 files changed, 53 insertions(+), 36 deletions(-) diff --git a/src/fhir/models.ts b/src/fhir/models.ts index ddc53fe..14d0a1f 100644 --- a/src/fhir/models.ts +++ b/src/fhir/models.ts @@ -139,4 +139,4 @@ const remsCaseCollectionSchema = new Schema({ ] }); -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 317ccf4..ae6b3a4 100644 --- a/src/hooks/hookResources.ts +++ b/src/hooks/hookResources.ts @@ -413,7 +413,8 @@ export const handleCardOrder = async ( const medicationRequestReference = `${request.resourceType}/${request.id}`; const prescriberChanged = remsCase.currentPrescriberId !== practitionerReference; - const pharmacyChanged = pharmacistReference && remsCase.currentPharmacyId !== pharmacistReference; + const pharmacyChanged = + pharmacistReference && remsCase.currentPharmacyId !== pharmacistReference; if (prescriberChanged || pharmacyChanged) { try { @@ -448,7 +449,7 @@ export const handleCardOrder = async ( 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}`; @@ -1035,4 +1036,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 55c4c4f..03eea90 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,8 +143,8 @@ export async function sendCommunicationToEHR( }; // Determine EHR endpoint: use originatingFhirServer if available, otherwise default - const ehrEndpoint = remsCase.originatingFhirServer || - config.fhirServerConfig?.auth?.resourceServer; + const ehrEndpoint = + remsCase.originatingFhirServer || config.fhirServerConfig?.auth?.resourceServer; if (!ehrEndpoint) { logger.warn('No EHR endpoint configured, Communication not sent'); @@ -152,7 +153,7 @@ export async function sendCommunicationToEHR( // 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 +165,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 246d3a6..3861b7e 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -206,7 +206,9 @@ export const createNewRemsCaseFromCDSHook = async ( }); 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 ${drug?.name}` + ); return existingCase; } @@ -302,10 +304,14 @@ 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; }; @@ -336,7 +342,9 @@ export const handleStakeholderChangesAndRecordEvent = async ( // Check if prescriber changed if (remsCase.currentPrescriberId !== practitionerReference) { - console.log(`Prescriber changed from ${remsCase.currentPrescriberId} to ${practitionerReference}`); + console.log( + `Prescriber changed from ${remsCase.currentPrescriberId} to ${practitionerReference}` + ); stakeholdersChanged = true; // Remove old prescriber requirements @@ -349,7 +357,9 @@ export const handleStakeholderChangesAndRecordEvent = async ( ); // Add new prescriber requirements - const prescriberRequirements = drug.requirements.filter(r => r.stakeholderType === 'prescriber'); + const prescriberRequirements = drug.requirements.filter( + r => r.stakeholderType === 'prescriber' + ); for (const requirement of prescriberRequirements) { if (requirement.requiredToDispense) { const existingMetReq = await metRequirementsCollection @@ -485,7 +495,7 @@ const createMetRequirementAndNewCase = async ( const patientDOB = patient.birthDate || ''; let message = ''; - // Check if case already exists + // Check if case already exists const existingCase = await remsCaseCollection.findOne({ patientFirstName: patientFirstName, patientLastName: patientLastName, @@ -495,12 +505,15 @@ const createMetRequirementAndNewCase = async ( if (existingCase) { // Case already exists - check for stakeholder changes before updating requirement - console.log(`Case ${existingCase.case_number} already exists, checking for stakeholder changes`); - + 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; - + const pharmacyChanged = + pharmacistReference && existingCase.currentPharmacyId !== pharmacistReference; + if (prescriberChanged || pharmacyChanged) { await handleStakeholderChangesAndRecordEvent( existingCase, @@ -511,7 +524,7 @@ const createMetRequirementAndNewCase = async ( originatingFhirServer ); } - + // Find and update the existing MetRequirement const matchedMetReq = await metRequirementsCollection .findOne({ @@ -530,10 +543,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 && req?.stakeholderId === matchedMetReq.stakeholderId) { + if ( + req?.requirementName === matchedMetReq.requirementName && + req?.stakeholderId === matchedMetReq.stakeholderId + ) { metReqArray[i].completed = true; req!.completed = true; await remsCaseCollection.updateOne( @@ -564,7 +580,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'; @@ -975,4 +991,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/server.ts b/src/server.ts index f2970f2..414b274 100644 --- a/src/server.ts +++ b/src/server.ts @@ -163,4 +163,4 @@ class REMSServer extends Server { // Start the application -export { REMSServer, initialize }; \ No newline at end of file +export { REMSServer, initialize }; From 63307a07258ddb33a750b0e89803aeda916ee715 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Tue, 13 Jan 2026 09:48:00 -0500 Subject: [PATCH 03/14] ncpdp endpoint implimentation --- .env | 1 + README.md | 2 + src/config.ts | 3 +- src/lib/communication.ts | 9 +- src/lib/winston.ts | 16 +- src/ncpdp/script.ts | 591 +++++++++++++++++++++++++++++++++++++-- 6 files changed, 586 insertions(+), 36 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/lib/communication.ts b/src/lib/communication.ts index 03eea90..3039fb6 100644 --- a/src/lib/communication.ts +++ b/src/lib/communication.ts @@ -143,7 +143,7 @@ export async function sendCommunicationToEHR( }; // Determine EHR endpoint: use originatingFhirServer if available, otherwise default - const ehrEndpoint = + let ehrEndpoint = remsCase.originatingFhirServer || config.fhirServerConfig?.auth?.resourceServer; if (!ehrEndpoint) { @@ -151,6 +151,13 @@ export async function sendCommunicationToEHR( 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}`); 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..f260371 100644 --- a/src/ncpdp/script.ts +++ b/src/ncpdp/script.ts @@ -1,40 +1,575 @@ -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'; + 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); + // req.body is already parsed by body-parser-xml middleware + 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)}`); + + // Route based on message type (tags are lowercase due to normalizeTags: true) + 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.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.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}`); - 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; + + if (!caseId) { + logger.error('Case ID not provided in request'); + return res.status(200).send(buildDeniedResponse(header, remsRequest, 'EC', 'Case ID not provided')); + } + + logger.info(`Looking up case: ${caseId}`); + + // Find the REMS case + const remsCase = await remsCaseCollection.findOne({ case_number: caseId }); + + if (!remsCase) { + logger.error(`Case not found: ${caseId}`); + 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, + drugCode: remsCase.drugCode, + numRequirements: remsCase.metRequirements?.length + }; + logger.info(`Case found: ${JSON.stringify(caseInfo)}`); + + // Get medication requirements + const medication = await medicationCollection.findOne({ + code: remsCase.drugCode, + name: remsCase.drugName + }); + + if (!medication) { + logger.error(`Medication configuration not found: code=${remsCase.drugCode}, name=${remsCase.drugName}`); + return res.status(200).send(buildDeniedResponse(header, remsRequest, 'ER', 'Medication configuration error')); + } + + const medInfo = { + name: medication.name, + code: medication.code, + 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); // 7-day authorization window + + const authDetails = { + authNumber, + effectiveDate: today.toISOString().split('T')[0], + expirationDate: expirationDate.toISOString().split('T')[0] + }; + logger.info(`Authorization details: ${JSON.stringify(authDetails)}`); + + return res.status(200).send(buildApprovedResponse( + header, + remsRequest, + caseId, + authNumber, + today.toISOString().split('T')[0], + expirationDate.toISOString().split('T')[0] + )); + } + + // Requirements not met - determine reason codes and send Communication + logger.info(`${outstandingRequirements.length} requirements not met - DENYING`); + const reasonCodes = determineReasonCodes(outstandingRequirements); + const reasonText = buildReasonText(outstandingRequirements); + + const denialDetails = { + reasonCodes: reasonCodes.join(','), + 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}`); + // Continue with denial response even if Communication fails + } + + logger.info('Sending DENIED response'); + return res.status(200).send(buildDeniedResponse(header, remsRequest, reasonCodes.join(','), reasonText)); + + } catch (error: any) { + logger.error(`ERROR in handleRemsRequest: ${error.message}`); + logger.error(`Stack trace: ${error.stack}`); + 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 drugCode = initRequest.medicationprescribed?.product?.drugcoded?.ndc; + + const requestInfo = { + patientName: `${patient?.names?.name?.firstname} ${patient?.names?.name?.lastname}`, + drugCode + }; + logger.info(`REMS Initiation request: ${JSON.stringify(requestInfo)}`); + + // Look up patient's REMS case + const remsCase = await remsCaseCollection.findOne({ + patientFirstName: patient?.names?.name?.firstname, + patientLastName: patient?.names?.name?.lastname, + patientDOB: patient?.dateofbirth?.date, + drugCode: drugCode + }); + + if (!remsCase) { + // No case exists - return "Closed" with EM (patient must enroll) + return res.status(200).send(buildInitiationClosedResponse( + header, + initRequest, + 'EM', + 'Patient must enroll/certify' + )); + } + + // Case exists - check requirements + const medication = await medicationCollection.findOne({ code: drugCode }); + + if (!medication) { + 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 reasonCodes = determineReasonCodes(outstandingRequirements); + const reasonText = buildReasonText(outstandingRequirements); + + return res.status(200).send(buildInitiationClosedResponse( + header, + initRequest, + reasonCodes.join(','), + reasonText + )); + } + + // All requirements met - return success with patient ID + return res.status(200).send(buildInitiationSuccessResponse(header, initRequest, remsCase)); + + } catch (error: any) { + logger.error(`ERROR in handleRemsInitiation: ${error.message}`); + return res.status(500).send(buildErrorResponse(error.message)); + } +}; + + +const handleRxFill = async (message: any, res: Response) => { + try { + logger.info('--- handleRxFill started ---'); + const rxFill = message.body.rxfill; + const patient = rxFill.patient?.humanpatient; + const drugCode = rxFill.medicationprescribed?.product?.drugcoded?.ndc; + const fillStatus = rxFill.fillstatus?.dispensed?.note || 'Dispensed'; + + const rxFillInfo = { + patientName: `${patient?.names?.name?.firstname} ${patient?.names?.name?.lastname}`, + patientDOB: patient?.dateofbirth?.date, + drugCode, + fillStatus + }; + logger.info(`RxFill notification: ${JSON.stringify(rxFillInfo)}`); + + // Update case dispense status + const updatedCase = await remsCaseCollection.findOneAndUpdate( + { + patientFirstName: patient?.names?.name?.firstname, + patientLastName: patient?.names?.name?.lastname, + patientDOB: patient?.dateofbirth?.date, + drugCode: drugCode + }, + { dispenseStatus: fillStatus }, + { new: true } + ); + + if (updatedCase) { + logger.info(`Updated dispense status for case ${updatedCase.case_number}: ${fillStatus}`); + } else { + logger.warn('No matching case found to update'); + } + + logger.info('Sending RxFill acknowledgment'); + // Simple acknowledgment response + res.status(200).send(buildRxFillResponse(message.header, rxFill)); + + } catch (error: any) { + logger.error(`ERROR in handleRxFill: ${error.message}`); + return res.status(500).send(buildErrorResponse(error.message)); + } +}; + +const determineReasonCodes = (outstandingRequirements: any[]): string[] => { + const codes = new Set(); + + for (const req of outstandingRequirements) { + switch (req.stakeholder) { + case 'patient': + codes.add('EM'); // Patient must enroll/certify + break; + case 'prescriber': + codes.add('ES'); // Prescriber must enroll/certify + break; + case 'pharmacist': + case 'pharmacy': + codes.add('EO'); // Pharmacy not enrolled/certified + break; + } + } + + return Array.from(codes); +}; + + +const buildReasonText = (outstandingRequirements: any[]): string => { + const reqNames = outstandingRequirements.map(r => `${r.name} (${r.stakeholder})`).join(', '); + return `Outstanding REMS requirements: ${reqNames}`; +}; + + +const buildApprovedResponse = ( + header: any, + request: any, + caseId: string, + authNumber: string, + effectiveDate: string, + expirationDate: 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' + }, + Header: header, + Body: { + REMSResponse: { + REMSReferenceID: request.REMSReferenceID, + Patient: request.Patient, + Pharmacy: request.Pharmacy, + Prescriber: request.Prescriber, + MedicationPrescribed: request.MedicationPrescribed, + Response: { + ResponseStatus: { + Approved: { + REMSCaseID: caseId, + REMSAuthorizationNumber: authNumber, + AuthorizationPeriod: { + EffectiveDate: { Date: effectiveDate }, + ExpirationDate: { Date: expirationDate } + } + } + } + } + } + } + } + }; + + return builder.buildObject(response); +}; + + +const buildDeniedResponse = ( + header: any, + request: any, + reasonCode: string, + note: 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' + }, + Header: header, + Body: { + REMSResponse: { + REMSReferenceID: request.REMSReferenceID, + Patient: request.Patient, + Pharmacy: request.Pharmacy, + Prescriber: request.Prescriber, + MedicationPrescribed: request.MedicationPrescribed, + Response: { + ResponseStatus: { + Denied: { + REMSCaseID: request.request?.solicitedmodel?.remscaseid, + 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 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: request.REMSReferenceID, + Patient: request.Patient, + Pharmacy: request.Pharmacy, + Prescriber: request.Prescriber, + MedicationPrescribed: request.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 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: request.REMSReferenceID, + Patient: { + ...request.Patient, + HumanPatient: { + ...request.Patient.HumanPatient, + Identification: { + REMSPatientID: remsCase.case_number // Return case number as patient ID + } + } + }, + Pharmacy: request.Pharmacy, + Prescriber: request.Prescriber, + MedicationPrescribed: request.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', // Success code + Description: 'Dispense notification received' + } + } + } + }; + + 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 From a96b5ffde3c98955b06ab53fb691e63dedd386a0 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Tue, 13 Jan 2026 11:27:14 -0500 Subject: [PATCH 04/14] update routing --- src/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.ts b/src/server.ts index 414b274..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; } From 77a9c231f1d862947c04ea91d257bc65b651b4c8 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Tue, 13 Jan 2026 12:29:58 -0500 Subject: [PATCH 05/14] add package ndc code tracking for ncpdp messages --- src/fhir/models.ts | 4 ++++ src/fhir/utilities.ts | 4 ++++ src/lib/etasu.ts | 4 ++++ src/ncpdp/script.ts | 4 ++-- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/fhir/models.ts b/src/fhir/models.ts index 14d0a1f..d9809c6 100644 --- a/src/fhir/models.ts +++ b/src/fhir/models.ts @@ -16,6 +16,7 @@ export interface Medication extends Document { name: string; codeSystem: string; code: string; + ndcCode: string, requirements: Requirement[]; } @@ -45,6 +46,7 @@ export interface RemsCase extends Document { dispenseStatus: string; drugName: string; drugCode: string; + drugNdcCode?: string; patientFirstName: string; patientLastName: string; patientDOB: string; @@ -62,6 +64,7 @@ const medicationCollectionSchema = new Schema({ name: { type: String }, codeSystem: { type: String }, code: { type: String }, + ndcCode: { type: String }, requirements: [ { name: { type: String }, @@ -112,6 +115,7 @@ const remsCaseCollectionSchema = new Schema({ patientLastName: { type: String }, patientDOB: { type: String }, drugCode: { type: String }, + drugNdcCode: { type: String }, currentPrescriberId: { type: String }, currentPharmacyId: { type: String }, prescriberHistory: [{ type: String }], 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/lib/etasu.ts b/src/lib/etasu.ts index 3861b7e..4c16ee3 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -220,6 +220,7 @@ export const createNewRemsCaseFromCDSHook = async ( | 'dispenseStatus' | 'drugName' | 'drugCode' + | 'drugNdcCode' | 'patientFirstName' | 'patientLastName' | 'patientDOB' @@ -236,6 +237,7 @@ export const createNewRemsCaseFromCDSHook = async ( dispenseStatus: 'Pending', drugName: drug?.name, drugCode: drug?.code, + drugNdcCode: drug?.ndcCode, patientFirstName: patientFirstName, patientLastName: patientLastName, patientDOB: patientDOB, @@ -591,6 +593,7 @@ const createMetRequirementAndNewCase = async ( | 'dispenseStatus' | 'drugName' | 'drugCode' + | 'drugNdcCode' | 'patientFirstName' | 'patientLastName' | 'patientDOB' @@ -607,6 +610,7 @@ const createMetRequirementAndNewCase = async ( dispenseStatus: dispenseStatusDefault, drugName: drug?.name, drugCode: drug?.code, + drugNdcCode: drug?.ndcCode, patientFirstName: patientFirstName, patientLastName: patientLastName, patientDOB: patientDOB, diff --git a/src/ncpdp/script.ts b/src/ncpdp/script.ts index f260371..fbe7c78 100644 --- a/src/ncpdp/script.ts +++ b/src/ncpdp/script.ts @@ -204,7 +204,7 @@ const handleRemsInitiation = async (message: any, res: Response) => { patientFirstName: patient?.names?.name?.firstname, patientLastName: patient?.names?.name?.lastname, patientDOB: patient?.dateofbirth?.date, - drugCode: drugCode + drugNdcCode: drugCode }); if (!remsCase) { @@ -290,7 +290,7 @@ const handleRxFill = async (message: any, res: Response) => { patientFirstName: patient?.names?.name?.firstname, patientLastName: patient?.names?.name?.lastname, patientDOB: patient?.dateofbirth?.date, - drugCode: drugCode + drugNdcCode: drugCode }, { dispenseStatus: fillStatus }, { new: true } From 3f5cc637a8c440420bba9a584e992de28b527eb4 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Tue, 13 Jan 2026 13:42:50 -0500 Subject: [PATCH 06/14] fix ndc logic --- src/ncpdp/script.ts | 107 ++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 53 deletions(-) diff --git a/src/ncpdp/script.ts b/src/ncpdp/script.ts index fbe7c78..5d8b3f2 100644 --- a/src/ncpdp/script.ts +++ b/src/ncpdp/script.ts @@ -3,13 +3,13 @@ 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('/', async (req: Request, res: Response) => { try { - // req.body is already parsed by body-parser-xml middleware const parsedMessage = req.body; logger.info('=== NCPDP Request Received ==='); @@ -25,7 +25,6 @@ router.post('/', async (req: Request, res: Response) => { }; logger.info(`Message type check: ${JSON.stringify(messageInfo)}`); - // Route based on message type (tags are lowercase due to normalizeTags: true) if (body?.remsrequest) { logger.info('Routing to handleRemsRequest'); await handleRemsRequest(message, res); @@ -65,7 +64,6 @@ const handleRemsRequest = async (message: any, res: Response) => { logger.info(`Looking up case: ${caseId}`); - // Find the REMS case const remsCase = await remsCaseCollection.findOne({ case_number: caseId }); if (!remsCase) { @@ -77,25 +75,23 @@ const handleRemsRequest = async (message: any, res: Response) => { case_number: remsCase.case_number, status: remsCase.status, drugName: remsCase.drugName, - drugCode: remsCase.drugCode, + drugNdcCode: remsCase.drugNdcCode, numRequirements: remsCase.metRequirements?.length }; logger.info(`Case found: ${JSON.stringify(caseInfo)}`); - // Get medication requirements const medication = await medicationCollection.findOne({ - code: remsCase.drugCode, - name: remsCase.drugName + ndcCode: remsCase.drugNdcCode }); if (!medication) { - logger.error(`Medication configuration not found: code=${remsCase.drugCode}, name=${remsCase.drugName}`); + logger.error(`Medication configuration not found for NDC: ${remsCase.drugNdcCode}`); return res.status(200).send(buildDeniedResponse(header, remsRequest, 'ER', 'Medication configuration error')); } const medInfo = { name: medication.name, - code: medication.code, + ndcCode: medication.ndcCode, totalRequirements: medication.requirements.length, requiredToDispense: medication.requirements.filter((r: any) => r.requiredToDispense).length }; @@ -130,7 +126,7 @@ const handleRemsRequest = async (message: any, res: Response) => { const authNumber = `RDA${Math.floor(Math.random() * 10000000)}`; const today = new Date(); const expirationDate = new Date(today); - expirationDate.setDate(expirationDate.getDate() + 7); // 7-day authorization window + expirationDate.setDate(expirationDate.getDate() + 7); const authDetails = { authNumber, @@ -149,8 +145,7 @@ const handleRemsRequest = async (message: any, res: Response) => { )); } - // Requirements not met - determine reason codes and send Communication - logger.info(`${outstandingRequirements.length} requirements not met - DENYING`); + // Requirements not met - denial with reason codes const reasonCodes = determineReasonCodes(outstandingRequirements); const reasonText = buildReasonText(outstandingRequirements); @@ -169,7 +164,6 @@ const handleRemsRequest = async (message: any, res: Response) => { logger.info(`Communication sent successfully to: ${ehrEndpoint}`); } catch (commError: any) { logger.error(`Failed to send Communication: ${commError.message}`); - // Continue with denial response even if Communication fails } logger.info('Sending DENIED response'); @@ -191,24 +185,23 @@ const handleRemsInitiation = async (message: any, res: Response) => { const patient = initRequest.patient?.humanpatient; const prescriber = initRequest.prescriber?.nonveterinarian; const pharmacy = initRequest.pharmacy; - const drugCode = initRequest.medicationprescribed?.product?.drugcoded?.ndc; + const drugNdcCode = initRequest.medicationprescribed?.product?.drugcoded?.ndc; const requestInfo = { patientName: `${patient?.names?.name?.firstname} ${patient?.names?.name?.lastname}`, - drugCode + drugNdcCode: drugNdcCode }; logger.info(`REMS Initiation request: ${JSON.stringify(requestInfo)}`); - // Look up patient's REMS case const remsCase = await remsCaseCollection.findOne({ patientFirstName: patient?.names?.name?.firstname, patientLastName: patient?.names?.name?.lastname, patientDOB: patient?.dateofbirth?.date, - drugNdcCode: drugCode + drugNdcCode: drugNdcCode }); if (!remsCase) { - // No case exists - return "Closed" with EM (patient must enroll) + logger.info('No case exists - patient must enroll'); return res.status(200).send(buildInitiationClosedResponse( header, initRequest, @@ -218,9 +211,12 @@ const handleRemsInitiation = async (message: any, res: Response) => { } // Case exists - check requirements - const medication = await medicationCollection.findOne({ code: drugCode }); + const medication = await medicationCollection.findOne({ + ndcCode: drugNdcCode + }); if (!medication) { + logger.error(`Medication not found for NDC: ${drugNdcCode}`); return res.status(200).send(buildInitiationClosedResponse( header, initRequest, @@ -250,6 +246,7 @@ const handleRemsInitiation = async (message: any, res: Response) => { const reasonCodes = determineReasonCodes(outstandingRequirements); const reasonText = buildReasonText(outstandingRequirements); + logger.info(`Requirements not met - closing with: ${reasonCodes.join(',')}`); return res.status(200).send(buildInitiationClosedResponse( header, initRequest, @@ -259,6 +256,7 @@ const handleRemsInitiation = async (message: any, res: Response) => { } // All requirements met - return success with patient ID + logger.info('All requirements met - returning success'); return res.status(200).send(buildInitiationSuccessResponse(header, initRequest, remsCase)); } catch (error: any) { @@ -271,61 +269,51 @@ const handleRemsInitiation = async (message: any, res: Response) => { 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 drugCode = rxFill.medicationprescribed?.product?.drugcoded?.ndc; - const fillStatus = rxFill.fillstatus?.dispensed?.note || 'Dispensed'; + const drugNdcCode = rxFill.medicationprescribed?.product?.drugcoded?.ndc; - const rxFillInfo = { - patientName: `${patient?.names?.name?.firstname} ${patient?.names?.name?.lastname}`, - patientDOB: patient?.dateofbirth?.date, - drugCode, - fillStatus - }; - logger.info(`RxFill notification: ${JSON.stringify(rxFillInfo)}`); + logger.info(`RxFill for patient: ${patient?.names?.name?.firstname} ${patient?.names?.name?.lastname}, NDC: ${drugNdcCode}`); // Update case dispense status - const updatedCase = await remsCaseCollection.findOneAndUpdate( - { - patientFirstName: patient?.names?.name?.firstname, - patientLastName: patient?.names?.name?.lastname, - patientDOB: patient?.dateofbirth?.date, - drugNdcCode: drugCode - }, - { dispenseStatus: fillStatus }, - { new: true } - ); + const remsCase = await remsCaseCollection.findOne({ + patientFirstName: patient?.names?.name?.firstname, + patientLastName: patient?.names?.name?.lastname, + patientDOB: patient?.dateofbirth?.date, + drugNdcCode: drugNdcCode + }); - if (updatedCase) { - logger.info(`Updated dispense status for case ${updatedCase.case_number}: ${fillStatus}`); + if (remsCase) { + remsCase.dispenseStatus = 'Dispensed'; + await remsCase.save(); + logger.info(`Updated case ${remsCase.case_number} dispense status to Dispensed`); } else { - logger.warn('No matching case found to update'); + logger.warn('Case not found for RxFill notification'); } - logger.info('Sending RxFill acknowledgment'); - // Simple acknowledgment response - res.status(200).send(buildRxFillResponse(message.header, rxFill)); - + return res.status(200).send(buildRxFillResponse(header, rxFill)); } catch (error: any) { logger.error(`ERROR in handleRxFill: ${error.message}`); return res.status(500).send(buildErrorResponse(error.message)); } }; + const determineReasonCodes = (outstandingRequirements: any[]): string[] => { const codes = new Set(); for (const req of outstandingRequirements) { - switch (req.stakeholder) { + switch (req.stakeholder?.toLowerCase()) { case 'patient': - codes.add('EM'); // Patient must enroll/certify + codes.add('EM'); break; case 'prescriber': - codes.add('ES'); // Prescriber must enroll/certify + codes.add('ES'); break; case 'pharmacist': case 'pharmacy': - codes.add('EO'); // Pharmacy not enrolled/certified + codes.add('EO'); break; } } @@ -482,6 +470,9 @@ const buildInitiationClosedResponse = ( const buildInitiationSuccessResponse = (header: any, request: any, remsCase: any): string => { const builder = new Builder({ headless: false }); + const patient = request.Patient || request.patient; + const humanPatient = patient?.HumanPatient || patient?.humanpatient; + const response = { Message: { $: { @@ -498,11 +489,21 @@ const buildInitiationSuccessResponse = (header: any, request: any, remsCase: any REMSInitiationResponse: { REMSReferenceID: request.REMSReferenceID, Patient: { - ...request.Patient, HumanPatient: { - ...request.Patient.HumanPatient, + $: { + 'xsi:type': 'PatientMandatoryAddress' + }, Identification: { - REMSPatientID: remsCase.case_number // Return case number as patient ID + REMSPatientID: remsCase.remsPatientId || remsCase.case_number + }, + Names: humanPatient?.Names || humanPatient?.names, + GenderAndSex: humanPatient?.GenderAndSex || humanPatient?.genderandsex, + DateOfBirth: humanPatient?.DateOfBirth || humanPatient?.dateofbirth, + Address: { + $: { + 'xsi:type': 'MandatoryAddressType' + }, + ...(humanPatient?.Address || humanPatient?.address) } } }, @@ -535,7 +536,7 @@ const buildRxFillResponse = (header: any, rxFill: any): string => { Header: header, Body: { Status: { - Code: '000', // Success code + Code: '000', Description: 'Dispense notification received' } } From 04611ac38a85bb01f172ed04c731edb9ab4f317d Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Tue, 13 Jan 2026 13:47:18 -0500 Subject: [PATCH 07/14] updated models for patient id tracking --- src/fhir/models.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/fhir/models.ts b/src/fhir/models.ts index d9809c6..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,8 +15,8 @@ export interface Requirement { export interface Medication extends Document { name: string; codeSystem: string; - code: string; - ndcCode: string, + code: string; // RxNorm code (used for CDS Hooks) + ndcCode: string; // NDC code (used for NCPDP SCRIPT) requirements: Requirement[]; } @@ -42,6 +42,7 @@ export interface PrescriptionEvent { export interface RemsCase extends Document { case_number: string; + remsPatientId?: string; status: string; dispenseStatus: string; drugName: string; @@ -80,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', @@ -108,13 +111,14 @@ 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 }, @@ -143,4 +147,12 @@ const remsCaseCollectionSchema = new Schema({ ] }); -export const remsCaseCollection = model('RemsCaseCollection', remsCaseCollectionSchema); +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 From b072235639205712c6fa63017dcc11810fabfc93 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Tue, 13 Jan 2026 15:30:11 -0500 Subject: [PATCH 08/14] include response type in ncpdp messages --- src/ncpdp/script.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/ncpdp/script.ts b/src/ncpdp/script.ts index 5d8b3f2..55b259b 100644 --- a/src/ncpdp/script.ts +++ b/src/ncpdp/script.ts @@ -36,11 +36,13 @@ router.post('/', async (req: Request, res: Response) => { await handleRxFill(message, res); } else { logger.error('Unknown NCPDP message type'); + res.type('application/xml'); res.status(400).send(buildErrorResponse('Unknown message type')); } } 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')); } }); @@ -59,6 +61,7 @@ const handleRemsRequest = async (message: any, res: Response) => { 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')); } @@ -68,6 +71,7 @@ const handleRemsRequest = async (message: any, res: Response) => { if (!remsCase) { logger.error(`Case not found: ${caseId}`); + res.type('application/xml'); return res.status(200).send(buildDeniedResponse(header, remsRequest, 'EC', 'Case not found')); } @@ -86,6 +90,7 @@ const handleRemsRequest = async (message: any, res: Response) => { 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')); } @@ -135,6 +140,7 @@ const handleRemsRequest = async (message: any, res: Response) => { }; logger.info(`Authorization details: ${JSON.stringify(authDetails)}`); + res.type('application/xml'); return res.status(200).send(buildApprovedResponse( header, remsRequest, @@ -167,11 +173,13 @@ const handleRemsRequest = async (message: any, res: Response) => { } logger.info('Sending DENIED response'); + res.type('application/xml'); return res.status(200).send(buildDeniedResponse(header, remsRequest, reasonCodes.join(','), 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)); } }; @@ -202,6 +210,7 @@ const handleRemsInitiation = async (message: any, res: Response) => { if (!remsCase) { logger.info('No case exists - patient must enroll'); + res.type('application/xml'); return res.status(200).send(buildInitiationClosedResponse( header, initRequest, @@ -217,6 +226,7 @@ const handleRemsInitiation = async (message: any, res: Response) => { if (!medication) { logger.error(`Medication not found for NDC: ${drugNdcCode}`); + res.type('application/xml'); return res.status(200).send(buildInitiationClosedResponse( header, initRequest, @@ -247,6 +257,7 @@ const handleRemsInitiation = async (message: any, res: Response) => { const reasonText = buildReasonText(outstandingRequirements); logger.info(`Requirements not met - closing with: ${reasonCodes.join(',')}`); + res.type('application/xml'); return res.status(200).send(buildInitiationClosedResponse( header, initRequest, @@ -257,10 +268,12 @@ const handleRemsInitiation = async (message: any, res: Response) => { // 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)); } }; @@ -292,9 +305,11 @@ const handleRxFill = async (message: any, res: Response) => { logger.warn('Case not found for RxFill notification'); } + res.type('application/xml'); return res.status(200).send(buildRxFillResponse(header, rxFill)); } catch (error: any) { logger.error(`ERROR in handleRxFill: ${error.message}`); + res.type('application/xml'); return res.status(500).send(buildErrorResponse(error.message)); } }; From 12ed4a83ad382bf46670c74a89146c8f5f727955 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 08:20:17 -0500 Subject: [PATCH 09/14] use saved medication on rems case creation to get ndc code from rxnorm --- src/lib/etasu.ts | 52 ++++++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index 4c16ee3..41d92e5 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -197,17 +197,24 @@ 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}` + `Case already exists for patient ${patientFirstName} ${patientLastName} and drug ${medicationData?.name}` ); return existingCase; } @@ -235,9 +242,9 @@ export const createNewRemsCaseFromCDSHook = async ( case_number: case_number, status: 'Pending', dispenseStatus: 'Pending', - drugName: drug?.name, - drugCode: drug?.code, - drugNdcCode: drug?.ndcCode, + drugName: medicationData?.name, + drugCode: medicationData?.code, + drugNdcCode: medicationData?.ndcCode, patientFirstName: patientFirstName, patientLastName: patientLastName, patientDOB: patientDOB, @@ -261,7 +268,7 @@ export const createNewRemsCaseFromCDSHook = async ( }; // 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 @@ -278,7 +285,7 @@ export const createNewRemsCaseFromCDSHook = async ( .findOne({ stakeholderId: stakeholderReference, requirementName: requirement.name, - drugName: drug?.name + drugName: medicationData?.name }) .exec(); @@ -293,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] }; @@ -497,12 +504,19 @@ const createMetRequirementAndNewCase = async ( const patientDOB = patient.birthDate || ''; let message = ''; + // 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) { @@ -519,7 +533,7 @@ const createMetRequirementAndNewCase = async ( if (prescriberChanged || pharmacyChanged) { await handleStakeholderChangesAndRecordEvent( existingCase, - drug, + medicationData, practitionerReference, pharmacistReference, medicationRequestReference, @@ -532,7 +546,7 @@ const createMetRequirementAndNewCase = async ( .findOne({ stakeholderId: reqStakeholderReference, requirementName: requirement.name, - drugName: drug?.name + drugName: medicationData?.name }) .exec(); @@ -608,9 +622,9 @@ const createMetRequirementAndNewCase = async ( case_number: case_number, status: remsRequestCompletedStatus, dispenseStatus: dispenseStatusDefault, - drugName: drug?.name, - drugCode: drug?.code, - drugNdcCode: drug?.ndcCode, + drugName: medicationData?.name, + drugCode: medicationData?.code, + drugNdcCode: medicationData?.ndcCode, patientFirstName: patientFirstName, patientLastName: patientLastName, patientDOB: patientDOB, @@ -639,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] }; @@ -651,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) { @@ -668,7 +682,7 @@ const createMetRequirementAndNewCase = async ( .findOne({ stakeholderId: reqStakeholder2Reference, requirementName: requirement2.name, - drugName: drug?.name + drugName: medicationData?.name }) .exec(); if (matchedMetReq2) { @@ -685,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] }; @@ -995,4 +1009,4 @@ export const processQuestionnaireResponseSubmission = async (requestBody: Bundle export { getResource, getQuestionnaireResponse }; -export default router; +export default router; \ No newline at end of file From 33fb18cbf35f60d76fce2b66c9f7fe5720ef08b8 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 08:44:22 -0500 Subject: [PATCH 10/14] don't show patient status until patient enrolled --- src/hooks/hookResources.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/hooks/hookResources.ts b/src/hooks/hookResources.ts index ae6b3a4..677afc0 100644 --- a/src/hooks/hookResources.ts +++ b/src/hooks/hookResources.ts @@ -566,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; }; @@ -825,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({ @@ -867,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; }; @@ -1036,4 +1046,4 @@ export function createQuestionnaireCompletionTask( ] }; return taskResource; -} +} \ No newline at end of file From 98c48cf311a34ce98f896b543c80fa8ba96bc03d Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 10:36:38 -0500 Subject: [PATCH 11/14] rxfill --- src/ncpdp/script.ts | 50 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/src/ncpdp/script.ts b/src/ncpdp/script.ts index 55b259b..0cab283 100644 --- a/src/ncpdp/script.ts +++ b/src/ncpdp/script.ts @@ -285,30 +285,56 @@ const handleRxFill = async (message: any, res: Response) => { const header = message.header; const rxFill = message.body.rxfill; const patient = rxFill.patient?.humanpatient; - const drugNdcCode = rxFill.medicationprescribed?.product?.drugcoded?.ndc; + + const medicationDispensed = rxFill.medicationdispensed; - logger.info(`RxFill for patient: ${patient?.names?.name?.firstname} ${patient?.names?.name?.lastname}, NDC: ${drugNdcCode}`); + if (!medicationDispensed) { + logger.error('MedicationDispensed not found in RxFill message'); + logger.error(`Available RxFill fields: ${JSON.stringify(Object.keys(rxFill))}`); + } - // Update case dispense status - const remsCase = await remsCaseCollection.findOne({ - patientFirstName: patient?.names?.name?.firstname, - patientLastName: patient?.names?.name?.lastname, - patientDOB: patient?.dateofbirth?.date, - drugNdcCode: drugNdcCode - }); + 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(`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)); } 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)); } @@ -552,7 +578,7 @@ const buildRxFillResponse = (header: any, rxFill: any): string => { Body: { Status: { Code: '000', - Description: 'Dispense notification received' + Description: 'Dispense notification received and processed' } } } From fcae57df0a0bf3c8113a3dd0d614b2242c2623f6 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 11:06:28 -0500 Subject: [PATCH 12/14] proper reason code handling --- src/ncpdp/script.ts | 128 +++++++++++++++++++++++++++++--------------- 1 file changed, 86 insertions(+), 42 deletions(-) diff --git a/src/ncpdp/script.ts b/src/ncpdp/script.ts index 0cab283..602a89e 100644 --- a/src/ncpdp/script.ts +++ b/src/ncpdp/script.ts @@ -151,12 +151,13 @@ const handleRemsRequest = async (message: any, res: Response) => { )); } - // Requirements not met - denial with reason codes + // Requirements not met - denial with reason code const reasonCodes = determineReasonCodes(outstandingRequirements); - const reasonText = buildReasonText(outstandingRequirements); + const reasonCode = reasonCodes[0]; + const reasonText = buildReasonText(reasonCode); const denialDetails = { - reasonCodes: reasonCodes.join(','), + reasonCode: reasonCode, reasonText, outstandingCount: outstandingRequirements.length }; @@ -174,7 +175,7 @@ const handleRemsRequest = async (message: any, res: Response) => { logger.info('Sending DENIED response'); res.type('application/xml'); - return res.status(200).send(buildDeniedResponse(header, remsRequest, reasonCodes.join(','), reasonText)); + return res.status(200).send(buildDeniedResponse(header, remsRequest, reasonCode, reasonText)); } catch (error: any) { logger.error(`ERROR in handleRemsRequest: ${error.message}`); @@ -342,30 +343,48 @@ const handleRxFill = async (message: any, res: Response) => { const determineReasonCodes = (outstandingRequirements: any[]): string[] => { - const codes = new Set(); + let hasPatientReq = false; + let hasPrescriberReq = false; + let hasPharmacyReq = false; for (const req of outstandingRequirements) { - switch (req.stakeholder?.toLowerCase()) { - case 'patient': - codes.add('EM'); - break; - case 'prescriber': - codes.add('ES'); - break; - case 'pharmacist': - case 'pharmacy': - codes.add('EO'); - break; + 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 Array.from(codes); + // 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 = (outstandingRequirements: any[]): string => { - const reqNames = outstandingRequirements.map(r => `${r.name} (${r.stakeholder})`).join(', '); - return `Outstanding REMS requirements: ${reqNames}`; +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'; }; @@ -379,6 +398,13 @@ const buildApprovedResponse = ( ): string => { const builder = new Builder({ headless: false }); + // Handle both capitalized and lowercased keys from parsed XML + const patient = request.patient; + const pharmacy = request.pharmacy; + const prescriber = request.prescriber; + const medicationPrescribed = request.medicationprescribed; + const remsReferenceID = request.remsreferenceid; + const response = { Message: { $: { @@ -393,11 +419,11 @@ const buildApprovedResponse = ( Header: header, Body: { REMSResponse: { - REMSReferenceID: request.REMSReferenceID, - Patient: request.Patient, - Pharmacy: request.Pharmacy, - Prescriber: request.Prescriber, - MedicationPrescribed: request.MedicationPrescribed, + REMSReferenceID: remsReferenceID, + Patient: patient, + Pharmacy: pharmacy, + Prescriber: prescriber, + MedicationPrescribed: medicationPrescribed, Response: { ResponseStatus: { Approved: { @@ -427,6 +453,14 @@ const buildDeniedResponse = ( ): 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: { $: { @@ -441,15 +475,15 @@ const buildDeniedResponse = ( Header: header, Body: { REMSResponse: { - REMSReferenceID: request.REMSReferenceID, - Patient: request.Patient, - Pharmacy: request.Pharmacy, - Prescriber: request.Prescriber, - MedicationPrescribed: request.MedicationPrescribed, + REMSReferenceID: remsReferenceID, + Patient: patient, + Pharmacy: pharmacy, + Prescriber: prescriber, + MedicationPrescribed: medicationPrescribed, Response: { ResponseStatus: { Denied: { - REMSCaseID: request.request?.solicitedmodel?.remscaseid, + REMSCaseID: caseId, DeniedReasonCode: reasonCode, REMSNote: note } @@ -472,6 +506,12 @@ const buildInitiationClosedResponse = ( ): 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: { $: { @@ -486,11 +526,11 @@ const buildInitiationClosedResponse = ( Header: header, Body: { REMSInitiationResponse: { - REMSReferenceID: request.REMSReferenceID, - Patient: request.Patient, - Pharmacy: request.Pharmacy, - Prescriber: request.Prescriber, - MedicationPrescribed: request.MedicationPrescribed, + REMSReferenceID: remsReferenceID, + Patient: patient, + Pharmacy: pharmacy, + Prescriber: prescriber, + MedicationPrescribed: medicationPrescribed, Response: { ResponseStatus: { Closed: { @@ -511,8 +551,12 @@ const buildInitiationClosedResponse = ( const buildInitiationSuccessResponse = (header: any, request: any, remsCase: any): string => { const builder = new Builder({ headless: false }); - const patient = request.Patient || request.patient; - const humanPatient = patient?.HumanPatient || patient?.humanpatient; + 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: { @@ -528,7 +572,7 @@ const buildInitiationSuccessResponse = (header: any, request: any, remsCase: any Header: header, Body: { REMSInitiationResponse: { - REMSReferenceID: request.REMSReferenceID, + REMSReferenceID: remsReferenceID, Patient: { HumanPatient: { $: { @@ -548,9 +592,9 @@ const buildInitiationSuccessResponse = (header: any, request: any, remsCase: any } } }, - Pharmacy: request.Pharmacy, - Prescriber: request.Prescriber, - MedicationPrescribed: request.MedicationPrescribed + Pharmacy: pharmacy, + Prescriber: prescriber, + MedicationPrescribed: medicationPrescribed } } } From 0f97196672218c30cb47714efd8f249a0e336bb0 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 11:18:21 -0500 Subject: [PATCH 13/14] singular denial reason --- src/ncpdp/script.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/ncpdp/script.ts b/src/ncpdp/script.ts index 602a89e..5bd79e0 100644 --- a/src/ncpdp/script.ts +++ b/src/ncpdp/script.ts @@ -152,8 +152,7 @@ const handleRemsRequest = async (message: any, res: Response) => { } // Requirements not met - denial with reason code - const reasonCodes = determineReasonCodes(outstandingRequirements); - const reasonCode = reasonCodes[0]; + const reasonCode = determineReasonCodes(outstandingRequirements); const reasonText = buildReasonText(reasonCode); const denialDetails = { @@ -254,15 +253,15 @@ const handleRemsInitiation = async (message: any, res: Response) => { } if (outstandingRequirements.length > 0) { - const reasonCodes = determineReasonCodes(outstandingRequirements); - const reasonText = buildReasonText(outstandingRequirements); + const reasonCode = determineReasonCodes(outstandingRequirements); + const reasonText = buildReasonText(reasonCode); - logger.info(`Requirements not met - closing with: ${reasonCodes.join(',')}`); + logger.info(`Requirements not met - closing with: ${reasonCode}`); res.type('application/xml'); return res.status(200).send(buildInitiationClosedResponse( header, initRequest, - reasonCodes.join(','), + reasonCode, reasonText )); } @@ -342,7 +341,7 @@ const handleRxFill = async (message: any, res: Response) => { }; -const determineReasonCodes = (outstandingRequirements: any[]): string[] => { +const determineReasonCodes = (outstandingRequirements: any[]): string => { let hasPatientReq = false; let hasPrescriberReq = false; let hasPharmacyReq = false; @@ -360,15 +359,15 @@ const determineReasonCodes = (outstandingRequirements: any[]): string[] => { // Return only the highest priority requirement if (hasPatientReq) { - return ['EM']; + return 'EM'; } else if (hasPrescriberReq) { - return ['ES']; + return 'ES'; } else if (hasPharmacyReq) { - return ['EO']; + return 'EO'; } // Fallback - should not reach here - return ['EC']; + return 'EC'; }; From 143c8e7b5e0c28d4c71b60f1935753e782fd4d95 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 11:35:17 -0500 Subject: [PATCH 14/14] single denial reason --- src/ncpdp/script.ts | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/ncpdp/script.ts b/src/ncpdp/script.ts index 5bd79e0..872d7ed 100644 --- a/src/ncpdp/script.ts +++ b/src/ncpdp/script.ts @@ -152,7 +152,7 @@ const handleRemsRequest = async (message: any, res: Response) => { } // Requirements not met - denial with reason code - const reasonCode = determineReasonCodes(outstandingRequirements); + const reasonCode = determineReasonCode(outstandingRequirements); const reasonText = buildReasonText(reasonCode); const denialDetails = { @@ -253,7 +253,7 @@ const handleRemsInitiation = async (message: any, res: Response) => { } if (outstandingRequirements.length > 0) { - const reasonCode = determineReasonCodes(outstandingRequirements); + const reasonCode = determineReasonCode(outstandingRequirements); const reasonText = buildReasonText(reasonCode); logger.info(`Requirements not met - closing with: ${reasonCode}`); @@ -341,7 +341,7 @@ const handleRxFill = async (message: any, res: Response) => { }; -const determineReasonCodes = (outstandingRequirements: any[]): string => { +const determineReasonCode = (outstandingRequirements: any[]): string => { let hasPatientReq = false; let hasPrescriberReq = false; let hasPharmacyReq = false; @@ -397,10 +397,9 @@ const buildApprovedResponse = ( ): string => { const builder = new Builder({ headless: false }); - // Handle both capitalized and lowercased keys from parsed XML const patient = request.patient; - const pharmacy = request.pharmacy; - const prescriber = request.prescriber; + const pharmacy = request.pharmacy; + const prescriber = request.prescriber; const medicationPrescribed = request.medicationprescribed; const remsReferenceID = request.remsreferenceid; @@ -452,12 +451,12 @@ const buildDeniedResponse = ( ): string => { const builder = new Builder({ headless: false }); - const patient = request.patient; - const pharmacy = request.pharmacy; - const prescriber = request.prescriber; - const medicationPrescribed = request.medicationprescribed; + 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 solicitedModel = request.request?.solicitedmodel; const caseId = solicitedModel?.remscaseid; const response = { @@ -505,11 +504,11 @@ const buildInitiationClosedResponse = ( ): string => { const builder = new Builder({ headless: false }); - const patient = request.patient; - const pharmacy = request.pharmacy; + const patient = request.patient; + const pharmacy = request.pharmacy; const prescriber = request.prescriber; - const medicationPrescribed = request.medicationprescribed; - const remsReferenceID = request.remsreferenceid; + const medicationPrescribed = request.medicationprescribed; + const remsReferenceID = request.remsreferenceid; const response = { Message: { @@ -550,7 +549,7 @@ const buildInitiationClosedResponse = ( const buildInitiationSuccessResponse = (header: any, request: any, remsCase: any): string => { const builder = new Builder({ headless: false }); - const patient = request.patient; + const patient = request.patient; const humanPatient = patient?.humanpatient; const pharmacy = request.pharmacy; const prescriber = request.prescriber; @@ -580,14 +579,14 @@ const buildInitiationSuccessResponse = (header: any, request: any, remsCase: any Identification: { REMSPatientID: remsCase.remsPatientId || remsCase.case_number }, - Names: humanPatient?.Names || humanPatient?.names, - GenderAndSex: humanPatient?.GenderAndSex || humanPatient?.genderandsex, - DateOfBirth: humanPatient?.DateOfBirth || humanPatient?.dateofbirth, + Names: humanPatient?.names, + GenderAndSex: humanPatient?.genderandsex, + DateOfBirth: humanPatient?.dateofbirth, Address: { $: { 'xsi:type': 'MandatoryAddressType' }, - ...(humanPatient?.Address || humanPatient?.address) + ...humanPatient?.address } } },