@openmrs/esm-billing-app 1.1.1 → 1.1.2-pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/cache/53e233a916ffe7d2-meta.json +1 -0
- package/.turbo/cache/53e233a916ffe7d2.tar.zst +0 -0
- package/.turbo/turbo-build.log +44 -0
- package/__mocks__/bills.mock.ts +6 -5
- package/dist/1119.js +1 -1
- package/dist/1197.js +1 -1
- package/dist/1435.js +1 -0
- package/dist/1435.js.map +1 -0
- package/dist/1807.js +1 -0
- package/dist/1807.js.map +1 -0
- package/dist/2146.js +1 -1
- package/dist/2177.js +1 -1
- package/dist/2177.js.map +1 -1
- package/dist/2690.js +1 -1
- package/dist/2704.js +1 -0
- package/dist/2704.js.map +1 -0
- package/dist/3002.js +1 -0
- package/dist/3002.js.map +1 -0
- package/dist/3041.js +1 -1
- package/dist/3041.js.map +1 -1
- package/dist/3099.js +1 -1
- package/dist/3184.js +1 -1
- package/dist/3184.js.map +1 -1
- package/dist/3584.js +1 -1
- package/dist/4055.js +1 -1
- package/dist/4132.js +1 -1
- package/dist/4225.js +1 -1
- package/dist/4225.js.map +1 -1
- package/dist/4300.js +1 -1
- package/dist/4335.js +1 -1
- package/dist/439.js +1 -1
- package/dist/4618.js +1 -1
- package/dist/4652.js +1 -1
- package/dist/4944.js +1 -1
- package/dist/5173.js +1 -1
- package/dist/5241.js +1 -1
- package/dist/5422.js +1 -1
- package/dist/5422.js.map +1 -1
- package/dist/5442.js +1 -1
- package/dist/5661.js +1 -1
- package/dist/6022.js +1 -1
- package/dist/6404.js +1 -0
- package/dist/6404.js.map +1 -0
- package/dist/6468.js +1 -1
- package/dist/6540.js +1 -1
- package/dist/6540.js.map +1 -1
- package/dist/6589.js +1 -1
- package/dist/6606.js +1 -1
- package/dist/6606.js.map +1 -1
- package/dist/6679.js +1 -1
- package/dist/6792.js +1 -0
- package/dist/6792.js.map +1 -0
- package/dist/6840.js +1 -1
- package/dist/6859.js +1 -1
- package/dist/7097.js +1 -1
- package/dist/7159.js +1 -1
- package/dist/723.js +1 -1
- package/dist/7255.js +1 -1
- package/dist/7255.js.map +1 -1
- package/dist/7617.js +1 -1
- package/dist/795.js +1 -1
- package/dist/8163.js +1 -1
- package/dist/8341.js +2 -0
- package/dist/{1907.js.LICENSE.txt → 8341.js.LICENSE.txt} +0 -15
- package/dist/8341.js.map +1 -0
- package/dist/8349.js +1 -1
- package/dist/8371.js +1 -1
- package/dist/8421.js +1 -0
- package/dist/8421.js.map +1 -0
- package/dist/8618.js +1 -1
- package/dist/890.js +1 -1
- package/dist/9214.js +1 -1
- package/dist/9538.js +1 -1
- package/dist/9569.js +1 -1
- package/dist/961.js +1 -1
- package/dist/961.js.map +1 -1
- package/dist/986.js +1 -1
- package/dist/9879.js +1 -1
- package/dist/9895.js +1 -1
- package/dist/9900.js +1 -1
- package/dist/9913.js +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.LICENSE.txt +0 -15
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-billing-app.js +1 -1
- package/dist/openmrs-esm-billing-app.js.buildmanifest.json +284 -259
- package/dist/openmrs-esm-billing-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/e2e/commands/patient-operations.ts +1 -1
- package/e2e/pages/billing-dashboard-page.ts +3 -1
- package/e2e/pages/billing-form-page.ts +5 -0
- package/e2e/pages/invoice-page.ts +10 -0
- package/e2e/specs/billing-dashboard.spec.ts +126 -3
- package/e2e/specs/billing-patient-chart.spec.ts +95 -9
- package/package.json +3 -6
- package/src/bill-history/bill-action-menu.component.tsx +41 -0
- package/src/bill-history/bill-action-menu.scss +3 -0
- package/src/bill-history/bill-history.component.tsx +15 -5
- package/src/bill-history/bill-history.scss +0 -1
- package/src/bill-history/bill-history.test.tsx +78 -1
- package/src/bill-item-actions/edit-bill-item.modal.tsx +1 -1
- package/src/bill-item-actions/edit-bill-item.test.tsx +40 -0
- package/src/billable-services/bill-waiver/bill-waiver.component.tsx +3 -1
- package/src/billing-dashboard/billing-dashboard.component.tsx +3 -16
- package/src/billing-form/billing-checkin-form.component.tsx +116 -57
- package/src/billing-form/billing-checkin-form.scss +26 -2
- package/src/billing-form/billing-checkin-form.test.tsx +51 -1
- package/src/billing-form/billing-form.resource.test.ts +87 -0
- package/src/billing-form/billing-form.resource.ts +33 -0
- package/src/billing-form/billing-form.scss +54 -7
- package/src/billing-form/billing-form.test.tsx +547 -0
- package/src/billing-form/billing-form.workspace.tsx +150 -45
- package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +25 -2
- package/src/billing-form/visit-attributes/visit-attributes-form.scss +29 -0
- package/src/billing-header/billing-header.component.tsx +1 -34
- package/src/billing-header/billing-header.scss +0 -50
- package/src/billing.resource.test.ts +11 -11
- package/src/billing.resource.ts +42 -12
- package/src/bills-table/bills-table.component.tsx +16 -12
- package/src/bills-table/bills-table.test.tsx +84 -7
- package/src/index.ts +5 -0
- package/src/invoice/invoice.component.tsx +46 -16
- package/src/invoice/invoice.scss +9 -8
- package/src/invoice/invoice.test.tsx +128 -7
- package/src/invoice/line-item-action-menu.component.tsx +2 -2
- package/src/invoice/payments/payments.component.tsx +2 -2
- package/src/invoice/payments/payments.test.tsx +31 -2
- package/src/metrics-cards/metrics.resource.ts +3 -4
- package/src/modal/finalize-bill-confirmation.modal.test.tsx +209 -0
- package/src/modal/finalize-bill-confirmation.modal.tsx +86 -0
- package/src/modal/require-payment.modal.tsx +2 -1
- package/src/routes.json +4 -0
- package/src/types/index.ts +10 -1
- package/tools/setup-tests.ts +7 -6
- package/translations/am.json +28 -0
- package/translations/ar.json +28 -0
- package/translations/ar_SY.json +28 -0
- package/translations/bn.json +28 -0
- package/translations/cs.json +28 -0
- package/translations/de.json +266 -238
- package/translations/en.json +29 -0
- package/translations/en_US.json +28 -0
- package/translations/es.json +28 -0
- package/translations/es_MX.json +28 -0
- package/translations/fr.json +28 -0
- package/translations/he.json +28 -0
- package/translations/hi.json +28 -0
- package/translations/hi_IN.json +28 -0
- package/translations/id.json +28 -0
- package/translations/it.json +28 -0
- package/translations/ka.json +28 -0
- package/translations/km.json +28 -0
- package/translations/ku.json +28 -0
- package/translations/ky.json +28 -0
- package/translations/lg.json +28 -0
- package/translations/ne.json +28 -0
- package/translations/pl.json +28 -0
- package/translations/pt.json +28 -0
- package/translations/pt_BR.json +28 -0
- package/translations/qu.json +28 -0
- package/translations/ro_RO.json +28 -0
- package/translations/ru_RU.json +28 -0
- package/translations/si.json +28 -0
- package/translations/sq.json +28 -0
- package/translations/sw.json +28 -0
- package/translations/sw_KE.json +28 -0
- package/translations/tr.json +28 -0
- package/translations/tr_TR.json +28 -0
- package/translations/uk.json +28 -0
- package/translations/uz.json +28 -0
- package/translations/uz@Latn.json +28 -0
- package/translations/uz_UZ.json +28 -0
- package/translations/vi.json +28 -0
- package/translations/zh.json +268 -240
- package/translations/zh_CN.json +30 -2
- package/translations/zh_TW.json +28 -0
- package/turbo.json +29 -0
- package/dist/1537.js +0 -1
- package/dist/1537.js.map +0 -1
- package/dist/1907.js +0 -2
- package/dist/1907.js.map +0 -1
- package/dist/1981.js +0 -1
- package/dist/1981.js.map +0 -1
- package/dist/2820.js +0 -1
- package/dist/2820.js.map +0 -1
- package/dist/8025.js +0 -1
- package/dist/8025.js.map +0 -1
- package/dist/9727.js +0 -2
- package/dist/9727.js.LICENSE.txt +0 -14
- package/dist/9727.js.map +0 -1
- package/dist/9756.js +0 -1
- package/dist/9756.js.map +0 -1
- package/src/hooks/selectedDateContext.ts +0 -10
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import { getDefaultsFromConfigSchema, showSnackbar, useConfig } from '@openmrs/esm-framework';
|
|
5
|
+
import { configSchema, type BillingConfig } from '../config-schema';
|
|
6
|
+
import { processBillItems, updateBillItems, useBill, useBillableServices } from '../billing.resource';
|
|
7
|
+
import { useBillableServices as useBillableServicesList } from '../billable-services/billable-service.resource';
|
|
8
|
+
import { getBillableServiceUuid } from '../invoice/payments/utils';
|
|
9
|
+
import BillingForm from './billing-form.workspace';
|
|
10
|
+
|
|
11
|
+
const mockUseConfig = jest.mocked(useConfig<BillingConfig>);
|
|
12
|
+
const mockUseBillableServices = jest.mocked(useBillableServices);
|
|
13
|
+
const mockUseBill = jest.mocked(useBill);
|
|
14
|
+
const mockUseBillableServicesList = jest.mocked(useBillableServicesList);
|
|
15
|
+
const mockProcessBillItems = jest.mocked(processBillItems);
|
|
16
|
+
const mockUpdateBillItems = jest.mocked(updateBillItems);
|
|
17
|
+
const mockGetBillableServiceUuid = jest.mocked(getBillableServiceUuid);
|
|
18
|
+
const mockShowSnackbar = jest.mocked(showSnackbar);
|
|
19
|
+
|
|
20
|
+
jest.mock('../billing.resource', () => ({
|
|
21
|
+
processBillItems: jest.fn().mockResolvedValue({}),
|
|
22
|
+
updateBillItems: jest.fn().mockResolvedValue({}),
|
|
23
|
+
useBill: jest.fn(),
|
|
24
|
+
useBillableServices: jest.fn(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
jest.mock('../billable-services/billable-service.resource', () => ({
|
|
28
|
+
useBillableServices: jest.fn(),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.mock('../invoice/payments/utils', () => ({
|
|
32
|
+
getBillableServiceUuid: jest.fn(),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
jest.mock('../helpers/functions', () => ({
|
|
36
|
+
calculateTotalAmount: jest.fn((items) =>
|
|
37
|
+
Array.isArray(items) ? items.reduce((sum, item) => sum + item.price * item.quantity, 0) : 0,
|
|
38
|
+
),
|
|
39
|
+
convertToCurrency: jest.fn((amount) => `KES ${amount}`),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
window.i18next = {
|
|
43
|
+
language: 'en',
|
|
44
|
+
} as any;
|
|
45
|
+
|
|
46
|
+
const mockBillableItems = [
|
|
47
|
+
{
|
|
48
|
+
uuid: 'service-1',
|
|
49
|
+
name: 'Consultation',
|
|
50
|
+
servicePrices: [{ uuid: 'price-1', name: 'Default', price: 500 }],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
uuid: 'service-2',
|
|
54
|
+
name: 'Lab Test',
|
|
55
|
+
servicePrices: [{ uuid: 'price-2', name: 'Default', price: 1000 }],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
uuid: 'service-3',
|
|
59
|
+
name: 'Hemoglobin',
|
|
60
|
+
servicePrices: [{ uuid: 'price-3', name: 'Default', price: 100 }],
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const mockExistingBill = {
|
|
65
|
+
uuid: 'bill-123',
|
|
66
|
+
patientUuid: 'patient-uuid',
|
|
67
|
+
patientName: 'John Doe',
|
|
68
|
+
status: 'PENDING',
|
|
69
|
+
cashPointUuid: 'cashpoint-uuid',
|
|
70
|
+
cashPointName: 'Main Cashier',
|
|
71
|
+
cashPointLocation: 'Main Hospital',
|
|
72
|
+
cashier: { uuid: 'cashier-uuid', display: 'Dr. Smith', links: [] },
|
|
73
|
+
receiptNumber: 'REC-001',
|
|
74
|
+
dateCreated: '2024-01-01',
|
|
75
|
+
lineItems: [
|
|
76
|
+
{
|
|
77
|
+
uuid: 'line-1',
|
|
78
|
+
billableService: 'Hemoglobin',
|
|
79
|
+
item: 'Hemoglobin',
|
|
80
|
+
display: 'Hemoglobin',
|
|
81
|
+
quantity: 1,
|
|
82
|
+
price: 100,
|
|
83
|
+
paymentStatus: 'PENDING',
|
|
84
|
+
lineItemOrder: 0,
|
|
85
|
+
voided: false,
|
|
86
|
+
voidReason: null,
|
|
87
|
+
priceName: '',
|
|
88
|
+
priceUuid: '',
|
|
89
|
+
resourceVersion: '1.8',
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
payments: [],
|
|
93
|
+
totalAmount: 100,
|
|
94
|
+
tenderedAmount: 0,
|
|
95
|
+
billingService: 'Hemoglobin',
|
|
96
|
+
identifier: 'ID-001',
|
|
97
|
+
id: 1,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const closeWorkspace = jest.fn();
|
|
101
|
+
const onMutate = jest.fn();
|
|
102
|
+
|
|
103
|
+
const defaultCreateProps = {
|
|
104
|
+
workspaceProps: { patientUuid: 'patient-uuid', onMutate },
|
|
105
|
+
closeWorkspace,
|
|
106
|
+
} as any;
|
|
107
|
+
|
|
108
|
+
const editModeProps = {
|
|
109
|
+
workspaceProps: { patientUuid: 'patient-uuid', onMutate, billUuid: 'bill-123' },
|
|
110
|
+
closeWorkspace,
|
|
111
|
+
} as any;
|
|
112
|
+
|
|
113
|
+
describe('BillingForm', () => {
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
jest.clearAllMocks();
|
|
116
|
+
mockUseConfig.mockReturnValue({
|
|
117
|
+
...getDefaultsFromConfigSchema(configSchema),
|
|
118
|
+
defaultCurrency: 'KES',
|
|
119
|
+
});
|
|
120
|
+
mockUseBillableServices.mockReturnValue({
|
|
121
|
+
data: mockBillableItems as any,
|
|
122
|
+
error: null,
|
|
123
|
+
isLoading: false,
|
|
124
|
+
} as any);
|
|
125
|
+
mockUseBill.mockReturnValue({
|
|
126
|
+
bill: null,
|
|
127
|
+
error: null,
|
|
128
|
+
isLoading: false,
|
|
129
|
+
isValidating: false,
|
|
130
|
+
mutate: jest.fn(),
|
|
131
|
+
});
|
|
132
|
+
mockUseBillableServicesList.mockReturnValue({
|
|
133
|
+
billableServices: [{ uuid: 'bs-uuid-1', name: 'Hemoglobin' }],
|
|
134
|
+
isLoading: false,
|
|
135
|
+
isValidating: false,
|
|
136
|
+
error: null,
|
|
137
|
+
mutate: jest.fn(),
|
|
138
|
+
} as any);
|
|
139
|
+
mockGetBillableServiceUuid.mockReturnValue('bs-uuid-1');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('Create mode (no billUuid)', () => {
|
|
143
|
+
it('should render the search items combobox', () => {
|
|
144
|
+
render(<BillingForm {...defaultCreateProps} />);
|
|
145
|
+
expect(screen.getByText(/search items and services/i)).toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should not show existing items section', () => {
|
|
149
|
+
render(<BillingForm {...defaultCreateProps} />);
|
|
150
|
+
expect(screen.queryByText(/existing items/i)).not.toBeInTheDocument();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should not show edit mode title in create mode', () => {
|
|
154
|
+
render(<BillingForm {...defaultCreateProps} />);
|
|
155
|
+
expect(screen.queryByText(/add items to bill/i)).not.toBeInTheDocument();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should call processBillItems on submit in create mode', async () => {
|
|
159
|
+
const user = userEvent.setup();
|
|
160
|
+
render(<BillingForm {...defaultCreateProps} />);
|
|
161
|
+
|
|
162
|
+
// Open the combobox and select an item
|
|
163
|
+
const combobox = screen.getByRole('combobox');
|
|
164
|
+
await user.click(combobox);
|
|
165
|
+
await user.click(screen.getByText('Consultation'));
|
|
166
|
+
|
|
167
|
+
// Submit the form
|
|
168
|
+
const submitButton = screen.getByRole('button', { name: /save and close/i });
|
|
169
|
+
await user.click(submitButton);
|
|
170
|
+
|
|
171
|
+
await waitFor(() => {
|
|
172
|
+
expect(mockProcessBillItems).toHaveBeenCalledWith(
|
|
173
|
+
expect.objectContaining({
|
|
174
|
+
patient: 'patient-uuid',
|
|
175
|
+
status: 'PENDING',
|
|
176
|
+
lineItems: expect.arrayContaining([
|
|
177
|
+
expect.objectContaining({
|
|
178
|
+
billableService: 'service-1',
|
|
179
|
+
quantity: 1,
|
|
180
|
+
price: 500,
|
|
181
|
+
paymentStatus: 'PENDING',
|
|
182
|
+
}),
|
|
183
|
+
]),
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should disable submit button when no items are selected', () => {
|
|
190
|
+
render(<BillingForm {...defaultCreateProps} />);
|
|
191
|
+
const submitButton = screen.getByRole('button', { name: /save and close/i });
|
|
192
|
+
expect(submitButton).toBeDisabled();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('Edit mode (with billUuid)', () => {
|
|
197
|
+
beforeEach(() => {
|
|
198
|
+
mockUseBill.mockReturnValue({
|
|
199
|
+
bill: mockExistingBill as any,
|
|
200
|
+
error: null,
|
|
201
|
+
isLoading: false,
|
|
202
|
+
isValidating: false,
|
|
203
|
+
mutate: jest.fn(),
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should show existing line items in read-only format', () => {
|
|
208
|
+
render(<BillingForm {...editModeProps} />);
|
|
209
|
+
expect(screen.getByText(/existing items/i)).toBeInTheDocument();
|
|
210
|
+
expect(screen.getByText('Hemoglobin')).toBeInTheDocument();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should show the "New items" heading', () => {
|
|
214
|
+
render(<BillingForm {...editModeProps} />);
|
|
215
|
+
expect(screen.getByText(/new items/i)).toBeInTheDocument();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should show existing items subtotal', () => {
|
|
219
|
+
render(<BillingForm {...editModeProps} />);
|
|
220
|
+
expect(screen.getByText(/subtotal/i)).toBeInTheDocument();
|
|
221
|
+
expect(screen.getByText('KES 100')).toBeInTheDocument();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should show loading state while bill is loading', () => {
|
|
225
|
+
mockUseBill.mockReturnValue({
|
|
226
|
+
bill: null,
|
|
227
|
+
error: null,
|
|
228
|
+
isLoading: true,
|
|
229
|
+
isValidating: false,
|
|
230
|
+
mutate: jest.fn(),
|
|
231
|
+
});
|
|
232
|
+
render(<BillingForm {...editModeProps} />);
|
|
233
|
+
expect(screen.getByText(/loading\.\.\./i)).toBeInTheDocument();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should show error state when bill fails to load', () => {
|
|
237
|
+
mockUseBill.mockReturnValue({
|
|
238
|
+
bill: null,
|
|
239
|
+
error: new Error('Failed to load'),
|
|
240
|
+
isLoading: false,
|
|
241
|
+
isValidating: false,
|
|
242
|
+
mutate: jest.fn(),
|
|
243
|
+
});
|
|
244
|
+
render(<BillingForm {...editModeProps} />);
|
|
245
|
+
expect(screen.getByText(/error loading bill/i)).toBeInTheDocument();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should call updateBillItems on submit in edit mode', async () => {
|
|
249
|
+
const user = userEvent.setup();
|
|
250
|
+
render(<BillingForm {...editModeProps} />);
|
|
251
|
+
|
|
252
|
+
// Open the combobox and select a new item
|
|
253
|
+
const combobox = screen.getByRole('combobox');
|
|
254
|
+
await user.click(combobox);
|
|
255
|
+
await user.click(screen.getByText('Lab Test'));
|
|
256
|
+
|
|
257
|
+
// Submit the form
|
|
258
|
+
const submitButton = screen.getByRole('button', { name: /save and close/i });
|
|
259
|
+
await user.click(submitButton);
|
|
260
|
+
|
|
261
|
+
await waitFor(() => {
|
|
262
|
+
expect(mockUpdateBillItems).toHaveBeenCalledWith(
|
|
263
|
+
expect.objectContaining({
|
|
264
|
+
uuid: 'bill-123',
|
|
265
|
+
cashPoint: 'cashpoint-uuid',
|
|
266
|
+
cashier: 'cashier-uuid',
|
|
267
|
+
patient: 'patient-uuid',
|
|
268
|
+
status: 'PENDING',
|
|
269
|
+
lineItems: expect.arrayContaining([
|
|
270
|
+
// Existing item with resolved billableService UUID
|
|
271
|
+
expect.objectContaining({
|
|
272
|
+
uuid: 'line-1',
|
|
273
|
+
billableService: 'bs-uuid-1',
|
|
274
|
+
}),
|
|
275
|
+
// New item
|
|
276
|
+
expect.objectContaining({
|
|
277
|
+
billableService: 'service-2',
|
|
278
|
+
quantity: 1,
|
|
279
|
+
price: 1000,
|
|
280
|
+
paymentStatus: 'PENDING',
|
|
281
|
+
}),
|
|
282
|
+
]),
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('should not call processBillItems in edit mode', async () => {
|
|
289
|
+
const user = userEvent.setup();
|
|
290
|
+
render(<BillingForm {...editModeProps} />);
|
|
291
|
+
|
|
292
|
+
const combobox = screen.getByRole('combobox');
|
|
293
|
+
await user.click(combobox);
|
|
294
|
+
await user.click(screen.getByText('Lab Test'));
|
|
295
|
+
|
|
296
|
+
const submitButton = screen.getByRole('button', { name: /save and close/i });
|
|
297
|
+
await user.click(submitButton);
|
|
298
|
+
|
|
299
|
+
await waitFor(() => {
|
|
300
|
+
expect(mockUpdateBillItems).toHaveBeenCalled();
|
|
301
|
+
});
|
|
302
|
+
expect(mockProcessBillItems).not.toHaveBeenCalled();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('should show success snackbar after adding items to bill', async () => {
|
|
306
|
+
const user = userEvent.setup();
|
|
307
|
+
render(<BillingForm {...editModeProps} />);
|
|
308
|
+
|
|
309
|
+
const combobox = screen.getByRole('combobox');
|
|
310
|
+
await user.click(combobox);
|
|
311
|
+
await user.click(screen.getByText('Lab Test'));
|
|
312
|
+
|
|
313
|
+
const submitButton = screen.getByRole('button', { name: /save and close/i });
|
|
314
|
+
await user.click(submitButton);
|
|
315
|
+
|
|
316
|
+
await waitFor(() => {
|
|
317
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
318
|
+
expect.objectContaining({
|
|
319
|
+
title: 'Items added to bill',
|
|
320
|
+
kind: 'success',
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should call onMutate after successful submission', async () => {
|
|
327
|
+
const user = userEvent.setup();
|
|
328
|
+
render(<BillingForm {...editModeProps} />);
|
|
329
|
+
|
|
330
|
+
const combobox = screen.getByRole('combobox');
|
|
331
|
+
await user.click(combobox);
|
|
332
|
+
await user.click(screen.getByText('Lab Test'));
|
|
333
|
+
|
|
334
|
+
const submitButton = screen.getByRole('button', { name: /save and close/i });
|
|
335
|
+
await user.click(submitButton);
|
|
336
|
+
|
|
337
|
+
await waitFor(() => {
|
|
338
|
+
expect(onMutate).toHaveBeenCalled();
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should include existing items total in grand total', async () => {
|
|
343
|
+
const user = userEvent.setup();
|
|
344
|
+
render(<BillingForm {...editModeProps} />);
|
|
345
|
+
|
|
346
|
+
// Add a new item
|
|
347
|
+
const combobox = screen.getByRole('combobox');
|
|
348
|
+
await user.click(combobox);
|
|
349
|
+
await user.click(screen.getByText('Lab Test'));
|
|
350
|
+
|
|
351
|
+
// Grand total should include existing (100) + new (1000) = 1100
|
|
352
|
+
// The convertToCurrency mock formats as "KES <amount>"
|
|
353
|
+
await waitFor(() => {
|
|
354
|
+
expect(screen.getByText(/KES 1100/)).toBeInTheDocument();
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should show error snackbar when updateBillItems fails', async () => {
|
|
359
|
+
mockUpdateBillItems.mockRejectedValueOnce(new Error('Server error'));
|
|
360
|
+
|
|
361
|
+
const user = userEvent.setup();
|
|
362
|
+
render(<BillingForm {...editModeProps} />);
|
|
363
|
+
|
|
364
|
+
const combobox = screen.getByRole('combobox');
|
|
365
|
+
await user.click(combobox);
|
|
366
|
+
await user.click(screen.getByText('Lab Test'));
|
|
367
|
+
|
|
368
|
+
const submitButton = screen.getByRole('button', { name: /save and close/i });
|
|
369
|
+
await user.click(submitButton);
|
|
370
|
+
|
|
371
|
+
await waitFor(() => {
|
|
372
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
373
|
+
expect.objectContaining({
|
|
374
|
+
title: 'Bill processing error',
|
|
375
|
+
kind: 'error',
|
|
376
|
+
subtitle: 'Server error',
|
|
377
|
+
}),
|
|
378
|
+
);
|
|
379
|
+
});
|
|
380
|
+
expect(closeWorkspace).not.toHaveBeenCalled();
|
|
381
|
+
expect(onMutate).not.toHaveBeenCalled();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should exclude existing bill line items from the combobox options', async () => {
|
|
385
|
+
const user = userEvent.setup();
|
|
386
|
+
render(<BillingForm {...editModeProps} />);
|
|
387
|
+
|
|
388
|
+
const combobox = screen.getByRole('combobox');
|
|
389
|
+
await user.click(combobox);
|
|
390
|
+
|
|
391
|
+
// Hemoglobin is already on the bill, so it should not appear in the dropdown
|
|
392
|
+
const options = screen.getAllByRole('option');
|
|
393
|
+
const optionTexts = options.map((option) => option.textContent);
|
|
394
|
+
expect(optionTexts).toContain('Consultation');
|
|
395
|
+
expect(optionTexts).toContain('Lab Test');
|
|
396
|
+
expect(optionTexts).not.toContain('Hemoglobin');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should exclude items matching by the item field when billableService is null', async () => {
|
|
400
|
+
mockUseBill.mockReturnValue({
|
|
401
|
+
bill: {
|
|
402
|
+
...mockExistingBill,
|
|
403
|
+
lineItems: [
|
|
404
|
+
{
|
|
405
|
+
...mockExistingBill.lineItems[0],
|
|
406
|
+
billableService: null,
|
|
407
|
+
item: 'Hemoglobin',
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
} as any,
|
|
411
|
+
error: null,
|
|
412
|
+
isLoading: false,
|
|
413
|
+
isValidating: false,
|
|
414
|
+
mutate: jest.fn(),
|
|
415
|
+
});
|
|
416
|
+
const user = userEvent.setup();
|
|
417
|
+
render(<BillingForm {...editModeProps} />);
|
|
418
|
+
|
|
419
|
+
const combobox = screen.getByRole('combobox');
|
|
420
|
+
await user.click(combobox);
|
|
421
|
+
|
|
422
|
+
const options = screen.getAllByRole('option');
|
|
423
|
+
const optionTexts = options.map((option) => option.textContent);
|
|
424
|
+
expect(optionTexts).not.toContain('Hemoglobin');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('should show all billable items in create mode including those on existing bills', async () => {
|
|
428
|
+
mockUseBill.mockReturnValue({
|
|
429
|
+
bill: null,
|
|
430
|
+
error: null,
|
|
431
|
+
isLoading: false,
|
|
432
|
+
isValidating: false,
|
|
433
|
+
mutate: jest.fn(),
|
|
434
|
+
});
|
|
435
|
+
const user = userEvent.setup();
|
|
436
|
+
render(<BillingForm {...defaultCreateProps} />);
|
|
437
|
+
|
|
438
|
+
const combobox = screen.getByRole('combobox');
|
|
439
|
+
await user.click(combobox);
|
|
440
|
+
|
|
441
|
+
const options = screen.getAllByRole('option');
|
|
442
|
+
const optionTexts = options.map((option) => option.textContent);
|
|
443
|
+
expect(optionTexts).toContain('Consultation');
|
|
444
|
+
expect(optionTexts).toContain('Lab Test');
|
|
445
|
+
expect(optionTexts).toContain('Hemoglobin');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('should resolve service UUID using the item field when billableService is null', async () => {
|
|
449
|
+
mockUseBill.mockReturnValue({
|
|
450
|
+
bill: {
|
|
451
|
+
...mockExistingBill,
|
|
452
|
+
lineItems: [
|
|
453
|
+
{
|
|
454
|
+
...mockExistingBill.lineItems[0],
|
|
455
|
+
billableService: null,
|
|
456
|
+
item: 'Hemoglobin',
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
} as any,
|
|
460
|
+
error: null,
|
|
461
|
+
isLoading: false,
|
|
462
|
+
isValidating: false,
|
|
463
|
+
mutate: jest.fn(),
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const user = userEvent.setup();
|
|
467
|
+
render(<BillingForm {...editModeProps} />);
|
|
468
|
+
|
|
469
|
+
const combobox = screen.getByRole('combobox');
|
|
470
|
+
await user.click(combobox);
|
|
471
|
+
await user.click(screen.getByText('Lab Test'));
|
|
472
|
+
|
|
473
|
+
const submitButton = screen.getByRole('button', { name: /save and close/i });
|
|
474
|
+
await user.click(submitButton);
|
|
475
|
+
|
|
476
|
+
await waitFor(() => {
|
|
477
|
+
expect(mockGetBillableServiceUuid).toHaveBeenCalledWith(expect.anything(), 'Hemoglobin');
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('should disable submit button while billable services list is loading in edit mode', () => {
|
|
482
|
+
mockUseBillableServicesList.mockReturnValue({
|
|
483
|
+
billableServices: [],
|
|
484
|
+
isLoading: true,
|
|
485
|
+
isValidating: false,
|
|
486
|
+
error: null,
|
|
487
|
+
mutate: jest.fn(),
|
|
488
|
+
} as any);
|
|
489
|
+
|
|
490
|
+
render(<BillingForm {...editModeProps} />);
|
|
491
|
+
const submitButton = screen.getByRole('button', { name: /save and close/i });
|
|
492
|
+
expect(submitButton).toBeDisabled();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('should show error notification when billable services list fails to load in edit mode', () => {
|
|
496
|
+
mockUseBillableServicesList.mockReturnValue({
|
|
497
|
+
billableServices: [],
|
|
498
|
+
isLoading: false,
|
|
499
|
+
isValidating: false,
|
|
500
|
+
error: new Error('Failed to load services'),
|
|
501
|
+
mutate: jest.fn(),
|
|
502
|
+
} as any);
|
|
503
|
+
|
|
504
|
+
render(<BillingForm {...editModeProps} />);
|
|
505
|
+
expect(screen.getByText(/error loading billable services/i)).toBeInTheDocument();
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('should not submit when billable services list has an error in edit mode', async () => {
|
|
509
|
+
mockUseBillableServicesList.mockReturnValue({
|
|
510
|
+
billableServices: [],
|
|
511
|
+
isLoading: false,
|
|
512
|
+
isValidating: false,
|
|
513
|
+
error: new Error('Failed to load services'),
|
|
514
|
+
mutate: jest.fn(),
|
|
515
|
+
} as any);
|
|
516
|
+
|
|
517
|
+
// The error notification replaces the form content, so we can't select items.
|
|
518
|
+
// Verify that updateBillItems is never called.
|
|
519
|
+
render(<BillingForm {...editModeProps} />);
|
|
520
|
+
expect(mockUpdateBillItems).not.toHaveBeenCalled();
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('should show error when billable service UUID cannot be resolved', async () => {
|
|
524
|
+
mockGetBillableServiceUuid.mockReturnValue(null);
|
|
525
|
+
|
|
526
|
+
const user = userEvent.setup();
|
|
527
|
+
render(<BillingForm {...editModeProps} />);
|
|
528
|
+
|
|
529
|
+
const combobox = screen.getByRole('combobox');
|
|
530
|
+
await user.click(combobox);
|
|
531
|
+
await user.click(screen.getByText('Lab Test'));
|
|
532
|
+
|
|
533
|
+
const submitButton = screen.getByRole('button', { name: /save and close/i });
|
|
534
|
+
await user.click(submitButton);
|
|
535
|
+
|
|
536
|
+
await waitFor(() => {
|
|
537
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith(
|
|
538
|
+
expect.objectContaining({
|
|
539
|
+
title: 'Bill processing error',
|
|
540
|
+
kind: 'error',
|
|
541
|
+
}),
|
|
542
|
+
);
|
|
543
|
+
});
|
|
544
|
+
expect(mockUpdateBillItems).not.toHaveBeenCalled();
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
});
|