Skip to content

Commit cc61ce1

Browse files
smalho01github-advanced-security[bot]plarocque4
authored
send communication resource to EHR (#182)
* send communication resource to EHR * Potential fix for code scanning alert no. 32: Database query built from user-controlled sources Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * create case on cds hook * update dispense auth * remove dispense auth endpoint - replacing with ncpdp * rename dispense to communcation resource * update create new case to check for existing case * 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 * fix prettier and lint issues * Potential fix for code scanning alert no. 33: Database query built from user-controlled sources Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Patrick LaRocque <plarocque@mitre.org> Co-authored-by: Patrick LaRocque <41444457+plarocque4@users.noreply.github.com>
1 parent 2679296 commit cc61ce1

File tree

11 files changed

+1408
-57
lines changed

11 files changed

+1408
-57
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: 56 additions & 2 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,22 +31,41 @@ 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[];
59+
medicationRequestReference?: string;
60+
originatingFhirServer?: string;
4261
metRequirements: Partial<MetRequirements>[];
4362
}
4463

4564
const medicationCollectionSchema = new Schema<Medication>({
4665
name: { type: String },
4766
codeSystem: { type: String },
4867
code: { type: String },
68+
ndcCode: { type: String },
4969
requirements: [
5070
{
5171
name: { type: String },
@@ -61,6 +81,8 @@ const medicationCollectionSchema = new Schema<Medication>({
6181
});
6282

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

6587
export const medicationCollection = model<Medication>(
6688
'medicationCollection',
@@ -89,13 +111,31 @@ export const metRequirementsCollection = model<MetRequirements>(
89111

90112
const remsCaseCollectionSchema = new Schema<RemsCase>({
91113
case_number: { type: String },
114+
remsPatientId: { type: String },
92115
status: { type: String },
93116
dispenseStatus: { type: String },
94117
drugName: { type: String },
95118
patientFirstName: { type: String },
96119
patientLastName: { type: String },
97120
patientDOB: { type: String },
98121
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+
],
137+
medicationRequestReference: { type: String },
138+
originatingFhirServer: { type: String },
99139
metRequirements: [
100140
{
101141
metRequirementId: { type: String },
@@ -107,4 +147,18 @@ const remsCaseCollectionSchema = new Schema<RemsCase>({
107147
]
108148
});
109149

150+
remsCaseCollectionSchema.index({
151+
patientFirstName: 1,
152+
patientLastName: 1,
153+
patientDOB: 1,
154+
drugNdcCode: 1
155+
});
156+
157+
remsCaseCollectionSchema.index({
158+
patientFirstName: 1,
159+
patientLastName: 1,
160+
patientDOB: 1,
161+
drugCode: 1
162+
});
163+
110164
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: 90 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ 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, handleStakeholderChangesAndRecordEvent } from '../lib/etasu';
2728

2829
type HandleCallback = (
2930
res: any,
3031
hydratedPrefetch: HookPrefetch | undefined,
3132
contextRequest: FhirResource | undefined,
32-
patient: FhirResource | undefined
33+
patient: FhirResource | undefined,
34+
fhirServer?: string
3335
) => Promise<void>;
3436

3537
export interface CardRule {
@@ -366,7 +368,8 @@ export const handleCardOrder = async (
366368
res: any,
367369
hydratedPrefetch: HookPrefetch | undefined,
368370
contextRequest: FhirResource | undefined,
369-
resource: FhirResource | undefined
371+
resource: FhirResource | undefined,
372+
fhirServer?: string
370373
): Promise<void> => {
371374
const patient = resource?.resourceType === 'Patient' ? resource : undefined;
372375

@@ -396,13 +399,82 @@ export const handleCardOrder = async (
396399
// find a matching REMS case for the patient and this drug to only return needed results
397400
const patientName = patient?.name?.[0];
398401
const patientBirth = patient?.birthDate;
399-
const remsCase = await remsCaseCollection.findOne({
402+
let remsCase = await remsCaseCollection.findOne({
400403
patientFirstName: patientName?.given?.[0],
401404
patientLastName: patientName?.family,
402405
patientDOB: patientBirth,
403406
drugCode: code
404407
});
405408

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+
448+
// If no REMS case exists and drug has requirements, create case with all requirements unmet
449+
if (!remsCase && drug && patient && request) {
450+
const requiresCase = drug.requirements.some(req => req.requiredToDispense);
451+
452+
if (requiresCase && fhirServer) {
453+
try {
454+
const patientReference = `Patient/${patient.id}`;
455+
const medicationRequestReference = `${request.resourceType}/${request.id}`;
456+
const practitionerReference = request.requester?.reference || '';
457+
const pharmacistReference = pharmacy?.id ? `HealthcareService/${pharmacy.id}` : '';
458+
459+
const newCase = await createNewRemsCaseFromCDSHook(
460+
patient,
461+
drug,
462+
practitionerReference,
463+
pharmacistReference,
464+
patientReference,
465+
medicationRequestReference,
466+
fhirServer
467+
);
468+
469+
remsCase = newCase;
470+
471+
console.log(`Created REMS case from CDS Hook with originating server: ${fhirServer}`);
472+
} catch (error) {
473+
console.error('Failed to create REMS case from CDS Hook:', error);
474+
}
475+
}
476+
}
477+
406478
const codeRule = (code && codeMap[code]) || [];
407479

408480
const cardPromises = codeRule.map(
@@ -494,6 +566,11 @@ const getCardOrEmptyArrayFromRules =
494566
const notFound = remsCase && !metRequirement;
495567
const noEtasuToCheckAndRequiredToDispense = !remsCase && requirement.requiredToDispense;
496568

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+
497574
return formNotProcessed || notFound || noEtasuToCheckAndRequiredToDispense;
498575
};
499576

@@ -594,6 +671,7 @@ export async function handleCard(
594671
const context = req.body.context;
595672
const patient = hydratedPrefetch?.patient;
596673
const practitioner = hydratedPrefetch?.practitioner;
674+
const fhirServer = req.body.fhirServer;
597675

598676
console.log(' Patient: ' + patient?.id);
599677

@@ -612,7 +690,7 @@ export async function handleCard(
612690
res.json(buildErrorCard('Context userId does not match prefetch Practitioner ID'));
613691
return;
614692
}
615-
return callback(res, hydratedPrefetch, contextRequest, patient);
693+
return callback(res, hydratedPrefetch, contextRequest, patient, fhirServer);
616694
}
617695

618696
// handles all hooks, any supported hook should pass through this function
@@ -752,7 +830,7 @@ const containsMatchingMedicationRequest =
752830

753831
const getCardOrEmptyArrayFromCases =
754832
(entries: BundleEntry[] | undefined) =>
755-
async ({ drugCode, drugName, metRequirements }: RemsCase): Promise<Card | never[]> => {
833+
async ({ drugCode, drugName, metRequirements, status }: RemsCase): Promise<Card | never[]> => {
756834
// find the drug in the medicationCollection that matches the REMS case to get the smart links
757835
const drug = await medicationCollection
758836
.findOne({
@@ -794,6 +872,11 @@ const getCardOrEmptyArrayFromCases =
794872
const formNotProcessed = metRequirement && !metRequirement.completed;
795873
const notFound = !metRequirement;
796874

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+
797880
return formNotProcessed || notFound;
798881
};
799882

@@ -836,7 +919,8 @@ export const handleCardEncounter = async (
836919
res: any,
837920
hookPrefetch: HookPrefetch | undefined,
838921
_contextRequest: FhirResource | undefined,
839-
resource: FhirResource | undefined
922+
resource: FhirResource | undefined,
923+
fhirServer?: string
840924
): Promise<void> => {
841925
const patient = resource?.resourceType === 'Patient' ? resource : undefined;
842926
const medResource = hookPrefetch?.medicationRequests;

0 commit comments

Comments
 (0)