@openmrs/esm-billing-app 1.1.2-pre.9 → 1.2.0
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/.turbo/cache/31f1dfc7f71601df-meta.json +1 -0
- package/.turbo/cache/31f1dfc7f71601df.tar.zst +0 -0
- package/.turbo/turbo-build.log +13 -42
- package/__mocks__/bills.mock.ts +3 -2
- package/dist/1480.js +1 -0
- package/dist/1480.js.map +1 -0
- package/dist/1564.js +1 -0
- package/dist/1564.js.map +1 -0
- package/dist/1578.js +1 -0
- package/dist/1578.js.map +1 -0
- package/dist/1646.js +1 -0
- package/dist/1646.js.map +1 -0
- package/dist/1869.js +1 -0
- package/dist/1869.js.map +1 -0
- package/dist/1877.js +1 -0
- package/dist/1877.js.map +1 -0
- package/dist/1899.js +1 -0
- package/dist/1899.js.map +1 -0
- package/dist/196.js +2 -0
- package/dist/196.js.map +1 -0
- package/dist/2250.js +43 -0
- package/dist/2250.js.map +1 -0
- package/dist/2269.js +1 -0
- package/dist/2269.js.map +1 -0
- package/dist/2317.js +1 -0
- package/dist/2317.js.map +1 -0
- package/dist/2416.js +1 -0
- package/dist/2416.js.map +1 -0
- package/dist/2489.js +1 -0
- package/dist/2489.js.map +1 -0
- package/dist/282.js +1 -0
- package/dist/282.js.map +1 -0
- package/dist/2881.js +1 -0
- package/dist/2881.js.map +1 -0
- package/dist/2997.js +1 -0
- package/dist/2997.js.map +1 -0
- package/dist/3378.js +1 -0
- package/dist/3378.js.map +1 -0
- package/dist/3379.js +1 -0
- package/dist/3379.js.map +1 -0
- package/dist/3784.js +1 -0
- package/dist/3784.js.map +1 -0
- package/dist/3963.js +1 -0
- package/dist/3963.js.map +1 -0
- package/dist/4106.js +1 -0
- package/dist/4106.js.map +1 -0
- package/dist/4111.js +1 -0
- package/dist/4111.js.map +1 -0
- package/dist/434.js +1 -0
- package/dist/434.js.map +1 -0
- package/dist/4348.js +1 -0
- package/dist/4348.js.map +1 -0
- package/dist/4383.js +1 -0
- package/dist/4383.js.map +1 -0
- package/dist/4658.js +1 -0
- package/dist/4658.js.map +1 -0
- package/dist/4870.js +1 -0
- package/dist/4870.js.map +1 -0
- package/dist/4928.js +1 -0
- package/dist/4928.js.map +1 -0
- package/dist/5098.js +1 -0
- package/dist/5098.js.map +1 -0
- package/dist/5117.js +1 -0
- package/dist/5117.js.map +1 -0
- package/dist/5132.js +1 -0
- package/dist/5132.js.map +1 -0
- package/dist/5145.js +1 -0
- package/dist/5145.js.map +1 -0
- package/dist/5390.js +1 -0
- package/dist/5390.js.map +1 -0
- package/dist/5503.js +1 -0
- package/dist/5503.js.map +1 -0
- package/dist/556.js +1 -0
- package/dist/556.js.map +1 -0
- package/dist/5644.js +1 -0
- package/dist/5644.js.map +1 -0
- package/dist/5898.js +1 -0
- package/dist/5898.js.map +1 -0
- package/dist/5940.js +1 -0
- package/dist/5940.js.map +1 -0
- package/dist/6047.js +1 -0
- package/dist/6047.js.map +1 -0
- package/dist/6237.js +1 -0
- package/dist/6237.js.map +1 -0
- package/dist/6362.js +1 -0
- package/dist/6362.js.map +1 -0
- package/dist/6371.js +1 -0
- package/dist/6371.js.map +1 -0
- package/dist/6377.js +1 -0
- package/dist/6377.js.map +1 -0
- package/dist/6444.js +1 -0
- package/dist/6444.js.map +1 -0
- package/dist/6508.js +1 -0
- package/dist/6508.js.map +1 -0
- package/dist/6594.js +1 -0
- package/dist/6594.js.map +1 -0
- package/dist/6724.js +1 -0
- package/dist/6724.js.map +1 -0
- package/dist/6904.js +1 -0
- package/dist/6904.js.map +1 -0
- package/dist/7045.js +1 -0
- package/dist/7045.js.map +1 -0
- package/dist/7175.js +1 -0
- package/dist/7175.js.map +1 -0
- package/dist/7182.js +1 -0
- package/dist/7182.js.map +1 -0
- package/dist/7247.js +1 -0
- package/dist/7247.js.map +1 -0
- package/dist/7742.js +1 -0
- package/dist/7742.js.map +1 -0
- package/dist/7912.js +1 -0
- package/dist/7912.js.map +1 -0
- package/dist/8358.js +1 -0
- package/dist/8358.js.map +1 -0
- package/dist/8359.js +1 -0
- package/dist/8359.js.map +1 -0
- package/dist/8695.js +1 -0
- package/dist/8695.js.map +1 -0
- package/dist/903.js +1 -0
- package/dist/903.js.map +1 -0
- package/dist/9072.js +1 -0
- package/dist/9072.js.map +1 -0
- package/dist/9414.js +1 -0
- package/dist/9414.js.map +1 -0
- package/dist/9655.js +11 -0
- package/dist/9655.js.map +1 -0
- package/dist/9806.js +1 -0
- package/dist/9806.js.map +1 -0
- package/dist/990.js +1 -0
- package/dist/990.js.map +1 -0
- package/dist/main.js +17 -2
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-billing-app.js +6 -1
- package/dist/openmrs-esm-billing-app.js.buildmanifest.json +643 -436
- package/dist/openmrs-esm-billing-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/e2e/commands/billing-operations.ts +21 -0
- package/e2e/commands/types.ts +9 -1
- package/e2e/pages/discounts-page.ts +75 -0
- package/e2e/pages/index.ts +1 -0
- package/e2e/pages/invoice-page.ts +7 -7
- package/e2e/specs/bill-discounts.spec.ts +255 -0
- package/e2e/specs/billing-dashboard.spec.ts +3 -3
- package/e2e/specs/billing-patient-chart.spec.ts +2 -2
- package/package.json +13 -22
- package/rspack.config.js +1 -0
- package/src/bill-history/bill-action-menu.component.tsx +20 -2
- package/src/bill-history/bill-history.test.tsx +23 -22
- package/src/bill-item-actions/edit-bill-item.modal.tsx +1 -1
- package/src/bill-item-actions/edit-bill-item.test.tsx +29 -27
- package/src/billable-services/billable-service-form/billable-service-form.test.tsx +74 -73
- package/src/billable-services/billable-services-home.component.tsx +4 -2
- package/src/billable-services/billable-services.test.tsx +8 -7
- package/src/billable-services/dashboard/dashboard.test.tsx +3 -2
- package/src/billable-services-admin-card-link.test.tsx +2 -1
- package/src/billing-dashboard/billing-dashboard.test.tsx +19 -3
- package/src/billing-form/billing-checkin-form.component.tsx +7 -3
- package/src/billing-form/billing-checkin-form.test.tsx +22 -21
- package/src/billing-form/billing-form.resource.test.ts +7 -6
- package/src/billing-form/billing-form.test.tsx +77 -40
- package/src/billing-form/billing-form.workspace.tsx +25 -6
- package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +6 -2
- package/src/billing.resource.test.ts +43 -41
- package/src/billing.resource.ts +65 -16
- package/src/bills-table/bills-table.component.tsx +15 -4
- package/src/bills-table/bills-table.test.tsx +72 -71
- package/src/config-schema.ts +0 -7
- package/src/discounts/admin/discount-requests-left-panel-link.component.tsx +43 -0
- package/src/discounts/admin/discount-requests.component.tsx +316 -0
- package/src/discounts/admin/discount-requests.scss +133 -0
- package/src/discounts/admin/discount-requests.test.tsx +104 -0
- package/src/discounts/admin/review-bill-discounts/bill-line-items-table/bill-line-items-table.component.tsx +42 -0
- package/src/discounts/admin/review-bill-discounts/bill-line-items-table/bill-line-items-table.scss +76 -0
- package/src/discounts/admin/review-bill-discounts/bill-payments-table/bill-payments-table.component.tsx +50 -0
- package/src/discounts/admin/review-bill-discounts/bill-payments-table/bill-payments-table.scss +63 -0
- package/src/discounts/admin/review-bill-discounts/bill-receipt-rail/bill-receipt-rail.component.tsx +73 -0
- package/src/discounts/admin/review-bill-discounts/bill-receipt-rail/bill-receipt-rail.scss +54 -0
- package/src/discounts/admin/review-bill-discounts/bill-totals-summary/bill-totals-summary.component.tsx +95 -0
- package/src/discounts/admin/review-bill-discounts/bill-totals-summary/bill-totals-summary.scss +128 -0
- package/src/discounts/admin/review-bill-discounts/discount-card/discount-card.component.tsx +158 -0
- package/src/discounts/admin/review-bill-discounts/discount-card/discount-card.scss +164 -0
- package/src/discounts/admin/review-bill-discounts/discount-review-stack/discount-review-stack.component.tsx +86 -0
- package/src/discounts/admin/review-bill-discounts/discount-review-stack/discount-review-stack.scss +40 -0
- package/src/discounts/admin/review-bill-discounts/review-bill-discounts.modal.scss +14 -0
- package/src/discounts/admin/review-bill-discounts/review-bill-discounts.modal.test.tsx +153 -0
- package/src/discounts/admin/review-bill-discounts/review-bill-discounts.modal.tsx +167 -0
- package/src/discounts/admin/review-bill-discounts/review-bill-discounts.utils.ts +42 -0
- package/src/discounts/discounts-table.component.tsx +109 -0
- package/src/discounts/discounts-table.scss +37 -0
- package/src/discounts/discounts-table.test.tsx +67 -0
- package/src/discounts/discounts.resource.ts +71 -0
- package/src/discounts/request-discount.modal.scss +88 -0
- package/src/discounts/request-discount.modal.test.tsx +161 -0
- package/src/discounts/request-discount.modal.tsx +253 -0
- package/src/index.ts +52 -21
- package/src/invoice/invoice-table.component.tsx +116 -18
- package/src/invoice/invoice-table.test.tsx +165 -13
- package/src/invoice/invoice.component.tsx +111 -7
- package/src/invoice/invoice.test.tsx +366 -66
- package/src/invoice/line-item-action-menu.component.tsx +31 -1
- package/src/invoice/payments/payment-form/payment-form.test.tsx +20 -19
- package/src/invoice/payments/payment-history/payment-history.test.tsx +13 -10
- package/src/invoice/payments/payments.component.tsx +20 -6
- package/src/invoice/payments/payments.test.tsx +88 -23
- package/src/invoice/printable-invoice/print-receipt.test.tsx +10 -28
- package/src/invoice/printable-invoice/printable-footer.test.tsx +5 -4
- package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +3 -3
- package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +26 -11
- package/src/invoice/printable-invoice/printable-invoice.component.tsx +38 -15
- package/src/left-panel-link.test.tsx +3 -3
- package/src/metrics-cards/metrics-cards.test.tsx +11 -10
- package/src/modal/delete-bill-confirmation.modal.test.tsx +134 -0
- package/src/modal/delete-bill-confirmation.modal.tsx +98 -0
- package/src/modal/delete-line-item-confirmation.modal.test.tsx +11 -9
- package/src/modal/finalize-bill-confirmation.modal.test.tsx +10 -8
- package/src/modal/require-payment-modal.test.tsx +12 -11
- package/src/payment-status-tag/payment-status-tag.component.tsx +50 -0
- package/src/payment-status-tag/payment-status-tag.scss +6 -0
- package/src/payment-status-tag/payment-status-tag.test.tsx +113 -0
- package/src/refunds/admin/refund-requests-left-panel-link.component.tsx +43 -0
- package/src/refunds/admin/refund-requests.component.tsx +324 -0
- package/src/refunds/admin/refund-requests.scss +133 -0
- package/src/refunds/admin/refund-requests.test.tsx +99 -0
- package/src/refunds/admin/review-bill-refunds/bill-line-items-table/bill-line-items-table.component.tsx +42 -0
- package/src/refunds/admin/review-bill-refunds/bill-line-items-table/bill-line-items-table.scss +76 -0
- package/src/refunds/admin/review-bill-refunds/bill-payments-table/bill-payments-table.component.tsx +50 -0
- package/src/refunds/admin/review-bill-refunds/bill-payments-table/bill-payments-table.scss +63 -0
- package/src/refunds/admin/review-bill-refunds/bill-receipt-rail/bill-receipt-rail.component.tsx +84 -0
- package/src/refunds/admin/review-bill-refunds/bill-receipt-rail/bill-receipt-rail.scss +54 -0
- package/src/refunds/admin/review-bill-refunds/bill-totals-summary/bill-totals-summary.component.tsx +83 -0
- package/src/refunds/admin/review-bill-refunds/bill-totals-summary/bill-totals-summary.scss +65 -0
- package/src/refunds/admin/review-bill-refunds/refund-card/refund-card.component.tsx +170 -0
- package/src/refunds/admin/review-bill-refunds/refund-card/refund-card.scss +155 -0
- package/src/refunds/admin/review-bill-refunds/refund-review-stack/refund-review-stack.component.tsx +86 -0
- package/src/refunds/admin/review-bill-refunds/refund-review-stack/refund-review-stack.scss +40 -0
- package/src/refunds/admin/review-bill-refunds/review-bill-refunds.modal.scss +14 -0
- package/src/refunds/admin/review-bill-refunds/review-bill-refunds.modal.test.tsx +313 -0
- package/src/refunds/admin/review-bill-refunds/review-bill-refunds.modal.tsx +188 -0
- package/src/refunds/admin/review-bill-refunds/review-bill-refunds.utils.ts +66 -0
- package/src/refunds/refunds-table.component.tsx +137 -0
- package/src/refunds/refunds-table.scss +37 -0
- package/src/refunds/refunds-table.test.tsx +105 -0
- package/src/refunds/refunds.resource.test.ts +44 -0
- package/src/refunds/refunds.resource.ts +42 -0
- package/src/refunds/refunds.types.test.ts +15 -0
- package/src/refunds/request-refund.modal.scss +84 -0
- package/src/refunds/request-refund.modal.test.tsx +204 -0
- package/src/refunds/request-refund.modal.tsx +218 -0
- package/src/routes.json +36 -2
- package/src/types/index.ts +116 -1
- package/src/visit-bills/visit-bills-panel.component.tsx +151 -0
- package/src/visit-bills/visit-bills-panel.scss +31 -0
- package/src/visit-bills/visit-bills-panel.test.tsx +113 -0
- package/tools/empty-module.ts +1 -0
- package/tools/setup-tests.ts +9 -9
- package/translations/am.json +154 -16
- package/translations/ar.json +154 -16
- package/translations/ar_SY.json +154 -16
- package/translations/bn.json +154 -16
- package/translations/cs.json +154 -16
- package/translations/de.json +154 -16
- package/translations/en.json +154 -16
- package/translations/en_US.json +154 -16
- package/translations/es.json +154 -16
- package/translations/es_MX.json +154 -16
- package/translations/fr.json +154 -16
- package/translations/he.json +154 -16
- package/translations/hi.json +154 -16
- package/translations/hi_IN.json +154 -16
- package/translations/id.json +154 -16
- package/translations/it.json +154 -16
- package/translations/ka.json +154 -16
- package/translations/km.json +154 -16
- package/translations/ku.json +154 -16
- package/translations/ky.json +154 -16
- package/translations/lg.json +154 -16
- package/translations/ne.json +154 -16
- package/translations/pl.json +154 -16
- package/translations/pt.json +154 -16
- package/translations/pt_BR.json +154 -16
- package/translations/qu.json +154 -16
- package/translations/ro_RO.json +154 -16
- package/translations/ru_RU.json +154 -16
- package/translations/si.json +154 -16
- package/translations/sq.json +154 -16
- package/translations/sw.json +154 -16
- package/translations/sw_KE.json +154 -16
- package/translations/tr.json +154 -16
- package/translations/tr_TR.json +154 -16
- package/translations/uk.json +154 -16
- package/translations/uz.json +154 -16
- package/translations/uz@Latn.json +154 -16
- package/translations/uz_UZ.json +154 -16
- package/translations/vi.json +154 -16
- package/translations/zh.json +154 -16
- package/translations/zh_CN.json +179 -41
- package/translations/zh_TW.json +154 -16
- package/tsconfig.json +3 -3
- package/vitest.config.js +28 -0
- package/.turbo/cache/4e30f71f570fc412-meta.json +0 -1
- package/.turbo/cache/4e30f71f570fc412.tar.zst +0 -0
- package/dist/1119.js +0 -1
- package/dist/1197.js +0 -1
- package/dist/1435.js +0 -1
- package/dist/1435.js.map +0 -1
- package/dist/1807.js +0 -1
- package/dist/1807.js.map +0 -1
- package/dist/2146.js +0 -1
- package/dist/2177.js +0 -2
- package/dist/2177.js.LICENSE.txt +0 -9
- package/dist/2177.js.map +0 -1
- package/dist/2690.js +0 -1
- package/dist/2704.js +0 -1
- package/dist/2704.js.map +0 -1
- package/dist/3002.js +0 -1
- package/dist/3002.js.map +0 -1
- package/dist/3041.js +0 -1
- package/dist/3041.js.map +0 -1
- package/dist/3099.js +0 -1
- package/dist/3184.js +0 -2
- package/dist/3184.js.LICENSE.txt +0 -14
- package/dist/3184.js.map +0 -1
- package/dist/3584.js +0 -1
- package/dist/4055.js +0 -1
- package/dist/4132.js +0 -1
- package/dist/4225.js +0 -1
- package/dist/4225.js.map +0 -1
- package/dist/4300.js +0 -1
- package/dist/4335.js +0 -1
- package/dist/439.js +0 -1
- package/dist/4618.js +0 -1
- package/dist/4652.js +0 -1
- package/dist/4944.js +0 -1
- package/dist/5173.js +0 -1
- package/dist/5241.js +0 -1
- package/dist/5422.js +0 -1
- package/dist/5422.js.map +0 -1
- package/dist/5442.js +0 -1
- package/dist/5661.js +0 -1
- package/dist/6022.js +0 -1
- package/dist/6404.js +0 -1
- package/dist/6404.js.map +0 -1
- package/dist/6468.js +0 -1
- package/dist/6540.js +0 -2
- package/dist/6540.js.LICENSE.txt +0 -9
- package/dist/6540.js.map +0 -1
- package/dist/6589.js +0 -1
- package/dist/6606.js +0 -1
- package/dist/6606.js.map +0 -1
- package/dist/6679.js +0 -1
- package/dist/6792.js +0 -1
- package/dist/6792.js.map +0 -1
- package/dist/6840.js +0 -1
- package/dist/6859.js +0 -1
- package/dist/7097.js +0 -1
- package/dist/7159.js +0 -1
- package/dist/723.js +0 -1
- package/dist/7255.js +0 -1
- package/dist/7255.js.map +0 -1
- package/dist/7617.js +0 -1
- package/dist/795.js +0 -1
- package/dist/8163.js +0 -1
- package/dist/8341.js +0 -2
- package/dist/8341.js.LICENSE.txt +0 -52
- package/dist/8341.js.map +0 -1
- package/dist/8349.js +0 -1
- package/dist/8371.js +0 -1
- package/dist/8421.js +0 -1
- package/dist/8421.js.map +0 -1
- package/dist/8618.js +0 -1
- package/dist/890.js +0 -1
- package/dist/9214.js +0 -1
- package/dist/9538.js +0 -1
- package/dist/9569.js +0 -1
- package/dist/961.js +0 -2
- package/dist/961.js.LICENSE.txt +0 -19
- package/dist/961.js.map +0 -1
- package/dist/986.js +0 -1
- package/dist/9879.js +0 -1
- package/dist/9895.js +0 -1
- package/dist/9900.js +0 -1
- package/dist/9913.js +0 -1
- package/dist/main.js.LICENSE.txt +0 -62
- package/src/billable-services/bill-waiver/bill-selection.component.tsx +0 -76
- package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +0 -107
- package/src/billable-services/bill-waiver/bill-waiver-form.scss +0 -34
- package/src/billable-services/bill-waiver/bill-waiver.component.tsx +0 -34
- package/src/billable-services/bill-waiver/bill-waiver.scss +0 -10
- package/src/billable-services/bill-waiver/patient-bills.component.tsx +0 -134
- package/webpack.config.js +0 -1
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import { showSnackbar, useSession } from '@openmrs/esm-framework';
|
|
6
|
+
import ReviewBillRefundsModal from './review-bill-refunds.modal';
|
|
7
|
+
import { actOnRefund, voidRefund } from '../../refunds.resource';
|
|
8
|
+
import { useBill } from '../../../billing.resource';
|
|
9
|
+
import { RefundStatus, BillStatus, type PatientInvoice } from '../../../types';
|
|
10
|
+
|
|
11
|
+
vi.mock('@openmrs/esm-framework', () => ({
|
|
12
|
+
showSnackbar: vi.fn(),
|
|
13
|
+
useSession: vi.fn(),
|
|
14
|
+
useConfig: vi.fn().mockReturnValue({ defaultCurrency: 'USD' }),
|
|
15
|
+
formatDate: () => '2026-05-21',
|
|
16
|
+
parseDate: (s: string) => new Date(s),
|
|
17
|
+
getCoreTranslation: (key: string) => key,
|
|
18
|
+
restBaseUrl: '/ws/rest/v1',
|
|
19
|
+
}));
|
|
20
|
+
vi.mock('../../refunds.resource');
|
|
21
|
+
vi.mock('../../../billing.resource', () => ({
|
|
22
|
+
useBill: vi.fn().mockReturnValue({ bill: null, mutate: vi.fn(), isLoading: false, isValidating: false }),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
globalThis.i18next = { language: 'en-US' } as any;
|
|
26
|
+
|
|
27
|
+
const closeModal = vi.fn();
|
|
28
|
+
const onMutate = vi.fn();
|
|
29
|
+
|
|
30
|
+
const makeBill = (refunds: any[] = []): PatientInvoice =>
|
|
31
|
+
({
|
|
32
|
+
uuid: 'b1',
|
|
33
|
+
status: BillStatus.PAID,
|
|
34
|
+
total: 5000,
|
|
35
|
+
amountAfterDiscount: 5000,
|
|
36
|
+
receiptNumber: 'INV-1',
|
|
37
|
+
patient: { uuid: 'p1', display: 'John Doe', links: [] },
|
|
38
|
+
cashier: { uuid: 'c1', display: 'cashier', links: [] },
|
|
39
|
+
dateCreated: '2026-05-21T00:00:00.000+0000',
|
|
40
|
+
lineItems: [],
|
|
41
|
+
payments: [{ uuid: 'pay1', amountTendered: 5000, amount: 5000, voided: false } as any],
|
|
42
|
+
refunds,
|
|
43
|
+
discounts: [],
|
|
44
|
+
}) as any;
|
|
45
|
+
|
|
46
|
+
const requestedRefund = {
|
|
47
|
+
uuid: 'r1',
|
|
48
|
+
billUuid: 'b1',
|
|
49
|
+
lineItemUuid: null,
|
|
50
|
+
refundAmount: 500,
|
|
51
|
+
reason: 'overcharged',
|
|
52
|
+
initiator: { uuid: 'u1', display: 'cashier' },
|
|
53
|
+
approver: null,
|
|
54
|
+
completer: null,
|
|
55
|
+
dateApproved: null,
|
|
56
|
+
dateCompleted: null,
|
|
57
|
+
dateCreated: '2026-05-21T00:00:00.000+0000',
|
|
58
|
+
status: RefundStatus.REQUESTED,
|
|
59
|
+
voided: false,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
vi.clearAllMocks();
|
|
64
|
+
vi.mocked(useSession).mockReturnValue({ user: { uuid: 'u-admin' } } as any);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('ReviewBillRefundsModal', () => {
|
|
68
|
+
it('renders the receipt rail and review stack', () => {
|
|
69
|
+
render(<ReviewBillRefundsModal closeModal={closeModal} bill={makeBill([requestedRefund])} onMutate={onMutate} />);
|
|
70
|
+
expect(screen.getByText(/John Doe/)).toBeInTheDocument();
|
|
71
|
+
expect(screen.getByText(/requested refunds/i)).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('approves a REQUESTED refund', async () => {
|
|
75
|
+
vi.mocked(actOnRefund).mockResolvedValue({} as any);
|
|
76
|
+
const user = userEvent.setup();
|
|
77
|
+
render(<ReviewBillRefundsModal closeModal={closeModal} bill={makeBill([requestedRefund])} onMutate={onMutate} />);
|
|
78
|
+
await user.click(screen.getByRole('button', { name: /approve/i }));
|
|
79
|
+
await waitFor(() =>
|
|
80
|
+
expect(actOnRefund).toHaveBeenCalledWith('r1', { status: RefundStatus.APPROVED, approver: 'u-admin' }),
|
|
81
|
+
);
|
|
82
|
+
expect(onMutate).toHaveBeenCalled();
|
|
83
|
+
expect(showSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'success' }));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('blocks approve when totalApprovedRefunds + refundAmount exceeds amountAfterDiscount', async () => {
|
|
87
|
+
const bigRefund = { ...requestedRefund, uuid: 'r2', refundAmount: 6000 };
|
|
88
|
+
const user = userEvent.setup();
|
|
89
|
+
render(<ReviewBillRefundsModal closeModal={closeModal} bill={makeBill([bigRefund])} onMutate={onMutate} />);
|
|
90
|
+
await user.click(screen.getByRole('button', { name: /approve/i }));
|
|
91
|
+
expect(actOnRefund).not.toHaveBeenCalled();
|
|
92
|
+
expect(showSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'error' }));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('rejects a REQUESTED refund after inline confirm', async () => {
|
|
96
|
+
vi.mocked(actOnRefund).mockResolvedValue({} as any);
|
|
97
|
+
const user = userEvent.setup();
|
|
98
|
+
render(<ReviewBillRefundsModal closeModal={closeModal} bill={makeBill([requestedRefund])} onMutate={onMutate} />);
|
|
99
|
+
await user.click(screen.getByRole('button', { name: /reject/i }));
|
|
100
|
+
expect(screen.getByText(/reject this refund/i)).toBeInTheDocument();
|
|
101
|
+
await user.click(screen.getByRole('button', { name: /confirm reject/i }));
|
|
102
|
+
await waitFor(() =>
|
|
103
|
+
expect(actOnRefund).toHaveBeenCalledWith('r1', { status: RefundStatus.REJECTED, approver: 'u-admin' }),
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('shows read-only state for APPROVED refund', () => {
|
|
108
|
+
const approvedRefund = { ...requestedRefund, uuid: 'r3', status: RefundStatus.APPROVED };
|
|
109
|
+
render(<ReviewBillRefundsModal closeModal={closeModal} bill={makeBill([approvedRefund])} onMutate={onMutate} />);
|
|
110
|
+
expect(screen.getByText(/awaiting cashier processing/i)).toBeInTheDocument();
|
|
111
|
+
expect(screen.queryByRole('button', { name: /approve/i })).not.toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('blocks approve when completed refunds + new refund amount exceeds amountAfterDiscount', async () => {
|
|
115
|
+
const completedRefund = {
|
|
116
|
+
...requestedRefund,
|
|
117
|
+
uuid: 'r-done',
|
|
118
|
+
refundAmount: 700,
|
|
119
|
+
status: RefundStatus.COMPLETED,
|
|
120
|
+
voided: false,
|
|
121
|
+
};
|
|
122
|
+
const newRequest = { ...requestedRefund, uuid: 'r-new', refundAmount: 500 };
|
|
123
|
+
const bill = makeBill([completedRefund, newRequest]);
|
|
124
|
+
// amountAfterDiscount = 5000 on makeBill, override to 1000 to make the sum exceed
|
|
125
|
+
const smallBill = { ...bill, amountAfterDiscount: 1000 };
|
|
126
|
+
const user = userEvent.setup();
|
|
127
|
+
render(<ReviewBillRefundsModal closeModal={closeModal} bill={smallBill as any} onMutate={onMutate} />);
|
|
128
|
+
await user.click(screen.getByRole('button', { name: /approve/i }));
|
|
129
|
+
expect(actOnRefund).not.toHaveBeenCalled();
|
|
130
|
+
expect(showSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'error' }));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('does not throw when session.user is null and approve is clicked', async () => {
|
|
134
|
+
vi.mocked(useSession).mockReturnValue({ user: null } as any);
|
|
135
|
+
vi.mocked(actOnRefund).mockResolvedValue({} as any);
|
|
136
|
+
const user = userEvent.setup();
|
|
137
|
+
render(<ReviewBillRefundsModal closeModal={closeModal} bill={makeBill([requestedRefund])} onMutate={onMutate} />);
|
|
138
|
+
await user.click(screen.getByRole('button', { name: /approve/i }));
|
|
139
|
+
await waitFor(() =>
|
|
140
|
+
expect(actOnRefund).toHaveBeenCalledWith('r1', { status: RefundStatus.APPROVED, approver: undefined }),
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('shows a progress bar above the content and disables action buttons while loading', () => {
|
|
145
|
+
vi.mocked(useBill).mockReturnValueOnce({
|
|
146
|
+
bill: null,
|
|
147
|
+
mutate: vi.fn(),
|
|
148
|
+
isLoading: true,
|
|
149
|
+
isValidating: false,
|
|
150
|
+
error: undefined,
|
|
151
|
+
} as any);
|
|
152
|
+
render(<ReviewBillRefundsModal closeModal={closeModal} bill={makeBill([requestedRefund])} onMutate={onMutate} />);
|
|
153
|
+
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
|
154
|
+
expect(screen.getByText(/requested refunds/i)).toBeInTheDocument();
|
|
155
|
+
expect(screen.getByRole('button', { name: /approve/i })).toBeDisabled();
|
|
156
|
+
expect(screen.getByRole('button', { name: /reject/i })).toBeDisabled();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('shows an error notification and hides bill content when bill fetch fails', () => {
|
|
160
|
+
vi.mocked(useBill).mockReturnValueOnce({
|
|
161
|
+
bill: null,
|
|
162
|
+
mutate: vi.fn(),
|
|
163
|
+
isLoading: false,
|
|
164
|
+
isValidating: false,
|
|
165
|
+
error: new Error('Network error'),
|
|
166
|
+
} as any);
|
|
167
|
+
render(<ReviewBillRefundsModal closeModal={closeModal} bill={makeBill([requestedRefund])} onMutate={onMutate} />);
|
|
168
|
+
expect(screen.getByText(/failed to load bill/i)).toBeInTheDocument();
|
|
169
|
+
expect(screen.queryByText(/requested refunds/i)).not.toBeInTheDocument();
|
|
170
|
+
expect(screen.queryByRole('button', { name: /approve/i })).not.toBeInTheDocument();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('line item refunds', () => {
|
|
174
|
+
const lineItem = { uuid: 'li-1', item: 'Lab Test', price: 1000, quantity: 1, status: 'PENDING' as any };
|
|
175
|
+
const lineItemRefund = { ...requestedRefund, uuid: 'r-li', lineItemUuid: 'li-1', refundAmount: 500 };
|
|
176
|
+
|
|
177
|
+
it('shows the line item name as the refund scope instead of "Whole bill"', () => {
|
|
178
|
+
const bill = { ...makeBill([lineItemRefund]), lineItems: [lineItem] };
|
|
179
|
+
render(<ReviewBillRefundsModal closeModal={closeModal} bill={bill as any} onMutate={onMutate} />);
|
|
180
|
+
// "Lab Test" appears in both the receipt rail and the refund card scope
|
|
181
|
+
expect(screen.getAllByText('Lab Test').length).toBeGreaterThan(0);
|
|
182
|
+
// No refund should show "Whole bill" when all refunds are line-item-scoped
|
|
183
|
+
expect(screen.queryByText(/whole bill/i)).not.toBeInTheDocument();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('approves a line-item refund within the line item total', async () => {
|
|
187
|
+
vi.mocked(actOnRefund).mockResolvedValue({} as any);
|
|
188
|
+
const bill = { ...makeBill([lineItemRefund]), lineItems: [lineItem] };
|
|
189
|
+
const user = userEvent.setup();
|
|
190
|
+
render(<ReviewBillRefundsModal closeModal={closeModal} bill={bill as any} onMutate={onMutate} />);
|
|
191
|
+
await user.click(screen.getByRole('button', { name: /approve/i }));
|
|
192
|
+
await waitFor(() =>
|
|
193
|
+
expect(actOnRefund).toHaveBeenCalledWith('r-li', { status: RefundStatus.APPROVED, approver: 'u-admin' }),
|
|
194
|
+
);
|
|
195
|
+
expect(showSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'success' }));
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('blocks approving a line-item refund that would exceed the line item total', async () => {
|
|
199
|
+
const overRefund = { ...lineItemRefund, refundAmount: 1500 };
|
|
200
|
+
const bill = { ...makeBill([overRefund]), lineItems: [lineItem] };
|
|
201
|
+
const user = userEvent.setup();
|
|
202
|
+
render(<ReviewBillRefundsModal closeModal={closeModal} bill={bill as any} onMutate={onMutate} />);
|
|
203
|
+
await user.click(screen.getByRole('button', { name: /approve/i }));
|
|
204
|
+
expect(actOnRefund).not.toHaveBeenCalled();
|
|
205
|
+
expect(showSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'error' }));
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('delete (void) flow', () => {
|
|
210
|
+
// Carbon danger buttons get accessible name "danger{Label}" (e.g. "dangerDelete"),
|
|
211
|
+
// so we match with /delete/i (no anchors) which safely covers "dangerDelete".
|
|
212
|
+
it('shows the delete confirm prompt when Delete is clicked', async () => {
|
|
213
|
+
const user = userEvent.setup();
|
|
214
|
+
render(<ReviewBillRefundsModal closeModal={closeModal} bill={makeBill([requestedRefund])} onMutate={onMutate} />);
|
|
215
|
+
await user.click(screen.getByRole('button', { name: /delete/i }));
|
|
216
|
+
expect(screen.getByText(/delete this refund/i)).toBeInTheDocument();
|
|
217
|
+
expect(screen.getByRole('button', { name: /confirm delete/i })).toBeInTheDocument();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('cancels delete and restores the default actions when cancel is clicked', async () => {
|
|
221
|
+
const user = userEvent.setup();
|
|
222
|
+
render(<ReviewBillRefundsModal closeModal={closeModal} bill={makeBill([requestedRefund])} onMutate={onMutate} />);
|
|
223
|
+
await user.click(screen.getByRole('button', { name: /delete/i }));
|
|
224
|
+
await user.click(screen.getByRole('button', { name: /^cancel$/i }));
|
|
225
|
+
expect(screen.queryByText(/delete this refund/i)).not.toBeInTheDocument();
|
|
226
|
+
expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('calls voidRefund and shows a success snackbar when delete is confirmed', async () => {
|
|
230
|
+
vi.mocked(voidRefund).mockResolvedValue({} as any);
|
|
231
|
+
const user = userEvent.setup();
|
|
232
|
+
render(<ReviewBillRefundsModal closeModal={closeModal} bill={makeBill([requestedRefund])} onMutate={onMutate} />);
|
|
233
|
+
await user.click(screen.getByRole('button', { name: /delete/i }));
|
|
234
|
+
await user.click(screen.getByRole('button', { name: /confirm delete/i }));
|
|
235
|
+
await waitFor(() => expect(voidRefund).toHaveBeenCalledWith('r1', 'Voided by admin'));
|
|
236
|
+
expect(onMutate).toHaveBeenCalled();
|
|
237
|
+
expect(showSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'success' }));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('shows an error snackbar and does not call onMutate when delete fails', async () => {
|
|
241
|
+
vi.mocked(voidRefund).mockRejectedValue(new Error('Network error'));
|
|
242
|
+
const user = userEvent.setup();
|
|
243
|
+
render(<ReviewBillRefundsModal closeModal={closeModal} bill={makeBill([requestedRefund])} onMutate={onMutate} />);
|
|
244
|
+
await user.click(screen.getByRole('button', { name: /delete/i }));
|
|
245
|
+
await user.click(screen.getByRole('button', { name: /confirm delete/i }));
|
|
246
|
+
await waitFor(() => expect(showSnackbar).toHaveBeenCalledWith(expect.objectContaining({ kind: 'error' })));
|
|
247
|
+
expect(onMutate).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('disables Approve on all cards while any card approval is in flight', async () => {
|
|
252
|
+
let resolveApprove: (v: any) => void;
|
|
253
|
+
vi.mocked(actOnRefund).mockReturnValue(new Promise((res) => (resolveApprove = res)));
|
|
254
|
+
|
|
255
|
+
const refund2 = { ...requestedRefund, uuid: 'r2', reason: 'duplicate charge' };
|
|
256
|
+
const user = userEvent.setup();
|
|
257
|
+
render(
|
|
258
|
+
<ReviewBillRefundsModal
|
|
259
|
+
closeModal={closeModal}
|
|
260
|
+
bill={makeBill([requestedRefund, refund2])}
|
|
261
|
+
onMutate={onMutate}
|
|
262
|
+
/>,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const [approveA, approveB] = screen.getAllByRole('button', { name: /approve/i });
|
|
266
|
+
await user.click(approveA);
|
|
267
|
+
|
|
268
|
+
expect(approveB).toBeDisabled();
|
|
269
|
+
resolveApprove({});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('disables Confirm reject on any card while another card approval is in flight', async () => {
|
|
273
|
+
let resolveApprove: (v: any) => void;
|
|
274
|
+
vi.mocked(actOnRefund).mockReturnValue(new Promise((res) => (resolveApprove = res)));
|
|
275
|
+
|
|
276
|
+
const refund2 = { ...requestedRefund, uuid: 'r2', reason: 'duplicate charge' };
|
|
277
|
+
const user = userEvent.setup();
|
|
278
|
+
render(
|
|
279
|
+
<ReviewBillRefundsModal
|
|
280
|
+
closeModal={closeModal}
|
|
281
|
+
bill={makeBill([requestedRefund, refund2])}
|
|
282
|
+
onMutate={onMutate}
|
|
283
|
+
/>,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// Open reject confirmation on card 2 (no API call yet)
|
|
287
|
+
// Filter to plain Reject buttons (not "Confirm reject") — there is one per REQUESTED card
|
|
288
|
+
const rejectButtons = screen
|
|
289
|
+
.getAllByRole('button', { name: /reject/i })
|
|
290
|
+
.filter((btn) => !/confirm/i.test(btn.textContent ?? ''));
|
|
291
|
+
await user.click(rejectButtons[1]);
|
|
292
|
+
expect(screen.getByText(/reject this refund/i)).toBeInTheDocument();
|
|
293
|
+
|
|
294
|
+
// Approve card 1 — leaves processing in flight
|
|
295
|
+
await user.click(screen.getByRole('button', { name: /approve/i }));
|
|
296
|
+
|
|
297
|
+
// Confirm reject on card 2 should now be disabled
|
|
298
|
+
expect(screen.getByRole('button', { name: /confirm reject/i })).toBeDisabled();
|
|
299
|
+
resolveApprove({});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('does not throw when session.user is null and reject-confirm is clicked', async () => {
|
|
303
|
+
vi.mocked(useSession).mockReturnValue({ user: null } as any);
|
|
304
|
+
vi.mocked(actOnRefund).mockResolvedValue({} as any);
|
|
305
|
+
const user = userEvent.setup();
|
|
306
|
+
render(<ReviewBillRefundsModal closeModal={closeModal} bill={makeBill([requestedRefund])} onMutate={onMutate} />);
|
|
307
|
+
await user.click(screen.getByRole('button', { name: /reject/i }));
|
|
308
|
+
await user.click(screen.getByRole('button', { name: /confirm reject/i }));
|
|
309
|
+
await waitFor(() =>
|
|
310
|
+
expect(actOnRefund).toHaveBeenCalledWith('r1', { status: RefundStatus.REJECTED, approver: undefined }),
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import { InlineNotification, ModalBody, ModalHeader, ProgressBar } from '@carbon/react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { showSnackbar, useSession } from '@openmrs/esm-framework';
|
|
5
|
+
import BillReceiptRail from './bill-receipt-rail/bill-receipt-rail.component';
|
|
6
|
+
import RefundReviewStack from './refund-review-stack/refund-review-stack.component';
|
|
7
|
+
import { useBill } from '../../../billing.resource';
|
|
8
|
+
import { actOnRefund, voidRefund } from '../../refunds.resource';
|
|
9
|
+
import { useReviewRefundModel } from './review-bill-refunds.utils';
|
|
10
|
+
import { RefundStatus, type BillRefund, type PatientInvoice } from '../../../types';
|
|
11
|
+
import styles from './review-bill-refunds.modal.scss';
|
|
12
|
+
|
|
13
|
+
const extractErrorMessage = (e: any): string =>
|
|
14
|
+
e?.responseBody?.error?.message ?? (e instanceof Error ? e.message : String(e));
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
closeModal: () => void;
|
|
18
|
+
bill: PatientInvoice;
|
|
19
|
+
onMutate: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ReviewBillRefundsModal: React.FC<Props> = ({ closeModal, bill, onMutate }) => {
|
|
23
|
+
const { t } = useTranslation();
|
|
24
|
+
const session = useSession();
|
|
25
|
+
const { bill: localBill, mutate: localMutate, isLoading, isValidating, error } = useBill(bill.uuid, false);
|
|
26
|
+
const activeBill = localBill ?? bill;
|
|
27
|
+
const [processingRefundId, setProcessingRefundId] = useState<string | null>(null);
|
|
28
|
+
const [rejectingId, setRejectingId] = useState<string | null>(null);
|
|
29
|
+
const [voidingId, setVoidingId] = useState<string | null>(null);
|
|
30
|
+
|
|
31
|
+
const {
|
|
32
|
+
activeRefunds,
|
|
33
|
+
requestedRefunds,
|
|
34
|
+
decidedRefunds,
|
|
35
|
+
approvedRefunds,
|
|
36
|
+
completedRefunds,
|
|
37
|
+
totalApprovedRefunds,
|
|
38
|
+
totalCompletedRefunds,
|
|
39
|
+
lineItems,
|
|
40
|
+
payments,
|
|
41
|
+
paymentsTotal,
|
|
42
|
+
subtotal,
|
|
43
|
+
} = useReviewRefundModel(activeBill);
|
|
44
|
+
|
|
45
|
+
const handleApprove = useCallback(
|
|
46
|
+
async (r: BillRefund) => {
|
|
47
|
+
const isCommitted = (s: RefundStatus) =>
|
|
48
|
+
s === RefundStatus.REQUESTED || s === RefundStatus.APPROVED || s === RefundStatus.COMPLETED;
|
|
49
|
+
const otherCommitted = activeRefunds
|
|
50
|
+
.filter((x) => x.uuid !== r.uuid && isCommitted(x.status))
|
|
51
|
+
.reduce((sum, x) => sum + x.refundAmount, 0);
|
|
52
|
+
|
|
53
|
+
if (r.lineItemUuid != null) {
|
|
54
|
+
const lineItem = lineItems.find((li) => li.uuid === r.lineItemUuid);
|
|
55
|
+
if (lineItem != null) {
|
|
56
|
+
const lineTotal = lineItem.price * lineItem.quantity;
|
|
57
|
+
const otherOnLine = activeRefunds
|
|
58
|
+
.filter((x) => x.uuid !== r.uuid && x.lineItemUuid === r.lineItemUuid && isCommitted(x.status))
|
|
59
|
+
.reduce((sum, x) => sum + x.refundAmount, 0);
|
|
60
|
+
if (otherOnLine + r.refundAmount > lineTotal) {
|
|
61
|
+
showSnackbar({
|
|
62
|
+
title: t('approveBlockedRefund', 'Cannot approve refund'),
|
|
63
|
+
subtitle: t('approveBlockedExceedsLineTotal', 'Approving this refund would exceed the line item total.'),
|
|
64
|
+
kind: 'error',
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (otherCommitted + r.refundAmount > activeBill.amountAfterDiscount) {
|
|
72
|
+
showSnackbar({
|
|
73
|
+
title: t('approveBlockedRefund', 'Cannot approve refund'),
|
|
74
|
+
subtitle: t('approveBlockedExceedsTotal', 'Approving this refund would exceed the bill total.'),
|
|
75
|
+
kind: 'error',
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setProcessingRefundId(r.uuid);
|
|
81
|
+
try {
|
|
82
|
+
await actOnRefund(r.uuid, { status: RefundStatus.APPROVED, approver: session.user?.uuid });
|
|
83
|
+
showSnackbar({ title: t('refundApproved', 'Refund approved'), kind: 'success' });
|
|
84
|
+
await localMutate();
|
|
85
|
+
onMutate();
|
|
86
|
+
} catch (e: unknown) {
|
|
87
|
+
showSnackbar({
|
|
88
|
+
title: t('approveFailed', 'Approve failed'),
|
|
89
|
+
subtitle: extractErrorMessage(e),
|
|
90
|
+
kind: 'error',
|
|
91
|
+
});
|
|
92
|
+
} finally {
|
|
93
|
+
setProcessingRefundId(null);
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
[activeRefunds, activeBill.amountAfterDiscount, lineItems, t, session.user?.uuid, localMutate, onMutate],
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const confirmReject = useCallback(
|
|
100
|
+
async (r: BillRefund) => {
|
|
101
|
+
setProcessingRefundId(r.uuid);
|
|
102
|
+
try {
|
|
103
|
+
await actOnRefund(r.uuid, { status: RefundStatus.REJECTED, approver: session.user?.uuid });
|
|
104
|
+
showSnackbar({ title: t('refundRejected', 'Refund rejected'), kind: 'success' });
|
|
105
|
+
await localMutate();
|
|
106
|
+
onMutate();
|
|
107
|
+
} catch (e: unknown) {
|
|
108
|
+
showSnackbar({ title: t('rejectFailed', 'Reject failed'), subtitle: extractErrorMessage(e), kind: 'error' });
|
|
109
|
+
} finally {
|
|
110
|
+
setProcessingRefundId(null);
|
|
111
|
+
setRejectingId(null);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
[t, session.user?.uuid, localMutate, onMutate],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const confirmVoid = useCallback(
|
|
118
|
+
async (r: BillRefund) => {
|
|
119
|
+
setProcessingRefundId(r.uuid);
|
|
120
|
+
try {
|
|
121
|
+
await voidRefund(r.uuid, 'Voided by admin');
|
|
122
|
+
showSnackbar({ title: t('refundDeleted', 'Refund deleted'), kind: 'success' });
|
|
123
|
+
await localMutate();
|
|
124
|
+
onMutate();
|
|
125
|
+
} catch (e: unknown) {
|
|
126
|
+
showSnackbar({ title: t('deleteFailed', 'Delete failed'), subtitle: extractErrorMessage(e), kind: 'error' });
|
|
127
|
+
} finally {
|
|
128
|
+
setProcessingRefundId(null);
|
|
129
|
+
setVoidingId(null);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
[t, localMutate, onMutate],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const showProgress = !!processingRefundId || isLoading || isValidating;
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<>
|
|
139
|
+
<ModalHeader closeModal={closeModal} title={t('reviewRefunds', 'Review refunds')} />
|
|
140
|
+
<ModalBody>
|
|
141
|
+
{error ? (
|
|
142
|
+
<InlineNotification
|
|
143
|
+
kind="error"
|
|
144
|
+
title={t('billLoadFailed', 'Failed to load bill')}
|
|
145
|
+
subtitle={t('billLoadFailedSubtitle', 'Bill data could not be loaded. Please close and try again.')}
|
|
146
|
+
lowContrast
|
|
147
|
+
hideCloseButton
|
|
148
|
+
/>
|
|
149
|
+
) : (
|
|
150
|
+
<>
|
|
151
|
+
{showProgress && <ProgressBar label="" hideLabel />}
|
|
152
|
+
<div className={styles.layout}>
|
|
153
|
+
<BillReceiptRail
|
|
154
|
+
bill={activeBill}
|
|
155
|
+
lineItems={lineItems}
|
|
156
|
+
payments={payments}
|
|
157
|
+
paymentsTotal={paymentsTotal}
|
|
158
|
+
subtotal={subtotal}
|
|
159
|
+
totalApprovedRefunds={totalApprovedRefunds}
|
|
160
|
+
approvedRefunds={approvedRefunds}
|
|
161
|
+
completedRefunds={completedRefunds}
|
|
162
|
+
totalCompletedRefunds={totalCompletedRefunds}
|
|
163
|
+
/>
|
|
164
|
+
<RefundReviewStack
|
|
165
|
+
requestedRefunds={requestedRefunds}
|
|
166
|
+
decidedRefunds={decidedRefunds}
|
|
167
|
+
lineItems={lineItems}
|
|
168
|
+
processingRefundId={processingRefundId}
|
|
169
|
+
disabled={isLoading || isValidating}
|
|
170
|
+
rejectingId={rejectingId}
|
|
171
|
+
voidingId={voidingId}
|
|
172
|
+
onApprove={handleApprove}
|
|
173
|
+
onStartReject={setRejectingId}
|
|
174
|
+
onCancelReject={() => setRejectingId(null)}
|
|
175
|
+
onConfirmReject={confirmReject}
|
|
176
|
+
onStartVoid={setVoidingId}
|
|
177
|
+
onCancelVoid={() => setVoidingId(null)}
|
|
178
|
+
onConfirmVoid={confirmVoid}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
</>
|
|
182
|
+
)}
|
|
183
|
+
</ModalBody>
|
|
184
|
+
</>
|
|
185
|
+
);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export default ReviewBillRefundsModal;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { RefundStatus, type BillRefund, type LineItem, type PatientInvoice, type Payment } from '../../../types';
|
|
3
|
+
|
|
4
|
+
export function useReviewRefundModel(bill: PatientInvoice) {
|
|
5
|
+
const activeRefunds: BillRefund[] = useMemo(
|
|
6
|
+
() => (bill.refunds ?? []).filter((r: BillRefund) => !r.voided),
|
|
7
|
+
[bill.refunds],
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
requestedRefunds,
|
|
12
|
+
decidedRefunds,
|
|
13
|
+
approvedRefunds,
|
|
14
|
+
completedRefunds,
|
|
15
|
+
totalApprovedRefunds,
|
|
16
|
+
totalCompletedRefunds,
|
|
17
|
+
totalCommittedRefunds,
|
|
18
|
+
lineItems,
|
|
19
|
+
payments,
|
|
20
|
+
paymentsTotal,
|
|
21
|
+
subtotal,
|
|
22
|
+
} = useMemo(() => {
|
|
23
|
+
const requestedRefunds = activeRefunds.filter((r) => r.status === RefundStatus.REQUESTED);
|
|
24
|
+
const decidedRefunds = activeRefunds.filter((r) => r.status !== RefundStatus.REQUESTED);
|
|
25
|
+
const approvedRefunds = activeRefunds.filter((r) => r.status === RefundStatus.APPROVED);
|
|
26
|
+
const completedRefunds = activeRefunds.filter((r) => r.status === RefundStatus.COMPLETED);
|
|
27
|
+
const totalApprovedRefunds = approvedRefunds.reduce((sum, r) => sum + r.refundAmount, 0);
|
|
28
|
+
const totalCompletedRefunds = completedRefunds.reduce((sum, r) => sum + r.refundAmount, 0);
|
|
29
|
+
const totalCommittedRefunds = totalApprovedRefunds + totalCompletedRefunds;
|
|
30
|
+
|
|
31
|
+
const lineItems = (bill.lineItems ?? []).filter((li: LineItem) => !li.voided);
|
|
32
|
+
const payments = (bill.payments ?? []).filter((p: Payment) => !p.voided);
|
|
33
|
+
const paymentsTotal = payments.reduce((sum: number, p: Payment) => sum + (p.amountTendered ?? 0), 0);
|
|
34
|
+
|
|
35
|
+
const subtotal = bill.amountAfterDiscount;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
requestedRefunds,
|
|
39
|
+
decidedRefunds,
|
|
40
|
+
approvedRefunds,
|
|
41
|
+
completedRefunds,
|
|
42
|
+
totalApprovedRefunds,
|
|
43
|
+
totalCompletedRefunds,
|
|
44
|
+
totalCommittedRefunds,
|
|
45
|
+
lineItems,
|
|
46
|
+
payments,
|
|
47
|
+
paymentsTotal,
|
|
48
|
+
subtotal,
|
|
49
|
+
};
|
|
50
|
+
}, [activeRefunds, bill.lineItems, bill.payments, bill.amountAfterDiscount]);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
activeRefunds,
|
|
54
|
+
requestedRefunds,
|
|
55
|
+
decidedRefunds,
|
|
56
|
+
approvedRefunds,
|
|
57
|
+
completedRefunds,
|
|
58
|
+
totalApprovedRefunds,
|
|
59
|
+
totalCompletedRefunds,
|
|
60
|
+
totalCommittedRefunds,
|
|
61
|
+
lineItems,
|
|
62
|
+
payments,
|
|
63
|
+
paymentsTotal,
|
|
64
|
+
subtotal,
|
|
65
|
+
};
|
|
66
|
+
}
|