@openmrs/esm-billing-app 1.0.2-pre.74 → 1.0.2-pre.749

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 (196) hide show
  1. package/README.md +55 -9
  2. package/__mocks__/bills.mock.ts +12 -0
  3. package/__mocks__/react-i18next.js +6 -5
  4. package/dist/1119.js +1 -1
  5. package/dist/1146.js +1 -2
  6. package/dist/1146.js.map +1 -1
  7. package/dist/1197.js +1 -1
  8. package/dist/1856.js +1 -0
  9. package/dist/1856.js.map +1 -0
  10. package/dist/2146.js +1 -1
  11. package/dist/2177.js +2 -0
  12. package/dist/2177.js.LICENSE.txt +9 -0
  13. package/dist/2177.js.map +1 -0
  14. package/dist/2524.js +1 -0
  15. package/dist/2524.js.map +1 -0
  16. package/dist/2690.js +1 -1
  17. package/dist/3041.js +1 -0
  18. package/dist/3041.js.map +1 -0
  19. package/dist/3099.js +1 -1
  20. package/dist/3584.js +1 -1
  21. package/dist/4055.js +1 -1
  22. package/dist/4132.js +1 -1
  23. package/dist/4225.js +1 -0
  24. package/dist/4225.js.map +1 -0
  25. package/dist/4300.js +1 -1
  26. package/dist/4335.js +1 -1
  27. package/dist/4618.js +1 -1
  28. package/dist/4652.js +1 -1
  29. package/dist/4724.js +1 -0
  30. package/dist/4724.js.map +1 -0
  31. package/dist/4739.js +1 -1
  32. package/dist/4739.js.map +1 -1
  33. package/dist/4944.js +1 -1
  34. package/dist/5173.js +1 -1
  35. package/dist/5241.js +1 -1
  36. package/dist/5422.js +1 -0
  37. package/dist/5422.js.map +1 -0
  38. package/dist/5442.js +1 -1
  39. package/dist/5661.js +1 -1
  40. package/dist/6022.js +1 -1
  41. package/dist/6468.js +1 -1
  42. package/dist/6540.js +1 -1
  43. package/dist/6540.js.map +1 -1
  44. package/dist/6606.js +1 -0
  45. package/dist/6606.js.map +1 -0
  46. package/dist/6679.js +1 -1
  47. package/dist/6840.js +1 -1
  48. package/dist/6859.js +1 -1
  49. package/dist/7097.js +1 -1
  50. package/dist/7159.js +1 -1
  51. package/dist/723.js +1 -1
  52. package/dist/7452.js +2 -0
  53. package/dist/7452.js.map +1 -0
  54. package/dist/7617.js +1 -1
  55. package/dist/795.js +1 -0
  56. package/dist/8163.js +1 -1
  57. package/dist/8349.js +1 -1
  58. package/dist/8618.js +1 -1
  59. package/dist/890.js +1 -1
  60. package/dist/8930.js +2 -0
  61. package/dist/{6525.js.LICENSE.txt → 8930.js.LICENSE.txt} +16 -4
  62. package/dist/8930.js.map +1 -0
  63. package/dist/9214.js +1 -1
  64. package/dist/942.js +1 -0
  65. package/dist/942.js.map +1 -0
  66. package/dist/9538.js +1 -1
  67. package/dist/9569.js +1 -0
  68. package/dist/961.js +1 -1
  69. package/dist/961.js.map +1 -1
  70. package/dist/986.js +1 -1
  71. package/dist/9879.js +1 -1
  72. package/dist/9895.js +1 -1
  73. package/dist/9900.js +1 -1
  74. package/dist/9913.js +1 -1
  75. package/dist/main.js +1 -1
  76. package/dist/main.js.map +1 -1
  77. package/dist/openmrs-esm-billing-app.js +1 -1
  78. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +381 -231
  79. package/dist/openmrs-esm-billing-app.js.map +1 -1
  80. package/dist/routes.json +1 -1
  81. package/e2e/README.md +19 -18
  82. package/e2e/specs/sample-test.spec.ts +0 -1
  83. package/package.json +10 -10
  84. package/src/bill-history/bill-history.component.tsx +17 -25
  85. package/src/bill-history/bill-history.scss +4 -94
  86. package/src/bill-history/bill-history.test.tsx +37 -78
  87. package/src/bill-item-actions/bill-item-actions.scss +0 -4
  88. package/src/bill-item-actions/{edit-bill-item.component.tsx → edit-bill-item.modal.tsx} +58 -56
  89. package/src/bill-item-actions/edit-bill-item.test.tsx +22 -24
  90. package/src/billable-services/bill-waiver/bill-selection.component.tsx +2 -2
  91. package/src/billable-services/bill-waiver/patient-bills.component.tsx +3 -3
  92. package/src/billable-services/billable-service.resource.ts +4 -3
  93. package/src/billable-services/billable-services-home.component.tsx +1 -1
  94. package/src/billable-services/billable-services.component.tsx +115 -132
  95. package/src/billable-services/billable-services.scss +3 -0
  96. package/src/billable-services/billable-services.test.tsx +2 -45
  97. package/src/billable-services/cash-point/add-cash-point.modal.tsx +168 -0
  98. package/src/billable-services/cash-point/cash-point-configuration.component.tsx +17 -192
  99. package/src/billable-services/cash-point/cash-point-configuration.scss +1 -5
  100. package/src/billable-services/create-edit/add-billable-service.component.tsx +28 -24
  101. package/src/billable-services/create-edit/add-billable-service.scss +2 -5
  102. package/src/billable-services/create-edit/add-billable-service.test.tsx +6 -6
  103. package/src/billable-services/create-edit/edit-billable-service.modal.tsx +50 -0
  104. package/src/billable-services/payment-modes/add-payment-mode.modal.tsx +121 -0
  105. package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +72 -0
  106. package/src/billable-services/payment-modes/payment-modes-config.component.tsx +125 -0
  107. package/src/billable-services/{payyment-modes → payment-modes}/payment-modes-config.scss +5 -1
  108. package/src/billing-form/billing-checkin-form.component.tsx +2 -3
  109. package/src/billing-form/billing-checkin-form.test.tsx +0 -2
  110. package/src/billing-form/billing-form.component.tsx +210 -268
  111. package/src/billing-form/billing-form.scss +143 -0
  112. package/src/billing.resource.ts +16 -19
  113. package/src/bills-table/bills-table.test.tsx +97 -53
  114. package/src/config-schema.ts +52 -18
  115. package/src/dashboard.meta.ts +4 -2
  116. package/src/helpers/functions.ts +5 -4
  117. package/src/index.ts +17 -6
  118. package/src/invoice/invoice-table.component.tsx +24 -54
  119. package/src/invoice/invoice-table.scss +1 -5
  120. package/src/invoice/invoice-table.test.tsx +21 -47
  121. package/src/invoice/invoice.component.tsx +36 -29
  122. package/src/invoice/invoice.scss +7 -4
  123. package/src/invoice/invoice.test.tsx +22 -48
  124. package/src/invoice/payments/payment-form/payment-form.component.tsx +2 -9
  125. package/src/invoice/payments/payment-form/payment-form.test.tsx +14 -46
  126. package/src/invoice/payments/payment-history/payment-history.component.tsx +6 -4
  127. package/src/invoice/payments/payment-history/payment-history.test.tsx +9 -14
  128. package/src/invoice/payments/payments.component.tsx +16 -27
  129. package/src/invoice/payments/{payments.component.test.tsx → payments.test.tsx} +24 -10
  130. package/src/invoice/payments/utils.ts +4 -22
  131. package/src/invoice/printable-invoice/print-receipt.component.tsx +3 -2
  132. package/src/invoice/printable-invoice/print-receipt.test.tsx +14 -25
  133. package/src/invoice/printable-invoice/printable-footer.component.tsx +2 -2
  134. package/src/invoice/printable-invoice/printable-footer.test.tsx +4 -13
  135. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +12 -11
  136. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +16 -14
  137. package/src/invoice/printable-invoice/printable-invoice.component.tsx +19 -33
  138. package/src/metrics-cards/metrics-cards.test.tsx +18 -5
  139. package/src/modal/require-payment-modal.test.tsx +25 -20
  140. package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +17 -18
  141. package/src/routes.json +22 -2
  142. package/src/types/index.ts +13 -12
  143. package/translations/am.json +33 -16
  144. package/translations/ar.json +33 -16
  145. package/translations/ar_SY.json +33 -16
  146. package/translations/bn.json +38 -21
  147. package/translations/de.json +33 -16
  148. package/translations/en.json +33 -16
  149. package/translations/en_US.json +187 -0
  150. package/translations/es.json +48 -31
  151. package/translations/es_MX.json +33 -16
  152. package/translations/fr.json +47 -30
  153. package/translations/he.json +33 -16
  154. package/translations/hi.json +33 -16
  155. package/translations/hi_IN.json +33 -16
  156. package/translations/id.json +180 -163
  157. package/translations/it.json +70 -53
  158. package/translations/ka.json +187 -0
  159. package/translations/km.json +33 -16
  160. package/translations/ku.json +33 -16
  161. package/translations/ky.json +33 -16
  162. package/translations/lg.json +33 -16
  163. package/translations/ne.json +33 -16
  164. package/translations/pl.json +33 -16
  165. package/translations/pt.json +33 -16
  166. package/translations/pt_BR.json +33 -16
  167. package/translations/qu.json +33 -16
  168. package/translations/ro_RO.json +182 -165
  169. package/translations/ru_RU.json +33 -16
  170. package/translations/si.json +33 -16
  171. package/translations/sw.json +33 -16
  172. package/translations/sw_KE.json +33 -16
  173. package/translations/tr.json +33 -16
  174. package/translations/tr_TR.json +33 -16
  175. package/translations/uk.json +33 -16
  176. package/translations/uz.json +33 -16
  177. package/translations/uz@Latn.json +33 -16
  178. package/translations/uz_UZ.json +33 -16
  179. package/translations/vi.json +33 -16
  180. package/translations/zh.json +33 -16
  181. package/translations/zh_CN.json +91 -74
  182. package/dist/1146.js.LICENSE.txt +0 -21
  183. package/dist/2352.js +0 -1
  184. package/dist/2352.js.map +0 -1
  185. package/dist/246.js +0 -1
  186. package/dist/246.js.map +0 -1
  187. package/dist/6525.js +0 -2
  188. package/dist/6525.js.map +0 -1
  189. package/dist/8556.js +0 -2
  190. package/dist/8556.js.map +0 -1
  191. package/dist/8638.js +0 -1
  192. package/dist/8638.js.map +0 -1
  193. package/dist/9968.js +0 -1
  194. package/dist/9968.js.map +0 -1
  195. package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
  196. /package/dist/{8556.js.LICENSE.txt → 7452.js.LICENSE.txt} +0 -0
@@ -1,10 +1,10 @@
1
1
  import React from 'react';
2
- import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen, waitFor } from '@testing-library/react';
3
4
  import { FormProvider, useForm } from 'react-hook-form';
4
5
  import type { PaymentFormValue } from '../payments.component';
5
6
  import PaymentForm from './payment-form.component';
6
7
 
7
- // Mock the payment resource
8
8
  jest.mock('../payment.resource', () => ({
9
9
  usePaymentModes: jest.fn(),
10
10
  }));
@@ -21,10 +21,6 @@ const Wrapper: React.FC<WrapperProps> = ({ children }) => {
21
21
  };
22
22
 
23
23
  describe('PaymentForm Component', () => {
24
- beforeEach(() => {
25
- jest.clearAllMocks();
26
- });
27
-
28
24
  test('should render skeleton while loading payment modes', () => {
29
25
  usePaymentModes.mockReturnValue({
30
26
  paymentModes: [],
@@ -35,12 +31,7 @@ describe('PaymentForm Component', () => {
35
31
 
36
32
  render(
37
33
  <Wrapper>
38
- <PaymentForm
39
- disablePayment={false}
40
- clientBalance={100}
41
- isSingleLineItemSelected={false}
42
- isSingleLineItem={false}
43
- />
34
+ <PaymentForm disablePayment={false} isSingleLineItem={false} />
44
35
  </Wrapper>,
45
36
  );
46
37
 
@@ -57,12 +48,7 @@ describe('PaymentForm Component', () => {
57
48
 
58
49
  render(
59
50
  <Wrapper>
60
- <PaymentForm
61
- disablePayment={false}
62
- clientBalance={100}
63
- isSingleLineItemSelected={false}
64
- isSingleLineItem={false}
65
- />
51
+ <PaymentForm disablePayment={false} isSingleLineItem={false} />
66
52
  </Wrapper>,
67
53
  );
68
54
 
@@ -79,12 +65,7 @@ describe('PaymentForm Component', () => {
79
65
 
80
66
  render(
81
67
  <Wrapper>
82
- <PaymentForm
83
- disablePayment={false}
84
- clientBalance={100}
85
- isSingleLineItemSelected={false}
86
- isSingleLineItem={true}
87
- />
68
+ <PaymentForm disablePayment={false} isSingleLineItem={true} />
88
69
  </Wrapper>,
89
70
  );
90
71
 
@@ -96,7 +77,8 @@ describe('PaymentForm Component', () => {
96
77
  expect(screen.getByPlaceholderText(/enter amount/i)).toBeInTheDocument();
97
78
  });
98
79
 
99
- test('should append a payment field when add payment option button is clicked', () => {
80
+ test('should append a payment field when add payment option button is clicked', async () => {
81
+ const user = userEvent.setup();
100
82
  usePaymentModes.mockReturnValue({
101
83
  paymentModes: [{ uuid: '1', name: 'Credit Card' }],
102
84
  isLoading: false,
@@ -106,17 +88,12 @@ describe('PaymentForm Component', () => {
106
88
 
107
89
  render(
108
90
  <Wrapper>
109
- <PaymentForm
110
- disablePayment={false}
111
- clientBalance={100}
112
- isSingleLineItemSelected={true}
113
- isSingleLineItem={false}
114
- />
91
+ <PaymentForm disablePayment={false} isSingleLineItem={false} />
115
92
  </Wrapper>,
116
93
  );
117
94
 
118
95
  const addButton = screen.getByText(/add payment option/i);
119
- fireEvent.click(addButton);
96
+ await user.click(addButton);
120
97
  const paymentMethodElements = screen.getAllByLabelText(/payment method/i);
121
98
  expect(paymentMethodElements).toHaveLength(2);
122
99
  });
@@ -131,12 +108,7 @@ describe('PaymentForm Component', () => {
131
108
 
132
109
  render(
133
110
  <Wrapper>
134
- <PaymentForm
135
- disablePayment={true}
136
- clientBalance={100}
137
- isSingleLineItemSelected={true}
138
- isSingleLineItem={false}
139
- />
111
+ <PaymentForm disablePayment={true} isSingleLineItem={false} />
140
112
  </Wrapper>,
141
113
  );
142
114
 
@@ -144,6 +116,7 @@ describe('PaymentForm Component', () => {
144
116
  });
145
117
 
146
118
  test('should remove payment field when trash can icon is clicked', async () => {
119
+ const user = userEvent.setup();
147
120
  usePaymentModes.mockReturnValue({
148
121
  paymentModes: [{ uuid: '1', name: 'Credit Card' }],
149
122
  isLoading: false,
@@ -153,19 +126,14 @@ describe('PaymentForm Component', () => {
153
126
 
154
127
  render(
155
128
  <Wrapper>
156
- <PaymentForm
157
- disablePayment={false}
158
- clientBalance={100}
159
- isSingleLineItemSelected={true}
160
- isSingleLineItem={false}
161
- />
129
+ <PaymentForm disablePayment={false} isSingleLineItem={false} />
162
130
  </Wrapper>,
163
131
  );
164
132
 
165
- fireEvent.click(screen.getByText(/add payment option/i));
133
+ await user.click(screen.getByText(/add payment option/i));
166
134
 
167
135
  const trashCanIcon = screen.getByTestId('trash-can-icon');
168
- fireEvent.click(trashCanIcon);
136
+ await user.click(trashCanIcon);
169
137
 
170
138
  await waitFor(() => {
171
139
  expect(screen.queryByPlaceholderText(/enter amount/i)).not.toBeInTheDocument();
@@ -1,5 +1,6 @@
1
1
  import React from 'react';
2
2
  import { DataTable, Table, TableHead, TableRow, TableHeader, TableBody, TableCell } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
3
4
  import { type MappedBill } from '../../../types';
4
5
  import { formatDate, useConfig } from '@openmrs/esm-framework';
5
6
  import { convertToCurrency } from '../../../helpers';
@@ -9,23 +10,24 @@ type PaymentHistoryProps = {
9
10
  };
10
11
 
11
12
  const PaymentHistory: React.FC<PaymentHistoryProps> = ({ bill }) => {
13
+ const { t } = useTranslation();
12
14
  const { defaultCurrency } = useConfig();
13
15
  const headers = [
14
16
  {
15
17
  key: 'dateCreated',
16
- header: 'Date of payment',
18
+ header: t('dateOfPayment', 'Date of payment'),
17
19
  },
18
20
  {
19
21
  key: 'amount',
20
- header: 'Bill amount',
22
+ header: t('billAmount', 'Bill amount'),
21
23
  },
22
24
  {
23
25
  key: 'amountTendered',
24
- header: 'Amount tendered',
26
+ header: t('amountTendered', 'Amount tendered'),
25
27
  },
26
28
  {
27
29
  key: 'paymentMethod',
28
- header: 'Payment method',
30
+ header: t('paymentMethod', 'Payment method'),
29
31
  },
30
32
  ];
31
33
  const rows = bill?.payments?.map((payment, index) => {
@@ -1,24 +1,19 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
- import { useConfig } from '@openmrs/esm-framework';
4
- import PaymentHistory from './payment-history.component';
3
+ import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
4
+ import { type BillingConfig, configSchema } from '../../../config-schema';
5
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
- }));
6
+ import PaymentHistory from './payment-history.component';
12
7
 
13
8
  jest.mock('../../../helpers', () => ({
14
9
  convertToCurrency: jest.fn((amount, currency) => `${currency} ${amount.toFixed(2)}`),
15
10
  }));
16
11
 
12
+ const mockUseConfig = jest.mocked(useConfig<BillingConfig>);
13
+
17
14
  describe('PaymentHistory Component', () => {
18
15
  beforeEach(() => {
19
- (useConfig as jest.Mock).mockReturnValue({
20
- defaultCurrency: 'USD',
21
- });
16
+ mockUseConfig.mockReturnValue({ ...getDefaultsFromConfigSchema(configSchema), defaultCurrency: 'USD' });
22
17
  });
23
18
 
24
19
  const mockBill: MappedBill = {
@@ -129,12 +124,12 @@ describe('PaymentHistory Component', () => {
129
124
  test('renders correct data in the rows', () => {
130
125
  render(<PaymentHistory bill={mockBill} />);
131
126
 
132
- expect(screen.getByText('2023-09-01')).toBeInTheDocument();
127
+ expect(screen.getByText('01-Sept-2023')).toBeInTheDocument();
133
128
  expect(screen.getByText('USD 80.00')).toBeInTheDocument();
134
129
  expect(screen.getByText('USD 100.00')).toBeInTheDocument();
135
130
  expect(screen.getByText('Credit Card')).toBeInTheDocument();
136
131
 
137
- expect(screen.getByText('2023-09-05')).toBeInTheDocument();
132
+ expect(screen.getByText('05-Sept-2023')).toBeInTheDocument();
138
133
  expect(screen.getByText('USD 180.00')).toBeInTheDocument();
139
134
  expect(screen.getByText('USD 200.00')).toBeInTheDocument();
140
135
  expect(screen.getByText('Cash')).toBeInTheDocument();
@@ -153,7 +148,7 @@ describe('PaymentHistory Component', () => {
153
148
  test('formats dates and converts amounts correctly', () => {
154
149
  render(<PaymentHistory bill={mockBill} />);
155
150
 
156
- expect(screen.getByText('2023-09-01')).toBeInTheDocument();
151
+ expect(screen.getByText('01-Sept-2023')).toBeInTheDocument();
157
152
  expect(screen.getByText('USD 80.00')).toBeInTheDocument();
158
153
  expect(screen.getByText('USD 100.00')).toBeInTheDocument();
159
154
  });
@@ -6,20 +6,19 @@ import { zodResolver } from '@hookform/resolvers/zod';
6
6
  import { navigate, showSnackbar, useConfig, useVisit } from '@openmrs/esm-framework';
7
7
  import { Button } from '@carbon/react';
8
8
  import { CardHeader } from '@openmrs/esm-patient-common-lib';
9
- import { type LineItem, type MappedBill } from '../../types';
10
- import { convertToCurrency } from '../../helpers';
11
- import { createPaymentPayload } from './utils';
12
- import { processBillPayment } from '../../billing.resource';
13
9
  import { InvoiceBreakDown } from './invoice-breakdown/invoice-breakdown.component';
14
10
  import PaymentHistory from './payment-history/payment-history.component';
15
11
  import PaymentForm from './payment-form/payment-form.component';
12
+ import { convertToCurrency } from '../../helpers';
13
+ import { createPaymentPayload } from './utils';
14
+ import { processBillPayment } from '../../billing.resource';
15
+ import { useBillableServices } from '../../billable-services/billable-service.resource';
16
16
  import { updateBillVisitAttribute } from './payment.resource';
17
+ import { type MappedBill } from '../../types';
17
18
  import styles from './payments.scss';
18
- import { useBillableServices } from '../../billable-services/billable-service.resource';
19
19
 
20
20
  type PaymentProps = {
21
21
  bill: MappedBill;
22
- selectedLineItems: Array<LineItem>;
23
22
  mutate: () => void;
24
23
  };
25
24
 
@@ -29,7 +28,7 @@ export type PaymentFormValue = {
29
28
  payment: Array<Payment>;
30
29
  };
31
30
 
32
- const Payments: React.FC<PaymentProps> = ({ bill, mutate, selectedLineItems }) => {
31
+ const Payments: React.FC<PaymentProps> = ({ bill, mutate }) => {
33
32
  const { t } = useTranslation();
34
33
  const { billableServices, isLoading, isValidating, error } = useBillableServices();
35
34
  const paymentSchema = z.object({
@@ -54,25 +53,23 @@ const Payments: React.FC<PaymentProps> = ({ bill, mutate, selectedLineItems }) =
54
53
  control: methods.control,
55
54
  });
56
55
 
57
- const selectedLineItemsTotal = selectedLineItems.reduce((total, item) => total + item.price * item.quantity, 0);
58
- const totalAmountTendered = formValues?.reduce((curr: number, prev) => curr + Number(prev.amount) ?? 0, 0) ?? 0;
59
- const amountDue = bill ? bill.totalAmount - selectedLineItemsTotal : 0;
60
- const clientBalance = bill ? bill.totalAmount - (bill.tenderedAmount + totalAmountTendered) : 0;
61
-
62
56
  const handleNavigateToBillingDashboard = () =>
63
57
  navigate({
64
58
  to: window.getOpenmrsSpaBase() + 'home/billing',
65
59
  });
66
60
 
61
+ const amountDue = bill.totalAmount - bill.tenderedAmount;
62
+
67
63
  const handleProcessPayment = () => {
68
64
  if (bill) {
65
+ const amountBeingTendered = formValues?.reduce((acc, curr) => acc + Number(curr.amount), 0);
66
+ const amountRemaining = amountDue - amountBeingTendered;
69
67
  const paymentPayload = createPaymentPayload(
70
68
  bill,
71
69
  bill?.patientUuid,
72
70
  formValues,
73
- amountDue,
71
+ amountRemaining,
74
72
  billableServices,
75
- selectedLineItems,
76
73
  );
77
74
  paymentPayload.payments.forEach((payment) => {
78
75
  payment.dateCreated = new Date(payment.dateCreated);
@@ -103,9 +100,6 @@ const Payments: React.FC<PaymentProps> = ({ bill, mutate, selectedLineItems }) =
103
100
  return null;
104
101
  }
105
102
 
106
- const amountDueLabel = selectedLineItems.length ? t('amountDue', 'Amount Due') : t('clientBalance', 'Client Balance');
107
- const amountDueValue = selectedLineItems.length ? amountDue : clientBalance;
108
-
109
103
  return (
110
104
  <FormProvider {...methods}>
111
105
  <div className={styles.wrapper}>
@@ -115,12 +109,7 @@ const Payments: React.FC<PaymentProps> = ({ bill, mutate, selectedLineItems }) =
115
109
  </CardHeader>
116
110
  <div>
117
111
  {bill && <PaymentHistory bill={bill} />}
118
- <PaymentForm
119
- disablePayment={clientBalance <= 0}
120
- clientBalance={clientBalance}
121
- isSingleLineItemSelected={selectedLineItems.length > 0}
122
- isSingleLineItem={bill.lineItems.length === 1}
123
- />
112
+ <PaymentForm disablePayment={amountDue <= 0} isSingleLineItem={bill.lineItems.length === 1} />
124
113
  </div>
125
114
  </div>
126
115
  <div className={styles.divider} />
@@ -131,13 +120,13 @@ const Payments: React.FC<PaymentProps> = ({ bill, mutate, selectedLineItems }) =
131
120
  />
132
121
  <InvoiceBreakDown
133
122
  label={t('totalTendered', 'Total Tendered')}
134
- value={convertToCurrency(bill?.tenderedAmount + totalAmountTendered, defaultCurrency)}
123
+ value={convertToCurrency(bill.tenderedAmount, defaultCurrency)}
135
124
  />
136
125
  <InvoiceBreakDown label={t('discount', 'Discount')} value={'--'} />
137
126
  <InvoiceBreakDown
138
- hasBalance={amountDueValue < 0}
139
- label={amountDueLabel}
140
- value={convertToCurrency(amountDueValue < 0 ? -amountDueValue : amountDueValue, defaultCurrency)}
127
+ hasBalance={amountDue < 0}
128
+ label={t('amountDue', 'Amount Due')}
129
+ value={convertToCurrency(amountDue < 0 ? -amountDue : amountDue, defaultCurrency)}
141
130
  />
142
131
  <div className={styles.processPayments}>
143
132
  <Button onClick={handleNavigateToBillingDashboard} kind="secondary">
@@ -1,12 +1,21 @@
1
1
  import React from 'react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import { render, screen } from '@testing-library/react';
4
- import { useVisit, useConfig, navigate } from '@openmrs/esm-framework';
4
+ import {
5
+ useVisit,
6
+ useConfig,
7
+ navigate,
8
+ getDefaultsFromConfigSchema,
9
+ type VisitReturnType,
10
+ } from '@openmrs/esm-framework';
5
11
  import { useBillableServices } from '../../billable-services/billable-service.resource';
6
12
  import { type MappedBill, type LineItem } from '../../types';
13
+ import { configSchema, type BillingConfig } from '../../config-schema';
7
14
  import Payments from './payments.component';
8
15
 
9
- // Add this mock for currency formatting
16
+ const mockUseVisit = jest.mocked(useVisit);
17
+ const mockUseConfig = jest.mocked(useConfig<BillingConfig>);
18
+ const mockUseBillableServices = jest.mocked(useBillableServices);
10
19
  const mockFormatToParts = jest.fn().mockReturnValue([{ type: 'integer', value: '1000' }]);
11
20
  const mockFormat = jest.fn().mockReturnValue('$1000.00');
12
21
  global.Intl.NumberFormat = jest.fn().mockImplementation(() => ({
@@ -87,21 +96,26 @@ describe('Payments', () => {
87
96
  const mockSelectedLineItems: LineItem[] = [];
88
97
 
89
98
  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 });
99
+ mockUseVisit.mockReturnValue({ currentVisit: null } as unknown as VisitReturnType);
100
+ mockUseConfig.mockReturnValue({ ...getDefaultsFromConfigSchema(configSchema), defaultCurrency: 'USD' });
101
+ mockUseBillableServices.mockReturnValue({
102
+ billableServices: [],
103
+ isLoading: false,
104
+ isValidating: false,
105
+ error: null,
106
+ mutate: jest.fn(),
107
+ });
94
108
  });
95
109
 
96
110
  it('renders payment form and history', () => {
97
- render(<Payments bill={mockBill} mutate={mockMutate} selectedLineItems={mockSelectedLineItems} />);
111
+ render(<Payments bill={mockBill} mutate={mockMutate} />);
98
112
  expect(screen.getByText('Payments')).toBeInTheDocument();
99
113
  expect(screen.getByText('Total Amount:')).toBeInTheDocument();
100
114
  expect(screen.getByText('Total Tendered:')).toBeInTheDocument();
101
115
  });
102
116
 
103
117
  it('calculates and displays correct amounts', () => {
104
- render(<Payments bill={mockBill} mutate={mockMutate} selectedLineItems={mockSelectedLineItems} />);
118
+ render(<Payments bill={mockBill} mutate={mockMutate} />);
105
119
  const amountElements = screen.getAllByText('$1000.00');
106
120
  expect(amountElements[amountElements.length - 3]).toBeInTheDocument();
107
121
  expect(amountElements[amountElements.length - 2]).toBeInTheDocument();
@@ -109,12 +123,12 @@ describe('Payments', () => {
109
123
  });
110
124
 
111
125
  it('disables Process Payment button when form is invalid', () => {
112
- render(<Payments bill={mockBill} mutate={mockMutate} selectedLineItems={mockSelectedLineItems} />);
126
+ render(<Payments bill={mockBill} mutate={mockMutate} />);
113
127
  expect(screen.getByText('Process Payment')).toBeDisabled();
114
128
  });
115
129
 
116
130
  it('navigates to billing dashboard when Discard is clicked', async () => {
117
- render(<Payments bill={mockBill} mutate={mockMutate} selectedLineItems={mockSelectedLineItems} />);
131
+ render(<Payments bill={mockBill} mutate={mockMutate} />);
118
132
  await userEvent.click(screen.getByText('Discard'));
119
133
  expect(navigate).toHaveBeenCalled();
120
134
  });
@@ -1,24 +1,15 @@
1
- import { type LineItem, type MappedBill } from '../../types';
1
+ import { type MappedBill } from '../../types';
2
2
  import { type Payment } from './payments.component';
3
3
 
4
- const hasLineItem = (lineItems: Array<LineItem>, item: LineItem) => {
5
- if (lineItems?.length === 0) {
6
- return false;
7
- }
8
- const foundItem = lineItems.find((lineItem) => lineItem.uuid === item.uuid);
9
- return Boolean(foundItem);
10
- };
11
-
12
4
  export const createPaymentPayload = (
13
5
  bill: MappedBill,
14
6
  patientUuid: string,
15
7
  formValues: Array<Payment>,
16
8
  amountDue: number,
17
9
  billableServices: Array<any>,
18
- selectedLineItems: Array<LineItem>,
19
10
  ) => {
20
11
  const { cashier } = bill;
21
- const totalAmount = bill?.totalAmount;
12
+ const totalAmount = bill.totalAmount ?? 0;
22
13
  const paymentStatus = amountDue <= 0 ? 'PAID' : 'PENDING';
23
14
  const previousPayments = bill?.payments.map((payment) => ({
24
15
  amount: payment.amount,
@@ -37,30 +28,21 @@ export const createPaymentPayload = (
37
28
  }));
38
29
 
39
30
  const updatedPayments = [...newPayments, ...previousPayments];
40
- const totalAmountRendered = updatedPayments.reduce((acc, payment) => acc + payment.amountTendered, 0);
41
31
 
42
32
  const updatedLineItems = bill?.lineItems.map((lineItem) => ({
43
33
  ...lineItem,
44
34
  billableService: getBillableServiceUuid(billableServices, lineItem.billableService),
45
35
  item: processBillItem?.(lineItem),
46
- paymentStatus:
47
- bill?.lineItems.length > 1
48
- ? hasLineItem(selectedLineItems ?? [], lineItem) && totalAmountRendered >= lineItem.price * lineItem.quantity
49
- ? 'PAID'
50
- : 'PENDING'
51
- : paymentStatus,
36
+ paymentStatus: lineItem.paymentStatus === 'PAID' ? 'PAID' : paymentStatus,
52
37
  }));
53
38
 
54
- const allItemsBillPaymentStatus =
55
- updatedLineItems.filter((item) => item.paymentStatus === 'PENDING').length === 0 ? 'PAID' : 'PENDING';
56
-
57
39
  const processedPayment = {
58
40
  cashPoint: bill?.cashPointUuid,
59
41
  cashier: cashier.uuid,
60
42
  lineItems: updatedLineItems,
61
43
  payments: [...updatedPayments],
62
44
  patient: patientUuid,
63
- status: selectedLineItems?.length > 0 ? allItemsBillPaymentStatus : paymentStatus,
45
+ status: paymentStatus,
64
46
  };
65
47
 
66
48
  return processedPayment;
@@ -2,8 +2,9 @@ import React, { useState } from 'react';
2
2
  import { Button } from '@carbon/react';
3
3
  import { Printer } from '@carbon/react/icons';
4
4
  import { useTranslation } from 'react-i18next';
5
- import styles from './print-receipt.scss';
5
+ import { getCoreTranslation } from '@openmrs/esm-framework';
6
6
  import { apiBasePath } from '../../constants';
7
+ import styles from './print-receipt.scss';
7
8
 
8
9
  interface PrintReceiptProps {
9
10
  billId: number;
@@ -36,7 +37,7 @@ const PrintReceipt: React.FC<PrintReceiptProps> = ({ billId }) => {
36
37
  renderIcon={(props) => <Printer size={24} {...props} />}
37
38
  onClick={handlePrintReceiptClick}
38
39
  disabled={isRedirecting}>
39
- {isRedirecting ? t('loading', 'Loading') : t('printReceipt', 'Print receipt')}
40
+ {isRedirecting ? getCoreTranslation('loading') : t('printReceipt', 'Print receipt')}
40
41
  </Button>
41
42
  );
42
43
  };
@@ -1,50 +1,39 @@
1
1
  import React from 'react';
2
- import { render, screen, fireEvent } from '@testing-library/react';
3
- import { useTranslation } from 'react-i18next';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/react';
4
4
  import PrintReceipt from './print-receipt.component';
5
5
 
6
- jest.mock('react-i18next', () => ({
7
- useTranslation: jest.fn(),
8
- }));
9
-
10
- jest.mock('@carbon/react/icons', () => ({
11
- Printer: jest.fn(() => <div data-testid="printer-icon" />),
12
- }));
13
-
14
6
  describe('PrintReceipt', () => {
15
- const mockT = jest.fn((key) => key);
16
-
17
7
  beforeEach(() => {
18
- (useTranslation as jest.Mock).mockReturnValue({ t: mockT });
19
- jest.useFakeTimers();
20
8
  window.URL.createObjectURL = jest.fn();
21
9
  });
22
10
 
23
- afterEach(() => {
24
- jest.useRealTimers();
25
- });
26
-
27
11
  it('renders button with correct text and icon', () => {
28
12
  render(<PrintReceipt billId={123} />);
29
- expect(screen.getByText('printReceipt')).toBeInTheDocument();
30
- expect(screen.getByTestId('printer-icon')).toBeInTheDocument();
13
+ expect(screen.getByText('Print receipt')).toBeInTheDocument();
31
14
  });
32
15
 
33
- it('displays "Loading" and disables button when isRedirecting is true', () => {
16
+ it('displays "Loading" and disables button when isRedirecting is true', async () => {
17
+ const user = userEvent.setup();
18
+
34
19
  render(<PrintReceipt billId={123} />);
20
+
35
21
  const button = screen.getByRole('button');
36
- fireEvent.click(button);
37
- expect(screen.getByText('loading')).toBeInTheDocument();
22
+
23
+ await user.click(button);
24
+ expect(screen.getByText(/loading/i)).toBeInTheDocument();
38
25
  expect(button).toBeDisabled();
39
26
  });
40
27
 
41
- it('applies correct CSS class to button', () => {
28
+ it('applies correct CSS class to button', async () => {
29
+ const user = userEvent.setup();
30
+
42
31
  render(<PrintReceipt billId={123} />);
43
32
  expect(screen.getByRole('button')).toHaveClass('button');
44
33
  });
45
34
 
46
35
  it('translates button text correctly', () => {
47
36
  render(<PrintReceipt billId={123} />);
48
- expect(mockT).toHaveBeenCalledWith('printReceipt', 'Print receipt');
37
+ expect(screen.getByText('Print receipt')).toBeInTheDocument();
49
38
  });
50
39
  });
@@ -3,9 +3,9 @@ import { useDefaultFacility } from '../../billing.resource';
3
3
  import styles from './printable-footer.scss';
4
4
 
5
5
  const PrintableFooter = () => {
6
- const { data, isLoading } = useDefaultFacility();
6
+ const { data } = useDefaultFacility();
7
7
 
8
- if (isLoading) {
8
+ if (!data) {
9
9
  return <div>--</div>;
10
10
  }
11
11
 
@@ -3,28 +3,19 @@ import { screen, render } from '@testing-library/react';
3
3
  import { useDefaultFacility } from '../../billing.resource';
4
4
  import PrintableFooter from './printable-footer.component';
5
5
 
6
- const mockUseDefaultFacility = useDefaultFacility as jest.MockedFunction<typeof useDefaultFacility>;
6
+ const mockUseDefaultFacility = jest.mocked<typeof useDefaultFacility>(useDefaultFacility);
7
7
 
8
8
  jest.mock('../../billing.resource', () => ({
9
9
  useDefaultFacility: jest.fn(),
10
10
  }));
11
11
 
12
12
  describe('PrintableFooter', () => {
13
- beforeEach(() => {
14
- jest.clearAllMocks();
15
- });
16
-
17
13
  test('should render PrintableFooter component', () => {
18
- mockUseDefaultFacility.mockReturnValue({ data: { display: 'MTRH', uuid: 'mtrh-uuid' }, isLoading: false });
14
+ mockUseDefaultFacility.mockReturnValue({
15
+ data: { display: 'MTRH', uuid: 'mtrh-uuid', links: [] },
16
+ });
19
17
  render(<PrintableFooter />);
20
18
  const footer = screen.getByText('MTRH');
21
19
  expect(footer).toBeInTheDocument();
22
20
  });
23
-
24
- test('should show placeholder text when facility isLoading', () => {
25
- mockUseDefaultFacility.mockReturnValue({ data: { display: 'MTRH', uuid: 'mtrh-uuid' }, isLoading: true });
26
- render(<PrintableFooter />);
27
- const footer = screen.getByText('--');
28
- expect(footer).toBeInTheDocument();
29
- });
30
21
  });
@@ -1,27 +1,28 @@
1
1
  import React from 'react';
2
2
  import { type PatientDetails } from '../../types';
3
- import { useConfig } from '@openmrs/esm-framework';
3
+ import { type SessionLocation, useConfig, interpolateUrl } from '@openmrs/esm-framework';
4
4
  import { useTranslation } from 'react-i18next';
5
- import { useDefaultFacility } from '../../billing.resource';
5
+ import type { BillingConfig } from '../../config-schema';
6
6
  import styles from './printable-invoice-header.scss';
7
+ import isEmpty from 'lodash/isEmpty';
7
8
 
8
9
  interface PrintableInvoiceHeaderProps {
9
10
  patientDetails: PatientDetails;
11
+ defaultFacility: SessionLocation | null;
10
12
  }
11
13
 
12
- const PrintableInvoiceHeader: React.FC<PrintableInvoiceHeaderProps> = ({ patientDetails }) => {
14
+ const PrintableInvoiceHeader: React.FC<PrintableInvoiceHeaderProps> = ({ patientDetails, defaultFacility }) => {
13
15
  const { t } = useTranslation();
14
- const { logo } = useConfig();
15
- const { data } = useDefaultFacility();
16
+ const { logo, country } = useConfig<BillingConfig>();
16
17
 
17
18
  return (
18
19
  <div className={styles.container}>
19
20
  <div className={styles.printableHeader}>
20
21
  <p className={styles.heading}>{t('invoice', 'Invoice')}</p>
21
- {logo?.src ? (
22
- <img className={styles.img} src={logo.src} alt={logo.alt} />
23
- ) : logo?.name ? (
24
- logo.name
22
+ {logo?.src && !isEmpty(logo.src) ? (
23
+ <img className={styles.img} src={interpolateUrl(logo.src)} alt={logo.alt} />
24
+ ) : logo?.alt && !isEmpty(logo.alt) ? (
25
+ logo.alt
25
26
  ) : (
26
27
  // OpenMRS Logo
27
28
  <svg
@@ -52,8 +53,8 @@ const PrintableInvoiceHeader: React.FC<PrintableInvoiceHeaderProps> = ({ patient
52
53
  </div>
53
54
 
54
55
  <div className={styles.facilityDetails}>
55
- <p className={styles.facilityName}>{data?.display}</p>
56
- <p className={styles.itemLabel}>Kenya</p>
56
+ <p className={styles.facilityName}>{defaultFacility?.display}</p>
57
+ <p className={styles.itemLabel}>{country}</p>
57
58
  </div>
58
59
  </div>
59
60
  </div>