Skip to content

Commit

Permalink
feat(KHP3-5778): toggle lab order picking based on pending bill status (
Browse files Browse the repository at this point in the history
#233)

* feat(KHP3-5778): Toggle lab order picking based on pending bill status

* add config for billing query endpoints
  • Loading branch information
donaldkibet authored Jun 21, 2024
1 parent dd907e8 commit 2eafce2
Show file tree
Hide file tree
Showing 15 changed files with 478 additions and 7 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@testing-library/dom": "^8.16.0",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.5.1",
"@types/jest": "^28.1.4",
"@types/lodash-es": "^4.17.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from 'react';
import { Drug } from '@openmrs/esm-patient-common-lib';
import { DosingUnit, MedicationFrequency, MedicationRoute, QuantityUnit } from '../../types';
import { useBillableItem, useSockItemInventory } from './useBilliableItem';
import { DosingUnit, MedicationFrequency, MedicationRoute, QuantityUnit } from '../../../types';
import { useBillableItem, useSockItemInventory } from '../useBillableItem';
import { useTranslation } from 'react-i18next';
import styles from './drug-order.scss';
import { convertToCurrency } from '../../helpers';
import { convertToCurrency } from '../../../helpers';

type DrugOrderProps = {
order: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import { convertToCurrency } from '../../helpers';
import { useBillableItem } from './useBilliableItem';
import { convertToCurrency } from '../../../helpers';
import { useBillableItem } from '../useBillableItem';

type LabOrderProps = {
order: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { OverflowMenuItem } from '@carbon/react';
import { Order } from '@openmrs/esm-patient-common-lib';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useTestOrderBillStatus } from './test-order-action.resource';
import { showModal } from '@openmrs/esm-framework';

type TestOrderProps = { order: Order };

const TestOrderAction: React.FC<TestOrderProps> = ({ order }) => {
const { t } = useTranslation();
const { isLoading, hasPendingPayment } = useTestOrderBillStatus(order.uuid, order.patient.uuid);

const launchModal = useCallback(() => {
const dispose = showModal('pickup-lab-request-modal', {
closeModal: () => dispose(),
order,
});
}, [order]);

// Show the test order if the following conditions are met:
// 1. The current visit is in-patient
// 2. The test order has been paid in full
// 3. The patient is an emergency patient

if (isLoading) {
return <OverflowMenuItem itemText={t('loading', 'Loading...')} />;
}

return (
<OverflowMenuItem
onClick={launchModal}
disabled={hasPendingPayment}
itemText={
hasPendingPayment ? t('unsettledBill', 'Unsettled bill for test.') : t('pickLabRequest', 'Pick Lab Request')
}
/>
);
};

export default TestOrderAction;
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
import { renderHook } from '@testing-library/react';
import * as testResultActionResource from './test-order-action.resource';
import useSWR from 'swr';
import { useConfig, useVisit } from '@openmrs/esm-framework';
import { mockCurrentVisit } from '../../../../__mocks__/visit.mock';

const mockedUseVisit = useVisit as jest.MockedFunction<typeof useVisit>;
const mockedUseConfig = useConfig as jest.MockedFunction<typeof useConfig>;

const mockedBillResult = [
{
uuid: 'ec60bf1f-a7ba-4ca2-a31e-9914a531117b',
display: 'BillLineItem',
voided: false,
voidReason: null,
auditInfo: {
creator: {
uuid: '3473e65d-3ddc-11e2-8b67-0800271ad0ce',
display: 'admin',
links: [],
},
dateCreated: '2024-06-13T15:33:26.000+0300',
changedBy: null,
dateChanged: null,
},
item: '',
billableService: 'ac3ccc83-83d4-4dff-ab35-1077e3edc61b:Brucella Test',
quantity: 1,
price: 300,
priceName: '',
priceUuid: '',
lineItemOrder: 0,
paymentStatus: 'PENDING',
order: {
uuid: 'e06159a1-65f3-45d9-9f57-8578f6c85dee',
display: 'BRUCELLA TEST',
links: [],
type: 'testorder',
},
resourceVersion: '1.8',
},
];

const mockedPatientQueue = [
{
visit: {
uuid: 'ae548a0f-cfd0-47e0-9103-c01e96492be1',
display: 'Outpatient @ Ngarua Health Centre - 12/06/2024 15:13',
links: [
{
rel: 'self',
uri: 'http://ba.kenyahmis.org/openmrs/ws/rest/v1/visit/ae548a0f-cfd0-47e0-9103-c01e96492be1',
resourceAlias: 'visit',
},
],
},
queueEntry: {
uuid: '45d729c7-9bd9-474a-8532-cbfb809c6139',
display: 'Baby J Em',
queue: {
uuid: 'b47f1e37-9531-467e-ba85-74acbd0c462c',
display: 'Triage service',
name: 'Triage service',
description: 'Triage service',
links: [],
},
status: {
uuid: '167407AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
display: 'Waiting',
links: [],
},
patient: {
uuid: '7b7342a9-67c9-4ecc-a37f-ce62560b503b',
display: 'MGKDH7 - Baby J Em',
links: [],
},
visit: {
uuid: 'ae548a0f-cfd0-47e0-9103-c01e96492be1',
display: 'Outpatient @ Ngarua Health Centre - 12/06/2024 15:13',
links: [],
},
priority: {
uuid: '80cd8f8c-5d82-4cdc-b96e-a6addeb94b7f',
display: 'Not Urgent',
links: [],
},
priorityComment: null,
sortWeight: 0,
startedAt: '2024-06-12T15:14:00.000+0300',
endedAt: null,
locationWaitingFor: null,
queueComingFrom: null,
providerWaitingFor: null,
links: [],
},
},
];

const mockSWR = useSWR as jest.Mock;

jest.mock('@openmrs/esm-framework', () => ({
...jest.requireActual('@openmrs/esm-framework'),
restBaseUrl: '/ws/rest/v1',
useConfig: jest.fn(),
useVisit: jest.fn(),
}));

jest.mock('swr', () => ({
__esModule: true,
default: jest.fn(),
}));

jest.mock('./test-order-action.resource', () => ({
...jest.requireActual('./test-order-action.resource'),
usePatientQueue: jest.fn(),
usePatientBill: jest.fn(),
}));

test('should return isLoading as true when isLoadingQueue is true', () => {
mockedUseConfig.mockReturnValue({
inPatientVisitTypeUuid: '3371a4d4-f66f-4454-a86d-92c7b3da990c',
billingStatusQueryUrl: `/ws/rest/v1/cashier/billLineItem?orderUuid={{orderUuid}}&v=full`,
});
mockedUseVisit.mockReturnValue({
currentVisit: mockCurrentVisit,
isLoading: false,
isValidating: false,
activeVisit: mockCurrentVisit,
} as unknown as any);
mockSWR.mockImplementation((url) => {
if (url.includes('visit-queue-entry')) {
return { data: { data: { results: mockedPatientQueue } }, isLoading: true, error: null } as any;
}
if (url.includes('cashier/billLineItem')) {
return { data: { data: { results: mockedBillResult } }, isLoading: true, error: null } as any;
}
});
const { result } = renderHook(() => testResultActionResource.useTestOrderBillStatus('orderUuid', 'patientUuid'));
expect(result.current.isLoading).toBe(true);
});

test('should return `hasPendingPayment` as false when current visit type is inpatient', () => {
mockedUseConfig.mockReturnValue({
inPatientVisitTypeUuid: '3371a4d4-f66f-4454-a86d-92c7b3da990c',
billingStatusQueryUrl: `/ws/rest/v1/cashier/billLineItem?orderUuid={{orderUuid}}&v=full`,
});
mockedUseVisit.mockReturnValue({
currentVisit: { mockCurrentVisit, visitType: { uuid: '3371a4d4-f66f-4454-a86d-92c7b3da990c' } },
isLoading: false,
isValidating: false,
activeVisit: mockCurrentVisit,
} as unknown as any);
mockSWR.mockImplementation((url) => {
if (url.includes('visit-queue-entry')) {
return { data: { data: { results: mockedPatientQueue } }, isLoading: false, error: null } as any;
}
if (url.includes('cashier/billLineItem')) {
return { data: { data: { results: mockedBillResult } }, isLoading: false, error: null } as any;
}
});
const { result } = renderHook(() => testResultActionResource.useTestOrderBillStatus('orderUuid', 'patientUuid'));
expect(result.current.hasPendingPayment).toBe(false);
});

test('should return `hasPendingPayment` as false when patient is an emergency case', () => {
mockedUseConfig.mockReturnValue({
concepts: { emergencyPriorityConceptUuid: '80cd8f8c-5d82-4cdc-b96e-a6addeb94b7f' },
billingStatusQueryUrl: `/ws/rest/v1/cashier/billLineItem?orderUuid={{orderUuid}}&v=full`,
});
mockedUseVisit.mockReturnValue({
currentVisit: { mockCurrentVisit, visitType: { uuid: '3371a4d4-f66f-4454-a86d-92c7b3da990c' } },
isLoading: false,
isValidating: false,
activeVisit: mockCurrentVisit,
} as unknown as any);
mockSWR.mockImplementation((url) => {
if (url.includes('visit-queue-entry')) {
return { data: { data: { results: mockedPatientQueue } }, isLoading: false, error: null } as any;
}
if (url.includes('cashier/billLineItem')) {
return { data: { data: { results: mockedBillResult } }, isLoading: false, error: null } as any;
}
});
const { result } = renderHook(() => testResultActionResource.useTestOrderBillStatus('orderUuid', 'patientUuid'));
expect(result.current.hasPendingPayment).toBe(false);
});

test('should return `hasPendingPayment` as false when patient has completed payments', () => {
mockedUseConfig.mockReturnValue({
concepts: { emergencyPriorityConceptUuid: '180cd8f8c-5d82-4cdc-b96e-a6addeb94b7f' },
billingStatusQueryUrl: `/ws/rest/v1/cashier/billLineItem?orderUuid={{orderUuid}}&v=full`,
});
mockedUseVisit.mockReturnValue({
currentVisit: { mockCurrentVisit, visitType: { uuid: '13371a4d4-f66f-4454-a86d-92c7b3da990c' } },
isLoading: false,
isValidating: false,
activeVisit: mockCurrentVisit,
} as unknown as any);
mockSWR.mockImplementation((url) => {
if (url.includes('visit-queue-entry')) {
return { data: { data: { results: mockedPatientQueue } }, isLoading: false, error: null } as any;
}
if (url.includes('cashier/billLineItem')) {
return {
data: { data: { results: [{ ...mockedBillResult, paymentStatus: 'PAID' }] } },
isLoading: false,
error: null,
} as any;
}
});
const { result } = renderHook(() => testResultActionResource.useTestOrderBillStatus('orderUuid', 'patientUuid'));
expect(result.current.hasPendingPayment).toBe(false);
});

test('should return `hasPendingPayment` as true when patient has done partial payments', () => {
mockedUseConfig.mockReturnValue({
concepts: { emergencyPriorityConceptUuid: '180cd8f8c-5d82-4cdc-b96e-a6addeb94b7f' },
billingStatusQueryUrl: `/ws/rest/v1/cashier/billLineItem?orderUuid={{orderUuid}}&v=full`,
});
mockedUseVisit.mockReturnValue({
currentVisit: { mockCurrentVisit, visitType: { uuid: '13371a4d4-f66f-4454-a86d-92c7b3da990c' } },
isLoading: false,
isValidating: false,
activeVisit: mockCurrentVisit,
} as unknown as any);
mockSWR.mockImplementation((url) => {
if (url.includes('visit-queue-entry')) {
return { data: { data: { results: mockedPatientQueue } }, isLoading: false, error: null } as any;
}
if (url.includes('cashier/billLineItem')) {
return {
data: { data: { results: [{ ...mockedBillResult, paymentStatus: 'POSTED' }] } },
isLoading: false,
error: null,
} as any;
}
});
const { result } = renderHook(() => testResultActionResource.useTestOrderBillStatus('orderUuid', 'patientUuid'));
expect(result.current.hasPendingPayment).toBe(true);
});

test('should return `hasPendingPayment` as true when patient has unsettled payments', () => {
mockedUseConfig.mockReturnValue({
concepts: { emergencyPriorityConceptUuid: '180cd8f8c-5d82-4cdc-b96e-a6addeb94b7f' },
billingStatusQueryUrl: `/ws/rest/v1/cashier/billLineItem?orderUuid={{orderUuid}}&v=full`,
});
mockedUseVisit.mockReturnValue({
currentVisit: { mockCurrentVisit, visitType: { uuid: '13371a4d4-f66f-4454-a86d-92c7b3da990c' } },
isLoading: false,
isValidating: false,
activeVisit: mockCurrentVisit,
} as unknown as any);
mockSWR.mockImplementation((url) => {
if (url.includes('visit-queue-entry')) {
return { data: { data: { results: mockedPatientQueue } }, isLoading: false, error: null } as any;
}
if (url.includes('cashier/billLineItem')) {
return {
data: { data: { results: [{ ...mockedBillResult, paymentStatus: 'PENDING' }] } },
isLoading: false,
error: null,
} as any;
}
});
const { result } = renderHook(() => testResultActionResource.useTestOrderBillStatus('orderUuid', 'patientUuid'));
expect(result.current.hasPendingPayment).toBe(true);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { openmrsFetch, restBaseUrl, useConfig, useVisit } from '@openmrs/esm-framework';
import useSWR from 'swr';
import { LineItem, QueueEntry } from '../../../types';
import { BillingConfig } from '../../../config-schema';

export const useTestOrderBillStatus = (orderUuid: string, patientUuid: string) => {
const config = useConfig<BillingConfig>();
const { currentVisit } = useVisit(patientUuid);
const { isEmergencyPatient, isLoading: isLoadingQueue } = usePatientQueue(patientUuid);
const { isLoading: isLoadingBill, hasPendingPayment } = usePatientBill(orderUuid);

if (isLoadingQueue || isLoadingBill) {
return { hasPendingPayment: false, isLoading: true };
}

// If current visit type is inpatient or the patient is in the emergency queue, we should allow the patient to receive services without paying the bill
if (currentVisit?.visitType?.uuid === config?.inPatientVisitTypeUuid || isEmergencyPatient) {
return { hasPendingPayment: false, isLoading: false };
}

// If the patient is not in the queue then we should check if the patient has a pending bill
return { hasPendingPayment, isLoading: false };
};

export const usePatientQueue = (patientUuid: string) => {
const config = useConfig({ externalModuleName: '@openmrs/esm-service-queues-app' });
const url = `${restBaseUrl}/visit-queue-entry?patient=${patientUuid}`;
const { data, isLoading, error } = useSWR<{
data: { results: Array<QueueEntry> };
}>(url, openmrsFetch);

const isEmergencyPatient =
data?.data?.results?.[0]?.queueEntry?.priority?.uuid === config?.concepts?.emergencyPriorityConceptUuid;
const isInQueue = data?.data?.results?.length > 0;
return { isInQueue, isLoading, error, isEmergencyPatient };
};

export const usePatientBill = (orderUuid: string) => {
const { billingStatusQueryUrl } = useConfig<BillingConfig>();
const billUrl = createUrl(restBaseUrl, orderUuid, billingStatusQueryUrl);
const { data, isLoading, error } = useSWR<{
data: { results: Array<LineItem> };
}>(billUrl, openmrsFetch);

const hasPendingPayment = data?.data?.results?.some(
(lineItem) => lineItem.paymentStatus === 'PENDING' || lineItem.paymentStatus === 'POSTED',
);

return { hasPendingPayment, isLoading, error };
};

function createUrl(restBaseUrl: string, orderUuid: string, templateUrl: string): string {
return templateUrl.replace('${restBaseUrl}', restBaseUrl).replace('${orderUuid}', orderUuid);
}
Loading

0 comments on commit 2eafce2

Please sign in to comment.