@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.
- package/README.md +55 -9
- package/__mocks__/bills.mock.ts +12 -0
- package/__mocks__/react-i18next.js +6 -5
- package/dist/1119.js +1 -1
- package/dist/1146.js +1 -2
- package/dist/1146.js.map +1 -1
- package/dist/1197.js +1 -1
- package/dist/1856.js +1 -0
- package/dist/1856.js.map +1 -0
- package/dist/2146.js +1 -1
- package/dist/2177.js +2 -0
- package/dist/2177.js.LICENSE.txt +9 -0
- package/dist/2177.js.map +1 -0
- package/dist/2524.js +1 -0
- package/dist/2524.js.map +1 -0
- package/dist/2690.js +1 -1
- package/dist/3041.js +1 -0
- package/dist/3041.js.map +1 -0
- package/dist/3099.js +1 -1
- package/dist/3584.js +1 -1
- package/dist/4055.js +1 -1
- package/dist/4132.js +1 -1
- package/dist/4225.js +1 -0
- package/dist/4225.js.map +1 -0
- package/dist/4300.js +1 -1
- package/dist/4335.js +1 -1
- package/dist/4618.js +1 -1
- package/dist/4652.js +1 -1
- package/dist/4724.js +1 -0
- package/dist/4724.js.map +1 -0
- package/dist/4739.js +1 -1
- package/dist/4739.js.map +1 -1
- package/dist/4944.js +1 -1
- package/dist/5173.js +1 -1
- package/dist/5241.js +1 -1
- package/dist/5422.js +1 -0
- package/dist/5422.js.map +1 -0
- package/dist/5442.js +1 -1
- package/dist/5661.js +1 -1
- package/dist/6022.js +1 -1
- package/dist/6468.js +1 -1
- package/dist/6540.js +1 -1
- package/dist/6540.js.map +1 -1
- package/dist/6606.js +1 -0
- package/dist/6606.js.map +1 -0
- package/dist/6679.js +1 -1
- package/dist/6840.js +1 -1
- package/dist/6859.js +1 -1
- package/dist/7097.js +1 -1
- package/dist/7159.js +1 -1
- package/dist/723.js +1 -1
- package/dist/7452.js +2 -0
- package/dist/7452.js.map +1 -0
- package/dist/7617.js +1 -1
- package/dist/795.js +1 -0
- package/dist/8163.js +1 -1
- package/dist/8349.js +1 -1
- package/dist/8618.js +1 -1
- package/dist/890.js +1 -1
- package/dist/8930.js +2 -0
- package/dist/{6525.js.LICENSE.txt → 8930.js.LICENSE.txt} +16 -4
- package/dist/8930.js.map +1 -0
- package/dist/9214.js +1 -1
- package/dist/942.js +1 -0
- package/dist/942.js.map +1 -0
- package/dist/9538.js +1 -1
- package/dist/9569.js +1 -0
- package/dist/961.js +1 -1
- package/dist/961.js.map +1 -1
- package/dist/986.js +1 -1
- package/dist/9879.js +1 -1
- package/dist/9895.js +1 -1
- package/dist/9900.js +1 -1
- package/dist/9913.js +1 -1
- package/dist/main.js +1 -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 +381 -231
- package/dist/openmrs-esm-billing-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/e2e/README.md +19 -18
- package/e2e/specs/sample-test.spec.ts +0 -1
- package/package.json +10 -10
- package/src/bill-history/bill-history.component.tsx +17 -25
- package/src/bill-history/bill-history.scss +4 -94
- package/src/bill-history/bill-history.test.tsx +37 -78
- package/src/bill-item-actions/bill-item-actions.scss +0 -4
- package/src/bill-item-actions/{edit-bill-item.component.tsx → edit-bill-item.modal.tsx} +58 -56
- package/src/bill-item-actions/edit-bill-item.test.tsx +22 -24
- package/src/billable-services/bill-waiver/bill-selection.component.tsx +2 -2
- package/src/billable-services/bill-waiver/patient-bills.component.tsx +3 -3
- package/src/billable-services/billable-service.resource.ts +4 -3
- package/src/billable-services/billable-services-home.component.tsx +1 -1
- package/src/billable-services/billable-services.component.tsx +115 -132
- package/src/billable-services/billable-services.scss +3 -0
- package/src/billable-services/billable-services.test.tsx +2 -45
- package/src/billable-services/cash-point/add-cash-point.modal.tsx +168 -0
- package/src/billable-services/cash-point/cash-point-configuration.component.tsx +17 -192
- package/src/billable-services/cash-point/cash-point-configuration.scss +1 -5
- package/src/billable-services/create-edit/add-billable-service.component.tsx +28 -24
- package/src/billable-services/create-edit/add-billable-service.scss +2 -5
- package/src/billable-services/create-edit/add-billable-service.test.tsx +6 -6
- package/src/billable-services/create-edit/edit-billable-service.modal.tsx +50 -0
- package/src/billable-services/payment-modes/add-payment-mode.modal.tsx +121 -0
- package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +72 -0
- package/src/billable-services/payment-modes/payment-modes-config.component.tsx +125 -0
- package/src/billable-services/{payyment-modes → payment-modes}/payment-modes-config.scss +5 -1
- package/src/billing-form/billing-checkin-form.component.tsx +2 -3
- package/src/billing-form/billing-checkin-form.test.tsx +0 -2
- package/src/billing-form/billing-form.component.tsx +210 -268
- package/src/billing-form/billing-form.scss +143 -0
- package/src/billing.resource.ts +16 -19
- package/src/bills-table/bills-table.test.tsx +97 -53
- package/src/config-schema.ts +52 -18
- package/src/dashboard.meta.ts +4 -2
- package/src/helpers/functions.ts +5 -4
- package/src/index.ts +17 -6
- package/src/invoice/invoice-table.component.tsx +24 -54
- package/src/invoice/invoice-table.scss +1 -5
- package/src/invoice/invoice-table.test.tsx +21 -47
- package/src/invoice/invoice.component.tsx +36 -29
- package/src/invoice/invoice.scss +7 -4
- package/src/invoice/invoice.test.tsx +22 -48
- package/src/invoice/payments/payment-form/payment-form.component.tsx +2 -9
- package/src/invoice/payments/payment-form/payment-form.test.tsx +14 -46
- package/src/invoice/payments/payment-history/payment-history.component.tsx +6 -4
- package/src/invoice/payments/payment-history/payment-history.test.tsx +9 -14
- package/src/invoice/payments/payments.component.tsx +16 -27
- package/src/invoice/payments/{payments.component.test.tsx → payments.test.tsx} +24 -10
- package/src/invoice/payments/utils.ts +4 -22
- package/src/invoice/printable-invoice/print-receipt.component.tsx +3 -2
- package/src/invoice/printable-invoice/print-receipt.test.tsx +14 -25
- package/src/invoice/printable-invoice/printable-footer.component.tsx +2 -2
- package/src/invoice/printable-invoice/printable-footer.test.tsx +4 -13
- package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +12 -11
- package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +16 -14
- package/src/invoice/printable-invoice/printable-invoice.component.tsx +19 -33
- package/src/metrics-cards/metrics-cards.test.tsx +18 -5
- package/src/modal/require-payment-modal.test.tsx +25 -20
- package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +17 -18
- package/src/routes.json +22 -2
- package/src/types/index.ts +13 -12
- package/translations/am.json +33 -16
- package/translations/ar.json +33 -16
- package/translations/ar_SY.json +33 -16
- package/translations/bn.json +38 -21
- package/translations/de.json +33 -16
- package/translations/en.json +33 -16
- package/translations/en_US.json +187 -0
- package/translations/es.json +48 -31
- package/translations/es_MX.json +33 -16
- package/translations/fr.json +47 -30
- package/translations/he.json +33 -16
- package/translations/hi.json +33 -16
- package/translations/hi_IN.json +33 -16
- package/translations/id.json +180 -163
- package/translations/it.json +70 -53
- package/translations/ka.json +187 -0
- package/translations/km.json +33 -16
- package/translations/ku.json +33 -16
- package/translations/ky.json +33 -16
- package/translations/lg.json +33 -16
- package/translations/ne.json +33 -16
- package/translations/pl.json +33 -16
- package/translations/pt.json +33 -16
- package/translations/pt_BR.json +33 -16
- package/translations/qu.json +33 -16
- package/translations/ro_RO.json +182 -165
- package/translations/ru_RU.json +33 -16
- package/translations/si.json +33 -16
- package/translations/sw.json +33 -16
- package/translations/sw_KE.json +33 -16
- package/translations/tr.json +33 -16
- package/translations/tr_TR.json +33 -16
- package/translations/uk.json +33 -16
- package/translations/uz.json +33 -16
- package/translations/uz@Latn.json +33 -16
- package/translations/uz_UZ.json +33 -16
- package/translations/vi.json +33 -16
- package/translations/zh.json +33 -16
- package/translations/zh_CN.json +91 -74
- package/dist/1146.js.LICENSE.txt +0 -21
- package/dist/2352.js +0 -1
- package/dist/2352.js.map +0 -1
- package/dist/246.js +0 -1
- package/dist/246.js.map +0 -1
- package/dist/6525.js +0 -2
- package/dist/6525.js.map +0 -1
- package/dist/8556.js +0 -2
- package/dist/8556.js.map +0 -1
- package/dist/8638.js +0 -1
- package/dist/8638.js.map +0 -1
- package/dist/9968.js +0 -1
- package/dist/9968.js.map +0 -1
- package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
- /package/dist/{8556.js.LICENSE.txt → 7452.js.LICENSE.txt} +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
133
|
+
await user.click(screen.getByText(/add payment option/i));
|
|
166
134
|
|
|
167
135
|
const trashCanIcon = screen.getByTestId('trash-can-icon');
|
|
168
|
-
|
|
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
|
|
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
|
-
(
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
|
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
|
-
|
|
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
|
|
123
|
+
value={convertToCurrency(bill.tenderedAmount, defaultCurrency)}
|
|
135
124
|
/>
|
|
136
125
|
<InvoiceBreakDown label={t('discount', 'Discount')} value={'--'} />
|
|
137
126
|
<InvoiceBreakDown
|
|
138
|
-
hasBalance={
|
|
139
|
-
label={
|
|
140
|
-
value={convertToCurrency(
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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 ?
|
|
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
|
|
3
|
-
import {
|
|
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('
|
|
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
|
-
|
|
37
|
-
|
|
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(
|
|
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
|
|
6
|
+
const { data } = useDefaultFacility();
|
|
7
7
|
|
|
8
|
-
if (
|
|
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 =
|
|
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({
|
|
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 {
|
|
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?.
|
|
24
|
-
logo.
|
|
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}>{
|
|
56
|
-
<p className={styles.itemLabel}>
|
|
56
|
+
<p className={styles.facilityName}>{defaultFacility?.display}</p>
|
|
57
|
+
<p className={styles.itemLabel}>{country}</p>
|
|
57
58
|
</div>
|
|
58
59
|
</div>
|
|
59
60
|
</div>
|