@openmrs/esm-billing-app 1.0.1-pre.98 → 1.0.2-pre.58

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 (224) hide show
  1. package/.eslintignore +0 -1
  2. package/.eslintrc +33 -24
  3. package/.husky/pre-commit +1 -1
  4. package/.turbo.json +1 -1
  5. package/.tx/config +11 -0
  6. package/README.md +111 -1
  7. package/dist/1119.js +1 -0
  8. package/dist/1197.js +1 -0
  9. package/dist/1362.js +1 -0
  10. package/dist/1362.js.map +1 -0
  11. package/dist/2146.js +1 -0
  12. package/dist/2690.js +1 -0
  13. package/dist/3029.js +2 -0
  14. package/dist/3029.js.LICENSE.txt +7 -0
  15. package/dist/3029.js.map +1 -0
  16. package/dist/3099.js +1 -0
  17. package/dist/3511.js +1 -0
  18. package/dist/3511.js.map +1 -0
  19. package/dist/3584.js +1 -0
  20. package/dist/4055.js +1 -0
  21. package/dist/4132.js +1 -0
  22. package/dist/4225.js +1 -0
  23. package/dist/4225.js.map +1 -0
  24. package/dist/4300.js +1 -0
  25. package/dist/4335.js +1 -0
  26. package/dist/4618.js +1 -0
  27. package/dist/4652.js +1 -0
  28. package/dist/4817.js +2 -0
  29. package/dist/4817.js.LICENSE.txt +77 -0
  30. package/dist/4817.js.map +1 -0
  31. package/dist/4944.js +1 -0
  32. package/dist/4993.js +1 -0
  33. package/dist/4993.js.map +1 -0
  34. package/dist/5173.js +1 -0
  35. package/dist/5241.js +1 -0
  36. package/dist/5442.js +1 -0
  37. package/dist/5661.js +1 -0
  38. package/dist/6022.js +1 -0
  39. package/dist/6468.js +1 -0
  40. package/dist/6540.js +2 -0
  41. package/dist/6540.js.map +1 -0
  42. package/dist/6606.js +2 -0
  43. package/dist/{591.js.LICENSE.txt → 6606.js.LICENSE.txt} +2 -2
  44. package/dist/6606.js.map +1 -0
  45. package/dist/6679.js +1 -0
  46. package/dist/6840.js +1 -0
  47. package/dist/6859.js +1 -0
  48. package/dist/6941.js +1 -0
  49. package/dist/6941.js.map +1 -0
  50. package/dist/7097.js +1 -0
  51. package/dist/7159.js +1 -0
  52. package/dist/723.js +1 -0
  53. package/dist/7255.js +1 -0
  54. package/dist/7255.js.map +1 -0
  55. package/dist/7617.js +1 -0
  56. package/dist/763.js +1 -0
  57. package/dist/763.js.map +1 -0
  58. package/dist/8163.js +1 -0
  59. package/dist/8349.js +1 -0
  60. package/dist/8618.js +1 -0
  61. package/dist/890.js +1 -0
  62. package/dist/9055.js +1 -0
  63. package/dist/9055.js.map +1 -0
  64. package/dist/9214.js +1 -0
  65. package/dist/9538.js +1 -0
  66. package/dist/{935.js → 961.js} +2 -2
  67. package/dist/{935.js.map → 961.js.map} +1 -1
  68. package/dist/986.js +1 -0
  69. package/dist/9879.js +1 -0
  70. package/dist/9895.js +1 -0
  71. package/dist/9900.js +1 -0
  72. package/dist/9913.js +1 -0
  73. package/dist/main.js +1 -1
  74. package/dist/main.js.LICENSE.txt +31 -1
  75. package/dist/main.js.map +1 -1
  76. package/dist/openmrs-esm-billing-app.js +1 -1
  77. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +844 -165
  78. package/dist/openmrs-esm-billing-app.js.map +1 -1
  79. package/dist/routes.json +1 -1
  80. package/jest.config.js +4 -1
  81. package/package.json +19 -21
  82. package/src/bill-history/bill-history.component.tsx +5 -3
  83. package/src/bill-history/bill-history.scss +24 -9
  84. package/src/bill-history/bill-history.test.tsx +58 -16
  85. package/src/bill-item-actions/bill-item-actions.scss +26 -0
  86. package/src/bill-item-actions/edit-bill-item.component.tsx +221 -0
  87. package/src/bill-item-actions/edit-bill-item.test.tsx +137 -0
  88. package/src/billable-services/bill-waiver/bill-selection.component.tsx +1 -1
  89. package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +2 -2
  90. package/src/billable-services/bill-waiver/bill-waiver-form.scss +4 -4
  91. package/src/billable-services/bill-waiver/bill-waiver.component.tsx +4 -4
  92. package/src/billable-services/bill-waiver/patient-bills.component.tsx +1 -1
  93. package/src/billable-services/billable-service.resource.ts +19 -6
  94. package/src/billable-services/billable-services-home.component.tsx +19 -3
  95. package/src/billable-services/billable-services-menu-item/item.component.tsx +17 -0
  96. package/src/billable-services/billable-services-menu-item/item.scss +14 -0
  97. package/src/billable-services/billable-services.component.tsx +48 -9
  98. package/src/billable-services/billable-services.scss +10 -9
  99. package/src/billable-services/billable-services.test.tsx +172 -8
  100. package/src/billable-services/cash-point/cash-point-configuration.component.tsx +276 -0
  101. package/src/billable-services/cash-point/cash-point-configuration.scss +23 -0
  102. package/src/billable-services/create-edit/add-billable-service.component.tsx +126 -47
  103. package/src/billable-services/create-edit/add-billable-service.scss +14 -8
  104. package/src/billable-services/create-edit/add-billable-service.test.tsx +12 -10
  105. package/src/billable-services/dashboard/dashboard.scss +3 -3
  106. package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +280 -0
  107. package/src/billable-services/payyment-modes/payment-modes-config.scss +23 -0
  108. package/src/billing-dashboard/billing-dashboard.component.tsx +17 -4
  109. package/src/billing-dashboard/billing-dashboard.scss +3 -3
  110. package/src/billing-form/billing-form.component.tsx +31 -25
  111. package/src/billing-form/billing-form.scss +9 -10
  112. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +38 -14
  113. package/src/billing-header/billing-header.component.tsx +21 -5
  114. package/src/billing-header/billing-header.scss +1 -1
  115. package/src/billing.resource.ts +21 -4
  116. package/src/bills-table/bills-table.component.tsx +46 -36
  117. package/src/bills-table/bills-table.scss +6 -6
  118. package/src/bills-table/bills-table.test.tsx +108 -68
  119. package/src/config-schema.ts +36 -1
  120. package/src/constants.ts +2 -0
  121. package/src/dashboard.meta.ts +2 -1
  122. package/src/helpers/functions.ts +0 -2
  123. package/src/hooks/selectedDateContext.ts +10 -0
  124. package/src/index.ts +22 -27
  125. package/src/invoice/invoice-table.component.tsx +95 -56
  126. package/src/invoice/invoice-table.scss +7 -8
  127. package/src/invoice/invoice-table.test.tsx +151 -0
  128. package/src/invoice/invoice.component.tsx +7 -9
  129. package/src/invoice/invoice.scss +2 -2
  130. package/src/invoice/invoice.test.tsx +199 -169
  131. package/src/invoice/payments/payment-form/payment-form.component.tsx +84 -55
  132. package/src/invoice/payments/payment-form/payment-form.test.tsx +174 -0
  133. package/src/invoice/payments/payment-history/payment-history.component.tsx +9 -7
  134. package/src/invoice/payments/payment-history/payment-history.test.tsx +160 -0
  135. package/src/invoice/payments/payments.component.test.tsx +121 -0
  136. package/src/invoice/payments/payments.component.tsx +57 -48
  137. package/src/invoice/payments/utils.ts +17 -13
  138. package/src/invoice/printable-invoice/print-receipt.component.tsx +23 -8
  139. package/src/invoice/printable-invoice/print-receipt.test.tsx +50 -0
  140. package/src/metrics-cards/card.component.tsx +4 -2
  141. package/src/metrics-cards/metrics-cards.test.tsx +1 -1
  142. package/src/modal/require-payment-modal.component.tsx +2 -2
  143. package/src/modal/require-payment-modal.test.tsx +66 -0
  144. package/src/modal/require-payment.scss +2 -1
  145. package/src/routes.json +40 -8
  146. package/src/types/index.ts +15 -0
  147. package/{i18next-parser.config.js → tools/i18next-parser.config.js} +19 -19
  148. package/tools/update-openmrs-deps.mjs +42 -0
  149. package/translations/am.json +53 -0
  150. package/translations/ar.json +170 -0
  151. package/translations/ar_SY.json +170 -0
  152. package/translations/bn.json +170 -0
  153. package/translations/de.json +170 -0
  154. package/translations/en.json +53 -0
  155. package/translations/es.json +53 -0
  156. package/translations/es_MX.json +170 -0
  157. package/translations/fr.json +53 -0
  158. package/translations/he.json +53 -0
  159. package/translations/hi.json +170 -0
  160. package/translations/hi_IN.json +170 -0
  161. package/translations/id.json +170 -0
  162. package/translations/it.json +170 -0
  163. package/translations/km.json +53 -0
  164. package/translations/ku.json +170 -0
  165. package/translations/ky.json +170 -0
  166. package/translations/lg.json +170 -0
  167. package/translations/ne.json +170 -0
  168. package/translations/pl.json +170 -0
  169. package/translations/pt.json +170 -0
  170. package/translations/pt_BR.json +170 -0
  171. package/translations/qu.json +170 -0
  172. package/translations/ro_RO.json +170 -0
  173. package/translations/ru_RU.json +170 -0
  174. package/translations/si.json +170 -0
  175. package/translations/sw.json +170 -0
  176. package/translations/sw_KE.json +170 -0
  177. package/translations/tr.json +170 -0
  178. package/translations/tr_TR.json +170 -0
  179. package/translations/uk.json +170 -0
  180. package/translations/uz.json +170 -0
  181. package/translations/uz@Latn.json +170 -0
  182. package/translations/uz_UZ.json +170 -0
  183. package/translations/vi.json +170 -0
  184. package/translations/zh.json +170 -0
  185. package/translations/zh_CN.json +170 -0
  186. package/tsconfig.json +10 -8
  187. package/webpack.config.js +1 -1
  188. package/dist/146.js +0 -1
  189. package/dist/146.js.map +0 -1
  190. package/dist/294.js +0 -2
  191. package/dist/294.js.map +0 -1
  192. package/dist/319.js +0 -1
  193. package/dist/384.js +0 -1
  194. package/dist/384.js.map +0 -1
  195. package/dist/421.js +0 -1
  196. package/dist/421.js.map +0 -1
  197. package/dist/533.js +0 -1
  198. package/dist/533.js.map +0 -1
  199. package/dist/574.js +0 -1
  200. package/dist/591.js +0 -2
  201. package/dist/591.js.map +0 -1
  202. package/dist/614.js +0 -2
  203. package/dist/614.js.LICENSE.txt +0 -37
  204. package/dist/614.js.map +0 -1
  205. package/dist/753.js +0 -1
  206. package/dist/753.js.map +0 -1
  207. package/dist/757.js +0 -1
  208. package/dist/770.js +0 -1
  209. package/dist/770.js.map +0 -1
  210. package/dist/783.js +0 -1
  211. package/dist/783.js.map +0 -1
  212. package/dist/788.js +0 -1
  213. package/dist/800.js +0 -2
  214. package/dist/800.js.LICENSE.txt +0 -3
  215. package/dist/800.js.map +0 -1
  216. package/dist/807.js +0 -1
  217. package/dist/833.js +0 -1
  218. package/dist/992.js +0 -1
  219. package/dist/992.js.map +0 -1
  220. package/src/root.scss +0 -30
  221. /package/dist/{294.js.LICENSE.txt → 6540.js.LICENSE.txt} +0 -0
  222. /package/dist/{935.js.LICENSE.txt → 961.js.LICENSE.txt} +0 -0
  223. /package/{src → tools}/setup-tests.ts +0 -0
  224. /package/{test-helpers.tsx → tools/test-helpers.tsx} +0 -0
@@ -2,67 +2,142 @@ import React from 'react';
2
2
  import { screen, render } from '@testing-library/react';
3
3
  import userEvent from '@testing-library/user-event';
4
4
  import { useReactToPrint } from 'react-to-print';
5
- import { showSnackbar } from '@openmrs/esm-framework';
6
- import { mockPayments, mockBill } from '../../__mocks__/bills.mock';
5
+ import { mockBill } from '../../__mocks__/bills.mock';
7
6
  import { useBill, processBillPayment } from '../billing.resource';
8
7
  import { usePaymentModes } from './payments/payment.resource';
9
8
  import Invoice from './invoice.component';
10
9
 
11
- const mockedBill = jest.mocked(useBill);
12
- const mockedProcessBillPayment = jest.mocked(processBillPayment);
13
- const mockedUsePaymentModes = jest.mocked(usePaymentModes);
14
- const mockedUseReactToPrint = jest.mocked(useReactToPrint);
10
+ // Mock convertToCurrency
11
+ jest.mock('../helpers/functions', () => ({
12
+ convertToCurrency: jest.fn((amount) => `USD ${amount}`),
13
+ }));
14
+
15
+ // Mock i18next
16
+ jest.mock('react-i18next', () => ({
17
+ useTranslation: () => ({
18
+ t: (key: string) => key,
19
+ }),
20
+ }));
15
21
 
22
+ // Set window.i18next
23
+ window.i18next = {
24
+ language: 'en',
25
+ } as any;
26
+
27
+ // Mock InvoiceTable component
28
+ jest.mock('./invoice-table.component', () =>
29
+ jest.fn(({ bill }) => <div data-testid="mock-invoice-table">Invoice Table Mock</div>),
30
+ );
31
+
32
+ // Mock payments component
33
+ jest.mock('./payments/payments.component', () =>
34
+ jest.fn(({ bill, mutate, selectedLineItems }) => (
35
+ <div data-testid="mock-payments">
36
+ <h2>Payments</h2>
37
+ <button>Add payment option</button>
38
+ </div>
39
+ )),
40
+ );
41
+
42
+ // Mock PrintReceipt component
43
+ jest.mock('./printable-invoice/print-receipt.component', () =>
44
+ jest.fn(({ billId }) => <div data-testid="mock-print-receipt">Print Receipt Mock</div>),
45
+ );
46
+
47
+ // Mock PrintableInvoice component
48
+ jest.mock('./printable-invoice/printable-invoice.component', () =>
49
+ jest.fn(({ bill, patient }) => <div data-testid="mock-printable-invoice">Printable Invoice Mock</div>),
50
+ );
51
+
52
+ // Mock payment resource
16
53
  jest.mock('./payments/payment.resource', () => ({
17
54
  usePaymentModes: jest.fn(),
18
55
  updateBillVisitAttribute: jest.fn(),
19
56
  }));
20
57
 
58
+ // Mock billing resource
21
59
  jest.mock('../billing.resource', () => ({
22
60
  useBill: jest.fn(),
23
61
  processBillPayment: jest.fn(),
24
- useDefaultFacility: jest.fn().mockReturnValue({ uuid: '54065383-b4d4-42d2-af4d-d250a1fd2590', display: 'MTRH' }),
62
+ useDefaultFacility: jest.fn().mockReturnValue({
63
+ uuid: '54065383-b4d4-42d2-af4d-d250a1fd2590',
64
+ display: 'MTRH',
65
+ }),
25
66
  }));
26
67
 
27
- jest.mock('react-router-dom', () => {
28
- const originalModule = jest.requireActual('react-router-dom');
29
-
30
- return {
31
- ...originalModule,
32
- useParams: jest.fn().mockReturnValue({ patientUuid: 'patientUuid', billUuid: 'billUuid' }),
33
- };
34
- });
68
+ // Mock react-router-dom
69
+ jest.mock('react-router-dom', () => ({
70
+ useParams: jest.fn().mockReturnValue({
71
+ patientUuid: 'patientUuid',
72
+ billUuid: 'billUuid',
73
+ }),
74
+ }));
35
75
 
36
- jest.mock('react-to-print', () => {
37
- const originalModule = jest.requireActual('react-to-print');
76
+ // Mock react-to-print
77
+ jest.mock('react-to-print', () => ({
78
+ useReactToPrint: jest.fn(),
79
+ }));
38
80
 
39
- return {
40
- ...originalModule,
41
- useReactToPrint: jest.fn(),
42
- };
43
- });
81
+ // Mock OpenMRS framework
82
+ jest.mock('@openmrs/esm-framework', () => ({
83
+ showSnackbar: jest.fn(),
84
+ useLayoutType: jest.fn(() => 'desktop'),
85
+ isDesktop: jest.fn(() => true),
86
+ useConfig: jest.fn(() => ({
87
+ defaultCurrency: 'USD',
88
+ })),
89
+ formatDate: jest.fn((date) => date?.toString() ?? ''),
90
+ ExtensionSlot: jest.fn(({ children }) => <div data-testid="extension-slot">{children}</div>),
91
+ usePatient: jest.fn().mockReturnValue({
92
+ patient: {
93
+ id: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
94
+ name: [{ given: ['John'], family: 'Doe' }],
95
+ },
96
+ patientUuid: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
97
+ isLoading: false,
98
+ error: null,
99
+ }),
100
+ createGlobalStore: jest.fn(),
101
+ getGlobalStore: jest.fn(() => ({
102
+ subscribe: jest.fn(),
103
+ getState: jest.fn(),
104
+ setState: jest.fn(),
105
+ })),
106
+ }));
44
107
 
45
- jest.mock('@openmrs/esm-framework', () => {
46
- const originalModule = jest.requireActual('@openmrs/esm-framework');
108
+ // Mock patient common lib
109
+ jest.mock('@openmrs/esm-patient-common-lib', () => ({
110
+ ErrorState: jest.fn(({ error }) => <div data-testid="error-state">Error: {error?.message || error}</div>),
111
+ }));
47
112
 
48
- return {
49
- ...originalModule,
50
- usePatient: jest.fn().mockReturnValue({
51
- patient: {
52
- id: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
53
- name: [{ given: ['John'], family: 'Doe' }],
113
+ describe('Invoice', () => {
114
+ const mockedBill = useBill as jest.Mock;
115
+ const mockedProcessBillPayment = processBillPayment as jest.Mock;
116
+ const mockedUsePaymentModes = usePaymentModes as jest.Mock;
117
+ const mockedUseReactToPrint = useReactToPrint as jest.Mock;
118
+
119
+ const defaultBillData = {
120
+ ...mockBill,
121
+ uuid: 'test-uuid',
122
+ status: 'PENDING',
123
+ totalAmount: 1000,
124
+ tenderedAmount: 0,
125
+ receiptNumber: 'RCPT-001',
126
+ dateCreated: '2024-01-01',
127
+ lineItems: [
128
+ {
129
+ uuid: 'item-1',
130
+ item: 'Test Service',
131
+ quantity: 1,
132
+ price: 1000,
133
+ paymentStatus: 'PENDING',
54
134
  },
55
- patientUuid: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
56
- isLoading: false,
57
- error: null,
58
- }),
135
+ ],
59
136
  };
60
- });
61
137
 
62
- xdescribe('Invoice', () => {
63
138
  beforeEach(() => {
64
139
  mockedBill.mockReturnValue({
65
- bill: mockBill,
140
+ bill: defaultBillData,
66
141
  isLoading: false,
67
142
  error: null,
68
143
  isValidating: false,
@@ -71,172 +146,127 @@ xdescribe('Invoice', () => {
71
146
 
72
147
  mockedUsePaymentModes.mockReturnValue({
73
148
  paymentModes: [
74
- { uuid: 'uuid', name: 'Cash', description: 'Cash Method', retired: false },
75
- { uuid: 'uuid1', name: 'MPESA', description: 'MPESA Method', retired: false },
149
+ { uuid: 'cash-uuid', name: 'Cash', description: 'Cash Method', retired: false },
150
+ { uuid: 'mpesa-uuid', name: 'MPESA', description: 'MPESA Method', retired: false },
76
151
  ],
77
152
  isLoading: false,
78
153
  error: null,
79
154
  mutate: jest.fn(),
80
155
  });
81
- });
82
-
83
- afterEach(() => jest.clearAllMocks());
84
156
 
85
- test('should be able to search through the invoice table and settle a bill', async () => {
86
- const user = userEvent.setup();
87
-
88
- renderInvoice();
89
-
90
- const expectedHeaders = [
91
- /Total amount/i,
92
- /Amount tendered/i,
93
- /Date and time/i,
94
- /Invoice status/i,
95
- /Invoice number/i,
96
- ];
97
-
98
- expectedHeaders.forEach((header) => {
99
- expect(screen.getByRole('heading', { name: header })).toBeInTheDocument();
100
- });
101
-
102
- const printButton = screen.getByRole('button', { name: /Print bill/i });
103
- expect(printButton).toBeInTheDocument();
157
+ // Setup print handler mock
158
+ const printHandler = jest.fn();
159
+ mockedUseReactToPrint.mockReturnValue(printHandler);
160
+ });
104
161
 
105
- // Should show the line items table with the correct headers
106
- const expectedColumnHeaders = [/No/i, /Bill item/i, /Bill code/i, /Status/i, /Quantity/i, /Price/i, /Total/i];
162
+ afterEach(() => {
163
+ jest.clearAllMocks();
164
+ });
107
165
 
108
- expectedColumnHeaders.forEach((columnHeader) => {
109
- expect(screen.getByRole('columnheader', { name: columnHeader })).toBeInTheDocument();
166
+ it('should render error state correctly', () => {
167
+ mockedBill.mockReturnValue({
168
+ bill: null,
169
+ isLoading: false,
170
+ error: new Error('Test error'),
171
+ isValidating: false,
172
+ mutate: jest.fn(),
110
173
  });
111
174
 
112
- expect(screen.getByRole('heading', { name: /Line items/i })).toBeInTheDocument();
113
- expect(screen.getByText(/Items to be billed/i)).toBeInTheDocument();
114
-
115
- // Should be able to search the line items table
116
- const searchInput = screen.getByRole('searchbox');
117
- expect(searchInput).toBeInTheDocument();
118
- await user.type(searchInput, 'Hemoglobin');
119
- expect(screen.getByText('Hemoglobin')).toBeInTheDocument();
120
-
121
- await user.type(searchInput, 'Some random text');
122
- expect(screen.queryByText('Hemoglobin')).not.toBeInTheDocument();
123
- expect(screen.getByText(/No matching items to display/i)).toBeInTheDocument();
124
- await user.clear(searchInput);
125
-
126
- const row = mockBill.lineItems[0].item + ' ' + mockBill.receiptNumber + ' ' + mockBill.status.toUpperCase();
127
-
128
- expect(screen.getByRole('row', { name: new RegExp(row, 'i') })).toBeInTheDocument();
129
-
130
- // should be able to handle payments
131
- const paymentSection = await screen.findByRole('heading', { name: /Payments/i });
132
- expect(paymentSection).toBeInTheDocument();
133
-
134
- const addPaymentOptionButton = await screen.findByRole('button', { name: /Add payment option/i });
135
- expect(addPaymentOptionButton).toBeInTheDocument();
136
- await user.click(addPaymentOptionButton);
137
- const paymentModeInput = screen.getByRole('combobox', { name: /Payment method/i });
138
- expect(paymentModeInput).toBeInTheDocument();
139
- await user.click(paymentModeInput);
140
-
141
- // select cash payment mode
142
- const cashPaymentMode = await screen.findByText('Cash');
143
- expect(cashPaymentMode).toBeInTheDocument();
144
- await user.click(cashPaymentMode);
145
-
146
- // enter payment amount
147
- const paymentAmountInput = screen.getByPlaceholderText('Enter amount');
148
- expect(paymentAmountInput).toBeInTheDocument();
149
- await user.type(paymentAmountInput, '100');
175
+ render(<Invoice />);
176
+ expect(screen.getByTestId('error-state')).toBeInTheDocument();
177
+ expect(screen.getByText(/Test error/i)).toBeInTheDocument();
178
+ });
150
179
 
151
- // enter payment reference number
152
- const paymentReferenceNumberInput = screen.getByRole('textbox', { name: /Reference number/ });
153
- expect(paymentReferenceNumberInput).toBeInTheDocument();
154
- await user.type(paymentReferenceNumberInput, '123456');
180
+ it('should render invoice details correctly', () => {
181
+ render(<Invoice />);
155
182
 
156
- expect(addPaymentOptionButton).toBeDisabled();
183
+ // Check invoice details
184
+ expect(screen.getByText(/Total Amount/i)).toBeInTheDocument();
185
+ expect(screen.getByText(/Amount Tendered/i)).toBeInTheDocument();
186
+ expect(screen.getByText(/Invoice Number/i)).toBeInTheDocument();
187
+ expect(screen.getByText(/Date And Time/i)).toBeInTheDocument();
188
+ expect(screen.getByText(/Invoice Status/i)).toBeInTheDocument();
157
189
 
158
- // should process payment
159
- mockedProcessBillPayment.mockResolvedValueOnce(Promise.resolve({} as any));
160
- const processPaymentButton = screen.getByRole('button', { name: /Process Payment/i });
161
- expect(processPaymentButton).toBeInTheDocument();
162
- await user.click(processPaymentButton);
190
+ // Check mock components
191
+ expect(screen.getByTestId('mock-invoice-table')).toBeInTheDocument();
192
+ expect(screen.getByTestId('mock-payments')).toBeInTheDocument();
193
+ });
163
194
 
164
- expect(processBillPayment).toHaveBeenCalledTimes(1);
165
- expect(processBillPayment).toHaveBeenCalledWith(
166
- {
167
- cashPoint: '54065383-b4d4-42d2-af4d-d250a1fd2590',
168
- cashier: 'fe00dd43-4c39-4ce9-9832-bc3620c80c6c',
169
- patient: 'b2fcf02b-7ee3-4d16-a48f-576be2b103aa',
170
- payments: [{ amount: 100, amountTendered: 100, attributes: [], instanceType: 'uuid' }],
195
+ it('should show print receipt button for paid bills', () => {
196
+ mockedBill.mockReturnValue({
197
+ bill: {
198
+ ...defaultBillData,
171
199
  status: 'PAID',
200
+ tenderedAmount: 1000,
172
201
  },
173
- '6eb8d678-514d-46ad-9554-51e48d96d567',
174
- );
175
- expect(showSnackbar).toHaveBeenCalled();
176
- expect(showSnackbar).toHaveBeenCalledWith({
177
- kind: 'success',
178
- subtitle: 'Bill payment processing has been successful',
179
- timeoutInMs: 3000,
180
- title: 'Bill payment',
202
+ isLoading: false,
203
+ error: null,
204
+ isValidating: false,
205
+ mutate: jest.fn(),
181
206
  });
182
- });
183
207
 
184
- test('should show print preview when print button is clicked', async () => {
185
- const user = userEvent.setup();
186
-
187
- renderInvoice();
188
-
189
- const printButton = screen.getByRole('button', { name: /Print bill/i });
190
- expect(printButton).toBeInTheDocument();
191
- await user.click(printButton);
192
- expect(mockedUseReactToPrint).toHaveBeenCalledTimes(1);
193
- expect(mockedUseReactToPrint).toHaveBeenCalledWith(
194
- expect.objectContaining({
195
- documentTitle: 'Invoice 0035-6 - John Doe',
196
- }),
197
- );
208
+ render(<Invoice />);
209
+ expect(screen.getByTestId('mock-print-receipt')).toBeInTheDocument();
198
210
  });
199
211
 
200
- test('should show payment history if bill is paid and disable adding more payments', async () => {
212
+ it('should handle bill payment processing', async () => {
201
213
  const user = userEvent.setup();
214
+ const mockMutate = jest.fn();
215
+
202
216
  mockedBill.mockReturnValue({
203
- bill: {
204
- ...mockBill,
205
- status: 'PAID',
206
- payments: mockPayments,
207
- tenderedAmount: 100,
208
- },
217
+ bill: defaultBillData,
209
218
  isLoading: false,
210
219
  error: null,
211
220
  isValidating: false,
212
- mutate: jest.fn(),
221
+ mutate: mockMutate,
213
222
  });
214
223
 
215
- mockedUsePaymentModes.mockReturnValue({
216
- paymentModes: [
217
- { uuid: 'uuid', name: 'Cash', description: 'Cash Method', retired: false },
218
- { uuid: 'uuid1', name: 'MPESA', description: 'MPESA Method', retired: false },
224
+ mockedProcessBillPayment.mockResolvedValue({});
225
+
226
+ render(<Invoice />);
227
+
228
+ // Add payment flow would go here
229
+ // Note: Detailed payment interaction testing should be in the Payments component tests
230
+
231
+ expect(screen.getByText(/Payments/i)).toBeInTheDocument();
232
+ });
233
+
234
+ it('should update line items when bill data changes', () => {
235
+ const { rerender } = render(<Invoice />);
236
+
237
+ // Update bill with new line items
238
+ const updatedBill = {
239
+ ...defaultBillData,
240
+ lineItems: [
241
+ ...defaultBillData.lineItems,
242
+ {
243
+ uuid: 'item-2',
244
+ item: 'New Service',
245
+ quantity: 1,
246
+ price: 500,
247
+ paymentStatus: 'PENDING',
248
+ },
219
249
  ],
250
+ };
251
+
252
+ mockedBill.mockReturnValue({
253
+ bill: updatedBill,
220
254
  isLoading: false,
221
255
  error: null,
256
+ isValidating: false,
222
257
  mutate: jest.fn(),
223
258
  });
224
259
 
225
- renderInvoice();
226
- const paymentHistorySection = screen.getByRole('heading', { name: /Payments/i });
227
- expect(paymentHistorySection).toBeInTheDocument();
260
+ rerender(<Invoice />);
228
261
 
229
- const expectedColumnHeaders = [/Date of payment/, /Bill amount/, /Amount tendered/, /Payment method/];
230
- expectedColumnHeaders.forEach((header) => {
231
- expect(screen.getByRole('columnheader', { name: new RegExp(header, 'i') })).toBeInTheDocument();
232
- });
262
+ // The mock invoice table should receive updated props
263
+ expect(screen.getByTestId('mock-invoice-table')).toBeInTheDocument();
264
+ });
233
265
 
234
- const addPaymentOptionButton = await screen.findByRole('button', { name: /Add payment option/i });
235
- expect(addPaymentOptionButton).toBeInTheDocument();
236
- expect(addPaymentOptionButton).toBeDisabled();
266
+ it('should show patient information correctly', () => {
267
+ render(<Invoice />);
268
+ expect(screen.getByTestId('extension-slot')).toBeInTheDocument();
237
269
  });
238
- });
239
270
 
240
- function renderInvoice() {
241
- return render(<Invoice />);
242
- }
271
+ // Add more test cases as needed for specific features or edge cases
272
+ });
@@ -1,4 +1,4 @@
1
- import React, { useCallback } from 'react';
1
+ import React, { useCallback, useState, useEffect } from 'react';
2
2
  import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { TrashCan, Add } from '@carbon/react/icons';
@@ -8,11 +8,21 @@ import { type PaymentFormValue } from '../payments.component';
8
8
  import { usePaymentModes } from '../payment.resource';
9
9
  import styles from './payment-form.scss';
10
10
 
11
- type PaymentFormProps = { disablePayment: boolean; amountDue: number };
11
+ type PaymentFormProps = {
12
+ disablePayment: boolean;
13
+ clientBalance: number;
14
+ isSingleLineItemSelected: boolean;
15
+ isSingleLineItem: boolean;
16
+ };
12
17
 
13
18
  const DEFAULT_PAYMENT = { method: '', amount: 0, referenceCode: '' };
14
19
 
15
- const PaymentForm: React.FC<PaymentFormProps> = ({ disablePayment, amountDue }) => {
20
+ const PaymentForm: React.FC<PaymentFormProps> = ({
21
+ disablePayment,
22
+ clientBalance,
23
+ isSingleLineItemSelected,
24
+ isSingleLineItem,
25
+ }) => {
16
26
  const { t } = useTranslation();
17
27
  const {
18
28
  control,
@@ -20,12 +30,25 @@ const PaymentForm: React.FC<PaymentFormProps> = ({ disablePayment, amountDue })
20
30
  } = useFormContext<PaymentFormValue>();
21
31
  const { paymentModes, isLoading, error } = usePaymentModes();
22
32
  const { fields, remove, append } = useFieldArray({ name: 'payment', control: control });
33
+ const [isFormVisible, setIsFormVisible] = useState(isSingleLineItem);
34
+
35
+ useEffect(() => {
36
+ if (isSingleLineItem) {
37
+ setIsFormVisible(true);
38
+ if (fields.length === 0) {
39
+ append(DEFAULT_PAYMENT);
40
+ }
41
+ }
42
+ }, [isSingleLineItem, append, fields.length]);
23
43
 
24
- const handleAppendPaymentMode = useCallback(() => append(DEFAULT_PAYMENT), [append]);
44
+ const handleAppendPaymentMode = useCallback(() => {
45
+ setIsFormVisible(true);
46
+ append(DEFAULT_PAYMENT);
47
+ }, [append]);
25
48
  const handleRemovePaymentMode = useCallback((index) => remove(index), [remove]);
26
49
 
27
50
  if (isLoading) {
28
- return <NumberInputSkeleton />;
51
+ return <NumberInputSkeleton data-testid="number-input-skeleton" />;
29
52
  }
30
53
 
31
54
  if (error) {
@@ -38,59 +61,65 @@ const PaymentForm: React.FC<PaymentFormProps> = ({ disablePayment, amountDue })
38
61
 
39
62
  return (
40
63
  <div className={styles.container}>
41
- {fields.map((field, index) => (
42
- <div key={field.id} className={styles.paymentMethodContainer}>
43
- <Controller
44
- control={control}
45
- name={`payment.${index}.method`}
46
- render={({ field }) => (
47
- <Dropdown
48
- id="paymentMethod"
49
- onChange={({ selectedItem }) => field.onChange(selectedItem?.uuid)}
50
- titleText={t('paymentMethod', 'Payment method')}
51
- label={t('selectPaymentMethod', 'Select payment method')}
52
- items={paymentModes}
53
- itemToString={(item) => (item ? item.name : '')}
54
- invalid={!!errors?.payment?.[index]?.method}
55
- invalidText={errors?.payment?.[index]?.method?.message}
56
- />
57
- )}
58
- />
59
- <Controller
60
- control={control}
61
- name={`payment.${index}.amount`}
62
- render={({ field }) => (
63
- <NumberInput
64
- id="paymentAmount"
65
- {...field}
66
- onChange={(e) => field.onChange(Number(e.target.value))}
67
- invalid={!!errors?.payment?.[index]?.amount}
68
- invalidText={errors?.payment?.[index]?.amount?.message}
69
- label={t('amount', 'Amount')}
70
- placeholder={t('enterAmount', 'Enter amount')}
71
- />
72
- )}
73
- />
74
- <Controller
75
- name={`payment.${index}.referenceCode`}
76
- control={control}
77
- render={({ field }) => (
78
- <TextInput
79
- id="paymentReferenceCode"
80
- {...field}
81
- labelText={t('referenceNumber', 'Reference number')}
82
- placeholder={t('enterReferenceNumber', 'Enter ref. number')}
83
- type="text"
64
+ {isFormVisible &&
65
+ fields.map((field, index) => (
66
+ <div key={field.id} className={styles.paymentMethodContainer}>
67
+ <Controller
68
+ control={control}
69
+ name={`payment.${index}.method`}
70
+ render={({ field }) => (
71
+ <Dropdown
72
+ id="paymentMethod"
73
+ onChange={({ selectedItem }) => field.onChange(selectedItem?.uuid)}
74
+ titleText={t('paymentMethod', 'Payment method')}
75
+ label={t('selectPaymentMethod', 'Select payment method')}
76
+ items={paymentModes}
77
+ itemToString={(item) => (item ? item.name : '')}
78
+ invalid={!!errors?.payment?.[index]?.method}
79
+ invalidText={errors?.payment?.[index]?.method?.message}
80
+ />
81
+ )}
82
+ />
83
+ <Controller
84
+ control={control}
85
+ name={`payment.${index}.amount`}
86
+ render={({ field }) => (
87
+ <NumberInput
88
+ id="paymentAmount"
89
+ {...field}
90
+ onChange={(e) => field.onChange(Number(e.target.value))}
91
+ invalid={!!errors?.payment?.[index]?.amount}
92
+ invalidText={errors?.payment?.[index]?.amount?.message}
93
+ label={t('amount', 'Amount')}
94
+ placeholder={t('enterAmount', 'Enter amount')}
95
+ />
96
+ )}
97
+ />
98
+ <Controller
99
+ name={`payment.${index}.referenceCode`}
100
+ control={control}
101
+ render={({ field }) => (
102
+ <TextInput
103
+ id="paymentReferenceCode"
104
+ {...field}
105
+ labelText={t('referenceNumber', 'Reference number')}
106
+ placeholder={t('enterReferenceNumber', 'Enter ref. number')}
107
+ type="text"
108
+ />
109
+ )}
110
+ />
111
+ <div className={styles.removeButtonContainer}>
112
+ <TrashCan
113
+ onClick={() => handleRemovePaymentMode(index)}
114
+ className={styles.removeButton}
115
+ size={20}
116
+ data-testid="trash-can-icon"
84
117
  />
85
- )}
86
- />
87
- <div className={styles.removeButtonContainer}>
88
- <TrashCan onClick={handleRemovePaymentMode} className={styles.removeButton} size={20} />
118
+ </div>
89
119
  </div>
90
- </div>
91
- ))}
120
+ ))}
92
121
  <Button
93
- disabled={disablePayment}
122
+ disabled={disablePayment || (!isSingleLineItem && !isSingleLineItemSelected)}
94
123
  size="md"
95
124
  onClick={handleAppendPaymentMode}
96
125
  className={styles.paymentButtons}