Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Box, IconButton, Typography, useTheme } from '@mui/material';
import { enqueueSnackbar } from 'notistack';
import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ExtendedMedicationDataForResponse } from 'utils';
import { ExtendedMedicationDataForResponse, getApiError } from 'utils';
import { CustomDialog } from '../../../../../../components/dialogs/CustomDialog';
import { useMedicationManagement } from '../../../hooks/useMedicationManagement';
import { getEditOrderUrl } from '../../../routing/helpers';
Expand Down Expand Up @@ -53,8 +53,12 @@ export const MedicationActions: React.FC<MedicationActionsProps> = ({ medication
try {
await deleteMedication(medication.id);
setIsDeleteDialogOpen(false);
} catch {
setError('An error occurred while deleting the medication. Please try again.');
} catch (error) {
const errorMessage = getApiError({
error,
defaultError: 'An error occurred while deleting the medication. Please try again.',
});
setError(errorMessage);
}
setIsDeleting(false);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useAppointmentData } from 'src/features/visits/shared/stores/appointmen
import { useApiClients } from 'src/hooks/useAppClients';
import {
ExtendedMedicationDataForResponse,
getApiError,
getMedicationName,
MEDICAL_HISTORY_CONFIG,
MedicationData,
Expand Down Expand Up @@ -128,8 +129,9 @@ export const EditableMedicationCard: React.FC<{
if (appointmentId) {
navigate(getInHouseMedicationMARUrl(appointmentId));
}
} catch {
enqueueSnackbar('Failed to delete medication. Please try again.', { variant: 'error' });
} catch (error) {
const errorMessage = getApiError({ error, defaultError: 'Failed to delete medication. Please try again.' });
enqueueSnackbar(errorMessage, { variant: 'error' });
} finally {
setIsDeleting(false);
}
Expand Down Expand Up @@ -281,6 +283,8 @@ export const EditableMedicationCard: React.FC<{
void refetchHistory();
} catch (error) {
console.error(error);
const errorMessage = getApiError({ error, defaultError: 'Failed to save medication. Please try again.' });
enqueueSnackbar(errorMessage, { variant: 'error' });
Comment thread
ValeriyDyachenko marked this conversation as resolved.
} finally {
setIsOrderUpdating(false);
setShowErrors(false);
Expand Down
14 changes: 14 additions & 0 deletions packages/utils/lib/helpers/oystehrApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ export const apiErrorToThrow = (error: any): APIError => {
}
};

/**
* Extracts the error message from an API error with fallback to default message.
*/
export const getApiError = ({ error, defaultError }: { error: any; defaultError: string }): string => {
// Errors from Oystehr SDK can have nested structure: { output: { message, code } }
if (error?.output?.message) {
return error.output.message;
}
if (error?.message) {
return error.message;
}
return defaultError;
};

export function NotFoundAppointmentErrorHandler(error: any): void {
if (error.message === 'Appointment is not found') {
throw error;
Expand Down
17 changes: 10 additions & 7 deletions packages/zambdas/src/ehr/create-update-medication-order/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import Oystehr, { TerminologySearchCptResponse, TerminologySearchHcpcsResponse }
import { Medication, MedicationAdministration } from 'fhir/r4b';
import {
CPTCodeOption,
FHIR_RESOURCE_NOT_FOUND_CUSTOM,
getAllCptCodesFromInHouseMedication,
getAllHcpcsCodesFromInHouseMedication,
getDosageUnitsAndRouteOfMedication,
getLocationCodeFromMedicationAdministration,
getResourcesFromBatchInlineRequests,
INVALID_INPUT_ERROR,
INVENTORY_MEDICATION_TYPE_CODE,
MedicationData,
MedicationOrderStatuses,
Expand Down Expand Up @@ -48,14 +50,14 @@ export function createMedicationCopy(
export async function practitionerIdFromZambdaInput(userToken: string, secrets: Secrets | null): Promise<string> {
const oystehr = createOystehrClient(userToken, secrets);
const myPractitionerId = removePrefix('Practitioner/', (await oystehr.user.me()).profile);
if (!myPractitionerId) throw new Error('No practitioner id was found for token provided');
if (!myPractitionerId) throw FHIR_RESOURCE_NOT_FOUND_CUSTOM('No practitioner id was found for token provided');
return myPractitionerId;
}

export async function getMedicationByName(oystehr: Oystehr, medicationName: string): Promise<Medication> {
const medications = await getResourcesFromBatchInlineRequests(oystehr, [`Medication?identifier=${medicationName}`]);
const medication = medications.find((res) => res.resourceType === 'Medication') as Medication;
if (!medication) throw new Error(`No medication was found with this name: ${medicationName}`);
if (!medication) throw FHIR_RESOURCE_NOT_FOUND_CUSTOM(`No medication was found with this name: ${medicationName}`);
return medication;
}

Expand All @@ -64,7 +66,7 @@ export async function getMedicationById(oystehr: Oystehr, medicationId: string):
resourceType: 'Medication',
id: medicationId,
});
if (!medication) throw new Error(`No medication was found for this id: ${medicationId}`);
if (!medication) throw FHIR_RESOURCE_NOT_FOUND_CUSTOM(`No medication was found for this id: ${medicationId}`);
return medication;
}

Expand All @@ -80,7 +82,7 @@ export function validateProviderAccess(
// When we receive new data and new status, it means that we are on 'Medication Details' screen so
// we don't need provider validation because everybody can do it
if (orderData && !newStatus && getPerformerId(orderPkg.medicationAdministration) !== practitionerId)
throw new Error(`You can't edit this order, because it was created by another provider`);
throw INVALID_INPUT_ERROR(`You can't edit this order, because it was created by another provider`);
Comment thread
ValeriyDyachenko marked this conversation as resolved.
}

export function updateMedicationAdministrationData(data: {
Expand All @@ -95,15 +97,16 @@ export function updateMedicationAdministrationData(data: {
? orderData.route
: getDosageUnitsAndRouteOfMedication(orderResources.medicationAdministration).route;
const routeCoding = searchRouteByCode(routeCode!);
if (orderData.route && !routeCoding) throw new Error(`No route found with code provided: ${orderData.route}`);
if (orderData.route && !routeCoding)
throw INVALID_INPUT_ERROR(`No route found with code provided: ${orderData.route}`);
const locationCode = orderData.location
? orderData.location
: getLocationCodeFromMedicationAdministration(orderResources.medicationAdministration);
const locationCoding = locationCode ? searchMedicationLocation(locationCode) : undefined;
if (orderData.location && !locationCoding)
throw new Error(`No location found with code provided: ${orderData.location}`);
throw INVALID_INPUT_ERROR(`No location found with code provided: ${orderData.location}`);

if (!routeCoding) throw new Error(`No medication appliance route was found for code: ${routeCode}`);
if (!routeCoding) throw INVALID_INPUT_ERROR(`No medication appliance route was found for code: ${routeCode}`);
const newMA = createMedicationAdministrationResource({
orderData,
status: orderResources.medicationAdministration.status,
Expand Down
25 changes: 15 additions & 10 deletions packages/zambdas/src/ehr/create-update-medication-order/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ import { DateTime } from 'luxon';
import {
chooseJson,
createCancellationTagOperations,
FHIR_RESOURCE_NOT_FOUND_CUSTOM,
GetChartDataRequest,
GetChartDataResponse,
getMedicationFromMA,
getMedicationName,
getMedicationTypeCode,
getPatchBinary,
IN_HOUSE_CONTAINED_MEDICATION_ID,
INVALID_INPUT_ERROR,
INVENTORY_MEDICATION_TYPE_CODE,
mapFhirToOrderStatus,
mapOrderStatusToFhir,
Expand Down Expand Up @@ -146,7 +148,7 @@ async function updateOrder(

const currentStatus = mapFhirToOrderStatus(orderResources.medicationAdministration);
if (currentStatus !== 'pending' && newStatus)
throw new Error(`Can't change status if current is not 'pending'. Current status is: ${currentStatus}`);
throw INVALID_INPUT_ERROR(`Can't change status if current is not 'pending'. Current status is: ${currentStatus}`);
console.log(`Current order status is: ${currentStatus}`);

if (newStatus) validateProviderAccess(orderData, newStatus, orderResources, practitionerIdCalledZambda);
Expand Down Expand Up @@ -189,15 +191,16 @@ async function updateOrder(
}

if (newStatus === 'administered' || newStatus === 'administered-partly') {
if (!newMedicationCopy) throw new Error(`Can't create MedicationStatement for order, no Medication copy.`);
if (!newMedicationCopy)
throw INVALID_INPUT_ERROR(`Can't create MedicationStatement for order, no Medication copy.`);

const erxDataFromMedication = newMedicationCopy.code?.coding?.find(
(code) => code.system === MEDICATION_DISPENSABLE_DRUG_ID
);

if (!erxDataFromMedication)
throw new Error(
`Can't create MedicationStatement for order, Medication resource don't have coding with ERX data in it`
throw INVALID_INPUT_ERROR(
`Can't create MedicationStatement for order, Medication resource doesn't have coding with ERX data in it`
);

const medicationCodeableConcept: CodeableConcept = {
Expand Down Expand Up @@ -282,21 +285,21 @@ async function createOrder(
): Promise<string | undefined> {
console.log('createOrder');

if (!orderData.medicationId) throw new Error('No "medicationId" provided');
if (!orderData.medicationId) throw INVALID_INPUT_ERROR('No "medicationId" provided');
const inventoryMedication = await getMedicationById(oystehr, orderData.medicationId);
if (inventoryMedication && getMedicationTypeCode(inventoryMedication) !== INVENTORY_MEDICATION_TYPE_CODE) {
throw new Error(
throw INVALID_INPUT_ERROR(
`Medication with id ${orderData.medicationId} is not medication inventory item, can't copy that resource`
);
}
const medicationCopy = createMedicationCopy(inventoryMedication, orderData);
console.log(`Created medication copy: ${getMedicationName(medicationCopy)}`);

const routeCoding = searchRouteByCode(orderData.route);
if (!routeCoding) throw new Error(`No medication appliance route was found for code: ${orderData.route}`);
if (!routeCoding) throw INVALID_INPUT_ERROR(`No medication appliance route was found for code: ${orderData.route}`);
const locationCoding = orderData.location ? searchMedicationLocation(orderData.location) : undefined;
if (orderData.location && !locationCoding)
throw new Error(`No location found with code provided: ${orderData.location}`);
throw INVALID_INPUT_ERROR(`No location found with code provided: ${orderData.location}`);

const medicationRequestToCreate = createMedicationRequest(orderData, interactions, medicationCopy);
const medicationRequestFullUrl = 'urn:uuid:' + randomUUID();
Expand Down Expand Up @@ -412,9 +415,11 @@ async function getOrderResources(oystehr: Oystehr, orderId: string): Promise<Ord
const medicationAdministration = resources.find(
(res) => res.resourceType === 'MedicationAdministration'
) as MedicationAdministration;
if (!medicationAdministration) throw new Error(`No medicationAdministration was found with id ${orderId}`);
if (!medicationAdministration)
throw FHIR_RESOURCE_NOT_FOUND_CUSTOM(`No medicationAdministration was found with id ${orderId}`);
const patient = resources.find((res) => res.resourceType === 'Patient') as Patient;
if (!patient) throw new Error(`No patient was found for medicationAdministration with id ${orderId}`);
if (!patient)
throw FHIR_RESOURCE_NOT_FOUND_CUSTOM(`No patient was found for medicationAdministration with id ${orderId}`);
const medicationStatement = resources.find(
(res) => res.resourceType === 'MedicationStatement'
) as MedicationStatement;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MedicationInteractions, UpdateMedicationOrderInput } from 'utils';
import { INVALID_INPUT_ERROR, MedicationInteractions, MISSING_REQUEST_BODY, UpdateMedicationOrderInput } from 'utils';
import { ZambdaInput } from '../../shared';

export function validateRequestParameters(
Expand All @@ -7,24 +7,34 @@ export function validateRequestParameters(
console.group('validateRequestParameters');

if (!input.body) {
throw new Error('No request body provided');
throw MISSING_REQUEST_BODY;
}

const { orderId, newStatus, orderData, interactions } = JSON.parse(input.body);
let parsedBody;
try {
parsedBody = JSON.parse(input.body);
} catch {
throw INVALID_INPUT_ERROR('Request body must be valid JSON');
}
const { orderId, newStatus, orderData, interactions } = parsedBody;

if (newStatus) {
if (newStatus === 'administered' && !orderData) {
throw new Error(`With status 'administered' order data should be provided.`);
throw INVALID_INPUT_ERROR(`With status 'administered' order data should be provided.`);
}
if (newStatus === 'pending') {
throw new Error('Cannot change status back to pending.');
throw INVALID_INPUT_ERROR('Cannot change status back to pending.');
}
if (orderId && newStatus !== 'administered' && newStatus !== 'cancelled' && !orderData?.reason) {
throw new Error(`Reason should be provided if you changing status to anything except 'administered'`);
throw INVALID_INPUT_ERROR(
`Reason should be provided if you are changing status to anything except 'administered'`
);
}
if (newStatus === 'administered') {
if (!orderData.effectiveDateTime)
throw new Error('On status change to "administered" effectiveDateTime field should be present in zambda input');
throw INVALID_INPUT_ERROR(
'On status change to "administered" effectiveDateTime field should be present in zambda input'
);
}

const missedFields: string[] = [];
Expand All @@ -36,7 +46,7 @@ export function validateRequestParameters(
if (!orderData.dose) missedFields.push('dose');
if (!orderData.route) missedFields.push('route');
}
if (missedFields.length > 0) throw new Error(`Missing fields in orderData: ${missedFields.join(', ')}`);
if (missedFields.length > 0) throw INVALID_INPUT_ERROR(`Missing fields in orderData: ${missedFields.join(', ')}`);
}

validateInteractions(interactions);
Expand Down Expand Up @@ -66,6 +76,6 @@ function validateInteractions(interactions?: MedicationInteractions): void {
}
});
if (missingOverrideReason.length > 0) {
throw new Error(`overrideReason is missing for ${missingOverrideReason.join(', ')}`);
throw INVALID_INPUT_ERROR(`overrideReason is missing for ${missingOverrideReason.join(', ')}`);
}
}
Loading