Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions backend/env.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,13 @@
"REMS_ADMIN_NCPDP": {
"type": "string",
"default": "http://localhost:8090/ncpdp/script"
},
"INTERMEDIARY_URL": {
"type": "string",
"default": "http://localhost:8090/ncpdp/script"
},
"EHR_NCPDP_URL": {
"type": "string",
"default": "|| 'http://localhost:8080/ncpdp/script'"
}
}
56 changes: 56 additions & 0 deletions backend/src/lib/pharmacyConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@

// Configuration state
let config = {
useIntermediary: process.env.USE_INTERMEDIARY,
intermediaryUrl: process.env.INTERMEDIARY_URL,
remsAdminUrl: process.env.REMS_ADMIN_NCPDP,
ehrUrl: process.env.EHR_NCPDP_URL
};



export function getConfig() {
return { ...config };
}


export function updateConfig(newConfig) {
config = { ...config, ...newConfig };
console.log('Configuration updated:', config);
return { ...config };
}

/**
* Get the endpoint for NCPDP messages (REMS)
*/
export function getNCPDPEndpoint() {
if (config.useIntermediary) {
return `${config.intermediaryUrl}/ncpdp/script`;
}
return config.remsAdminUrl;
}

/**
* Get the endpoint for ETASU requests
*/
export function getETASUEndpoint() {
if (config.useIntermediary) {
return `${config.intermediaryUrl}/etasu`;
}
// Direct ETASU endpoint to REMS Admin
return config.remsAdminUrl.replace('/ncpdp', '/4_0_0/GuidanceResponse/$rems-etasu');
}

/**
* Get the endpoint for RxFill messages (to EHR)
* RxFill is sent to both EHR and REMS Admin
* If using intermediary, send to intermediary (it forwards to both)
* If not using intermediary, return EHR endpoint (caller must also send to REMS)
*/
export function getRxFillEndpoint() {
if (config.useIntermediary) {
// Intermediary handles forwarding to both EHR and REMS Admin
return `${config.intermediaryUrl}/ncpdp/script`;
}
return config.ehrUrl;
}
102 changes: 71 additions & 31 deletions backend/src/routes/doctorOrders.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// XML Parsing Middleware used for NCPDP SCRIPT
import bodyParser from 'body-parser';
import bpx from 'body-parser-xml';
import { parseStringPromise } from "xml2js";

Check failure on line 8 in backend/src/routes/doctorOrders.js

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier on back end

Strings must use singlequote
import env from 'var';
import {
buildRxStatus,
Expand All @@ -16,6 +16,7 @@
} from '../ncpdpScriptBuilder/buildScript.v2017071.js';
import { NewRx } from '../database/schemas/newRx.js';
import { medicationRequestToRemsAdmins } from '../database/data.js';
import { getConfig, updateConfig, getNCPDPEndpoint, getETASUEndpoint, getRxFillEndpoint } from '../lib/pharmacyConfig.js';

Check warning on line 19 in backend/src/routes/doctorOrders.js

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier on back end

'getETASUEndpoint' is defined but never used

bpx(bodyParser);
router.use(
Expand Down Expand Up @@ -276,37 +277,48 @@

const rxFill = buildRxFill(newRx);
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'
}

const config = getConfig();

if (config.useIntermediary) {
// Send to intermediary - it will forward to both EHR and REMS Admin
const endpoint = getRxFillEndpoint();
console.log(`Sending RxFill to intermediary: ${endpoint}`);
await axios.post(endpoint, rxFill, {
headers: { '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);
}

// Send to REMS Admin (required by NCPDP spec for REMS drugs)
const order = await doctorOrder.findOne({ prescriberOrderNumber });
if (isRemsDrug(order)) {
} else {
// Send to EHR
try {
const remsAdminStatus = await axios.post(
env.REMS_ADMIN_NCPDP,
rxFill,
{
headers: {
Accept: 'application/xml',
'Content-Type': 'application/xml'
}
const ehrStatus = await axios.post(env.EHR_RXFILL_URL, 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 EHR, received status:', ehrStatus.data);
} catch (ehrError) {
console.log('Failed to send RxFill to EHR:', ehrError.message);
}

// 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);
}
}
}
} catch (error) {
Expand Down Expand Up @@ -482,7 +494,7 @@

try {
const response = await axios.post(etasuUrl, body, {
headers: {
headers: {
'content-type': 'application/json'
}
});
Expand Down Expand Up @@ -514,10 +526,13 @@
const initiationRequest = buildREMSInitiationRequest(newRx);
console.log('Sending REMSInitiationRequest to REMS Admin');

console.log(initiationRequest)

Check failure on line 529 in backend/src/routes/doctorOrders.js

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier on back end

Missing semicolon

const endpoint = getNCPDPEndpoint();
console.log(`Sending REMSInitiationRequest to: ${endpoint}`);

const response = await axios.post(
env.REMS_ADMIN_NCPDP,
endpoint,
initiationRequest,
{
headers: {
Expand All @@ -527,6 +542,7 @@
}
);


const parsedResponse = await parseStringPromise(response.data, XML2JS_OPTS);

console.log('Received REMSInitiationResponse');
Expand Down Expand Up @@ -561,10 +577,13 @@

const remsRequest = buildREMSRequest(newRx, order.caseNumber);
console.log('Sending REMSRequest to REMS Admin for case:', order.caseNumber);
console.log(remsRequest)

Check failure on line 580 in backend/src/routes/doctorOrders.js

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier on back end

Missing semicolon

const endpoint = getNCPDPEndpoint();
console.log(`Sending REMSRequest to: ${endpoint}`);

const response = await axios.post(
env.REMS_ADMIN_NCPDP,
endpoint,
remsRequest,
{
headers: {
Expand Down Expand Up @@ -654,7 +673,7 @@
return null;
}

const request = remsResponse.request;

Check warning on line 676 in backend/src/routes/doctorOrders.js

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier on back end

'request' is assigned a value but never used

const response = remsResponse.response;
const responseStatus = response?.responsestatus;
Expand Down Expand Up @@ -774,4 +793,25 @@
return order;
}

/**
* Route: 'doctorOrders/api/config'
* Description: 'Get current pharmacy configuration'
*/
router.get('/api/config', async (_req, res) => {
const config = getConfig();
console.log('Returning configuration:', config);
res.json(config);
});

/**
* Route: 'doctorOrders/api/config'
* Description: 'Update pharmacy configuration'
*/
router.post('/api/config', async (req, res) => {
const newConfig = updateConfig(req.body);
console.log('Configuration updated:', newConfig);
res.json(newConfig);
});


export default router;
4 changes: 3 additions & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import DoctorOrders from './views/DoctorOrders/DoctorOrders';
import Login from './views/Login/Login';
import ProtectedRoute from './components/ProtectedRoute';
import { AuthProvider } from './contexts/AuthContext';
import ConfigToggle from './components/ConfigToggle';
import axios from 'axios';

axios.defaults.baseURL = process.env.REACT_APP_PIMS_BACKEND_URL
Expand Down Expand Up @@ -38,6 +39,7 @@ function App() {
<Link className="NavButtons" to="/Login">
<Button variant="contained">Login</Button>
</Link>
<ConfigToggle />
</div>
</div>
</Container>
Expand All @@ -60,4 +62,4 @@ function App() {
);
}

export default App;
export default App;
85 changes: 85 additions & 0 deletions frontend/src/components/ConfigToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { IconButton, Menu, MenuItem, Switch, Typography, Box, Divider } from '@mui/material';
import SettingsIcon from '@mui/icons-material/Settings';
import { useState, useEffect } from 'react';
import axios from 'axios';

export default function ConfigToggle() {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [useIntermediary, setUseIntermediary] = useState(false);
const open = Boolean(anchorEl);

// Load config on mount
useEffect(() => {
const saved = localStorage.getItem('useIntermediary');
if (saved !== null) {
setUseIntermediary(saved === 'true');
}
}, []);

const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};

const handleClose = () => {
setAnchorEl(null);
};

const handleToggle = async () => {
const newValue = !useIntermediary;
setUseIntermediary(newValue);
localStorage.setItem('useIntermediary', String(newValue));

// Update backend
try {
await axios.post('/doctorOrders/api/config', { useIntermediary: newValue });
console.log('Configuration updated:', newValue ? 'Using Intermediary' : 'Direct Connection');
} catch (error) {
console.error('Failed to update backend config:', error);
}
};

return (
<>
<IconButton
onClick={handleClick}
sx={{
color: 'white',
'&:hover': { backgroundColor: 'rgba(255, 255, 255, 0.1)' }
}}
>
<SettingsIcon />
</IconButton>

<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
PaperProps={{
sx: { minWidth: 280, p: 1 }
}}
>
<Box sx={{ px: 2, py: 1 }}>
<Typography variant="subtitle2" fontWeight="bold">
NCPDP Routing
</Typography>
</Box>
<Divider />
<MenuItem onClick={handleToggle} sx={{ py: 1.5 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '100%' }}>
<Switch checked={useIntermediary} size="small" />
<Box>
<Typography variant="body2" fontWeight="medium">
Use Intermediary
</Typography>
<Typography variant="caption" color="text.secondary">
{useIntermediary
? 'Routing via intermediary'
: 'Direct to REMS Admin'}
</Typography>
</Box>
</Box>
</MenuItem>
</Menu>
</>
);
}
Loading