@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,503 @@
1
+ import { mapBillProperties } from './billing.resource';
2
+ import type { LineItem, PatientInvoice, Payment } from './types';
3
+
4
+ type CashPoint = PatientInvoice['cashPoint'];
5
+ type Provider = PatientInvoice['cashier'];
6
+ type Patient = PatientInvoice['patient'];
7
+
8
+ const mockFormatDate = jest.fn((date: Date, options?: Partial<{ mode: string }>) => `formatted-${date}`);
9
+ const mockParseDate = jest.fn((dateString: string): Date => new Date(dateString));
10
+
11
+ jest.mock('@openmrs/esm-framework', () => ({
12
+ formatDate: (date: Date, options?: Partial<{ mode: string }>) => mockFormatDate(date, options),
13
+ parseDate: (dateString: string) => mockParseDate(dateString),
14
+ }));
15
+
16
+ const createLineItem = (overrides: Partial<LineItem> = {}): LineItem => ({
17
+ uuid: 'line-item-uuid',
18
+ item: 'Service Item',
19
+ billableService: 'Billable Service',
20
+ paymentStatus: 'PENDING',
21
+ quantity: 1,
22
+ price: 100,
23
+ priceName: 'Standard',
24
+ priceUuid: 'price-uuid',
25
+ lineItemOrder: 1,
26
+ resourceVersion: '1.0',
27
+ display: 'Line Item Display',
28
+ voided: false,
29
+ voidReason: null,
30
+ ...overrides,
31
+ });
32
+
33
+ const createCashPoint = (overrides: Partial<CashPoint> = {}): CashPoint => ({
34
+ uuid: 'cp1',
35
+ name: 'Cash Point 1',
36
+ description: 'Main cash point',
37
+ retired: false,
38
+ location: {
39
+ uuid: 'loc1',
40
+ display: 'Location 1',
41
+ links: [],
42
+ },
43
+ ...overrides,
44
+ });
45
+
46
+ const createProvider = (overrides: Partial<Provider> = {}): Provider => ({
47
+ uuid: 'provider-uuid',
48
+ display: 'Cashier',
49
+ links: [],
50
+ ...overrides,
51
+ });
52
+
53
+ const createPatient = (overrides: Partial<Patient> = {}): Patient => ({
54
+ uuid: 'patient-uuid',
55
+ display: '12345 - John Doe',
56
+ links: [],
57
+ ...overrides,
58
+ });
59
+
60
+ const createPayment = (overrides: Partial<Payment> = {}): Payment => ({
61
+ uuid: 'payment-uuid',
62
+ instanceType: {
63
+ uuid: 'instance-uuid',
64
+ name: 'Payment',
65
+ description: 'Payment instance',
66
+ retired: false,
67
+ },
68
+ attributes: [],
69
+ amount: 250,
70
+ amountTendered: 250,
71
+ dateCreated: Date.now(),
72
+ voided: false,
73
+ resourceVersion: '1.0',
74
+ ...overrides,
75
+ });
76
+
77
+ const createBaseBill = (overrides: Partial<PatientInvoice> = {}): PatientInvoice => ({
78
+ uuid: 'uuid1',
79
+ display: '12345 - John Doe',
80
+ voided: false,
81
+ voidReason: null,
82
+ adjustedBy: [],
83
+ billAdjusted: null,
84
+ cashPoint: createCashPoint(),
85
+ cashier: createProvider(),
86
+ dateCreated: '2024-01-01T00:00:00Z',
87
+ lineItems: [],
88
+ patient: createPatient(),
89
+ payments: [],
90
+ receiptNumber: 'R123',
91
+ status: 'PENDING',
92
+ adjustmentReason: null,
93
+ id: 1,
94
+ resourceVersion: '1.0',
95
+ ...overrides,
96
+ });
97
+
98
+ describe('mapBillProperties', () => {
99
+ describe('Amount calculations', () => {
100
+ it('Calculates totalAmount correctly for multiple items', () => {
101
+ const bill = createBaseBill({
102
+ lineItems: [createLineItem({ price: 100, quantity: 2 }), createLineItem({ price: 50, quantity: 1 })],
103
+ });
104
+ const result = mapBillProperties(bill);
105
+ expect(result.totalAmount).toBe(250);
106
+ });
107
+
108
+ it('Calculates tenderedAmount from single payment', () => {
109
+ const bill = createBaseBill({
110
+ payments: [createPayment({ amountTendered: 250 })],
111
+ });
112
+ const result = mapBillProperties(bill);
113
+ expect(result.tenderedAmount).toBe(250);
114
+ });
115
+
116
+ it('Calculates tenderedAmount from multiple payments', () => {
117
+ const bill = createBaseBill({
118
+ payments: [
119
+ createPayment({ amountTendered: 100 }),
120
+ createPayment({ amountTendered: 150 }),
121
+ createPayment({ amountTendered: 50 }),
122
+ ],
123
+ });
124
+ const result = mapBillProperties(bill);
125
+ expect(result.tenderedAmount).toBe(300);
126
+ });
127
+
128
+ it('Handles null/undefined prices and quantities gracefully', () => {
129
+ const bill = createBaseBill({
130
+ lineItems: [
131
+ createLineItem({ price: null as unknown as number, quantity: 2 }),
132
+ createLineItem({ price: 100, quantity: undefined as unknown as number }),
133
+ ],
134
+ });
135
+ const result = mapBillProperties(bill);
136
+ expect(result.totalAmount).toBe(0);
137
+ });
138
+
139
+ it('Handles null/undefined amountTendered in payments', () => {
140
+ const bill = createBaseBill({
141
+ payments: [
142
+ createPayment({ amountTendered: 100 }),
143
+ createPayment({ amountTendered: null as unknown as number }),
144
+ createPayment({ amountTendered: undefined as unknown as number }),
145
+ ],
146
+ });
147
+ const result = mapBillProperties(bill);
148
+ expect(result.tenderedAmount).toBe(100);
149
+ });
150
+
151
+ it('Handles zero amounts correctly', () => {
152
+ const bill = createBaseBill({
153
+ lineItems: [createLineItem({ price: 0, quantity: 5 }), createLineItem({ price: 100, quantity: 0 })],
154
+ payments: [createPayment({ amountTendered: 0 })],
155
+ });
156
+ const result = mapBillProperties(bill);
157
+ expect(result.totalAmount).toBe(0);
158
+ expect(result.tenderedAmount).toBe(0);
159
+ });
160
+
161
+ it('Handles negative amounts (refunds)', () => {
162
+ const bill = createBaseBill({
163
+ lineItems: [createLineItem({ price: -50, quantity: 1 })],
164
+ payments: [createPayment({ amountTendered: -50 })],
165
+ });
166
+ const result = mapBillProperties(bill);
167
+ expect(result.totalAmount).toBe(-50);
168
+ expect(result.tenderedAmount).toBe(-50);
169
+ });
170
+
171
+ it('Handles decimal precision correctly', () => {
172
+ const bill = createBaseBill({
173
+ lineItems: [createLineItem({ price: 0.1, quantity: 1 }), createLineItem({ price: 0.2, quantity: 1 })],
174
+ });
175
+ const result = mapBillProperties(bill);
176
+ // 0.1 + 0.2 = 0.30000000000000004 in JavaScript
177
+ expect(result.totalAmount).toBeCloseTo(0.3, 2);
178
+ });
179
+
180
+ it('Handles very large amounts', () => {
181
+ const bill = createBaseBill({
182
+ lineItems: [createLineItem({ price: Number.MAX_SAFE_INTEGER, quantity: 1 })],
183
+ payments: [createPayment({ amountTendered: Number.MAX_SAFE_INTEGER })],
184
+ });
185
+ const result = mapBillProperties(bill);
186
+ expect(result.totalAmount).toBe(Number.MAX_SAFE_INTEGER);
187
+ expect(result.tenderedAmount).toBe(Number.MAX_SAFE_INTEGER);
188
+ });
189
+ });
190
+
191
+ describe('Patient info parsing', () => {
192
+ it('Parses patient name and identifier from display', () => {
193
+ const bill = createBaseBill({
194
+ patient: createPatient({ display: '12345 - John Doe' }),
195
+ });
196
+ const result = mapBillProperties(bill);
197
+ expect(result.identifier).toBe('12345 ');
198
+ expect(result.patientName).toBe(' John Doe');
199
+ expect(result.patientUuid).toBe('patient-uuid');
200
+ });
201
+
202
+ it('Handles patient display without dash separator', () => {
203
+ const bill = createBaseBill({
204
+ patient: createPatient({ display: 'John Doe' }),
205
+ });
206
+ const result = mapBillProperties(bill);
207
+ expect(result.identifier).toBe('John Doe');
208
+ expect(result.patientName).toBeUndefined();
209
+ });
210
+
211
+ it('Handles patient display with multiple dashes', () => {
212
+ const bill = createBaseBill({
213
+ patient: createPatient({ display: '12345 - John - Doe - Jr' }),
214
+ });
215
+ const result = mapBillProperties(bill);
216
+ expect(result.identifier).toBe('12345 ');
217
+ // Note: split('-')[1] only takes the second element, not everything after first dash
218
+ expect(result.patientName).toBe(' John ');
219
+ });
220
+
221
+ it('Handles empty patient display string', () => {
222
+ const bill = createBaseBill({
223
+ patient: createPatient({ display: '' }),
224
+ });
225
+ const result = mapBillProperties(bill);
226
+ expect(result.identifier).toBe('');
227
+ expect(result.patientName).toBeUndefined();
228
+ });
229
+
230
+ it('Handles undefined patient gracefully', () => {
231
+ const bill = createBaseBill({
232
+ patient: undefined as unknown as Patient,
233
+ });
234
+ const result = mapBillProperties(bill);
235
+ expect(result.patientUuid).toBeUndefined();
236
+ expect(result.identifier).toBeUndefined();
237
+ expect(result.patientName).toBeUndefined();
238
+ });
239
+
240
+ it('Handles patient with undefined display', () => {
241
+ const bill = createBaseBill({
242
+ patient: {
243
+ ...createPatient(),
244
+ display: undefined as unknown as string,
245
+ },
246
+ });
247
+ const result = mapBillProperties(bill);
248
+ expect(result.identifier).toBeUndefined();
249
+ expect(result.patientName).toBeUndefined();
250
+ });
251
+ });
252
+
253
+ describe('Date formatting', () => {
254
+ it('Formats dateCreated when present', () => {
255
+ mockFormatDate.mockReturnValue('01 January 2024');
256
+ const bill = createBaseBill({
257
+ dateCreated: '2024-01-01T00:00:00Z',
258
+ });
259
+ const result = mapBillProperties(bill);
260
+ expect(result.dateCreated).toBe('01 January 2024');
261
+ });
262
+
263
+ it('Returns "--" when dateCreated is null', () => {
264
+ const bill = createBaseBill({
265
+ dateCreated: null as unknown as string,
266
+ });
267
+ const result = mapBillProperties(bill);
268
+ expect(result.dateCreated).toBe('--');
269
+ });
270
+
271
+ it('Returns "--" when dateCreated is undefined', () => {
272
+ const bill = createBaseBill({
273
+ dateCreated: undefined as unknown as string,
274
+ });
275
+ const result = mapBillProperties(bill);
276
+ expect(result.dateCreated).toBe('--');
277
+ });
278
+
279
+ it('Returns "--" when dateCreated is empty string', () => {
280
+ const bill = createBaseBill({
281
+ dateCreated: '',
282
+ });
283
+ const result = mapBillProperties(bill);
284
+ expect(result.dateCreated).toBe('--');
285
+ });
286
+ });
287
+
288
+ describe('Billing service concatenation', () => {
289
+ it('Concatenates multiple line item names with double space', () => {
290
+ const bill = createBaseBill({
291
+ lineItems: [
292
+ createLineItem({ item: 'Service A' }),
293
+ createLineItem({ item: 'Service B' }),
294
+ createLineItem({ item: 'Service C' }),
295
+ ],
296
+ });
297
+ const result = mapBillProperties(bill);
298
+ expect(result.billingService).toBe('Service A Service B Service C');
299
+ });
300
+
301
+ it('Falls back to billableService when item is missing', () => {
302
+ const bill = createBaseBill({
303
+ lineItems: [createLineItem({ item: undefined, billableService: 'Billable Service' })],
304
+ });
305
+ const result = mapBillProperties(bill);
306
+ expect(result.billingService).toBe('Billable Service');
307
+ });
308
+
309
+ it('Falls back to "--" when both item and billableService are missing', () => {
310
+ const bill = createBaseBill({
311
+ lineItems: [createLineItem({ item: undefined, billableService: undefined })],
312
+ });
313
+ const result = mapBillProperties(bill);
314
+ expect(result.billingService).toBe('--');
315
+ });
316
+
317
+ it('Handles mixed item types correctly', () => {
318
+ const bill = createBaseBill({
319
+ lineItems: [
320
+ createLineItem({ item: 'Item A', billableService: 'Service A' }),
321
+ createLineItem({ item: undefined, billableService: 'Service B' }),
322
+ createLineItem({ item: undefined, billableService: undefined }),
323
+ ],
324
+ });
325
+ const result = mapBillProperties(bill);
326
+ expect(result.billingService).toBe('Item A Service B --');
327
+ });
328
+ });
329
+
330
+ describe('Field mapping', () => {
331
+ it('Maps all bill fields correctly', () => {
332
+ const bill = createBaseBill({
333
+ id: 123,
334
+ uuid: 'bill-uuid-123',
335
+ receiptNumber: 'RCP-456',
336
+ cashPoint: createCashPoint({
337
+ uuid: 'cashpoint-uuid',
338
+ name: 'Main Cash Point',
339
+ location: {
340
+ uuid: 'loc-uuid',
341
+ display: 'Main Hospital',
342
+ links: [],
343
+ },
344
+ }),
345
+ cashier: createProvider({ uuid: 'cashier-uuid', display: 'Jane Cashier' }),
346
+ display: 'Bill Display',
347
+ });
348
+ const result = mapBillProperties(bill);
349
+
350
+ expect(result.id).toBe(123);
351
+ expect(result.uuid).toBe('bill-uuid-123');
352
+ expect(result.receiptNumber).toBe('RCP-456');
353
+ expect(result.cashPointUuid).toBe('cashpoint-uuid');
354
+ expect(result.cashPointName).toBe('Main Cash Point');
355
+ expect(result.cashPointLocation).toBe('Main Hospital');
356
+ expect(result.cashier).toEqual({ uuid: 'cashier-uuid', display: 'Jane Cashier', links: [] });
357
+ expect(result.display).toBe('Bill Display');
358
+ });
359
+
360
+ it('Handles missing cashPoint gracefully', () => {
361
+ const bill = createBaseBill({
362
+ cashPoint: undefined as unknown as CashPoint,
363
+ });
364
+ const result = mapBillProperties(bill);
365
+ expect(result.cashPointUuid).toBeUndefined();
366
+ expect(result.cashPointName).toBeUndefined();
367
+ expect(result.cashPointLocation).toBeUndefined();
368
+ });
369
+
370
+ it('Handles missing cashier gracefully', () => {
371
+ const bill = createBaseBill({
372
+ cashier: undefined as unknown as Provider,
373
+ });
374
+ const result = mapBillProperties(bill);
375
+ expect(result.cashier).toBeUndefined();
376
+ });
377
+ });
378
+
379
+ describe('Real-world scenarios', () => {
380
+ it('Handles partial payment correctly', () => {
381
+ const bill = createBaseBill({
382
+ lineItems: [
383
+ createLineItem({ price: 100, quantity: 1, paymentStatus: 'PAID' }),
384
+ createLineItem({ price: 50, quantity: 1, paymentStatus: 'PENDING' }),
385
+ ],
386
+ payments: [createPayment({ amountTendered: 100 })],
387
+ });
388
+ const result = mapBillProperties(bill);
389
+ expect(result.status).toBe('PENDING');
390
+ expect(result.totalAmount).toBe(150);
391
+ expect(result.tenderedAmount).toBe(100);
392
+ });
393
+
394
+ it('Handles overpayment (change due scenario)', () => {
395
+ const bill = createBaseBill({
396
+ status: 'PAID',
397
+ lineItems: [createLineItem({ price: 100, quantity: 1, paymentStatus: 'PAID' })],
398
+ payments: [createPayment({ amount: 100, amountTendered: 150 })],
399
+ });
400
+ const result = mapBillProperties(bill);
401
+ expect(result.status).toBe('PAID');
402
+ expect(result.totalAmount).toBe(100);
403
+ expect(result.tenderedAmount).toBe(150);
404
+ });
405
+
406
+ it('Handles complex bill with mixed item states', () => {
407
+ const bill = createBaseBill({
408
+ lineItems: [
409
+ createLineItem({ paymentStatus: 'PAID', voided: false, price: 100, quantity: 1, item: 'Consultation' }),
410
+ createLineItem({ paymentStatus: 'PENDING', voided: false, price: 50, quantity: 2, item: 'Lab Test' }),
411
+ createLineItem({ paymentStatus: 'PAID', voided: true, price: 200, quantity: 1, item: 'Canceled X-Ray' }),
412
+ createLineItem({
413
+ paymentStatus: 'PAID',
414
+ voided: false,
415
+ price: 25,
416
+ quantity: 3,
417
+ item: undefined,
418
+ billableService: 'Medication',
419
+ }),
420
+ ],
421
+ payments: [createPayment({ amountTendered: 100 }), createPayment({ amountTendered: 75 })],
422
+ });
423
+ const result = mapBillProperties(bill);
424
+
425
+ expect(result.status).toBe('PENDING'); // One item still pending
426
+ expect(result.totalAmount).toBe(275); // 100 + 100 + 75 (voided item excluded)
427
+ expect(result.tenderedAmount).toBe(175); // 100 + 75
428
+ expect(result.lineItems).toHaveLength(3); // Only non-voided items
429
+ expect(result.billingService).toBe('Consultation Lab Test Medication'); // Proper concatenation with fallback
430
+ });
431
+
432
+ it('Handles bill with all items voided', () => {
433
+ const bill = createBaseBill({
434
+ status: 'PAID',
435
+ lineItems: [
436
+ createLineItem({ paymentStatus: 'PENDING', voided: true }),
437
+ createLineItem({ paymentStatus: 'PENDING', voided: true }),
438
+ ],
439
+ });
440
+ const result = mapBillProperties(bill);
441
+ expect(result.status).toBe('PAID'); // Falls back to bill.status
442
+ expect(result.lineItems).toHaveLength(0);
443
+ expect(result.totalAmount).toBe(0);
444
+ expect(result.billingService).toBe(''); // No active items
445
+ });
446
+
447
+ it('Handles bill with no payments yet', () => {
448
+ const bill = createBaseBill({
449
+ lineItems: [createLineItem({ price: 100, quantity: 1, paymentStatus: 'PENDING' })],
450
+ payments: [],
451
+ });
452
+ const result = mapBillProperties(bill);
453
+ expect(result.status).toBe('PENDING');
454
+ expect(result.totalAmount).toBe(100);
455
+ expect(result.tenderedAmount).toBe(0);
456
+ });
457
+
458
+ it('Handles bill with adjusted items (quantity changes)', () => {
459
+ const bill = createBaseBill({
460
+ status: 'PAID',
461
+ lineItems: [
462
+ createLineItem({ price: 50, quantity: 3, paymentStatus: 'PAID' }), // Originally 1, adjusted to 3
463
+ createLineItem({ price: 100, quantity: 0, paymentStatus: 'PAID' }), // Adjusted to 0
464
+ ],
465
+ payments: [createPayment({ amountTendered: 150 })],
466
+ });
467
+ const result = mapBillProperties(bill);
468
+ expect(result.status).toBe('PAID');
469
+ expect(result.totalAmount).toBe(150); // 50*3 + 100*0
470
+ });
471
+ });
472
+
473
+ describe('Edge cases for billing service', () => {
474
+ it('Handles empty string item and billableService', () => {
475
+ const bill = createBaseBill({
476
+ lineItems: [createLineItem({ item: '', billableService: '' })],
477
+ });
478
+ const result = mapBillProperties(bill);
479
+ expect(result.billingService).toBe('--');
480
+ });
481
+
482
+ it('Handles special characters in service names', () => {
483
+ const bill = createBaseBill({
484
+ lineItems: [
485
+ createLineItem({ item: 'Test & Diagnosis (urgent)' }),
486
+ createLineItem({ item: 'X-Ray: Chest/Abdomen' }),
487
+ ],
488
+ });
489
+ const result = mapBillProperties(bill);
490
+ expect(result.billingService).toBe('Test & Diagnosis (urgent) X-Ray: Chest/Abdomen');
491
+ });
492
+
493
+ it('Handles very long service names', () => {
494
+ const longServiceName = 'A'.repeat(500);
495
+ const bill = createBaseBill({
496
+ lineItems: [createLineItem({ item: longServiceName })],
497
+ });
498
+ const result = mapBillProperties(bill);
499
+ expect(result.billingService).toBe(longServiceName);
500
+ expect(result.billingService.length).toBe(500);
501
+ });
502
+ });
503
+ });
@@ -1,48 +1,76 @@
1
- import { useContext } from 'react';
2
- import dayjs from 'dayjs';
3
- import isEmpty from 'lodash-es/isEmpty';
4
- import sortBy from 'lodash-es/sortBy';
5
1
  import useSWR from 'swr';
6
- import { formatDate, parseDate, openmrsFetch, useSession, useVisit, restBaseUrl } from '@openmrs/esm-framework';
7
- import { apiBasePath, omrsDateFormat } from './constants';
8
- import type { FacilityDetail, MappedBill, PatientInvoice } from './types';
9
- import SelectedDateContext from './hooks/selectedDateContext';
10
-
11
- export const useBills = (patientUuid: string = '', billStatus: string = '') => {
12
- const { selectedDate } = useContext(SelectedDateContext);
13
- const endDate = dayjs().endOf('day').format(omrsDateFormat);
14
- const url = `${apiBasePath}bill?q=&v=full`;
15
-
16
- const patientUrl = `${apiBasePath}bill?patientUuid=${patientUuid}&v=full`;
17
-
18
- const { data, error, isLoading, isValidating, mutate } = useSWR<{ data: { results: Array<PatientInvoice> } }>(
19
- isEmpty(patientUuid) ? url : patientUrl,
20
- openmrsFetch,
21
- );
2
+ import sortBy from 'lodash-es/sortBy';
3
+ import {
4
+ formatDate,
5
+ parseDate,
6
+ openmrsFetch,
7
+ useSession,
8
+ useVisit,
9
+ type SessionLocation,
10
+ useOpenmrsFetchAll,
11
+ } from '@openmrs/esm-framework';
12
+ import { apiBasePath } from './constants';
13
+ import type {
14
+ MappedBill,
15
+ PatientInvoice,
16
+ BillableItem,
17
+ BillPaymentPayload,
18
+ CreateBillPayload,
19
+ UpdateBillPayload,
20
+ } from './types';
21
+
22
+ export const mapBillProperties = (bill: PatientInvoice): MappedBill => {
23
+ const activeLineItems = bill?.lineItems?.filter((item) => !item.voided) || [];
22
24
 
23
- const mapBillProperties = (bill: PatientInvoice): MappedBill => ({
25
+ return {
24
26
  id: bill?.id,
25
27
  uuid: bill?.uuid,
26
- patientName: bill?.patient?.display.split('-')?.[1],
27
- identifier: bill?.patient?.display.split('-')?.[0],
28
+ patientName: bill?.patient?.display?.split('-')?.[1],
29
+ identifier: bill?.patient?.display?.split('-')?.[0],
28
30
  patientUuid: bill?.patient?.uuid,
29
- status: bill.lineItems.some((item) => item.paymentStatus === 'PENDING') ? 'PENDING' : 'PAID',
31
+ status: bill?.status,
30
32
  receiptNumber: bill?.receiptNumber,
31
33
  cashier: bill?.cashier,
32
34
  cashPointUuid: bill?.cashPoint?.uuid,
33
35
  cashPointName: bill?.cashPoint?.name,
34
36
  cashPointLocation: bill?.cashPoint?.location?.display,
35
37
  dateCreated: bill?.dateCreated ? formatDate(parseDate(bill.dateCreated), { mode: 'wide' }) : '--',
36
- lineItems: bill?.lineItems,
37
- billingService: bill?.lineItems.map((bill) => bill?.item || bill?.billableService || '--').join(' '),
38
- payments: bill?.payments,
38
+ lineItems: activeLineItems,
39
+ billingService: activeLineItems.map((lineItem) => lineItem?.item || lineItem?.billableService || '--').join(' '),
40
+ payments: bill.payments,
39
41
  display: bill?.display,
40
- totalAmount: bill?.lineItems?.map((item) => item.price * item.quantity).reduce((prev, curr) => prev + curr, 0),
41
- });
42
+ totalAmount: activeLineItems
43
+ .map((item) => (item.price ?? 0) * (item.quantity ?? 0))
44
+ .reduce((prev, curr) => prev + curr, 0),
45
+ tenderedAmount: (bill?.payments ?? [])
46
+ .map((item) => item.amountTendered ?? 0)
47
+ .reduce((prev, curr) => prev + curr, 0),
48
+ };
49
+ };
50
+
51
+ export const useBills = (patientUuid: string = '', billStatus: string = '') => {
52
+ // Build URL with status parameter if provided
53
+ const buildUrl = () => {
54
+ let url = `${apiBasePath}bill?v=full`;
55
+
56
+ if (patientUuid) {
57
+ url += `&patientUuid=${patientUuid}`;
58
+ }
59
+
60
+ if (billStatus) {
61
+ url += `&status=${billStatus}`;
62
+ }
63
+
64
+ return url;
65
+ };
66
+
67
+ const { data, error, isLoading, isValidating, mutate } = useSWR<{ data: { results: Array<PatientInvoice> } }>(
68
+ buildUrl(),
69
+ openmrsFetch,
70
+ );
42
71
 
43
72
  const sortedBills = sortBy(data?.data?.results ?? [], ['dateCreated']).reverse();
44
- const filteredBills = billStatus === '' ? sortedBills : sortedBills?.filter((bill) => bill?.status === billStatus);
45
- const mappedResults = filteredBills?.map((bill) => mapBillProperties(bill));
73
+ const mappedResults = sortedBills?.map((bill) => mapBillProperties(bill));
46
74
 
47
75
  return {
48
76
  bills: mappedResults,
@@ -60,31 +88,6 @@ export const useBill = (billUuid: string) => {
60
88
  openmrsFetch,
61
89
  );
62
90
 
63
- const mapBillProperties = (bill: PatientInvoice): MappedBill => ({
64
- id: bill?.id,
65
- uuid: bill?.uuid,
66
- patientName: bill?.patient?.display.split('-')?.[1],
67
- identifier: bill?.patient?.display.split('-')?.[0],
68
- patientUuid: bill?.patient?.uuid,
69
- status:
70
- bill.lineItems.length > 1
71
- ? bill.lineItems.some((item) => item.paymentStatus === 'PENDING')
72
- ? 'PENDING'
73
- : 'PAID'
74
- : bill.status,
75
- receiptNumber: bill?.receiptNumber,
76
- cashier: bill?.cashier,
77
- cashPointUuid: bill?.cashPoint?.uuid,
78
- cashPointName: bill?.cashPoint?.name,
79
- cashPointLocation: bill?.cashPoint?.location?.display,
80
- dateCreated: bill?.dateCreated ? formatDate(parseDate(bill.dateCreated), { mode: 'wide' }) : '--',
81
- lineItems: bill?.lineItems,
82
- billingService: bill?.lineItems.map((bill) => bill.item).join(' '),
83
- payments: bill.payments,
84
- totalAmount: bill?.lineItems?.map((item) => item.price * item.quantity).reduce((prev, curr) => prev + curr, 0),
85
- tenderedAmount: bill?.payments?.map((item) => item.amountTendered).reduce((prev, curr) => prev + curr, 0),
86
- });
87
-
88
91
  const formattedBill = data?.data ? mapBillProperties(data?.data) : null;
89
92
 
90
93
  return {
@@ -96,7 +99,7 @@ export const useBill = (billUuid: string) => {
96
99
  };
97
100
  };
98
101
 
99
- export const processBillPayment = (payload, billUuid: string) => {
102
+ export const processBillPayment = (payload: BillPaymentPayload, billUuid: string) => {
100
103
  const url = `${apiBasePath}bill/${billUuid}`;
101
104
 
102
105
  return openmrsFetch(url, {
@@ -108,13 +111,9 @@ export const processBillPayment = (payload, billUuid: string) => {
108
111
  });
109
112
  };
110
113
 
111
- export function useDefaultFacility() {
112
- const url = `${restBaseUrl}/kenyaemr/default-facility`;
113
- const { authenticated } = useSession();
114
-
115
- const { data, isLoading } = useSWR<{ data: FacilityDetail }>(authenticated ? url : null, openmrsFetch);
116
-
117
- return { data: data?.data, isLoading: isLoading };
114
+ export function useDefaultFacility(): { data: SessionLocation | null } {
115
+ const { sessionLocation } = useSession();
116
+ return { data: sessionLocation };
118
117
  }
119
118
 
120
119
  export const usePatientPaymentInfo = (patientUuid: string) => {
@@ -130,19 +129,12 @@ export const usePatientPaymentInfo = (patientUuid: string) => {
130
129
  return paymentInformation;
131
130
  };
132
131
 
133
- export function useFetchSearchResults(searchVal, category) {
134
- let url = ``;
135
- if (category == 'Stock Item') {
136
- url = `${restBaseUrl}/stockmanagement/stockitem?v=default&limit=10&q=${searchVal}`;
137
- } else {
138
- url = `${apiBasePath}billableService?v=custom:(uuid,name,shortName,serviceStatus,serviceType:(display),servicePrices:(uuid,name,price,paymentMode))`;
139
- }
140
- const { data, error, isLoading, isValidating } = useSWR(searchVal ? url : null, openmrsFetch, {});
141
-
142
- return { data: data?.data, error, isLoading: isLoading, isValidating };
132
+ export function useBillableServices() {
133
+ const url = `${apiBasePath}billableService?v=custom:(uuid,name,shortName,serviceStatus,serviceType:(display),servicePrices:(uuid,name,price,paymentMode))`;
134
+ return useOpenmrsFetchAll<BillableItem>(url);
143
135
  }
144
136
 
145
- export const processBillItems = (payload) => {
137
+ export const processBillItems = (payload: CreateBillPayload) => {
146
138
  const url = `${apiBasePath}bill`;
147
139
  return openmrsFetch(url, {
148
140
  method: 'POST',
@@ -153,7 +145,7 @@ export const processBillItems = (payload) => {
153
145
  });
154
146
  };
155
147
 
156
- export const updateBillItems = (payload) => {
148
+ export const updateBillItems = (payload: UpdateBillPayload) => {
157
149
  const url = `${apiBasePath}bill/${payload.uuid}`;
158
150
  return openmrsFetch(url, {
159
151
  method: 'POST',