diff --git a/backend/src/database/schemas/doctorOrderSchemas.js b/backend/src/database/schemas/doctorOrderSchemas.js index aba505c..c0cd577 100644 --- a/backend/src/database/schemas/doctorOrderSchemas.js +++ b/backend/src/database/schemas/doctorOrderSchemas.js @@ -25,6 +25,10 @@ export const orderSchema = new mongoose.Schema({ total: Number, pickupDate: String, dispenseStatus: String, + authorizationNumber: String, + authorizationExpiration: String, + denialReasonCode: String, + remsNote: String, metRequirements: [ { name: String, @@ -44,4 +48,4 @@ export const orderSchema = new mongoose.Schema({ // Compound index is used to prevent duplicates based off of the given parameters orderSchema.index({ simpleDrugName: 1, patientName: 1 }, { unique: true }); // schema level -export const doctorOrder = mongoose.model('doctorOrder', orderSchema); +export const doctorOrder = mongoose.model('doctorOrder', orderSchema); \ No newline at end of file diff --git a/backend/src/ncpdpScriptBuilder/buildScript.v2017071.js b/backend/src/ncpdpScriptBuilder/buildScript.v2017071.js index a642710..fbd3953 100644 --- a/backend/src/ncpdpScriptBuilder/buildScript.v2017071.js +++ b/backend/src/ncpdpScriptBuilder/buildScript.v2017071.js @@ -1,4 +1,4 @@ -/* NCPDP SCRIPT v2017071 Support */ +/* NCPDP SCRIPT v2017071 Support - Enhanced for Full REMS Compliance */ import { XMLBuilder } from 'fast-xml-parser'; import { v4 as uuidv4 } from 'uuid'; @@ -12,6 +12,9 @@ const XML_BUILDER_OPTIONS = { oneListGroup: 'true' }; +/** + * Build base NCPDP message structure + */ function buildMessage(inputMessage, body) { const { Message } = inputMessage; const { Header, Body } = Message; @@ -33,8 +36,10 @@ function buildMessage(inputMessage, body) { } }, { - Message: - 'NewRx Request Received For: ' + Body.NewRx.MedicationPrescribed.DrugDescription + MessageID: Header.MessageID + }, + { + Message: 'NewRx Request Received For: ' + Body.NewRx.MedicationPrescribed.DrugDescription }, { RelatesToMessageID: Header.MessageID }, { SentTime: time.toISOString() }, @@ -49,12 +54,15 @@ function buildMessage(inputMessage, body) { return message; } +/** + * Build NCPDP Status message (success response) + */ export function buildRxStatus(newRxMessageConvertedToJSON) { const body = [ { Status: [ { - Code: '000' // Placeholder: This is dependent on individual pharmacy + Code: '000' } ] } @@ -64,13 +72,16 @@ export function buildRxStatus(newRxMessageConvertedToJSON) { return builder.build(rxStatus); } +/** + * Build NCPDP Error message + */ export function buildRxError(newRxMessageConvertedToJSON, errorMessage) { const body = [ { Error: [ { - Code: 900, // Transaction was rejected - DescriptionCode: 1000, // Unable to identify based on information submitted + Code: 900, + DescriptionCode: 1000, Description: errorMessage } ] @@ -81,13 +92,56 @@ export function buildRxError(newRxMessageConvertedToJSON, errorMessage) { return builder.build(rxStatus); } +/** + * Build NCPDP RxFill message + * Per NCPDP spec: Sent when medication is dispensed/picked up + * Must be sent to both EHR and REMS Admin for REMS drugs + */ export const buildRxFill = newRx => { const { Message } = JSON.parse(newRx.serializedJSON); const { Header, Body } = Message; - console.log('Message', Message); + console.log('Building RxFill per NCPDP SCRIPT'); const time = new Date(); + + // Extract medication data from NewRx + const medicationPrescribed = Body.NewRx.MedicationPrescribed; + const drugCoded = medicationPrescribed.DrugCoded; + + const medicationDispensed = { + DrugDescription: medicationPrescribed.DrugDescription, + DrugCoded: { + ProductCode: drugCoded.ProductCode ? { + Code: drugCoded.ProductCode.Code, + Qualifier: drugCoded.ProductCode.Qualifier + } : undefined, + Strength: drugCoded.Strength ? { + StrengthValue: drugCoded.Strength.StrengthValue, + StrengthForm: drugCoded.Strength.StrengthForm, + StrengthUnitOfMeasure: drugCoded.Strength.StrengthUnitOfMeasure + } : undefined + }, + Quantity: { + Value: medicationPrescribed.Quantity.Value, + CodeListQualifier: medicationPrescribed.Quantity.CodeListQualifier || '87', + QuantityUnitOfMeasure: medicationPrescribed.Quantity.QuantityUnitOfMeasure + }, + DaysSupply: medicationPrescribed.DaysSupply, + WrittenDate: medicationPrescribed.WrittenDate, + Substitutions: medicationPrescribed.Substitutions?.Substitutions || + medicationPrescribed.Substitutions || '0', + NumberOfRefills: medicationPrescribed.Refills?.Quantity || + medicationPrescribed.NumberOfRefills || 0, + Sig: medicationPrescribed.Sig + }; + const message = { Message: { + '@@DatatypesVersion': '20170715', + '@@TransportVersion': '20170715', + '@@TransactionDomain': 'SCRIPT', + '@@TransactionVersion': '20170715', + '@@StructuresVersion': '20170715', + '@@ECLVersion': '20170715', Header: [ { To: { @@ -104,6 +158,7 @@ export const buildRxFill = newRx => { { MessageID: uuidv4() }, { RelatesToMessageID: Header.MessageID }, { SentTime: time.toISOString() }, + { RxReferenceNumber: Header.MessageID }, { PrescriberOrderNumber: Header.PrescriberOrderNumber } ], Body: [ @@ -117,16 +172,16 @@ export const buildRxFill = newRx => { Patient: Body.NewRx.Patient, Pharmacy: { Identification: { - NCPDPID: MOCK_VALUE, + NCPDPID: Header.To._ || MOCK_VALUE, NPI: MOCK_VALUE }, - BusinessName: Header.To._, + BusinessName: Header.To._ || 'Pharmacy', Address: { AddressLine1: MOCK_VALUE, City: MOCK_VALUE, StateProvince: MOCK_VALUE, PostalCode: MOCK_VALUE, - Country: MOCK_VALUE + CountryCode: 'US' }, CommunicationNumbers: { PrimaryTelephone: { @@ -135,12 +190,194 @@ export const buildRxFill = newRx => { } }, Prescriber: Body.NewRx.Prescriber, + MedicationDispensed: medicationDispensed + } + } + ] + } + }; + const builder = new XMLBuilder(XML_BUILDER_OPTIONS); + return builder.build(message); +}; + +/** + * Build NCPDP REMSInitiationRequest + */ +export const buildREMSInitiationRequest = newRx => { + const { Message } = JSON.parse(newRx.serializedJSON); + const { Header, Body } = Message; + const time = new Date(); + + // Extract NDC from medication (prioritize NDC, fallback to other codes) + const drugCoded = Body.NewRx.MedicationPrescribed.DrugCoded; + const ndcCode = + drugCoded?.NDC || drugCoded?.ProductCode?.Code; + const humanPatient = Body.NewRx.Patient.HumanPatient; + const patient = { + HumanPatient: { + Identification: {}, + Names: humanPatient.Names, + GenderAndSex: humanPatient.GenderAndSex, + DateOfBirth: humanPatient.DateOfBirth, + Address: humanPatient.Address + } + }; + + const message = { + Message: { + '@@DatatypesVersion': '2024011', + '@@TransportVersion': '2024011', + '@@TransactionDomain': 'SCRIPT', + '@@TransactionVersion': '2024011', + '@@StructuresVersion': '2024011', + '@@ECLVersion': '2024011', + Header: [ + { + To: { + '#text': ndcCode, + '@@Qualifier': 'ZZZ' + } + }, + { + From: { + '#text': Header.To._ || 'PIMS Pharmacy', + '@@Qualifier': 'REMS' + } + }, + { MessageID: uuidv4() }, + { SentTime: time.toISOString() }, + { + Security: { + Sender: { + SecondaryIdentification: 'PASSWORDR' + } + } + }, + { + SenderSoftware: { + SenderSoftwareDeveloper: 'PIMS', + SenderSoftwareProduct: 'PharmacySystem', + SenderSoftwareVersionRelease: '1' + } + }, + { TestMessage: 'false' } + ], + Body: [ + { + REMSInitiationRequest: { + REMSReferenceID: uuidv4().replace(/-/g, '').substring(0, 25), + Patient: patient, + Pharmacy: { + Identification: { + NCPDPID: Header.To._ || MOCK_VALUE, + NPI: MOCK_VALUE + }, + BusinessName: Header.To._ || 'PIMS Pharmacy', + CommunicationNumbers: { + PrimaryTelephone: { + Number: MOCK_VALUE + } + } + }, + Prescriber: Body.NewRx.Prescriber, MedicationPrescribed: Body.NewRx.MedicationPrescribed } } ] } }; + const builder = new XMLBuilder(XML_BUILDER_OPTIONS); return builder.build(message); }; + +/** + * Build NCPDP REMSRequest + */ +export const buildREMSRequest = (newRx, caseNumber) => { + const { Message } = JSON.parse(newRx.serializedJSON); + const { Header, Body } = Message; + const time = new Date(); + const deadlineDate = new Date(); + deadlineDate.setDate(deadlineDate.getDate() + 7); + + // Extract NDC from medication + const drugCoded = Body.NewRx.MedicationPrescribed.DrugCoded; + const ndcCode = + drugCoded?.NDC || drugCoded?.ProductCode?.Code || '66215050130'; + + const message = { + Message: { + '@@DatatypesVersion': '2024011', + '@@TransportVersion': '2024011', + '@@TransactionDomain': 'SCRIPT', + '@@TransactionVersion': '2024011', + '@@StructuresVersion': '2024011', + '@@ECLVersion': '2024011', + Header: [ + { + To: { + '#text': ndcCode, + '@@Qualifier': 'ZZZ' + } + }, + { + From: { + '#text': Header.To._ || 'PIMS Pharmacy', + '@@Qualifier': 'REMS' + } + }, + { MessageID: uuidv4() }, + { SentTime: time.toISOString() }, + { + Security: { + Sender: { + SecondaryIdentification: 'PASSWORD' + } + } + }, + { + SenderSoftware: { + SenderSoftwareDeveloper: 'PIMS', + SenderSoftwareProduct: 'PharmacySystem', + SenderSoftwareVersionRelease: '1' + } + }, + { TestMessage: 'false' } + ], + Body: [ + { + REMSRequest: { + REMSReferenceID: uuidv4().replace(/-/g, '').substring(0, 25), + Patient: Body.NewRx.Patient, + Pharmacy: { + Identification: { + NCPDPID: Header.To._ || MOCK_VALUE, + NPI: MOCK_VALUE + }, + BusinessName: Header.To._ || 'PIMS Pharmacy', + CommunicationNumbers: { + PrimaryTelephone: { + Number: MOCK_VALUE + } + } + }, + Prescriber: Body.NewRx.Prescriber, + MedicationPrescribed: Body.NewRx.MedicationPrescribed, + Request: { + SolicitedModel: { + REMSCaseID: caseNumber, + DeadlineForReply: { + Date: deadlineDate.toISOString().split('T')[0] + } + } + } + } + } + ] + } + }; + + const builder = new XMLBuilder(XML_BUILDER_OPTIONS); + return builder.build(message); +}; \ No newline at end of file diff --git a/backend/src/routes/doctorOrders.js b/backend/src/routes/doctorOrders.js index 9af2b78..841ac08 100644 --- a/backend/src/routes/doctorOrders.js +++ b/backend/src/routes/doctorOrders.js @@ -5,11 +5,14 @@ import axios from 'axios'; // XML Parsing Middleware used for NCPDP SCRIPT import bodyParser from 'body-parser'; import bpx from 'body-parser-xml'; +import { parseStringPromise } from "xml2js"; import env from 'var'; import { buildRxStatus, buildRxFill, - buildRxError + buildRxError, + buildREMSInitiationRequest, + buildREMSRequest } from '../ncpdpScriptBuilder/buildScript.v2017071.js'; import { NewRx } from '../database/schemas/newRx.js'; import { medicationRequestToRemsAdmins } from '../database/data.js'; @@ -17,14 +20,22 @@ import { medicationRequestToRemsAdmins } from '../database/data.js'; bpx(bodyParser); router.use( bodyParser.xml({ + type: ['application/xml'], xmlParseOptions: { - normalize: true, // Trim whitespace inside text nodes - explicitArray: false // Only put nodes in array if >1 + normalize: true, + explicitArray: false } }) ); router.use(bodyParser.urlencoded({ extended: false })); +const XML2JS_OPTS = { + explicitArray: false, + trim: true, + normalize: true, + normalizeTags: true, // <-- makes all tag names lower case +}; + /** * Route: 'doctorOrders/api/getRx/pending' * Description: 'Returns all pending documents in database for PIMS' @@ -59,7 +70,6 @@ router.get('/api/getRx/pickedUp', async (_req, res) => { * Description: Process addRx / NewRx NCPDP message. */ export async function processNewRx(newRxMessageConvertedToJSON) { - console.log('processNewRx NCPDP SCRIPT message'); const newOrder = await parseNCPDPScript(newRxMessageConvertedToJSON); try { @@ -84,7 +94,43 @@ export async function processNewRx(newRxMessageConvertedToJSON) { return buildRxError(errorStr); } - return buildRxStatus(newRxMessageConvertedToJSON); + const rxStatus = buildRxStatus(newRxMessageConvertedToJSON); + console.log('Returning RxStatus'); + console.log(rxStatus); + + // If REMS drug, send REMSInitiationRequest per NCPDP spec + if (isRemsDrug(newOrder)) { + console.log('REMS drug detected - sending REMSInitiationRequest per NCPDP workflow'); + try { + const initiationResponse = await sendREMSInitiationRequest(newOrder); + + if (initiationResponse) { + const updateData = { + remsNote: initiationResponse.remsNote + }; + + if (initiationResponse.caseNumber) { + updateData.caseNumber = initiationResponse.caseNumber; + console.log('Received REMS Case Number:', initiationResponse.caseNumber); + } + + if (initiationResponse.remsPatientId) { + console.log('Received REMS Patient ID:', initiationResponse.remsPatientId); + } + + if (initiationResponse.status === 'CLOSED') { + updateData.denialReasonCode = initiationResponse.reasonCode; + console.log('REMSInitiation CLOSED:', initiationResponse.reasonCode); + } + + await doctorOrder.updateOne({ _id: newOrder._id }, updateData); + console.log('Updated order with REMSInitiation response'); + } + } catch (error) { + console.log('Error processing REMSInitiationRequest:', error); + } + } + return rxStatus; } /** @@ -94,6 +140,8 @@ export async function processNewRx(newRxMessageConvertedToJSON) { router.post('/api/addRx', async (req, res) => { // Parsing incoming NCPDP SCRIPT XML to doctorOrder JSON const newRxMessageConvertedToJSON = req.body; + console.log('processNewRx NCPDP SCRIPT message'); + console.log(JSON.stringify(req.body)); const status = await processNewRx(newRxMessageConvertedToJSON); res.send(status); console.log('Sent Status/Error'); @@ -101,28 +149,65 @@ router.post('/api/addRx', async (req, res) => { /** * Route: 'doctorOrders/api/updateRx/:id' - * Description : 'Updates prescription based on mongo id, used in etasu' + * Description : 'Updates prescription based on mongo id, sends NCPDP REMSRequest for authorization' */ router.patch('/api/updateRx/:id', async (req, res) => { try { - // Finding by id const order = await doctorOrder.findById(req.params.id).exec(); console.log('Found doctor order by id! --- ', order); - const guidanceResponse = await getGuidanceResponse(order); - const metRequirements = - guidanceResponse?.contained?.[0]?.['parameter'] || order.metRequirements; - const dispenseStatus = getDispenseStatus(order, guidanceResponse); + // Non-REMS drugs auto-approve + if (!isRemsDrug(order)) { + const newOrder = await doctorOrder.findOneAndUpdate( + { _id: req.params.id }, + { dispenseStatus: 'Approved' }, + { new: true } + ); + res.send(newOrder); + console.log('Non-REMS drug - auto-approved'); + return; + } + + // REMS drugs - send NCPDP REMSRequest per spec + console.log('REMS drug - sending REMSRequest for authorization per NCPDP workflow'); + const ncpdpResponse = await sendREMSRequest(order); + + if (!ncpdpResponse) { + res.send(order); + console.log('NCPDP REMSRequest failed'); + return; + } + + // Update based on NCPDP response + const updateData = { + dispenseStatus: getDispenseStatus(order, ncpdpResponse) + }; + + if (ncpdpResponse.status === 'APPROVED') { + updateData.authorizationNumber = ncpdpResponse.authorizationNumber; + updateData.authorizationExpiration = ncpdpResponse.authorizationExpiration; + updateData.caseNumber = ncpdpResponse.caseId; + + // Format approval note with ETASU summary + let approvalNote = `APPROVED - Authorization: ${ncpdpResponse.authorizationNumber}, Expires: ${ncpdpResponse.authorizationExpiration}`; + updateData.remsNote = approvalNote; + updateData.denialReasonCode = null; + console.log('APPROVED:', ncpdpResponse.authorizationNumber); + } else if (ncpdpResponse.status === 'DENIED') { + updateData.denialReasonCode = ncpdpResponse.reasonCode; + updateData.remsNote = ncpdpResponse.remsNote; + updateData.caseNumber = ncpdpResponse.caseId; + console.log('DENIED:', ncpdpResponse.reasonCode); + } - // Saving and updating const newOrder = await doctorOrder.findOneAndUpdate( { _id: req.params.id }, - { dispenseStatus, metRequirements }, + updateData, { new: true } ); res.send(newOrder); - console.log('Updated order'); + console.log('Updated order with NCPDP response'); } catch (error) { console.log('Error', error); return error; @@ -160,7 +245,7 @@ router.patch('/api/updateRx/:id/metRequirements', async (req, res) => { /** * Route: 'doctorOrders/api/updateRx/:id/pickedUp' - * Description : 'Updates prescription dispense status based on mongo id to be picked up ' + * Description : 'Updates prescription dispense status to picked up and sends RxFill per NCPDP spec' */ router.patch('/api/updateRx/:id/pickedUp', async (req, res) => { let prescriberOrderNumber = null; @@ -178,40 +263,60 @@ router.patch('/api/updateRx/:id/pickedUp', async (req, res) => { return error; } + // Send RxFill per NCPDP spec to BOTH EHR and REMS Admin try { - // Reach out to EHR to update dispense status as XML const newRx = await NewRx.findOne({ prescriberOrderNumber: prescriberOrderNumber }); + + if (!newRx) { + console.log('NewRx not found for RxFill'); + return; + } + const rxFill = buildRxFill(newRx); - const status = await axios.post(env.EHR_RXFILL_URL, rxFill, { - headers: { - Accept: 'application/xml', // Expect that the Status that the EHR returns back is in XML - 'Content-Type': 'application/xml' // Tell the EHR that the RxFill is in XML - } - }); - console.log('Sent RxFill to EHR and received status from EHR', status.data); + console.log('Sending RxFill per NCPDP workflow'); + + // Send to EHR + try { + const ehrStatus = await axios.post(env.EHR_RXFILL_URL, rxFill, { + headers: { + Accept: 'application/xml', + 'Content-Type': 'application/xml' + } + }); + console.log('Sent RxFill to EHR, received status:', ehrStatus.data); + } catch (ehrError) { + console.log('Failed to send RxFill to EHR:', ehrError.message); + } - const remsAdminStatus = await axios.post(env.REMS_ADMIN_NCPDP, rxFill, { - headers: { - Accept: 'application/xml', // Expect that the Status that the rems admin returns back is in XML - 'Content-Type': 'application/xml' // Tell the rems admin that the RxFill is in XML + // Send to REMS Admin (required by NCPDP spec for REMS drugs) + const order = await doctorOrder.findOne({ prescriberOrderNumber }); + if (isRemsDrug(order)) { + try { + const remsAdminStatus = await axios.post( + env.REMS_ADMIN_NCPDP, + rxFill, + { + headers: { + Accept: 'application/xml', + 'Content-Type': 'application/xml' + } + } + ); + console.log('Sent RxFill to REMS Admin, received status:', remsAdminStatus.data); + } catch (remsError) { + console.log('Failed to send RxFill to REMS Admin:', remsError.message); } - }); - - console.log('Sent RxFill to rems admin and received status from rems admin: ', remsAdminStatus); + } } catch (error) { - console.log('Could not send RxFill to EHR', error); - return error; + console.log('Error in RxFill workflow:', error); } }); /** * Route : 'doctorOrders/api/getRx/patient/:patientName/drug/:simpleDrugName` * Description : 'Fetches first available doctor order based on patientFirstName, patientLastName and patientDOB' - * 'To retrieve a specific one for a drug on a given date, supply the drugNdcCode and rxDate in the query parameters' - * 'Required Parameters : patientFirstName, patientLastName patientDOB are part of the path' - * 'Optional Parameters : all remaining values in the orderSchema as query parameters (?drugNdcCode=0245-0571-01,rxDate=2020-07-11)' */ router.get('/api/getRx/:patientFirstName/:patientLastName/:patientDOB', async (req, res) => { var searchDict = { @@ -221,11 +326,8 @@ router.get('/api/getRx/:patientFirstName/:patientLastName/:patientDOB', async (r }; if (req.query && Object.keys(req.query).length > 0) { - // add the query parameters for (const prop in req.query) { - // verify that the parameter is in the orderSchema if (orderSchema.path(prop) != undefined) { - // add the parameters to the search query searchDict[prop] = req.query[prop]; } } @@ -249,6 +351,7 @@ router.delete('/api/deleteAll', async (req, res) => { }); const isRemsDrug = order => { + console.log(order); return medicationRequestToRemsAdmins.some(entry => { if (order.drugNdcCode && entry.ndc) { return order.drugNdcCode === entry.ndc; @@ -262,6 +365,11 @@ const isRemsDrug = order => { }); }; + +/** + * Get FHIR ETASU URL for the order + * Used for GuidanceResponse calls (View ETASU) + */ const getEtasuUrl = order => { let baseUrl; @@ -286,6 +394,10 @@ const getEtasuUrl = order => { return baseUrl ? etasuUrl : null; }; +/** + * Get FHIR GuidanceResponse for ETASU requirements + * Used by View ETASU button + */ const getGuidanceResponse = async order => { const etasuUrl = getEtasuUrl(order); @@ -368,31 +480,247 @@ const getGuidanceResponse = async order => { }; } - const response = await axios.post(etasuUrl, body, { - headers: { - 'content-type': 'application/json' + try { + const response = await axios.post(etasuUrl, body, { + headers: { + 'content-type': 'application/json' + } + }); + console.log('Retrieved FHIR GuidanceResponse', JSON.stringify(response.data, null, 4)); + console.log('URL', etasuUrl); + const responseResource = response.data.parameter?.[0]?.resource; + return responseResource; + } catch (error) { + console.log('Error fetching FHIR GuidanceResponse:', error.message); + return null; + } +}; + +/** + * Send NCPDP REMSInitiationRequest to REMS Admin + * Per NCPDP spec: Sent when prescription arrives to check REMS case status + */ +const sendREMSInitiationRequest = async order => { + try { + const newRx = await NewRx.findOne({ + prescriberOrderNumber: order.prescriberOrderNumber + }); + + if (!newRx) { + console.log('NewRx not found for REMSInitiationRequest'); + return null; } - }); - console.log('Retrieved order', JSON.stringify(response.data, null, 4)); - console.log('URL', etasuUrl); - const responseResource = response.data.parameter?.[0]?.resource; - return responseResource; + + const initiationRequest = buildREMSInitiationRequest(newRx); + console.log('Sending REMSInitiationRequest to REMS Admin'); + + console.log(initiationRequest) + + const response = await axios.post( + env.REMS_ADMIN_NCPDP, + initiationRequest, + { + headers: { + Accept: 'application/xml', + 'Content-Type': 'application/xml' + } + } + ); + + const parsedResponse = await parseStringPromise(response.data, XML2JS_OPTS); + + console.log('Received REMSInitiationResponse'); + console.log('Response:', response.data); + + return parseREMSInitiationResponse(parsedResponse); + } catch (error) { + console.log('Error sending REMSInitiationRequest:', error.message); + return null; + } +}; + +/** + * Send NCPDP REMSRequest to REMS Admin for authorization + * Per NCPDP spec: Sent at pickup time for authorization check + */ +const sendREMSRequest = async order => { + try { + const newRx = await NewRx.findOne({ + prescriberOrderNumber: order.prescriberOrderNumber + }); + + if (!newRx) { + console.log('NewRx not found for REMSRequest'); + return null; + } + + if (!order.caseNumber) { + console.log('No case number - need REMSInitiationRequest first'); + return null; + } + + const remsRequest = buildREMSRequest(newRx, order.caseNumber); + console.log('Sending REMSRequest to REMS Admin for case:', order.caseNumber); + console.log(remsRequest) + + const response = await axios.post( + env.REMS_ADMIN_NCPDP, + remsRequest, + { + headers: { + Accept: 'application/xml', + 'Content-Type': 'application/xml' + } + } + ); + + const parsedResponse = await parseStringPromise(response.data, XML2JS_OPTS); + + console.log('Received REMSResponse'); + console.log('Response:', response.data); + return parseREMSResponse(parsedResponse); + } catch (error) { + console.log('Error sending REMSRequest:', error.message); + return null; + } +}; + +/** + * Parse NCPDP REMSInitiationResponse per spec + * Extracts case info, status, and requirements + */ +const parseREMSInitiationResponse = parsedXml => { + const message = parsedXml?.message; + const body = message?.body; + const initResponse = body?.remsinitiationresponse; + console.log(message); + console.log(initResponse); + + if (!initResponse) { + console.log('No REMSInitiationResponse found'); + return null; + } + + const response = initResponse.response; + const responseStatus = response?.responsestatus; + + // Check for Closed status (requirements not met) + const closed = responseStatus?.closed; + if (closed) { + const reasonCode = closed.reasoncode; + const remsNote = closed.remsnote || ''; + + return { + status: 'CLOSED', + reasonCode: reasonCode, + remsNote: remsNote, + }; + } + + // Extract case ID and patient ID from successful initiation + const patient = initResponse.patient; + const humanPatient = patient?.humanpatient; + const identification = humanPatient?.identification; + const remsPatientId = identification?.remspatientid; + + // Check if there's a case number in the response + let caseNumber = null; + const medication = initResponse.medicationprescribed; + if (medication) { + // Some implementations include case number in initiation success + caseNumber = remsPatientId; // Often the case number is returned as patient ID + } + + return { + status: 'OPEN', + remsPatientId: remsPatientId, + caseNumber: caseNumber, + }; +}; + +/** + * Parse NCPDP REMSResponse per spec + * Extracts authorization status, case ID, and NCPDP rejection code + */ +const parseREMSResponse = parsedXml => { + const message = parsedXml?.message; + const body = message?.body; + const remsResponse = body?.remsresponse; + console.log(message); + console.log(remsResponse); + + if (!remsResponse) { + console.log('No REMSResponse found'); + return null; + } + + const request = remsResponse.request; + + const response = remsResponse.response; + const responseStatus = response?.responsestatus; + + // Check for APPROVED status + const approved = responseStatus?.approved; + if (approved) { + const caseId = approved.remscaseid; + const authNumber = approved.remsauthorizationnumber; + const authPeriod = approved.authorizationperiod; + const expiration = authPeriod?.expirationdate?.date; + + return { + status: 'APPROVED', + caseId: caseId, + authorizationNumber: authNumber, + authorizationExpiration: expiration, + remsNote: 'All REMS requirements have been met and verified. Authorization granted for dispensing.', + }; + } + + // Check for DENIED status + const denied = responseStatus?.denied; + if (denied) { + const caseId = denied.remscaseid; + const reasonCode = denied.deniedreasoncode; + const remsNote = denied.remsnote || ''; + + + + return { + status: 'DENIED', + caseId: caseId, + reasonCode: reasonCode, + remsNote: remsNote, + }; + } + + return null; }; -const getDispenseStatus = (order, guidanceResponse) => { - const isNotRemsDrug = !guidanceResponse; - const isRemsDrugAndMetEtasu = guidanceResponse?.status === 'success'; - const isPickedUp = order.dispenseStatus === 'Picked Up'; - if (isNotRemsDrug && order.dispenseStatus === 'Pending') return 'Approved'; - if (isRemsDrugAndMetEtasu) return 'Approved'; - if (isPickedUp) return 'Picked Up'; +/** + * Determine dispense status based on NCPDP response + */ +const getDispenseStatus = (order, ncpdpResponse) => { + // Non-REMS drugs auto-approve + if (!ncpdpResponse) { + if (order.dispenseStatus === 'Pending') return 'Approved'; + if (order.dispenseStatus === 'Picked Up') return 'Picked Up'; + return order.dispenseStatus; + } + + // REMS drugs - check NCPDP response per spec + if (ncpdpResponse.status === 'APPROVED') { + return 'Approved'; + } + + if (order.dispenseStatus === 'Picked Up') { + return 'Picked Up'; + } + return 'Pending'; }; /** - * Description : 'Returns parsed NCPDP NewRx as JSON' - * In : NCPDP SCRIPT XML - * Return : Mongoose schema of a newOrder + * Parse NCPDP SCRIPT NewRx to order format */ async function parseNCPDPScript(newRx) { // Parsing XML NCPDP SCRIPT from EHR @@ -401,7 +729,7 @@ async function parseNCPDPScript(newRx) { const medicationPrescribed = newRx.Message.Body.NewRx.MedicationPrescribed; const incompleteOrder = { - orderId: newRx.Message.Header.MessageID.toString(), // Will need to return to this and use actual pt identifier or uuid + orderId: newRx.Message.Header.MessageID.toString(), caseNumber: newRx.Message.Header.AuthorizationNumber, prescriberOrderNumber: newRx.Message.Header.PrescriberOrderNumber, patientName: patient.HumanPatient.Name.FirstName + ' ' + patient.HumanPatient.Name.LastName, @@ -424,17 +752,15 @@ async function parseNCPDPScript(newRx) { simpleDrugName: medicationPrescribed.DrugDescription?.split(' ')[0], drugNdcCode: - medicationPrescribed.DrugCoded.ProductCode?.Code || - medicationPrescribed.DrugCoded.NDC || - null, + medicationPrescribed.DrugCoded.ProductCode?.Code || medicationPrescribed.DrugCoded.NDC || null, drugRxnormCode: medicationPrescribed.DrugCoded.DrugDBCode?.Code || null, rxDate: medicationPrescribed.WrittenDate.Date, - drugPrice: 200, // Add later? + drugPrice: 200, quantities: medicationPrescribed.Quantity.Value, total: 1800, - pickupDate: 'Tue Dec 13 2022', // Add later? + pickupDate: 'Tue Dec 13 2022', dispenseStatus: 'Pending' }; @@ -448,4 +774,4 @@ async function parseNCPDPScript(newRx) { return order; } -export default router; +export default router; \ No newline at end of file diff --git a/backend/src/routes/ncpdp.js b/backend/src/routes/ncpdp.js index 6bed68d..373d8f2 100644 --- a/backend/src/routes/ncpdp.js +++ b/backend/src/routes/ncpdp.js @@ -11,8 +11,8 @@ bpx(bodyParser); router.use( bodyParser.xml({ xmlParseOptions: { - normalize: true, // Trim whitespace inside text nodes - explicitArray: false // Only put nodes in array if >1 + normalize: true, + explicitArray: false } }) ); @@ -42,4 +42,4 @@ router.post('/script', async (req, res) => { console.log('Sent Status/Error'); }); -export default router; +export default router; \ No newline at end of file diff --git a/frontend/src/views/DoctorOrders/OrderCard/DenialNotification.tsx b/frontend/src/views/DoctorOrders/OrderCard/DenialNotification.tsx new file mode 100644 index 0000000..eb46178 --- /dev/null +++ b/frontend/src/views/DoctorOrders/OrderCard/DenialNotification.tsx @@ -0,0 +1,50 @@ +import { Alert, Snackbar } from '@mui/material'; +import React from 'react'; + +// NCPDP Denial Reason Code mapping +const DENIAL_CODE_MESSAGES: Record = { + EM: 'Patient Enrollment/Certification Required', + ES: 'Prescriber Enrollment/Certification Required', + EO: 'Pharmacy Enrollment/Certification Required', + EC: 'Case Information Missing or Invalid', + ER: 'REMS Program Error', + EX: 'Prescriber Deactivated/Decertified', + EY: 'Pharmacy Deactivated/Decertified', + EZ: 'Patient Deactivated/Decertified' +}; + +type DenialNotificationProps = { + open: boolean; + onClose: () => void; + denialCode?: string; + remsNote?: string; +} +const DenialNotification = (props: DenialNotificationProps) => { + const getMessage = () => { + if (props.remsNote) { + return props.remsNote; + } + + // Fallback to hardcoded messages if remsNote is empty + if (props.denialCode) { + return DENIAL_CODE_MESSAGES[props.denialCode] || `Denial Code: ${props.denialCode}`; + } + + return 'Order verification denied'; + }; + + return ( + + + {getMessage()} + + + ); +}; + +export default DenialNotification; \ No newline at end of file diff --git a/frontend/src/views/DoctorOrders/OrderCard/OrderCard.tsx b/frontend/src/views/DoctorOrders/OrderCard/OrderCard.tsx index 998678b..84e099b 100644 --- a/frontend/src/views/DoctorOrders/OrderCard/OrderCard.tsx +++ b/frontend/src/views/DoctorOrders/OrderCard/OrderCard.tsx @@ -34,6 +34,8 @@ export type DoctorOrder = { total?: number; pickupDate?: string; dispenseStatus?: string; + denialReasonCode?: string; + remsNote?: string; metRequirements: | { name: string; @@ -194,4 +196,4 @@ const OrderCard = (props: { tabStatus: TabStatus }) => { } }; -export default OrderCard; +export default OrderCard; \ No newline at end of file diff --git a/frontend/src/views/DoctorOrders/OrderCard/VerifyButton.tsx b/frontend/src/views/DoctorOrders/OrderCard/VerifyButton.tsx index 0b6f7c4..74cbfe0 100644 --- a/frontend/src/views/DoctorOrders/OrderCard/VerifyButton.tsx +++ b/frontend/src/views/DoctorOrders/OrderCard/VerifyButton.tsx @@ -1,26 +1,55 @@ import Button from '@mui/material/Button'; import axios from 'axios'; +import { useState } from 'react'; import { DoctorOrder } from './OrderCard'; +import DenialNotification from './DenialNotification'; type VerifyButtonProps = { row: DoctorOrder; getAllDoctorOrders: () => Promise }; const VerifyButton = (props: VerifyButtonProps) => { + const [showDenial, setShowDenial] = useState(false); + const [denialCode, setDenialCode] = useState(); + const [remsNote, setRemsNote] = useState(); + const verifyOrder = () => { const url = '/doctorOrders/api/updateRx/' + props.row._id; axios .patch(url) .then(function (response) { + const updatedOrder = response.data; + + // Check if the order was denied by NCPDP REMS + if (updatedOrder.denialReasonCode) { + setDenialCode(updatedOrder.denialReasonCode); + setRemsNote(updatedOrder.remsNote); + setShowDenial(true); + } + props.getAllDoctorOrders(); console.log(response.data); }) - .catch(error => console.error('Error', error)); + .catch(error => { + console.error('Error', error); + }); + }; + + const handleCloseDenial = () => { + setShowDenial(false); }; return ( - + <> + + + ); }; -export default VerifyButton; +export default VerifyButton; \ No newline at end of file