Skip to content
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ VSAC_API_KEY = changeMe
WHITELIST = *
SERVER_NAME = CodeX REMS Administrator Prototype
FULL_RESOURCE_IN_APP_CONTEXT = false
DOCKERED_EHR_CONTAINER_NAME = false

#Frontend Vars
FRONTEND_PORT=9090
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ Following are a list of modifiable paths:
| WHITELIST | `http://localhost, http://localhost:3005` | List of valid URLs for CORS. Should include any URLs the server accesses for resources. |
| SERVER_NAME | `CodeX REMS Administrator Prototype` | Name of the server that is returned in the card source. |
| FULL_RESOURCE_IN_APP_CONTEXT | 'false' | If true, the entire order resource will be included in the appContext, otherwise only a reference will be. |
| DOCKERED_EHR_CONTAINER_NAME | '' | String of the EHR container name for local docker networking communication |

| FRONTEND_PORT | `9080` | Port that the frontend server should run on, change if there are conflicts with port usage. |
| VITE_REALM | `ClientFhirServer` | Keycloak realm for frontend authentication. |
| VITE_AUTH | `http://localhost:8180` | Keycloak authentication server URL for frontend. |
Expand Down
3 changes: 2 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export default {
fhirServerConfig: {
auth: {
// This server's URI
resourceServer: env.get('RESOURCE_SERVER').required().asUrlString()
resourceServer: env.get('RESOURCE_SERVER').required().asUrlString(),
dockered_ehr_container_name: env.get('DOCKERED_EHR_CONTAINER_NAME').asString()
//
// if you use this strategy, you need to add the corresponding env vars to docker-compose
//
Expand Down
58 changes: 56 additions & 2 deletions src/fhir/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export interface Requirement {
name: string;
description: string;
questionnaire: Questionnaire | null;
stakeholderType: 'patient' | 'prescriber' | 'pharmacist' | string; // From fhir4.Parameters.parameter.name
stakeholderType: 'patient' | 'prescriber' | 'pharmacist' | string;
createNewCase: boolean;
resourceId: string;
requiredToDispense: boolean;
Expand All @@ -15,7 +15,8 @@ export interface Requirement {
export interface Medication extends Document {
name: string;
codeSystem: string;
code: string;
code: string; // RxNorm code (used for CDS Hooks)
ndcCode: string; // NDC code (used for NCPDP SCRIPT)
requirements: Requirement[];
}

Expand All @@ -30,22 +31,41 @@ export interface MetRequirements extends Document {
metRequirementId: any;
}

export interface PrescriptionEvent {
medicationRequestReference: string;
prescriberId: string;
pharmacyId?: string;
timestamp: Date;
originatingFhirServer?: string;
caseStatusAtTime: string;
}

export interface RemsCase extends Document {
case_number: string;
remsPatientId?: string;
status: string;
dispenseStatus: string;
drugName: string;
drugCode: string;
drugNdcCode?: string;
patientFirstName: string;
patientLastName: string;
patientDOB: string;
currentPrescriberId?: string;
currentPharmacyId?: string;
prescriberHistory: string[];
pharmacyHistory: string[];
prescriptionEvents: PrescriptionEvent[];
medicationRequestReference?: string;
originatingFhirServer?: string;
metRequirements: Partial<MetRequirements>[];
}

const medicationCollectionSchema = new Schema<Medication>({
name: { type: String },
codeSystem: { type: String },
code: { type: String },
ndcCode: { type: String },
requirements: [
{
name: { type: String },
Expand All @@ -61,6 +81,8 @@ const medicationCollectionSchema = new Schema<Medication>({
});

medicationCollectionSchema.index({ name: 1 }, { unique: true });
medicationCollectionSchema.index({ code: 1 });
medicationCollectionSchema.index({ ndcCode: 1 });

export const medicationCollection = model<Medication>(
'medicationCollection',
Expand Down Expand Up @@ -89,13 +111,31 @@ export const metRequirementsCollection = model<MetRequirements>(

const remsCaseCollectionSchema = new Schema<RemsCase>({
case_number: { type: String },
remsPatientId: { type: String },
status: { type: String },
dispenseStatus: { type: String },
drugName: { type: String },
patientFirstName: { type: String },
patientLastName: { type: String },
patientDOB: { type: String },
drugCode: { type: String },
drugNdcCode: { type: String },
currentPrescriberId: { type: String },
currentPharmacyId: { type: String },
prescriberHistory: [{ type: String }],
pharmacyHistory: [{ type: String }],
prescriptionEvents: [
{
medicationRequestReference: { type: String },
prescriberId: { type: String },
pharmacyId: { type: String },
timestamp: { type: Date },
originatingFhirServer: { type: String },
caseStatusAtTime: { type: String }
}
],
medicationRequestReference: { type: String },
originatingFhirServer: { type: String },
metRequirements: [
{
metRequirementId: { type: String },
Expand All @@ -107,4 +147,18 @@ const remsCaseCollectionSchema = new Schema<RemsCase>({
]
});

remsCaseCollectionSchema.index({
patientFirstName: 1,
patientLastName: 1,
patientDOB: 1,
drugNdcCode: 1
});

remsCaseCollectionSchema.index({
patientFirstName: 1,
patientLastName: 1,
patientDOB: 1,
drugCode: 1
});

export const remsCaseCollection = model<RemsCase>('RemsCaseCollection', remsCaseCollectionSchema);
4 changes: 4 additions & 0 deletions src/fhir/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export class FhirUtilities {
name: 'Turalio',
codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm',
code: '2183126',
ndcCode: '65597-407-20',
requirements: [
{
name: 'Patient Enrollment',
Expand Down Expand Up @@ -196,6 +197,7 @@ export class FhirUtilities {
name: 'TIRF',
codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm',
code: '1237051',
ndcCode: '63459-502-30',
requirements: [
{
name: 'Patient Enrollment',
Expand Down Expand Up @@ -262,6 +264,7 @@ export class FhirUtilities {
name: 'Isotretinoin',
codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm',
code: '6064',
ndcCode: '0245-0571-01',
requirements: [
{
name: 'Patient Enrollment',
Expand Down Expand Up @@ -305,6 +308,7 @@ export class FhirUtilities {
name: 'Addyi',
codeSystem: 'http://www.nlm.nih.gov/research/umls/rxnorm',
code: '1666386',
ndcCode: '58604-214-30',
requirements: []
}
];
Expand Down
96 changes: 90 additions & 6 deletions src/hooks/hookResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@
import axios from 'axios';
import { ServicePrefetch } from '../rems-cds-hooks/resources/CdsService';
import { hydrate } from '../rems-cds-hooks/prefetch/PrefetchHydrator';
import { createNewRemsCaseFromCDSHook, handleStakeholderChangesAndRecordEvent } from '../lib/etasu';

type HandleCallback = (
res: any,
hydratedPrefetch: HookPrefetch | undefined,
contextRequest: FhirResource | undefined,
patient: FhirResource | undefined
patient: FhirResource | undefined,
fhirServer?: string
) => Promise<void>;

export interface CardRule {
Expand Down Expand Up @@ -366,7 +368,8 @@
res: any,
hydratedPrefetch: HookPrefetch | undefined,
contextRequest: FhirResource | undefined,
resource: FhirResource | undefined
resource: FhirResource | undefined,
fhirServer?: string
): Promise<void> => {
const patient = resource?.resourceType === 'Patient' ? resource : undefined;

Expand Down Expand Up @@ -396,13 +399,82 @@
// find a matching REMS case for the patient and this drug to only return needed results
const patientName = patient?.name?.[0];
const patientBirth = patient?.birthDate;
const remsCase = await remsCaseCollection.findOne({
let remsCase = await remsCaseCollection.findOne({
patientFirstName: patientName?.given?.[0],
patientLastName: patientName?.family,
patientDOB: patientBirth,
drugCode: code
});

// If case exists, check for stakeholder changes and record prescription event
if (remsCase && drug && fhirServer) {
const practitionerReference = request.requester?.reference || '';
const pharmacistReference = pharmacy?.id ? `HealthcareService/${pharmacy.id}` : '';
const medicationRequestReference = `${request.resourceType}/${request.id}`;

const prescriberChanged = remsCase.currentPrescriberId !== practitionerReference;
const pharmacyChanged =
pharmacistReference && remsCase.currentPharmacyId !== pharmacistReference;

if (prescriberChanged || pharmacyChanged) {
try {
const updatedCase = await handleStakeholderChangesAndRecordEvent(
remsCase,
drug,
practitionerReference,
pharmacistReference,
medicationRequestReference,
fhirServer
);
console.log(`Updated case ${updatedCase?.case_number} with stakeholder changes`);
} catch (error) {
console.error('Failed to handle stakeholder changes:', error);
}
} else {
// Record prescription event even if no stakeholder change
remsCase.prescriptionEvents.push({
medicationRequestReference: medicationRequestReference,
prescriberId: practitionerReference,
pharmacyId: pharmacistReference,
timestamp: new Date(),
originatingFhirServer: fhirServer,
caseStatusAtTime: remsCase.status
});
remsCase.medicationRequestReference = medicationRequestReference;
await remsCase.save();
}
}

// If no REMS case exists and drug has requirements, create case with all requirements unmet
if (!remsCase && drug && patient && request) {
const requiresCase = drug.requirements.some(req => req.requiredToDispense);

if (requiresCase && fhirServer) {
try {
const patientReference = `Patient/${patient.id}`;
const medicationRequestReference = `${request.resourceType}/${request.id}`;
const practitionerReference = request.requester?.reference || '';
const pharmacistReference = pharmacy?.id ? `HealthcareService/${pharmacy.id}` : '';

const newCase = await createNewRemsCaseFromCDSHook(
patient,
drug,
practitionerReference,
pharmacistReference,
patientReference,
medicationRequestReference,
fhirServer
);

remsCase = newCase;

console.log(`Created REMS case from CDS Hook with originating server: ${fhirServer}`);
} catch (error) {
console.error('Failed to create REMS case from CDS Hook:', error);
}
}
}

const codeRule = (code && codeMap[code]) || [];

const cardPromises = codeRule.map(
Expand Down Expand Up @@ -494,6 +566,11 @@
const notFound = remsCase && !metRequirement;
const noEtasuToCheckAndRequiredToDispense = !remsCase && requirement.requiredToDispense;

// Only show forms that are not required to dispense (like patient status) if case is approved
if (!requirement.requiredToDispense && remsCase && remsCase.status !== 'Approved') {
return false;
}

return formNotProcessed || notFound || noEtasuToCheckAndRequiredToDispense;
};

Expand Down Expand Up @@ -594,6 +671,7 @@
const context = req.body.context;
const patient = hydratedPrefetch?.patient;
const practitioner = hydratedPrefetch?.practitioner;
const fhirServer = req.body.fhirServer;

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

Expand All @@ -612,7 +690,7 @@
res.json(buildErrorCard('Context userId does not match prefetch Practitioner ID'));
return;
}
return callback(res, hydratedPrefetch, contextRequest, patient);
return callback(res, hydratedPrefetch, contextRequest, patient, fhirServer);
}

// handles all hooks, any supported hook should pass through this function
Expand Down Expand Up @@ -752,7 +830,7 @@

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

// Only show forms that are not required to dispense (like patient status) if case is approved
if (!requirement.requiredToDispense && status !== 'Approved') {
return false;
}

return formNotProcessed || notFound;
};

Expand Down Expand Up @@ -836,7 +919,8 @@
res: any,
hookPrefetch: HookPrefetch | undefined,
_contextRequest: FhirResource | undefined,
resource: FhirResource | undefined
resource: FhirResource | undefined,
fhirServer?: string

Check warning on line 923 in src/hooks/hookResources.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

'fhirServer' is defined but never used. Allowed unused args must match /^_/u
): Promise<void> => {
const patient = resource?.resourceType === 'Patient' ? resource : undefined;
const medResource = hookPrefetch?.medicationRequests;
Expand Down
Loading
Loading