@openmrs/esm-billing-app 1.0.2-pre.84 → 1.0.2-pre.849
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 +28 -12
- 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 +356 -300
- package/src/billable-services/create-edit/add-billable-service.scss +6 -65
- 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 +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 -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 +80 -18
- 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
|
@@ -6,37 +6,42 @@ 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
9
|
import { convertToCurrency } from '../../helpers';
|
|
11
10
|
import { createPaymentPayload } from './utils';
|
|
12
|
-
import { processBillPayment } from '../../billing.resource';
|
|
13
11
|
import { InvoiceBreakDown } from './invoice-breakdown/invoice-breakdown.component';
|
|
14
|
-
import
|
|
15
|
-
import
|
|
12
|
+
import { processBillPayment } from '../../billing.resource';
|
|
13
|
+
import { type MappedBill } from '../../types';
|
|
16
14
|
import { updateBillVisitAttribute } from './payment.resource';
|
|
17
|
-
import styles from './payments.scss';
|
|
18
15
|
import { useBillableServices } from '../../billable-services/billable-service.resource';
|
|
16
|
+
import PaymentForm from './payment-form/payment-form.component';
|
|
17
|
+
import PaymentHistory from './payment-history/payment-history.component';
|
|
18
|
+
import styles from './payments.scss';
|
|
19
19
|
|
|
20
20
|
type PaymentProps = {
|
|
21
21
|
bill: MappedBill;
|
|
22
|
-
selectedLineItems: Array<LineItem>;
|
|
23
22
|
mutate: () => void;
|
|
24
23
|
};
|
|
25
24
|
|
|
26
|
-
export type Payment = { method: string; amount:
|
|
25
|
+
export type Payment = { method: string; amount: number | undefined; referenceCode?: number | string };
|
|
27
26
|
|
|
28
27
|
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
|
-
const { billableServices
|
|
33
|
+
const { billableServices } = useBillableServices();
|
|
35
34
|
const paymentSchema = z.object({
|
|
36
35
|
method: z.string().refine((value) => !!value, 'Payment method is required'),
|
|
37
36
|
amount: z
|
|
38
|
-
.number(
|
|
39
|
-
|
|
37
|
+
.number({
|
|
38
|
+
required_error: t('amountRequired', 'Amount is required'),
|
|
39
|
+
invalid_type_error: t('amountRequired', 'Amount is required'),
|
|
40
|
+
})
|
|
41
|
+
.positive({ message: t('amountMustBePositive', 'Amount must be greater than 0') })
|
|
42
|
+
.max(bill?.totalAmount - bill?.tenderedAmount, {
|
|
43
|
+
message: t('paymentAmountCannotExceedAmountDue', 'Payment amount cannot exceed amount due'),
|
|
44
|
+
}),
|
|
40
45
|
referenceCode: z.union([z.number(), z.string()]).optional(),
|
|
41
46
|
});
|
|
42
47
|
|
|
@@ -54,57 +59,45 @@ const Payments: React.FC<PaymentProps> = ({ bill, mutate, selectedLineItems }) =
|
|
|
54
59
|
control: methods.control,
|
|
55
60
|
});
|
|
56
61
|
|
|
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
62
|
const handleNavigateToBillingDashboard = () =>
|
|
63
63
|
navigate({
|
|
64
64
|
to: window.getOpenmrsSpaBase() + 'home/billing',
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
const handleProcessPayment = () => {
|
|
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
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
67
|
if (!bill) {
|
|
103
68
|
return null;
|
|
104
69
|
}
|
|
105
70
|
|
|
106
|
-
const
|
|
107
|
-
|
|
71
|
+
const amountDue = bill.totalAmount - bill.tenderedAmount;
|
|
72
|
+
|
|
73
|
+
const handleProcessPayment = async () => {
|
|
74
|
+
const amountBeingTendered = formValues?.reduce((acc, curr) => acc + (curr.amount || 0), 0);
|
|
75
|
+
const amountRemaining = amountDue - amountBeingTendered;
|
|
76
|
+
const paymentPayload = createPaymentPayload(bill, bill?.patientUuid, formValues, amountRemaining, billableServices);
|
|
77
|
+
paymentPayload.payments.forEach((payment) => {
|
|
78
|
+
payment.dateCreated = new Date(payment.dateCreated);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await processBillPayment(paymentPayload, bill.uuid);
|
|
83
|
+
showSnackbar({
|
|
84
|
+
title: t('billPayment', 'Bill payment'),
|
|
85
|
+
subtitle: t('paymentProcessedSuccessfully', 'Payment processed successfully'),
|
|
86
|
+
kind: 'success',
|
|
87
|
+
});
|
|
88
|
+
if (currentVisit) {
|
|
89
|
+
updateBillVisitAttribute(currentVisit);
|
|
90
|
+
}
|
|
91
|
+
methods.reset({ payment: [{ method: '', amount: null, referenceCode: '' }] });
|
|
92
|
+
mutate();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
showSnackbar({
|
|
95
|
+
title: t('errorProcessingPayment', 'Error processing payment'),
|
|
96
|
+
kind: 'error',
|
|
97
|
+
subtitle: error?.message,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
};
|
|
108
101
|
|
|
109
102
|
return (
|
|
110
103
|
<FormProvider {...methods}>
|
|
@@ -114,36 +107,31 @@ const Payments: React.FC<PaymentProps> = ({ bill, mutate, selectedLineItems }) =
|
|
|
114
107
|
<span></span>
|
|
115
108
|
</CardHeader>
|
|
116
109
|
<div>
|
|
117
|
-
|
|
118
|
-
<PaymentForm
|
|
119
|
-
disablePayment={clientBalance <= 0}
|
|
120
|
-
clientBalance={clientBalance}
|
|
121
|
-
isSingleLineItemSelected={selectedLineItems.length > 0}
|
|
122
|
-
isSingleLineItem={bill.lineItems.length === 1}
|
|
123
|
-
/>
|
|
110
|
+
<PaymentHistory bill={bill} />
|
|
111
|
+
<PaymentForm disablePayment={amountDue <= 0} isSingleLineItem={bill.lineItems.length === 1} />
|
|
124
112
|
</div>
|
|
125
113
|
</div>
|
|
126
114
|
<div className={styles.divider} />
|
|
127
115
|
<div className={styles.paymentTotals}>
|
|
128
116
|
<InvoiceBreakDown
|
|
129
|
-
label={t('totalAmount', 'Total
|
|
117
|
+
label={t('totalAmount', 'Total amount')}
|
|
130
118
|
value={convertToCurrency(bill.totalAmount, defaultCurrency)}
|
|
131
119
|
/>
|
|
132
120
|
<InvoiceBreakDown
|
|
133
|
-
label={t('totalTendered', 'Total
|
|
134
|
-
value={convertToCurrency(bill
|
|
121
|
+
label={t('totalTendered', 'Total tendered')}
|
|
122
|
+
value={convertToCurrency(bill.tenderedAmount, defaultCurrency)}
|
|
135
123
|
/>
|
|
136
124
|
<InvoiceBreakDown label={t('discount', 'Discount')} value={'--'} />
|
|
137
125
|
<InvoiceBreakDown
|
|
138
|
-
hasBalance={
|
|
139
|
-
label={
|
|
140
|
-
value={convertToCurrency(
|
|
126
|
+
hasBalance={amountDue < 0}
|
|
127
|
+
label={t('amountDue', 'Amount due')}
|
|
128
|
+
value={convertToCurrency(amountDue < 0 ? -amountDue : amountDue, defaultCurrency)}
|
|
141
129
|
/>
|
|
142
130
|
<div className={styles.processPayments}>
|
|
143
131
|
<Button onClick={handleNavigateToBillingDashboard} kind="secondary">
|
|
144
132
|
{t('discard', 'Discard')}
|
|
145
133
|
</Button>
|
|
146
|
-
<Button onClick={
|
|
134
|
+
<Button onClick={handleProcessPayment} disabled={!formValues?.length || !methods.formState.isValid}>
|
|
147
135
|
{t('processPayment', 'Process Payment')}
|
|
148
136
|
</Button>
|
|
149
137
|
</div>
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
.paymentContainer {
|
|
17
17
|
margin: layout.$layout-01 0;
|
|
18
|
-
padding: layout.$layout-01;
|
|
18
|
+
padding: layout.$layout-01 0;
|
|
19
19
|
width: 70%;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -32,14 +32,15 @@
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
.paymentTotals {
|
|
35
|
-
margin: layout.$spacing-05
|
|
35
|
+
margin: layout.$spacing-05 auto;
|
|
36
36
|
padding: layout.$spacing-07 layout.$spacing-05;
|
|
37
|
+
max-width: 90%;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
.processPayments {
|
|
40
41
|
display: flex;
|
|
41
42
|
justify-content: flex-end;
|
|
42
|
-
margin: layout.$spacing-05;
|
|
43
|
+
margin: layout.$spacing-05 0;
|
|
43
44
|
padding-top: layout.$spacing-05;
|
|
44
45
|
column-gap: layout.$spacing-04;
|
|
45
46
|
border-top: 1px solid colors.$cool-gray-40;
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import {
|
|
5
|
+
useVisit,
|
|
6
|
+
useConfig,
|
|
7
|
+
navigate,
|
|
8
|
+
getDefaultsFromConfigSchema,
|
|
9
|
+
type VisitReturnType,
|
|
10
|
+
} from '@openmrs/esm-framework';
|
|
11
|
+
import { useBillableServices } from '../../billable-services/billable-service.resource';
|
|
12
|
+
import { type MappedBill } from '../../types';
|
|
13
|
+
import { configSchema, type BillingConfig } from '../../config-schema';
|
|
14
|
+
import { usePaymentModes } from './payment.resource';
|
|
15
|
+
import Payments from './payments.component';
|
|
16
|
+
|
|
17
|
+
const mockUseVisit = jest.mocked(useVisit);
|
|
18
|
+
const mockUseConfig = jest.mocked(useConfig<BillingConfig>);
|
|
19
|
+
const mockUseBillableServices = jest.mocked(useBillableServices);
|
|
20
|
+
const mockUsePaymentModes = jest.mocked(usePaymentModes);
|
|
21
|
+
const mockFormatToParts = jest.fn().mockReturnValue([{ type: 'integer', value: '1000' }]);
|
|
22
|
+
const mockFormat = jest.fn().mockReturnValue('$1000.00');
|
|
23
|
+
const mockResolvedOptions = jest.fn().mockReturnValue({
|
|
24
|
+
locale: 'en-US',
|
|
25
|
+
numberingSystem: 'latn',
|
|
26
|
+
style: 'currency',
|
|
27
|
+
currency: 'USD',
|
|
28
|
+
minimumFractionDigits: 2,
|
|
29
|
+
maximumFractionDigits: 2,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
global.Intl.NumberFormat.supportedLocalesOf = jest.fn().mockReturnValue(['en-US']);
|
|
33
|
+
global.Intl.NumberFormat = jest.fn().mockImplementation(() => ({
|
|
34
|
+
formatToParts: mockFormatToParts,
|
|
35
|
+
format: mockFormat,
|
|
36
|
+
resolvedOptions: mockResolvedOptions,
|
|
37
|
+
})) as any;
|
|
38
|
+
|
|
39
|
+
jest.mock('../../billing.resource', () => ({
|
|
40
|
+
processBillPayment: jest.fn(),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
jest.mock('./payment.resource', () => ({
|
|
44
|
+
updateBillVisitAttribute: jest.fn(),
|
|
45
|
+
usePaymentModes: jest.fn(),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
jest.mock('../../billable-services/billable-service.resource', () => ({
|
|
49
|
+
useBillableServices: jest.fn(),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
describe('Payments', () => {
|
|
53
|
+
const mockBill: MappedBill = {
|
|
54
|
+
uuid: 'bill-uuid',
|
|
55
|
+
id: 1,
|
|
56
|
+
patientUuid: 'patient-uuid',
|
|
57
|
+
patientName: 'John Doe',
|
|
58
|
+
cashPointUuid: 'cash-point-uuid',
|
|
59
|
+
cashPointName: 'Main Cash Point',
|
|
60
|
+
cashPointLocation: 'Main Hospital',
|
|
61
|
+
cashier: {
|
|
62
|
+
uuid: 'provider-1',
|
|
63
|
+
display: 'Jane Doe',
|
|
64
|
+
links: [
|
|
65
|
+
{
|
|
66
|
+
rel: 'self',
|
|
67
|
+
uri: 'http://example.com/provider/1',
|
|
68
|
+
resourceAlias: 'Jane Doe',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
payments: [
|
|
73
|
+
{
|
|
74
|
+
uuid: 'payment-1',
|
|
75
|
+
dateCreated: new Date('2023-09-01T12:00:00Z').getTime(),
|
|
76
|
+
amountTendered: 100,
|
|
77
|
+
amount: 80,
|
|
78
|
+
instanceType: {
|
|
79
|
+
uuid: 'instance-1',
|
|
80
|
+
name: 'Credit Card',
|
|
81
|
+
description: 'Credit Card payment',
|
|
82
|
+
retired: false,
|
|
83
|
+
},
|
|
84
|
+
attributes: [],
|
|
85
|
+
voided: false,
|
|
86
|
+
resourceVersion: '1.0',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
uuid: 'payment-2',
|
|
90
|
+
dateCreated: new Date('2023-09-05T14:00:00Z').getTime(),
|
|
91
|
+
amountTendered: 200,
|
|
92
|
+
amount: 180,
|
|
93
|
+
instanceType: {
|
|
94
|
+
uuid: 'instance-2',
|
|
95
|
+
name: 'Cash',
|
|
96
|
+
description: 'Cash payment',
|
|
97
|
+
retired: false,
|
|
98
|
+
},
|
|
99
|
+
attributes: [],
|
|
100
|
+
voided: false,
|
|
101
|
+
resourceVersion: '1.0',
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
receiptNumber: '12345',
|
|
105
|
+
status: 'PAID',
|
|
106
|
+
identifier: 'invoice-123',
|
|
107
|
+
dateCreated: '2023-09-01T12:00:00Z',
|
|
108
|
+
lineItems: [],
|
|
109
|
+
billingService: 'Billing Service',
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const mockMutate = jest.fn();
|
|
113
|
+
|
|
114
|
+
beforeEach(() => {
|
|
115
|
+
mockUseVisit.mockReturnValue({ currentVisit: null } as unknown as VisitReturnType);
|
|
116
|
+
mockUseConfig.mockReturnValue({ ...getDefaultsFromConfigSchema(configSchema), defaultCurrency: 'USD' });
|
|
117
|
+
mockUseBillableServices.mockReturnValue({
|
|
118
|
+
billableServices: [],
|
|
119
|
+
isLoading: false,
|
|
120
|
+
isValidating: false,
|
|
121
|
+
error: null,
|
|
122
|
+
mutate: jest.fn(),
|
|
123
|
+
});
|
|
124
|
+
mockUsePaymentModes.mockReturnValue({
|
|
125
|
+
paymentModes: [
|
|
126
|
+
{ uuid: '1', name: 'Cash', description: 'Cash payment', retired: false },
|
|
127
|
+
{ uuid: '2', name: 'Credit Card', description: 'Credit Card payment', retired: false },
|
|
128
|
+
],
|
|
129
|
+
isLoading: false,
|
|
130
|
+
error: null,
|
|
131
|
+
mutate: jest.fn(),
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('renders payment form and history', () => {
|
|
136
|
+
render(<Payments bill={mockBill} mutate={mockMutate} />);
|
|
137
|
+
expect(screen.getByText(/payments/i)).toBeInTheDocument();
|
|
138
|
+
expect(screen.getByText(/total amount:/i)).toBeInTheDocument();
|
|
139
|
+
expect(screen.getByText(/total tendered:/i)).toBeInTheDocument();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('displays formatted currency amounts', () => {
|
|
143
|
+
render(<Payments bill={mockBill} mutate={mockMutate} />);
|
|
144
|
+
// Verify that currency formatting is applied (mocked to return '$1000.00')
|
|
145
|
+
const formattedAmounts = screen.getAllByText('$1000.00');
|
|
146
|
+
expect(formattedAmounts.length).toBeGreaterThan(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('disables Process Payment button when form is invalid', () => {
|
|
150
|
+
render(<Payments bill={mockBill} mutate={mockMutate} />);
|
|
151
|
+
expect(screen.getByText('Process Payment')).toBeDisabled();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('navigates to billing dashboard when Discard is clicked', async () => {
|
|
155
|
+
render(<Payments bill={mockBill} mutate={mockMutate} />);
|
|
156
|
+
await userEvent.click(screen.getByText('Discard'));
|
|
157
|
+
expect(navigate).toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should validate amount is required', () => {
|
|
161
|
+
const billWithAmountDue: MappedBill = {
|
|
162
|
+
...mockBill,
|
|
163
|
+
totalAmount: 100,
|
|
164
|
+
tenderedAmount: 0,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
render(<Payments bill={billWithAmountDue} mutate={mockMutate} />);
|
|
168
|
+
|
|
169
|
+
// Process Payment button should be disabled when no amount is entered
|
|
170
|
+
expect(screen.getByText('Process Payment')).toBeDisabled();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should handle undefined amount values correctly', () => {
|
|
174
|
+
const billWithAmountDue: MappedBill = {
|
|
175
|
+
...mockBill,
|
|
176
|
+
totalAmount: 100,
|
|
177
|
+
tenderedAmount: 0,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
render(<Payments bill={billWithAmountDue} mutate={mockMutate} />);
|
|
181
|
+
|
|
182
|
+
expect(screen.getByText('Process Payment')).toBeDisabled();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should display amount due when there is a balance', () => {
|
|
186
|
+
const billWithBalance: MappedBill = {
|
|
187
|
+
...mockBill,
|
|
188
|
+
totalAmount: 500,
|
|
189
|
+
tenderedAmount: 200,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
render(<Payments bill={billWithBalance} mutate={mockMutate} />);
|
|
193
|
+
|
|
194
|
+
expect(screen.getByText(/amount due:/i)).toBeInTheDocument();
|
|
195
|
+
// The amount due section should be visible for bills with remaining balance
|
|
196
|
+
const formattedAmounts = screen.getAllByText('$1000.00');
|
|
197
|
+
expect(formattedAmounts.length).toBeGreaterThan(0);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should display amount due as absolute value when overpaid', () => {
|
|
201
|
+
const billWithOverpayment: MappedBill = {
|
|
202
|
+
...mockBill,
|
|
203
|
+
totalAmount: 100,
|
|
204
|
+
tenderedAmount: 150,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
render(<Payments bill={billWithOverpayment} mutate={mockMutate} />);
|
|
208
|
+
|
|
209
|
+
// Even with negative amount due (overpayment), the display should show positive value
|
|
210
|
+
expect(screen.getByText(/amount due:/i)).toBeInTheDocument();
|
|
211
|
+
const formattedAmounts = screen.getAllByText('$1000.00');
|
|
212
|
+
expect(formattedAmounts.length).toBeGreaterThan(0);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('should disable adding payment methods when amount due is zero or less', () => {
|
|
216
|
+
const fullyPaidBill: MappedBill = {
|
|
217
|
+
...mockBill,
|
|
218
|
+
totalAmount: 100,
|
|
219
|
+
tenderedAmount: 100,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
render(<Payments bill={fullyPaidBill} mutate={mockMutate} />);
|
|
223
|
+
|
|
224
|
+
expect(screen.getByText(/add payment method/i)).toBeDisabled();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should return null when bill is not provided', () => {
|
|
228
|
+
const { container } = render(<Payments bill={null} mutate={mockMutate} />);
|
|
229
|
+
expect(container).toBeEmptyDOMElement();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should render add payment method button for bills with amount due', () => {
|
|
233
|
+
const billWithAmountDue: MappedBill = {
|
|
234
|
+
...mockBill,
|
|
235
|
+
totalAmount: 100,
|
|
236
|
+
tenderedAmount: 0,
|
|
237
|
+
lineItems: [],
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
render(<Payments bill={billWithAmountDue} mutate={mockMutate} />);
|
|
241
|
+
|
|
242
|
+
// Verify add payment method button is available
|
|
243
|
+
expect(screen.getByText(/add payment method/i)).toBeInTheDocument();
|
|
244
|
+
expect(screen.getByText(/add payment method/i)).toBeEnabled();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should display process payment button', () => {
|
|
248
|
+
const billWithAmountDue: MappedBill = {
|
|
249
|
+
...mockBill,
|
|
250
|
+
totalAmount: 100,
|
|
251
|
+
tenderedAmount: 0,
|
|
252
|
+
lineItems: [],
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
render(<Payments bill={billWithAmountDue} mutate={mockMutate} />);
|
|
256
|
+
|
|
257
|
+
// Process payment button should be visible
|
|
258
|
+
expect(screen.getByText('Process Payment')).toBeInTheDocument();
|
|
259
|
+
// Button should be disabled when no payment methods are added
|
|
260
|
+
expect(screen.getByText('Process Payment')).toBeDisabled();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should allow adding multiple payment methods for split payments', async () => {
|
|
264
|
+
const user = userEvent.setup();
|
|
265
|
+
const billWithAmountDue: MappedBill = {
|
|
266
|
+
...mockBill,
|
|
267
|
+
totalAmount: 100,
|
|
268
|
+
tenderedAmount: 0,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
render(<Payments bill={billWithAmountDue} mutate={mockMutate} />);
|
|
272
|
+
|
|
273
|
+
// Add first payment method
|
|
274
|
+
await user.click(screen.getByText(/add payment method/i));
|
|
275
|
+
expect(screen.getByPlaceholderText(/enter amount/i)).toBeInTheDocument();
|
|
276
|
+
|
|
277
|
+
// Add second payment method
|
|
278
|
+
await user.click(screen.getByText(/add payment method/i));
|
|
279
|
+
const amountInputs = screen.getAllByPlaceholderText(/enter amount/i);
|
|
280
|
+
expect(amountInputs).toHaveLength(2);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
@@ -1,24 +1,15 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type MappedBill, type BillableService } 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
|
-
billableServices: Array<
|
|
18
|
-
selectedLineItems: Array<LineItem>,
|
|
9
|
+
billableServices: Array<BillableService>,
|
|
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,
|
|
@@ -30,43 +21,40 @@ export const createPaymentPayload = (
|
|
|
30
21
|
|
|
31
22
|
const newPayments = formValues.map((formValue) => ({
|
|
32
23
|
amount: parseFloat(totalAmount.toFixed(2)),
|
|
33
|
-
amountTendered: parseFloat(Number(formValue.amount).toFixed(2)),
|
|
24
|
+
amountTendered: parseFloat(Number(formValue.amount || 0).toFixed(2)),
|
|
34
25
|
attributes: [],
|
|
35
26
|
instanceType: formValue.method,
|
|
36
27
|
dateCreated: new Date(),
|
|
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;
|
|
67
49
|
};
|
|
68
50
|
|
|
69
|
-
export const getBillableServiceUuid = (billableServices: Array<
|
|
70
|
-
|
|
51
|
+
export const getBillableServiceUuid = (billableServices: Array<BillableService>, serviceName: string) => {
|
|
52
|
+
if (!billableServices.length) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const service = billableServices.find((service) => service.name === serviceName);
|
|
56
|
+
return service?.uuid ?? null;
|
|
71
57
|
};
|
|
72
|
-
|
|
58
|
+
|
|
59
|
+
const processBillItem = (item: { item?: string; billableService?: string }) =>
|
|
60
|
+
(item.item || item.billableService)?.split(':')[0];
|
|
@@ -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
|
|