diff --git a/apps/ehr/src/api/api.ts b/apps/ehr/src/api/api.ts index ce8a82c5b4..1b7d97e959 100644 --- a/apps/ehr/src/api/api.ts +++ b/apps/ehr/src/api/api.ts @@ -53,8 +53,6 @@ import { GetAppointmentsZambdaOutput, GetConversationInput, GetConversationZambdaOutput, - GetCreateInHouseLabOrderResourcesParameters, - GetCreateInHouseLabOrderResourcesResponse, GetEmployeesResponse, GetInHouseOrdersParameters, GetLabelPdfParameters, @@ -168,7 +166,6 @@ const CREATE_SCHEDULE_ZAMBDA_ID = 'create-schedule'; const CREATE_SLOT_ZAMBDA_ID = 'create-slot'; const CREATE_IN_HOUSE_LAB_ORDER_ZAMBDA_ID = 'create-in-house-lab-order'; const GET_IN_HOUSE_ORDERS_ZAMBDA_ID = 'get-in-house-orders'; -const GET_CREATE_IN_HOUSE_LAB_ORDER_RESOURCES = 'get-create-in-house-lab-order-resources'; const COLLECT_IN_HOUSE_LAB_SPECIMEN = 'collect-in-house-lab-specimen'; const HANDLE_IN_HOUSE_LAB_RESULTS = 'handle-in-house-lab-results'; const DELETE_IN_HOUSE_LAB_ORDER = 'delete-in-house-lab-order'; @@ -1093,25 +1090,6 @@ export const getInHouseOrders = async => { - try { - if (GET_CREATE_IN_HOUSE_LAB_ORDER_RESOURCES == null) { - throw new Error('get create in house lab order resources zambda environment variable could not be loaded'); - } - const response = await oystehr.zambda.execute({ - id: GET_CREATE_IN_HOUSE_LAB_ORDER_RESOURCES, - ...parameters, - }); - return chooseJson(response); - } catch (error: unknown) { - console.log(error); - throw error; - } -}; - export const collectInHouseLabSpecimen = async ( oystehr: Oystehr, parameters: CollectInHouseLabSpecimenParameters diff --git a/apps/ehr/src/features/external-labs/components/LabSets.tsx b/apps/ehr/src/features/external-labs/components/LabSets.tsx index b7778bc630..6d6be1d086 100644 --- a/apps/ehr/src/features/external-labs/components/LabSets.tsx +++ b/apps/ehr/src/features/external-labs/components/LabSets.tsx @@ -4,12 +4,11 @@ import { LoadingButton } from '@mui/lab'; import { Box, Dialog, DialogContent, DialogTitle, Divider, Grid, IconButton, Typography } from '@mui/material'; import Oystehr from '@oystehr/sdk'; import { FC, useState } from 'react'; -import { useOystehrAPIClient } from 'src/features/visits/shared/hooks/useOystehrAPIClient'; -import { LabListsDTO, OrderableItemSearchResult } from 'utils'; +import { LabListsDTO } from 'utils'; type LabSetsProps = { labSets: LabListsDTO[]; - setSelectedLabs: React.Dispatch>; + setSelectedLabs: (labSet: LabListsDTO) => Promise; }; export const LabSets: FC = ({ labSets, setSelectedLabs }) => { @@ -17,26 +16,11 @@ export const LabSets: FC = ({ labSets, setSelectedLabs }) => { const [loadingId, setLoadingId] = useState(null); const [error, setError] = useState(undefined); - const apiClient = useOystehrAPIClient(); - const handleSelectLabSet = async (labSet: LabListsDTO): Promise => { setLoadingId(labSet.listId); // start loading for this button only try { - const res = await apiClient?.getCreateExternalLabResources({ - selectedLabSet: labSet, - }); - const labs = res?.labs; - - if (labs) { - setSelectedLabs((currentLabs) => { - const existingCodes = new Set(currentLabs.map((lab) => `${lab.item.itemCode}${lab.lab.labGuid}`)); - - const newLabs = labs.filter((lab) => !existingCodes.has(`${lab.item.itemCode}${lab.lab.labGuid}`)); - - return [...currentLabs, ...newLabs]; - }); - setOpen(false); - } + await setSelectedLabs(labSet); + setOpen(false); } catch (e) { const sdkError = e as Oystehr.OystehrSdkError; console.log('error selecting this lab set', sdkError.code, sdkError.message); @@ -68,7 +52,7 @@ export const LabSets: FC = ({ labSets, setSelectedLabs }) => { p: '24px 24px 16px 24px', }} > - + Lab Sets = ({ labSets, setSelectedLabs }) => { {labSets.map((set, idx) => ( - <> - + + = ({ labSets, setSelectedLabs }) => { {set.listName}: - {set.labs.map((lab) => ( - {lab.display} + {set.labs.map((lab, idx) => ( + {lab.display} ))} @@ -115,7 +99,7 @@ export const LabSets: FC = ({ labSets, setSelectedLabs }) => { {idx < labSets.length - 1 && } - + ))} {Array.isArray(error) && error.length > 0 && diff --git a/apps/ehr/src/features/external-labs/components/LabsAutocomplete.tsx b/apps/ehr/src/features/external-labs/components/LabsAutocomplete.tsx index 69a6909f19..d80d76f0ff 100644 --- a/apps/ehr/src/features/external-labs/components/LabsAutocomplete.tsx +++ b/apps/ehr/src/features/external-labs/components/LabsAutocomplete.tsx @@ -3,9 +3,10 @@ import { enqueueSnackbar } from 'notistack'; import { FC, useState } from 'react'; import { ActionsList } from 'src/components/ActionsList'; import { DeleteIconButton } from 'src/components/DeleteIconButton'; +import { useOystehrAPIClient } from 'src/features/visits/shared/hooks/useOystehrAPIClient'; import { useGetCreateExternalLabResources } from 'src/features/visits/shared/stores/appointment/appointment.queries'; import { useDebounce } from 'src/shared/hooks/useDebounce'; -import { LabListsDTO, nameLabTest, OrderableItemSearchResult } from 'utils'; +import { LabListsDTO, LabType, nameLabTest, OrderableItemSearchResult } from 'utils'; import { LabSets } from './LabSets'; type LabsAutocompleteProps = { @@ -18,6 +19,7 @@ type LabsAutocompleteProps = { export const LabsAutocomplete: FC = (props) => { const { selectedLabs, setSelectedLabs, labOrgIdsString, labSets } = props; const [debouncedLabSearchTerm, setDebouncedLabSearchTerm] = useState(undefined); + const apiClient = useOystehrAPIClient(); const { isFetching, @@ -40,6 +42,25 @@ export const LabsAutocomplete: FC = (props) => { if (resourceFetchError) console.log('resourceFetchError', resourceFetchError); + const handleSetSelectedLabsViaLabSets = async (labSet: LabListsDTO): Promise => { + if (labSet.listType === LabType.external) { + const res = await apiClient?.getCreateExternalLabResources({ + selectedLabSet: labSet, + }); + const labs = res?.labs; + + if (labs) { + setSelectedLabs((currentLabs) => { + const existingCodes = new Set(currentLabs.map((lab) => `${lab.item.itemCode}${lab.lab.labGuid}`)); + + const newLabs = labs.filter((lab) => !existingCodes.has(`${lab.item.itemCode}${lab.lab.labGuid}`)); + + return [...currentLabs, ...newLabs]; + }); + } + } + }; + return ( <> = (props) => { )} /> - {labSets && } + {labSets && } {selectedLabs.length > 0 && ( diff --git a/apps/ehr/src/features/in-house-labs/components/orders/InHouseLabsTable.tsx b/apps/ehr/src/features/in-house-labs/components/orders/InHouseLabsTable.tsx index 9ed79e93c0..e2c123005f 100644 --- a/apps/ehr/src/features/in-house-labs/components/orders/InHouseLabsTable.tsx +++ b/apps/ehr/src/features/in-house-labs/components/orders/InHouseLabsTable.tsx @@ -22,13 +22,12 @@ import { DatePicker } from '@mui/x-date-pickers'; import { LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterLuxon } from '@mui/x-date-pickers/AdapterLuxon'; import { DateTime } from 'luxon'; -import { ReactElement, useEffect, useState } from 'react'; +import { ReactElement, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { getCreateInHouseLabOrderResources } from 'src/api/api'; import { DropdownPlaceholder } from 'src/features/common/DropdownPlaceholder'; import { getInHouseLabOrderDetailsUrl } from 'src/features/visits/in-person/routing/helpers'; -import { useApiClients } from 'src/hooks/useAppClients'; -import { InHouseOrderListPageItemDTO, InHouseOrdersSearchBy, TestItem } from 'utils'; +import { useGetCreateInHouseLabResources } from 'src/features/visits/shared/stores/appointment/appointment.queries'; +import { InHouseOrderListPageItemDTO, InHouseOrdersSearchBy } from 'utils'; import { LabOrdersSearchBy } from 'utils/lib/types/data/labs'; import { InHouseLabsTableRow } from './InHouseLabsTableRow'; import { useInHouseLabOrders } from './useInHouseLabOrders'; @@ -78,32 +77,9 @@ export const InHouseLabsTable = ({ const [testTypeQuery, setTestTypeQuery] = useState(''); const [tempDateFilter, setTempDateFilter] = useState(visitDateFilter); - const [availableTests, setAvailableTests] = useState([]); - const [loadingTests, setLoadingTests] = useState(false); + const { isFetching: loadingTests, data: createInHouseLabResources } = useGetCreateInHouseLabResources({}); - const { oystehrZambda } = useApiClients(); - - // set data for filters - useEffect(() => { - if (!oystehrZambda || !showFilters) { - return; - } - - const fetchTests = async (): Promise => { - try { - setLoadingTests(true); - const response = await getCreateInHouseLabOrderResources(oystehrZambda, {}); - const testItems = response.labs || []; - setAvailableTests(testItems.sort((a, b) => a.name.localeCompare(b.name))); - } catch (error) { - console.error('Error fetching tests:', error); - } finally { - setLoadingTests(false); - } - }; - - void fetchTests(); - }, [oystehrZambda, showFilters]); + const availableTests = Object.values(createInHouseLabResources?.labs || {}); const submitFilterByDate = (date?: DateTime | null): void => { const dateToSet = date || tempDateFilter; diff --git a/apps/ehr/src/features/in-house-labs/pages/InHouseLabOrderCreatePage.tsx b/apps/ehr/src/features/in-house-labs/pages/InHouseLabOrderCreatePage.tsx index 2249a11eff..6a9694a47b 100644 --- a/apps/ehr/src/features/in-house-labs/pages/InHouseLabOrderCreatePage.tsx +++ b/apps/ehr/src/features/in-house-labs/pages/InHouseLabOrderCreatePage.tsx @@ -6,13 +6,18 @@ import { Chip, CircularProgress, FormControl, - FormControlLabel, Grid, InputLabel, MenuItem, Paper, Select, Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, TextField, Typography, useTheme, @@ -25,14 +30,19 @@ import { ActionsList } from 'src/components/ActionsList'; import { DeleteIconButton } from 'src/components/DeleteIconButton'; import { dataTestIds } from 'src/constants/data-test-ids'; import DetailPageContainer from 'src/features/common/DetailPageContainer'; +import { LabSets } from 'src/features/external-labs/components/LabSets'; import { useGetAppointmentAccessibility } from 'src/features/visits/shared/hooks/useGetAppointmentAccessibility'; import { useMainEncounterChartData } from 'src/features/visits/shared/hooks/useMainEncounterChartData'; -import { useICD10SearchNew } from 'src/features/visits/shared/stores/appointment/appointment.queries'; +import { useOystehrAPIClient } from 'src/features/visits/shared/hooks/useOystehrAPIClient'; +import { + useGetCreateInHouseLabResources, + useICD10SearchNew, +} from 'src/features/visits/shared/stores/appointment/appointment.queries'; import { useAppointmentData, useChartData } from 'src/features/visits/shared/stores/appointment/appointment.store'; import { useDebounce } from 'src/shared/hooks/useDebounce'; -import { getAttendingPractitionerId, isApiError, TestItem } from 'utils'; +import { getAttendingPractitionerId, isApiError, LabListsDTO, LabType, TestItem } from 'utils'; import { DiagnosisDTO } from 'utils/lib/types/api/chart-data'; -import { createInHouseLabOrder, getCreateInHouseLabOrderResources, getOrCreateVisitLabel } from '../../../api/api'; +import { createInHouseLabOrder, getOrCreateVisitLabel } from '../../../api/api'; import { useApiClients } from '../../../hooks/useAppClients'; import { InHouseLabsNotesCard } from '../components/details/InHouseLabsNotesCard'; import { InHouseLabsBreadcrumbs } from '../components/InHouseLabsBreadcrumbs'; @@ -43,13 +53,11 @@ export const InHouseLabOrderCreatePage: React.FC = () => { const navigate = useNavigate(); const location = useLocation(); const [loading, setLoading] = useState(false); - const [availableTests, setAvailableTests] = useState([]); - const [selectedTest, setSelectedTest] = useState(null); - const [relatedCptCode, setRelatedCptCode] = useState(''); + const [selectedTests, setSelectedTests] = useState([]); const [notes, setNotes] = useState(''); - const [providerName, setProviderName] = useState(''); const [error, setError] = useState(undefined); - const [repeatTest, setRepeatTest] = useState(false); + + const apiClient = useOystehrAPIClient(); const prefillData = location.state as { testItemName?: string; @@ -103,42 +111,24 @@ export const InHouseLabOrderCreatePage: React.FC = () => { const attendingPractitionerId = getAttendingPractitionerId(encounter); - useEffect(() => { - if (!oystehrZambda) { - return; - } - - const fetchLabs = async (): Promise => { - try { - setLoading(true); - const response = await getCreateInHouseLabOrderResources(oystehrZambda, { - encounterId: encounter.id, - }); - const testItems = Object.values(response.labs || {}); - setAvailableTests(testItems.sort((a, b) => a.name.localeCompare(b.name))); - setProviderName(response.providerName); - } catch (error) { - console.error('Error fetching labs:', error); - } finally { - setLoading(false); - } - }; + const { data: createInHouseLabResources } = useGetCreateInHouseLabResources({ + encounterId: encounter?.id, + }); - if (encounter.id) { - void fetchLabs(); - } - }, [oystehrZambda, encounter?.id]); + const availableTests = Object.values(createInHouseLabResources?.labs || {}); + const providerName = createInHouseLabResources?.providerName ?? ''; + const labSets = createInHouseLabResources?.labSets; useEffect(() => { if (prefillData) { const { testItemName, diagnoses } = prefillData; if (testItemName) { const found = availableTests.find((test) => test.name === testItemName); - console.log('found', found); if (found) { - setSelectedTest(found); - setRelatedCptCode(found.cptCode[0]); // we dont have any tests with more than one - if (prefillData.type === 'repeat') setRepeatTest(true); + if (prefillData.type === 'repeat') { + found.orderedAsRepeat = true; + } + setSelectedTests([found]); } } if (diagnoses) { @@ -151,7 +141,7 @@ export const InHouseLabOrderCreatePage: React.FC = () => { navigate(-1); }; - const canBeSubmitted = !!(encounter?.id && selectedTest && relatedCptCode); + const canBeSubmitted = !!(encounter?.id && selectedTests.length > 0); const handleSubmit = async (e: React.FormEvent | React.MouseEvent, shouldPrintLabel = false): Promise => { e.preventDefault(); @@ -161,11 +151,9 @@ export const InHouseLabOrderCreatePage: React.FC = () => { try { const res = await createInHouseLabOrder(oystehrZambda, { encounterId: encounter.id!, - testItem: selectedTest, - cptCode: relatedCptCode, + testItems: selectedTests, diagnosesAll: [...selectedAssessmentDiagnoses, ...selectedNewDiagnoses], diagnosesNew: selectedNewDiagnoses, - isRepeatTest: repeatTest, notes: notes, }); @@ -195,9 +183,7 @@ export const InHouseLabOrderCreatePage: React.FC = () => { window.open(labelPdf.presignedURL, '_blank'); } - if (res.serviceRequestId) { - navigate(`/in-person/${appointment?.id}/in-house-lab-orders/${res.serviceRequestId}/order-details`); - } + navigate(`/in-person/${appointment?.id}/in-house-lab-orders`); } catch (e) { const sdkError = e as Oystehr.OystehrSdkError; console.error('error creating in house lab order', sdkError.code, sdkError.message); @@ -212,7 +198,7 @@ export const InHouseLabOrderCreatePage: React.FC = () => { } } else if (!canBeSubmitted) { const errorMessage: string[] = []; - if (!selectedTest) errorMessage.push('Please select a test to order'); + if (!selectedTests.length) errorMessage.push('Please select a test to order'); if (!attendingPractitionerId) errorMessage.push('No attending practitioner has been assigned to this encounter'); if (errorMessage.length === 0) errorMessage.push(GENERIC_ERROR_MSG); setError(errorMessage); @@ -231,8 +217,28 @@ export const InHouseLabOrderCreatePage: React.FC = () => { return; } - setSelectedTest(foundEntry); - setRelatedCptCode(foundEntry.cptCode[0]); // we dont have any tests with more than one + setSelectedTests([...selectedTests, foundEntry]); + }; + + const handleSetSelectedLabsViaLabSets = async (labSet: LabListsDTO): Promise => { + if (labSet.listType === LabType.inHouse) { + const res = await apiClient?.getCreateInHouseLabOrderResources({ + selectedLabSet: labSet, + }); + const labs = res?.labs; + + console.log('inhouse labs', labs); + + if (labs) { + setSelectedTests((currentLabs) => { + const existingCodes = new Set(currentLabs.map((lab) => lab.adId)); + + const newLabs = labs.filter((lab) => !existingCodes.has(lab.adId)); + + return [...currentLabs, ...newLabs]; + }); + } + } }; return ( @@ -258,7 +264,6 @@ export const InHouseLabOrderCreatePage: React.FC = () => { { labelId="test-type-label" id="test-type" data-testid={dataTestIds.orderInHouseLabPage.testTypeField} - value={selectedTest?.name || ''} + value="" label="Test" onChange={(e) => handleTestSelection(e.target.value)} MenuProps={{ @@ -296,90 +301,87 @@ export const InHouseLabOrderCreatePage: React.FC = () => { }} > {availableTests.map((test) => ( - + {test.name} ))} - - {relatedCptCode && ( - <> - - - - {selectedTest?.repeatable && ( - - setRepeatTest(!repeatTest)} /> - } - label={Run as Repeat} - /> - - )} - - )} + {labSets && } - {repeatTest && ( - <> - - 0 && ( + + - - {/* indicates that the test is “CLIA waived”, should just be hardcoded for repeats */} - - QW - - - )} + > + + + + Test + + + CPT Code + + + Run as repeat + + + + + + + {selectedTests.map((test) => ( + + {test.name} + {test.cptCode} + + {test.repeatable && ( + { + const checked = e.target.checked; + + setSelectedTests((prev) => + prev.map((item) => + item.name === test.name ? { ...item, orderedAsRepeat: checked } : item + ) + ); + }} + /> + )} + + + + setSelectedTests((prev) => + prev.filter((tempLab) => { + const tempLabName = tempLab.name; + const labName = test.name; + + return tempLabName !== labName; + }) + ) + } + /> + + + ))} + +
+
+ )} +
= { @@ -121,6 +124,7 @@ const zambdasPublicityMap: Record = { 'get unsolicited results resources': false, 'update lab order resources': false, 'search places': false, + 'inhouse lab resource search': false, }; export type OystehrTelemedAPIClient = ReturnType; @@ -159,6 +163,7 @@ export const getOystehrTelemedAPI = ( getUnsolicitedResultsResources: typeof getUnsolicitedResultsResources; updateLabOrderResources: typeof updateLabOrderResources; searchPlaces: typeof searchPlaces; + getCreateInHouseLabOrderResources: typeof getCreateInHouseLabOrderResources; } => { const { getTelemedAppointmentsZambdaID, @@ -191,6 +196,7 @@ export const getOystehrTelemedAPI = ( getUnsolicitedResultsResourcesID, updateLabOrderResourcesID, searchPlacesID, + inhouseLabResourceSearchID, } = params; const zambdasToIdsMap: Record = { @@ -224,6 +230,7 @@ export const getOystehrTelemedAPI = ( 'get unsolicited results resources': getUnsolicitedResultsResourcesID, 'update lab order resources': updateLabOrderResourcesID, 'search places': searchPlacesID, + 'inhouse lab resource search': inhouseLabResourceSearchID, }; const isAppLocalProvided = params.isAppLocal != null; @@ -393,6 +400,12 @@ export const getOystehrTelemedAPI = ( return await makeZapRequest('search places', parameters); }; + const getCreateInHouseLabOrderResources = async ( + parameters: GetCreateInHouseLabOrderResourcesInput + ): Promise => { + return await makeZapRequest('inhouse lab resource search', parameters); + }; + return { getTelemedAppointments, initTelemedSession, @@ -424,5 +437,6 @@ export const getOystehrTelemedAPI = ( getUnsolicitedResultsResources, updateLabOrderResources, searchPlaces, + getCreateInHouseLabOrderResources, }; }; diff --git a/apps/ehr/src/features/visits/shared/api/types.ts b/apps/ehr/src/features/visits/shared/api/types.ts index 680fd135b2..1da563a315 100644 --- a/apps/ehr/src/features/visits/shared/api/types.ts +++ b/apps/ehr/src/features/visits/shared/api/types.ts @@ -30,6 +30,7 @@ export type GetOystehrTelemedAPIParams = { getUnsolicitedResultsResourcesID?: string; updateLabOrderResourcesID?: string; searchPlacesID?: string; + inhouseLabResourceSearchID?: string; }; export type { PromiseReturnType } from 'utils'; diff --git a/apps/ehr/src/features/visits/shared/hooks/useOystehrAPIClient.ts b/apps/ehr/src/features/visits/shared/hooks/useOystehrAPIClient.ts index 27be80dad5..7bfa7b5c6a 100644 --- a/apps/ehr/src/features/visits/shared/hooks/useOystehrAPIClient.ts +++ b/apps/ehr/src/features/visits/shared/hooks/useOystehrAPIClient.ts @@ -39,6 +39,7 @@ export const useOystehrAPIClient = (): ReturnType | getUnsolicitedResultsResourcesID: 'get-unsolicited-results-resources', updateLabOrderResourcesID: 'update-lab-order-resources', searchPlacesID: 'search-places', + inhouseLabResourceSearchID: 'get-create-in-house-lab-order-resources', }, oystehrZambda ); diff --git a/apps/ehr/src/features/visits/shared/stores/appointment/appointment.queries.ts b/apps/ehr/src/features/visits/shared/stores/appointment/appointment.queries.ts index c202e6c22d..589a24b6fb 100644 --- a/apps/ehr/src/features/visits/shared/stores/appointment/appointment.queries.ts +++ b/apps/ehr/src/features/visits/shared/stores/appointment/appointment.queries.ts @@ -35,6 +35,8 @@ import { DiagnosisDTO, filterResources, FinalizeUnsolicitedResultMatch, + GetCreateInHouseLabOrderResourcesInput, + GetCreateInHouseLabOrderResourcesOutput, GetCreateLabOrderResources, GetMedicationOrdersInput, GetMedicationOrdersResponse, @@ -339,6 +341,28 @@ export const useGetCreateExternalLabResources = ({ }); }; +export const useGetCreateInHouseLabResources = ({ + encounterId, +}: GetCreateInHouseLabOrderResourcesInput): UseQueryResult => { + const apiClient = useOystehrAPIClient(); + return useQuery({ + queryKey: ['inhouse lab resource search', encounterId], + + queryFn: async () => { + const res = await apiClient?.getCreateInHouseLabOrderResources({ encounterId }); + if (res) { + return res; + } else { + return null; + } + }, + + enabled: Boolean(apiClient), + placeholderData: keepPreviousData, + staleTime: QUERY_STALE_TIME, + }); +}; + export function useDisplayUnsolicitedResultsIcon( input: GetUnsolicitedResultsIconStatusInput ): UseQueryResult { diff --git a/packages/utils/lib/helpers/in-house-labs/index.ts b/packages/utils/lib/helpers/in-house-labs/index.ts index c15665c7b1..c9470b9520 100644 --- a/packages/utils/lib/helpers/in-house-labs/index.ts +++ b/packages/utils/lib/helpers/in-house-labs/index.ts @@ -361,6 +361,7 @@ export const convertActivityDefinitionToTestItem = ( reflexAlert, adUrl: activityDef.url, adVersion: activityDef.version, + adId: activityDef.id ?? 'unknown', }; console.log('successfully converted activity ActivityDefinition to testItem format for', testItem.name); diff --git a/packages/utils/lib/types/data/in-house/in-house.types.ts b/packages/utils/lib/types/data/in-house/in-house.types.ts index 058d4ea0d1..1b595888ae 100644 --- a/packages/utils/lib/types/data/in-house/in-house.types.ts +++ b/packages/utils/lib/types/data/in-house/in-house.types.ts @@ -1,5 +1,5 @@ import { Bundle, FhirResource } from 'fhir/r4b'; -import { DiagnosisDTO, OBSERVATION_CODES, Pagination } from '../..'; +import { DiagnosisDTO, InHouseLabListDTO, LabListsDTO, OBSERVATION_CODES, Pagination } from 'utils'; export interface TestItemMethods { manual?: { device: string }; @@ -83,6 +83,7 @@ export interface TestItem { reflexAlert: { alert: string; testName: string; canonicalUrl: string } | undefined; // for now we are only ever expecting one alert but this might change in the future adUrl: string; adVersion: string; + adId: string; note?: string; } @@ -154,25 +155,23 @@ export type GetInHouseOrdersParameters = InHouseOrdersSearchBy & export type CreateInHouseLabOrderParameters = { encounterId: string; - testItem: TestItem; - cptCode: string; + testItems: TestItem[]; diagnosesAll: DiagnosisDTO[]; diagnosesNew: DiagnosisDTO[]; - isRepeatTest: boolean; notes?: string; }; export type CreateInHouseLabOrderResponse = { transactionResponse: { output: Bundle }; saveChartDataResponse: { output: { chartData: { diagnosis: (DiagnosisDTO & { resourceId: string })[] } } }; - serviceRequestId?: string | undefined; }; -export type GetCreateInHouseLabOrderResourcesParameters = { encounterId?: string }; +export type GetCreateInHouseLabOrderResourcesInput = { encounterId?: string; selectedLabSet?: InHouseLabListDTO }; -export type GetCreateInHouseLabOrderResourcesResponse = { +export type GetCreateInHouseLabOrderResourcesOutput = { labs: TestItem[]; - providerName: string; + providerName?: string; + labSets?: LabListsDTO[] | undefined; }; export type CollectInHouseLabSpecimenParameters = { diff --git a/packages/utils/lib/types/data/labs/labs.types.ts b/packages/utils/lib/types/data/labs/labs.types.ts index 8fbf090d05..8a868c1c26 100644 --- a/packages/utils/lib/types/data/labs/labs.types.ts +++ b/packages/utils/lib/types/data/labs/labs.types.ts @@ -19,16 +19,33 @@ export interface OrderableItemSearchResult { lab: OrderableItemLab; } -export interface LabListsDTO { +export interface ExternalLabListItem { + display: string; // {test name / filler lab name} + itemCode: string; + labGuid: string; +} + +export interface InHouseLabListItem { + display: string; + activityDefinitionId: string; +} + +export interface ExternalLabListDTO { listId: string; listName: string; - labs: { - display: string; // formatted list {test name / filler lab name} - itemCode: string; - labGuid: string; - }[]; + listType: LabType.external; + labs: ExternalLabListItem[]; +} + +export interface InHouseLabListDTO { + listId: string; + listName: string; + listType: LabType.inHouse; + labs: InHouseLabListItem[]; } +export type LabListsDTO = ExternalLabListDTO | InHouseLabListDTO; + export interface sampleDTO { specimen: { id: string; collectionDate?: string }; // collectionDate exists after order is submitted definition: OrderableItemSpecimen; @@ -333,7 +350,7 @@ export type GetCreateLabOrderResources = { encounterId?: string; search?: string; labOrgIdsString?: string; - selectedLabSet?: LabListsDTO; + selectedLabSet?: ExternalLabListDTO; }; export type ModifiedOrderingLocation = { diff --git a/packages/zambdas/src/ehr/lab/external/get-create-lab-order-resources/index.ts b/packages/zambdas/src/ehr/lab/external/get-create-lab-order-resources/index.ts index 4afa1a0af5..da5fe9778e 100644 --- a/packages/zambdas/src/ehr/lab/external/get-create-lab-order-resources/index.ts +++ b/packages/zambdas/src/ehr/lab/external/get-create-lab-order-resources/index.ts @@ -1,6 +1,6 @@ import Oystehr, { BatchInputRequest, Bundle } from '@oystehr/sdk'; import { APIGatewayProxyResult } from 'aws-lambda'; -import { Account, Appointment, Coverage, Encounter, List, Location, Organization, Reference } from 'fhir/r4b'; +import { Account, Appointment, Coverage, Encounter, List, Location, Organization } from 'fhir/r4b'; import { CODE_SYSTEM_COVERAGE_CLASS, CPTCodeOption, @@ -12,11 +12,7 @@ import { isAppointmentWorkersComp, LAB_ACCOUNT_NUMBER_SYSTEM, LAB_LIST_CODE_CODING, - LAB_LIST_ITEM_SEARCH_FIELD_EXTENSION_URL, - LAB_LIST_SEARCH_FIELD_NESTED_EXTENSION_URL, LAB_ORG_TYPE_CODING, - LabListsDTO, - LabListSearchFieldKey, LabOrderResourcesRes, ModifiedOrderingLocation, OrderableItemSearchResult, @@ -28,6 +24,7 @@ import { import { checkOrCreateM2MClientToken, topLevelCatch, wrapHandler } from '../../../../shared'; import { createOystehrClient } from '../../../../shared/helpers'; import { ZambdaInput } from '../../../../shared/types'; +import { formatLabListDTOs } from '../../shared/helpers'; import { accountIsPatientBill, accountIsWorkersComp, sortCoveragesByPriority } from '../../shared/labs'; import { validateRequestParameters } from './validateRequestParameters'; @@ -374,47 +371,3 @@ const getCoverageInfo = (accounts: Account[], coverages: Coverage[]): CreateLabC return []; } }; - -const formatLabListDTOs = (labLists: List[]): LabListsDTO[] | undefined => { - if (labLists.length === 0) return; - const formattedListDTOs: LabListsDTO[] = []; - labLists.forEach((list, idx) => { - const formatted: LabListsDTO = { - listId: list.id ?? `missing-${idx}`, - listName: list.title ?? 'Lab List (title missing)', - labs: - list.entry?.map((lab) => { - const labForList = { - display: lab.item.display ?? 'lab item display missing', - itemCode: getLabListEntryFieldFromExtension(lab.item, 'itemCode', list.id), - labGuid: getLabListEntryFieldFromExtension(lab.item, 'labGuid', list.id), - }; - return labForList; - }) ?? [], - }; - formattedListDTOs.push(formatted); - }); - return formattedListDTOs; -}; - -const getLabListEntryFieldFromExtension = ( - lab: Reference, - field: LabListSearchFieldKey, - listId: string | undefined -): string => { - const searchFieldsExt = lab.extension?.find((ext) => ext.url === LAB_LIST_ITEM_SEARCH_FIELD_EXTENSION_URL); - - const nestedExtensionUrl = LAB_LIST_SEARCH_FIELD_NESTED_EXTENSION_URL[field]; - - const fieldValue = searchFieldsExt?.extension?.find((ext) => ext.url === nestedExtensionUrl)?.valueString; - - if (!fieldValue) { - throw Error( - `Lab list misconfiguration: unable to find nested extension with url ${nestedExtensionUrl} from the extension on ${JSON.stringify( - lab - )} within List/${listId}` - ); - } - - return fieldValue; -}; diff --git a/packages/zambdas/src/ehr/lab/external/get-create-lab-order-resources/validateRequestParameters.ts b/packages/zambdas/src/ehr/lab/external/get-create-lab-order-resources/validateRequestParameters.ts index 9378311f52..de8b7e7527 100644 --- a/packages/zambdas/src/ehr/lab/external/get-create-lab-order-resources/validateRequestParameters.ts +++ b/packages/zambdas/src/ehr/lab/external/get-create-lab-order-resources/validateRequestParameters.ts @@ -12,6 +12,10 @@ export function validateRequestParameters(input: ZambdaInput): GetCreateLabOrder throw new Error('One of the following must be passed as a parameter: patientId, search, selectedLabSet'); } + if (search && selectedLabSet) { + throw new Error('Please pass either a search test or a selectedLabSet, not both'); + } + return { patientId, encounterId, diff --git a/packages/zambdas/src/ehr/lab/in-house/create-in-house-lab-order/helpers.ts b/packages/zambdas/src/ehr/lab/in-house/create-in-house-lab-order/helpers.ts new file mode 100644 index 0000000000..fabc2c2f52 --- /dev/null +++ b/packages/zambdas/src/ehr/lab/in-house/create-in-house-lab-order/helpers.ts @@ -0,0 +1,312 @@ +import { BatchInputPostRequest } from '@oystehr/sdk'; +import { randomUUID } from 'crypto'; +import { + ActivityDefinition, + Coverage, + Encounter, + Location, + Patient, + Procedure, + Provenance, + ServiceRequest, + Task, +} from 'fhir/r4b'; +import { DateTime } from 'luxon'; +import { + CODE_SYSTEM_CPT, + DiagnosisDTO, + EXTENSION_URL_CPT_MODIFIER, + FHIR_IDC10_VALUESET_SYSTEM, + getFullestAvailableName, + IN_HOUSE_LAB_TASK, + PROVENANCE_ACTIVITY_CODING_ENTITY, + REFLEX_TEST_ORDER_DETAIL_TAG_CONFIG, +} from 'utils'; +import { fillMeta, makeCptModifierExtension } from '../../../../shared'; +import { createTask } from '../../../../shared/tasks'; + +export interface TestItemRequestData { + activityDefinition: ActivityDefinition; + serviceRequests: ServiceRequest[] | undefined; + orderedAsRepeat: boolean; + parentTestCanonicalUrl: string | undefined; // indicates this test is being run as reflex +} + +export interface TestItemResources { + activityDefinition: ActivityDefinition; + initialServiceRequest: ServiceRequest | undefined; + testDetailType: 'reflex' | 'repeat' | undefined; +} + +export interface CreateInHouseLabResources { + diagnosesAll: DiagnosisDTO[]; + notes: string | undefined; + testResources: TestItemResources[]; + encounter: Encounter; + patient: Patient; + coverage: Coverage | undefined; + location: Location | undefined; + currentUserPractitionerName: string | undefined; + currentUserPractitionerId: string; + attendingPractitionerName: string | undefined; + attendingPractitionerId: string; +} + +export const makeRequestsForCreateInHouseLabs = ( + resources: CreateInHouseLabResources +): BatchInputPostRequest[] => { + const { testResources } = resources; + + const requests: BatchInputPostRequest[] = []; + + testResources.forEach((testData) => { + const { activityDefinition, testDetailType } = testData; + + const serviceRequestFullUrl = `urn:uuid:${randomUUID()}`; + const serviceRequestConfig = makeServiceRequestConfig(resources, testData); + + const taskConfig = makeTaskConfig( + resources, + activityDefinition, + serviceRequestConfig.authoredOn, + serviceRequestFullUrl + ); + + const provenanceConfig = makeProvenanceConfig(resources, serviceRequestFullUrl); + + const procedureConfig = makeProcedureConfig(resources, activityDefinition, testDetailType); + + requests.push( + { + method: 'POST', + url: '/ServiceRequest', + resource: serviceRequestConfig, + fullUrl: serviceRequestFullUrl, + }, + { + method: 'POST', + url: '/Task', + resource: taskConfig, + }, + { + method: 'POST', + url: '/Provenance', + resource: provenanceConfig, + }, + { + method: 'POST', + url: '/Procedure', + resource: procedureConfig, + } + ); + }); + + return requests; +}; + +const makeServiceRequestConfig = ( + resources: CreateInHouseLabResources, + testData: TestItemResources +): ServiceRequest => { + const { diagnosesAll, notes, encounter, patient, coverage, location, attendingPractitionerId } = resources; + const { activityDefinition, initialServiceRequest, testDetailType } = testData; + + const serviceRequestConfig: ServiceRequest = { + resourceType: 'ServiceRequest', + status: 'draft', + intent: 'order', + subject: { + reference: `Patient/${patient.id}`, + }, + encounter: { + reference: `Encounter/${encounter.id}`, + }, + requester: { + reference: `Practitioner/${attendingPractitionerId}`, + }, + authoredOn: DateTime.now().toISO() || undefined, + priority: 'stat', + code: { + coding: activityDefinition.code?.coding, + text: activityDefinition.name, + }, + reasonCode: [...diagnosesAll].map((diagnosis) => { + return { + coding: [ + { + system: FHIR_IDC10_VALUESET_SYSTEM, + code: diagnosis?.code, + display: diagnosis?.display, + }, + ], + text: diagnosis?.display, + }; + }), + ...(location && { + locationReference: [ + { + type: 'Location', + reference: `Location/${location.id}`, + }, + ], + }), + ...(notes && { note: [{ text: notes }] }), + ...(coverage && { insurance: [{ reference: `Coverage/${coverage.id}` }] }), + instantiatesCanonical: [`${activityDefinition.url}|${activityDefinition.version}`], + }; + + // if an initialServiceRequest is defined, the test being ordered is repeat OR reflex and should be linked to the + // original test represented by initialServiceRequest + if (initialServiceRequest) { + serviceRequestConfig.basedOn = [ + { + reference: `ServiceRequest/${initialServiceRequest.id}`, + }, + ]; + } + + if (testDetailType === 'reflex') { + serviceRequestConfig.meta = { tag: [REFLEX_TEST_ORDER_DETAIL_TAG_CONFIG] }; + } + + return serviceRequestConfig; +}; + +const makeTaskConfig = ( + resources: CreateInHouseLabResources, + activityDefinition: ActivityDefinition, + serviceRequestConfigAuthoredOn: string | undefined, + serviceRequestFullUrl: string +): Task => { + const { encounter, patient, location, currentUserPractitionerName } = resources; + + const patientName = getFullestAvailableName(patient); + + const taskConfig = createTask({ + category: IN_HOUSE_LAB_TASK.category, + title: `Collect sample for “${activityDefinition.name}” for ${patientName}`, + code: { + system: IN_HOUSE_LAB_TASK.system, + code: IN_HOUSE_LAB_TASK.code.collectSampleTask, + }, + encounterId: encounter.id, + location: location?.id + ? { + id: location.id, + } + : undefined, + input: [ + { + type: IN_HOUSE_LAB_TASK.input.testName, + valueString: activityDefinition.name, + }, + { + type: IN_HOUSE_LAB_TASK.input.patientName, + valueString: patientName, + }, + { + type: IN_HOUSE_LAB_TASK.input.providerName, + valueString: currentUserPractitionerName ?? 'Unknown', + }, + { + type: IN_HOUSE_LAB_TASK.input.orderDate, + valueString: serviceRequestConfigAuthoredOn, + }, + { + type: IN_HOUSE_LAB_TASK.input.appointmentId, + valueString: encounter.appointment?.[0]?.reference?.split('/')?.[1], + }, + ], + basedOn: [serviceRequestFullUrl], + }); + + return taskConfig; +}; + +const makeProvenanceConfig = (resources: CreateInHouseLabResources, serviceRequestFullUrl: string): Provenance => { + const { + location, + currentUserPractitionerName, + currentUserPractitionerId, + attendingPractitionerName, + attendingPractitionerId, + } = resources; + + const provenanceConfig: Provenance = { + resourceType: 'Provenance', + activity: { + coding: [PROVENANCE_ACTIVITY_CODING_ENTITY.createOrder], + }, + target: [{ reference: serviceRequestFullUrl }], + ...(location && { location: { reference: `Location/${location.id}` } }), + recorded: DateTime.now().toISO(), + agent: [ + { + who: { + reference: `Practitioner/${currentUserPractitionerId}`, + display: currentUserPractitionerName, + }, + onBehalfOf: { + reference: `Practitioner/${attendingPractitionerId}`, + display: attendingPractitionerName, + }, + }, + ], + }; + + return provenanceConfig; +}; + +const makeProcedureConfig = ( + resources: CreateInHouseLabResources, + activityDefinition: ActivityDefinition, + testDetailType: TestItemResources['testDetailType'] +): Procedure => { + const { encounter, patient, attendingPractitionerId } = resources; + + let procedureCodeExtension = {}; + + if (testDetailType === 'repeat') { + // this logic will cover if we add a test that is repeatable and has an extra modifier on it + // otherwise it will be spread below + const additionalModifierExt = + activityDefinition.code?.coding + ?.find((coding) => coding.system === CODE_SYSTEM_CPT) + ?.extension?.filter((ext) => ext.url === EXTENSION_URL_CPT_MODIFIER && ext.valueCodeableConcept) ?? []; + + const repeatModifier = makeCptModifierExtension([ + { code: '91', display: 'Repeat Clinical Diagnostic Laboratory Test' }, + ]); + procedureCodeExtension = { extension: [repeatModifier, ...additionalModifierExt] }; + } + + const procedureConfig: Procedure = { + resourceType: 'Procedure', + status: 'completed', + subject: { + reference: `Patient/${patient.id}`, + }, + encounter: { + reference: `Encounter/${encounter.id}`, + }, + performer: [ + { + actor: { + reference: `Practitioner/${attendingPractitionerId}`, + }, + }, + ], + code: { + coding: [ + { + ...activityDefinition.code?.coding?.find((coding) => coding.system === CODE_SYSTEM_CPT), + display: activityDefinition.name, + ...procedureCodeExtension, + }, + ], + }, + meta: fillMeta('cpt-code', 'cpt-code'), // This is necessary to get the Assessment part of the chart showing the CPT codes. It is some kind of save-chart-data feature that this meta is used to find and save the CPT codes instead of just looking at the FHIR Procedure resources code values. + }; + + return procedureConfig; +}; diff --git a/packages/zambdas/src/ehr/lab/in-house/create-in-house-lab-order/index.ts b/packages/zambdas/src/ehr/lab/in-house/create-in-house-lab-order/index.ts index 2c64a93997..1dfb32b186 100644 --- a/packages/zambdas/src/ehr/lab/in-house/create-in-house-lab-order/index.ts +++ b/packages/zambdas/src/ehr/lab/in-house/create-in-house-lab-order/index.ts @@ -1,36 +1,24 @@ -import { BatchInputRequest } from '@oystehr/sdk'; import { APIGatewayProxyResult } from 'aws-lambda'; -import { randomUUID } from 'crypto'; import { Account, ActivityDefinition, Coverage, Encounter, - FhirResource, Location, Patient, Practitioner, - Procedure, - Provenance, ServiceRequest, } from 'fhir/r4b'; -import { DateTime } from 'luxon'; import { APIError, - CODE_SYSTEM_CPT, CreateInHouseLabOrderParameters, - EXTENSION_URL_CPT_MODIFIER, - FHIR_IDC10_VALUESET_SYSTEM, getAttendingPractitionerId, getFullestAvailableName, getSecret, IN_HOUSE_LAB_ERROR, - IN_HOUSE_LAB_TASK, IN_HOUSE_TEST_CODE_SYSTEM, isApiError, - PROVENANCE_ACTIVITY_CODING_ENTITY, REFLEX_ARTIFACT_DISPLAY, - REFLEX_TEST_ORDER_DETAIL_TAG_CONFIG, Secrets, SecretsKeys, SERVICE_REQUEST_REFLEX_TRIGGERED_TAG_CODES, @@ -39,16 +27,18 @@ import { import { checkOrCreateM2MClientToken, createOystehrClient, - fillMeta, getMyPractitionerId, - makeCptModifierExtension, - parseCreatedResourcesBundle, topLevelCatch, wrapHandler, ZambdaInput, } from '../../../../shared'; -import { createTask } from '../../../../shared/tasks'; import { accountIsPatientBill, getPrimaryInsurance } from '../../shared/labs'; +import { + CreateInHouseLabResources, + makeRequestsForCreateInHouseLabs, + TestItemRequestData, + TestItemResources, +} from './helpers'; import { validateRequestParameters } from './validateRequestParameters'; let m2mToken: string; @@ -78,15 +68,7 @@ export const index = wrapHandler(ZAMBDA_NAME, async (input: ZambdaInput): Promis const oystehr = createOystehrClient(m2mToken, secrets); const oystehrCurrentUser = createOystehrClient(validatedParameters.userToken, validatedParameters.secrets); - const { - encounterId, - testItem, - cptCode: cptCode, - diagnosesAll, - diagnosesNew, - isRepeatTest, - notes, - } = validatedParameters; + const { encounterId, testItems, diagnosesAll, diagnosesNew, notes } = validatedParameters; const encounterResourcesRequest = async (): Promise<(Encounter | Patient | Location | Coverage | Account)[]> => ( @@ -117,22 +99,85 @@ export const index = wrapHandler(ZAMBDA_NAME, async (input: ZambdaInput): Promis }) ).unbundle() as (Encounter | Patient | Location | Coverage | Account)[]; - const activeDefinitionRequest = async (): Promise => - ( - await oystehr.fhir.search({ - resourceType: 'ActivityDefinition', - params: [ - { - name: 'url', - value: testItem.adUrl, - }, - { - name: 'version', - value: testItem.adVersion, - }, - ], + const testItemRequests = (): Promise => { + return Promise.all( + testItems.map(async (item) => { + const activityDefs = await oystehr.fhir + .search({ + resourceType: 'ActivityDefinition', + params: [ + { name: 'url', value: item.adUrl }, + { name: 'version', value: item.adVersion }, + ], + }) + .then((result) => result.unbundle() as ActivityDefinition[]); + + if (activityDefs.length !== 1) { + throw Error( + `ActivityDefinition not found, results contain ${ + activityDefs.length + } activity definitions, ids: ${activityDefs + .map((resource) => `ActivityDefinition/${resource.id}`) + .join(', ')}` + ); + } + + const activityDefinition = activityDefs[0]; + + let parentTestCanonicalUrl: string | undefined; + activityDefinition.relatedArtifact?.forEach((artifact) => { + const isDependent = artifact.type === 'depends-on'; + const isRelatedViaReflex = artifact.display === REFLEX_ARTIFACT_DISPLAY; + + if (isDependent && isRelatedViaReflex) { + // todo labs this will take the last one it finds, so if we ever have a test be triggered by multiple parents, we'll need to update this + parentTestCanonicalUrl = artifact.resource; + } + }); + + let serviceRequests: ServiceRequest[] | undefined; + + if (item.orderedAsRepeat) { + console.log('run as repeat for', item.name); + // tests being run as repeat need to be linked via basedOn to the original test that was run + // so we are looking for a test with the same instantiatesCanonical that does not have any value in basedOn - this will be the initialServiceRequest + serviceRequests = await oystehr.fhir + .search({ + resourceType: 'ServiceRequest', + params: [ + { name: 'encounter', value: `Encounter/${encounterId}` }, + { name: 'instantiates-canonical', value: `${item.adUrl}|${item.adVersion}` }, + ], + }) + .then((result) => result.unbundle() as ServiceRequest[]); + } else if (parentTestCanonicalUrl) { + console.log('searching for parent test', parentTestCanonicalUrl); + // we should be able to search service request by this but looks like a fhir bug so will need to do some more round about searching here + serviceRequests = ( + await oystehr.fhir.search({ + resourceType: 'ServiceRequest', + params: [ + { + name: 'encounter', + value: `Encounter/${encounterId}`, + }, + { + name: 'code', + value: `${IN_HOUSE_TEST_CODE_SYSTEM}|`, + }, + { + name: '_sort', + value: '-_lastUpdated', + }, + ], + }) + ).unbundle(); + } + + return { activityDefinition, serviceRequests, orderedAsRepeat: item.orderedAsRepeat, parentTestCanonicalUrl }; }) - ).unbundle() as ActivityDefinition[]; + ); + }; const userPractitionerIdRequest = async (): Promise => { try { @@ -145,42 +190,13 @@ export const index = wrapHandler(ZAMBDA_NAME, async (input: ZambdaInput): Promis } }; - const requests: any[] = [encounterResourcesRequest(), activeDefinitionRequest(), userPractitionerIdRequest()]; - - if (isRepeatTest) { - console.log('run as repeat for', cptCode, testItem.name); - // tests being run as repeat need to be linked via basedOn to the original test that was run - // so we are looking for a test with the same cptCode that does not have any value in basedOn - this will be the initialServiceRequest - const initialServiceRequestSearch = async (): Promise => - ( - await oystehr.fhir.search({ - resourceType: 'ServiceRequest', - params: [ - { - name: 'encounter', - value: `Encounter/${encounterId}`, - }, - { - name: 'instantiates-canonical', - value: `${testItem.adUrl}|${testItem.adVersion}`, - }, - ], - }) - ).unbundle() as ServiceRequest[]; - requests.push(initialServiceRequestSearch()); - } + const requests: any[] = [encounterResourcesRequest(), testItemRequests(), userPractitionerIdRequest()]; const results = await Promise.all(requests); - const [ - encounterResources, - activeDefinitionResources, - userPractitionerId, - initialServiceRequestResources, // only exists if runAsRepeat is true - ] = results as [ + const [encounterResources, testItemResources, userPractitionerId] = results as [ (Encounter | Patient | Location | Coverage | Account)[], - ActivityDefinition[], + TestItemRequestData[], string, - ServiceRequest[]?, ]; const { @@ -213,107 +229,49 @@ export const index = wrapHandler(ZAMBDA_NAME, async (input: ZambdaInput): Promis } ); - const activityDefinition = (() => { - if (activeDefinitionResources.length !== 1) { - throw Error( - `ActivityDefinition not found, results contain ${ - activeDefinitionResources.length - } activity definitions, ids: ${activeDefinitionResources - .map((resource) => `ActivityDefinition/${resource.id}`) - .join(', ')}` - ); - } + console.log('whats here!', JSON.stringify(testItemResources)); - const activeDefinition = activeDefinitionResources[0]; + const testResources: TestItemResources[] = testItemResources.map((data) => { + const { activityDefinition, serviceRequests, orderedAsRepeat, parentTestCanonicalUrl } = data; - if (activeDefinition.status !== 'active' || !activeDefinition.id || !activeDefinition.url) { + if (activityDefinition.status !== 'active' || !activityDefinition.id || !activityDefinition.url) { throw Error( - `ActivityDefinition is not active or has no id or is missing a canonical url, status: ${activeDefinition.status}, id: ${activeDefinition.id}, url: ${activeDefinition.url}` + `ActivityDefinition is not active or has no id or is missing a canonical url, status: ${activityDefinition.status}, id: ${activityDefinition.id}, url: ${activityDefinition.url}` ); } - return activeDefinition; - })(); - - let parentTestCanonicalUrl: string | undefined; - activityDefinition.relatedArtifact?.forEach((artifact) => { - const isDependent = artifact.type === 'depends-on'; - const isRelatedViaReflex = artifact.display === REFLEX_ARTIFACT_DISPLAY; - - if (isDependent && isRelatedViaReflex) { - // todo labs this will take the last one it finds, so if we ever have a test be triggered by multiple parents, we'll need to update this - parentTestCanonicalUrl = artifact.resource; - } - }); + let initialServiceRequest: ServiceRequest | undefined; + let testDetailType: TestItemResources['testDetailType']; - const encounter = (() => { - const targetEncounter = encounterSearchResults.find((encounter) => encounter.id === encounterId); - if (!targetEncounter) throw Error('Encounter not found'); - return targetEncounter; - })(); - - const patient = (() => { - if (patientsSearchResults.length !== 1) { - throw Error(`Patient not found, results contain ${patientsSearchResults.length} patients`); - } - return patientsSearchResults[0]; - })(); - - const account = (() => { - if (accountSearchResults.length !== 1) { - throw Error(`Account not found, results contain ${accountSearchResults.length} accounts`); - } - return accountSearchResults[0]; - })(); - - // this logic assumes we won't have any repeat reflex - you can only be one :) - const initialServiceRequest = await (async () => { - if (isRepeatTest) { - if (!initialServiceRequestResources || initialServiceRequestResources.length === 0) { + if (orderedAsRepeat) { + if (!serviceRequests || serviceRequests.length === 0) { throw IN_HOUSE_LAB_ERROR( - 'You cannot run this as repeat, no initial tests could be found for this encounter.' + `You cannot run ${activityDefinition.name} as repeat, no initial tests could be found for this encounter.` ); } - const possibleInitialSRs = initialServiceRequestResources.reduce((acc: ServiceRequest[], sr) => { + const possibleInitialSRs = serviceRequests.reduce((acc: ServiceRequest[], sr) => { if (!sr.basedOn) acc.push(sr); return acc; }, []); if (possibleInitialSRs.length > 1) { - console.log('More than one initial tests found for this encounter'); + console.log( + `More than one initial tests found for ${activityDefinition.name} for this Encounter/${encounterId}` + ); // this really shouldn't happen, something is misconfigured throw IN_HOUSE_LAB_ERROR( - 'Could not deduce which test is initial since more than one test has previously been run today' + `Could not deduce which test is initial for ${activityDefinition.name} since more than one test has previously been run today` ); } if (possibleInitialSRs.length === 0) { // this really shouldn't happen, something is misconfigured - throw IN_HOUSE_LAB_ERROR('No initial tests could be found for this encounter.'); + throw IN_HOUSE_LAB_ERROR( + `No initial tests could be found for ${activityDefinition.name} for this encounter.` + ); } - const initialSR = possibleInitialSRs[0]; - return initialSR; - } else if (parentTestCanonicalUrl) { - console.log('searching for parent test', parentTestCanonicalUrl); - // we should be able to search service request by this but looks like a fhir bug so will need to do some more round about searching here - const serviceRequestSearch = ( - await oystehr.fhir.search({ - resourceType: 'ServiceRequest', - params: [ - { - name: 'encounter', - value: `Encounter/${encounterId}`, - }, - { - name: 'code', - value: `${IN_HOUSE_TEST_CODE_SYSTEM}|`, - }, - { - name: '_sort', - value: '-_lastUpdated', - }, - ], - }) - ).unbundle(); - const parentRequest = serviceRequestSearch.find((sr) => { + initialServiceRequest = possibleInitialSRs[0]; + testDetailType = 'repeat'; + } else if (parentTestCanonicalUrl && serviceRequests) { + const parentRequest = serviceRequests.find((sr) => { const isParentTest = sr.instantiatesCanonical?.some((url) => url === parentTestCanonicalUrl); const hasPendingTestTag = sr.meta?.tag?.find( (t) => @@ -323,9 +281,37 @@ export const index = wrapHandler(ZAMBDA_NAME, async (input: ZambdaInput): Promis return isParentTest && hasPendingTestTag; }); console.log('parentRequest', parentRequest?.id); - return parentRequest; + initialServiceRequest = parentRequest; + testDetailType = 'reflex'; } - return; + + const testItemResources: TestItemResources = { + activityDefinition, + initialServiceRequest, + testDetailType, + }; + + return testItemResources; + }); + + const encounter = (() => { + const targetEncounter = encounterSearchResults.find((encounter) => encounter.id === encounterId); + if (!targetEncounter) throw Error('Encounter not found'); + return targetEncounter; + })(); + + const patient = (() => { + if (patientsSearchResults.length !== 1) { + throw Error(`Patient not found, results contain ${patientsSearchResults.length} patients`); + } + return patientsSearchResults[0]; + })(); + + const account = (() => { + if (accountSearchResults.length !== 1) { + throw Error(`Account not found, results contain ${accountSearchResults.length} accounts`); + } + return accountSearchResults[0]; })(); const attendingPractitionerId = getAttendingPractitionerId(encounter); @@ -351,196 +337,23 @@ export const index = wrapHandler(ZAMBDA_NAME, async (input: ZambdaInput): Promis const location: Location | undefined = locationsSearchResults[0]; - const serviceRequestFullUrl = `urn:uuid:${randomUUID()}`; - - const serviceRequestConfig: ServiceRequest = { - resourceType: 'ServiceRequest', - status: 'draft', - intent: 'order', - subject: { - reference: `Patient/${patient.id}`, - }, - encounter: { - reference: `Encounter/${encounterId}`, - }, - requester: { - reference: `Practitioner/${attendingPractitionerId}`, - }, - authoredOn: DateTime.now().toISO() || undefined, - priority: 'stat', - code: { - coding: activityDefinition.code?.coding, - text: activityDefinition.name, - }, - reasonCode: [...diagnosesAll].map((diagnosis) => { - return { - coding: [ - { - system: FHIR_IDC10_VALUESET_SYSTEM, - code: diagnosis?.code, - display: diagnosis?.display, - }, - ], - text: diagnosis?.display, - }; - }), - ...(location && { - locationReference: [ - { - type: 'Location', - reference: `Location/${location.id}`, - }, - ], - }), - ...(notes && { note: [{ text: notes }] }), - ...(coverage && { insurance: [{ reference: `Coverage/${coverage.id}` }] }), - instantiatesCanonical: [`${activityDefinition.url}|${activityDefinition.version}`], - }; - // if an initialServiceRequest is defined, the test being ordered is repeat OR reflex and should be linked to the - // original test represented by initialServiceRequest - if (initialServiceRequest) { - serviceRequestConfig.basedOn = [ - { - reference: `ServiceRequest/${initialServiceRequest.id}`, - }, - ]; - } - - const patientName = getFullestAvailableName(patient); - - const taskConfig = createTask({ - category: IN_HOUSE_LAB_TASK.category, - title: `Collect sample for “${activityDefinition.name}” for ${patientName}`, - code: { - system: IN_HOUSE_LAB_TASK.system, - code: IN_HOUSE_LAB_TASK.code.collectSampleTask, - }, - encounterId: encounterId, - location: location?.id - ? { - id: location.id, - } - : undefined, - input: [ - { - type: IN_HOUSE_LAB_TASK.input.testName, - valueString: activityDefinition.name, - }, - { - type: IN_HOUSE_LAB_TASK.input.patientName, - valueString: patientName, - }, - { - type: IN_HOUSE_LAB_TASK.input.providerName, - valueString: currentUserPractitionerName ?? 'Unknown', - }, - { - type: IN_HOUSE_LAB_TASK.input.orderDate, - valueString: serviceRequestConfig.authoredOn, - }, - { - type: IN_HOUSE_LAB_TASK.input.appointmentId, - valueString: encounter.appointment?.[0]?.reference?.split('/')?.[1], - }, - ], - basedOn: [serviceRequestFullUrl], - }); - - const provenanceConfig: Provenance = { - resourceType: 'Provenance', - activity: { - coding: [PROVENANCE_ACTIVITY_CODING_ENTITY.createOrder], - }, - target: [{ reference: serviceRequestFullUrl }], - ...(location && { location: { reference: `Location/${location.id}` } }), - recorded: DateTime.now().toISO(), - agent: [ - { - who: { - reference: `Practitioner/${userPractitionerId}`, - display: currentUserPractitionerName, - }, - onBehalfOf: { - reference: `Practitioner/${attendingPractitionerId}`, - display: attendingPractitionerName, - }, - }, - ], - }; - - let procedureCodeExtension = {}; - if (isRepeatTest) { - // this logic will cover if we add a test that is repeatable and has an extra modifier on it - // otherwise it will be spread below - const additionalModifierExt = - activityDefinition.code?.coding - ?.find((coding) => coding.system === CODE_SYSTEM_CPT) - ?.extension?.filter((ext) => ext.url === EXTENSION_URL_CPT_MODIFIER && ext.valueCodeableConcept) ?? []; - - const repeatModifier = makeCptModifierExtension([ - { code: '91', display: 'Repeat Clinical Diagnostic Laboratory Test' }, - ]); - procedureCodeExtension = { extension: [repeatModifier, ...additionalModifierExt] }; - - serviceRequestConfig.meta = { tag: [REFLEX_TEST_ORDER_DETAIL_TAG_CONFIG] }; - } - - const procedureConfig: Procedure = { - resourceType: 'Procedure', - status: 'completed', - subject: { - reference: `Patient/${patient.id}`, - }, - encounter: { - reference: `Encounter/${encounterId}`, - }, - performer: [ - { - actor: { - reference: `Practitioner/${attendingPractitionerId}`, - }, - }, - ], - code: { - coding: [ - { - ...activityDefinition.code?.coding?.find((coding) => coding.system === CODE_SYSTEM_CPT), - display: activityDefinition.name, - ...procedureCodeExtension, - }, - ], - }, - meta: fillMeta('cpt-code', 'cpt-code'), // This is necessary to get the Assessment part of the chart showing the CPT codes. It is some kind of save-chart-data feature that this meta is used to find and save the CPT codes instead of just looking at the FHIR Procedure resources code values. + const resourcesToCreateAllRequests: CreateInHouseLabResources = { + diagnosesAll, + notes, + testResources, + encounter, + patient, + coverage, + location, + currentUserPractitionerName, + currentUserPractitionerId: userPractitionerId, + attendingPractitionerName, + attendingPractitionerId, }; - const transactionResponse = await oystehr.fhir.transaction({ - requests: [ - { - method: 'POST', - url: '/ServiceRequest', - resource: serviceRequestConfig, - fullUrl: serviceRequestFullUrl, - }, - { - method: 'POST', - url: '/Task', - resource: taskConfig, - }, - { - method: 'POST', - url: '/Provenance', - resource: provenanceConfig, - }, - { - method: 'POST', - url: '/Procedure', - resource: procedureConfig, - }, - ] as BatchInputRequest[], - }); + const resourcePostRequests = makeRequestsForCreateInHouseLabs(resourcesToCreateAllRequests); - const resources = parseCreatedResourcesBundle(transactionResponse); - const newServiceRequest = resources.find((r) => r.resourceType === 'ServiceRequest'); + const transactionResponse = await oystehr.fhir.transaction({ requests: resourcePostRequests }); if (!transactionResponse.entry?.every((entry) => entry.response?.status[0] === '2')) { throw Error('Error creating in-house lab order in transaction'); @@ -557,7 +370,6 @@ export const index = wrapHandler(ZAMBDA_NAME, async (input: ZambdaInput): Promis const response = { transactionResponse, saveChartDataResponse, - ...(newServiceRequest && { serviceRequestId: newServiceRequest.id }), }; return { diff --git a/packages/zambdas/src/ehr/lab/in-house/create-in-house-lab-order/validateRequestParameters.ts b/packages/zambdas/src/ehr/lab/in-house/create-in-house-lab-order/validateRequestParameters.ts index 91061e81e5..708b6c8f03 100644 --- a/packages/zambdas/src/ehr/lab/in-house/create-in-house-lab-order/validateRequestParameters.ts +++ b/packages/zambdas/src/ehr/lab/in-house/create-in-house-lab-order/validateRequestParameters.ts @@ -35,12 +35,8 @@ export function validateRequestParameters( throw new Error('Encounter ID is required'); } - if (!params.testItem || typeof params.testItem.name !== 'string') { - throw new Error('Test item is required and testItem.name must be a string'); - } - - if (!params.cptCode || typeof params.cptCode !== 'string') { - throw new Error('CPT code is required and must be a string'); + if (!params.testItems) { + throw new Error('testItems is required'); } if (!params.diagnosesAll || !Array.isArray(params.diagnosesAll)) { diff --git a/packages/zambdas/src/ehr/lab/in-house/get-create-in-house-lab-order-resources/index.ts b/packages/zambdas/src/ehr/lab/in-house/get-create-in-house-lab-order-resources/index.ts index a847bad36b..30f4417bac 100644 --- a/packages/zambdas/src/ehr/lab/in-house/get-create-in-house-lab-order-resources/index.ts +++ b/packages/zambdas/src/ehr/lab/in-house/get-create-in-house-lab-order-resources/index.ts @@ -1,13 +1,14 @@ import { APIGatewayProxyResult } from 'aws-lambda'; -import { Encounter, Location, Practitioner } from 'fhir/r4b'; +import { ActivityDefinition, Encounter, List, Practitioner } from 'fhir/r4b'; import { convertActivityDefinitionToTestItem, getAttendingPractitionerId, - GetCreateInHouseLabOrderResourcesParameters, - GetCreateInHouseLabOrderResourcesResponse, + GetCreateInHouseLabOrderResourcesInput, + GetCreateInHouseLabOrderResourcesOutput, getFullestAvailableName, getSecret, - getTimezone, + LAB_LIST_CODE_CODING, + LabListsDTO, Secrets, SecretsKeys, TestItem, @@ -15,11 +16,11 @@ import { import { checkOrCreateM2MClientToken, createOystehrClient, - getMyPractitionerId, topLevelCatch, wrapHandler, ZambdaInput, } from '../../../../shared'; +import { formatLabListDTOs } from '../../shared/helpers'; import { fetchActiveInHouseLabActivityDefinitions } from '../../shared/in-house-labs'; import { validateRequestParameters } from './validateRequestParameters'; @@ -28,7 +29,7 @@ const ZAMBDA_NAME = 'get-create-in-house-lab-order-resources'; export const index = wrapHandler(ZAMBDA_NAME, async (input: ZambdaInput): Promise => { let secrets = input.secrets; - let validatedParameters: GetCreateInHouseLabOrderResourcesParameters & { secrets: Secrets | null; userToken: string }; + let validatedParameters: GetCreateInHouseLabOrderResourcesInput & { secrets: Secrets | null; userToken: string }; try { validatedParameters = validateRequestParameters(input); @@ -49,102 +50,85 @@ export const index = wrapHandler(ZAMBDA_NAME, async (input: ZambdaInput): Promis m2mToken = await checkOrCreateM2MClientToken(m2mToken, secrets); const oystehr = createOystehrClient(m2mToken, secrets); - const { - attendingPractitionerName, - // not sure if we need these - // currentPractitionerName, - // attendingPractitionerId, - // currentPractitionerId, - // timezone, - } = await (async () => { - const oystehrCurrentUser = createOystehrClient(validatedParameters.userToken, validatedParameters.secrets); - - const [myPractitionerId, { encounter, timezone }] = await Promise.all([ - getMyPractitionerId(oystehrCurrentUser), - validatedParameters.encounterId - ? oystehr.fhir - .search({ - resourceType: 'Encounter', - params: [ - { name: '_id', value: validatedParameters.encounterId }, - { name: '_include', value: 'Encounter:location' }, - ], - }) - .then((bundle) => { - const resources = bundle.unbundle(); - const encounter = resources.find((r): r is Encounter => r.resourceType === 'Encounter'); - const location = resources.find((r): r is Location => r.resourceType === 'Location'); - const timezone = location && getTimezone(location); - return { encounter, timezone }; - }) - : Promise.resolve({ encounter: null, timezone: undefined }), - ]); - - if (!encounter) { - // todo: we don't have encounter in patient page, this zambda should return the test items only, - // the rest of data should be fetched from the get-orders zambda - return { - attendingPractitionerName: '', - currentPractitionerName: '', - attendingPractitionerId: '', - currentPractitionerId: '', - timezone: undefined, - }; - } - - const practitionerId = getAttendingPractitionerId(encounter); - - const attendingPractitionerPromise = practitionerId - ? oystehr.fhir.get({ - resourceType: 'Practitioner', - id: practitionerId, - }) - : Promise.resolve(null); - - const currentPractitionerPromise = myPractitionerId - ? oystehr.fhir.get({ - resourceType: 'Practitioner', - id: myPractitionerId, - }) - : Promise.resolve(null); + const testItems: TestItem[] = []; + let providerName: string | undefined; + let labSets: LabListsDTO[] | undefined; - const [attendingPractitioner, currentPractitioner] = await Promise.all([ - attendingPractitionerPromise, - currentPractitionerPromise, - ]); + if (validatedParameters.encounterId) { + const [attendingPractitionerName, activeActivityDefinitions, labLists] = await Promise.all([ + (async () => { + if (!validatedParameters.encounterId) return ''; - const attendingPractitionerName = attendingPractitioner - ? getFullestAvailableName(attendingPractitioner) || '' - : ''; + const encounter = await oystehr.fhir.get({ + resourceType: 'Encounter', + id: validatedParameters.encounterId, + }); - const currentPractitionerName = currentPractitioner ? getFullestAvailableName(currentPractitioner) || '' : ''; + if (!encounter) return ''; - const attendingPractitionerId = attendingPractitioner?.id || ''; - const currentPractitionerId = currentPractitioner?.id || ''; + const practitionerId = getAttendingPractitionerId(encounter); - return { - attendingPractitionerName, - currentPractitionerName, - attendingPractitionerId, - currentPractitionerId, - timezone, - }; - })(); + if (!practitionerId) return ''; - const activeActivityDefinitions = await fetchActiveInHouseLabActivityDefinitions(oystehr); + const practitioner = await oystehr.fhir.get({ + resourceType: 'Practitioner', + id: practitionerId, + }); + + return getFullestAvailableName(practitioner) || ''; + })(), + fetchActiveInHouseLabActivityDefinitions(oystehr), + (async () => { + return ( + await oystehr.fhir.search({ + resourceType: 'List', + params: [ + { name: 'code', value: `${LAB_LIST_CODE_CODING.inHouse.system}|${LAB_LIST_CODE_CODING.inHouse.code}` }, + ], + }) + ).unbundle(); + })(), + ]); - console.log(`Found ${activeActivityDefinitions.length} active ActivityDefinition resources`); + console.log(`Found ${activeActivityDefinitions.length} active ActivityDefinition resources`); - const testItems: TestItem[] = []; + for (const activeDefinition of activeActivityDefinitions) { + const testItem = convertActivityDefinitionToTestItem(activeDefinition); + testItems.push(testItem); + } - for (const activeDefinition of activeActivityDefinitions) { - const testItem = convertActivityDefinitionToTestItem(activeDefinition); - testItems.push(testItem); + labSets = formatLabListDTOs(labLists); + + providerName = attendingPractitionerName; + } else if (validatedParameters.selectedLabSet) { + const { selectedLabSet } = validatedParameters; + const activityDefinitionIds = selectedLabSet.labs.map((lab) => lab.activityDefinitionId); + const labSetActivityDefinitions = ( + await oystehr.fhir.search({ + resourceType: 'ActivityDefinition', + params: [ + { + name: '_id', + value: activityDefinitionIds.join(','), + }, + ], + }) + ).unbundle(); + + console.log( + `Found ${labSetActivityDefinitions.length} active ActivityDefinition resources for the labSet List/${selectedLabSet.listId}` + ); + + for (const activeDefinition of labSetActivityDefinitions) { + const testItem = convertActivityDefinitionToTestItem(activeDefinition); + testItems.push(testItem); + } } - const response: GetCreateInHouseLabOrderResourcesResponse = { - labs: testItems, - providerName: attendingPractitionerName, + const response: GetCreateInHouseLabOrderResourcesOutput = { + labs: testItems.sort((a, b) => a.name.localeCompare(b.name)), + providerName, + labSets, }; return { diff --git a/packages/zambdas/src/ehr/lab/in-house/get-create-in-house-lab-order-resources/validateRequestParameters.ts b/packages/zambdas/src/ehr/lab/in-house/get-create-in-house-lab-order-resources/validateRequestParameters.ts index 4e179221d8..91ba157a53 100644 --- a/packages/zambdas/src/ehr/lab/in-house/get-create-in-house-lab-order-resources/validateRequestParameters.ts +++ b/packages/zambdas/src/ehr/lab/in-house/get-create-in-house-lab-order-resources/validateRequestParameters.ts @@ -1,9 +1,9 @@ -import { GetCreateInHouseLabOrderResourcesParameters, Secrets } from 'utils'; +import { GetCreateInHouseLabOrderResourcesInput, Secrets } from 'utils'; import { ZambdaInput } from '../../../../shared'; export function validateRequestParameters( input: ZambdaInput -): GetCreateInHouseLabOrderResourcesParameters & { secrets: Secrets | null; userToken: string } { +): GetCreateInHouseLabOrderResourcesInput & { secrets: Secrets | null; userToken: string } { if (!input.body) { throw new Error('No request body provided'); } @@ -11,7 +11,7 @@ export function validateRequestParameters( const userToken = input.headers.Authorization.replace('Bearer ', ''); const secrets = input.secrets; - let params: GetCreateInHouseLabOrderResourcesParameters; + let params: GetCreateInHouseLabOrderResourcesInput; try { params = JSON.parse(input.body); @@ -23,5 +23,9 @@ export function validateRequestParameters( throw Error('Encounter ID must be a string'); } + if (params.selectedLabSet && params.encounterId) { + throw Error('Please pass either selectedLabSet or encounterId, not both parameters'); + } + return { userToken, secrets, ...params }; } diff --git a/packages/zambdas/src/ehr/lab/shared/helpers.ts b/packages/zambdas/src/ehr/lab/shared/helpers.ts index 6482c4ce36..b1acfeba9f 100644 --- a/packages/zambdas/src/ehr/lab/shared/helpers.ts +++ b/packages/zambdas/src/ehr/lab/shared/helpers.ts @@ -3,11 +3,22 @@ import { Communication, DiagnosticReport, DocumentReference, + List, QuestionnaireResponse, + Reference, ServiceRequest, Specimen, Task, } from 'fhir/r4b'; +import { + LAB_LIST_CODE_CODING, + LAB_LIST_CODING_SYSTEM, + LAB_LIST_ITEM_SEARCH_FIELD_EXTENSION_URL, + LAB_LIST_SEARCH_FIELD_NESTED_EXTENSION_URL, + LabListsDTO, + LabListSearchFieldKey, + LabType, +} from 'utils'; type SoftDeleteLabResourceTypes = | 'ServiceRequest' @@ -53,3 +64,91 @@ export const makeSoftDeleteStatusPatchRequest = ( ], }; }; + +export const formatLabListDTOs = (labLists: List[]): LabListsDTO[] | undefined => { + if (labLists.length === 0) return; + const formattedListDTOs: LabListsDTO[] = []; + labLists.forEach((list, idx) => { + const listType = getLabListType(list); + if (!listType) return; + const formattedBase = { + listId: list.id ?? `missing-${idx}`, + listName: list.title ?? 'Lab List (title missing)', + }; + if (listType === LabType.external) { + const formatted: LabListsDTO = { + ...formattedBase, + listType, + labs: + list.entry?.map((lab) => { + const labForList = { + display: lab.item.display ?? 'lab item display missing', + itemCode: getLabListEntryFieldFromExtension(lab.item, 'itemCode', list.id), + labGuid: getLabListEntryFieldFromExtension(lab.item, 'labGuid', list.id), + }; + return labForList; + }) ?? [], + }; + formattedListDTOs.push(formatted); + } else if (listType === LabType.inHouse) { + const formatted: LabListsDTO = { + ...formattedBase, + listType, + labs: + list.entry?.map((lab, idx) => { + const labForList = { + display: lab.item.display ?? 'lab item display missing', + activityDefinitionId: + lab.item.reference?.replace('ActivityDefinition/', '') ?? `inhouse-lab-list-item-${idx}-${list.id}`, + }; + return labForList; + }) ?? [], + }; + formattedListDTOs.push(formatted); + } + }); + return formattedListDTOs; +}; + +/** + * parses data from external lab list entry items + * @param lab the entry item from the list that represents the lab + * @param field which field you are looking for + * @param listId only used in error logging + * @returns string + */ +const getLabListEntryFieldFromExtension = ( + lab: Reference, + field: LabListSearchFieldKey, + listId: string | undefined +): string => { + const searchFieldsExt = lab.extension?.find((ext) => ext.url === LAB_LIST_ITEM_SEARCH_FIELD_EXTENSION_URL); + + const nestedExtensionUrl = LAB_LIST_SEARCH_FIELD_NESTED_EXTENSION_URL[field]; + + const fieldValue = searchFieldsExt?.extension?.find((ext) => ext.url === nestedExtensionUrl)?.valueString; + + if (!fieldValue) { + throw Error( + `Lab list misconfiguration: unable to find nested extension with url ${nestedExtensionUrl} from the extension on ${JSON.stringify( + lab + )} within List/${listId}` + ); + } + + return fieldValue; +}; + +const getLabListType = (list: List): LabType.external | LabType.inHouse | undefined => { + const code = list.code?.coding?.find((c) => c.system === LAB_LIST_CODING_SYSTEM)?.code; + if (!code) return; + + switch (code) { + case LAB_LIST_CODE_CODING.external.code: + return LabType.external; + case LAB_LIST_CODE_CODING.inHouse.code: + return LabType.inHouse; + default: + return; + } +};