@openmrs/esm-billing-app 1.2.0 → 1.2.1-pre.1869

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.
@@ -151,10 +151,10 @@
151
151
  "rendered": true,
152
152
  "initial": true,
153
153
  "entry": true,
154
- "size": 5531703,
154
+ "size": 5531794,
155
155
  "sizes": {
156
156
  "consume-shared": 210,
157
- "javascript": 5531157,
157
+ "javascript": 5531248,
158
158
  "runtime": 13411,
159
159
  "share-init": 336
160
160
  },
@@ -171,7 +171,7 @@
171
171
  "auxiliaryFiles": [
172
172
  "main.js.map"
173
173
  ],
174
- "hash": "e8b1902f9bccbcdf",
174
+ "hash": "f3cd0d09b604cb32",
175
175
  "childrenByOrder": {}
176
176
  },
177
177
  {
@@ -1658,10 +1658,10 @@
1658
1658
  "rendered": true,
1659
1659
  "initial": false,
1660
1660
  "entry": false,
1661
- "size": 1598242,
1661
+ "size": 1598333,
1662
1662
  "sizes": {
1663
1663
  "consume-shared": 42,
1664
- "javascript": 1598200
1664
+ "javascript": 1598291
1665
1665
  },
1666
1666
  "names": [],
1667
1667
  "idHints": [],
@@ -1674,7 +1674,7 @@
1674
1674
  "auxiliaryFiles": [
1675
1675
  "990.js.map"
1676
1676
  ],
1677
- "hash": "506460e25a0e7d17",
1677
+ "hash": "239f1da0a4ef4be4",
1678
1678
  "childrenByOrder": {}
1679
1679
  }
1680
1680
  ]
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"billing":">=2.3.0-0","webservices.rest":">=2.24.0"},"pages":[{"component":"billableServicesHome","route":"billable-services"}],"extensions":[{"component":"billingDashboardLink","name":"billing-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"billing","title":"billing","slot":"billing-dashboard-slot"}},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"visit-bills-panel","component":"visitBillsPanel","slot":"visit-summary-panels","meta":{"title":"Bills"}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"billing-history"}},{"name":"billable-services-app-menu-item","component":"billableServicesAppMenuItem","slot":"app-menu-item-slot","meta":{"name":"Billable Services"}},{"name":"billing-checkin-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm"},{"slot":"system-admin-page-card-link-slot","component":"billableServicesCardLink","name":"billable-services-admin-card-link"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"patient-banner-payment-status-tag","component":"patientPaymentStatusTag","slot":"patient-banner-tags-slot","order":3},{"name":"billable-services-left-panel-link","component":"billableServicesLeftPanelLink","slot":"billable-services-left-panel-slot","order":0},{"name":"billing-settings-left-panel-menu","component":"billingSettingsLeftPanelMenu","slot":"billable-services-left-panel-slot","order":1},{"name":"discount-requests-left-panel-link","component":"discountRequestsLeftPanelLink","slot":"billable-services-left-panel-slot","order":2},{"name":"refund-requests-left-panel-link","component":"refundRequestsLeftPanelLink","slot":"billable-services-left-panel-slot","order":3}],"modals":[{"name":"add-cash-point-modal","component":"addCashPointModal"},{"name":"payment-mode-form-modal","component":"paymentModeFormModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"edit-bill-item-modal","component":"editBillLineItemModal"},{"name":"edit-bill-line-item-modal","component":"editBillLineItemModal"},{"name":"require-billing-modal","component":"requirePaymentModal"},{"name":"delete-line-item-confirmation-modal","component":"deleteLineItemConfirmationModal"},{"name":"finalize-bill-confirmation-modal","component":"finalizeBillConfirmationModal"},{"name":"delete-bill-confirmation-modal","component":"deleteBillConfirmationModal"},{"name":"request-discount-modal","component":"requestDiscountModal"},{"name":"review-bill-discounts-modal","component":"reviewBillDiscountsModal"},{"name":"request-refund-modal","component":"requestRefundModal"},{"name":"review-bill-refunds-modal","component":"reviewBillRefundsModal"}],"workspaces2":[{"name":"billing-form-workspace","component":"billingFormWorkspace","window":"billing-form-window"},{"name":"billable-service-form","component":"billableServiceFormWorkspace","window":"billable-service-form-window"}],"workspaceWindows2":[{"name":"billing-form-window","group":"billingFormWorkspaceGroup"},{"name":"billable-service-form-window","group":"billingFormWorkspaceGroup","width":"wider"}],"workspaceGroups2":[{"name":"billingFormWorkspaceGroup","overlay":false}],"version":"1.2.0"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"billing":">=2.3.0-0","webservices.rest":">=2.24.0"},"pages":[{"component":"billableServicesHome","route":"billable-services"}],"extensions":[{"component":"billingDashboardLink","name":"billing-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"billing","title":"billing","slot":"billing-dashboard-slot"}},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"visit-bills-panel","component":"visitBillsPanel","slot":"visit-summary-panels","meta":{"title":"Bills"}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"billing-history"}},{"name":"billable-services-app-menu-item","component":"billableServicesAppMenuItem","slot":"app-menu-item-slot","meta":{"name":"Billable Services"}},{"name":"billing-checkin-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm"},{"slot":"system-admin-page-card-link-slot","component":"billableServicesCardLink","name":"billable-services-admin-card-link"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"patient-banner-payment-status-tag","component":"patientPaymentStatusTag","slot":"patient-banner-tags-slot","order":3},{"name":"billable-services-left-panel-link","component":"billableServicesLeftPanelLink","slot":"billable-services-left-panel-slot","order":0},{"name":"billing-settings-left-panel-menu","component":"billingSettingsLeftPanelMenu","slot":"billable-services-left-panel-slot","order":1},{"name":"discount-requests-left-panel-link","component":"discountRequestsLeftPanelLink","slot":"billable-services-left-panel-slot","order":2},{"name":"refund-requests-left-panel-link","component":"refundRequestsLeftPanelLink","slot":"billable-services-left-panel-slot","order":3}],"modals":[{"name":"add-cash-point-modal","component":"addCashPointModal"},{"name":"payment-mode-form-modal","component":"paymentModeFormModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"edit-bill-item-modal","component":"editBillLineItemModal"},{"name":"edit-bill-line-item-modal","component":"editBillLineItemModal"},{"name":"require-billing-modal","component":"requirePaymentModal"},{"name":"delete-line-item-confirmation-modal","component":"deleteLineItemConfirmationModal"},{"name":"finalize-bill-confirmation-modal","component":"finalizeBillConfirmationModal"},{"name":"delete-bill-confirmation-modal","component":"deleteBillConfirmationModal"},{"name":"request-discount-modal","component":"requestDiscountModal"},{"name":"review-bill-discounts-modal","component":"reviewBillDiscountsModal"},{"name":"request-refund-modal","component":"requestRefundModal"},{"name":"review-bill-refunds-modal","component":"reviewBillRefundsModal"}],"workspaces2":[{"name":"billing-form-workspace","component":"billingFormWorkspace","window":"billing-form-window"},{"name":"billable-service-form","component":"billableServiceFormWorkspace","window":"billable-service-form-window"}],"workspaceWindows2":[{"name":"billing-form-window","group":"billingFormWorkspaceGroup"},{"name":"billable-service-form-window","group":"billingFormWorkspaceGroup","width":"wider"}],"workspaceGroups2":[{"name":"billingFormWorkspaceGroup","overlay":false}],"version":"1.2.1-pre.1869"}
@@ -3,3 +3,4 @@ export * from './billing-form-page';
3
3
  export * from './invoice-page';
4
4
  export * from './payment-page';
5
5
  export * from './discounts-page';
6
+ export * from './refunds-page';
@@ -0,0 +1,63 @@
1
+ import { type Page, expect } from '@playwright/test';
2
+
3
+ export class RefundRequestModal {
4
+ constructor(readonly page: Page) {}
5
+
6
+ readonly modal = () =>
7
+ this.page.getByRole('dialog').filter({ has: this.page.getByRole('heading', { name: /request refund/i }) });
8
+ readonly amountInput = () => this.modal().locator('#refund-amount');
9
+ readonly reasonInput = () => this.modal().locator('#refund-reason');
10
+ readonly submitButton = () => this.modal().getByRole('button', { name: /submit request/i });
11
+
12
+ async submitRefund(amount: number, reason: string) {
13
+ await this.modal().waitFor({ state: 'visible' });
14
+ // Carbon NumberInput: clear then type
15
+ await this.amountInput().fill(amount.toString());
16
+ await this.reasonInput().fill(reason);
17
+ await expect(this.submitButton()).toBeEnabled();
18
+ await this.submitButton().click();
19
+ }
20
+ }
21
+
22
+ export class RefundRequestsAdminPage {
23
+ constructor(readonly page: Page) {}
24
+
25
+ readonly heading = () => this.page.getByRole('heading', { name: /refund requests/i });
26
+ readonly filterDropdown = () => this.page.getByRole('combobox', { name: /filter by/i });
27
+ readonly requestsTable = () => this.page.getByRole('table', { name: /refund requests/i });
28
+ readonly searchInput = () => this.page.getByTestId('refundRequestsSearchBar');
29
+
30
+ async goto() {
31
+ await this.page.goto('billable-services/refund-requests');
32
+ }
33
+
34
+ async waitForLoaded() {
35
+ await this.heading().waitFor({ state: 'visible' });
36
+ }
37
+
38
+ async openReviewForPatient(patientName: string) {
39
+ await this.searchInput().fill(patientName);
40
+ const row = this.requestsTable().locator('tbody tr').filter({ hasText: patientName }).first();
41
+ await row.waitFor({ state: 'visible' });
42
+ await row.getByRole('link').first().click();
43
+ }
44
+ }
45
+
46
+ export class ReviewBillRefundsModal {
47
+ constructor(readonly page: Page) {}
48
+
49
+ readonly modal = () =>
50
+ this.page.getByRole('dialog').filter({ has: this.page.getByRole('heading', { name: /review refunds/i }) });
51
+ readonly firstRefundCard = () => this.modal().locator('article').first();
52
+
53
+ async waitForLoaded() {
54
+ await this.modal().waitFor({ state: 'visible' });
55
+ await this.firstRefundCard().waitFor({ state: 'visible' });
56
+ }
57
+
58
+ async approveFirstPending() {
59
+ await this.firstRefundCard()
60
+ .getByRole('button', { name: /^approve$/i })
61
+ .click();
62
+ }
63
+ }
@@ -0,0 +1,173 @@
1
+ import { expect } from '@playwright/test';
2
+ import { test } from '../core/test';
3
+ import { deleteBill, ensureServiceHasPrices, extractNumericValue, waitForSuccessNotification } from '../commands';
4
+ import {
5
+ BillingDashboardPage,
6
+ BillingFormPage,
7
+ InvoicePage,
8
+ PaymentPage,
9
+ RefundRequestModal,
10
+ RefundRequestsAdminPage,
11
+ ReviewBillRefundsModal,
12
+ } from '../pages';
13
+
14
+ test.describe('Bill refund workflow', () => {
15
+ test.describe.configure({ mode: 'serial' });
16
+
17
+ let testServiceName: string;
18
+ const billsToCleanup = new Set<string>();
19
+
20
+ test.beforeAll(async ({ api }) => {
21
+ const serviceUuid = process.env.E2E_TEST_SERVICE_UUID;
22
+ if (!serviceUuid) {
23
+ throw new Error('E2E_TEST_SERVICE_UUID must be configured in .env file');
24
+ }
25
+
26
+ const service = await ensureServiceHasPrices(api, serviceUuid, 30);
27
+ testServiceName = service.name;
28
+
29
+ const cashPrice = service.servicePrices.find((sp) => sp.name === 'Cash');
30
+ if (!cashPrice) {
31
+ throw new Error('Cash price not found for test service');
32
+ }
33
+ });
34
+
35
+ test.afterEach(async ({ api }) => {
36
+ for (const billUuid of billsToCleanup) {
37
+ try {
38
+ await deleteBill(api, billUuid);
39
+ } catch (error) {
40
+ console.error(`Failed to delete bill ${billUuid}:`, error);
41
+ }
42
+ }
43
+ billsToCleanup.clear();
44
+ });
45
+
46
+ test('Cashier requests a refund, admin approves it, cashier processes it', async ({ page, api, patient }) => {
47
+ const billingDashboardPage = new BillingDashboardPage(page);
48
+ const billingFormPage = new BillingFormPage(page);
49
+ const invoicePage = new InvoicePage(page);
50
+ const paymentPage = new PaymentPage(page);
51
+ const refundModal = new RefundRequestModal(page);
52
+ const refundRequestsPage = new RefundRequestsAdminPage(page);
53
+ const reviewModal = new ReviewBillRefundsModal(page);
54
+
55
+ const patientUuid = patient.uuid;
56
+ const patientName = patient.person.display;
57
+ const refundReason = `e2e test refund ${Date.now()}`;
58
+ let billUuid: string;
59
+ let refundAmount: number;
60
+
61
+ await test.step('Given a PAID bill exists for the patient', async () => {
62
+ await page.goto(`patient/${patientUuid}/chart/billing-history`);
63
+ await page.getByRole('button', { name: /launch bill form|add bill|create bill/i }).click();
64
+
65
+ await billingFormPage.searchAndSelectBillableService(testServiceName);
66
+ await billingFormPage.selectPaymentMethodIfVisible();
67
+ await billingFormPage.saveBill();
68
+ await waitForSuccessNotification(page, /bill processed successfully/i);
69
+
70
+ const billsResponse = await api.get(`billing/bill?patient=${patientUuid}&v=full`);
71
+ const billsData = await billsResponse.json();
72
+ billUuid = billsData.results[0].uuid;
73
+ billsToCleanup.add(billUuid);
74
+
75
+ await billingDashboardPage.goto();
76
+ await billingDashboardPage.waitForBillsTableToLoad();
77
+ await billingDashboardPage.selectFilter('Pending confirmation');
78
+ await billingDashboardPage.waitForBillsTableToLoad();
79
+ await billingDashboardPage.clickInvoiceNumberLink(patientName);
80
+ await invoicePage.waitForInvoiceToLoad();
81
+
82
+ await invoicePage.finalizeBill();
83
+ await waitForSuccessNotification(page, /bill finalized/i);
84
+ await expect.poll(async () => await invoicePage.getInvoiceStatus()).toBe('POSTED');
85
+
86
+ await paymentPage.waitForPaymentForm();
87
+ const amountDue = await invoicePage.getAmountDue();
88
+ await paymentPage.addPayment('Cash', extractNumericValue(amountDue));
89
+ await paymentPage.processPayment();
90
+ await waitForSuccessNotification(page, /payment processed successfully/i);
91
+
92
+ await page.reload();
93
+ await invoicePage.waitForInvoiceToLoad();
94
+ await expect.poll(async () => await invoicePage.getInvoiceStatus()).toBe('PAID');
95
+ });
96
+
97
+ await test.step('When the cashier requests a refund for the full bill amount', async () => {
98
+ const totalAmount = await invoicePage.getTotalAmount();
99
+ refundAmount = extractNumericValue(totalAmount);
100
+
101
+ await page.getByRole('button', { name: /request refund/i }).click();
102
+ await refundModal.submitRefund(refundAmount, refundReason);
103
+ await waitForSuccessNotification(page, /refund request submitted/i);
104
+ });
105
+
106
+ await test.step('Then the refund appears as REQUESTED via the API', async () => {
107
+ const billResponse = await api.get(`billing/bill/${billUuid}?v=full`);
108
+ const billData = await billResponse.json();
109
+ const refunds: Array<{ status: string; refundAmount: number; reason: string }> = billData.refunds;
110
+ expect(refunds).toHaveLength(1);
111
+ expect(refunds[0].status).toBe('REQUESTED');
112
+ expect(refunds[0].refundAmount).toBeCloseTo(refundAmount, 2);
113
+ expect(refunds[0].reason).toBe(refundReason);
114
+ });
115
+
116
+ await test.step('When the admin approves the refund from the refund requests dashboard', async () => {
117
+ await refundRequestsPage.goto();
118
+ await refundRequestsPage.waitForLoaded();
119
+ await refundRequestsPage.openReviewForPatient(patientName);
120
+
121
+ await reviewModal.waitForLoaded();
122
+ await reviewModal.approveFirstPending();
123
+ await waitForSuccessNotification(page, /refund approved/i);
124
+ });
125
+
126
+ await test.step('Then the refund status is APPROVED via the API', async () => {
127
+ await expect
128
+ .poll(async () => {
129
+ const res = await api.get(`billing/bill/${billUuid}?v=full`);
130
+ const data = await res.json();
131
+ return (data.refunds ?? [])[0]?.status;
132
+ })
133
+ .toBe('APPROVED');
134
+ });
135
+
136
+ await test.step('When the cashier processes the approved refund from the invoice page', async () => {
137
+ await invoicePage.goto(patientUuid, billUuid);
138
+ await invoicePage.waitForInvoiceToLoad();
139
+
140
+ const processButton = page.getByRole('button', { name: /process refund/i });
141
+ await expect(processButton).toBeEnabled();
142
+ await processButton.click();
143
+ await waitForSuccessNotification(page, /refund processed/i);
144
+ });
145
+
146
+ await test.step('Then the refund status is COMPLETED via the API', async () => {
147
+ await expect
148
+ .poll(async () => {
149
+ const res = await api.get(`billing/bill/${billUuid}?v=full`);
150
+ const data = await res.json();
151
+ return (data.refunds ?? [])[0]?.status;
152
+ })
153
+ .toBe('COMPLETED');
154
+ });
155
+
156
+ await test.step('And the completed refund is visible in the refunds table on the invoice', async () => {
157
+ const billDataLoaded = page.waitForResponse(
158
+ (response) => response.url().includes(`billing/bill/${billUuid}`) && response.status() === 200,
159
+ );
160
+ await invoicePage.goto(patientUuid, billUuid);
161
+ await billDataLoaded;
162
+
163
+ const refundsTable = page.getByRole('table', { name: /^refunds$/i });
164
+ await expect(refundsTable).toBeVisible();
165
+
166
+ const refundRows = refundsTable.locator('tbody tr');
167
+ await expect(refundRows).toHaveCount(1);
168
+
169
+ const statusCell = refundRows.first().locator('td').nth(3);
170
+ await expect(statusCell).toContainText(/completed/i);
171
+ });
172
+ });
173
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-billing-app",
3
- "version": "1.2.0",
3
+ "version": "1.2.1-pre.1869",
4
4
  "description": "O3 frontend module for handling billing concerns in healthcare settings",
5
5
  "browser": "dist/openmrs-esm-billing-app.js",
6
6
  "main": "src/index.ts",
@@ -106,5 +106,6 @@
106
106
  "*.{ts,tsx}": "eslint --cache --fix --max-warnings 0",
107
107
  "*.{css,scss,ts,tsx}": "prettier --write --list-different"
108
108
  },
109
- "packageManager": "yarn@4.10.3"
109
+ "packageManager": "yarn@4.10.3",
110
+ "stableVersion": "1.2.0"
110
111
  }
@@ -34,6 +34,8 @@
34
34
  }
35
35
 
36
36
  .searchbox {
37
+ height: 3rem;
38
+
37
39
  input:focus {
38
40
  outline: 2px solid colors.$orange-40 !important;
39
41
  }
@@ -61,7 +63,7 @@
61
63
  border-bottom: 0.375rem solid var(--brand-03);
62
64
  }
63
65
 
64
- & > span {
66
+ &>span {
65
67
  @include type.type-style('body-01');
66
68
  }
67
- }
69
+ }
@@ -1 +0,0 @@
1
- {"hash":"31f1dfc7f71601df","duration":17644}