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
50 changes: 47 additions & 3 deletions src/fhir/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
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 Medication extends Document {
name: string;
codeSystem: string;
code: string;
code: string; // RxNorm code (used for CDS Hooks)

Check failure on line 18 in src/fhir/models.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Delete `·`

Check failure on line 18 in src/fhir/models.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Delete `·`
ndcCode: string; // NDC code (used for NCPDP SCRIPT)

Check failure on line 19 in src/fhir/models.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Delete `·`

Check failure on line 19 in src/fhir/models.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Delete `·`
requirements: Requirement[];
}

Expand All @@ -30,15 +31,31 @@
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>[];
Expand All @@ -48,6 +65,7 @@
name: { type: String },
codeSystem: { type: String },
code: { type: String },
ndcCode: { type: String },
requirements: [
{
name: { type: String },
Expand All @@ -63,6 +81,8 @@
});

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

export const medicationCollection = model<Medication>(
'medicationCollection',
Expand Down Expand Up @@ -91,13 +111,29 @@

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 },
drugCode: { type: String },

Check failure on line 121 in src/fhir/models.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Delete `·`

Check failure on line 121 in src/fhir/models.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Delete `·`
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: [
Expand All @@ -111,4 +147,12 @@
]
});

remsCaseCollectionSchema.index(

Check failure on line 150 in src/fhir/models.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Insert `{`

Check failure on line 150 in src/fhir/models.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Insert `{`
{ patientFirstName: 1, patientLastName: 1, patientDOB: 1, drugNdcCode: 1 }

Check failure on line 151 in src/fhir/models.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Replace `·{·patientFirstName:·1,·patientLastName:·1,·patientDOB:·1,·drugNdcCode:·1·}⏎` with `·patientFirstName:·1,⏎··patientLastName:·1,⏎··patientDOB:·1,⏎··drugNdcCode:·1⏎}`

Check failure on line 151 in src/fhir/models.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Replace `·{·patientFirstName:·1,·patientLastName:·1,·patientDOB:·1,·drugNdcCode:·1·}⏎` with `·patientFirstName:·1,⏎··patientLastName:·1,⏎··patientDOB:·1,⏎··drugNdcCode:·1⏎}`
);

remsCaseCollectionSchema.index(

Check failure on line 154 in src/fhir/models.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Insert `{`

Check failure on line 154 in src/fhir/models.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Insert `{`
{ patientFirstName: 1, patientLastName: 1, patientDOB: 1, drugCode: 1 }

Check failure on line 155 in src/fhir/models.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Replace `{·patientFirstName:·1,·patientLastName:·1,·patientDOB:·1,·drugCode:·1·}⏎` with `patientFirstName:·1,⏎··patientLastName:·1,⏎··patientDOB:·1,⏎··drugCode:·1⏎}`

Check failure on line 155 in src/fhir/models.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Replace `{·patientFirstName:·1,·patientLastName:·1,·patientDOB:·1,·drugCode:·1·}⏎` with `patientFirstName:·1,⏎··patientLastName:·1,⏎··patientDOB:·1,⏎··drugCode:·1⏎}`
);

export const remsCaseCollection = model<RemsCase>('RemsCaseCollection', remsCaseCollectionSchema);

Check failure on line 158 in src/fhir/models.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Insert `⏎`

Check failure on line 158 in src/fhir/models.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Insert `⏎`
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
55 changes: 52 additions & 3 deletions src/hooks/hookResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import axios from 'axios';
import { ServicePrefetch } from '../rems-cds-hooks/resources/CdsService';
import { hydrate } from '../rems-cds-hooks/prefetch/PrefetchHydrator';
import { createNewRemsCaseFromCDSHook } from '../lib/etasu';
import { createNewRemsCaseFromCDSHook, handleStakeholderChangesAndRecordEvent } from '../lib/etasu';

type HandleCallback = (
res: any,
Expand Down Expand Up @@ -406,11 +406,50 @@
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) {
if (requiresCase && fhirServer) {
try {
const patientReference = `Patient/${patient.id}`;
const medicationRequestReference = `${request.resourceType}/${request.id}`;
Expand Down Expand Up @@ -527,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 @@ -786,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 @@ -828,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 @@ -871,7 +920,7 @@
hookPrefetch: HookPrefetch | undefined,
_contextRequest: 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 Expand Up @@ -997,3 +1046,3 @@
]
};
return taskResource;
Expand Down
37 changes: 22 additions & 15 deletions src/lib/communication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,14 @@

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


export async function sendCommunicationToEHR(
remsCase: any,
medication: any,
outstandingRequirements: any[]
): Promise<void> {
try {
logger.info(`Creating Communication for case ${remsCase.case_number}`);

// Create patient object from REMS case
const patient: Patient = {
resourceType: 'Patient',
Expand Down Expand Up @@ -60,9 +59,10 @@
// Create Tasks for each outstanding requirement
const tasks: Task[] = [];
for (const outstandingReq of outstandingRequirements) {
const requirement = outstandingReq.requirement ||
const requirement =
outstandingReq.requirement ||
medication.requirements.find((r: Requirement) => r.name === outstandingReq.name);

if (requirement && requirement.appContext) {
const questionnaireUrl = requirement.appContext;
const task = createQuestionnaireCompletionTask(
Expand All @@ -80,7 +80,7 @@
const communication: Communication = {
resourceType: 'Communication',
id: `comm-${uid()}`,
status: 'completed',
status: 'completed',
category: [
{
coding: [
Expand All @@ -92,7 +92,7 @@
]
}
],
priority: 'urgent',
priority: 'urgent',
subject: {
reference: `Patient/${patient.id}`,
display: `${remsCase.patientFirstName} ${remsCase.patientLastName}`
Expand All @@ -107,7 +107,7 @@
],
text: 'Outstanding REMS Requirements for Medication Dispensing'
},
sent: new Date().toISOString(),
sent: new Date().toISOString(),
recipient: [
{
reference: medicationRequest.requester?.reference || ''
Expand All @@ -119,8 +119,9 @@
},
payload: [
{
contentString: `Medication dispensing authorization DENIED for ${remsCase.drugName}.\n\n` +
`The following REMS requirements must be completed:\n\n` +
contentString:
`Medication dispensing authorization DENIED for ${remsCase.drugName}.\n\n` +
'The following REMS requirements must be completed:\n\n' +
outstandingRequirements
.map((req, idx) => `${idx + 1}. ${req.name} (${req.stakeholder})`)
.join('\n') +
Expand All @@ -142,17 +143,24 @@
};

// Determine EHR endpoint: use originatingFhirServer if available, otherwise default
const ehrEndpoint = remsCase.originatingFhirServer ||
config.fhirServerConfig?.auth?.resourceServer;
let ehrEndpoint =
remsCase.originatingFhirServer || config.fhirServerConfig?.auth?.resourceServer;

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

if (config.fhirServerConfig.auth.dockered_ehr_container_name) {
const originalEhrEndpoint = ehrEndpoint;
ehrEndpoint = originalEhrEndpoint.replace(/localhost/g, config.fhirServerConfig.auth.dockered_ehr_container_name)

Check failure on line 156 in src/lib/communication.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Insert `⏎········`

Check failure on line 156 in src/lib/communication.ts

View workflow job for this annotation

GitHub Actions / Check tsc, lint, and prettier

Insert `⏎········`
.replace(/127\.0\.0\.1/g, config.fhirServerConfig.auth.dockered_ehr_container_name);
logger.info(`Running locally in Docker, converting EHR url from ${originalEhrEndpoint} to ${ehrEndpoint}`);
}

// Send Communication to EHR
logger.info(`Sending Communication to EHR: ${ehrEndpoint}`);

const response = await axios.post(`${ehrEndpoint}/Communication`, communication, {
headers: {
'Content-Type': 'application/fhir+json'
Expand All @@ -164,9 +172,8 @@
} else {
logger.warn(`Unexpected response status from EHR: ${response.status}`);
}

} catch (error: any) {
logger.error(`Failed to send Communication to EHR: ${error.message}`);
throw error;
throw error;
}
}
}
Loading
Loading