diff --git a/backend/env.json b/backend/env.json index 327b4c4..766a259 100644 --- a/backend/env.json +++ b/backend/env.json @@ -38,10 +38,6 @@ "default": "admin" }, - "REMS_ADMIN_FHIR_URL": { - "type": "string", - "default": "http://localhost:8090/4_0_0" - }, "HTTPS_KEY_PATH": { "type": "string", "default": "server.key" diff --git a/backend/src/database/data.js b/backend/src/database/data.js new file mode 100644 index 0000000..873fb05 --- /dev/null +++ b/backend/src/database/data.js @@ -0,0 +1,22 @@ +export const medicationRequestToRemsAdmins = Object.freeze([ + { + rxnorm: 2183126, + display: 'Turalio 200 MG Oral Capsule', + remsAdminFhirUrl: process.env.REMS_ADMIN_FHIR_URL || 'http://localhost:8090/4_0_0' + }, + { + rxnorm: 6064, + display: 'Isotretinoin 20 MG Oral Capsule', + remsAdminFhirUrl: process.env.REMS_ADMIN_FHIR_URL || 'http://localhost:8090/4_0_0' + }, + { + rxnorm: 1237051, + display: 'TIRF 200 UG Oral Transmucosal Lozenge', + remsAdminFhirUrl: process.env.REMS_ADMIN_FHIR_URL || 'http://localhost:8090/4_0_0' + }, + { + rxnorm: 1666386, + display: 'Addyi 100 MG Oral Tablet', + remsAdminFhirUrl: process.env.REMS_ADMIN_FHIR_URL || 'http://localhost:8090/4_0_0' + } +]); diff --git a/backend/src/database/schemas/doctorOrderSchemas.js b/backend/src/database/schemas/doctorOrderSchemas.js index e95ff52..10f1f8a 100644 --- a/backend/src/database/schemas/doctorOrderSchemas.js +++ b/backend/src/database/schemas/doctorOrderSchemas.js @@ -24,20 +24,21 @@ export const orderSchema = new mongoose.Schema({ total: Number, pickupDate: String, dispenseStatus: String, - metRequirements: [ - { - name: String, - resource: { - status: String, - moduleUri: String, - resourceType: String, - note: [{ text: String }], - subject: { - reference: String + metRequirements: + [ + { + name: String, + resource: { + status: String, + moduleUri: String, + resourceType: String, + note: [{ text: String }], + subject: { + reference: String + } } } - } - ] + ] | null }); // Compound index is used to prevent duplicates based off of the given parameters diff --git a/backend/src/routes/doctorOrders.js b/backend/src/routes/doctorOrders.js index 0cc151f..1734ee3 100644 --- a/backend/src/routes/doctorOrders.js +++ b/backend/src/routes/doctorOrders.js @@ -8,6 +8,7 @@ import bpx from 'body-parser-xml'; import env from 'var'; import { buildRxStatus, buildRxFill } from '../ncpdpScriptBuilder/buildScript.v2017071.js'; import { NewRx } from '../database/schemas/newRx.js'; +import { medicationRequestToRemsAdmins } from '../database/data.js'; bpx(bodyParser); router.use( @@ -21,14 +22,32 @@ router.use( router.use(bodyParser.urlencoded({ extended: false })); /** - * Route: 'doctorOrders/api/getRx' - * Description : 'Returns all documents in database for PIMS' + * Route: 'doctorOrders/api/getRx/pending' + * Description: 'Returns all pending documents in database for PIMS' */ -router.get('/api/getRx', async (req, res) => { - // finding all and adding it to the db - const order = await doctorOrder.find(); +router.get('/api/getRx/pending', async (_req, res) => { + const order = await doctorOrder.find({ dispenseStatus: 'Pending' }); + console.log('Database returned with new orders'); + res.json(order); +}); - console.log('Database returned with order'); +/** + * Route: 'doctorOrders/api/getRx/approved' + * Description: 'Returns all approved documents in database for PIMS' + */ +router.get('/api/getRx/approved', async (_req, res) => { + const order = await doctorOrder.find({ dispenseStatus: 'Approved' }); + console.log('Database returned with approved orders'); + res.json(order); +}); + +/** + * Route: 'doctorOrders/api/getRx/picked up' + * Description: 'Returns all picked up documents in database for PIMS' + */ +router.get('/api/getRx/pickedUp', async (_req, res) => { + const order = await doctorOrder.find({ dispenseStatus: 'Picked Up' }); + console.log('Database returned with picked up orders'); res.json(order); }); @@ -39,7 +58,7 @@ router.get('/api/getRx', async (req, res) => { router.post('/api/addRx', async (req, res) => { // Parsing incoming NCPDP SCRIPT XML to doctorOrder JSON const newRxMessageConvertedToJSON = req.body; - const newOrder = parseNCPDPScript(newRxMessageConvertedToJSON); + const newOrder = await parseNCPDPScript(newRxMessageConvertedToJSON); try { const newRx = new NewRx({ @@ -67,87 +86,54 @@ router.post('/api/addRx', async (req, res) => { }); /** - * Route: 'doctorOrders/api/updateRx/:_id' + * Route: 'doctorOrders/api/updateRx/:id' * Description : 'Updates prescription based on mongo id, used in etasu' */ router.patch('/api/updateRx/:id', async (req, res) => { try { - const doNotUpdateStatusBool = req.query.doNotUpdateStatus; // Finding by id const order = await doctorOrder.findById(req.params.id).exec(); console.log('Found doctor order by id! --- ', order); - const 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: [ - { - system: 'http://www.nlm.nih.gov/research/umls/rxnorm', - code: order.drugRxnormCode, - display: order.drugNames - } - ] - } - } - } - ] - }; + const guidanceResponse = await getGuidanceResponse(order); + const metRequirements = + guidanceResponse?.contained?.[0]?.['parameter'] || order.metRequirements; + const dispenseStatus = getDispenseStatus(order, guidanceResponse); - // Reaching out to REMS Admin finding by pt name and drug name - const remsBase = env.REMS_ADMIN_FHIR_URL; + // Saving and updating + const newOrder = await doctorOrder.findOneAndUpdate( + { _id: req.params.id }, + { dispenseStatus, metRequirements }, + { new: true } + ); - const newUrl = remsBase + '/GuidanceResponse/$rems-etasu'; + res.send(newOrder); + console.log('Updated order'); + } catch (error) { + console.log('Error', error); + return error; + } +}); - const response = await axios.post(newUrl, body, { - headers: { - 'content-type': 'application/json' - } - }); - console.log('Retrieved order'); - const responseResource = response.data.parameter[0].resource; - const params = []; - if (responseResource.contained && responseResource.contained[0]) { - for (const param of responseResource.contained[0]['parameter']) { - params.push(param); - } - } +/** + * 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 status = responseResource.status === 'success' ? 'Approved' : 'Pending'; + 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 }, - { - dispenseStatus: - doNotUpdateStatusBool || order.dispenseStatus === 'Picked Up' - ? order.dispenseStatus - : status, - metRequirements: params - }, - { - new: true - } + { metRequirements }, + { new: true } ); res.send(newOrder); @@ -239,14 +225,89 @@ router.delete('/api/deleteAll', async (req, res) => { res.send([]); }); +const getRemsAdminFhirUrl = order => { + const rxnorm = order.drugRxnormCode; + const remsDrug = medicationRequestToRemsAdmins.find(entry => { + return Number(rxnorm) === Number(entry.rxnorm); + }); + return remsDrug?.remsAdminFhirUrl; +}; + +const getGuidanceResponse = async order => { + const remsAdminFhirUrl = getRemsAdminFhirUrl(order); + + if (!remsAdminFhirUrl) { + return null; + } + + const 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: [ + { + system: 'http://www.nlm.nih.gov/research/umls/rxnorm', + code: order.drugRxnormCode, + display: order.drugNames + } + ] + } + } + } + ] + }; + + // Reaching out to REMS Admin finding by pt name and drug name + const newUrl = remsAdminFhirUrl + '/GuidanceResponse/$rems-etasu'; + + const response = await axios.post(newUrl, body, { + headers: { + 'content-type': 'application/json' + } + }); + console.log('Retrieved order'); + const responseResource = response.data.parameter[0].resource; + return responseResource; +}; + +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'; + return 'Pending'; +}; + /** * Description : 'Returns parsed NCPDP NewRx as JSON' * In : NCPDP SCRIPT XML * Return : Mongoose schema of a newOrder */ -function parseNCPDPScript(newRx) { +async function parseNCPDPScript(newRx) { // Parsing XML NCPDP SCRIPT from EHR - var newOrder = new doctorOrder({ + const incompleteOrder = { caseNumber: newRx.Message.Header.MessageID.toString(), // Will need to return to this and use actual pt identifier or uuid prescriberOrderNumber: newRx.Message.Header.PrescriberOrderNumber, patientName: @@ -280,11 +341,13 @@ function parseNCPDPScript(newRx) { quantities: newRx.Message.Body.NewRx.MedicationPrescribed.Quantity.Value, total: 1800, pickupDate: 'Tue Dec 13 2022', // Add later? - dispenseStatus: 'Pending', - metRequirements: [] // will fill later - }); + dispenseStatus: 'Pending' + }; - return newOrder; + const isRemsDrug = !!getRemsAdminFhirUrl(incompleteOrder); + const metRequirements = isRemsDrug ? [] : null; + const order = new doctorOrder({ ...incompleteOrder, metRequirements }); + return order; } export default router; diff --git a/frontend/src/views/DoctorOrders/NewOrders/NewOrders.tsx b/frontend/src/views/DoctorOrders/NewOrders/NewOrders.tsx index 799a02c..c282ee5 100644 --- a/frontend/src/views/DoctorOrders/NewOrders/NewOrders.tsx +++ b/frontend/src/views/DoctorOrders/NewOrders/NewOrders.tsx @@ -1,10 +1,10 @@ -import OrderCard from '../OrderCard/OrderCard'; +import OrderCard, { TabStatus } from '../OrderCard/OrderCard'; const NewOrders = () => { return (

New Orders

- +
); }; diff --git a/frontend/src/views/DoctorOrders/OrderCard/EtasuPopUp/EtasuPopUp.tsx b/frontend/src/views/DoctorOrders/OrderCard/EtasuPopUp/EtasuPopUp.tsx index 4dfb26a..fb60d12 100644 --- a/frontend/src/views/DoctorOrders/OrderCard/EtasuPopUp/EtasuPopUp.tsx +++ b/frontend/src/views/DoctorOrders/OrderCard/EtasuPopUp/EtasuPopUp.tsx @@ -54,18 +54,15 @@ const Transition = React.forwardRef(function Transition( const EtasuPopUp = (props: any) => { const [open, setOpen] = React.useState(false); - const [doctorOrder, getDoctorOrders] = useState(); + const [doctorOrder, setDoctorOrder] = useState(); const handleClickOpen = () => { setOpen(true); - // call api endpoint to update - const url = '/doctorOrders/api/updateRx/' + props.data._id + '?doNotUpdateStatus=true'; + const url = '/doctorOrders/api/updateRx/' + props.data._id + '/metRequirements'; axios .patch(url) .then(function (response) { - const DoctorOrders = response.data; - // Adding data to state - getDoctorOrders(DoctorOrders); + setDoctorOrder(response.data); }) .catch(error => console.error('Error', error)); }; diff --git a/frontend/src/views/DoctorOrders/OrderCard/OrderCard.test.tsx b/frontend/src/views/DoctorOrders/OrderCard/OrderCard.test.tsx index 6d82a33..d6df154 100644 --- a/frontend/src/views/DoctorOrders/OrderCard/OrderCard.test.tsx +++ b/frontend/src/views/DoctorOrders/OrderCard/OrderCard.test.tsx @@ -1,5 +1,5 @@ import { render, screen, waitFor } from '@testing-library/react'; -import OrderCard from './OrderCard'; +import OrderCard, { TabStatus } from './OrderCard'; import axios from 'axios'; const doctorOrders = [ @@ -35,7 +35,7 @@ jest.mock('axios'); describe('', () => { it('renders the order card with no doctor orders', async () => { axios.get = jest.fn().mockImplementationOnce(() => Promise.resolve({ data: [] })); - render(); + render(); await waitFor(() => { expect(screen.getByRole('heading', { name: /no orders yet\./i })).toBeInTheDocument(); @@ -44,7 +44,7 @@ describe('', () => { it('renders the order card and any doctor orders', async () => { axios.get = jest.fn().mockImplementationOnce(() => Promise.resolve({ data: doctorOrders })); - render(); + render(); await waitFor(() => { expect(screen.getByText(/Jon Snow/i)).toBeInTheDocument(); diff --git a/frontend/src/views/DoctorOrders/OrderCard/OrderCard.tsx b/frontend/src/views/DoctorOrders/OrderCard/OrderCard.tsx index 4e71ae1..d82068d 100644 --- a/frontend/src/views/DoctorOrders/OrderCard/OrderCard.tsx +++ b/frontend/src/views/DoctorOrders/OrderCard/OrderCard.tsx @@ -33,23 +33,44 @@ export type DoctorOrder = { total?: number; pickupDate?: string; dispenseStatus?: string; - metRequirements: { - name: string; - resource: { - status: string; - moduleUri: string; - resourceType: string; - note: [{ text: string }]; - subject: { - reference: string; - }; - }; - }[]; + metRequirements: + | { + name: string; + resource: { + status: string; + moduleUri: string; + resourceType: string; + note: [{ text: string }]; + subject: { + reference: string; + }; + }; + }[] + | null; _id: string; }; -const OrderCard = (props: { tabStatus: 'Pending' | 'Picked Up' | 'Approved' }) => { - const [doctorOrder, setDoctorOrders] = useState([]); +export enum TabStatus { + PENDING = 'Pending', + APPROVED = 'Approved', + PICKED_UP = 'Picked Up' +} + +const getAllRxEndpoint = (tabStatus: TabStatus): string => { + switch (tabStatus) { + case TabStatus.PENDING: + return '/doctorOrders/api/getRx/pending'; + case TabStatus.APPROVED: + return '/doctorOrders/api/getRx/approved'; + case TabStatus.PICKED_UP: + return '/doctorOrders/api/getRx/pickedUp'; + default: + return 'UNKNOWN_ENDPOINT'; + } +}; + +const OrderCard = (props: { tabStatus: TabStatus }) => { + const [doctorOrders, setDoctorOrders] = useState([]); const [isLoading, setIsLoading] = useState(true); //remove all doctorOrders @@ -58,7 +79,8 @@ const OrderCard = (props: { tabStatus: 'Pending' | 'Picked Up' | 'Approved' }) = setDoctorOrders(orders.data); console.log('Deleting all Doctor Orders'); }; - const url = '/doctorOrders/api/getRx'; + + const url = getAllRxEndpoint(props.tabStatus); // Running after component renders to call api useEffect(() => { @@ -69,9 +91,8 @@ const OrderCard = (props: { tabStatus: 'Pending' | 'Picked Up' | 'Approved' }) = await axios .get(url) .then(function (response) { - const allDoctorOrders = response.data; setIsLoading(false); - setDoctorOrders(allDoctorOrders); + setDoctorOrders(response.data); }) .catch(error => { setIsLoading(false); @@ -79,7 +100,7 @@ const OrderCard = (props: { tabStatus: 'Pending' | 'Picked Up' | 'Approved' }) = }); }; - if (doctorOrder.length < 1 && !isLoading) { + if (doctorOrders.length < 1 && !isLoading) { return (

No orders yet.

@@ -88,69 +109,64 @@ const OrderCard = (props: { tabStatus: 'Pending' | 'Picked Up' | 'Approved' }) = } else { return ( - {doctorOrder.map(row => ( + {doctorOrders.map(row => ( - {/* Checking dispense status for the right tab to display it correctly */} - {/* TODO: We should add an endpoint with the ability to fetch doctor orders based on the - tab/dispense status instead of fetching all doctor orders and filtering them out on the frontend. */} - {props.tabStatus === row.dispenseStatus && ( - - - - - {row.patientName} - - - DOB: {row.patientDOB} - - - {row.drugNames} - - - - - - - Dispense Status - Quantities - Drug Price - Total - Doctor Name - Doctor ID - Doctor Contact - Doctor Email - Pickup Date - - - - - {row.dispenseStatus} - {row.quantities} - {row.drugPrice} - {row.total} - {row.doctorName} - {row.doctorID} - {row.doctorContact} - {row.doctorEmail} - {row.pickupDate} - - -
-
-
- - - - {props.tabStatus === 'Pending' && ( - - )} - {props.tabStatus === 'Approved' && ( - - )} - - -
- )} + + + + + {row.patientName} + + + DOB: {row.patientDOB} + + + {row.drugNames} + + + + + + + Dispense Status + Quantities + Drug Price + Total + Doctor Name + Doctor ID + Doctor Contact + Doctor Email + Pickup Date + + + + + {row.dispenseStatus} + {row.quantities} + {row.drugPrice} + {row.total} + {row.doctorName} + {row.doctorID} + {row.doctorContact} + {row.doctorEmail} + {row.pickupDate} + + +
+
+
+ + + {row.metRequirements !== null && } + {props.tabStatus === 'Pending' && ( + + )} + {props.tabStatus === 'Approved' && ( + + )} + + +
))} Promise }; const VerifyButton = (props: VerifyButtonProps) => { - // verify the order const verifyOrder = () => { const url = '/doctorOrders/api/updateRx/' + props.row._id; axios diff --git a/frontend/src/views/DoctorOrders/PickedUpOrders/PickedUpOrders.tsx b/frontend/src/views/DoctorOrders/PickedUpOrders/PickedUpOrders.tsx index 26b966f..90397a2 100644 --- a/frontend/src/views/DoctorOrders/PickedUpOrders/PickedUpOrders.tsx +++ b/frontend/src/views/DoctorOrders/PickedUpOrders/PickedUpOrders.tsx @@ -1,10 +1,10 @@ -import OrderCard from '../OrderCard/OrderCard'; +import OrderCard, { TabStatus } from '../OrderCard/OrderCard'; const PickedUpOrders = () => { return (

Picked Up Orders

- +
); }; diff --git a/frontend/src/views/DoctorOrders/VerifiedOrders/VerifiedOrders.tsx b/frontend/src/views/DoctorOrders/VerifiedOrders/VerifiedOrders.tsx index 78437c1..54ed666 100644 --- a/frontend/src/views/DoctorOrders/VerifiedOrders/VerifiedOrders.tsx +++ b/frontend/src/views/DoctorOrders/VerifiedOrders/VerifiedOrders.tsx @@ -1,10 +1,10 @@ -import OrderCard from '../OrderCard/OrderCard'; +import OrderCard, { TabStatus } from '../OrderCard/OrderCard'; const VerifiedOrders = () => { return (

Verified Orders

- +
); };