@openmrs/esm-billing-app 1.0.2-pre.78 → 1.0.2-pre.786

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 (203) 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/1856.js +1 -0
  10. package/dist/1856.js.map +1 -0
  11. package/dist/2146.js +1 -1
  12. package/dist/2177.js +2 -0
  13. package/dist/2177.js.LICENSE.txt +9 -0
  14. package/dist/2177.js.map +1 -0
  15. package/dist/2524.js +1 -0
  16. package/dist/2524.js.map +1 -0
  17. package/dist/2690.js +1 -1
  18. package/dist/3041.js +1 -0
  19. package/dist/3041.js.map +1 -0
  20. package/dist/3099.js +1 -1
  21. package/dist/3584.js +1 -1
  22. package/dist/4055.js +1 -1
  23. package/dist/4132.js +1 -1
  24. package/dist/4225.js +1 -0
  25. package/dist/4225.js.map +1 -0
  26. package/dist/4300.js +1 -1
  27. package/dist/4335.js +1 -1
  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/5422.js +1 -0
  38. package/dist/5422.js.map +1 -0
  39. package/dist/5442.js +1 -1
  40. package/dist/5661.js +1 -1
  41. package/dist/6022.js +1 -1
  42. package/dist/6468.js +1 -1
  43. package/dist/6540.js +1 -1
  44. package/dist/6540.js.map +1 -1
  45. package/dist/6606.js +1 -0
  46. package/dist/6606.js.map +1 -0
  47. package/dist/6679.js +1 -1
  48. package/dist/6840.js +1 -1
  49. package/dist/6859.js +1 -1
  50. package/dist/7097.js +1 -1
  51. package/dist/7159.js +1 -1
  52. package/dist/723.js +1 -1
  53. package/dist/7452.js +2 -0
  54. package/dist/7452.js.map +1 -0
  55. package/dist/7617.js +1 -1
  56. package/dist/795.js +1 -1
  57. package/dist/8163.js +1 -1
  58. package/dist/8349.js +1 -1
  59. package/dist/8618.js +1 -1
  60. package/dist/890.js +1 -1
  61. package/dist/8930.js +2 -0
  62. package/dist/{6525.js.LICENSE.txt → 8930.js.LICENSE.txt} +16 -4
  63. package/dist/8930.js.map +1 -0
  64. package/dist/9214.js +1 -1
  65. package/dist/942.js +1 -0
  66. package/dist/942.js.map +1 -0
  67. package/dist/9538.js +1 -1
  68. package/dist/9569.js +1 -1
  69. package/dist/961.js +1 -1
  70. package/dist/961.js.map +1 -1
  71. package/dist/986.js +1 -1
  72. package/dist/9879.js +1 -1
  73. package/dist/9895.js +1 -1
  74. package/dist/9900.js +1 -1
  75. package/dist/9913.js +1 -1
  76. package/dist/main.js +1 -1
  77. package/dist/main.js.map +1 -1
  78. package/dist/openmrs-esm-billing-app.js +1 -1
  79. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +368 -262
  80. package/dist/openmrs-esm-billing-app.js.map +1 -1
  81. package/dist/routes.json +1 -1
  82. package/e2e/README.md +19 -18
  83. package/e2e/core/test.ts +1 -1
  84. package/e2e/fixtures/api.ts +1 -1
  85. package/e2e/specs/sample-test.spec.ts +0 -1
  86. package/e2e/support/github/Dockerfile +1 -1
  87. package/package.json +13 -10
  88. package/src/bill-history/bill-history.component.tsx +17 -25
  89. package/src/bill-history/bill-history.scss +4 -94
  90. package/src/bill-history/bill-history.test.tsx +37 -78
  91. package/src/bill-item-actions/bill-item-actions.scss +0 -4
  92. package/src/bill-item-actions/{edit-bill-item.component.tsx → edit-bill-item.modal.tsx} +100 -78
  93. package/src/bill-item-actions/edit-bill-item.test.tsx +116 -31
  94. package/src/billable-services/bill-waiver/bill-selection.component.tsx +2 -2
  95. package/src/billable-services/bill-waiver/patient-bills.component.tsx +3 -3
  96. package/src/billable-services/billable-service.resource.ts +17 -9
  97. package/src/billable-services/billable-services-home.component.tsx +1 -1
  98. package/src/billable-services/billable-services.component.tsx +142 -145
  99. package/src/billable-services/billable-services.scss +3 -0
  100. package/src/billable-services/billable-services.test.tsx +2 -45
  101. package/src/billable-services/cash-point/add-cash-point.modal.tsx +168 -0
  102. package/src/billable-services/cash-point/cash-point-configuration.component.tsx +18 -192
  103. package/src/billable-services/cash-point/cash-point-configuration.scss +1 -5
  104. package/src/billable-services/create-edit/add-billable-service.component.tsx +345 -298
  105. package/src/billable-services/create-edit/add-billable-service.scss +5 -6
  106. package/src/billable-services/create-edit/add-billable-service.test.tsx +37 -36
  107. package/src/billable-services/create-edit/edit-billable-service.modal.tsx +51 -0
  108. package/src/billable-services/payment-modes/add-payment-mode.modal.tsx +121 -0
  109. package/src/billable-services/payment-modes/delete-payment-mode.modal.tsx +72 -0
  110. package/src/billable-services/payment-modes/payment-modes-config.component.tsx +125 -0
  111. package/src/billable-services/{payyment-modes → payment-modes}/payment-modes-config.scss +5 -4
  112. package/src/billing-form/billing-checkin-form.component.tsx +2 -3
  113. package/src/billing-form/billing-checkin-form.test.tsx +97 -24
  114. package/src/billing-form/billing-form.component.tsx +216 -269
  115. package/src/billing-form/billing-form.scss +143 -0
  116. package/src/billing.resource.ts +16 -19
  117. package/src/bills-table/bills-table.test.tsx +98 -54
  118. package/src/config-schema.ts +52 -24
  119. package/src/dashboard.meta.ts +4 -2
  120. package/src/helpers/functions.ts +5 -4
  121. package/src/index.ts +17 -6
  122. package/src/invoice/invoice-table.component.tsx +35 -69
  123. package/src/invoice/invoice-table.scss +1 -5
  124. package/src/invoice/invoice-table.test.tsx +273 -62
  125. package/src/invoice/invoice.component.tsx +36 -29
  126. package/src/invoice/invoice.scss +7 -4
  127. package/src/invoice/invoice.test.tsx +324 -120
  128. package/src/invoice/payments/payment-form/payment-form.component.tsx +31 -29
  129. package/src/invoice/payments/payment-form/payment-form.scss +5 -6
  130. package/src/invoice/payments/payment-form/payment-form.test.tsx +216 -66
  131. package/src/invoice/payments/payment-history/payment-history.component.tsx +6 -4
  132. package/src/invoice/payments/payment-history/payment-history.test.tsx +9 -14
  133. package/src/invoice/payments/payments.component.tsx +53 -65
  134. package/src/invoice/payments/payments.test.tsx +282 -0
  135. package/src/invoice/payments/utils.ts +5 -23
  136. package/src/invoice/printable-invoice/print-receipt.component.tsx +3 -2
  137. package/src/invoice/printable-invoice/print-receipt.test.tsx +14 -25
  138. package/src/invoice/printable-invoice/printable-footer.component.tsx +2 -2
  139. package/src/invoice/printable-invoice/printable-footer.test.tsx +4 -13
  140. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +12 -11
  141. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +16 -14
  142. package/src/invoice/printable-invoice/printable-invoice.component.tsx +19 -33
  143. package/src/left-panel-link.test.tsx +1 -4
  144. package/src/metrics-cards/metrics-cards.test.tsx +18 -5
  145. package/src/modal/require-payment-modal.test.tsx +27 -22
  146. package/src/modal/{require-payment-modal.component.tsx → require-payment.modal.tsx} +17 -18
  147. package/src/routes.json +22 -2
  148. package/src/types/index.ts +26 -17
  149. package/translations/am.json +70 -21
  150. package/translations/ar.json +70 -21
  151. package/translations/ar_SY.json +70 -21
  152. package/translations/bn.json +75 -26
  153. package/translations/de.json +70 -21
  154. package/translations/en.json +70 -21
  155. package/translations/en_US.json +70 -21
  156. package/translations/es.json +70 -21
  157. package/translations/es_MX.json +70 -21
  158. package/translations/fr.json +83 -34
  159. package/translations/he.json +70 -21
  160. package/translations/hi.json +70 -21
  161. package/translations/hi_IN.json +70 -21
  162. package/translations/id.json +70 -21
  163. package/translations/it.json +105 -56
  164. package/translations/ka.json +70 -21
  165. package/translations/km.json +70 -21
  166. package/translations/ku.json +70 -21
  167. package/translations/ky.json +70 -21
  168. package/translations/lg.json +70 -21
  169. package/translations/ne.json +70 -21
  170. package/translations/pl.json +70 -21
  171. package/translations/pt.json +70 -21
  172. package/translations/pt_BR.json +70 -21
  173. package/translations/qu.json +70 -21
  174. package/translations/ro_RO.json +214 -165
  175. package/translations/ru_RU.json +70 -21
  176. package/translations/si.json +70 -21
  177. package/translations/sw.json +70 -21
  178. package/translations/sw_KE.json +70 -21
  179. package/translations/tr.json +70 -21
  180. package/translations/tr_TR.json +70 -21
  181. package/translations/uk.json +70 -21
  182. package/translations/uz.json +70 -21
  183. package/translations/uz@Latn.json +70 -21
  184. package/translations/uz_UZ.json +70 -21
  185. package/translations/vi.json +70 -21
  186. package/translations/zh.json +70 -21
  187. package/translations/zh_CN.json +125 -76
  188. package/dist/1146.js.LICENSE.txt +0 -21
  189. package/dist/2352.js +0 -1
  190. package/dist/2352.js.map +0 -1
  191. package/dist/246.js +0 -1
  192. package/dist/246.js.map +0 -1
  193. package/dist/6525.js +0 -2
  194. package/dist/6525.js.map +0 -1
  195. package/dist/8556.js +0 -2
  196. package/dist/8556.js.map +0 -1
  197. package/dist/8638.js +0 -1
  198. package/dist/8638.js.map +0 -1
  199. package/dist/9968.js +0 -1
  200. package/dist/9968.js.map +0 -1
  201. package/src/billable-services/payyment-modes/payment-modes-config.component.tsx +0 -280
  202. package/src/invoice/payments/payments.component.test.tsx +0 -121
  203. /package/dist/{8556.js.LICENSE.txt → 7452.js.LICENSE.txt} +0 -0
@@ -6,37 +6,42 @@ import { zodResolver } from '@hookform/resolvers/zod';
6
6
  import { navigate, showSnackbar, useConfig, useVisit } from '@openmrs/esm-framework';
7
7
  import { Button } from '@carbon/react';
8
8
  import { CardHeader } from '@openmrs/esm-patient-common-lib';
9
- import { type LineItem, type MappedBill } from '../../types';
10
9
  import { convertToCurrency } from '../../helpers';
11
10
  import { createPaymentPayload } from './utils';
12
- import { processBillPayment } from '../../billing.resource';
13
11
  import { InvoiceBreakDown } from './invoice-breakdown/invoice-breakdown.component';
14
- import PaymentHistory from './payment-history/payment-history.component';
15
- import PaymentForm from './payment-form/payment-form.component';
12
+ import { processBillPayment } from '../../billing.resource';
13
+ import { type MappedBill } from '../../types';
16
14
  import { updateBillVisitAttribute } from './payment.resource';
17
- import styles from './payments.scss';
18
15
  import { useBillableServices } from '../../billable-services/billable-service.resource';
16
+ import PaymentForm from './payment-form/payment-form.component';
17
+ import PaymentHistory from './payment-history/payment-history.component';
18
+ import styles from './payments.scss';
19
19
 
20
20
  type PaymentProps = {
21
21
  bill: MappedBill;
22
- selectedLineItems: Array<LineItem>;
23
22
  mutate: () => void;
24
23
  };
25
24
 
26
- export type Payment = { method: string; amount: string | number; referenceCode?: number | string };
25
+ export type Payment = { method: string; amount: number | undefined; referenceCode?: number | string };
27
26
 
28
27
  export type PaymentFormValue = {
29
28
  payment: Array<Payment>;
30
29
  };
31
30
 
32
- const Payments: React.FC<PaymentProps> = ({ bill, mutate, selectedLineItems }) => {
31
+ const Payments: React.FC<PaymentProps> = ({ bill, mutate }) => {
33
32
  const { t } = useTranslation();
34
- const { billableServices, isLoading, isValidating, error } = useBillableServices();
33
+ const { billableServices } = useBillableServices();
35
34
  const paymentSchema = z.object({
36
35
  method: z.string().refine((value) => !!value, 'Payment method is required'),
37
36
  amount: z
38
- .number()
39
- .lte(bill?.totalAmount - bill?.tenderedAmount, { message: 'Amount paid should not be greater than amount due' }),
37
+ .number({
38
+ required_error: t('amountRequired', 'Amount is required'),
39
+ invalid_type_error: t('amountRequired', 'Amount is required'),
40
+ })
41
+ .positive({ message: t('amountMustBePositive', 'Amount must be greater than 0') })
42
+ .max(bill?.totalAmount - bill?.tenderedAmount, {
43
+ message: t('paymentAmountCannotExceedAmountDue', 'Payment amount cannot exceed amount due'),
44
+ }),
40
45
  referenceCode: z.union([z.number(), z.string()]).optional(),
41
46
  });
42
47
 
@@ -54,57 +59,45 @@ const Payments: React.FC<PaymentProps> = ({ bill, mutate, selectedLineItems }) =
54
59
  control: methods.control,
55
60
  });
56
61
 
57
- const selectedLineItemsTotal = selectedLineItems.reduce((total, item) => total + item.price * item.quantity, 0);
58
- const totalAmountTendered = formValues?.reduce((curr: number, prev) => curr + Number(prev.amount) ?? 0, 0) ?? 0;
59
- const amountDue = bill ? bill.totalAmount - selectedLineItemsTotal : 0;
60
- const clientBalance = bill ? bill.totalAmount - (bill.tenderedAmount + totalAmountTendered) : 0;
61
-
62
62
  const handleNavigateToBillingDashboard = () =>
63
63
  navigate({
64
64
  to: window.getOpenmrsSpaBase() + 'home/billing',
65
65
  });
66
66
 
67
- const handleProcessPayment = () => {
68
- if (bill) {
69
- const paymentPayload = createPaymentPayload(
70
- bill,
71
- bill?.patientUuid,
72
- formValues,
73
- amountDue,
74
- billableServices,
75
- selectedLineItems,
76
- );
77
- paymentPayload.payments.forEach((payment) => {
78
- payment.dateCreated = new Date(payment.dateCreated);
79
- });
80
-
81
- processBillPayment(paymentPayload, bill.uuid).then(
82
- (res) => {
83
- showSnackbar({
84
- title: t('billPayment', 'Bill payment'),
85
- subtitle: 'Bill payment processing has been successful',
86
- kind: 'success',
87
- timeoutInMs: 3000,
88
- });
89
- if (currentVisit) {
90
- updateBillVisitAttribute(currentVisit);
91
- }
92
- methods.reset({ payment: [{ method: '', amount: '0', referenceCode: '' }] });
93
- mutate();
94
- },
95
- (error) => {
96
- showSnackbar({ title: 'Bill payment error', kind: 'error', subtitle: error?.message });
97
- },
98
- );
99
- }
100
- };
101
-
102
67
  if (!bill) {
103
68
  return null;
104
69
  }
105
70
 
106
- const amountDueLabel = selectedLineItems.length ? t('amountDue', 'Amount Due') : t('clientBalance', 'Client Balance');
107
- const amountDueValue = selectedLineItems.length ? amountDue : clientBalance;
71
+ const amountDue = bill.totalAmount - bill.tenderedAmount;
72
+
73
+ const handleProcessPayment = async () => {
74
+ const amountBeingTendered = formValues?.reduce((acc, curr) => acc + (curr.amount || 0), 0);
75
+ const amountRemaining = amountDue - amountBeingTendered;
76
+ const paymentPayload = createPaymentPayload(bill, bill?.patientUuid, formValues, amountRemaining, billableServices);
77
+ paymentPayload.payments.forEach((payment) => {
78
+ payment.dateCreated = new Date(payment.dateCreated);
79
+ });
80
+
81
+ try {
82
+ await processBillPayment(paymentPayload, bill.uuid);
83
+ showSnackbar({
84
+ title: t('billPayment', 'Bill payment'),
85
+ subtitle: t('paymentProcessedSuccessfully', 'Payment processed successfully'),
86
+ kind: 'success',
87
+ });
88
+ if (currentVisit) {
89
+ updateBillVisitAttribute(currentVisit);
90
+ }
91
+ methods.reset({ payment: [{ method: '', amount: undefined, referenceCode: '' }] });
92
+ mutate();
93
+ } catch (error) {
94
+ showSnackbar({
95
+ title: t('errorProcessingPayment', 'Error processing payment'),
96
+ kind: 'error',
97
+ subtitle: error?.message,
98
+ });
99
+ }
100
+ };
108
101
 
109
102
  return (
110
103
  <FormProvider {...methods}>
@@ -114,13 +107,8 @@ const Payments: React.FC<PaymentProps> = ({ bill, mutate, selectedLineItems }) =
114
107
  <span></span>
115
108
  </CardHeader>
116
109
  <div>
117
- {bill && <PaymentHistory bill={bill} />}
118
- <PaymentForm
119
- disablePayment={clientBalance <= 0}
120
- clientBalance={clientBalance}
121
- isSingleLineItemSelected={selectedLineItems.length > 0}
122
- isSingleLineItem={bill.lineItems.length === 1}
123
- />
110
+ <PaymentHistory bill={bill} />
111
+ <PaymentForm disablePayment={amountDue <= 0} isSingleLineItem={bill.lineItems.length === 1} />
124
112
  </div>
125
113
  </div>
126
114
  <div className={styles.divider} />
@@ -131,19 +119,19 @@ const Payments: React.FC<PaymentProps> = ({ bill, mutate, selectedLineItems }) =
131
119
  />
132
120
  <InvoiceBreakDown
133
121
  label={t('totalTendered', 'Total Tendered')}
134
- value={convertToCurrency(bill?.tenderedAmount + totalAmountTendered, defaultCurrency)}
122
+ value={convertToCurrency(bill.tenderedAmount, defaultCurrency)}
135
123
  />
136
124
  <InvoiceBreakDown label={t('discount', 'Discount')} value={'--'} />
137
125
  <InvoiceBreakDown
138
- hasBalance={amountDueValue < 0}
139
- label={amountDueLabel}
140
- value={convertToCurrency(amountDueValue < 0 ? -amountDueValue : amountDueValue, defaultCurrency)}
126
+ hasBalance={amountDue < 0}
127
+ label={t('amountDue', 'Amount Due')}
128
+ value={convertToCurrency(amountDue < 0 ? -amountDue : amountDue, defaultCurrency)}
141
129
  />
142
130
  <div className={styles.processPayments}>
143
131
  <Button onClick={handleNavigateToBillingDashboard} kind="secondary">
144
132
  {t('discard', 'Discard')}
145
133
  </Button>
146
- <Button onClick={() => handleProcessPayment()} disabled={!formValues?.length || !methods.formState.isValid}>
134
+ <Button onClick={handleProcessPayment} disabled={!formValues?.length || !methods.formState.isValid}>
147
135
  {t('processPayment', 'Process Payment')}
148
136
  </Button>
149
137
  </div>
@@ -0,0 +1,282 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/react';
4
+ import {
5
+ useVisit,
6
+ useConfig,
7
+ navigate,
8
+ getDefaultsFromConfigSchema,
9
+ type VisitReturnType,
10
+ } from '@openmrs/esm-framework';
11
+ import { useBillableServices } from '../../billable-services/billable-service.resource';
12
+ import { type MappedBill } from '../../types';
13
+ import { configSchema, type BillingConfig } from '../../config-schema';
14
+ import { usePaymentModes } from './payment.resource';
15
+ import Payments from './payments.component';
16
+
17
+ const mockUseVisit = jest.mocked(useVisit);
18
+ const mockUseConfig = jest.mocked(useConfig<BillingConfig>);
19
+ const mockUseBillableServices = jest.mocked(useBillableServices);
20
+ const mockUsePaymentModes = jest.mocked(usePaymentModes);
21
+ const mockFormatToParts = jest.fn().mockReturnValue([{ type: 'integer', value: '1000' }]);
22
+ const mockFormat = jest.fn().mockReturnValue('$1000.00');
23
+ const mockResolvedOptions = jest.fn().mockReturnValue({
24
+ locale: 'en-US',
25
+ numberingSystem: 'latn',
26
+ style: 'currency',
27
+ currency: 'USD',
28
+ minimumFractionDigits: 2,
29
+ maximumFractionDigits: 2,
30
+ });
31
+
32
+ global.Intl.NumberFormat.supportedLocalesOf = jest.fn().mockReturnValue(['en-US']);
33
+ global.Intl.NumberFormat = jest.fn().mockImplementation(() => ({
34
+ formatToParts: mockFormatToParts,
35
+ format: mockFormat,
36
+ resolvedOptions: mockResolvedOptions,
37
+ })) as any;
38
+
39
+ jest.mock('../../billing.resource', () => ({
40
+ processBillPayment: jest.fn(),
41
+ }));
42
+
43
+ jest.mock('./payment.resource', () => ({
44
+ updateBillVisitAttribute: jest.fn(),
45
+ usePaymentModes: jest.fn(),
46
+ }));
47
+
48
+ jest.mock('../../billable-services/billable-service.resource', () => ({
49
+ useBillableServices: jest.fn(),
50
+ }));
51
+
52
+ describe('Payments', () => {
53
+ const mockBill: MappedBill = {
54
+ uuid: 'bill-uuid',
55
+ id: 1,
56
+ patientUuid: 'patient-uuid',
57
+ patientName: 'John Doe',
58
+ cashPointUuid: 'cash-point-uuid',
59
+ cashPointName: 'Main Cash Point',
60
+ cashPointLocation: 'Main Hospital',
61
+ cashier: {
62
+ uuid: 'provider-1',
63
+ display: 'Jane Doe',
64
+ links: [
65
+ {
66
+ rel: 'self',
67
+ uri: 'http://example.com/provider/1',
68
+ resourceAlias: 'Jane Doe',
69
+ },
70
+ ],
71
+ },
72
+ payments: [
73
+ {
74
+ uuid: 'payment-1',
75
+ dateCreated: new Date('2023-09-01T12:00:00Z').getTime(),
76
+ amountTendered: 100,
77
+ amount: 80,
78
+ instanceType: {
79
+ uuid: 'instance-1',
80
+ name: 'Credit Card',
81
+ description: 'Credit Card payment',
82
+ retired: false,
83
+ },
84
+ attributes: [],
85
+ voided: false,
86
+ resourceVersion: '1.0',
87
+ },
88
+ {
89
+ uuid: 'payment-2',
90
+ dateCreated: new Date('2023-09-05T14:00:00Z').getTime(),
91
+ amountTendered: 200,
92
+ amount: 180,
93
+ instanceType: {
94
+ uuid: 'instance-2',
95
+ name: 'Cash',
96
+ description: 'Cash payment',
97
+ retired: false,
98
+ },
99
+ attributes: [],
100
+ voided: false,
101
+ resourceVersion: '1.0',
102
+ },
103
+ ],
104
+ receiptNumber: '12345',
105
+ status: 'PAID',
106
+ identifier: 'invoice-123',
107
+ dateCreated: '2023-09-01T12:00:00Z',
108
+ lineItems: [],
109
+ billingService: 'Billing Service',
110
+ };
111
+
112
+ const mockMutate = jest.fn();
113
+
114
+ beforeEach(() => {
115
+ mockUseVisit.mockReturnValue({ currentVisit: null } as unknown as VisitReturnType);
116
+ mockUseConfig.mockReturnValue({ ...getDefaultsFromConfigSchema(configSchema), defaultCurrency: 'USD' });
117
+ mockUseBillableServices.mockReturnValue({
118
+ billableServices: [],
119
+ isLoading: false,
120
+ isValidating: false,
121
+ error: null,
122
+ mutate: jest.fn(),
123
+ });
124
+ mockUsePaymentModes.mockReturnValue({
125
+ paymentModes: [
126
+ { uuid: '1', name: 'Cash', description: 'Cash payment', retired: false },
127
+ { uuid: '2', name: 'Credit Card', description: 'Credit Card payment', retired: false },
128
+ ],
129
+ isLoading: false,
130
+ error: null,
131
+ mutate: jest.fn(),
132
+ });
133
+ });
134
+
135
+ it('renders payment form and history', () => {
136
+ render(<Payments bill={mockBill} mutate={mockMutate} />);
137
+ expect(screen.getByText('Payments')).toBeInTheDocument();
138
+ expect(screen.getByText('Total Amount:')).toBeInTheDocument();
139
+ expect(screen.getByText('Total Tendered:')).toBeInTheDocument();
140
+ });
141
+
142
+ it('displays formatted currency amounts', () => {
143
+ render(<Payments bill={mockBill} mutate={mockMutate} />);
144
+ // Verify that currency formatting is applied (mocked to return '$1000.00')
145
+ const formattedAmounts = screen.getAllByText('$1000.00');
146
+ expect(formattedAmounts.length).toBeGreaterThan(0);
147
+ });
148
+
149
+ it('disables Process Payment button when form is invalid', () => {
150
+ render(<Payments bill={mockBill} mutate={mockMutate} />);
151
+ expect(screen.getByText('Process Payment')).toBeDisabled();
152
+ });
153
+
154
+ it('navigates to billing dashboard when Discard is clicked', async () => {
155
+ render(<Payments bill={mockBill} mutate={mockMutate} />);
156
+ await userEvent.click(screen.getByText('Discard'));
157
+ expect(navigate).toHaveBeenCalled();
158
+ });
159
+
160
+ it('should validate amount is required', () => {
161
+ const billWithAmountDue: MappedBill = {
162
+ ...mockBill,
163
+ totalAmount: 100,
164
+ tenderedAmount: 0,
165
+ };
166
+
167
+ render(<Payments bill={billWithAmountDue} mutate={mockMutate} />);
168
+
169
+ // Process Payment button should be disabled when no amount is entered
170
+ expect(screen.getByText('Process Payment')).toBeDisabled();
171
+ });
172
+
173
+ it('should handle undefined amount values correctly', () => {
174
+ const billWithAmountDue: MappedBill = {
175
+ ...mockBill,
176
+ totalAmount: 100,
177
+ tenderedAmount: 0,
178
+ };
179
+
180
+ render(<Payments bill={billWithAmountDue} mutate={mockMutate} />);
181
+
182
+ expect(screen.getByText('Process Payment')).toBeDisabled();
183
+ });
184
+
185
+ it('should display amount due when there is a balance', () => {
186
+ const billWithBalance: MappedBill = {
187
+ ...mockBill,
188
+ totalAmount: 500,
189
+ tenderedAmount: 200,
190
+ };
191
+
192
+ render(<Payments bill={billWithBalance} mutate={mockMutate} />);
193
+
194
+ expect(screen.getByText('Amount Due:')).toBeInTheDocument();
195
+ // The amount due section should be visible for bills with remaining balance
196
+ const formattedAmounts = screen.getAllByText('$1000.00');
197
+ expect(formattedAmounts.length).toBeGreaterThan(0);
198
+ });
199
+
200
+ it('should display amount due as absolute value when overpaid', () => {
201
+ const billWithOverpayment: MappedBill = {
202
+ ...mockBill,
203
+ totalAmount: 100,
204
+ tenderedAmount: 150,
205
+ };
206
+
207
+ render(<Payments bill={billWithOverpayment} mutate={mockMutate} />);
208
+
209
+ // Even with negative amount due (overpayment), the display should show positive value
210
+ expect(screen.getByText('Amount Due:')).toBeInTheDocument();
211
+ const formattedAmounts = screen.getAllByText('$1000.00');
212
+ expect(formattedAmounts.length).toBeGreaterThan(0);
213
+ });
214
+
215
+ it('should disable adding payment methods when amount due is zero or less', () => {
216
+ const fullyPaidBill: MappedBill = {
217
+ ...mockBill,
218
+ totalAmount: 100,
219
+ tenderedAmount: 100,
220
+ };
221
+
222
+ render(<Payments bill={fullyPaidBill} mutate={mockMutate} />);
223
+
224
+ expect(screen.getByText(/add payment method/i)).toBeDisabled();
225
+ });
226
+
227
+ it('should return null when bill is not provided', () => {
228
+ const { container } = render(<Payments bill={null} mutate={mockMutate} />);
229
+ expect(container).toBeEmptyDOMElement();
230
+ });
231
+
232
+ it('should render add payment method button for bills with amount due', () => {
233
+ const billWithAmountDue: MappedBill = {
234
+ ...mockBill,
235
+ totalAmount: 100,
236
+ tenderedAmount: 0,
237
+ lineItems: [],
238
+ };
239
+
240
+ render(<Payments bill={billWithAmountDue} mutate={mockMutate} />);
241
+
242
+ // Verify add payment method button is available
243
+ expect(screen.getByText(/add payment method/i)).toBeInTheDocument();
244
+ expect(screen.getByText(/add payment method/i)).toBeEnabled();
245
+ });
246
+
247
+ it('should display process payment button', () => {
248
+ const billWithAmountDue: MappedBill = {
249
+ ...mockBill,
250
+ totalAmount: 100,
251
+ tenderedAmount: 0,
252
+ lineItems: [],
253
+ };
254
+
255
+ render(<Payments bill={billWithAmountDue} mutate={mockMutate} />);
256
+
257
+ // Process payment button should be visible
258
+ expect(screen.getByText('Process Payment')).toBeInTheDocument();
259
+ // Button should be disabled when no payment methods are added
260
+ expect(screen.getByText('Process Payment')).toBeDisabled();
261
+ });
262
+
263
+ it('should allow adding multiple payment methods for split payments', async () => {
264
+ const user = userEvent.setup();
265
+ const billWithAmountDue: MappedBill = {
266
+ ...mockBill,
267
+ totalAmount: 100,
268
+ tenderedAmount: 0,
269
+ };
270
+
271
+ render(<Payments bill={billWithAmountDue} mutate={mockMutate} />);
272
+
273
+ // Add first payment method
274
+ await user.click(screen.getByText(/add payment method/i));
275
+ expect(screen.getByPlaceholderText(/enter amount/i)).toBeInTheDocument();
276
+
277
+ // Add second payment method
278
+ await user.click(screen.getByText(/add payment method/i));
279
+ const amountInputs = screen.getAllByPlaceholderText(/enter amount/i);
280
+ expect(amountInputs).toHaveLength(2);
281
+ });
282
+ });
@@ -1,24 +1,15 @@
1
- import { type LineItem, type MappedBill } from '../../types';
1
+ import { type MappedBill } from '../../types';
2
2
  import { type Payment } from './payments.component';
3
3
 
4
- const hasLineItem = (lineItems: Array<LineItem>, item: LineItem) => {
5
- if (lineItems?.length === 0) {
6
- return false;
7
- }
8
- const foundItem = lineItems.find((lineItem) => lineItem.uuid === item.uuid);
9
- return Boolean(foundItem);
10
- };
11
-
12
4
  export const createPaymentPayload = (
13
5
  bill: MappedBill,
14
6
  patientUuid: string,
15
7
  formValues: Array<Payment>,
16
8
  amountDue: number,
17
9
  billableServices: Array<any>,
18
- selectedLineItems: Array<LineItem>,
19
10
  ) => {
20
11
  const { cashier } = bill;
21
- const totalAmount = bill?.totalAmount;
12
+ const totalAmount = bill.totalAmount ?? 0;
22
13
  const paymentStatus = amountDue <= 0 ? 'PAID' : 'PENDING';
23
14
  const previousPayments = bill?.payments.map((payment) => ({
24
15
  amount: payment.amount,
@@ -30,37 +21,28 @@ export const createPaymentPayload = (
30
21
 
31
22
  const newPayments = formValues.map((formValue) => ({
32
23
  amount: parseFloat(totalAmount.toFixed(2)),
33
- amountTendered: parseFloat(Number(formValue.amount).toFixed(2)),
24
+ amountTendered: parseFloat(Number(formValue.amount || 0).toFixed(2)),
34
25
  attributes: [],
35
26
  instanceType: formValue.method,
36
27
  dateCreated: new Date(),
37
28
  }));
38
29
 
39
30
  const updatedPayments = [...newPayments, ...previousPayments];
40
- const totalAmountRendered = updatedPayments.reduce((acc, payment) => acc + payment.amountTendered, 0);
41
31
 
42
32
  const updatedLineItems = bill?.lineItems.map((lineItem) => ({
43
33
  ...lineItem,
44
34
  billableService: getBillableServiceUuid(billableServices, lineItem.billableService),
45
35
  item: processBillItem?.(lineItem),
46
- paymentStatus:
47
- bill?.lineItems.length > 1
48
- ? hasLineItem(selectedLineItems ?? [], lineItem) && totalAmountRendered >= lineItem.price * lineItem.quantity
49
- ? 'PAID'
50
- : 'PENDING'
51
- : paymentStatus,
36
+ paymentStatus: lineItem.paymentStatus === 'PAID' ? 'PAID' : paymentStatus,
52
37
  }));
53
38
 
54
- const allItemsBillPaymentStatus =
55
- updatedLineItems.filter((item) => item.paymentStatus === 'PENDING').length === 0 ? 'PAID' : 'PENDING';
56
-
57
39
  const processedPayment = {
58
40
  cashPoint: bill?.cashPointUuid,
59
41
  cashier: cashier.uuid,
60
42
  lineItems: updatedLineItems,
61
43
  payments: [...updatedPayments],
62
44
  patient: patientUuid,
63
- status: selectedLineItems?.length > 0 ? allItemsBillPaymentStatus : paymentStatus,
45
+ status: paymentStatus,
64
46
  };
65
47
 
66
48
  return processedPayment;
@@ -2,8 +2,9 @@ import React, { useState } from 'react';
2
2
  import { Button } from '@carbon/react';
3
3
  import { Printer } from '@carbon/react/icons';
4
4
  import { useTranslation } from 'react-i18next';
5
- import styles from './print-receipt.scss';
5
+ import { getCoreTranslation } from '@openmrs/esm-framework';
6
6
  import { apiBasePath } from '../../constants';
7
+ import styles from './print-receipt.scss';
7
8
 
8
9
  interface PrintReceiptProps {
9
10
  billId: number;
@@ -36,7 +37,7 @@ const PrintReceipt: React.FC<PrintReceiptProps> = ({ billId }) => {
36
37
  renderIcon={(props) => <Printer size={24} {...props} />}
37
38
  onClick={handlePrintReceiptClick}
38
39
  disabled={isRedirecting}>
39
- {isRedirecting ? t('loading', 'Loading') : t('printReceipt', 'Print receipt')}
40
+ {isRedirecting ? getCoreTranslation('loading') : t('printReceipt', 'Print receipt')}
40
41
  </Button>
41
42
  );
42
43
  };
@@ -1,50 +1,39 @@
1
1
  import React from 'react';
2
- import { render, screen, fireEvent } from '@testing-library/react';
3
- import { useTranslation } from 'react-i18next';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { render, screen } from '@testing-library/react';
4
4
  import PrintReceipt from './print-receipt.component';
5
5
 
6
- jest.mock('react-i18next', () => ({
7
- useTranslation: jest.fn(),
8
- }));
9
-
10
- jest.mock('@carbon/react/icons', () => ({
11
- Printer: jest.fn(() => <div data-testid="printer-icon" />),
12
- }));
13
-
14
6
  describe('PrintReceipt', () => {
15
- const mockT = jest.fn((key) => key);
16
-
17
7
  beforeEach(() => {
18
- (useTranslation as jest.Mock).mockReturnValue({ t: mockT });
19
- jest.useFakeTimers();
20
8
  window.URL.createObjectURL = jest.fn();
21
9
  });
22
10
 
23
- afterEach(() => {
24
- jest.useRealTimers();
25
- });
26
-
27
11
  it('renders button with correct text and icon', () => {
28
12
  render(<PrintReceipt billId={123} />);
29
- expect(screen.getByText('printReceipt')).toBeInTheDocument();
30
- expect(screen.getByTestId('printer-icon')).toBeInTheDocument();
13
+ expect(screen.getByText('Print receipt')).toBeInTheDocument();
31
14
  });
32
15
 
33
- it('displays "Loading" and disables button when isRedirecting is true', () => {
16
+ it('displays "Loading" and disables button when isRedirecting is true', async () => {
17
+ const user = userEvent.setup();
18
+
34
19
  render(<PrintReceipt billId={123} />);
20
+
35
21
  const button = screen.getByRole('button');
36
- fireEvent.click(button);
37
- expect(screen.getByText('loading')).toBeInTheDocument();
22
+
23
+ await user.click(button);
24
+ expect(screen.getByText(/loading/i)).toBeInTheDocument();
38
25
  expect(button).toBeDisabled();
39
26
  });
40
27
 
41
- it('applies correct CSS class to button', () => {
28
+ it('applies correct CSS class to button', async () => {
29
+ const user = userEvent.setup();
30
+
42
31
  render(<PrintReceipt billId={123} />);
43
32
  expect(screen.getByRole('button')).toHaveClass('button');
44
33
  });
45
34
 
46
35
  it('translates button text correctly', () => {
47
36
  render(<PrintReceipt billId={123} />);
48
- expect(mockT).toHaveBeenCalledWith('printReceipt', 'Print receipt');
37
+ expect(screen.getByText('Print receipt')).toBeInTheDocument();
49
38
  });
50
39
  });
@@ -3,9 +3,9 @@ import { useDefaultFacility } from '../../billing.resource';
3
3
  import styles from './printable-footer.scss';
4
4
 
5
5
  const PrintableFooter = () => {
6
- const { data, isLoading } = useDefaultFacility();
6
+ const { data } = useDefaultFacility();
7
7
 
8
- if (isLoading) {
8
+ if (!data) {
9
9
  return <div>--</div>;
10
10
  }
11
11
 
@@ -3,28 +3,19 @@ import { screen, render } from '@testing-library/react';
3
3
  import { useDefaultFacility } from '../../billing.resource';
4
4
  import PrintableFooter from './printable-footer.component';
5
5
 
6
- const mockUseDefaultFacility = useDefaultFacility as jest.MockedFunction<typeof useDefaultFacility>;
6
+ const mockUseDefaultFacility = jest.mocked<typeof useDefaultFacility>(useDefaultFacility);
7
7
 
8
8
  jest.mock('../../billing.resource', () => ({
9
9
  useDefaultFacility: jest.fn(),
10
10
  }));
11
11
 
12
12
  describe('PrintableFooter', () => {
13
- beforeEach(() => {
14
- jest.clearAllMocks();
15
- });
16
-
17
13
  test('should render PrintableFooter component', () => {
18
- mockUseDefaultFacility.mockReturnValue({ data: { display: 'MTRH', uuid: 'mtrh-uuid' }, isLoading: false });
14
+ mockUseDefaultFacility.mockReturnValue({
15
+ data: { display: 'MTRH', uuid: 'mtrh-uuid', links: [] },
16
+ });
19
17
  render(<PrintableFooter />);
20
18
  const footer = screen.getByText('MTRH');
21
19
  expect(footer).toBeInTheDocument();
22
20
  });
23
-
24
- test('should show placeholder text when facility isLoading', () => {
25
- mockUseDefaultFacility.mockReturnValue({ data: { display: 'MTRH', uuid: 'mtrh-uuid' }, isLoading: true });
26
- render(<PrintableFooter />);
27
- const footer = screen.getByText('--');
28
- expect(footer).toBeInTheDocument();
29
- });
30
21
  });