Skip to content

Commit 0009f6c

Browse files
authored
PACIO - added extended case history and prescriber/pharmacy change detection (#183)
* added advanced case history and prescriber/pharmacy change detection * run lint / prettier * ncpdp endpoint implimentation * update routing * add package ndc code tracking for ncpdp messages * fix ndc logic * updated models for patient id tracking * include response type in ncpdp messages * use saved medication on rems case creation to get ndc code from rxnorm * don't show patient status until patient enrolled * rxfill * proper reason code handling * singular denial reason * single denial reason
1 parent 6fcbad3 commit 0009f6c

File tree

11 files changed

+1042
-84
lines changed

11 files changed

+1042
-84
lines changed

.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ VSAC_API_KEY = changeMe
1212
WHITELIST = *
1313
SERVER_NAME = CodeX REMS Administrator Prototype
1414
FULL_RESOURCE_IN_APP_CONTEXT = false
15+
DOCKERED_EHR_CONTAINER_NAME = false
1516

1617
#Frontend Vars
1718
FRONTEND_PORT=9090

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ Following are a list of modifiable paths:
118118
| WHITELIST | `http://localhost, http://localhost:3005` | List of valid URLs for CORS. Should include any URLs the server accesses for resources. |
119119
| SERVER_NAME | `CodeX REMS Administrator Prototype` | Name of the server that is returned in the card source. |
120120
| FULL_RESOURCE_IN_APP_CONTEXT | 'false' | If true, the entire order resource will be included in the appContext, otherwise only a reference will be. |
121+
| DOCKERED_EHR_CONTAINER_NAME | '' | String of the EHR container name for local docker networking communication |
122+
121123
| FRONTEND_PORT | `9080` | Port that the frontend server should run on, change if there are conflicts with port usage. |
122124
| VITE_REALM | `ClientFhirServer` | Keycloak realm for frontend authentication. |
123125
| VITE_AUTH | `http://localhost:8180` | Keycloak authentication server URL for frontend. |

src/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ export default {
4141
fhirServerConfig: {
4242
auth: {
4343
// This server's URI
44-
resourceServer: env.get('RESOURCE_SERVER').required().asUrlString()
44+
resourceServer: env.get('RESOURCE_SERVER').required().asUrlString(),
45+
dockered_ehr_container_name: env.get('DOCKERED_EHR_CONTAINER_NAME').asString()
4546
//
4647
// if you use this strategy, you need to add the corresponding env vars to docker-compose
4748
//

src/fhir/models.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export interface Requirement {
55
name: string;
66
description: string;
77
questionnaire: Questionnaire | null;
8-
stakeholderType: 'patient' | 'prescriber' | 'pharmacist' | string; // From fhir4.Parameters.parameter.name
8+
stakeholderType: 'patient' | 'prescriber' | 'pharmacist' | string;
99
createNewCase: boolean;
1010
resourceId: string;
1111
requiredToDispense: boolean;
@@ -15,7 +15,8 @@ export interface Requirement {
1515
export interface Medication extends Document {
1616
name: string;
1717
codeSystem: string;
18-
code: string;
18+
code: string; // RxNorm code (used for CDS Hooks)
19+
ndcCode: string; // NDC code (used for NCPDP SCRIPT)
1920
requirements: Requirement[];
2021
}
2122

@@ -30,15 +31,31 @@ export interface MetRequirements extends Document {
3031
metRequirementId: any;
3132
}
3233

34+
export interface PrescriptionEvent {
35+
medicationRequestReference: string;
36+
prescriberId: string;
37+
pharmacyId?: string;
38+
timestamp: Date;
39+
originatingFhirServer?: string;
40+
caseStatusAtTime: string;
41+
}
42+
3343
export interface RemsCase extends Document {
3444
case_number: string;
45+
remsPatientId?: string;
3546
status: string;
3647
dispenseStatus: string;
3748
drugName: string;
3849
drugCode: string;
50+
drugNdcCode?: string;
3951
patientFirstName: string;
4052
patientLastName: string;
4153
patientDOB: string;
54+
currentPrescriberId?: string;
55+
currentPharmacyId?: string;
56+
prescriberHistory: string[];
57+
pharmacyHistory: string[];
58+
prescriptionEvents: PrescriptionEvent[];
4259
medicationRequestReference?: string;
4360
originatingFhirServer?: string;
4461
metRequirements: Partial<MetRequirements>[];
@@ -48,6 +65,7 @@ const medicationCollectionSchema = new Schema<Medication>({
4865
name: { type: String },
4966
codeSystem: { type: String },
5067
code: { type: String },
68+
ndcCode: { type: String },
5169
requirements: [
5270
{
5371
name: { type: String },
@@ -63,6 +81,8 @@ const medicationCollectionSchema = new Schema<Medication>({
6381
});
6482

6583
medicationCollectionSchema.index({ name: 1 }, { unique: true });
84+
medicationCollectionSchema.index({ code: 1 });
85+
medicationCollectionSchema.index({ ndcCode: 1 });
6686

6787
export const medicationCollection = model<Medication>(
6888
'medicationCollection',
@@ -91,13 +111,29 @@ export const metRequirementsCollection = model<MetRequirements>(
91111

92112
const remsCaseCollectionSchema = new Schema<RemsCase>({
93113
case_number: { type: String },
114+
remsPatientId: { type: String },
94115
status: { type: String },
95116
dispenseStatus: { type: String },
96117
drugName: { type: String },
97118
patientFirstName: { type: String },
98119
patientLastName: { type: String },
99120
patientDOB: { type: String },
100-
drugCode: { type: String },
121+
drugCode: { type: String },
122+
drugNdcCode: { type: String },
123+
currentPrescriberId: { type: String },
124+
currentPharmacyId: { type: String },
125+
prescriberHistory: [{ type: String }],
126+
pharmacyHistory: [{ type: String }],
127+
prescriptionEvents: [
128+
{
129+
medicationRequestReference: { type: String },
130+
prescriberId: { type: String },
131+
pharmacyId: { type: String },
132+
timestamp: { type: Date },
133+
originatingFhirServer: { type: String },
134+
caseStatusAtTime: { type: String }
135+
}
136+
],
101137
medicationRequestReference: { type: String },
102138
originatingFhirServer: { type: String },
103139
metRequirements: [
@@ -111,4 +147,12 @@ const remsCaseCollectionSchema = new Schema<RemsCase>({
111147
]
112148
});
113149

150+
remsCaseCollectionSchema.index(
151+
{ patientFirstName: 1, patientLastName: 1, patientDOB: 1, drugNdcCode: 1 }
152+
);
153+
154+
remsCaseCollectionSchema.index(
155+
{ patientFirstName: 1, patientLastName: 1, patientDOB: 1, drugCode: 1 }
156+
);
157+
114158
export const remsCaseCollection = model<RemsCase>('RemsCaseCollection', remsCaseCollectionSchema);

src/fhir/utilities.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export class FhirUtilities {
127127
name: 'Turalio',
128128
codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm',
129129
code: '2183126',
130+
ndcCode: '65597-407-20',
130131
requirements: [
131132
{
132133
name: 'Patient Enrollment',
@@ -196,6 +197,7 @@ export class FhirUtilities {
196197
name: 'TIRF',
197198
codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm',
198199
code: '1237051',
200+
ndcCode: '63459-502-30',
199201
requirements: [
200202
{
201203
name: 'Patient Enrollment',
@@ -262,6 +264,7 @@ export class FhirUtilities {
262264
name: 'Isotretinoin',
263265
codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm',
264266
code: '6064',
267+
ndcCode: '0245-0571-01',
265268
requirements: [
266269
{
267270
name: 'Patient Enrollment',
@@ -305,6 +308,7 @@ export class FhirUtilities {
305308
name: 'Addyi',
306309
codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm',
307310
code: '1666386',
311+
ndcCode: '58604-214-30',
308312
requirements: []
309313
}
310314
];

src/hooks/hookResources.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
import axios from 'axios';
2525
import { ServicePrefetch } from '../rems-cds-hooks/resources/CdsService';
2626
import { hydrate } from '../rems-cds-hooks/prefetch/PrefetchHydrator';
27-
import { createNewRemsCaseFromCDSHook } from '../lib/etasu';
27+
import { createNewRemsCaseFromCDSHook, handleStakeholderChangesAndRecordEvent } from '../lib/etasu';
2828

2929
type HandleCallback = (
3030
res: any,
@@ -406,11 +406,50 @@ export const handleCardOrder = async (
406406
drugCode: code
407407
});
408408

409+
// If case exists, check for stakeholder changes and record prescription event
410+
if (remsCase && drug && fhirServer) {
411+
const practitionerReference = request.requester?.reference || '';
412+
const pharmacistReference = pharmacy?.id ? `HealthcareService/${pharmacy.id}` : '';
413+
const medicationRequestReference = `${request.resourceType}/${request.id}`;
414+
415+
const prescriberChanged = remsCase.currentPrescriberId !== practitionerReference;
416+
const pharmacyChanged =
417+
pharmacistReference && remsCase.currentPharmacyId !== pharmacistReference;
418+
419+
if (prescriberChanged || pharmacyChanged) {
420+
try {
421+
const updatedCase = await handleStakeholderChangesAndRecordEvent(
422+
remsCase,
423+
drug,
424+
practitionerReference,
425+
pharmacistReference,
426+
medicationRequestReference,
427+
fhirServer
428+
);
429+
console.log(`Updated case ${updatedCase?.case_number} with stakeholder changes`);
430+
} catch (error) {
431+
console.error('Failed to handle stakeholder changes:', error);
432+
}
433+
} else {
434+
// Record prescription event even if no stakeholder change
435+
remsCase.prescriptionEvents.push({
436+
medicationRequestReference: medicationRequestReference,
437+
prescriberId: practitionerReference,
438+
pharmacyId: pharmacistReference,
439+
timestamp: new Date(),
440+
originatingFhirServer: fhirServer,
441+
caseStatusAtTime: remsCase.status
442+
});
443+
remsCase.medicationRequestReference = medicationRequestReference;
444+
await remsCase.save();
445+
}
446+
}
447+
409448
// If no REMS case exists and drug has requirements, create case with all requirements unmet
410449
if (!remsCase && drug && patient && request) {
411450
const requiresCase = drug.requirements.some(req => req.requiredToDispense);
412451

413-
if (requiresCase && fhirServer) {
452+
if (requiresCase && fhirServer) {
414453
try {
415454
const patientReference = `Patient/${patient.id}`;
416455
const medicationRequestReference = `${request.resourceType}/${request.id}`;
@@ -527,6 +566,11 @@ const getCardOrEmptyArrayFromRules =
527566
const notFound = remsCase && !metRequirement;
528567
const noEtasuToCheckAndRequiredToDispense = !remsCase && requirement.requiredToDispense;
529568

569+
// Only show forms that are not required to dispense (like patient status) if case is approved
570+
if (!requirement.requiredToDispense && remsCase && remsCase.status !== 'Approved') {
571+
return false;
572+
}
573+
530574
return formNotProcessed || notFound || noEtasuToCheckAndRequiredToDispense;
531575
};
532576

@@ -786,7 +830,7 @@ const containsMatchingMedicationRequest =
786830

787831
const getCardOrEmptyArrayFromCases =
788832
(entries: BundleEntry[] | undefined) =>
789-
async ({ drugCode, drugName, metRequirements }: RemsCase): Promise<Card | never[]> => {
833+
async ({ drugCode, drugName, metRequirements, status }: RemsCase): Promise<Card | never[]> => {
790834
// find the drug in the medicationCollection that matches the REMS case to get the smart links
791835
const drug = await medicationCollection
792836
.findOne({
@@ -828,6 +872,11 @@ const getCardOrEmptyArrayFromCases =
828872
const formNotProcessed = metRequirement && !metRequirement.completed;
829873
const notFound = !metRequirement;
830874

875+
// Only show forms that are not required to dispense (like patient status) if case is approved
876+
if (!requirement.requiredToDispense && status !== 'Approved') {
877+
return false;
878+
}
879+
831880
return formNotProcessed || notFound;
832881
};
833882

src/lib/communication.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@ import { Requirement } from '../fhir/models';
88

99
const logger = container.get('application');
1010

11-
1211
export async function sendCommunicationToEHR(
1312
remsCase: any,
1413
medication: any,
1514
outstandingRequirements: any[]
1615
): Promise<void> {
1716
try {
1817
logger.info(`Creating Communication for case ${remsCase.case_number}`);
19-
18+
2019
// Create patient object from REMS case
2120
const patient: Patient = {
2221
resourceType: 'Patient',
@@ -60,9 +59,10 @@ export async function sendCommunicationToEHR(
6059
// Create Tasks for each outstanding requirement
6160
const tasks: Task[] = [];
6261
for (const outstandingReq of outstandingRequirements) {
63-
const requirement = outstandingReq.requirement ||
62+
const requirement =
63+
outstandingReq.requirement ||
6464
medication.requirements.find((r: Requirement) => r.name === outstandingReq.name);
65-
65+
6666
if (requirement && requirement.appContext) {
6767
const questionnaireUrl = requirement.appContext;
6868
const task = createQuestionnaireCompletionTask(
@@ -80,7 +80,7 @@ export async function sendCommunicationToEHR(
8080
const communication: Communication = {
8181
resourceType: 'Communication',
8282
id: `comm-${uid()}`,
83-
status: 'completed',
83+
status: 'completed',
8484
category: [
8585
{
8686
coding: [
@@ -92,7 +92,7 @@ export async function sendCommunicationToEHR(
9292
]
9393
}
9494
],
95-
priority: 'urgent',
95+
priority: 'urgent',
9696
subject: {
9797
reference: `Patient/${patient.id}`,
9898
display: `${remsCase.patientFirstName} ${remsCase.patientLastName}`
@@ -107,7 +107,7 @@ export async function sendCommunicationToEHR(
107107
],
108108
text: 'Outstanding REMS Requirements for Medication Dispensing'
109109
},
110-
sent: new Date().toISOString(),
110+
sent: new Date().toISOString(),
111111
recipient: [
112112
{
113113
reference: medicationRequest.requester?.reference || ''
@@ -119,8 +119,9 @@ export async function sendCommunicationToEHR(
119119
},
120120
payload: [
121121
{
122-
contentString: `Medication dispensing authorization DENIED for ${remsCase.drugName}.\n\n` +
123-
`The following REMS requirements must be completed:\n\n` +
122+
contentString:
123+
`Medication dispensing authorization DENIED for ${remsCase.drugName}.\n\n` +
124+
'The following REMS requirements must be completed:\n\n' +
124125
outstandingRequirements
125126
.map((req, idx) => `${idx + 1}. ${req.name} (${req.stakeholder})`)
126127
.join('\n') +
@@ -142,17 +143,24 @@ export async function sendCommunicationToEHR(
142143
};
143144

144145
// Determine EHR endpoint: use originatingFhirServer if available, otherwise default
145-
const ehrEndpoint = remsCase.originatingFhirServer ||
146-
config.fhirServerConfig?.auth?.resourceServer;
146+
let ehrEndpoint =
147+
remsCase.originatingFhirServer || config.fhirServerConfig?.auth?.resourceServer;
147148

148149
if (!ehrEndpoint) {
149150
logger.warn('No EHR endpoint configured, Communication not sent');
150151
return;
151152
}
152153

154+
if (config.fhirServerConfig.auth.dockered_ehr_container_name) {
155+
const originalEhrEndpoint = ehrEndpoint;
156+
ehrEndpoint = originalEhrEndpoint.replace(/localhost/g, config.fhirServerConfig.auth.dockered_ehr_container_name)
157+
.replace(/127\.0\.0\.1/g, config.fhirServerConfig.auth.dockered_ehr_container_name);
158+
logger.info(`Running locally in Docker, converting EHR url from ${originalEhrEndpoint} to ${ehrEndpoint}`);
159+
}
160+
153161
// Send Communication to EHR
154162
logger.info(`Sending Communication to EHR: ${ehrEndpoint}`);
155-
163+
156164
const response = await axios.post(`${ehrEndpoint}/Communication`, communication, {
157165
headers: {
158166
'Content-Type': 'application/fhir+json'
@@ -164,9 +172,8 @@ export async function sendCommunicationToEHR(
164172
} else {
165173
logger.warn(`Unexpected response status from EHR: ${response.status}`);
166174
}
167-
168175
} catch (error: any) {
169176
logger.error(`Failed to send Communication to EHR: ${error.message}`);
170-
throw error;
177+
throw error;
171178
}
172-
}
179+
}

0 commit comments

Comments
 (0)