@openmrs/esm-billing-app 1.0.2-pre.86 → 1.0.2-pre.863
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/.eslintrc +16 -2
- package/README.md +54 -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/3717.js +2 -0
- package/dist/3717.js.map +1 -0
- 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/4344.js +1 -0
- package/dist/4344.js.map +1 -0
- 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/6295.js +2 -0
- package/dist/{6525.js.LICENSE.txt → 6295.js.LICENSE.txt} +16 -4
- package/dist/6295.js.map +1 -0
- 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/7617.js +1 -1
- package/dist/795.js +1 -1
- 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/9214.js +1 -1
- package/dist/9538.js +1 -1
- package/dist/9569.js +1 -1
- 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 +388 -282
- package/dist/openmrs-esm-billing-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/e2e/README.md +19 -18
- package/e2e/core/test.ts +1 -1
- package/e2e/fixtures/api.ts +1 -1
- package/e2e/specs/sample-test.spec.ts +0 -1
- package/e2e/support/github/Dockerfile +1 -1
- package/package.json +13 -10
- package/src/bill-history/bill-history.component.tsx +20 -28
- 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 +21 -5
- package/src/bill-item-actions/edit-bill-item.modal.tsx +225 -0
- package/src/bill-item-actions/edit-bill-item.test.tsx +214 -40
- package/src/billable-services/bill-waiver/bill-selection.component.tsx +5 -5
- package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +28 -32
- package/src/billable-services/bill-waiver/patient-bills.component.tsx +7 -7
- package/src/billable-services/bill-waiver/utils.ts +13 -3
- package/src/billable-services/billable-service.resource.ts +42 -26
- package/src/billable-services/billable-services-home.component.tsx +4 -4
- package/src/billable-services/billable-services.component.tsx +149 -148
- package/src/billable-services/billable-services.scss +3 -0
- package/src/billable-services/billable-services.test.tsx +6 -49
- package/src/billable-services/cash-point/add-cash-point.modal.tsx +168 -0
- package/src/billable-services/cash-point/cash-point-configuration.component.tsx +19 -193
- package/src/billable-services/cash-point/cash-point-configuration.scss +1 -5
- package/src/billable-services/create-edit/add-billable-service.component.tsx +365 -299
- package/src/billable-services/create-edit/add-billable-service.scss +7 -68
- package/src/billable-services/create-edit/add-billable-service.test.tsx +167 -81
- package/src/billable-services/create-edit/edit-billable-service.modal.tsx +51 -0
- package/src/billable-services/dashboard/service-metrics.component.tsx +11 -3
- package/src/billable-services/payment-modes/add-payment-mode.modal.tsx +121 -0
- package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +74 -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 -4
- package/src/billing-dashboard/billing-dashboard.scss +1 -1
- package/src/billing-form/billing-checkin-form.component.tsx +21 -17
- package/src/billing-form/billing-checkin-form.test.tsx +99 -26
- package/src/billing-form/billing-form.component.tsx +222 -292
- package/src/billing-form/billing-form.scss +143 -0
- package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +1 -1
- package/src/billing.resource.ts +69 -74
- package/src/bills-table/bills-table.component.tsx +3 -3
- package/src/bills-table/bills-table.test.tsx +98 -54
- package/src/config-schema.ts +52 -24
- 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 +36 -70
- package/src/invoice/invoice-table.scss +8 -5
- package/src/invoice/invoice-table.test.tsx +273 -62
- package/src/invoice/invoice.component.tsx +39 -32
- package/src/invoice/invoice.scss +11 -4
- package/src/invoice/invoice.test.tsx +324 -120
- package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +9 -9
- package/src/invoice/payments/payment-form/payment-form.component.tsx +43 -34
- package/src/invoice/payments/payment-form/payment-form.scss +5 -6
- package/src/invoice/payments/payment-form/payment-form.test.tsx +216 -66
- 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 +55 -67
- package/src/invoice/payments/payments.scss +4 -3
- package/src/invoice/payments/payments.test.tsx +282 -0
- package/src/invoice/payments/utils.ts +15 -27
- 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 +20 -34
- package/src/left-panel-link.test.tsx +1 -4
- package/src/metrics-cards/metrics-cards.component.tsx +12 -2
- package/src/metrics-cards/metrics-cards.scss +4 -0
- package/src/metrics-cards/metrics-cards.test.tsx +18 -5
- package/src/modal/require-payment-modal.test.tsx +27 -22
- package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +18 -19
- package/src/routes.json +25 -7
- package/src/types/index.ts +81 -23
- package/translations/am.json +125 -74
- package/translations/ar.json +126 -75
- package/translations/ar_SY.json +126 -75
- package/translations/bn.json +128 -77
- package/translations/de.json +126 -75
- package/translations/en.json +126 -75
- package/translations/en_US.json +126 -75
- package/translations/es.json +125 -74
- package/translations/es_MX.json +126 -75
- package/translations/fr.json +131 -80
- package/translations/he.json +125 -74
- package/translations/hi.json +126 -75
- package/translations/hi_IN.json +126 -75
- package/translations/id.json +126 -75
- package/translations/it.json +152 -101
- package/translations/ka.json +126 -75
- package/translations/km.json +125 -74
- package/translations/ku.json +126 -75
- package/translations/ky.json +126 -75
- package/translations/lg.json +126 -75
- package/translations/ne.json +126 -75
- package/translations/pl.json +126 -75
- package/translations/pt.json +126 -75
- package/translations/pt_BR.json +126 -75
- package/translations/qu.json +126 -75
- package/translations/ro_RO.json +216 -165
- package/translations/ru_RU.json +126 -75
- package/translations/si.json +126 -75
- package/translations/sw.json +126 -75
- package/translations/sw_KE.json +126 -75
- package/translations/tr.json +126 -75
- package/translations/tr_TR.json +126 -75
- package/translations/uk.json +126 -75
- package/translations/uz.json +126 -75
- package/translations/uz@Latn.json +126 -75
- package/translations/uz_UZ.json +126 -75
- package/translations/vi.json +126 -75
- package/translations/zh.json +126 -75
- package/translations/zh_CN.json +158 -107
- 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/bill-item-actions/edit-bill-item.component.tsx +0 -221
- package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
- package/src/invoice/payments/payments.component.test.tsx +0 -121
- /package/dist/{8556.js.LICENSE.txt → 3717.js.LICENSE.txt} +0 -0
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import { type FetchResponse, getDefaultsFromConfigSchema, showSnackbar, useConfig } from '@openmrs/esm-framework';
|
|
5
|
+
import { configSchema, type BillingConfig } from '../config-schema';
|
|
4
6
|
import { type MappedBill } from '../types';
|
|
5
7
|
import { updateBillItems } from '../billing.resource';
|
|
6
|
-
import
|
|
8
|
+
import EditBillLineItemModal from './edit-bill-item.modal';
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
jest.
|
|
10
|
-
|
|
11
|
-
}));
|
|
10
|
+
const mockUpdateBillItems = jest.mocked(updateBillItems);
|
|
11
|
+
const mockShowSnackbar = jest.mocked(showSnackbar);
|
|
12
|
+
const mockUseConfig = jest.mocked(useConfig<BillingConfig>);
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
const mockBillableServices = [
|
|
15
|
+
{ name: 'X-Ray Service', uuid: 'xray-uuid-123' },
|
|
16
|
+
{ name: 'Lab Test Service', uuid: 'lab-uuid-456' },
|
|
17
|
+
{ name: 'Consultation Service', uuid: 'consult-uuid-789' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
jest.mock('../billing.resource', () => ({
|
|
21
|
+
updateBillItems: jest.fn().mockResolvedValue({}),
|
|
15
22
|
}));
|
|
16
23
|
|
|
17
24
|
jest.mock('../billable-services/billable-service.resource', () => ({
|
|
18
25
|
useBillableServices: jest.fn(() => ({
|
|
19
|
-
billableServices:
|
|
26
|
+
billableServices: mockBillableServices,
|
|
20
27
|
})),
|
|
21
28
|
}));
|
|
22
29
|
|
|
@@ -41,7 +48,7 @@ const mockBill: MappedBill = {
|
|
|
41
48
|
voided: false,
|
|
42
49
|
voidReason: null,
|
|
43
50
|
priceName: 'Service Price',
|
|
44
|
-
billableService: '
|
|
51
|
+
billableService: 'X-Ray Service',
|
|
45
52
|
priceUuid: 'price-uuid',
|
|
46
53
|
lineItemOrder: 1,
|
|
47
54
|
resourceVersion: '1.0',
|
|
@@ -62,7 +69,7 @@ const mockItem = {
|
|
|
62
69
|
uuid: 'item-uuid',
|
|
63
70
|
quantity: 2,
|
|
64
71
|
price: 100,
|
|
65
|
-
billableService: '
|
|
72
|
+
billableService: 'X-Ray Service',
|
|
66
73
|
paymentStatus: 'UNPAID',
|
|
67
74
|
item: 'Test Service',
|
|
68
75
|
display: 'Test Service',
|
|
@@ -74,64 +81,231 @@ const mockItem = {
|
|
|
74
81
|
resourceVersion: '1.0',
|
|
75
82
|
};
|
|
76
83
|
|
|
77
|
-
describe('
|
|
78
|
-
const closeModalMock = jest.fn();
|
|
79
|
-
|
|
84
|
+
describe('EditBillItem', () => {
|
|
80
85
|
beforeEach(() => {
|
|
81
|
-
|
|
86
|
+
mockUseConfig.mockReturnValue({
|
|
87
|
+
...getDefaultsFromConfigSchema(configSchema),
|
|
88
|
+
defaultCurrency: 'USD',
|
|
89
|
+
});
|
|
82
90
|
});
|
|
83
91
|
|
|
92
|
+
const mockCloseModal = jest.fn();
|
|
93
|
+
|
|
84
94
|
test('renders the form with correct fields and default values', () => {
|
|
85
|
-
render(<
|
|
95
|
+
render(<EditBillLineItemModal bill={mockBill} item={mockItem} closeModal={mockCloseModal} />);
|
|
86
96
|
|
|
87
|
-
expect(screen.getByText(
|
|
88
|
-
expect(screen.getByText(
|
|
89
|
-
expect(screen.
|
|
90
|
-
expect(screen.
|
|
91
|
-
expect(screen.
|
|
97
|
+
expect(screen.getByText(/edit bill line item/i)).toBeInTheDocument();
|
|
98
|
+
expect(screen.getByText(/John Doe/)).toBeInTheDocument();
|
|
99
|
+
expect(screen.getByText(/Main Cashpoint/)).toBeInTheDocument();
|
|
100
|
+
expect(screen.getByText(/123456/)).toBeInTheDocument();
|
|
101
|
+
expect(screen.getByRole('spinbutton', { name: /quantity/i })).toHaveValue(2);
|
|
102
|
+
expect(screen.getByLabelText(/unit price/i)).toHaveValue('100');
|
|
103
|
+
expect(screen.getByText(/total/i)).toHaveTextContent(/200/);
|
|
92
104
|
});
|
|
93
105
|
|
|
94
|
-
test('updates total when quantity is changed', () => {
|
|
95
|
-
|
|
106
|
+
test('updates total when quantity is changed', async () => {
|
|
107
|
+
const user = userEvent.setup();
|
|
108
|
+
render(<EditBillLineItemModal bill={mockBill} item={mockItem} closeModal={mockCloseModal} />);
|
|
96
109
|
|
|
97
|
-
const quantityInput = screen.getByRole('spinbutton', { name: /
|
|
98
|
-
|
|
110
|
+
const quantityInput = screen.getByRole('spinbutton', { name: /quantity/i });
|
|
111
|
+
await user.clear(quantityInput);
|
|
112
|
+
await user.type(quantityInput, '3');
|
|
99
113
|
|
|
100
|
-
expect(screen.getByText(/
|
|
114
|
+
expect(screen.getByText(/total/i)).toHaveTextContent(/300/);
|
|
101
115
|
});
|
|
102
116
|
|
|
103
117
|
test('submits the form and shows a success notification', async () => {
|
|
104
|
-
|
|
118
|
+
const user = userEvent.setup();
|
|
119
|
+
mockUpdateBillItems.mockResolvedValueOnce({} as FetchResponse<any>);
|
|
105
120
|
|
|
106
|
-
render(<
|
|
121
|
+
render(<EditBillLineItemModal bill={mockBill} item={mockItem} closeModal={mockCloseModal} />);
|
|
107
122
|
|
|
108
|
-
|
|
123
|
+
await user.click(screen.getByText(/save/i));
|
|
109
124
|
|
|
110
125
|
await waitFor(() => {
|
|
111
|
-
expect(
|
|
126
|
+
expect(mockUpdateBillItems).toHaveBeenCalled();
|
|
112
127
|
expect(showSnackbar).toHaveBeenCalledWith({
|
|
113
|
-
title: '
|
|
114
|
-
subtitle: '
|
|
128
|
+
title: 'Line item updated',
|
|
129
|
+
subtitle: 'The bill line item has been updated successfully',
|
|
115
130
|
kind: 'success',
|
|
116
|
-
timeoutInMs: 3000,
|
|
117
131
|
});
|
|
118
|
-
expect(
|
|
132
|
+
expect(mockCloseModal).toHaveBeenCalled();
|
|
119
133
|
});
|
|
120
134
|
});
|
|
121
135
|
|
|
122
136
|
test('shows error notification when submission fails', async () => {
|
|
123
|
-
|
|
137
|
+
const user = userEvent.setup();
|
|
138
|
+
mockUpdateBillItems.mockRejectedValueOnce({ message: 'Error occurred' });
|
|
124
139
|
|
|
125
|
-
render(<
|
|
140
|
+
render(<EditBillLineItemModal bill={mockBill} item={mockItem} closeModal={mockCloseModal} />);
|
|
126
141
|
|
|
127
|
-
|
|
142
|
+
await user.click(screen.getByText(/Save/));
|
|
128
143
|
|
|
129
144
|
await waitFor(() => {
|
|
130
|
-
expect(
|
|
131
|
-
title: '
|
|
145
|
+
expect(mockShowSnackbar).toHaveBeenCalledWith({
|
|
146
|
+
title: 'Failed to update line item',
|
|
132
147
|
kind: 'error',
|
|
133
148
|
subtitle: 'Error occurred',
|
|
134
149
|
});
|
|
135
150
|
});
|
|
136
151
|
});
|
|
152
|
+
|
|
153
|
+
test('preserves billable service UUIDs for other line items when editing', async () => {
|
|
154
|
+
const user = userEvent.setup();
|
|
155
|
+
mockUpdateBillItems.mockResolvedValueOnce({} as FetchResponse<any>);
|
|
156
|
+
|
|
157
|
+
// Bill with multiple line items with different billable services
|
|
158
|
+
const billWithMultipleItems: MappedBill = {
|
|
159
|
+
...mockBill,
|
|
160
|
+
lineItems: [
|
|
161
|
+
{
|
|
162
|
+
uuid: 'item-1',
|
|
163
|
+
quantity: 1,
|
|
164
|
+
price: 100,
|
|
165
|
+
billableService: 'X-Ray Service',
|
|
166
|
+
paymentStatus: 'PENDING',
|
|
167
|
+
item: 'X-Ray',
|
|
168
|
+
display: 'X-Ray',
|
|
169
|
+
voided: false,
|
|
170
|
+
voidReason: null,
|
|
171
|
+
priceName: 'X-Ray Price',
|
|
172
|
+
priceUuid: 'xray-price-uuid',
|
|
173
|
+
lineItemOrder: 1,
|
|
174
|
+
resourceVersion: '1.0',
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
uuid: 'item-2',
|
|
178
|
+
quantity: 2,
|
|
179
|
+
price: 50,
|
|
180
|
+
billableService: 'Lab Test Service',
|
|
181
|
+
paymentStatus: 'PENDING',
|
|
182
|
+
item: 'Lab Test',
|
|
183
|
+
display: 'Lab Test',
|
|
184
|
+
voided: false,
|
|
185
|
+
voidReason: null,
|
|
186
|
+
priceName: 'Lab Price',
|
|
187
|
+
priceUuid: 'lab-price-uuid',
|
|
188
|
+
lineItemOrder: 2,
|
|
189
|
+
resourceVersion: '1.0',
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
uuid: 'item-3',
|
|
193
|
+
quantity: 1,
|
|
194
|
+
price: 200,
|
|
195
|
+
billableService: 'Consultation Service',
|
|
196
|
+
paymentStatus: 'PENDING',
|
|
197
|
+
item: 'Consultation',
|
|
198
|
+
display: 'Consultation',
|
|
199
|
+
voided: false,
|
|
200
|
+
voidReason: null,
|
|
201
|
+
priceName: 'Consult Price',
|
|
202
|
+
priceUuid: 'consult-price-uuid',
|
|
203
|
+
lineItemOrder: 3,
|
|
204
|
+
resourceVersion: '1.0',
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Editing the Lab Test item (item-2)
|
|
210
|
+
const itemToEdit = billWithMultipleItems.lineItems[1];
|
|
211
|
+
|
|
212
|
+
render(<EditBillLineItemModal bill={billWithMultipleItems} item={itemToEdit} closeModal={mockCloseModal} />);
|
|
213
|
+
|
|
214
|
+
await user.click(screen.getByText(/Save/));
|
|
215
|
+
|
|
216
|
+
await waitFor(() => {
|
|
217
|
+
expect(mockUpdateBillItems).toHaveBeenCalled();
|
|
218
|
+
const payload = mockUpdateBillItems.mock.calls[0][0];
|
|
219
|
+
|
|
220
|
+
// Verify that each line item has the correct billable service UUID
|
|
221
|
+
const xrayItem = payload.lineItems.find((li) => li.uuid === 'item-1');
|
|
222
|
+
const consultItem = payload.lineItems.find((li) => li.uuid === 'item-3');
|
|
223
|
+
|
|
224
|
+
// These should NOT have the Lab Test UUID (lab-uuid-456)
|
|
225
|
+
// They should keep their original UUIDs
|
|
226
|
+
expect(xrayItem?.billableService).toBe('xray-uuid-123');
|
|
227
|
+
expect(consultItem?.billableService).toBe('consult-uuid-789');
|
|
228
|
+
|
|
229
|
+
// The edited item should have the Lab Test UUID
|
|
230
|
+
const labItem = payload.lineItems.find((li) => li.uuid === 'item-2');
|
|
231
|
+
expect(labItem?.billableService).toBe('lab-uuid-456');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('shows validation error for quantity less than 1', async () => {
|
|
236
|
+
const user = userEvent.setup();
|
|
237
|
+
render(<EditBillLineItemModal bill={mockBill} item={mockItem} closeModal={mockCloseModal} />);
|
|
238
|
+
|
|
239
|
+
const quantityInput = screen.getByRole('spinbutton', { name: /Quantity/ });
|
|
240
|
+
await user.clear(quantityInput);
|
|
241
|
+
await user.type(quantityInput, '0');
|
|
242
|
+
|
|
243
|
+
// Try to submit
|
|
244
|
+
await user.click(screen.getByText(/Save/));
|
|
245
|
+
|
|
246
|
+
// Should show validation error
|
|
247
|
+
await waitFor(() => {
|
|
248
|
+
expect(screen.getByText(/Quantity must be at least 1/)).toBeInTheDocument();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Should NOT call the update function
|
|
252
|
+
expect(mockUpdateBillItems).not.toHaveBeenCalled();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('shows validation error for quantity greater than 100', async () => {
|
|
256
|
+
const user = userEvent.setup();
|
|
257
|
+
render(<EditBillLineItemModal bill={mockBill} item={mockItem} closeModal={mockCloseModal} />);
|
|
258
|
+
|
|
259
|
+
const quantityInput = screen.getByRole('spinbutton', { name: /Quantity/ });
|
|
260
|
+
await user.clear(quantityInput);
|
|
261
|
+
await user.type(quantityInput, '101');
|
|
262
|
+
|
|
263
|
+
await user.click(screen.getByText(/Save/));
|
|
264
|
+
|
|
265
|
+
await waitFor(() => {
|
|
266
|
+
expect(screen.getByText(/Quantity cannot exceed 100/)).toBeInTheDocument();
|
|
267
|
+
});
|
|
268
|
+
expect(mockUpdateBillItems).not.toHaveBeenCalled();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('shows validation error for non-integer quantity', async () => {
|
|
272
|
+
const user = userEvent.setup();
|
|
273
|
+
render(<EditBillLineItemModal bill={mockBill} item={mockItem} closeModal={mockCloseModal} />);
|
|
274
|
+
|
|
275
|
+
const quantityInput = screen.getByRole('spinbutton', { name: /Quantity/ });
|
|
276
|
+
await user.clear(quantityInput);
|
|
277
|
+
await user.type(quantityInput, '2.5');
|
|
278
|
+
|
|
279
|
+
await user.click(screen.getByText(/Save/));
|
|
280
|
+
|
|
281
|
+
await waitFor(() => {
|
|
282
|
+
expect(screen.getByText(/Quantity must be a whole number/)).toBeInTheDocument();
|
|
283
|
+
});
|
|
284
|
+
expect(mockUpdateBillItems).not.toHaveBeenCalled();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('clears validation error when valid quantity is entered', async () => {
|
|
288
|
+
const user = userEvent.setup();
|
|
289
|
+
render(<EditBillLineItemModal bill={mockBill} item={mockItem} closeModal={mockCloseModal} />);
|
|
290
|
+
|
|
291
|
+
const quantityInput = screen.getByRole('spinbutton', { name: /Quantity/ });
|
|
292
|
+
|
|
293
|
+
// Enter invalid value
|
|
294
|
+
await user.clear(quantityInput);
|
|
295
|
+
await user.type(quantityInput, '0');
|
|
296
|
+
await user.click(screen.getByText(/Save/));
|
|
297
|
+
|
|
298
|
+
await waitFor(() => {
|
|
299
|
+
expect(screen.getByText(/Quantity must be at least 1/)).toBeInTheDocument();
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Fix it
|
|
303
|
+
await user.clear(quantityInput);
|
|
304
|
+
await user.type(quantityInput, '5');
|
|
305
|
+
|
|
306
|
+
// Error should disappear
|
|
307
|
+
await waitFor(() => {
|
|
308
|
+
expect(screen.queryByText(/Quantity must be at least 1/)).not.toBeInTheDocument();
|
|
309
|
+
});
|
|
310
|
+
});
|
|
137
311
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Checkbox,
|
|
4
4
|
Layer,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
StructuredListWrapper,
|
|
10
10
|
} from '@carbon/react';
|
|
11
11
|
import { useTranslation } from 'react-i18next';
|
|
12
|
-
import { useConfig } from '@openmrs/esm-framework';
|
|
12
|
+
import { getCoreTranslation, useConfig } from '@openmrs/esm-framework';
|
|
13
13
|
import { convertToCurrency } from '../../helpers';
|
|
14
14
|
import { type MappedBill, type LineItem } from '../../types';
|
|
15
15
|
import BillWaiverForm from './bill-waiver-form.component';
|
|
@@ -20,7 +20,7 @@ const PatientBillsSelections: React.FC<{ bills: MappedBill; setPatientUuid: (pat
|
|
|
20
20
|
setPatientUuid,
|
|
21
21
|
}) => {
|
|
22
22
|
const { t } = useTranslation();
|
|
23
|
-
const [selectedBills, setSelectedBills] =
|
|
23
|
+
const [selectedBills, setSelectedBills] = useState<Array<LineItem>>([]);
|
|
24
24
|
const { defaultCurrency } = useConfig();
|
|
25
25
|
|
|
26
26
|
const checkBoxLabel = (lineItem) => {
|
|
@@ -42,9 +42,9 @@ const PatientBillsSelections: React.FC<{ bills: MappedBill; setPatientUuid: (pat
|
|
|
42
42
|
<StructuredListRow head>
|
|
43
43
|
<StructuredListCell head>{t('billItem', 'Bill item')}</StructuredListCell>
|
|
44
44
|
<StructuredListCell head>{t('quantity', 'Quantity')}</StructuredListCell>
|
|
45
|
-
<StructuredListCell head>{t('unitPrice', 'Unit
|
|
45
|
+
<StructuredListCell head>{t('unitPrice', 'Unit price')}</StructuredListCell>
|
|
46
46
|
<StructuredListCell head>{t('total', 'Total')}</StructuredListCell>
|
|
47
|
-
<StructuredListCell head>{
|
|
47
|
+
<StructuredListCell head>{getCoreTranslation('actions')}</StructuredListCell>
|
|
48
48
|
</StructuredListRow>
|
|
49
49
|
</StructuredListHead>
|
|
50
50
|
<StructuredListBody>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import { Form, Stack, FormGroup, Layer, Button, NumberInput } from '@carbon/react';
|
|
3
3
|
import { TaskAdd } from '@carbon/react/icons';
|
|
4
4
|
import { mutate } from 'swr';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { showSnackbar, useConfig } from '@openmrs/esm-framework';
|
|
7
7
|
import { createBillWaiverPayload } from './utils';
|
|
8
|
-
import { convertToCurrency } from '../../helpers';
|
|
8
|
+
import { calculateTotalAmount, convertToCurrency } from '../../helpers';
|
|
9
9
|
import { processBillPayment } from '../../billing.resource';
|
|
10
10
|
import { useBillableItems } from '../../billing-form/billing-form.resource';
|
|
11
11
|
import type { LineItem, MappedBill } from '../../types';
|
|
@@ -20,16 +20,16 @@ type BillWaiverFormProps = {
|
|
|
20
20
|
|
|
21
21
|
const BillWaiverForm: React.FC<BillWaiverFormProps> = ({ bill, lineItems, setPatientUuid }) => {
|
|
22
22
|
const { t } = useTranslation();
|
|
23
|
-
const [waiverAmount, setWaiverAmount] =
|
|
24
|
-
const { lineItems: billableLineItems
|
|
25
|
-
const totalAmount = lineItems
|
|
23
|
+
const [waiverAmount, setWaiverAmount] = useState(0);
|
|
24
|
+
const { lineItems: billableLineItems } = useBillableItems();
|
|
25
|
+
const totalAmount = calculateTotalAmount(lineItems);
|
|
26
26
|
const { defaultCurrency } = useConfig();
|
|
27
27
|
|
|
28
28
|
if (lineItems?.length === 0) {
|
|
29
29
|
return null;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const handleProcessPayment = (
|
|
32
|
+
const handleProcessPayment = async () => {
|
|
33
33
|
const waiverEndPointPayload = createBillWaiverPayload(
|
|
34
34
|
bill,
|
|
35
35
|
waiverAmount,
|
|
@@ -38,30 +38,26 @@ const BillWaiverForm: React.FC<BillWaiverFormProps> = ({ bill, lineItems, setPat
|
|
|
38
38
|
billableLineItems,
|
|
39
39
|
);
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
isLowContrast: true,
|
|
62
|
-
});
|
|
63
|
-
},
|
|
64
|
-
);
|
|
41
|
+
try {
|
|
42
|
+
await processBillPayment(waiverEndPointPayload, bill.uuid);
|
|
43
|
+
showSnackbar({
|
|
44
|
+
title: t('billWaiver', 'Bill waiver'),
|
|
45
|
+
subtitle: t('billWaiverSuccess', 'Bill waiver successful'),
|
|
46
|
+
kind: 'success',
|
|
47
|
+
isLowContrast: true,
|
|
48
|
+
});
|
|
49
|
+
setPatientUuid('');
|
|
50
|
+
mutate((key) => typeof key === 'string' && key.startsWith(`${apiBasePath}bill?v=full`), undefined, {
|
|
51
|
+
revalidate: true,
|
|
52
|
+
});
|
|
53
|
+
} catch (error) {
|
|
54
|
+
showSnackbar({
|
|
55
|
+
title: t('billWaiver', 'Bill waiver'),
|
|
56
|
+
subtitle: t('billWaiverError', 'Bill waiver failed {{error}}', { error: error?.message }),
|
|
57
|
+
kind: 'error',
|
|
58
|
+
isLowContrast: true,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
65
61
|
};
|
|
66
62
|
|
|
67
63
|
return (
|
|
@@ -72,7 +68,7 @@ const BillWaiverForm: React.FC<BillWaiverFormProps> = ({ bill, lineItems, setPat
|
|
|
72
68
|
<section className={styles.billWaiverDescription}>
|
|
73
69
|
<label className={styles.label}>{t('billItems', 'Bill Items')}</label>
|
|
74
70
|
<p className={styles.value}>
|
|
75
|
-
{t('billName', '
|
|
71
|
+
{t('billName', '{{billName}}', {
|
|
76
72
|
billName: lineItems.map((item) => item.item || item.billableService).join(', ') ?? '--',
|
|
77
73
|
})}
|
|
78
74
|
</p>
|
|
@@ -84,7 +80,7 @@ const BillWaiverForm: React.FC<BillWaiverFormProps> = ({ bill, lineItems, setPat
|
|
|
84
80
|
|
|
85
81
|
<Layer className={styles.formControlLayer}>
|
|
86
82
|
<NumberInput
|
|
87
|
-
label={t('amountToWaiveLabel', 'Amount to
|
|
83
|
+
label={t('amountToWaiveLabel', 'Amount to waive')}
|
|
88
84
|
helperText={t('amountToWaiveHelper', 'Specify the amount to be deducted from the bill')}
|
|
89
85
|
aria-label={t('amountToWaiveAriaLabel', 'Enter amount to waive')}
|
|
90
86
|
hideSteppers
|
|
@@ -37,9 +37,9 @@ const PatientBills: React.FC<PatientBillsProps> = ({ patientUuid, bills, setPati
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const tableHeaders = [
|
|
40
|
-
{ header: 'Date', key: 'date' },
|
|
41
|
-
{ header: 'Billable
|
|
42
|
-
{ header: 'Total
|
|
40
|
+
{ header: t('date', 'Date'), key: 'date' },
|
|
41
|
+
{ header: t('billableService', 'Billable service'), key: 'billableService' },
|
|
42
|
+
{ header: t('totalAmount', 'Total amount'), key: 'totalAmount' },
|
|
43
43
|
];
|
|
44
44
|
|
|
45
45
|
const tableRows = bills.map((bill) => ({
|
|
@@ -58,7 +58,7 @@ const PatientBills: React.FC<PatientBillsProps> = ({ patientUuid, bills, setPati
|
|
|
58
58
|
<div className={styles.illo}>
|
|
59
59
|
<EmptyDataIllustration />
|
|
60
60
|
</div>
|
|
61
|
-
<p className={styles.content}>{t('
|
|
61
|
+
<p className={styles.content}>{t('noBillToDisplay', 'There are no bills to display for this patient')}</p>
|
|
62
62
|
</Tile>
|
|
63
63
|
</Layer>
|
|
64
64
|
</div>
|
|
@@ -84,13 +84,13 @@ const PatientBills: React.FC<PatientBillsProps> = ({ patientUuid, bills, setPati
|
|
|
84
84
|
getTableContainerProps,
|
|
85
85
|
}) => (
|
|
86
86
|
<TableContainer
|
|
87
|
-
title={t('patientBills', 'Patient
|
|
87
|
+
title={t('patientBills', 'Patient bills')}
|
|
88
88
|
description={t('patientBillsDescription', 'List of patient bills')}
|
|
89
89
|
{...getTableContainerProps()}>
|
|
90
|
-
<Table {...getTableProps()} aria-label=
|
|
90
|
+
<Table {...getTableProps()} aria-label={t('billsTable', 'Bills table')}>
|
|
91
91
|
<TableHead>
|
|
92
92
|
<TableRow>
|
|
93
|
-
<TableExpandHeader enableToggle
|
|
93
|
+
<TableExpandHeader enableToggle {...getExpandHeaderProps()} />
|
|
94
94
|
{headers.map((header, i) => (
|
|
95
95
|
<TableHeader
|
|
96
96
|
key={i}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type OpenmrsResource } from '@openmrs/esm-framework';
|
|
2
|
-
import type { LineItem, MappedBill } from '../../types';
|
|
2
|
+
import type { LineItem, MappedBill, PaymentPayload } from '../../types';
|
|
3
3
|
|
|
4
|
+
// TODO: Move this UUID to the config schema
|
|
4
5
|
const WAIVER_UUID = 'eb6173cb-9678-4614-bbe1-0ccf7ed9d1d4';
|
|
5
6
|
|
|
6
7
|
export const createBillWaiverPayload = (
|
|
@@ -12,7 +13,7 @@ export const createBillWaiverPayload = (
|
|
|
12
13
|
) => {
|
|
13
14
|
const { cashier } = bill;
|
|
14
15
|
|
|
15
|
-
const billPayment = {
|
|
16
|
+
const billPayment: PaymentPayload = {
|
|
16
17
|
amount: parseFloat(totalAmount.toFixed(2)),
|
|
17
18
|
amountTendered: parseFloat(Number(amountWaived).toFixed(2)),
|
|
18
19
|
attributes: [],
|
|
@@ -25,11 +26,20 @@ export const createBillWaiverPayload = (
|
|
|
25
26
|
paymentStatus: 'PAID',
|
|
26
27
|
}));
|
|
27
28
|
|
|
29
|
+
// Transform existing payments to PaymentPayload format
|
|
30
|
+
const existingPayments: PaymentPayload[] = bill.payments.map((payment) => ({
|
|
31
|
+
amount: payment.amount,
|
|
32
|
+
amountTendered: payment.amountTendered,
|
|
33
|
+
attributes: payment.attributes,
|
|
34
|
+
instanceType: payment.instanceType.uuid,
|
|
35
|
+
dateCreated: payment.dateCreated,
|
|
36
|
+
}));
|
|
37
|
+
|
|
28
38
|
const processedPayment = {
|
|
29
39
|
cashPoint: bill.cashPointUuid,
|
|
30
40
|
cashier: cashier.uuid,
|
|
31
41
|
lineItems: processedLineItems,
|
|
32
|
-
payments: [...
|
|
42
|
+
payments: [...existingPayments, billPayment],
|
|
33
43
|
patient: bill.patientUuid,
|
|
34
44
|
};
|
|
35
45
|
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import useSWR from 'swr';
|
|
2
2
|
import { type OpenmrsResource, openmrsFetch, restBaseUrl, useOpenmrsFetchAll, useConfig } from '@openmrs/esm-framework';
|
|
3
|
-
import { type ServiceConcept } from '../types';
|
|
4
3
|
import { apiBasePath } from '../constants';
|
|
5
|
-
import
|
|
4
|
+
import type {
|
|
5
|
+
BillableService,
|
|
6
|
+
ConceptSearchResult,
|
|
7
|
+
CreateBillableServicePayload,
|
|
8
|
+
UpdateBillableServicePayload,
|
|
9
|
+
} from '../types';
|
|
10
|
+
import type { BillingConfig } from '../config-schema';
|
|
6
11
|
|
|
7
12
|
type ResponseObject = {
|
|
8
13
|
results: Array<OpenmrsResource>;
|
|
9
14
|
};
|
|
10
15
|
|
|
11
16
|
export const useBillableServices = () => {
|
|
12
|
-
const url = `${apiBasePath}billableService?v=custom:(uuid,name,shortName,serviceStatus,concept:(uuid,display,name:(name)),serviceType:(display),servicePrices:(uuid,name,price,paymentMode:(uuid,name)))`;
|
|
13
|
-
const { data, isLoading, isValidating, error, mutate } = useOpenmrsFetchAll<BillableService
|
|
17
|
+
const url = `${apiBasePath}billableService?v=custom:(uuid,name,shortName,serviceStatus,concept:(uuid,display,name:(name)),serviceType:(display,uuid),servicePrices:(uuid,name,price,paymentMode:(uuid,name)))`;
|
|
18
|
+
const { data, isLoading, isValidating, error, mutate } = useOpenmrsFetchAll<BillableService>(url);
|
|
14
19
|
|
|
15
20
|
return {
|
|
16
21
|
billableServices: data ?? [],
|
|
@@ -22,16 +27,23 @@ export const useBillableServices = () => {
|
|
|
22
27
|
};
|
|
23
28
|
|
|
24
29
|
export function useServiceTypes() {
|
|
25
|
-
const
|
|
26
|
-
const serviceConceptUuid =
|
|
30
|
+
const { serviceTypes } = useConfig<BillingConfig>();
|
|
31
|
+
const serviceConceptUuid = serviceTypes.billableService;
|
|
27
32
|
const url = `${restBaseUrl}/concept/${serviceConceptUuid}?v=custom:(setMembers:(uuid,display))`;
|
|
28
33
|
|
|
29
|
-
const { data, error, isLoading } = useSWR<{ data }>(
|
|
34
|
+
const { data, error, isLoading } = useSWR<{ data: { setMembers: Array<{ uuid: string; display: string }> } }>(
|
|
35
|
+
url,
|
|
36
|
+
openmrsFetch,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const sortedServiceTypes = data?.data.setMembers
|
|
40
|
+
? [...data.data.setMembers].sort((a, b) => a.display.localeCompare(b.display))
|
|
41
|
+
: [];
|
|
30
42
|
|
|
31
43
|
return {
|
|
32
|
-
serviceTypes:
|
|
44
|
+
serviceTypes: sortedServiceTypes,
|
|
33
45
|
error,
|
|
34
|
-
isLoading,
|
|
46
|
+
isLoadingServiceTypes: isLoading,
|
|
35
47
|
};
|
|
36
48
|
}
|
|
37
49
|
|
|
@@ -40,28 +52,21 @@ export const usePaymentModes = () => {
|
|
|
40
52
|
|
|
41
53
|
const { data, error, isLoading } = useSWR<{ data: ResponseObject }>(url, openmrsFetch);
|
|
42
54
|
|
|
55
|
+
const sortedPaymentModes = data?.data.results
|
|
56
|
+
? [...data.data.results].sort((a, b) => a.name.localeCompare(b.name))
|
|
57
|
+
: [];
|
|
58
|
+
|
|
43
59
|
return {
|
|
44
|
-
paymentModes:
|
|
60
|
+
paymentModes: sortedPaymentModes,
|
|
45
61
|
error,
|
|
46
|
-
isLoading,
|
|
62
|
+
isLoadingPaymentModes: isLoading,
|
|
47
63
|
};
|
|
48
64
|
};
|
|
49
65
|
|
|
50
|
-
export const createBillableSerice = (payload: any) => {
|
|
51
|
-
const url = `${apiBasePath}api/billable-service`;
|
|
52
|
-
return openmrsFetch(url, {
|
|
53
|
-
method: 'POST',
|
|
54
|
-
body: payload,
|
|
55
|
-
headers: {
|
|
56
|
-
'Content-Type': 'application/json',
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
};
|
|
60
|
-
|
|
61
66
|
export function useConceptsSearch(conceptToLookup: string) {
|
|
62
67
|
const conditionsSearchUrl = `${restBaseUrl}/conceptsearch?q=${conceptToLookup}`;
|
|
63
68
|
|
|
64
|
-
const { data, error, isLoading } = useSWR<{ data: { results: Array<
|
|
69
|
+
const { data, error, isLoading } = useSWR<{ data: { results: Array<ConceptSearchResult> } }, Error>(
|
|
65
70
|
conceptToLookup ? conditionsSearchUrl : null,
|
|
66
71
|
openmrsFetch,
|
|
67
72
|
);
|
|
@@ -73,11 +78,22 @@ export function useConceptsSearch(conceptToLookup: string) {
|
|
|
73
78
|
};
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
export const
|
|
77
|
-
const url = `${apiBasePath}/
|
|
81
|
+
export const createBillableService = (payload: CreateBillableServicePayload) => {
|
|
82
|
+
const url = `${apiBasePath}api/billable-service`;
|
|
78
83
|
return openmrsFetch(url, {
|
|
79
84
|
method: 'POST',
|
|
80
|
-
body:
|
|
85
|
+
body: payload,
|
|
86
|
+
headers: {
|
|
87
|
+
'Content-Type': 'application/json',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const updateBillableService = (uuid: string, payload: UpdateBillableServicePayload) => {
|
|
93
|
+
const url = `${apiBasePath}billableService/${uuid}`;
|
|
94
|
+
return openmrsFetch(url, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
body: payload,
|
|
81
97
|
headers: {
|
|
82
98
|
'Content-Type': 'application/json',
|
|
83
99
|
},
|