From 488434838f8d188357a9bf909e8742ad52ec9f17 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 08:56:03 -0500 Subject: [PATCH 01/14] send rems initiation and rems request --- .../database/schemas/doctorOrderSchemas.js | 6 +- .../buildScript.v2017071.js | 223 +++++- backend/src/routes/doctorOrders.js | 704 ++++++++++++++---- backend/src/routes/ncpdp.js | 6 +- 4 files changed, 774 insertions(+), 165 deletions(-) diff --git a/backend/src/database/schemas/doctorOrderSchemas.js b/backend/src/database/schemas/doctorOrderSchemas.js index aba505c..3fe1966 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, + denialReasonCodes: 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..3fc4543 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,25 @@ 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 spec'); const time = new Date(); + const message = { Message: { + '@@DatatypesVersion': '2024011', + '@@TransportVersion': '2024011', + '@@TransactionDomain': 'SCRIPT', + '@@TransactionVersion': '2024011', + '@@StructuresVersion': '2024011', + '@@ECLVersion': '2024011', Header: [ { To: { @@ -117,10 +140,10 @@ 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, @@ -144,3 +167,185 @@ export const buildRxFill = newRx => { 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..2593a55 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' @@ -60,6 +71,7 @@ router.get('/api/getRx/pickedUp', async (_req, res) => { */ export async function processNewRx(newRxMessageConvertedToJSON) { console.log('processNewRx NCPDP SCRIPT message'); + console.log(JSON.stringify(newRxMessageConvertedToJSON)); const newOrder = await parseNCPDPScript(newRxMessageConvertedToJSON); try { @@ -84,7 +96,44 @@ 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, + metRequirements: initiationResponse.metRequirements || [] + }; + + 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.denialReasonCodes = initiationResponse.reasonCodes.join(','); + console.log('REMSInitiation CLOSED:', initiationResponse.reasonCodes); + } + + await doctorOrder.updateOne({ _id: newOrder._id }, updateData); + console.log('Updated order with REMSInitiation response'); + } + } catch (error) { + console.log('Error processing REMSInitiationRequest:', error); + } + } + return rxStatus; } /** @@ -101,28 +150,69 @@ 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), + metRequirements: ncpdpResponse.metRequirements || order.metRequirements + }; + + 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}`; + if (ncpdpResponse.etasuSummary) { + approvalNote += `\n\nETASU Requirements Met:\n${ncpdpResponse.etasuSummary}`; + } + updateData.remsNote = approvalNote; + updateData.denialReasonCodes = null; + console.log('APPROVED:', ncpdpResponse.authorizationNumber); + } else if (ncpdpResponse.status === 'DENIED') { + updateData.denialReasonCodes = ncpdpResponse.reasonCodes.join(','); + updateData.remsNote = ncpdpResponse.remsNote; + updateData.caseNumber = ncpdpResponse.caseId; + console.log('DENIED:', ncpdpResponse.reasonCodes); + } - // 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; @@ -131,27 +221,57 @@ router.patch('/api/updateRx/:id', async (req, res) => { /** * Route: 'doctorOrders/api/updateRx/:id/metRequirements' - * Description : 'Updates prescription metRequirements based on mongo id' + * Description : 'Refreshes metRequirements via NCPDP REMSRequest' */ router.patch('/api/updateRx/:id/metRequirements', 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; + // Non-REMS drugs have no requirements + if (!isRemsDrug(order)) { + res.send(order); + return; + } + + // Check if we have a case number + if (!order.caseNumber) { + console.log('No case number available - need REMSInitiation first'); + res.send(order); + return; + } + + // REMS drugs with case number - refresh via REMSRequest + console.log('Refreshing REMS requirements via REMSRequest for case:', order.caseNumber); + const remsResponse = await sendREMSRequest(order); + + if (!remsResponse) { + res.send(order); + console.log('REMSRequest failed'); + return; + } + + const updateData = { + metRequirements: remsResponse.metRequirements || order.metRequirements, + remsNote: remsResponse.remsNote + }; + + if (remsResponse.status === 'APPROVED') { + // Don't change dispense status here - only update requirements info + updateData.authorizationNumber = remsResponse.authorizationNumber; + updateData.authorizationExpiration = remsResponse.authorizationExpiration; + } else if (remsResponse.status === 'DENIED') { + updateData.denialReasonCodes = remsResponse.reasonCodes.join(','); + } - // Saving and updating const newOrder = await doctorOrder.findOneAndUpdate( { _id: req.params.id }, - { metRequirements }, + updateData, { new: true } ); res.send(newOrder); - console.log('Updated order'); + console.log('Updated metRequirements from NCPDP REMSRequest'); } catch (error) { console.log('Error', error); return error; @@ -160,7 +280,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 +298,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 +361,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]; } } @@ -262,137 +399,402 @@ const isRemsDrug = order => { }); }; -const getEtasuUrl = order => { - let baseUrl; +/** + * 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; + } - if (env.USE_INTERMEDIARY) { - baseUrl = env.INTERMEDIARY_FHIR_URL; - } else { - const remsDrug = medicationRequestToRemsAdmins.find(entry => { - if (order.drugNdcCode && entry.ndc) { - return order.drugNdcCode === entry.ndc; - } + const initiationRequest = buildREMSInitiationRequest(newRx); + console.log('Sending REMSInitiationRequest to REMS Admin'); - if (order.drugRxnormCode && entry.rxnorm) { - return Number(order.drugRxnormCode) === Number(entry.rxnorm); + console.log(initiationRequest) + + const response = await axios.post( + env.REMS_ADMIN_NCPDP, + initiationRequest, + { + headers: { + Accept: 'application/xml', + 'Content-Type': 'application/xml' + } } + ); - return false; - }); - baseUrl = remsDrug?.remsAdminFhirUrl; + 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 etasuUrl = baseUrl + '/GuidanceResponse/$rems-etasu'; - return baseUrl ? etasuUrl : null; + const remsRequest = buildREMSRequest(newRx, order.caseNumber); + console.log('Sending REMSRequest to REMS Admin for case:', order.caseNumber); + + 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; + } }; -const getGuidanceResponse = async order => { - const etasuUrl = getEtasuUrl(order); +/** + * Parse NCPDP REMSInitiationResponse per spec + * Extracts case info, status, and requirements + */ +const parseREMSInitiationResponse = parsedXml => { + const message = parsedXml?.Message || parsedXml?.message; + const body = message?.Body || message?.body; + const initResponse = body?.REMSInitiationResponse || body?.remsinitiationresponse; + console.log(message); + console.log(initResponse); + + if (!initResponse) { + console.log('No REMSInitiationResponse found'); + return null; + } + + const response = initResponse.Response || initResponse.response; + const responseStatus = response?.ResponseStatus || response?.responsestatus; - if (!etasuUrl) { + // Check for Closed status (requirements not met) + const closed = responseStatus?.Closed || responseStatus?.closed; + if (closed) { + let reasonCodes = closed.ReasonCode || closed.reasoncode; + if (!Array.isArray(reasonCodes)) { + reasonCodes = [reasonCodes]; + } + const remsNote = closed.REMSNote || closed.remsnote || ''; + + return { + status: 'CLOSED', + reasonCodes: reasonCodes, + remsNote: remsNote, + metRequirements: parseReasonCodesToRequirements(reasonCodes, remsNote) + }; + } + + // Extract case ID and patient ID from successful initiation + const patient = initResponse.Patient || initResponse.patient; + const humanPatient = patient?.HumanPatient || patient?.humanpatient; + const identification = humanPatient?.Identification || humanPatient?.identification; + const remsPatientId = identification?.REMSPatientID || identification?.remspatientid; + + // Check if there's a case number in the response + let caseNumber = null; + const medication = initResponse.MedicationPrescribed || 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, + metRequirements: [] // No outstanding requirements + }; +}; + +/** + * Parse NCPDP REMSResponse per spec + * Extracts authorization status, case ID, and ETASU requirements from QuestionSet + */ +const parseREMSResponse = parsedXml => { + const message = parsedXml?.Message || parsedXml?.message; + const body = message?.Body || message?.body; + const remsResponse = body?.REMSResponse || body?.remsresponse; + console.log(message); + console.log(remsResponse); + + if (!remsResponse) { + console.log('No REMSResponse found'); return null; } - // Make the etasu call with the case number if it exists, if not call with patient and medication - let body = {}; - if (order.caseNumber && !env.USE_INTERMEDIARY) { - body = { - resourceType: 'Parameters', - parameter: [ - { - name: 'caseNumber', - valueString: order.caseNumber + const request = remsResponse.Request || remsResponse.request; + const solicitedModel = request?.SolicitedModel || request?.solicitedmodel; + const questionSet = solicitedModel?.QuestionSet || solicitedModel?.questionset; + + const response = remsResponse.Response || remsResponse.response; + const responseStatus = response?.ResponseStatus || response?.responsestatus; + + // Check for APPROVED status + const approved = responseStatus?.Approved || responseStatus?.approved; + if (approved) { + const caseId = approved.REMSCaseID || approved.remscaseid; + const authNumber = approved.REMSAuthorizationNumber || approved.remsauthorizationnumber; + const authPeriod = approved.AuthorizationPeriod || approved.authorizationperiod; + const expiration = + authPeriod?.ExpirationDate?.Date || + authPeriod?.expirationdate?.date || + authPeriod?.expirationdate?.Date; + + // Parse QuestionSet to extract ETASU that were checked + const etasuInfo = questionSet ? parseQuestionSetToETASU(questionSet) : null; + + // Create summary of met requirements + let etasuSummary = ''; + let metRequirements = []; + + if (etasuInfo && etasuInfo.questions.length > 0) { + etasuSummary = etasuInfo.questions + .map(q => `• ${q.questionText}: ${q.answer}`) + .join('\n'); + + // Convert questions to metRequirements format + metRequirements = etasuInfo.questions.map((q, idx) => ({ + name: q.questionText, + resource: { + status: 'success', + resourceType: 'Observation', + moduleUri: q.questionId, + note: [{ text: `Verified: ${q.answer}` }], + subject: { + reference: 'patient' + } } - ] - }; - } else { - let medicationCoding = []; - - if (order.drugNdcCode) { - medicationCoding.push({ - system: 'http://hl7.org/fhir/sid/ndc', - code: order.drugNdcCode, - display: order.drugNames - }); + })); } - if (order.drugRxnormCode) { - medicationCoding.push({ - system: 'http://www.nlm.nih.gov/research/umls/rxnorm', - code: order.drugRxnormCode, - display: order.drugNames - }); - } else { - const remsDrug = medicationRequestToRemsAdmins.find(entry => { - return order.drugNdcCode && entry.ndc && order.drugNdcCode === entry.ndc; - }); + return { + status: 'APPROVED', + caseId: caseId, + authorizationNumber: authNumber, + authorizationExpiration: expiration, + remsNote: 'All REMS requirements have been met and verified. Authorization granted for dispensing.', + etasuSummary: etasuSummary, + metRequirements: metRequirements + }; + } - if (remsDrug && remsDrug.rxnorm) { - medicationCoding.push({ - system: 'http://www.nlm.nih.gov/research/umls/rxnorm', - code: remsDrug.rxnorm.toString(), - display: order.drugNames - }); - } + // Check for DENIED status + const denied = responseStatus?.Denied || responseStatus?.denied; + if (denied) { + const caseId = denied.REMSCaseID || denied.remscaseid; + let reasonCodes = denied.DeniedReasonCode || denied.deniedreason || denied.deniedreason.code; + if (!Array.isArray(reasonCodes)) { + reasonCodes = [reasonCodes]; + } + const remsNote = denied.REMSNote || denied.remsnote || ''; + + // Parse QuestionSet if present to show which ETASU failed + const etasuInfo = questionSet ? parseQuestionSetToETASU(questionSet) : null; + + // Convert to metRequirements with failure status + let metRequirements = parseReasonCodesToRequirements(reasonCodes, remsNote); + + // Add QuestionSet information if available + if (etasuInfo && etasuInfo.questions.length > 0) { + const questionReqs = etasuInfo.questions.map((q, idx) => ({ + name: q.questionText, + resource: { + status: 'pending', + resourceType: 'Task', + moduleUri: q.questionId, + note: [{ text: `Required: ${q.questionText}` }], + subject: { + reference: 'patient' + } + } + })); + // Prepend question-based requirements + metRequirements = [...questionReqs, ...metRequirements]; } - body = { - resourceType: 'Parameters', - parameter: [ - { - name: 'patient', - resource: { - resourceType: 'Patient', - id: order.prescriberOrderNumber, - name: [ - { - family: order.patientLastName, - given: order.patientName.split(' '), - use: 'official' + return { + status: 'DENIED', + caseId: caseId, + reasonCodes: reasonCodes, + remsNote: remsNote, + metRequirements: metRequirements + }; + } + + return null; +}; + +/** + * Parse NCPDP QuestionSet to extract ETASU requirements + * Per NCPDP spec: QuestionSet contains the REMS questions and answers + */ +const parseQuestionSetToETASU = questionSet => { + const header = questionSet.Header || questionSet.header; + const questions = questionSet.Question || questionSet.question; + + const questionArray = Array.isArray(questions) ? questions : [questions]; + + const parsedQuestions = questionArray + .filter(q => q) // Filter out null/undefined + .map(q => { + const questionId = q.QuestionID || q.questionid; + const sequenceNumber = q.SequenceNumber || q.sequencenumber; + const questionText = q.QuestionText || q.questiontext; + const questionType = q.QuestionType || q.questiontype; + + // Extract answer if present + let answer = 'Not answered'; + if (questionType) { + const select = questionType.Select || questionType.select; + if (select) { + const answerObj = select.Answer || select.answer; + if (answerObj) { + const submittedAnswer = + answerObj.SubmitterProvidedAnswer || answerObj.submitterprovided.answer; + if (submittedAnswer) { + const choiceId = submittedAnswer.ChoiceID || submittedAnswer.choiceid; + + // Find the choice text + const choices = select.Choice || select.choice; + const choiceArray = Array.isArray(choices) ? choices : [choices]; + + const matchingChoice = choiceArray.find(c => { + const cId = c.ChoiceID || c.choiceid; + return cId === choiceId; + }); + + if (matchingChoice) { + answer = matchingChoice.ChoiceText || matchingChoice.choicetext || choiceId; + } else { + answer = choiceId || 'Yes'; } - ], - birthDate: order.patientDOB - } - }, - { - name: 'medication', - resource: { - resourceType: 'Medication', - id: order.prescriberOrderNumber, - code: { - coding: medicationCoding } } } - ] + } + + return { + questionId, + sequenceNumber, + questionText, + answer + }; + }); + + return { + questionSetId: header?.QuestionSetID || header?.questionsetid, + questionSetTitle: header?.QuestionSetTitle || header?.questionsettitle, + questions: parsedQuestions + }; +}; + +/** + * Convert NCPDP reason codes to metRequirements format + * Per NCPDP spec: Reason codes indicate which stakeholder requirements are not met + */ +const parseReasonCodesToRequirements = (reasonCodes, remsNote) => { + const codes = Array.isArray(reasonCodes) ? reasonCodes : [reasonCodes]; + const requirements = []; + + // NCPDP Reason Code mapping per spec + const reasonCodeMap = { + EM: { name: 'Patient Enrollment/Certification', stakeholder: 'patient' }, + ES: { name: 'Prescriber Enrollment/Certification', stakeholder: 'prescriber' }, + EO: { name: 'Pharmacy Enrollment/Certification', stakeholder: 'pharmacy' }, + EC: { name: 'Case Information', stakeholder: 'system' }, + ER: { name: 'REMS Program Error', stakeholder: 'system' }, + EX: { name: 'Prescriber Deactivated/Decertified', stakeholder: 'prescriber' }, + EY: { name: 'Pharmacy Deactivated/Decertified', stakeholder: 'pharmacy' }, + EZ: { name: 'Patient Deactivated/Decertified', stakeholder: 'patient' } + }; + + codes.forEach(code => { + const mapping = reasonCodeMap[code] || { + name: `REMS Requirement (${code})`, + stakeholder: 'unknown' }; - } - const response = await axios.post(etasuUrl, body, { - headers: { - 'content-type': 'application/json' - } + requirements.push({ + name: mapping.name, + resource: { + status: 'pending', + resourceType: 'Task', + moduleUri: `rems-requirement-${code}`, + note: [{ text: remsNote || `${mapping.name} required` }], + subject: { + reference: mapping.stakeholder + } + } + }); }); - console.log('Retrieved order', JSON.stringify(response.data, null, 4)); - console.log('URL', etasuUrl); - const responseResource = response.data.parameter?.[0]?.resource; - return responseResource; + + return requirements; }; -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 +803,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 +826,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 +848,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 From d7fc774e692ae2cffe8e14c39d8a5c828fc3024a Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 09:00:53 -0500 Subject: [PATCH 02/14] remove questionset logic - unused --- backend/src/routes/doctorOrders.js | 69 ++---------------------------- 1 file changed, 3 insertions(+), 66 deletions(-) diff --git a/backend/src/routes/doctorOrders.js b/backend/src/routes/doctorOrders.js index 2593a55..7adb9f2 100644 --- a/backend/src/routes/doctorOrders.js +++ b/backend/src/routes/doctorOrders.js @@ -70,8 +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'); - console.log(JSON.stringify(newRxMessageConvertedToJSON)); const newOrder = await parseNCPDPScript(newRxMessageConvertedToJSON); try { @@ -143,6 +141,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'); @@ -464,6 +464,7 @@ const sendREMSRequest = async order => { 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, @@ -663,70 +664,6 @@ const parseREMSResponse = parsedXml => { return null; }; -/** - * Parse NCPDP QuestionSet to extract ETASU requirements - * Per NCPDP spec: QuestionSet contains the REMS questions and answers - */ -const parseQuestionSetToETASU = questionSet => { - const header = questionSet.Header || questionSet.header; - const questions = questionSet.Question || questionSet.question; - - const questionArray = Array.isArray(questions) ? questions : [questions]; - - const parsedQuestions = questionArray - .filter(q => q) // Filter out null/undefined - .map(q => { - const questionId = q.QuestionID || q.questionid; - const sequenceNumber = q.SequenceNumber || q.sequencenumber; - const questionText = q.QuestionText || q.questiontext; - const questionType = q.QuestionType || q.questiontype; - - // Extract answer if present - let answer = 'Not answered'; - if (questionType) { - const select = questionType.Select || questionType.select; - if (select) { - const answerObj = select.Answer || select.answer; - if (answerObj) { - const submittedAnswer = - answerObj.SubmitterProvidedAnswer || answerObj.submitterprovided.answer; - if (submittedAnswer) { - const choiceId = submittedAnswer.ChoiceID || submittedAnswer.choiceid; - - // Find the choice text - const choices = select.Choice || select.choice; - const choiceArray = Array.isArray(choices) ? choices : [choices]; - - const matchingChoice = choiceArray.find(c => { - const cId = c.ChoiceID || c.choiceid; - return cId === choiceId; - }); - - if (matchingChoice) { - answer = matchingChoice.ChoiceText || matchingChoice.choicetext || choiceId; - } else { - answer = choiceId || 'Yes'; - } - } - } - } - } - - return { - questionId, - sequenceNumber, - questionText, - answer - }; - }); - - return { - questionSetId: header?.QuestionSetID || header?.questionsetid, - questionSetTitle: header?.QuestionSetTitle || header?.questionsettitle, - questions: parsedQuestions - }; -}; - /** * Convert NCPDP reason codes to metRequirements format * Per NCPDP spec: Reason codes indicate which stakeholder requirements are not met From 25622a4f24faa73f3cc079eeb77e82d2a3819854 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 10:06:37 -0500 Subject: [PATCH 03/14] rxfill update --- .../buildScript.v2017071.js | 44 +++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/backend/src/ncpdpScriptBuilder/buildScript.v2017071.js b/backend/src/ncpdpScriptBuilder/buildScript.v2017071.js index 3fc4543..d58c72a 100644 --- a/backend/src/ncpdpScriptBuilder/buildScript.v2017071.js +++ b/backend/src/ncpdpScriptBuilder/buildScript.v2017071.js @@ -100,17 +100,44 @@ export function buildRxError(newRxMessageConvertedToJSON, errorMessage) { export const buildRxFill = newRx => { const { Message } = JSON.parse(newRx.serializedJSON); const { Header, Body } = Message; - console.log('Building RxFill per NCPDP spec'); + 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: { + 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': '2024011', - '@@TransportVersion': '2024011', + '@@DatatypesVersion': '20170715', + '@@TransportVersion': '20170715', '@@TransactionDomain': 'SCRIPT', - '@@TransactionVersion': '2024011', - '@@StructuresVersion': '2024011', - '@@ECLVersion': '2024011', + '@@TransactionVersion': '20170715', + '@@StructuresVersion': '20170715', + '@@ECLVersion': '20170715', Header: [ { To: { @@ -127,6 +154,7 @@ export const buildRxFill = newRx => { { MessageID: uuidv4() }, { RelatesToMessageID: Header.MessageID }, { SentTime: time.toISOString() }, + { RxReferenceNumber: Header.MessageID }, { PrescriberOrderNumber: Header.PrescriberOrderNumber } ], Body: [ @@ -149,7 +177,7 @@ export const buildRxFill = newRx => { City: MOCK_VALUE, StateProvince: MOCK_VALUE, PostalCode: MOCK_VALUE, - Country: MOCK_VALUE + CountryCode: 'US' }, CommunicationNumbers: { PrimaryTelephone: { @@ -158,7 +186,7 @@ export const buildRxFill = newRx => { } }, Prescriber: Body.NewRx.Prescriber, - MedicationPrescribed: Body.NewRx.MedicationPrescribed + MedicationDispensed: medicationDispensed } } ] From 794bd5e75dfd4552b5885ae19ab58fbe445fe54d Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 10:36:29 -0500 Subject: [PATCH 04/14] rxfill --- backend/src/ncpdpScriptBuilder/buildScript.v2017071.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/src/ncpdpScriptBuilder/buildScript.v2017071.js b/backend/src/ncpdpScriptBuilder/buildScript.v2017071.js index d58c72a..fbd3953 100644 --- a/backend/src/ncpdpScriptBuilder/buildScript.v2017071.js +++ b/backend/src/ncpdpScriptBuilder/buildScript.v2017071.js @@ -110,6 +110,10 @@ export const buildRxFill = newRx => { 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, From 1a82e640c3e8b6163e8b08b1b30aaf36fb4c5ff2 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 11:29:49 -0500 Subject: [PATCH 05/14] single denial code --- backend/src/routes/doctorOrders.js | 125 +++++++++++++---------------- 1 file changed, 58 insertions(+), 67 deletions(-) diff --git a/backend/src/routes/doctorOrders.js b/backend/src/routes/doctorOrders.js index 7adb9f2..42f0e77 100644 --- a/backend/src/routes/doctorOrders.js +++ b/backend/src/routes/doctorOrders.js @@ -120,8 +120,9 @@ export async function processNewRx(newRxMessageConvertedToJSON) { } if (initiationResponse.status === 'CLOSED') { - updateData.denialReasonCodes = initiationResponse.reasonCodes.join(','); - console.log('REMSInitiation CLOSED:', initiationResponse.reasonCodes); + // Per NCPDP spec: only one denial code at a time + updateData.denialReasonCodes = initiationResponse.reasonCode; + console.log('REMSInitiation CLOSED:', initiationResponse.reasonCode); } await doctorOrder.updateOne({ _id: newOrder._id }, updateData); @@ -199,10 +200,11 @@ router.patch('/api/updateRx/:id', async (req, res) => { updateData.denialReasonCodes = null; console.log('APPROVED:', ncpdpResponse.authorizationNumber); } else if (ncpdpResponse.status === 'DENIED') { - updateData.denialReasonCodes = ncpdpResponse.reasonCodes.join(','); + // Per NCPDP spec: only one denial code at a time + updateData.denialReasonCodes = ncpdpResponse.reasonCode; updateData.remsNote = ncpdpResponse.remsNote; updateData.caseNumber = ncpdpResponse.caseId; - console.log('DENIED:', ncpdpResponse.reasonCodes); + console.log('DENIED:', ncpdpResponse.reasonCode); } const newOrder = await doctorOrder.findOneAndUpdate( @@ -493,9 +495,9 @@ const sendREMSRequest = async order => { * Extracts case info, status, and requirements */ const parseREMSInitiationResponse = parsedXml => { - const message = parsedXml?.Message || parsedXml?.message; - const body = message?.Body || message?.body; - const initResponse = body?.REMSInitiationResponse || body?.remsinitiationresponse; + const message = parsedXml?.message; + const body = message?.body; + const initResponse = body?.remsinitiationresponse; console.log(message); console.log(initResponse); @@ -504,35 +506,32 @@ const parseREMSInitiationResponse = parsedXml => { return null; } - const response = initResponse.Response || initResponse.response; - const responseStatus = response?.ResponseStatus || response?.responsestatus; + const response = initResponse.response; + const responseStatus = response?.responsestatus; // Check for Closed status (requirements not met) - const closed = responseStatus?.Closed || responseStatus?.closed; + const closed = responseStatus?.closed; if (closed) { - let reasonCodes = closed.ReasonCode || closed.reasoncode; - if (!Array.isArray(reasonCodes)) { - reasonCodes = [reasonCodes]; - } - const remsNote = closed.REMSNote || closed.remsnote || ''; + const reasonCode = closed.reasoncode; + const remsNote = closed.remsnote || ''; return { status: 'CLOSED', - reasonCodes: reasonCodes, + reasonCode: reasonCode, remsNote: remsNote, - metRequirements: parseReasonCodesToRequirements(reasonCodes, remsNote) + metRequirements: parseReasonCodeToRequirements(reasonCode, remsNote) }; } // Extract case ID and patient ID from successful initiation - const patient = initResponse.Patient || initResponse.patient; - const humanPatient = patient?.HumanPatient || patient?.humanpatient; - const identification = humanPatient?.Identification || humanPatient?.identification; - const remsPatientId = identification?.REMSPatientID || identification?.remspatientid; + 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 || initResponse.medicationprescribed; + 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 @@ -551,9 +550,9 @@ const parseREMSInitiationResponse = parsedXml => { * Extracts authorization status, case ID, and ETASU requirements from QuestionSet */ const parseREMSResponse = parsedXml => { - const message = parsedXml?.Message || parsedXml?.message; - const body = message?.Body || message?.body; - const remsResponse = body?.REMSResponse || body?.remsresponse; + const message = parsedXml?.message; + const body = message?.body; + const remsResponse = body?.remsresponse; console.log(message); console.log(remsResponse); @@ -562,23 +561,20 @@ const parseREMSResponse = parsedXml => { return null; } - const request = remsResponse.Request || remsResponse.request; - const solicitedModel = request?.SolicitedModel || request?.solicitedmodel; - const questionSet = solicitedModel?.QuestionSet || solicitedModel?.questionset; + const request = remsResponse.request; + const solicitedModel = request?.solicitedmodel; + const questionSet = solicitedModel?.questionset; - const response = remsResponse.Response || remsResponse.response; - const responseStatus = response?.ResponseStatus || response?.responsestatus; + const response = remsResponse.response; + const responseStatus = response?.responsestatus; // Check for APPROVED status - const approved = responseStatus?.Approved || responseStatus?.approved; + const approved = responseStatus?.approved; if (approved) { - const caseId = approved.REMSCaseID || approved.remscaseid; - const authNumber = approved.REMSAuthorizationNumber || approved.remsauthorizationnumber; - const authPeriod = approved.AuthorizationPeriod || approved.authorizationperiod; - const expiration = - authPeriod?.ExpirationDate?.Date || - authPeriod?.expirationdate?.date || - authPeriod?.expirationdate?.Date; + const caseId = approved.remscaseid; + const authNumber = approved.remsauthorizationnumber; + const authPeriod = approved.authorizationperiod; + const expiration = authPeriod?.expirationdate?.date; // Parse QuestionSet to extract ETASU that were checked const etasuInfo = questionSet ? parseQuestionSetToETASU(questionSet) : null; @@ -619,20 +615,18 @@ const parseREMSResponse = parsedXml => { } // Check for DENIED status - const denied = responseStatus?.Denied || responseStatus?.denied; + const denied = responseStatus?.denied; if (denied) { - const caseId = denied.REMSCaseID || denied.remscaseid; - let reasonCodes = denied.DeniedReasonCode || denied.deniedreason || denied.deniedreason.code; - if (!Array.isArray(reasonCodes)) { - reasonCodes = [reasonCodes]; - } - const remsNote = denied.REMSNote || denied.remsnote || ''; + const caseId = denied.remscaseid; + // Per NCPDP spec: DeniedReasonCode is a single code, not an array + const reasonCode = denied.deniedreason code; + const remsNote = denied.remsnote || ''; // Parse QuestionSet if present to show which ETASU failed const etasuInfo = questionSet ? parseQuestionSetToETASU(questionSet) : null; // Convert to metRequirements with failure status - let metRequirements = parseReasonCodesToRequirements(reasonCodes, remsNote); + let metRequirements = parseReasonCodeToRequirements(reasonCode, remsNote); // Add QuestionSet information if available if (etasuInfo && etasuInfo.questions.length > 0) { @@ -655,7 +649,7 @@ const parseREMSResponse = parsedXml => { return { status: 'DENIED', caseId: caseId, - reasonCodes: reasonCodes, + reasonCode: reasonCode, remsNote: remsNote, metRequirements: metRequirements }; @@ -665,11 +659,10 @@ const parseREMSResponse = parsedXml => { }; /** - * Convert NCPDP reason codes to metRequirements format - * Per NCPDP spec: Reason codes indicate which stakeholder requirements are not met + * Convert NCPDP reason code to metRequirements format + * Per NCPDP spec: Reason code indicates which stakeholder requirement is not met */ -const parseReasonCodesToRequirements = (reasonCodes, remsNote) => { - const codes = Array.isArray(reasonCodes) ? reasonCodes : [reasonCodes]; +const parseReasonCodeToRequirements = (reasonCode, remsNote) => { const requirements = []; // NCPDP Reason Code mapping per spec @@ -684,24 +677,22 @@ const parseReasonCodesToRequirements = (reasonCodes, remsNote) => { EZ: { name: 'Patient Deactivated/Decertified', stakeholder: 'patient' } }; - codes.forEach(code => { - const mapping = reasonCodeMap[code] || { - name: `REMS Requirement (${code})`, - stakeholder: 'unknown' - }; + const mapping = reasonCodeMap[reasonCode] || { + name: `REMS Requirement (${reasonCode})`, + stakeholder: 'unknown' + }; - requirements.push({ - name: mapping.name, - resource: { - status: 'pending', - resourceType: 'Task', - moduleUri: `rems-requirement-${code}`, - note: [{ text: remsNote || `${mapping.name} required` }], - subject: { - reference: mapping.stakeholder - } + requirements.push({ + name: mapping.name, + resource: { + status: 'pending', + resourceType: 'Task', + moduleUri: `rems-requirement-${reasonCode}`, + note: [{ text: remsNote || `${mapping.name} required` }], + subject: { + reference: mapping.stakeholder } - }); + } }); return requirements; From ae832a6f4412b7a2999d0afc5d58454399ad7192 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 11:30:07 -0500 Subject: [PATCH 06/14] single denial code --- backend/src/routes/doctorOrders.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/backend/src/routes/doctorOrders.js b/backend/src/routes/doctorOrders.js index 42f0e77..63a7d13 100644 --- a/backend/src/routes/doctorOrders.js +++ b/backend/src/routes/doctorOrders.js @@ -120,7 +120,6 @@ export async function processNewRx(newRxMessageConvertedToJSON) { } if (initiationResponse.status === 'CLOSED') { - // Per NCPDP spec: only one denial code at a time updateData.denialReasonCodes = initiationResponse.reasonCode; console.log('REMSInitiation CLOSED:', initiationResponse.reasonCode); } @@ -200,7 +199,6 @@ router.patch('/api/updateRx/:id', async (req, res) => { updateData.denialReasonCodes = null; console.log('APPROVED:', ncpdpResponse.authorizationNumber); } else if (ncpdpResponse.status === 'DENIED') { - // Per NCPDP spec: only one denial code at a time updateData.denialReasonCodes = ncpdpResponse.reasonCode; updateData.remsNote = ncpdpResponse.remsNote; updateData.caseNumber = ncpdpResponse.caseId; @@ -618,8 +616,7 @@ const parseREMSResponse = parsedXml => { const denied = responseStatus?.denied; if (denied) { const caseId = denied.remscaseid; - // Per NCPDP spec: DeniedReasonCode is a single code, not an array - const reasonCode = denied.deniedreason code; + const reasonCode = denied.deniedreasoncode; const remsNote = denied.remsnote || ''; // Parse QuestionSet if present to show which ETASU failed From 65b5f2b11006a3e2daff7b866318d21b4b40c9e3 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 11:32:40 -0500 Subject: [PATCH 07/14] single denial reason --- backend/src/database/schemas/doctorOrderSchemas.js | 2 +- backend/src/routes/doctorOrders.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/database/schemas/doctorOrderSchemas.js b/backend/src/database/schemas/doctorOrderSchemas.js index 3fe1966..c0cd577 100644 --- a/backend/src/database/schemas/doctorOrderSchemas.js +++ b/backend/src/database/schemas/doctorOrderSchemas.js @@ -27,7 +27,7 @@ export const orderSchema = new mongoose.Schema({ dispenseStatus: String, authorizationNumber: String, authorizationExpiration: String, - denialReasonCodes: String, + denialReasonCode: String, remsNote: String, metRequirements: [ { diff --git a/backend/src/routes/doctorOrders.js b/backend/src/routes/doctorOrders.js index 63a7d13..e05e39c 100644 --- a/backend/src/routes/doctorOrders.js +++ b/backend/src/routes/doctorOrders.js @@ -120,7 +120,7 @@ export async function processNewRx(newRxMessageConvertedToJSON) { } if (initiationResponse.status === 'CLOSED') { - updateData.denialReasonCodes = initiationResponse.reasonCode; + updateData.denialReasonCode = initiationResponse.reasonCode; console.log('REMSInitiation CLOSED:', initiationResponse.reasonCode); } @@ -196,10 +196,10 @@ router.patch('/api/updateRx/:id', async (req, res) => { approvalNote += `\n\nETASU Requirements Met:\n${ncpdpResponse.etasuSummary}`; } updateData.remsNote = approvalNote; - updateData.denialReasonCodes = null; + updateData.denialReasonCode = null; console.log('APPROVED:', ncpdpResponse.authorizationNumber); } else if (ncpdpResponse.status === 'DENIED') { - updateData.denialReasonCodes = ncpdpResponse.reasonCode; + updateData.denialReasonCode = ncpdpResponse.reasonCode; updateData.remsNote = ncpdpResponse.remsNote; updateData.caseNumber = ncpdpResponse.caseId; console.log('DENIED:', ncpdpResponse.reasonCode); @@ -261,7 +261,7 @@ router.patch('/api/updateRx/:id/metRequirements', async (req, res) => { updateData.authorizationNumber = remsResponse.authorizationNumber; updateData.authorizationExpiration = remsResponse.authorizationExpiration; } else if (remsResponse.status === 'DENIED') { - updateData.denialReasonCodes = remsResponse.reasonCodes.join(','); + updateData.denialReasonCode = remsResponse.reasonCodes; } const newOrder = await doctorOrder.findOneAndUpdate( From c557d2298867357d31f43d9792701da0fe73671a Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 11:38:27 -0500 Subject: [PATCH 08/14] remove question set logic --- backend/src/routes/doctorOrders.js | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/backend/src/routes/doctorOrders.js b/backend/src/routes/doctorOrders.js index e05e39c..c493975 100644 --- a/backend/src/routes/doctorOrders.js +++ b/backend/src/routes/doctorOrders.js @@ -545,7 +545,7 @@ const parseREMSInitiationResponse = parsedXml => { /** * Parse NCPDP REMSResponse per spec - * Extracts authorization status, case ID, and ETASU requirements from QuestionSet + * Extracts authorization status, case ID, and NCPDP rejection code */ const parseREMSResponse = parsedXml => { const message = parsedXml?.message; @@ -561,7 +561,6 @@ const parseREMSResponse = parsedXml => { const request = remsResponse.request; const solicitedModel = request?.solicitedmodel; - const questionSet = solicitedModel?.questionset; const response = remsResponse.response; const responseStatus = response?.responsestatus; @@ -574,9 +573,6 @@ const parseREMSResponse = parsedXml => { const authPeriod = approved.authorizationperiod; const expiration = authPeriod?.expirationdate?.date; - // Parse QuestionSet to extract ETASU that were checked - const etasuInfo = questionSet ? parseQuestionSetToETASU(questionSet) : null; - // Create summary of met requirements let etasuSummary = ''; let metRequirements = []; @@ -619,30 +615,9 @@ const parseREMSResponse = parsedXml => { const reasonCode = denied.deniedreasoncode; const remsNote = denied.remsnote || ''; - // Parse QuestionSet if present to show which ETASU failed - const etasuInfo = questionSet ? parseQuestionSetToETASU(questionSet) : null; - // Convert to metRequirements with failure status let metRequirements = parseReasonCodeToRequirements(reasonCode, remsNote); - // Add QuestionSet information if available - if (etasuInfo && etasuInfo.questions.length > 0) { - const questionReqs = etasuInfo.questions.map((q, idx) => ({ - name: q.questionText, - resource: { - status: 'pending', - resourceType: 'Task', - moduleUri: q.questionId, - note: [{ text: `Required: ${q.questionText}` }], - subject: { - reference: 'patient' - } - } - })); - // Prepend question-based requirements - metRequirements = [...questionReqs, ...metRequirements]; - } - return { status: 'DENIED', caseId: caseId, From 0000e01a4b0adedb71007da7f8febb3f1b0bc861 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 12:05:24 -0500 Subject: [PATCH 09/14] clean up --- backend/src/routes/doctorOrders.js | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/routes/doctorOrders.js b/backend/src/routes/doctorOrders.js index c493975..4981fa7 100644 --- a/backend/src/routes/doctorOrders.js +++ b/backend/src/routes/doctorOrders.js @@ -560,7 +560,6 @@ const parseREMSResponse = parsedXml => { } const request = remsResponse.request; - const solicitedModel = request?.solicitedmodel; const response = remsResponse.response; const responseStatus = response?.responsestatus; From e4bc1f738ddbe696f718fdda5d2fdd75f9155ca1 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 12:08:05 -0500 Subject: [PATCH 10/14] replace view etasu in backend --- backend/src/routes/doctorOrders.js | 67 +++++++++++++++--------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/backend/src/routes/doctorOrders.js b/backend/src/routes/doctorOrders.js index 4981fa7..0dba59c 100644 --- a/backend/src/routes/doctorOrders.js +++ b/backend/src/routes/doctorOrders.js @@ -221,57 +221,56 @@ router.patch('/api/updateRx/:id', async (req, res) => { /** * Route: 'doctorOrders/api/updateRx/:id/metRequirements' - * Description : 'Refreshes metRequirements via NCPDP REMSRequest' + * Description : 'Updates prescription metRequirements based on mongo id' */ router.patch('/api/updateRx/:id/metRequirements', async (req, res) => { try { + // Finding by id const order = await doctorOrder.findById(req.params.id).exec(); console.log('Found doctor order by id! --- ', order); - // Non-REMS drugs have no requirements - if (!isRemsDrug(order)) { - res.send(order); - return; - } - - // Check if we have a case number - if (!order.caseNumber) { - console.log('No case number available - need REMSInitiation first'); - res.send(order); - return; - } + const guidanceResponse = await getGuidanceResponse(order); + const metRequirements = + guidanceResponse?.contained?.[0]?.['parameter'] || order.metRequirements; - // REMS drugs with case number - refresh via REMSRequest - console.log('Refreshing REMS requirements via REMSRequest for case:', order.caseNumber); - const remsResponse = await sendREMSRequest(order); + // Saving and updating + const newOrder = await doctorOrder.findOneAndUpdate( + { _id: req.params.id }, + { metRequirements }, + { new: true } + ); - if (!remsResponse) { - res.send(order); - console.log('REMSRequest failed'); - return; - } + res.send(newOrder); + console.log('Updated order'); + } catch (error) { + console.log('Error', error); + return error; + } +}); - const updateData = { - metRequirements: remsResponse.metRequirements || order.metRequirements, - remsNote: remsResponse.remsNote - }; +/** + * Route: 'doctorOrders/api/updateRx/:id/metRequirements' + * Description : 'Updates prescription metRequirements based on mongo id' + */ +router.patch('/api/updateRx/:id/metRequirements', async (req, res) => { + try { + // Finding by id + const order = await doctorOrder.findById(req.params.id).exec(); + console.log('Found doctor order by id! --- ', order); - if (remsResponse.status === 'APPROVED') { - // Don't change dispense status here - only update requirements info - updateData.authorizationNumber = remsResponse.authorizationNumber; - updateData.authorizationExpiration = remsResponse.authorizationExpiration; - } else if (remsResponse.status === 'DENIED') { - updateData.denialReasonCode = remsResponse.reasonCodes; - } + const guidanceResponse = await getGuidanceResponse(order); + const metRequirements = + guidanceResponse?.contained?.[0]?.['parameter'] || order.metRequirements; + // Saving and updating const newOrder = await doctorOrder.findOneAndUpdate( { _id: req.params.id }, - updateData, + { metRequirements }, { new: true } ); res.send(newOrder); - console.log('Updated metRequirements from NCPDP REMSRequest'); + console.log('Updated order'); } catch (error) { console.log('Error', error); return error; From 008e5098937e3dd9a217fd415bb827884891740a Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 12:29:36 -0500 Subject: [PATCH 11/14] return helper functions for old guidance response approach --- backend/src/routes/doctorOrders.js | 131 +++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/backend/src/routes/doctorOrders.js b/backend/src/routes/doctorOrders.js index 0dba59c..5006ab9 100644 --- a/backend/src/routes/doctorOrders.js +++ b/backend/src/routes/doctorOrders.js @@ -398,6 +398,137 @@ const isRemsDrug = order => { }); }; + +/** + * Get FHIR ETASU URL for the order + * Used for GuidanceResponse calls (View ETASU) + */ +const getEtasuUrl = order => { + let baseUrl; + + if (env.USE_INTERMEDIARY) { + baseUrl = env.INTERMEDIARY_FHIR_URL; + } else { + const remsDrug = medicationRequestToRemsAdmins.find(entry => { + if (order.drugNdcCode && entry.ndc) { + return order.drugNdcCode === entry.ndc; + } + + if (order.drugRxnormCode && entry.rxnorm) { + return Number(order.drugRxnormCode) === Number(entry.rxnorm); + } + + return false; + }); + baseUrl = remsDrug?.remsAdminFhirUrl; + } + + const etasuUrl = baseUrl + '/GuidanceResponse/$rems-etasu'; + return baseUrl ? etasuUrl : null; +}; + +/** + * Get FHIR GuidanceResponse for ETASU requirements + * Used by View ETASU button + */ +const getGuidanceResponse = async order => { + const etasuUrl = getEtasuUrl(order); + + if (!etasuUrl) { + return null; + } + + // Make the etasu call with the case number if it exists, if not call with patient and medication + let body = {}; + if (order.caseNumber && !env.USE_INTERMEDIARY) { + body = { + resourceType: 'Parameters', + parameter: [ + { + name: 'caseNumber', + valueString: order.caseNumber + } + ] + }; + } else { + let medicationCoding = []; + + if (order.drugNdcCode) { + medicationCoding.push({ + system: 'http://hl7.org/fhir/sid/ndc', + code: order.drugNdcCode, + display: order.drugNames + }); + } + + if (order.drugRxnormCode) { + medicationCoding.push({ + system: 'http://www.nlm.nih.gov/research/umls/rxnorm', + code: order.drugRxnormCode, + display: order.drugNames + }); + } else { + const remsDrug = medicationRequestToRemsAdmins.find(entry => { + return order.drugNdcCode && entry.ndc && order.drugNdcCode === entry.ndc; + }); + + if (remsDrug && remsDrug.rxnorm) { + medicationCoding.push({ + system: 'http://www.nlm.nih.gov/research/umls/rxnorm', + code: remsDrug.rxnorm.toString(), + display: order.drugNames + }); + } + } + + body = { + resourceType: 'Parameters', + parameter: [ + { + name: 'patient', + resource: { + resourceType: 'Patient', + id: order.prescriberOrderNumber, + name: [ + { + family: order.patientLastName, + given: order.patientName.split(' '), + use: 'official' + } + ], + birthDate: order.patientDOB + } + }, + { + name: 'medication', + resource: { + resourceType: 'Medication', + id: order.prescriberOrderNumber, + code: { + coding: medicationCoding + } + } + } + ] + }; + } + + 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 From 8a4891cb9e2a689fc7c6c76826e74bdcb94480e5 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 12:47:00 -0500 Subject: [PATCH 12/14] frontend changes for fhir/ncpdp hybrid approach --- .../OrderCard/DenialNotification.tsx | 50 +++++++++++++++++++ .../DoctorOrders/OrderCard/OrderCard.tsx | 4 +- .../DoctorOrders/OrderCard/VerifyButton.tsx | 39 +++++++++++++-- 3 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 frontend/src/views/DoctorOrders/OrderCard/DenialNotification.tsx 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 From 9226bdd3f2e06905f4ae752c9390cc8bd37a56f2 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 13:05:26 -0500 Subject: [PATCH 13/14] patch metReq update --- backend/src/routes/doctorOrders.js | 107 ++--------------------------- 1 file changed, 4 insertions(+), 103 deletions(-) diff --git a/backend/src/routes/doctorOrders.js b/backend/src/routes/doctorOrders.js index 5006ab9..fdaca5d 100644 --- a/backend/src/routes/doctorOrders.js +++ b/backend/src/routes/doctorOrders.js @@ -106,8 +106,7 @@ export async function processNewRx(newRxMessageConvertedToJSON) { if (initiationResponse) { const updateData = { - remsNote: initiationResponse.remsNote, - metRequirements: initiationResponse.metRequirements || [] + remsNote: initiationResponse.remsNote }; if (initiationResponse.caseNumber) { @@ -181,8 +180,7 @@ router.patch('/api/updateRx/:id', async (req, res) => { // Update based on NCPDP response const updateData = { - dispenseStatus: getDispenseStatus(order, ncpdpResponse), - metRequirements: ncpdpResponse.metRequirements || order.metRequirements + dispenseStatus: getDispenseStatus(order, ncpdpResponse) }; if (ncpdpResponse.status === 'APPROVED') { @@ -248,35 +246,6 @@ router.patch('/api/updateRx/:id/metRequirements', async (req, res) => { } }); -/** - * Route: 'doctorOrders/api/updateRx/:id/metRequirements' - * Description : 'Updates prescription metRequirements based on mongo id' - */ -router.patch('/api/updateRx/:id/metRequirements', 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; - - // Saving and updating - const newOrder = await doctorOrder.findOneAndUpdate( - { _id: req.params.id }, - { metRequirements }, - { new: true } - ); - - res.send(newOrder); - console.log('Updated order'); - } catch (error) { - console.log('Error', error); - return error; - } -}); - /** * Route: 'doctorOrders/api/updateRx/:id/pickedUp' * Description : 'Updates prescription dispense status to picked up and sends RxFill per NCPDP spec' @@ -385,6 +354,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; @@ -647,7 +617,6 @@ const parseREMSInitiationResponse = parsedXml => { status: 'CLOSED', reasonCode: reasonCode, remsNote: remsNote, - metRequirements: parseReasonCodeToRequirements(reasonCode, remsNote) }; } @@ -669,7 +638,6 @@ const parseREMSInitiationResponse = parsedXml => { status: 'OPEN', remsPatientId: remsPatientId, caseNumber: caseNumber, - metRequirements: [] // No outstanding requirements }; }; @@ -702,30 +670,6 @@ const parseREMSResponse = parsedXml => { const authPeriod = approved.authorizationperiod; const expiration = authPeriod?.expirationdate?.date; - // Create summary of met requirements - let etasuSummary = ''; - let metRequirements = []; - - if (etasuInfo && etasuInfo.questions.length > 0) { - etasuSummary = etasuInfo.questions - .map(q => `• ${q.questionText}: ${q.answer}`) - .join('\n'); - - // Convert questions to metRequirements format - metRequirements = etasuInfo.questions.map((q, idx) => ({ - name: q.questionText, - resource: { - status: 'success', - resourceType: 'Observation', - moduleUri: q.questionId, - note: [{ text: `Verified: ${q.answer}` }], - subject: { - reference: 'patient' - } - } - })); - } - return { status: 'APPROVED', caseId: caseId, @@ -733,7 +677,6 @@ const parseREMSResponse = parsedXml => { authorizationExpiration: expiration, remsNote: 'All REMS requirements have been met and verified. Authorization granted for dispensing.', etasuSummary: etasuSummary, - metRequirements: metRequirements }; } @@ -744,61 +687,19 @@ const parseREMSResponse = parsedXml => { const reasonCode = denied.deniedreasoncode; const remsNote = denied.remsnote || ''; - // Convert to metRequirements with failure status - let metRequirements = parseReasonCodeToRequirements(reasonCode, remsNote); + return { status: 'DENIED', caseId: caseId, reasonCode: reasonCode, remsNote: remsNote, - metRequirements: metRequirements }; } return null; }; -/** - * Convert NCPDP reason code to metRequirements format - * Per NCPDP spec: Reason code indicates which stakeholder requirement is not met - */ -const parseReasonCodeToRequirements = (reasonCode, remsNote) => { - const requirements = []; - - // NCPDP Reason Code mapping per spec - const reasonCodeMap = { - EM: { name: 'Patient Enrollment/Certification', stakeholder: 'patient' }, - ES: { name: 'Prescriber Enrollment/Certification', stakeholder: 'prescriber' }, - EO: { name: 'Pharmacy Enrollment/Certification', stakeholder: 'pharmacy' }, - EC: { name: 'Case Information', stakeholder: 'system' }, - ER: { name: 'REMS Program Error', stakeholder: 'system' }, - EX: { name: 'Prescriber Deactivated/Decertified', stakeholder: 'prescriber' }, - EY: { name: 'Pharmacy Deactivated/Decertified', stakeholder: 'pharmacy' }, - EZ: { name: 'Patient Deactivated/Decertified', stakeholder: 'patient' } - }; - - const mapping = reasonCodeMap[reasonCode] || { - name: `REMS Requirement (${reasonCode})`, - stakeholder: 'unknown' - }; - - requirements.push({ - name: mapping.name, - resource: { - status: 'pending', - resourceType: 'Task', - moduleUri: `rems-requirement-${reasonCode}`, - note: [{ text: remsNote || `${mapping.name} required` }], - subject: { - reference: mapping.stakeholder - } - } - }); - - return requirements; -}; - /** * Determine dispense status based on NCPDP response */ From 2f15d916924c3f9f448959896d085b71da501e72 Mon Sep 17 00:00:00 2001 From: Sahil Malhotra Date: Wed, 14 Jan 2026 14:46:21 -0500 Subject: [PATCH 14/14] updates for success case --- backend/src/routes/doctorOrders.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/src/routes/doctorOrders.js b/backend/src/routes/doctorOrders.js index fdaca5d..841ac08 100644 --- a/backend/src/routes/doctorOrders.js +++ b/backend/src/routes/doctorOrders.js @@ -190,9 +190,6 @@ router.patch('/api/updateRx/:id', async (req, res) => { // Format approval note with ETASU summary let approvalNote = `APPROVED - Authorization: ${ncpdpResponse.authorizationNumber}, Expires: ${ncpdpResponse.authorizationExpiration}`; - if (ncpdpResponse.etasuSummary) { - approvalNote += `\n\nETASU Requirements Met:\n${ncpdpResponse.etasuSummary}`; - } updateData.remsNote = approvalNote; updateData.denialReasonCode = null; console.log('APPROVED:', ncpdpResponse.authorizationNumber); @@ -676,7 +673,6 @@ const parseREMSResponse = parsedXml => { authorizationNumber: authNumber, authorizationExpiration: expiration, remsNote: 'All REMS requirements have been met and verified. Authorization granted for dispensing.', - etasuSummary: etasuSummary, }; }