@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.
Files changed (193) hide show
  1. package/.turbo/cache/53e233a916ffe7d2-meta.json +1 -0
  2. package/.turbo/cache/53e233a916ffe7d2.tar.zst +0 -0
  3. package/.turbo/turbo-build.log +44 -0
  4. package/__mocks__/bills.mock.ts +6 -5
  5. package/dist/1119.js +1 -1
  6. package/dist/1197.js +1 -1
  7. package/dist/1435.js +1 -0
  8. package/dist/1435.js.map +1 -0
  9. package/dist/1807.js +1 -0
  10. package/dist/1807.js.map +1 -0
  11. package/dist/2146.js +1 -1
  12. package/dist/2177.js +1 -1
  13. package/dist/2177.js.map +1 -1
  14. package/dist/2690.js +1 -1
  15. package/dist/2704.js +1 -0
  16. package/dist/2704.js.map +1 -0
  17. package/dist/3002.js +1 -0
  18. package/dist/3002.js.map +1 -0
  19. package/dist/3041.js +1 -1
  20. package/dist/3041.js.map +1 -1
  21. package/dist/3099.js +1 -1
  22. package/dist/3184.js +1 -1
  23. package/dist/3184.js.map +1 -1
  24. package/dist/3584.js +1 -1
  25. package/dist/4055.js +1 -1
  26. package/dist/4132.js +1 -1
  27. package/dist/4225.js +1 -1
  28. package/dist/4225.js.map +1 -1
  29. package/dist/4300.js +1 -1
  30. package/dist/4335.js +1 -1
  31. package/dist/439.js +1 -1
  32. package/dist/4618.js +1 -1
  33. package/dist/4652.js +1 -1
  34. package/dist/4944.js +1 -1
  35. package/dist/5173.js +1 -1
  36. package/dist/5241.js +1 -1
  37. package/dist/5422.js +1 -1
  38. package/dist/5422.js.map +1 -1
  39. package/dist/5442.js +1 -1
  40. package/dist/5661.js +1 -1
  41. package/dist/6022.js +1 -1
  42. package/dist/6404.js +1 -0
  43. package/dist/6404.js.map +1 -0
  44. package/dist/6468.js +1 -1
  45. package/dist/6540.js +1 -1
  46. package/dist/6540.js.map +1 -1
  47. package/dist/6589.js +1 -1
  48. package/dist/6606.js +1 -1
  49. package/dist/6606.js.map +1 -1
  50. package/dist/6679.js +1 -1
  51. package/dist/6792.js +1 -0
  52. package/dist/6792.js.map +1 -0
  53. package/dist/6840.js +1 -1
  54. package/dist/6859.js +1 -1
  55. package/dist/7097.js +1 -1
  56. package/dist/7159.js +1 -1
  57. package/dist/723.js +1 -1
  58. package/dist/7255.js +1 -1
  59. package/dist/7255.js.map +1 -1
  60. package/dist/7617.js +1 -1
  61. package/dist/795.js +1 -1
  62. package/dist/8163.js +1 -1
  63. package/dist/8341.js +2 -0
  64. package/dist/{1907.js.LICENSE.txt → 8341.js.LICENSE.txt} +0 -15
  65. package/dist/8341.js.map +1 -0
  66. package/dist/8349.js +1 -1
  67. package/dist/8371.js +1 -1
  68. package/dist/8421.js +1 -0
  69. package/dist/8421.js.map +1 -0
  70. package/dist/8618.js +1 -1
  71. package/dist/890.js +1 -1
  72. package/dist/9214.js +1 -1
  73. package/dist/9538.js +1 -1
  74. package/dist/9569.js +1 -1
  75. package/dist/961.js +1 -1
  76. package/dist/961.js.map +1 -1
  77. package/dist/986.js +1 -1
  78. package/dist/9879.js +1 -1
  79. package/dist/9895.js +1 -1
  80. package/dist/9900.js +1 -1
  81. package/dist/9913.js +1 -1
  82. package/dist/main.js +1 -1
  83. package/dist/main.js.LICENSE.txt +0 -15
  84. package/dist/main.js.map +1 -1
  85. package/dist/openmrs-esm-billing-app.js +1 -1
  86. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +284 -259
  87. package/dist/openmrs-esm-billing-app.js.map +1 -1
  88. package/dist/routes.json +1 -1
  89. package/e2e/commands/patient-operations.ts +1 -1
  90. package/e2e/pages/billing-dashboard-page.ts +3 -1
  91. package/e2e/pages/billing-form-page.ts +5 -0
  92. package/e2e/pages/invoice-page.ts +10 -0
  93. package/e2e/specs/billing-dashboard.spec.ts +126 -3
  94. package/e2e/specs/billing-patient-chart.spec.ts +95 -9
  95. package/package.json +3 -6
  96. package/src/bill-history/bill-action-menu.component.tsx +41 -0
  97. package/src/bill-history/bill-action-menu.scss +3 -0
  98. package/src/bill-history/bill-history.component.tsx +15 -5
  99. package/src/bill-history/bill-history.scss +0 -1
  100. package/src/bill-history/bill-history.test.tsx +78 -1
  101. package/src/bill-item-actions/edit-bill-item.modal.tsx +1 -1
  102. package/src/bill-item-actions/edit-bill-item.test.tsx +40 -0
  103. package/src/billable-services/bill-waiver/bill-waiver.component.tsx +3 -1
  104. package/src/billing-dashboard/billing-dashboard.component.tsx +3 -16
  105. package/src/billing-form/billing-checkin-form.component.tsx +116 -57
  106. package/src/billing-form/billing-checkin-form.scss +26 -2
  107. package/src/billing-form/billing-checkin-form.test.tsx +51 -1
  108. package/src/billing-form/billing-form.resource.test.ts +87 -0
  109. package/src/billing-form/billing-form.resource.ts +33 -0
  110. package/src/billing-form/billing-form.scss +54 -7
  111. package/src/billing-form/billing-form.test.tsx +547 -0
  112. package/src/billing-form/billing-form.workspace.tsx +150 -45
  113. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +25 -2
  114. package/src/billing-form/visit-attributes/visit-attributes-form.scss +29 -0
  115. package/src/billing-header/billing-header.component.tsx +1 -34
  116. package/src/billing-header/billing-header.scss +0 -50
  117. package/src/billing.resource.test.ts +11 -11
  118. package/src/billing.resource.ts +42 -12
  119. package/src/bills-table/bills-table.component.tsx +16 -12
  120. package/src/bills-table/bills-table.test.tsx +84 -7
  121. package/src/index.ts +5 -0
  122. package/src/invoice/invoice.component.tsx +46 -16
  123. package/src/invoice/invoice.scss +9 -8
  124. package/src/invoice/invoice.test.tsx +128 -7
  125. package/src/invoice/line-item-action-menu.component.tsx +2 -2
  126. package/src/invoice/payments/payments.component.tsx +2 -2
  127. package/src/invoice/payments/payments.test.tsx +31 -2
  128. package/src/metrics-cards/metrics.resource.ts +3 -4
  129. package/src/modal/finalize-bill-confirmation.modal.test.tsx +209 -0
  130. package/src/modal/finalize-bill-confirmation.modal.tsx +86 -0
  131. package/src/modal/require-payment.modal.tsx +2 -1
  132. package/src/routes.json +4 -0
  133. package/src/types/index.ts +10 -1
  134. package/tools/setup-tests.ts +7 -6
  135. package/translations/am.json +28 -0
  136. package/translations/ar.json +28 -0
  137. package/translations/ar_SY.json +28 -0
  138. package/translations/bn.json +28 -0
  139. package/translations/cs.json +28 -0
  140. package/translations/de.json +266 -238
  141. package/translations/en.json +29 -0
  142. package/translations/en_US.json +28 -0
  143. package/translations/es.json +28 -0
  144. package/translations/es_MX.json +28 -0
  145. package/translations/fr.json +28 -0
  146. package/translations/he.json +28 -0
  147. package/translations/hi.json +28 -0
  148. package/translations/hi_IN.json +28 -0
  149. package/translations/id.json +28 -0
  150. package/translations/it.json +28 -0
  151. package/translations/ka.json +28 -0
  152. package/translations/km.json +28 -0
  153. package/translations/ku.json +28 -0
  154. package/translations/ky.json +28 -0
  155. package/translations/lg.json +28 -0
  156. package/translations/ne.json +28 -0
  157. package/translations/pl.json +28 -0
  158. package/translations/pt.json +28 -0
  159. package/translations/pt_BR.json +28 -0
  160. package/translations/qu.json +28 -0
  161. package/translations/ro_RO.json +28 -0
  162. package/translations/ru_RU.json +28 -0
  163. package/translations/si.json +28 -0
  164. package/translations/sq.json +28 -0
  165. package/translations/sw.json +28 -0
  166. package/translations/sw_KE.json +28 -0
  167. package/translations/tr.json +28 -0
  168. package/translations/tr_TR.json +28 -0
  169. package/translations/uk.json +28 -0
  170. package/translations/uz.json +28 -0
  171. package/translations/uz@Latn.json +28 -0
  172. package/translations/uz_UZ.json +28 -0
  173. package/translations/vi.json +28 -0
  174. package/translations/zh.json +268 -240
  175. package/translations/zh_CN.json +30 -2
  176. package/translations/zh_TW.json +28 -0
  177. package/turbo.json +29 -0
  178. package/dist/1537.js +0 -1
  179. package/dist/1537.js.map +0 -1
  180. package/dist/1907.js +0 -2
  181. package/dist/1907.js.map +0 -1
  182. package/dist/1981.js +0 -1
  183. package/dist/1981.js.map +0 -1
  184. package/dist/2820.js +0 -1
  185. package/dist/2820.js.map +0 -1
  186. package/dist/8025.js +0 -1
  187. package/dist/8025.js.map +0 -1
  188. package/dist/9727.js +0 -2
  189. package/dist/9727.js.LICENSE.txt +0 -14
  190. package/dist/9727.js.map +0 -1
  191. package/dist/9756.js +0 -1
  192. package/dist/9756.js.map +0 -1
  193. 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
+ });