@openmrs/esm-billing-app 1.0.2-pre.764 → 1.0.2-pre.768

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.
@@ -1,68 +1,51 @@
1
1
  import React from 'react';
2
2
  import userEvent from '@testing-library/user-event';
3
- import { screen, render } from '@testing-library/react';
3
+ import { render, screen, waitFor } from '@testing-library/react';
4
4
  import { useReactToPrint } from 'react-to-print';
5
5
  import { getDefaultsFromConfigSchema, useConfig, usePatient } from '@openmrs/esm-framework';
6
6
  import { configSchema, type BillingConfig } from '../config-schema';
7
7
  import { mockBill, mockPatient } from '../../__mocks__/bills.mock';
8
- import { useBill, processBillPayment } from '../billing.resource';
8
+ import { useBill } from '../billing.resource';
9
9
  import { usePaymentModes } from './payments/payment.resource';
10
10
  import Invoice from './invoice.component';
11
11
 
12
12
  const mockUseConfig = jest.mocked(useConfig<BillingConfig>);
13
+ const mockUseBill = jest.mocked(useBill);
14
+ const mockUsePatient = jest.mocked(usePatient);
15
+ const mockUsePaymentModes = jest.mocked(usePaymentModes);
16
+ const mockUseReactToPrint = jest.mocked(useReactToPrint);
13
17
 
14
- // Mock convertToCurrency
15
18
  jest.mock('../helpers/functions', () => ({
16
19
  convertToCurrency: jest.fn((amount) => `USD ${amount}`),
17
20
  }));
18
21
 
19
- // Set window.i18next
20
22
  window.i18next = {
21
23
  language: 'en',
22
24
  } as any;
23
25
 
24
- // Mock InvoiceTable component
25
- jest.mock('./invoice-table.component', () =>
26
- jest.fn(({ bill }) => <div data-testid="mock-invoice-table">Invoice Table Mock</div>),
27
- );
28
-
29
- // Mock payments component
30
- jest.mock('./payments/payments.component', () =>
31
- jest.fn(({ bill, mutate, selectedLineItems }) => (
32
- <div data-testid="mock-payments">
33
- <h2>Payments</h2>
34
- <button>Add payment option</button>
35
- </div>
36
- )),
37
- );
38
-
39
- // Mock PrintReceipt component
40
26
  jest.mock('./printable-invoice/print-receipt.component', () =>
41
- jest.fn(({ billId }) => <div data-testid="mock-print-receipt">Print Receipt Mock</div>),
27
+ jest.fn(() => <div data-testid="mock-print-receipt">Print Receipt Mock</div>),
42
28
  );
43
29
 
44
- // Mock PrintableInvoice component
45
30
  jest.mock('./printable-invoice/printable-invoice.component', () =>
46
- jest.fn(({ bill, patient }) => <div data-testid="mock-printable-invoice">Printable Invoice Mock</div>),
31
+ jest.fn(() => <div data-testid="mock-printable-invoice">Printable Invoice Mock</div>),
47
32
  );
48
33
 
49
- // Mock payment resource
50
34
  jest.mock('./payments/payment.resource', () => ({
51
35
  usePaymentModes: jest.fn(),
52
36
  updateBillVisitAttribute: jest.fn(),
53
37
  }));
54
38
 
55
- // Mock billing resource
56
39
  jest.mock('../billing.resource', () => ({
57
40
  useBill: jest.fn(),
58
- processBillPayment: jest.fn(),
59
41
  useDefaultFacility: jest.fn().mockReturnValue({
60
- uuid: '54065383-b4d4-42d2-af4d-d250a1fd2590',
61
- display: 'MTRH',
42
+ data: {
43
+ uuid: '54065383-b4d4-42d2-af4d-d250a1fd2590',
44
+ display: 'MTRH',
45
+ },
62
46
  }),
63
47
  }));
64
48
 
65
- // Mock react-router-dom
66
49
  jest.mock('react-router-dom', () => ({
67
50
  useParams: jest.fn().mockReturnValue({
68
51
  patientUuid: 'patientUuid',
@@ -70,18 +53,11 @@ jest.mock('react-router-dom', () => ({
70
53
  }),
71
54
  }));
72
55
 
73
- // Mock react-to-print
74
56
  jest.mock('react-to-print', () => ({
75
57
  useReactToPrint: jest.fn(),
76
58
  }));
77
59
 
78
60
  describe('Invoice', () => {
79
- const mockedBill = useBill as jest.Mock;
80
- const mockedPatient = usePatient as jest.Mock;
81
- const mockedProcessBillPayment = processBillPayment as jest.Mock;
82
- const mockedUsePaymentModes = usePaymentModes as jest.Mock;
83
- const mockedUseReactToPrint = useReactToPrint as jest.Mock;
84
-
85
61
  const defaultBillData = {
86
62
  ...mockBill,
87
63
  uuid: 'test-uuid',
@@ -97,12 +73,20 @@ describe('Invoice', () => {
97
73
  quantity: 1,
98
74
  price: 1000,
99
75
  paymentStatus: 'PENDING',
76
+ billableService: 'Test Service',
77
+ display: '',
78
+ voided: false,
79
+ voidReason: '',
80
+ priceName: '',
81
+ priceUuid: '',
82
+ lineItemOrder: 0,
83
+ resourceVersion: '',
100
84
  },
101
85
  ],
102
86
  };
103
87
 
104
88
  beforeEach(() => {
105
- mockedBill.mockReturnValue({
89
+ mockUseBill.mockReturnValue({
106
90
  bill: defaultBillData,
107
91
  isLoading: false,
108
92
  error: null,
@@ -110,15 +94,14 @@ describe('Invoice', () => {
110
94
  mutate: jest.fn(),
111
95
  });
112
96
 
113
- mockedPatient.mockReturnValue({
114
- patient: mockPatient,
97
+ mockUsePatient.mockReturnValue({
98
+ patient: mockPatient as any,
115
99
  isLoading: false,
116
100
  error: null,
117
- isValidating: false,
118
- mutate: jest.fn(),
101
+ patientUuid: 'patientUuid',
119
102
  });
120
103
 
121
- mockedUsePaymentModes.mockReturnValue({
104
+ mockUsePaymentModes.mockReturnValue({
122
105
  paymentModes: [
123
106
  { uuid: 'cash-uuid', name: 'Cash', description: 'Cash Method', retired: false },
124
107
  { uuid: 'mpesa-uuid', name: 'MPESA', description: 'MPESA Method', retired: false },
@@ -130,42 +113,75 @@ describe('Invoice', () => {
130
113
 
131
114
  mockUseConfig.mockReturnValue({ ...getDefaultsFromConfigSchema(configSchema), defaultCurrency: 'USD' });
132
115
 
133
- // Setup print handler mock
134
116
  const printHandler = jest.fn();
135
- mockedUseReactToPrint.mockReturnValue(printHandler);
117
+ mockUseReactToPrint.mockReturnValue(printHandler);
118
+ });
119
+
120
+ it('should render loading state when bill is loading', () => {
121
+ mockUseBill.mockReturnValue({
122
+ bill: null,
123
+ isLoading: true,
124
+ error: null,
125
+ isValidating: false,
126
+ mutate: jest.fn(),
127
+ });
128
+
129
+ render(<Invoice />);
130
+ expect(screen.getByText(/loading bill information/i)).toBeInTheDocument();
131
+ });
132
+
133
+ it('should render loading state when patient is loading', () => {
134
+ mockUsePatient.mockReturnValue({
135
+ patient: null as any,
136
+ isLoading: true,
137
+ error: null,
138
+ patientUuid: 'patientUuid',
139
+ });
140
+
141
+ render(<Invoice />);
142
+ expect(screen.getByText(/loading bill information/i)).toBeInTheDocument();
136
143
  });
137
144
 
138
- it('should render error state correctly', () => {
139
- mockedBill.mockReturnValue({
145
+ it('should render error state when bill fails to load', () => {
146
+ mockUseBill.mockReturnValue({
140
147
  bill: null,
141
148
  isLoading: false,
142
- error: new Error('Test error'),
149
+ error: new Error('Failed to load bill'),
143
150
  isValidating: false,
144
151
  mutate: jest.fn(),
145
152
  });
146
153
 
147
154
  render(<Invoice />);
148
- expect(screen.getByText(/Invoice error/i)).toBeInTheDocument();
149
- expect(screen.getByText(/Error/)).toBeInTheDocument();
155
+ expect(screen.getByText(/invoice error/i)).toBeInTheDocument();
150
156
  });
151
157
 
152
158
  it('should render invoice details correctly', () => {
153
159
  render(<Invoice />);
154
160
 
155
- // Check invoice details
156
- expect(screen.getByText(/Total Amount/i)).toBeInTheDocument();
157
- expect(screen.getByText(/Amount Tendered/i)).toBeInTheDocument();
158
- expect(screen.getByText(/Invoice Number/i)).toBeInTheDocument();
159
- expect(screen.getByText(/Date And Time/i)).toBeInTheDocument();
160
- expect(screen.getByText(/Invoice Status/i)).toBeInTheDocument();
161
+ expect(screen.getAllByText(/total amount/i).length).toBeGreaterThan(0);
162
+ expect(screen.getAllByText(/amount tendered/i).length).toBeGreaterThan(0);
163
+ expect(screen.getByText(/invoice number/i)).toBeInTheDocument();
164
+ expect(screen.getByText(/date and time/i)).toBeInTheDocument();
165
+ expect(screen.getByText(/invoice status/i)).toBeInTheDocument();
166
+ expect(screen.getAllByText('RCPT-001').length).toBeGreaterThan(0);
167
+ expect(screen.getAllByText('PENDING').length).toBeGreaterThan(0);
168
+ });
169
+
170
+ it('should render invoice table with line items', () => {
171
+ render(<Invoice />);
161
172
 
162
- // Check mock components
163
- expect(screen.getByTestId('mock-invoice-table')).toBeInTheDocument();
164
- expect(screen.getByTestId('mock-payments')).toBeInTheDocument();
173
+ expect(screen.getByText(/line items/i)).toBeInTheDocument();
174
+ expect(screen.getByText('Test Service')).toBeInTheDocument();
175
+ });
176
+
177
+ it('should render payments section', () => {
178
+ render(<Invoice />);
179
+
180
+ expect(screen.getByText(/payments/i)).toBeInTheDocument();
165
181
  });
166
182
 
167
183
  it('should show print receipt button for paid bills', () => {
168
- mockedBill.mockReturnValue({
184
+ mockUseBill.mockReturnValue({
169
185
  bill: {
170
186
  ...defaultBillData,
171
187
  status: 'PAID',
@@ -181,66 +197,280 @@ describe('Invoice', () => {
181
197
  expect(screen.getByTestId('mock-print-receipt')).toBeInTheDocument();
182
198
  });
183
199
 
184
- it('should handle bill payment processing', async () => {
185
- const user = userEvent.setup();
186
- const mockMutate = jest.fn();
187
-
188
- mockedBill.mockReturnValue({
189
- bill: defaultBillData,
200
+ it('should show print receipt button for bills with tendered amount', () => {
201
+ mockUseBill.mockReturnValue({
202
+ bill: {
203
+ ...defaultBillData,
204
+ status: 'PENDING',
205
+ tenderedAmount: 500,
206
+ },
190
207
  isLoading: false,
191
208
  error: null,
192
209
  isValidating: false,
193
- mutate: mockMutate,
210
+ mutate: jest.fn(),
194
211
  });
195
212
 
196
- mockedProcessBillPayment.mockResolvedValue({});
213
+ render(<Invoice />);
214
+ expect(screen.getByTestId('mock-print-receipt')).toBeInTheDocument();
215
+ });
216
+
217
+ it('should not show print receipt button for unpaid bills', () => {
218
+ render(<Invoice />);
219
+ expect(screen.queryByTestId('mock-print-receipt')).not.toBeInTheDocument();
220
+ });
221
+
222
+ it('should handle print button click', async () => {
223
+ const handlePrintMock = jest.fn();
224
+ const user = userEvent.setup();
225
+ mockUseReactToPrint.mockReturnValue(handlePrintMock);
197
226
 
198
227
  render(<Invoice />);
199
228
 
200
- // Add payment flow would go here
201
- // Note: Detailed payment interaction testing should be in the Payments component tests
229
+ const printButton = screen.getByRole('button', { name: /print bill/i });
230
+ await user.click(printButton);
202
231
 
203
- expect(screen.getByText(/Payments/i)).toBeInTheDocument();
232
+ await waitFor(() => {
233
+ expect(handlePrintMock).toHaveBeenCalled();
234
+ });
204
235
  });
205
236
 
206
- it('should update line items when bill data changes', () => {
207
- const { rerender } = render(<Invoice />);
237
+ it('should disable print button while printing', () => {
238
+ render(<Invoice />);
208
239
 
209
- // Update bill with new line items
210
- const updatedBill = {
240
+ const printButton = screen.getByRole('button', { name: /print bill/i });
241
+ expect(printButton).toBeEnabled();
242
+ });
243
+
244
+ it('should render patient header when patient data is available', () => {
245
+ render(<Invoice />);
246
+
247
+ // Patient header is rendered via ExtensionSlot
248
+ expect(screen.getByText(/line items/i)).toBeInTheDocument();
249
+ });
250
+
251
+ it('should search and filter line items in the table', async () => {
252
+ const billWithMultipleItems = {
211
253
  ...defaultBillData,
212
254
  lineItems: [
213
- ...defaultBillData.lineItems,
255
+ {
256
+ uuid: 'item-1',
257
+ item: 'Lab Test',
258
+ quantity: 1,
259
+ price: 500,
260
+ paymentStatus: 'PENDING',
261
+ billableService: 'Lab Test',
262
+ display: '',
263
+ voided: false,
264
+ voidReason: '',
265
+ priceName: '',
266
+ priceUuid: '',
267
+ lineItemOrder: 0,
268
+ resourceVersion: '',
269
+ },
214
270
  {
215
271
  uuid: 'item-2',
216
- item: 'New Service',
272
+ item: 'X-Ray',
217
273
  quantity: 1,
218
274
  price: 500,
219
275
  paymentStatus: 'PENDING',
276
+ billableService: 'X-Ray',
277
+ display: '',
278
+ voided: false,
279
+ voidReason: '',
280
+ priceName: '',
281
+ priceUuid: '',
282
+ lineItemOrder: 1,
283
+ resourceVersion: '',
220
284
  },
221
285
  ],
222
286
  };
223
287
 
224
- mockedBill.mockReturnValue({
225
- bill: updatedBill,
288
+ mockUseBill.mockReturnValue({
289
+ bill: billWithMultipleItems,
226
290
  isLoading: false,
227
291
  error: null,
228
292
  isValidating: false,
229
293
  mutate: jest.fn(),
230
294
  });
231
295
 
296
+ const user = userEvent.setup();
297
+ render(<Invoice />);
298
+
299
+ expect(screen.getByText('Lab Test')).toBeInTheDocument();
300
+ expect(screen.getByText('X-Ray')).toBeInTheDocument();
301
+
302
+ const searchInput = screen.getByPlaceholderText(/search this table/i);
303
+ await user.type(searchInput, 'Lab Test');
304
+
305
+ await waitFor(() => {
306
+ expect(screen.getByText('Lab Test')).toBeInTheDocument();
307
+ expect(screen.queryByText('X-Ray')).not.toBeInTheDocument();
308
+ });
309
+ });
310
+
311
+ it('should handle bill data updates via mutate', () => {
312
+ const mockMutate = jest.fn();
313
+ mockUseBill.mockReturnValue({
314
+ bill: defaultBillData,
315
+ isLoading: false,
316
+ error: null,
317
+ isValidating: false,
318
+ mutate: mockMutate,
319
+ });
320
+
321
+ const { rerender } = render(<Invoice />);
322
+
323
+ const updatedBill = {
324
+ ...defaultBillData,
325
+ status: 'PAID',
326
+ tenderedAmount: 1000,
327
+ };
328
+
329
+ mockUseBill.mockReturnValue({
330
+ bill: updatedBill,
331
+ isLoading: false,
332
+ error: null,
333
+ isValidating: false,
334
+ mutate: mockMutate,
335
+ });
336
+
232
337
  rerender(<Invoice />);
233
338
 
234
- // The mock invoice table should receive updated props
235
- expect(screen.getByTestId('mock-invoice-table')).toBeInTheDocument();
339
+ expect(screen.getByText('PAID')).toBeInTheDocument();
340
+ });
341
+
342
+ it('should display correct currency formatting', () => {
343
+ render(<Invoice />);
344
+
345
+ // convertToCurrency is mocked to return "USD ${amount}"
346
+ expect(screen.getAllByText('USD 1000').length).toBeGreaterThan(0);
347
+ expect(screen.getAllByText('USD 0').length).toBeGreaterThan(0);
348
+ });
349
+
350
+ it('should disable print button when isPrinting state is true', () => {
351
+ // Mock isPrinting state by checking the button's disabled state when loading
352
+ mockUseBill.mockReturnValue({
353
+ bill: defaultBillData,
354
+ isLoading: true,
355
+ error: null,
356
+ isValidating: false,
357
+ mutate: jest.fn(),
358
+ });
359
+
360
+ render(<Invoice />);
361
+ // When bill is loading, component shows loading state, not the button
362
+ expect(screen.getByText(/loading bill information/i)).toBeInTheDocument();
363
+ });
364
+
365
+ it('should not render PrintableInvoice when bill is missing', () => {
366
+ mockUseBill.mockReturnValue({
367
+ bill: null,
368
+ isLoading: false,
369
+ error: null,
370
+ isValidating: false,
371
+ mutate: jest.fn(),
372
+ });
373
+
374
+ mockUsePatient.mockReturnValue({
375
+ patient: mockPatient as any,
376
+ isLoading: false,
377
+ error: null,
378
+ patientUuid: 'patientUuid',
379
+ });
380
+
381
+ render(<Invoice />);
382
+
383
+ // PrintableInvoice should not be rendered when bill is null
384
+ // Since it's in a hidden div, we can't easily assert its absence
385
+ // but we can verify the main content doesn't have the print container
386
+ expect(screen.queryByTestId('mock-printable-invoice')).not.toBeInTheDocument();
387
+ });
388
+
389
+ it('should not render PrintableInvoice when patient is missing', () => {
390
+ mockUsePatient.mockReturnValue({
391
+ patient: null as any,
392
+ isLoading: false,
393
+ error: null,
394
+ patientUuid: 'patientUuid',
395
+ });
396
+
397
+ render(<Invoice />);
398
+
399
+ // PrintableInvoice requires both bill and patient
400
+ expect(screen.queryByTestId('mock-printable-invoice')).not.toBeInTheDocument();
401
+ });
402
+
403
+ it('should render PrintableInvoice when both bill and patient exist', () => {
404
+ render(<Invoice />);
405
+
406
+ // PrintableInvoice should be rendered with both bill and patient
407
+ expect(screen.getByTestId('mock-printable-invoice')).toBeInTheDocument();
408
+ });
409
+
410
+ it('should pass correct props to InvoiceTable', () => {
411
+ render(<Invoice />);
412
+
413
+ // Verify InvoiceTable is rendered with line items
414
+ expect(screen.getByText('Test Service')).toBeInTheDocument();
415
+ expect(screen.getByText(/line items/i)).toBeInTheDocument();
236
416
  });
237
417
 
238
- it('should show patient information correctly', () => {
418
+ it('should pass mutate function to Payments component', () => {
419
+ const mockMutate = jest.fn();
420
+ mockUseBill.mockReturnValue({
421
+ bill: defaultBillData,
422
+ isLoading: false,
423
+ error: null,
424
+ isValidating: false,
425
+ mutate: mockMutate,
426
+ });
427
+
239
428
  render(<Invoice />);
240
- // Check that the invoice details are rendered
241
- expect(screen.getByText('Invoice Number')).toBeInTheDocument();
242
- expect(screen.getByText('RCPT-001')).toBeInTheDocument();
429
+
430
+ // Payments component should be rendered
431
+ expect(screen.getByText(/payments/i)).toBeInTheDocument();
243
432
  });
244
433
 
245
- // Add more test cases as needed for specific features or edge cases
434
+ it('should show print receipt for bills with partial payment', () => {
435
+ mockUseBill.mockReturnValue({
436
+ bill: {
437
+ ...defaultBillData,
438
+ status: 'PENDING',
439
+ totalAmount: 1000,
440
+ tenderedAmount: 500, // Partial payment
441
+ },
442
+ isLoading: false,
443
+ error: null,
444
+ isValidating: false,
445
+ mutate: jest.fn(),
446
+ });
447
+
448
+ render(<Invoice />);
449
+ expect(screen.getByTestId('mock-print-receipt')).toBeInTheDocument();
450
+ });
451
+
452
+ it('should render ExtensionSlot when patient and patientUuid exist', () => {
453
+ render(<Invoice />);
454
+
455
+ // The component renders, which includes the ExtensionSlot
456
+ // We can verify this indirectly by checking the main content is present
457
+ expect(screen.getByText(/invoice number/i)).toBeInTheDocument();
458
+ });
459
+
460
+ it('should not show print receipt for bills with zero tendered amount', () => {
461
+ mockUseBill.mockReturnValue({
462
+ bill: {
463
+ ...defaultBillData,
464
+ status: 'PENDING',
465
+ tenderedAmount: 0,
466
+ },
467
+ isLoading: false,
468
+ error: null,
469
+ isValidating: false,
470
+ mutate: jest.fn(),
471
+ });
472
+
473
+ render(<Invoice />);
474
+ expect(screen.queryByTestId('mock-print-receipt')).not.toBeInTheDocument();
475
+ });
246
476
  });
@@ -34,13 +34,15 @@ describe('PaymentForm Component', () => {
34
34
  mutate: jest.fn(),
35
35
  });
36
36
 
37
- const { container } = render(
37
+ render(
38
38
  <Wrapper>
39
39
  <PaymentForm disablePayment={false} isSingleLineItem={false} />
40
40
  </Wrapper>,
41
41
  );
42
42
 
43
- expect(container.querySelector('.cds--skeleton')).toBeInTheDocument();
43
+ // When loading, payment method elements should not be present
44
+ expect(screen.queryByText(/select payment method/i)).not.toBeInTheDocument();
45
+ expect(screen.queryByPlaceholderText(/enter amount/i)).not.toBeInTheDocument();
44
46
  });
45
47
 
46
48
  test('should render error message when payment modes fail to load', () => {
@@ -226,7 +226,7 @@ describe('Payments', () => {
226
226
 
227
227
  it('should return null when bill is not provided', () => {
228
228
  const { container } = render(<Payments bill={null} mutate={mockMutate} />);
229
- expect(container.firstChild).toBeNull();
229
+ expect(container).toBeEmptyDOMElement();
230
230
  });
231
231
 
232
232
  it('should render add payment method button for bills with amount due', () => {
@@ -241,7 +241,7 @@ describe('Payments', () => {
241
241
 
242
242
  // Verify add payment method button is available
243
243
  expect(screen.getByText(/add payment method/i)).toBeInTheDocument();
244
- expect(screen.getByText(/add payment method/i)).not.toBeDisabled();
244
+ expect(screen.getByText(/add payment method/i)).toBeEnabled();
245
245
  });
246
246
 
247
247
  it('should display process payment button', () => {
@@ -23,7 +23,6 @@ describe('LinkExtension Component', () => {
23
23
  });
24
24
 
25
25
  describe('createLeftPanelLink Function', () => {
26
- const user = userEvent.setup();
27
26
  test('returns a component that renders LinkExtension', () => {
28
27
  const config = { name: 'billing', title: 'Billing' };
29
28
  const TestComponent = createLeftPanelLink(config);
@@ -31,8 +30,6 @@ describe('createLeftPanelLink Function', () => {
31
30
  render(<TestComponent />);
32
31
  expect(screen.getByText('Billing')).toBeInTheDocument();
33
32
  const testLink = screen.getByRole('link', { name: 'Billing' });
34
- user.click(testLink);
35
- expect(window.location.pathname).toBe('/billing/6eb8d678-514d-46ad-9554-51e48d96d567');
36
- // expect(testLink).toHaveClass('active-left-nav-link');
33
+ expect(testLink).toHaveAttribute('href', '/openmrs/spa/home/billing');
37
34
  });
38
35
  });