Skip to content
Open
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
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

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