diff --git a/apps/ehr/src/features/visits/in-person/components/medication-administration/mar/MedicationActions.tsx b/apps/ehr/src/features/visits/in-person/components/medication-administration/mar/MedicationActions.tsx index d64d9bf034..676740768e 100644 --- a/apps/ehr/src/features/visits/in-person/components/medication-administration/mar/MedicationActions.tsx +++ b/apps/ehr/src/features/visits/in-person/components/medication-administration/mar/MedicationActions.tsx @@ -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'; @@ -53,8 +53,12 @@ export const MedicationActions: React.FC = ({ 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); }; diff --git a/apps/ehr/src/features/visits/in-person/components/medication-administration/medication-editable-card/EditableMedicationCard.tsx b/apps/ehr/src/features/visits/in-person/components/medication-administration/medication-editable-card/EditableMedicationCard.tsx index 1ce6f80398..7e7090b814 100644 --- a/apps/ehr/src/features/visits/in-person/components/medication-administration/medication-editable-card/EditableMedicationCard.tsx +++ b/apps/ehr/src/features/visits/in-person/components/medication-administration/medication-editable-card/EditableMedicationCard.tsx @@ -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, @@ -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); } @@ -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' }); } finally { setIsOrderUpdating(false); setShowErrors(false); diff --git a/packages/utils/lib/helpers/oystehrApi.ts b/packages/utils/lib/helpers/oystehrApi.ts index b47839741c..3f2d47af28 100644 --- a/packages/utils/lib/helpers/oystehrApi.ts +++ b/packages/utils/lib/helpers/oystehrApi.ts @@ -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; diff --git a/packages/zambdas/src/ehr/create-update-medication-order/helpers.ts b/packages/zambdas/src/ehr/create-update-medication-order/helpers.ts index b2bae353fe..bbc59237a1 100644 --- a/packages/zambdas/src/ehr/create-update-medication-order/helpers.ts +++ b/packages/zambdas/src/ehr/create-update-medication-order/helpers.ts @@ -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, @@ -48,14 +50,14 @@ export function createMedicationCopy( export async function practitionerIdFromZambdaInput(userToken: string, secrets: Secrets | null): Promise { 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 { 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; } @@ -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; } @@ -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`); } export function updateMedicationAdministrationData(data: { @@ -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, diff --git a/packages/zambdas/src/ehr/create-update-medication-order/index.ts b/packages/zambdas/src/ehr/create-update-medication-order/index.ts index bea8c1bcfe..6eeb4cea0e 100644 --- a/packages/zambdas/src/ehr/create-update-medication-order/index.ts +++ b/packages/zambdas/src/ehr/create-update-medication-order/index.ts @@ -15,6 +15,7 @@ import { DateTime } from 'luxon'; import { chooseJson, createCancellationTagOperations, + FHIR_RESOURCE_NOT_FOUND_CUSTOM, GetChartDataRequest, GetChartDataResponse, getMedicationFromMA, @@ -22,6 +23,7 @@ import { getMedicationTypeCode, getPatchBinary, IN_HOUSE_CONTAINED_MEDICATION_ID, + INVALID_INPUT_ERROR, INVENTORY_MEDICATION_TYPE_CODE, mapFhirToOrderStatus, mapOrderStatusToFhir, @@ -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); @@ -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 = { @@ -282,10 +285,10 @@ async function createOrder( ): Promise { 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` ); } @@ -293,10 +296,10 @@ async function createOrder( 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(); @@ -412,9 +415,11 @@ async function getOrderResources(oystehr: Oystehr, orderId: string): Promise 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; diff --git a/packages/zambdas/src/ehr/create-update-medication-order/validateRequestParameters.ts b/packages/zambdas/src/ehr/create-update-medication-order/validateRequestParameters.ts index 0e7c11a9c5..d9f99c91d2 100644 --- a/packages/zambdas/src/ehr/create-update-medication-order/validateRequestParameters.ts +++ b/packages/zambdas/src/ehr/create-update-medication-order/validateRequestParameters.ts @@ -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( @@ -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[] = []; @@ -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); @@ -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(', ')}`); } }