diff --git a/.env b/.env index 3ae3aff..34a5e1b 100644 --- a/.env +++ b/.env @@ -12,6 +12,7 @@ VSAC_API_KEY = changeMe WHITELIST = * SERVER_NAME = CodeX REMS Administrator Prototype FULL_RESOURCE_IN_APP_CONTEXT = false +DOCKERED_EHR_CONTAINER_NAME = false #Frontend Vars FRONTEND_PORT=9090 diff --git a/README.md b/README.md index 22421b3..c4219f8 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,8 @@ Following are a list of modifiable paths: | WHITELIST | `http://localhost, http://localhost:3005` | List of valid URLs for CORS. Should include any URLs the server accesses for resources. | | SERVER_NAME | `CodeX REMS Administrator Prototype` | Name of the server that is returned in the card source. | | FULL_RESOURCE_IN_APP_CONTEXT | 'false' | If true, the entire order resource will be included in the appContext, otherwise only a reference will be. | +| DOCKERED_EHR_CONTAINER_NAME | '' | String of the EHR container name for local docker networking communication | + | FRONTEND_PORT | `9080` | Port that the frontend server should run on, change if there are conflicts with port usage. | | VITE_REALM | `ClientFhirServer` | Keycloak realm for frontend authentication. | | VITE_AUTH | `http://localhost:8180` | Keycloak authentication server URL for frontend. | diff --git a/src/config.ts b/src/config.ts index de555c5..eedc332 100644 --- a/src/config.ts +++ b/src/config.ts @@ -41,7 +41,8 @@ export default { fhirServerConfig: { auth: { // This server's URI - resourceServer: env.get('RESOURCE_SERVER').required().asUrlString() + resourceServer: env.get('RESOURCE_SERVER').required().asUrlString(), + dockered_ehr_container_name: env.get('DOCKERED_EHR_CONTAINER_NAME').asString() // // if you use this strategy, you need to add the corresponding env vars to docker-compose // diff --git a/src/fhir/models.ts b/src/fhir/models.ts index 4873016..d997b57 100644 --- a/src/fhir/models.ts +++ b/src/fhir/models.ts @@ -5,7 +5,7 @@ export interface Requirement { name: string; description: string; questionnaire: Questionnaire | null; - stakeholderType: 'patient' | 'prescriber' | 'pharmacist' | string; // From fhir4.Parameters.parameter.name + stakeholderType: 'patient' | 'prescriber' | 'pharmacist' | string; createNewCase: boolean; resourceId: string; requiredToDispense: boolean; @@ -15,7 +15,8 @@ export interface Requirement { export interface Medication extends Document { name: string; codeSystem: string; - code: string; + code: string; // RxNorm code (used for CDS Hooks) + ndcCode: string; // NDC code (used for NCPDP SCRIPT) requirements: Requirement[]; } @@ -30,15 +31,31 @@ export interface MetRequirements extends Document { metRequirementId: any; } +export interface PrescriptionEvent { + medicationRequestReference: string; + prescriberId: string; + pharmacyId?: string; + timestamp: Date; + originatingFhirServer?: string; + caseStatusAtTime: string; +} + export interface RemsCase extends Document { case_number: string; + remsPatientId?: string; status: string; dispenseStatus: string; drugName: string; drugCode: string; + drugNdcCode?: string; patientFirstName: string; patientLastName: string; patientDOB: string; + currentPrescriberId?: string; + currentPharmacyId?: string; + prescriberHistory: string[]; + pharmacyHistory: string[]; + prescriptionEvents: PrescriptionEvent[]; medicationRequestReference?: string; originatingFhirServer?: string; metRequirements: Partial[]; @@ -48,6 +65,7 @@ const medicationCollectionSchema = new Schema({ name: { type: String }, codeSystem: { type: String }, code: { type: String }, + ndcCode: { type: String }, requirements: [ { name: { type: String }, @@ -63,6 +81,8 @@ const medicationCollectionSchema = new Schema({ }); medicationCollectionSchema.index({ name: 1 }, { unique: true }); +medicationCollectionSchema.index({ code: 1 }); +medicationCollectionSchema.index({ ndcCode: 1 }); export const medicationCollection = model( 'medicationCollection', @@ -91,13 +111,29 @@ export const metRequirementsCollection = model( const remsCaseCollectionSchema = new Schema({ case_number: { type: String }, + remsPatientId: { type: String }, status: { type: String }, dispenseStatus: { type: String }, drugName: { type: String }, patientFirstName: { type: String }, patientLastName: { type: String }, patientDOB: { type: String }, - drugCode: { type: String }, + drugCode: { type: String }, + drugNdcCode: { type: String }, + currentPrescriberId: { type: String }, + currentPharmacyId: { type: String }, + prescriberHistory: [{ type: String }], + pharmacyHistory: [{ type: String }], + prescriptionEvents: [ + { + medicationRequestReference: { type: String }, + prescriberId: { type: String }, + pharmacyId: { type: String }, + timestamp: { type: Date }, + originatingFhirServer: { type: String }, + caseStatusAtTime: { type: String } + } + ], medicationRequestReference: { type: String }, originatingFhirServer: { type: String }, metRequirements: [ @@ -111,4 +147,12 @@ const remsCaseCollectionSchema = new Schema({ ] }); +remsCaseCollectionSchema.index( + { patientFirstName: 1, patientLastName: 1, patientDOB: 1, drugNdcCode: 1 } +); + +remsCaseCollectionSchema.index( + { patientFirstName: 1, patientLastName: 1, patientDOB: 1, drugCode: 1 } +); + export const remsCaseCollection = model('RemsCaseCollection', remsCaseCollectionSchema); \ No newline at end of file diff --git a/src/fhir/utilities.ts b/src/fhir/utilities.ts index 72e66b3..eba7e77 100644 --- a/src/fhir/utilities.ts +++ b/src/fhir/utilities.ts @@ -127,6 +127,7 @@ export class FhirUtilities { name: 'Turalio', codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '2183126', + ndcCode: '65597-407-20', requirements: [ { name: 'Patient Enrollment', @@ -196,6 +197,7 @@ export class FhirUtilities { name: 'TIRF', codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '1237051', + ndcCode: '63459-502-30', requirements: [ { name: 'Patient Enrollment', @@ -262,6 +264,7 @@ export class FhirUtilities { name: 'Isotretinoin', codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '6064', + ndcCode: '0245-0571-01', requirements: [ { name: 'Patient Enrollment', @@ -305,6 +308,7 @@ export class FhirUtilities { name: 'Addyi', codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm', code: '1666386', + ndcCode: '58604-214-30', requirements: [] } ]; diff --git a/src/hooks/hookResources.ts b/src/hooks/hookResources.ts index 0787b5d..677afc0 100644 --- a/src/hooks/hookResources.ts +++ b/src/hooks/hookResources.ts @@ -24,7 +24,7 @@ import { import axios from 'axios'; import { ServicePrefetch } from '../rems-cds-hooks/resources/CdsService'; import { hydrate } from '../rems-cds-hooks/prefetch/PrefetchHydrator'; -import { createNewRemsCaseFromCDSHook } from '../lib/etasu'; +import { createNewRemsCaseFromCDSHook, handleStakeholderChangesAndRecordEvent } from '../lib/etasu'; type HandleCallback = ( res: any, @@ -406,11 +406,50 @@ export const handleCardOrder = async ( drugCode: code }); + // If case exists, check for stakeholder changes and record prescription event + if (remsCase && drug && fhirServer) { + const practitionerReference = request.requester?.reference || ''; + const pharmacistReference = pharmacy?.id ? `HealthcareService/${pharmacy.id}` : ''; + const medicationRequestReference = `${request.resourceType}/${request.id}`; + + const prescriberChanged = remsCase.currentPrescriberId !== practitionerReference; + const pharmacyChanged = + pharmacistReference && remsCase.currentPharmacyId !== pharmacistReference; + + if (prescriberChanged || pharmacyChanged) { + try { + const updatedCase = await handleStakeholderChangesAndRecordEvent( + remsCase, + drug, + practitionerReference, + pharmacistReference, + medicationRequestReference, + fhirServer + ); + console.log(`Updated case ${updatedCase?.case_number} with stakeholder changes`); + } catch (error) { + console.error('Failed to handle stakeholder changes:', error); + } + } else { + // Record prescription event even if no stakeholder change + remsCase.prescriptionEvents.push({ + medicationRequestReference: medicationRequestReference, + prescriberId: practitionerReference, + pharmacyId: pharmacistReference, + timestamp: new Date(), + originatingFhirServer: fhirServer, + caseStatusAtTime: remsCase.status + }); + remsCase.medicationRequestReference = medicationRequestReference; + await remsCase.save(); + } + } + // If no REMS case exists and drug has requirements, create case with all requirements unmet if (!remsCase && drug && patient && request) { const requiresCase = drug.requirements.some(req => req.requiredToDispense); - if (requiresCase && fhirServer) { + if (requiresCase && fhirServer) { try { const patientReference = `Patient/${patient.id}`; const medicationRequestReference = `${request.resourceType}/${request.id}`; @@ -527,6 +566,11 @@ const getCardOrEmptyArrayFromRules = const notFound = remsCase && !metRequirement; const noEtasuToCheckAndRequiredToDispense = !remsCase && requirement.requiredToDispense; + // Only show forms that are not required to dispense (like patient status) if case is approved + if (!requirement.requiredToDispense && remsCase && remsCase.status !== 'Approved') { + return false; + } + return formNotProcessed || notFound || noEtasuToCheckAndRequiredToDispense; }; @@ -786,7 +830,7 @@ const containsMatchingMedicationRequest = const getCardOrEmptyArrayFromCases = (entries: BundleEntry[] | undefined) => - async ({ drugCode, drugName, metRequirements }: RemsCase): Promise => { + async ({ drugCode, drugName, metRequirements, status }: RemsCase): Promise => { // find the drug in the medicationCollection that matches the REMS case to get the smart links const drug = await medicationCollection .findOne({ @@ -828,6 +872,11 @@ const getCardOrEmptyArrayFromCases = const formNotProcessed = metRequirement && !metRequirement.completed; const notFound = !metRequirement; + // Only show forms that are not required to dispense (like patient status) if case is approved + if (!requirement.requiredToDispense && status !== 'Approved') { + return false; + } + return formNotProcessed || notFound; }; diff --git a/src/lib/communication.ts b/src/lib/communication.ts index 55c4c4f..3039fb6 100644 --- a/src/lib/communication.ts +++ b/src/lib/communication.ts @@ -8,7 +8,6 @@ import { Requirement } from '../fhir/models'; const logger = container.get('application'); - export async function sendCommunicationToEHR( remsCase: any, medication: any, @@ -16,7 +15,7 @@ export async function sendCommunicationToEHR( ): Promise { try { logger.info(`Creating Communication for case ${remsCase.case_number}`); - + // Create patient object from REMS case const patient: Patient = { resourceType: 'Patient', @@ -60,9 +59,10 @@ export async function sendCommunicationToEHR( // Create Tasks for each outstanding requirement const tasks: Task[] = []; for (const outstandingReq of outstandingRequirements) { - const requirement = outstandingReq.requirement || + const requirement = + outstandingReq.requirement || medication.requirements.find((r: Requirement) => r.name === outstandingReq.name); - + if (requirement && requirement.appContext) { const questionnaireUrl = requirement.appContext; const task = createQuestionnaireCompletionTask( @@ -80,7 +80,7 @@ export async function sendCommunicationToEHR( const communication: Communication = { resourceType: 'Communication', id: `comm-${uid()}`, - status: 'completed', + status: 'completed', category: [ { coding: [ @@ -92,7 +92,7 @@ export async function sendCommunicationToEHR( ] } ], - priority: 'urgent', + priority: 'urgent', subject: { reference: `Patient/${patient.id}`, display: `${remsCase.patientFirstName} ${remsCase.patientLastName}` @@ -107,7 +107,7 @@ export async function sendCommunicationToEHR( ], text: 'Outstanding REMS Requirements for Medication Dispensing' }, - sent: new Date().toISOString(), + sent: new Date().toISOString(), recipient: [ { reference: medicationRequest.requester?.reference || '' @@ -119,8 +119,9 @@ export async function sendCommunicationToEHR( }, payload: [ { - contentString: `Medication dispensing authorization DENIED for ${remsCase.drugName}.\n\n` + - `The following REMS requirements must be completed:\n\n` + + contentString: + `Medication dispensing authorization DENIED for ${remsCase.drugName}.\n\n` + + 'The following REMS requirements must be completed:\n\n' + outstandingRequirements .map((req, idx) => `${idx + 1}. ${req.name} (${req.stakeholder})`) .join('\n') + @@ -142,17 +143,24 @@ export async function sendCommunicationToEHR( }; // Determine EHR endpoint: use originatingFhirServer if available, otherwise default - const ehrEndpoint = remsCase.originatingFhirServer || - config.fhirServerConfig?.auth?.resourceServer; + let ehrEndpoint = + remsCase.originatingFhirServer || config.fhirServerConfig?.auth?.resourceServer; if (!ehrEndpoint) { logger.warn('No EHR endpoint configured, Communication not sent'); return; } + if (config.fhirServerConfig.auth.dockered_ehr_container_name) { + const originalEhrEndpoint = ehrEndpoint; + ehrEndpoint = originalEhrEndpoint.replace(/localhost/g, config.fhirServerConfig.auth.dockered_ehr_container_name) + .replace(/127\.0\.0\.1/g, config.fhirServerConfig.auth.dockered_ehr_container_name); + logger.info(`Running locally in Docker, converting EHR url from ${originalEhrEndpoint} to ${ehrEndpoint}`); + } + // Send Communication to EHR logger.info(`Sending Communication to EHR: ${ehrEndpoint}`); - + const response = await axios.post(`${ehrEndpoint}/Communication`, communication, { headers: { 'Content-Type': 'application/fhir+json' @@ -164,9 +172,8 @@ export async function sendCommunicationToEHR( } else { logger.warn(`Unexpected response status from EHR: ${response.status}`); } - } catch (error: any) { logger.error(`Failed to send Communication to EHR: ${error.message}`); - throw error; + throw error; } -} \ No newline at end of file +} diff --git a/src/lib/etasu.ts b/src/lib/etasu.ts index 305c4cc..41d92e5 100644 --- a/src/lib/etasu.ts +++ b/src/lib/etasu.ts @@ -197,16 +197,25 @@ export const createNewRemsCaseFromCDSHook = async ( const patientDOB = patient.birthDate || ''; const case_number = uid(); + // Fetch the full medication from database to get NDC code + const fullMedication = await medicationCollection.findOne({ + code: drug?.code + }).exec(); + + const medicationData = fullMedication || drug; + // Check if case already exists const existingCase = await remsCaseCollection.findOne({ patientFirstName: patientFirstName, patientLastName: patientLastName, patientDOB: patientDOB, - drugCode: drug?.code + drugCode: medicationData?.code }); if (existingCase) { - console.log(`Case already exists for patient ${patientFirstName} ${patientLastName} and drug ${drug?.name}`); + console.log( + `Case already exists for patient ${patientFirstName} ${patientLastName} and drug ${medicationData?.name}` + ); return existingCase; } @@ -218,27 +227,48 @@ export const createNewRemsCaseFromCDSHook = async ( | 'dispenseStatus' | 'drugName' | 'drugCode' + | 'drugNdcCode' | 'patientFirstName' | 'patientLastName' | 'patientDOB' | 'medicationRequestReference' + | 'currentPrescriberId' + | 'currentPharmacyId' + | 'prescriberHistory' + | 'pharmacyHistory' + | 'prescriptionEvents' | 'metRequirements' > & { originatingFhirServer?: string } = { case_number: case_number, - status: 'Pending', // All requirements unmet, so status is Pending + status: 'Pending', dispenseStatus: 'Pending', - drugName: drug?.name, - drugCode: drug?.code, + drugName: medicationData?.name, + drugCode: medicationData?.code, + drugNdcCode: medicationData?.ndcCode, patientFirstName: patientFirstName, patientLastName: patientLastName, patientDOB: patientDOB, medicationRequestReference: medicationRequestReference, + currentPrescriberId: practitionerReference, + currentPharmacyId: pharmacistReference, + prescriberHistory: [practitionerReference], + pharmacyHistory: pharmacistReference ? [pharmacistReference] : [], + prescriptionEvents: [ + { + medicationRequestReference: medicationRequestReference, + prescriberId: practitionerReference, + pharmacyId: pharmacistReference, + timestamp: new Date(), + originatingFhirServer: originatingFhirServer, + caseStatusAtTime: 'Pending' + } + ], originatingFhirServer: originatingFhirServer, metRequirements: [] }; // Iterate through ALL requirements and create as unmet (or link to existing if already completed) - for (const requirement of drug.requirements) { + for (const requirement of medicationData.requirements) { // Only process requirements that are required to dispense if (requirement.requiredToDispense) { // Figure out which stakeholder the requirement corresponds to @@ -255,7 +285,7 @@ export const createNewRemsCaseFromCDSHook = async ( .findOne({ stakeholderId: stakeholderReference, requirementName: requirement.name, - drugName: drug?.name + drugName: medicationData?.name }) .exec(); @@ -270,7 +300,7 @@ export const createNewRemsCaseFromCDSHook = async ( completed: false, requirementName: requirement.name, requirementDescription: requirement.description, - drugName: drug?.name, + drugName: medicationData?.name, stakeholderId: stakeholderReference, case_numbers: [case_number] }; @@ -283,13 +313,161 @@ export const createNewRemsCaseFromCDSHook = async ( } // Save the new case - remsRequest.status = remsRequest.metRequirements.every(req => req.completed) ? 'Approved' : 'Pending'; + remsRequest.status = remsRequest.metRequirements.every(req => req.completed) + ? 'Approved' + : 'Pending'; const newCase = await remsCaseCollection.create(remsRequest); - - console.log(`Created new REMS case ${case_number} with all requirements unmet (or linked to existing)`); + + console.log( + `Created new REMS case ${case_number} with all requirements unmet (or linked to existing)` + ); return newCase; }; +export const handleStakeholderChangesAndRecordEvent = async ( + remsCase: RemsCase, + drug: Medication, + practitionerReference: string, + pharmacistReference: string, + medicationRequestReference: string, + originatingFhirServer?: string +) => { + let stakeholdersChanged = false; + + // Record prescription event + const prescriptionEvent = { + medicationRequestReference: medicationRequestReference, + prescriberId: practitionerReference, + pharmacyId: pharmacistReference, + timestamp: new Date(), + originatingFhirServer: originatingFhirServer, + caseStatusAtTime: remsCase.status + }; + remsCase.prescriptionEvents.push(prescriptionEvent); + remsCase.medicationRequestReference = medicationRequestReference; + if (originatingFhirServer) { + remsCase.originatingFhirServer = originatingFhirServer; + } + + // Check if prescriber changed + if (remsCase.currentPrescriberId !== practitionerReference) { + console.log( + `Prescriber changed from ${remsCase.currentPrescriberId} to ${practitionerReference}` + ); + stakeholdersChanged = true; + + // Remove old prescriber requirements + remsCase.metRequirements = remsCase.metRequirements.filter( + req => + req.stakeholderId !== remsCase.currentPrescriberId || + !drug.requirements.some( + r => r.name === req.requirementName && r.stakeholderType === 'prescriber' + ) + ); + + // Add new prescriber requirements + const prescriberRequirements = drug.requirements.filter( + r => r.stakeholderType === 'prescriber' + ); + for (const requirement of prescriberRequirements) { + if (requirement.requiredToDispense) { + const existingMetReq = await metRequirementsCollection + .findOne({ + stakeholderId: practitionerReference, + requirementName: requirement.name, + drugName: drug?.name + }) + .exec(); + + if (existingMetReq) { + pushMetRequirements(existingMetReq, remsCase); + if (!existingMetReq.case_numbers.includes(remsCase.case_number)) { + existingMetReq.case_numbers.push(remsCase.case_number); + await existingMetReq.save(); + } + } else { + const newMetReq = { + completed: false, + requirementName: requirement.name, + requirementDescription: requirement.description, + drugName: drug?.name, + stakeholderId: practitionerReference, + case_numbers: [remsCase.case_number] + }; + await createAndPushMetRequirements(newMetReq, remsCase); + } + } + } + + // Update prescriber tracking + remsCase.currentPrescriberId = practitionerReference; + if (!remsCase.prescriberHistory.includes(practitionerReference)) { + remsCase.prescriberHistory.push(practitionerReference); + } + } + + // Check if pharmacy changed + if (pharmacistReference && remsCase.currentPharmacyId !== pharmacistReference) { + console.log(`Pharmacy changed from ${remsCase.currentPharmacyId} to ${pharmacistReference}`); + stakeholdersChanged = true; + + // Remove old pharmacy requirements + remsCase.metRequirements = remsCase.metRequirements.filter( + req => + req.stakeholderId !== remsCase.currentPharmacyId || + !drug.requirements.some( + r => r.name === req.requirementName && r.stakeholderType === 'pharmacist' + ) + ); + + // Add new pharmacy requirements + const pharmacyRequirements = drug.requirements.filter(r => r.stakeholderType === 'pharmacist'); + for (const requirement of pharmacyRequirements) { + if (requirement.requiredToDispense) { + const existingMetReq = await metRequirementsCollection + .findOne({ + stakeholderId: pharmacistReference, + requirementName: requirement.name, + drugName: drug?.name + }) + .exec(); + + if (existingMetReq) { + pushMetRequirements(existingMetReq, remsCase); + if (!existingMetReq.case_numbers.includes(remsCase.case_number)) { + existingMetReq.case_numbers.push(remsCase.case_number); + await existingMetReq.save(); + } + } else { + const newMetReq = { + completed: false, + requirementName: requirement.name, + requirementDescription: requirement.description, + drugName: drug?.name, + stakeholderId: pharmacistReference, + case_numbers: [remsCase.case_number] + }; + await createAndPushMetRequirements(newMetReq, remsCase); + } + } + } + + // Update pharmacy tracking + remsCase.currentPharmacyId = pharmacistReference; + if (!remsCase.pharmacyHistory.includes(pharmacistReference)) { + remsCase.pharmacyHistory.push(pharmacistReference); + } + } + + // Recalculate status if stakeholders changed + if (stakeholdersChanged) { + remsCase.status = remsCase.metRequirements.every(req => req.completed) ? 'Approved' : 'Pending'; + } + + await remsCase.save(); + return remsCase; +}; + const createMetRequirements = async (metReq: Partial) => { return await metRequirementsCollection.create(metReq); }; @@ -326,24 +504,49 @@ const createMetRequirementAndNewCase = async ( const patientDOB = patient.birthDate || ''; let message = ''; - // Check if case already exists + // Fetch the full medication from database to get NDC code + const fullMedication = await medicationCollection.findOne({ + code: drug?.code + }).exec(); + + const medicationData = fullMedication || drug; + + // Check if case already exists const existingCase = await remsCaseCollection.findOne({ patientFirstName: patientFirstName, patientLastName: patientLastName, patientDOB: patientDOB, - drugCode: drug?.code + drugCode: medicationData?.code }); if (existingCase) { - // Case already exists - update the existing requirement instead of creating new case - console.log(`Case ${existingCase.case_number} already exists, updating requirement ${requirement.name}`); - + // Case already exists - check for stakeholder changes before updating requirement + console.log( + `Case ${existingCase.case_number} already exists, checking for stakeholder changes` + ); + + // Check if prescriber or pharmacy changed and handle accordingly + const prescriberChanged = existingCase.currentPrescriberId !== practitionerReference; + const pharmacyChanged = + pharmacistReference && existingCase.currentPharmacyId !== pharmacistReference; + + if (prescriberChanged || pharmacyChanged) { + await handleStakeholderChangesAndRecordEvent( + existingCase, + medicationData, + practitionerReference, + pharmacistReference, + medicationRequestReference, + originatingFhirServer + ); + } + // Find and update the existing MetRequirement const matchedMetReq = await metRequirementsCollection .findOne({ stakeholderId: reqStakeholderReference, requirementName: requirement.name, - drugName: drug?.name + drugName: medicationData?.name }) .exec(); @@ -356,10 +559,13 @@ const createMetRequirementAndNewCase = async ( // Update the case's metRequirements array const metReqArray = existingCase.metRequirements || []; let foundUncompleted = false; - + for (let i = 0; i < metReqArray.length; i++) { const req = existingCase.metRequirements[i]; - if (req?.requirementName === matchedMetReq.requirementName) { + if ( + req?.requirementName === matchedMetReq.requirementName && + req?.stakeholderId === matchedMetReq.stakeholderId + ) { metReqArray[i].completed = true; req!.completed = true; await remsCaseCollection.updateOne( @@ -390,7 +596,7 @@ const createMetRequirementAndNewCase = async ( // No existing case - create new one const case_number = uid(); - + // create new rems request and add the created metReq to it let remsRequestCompletedStatus = 'Approved'; const dispenseStatusDefault = 'Pending'; @@ -401,21 +607,42 @@ const createMetRequirementAndNewCase = async ( | 'dispenseStatus' | 'drugName' | 'drugCode' + | 'drugNdcCode' | 'patientFirstName' | 'patientLastName' | 'patientDOB' | 'medicationRequestReference' + | 'currentPrescriberId' + | 'currentPharmacyId' + | 'prescriberHistory' + | 'pharmacyHistory' + | 'prescriptionEvents' | 'metRequirements' > & { originatingFhirServer?: string } = { case_number: case_number, status: remsRequestCompletedStatus, dispenseStatus: dispenseStatusDefault, - drugName: drug?.name, - drugCode: drug?.code, + drugName: medicationData?.name, + drugCode: medicationData?.code, + drugNdcCode: medicationData?.ndcCode, patientFirstName: patientFirstName, patientLastName: patientLastName, patientDOB: patientDOB, medicationRequestReference: medicationRequestReference, + currentPrescriberId: practitionerReference, + currentPharmacyId: pharmacistReference, + prescriberHistory: [practitionerReference], + pharmacyHistory: pharmacistReference ? [pharmacistReference] : [], + prescriptionEvents: [ + { + medicationRequestReference: medicationRequestReference, + prescriberId: practitionerReference, + pharmacyId: pharmacistReference, + timestamp: new Date(), + originatingFhirServer: originatingFhirServer, + caseStatusAtTime: remsRequestCompletedStatus + } + ], originatingFhirServer: originatingFhirServer, metRequirements: [] }; @@ -426,7 +653,7 @@ const createMetRequirementAndNewCase = async ( completedQuestionnaire: questionnaireResponse, requirementName: requirement.name, requirementDescription: requirement.description, - drugName: drug?.name, + drugName: medicationData?.name, stakeholderId: reqStakeholderReference, case_numbers: [case_number] }; @@ -438,7 +665,7 @@ const createMetRequirementAndNewCase = async ( } // iterate through all other requirements again to create corresponding false metRequirements / assign to existing - for (const requirement2 of drug.requirements) { + for (const requirement2 of medicationData.requirements) { // skip if the req found is the same as in the outer loop and has already been processed // && If the requirement is not the patient Status Form (when requiredToDispense == false) if (!(requirement2.resourceId === requirement.resourceId) && requirement2.requiredToDispense) { @@ -455,7 +682,7 @@ const createMetRequirementAndNewCase = async ( .findOne({ stakeholderId: reqStakeholder2Reference, requirementName: requirement2.name, - drugName: drug?.name + drugName: medicationData?.name }) .exec(); if (matchedMetReq2) { @@ -472,7 +699,7 @@ const createMetRequirementAndNewCase = async ( completed: false, requirementName: requirement2.name, requirementDescription: requirement2.description, - drugName: drug?.name, + drugName: medicationData?.name, stakeholderId: reqStakeholder2Reference, case_numbers: [case_number] }; diff --git a/src/lib/winston.ts b/src/lib/winston.ts index 975e568..411f1b1 100644 --- a/src/lib/winston.ts +++ b/src/lib/winston.ts @@ -16,26 +16,30 @@ const applicationTransports = []; const transportConsole = new transports.Console({ level: logConfig.level, format: winston.format.combine( - winston.format.prettyPrint(), - winston.format.json(), - winston.format.splat() + winston.format.timestamp(), + winston.format.json() ) }); applicationTransports.push(transportConsole); + if (logConfig.directory) { const transportDailyFile = new transports.DailyRotateFile({ filename: path.join(logConfig.directory, 'application-%DATE%.log'), datePattern: 'YYYY-MM-DD-HH', level: logging.level, zippedArchive: true, - maxSize: '20m' + maxSize: '20m', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ) }); applicationTransports.push(transportDailyFile); } + // Add a default application logger container.add('application', { - format: format.combine(format.timestamp(), format.logstash()), transports: applicationTransports }); @@ -44,4 +48,4 @@ container.add('application', { * @static * @summary Logging container for the application */ -export default container; +export default container; \ No newline at end of file diff --git a/src/ncpdp/script.ts b/src/ncpdp/script.ts index df4c210..872d7ed 100644 --- a/src/ncpdp/script.ts +++ b/src/ncpdp/script.ts @@ -1,40 +1,659 @@ -import { Router, Request } from 'express'; -import { remsCaseCollection } from '../fhir/models'; +import { Router, Request, Response } from 'express'; +import { remsCaseCollection, medicationCollection } from '../fhir/models'; +import container from '../lib/winston'; +import { Builder } from 'xml2js'; +import { sendCommunicationToEHR } from '../lib/communication'; +import { v4 as uuidv4 } from 'uuid'; + const router = Router(); +const logger = container.get('application'); -router.post('/ncpdp/script', async (req: Request) => { +router.post('/', async (req: Request, res: Response) => { try { - const requestBody = req.body; - if (requestBody.message?.body?.rxfill) { - // call to handle rxfill - handleRxFill(requestBody); + const parsedMessage = req.body; + + logger.info('=== NCPDP Request Received ==='); + + const message = parsedMessage.message; + const body = message?.body; + + const messageInfo = { + hasRemsRequest: !!body?.remsrequest, + hasRemsInitiation: !!body?.remsinitiationrequest, + hasRxFill: !!body?.rxfill, + bodyKeys: body ? Object.keys(body).join(', ') : 'no body' + }; + logger.info(`Message type check: ${JSON.stringify(messageInfo)}`); + + if (body?.remsrequest) { + logger.info('Routing to handleRemsRequest'); + await handleRemsRequest(message, res); + } else if (body?.remsinitiationrequest) { + logger.info('Routing to handleRemsInitiation'); + await handleRemsInitiation(message, res); + } else if (body?.rxfill) { + logger.info('Routing to handleRxFill'); + await handleRxFill(message, res); } else { - // do nothing for now + logger.error('Unknown NCPDP message type'); + res.type('application/xml'); + res.status(400).send(buildErrorResponse('Unknown message type')); } - } catch (error) { - console.log(error); - throw error; + } catch (error: any) { + logger.error(`ERROR processing NCPDP message: ${error.message}`); + logger.error(`Stack: ${error.stack}`); + res.type('application/xml'); + res.status(500).send(buildErrorResponse('Internal server error')); } }); -const handleRxFill = async (bundle: any) => { - const rxfill = bundle.message?.body?.rxfill; - const fillStatus = rxfill?.fillstatus?.dispensed?.note; - const patient = rxfill?.patient; - const code = rxfill?.medicationprescribed?.drugcoded?.drugdbcode?.code; +const handleRemsRequest = async (message: any, res: Response) => { + try { + logger.info('--- handleRemsRequest started ---'); + + const header = message.header; + const remsRequest = message.body.remsrequest; + const caseId = remsRequest.request?.solicitedmodel?.remscaseid; + + logger.info(`Extracted case ID: ${caseId}`); + + + if (!caseId) { + logger.error('Case ID not provided in request'); + res.type('application/xml'); + return res.status(200).send(buildDeniedResponse(header, remsRequest, 'EC', 'Case ID not provided')); + } + + logger.info(`Looking up case: ${caseId}`); + + const remsCase = await remsCaseCollection.findOne({ case_number: caseId }); + + if (!remsCase) { + logger.error(`Case not found: ${caseId}`); + res.type('application/xml'); + return res.status(200).send(buildDeniedResponse(header, remsRequest, 'EC', 'Case not found')); + } + + const caseInfo = { + case_number: remsCase.case_number, + status: remsCase.status, + drugName: remsCase.drugName, + drugNdcCode: remsCase.drugNdcCode, + numRequirements: remsCase.metRequirements?.length + }; + logger.info(`Case found: ${JSON.stringify(caseInfo)}`); + + const medication = await medicationCollection.findOne({ + ndcCode: remsCase.drugNdcCode + }); + + if (!medication) { + logger.error(`Medication configuration not found for NDC: ${remsCase.drugNdcCode}`); + res.type('application/xml'); + return res.status(200).send(buildDeniedResponse(header, remsRequest, 'ER', 'Medication configuration error')); + } + + const medInfo = { + name: medication.name, + ndcCode: medication.ndcCode, + totalRequirements: medication.requirements.length, + requiredToDispense: medication.requirements.filter((r: any) => r.requiredToDispense).length + }; + logger.info(`Medication found: ${JSON.stringify(medInfo)}`); + + // Check if all requiredToDispense requirements are met + const requiredRequirements = medication.requirements.filter((req: any) => req.requiredToDispense); + const outstandingRequirements: any[] = []; + + logger.info('Checking requirements...'); + + for (const requirement of requiredRequirements) { + const matchingMetReq = remsCase.metRequirements?.find( + (mr: any) => mr.requirementName === requirement.name + ); + + const isComplete = matchingMetReq && matchingMetReq.completed; + logger.info(` Requirement: ${requirement.name} - ${isComplete ? 'COMPLETE' : 'INCOMPLETE'}`); + + if (!matchingMetReq || !matchingMetReq.completed) { + outstandingRequirements.push({ + name: requirement.name, + stakeholder: requirement.stakeholderType, + requirement: requirement + }); + } + } + + // If all requirements met, approve + if (outstandingRequirements.length === 0) { + logger.info('All requirements met - APPROVING'); + const authNumber = `RDA${Math.floor(Math.random() * 10000000)}`; + const today = new Date(); + const expirationDate = new Date(today); + expirationDate.setDate(expirationDate.getDate() + 7); + + const authDetails = { + authNumber, + effectiveDate: today.toISOString().split('T')[0], + expirationDate: expirationDate.toISOString().split('T')[0] + }; + logger.info(`Authorization details: ${JSON.stringify(authDetails)}`); + + res.type('application/xml'); + return res.status(200).send(buildApprovedResponse( + header, + remsRequest, + caseId, + authNumber, + today.toISOString().split('T')[0], + expirationDate.toISOString().split('T')[0] + )); + } + + // Requirements not met - denial with reason code + const reasonCode = determineReasonCode(outstandingRequirements); + const reasonText = buildReasonText(reasonCode); + + const denialDetails = { + reasonCode: reasonCode, + reasonText, + outstandingCount: outstandingRequirements.length + }; + logger.info(`Denial details: ${JSON.stringify(denialDetails)}`); + + // Send Communication to EHR with outstanding requirements + logger.info('Attempting to send Communication to EHR...'); + try { + await sendCommunicationToEHR(remsCase, medication, outstandingRequirements); + const ehrEndpoint = remsCase.originatingFhirServer || 'default server'; + logger.info(`Communication sent successfully to: ${ehrEndpoint}`); + } catch (commError: any) { + logger.error(`Failed to send Communication: ${commError.message}`); + } + + logger.info('Sending DENIED response'); + res.type('application/xml'); + return res.status(200).send(buildDeniedResponse(header, remsRequest, reasonCode, reasonText)); + + } catch (error: any) { + logger.error(`ERROR in handleRemsRequest: ${error.message}`); + logger.error(`Stack trace: ${error.stack}`); + res.type('application/xml'); + return res.status(500).send(buildErrorResponse(error.message)); + } +}; + + +const handleRemsInitiation = async (message: any, res: Response) => { + try { + logger.info('--- handleRemsInitiation started ---'); + const header = message.header; + const initRequest = message.body.remsinitiationrequest; + const patient = initRequest.patient?.humanpatient; + const prescriber = initRequest.prescriber?.nonveterinarian; + const pharmacy = initRequest.pharmacy; + const drugNdcCode = initRequest.medicationprescribed?.product?.drugcoded?.ndc; + + const requestInfo = { + patientName: `${patient?.names?.name?.firstname} ${patient?.names?.name?.lastname}`, + drugNdcCode: drugNdcCode + }; + logger.info(`REMS Initiation request: ${JSON.stringify(requestInfo)}`); + + const remsCase = await remsCaseCollection.findOne({ + patientFirstName: patient?.names?.name?.firstname, + patientLastName: patient?.names?.name?.lastname, + patientDOB: patient?.dateofbirth?.date, + drugNdcCode: drugNdcCode + }); + + if (!remsCase) { + logger.info('No case exists - patient must enroll'); + res.type('application/xml'); + return res.status(200).send(buildInitiationClosedResponse( + header, + initRequest, + 'EM', + 'Patient must enroll/certify' + )); + } + + // Case exists - check requirements + const medication = await medicationCollection.findOne({ + ndcCode: drugNdcCode + }); + + if (!medication) { + logger.error(`Medication not found for NDC: ${drugNdcCode}`); + res.type('application/xml'); + return res.status(200).send(buildInitiationClosedResponse( + header, + initRequest, + 'ER', + 'Medication configuration error' + )); + } + + // Check for outstanding requirements + const requiredRequirements = medication.requirements.filter(req => req.requiredToDispense); + const outstandingRequirements: any[] = []; + + for (const requirement of requiredRequirements) { + const matchingMetReq = remsCase.metRequirements?.find( + mr => mr.requirementName === requirement.name + ); + + if (!matchingMetReq || !matchingMetReq.completed) { + outstandingRequirements.push({ + name: requirement.name, + stakeholder: requirement.stakeholderType + }); + } + } + + if (outstandingRequirements.length > 0) { + const reasonCode = determineReasonCode(outstandingRequirements); + const reasonText = buildReasonText(reasonCode); + + logger.info(`Requirements not met - closing with: ${reasonCode}`); + res.type('application/xml'); + return res.status(200).send(buildInitiationClosedResponse( + header, + initRequest, + reasonCode, + reasonText + )); + } + + // All requirements met - return success with patient ID + logger.info('All requirements met - returning success'); + res.type('application/xml'); + return res.status(200).send(buildInitiationSuccessResponse(header, initRequest, remsCase)); + + } catch (error: any) { + logger.error(`ERROR in handleRemsInitiation: ${error.message}`); + res.type('application/xml'); + return res.status(500).send(buildErrorResponse(error.message)); + } +}; + + +const handleRxFill = async (message: any, res: Response) => { + try { + logger.info('--- handleRxFill started ---'); + const header = message.header; + const rxFill = message.body.rxfill; + const patient = rxFill.patient?.humanpatient; + + const medicationDispensed = rxFill.medicationdispensed; + + if (!medicationDispensed) { + logger.error('MedicationDispensed not found in RxFill message'); + logger.error(`Available RxFill fields: ${JSON.stringify(Object.keys(rxFill))}`); + } + + let drugNdcCode = medicationDispensed?.drugcoded?.productcode?.code + + const patientInfo = { + firstName: patient?.name?.firstname || patient?.names?.name?.firstname, + lastName: patient?.name?.lastname || patient?.names?.name?.lastname, + dob: patient?.dateofbirth?.date, + ndc: drugNdcCode, + }; + + logger.info(`RxFill received for: ${JSON.stringify(patientInfo)}`); + + // Try to find case - if NDC not available, try by patient + drug description + let remsCase = null; + + if (drugNdcCode) { + remsCase = await remsCaseCollection.findOne({ + patientFirstName: patientInfo.firstName, + patientLastName: patientInfo.lastName, + patientDOB: patientInfo.dob, + drugNdcCode: drugNdcCode + }); + } + + if (remsCase) { + remsCase.dispenseStatus = 'Dispensed'; + await remsCase.save(); + + logger.info(`Updated case ${remsCase.case_number} dispense status to 'Dispensed'`); + logger.info(` Patient: ${remsCase.patientFirstName} ${remsCase.patientLastName}`); + logger.info(` Drug: ${remsCase.drugName} (NDC: ${remsCase.drugNdcCode})`); + logger.info(` Case Status: ${remsCase.status}`); + } else { + logger.warn(`Case not found for RxFill notification`); + logger.warn(` Searched for: ${JSON.stringify(patientInfo)}`); + } + + // Return success status per NCPDP + res.type('application/xml'); + return res.status(200).send(buildRxFillResponse(header, rxFill)); + } catch (error: any) { + logger.error(`ERROR in handleRxFill: ${error.message}`); + logger.error(`Stack trace: ${error.stack}`); + res.type('application/xml'); + return res.status(500).send(buildErrorResponse(error.message)); + } +}; + + +const determineReasonCode = (outstandingRequirements: any[]): string => { + let hasPatientReq = false; + let hasPrescriberReq = false; + let hasPharmacyReq = false; + + for (const req of outstandingRequirements) { + const stakeholder = req.stakeholder?.toLowerCase(); + if (stakeholder === 'patient') { + hasPatientReq = true; + } else if (stakeholder === 'prescriber') { + hasPrescriberReq = true; + } else if (stakeholder === 'pharmacist' || stakeholder === 'pharmacy') { + hasPharmacyReq = true; + } + } + + // Return only the highest priority requirement + if (hasPatientReq) { + return 'EM'; + } else if (hasPrescriberReq) { + return 'ES'; + } else if (hasPharmacyReq) { + return 'EO'; + } + + // Fallback - should not reach here + return 'EC'; +}; + + +const buildReasonText = (reasonCode: string): string => { + const reasonCodeNotes: { [key: string]: string } = { + 'EM': 'Patient enrollment/certification required', + 'ES': 'Prescriber enrollment/certification required', + 'EO': 'Pharmacy enrollment/certification required', + 'EC': 'Case information incomplete or invalid', + 'ER': 'REMS program error', + 'EX': 'Prescriber deactivated/decertified', + 'EY': 'Pharmacy deactivated/decertified', + 'EZ': 'Patient deactivated/decertified' + }; + + return reasonCodeNotes[reasonCode] || 'REMS requirement not met'; +}; + + +const buildApprovedResponse = ( + header: any, + request: any, + caseId: string, + authNumber: string, + effectiveDate: string, + expirationDate: string +): string => { + const builder = new Builder({ headless: false }); + + const patient = request.patient; + const pharmacy = request.pharmacy; + const prescriber = request.prescriber; + const medicationPrescribed = request.medicationprescribed; + const remsReferenceID = request.remsreferenceid; + + const response = { + Message: { + $: { + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + DatatypesVersion: 'V2024071', + TransportVersion: 'V2024071', + TransactionDomain: 'SCRIPT', + TransactionVersion: 'V2024071', + StructuresVersion: 'V2024071', + ECLVersion: 'V2024071' + }, + Header: header, + Body: { + REMSResponse: { + REMSReferenceID: remsReferenceID, + Patient: patient, + Pharmacy: pharmacy, + Prescriber: prescriber, + MedicationPrescribed: medicationPrescribed, + Response: { + ResponseStatus: { + Approved: { + REMSCaseID: caseId, + REMSAuthorizationNumber: authNumber, + AuthorizationPeriod: { + EffectiveDate: { Date: effectiveDate }, + ExpirationDate: { Date: expirationDate } + } + } + } + } + } + } + } + }; + + return builder.buildObject(response); +}; + - await remsCaseCollection.findOneAndUpdate( - { - patientFirstName: patient?.humanpatient?.name?.firstname, - patientLastName: patient?.humanpatient?.name?.lastname, - patientDOB: patient?.humanpatient?.dateofbirth?.date, - drugCode: code - }, - { dispenseStatus: fillStatus }, - { new: true } - ); - return fillStatus; +const buildDeniedResponse = ( + header: any, + request: any, + reasonCode: string, + note: string +): string => { + const builder = new Builder({ headless: false }); + + const patient = request.patient; + const pharmacy = request.pharmacy; + const prescriber = request.prescriber; + const medicationPrescribed = request.medicationprescribed; + const remsReferenceID = request.remsreferenceid; + const solicitedModel = request.request?.solicitedmodel; + const caseId = solicitedModel?.remscaseid; + + const response = { + Message: { + $: { + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + DatatypesVersion: 'V2024071', + TransportVersion: 'V2024071', + TransactionDomain: 'SCRIPT', + TransactionVersion: 'V2024071', + StructuresVersion: 'V2024071', + ECLVersion: 'V2024071' + }, + Header: header, + Body: { + REMSResponse: { + REMSReferenceID: remsReferenceID, + Patient: patient, + Pharmacy: pharmacy, + Prescriber: prescriber, + MedicationPrescribed: medicationPrescribed, + Response: { + ResponseStatus: { + Denied: { + REMSCaseID: caseId, + DeniedReasonCode: reasonCode, + REMSNote: note + } + } + } + } + } + } + }; + + return builder.buildObject(response); +}; + + +const buildInitiationClosedResponse = ( + header: any, + request: any, + reasonCode: string, + note: string +): string => { + const builder = new Builder({ headless: false }); + + const patient = request.patient; + const pharmacy = request.pharmacy; + const prescriber = request.prescriber; + const medicationPrescribed = request.medicationprescribed; + const remsReferenceID = request.remsreferenceid; + + const response = { + Message: { + $: { + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + DatatypesVersion: 'V2024071', + TransportVersion: 'V2024071', + TransactionDomain: 'SCRIPT', + TransactionVersion: 'V2024071', + StructuresVersion: 'V2024071', + ECLVersion: 'V2024071' + }, + Header: header, + Body: { + REMSInitiationResponse: { + REMSReferenceID: remsReferenceID, + Patient: patient, + Pharmacy: pharmacy, + Prescriber: prescriber, + MedicationPrescribed: medicationPrescribed, + Response: { + ResponseStatus: { + Closed: { + ReasonCode: reasonCode, + REMSNote: note + } + } + } + } + } + } + }; + + return builder.buildObject(response); +}; + + +const buildInitiationSuccessResponse = (header: any, request: any, remsCase: any): string => { + const builder = new Builder({ headless: false }); + + const patient = request.patient; + const humanPatient = patient?.humanpatient; + const pharmacy = request.pharmacy; + const prescriber = request.prescriber; + const medicationPrescribed = request.medicationprescribed; + const remsReferenceID = request.remsreferenceid; + + const response = { + Message: { + $: { + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + DatatypesVersion: 'V2024071', + TransportVersion: 'V2024071', + TransactionDomain: 'SCRIPT', + TransactionVersion: 'V2024071', + StructuresVersion: 'V2024071', + ECLVersion: 'V2024071' + }, + Header: header, + Body: { + REMSInitiationResponse: { + REMSReferenceID: remsReferenceID, + Patient: { + HumanPatient: { + $: { + 'xsi:type': 'PatientMandatoryAddress' + }, + Identification: { + REMSPatientID: remsCase.remsPatientId || remsCase.case_number + }, + Names: humanPatient?.names, + GenderAndSex: humanPatient?.genderandsex, + DateOfBirth: humanPatient?.dateofbirth, + Address: { + $: { + 'xsi:type': 'MandatoryAddressType' + }, + ...humanPatient?.address + } + } + }, + Pharmacy: pharmacy, + Prescriber: prescriber, + MedicationPrescribed: medicationPrescribed + } + } + } + }; + + return builder.buildObject(response); +}; + + +const buildRxFillResponse = (header: any, rxFill: any): string => { + const builder = new Builder({ headless: false }); + + const response = { + Message: { + $: { + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + DatatypesVersion: 'V2024071', + TransportVersion: 'V2024071', + TransactionDomain: 'SCRIPT', + TransactionVersion: 'V2024071', + StructuresVersion: 'V2024071', + ECLVersion: 'V2024071' + }, + Header: header, + Body: { + Status: { + Code: '000', + Description: 'Dispense notification received and processed' + } + } + } + }; + + return builder.buildObject(response); +}; + + +const buildErrorResponse = (errorMessage: string): string => { + const builder = new Builder({ headless: false }); + + const response = { + Message: { + $: { + 'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance', + DatatypesVersion: 'V2024071', + TransportVersion: 'V2024071', + TransactionDomain: 'SCRIPT', + TransactionVersion: 'V2024071', + StructuresVersion: 'V2024071', + ECLVersion: 'V2024071' + }, + Body: { + Error: { + Code: 'ER', + Description: errorMessage + } + } + } + }; + + return builder.buildObject(response); }; -export default router; +export default router; \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index f2970f2..d75e5c6 100644 --- a/src/server.ts +++ b/src/server.ts @@ -133,7 +133,7 @@ class REMSServer extends Server { } }) ); - this.app.use('/ncpdp', Ncpdp); + this.app.use('/ncpdp/script', Ncpdp); return this; } @@ -163,4 +163,4 @@ class REMSServer extends Server { // Start the application -export { REMSServer, initialize }; \ No newline at end of file +export { REMSServer, initialize };