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 b94727b..218052e 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,33 @@ 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[]; } @@ -46,6 +65,7 @@ const medicationCollectionSchema = new Schema({ name: { type: String }, codeSystem: { type: String }, code: { type: String }, + ndcCode: { type: String }, requirements: [ { name: { type: String }, @@ -61,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', @@ -89,6 +111,7 @@ export const metRequirementsCollection = model( const remsCaseCollectionSchema = new Schema({ case_number: { type: String }, + remsPatientId: { type: String }, status: { type: String }, dispenseStatus: { type: String }, drugName: { type: String }, @@ -96,6 +119,23 @@ 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 }], + 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: [ { metRequirementId: { type: String }, @@ -107,4 +147,18 @@ 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); 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 7646a44..23269ad 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, handleStakeholderChangesAndRecordEvent } 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,82 @@ 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 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) { + 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( @@ -494,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; }; @@ -594,6 +671,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 +690,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 @@ -752,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({ @@ -794,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; }; @@ -836,7 +919,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; diff --git a/src/lib/communication.ts b/src/lib/communication.ts new file mode 100644 index 0000000..2f7a62c --- /dev/null +++ b/src/lib/communication.ts @@ -0,0 +1,182 @@ +import { Communication, Task, Patient, MedicationRequest } from 'fhir/r4'; +import axios from 'axios'; +import config from '../config'; +import { uid } from 'uid'; +import container from './winston'; +import { createQuestionnaireCompletionTask } from '../hooks/hookResources'; +import { Requirement } from '../fhir/models'; + +const logger = container.get('application'); + +export async function sendCommunicationToEHR( + remsCase: any, + medication: any, + outstandingRequirements: any[] +): Promise { + try { + logger.info(`Creating Communication for case ${remsCase.case_number}`); + + // Create patient object from REMS case + const patient: Patient = { + resourceType: 'Patient', + id: `${remsCase.patientFirstName}-${remsCase.patientLastName}`.replace(/\s+/g, '-'), + name: [ + { + given: [remsCase.patientFirstName], + family: remsCase.patientLastName + } + ], + birthDate: remsCase.patientDOB + }; + + // Get the stored MedicationRequest reference + const medicationRequestRef = remsCase.medicationRequestReference; + + // Create a minimal MedicationRequest for task context + const medicationRequest: MedicationRequest = { + resourceType: 'MedicationRequest', + status: 'active', + intent: 'order', + medicationCodeableConcept: { + coding: [ + { + system: 'http://www.nlm.nih.gov/research/umls/rxnorm', + code: remsCase.drugCode, + display: remsCase.drugName + } + ] + }, + subject: { + reference: `Patient/${patient.id}` + }, + requester: { + reference: remsCase.metRequirements.find((mr: any) => + mr.requirementName?.toLowerCase().includes('prescriber') + )?.stakeholderId + } + }; + + // Create Tasks for each outstanding requirement + const tasks: Task[] = []; + for (const outstandingReq of outstandingRequirements) { + const requirement = + outstandingReq.requirement || + medication.requirements.find((r: Requirement) => r.name === outstandingReq.name); + + if (requirement && requirement.appContext) { + const questionnaireUrl = requirement.appContext; + const task = createQuestionnaireCompletionTask( + requirement, + patient, + questionnaireUrl, + medicationRequest + ); + task.id = `task-${uid()}`; + tasks.push(task); + } + } + + // Create Communication resource + const communication: Communication = { + resourceType: 'Communication', + id: `comm-${uid()}`, + status: 'completed', + category: [ + { + coding: [ + { + system: 'http://terminology.hl7.org/CodeSystem/communication-category', + code: 'notification', + display: 'Notification' + } + ] + } + ], + priority: 'urgent', + subject: { + reference: `Patient/${patient.id}`, + display: `${remsCase.patientFirstName} ${remsCase.patientLastName}` + }, + topic: { + coding: [ + { + system: 'http://terminology.hl7.org/CodeSystem/communication-topic', + code: 'progress-update', + display: 'Progress Update' + } + ], + text: 'Outstanding REMS Requirements for Medication Dispensing' + }, + sent: new Date().toISOString(), + recipient: [ + { + reference: medicationRequest.requester?.reference || '' + } + ], + sender: { + reference: 'Organization/rems-admin', + display: config.server?.name || 'REMS Administrator' + }, + payload: [ + { + contentString: + `Medication dispensing authorization DENIED for ${remsCase.drugName}.\n\n` + + 'The following REMS requirements must be completed:\n\n' + + outstandingRequirements + .map((req, idx) => `${idx + 1}. ${req.name} (${req.stakeholder})`) + .join('\n') + + `\n\nCase Number: ${remsCase.case_number}\n` + + `Patient: ${remsCase.patientFirstName} ${remsCase.patientLastName} (DOB: ${remsCase.patientDOB})` + } + ], + contained: tasks, + about: [ + { + reference: medicationRequestRef, + display: `Prescription for ${remsCase.drugName}` + }, + ...tasks.map(task => ({ + reference: `#${task.id}`, + display: task.description + })) + ] + }; + + // Determine EHR endpoint: use originatingFhirServer if available, otherwise default + 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' + } + }); + + if (response.status === 200 || response.status === 201) { + logger.info(`Communication successfully sent to EHR for case ${remsCase.case_number}`); + } else { + logger.warn(`Unexpected response status from EHR: ${response.status}`); + } + } catch (error: any) { + logger.error(`Failed to send Communication to EHR: ${error.message}`); + throw error; + } +} diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index 63ef37b..341d91e 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -183,6 +183,293 @@ 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(); + + // 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: medicationData?.code + }); + + if (existingCase) { + console.log( + `Case already exists for patient ${patientFirstName} ${patientLastName} and drug ${medicationData?.name}` + ); + return existingCase; + } + + // Create new case with all requirements pending + const remsRequest: Pick< + RemsCase, + | 'case_number' + | 'status' + | 'dispenseStatus' + | 'drugName' + | 'drugCode' + | 'drugNdcCode' + | 'patientFirstName' + | 'patientLastName' + | 'patientDOB' + | 'medicationRequestReference' + | 'currentPrescriberId' + | 'currentPharmacyId' + | 'prescriberHistory' + | 'pharmacyHistory' + | 'prescriptionEvents' + | 'metRequirements' + > & { originatingFhirServer?: string } = { + case_number: case_number, + status: 'Pending', + dispenseStatus: 'Pending', + 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 medicationData.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: medicationData?.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: medicationData?.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; +}; + +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); }; @@ -210,12 +497,108 @@ const createMetRequirementAndNewCase = async ( reqStakeholderReference: string, practitionerReference: string, pharmacistReference: string, - patientReference: string + patientReference: string, + medicationRequestReference: string, + originatingFhirServer?: string ) => { const patientFirstName = patient.name?.[0].given?.[0] || ''; const patientLastName = patient.name?.[0].family || ''; 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: medicationData?.code + }); + + if (existingCase) { + // 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: medicationData?.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 && + req?.stakeholderId === matchedMetReq.stakeholderId + ) { + 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 @@ -228,19 +611,43 @@ 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: [] }; @@ -250,7 +657,7 @@ const createMetRequirementAndNewCase = async ( completedQuestionnaire: questionnaireResponse, requirementName: requirement.name, requirementDescription: requirement.description, - drugName: drug?.name, + drugName: medicationData?.name, stakeholderId: reqStakeholderReference, case_numbers: [case_number] }; @@ -262,7 +669,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) { @@ -279,7 +686,7 @@ const createMetRequirementAndNewCase = async ( .findOne({ stakeholderId: reqStakeholder2Reference, requirementName: requirement2.name, - drugName: drug?.name + drugName: medicationData?.name }) .exec(); if (matchedMetReq2) { @@ -296,7 +703,7 @@ const createMetRequirementAndNewCase = async ( completed: false, requirementName: requirement2.name, requirementDescription: requirement2.description, - drugName: drug?.name, + drugName: medicationData?.name, stakeholderId: reqStakeholder2Reference, case_numbers: [case_number] }; @@ -366,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 } } @@ -575,7 +982,8 @@ export const processQuestionnaireResponseSubmission = async (requestBody: Bundle stakeholderReference, practitionerReference, pharmacistReference, - patientReference + patientReference, + prescriptionReference ); } else { // If it's not the patient status requirement diff --git a/src/lib/winston.ts b/src/lib/winston.ts index 975e568..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,27 +15,25 @@ const applicationTransports = []; // Create a console transport const transportConsole = new transports.Console({ level: logConfig.level, - format: winston.format.combine( - winston.format.prettyPrint(), - winston.format.json(), - winston.format.splat() - ) + format: winston.format.combine(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 }); diff --git a/src/ncpdp/script.ts b/src/ncpdp/script.ts index df4c210..7bfbdad 100644 --- a/src/ncpdp/script.ts +++ b/src/ncpdp/script.ts @@ -1,40 +1,649 @@ -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); + 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; - - 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 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))}`); + } + + 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 + }; + + 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); +}; + +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; diff --git a/src/server.ts b/src/server.ts index b5ad26b..d75e5c6 100644 --- a/src/server.ts +++ b/src/server.ts @@ -133,7 +133,7 @@ class REMSServer extends Server { } }) ); - this.app.use('/', Ncpdp); + this.app.use('/ncpdp/script', Ncpdp); return this; }