@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.
- package/.eslintignore +0 -1
- package/.eslintrc +33 -24
- package/.husky/pre-commit +1 -1
- package/.turbo.json +1 -1
- package/.tx/config +11 -0
- package/README.md +111 -1
- package/dist/1119.js +1 -0
- package/dist/1197.js +1 -0
- package/dist/1362.js +1 -0
- package/dist/1362.js.map +1 -0
- package/dist/2146.js +1 -0
- package/dist/2690.js +1 -0
- package/dist/3029.js +2 -0
- package/dist/3029.js.LICENSE.txt +7 -0
- package/dist/3029.js.map +1 -0
- package/dist/3099.js +1 -0
- package/dist/3511.js +1 -0
- package/dist/3511.js.map +1 -0
- package/dist/3584.js +1 -0
- package/dist/4055.js +1 -0
- package/dist/4132.js +1 -0
- package/dist/4225.js +1 -0
- package/dist/4225.js.map +1 -0
- package/dist/4300.js +1 -0
- package/dist/4335.js +1 -0
- package/dist/4618.js +1 -0
- package/dist/4652.js +1 -0
- package/dist/4817.js +2 -0
- package/dist/4817.js.LICENSE.txt +77 -0
- package/dist/4817.js.map +1 -0
- package/dist/4944.js +1 -0
- package/dist/4993.js +1 -0
- package/dist/4993.js.map +1 -0
- package/dist/5173.js +1 -0
- package/dist/5241.js +1 -0
- package/dist/5442.js +1 -0
- package/dist/5661.js +1 -0
- package/dist/6022.js +1 -0
- package/dist/6468.js +1 -0
- package/dist/6540.js +2 -0
- package/dist/6540.js.map +1 -0
- package/dist/6606.js +2 -0
- package/dist/{591.js.LICENSE.txt → 6606.js.LICENSE.txt} +2 -2
- package/dist/6606.js.map +1 -0
- package/dist/6679.js +1 -0
- package/dist/6840.js +1 -0
- package/dist/6859.js +1 -0
- package/dist/6941.js +1 -0
- package/dist/6941.js.map +1 -0
- package/dist/7097.js +1 -0
- package/dist/7159.js +1 -0
- package/dist/723.js +1 -0
- package/dist/7255.js +1 -0
- package/dist/7255.js.map +1 -0
- package/dist/7617.js +1 -0
- package/dist/763.js +1 -0
- package/dist/763.js.map +1 -0
- package/dist/8163.js +1 -0
- package/dist/8349.js +1 -0
- package/dist/8618.js +1 -0
- package/dist/890.js +1 -0
- package/dist/9055.js +1 -0
- package/dist/9055.js.map +1 -0
- package/dist/9214.js +1 -0
- package/dist/9538.js +1 -0
- package/dist/{935.js → 961.js} +2 -2
- package/dist/{935.js.map → 961.js.map} +1 -1
- package/dist/986.js +1 -0
- package/dist/9879.js +1 -0
- package/dist/9895.js +1 -0
- package/dist/9900.js +1 -0
- package/dist/9913.js +1 -0
- package/dist/main.js +1 -1
- package/dist/main.js.LICENSE.txt +31 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-billing-app.js +1 -1
- package/dist/openmrs-esm-billing-app.js.buildmanifest.json +844 -165
- package/dist/openmrs-esm-billing-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/jest.config.js +4 -1
- package/package.json +19 -21
- package/src/bill-history/bill-history.component.tsx +5 -3
- package/src/bill-history/bill-history.scss +24 -9
- package/src/bill-history/bill-history.test.tsx +58 -16
- package/src/bill-item-actions/bill-item-actions.scss +26 -0
- package/src/bill-item-actions/edit-bill-item.component.tsx +221 -0
- package/src/bill-item-actions/edit-bill-item.test.tsx +137 -0
- package/src/billable-services/bill-waiver/bill-selection.component.tsx +1 -1
- package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +2 -2
- package/src/billable-services/bill-waiver/bill-waiver-form.scss +4 -4
- package/src/billable-services/bill-waiver/bill-waiver.component.tsx +4 -4
- package/src/billable-services/bill-waiver/patient-bills.component.tsx +1 -1
- package/src/billable-services/billable-service.resource.ts +19 -6
- package/src/billable-services/billable-services-home.component.tsx +19 -3
- package/src/billable-services/billable-services-menu-item/item.component.tsx +17 -0
- package/src/billable-services/billable-services-menu-item/item.scss +14 -0
- package/src/billable-services/billable-services.component.tsx +48 -9
- package/src/billable-services/billable-services.scss +10 -9
- package/src/billable-services/billable-services.test.tsx +172 -8
- package/src/billable-services/cash-point/cash-point-configuration.component.tsx +276 -0
- package/src/billable-services/cash-point/cash-point-configuration.scss +23 -0
- package/src/billable-services/create-edit/add-billable-service.component.tsx +126 -47
- package/src/billable-services/create-edit/add-billable-service.scss +14 -8
- package/src/billable-services/create-edit/add-billable-service.test.tsx +12 -10
- package/src/billable-services/dashboard/dashboard.scss +3 -3
- package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +280 -0
- package/src/billable-services/payyment-modes/payment-modes-config.scss +23 -0
- package/src/billing-dashboard/billing-dashboard.component.tsx +17 -4
- package/src/billing-dashboard/billing-dashboard.scss +3 -3
- package/src/billing-form/billing-form.component.tsx +31 -25
- package/src/billing-form/billing-form.scss +9 -10
- package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +38 -14
- package/src/billing-header/billing-header.component.tsx +21 -5
- package/src/billing-header/billing-header.scss +1 -1
- package/src/billing.resource.ts +21 -4
- package/src/bills-table/bills-table.component.tsx +46 -36
- package/src/bills-table/bills-table.scss +6 -6
- package/src/bills-table/bills-table.test.tsx +108 -68
- package/src/config-schema.ts +36 -1
- package/src/constants.ts +2 -0
- package/src/dashboard.meta.ts +2 -1
- package/src/helpers/functions.ts +0 -2
- package/src/hooks/selectedDateContext.ts +10 -0
- package/src/index.ts +22 -27
- package/src/invoice/invoice-table.component.tsx +95 -56
- package/src/invoice/invoice-table.scss +7 -8
- package/src/invoice/invoice-table.test.tsx +151 -0
- package/src/invoice/invoice.component.tsx +7 -9
- package/src/invoice/invoice.scss +2 -2
- package/src/invoice/invoice.test.tsx +199 -169
- package/src/invoice/payments/payment-form/payment-form.component.tsx +84 -55
- package/src/invoice/payments/payment-form/payment-form.test.tsx +174 -0
- package/src/invoice/payments/payment-history/payment-history.component.tsx +9 -7
- package/src/invoice/payments/payment-history/payment-history.test.tsx +160 -0
- package/src/invoice/payments/payments.component.test.tsx +121 -0
- package/src/invoice/payments/payments.component.tsx +57 -48
- package/src/invoice/payments/utils.ts +17 -13
- package/src/invoice/printable-invoice/print-receipt.component.tsx +23 -8
- package/src/invoice/printable-invoice/print-receipt.test.tsx +50 -0
- package/src/metrics-cards/card.component.tsx +4 -2
- package/src/metrics-cards/metrics-cards.test.tsx +1 -1
- package/src/modal/require-payment-modal.component.tsx +2 -2
- package/src/modal/require-payment-modal.test.tsx +66 -0
- package/src/modal/require-payment.scss +2 -1
- package/src/routes.json +40 -8
- package/src/types/index.ts +15 -0
- package/{i18next-parser.config.js → tools/i18next-parser.config.js} +19 -19
- package/tools/update-openmrs-deps.mjs +42 -0
- package/translations/am.json +53 -0
- package/translations/ar.json +170 -0
- package/translations/ar_SY.json +170 -0
- package/translations/bn.json +170 -0
- package/translations/de.json +170 -0
- package/translations/en.json +53 -0
- package/translations/es.json +53 -0
- package/translations/es_MX.json +170 -0
- package/translations/fr.json +53 -0
- package/translations/he.json +53 -0
- package/translations/hi.json +170 -0
- package/translations/hi_IN.json +170 -0
- package/translations/id.json +170 -0
- package/translations/it.json +170 -0
- package/translations/km.json +53 -0
- package/translations/ku.json +170 -0
- package/translations/ky.json +170 -0
- package/translations/lg.json +170 -0
- package/translations/ne.json +170 -0
- package/translations/pl.json +170 -0
- package/translations/pt.json +170 -0
- package/translations/pt_BR.json +170 -0
- package/translations/qu.json +170 -0
- package/translations/ro_RO.json +170 -0
- package/translations/ru_RU.json +170 -0
- package/translations/si.json +170 -0
- package/translations/sw.json +170 -0
- package/translations/sw_KE.json +170 -0
- package/translations/tr.json +170 -0
- package/translations/tr_TR.json +170 -0
- package/translations/uk.json +170 -0
- package/translations/uz.json +170 -0
- package/translations/uz@Latn.json +170 -0
- package/translations/uz_UZ.json +170 -0
- package/translations/vi.json +170 -0
- package/translations/zh.json +170 -0
- package/translations/zh_CN.json +170 -0
- package/tsconfig.json +10 -8
- package/webpack.config.js +1 -1
- package/dist/146.js +0 -1
- package/dist/146.js.map +0 -1
- package/dist/294.js +0 -2
- package/dist/294.js.map +0 -1
- package/dist/319.js +0 -1
- package/dist/384.js +0 -1
- package/dist/384.js.map +0 -1
- package/dist/421.js +0 -1
- package/dist/421.js.map +0 -1
- package/dist/533.js +0 -1
- package/dist/533.js.map +0 -1
- package/dist/574.js +0 -1
- package/dist/591.js +0 -2
- package/dist/591.js.map +0 -1
- package/dist/614.js +0 -2
- package/dist/614.js.LICENSE.txt +0 -37
- package/dist/614.js.map +0 -1
- package/dist/753.js +0 -1
- package/dist/753.js.map +0 -1
- package/dist/757.js +0 -1
- package/dist/770.js +0 -1
- package/dist/770.js.map +0 -1
- package/dist/783.js +0 -1
- package/dist/783.js.map +0 -1
- package/dist/788.js +0 -1
- package/dist/800.js +0 -2
- package/dist/800.js.LICENSE.txt +0 -3
- package/dist/800.js.map +0 -1
- package/dist/807.js +0 -1
- package/dist/833.js +0 -1
- package/dist/992.js +0 -1
- package/dist/992.js.map +0 -1
- package/src/root.scss +0 -30
- /package/dist/{294.js.LICENSE.txt → 6540.js.LICENSE.txt} +0 -0
- /package/dist/{935.js.LICENSE.txt → 961.js.LICENSE.txt} +0 -0
- /package/{src → tools}/setup-tests.ts +0 -0
- /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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 {
|
|
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
|
|
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 =
|
|
60
|
-
const
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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={
|
|
115
|
-
label={
|
|
116
|
-
value={convertToCurrency(
|
|
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;
|