@openmrs/esm-billing-app 1.0.2-pre.112 → 1.0.2-pre.1124

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.
Files changed (241) hide show
  1. package/.eslintrc +16 -2
  2. package/README.md +54 -9
  3. package/__mocks__/bills.mock.ts +12 -0
  4. package/__mocks__/react-i18next.js +6 -5
  5. package/dist/1119.js +1 -1
  6. package/dist/1146.js +1 -2
  7. package/dist/1146.js.map +1 -1
  8. package/dist/1197.js +1 -1
  9. package/dist/1537.js +1 -0
  10. package/dist/1537.js.map +1 -0
  11. package/dist/1856.js +1 -0
  12. package/dist/1856.js.map +1 -0
  13. package/dist/2146.js +1 -1
  14. package/dist/2177.js +1 -1
  15. package/dist/2177.js.map +1 -1
  16. package/dist/2524.js +1 -0
  17. package/dist/2524.js.map +1 -0
  18. package/dist/2690.js +1 -1
  19. package/dist/3099.js +1 -1
  20. package/dist/3584.js +1 -1
  21. package/dist/3717.js +2 -0
  22. package/dist/3717.js.map +1 -0
  23. package/dist/4055.js +1 -1
  24. package/dist/4132.js +1 -1
  25. package/dist/4300.js +1 -1
  26. package/dist/4335.js +1 -1
  27. package/dist/439.js +1 -0
  28. package/dist/4618.js +1 -1
  29. package/dist/4652.js +1 -1
  30. package/dist/4724.js +1 -0
  31. package/dist/4724.js.map +1 -0
  32. package/dist/4739.js +1 -1
  33. package/dist/4739.js.map +1 -1
  34. package/dist/4944.js +1 -1
  35. package/dist/5173.js +1 -1
  36. package/dist/5241.js +1 -1
  37. package/dist/5442.js +1 -1
  38. package/dist/5661.js +1 -1
  39. package/dist/6022.js +1 -1
  40. package/dist/6468.js +1 -1
  41. package/dist/6540.js +1 -1
  42. package/dist/6540.js.map +1 -1
  43. package/dist/6589.js +1 -0
  44. package/dist/6679.js +1 -1
  45. package/dist/6840.js +1 -1
  46. package/dist/6859.js +1 -1
  47. package/dist/7097.js +1 -1
  48. package/dist/7159.js +1 -1
  49. package/dist/723.js +1 -1
  50. package/dist/7255.js +1 -1
  51. package/dist/7255.js.map +1 -1
  52. package/dist/7617.js +1 -1
  53. package/dist/795.js +1 -1
  54. package/dist/8163.js +1 -1
  55. package/dist/8349.js +1 -1
  56. package/dist/8371.js +1 -0
  57. package/dist/8572.js +1 -0
  58. package/dist/8572.js.map +1 -0
  59. package/dist/8618.js +1 -1
  60. package/dist/8708.js +2 -0
  61. package/dist/{6557.js.LICENSE.txt → 8708.js.LICENSE.txt} +22 -0
  62. package/dist/8708.js.map +1 -0
  63. package/dist/890.js +1 -1
  64. package/dist/9214.js +1 -1
  65. package/dist/9538.js +1 -1
  66. package/dist/9569.js +1 -1
  67. package/dist/961.js +1 -1
  68. package/dist/961.js.map +1 -1
  69. package/dist/986.js +1 -1
  70. package/dist/9879.js +1 -1
  71. package/dist/9895.js +1 -1
  72. package/dist/9900.js +1 -1
  73. package/dist/9913.js +1 -1
  74. package/dist/main.js +1 -1
  75. package/dist/main.js.map +1 -1
  76. package/dist/openmrs-esm-billing-app.js +1 -1
  77. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +321 -249
  78. package/dist/openmrs-esm-billing-app.js.map +1 -1
  79. package/dist/routes.json +1 -1
  80. package/e2e/README.md +19 -18
  81. package/e2e/commands/billing-operations.ts +96 -0
  82. package/e2e/commands/index.ts +3 -0
  83. package/e2e/commands/patient-operations.ts +82 -0
  84. package/e2e/commands/types.ts +73 -0
  85. package/e2e/core/test.ts +12 -1
  86. package/e2e/fixtures/api.ts +2 -3
  87. package/e2e/pages/billing-dashboard-page.ts +84 -0
  88. package/e2e/pages/billing-form-page.ts +114 -0
  89. package/e2e/pages/index.ts +4 -1
  90. package/e2e/pages/invoice-page.ts +113 -0
  91. package/e2e/pages/payment-page.ts +104 -0
  92. package/e2e/specs/billing-dashboard.spec.ts +203 -0
  93. package/e2e/specs/billing-patient-chart.spec.ts +738 -0
  94. package/e2e/support/github/Dockerfile +1 -1
  95. package/e2e/support/github/run-e2e-docker-env.sh +29 -32
  96. package/example.env +15 -1
  97. package/package.json +19 -16
  98. package/playwright.config.ts +4 -2
  99. package/src/bill-history/bill-history.component.tsx +15 -17
  100. package/src/bill-history/bill-history.scss +4 -94
  101. package/src/bill-history/bill-history.test.tsx +37 -77
  102. package/src/bill-item-actions/bill-item-actions.scss +21 -5
  103. package/src/bill-item-actions/edit-bill-item.modal.tsx +227 -0
  104. package/src/bill-item-actions/edit-bill-item.test.tsx +233 -40
  105. package/src/billable-services/bill-waiver/bill-selection.component.tsx +5 -5
  106. package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +37 -39
  107. package/src/billable-services/bill-waiver/patient-bills.component.tsx +7 -7
  108. package/src/billable-services/bill-waiver/utils.ts +13 -3
  109. package/src/billable-services/{create-edit/add-billable-service.scss → billable-service-form/billable-service-form.scss} +32 -64
  110. package/src/billable-services/billable-service-form/billable-service-form.test.tsx +1099 -0
  111. package/src/billable-services/billable-service-form/billable-service-form.workspace.tsx +516 -0
  112. package/src/billable-services/billable-service.resource.ts +71 -27
  113. package/src/billable-services/billable-services-home.component.tsx +13 -42
  114. package/src/billable-services/billable-services-left-panel-link.component.tsx +50 -0
  115. package/src/billable-services/billable-services-left-panel-menu.component.tsx +48 -0
  116. package/src/billable-services/billable-services-menu-item/item.component.tsx +5 -4
  117. package/src/billable-services/billable-services.component.tsx +156 -152
  118. package/src/billable-services/billable-services.scss +29 -0
  119. package/src/billable-services/billable-services.test.tsx +6 -49
  120. package/src/billable-services/cash-point/add-cash-point.modal.tsx +170 -0
  121. package/src/billable-services/cash-point/cash-point-configuration.component.tsx +19 -193
  122. package/src/billable-services/cash-point/cash-point-configuration.scss +1 -5
  123. package/src/billable-services/dashboard/dashboard.component.tsx +0 -2
  124. package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +77 -0
  125. package/src/billable-services/payment-modes/payment-mode-form.modal.tsx +131 -0
  126. package/src/billable-services/payment-modes/payment-modes-config.component.tsx +139 -0
  127. package/src/billable-services/{payyment-modes → payment-modes}/payment-modes-config.scss +5 -4
  128. package/src/billable-services-admin-card-link.component.test.tsx +2 -2
  129. package/src/billable-services-admin-card-link.component.tsx +1 -1
  130. package/src/billing-dashboard/billing-dashboard.scss +1 -1
  131. package/src/billing-form/billing-checkin-form.component.tsx +29 -29
  132. package/src/billing-form/billing-checkin-form.scss +0 -7
  133. package/src/billing-form/billing-checkin-form.test.tsx +100 -27
  134. package/src/billing-form/billing-form.component.tsx +226 -284
  135. package/src/billing-form/billing-form.scss +143 -0
  136. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +47 -41
  137. package/src/billing-form/visit-attributes/visit-attributes-form.scss +16 -3
  138. package/src/billing.resource.test.ts +503 -0
  139. package/src/billing.resource.ts +68 -76
  140. package/src/bills-table/bills-table.component.tsx +17 -24
  141. package/src/bills-table/bills-table.test.tsx +98 -54
  142. package/src/config-schema.ts +84 -31
  143. package/src/dashboard.meta.ts +5 -3
  144. package/src/helpers/functions.ts +5 -4
  145. package/src/index.ts +75 -13
  146. package/src/invoice/invoice-table.component.tsx +36 -70
  147. package/src/invoice/invoice-table.scss +8 -5
  148. package/src/invoice/invoice-table.test.tsx +273 -62
  149. package/src/invoice/invoice.component.tsx +44 -38
  150. package/src/invoice/invoice.scss +11 -4
  151. package/src/invoice/invoice.test.tsx +324 -120
  152. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +9 -9
  153. package/src/invoice/payments/payment-form/payment-form.component.tsx +45 -36
  154. package/src/invoice/payments/payment-form/payment-form.scss +5 -6
  155. package/src/invoice/payments/payment-form/payment-form.test.tsx +217 -67
  156. package/src/invoice/payments/payment-history/payment-history.component.tsx +6 -4
  157. package/src/invoice/payments/payment-history/payment-history.test.tsx +9 -14
  158. package/src/invoice/payments/payments.component.tsx +56 -68
  159. package/src/invoice/payments/payments.scss +4 -3
  160. package/src/invoice/payments/payments.test.tsx +282 -0
  161. package/src/invoice/payments/utils.ts +15 -27
  162. package/src/invoice/printable-invoice/print-receipt.component.tsx +3 -3
  163. package/src/invoice/printable-invoice/print-receipt.test.tsx +14 -25
  164. package/src/invoice/printable-invoice/printable-footer.component.tsx +2 -2
  165. package/src/invoice/printable-invoice/printable-footer.test.tsx +4 -13
  166. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +20 -11
  167. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +95 -16
  168. package/src/invoice/printable-invoice/printable-invoice.component.tsx +21 -35
  169. package/src/left-panel-link.component.tsx +9 -8
  170. package/src/left-panel-link.test.tsx +1 -4
  171. package/src/metrics-cards/metrics-cards.component.tsx +16 -6
  172. package/src/metrics-cards/metrics-cards.scss +4 -0
  173. package/src/metrics-cards/metrics-cards.test.tsx +18 -5
  174. package/src/modal/require-payment-modal.test.tsx +27 -22
  175. package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +18 -19
  176. package/src/routes.json +44 -20
  177. package/src/types/index.ts +87 -24
  178. package/translations/am.json +140 -80
  179. package/translations/ar.json +141 -81
  180. package/translations/ar_SY.json +141 -81
  181. package/translations/bn.json +143 -83
  182. package/translations/cs.json +230 -0
  183. package/translations/de.json +141 -81
  184. package/translations/en.json +142 -81
  185. package/translations/en_US.json +141 -81
  186. package/translations/es.json +140 -80
  187. package/translations/es_MX.json +141 -81
  188. package/translations/fr.json +224 -164
  189. package/translations/he.json +140 -80
  190. package/translations/hi.json +141 -81
  191. package/translations/hi_IN.json +141 -81
  192. package/translations/id.json +141 -81
  193. package/translations/it.json +166 -106
  194. package/translations/ka.json +141 -81
  195. package/translations/km.json +140 -80
  196. package/translations/ku.json +141 -81
  197. package/translations/ky.json +141 -81
  198. package/translations/lg.json +141 -81
  199. package/translations/ne.json +141 -81
  200. package/translations/pl.json +141 -81
  201. package/translations/pt.json +141 -81
  202. package/translations/pt_BR.json +141 -81
  203. package/translations/qu.json +141 -81
  204. package/translations/ro_RO.json +225 -165
  205. package/translations/ru_RU.json +141 -81
  206. package/translations/si.json +141 -81
  207. package/translations/sq.json +230 -0
  208. package/translations/sw.json +141 -81
  209. package/translations/sw_KE.json +141 -81
  210. package/translations/tr.json +141 -81
  211. package/translations/tr_TR.json +141 -81
  212. package/translations/uk.json +141 -81
  213. package/translations/uz.json +141 -81
  214. package/translations/uz@Latn.json +141 -81
  215. package/translations/uz_UZ.json +141 -81
  216. package/translations/vi.json +141 -81
  217. package/translations/zh.json +141 -81
  218. package/translations/zh_CN.json +171 -111
  219. package/translations/zh_TW.json +230 -0
  220. package/dist/1146.js.LICENSE.txt +0 -21
  221. package/dist/2352.js +0 -1
  222. package/dist/2352.js.map +0 -1
  223. package/dist/246.js +0 -1
  224. package/dist/246.js.map +0 -1
  225. package/dist/4689.js +0 -2
  226. package/dist/4689.js.map +0 -1
  227. package/dist/6557.js +0 -2
  228. package/dist/6557.js.map +0 -1
  229. package/dist/8638.js +0 -1
  230. package/dist/8638.js.map +0 -1
  231. package/dist/9968.js +0 -1
  232. package/dist/9968.js.map +0 -1
  233. package/e2e/pages/home-page.ts +0 -9
  234. package/e2e/specs/sample-test.spec.ts +0 -11
  235. package/src/bill-item-actions/edit-bill-item.component.tsx +0 -221
  236. package/src/billable-services/create-edit/add-billable-service.component.tsx +0 -401
  237. package/src/billable-services/create-edit/add-billable-service.test.tsx +0 -154
  238. package/src/billable-services/dashboard/service-metrics.component.tsx +0 -41
  239. package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
  240. package/src/invoice/payments/payments.component.test.tsx +0 -121
  241. /package/dist/{4689.js.LICENSE.txt → 3717.js.LICENSE.txt} +0 -0
@@ -0,0 +1,738 @@
1
+ import { expect } from '@playwright/test';
2
+ import { test } from '../core/test';
3
+ import { deleteBill, extractNumericValue, ensureServiceHasPrices, waitForSuccessNotification } from '../commands';
4
+ import { BillingFormPage, InvoicePage, PaymentPage } from '../pages';
5
+
6
+ test.describe('Billing: Patient Chart workflow', () => {
7
+ // Run tests serially to avoid race conditions when setting up service prices
8
+ test.describe.configure({ mode: 'serial' });
9
+
10
+ let testServiceName: string;
11
+ let expectedServicePrice: number;
12
+ const billsToCleanup = new Set<string>();
13
+
14
+ test.beforeAll(async ({ api }) => {
15
+ const serviceUuid = process.env.E2E_TEST_SERVICE_UUID;
16
+ if (!serviceUuid) {
17
+ throw new Error('E2E_TEST_SERVICE_UUID must be configured in .env file');
18
+ }
19
+
20
+ // Ensure the test service has prices configured (required for billing tests)
21
+ // If prices already exist, this will skip; otherwise it adds a default Cash price of 30.00
22
+ const service = await ensureServiceHasPrices(api, serviceUuid, 30.0);
23
+
24
+ testServiceName = service.name;
25
+
26
+ const cashPrice = service.servicePrices.find((sp) => sp.name === 'Cash');
27
+ if (!cashPrice) {
28
+ throw new Error('Cash price not found for test service');
29
+ }
30
+ expectedServicePrice = parseFloat(cashPrice.price);
31
+ });
32
+
33
+ test.afterEach(async ({ api }) => {
34
+ // Cleanup: delete all bills created during tests
35
+ for (const billUuid of billsToCleanup) {
36
+ try {
37
+ await deleteBill(api, billUuid);
38
+ } catch (error) {
39
+ // Log but don't fail test if cleanup fails
40
+ console.error(`Failed to delete bill ${billUuid}:`, error);
41
+ }
42
+ }
43
+ billsToCleanup.clear();
44
+ });
45
+
46
+ test('Create bill, verify receipt number, and process full payment', async ({ page, api, patient }) => {
47
+ const billingFormPage = new BillingFormPage(page);
48
+ const invoicePage = new InvoicePage(page);
49
+ const paymentPage = new PaymentPage(page);
50
+ const patientUuid = patient.uuid;
51
+ let billUuid: string;
52
+
53
+ await test.step('When I navigate to the Billing history page', async () => {
54
+ await page.goto(`patient/${patientUuid}/chart/Billing history`);
55
+ });
56
+
57
+ await test.step('And I launch the Create Bill form', async () => {
58
+ const createBillButton = page.getByRole('button', { name: /launch bill form|add bill/i });
59
+ await createBillButton.click();
60
+ });
61
+
62
+ await test.step('And I search for and select a billable service', async () => {
63
+ await billingFormPage.searchAndSelectBillableService(testServiceName);
64
+ await expect(billingFormPage.billableServicesCombobox()).toBeVisible();
65
+ await expect(page.getByText(testServiceName, { exact: false })).toBeVisible();
66
+ await billingFormPage.selectPaymentMethodIfVisible();
67
+ });
68
+
69
+ await test.step('Then I should see the calculated grand total for the selected items', async () => {
70
+ const grandTotal = await billingFormPage.getGrandTotal();
71
+ expect(grandTotal).toBeTruthy();
72
+ const grandTotalValue = extractNumericValue(grandTotal);
73
+ // Use toBeCloseTo for decimal comparison (2 decimal places for currency)
74
+ expect(grandTotalValue).toBeCloseTo(expectedServicePrice, 2);
75
+ });
76
+
77
+ await test.step('When I save the bill', async () => {
78
+ await billingFormPage.saveBill();
79
+ await waitForSuccessNotification(page, 'Bill processed successfully');
80
+ });
81
+
82
+ await test.step('Then the bill should be created', async () => {
83
+ const billsResponse = await api.get(`billing/bill?patient=${patientUuid}&v=full`);
84
+ expect(billsResponse.ok()).toBeTruthy();
85
+ const billsData = await billsResponse.json();
86
+ expect(billsData.results.length).toBeGreaterThan(0);
87
+
88
+ const bill = billsData.results[0];
89
+ billUuid = bill.uuid;
90
+ billsToCleanup.add(billUuid);
91
+ });
92
+
93
+ await test.step('When I navigate to the invoice page', async () => {
94
+ await invoicePage.goto(patientUuid, billUuid);
95
+ await invoicePage.waitForInvoiceToLoad();
96
+ });
97
+
98
+ await test.step('Then I should see the invoice details with correct initial state', async () => {
99
+ const invoiceStatus = await invoicePage.getInvoiceStatus();
100
+ expect(invoiceStatus).toBe('PENDING');
101
+
102
+ const totalAmount = await invoicePage.getTotalAmount();
103
+ const amountDue = await invoicePage.getAmountDue();
104
+ expect(totalAmount).toBeTruthy();
105
+ expect(amountDue).toEqual(totalAmount);
106
+
107
+ const totalValue = extractNumericValue(totalAmount);
108
+ expect(totalValue).toBeCloseTo(expectedServicePrice, 2);
109
+
110
+ const receiptNumber = await invoicePage.getInvoiceNumber();
111
+ expect(receiptNumber).toBeTruthy();
112
+ await expect(invoicePage.invoiceNumberLabel()).toBeVisible();
113
+ });
114
+
115
+ await test.step('When I process the full amount due', async () => {
116
+ await paymentPage.waitForPaymentForm();
117
+ const amountDue = await invoicePage.getAmountDue();
118
+ const amountDueValue = extractNumericValue(amountDue);
119
+ await paymentPage.addPayment('Cash', amountDueValue);
120
+ await expect(paymentPage.amountInput()).toHaveValue(amountDueValue.toString());
121
+ await paymentPage.processPayment();
122
+ await waitForSuccessNotification(page, 'Payment processed successfully');
123
+ });
124
+
125
+ await test.step('Then the bill should be marked as paid', async () => {
126
+ await page.reload();
127
+ await invoicePage.waitForInvoiceToLoad();
128
+
129
+ const updatedStatus = await invoicePage.getInvoiceStatus();
130
+ expect(updatedStatus).toBe('PAID');
131
+
132
+ const billResponse = await api.get(`billing/bill/${billUuid}`);
133
+ const billData = await billResponse.json();
134
+ expect(billData.status).toBe('PAID');
135
+
136
+ const totalAmount = await invoicePage.getTotalAmount();
137
+ const updatedAmountTendered = await invoicePage.getAmountTendered();
138
+ expect(updatedAmountTendered).toEqual(totalAmount);
139
+
140
+ const updatedAmountDue = await invoicePage.getAmountDue();
141
+ expect(extractNumericValue(updatedAmountDue)).toBe(0);
142
+ });
143
+
144
+ await test.step('And the payment should appear in history', async () => {
145
+ const paymentHistory = await paymentPage.getPaymentHistory();
146
+ expect(paymentHistory.length).toBeGreaterThan(0);
147
+ expect(paymentHistory[0].method).toContain('Cash');
148
+ });
149
+ });
150
+
151
+ test('Create a bill with quantity update', async ({ page, api, patient }) => {
152
+ const billingFormPage = new BillingFormPage(page);
153
+ const invoicePage = new InvoicePage(page);
154
+ const patientUuid = patient.uuid;
155
+ let billUuid: string;
156
+
157
+ await test.step('When I navigate to the patient billing history', async () => {
158
+ await page.goto(`patient/${patientUuid}/chart/Billing history`);
159
+ });
160
+
161
+ await test.step('And I click the Launch bill form button', async () => {
162
+ const createBillButton = page.getByRole('button', { name: /launch bill form|add bill/i });
163
+ await createBillButton.click();
164
+ });
165
+
166
+ await test.step('And I search for and select a billable service and update quantity to 2', async () => {
167
+ await billingFormPage.searchAndSelectBillableService(testServiceName);
168
+ await expect(page.getByText(testServiceName, { exact: false })).toBeVisible();
169
+ await billingFormPage.selectPaymentMethodIfVisible();
170
+
171
+ const quantityInput = page.locator('input[type="number"]').first();
172
+ await expect(quantityInput).toHaveValue('1');
173
+
174
+ await quantityInput.fill('2');
175
+ await expect(quantityInput).toHaveValue('2');
176
+ });
177
+
178
+ await test.step('When I save the bill', async () => {
179
+ await billingFormPage.saveBill();
180
+ await waitForSuccessNotification(page, 'Bill processed successfully');
181
+ });
182
+
183
+ await test.step('Then the bill should be created with quantity 2', async () => {
184
+ const billsResponse = await api.get(`billing/bill?patient=${patientUuid}&v=full`);
185
+ const billsData = await billsResponse.json();
186
+ billUuid = billsData.results[0].uuid;
187
+ billsToCleanup.add(billUuid);
188
+
189
+ await invoicePage.goto(patientUuid, billUuid);
190
+ await invoicePage.waitForInvoiceToLoad();
191
+
192
+ const lineItems = await invoicePage.getLineItems();
193
+ expect(lineItems.length).toBe(1);
194
+ expect(lineItems[0].quantity).toBe('2');
195
+
196
+ const totalAmount = await invoicePage.getTotalAmount();
197
+ const totalValue = extractNumericValue(totalAmount);
198
+ expect(totalValue).toBeCloseTo(expectedServicePrice * 2, 2);
199
+ });
200
+ });
201
+
202
+ test('Discard bill without saving', async ({ page, api, patient }) => {
203
+ const billingFormPage = new BillingFormPage(page);
204
+ const patientUuid = patient.uuid;
205
+ let initialBillCount: number;
206
+
207
+ await test.step('Given I check the initial bill count', async () => {
208
+ const billsResponse = await api.get(`billing/bill?patient=${patientUuid}&v=full`);
209
+ const billsData = await billsResponse.json();
210
+ initialBillCount = billsData.results.length;
211
+ });
212
+
213
+ await test.step('When I navigate to the patient billing history', async () => {
214
+ await page.goto(`patient/${patientUuid}/chart/Billing history`);
215
+ });
216
+
217
+ await test.step('And I click the Launch bill form button', async () => {
218
+ const createBillButton = page.getByRole('button', { name: /launch bill form|add bill/i });
219
+ await createBillButton.click();
220
+ });
221
+
222
+ await test.step('And I search for and select a billable service', async () => {
223
+ await billingFormPage.searchAndSelectBillableService(testServiceName);
224
+ await expect(page.getByText(testServiceName, { exact: false })).toBeVisible();
225
+ await billingFormPage.selectPaymentMethodIfVisible();
226
+ });
227
+
228
+ await test.step('When I discard the bill', async () => {
229
+ await billingFormPage.discardBill();
230
+ await expect(billingFormPage.discardButton()).toBeHidden();
231
+ });
232
+
233
+ await test.step('Then the bill should not be created', async () => {
234
+ const billsResponse = await api.get(`billing/bill?patient=${patientUuid}&v=full`);
235
+ expect(billsResponse.ok()).toBeTruthy();
236
+ const billsData = await billsResponse.json();
237
+ expect(billsData.results.length).toBe(initialBillCount);
238
+ });
239
+ });
240
+
241
+ test('Remove line item from bill', async ({ page, patient }) => {
242
+ const billingFormPage = new BillingFormPage(page);
243
+ const patientUuid = patient.uuid;
244
+
245
+ await test.step('When I navigate to the patient billing history', async () => {
246
+ await page.goto(`patient/${patientUuid}/chart/Billing history`);
247
+ });
248
+
249
+ await test.step('And I click the Launch bill form button', async () => {
250
+ const createBillButton = page.getByRole('button', { name: /launch bill form|add bill/i });
251
+ await createBillButton.click();
252
+ });
253
+
254
+ await test.step('And I search for and select a billable service', async () => {
255
+ await billingFormPage.searchAndSelectBillableService(testServiceName);
256
+ await expect(page.getByText(testServiceName, { exact: false })).toBeVisible();
257
+ await billingFormPage.selectPaymentMethodIfVisible();
258
+ });
259
+
260
+ await test.step('Then I should see one line item', async () => {
261
+ await expect(billingFormPage.selectedItemCards().first()).toBeVisible();
262
+ const itemCount = await billingFormPage.getLineItemsCount();
263
+ expect(itemCount).toBe(1);
264
+ await expect(billingFormPage.saveButton()).toBeEnabled();
265
+ });
266
+
267
+ await test.step('When I remove the line item', async () => {
268
+ await billingFormPage.removeItem(0);
269
+
270
+ await expect.poll(async () => await billingFormPage.getLineItemsCount()).toBe(0);
271
+ });
272
+
273
+ await test.step('Then the save button should be disabled', async () => {
274
+ await expect(billingFormPage.saveButton()).toBeDisabled();
275
+ });
276
+
277
+ await test.step('When I discard the form', async () => {
278
+ await billingFormPage.discardBill();
279
+ await expect(billingFormPage.discardButton()).toBeHidden();
280
+ });
281
+ });
282
+
283
+ test('Process partial payment and then complete payment', async ({ page, api, patient }) => {
284
+ const billingFormPage = new BillingFormPage(page);
285
+ const invoicePage = new InvoicePage(page);
286
+ const paymentPage = new PaymentPage(page);
287
+ const patientUuid = patient.uuid;
288
+ let billUuid: string;
289
+ let partialAmount: number;
290
+
291
+ await test.step('Given I have created and saved a bill', async () => {
292
+ await page.goto(`patient/${patientUuid}/chart/Billing history`);
293
+
294
+ const createBillButton = page.getByRole('button', { name: /launch bill form|add bill/i });
295
+ await createBillButton.click();
296
+
297
+ await billingFormPage.searchAndSelectBillableService(testServiceName);
298
+ await billingFormPage.selectPaymentMethodIfVisible();
299
+ await billingFormPage.saveBill();
300
+ await waitForSuccessNotification(page, 'Bill processed successfully');
301
+
302
+ const billsResponse = await api.get(`billing/bill?patient=${patientUuid}&v=full`);
303
+ const billsData = await billsResponse.json();
304
+ billUuid = billsData.results[0].uuid;
305
+ billsToCleanup.add(billUuid);
306
+ });
307
+
308
+ await test.step('When I navigate to the invoice page', async () => {
309
+ await invoicePage.goto(patientUuid, billUuid);
310
+ await invoicePage.waitForInvoiceToLoad();
311
+ });
312
+
313
+ await test.step('And I make a partial payment (50% of amount due)', async () => {
314
+ await paymentPage.waitForPaymentForm();
315
+
316
+ const amountDue = await invoicePage.getAmountDue();
317
+ const amountDueValue = extractNumericValue(amountDue);
318
+
319
+ // Round to currency precision (2 decimals)
320
+ partialAmount = Math.round((amountDueValue / 2) * 100) / 100;
321
+
322
+ await paymentPage.addPayment('Cash', partialAmount);
323
+
324
+ await expect(paymentPage.amountInput()).toHaveValue(partialAmount.toString());
325
+
326
+ await paymentPage.processPayment();
327
+ await waitForSuccessNotification(page, 'Payment processed successfully');
328
+
329
+ await expect
330
+ .poll(async () => {
331
+ const history = await paymentPage.getPaymentHistory();
332
+ return history.length;
333
+ })
334
+ .toBeGreaterThan(0);
335
+ });
336
+
337
+ await test.step('Then the bill status should be POSTED after partial payment', async () => {
338
+ await page.reload();
339
+ await invoicePage.waitForInvoiceToLoad();
340
+ const status = await invoicePage.getInvoiceStatus();
341
+ expect(status).toBe('POSTED');
342
+ });
343
+
344
+ await test.step('And the tendered amount should equal the partial payment', async () => {
345
+ const tenderedAmount = await invoicePage.getAmountTendered();
346
+ const tenderedValue = extractNumericValue(tenderedAmount);
347
+ expect(tenderedValue).toBeCloseTo(partialAmount, 2);
348
+ });
349
+
350
+ await test.step('And the amount due should be reduced accordingly', async () => {
351
+ const totalAmount = await invoicePage.getTotalAmount();
352
+ const totalValue = extractNumericValue(totalAmount);
353
+ const amountDue = await invoicePage.getAmountDue();
354
+ const amountDueValue = extractNumericValue(amountDue);
355
+
356
+ expect(amountDueValue).toBeCloseTo(totalValue - partialAmount, 2);
357
+ });
358
+
359
+ await test.step('When I complete the remaining payment', async () => {
360
+ await paymentPage.waitForPaymentForm();
361
+ const amountDue = await invoicePage.getAmountDue();
362
+ const remainingAmount = extractNumericValue(amountDue);
363
+
364
+ await paymentPage.addPayment('Cash', remainingAmount);
365
+ await expect(paymentPage.amountInput()).toHaveValue(remainingAmount.toString());
366
+ await paymentPage.processPayment();
367
+ await waitForSuccessNotification(page, 'Payment processed successfully');
368
+ });
369
+
370
+ await test.step('Then the bill should be marked as PAID', async () => {
371
+ await page.reload();
372
+ await invoicePage.waitForInvoiceToLoad();
373
+
374
+ const status = await invoicePage.getInvoiceStatus();
375
+ expect(status).toBe('PAID');
376
+
377
+ const billResponse = await api.get(`billing/bill/${billUuid}`);
378
+ const billData = await billResponse.json();
379
+ expect(billData.status).toBe('PAID');
380
+ });
381
+
382
+ await test.step('And the amount due should be zero', async () => {
383
+ const amountDue = await invoicePage.getAmountDue();
384
+ const amountDueValue = extractNumericValue(amountDue);
385
+ expect(amountDueValue).toBe(0);
386
+
387
+ const totalAmount = await invoicePage.getTotalAmount();
388
+ const totalValue = extractNumericValue(totalAmount);
389
+ const tenderedAmount = await invoicePage.getAmountTendered();
390
+ const tenderedValue = extractNumericValue(tenderedAmount);
391
+ expect(tenderedValue).toBeCloseTo(totalValue, 2);
392
+ });
393
+ });
394
+
395
+ test('Create bill with increased service quantity and verify totals', async ({ page, api, patient }) => {
396
+ const billingFormPage = new BillingFormPage(page);
397
+ const invoicePage = new InvoicePage(page);
398
+ const paymentPage = new PaymentPage(page);
399
+ const patientUuid = patient.uuid;
400
+ let billUuid: string;
401
+
402
+ // Note: Selecting the same service multiple times increments quantity (doesn't create multiple line items)
403
+ // This test verifies quantity increment and total calculation
404
+ const quantity = 3;
405
+ const expectedTotal = expectedServicePrice * quantity;
406
+
407
+ await test.step('When I navigate to the Billing history page', async () => {
408
+ await page.goto(`patient/${patientUuid}/chart/Billing history`);
409
+ });
410
+
411
+ await test.step('And I launch the Create Bill form', async () => {
412
+ const createBillButton = page.getByRole('button', { name: /launch bill form|add bill/i });
413
+ await createBillButton.click();
414
+ });
415
+
416
+ await test.step('And I add the same service multiple times to increment quantity', async () => {
417
+ await billingFormPage.searchAndSelectBillableService(testServiceName);
418
+ await billingFormPage.selectPaymentMethodIfVisible();
419
+ await expect(page.getByText(testServiceName, { exact: false })).toBeVisible();
420
+
421
+ const quantityInput = page.locator('input[type="number"]').first();
422
+ await expect(quantityInput).toHaveValue('1');
423
+
424
+ for (let i = 1; i < quantity; i++) {
425
+ await billingFormPage.clearBillableServiceCombobox();
426
+ await billingFormPage.searchAndSelectBillableService(testServiceName);
427
+ await expect
428
+ .poll(
429
+ async () => {
430
+ const value = await quantityInput.inputValue();
431
+ return value;
432
+ },
433
+ { timeout: 5000 },
434
+ )
435
+ .toBe((i + 1).toString());
436
+ }
437
+
438
+ await expect(quantityInput).toHaveValue(quantity.toString());
439
+ });
440
+
441
+ await test.step('Then the grand total should equal price times quantity', async () => {
442
+ await billingFormPage.verifyGrandTotal(expectedTotal);
443
+ });
444
+
445
+ await test.step('When I save the bill', async () => {
446
+ await billingFormPage.saveBill();
447
+ await waitForSuccessNotification(page, 'Bill processed successfully');
448
+ });
449
+
450
+ await test.step('Then the bill should be created with correct line item', async () => {
451
+ const billsResponse = await api.get(`billing/bill?patient=${patientUuid}&v=full`);
452
+ expect(billsResponse.ok()).toBeTruthy();
453
+ const billsData = await billsResponse.json();
454
+ expect(billsData.results.length).toBeGreaterThan(0);
455
+
456
+ const bill = billsData.results[0];
457
+ billUuid = bill.uuid;
458
+ billsToCleanup.add(billUuid);
459
+
460
+ expect(bill.lineItems.length).toBe(1);
461
+ const lineItem = bill.lineItems[0];
462
+ expect(lineItem.billableService).toBeTruthy();
463
+ expect(lineItem.quantity).toBe(quantity);
464
+ expect(lineItem.price).toBeCloseTo(expectedServicePrice, 2);
465
+
466
+ const backendTotal = bill.lineItems.reduce(
467
+ (sum: number, item: { price: number; quantity: number }) => sum + item.price * item.quantity,
468
+ 0,
469
+ );
470
+ expect(backendTotal).toBeCloseTo(expectedTotal, 2);
471
+ });
472
+
473
+ await test.step('When I navigate to the invoice page', async () => {
474
+ await invoicePage.goto(patientUuid, billUuid);
475
+ await invoicePage.waitForInvoiceToLoad();
476
+ });
477
+
478
+ await test.step('Then the invoice should display the line item correctly', async () => {
479
+ const lineItems = await invoicePage.getLineItems();
480
+ expect(lineItems.length).toBe(1);
481
+ expect(lineItems[0].quantity).toBe(quantity.toString());
482
+ });
483
+
484
+ await test.step('And the total amount should match the calculated total', async () => {
485
+ const totalAmount = await invoicePage.getTotalAmount();
486
+ const totalValue = extractNumericValue(totalAmount);
487
+ expect(totalValue).toBeCloseTo(expectedTotal, 2);
488
+
489
+ const billResponse = await api.get(`billing/bill/${billUuid}?v=full`);
490
+ const billData = await billResponse.json();
491
+ const backendTotal = billData.lineItems.reduce(
492
+ (sum: number, item: { price: number; quantity: number }) => sum + item.price * item.quantity,
493
+ 0,
494
+ );
495
+ expect(backendTotal).toBeCloseTo(totalValue, 2);
496
+ });
497
+
498
+ await test.step('When I process payment for the full amount', async () => {
499
+ await paymentPage.waitForPaymentForm();
500
+ const amountDue = await invoicePage.getAmountDue();
501
+ const amountDueValue = extractNumericValue(amountDue);
502
+
503
+ await paymentPage.addPayment('Cash', amountDueValue);
504
+ await paymentPage.processPayment();
505
+ await waitForSuccessNotification(page, 'Payment processed successfully');
506
+ });
507
+
508
+ await test.step('Then the line item should be marked as PAID', async () => {
509
+ await page.reload();
510
+ await invoicePage.waitForInvoiceToLoad();
511
+
512
+ const lineItems = await invoicePage.getLineItems();
513
+ lineItems.forEach((lineItem) => {
514
+ expect(lineItem.status).toBe('PAID');
515
+ });
516
+
517
+ // Verify backend state
518
+ const billResponse = await api.get(`billing/bill/${billUuid}?v=full`);
519
+ const billData = await billResponse.json();
520
+ billData.lineItems.forEach((lineItem: { paymentStatus: string }) => {
521
+ expect(lineItem.paymentStatus).toBe('PAID');
522
+ });
523
+ });
524
+ });
525
+
526
+ test('Process payment with multiple payment methods', async ({ page, api, patient }) => {
527
+ const billingFormPage = new BillingFormPage(page);
528
+ const invoicePage = new InvoicePage(page);
529
+ const paymentPage = new PaymentPage(page);
530
+ const patientUuid = patient.uuid;
531
+ let billUuid: string;
532
+
533
+ await test.step('Given I have created a bill', async () => {
534
+ await page.goto(`patient/${patientUuid}/chart/Billing history`);
535
+
536
+ const createBillButton = page.getByRole('button', { name: /launch bill form|add bill/i });
537
+ await createBillButton.click();
538
+
539
+ await billingFormPage.searchAndSelectBillableService(testServiceName);
540
+ await billingFormPage.selectPaymentMethodIfVisible();
541
+ await billingFormPage.saveBill();
542
+ await waitForSuccessNotification(page, 'Bill processed successfully');
543
+
544
+ const billsResponse = await api.get(`billing/bill?patient=${patientUuid}&v=full`);
545
+ const billsData = await billsResponse.json();
546
+ billUuid = billsData.results[0].uuid;
547
+ billsToCleanup.add(billUuid);
548
+ });
549
+
550
+ await test.step('When I navigate to the invoice page', async () => {
551
+ await invoicePage.goto(patientUuid, billUuid);
552
+ await invoicePage.waitForInvoiceToLoad();
553
+ });
554
+
555
+ await test.step('And I add multiple payment methods', async () => {
556
+ await paymentPage.waitForPaymentForm();
557
+
558
+ const amountDue = await invoicePage.getAmountDue();
559
+ const amountDueValue = extractNumericValue(amountDue);
560
+
561
+ // Split payment: 60% Cash, 40% Cash (using same method for simplicity)
562
+ // In real scenario, would use different payment methods like Mobile Money
563
+ const cashAmount1 = Math.round(amountDueValue * 0.6 * 100) / 100;
564
+ const cashAmount2 = amountDueValue - cashAmount1;
565
+
566
+ await paymentPage.addMultiplePayments([
567
+ { method: 'Cash', amount: cashAmount1 },
568
+ { method: 'Cash', amount: cashAmount2 },
569
+ ]);
570
+ });
571
+
572
+ await test.step('When I process the payment', async () => {
573
+ await paymentPage.processPayment();
574
+ await waitForSuccessNotification(page, 'Payment processed successfully');
575
+ });
576
+
577
+ await test.step('Then the bill should be marked as PAID', async () => {
578
+ await page.reload();
579
+ await invoicePage.waitForInvoiceToLoad();
580
+
581
+ const status = await invoicePage.getInvoiceStatus();
582
+ expect(status).toBe('PAID');
583
+
584
+ const billResponse = await api.get(`billing/bill/${billUuid}`);
585
+ const billData = await billResponse.json();
586
+ expect(billData.status).toBe('PAID');
587
+ });
588
+
589
+ await test.step('And the payment history should show multiple payments', async () => {
590
+ const paymentHistory = await paymentPage.getPaymentHistory();
591
+ expect(paymentHistory.length).toBeGreaterThanOrEqual(2);
592
+ });
593
+
594
+ await test.step('And the backend should store payment methods correctly', async () => {
595
+ const billResponse = await api.get(`billing/bill/${billUuid}?v=full`);
596
+ const billData = await billResponse.json();
597
+ const payments = billData.payments;
598
+
599
+ expect(payments.length).toBeGreaterThanOrEqual(2);
600
+
601
+ // Verify each payment has instanceType (payment method)
602
+ payments.forEach((payment: { instanceType: { name: string }; amountTendered: number }) => {
603
+ expect(payment.instanceType).toBeTruthy();
604
+ expect(payment.instanceType.name).toBeTruthy();
605
+ expect(payment.amountTendered).toBeGreaterThan(0);
606
+ });
607
+ });
608
+
609
+ await test.step('And the amount tendered should equal the total amount', async () => {
610
+ const totalAmount = await invoicePage.getTotalAmount();
611
+ const tenderedAmount = await invoicePage.getAmountTendered();
612
+
613
+ const totalValue = extractNumericValue(totalAmount);
614
+ const tenderedValue = extractNumericValue(tenderedAmount);
615
+
616
+ expect(tenderedValue).toBeCloseTo(totalValue, 2);
617
+
618
+ // Verify backend
619
+ const billResponse = await api.get(`billing/bill/${billUuid}`);
620
+ const billData = await billResponse.json();
621
+ const backendTendered = billData.payments.reduce(
622
+ (sum: number, p: { amountTendered: number }) => sum + p.amountTendered,
623
+ 0,
624
+ );
625
+ expect(backendTendered).toBeCloseTo(totalValue, 2);
626
+ });
627
+ });
628
+
629
+ test('Create bill with quantity increase and process split payment', async ({ page, api, patient }) => {
630
+ const billingFormPage = new BillingFormPage(page);
631
+ const invoicePage = new InvoicePage(page);
632
+ const paymentPage = new PaymentPage(page);
633
+ const patientUuid = patient.uuid;
634
+ let billUuid: string;
635
+
636
+ // Note: Selecting same service twice increments quantity to 2
637
+ const quantity = 2;
638
+ const expectedTotal = expectedServicePrice * quantity;
639
+
640
+ await test.step('Given I create a bill with a service quantity of 2', async () => {
641
+ await page.goto(`patient/${patientUuid}/chart/Billing history`);
642
+
643
+ const createBillButton = page.getByRole('button', { name: /launch bill form|add bill/i });
644
+ await createBillButton.click();
645
+
646
+ await billingFormPage.searchAndSelectBillableService(testServiceName);
647
+ await billingFormPage.selectPaymentMethodIfVisible();
648
+ await expect(page.getByText(testServiceName, { exact: false })).toBeVisible();
649
+
650
+ await billingFormPage.clearBillableServiceCombobox();
651
+ await billingFormPage.searchAndSelectBillableService(testServiceName);
652
+
653
+ const quantityInput = page.locator('input[type="number"]').first();
654
+ await expect
655
+ .poll(
656
+ async () => {
657
+ const value = await quantityInput.inputValue();
658
+ return value;
659
+ },
660
+ { timeout: 5000 },
661
+ )
662
+ .toBe('2');
663
+
664
+ await billingFormPage.verifyGrandTotal(expectedTotal);
665
+ await billingFormPage.saveBill();
666
+ await waitForSuccessNotification(page, 'Bill processed successfully');
667
+
668
+ const billsResponse = await api.get(`billing/bill?patient=${patientUuid}&v=full`);
669
+ const billsData = await billsResponse.json();
670
+ billUuid = billsData.results[0].uuid;
671
+ billsToCleanup.add(billUuid);
672
+
673
+ const bill = billsData.results[0];
674
+ expect(bill.lineItems.length).toBe(1);
675
+ expect(bill.lineItems[0].quantity).toBe(quantity);
676
+ });
677
+
678
+ await test.step('When I navigate to the invoice page', async () => {
679
+ await invoicePage.goto(patientUuid, billUuid);
680
+ await invoicePage.waitForInvoiceToLoad();
681
+ });
682
+
683
+ await test.step('And I process payment with multiple payment methods', async () => {
684
+ await paymentPage.waitForPaymentForm();
685
+
686
+ const amountDue = await invoicePage.getAmountDue();
687
+ const amountDueValue = extractNumericValue(amountDue);
688
+
689
+ // Split payment: 50% + 50%
690
+ const payment1 = Math.round(amountDueValue * 0.5 * 100) / 100;
691
+ const payment2 = amountDueValue - payment1;
692
+
693
+ await paymentPage.addMultiplePayments([
694
+ { method: 'Cash', amount: payment1 },
695
+ { method: 'Cash', amount: payment2 },
696
+ ]);
697
+
698
+ await paymentPage.processPayment();
699
+ await waitForSuccessNotification(page, 'Payment processed successfully');
700
+ });
701
+
702
+ await test.step('Then the bill should be marked as PAID', async () => {
703
+ await page.reload();
704
+ await invoicePage.waitForInvoiceToLoad();
705
+
706
+ const status = await invoicePage.getInvoiceStatus();
707
+ expect(status).toBe('PAID');
708
+
709
+ const billResponse = await api.get(`billing/bill/${billUuid}`);
710
+ const billData = await billResponse.json();
711
+ expect(billData.status).toBe('PAID');
712
+ });
713
+
714
+ await test.step('And the line item should be marked as PAID', async () => {
715
+ const lineItems = await invoicePage.getLineItems();
716
+ lineItems.forEach((lineItem) => {
717
+ expect(lineItem.status).toBe('PAID');
718
+ });
719
+ });
720
+
721
+ await test.step('And the payment history should show multiple payments', async () => {
722
+ const paymentHistory = await paymentPage.getPaymentHistory();
723
+ expect(paymentHistory.length).toBeGreaterThanOrEqual(2);
724
+
725
+ // Verify backend payments
726
+ const billResponse = await api.get(`billing/bill/${billUuid}?v=full`);
727
+ const billData = await billResponse.json();
728
+ const payments = billData.payments;
729
+
730
+ expect(payments.length).toBeGreaterThanOrEqual(2);
731
+ payments.forEach((payment: { instanceType: { name: string }; amountTendered: number }) => {
732
+ expect(payment.instanceType).toBeTruthy();
733
+ expect(payment.instanceType.name).toBeTruthy();
734
+ expect(payment.amountTendered).toBeGreaterThan(0);
735
+ });
736
+ });
737
+ });
738
+ });