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

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
@@ -0,0 +1,174 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
+ import { FormProvider, useForm } from 'react-hook-form';
4
+ import type { PaymentFormValue } from '../payments.component';
5
+ import PaymentForm from './payment-form.component';
6
+
7
+ // Mock the payment resource
8
+ jest.mock('../payment.resource', () => ({
9
+ usePaymentModes: jest.fn(),
10
+ }));
11
+
12
+ const { usePaymentModes } = jest.requireMock('../payment.resource');
13
+
14
+ type WrapperProps = {
15
+ children: React.ReactNode;
16
+ };
17
+
18
+ const Wrapper: React.FC<WrapperProps> = ({ children }) => {
19
+ const methods = useForm<PaymentFormValue>();
20
+ return <FormProvider {...methods}>{children}</FormProvider>;
21
+ };
22
+
23
+ describe('PaymentForm Component', () => {
24
+ beforeEach(() => {
25
+ jest.clearAllMocks();
26
+ });
27
+
28
+ test('should render skeleton while loading payment modes', () => {
29
+ usePaymentModes.mockReturnValue({
30
+ paymentModes: [],
31
+ isLoading: true,
32
+ error: null,
33
+ mutate: jest.fn(),
34
+ });
35
+
36
+ render(
37
+ <Wrapper>
38
+ <PaymentForm
39
+ disablePayment={false}
40
+ clientBalance={100}
41
+ isSingleLineItemSelected={false}
42
+ isSingleLineItem={false}
43
+ />
44
+ </Wrapper>,
45
+ );
46
+
47
+ expect(screen.getByTestId('number-input-skeleton')).toBeInTheDocument();
48
+ });
49
+
50
+ test('should render error message when payment modes fail to load', () => {
51
+ usePaymentModes.mockReturnValue({
52
+ paymentModes: [],
53
+ isLoading: false,
54
+ error: new Error('Failed to load payment modes'),
55
+ mutate: jest.fn(),
56
+ });
57
+
58
+ render(
59
+ <Wrapper>
60
+ <PaymentForm
61
+ disablePayment={false}
62
+ clientBalance={100}
63
+ isSingleLineItemSelected={false}
64
+ isSingleLineItem={false}
65
+ />
66
+ </Wrapper>,
67
+ );
68
+
69
+ expect(screen.getByText(/payment modes error/i)).toBeInTheDocument();
70
+ });
71
+
72
+ test('should append default payment when isSingleLineItem is true', () => {
73
+ usePaymentModes.mockReturnValue({
74
+ paymentModes: [{ uuid: '1', name: 'Credit Card' }],
75
+ isLoading: false,
76
+ error: null,
77
+ mutate: jest.fn(),
78
+ });
79
+
80
+ render(
81
+ <Wrapper>
82
+ <PaymentForm
83
+ disablePayment={false}
84
+ clientBalance={100}
85
+ isSingleLineItemSelected={false}
86
+ isSingleLineItem={true}
87
+ />
88
+ </Wrapper>,
89
+ );
90
+
91
+ const paymentMethodElements = screen.getAllByText(/payment method/i);
92
+
93
+ expect(paymentMethodElements.length).toBeGreaterThan(0);
94
+ expect(paymentMethodElements[0]).toBeInTheDocument();
95
+
96
+ expect(screen.getByPlaceholderText(/enter amount/i)).toBeInTheDocument();
97
+ });
98
+
99
+ test('should append a payment field when add payment option button is clicked', () => {
100
+ usePaymentModes.mockReturnValue({
101
+ paymentModes: [{ uuid: '1', name: 'Credit Card' }],
102
+ isLoading: false,
103
+ error: null,
104
+ mutate: jest.fn(),
105
+ });
106
+
107
+ render(
108
+ <Wrapper>
109
+ <PaymentForm
110
+ disablePayment={false}
111
+ clientBalance={100}
112
+ isSingleLineItemSelected={true}
113
+ isSingleLineItem={false}
114
+ />
115
+ </Wrapper>,
116
+ );
117
+
118
+ const addButton = screen.getByText(/add payment option/i);
119
+ fireEvent.click(addButton);
120
+ const paymentMethodElements = screen.getAllByLabelText(/payment method/i);
121
+ expect(paymentMethodElements).toHaveLength(2);
122
+ });
123
+
124
+ test('should disable add payment button when disablePayment is true', () => {
125
+ usePaymentModes.mockReturnValue({
126
+ paymentModes: [{ uuid: '1', name: 'Credit Card' }],
127
+ isLoading: false,
128
+ error: null,
129
+ mutate: jest.fn(),
130
+ });
131
+
132
+ render(
133
+ <Wrapper>
134
+ <PaymentForm
135
+ disablePayment={true}
136
+ clientBalance={100}
137
+ isSingleLineItemSelected={true}
138
+ isSingleLineItem={false}
139
+ />
140
+ </Wrapper>,
141
+ );
142
+
143
+ expect(screen.getByText(/add payment option/i)).toBeDisabled();
144
+ });
145
+
146
+ test('should remove payment field when trash can icon is clicked', async () => {
147
+ usePaymentModes.mockReturnValue({
148
+ paymentModes: [{ uuid: '1', name: 'Credit Card' }],
149
+ isLoading: false,
150
+ error: null,
151
+ mutate: jest.fn(),
152
+ });
153
+
154
+ render(
155
+ <Wrapper>
156
+ <PaymentForm
157
+ disablePayment={false}
158
+ clientBalance={100}
159
+ isSingleLineItemSelected={true}
160
+ isSingleLineItem={false}
161
+ />
162
+ </Wrapper>,
163
+ );
164
+
165
+ fireEvent.click(screen.getByText(/add payment option/i));
166
+
167
+ const trashCanIcon = screen.getByTestId('trash-can-icon');
168
+ fireEvent.click(trashCanIcon);
169
+
170
+ await waitFor(() => {
171
+ expect(screen.queryByPlaceholderText(/enter amount/i)).not.toBeInTheDocument();
172
+ });
173
+ });
174
+ });
@@ -28,13 +28,15 @@ const PaymentHistory: React.FC<PaymentHistoryProps> = ({ bill }) => {
28
28
  header: 'Payment method',
29
29
  },
30
30
  ];
31
- const rows = bill?.payments?.map((payment) => ({
32
- id: `${payment.uuid}`,
33
- dateCreated: formatDate(new Date(payment.dateCreated)),
34
- amountTendered: convertToCurrency(payment.amountTendered, defaultCurrency),
35
- amount: convertToCurrency(payment.amount, defaultCurrency),
36
- paymentMethod: payment.instanceType.name,
37
- }));
31
+ const rows = bill?.payments?.map((payment, index) => {
32
+ return {
33
+ id: `${payment.uuid}-${index}`,
34
+ dateCreated: formatDate(new Date(payment.dateCreated)),
35
+ amountTendered: convertToCurrency(payment.amountTendered, defaultCurrency),
36
+ amount: convertToCurrency(payment.amount, defaultCurrency),
37
+ paymentMethod: payment.instanceType.name,
38
+ };
39
+ });
38
40
 
39
41
  if (Object.values(bill?.payments ?? {}).length === 0) {
40
42
  return;
@@ -0,0 +1,160 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { useConfig } from '@openmrs/esm-framework';
4
+ import PaymentHistory from './payment-history.component';
5
+ import { type MappedBill } from '../../../types';
6
+
7
+ // Mocking useConfig to return a default currency
8
+ jest.mock('@openmrs/esm-framework', () => ({
9
+ useConfig: jest.fn(),
10
+ formatDate: jest.fn((date) => date.toISOString().split('T')[0]),
11
+ }));
12
+
13
+ jest.mock('../../../helpers', () => ({
14
+ convertToCurrency: jest.fn((amount, currency) => `${currency} ${amount.toFixed(2)}`),
15
+ }));
16
+
17
+ describe('PaymentHistory Component', () => {
18
+ beforeEach(() => {
19
+ (useConfig as jest.Mock).mockReturnValue({
20
+ defaultCurrency: 'USD',
21
+ });
22
+ });
23
+
24
+ const mockBill: MappedBill = {
25
+ uuid: 'bill-uuid',
26
+ id: 1,
27
+ patientUuid: 'patient-uuid',
28
+ patientName: 'John Doe',
29
+ cashPointUuid: 'cash-point-uuid',
30
+ cashPointName: 'Main Cash Point',
31
+ cashPointLocation: 'Main Hospital',
32
+ cashier: {
33
+ uuid: 'provider-1',
34
+ display: 'Jane Doe',
35
+ links: [
36
+ {
37
+ rel: 'self',
38
+ uri: 'http://example.com/provider/1',
39
+ resourceAlias: 'Jane Doe',
40
+ },
41
+ ],
42
+ },
43
+ payments: [
44
+ {
45
+ uuid: 'payment-1',
46
+ dateCreated: new Date('2023-09-01T12:00:00Z').getTime(),
47
+ amountTendered: 100,
48
+ amount: 80,
49
+ instanceType: {
50
+ uuid: 'instance-1',
51
+ name: 'Credit Card',
52
+ description: 'Credit Card payment',
53
+ retired: false,
54
+ },
55
+ attributes: [],
56
+ voided: false,
57
+ resourceVersion: '1.0',
58
+ },
59
+ {
60
+ uuid: 'payment-2',
61
+ dateCreated: new Date('2023-09-05T14:00:00Z').getTime(),
62
+ amountTendered: 200,
63
+ amount: 180,
64
+ instanceType: {
65
+ uuid: 'instance-2',
66
+ name: 'Cash',
67
+ description: 'Cash payment',
68
+ retired: false,
69
+ },
70
+ attributes: [],
71
+ voided: false,
72
+ resourceVersion: '1.0',
73
+ },
74
+ ],
75
+ receiptNumber: '12345',
76
+ status: 'PAID',
77
+ identifier: 'invoice-123',
78
+ dateCreated: '2023-09-01T12:00:00Z',
79
+ lineItems: [],
80
+ billingService: 'Billing Service',
81
+ };
82
+
83
+ const emptyBill: MappedBill = {
84
+ uuid: 'bill-uuid',
85
+ id: 1,
86
+ patientUuid: 'patient-uuid',
87
+ patientName: 'John Doe',
88
+ cashPointUuid: 'cash-point-uuid',
89
+ cashPointName: 'Main Cash Point',
90
+ cashPointLocation: 'Main Hospital',
91
+ cashier: {
92
+ uuid: 'provider-2',
93
+ display: 'John Smith',
94
+ links: [
95
+ {
96
+ rel: 'self',
97
+ uri: 'http://example.com/provider/2',
98
+ resourceAlias: 'John Smith',
99
+ },
100
+ ],
101
+ },
102
+ payments: [],
103
+ receiptNumber: '12346',
104
+ status: 'PENDING',
105
+ identifier: 'invoice-124',
106
+ dateCreated: '2023-09-02T10:00:00Z',
107
+ lineItems: [],
108
+ billingService: 'Billing Service',
109
+ };
110
+
111
+ test('renders without crashing', () => {
112
+ render(<PaymentHistory bill={mockBill} />);
113
+ });
114
+
115
+ test('renders correct table headers', () => {
116
+ render(<PaymentHistory bill={mockBill} />);
117
+ expect(screen.getByText('Date of payment')).toBeInTheDocument();
118
+ expect(screen.getByText('Bill amount')).toBeInTheDocument();
119
+ expect(screen.getByText('Amount tendered')).toBeInTheDocument();
120
+ expect(screen.getByText('Payment method')).toBeInTheDocument();
121
+ });
122
+
123
+ test('renders the correct number of rows', () => {
124
+ render(<PaymentHistory bill={mockBill} />);
125
+ const rows = screen.getAllByRole('row');
126
+ expect(rows).toHaveLength(3);
127
+ });
128
+
129
+ test('renders correct data in the rows', () => {
130
+ render(<PaymentHistory bill={mockBill} />);
131
+
132
+ expect(screen.getByText('2023-09-01')).toBeInTheDocument();
133
+ expect(screen.getByText('USD 80.00')).toBeInTheDocument();
134
+ expect(screen.getByText('USD 100.00')).toBeInTheDocument();
135
+ expect(screen.getByText('Credit Card')).toBeInTheDocument();
136
+
137
+ expect(screen.getByText('2023-09-05')).toBeInTheDocument();
138
+ expect(screen.getByText('USD 180.00')).toBeInTheDocument();
139
+ expect(screen.getByText('USD 200.00')).toBeInTheDocument();
140
+ expect(screen.getByText('Cash')).toBeInTheDocument();
141
+ });
142
+
143
+ test('handles empty payments gracefully', () => {
144
+ render(<PaymentHistory bill={emptyBill} />);
145
+ expect(screen.queryByRole('table')).not.toBeInTheDocument();
146
+ });
147
+
148
+ test('does not render when bill is undefined', () => {
149
+ render(<PaymentHistory bill={undefined} />);
150
+ expect(screen.queryByRole('table')).not.toBeInTheDocument();
151
+ });
152
+
153
+ test('formats dates and converts amounts correctly', () => {
154
+ render(<PaymentHistory bill={mockBill} />);
155
+
156
+ expect(screen.getByText('2023-09-01')).toBeInTheDocument();
157
+ expect(screen.getByText('USD 80.00')).toBeInTheDocument();
158
+ expect(screen.getByText('USD 100.00')).toBeInTheDocument();
159
+ });
160
+ });
@@ -0,0 +1,121 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/react';
4
+ import { useVisit, useConfig, navigate } from '@openmrs/esm-framework';
5
+ import { useBillableServices } from '../../billable-services/billable-service.resource';
6
+ import { type MappedBill, type LineItem } from '../../types';
7
+ import Payments from './payments.component';
8
+
9
+ // Add this mock for currency formatting
10
+ const mockFormatToParts = jest.fn().mockReturnValue([{ type: 'integer', value: '1000' }]);
11
+ const mockFormat = jest.fn().mockReturnValue('$1000.00');
12
+ global.Intl.NumberFormat = jest.fn().mockImplementation(() => ({
13
+ formatToParts: mockFormatToParts,
14
+ format: mockFormat,
15
+ })) as any;
16
+ global.Intl.NumberFormat.supportedLocalesOf = jest.fn().mockReturnValue(['en-US']);
17
+
18
+ jest.mock('../../billing.resource', () => ({
19
+ processBillPayment: jest.fn(),
20
+ }));
21
+
22
+ jest.mock('../../billable-services/billable-service.resource', () => ({
23
+ useBillableServices: jest.fn(),
24
+ }));
25
+
26
+ describe('Payments', () => {
27
+ const mockBill: MappedBill = {
28
+ uuid: 'bill-uuid',
29
+ id: 1,
30
+ patientUuid: 'patient-uuid',
31
+ patientName: 'John Doe',
32
+ cashPointUuid: 'cash-point-uuid',
33
+ cashPointName: 'Main Cash Point',
34
+ cashPointLocation: 'Main Hospital',
35
+ cashier: {
36
+ uuid: 'provider-1',
37
+ display: 'Jane Doe',
38
+ links: [
39
+ {
40
+ rel: 'self',
41
+ uri: 'http://example.com/provider/1',
42
+ resourceAlias: 'Jane Doe',
43
+ },
44
+ ],
45
+ },
46
+ payments: [
47
+ {
48
+ uuid: 'payment-1',
49
+ dateCreated: new Date('2023-09-01T12:00:00Z').getTime(),
50
+ amountTendered: 100,
51
+ amount: 80,
52
+ instanceType: {
53
+ uuid: 'instance-1',
54
+ name: 'Credit Card',
55
+ description: 'Credit Card payment',
56
+ retired: false,
57
+ },
58
+ attributes: [],
59
+ voided: false,
60
+ resourceVersion: '1.0',
61
+ },
62
+ {
63
+ uuid: 'payment-2',
64
+ dateCreated: new Date('2023-09-05T14:00:00Z').getTime(),
65
+ amountTendered: 200,
66
+ amount: 180,
67
+ instanceType: {
68
+ uuid: 'instance-2',
69
+ name: 'Cash',
70
+ description: 'Cash payment',
71
+ retired: false,
72
+ },
73
+ attributes: [],
74
+ voided: false,
75
+ resourceVersion: '1.0',
76
+ },
77
+ ],
78
+ receiptNumber: '12345',
79
+ status: 'PAID',
80
+ identifier: 'invoice-123',
81
+ dateCreated: '2023-09-01T12:00:00Z',
82
+ lineItems: [],
83
+ billingService: 'Billing Service',
84
+ };
85
+
86
+ const mockMutate = jest.fn();
87
+ const mockSelectedLineItems: LineItem[] = [];
88
+
89
+ beforeEach(() => {
90
+ jest.clearAllMocks();
91
+ (useVisit as jest.Mock).mockReturnValue({ currentVisit: null });
92
+ (useConfig as jest.Mock).mockReturnValue({ defaultCurrency: 'USD' });
93
+ (useBillableServices as jest.Mock).mockReturnValue({ billableServices: [], isLoading: false });
94
+ });
95
+
96
+ it('renders payment form and history', () => {
97
+ render(<Payments bill={mockBill} mutate={mockMutate} selectedLineItems={mockSelectedLineItems} />);
98
+ expect(screen.getByText('Payments')).toBeInTheDocument();
99
+ expect(screen.getByText('Total Amount:')).toBeInTheDocument();
100
+ expect(screen.getByText('Total Tendered:')).toBeInTheDocument();
101
+ });
102
+
103
+ it('calculates and displays correct amounts', () => {
104
+ render(<Payments bill={mockBill} mutate={mockMutate} selectedLineItems={mockSelectedLineItems} />);
105
+ const amountElements = screen.getAllByText('$1000.00');
106
+ expect(amountElements[amountElements.length - 3]).toBeInTheDocument();
107
+ expect(amountElements[amountElements.length - 2]).toBeInTheDocument();
108
+ expect(amountElements[amountElements.length - 1]).toBeInTheDocument();
109
+ });
110
+
111
+ it('disables Process Payment button when form is invalid', () => {
112
+ render(<Payments bill={mockBill} mutate={mockMutate} selectedLineItems={mockSelectedLineItems} />);
113
+ expect(screen.getByText('Process Payment')).toBeDisabled();
114
+ });
115
+
116
+ it('navigates to billing dashboard when Discard is clicked', async () => {
117
+ render(<Payments bill={mockBill} mutate={mockMutate} selectedLineItems={mockSelectedLineItems} />);
118
+ await userEvent.click(screen.getByText('Discard'));
119
+ expect(navigate).toHaveBeenCalled();
120
+ });
121
+ });
@@ -20,6 +20,7 @@ import { useBillableServices } from '../../billable-services/billable-service.re
20
20
  type PaymentProps = {
21
21
  bill: MappedBill;
22
22
  selectedLineItems: Array<LineItem>;
23
+ mutate: () => void;
23
24
  };
24
25
 
25
26
  export type Payment = { method: string; amount: string | number; referenceCode?: number | string };
@@ -28,10 +29,9 @@ export type PaymentFormValue = {
28
29
  payment: Array<Payment>;
29
30
  };
30
31
 
31
- const Payments: React.FC<PaymentProps> = ({ bill, selectedLineItems }) => {
32
+ const Payments: React.FC<PaymentProps> = ({ bill, mutate, selectedLineItems }) => {
32
33
  const { t } = useTranslation();
33
- const { defaultCurrency } = useConfig();
34
- const { billableServices, isLoading, isValidating, error, mutate } = useBillableServices();
34
+ const { billableServices, isLoading, isValidating, error } = useBillableServices();
35
35
  const paymentSchema = z.object({
36
36
  method: z.string().refine((value) => !!value, 'Payment method is required'),
37
37
  amount: z
@@ -42,9 +42,10 @@ const Payments: React.FC<PaymentProps> = ({ bill, selectedLineItems }) => {
42
42
 
43
43
  const paymentFormSchema = z.object({ payment: z.array(paymentSchema) });
44
44
  const { currentVisit } = useVisit(bill?.patientUuid);
45
+ const { defaultCurrency } = useConfig();
45
46
  const methods = useForm<PaymentFormValue>({
46
47
  mode: 'all',
47
- defaultValues: {},
48
+ defaultValues: { payment: [] },
48
49
  resolver: zodResolver(paymentFormSchema),
49
50
  });
50
51
 
@@ -53,11 +54,10 @@ const Payments: React.FC<PaymentProps> = ({ bill, selectedLineItems }) => {
53
54
  control: methods.control,
54
55
  });
55
56
 
56
- const hasMoreThanOneLineItem = bill?.lineItems?.length > 1;
57
- const computedTotal = hasMoreThanOneLineItem ? computeTotalPrice(selectedLineItems) : bill?.totalAmount ?? 0;
57
+ const selectedLineItemsTotal = selectedLineItems.reduce((total, item) => total + item.price * item.quantity, 0);
58
58
  const totalAmountTendered = formValues?.reduce((curr: number, prev) => curr + Number(prev.amount) ?? 0, 0) ?? 0;
59
- const amountDue = Number(computedTotal) - (Number(bill?.tenderedAmount) + Number(totalAmountTendered));
60
- const newAmountDue = Number(bill?.totalAmount - bill?.tenderedAmount);
59
+ const amountDue = bill ? bill.totalAmount - selectedLineItemsTotal : 0;
60
+ const clientBalance = bill ? bill.totalAmount - (bill.tenderedAmount + totalAmountTendered) : 0;
61
61
 
62
62
  const handleNavigateToBillingDashboard = () =>
63
63
  navigate({
@@ -65,27 +65,46 @@ const Payments: React.FC<PaymentProps> = ({ bill, selectedLineItems }) => {
65
65
  });
66
66
 
67
67
  const handleProcessPayment = () => {
68
- const paymentPayload = createPaymentPayload(bill, bill.patientUuid, formValues, amountDue, billableServices, selectedLineItems);
69
- processBillPayment(paymentPayload, bill.uuid).then(
70
- (res) => {
71
- showSnackbar({
72
- title: t('billPayment', 'Bill payment'),
73
- subtitle: 'Bill payment processing has been successful',
74
- kind: 'success',
75
- timeoutInMs: 3000,
76
- });
77
- if (currentVisit) {
78
- updateBillVisitAttribute(currentVisit);
79
- }
80
- window.location.reload();
81
- },
82
- (error) => {
83
- showSnackbar({ title: 'Bill payment error', kind: 'error', subtitle: error?.message });
84
- },
85
- );
68
+ if (bill) {
69
+ const paymentPayload = createPaymentPayload(
70
+ bill,
71
+ bill?.patientUuid,
72
+ formValues,
73
+ amountDue,
74
+ billableServices,
75
+ selectedLineItems,
76
+ );
77
+ paymentPayload.payments.forEach((payment) => {
78
+ payment.dateCreated = new Date(payment.dateCreated);
79
+ });
80
+
81
+ processBillPayment(paymentPayload, bill.uuid).then(
82
+ (res) => {
83
+ showSnackbar({
84
+ title: t('billPayment', 'Bill payment'),
85
+ subtitle: 'Bill payment processing has been successful',
86
+ kind: 'success',
87
+ timeoutInMs: 3000,
88
+ });
89
+ if (currentVisit) {
90
+ updateBillVisitAttribute(currentVisit);
91
+ }
92
+ methods.reset({ payment: [{ method: '', amount: '0', referenceCode: '' }] });
93
+ mutate();
94
+ },
95
+ (error) => {
96
+ showSnackbar({ title: 'Bill payment error', kind: 'error', subtitle: error?.message });
97
+ },
98
+ );
99
+ }
86
100
  };
87
101
 
88
- const amountDueDisplay = (amount: number) => (amount < 0 ? 'Client balance' : 'Amount Due');
102
+ if (!bill) {
103
+ return null;
104
+ }
105
+
106
+ const amountDueLabel = selectedLineItems.length ? t('amountDue', 'Amount Due') : t('clientBalance', 'Client Balance');
107
+ const amountDueValue = selectedLineItems.length ? amountDue : clientBalance;
89
108
 
90
109
  return (
91
110
  <FormProvider {...methods}>
@@ -96,24 +115,29 @@ const Payments: React.FC<PaymentProps> = ({ bill, selectedLineItems }) => {
96
115
  </CardHeader>
97
116
  <div>
98
117
  {bill && <PaymentHistory bill={bill} />}
99
- <PaymentForm disablePayment={computedTotal <= 0} amountDue={amountDue} />
118
+ <PaymentForm
119
+ disablePayment={clientBalance <= 0}
120
+ clientBalance={clientBalance}
121
+ isSingleLineItemSelected={selectedLineItems.length > 0}
122
+ isSingleLineItem={bill.lineItems.length === 1}
123
+ />
100
124
  </div>
101
125
  </div>
102
126
  <div className={styles.divider} />
103
127
  <div className={styles.paymentTotals}>
104
128
  <InvoiceBreakDown
105
129
  label={t('totalAmount', 'Total Amount')}
106
- value={convertToCurrency(bill?.totalAmount, defaultCurrency)}
130
+ value={convertToCurrency(bill.totalAmount, defaultCurrency)}
107
131
  />
108
132
  <InvoiceBreakDown
109
133
  label={t('totalTendered', 'Total Tendered')}
110
- value={convertToCurrency(bill?.tenderedAmount, defaultCurrency)}
134
+ value={convertToCurrency(bill?.tenderedAmount + totalAmountTendered, defaultCurrency)}
111
135
  />
112
136
  <InvoiceBreakDown label={t('discount', 'Discount')} value={'--'} />
113
137
  <InvoiceBreakDown
114
- hasBalance={amountDue < 0 ?? false}
115
- label={amountDueDisplay(amountDue)}
116
- value={convertToCurrency(bill?.totalAmount - bill?.tenderedAmount, defaultCurrency)}
138
+ hasBalance={amountDueValue < 0}
139
+ label={amountDueLabel}
140
+ value={convertToCurrency(amountDueValue < 0 ? -amountDueValue : amountDueValue, defaultCurrency)}
117
141
  />
118
142
  <div className={styles.processPayments}>
119
143
  <Button onClick={handleNavigateToBillingDashboard} kind="secondary">
@@ -129,19 +153,4 @@ const Payments: React.FC<PaymentProps> = ({ bill, selectedLineItems }) => {
129
153
  );
130
154
  };
131
155
 
132
- const computeTotalPrice = (items) => {
133
- if (items && !items.length) {
134
- return 0;
135
- }
136
-
137
- let totalPrice = 0;
138
-
139
- items?.forEach((item) => {
140
- const { price, quantity } = item;
141
- totalPrice += price * quantity;
142
- });
143
-
144
- return totalPrice;
145
- };
146
-
147
156
  export default Payments;