@openmrs/esm-billing-app 1.0.1-pre.14

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 (167) hide show
  1. package/.editorconfig +12 -0
  2. package/.eslintignore +2 -0
  3. package/.eslintrc +57 -0
  4. package/.husky/pre-commit +7 -0
  5. package/.husky/pre-push +6 -0
  6. package/.prettierignore +14 -0
  7. package/.turbo.json +18 -0
  8. package/.yarn/plugins/@yarnpkg/plugin-outdated.cjs +35 -0
  9. package/LICENSE +401 -0
  10. package/README.md +7 -0
  11. package/__mocks__/bills.mock.ts +392 -0
  12. package/__mocks__/delivery-summary.mock.ts +87 -0
  13. package/__mocks__/encounter-observation.mock.ts +10649 -0
  14. package/__mocks__/encounter-observations.mock.ts +6187 -0
  15. package/__mocks__/hiv-summary.mock.ts +22 -0
  16. package/__mocks__/patient-summary.mock.ts +32 -0
  17. package/__mocks__/patient.mock.ts +59 -0
  18. package/__mocks__/program-summary.mock.ts +43 -0
  19. package/__mocks__/react-i18next.js +57 -0
  20. package/dist/294.js +2 -0
  21. package/dist/294.js.LICENSE.txt +9 -0
  22. package/dist/294.js.map +1 -0
  23. package/dist/319.js +1 -0
  24. package/dist/384.js +1 -0
  25. package/dist/384.js.map +1 -0
  26. package/dist/421.js +1 -0
  27. package/dist/421.js.map +1 -0
  28. package/dist/450.js +1 -0
  29. package/dist/450.js.map +1 -0
  30. package/dist/476.js +1 -0
  31. package/dist/476.js.map +1 -0
  32. package/dist/574.js +1 -0
  33. package/dist/757.js +1 -0
  34. package/dist/788.js +1 -0
  35. package/dist/800.js +2 -0
  36. package/dist/800.js.LICENSE.txt +3 -0
  37. package/dist/800.js.map +1 -0
  38. package/dist/807.js +1 -0
  39. package/dist/833.js +1 -0
  40. package/dist/935.js +2 -0
  41. package/dist/935.js.LICENSE.txt +19 -0
  42. package/dist/935.js.map +1 -0
  43. package/dist/96.js +2 -0
  44. package/dist/96.js.LICENSE.txt +47 -0
  45. package/dist/96.js.map +1 -0
  46. package/dist/main.js +2 -0
  47. package/dist/main.js.LICENSE.txt +47 -0
  48. package/dist/main.js.map +1 -0
  49. package/dist/openmrs-esm-billing-app.js +1 -0
  50. package/dist/openmrs-esm-billing-app.js.buildmanifest.json +462 -0
  51. package/dist/openmrs-esm-billing-app.js.map +1 -0
  52. package/dist/routes.json +1 -0
  53. package/e2e/README.md +115 -0
  54. package/e2e/core/global-setup.ts +32 -0
  55. package/e2e/core/index.ts +1 -0
  56. package/e2e/core/test.ts +20 -0
  57. package/e2e/fixtures/api.ts +26 -0
  58. package/e2e/fixtures/index.ts +1 -0
  59. package/e2e/pages/home-page.ts +9 -0
  60. package/e2e/pages/index.ts +1 -0
  61. package/e2e/specs/sample-test.spec.ts +11 -0
  62. package/e2e/support/github/Dockerfile +34 -0
  63. package/e2e/support/github/docker-compose.yml +24 -0
  64. package/e2e/support/github/run-e2e-docker-env.sh +49 -0
  65. package/example.env +6 -0
  66. package/i18next-parser.config.js +89 -0
  67. package/jest.config.js +34 -0
  68. package/package.json +123 -0
  69. package/playwright.config.ts +32 -0
  70. package/prettier.config.js +8 -0
  71. package/src/bill-history/bill-history.component.tsx +187 -0
  72. package/src/bill-history/bill-history.scss +151 -0
  73. package/src/bill-history/bill-history.test.tsx +122 -0
  74. package/src/billable-services/bill-waiver/bill-selection.component.tsx +72 -0
  75. package/src/billable-services/bill-waiver/bill-waiver-form.component.tsx +108 -0
  76. package/src/billable-services/bill-waiver/bill-waiver-form.scss +34 -0
  77. package/src/billable-services/bill-waiver/bill-waiver.component.tsx +32 -0
  78. package/src/billable-services/bill-waiver/bill-waiver.scss +10 -0
  79. package/src/billable-services/bill-waiver/patient-bills.component.tsx +135 -0
  80. package/src/billable-services/bill-waiver/utils.ts +41 -0
  81. package/src/billable-services/billable-service.resource.ts +71 -0
  82. package/src/billable-services/billable-services-home.component.tsx +51 -0
  83. package/src/billable-services/billable-services.component.tsx +255 -0
  84. package/src/billable-services/billable-services.scss +218 -0
  85. package/src/billable-services/billable-services.test.tsx +16 -0
  86. package/src/billable-services/create-edit/add-billable-service.component.tsx +322 -0
  87. package/src/billable-services/create-edit/add-billable-service.scss +131 -0
  88. package/src/billable-services/create-edit/add-billable-service.test.tsx +152 -0
  89. package/src/billable-services/dashboard/dashboard.component.tsx +15 -0
  90. package/src/billable-services/dashboard/dashboard.scss +27 -0
  91. package/src/billable-services/dashboard/dashboard.test.tsx +11 -0
  92. package/src/billable-services/dashboard/service-metrics.component.tsx +42 -0
  93. package/src/billable-services-admin-card-link.component.test.tsx +21 -0
  94. package/src/billable-services-admin-card-link.component.tsx +25 -0
  95. package/src/billing-dashboard/billing-dashboard.component.tsx +20 -0
  96. package/src/billing-dashboard/billing-dashboard.scss +27 -0
  97. package/src/billing-dashboard/billing-dashboard.test.tsx +13 -0
  98. package/src/billing-form/billing-checkin-form.component.tsx +131 -0
  99. package/src/billing-form/billing-checkin-form.scss +13 -0
  100. package/src/billing-form/billing-checkin-form.test.tsx +134 -0
  101. package/src/billing-form/billing-form.component.tsx +25 -0
  102. package/src/billing-form/billing-form.resource.ts +31 -0
  103. package/src/billing-form/billing-form.scss +5 -0
  104. package/src/billing-form/visit-attributes/visit-attributes-form.component.tsx +173 -0
  105. package/src/billing-form/visit-attributes/visit-attributes-form.scss +22 -0
  106. package/src/billing-header/billing-header.component.tsx +43 -0
  107. package/src/billing-header/billing-header.scss +83 -0
  108. package/src/billing-header/billing-illustration.component.tsx +30 -0
  109. package/src/billing.resource.ts +120 -0
  110. package/src/bills-table/bills-table.component.tsx +280 -0
  111. package/src/bills-table/bills-table.scss +181 -0
  112. package/src/bills-table/bills-table.test.tsx +154 -0
  113. package/src/config-schema.ts +3 -0
  114. package/src/dashboard.meta.ts +6 -0
  115. package/src/declarations.d.ts +4 -0
  116. package/src/helpers/functions.ts +63 -0
  117. package/src/helpers/index.ts +1 -0
  118. package/src/index.ts +56 -0
  119. package/src/invoice/invoice-table.component.tsx +185 -0
  120. package/src/invoice/invoice-table.scss +91 -0
  121. package/src/invoice/invoice.component.tsx +138 -0
  122. package/src/invoice/invoice.scss +93 -0
  123. package/src/invoice/invoice.test.tsx +242 -0
  124. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.component.tsx +17 -0
  125. package/src/invoice/payments/invoice-breakdown/invoice-breakdown.scss +29 -0
  126. package/src/invoice/payments/payment-form/payment-form.component.tsx +105 -0
  127. package/src/invoice/payments/payment-form/payment-form.scss +54 -0
  128. package/src/invoice/payments/payment-history/payment-history.component.tsx +68 -0
  129. package/src/invoice/payments/payment.resource.ts +43 -0
  130. package/src/invoice/payments/payments.component.tsx +140 -0
  131. package/src/invoice/payments/payments.scss +46 -0
  132. package/src/invoice/payments/utils.ts +30 -0
  133. package/src/invoice/payments/visit-tags/visit-attribute.component.tsx +21 -0
  134. package/src/invoice/printable-invoice/print-receipt.component.tsx +28 -0
  135. package/src/invoice/printable-invoice/print-receipt.scss +14 -0
  136. package/src/invoice/printable-invoice/printable-footer.component.tsx +19 -0
  137. package/src/invoice/printable-invoice/printable-footer.scss +17 -0
  138. package/src/invoice/printable-invoice/printable-footer.test.tsx +30 -0
  139. package/src/invoice/printable-invoice/printable-invoice-header.component.tsx +63 -0
  140. package/src/invoice/printable-invoice/printable-invoice-header.scss +61 -0
  141. package/src/invoice/printable-invoice/printable-invoice-header.test.tsx +58 -0
  142. package/src/invoice/printable-invoice/printable-invoice.component.tsx +146 -0
  143. package/src/invoice/printable-invoice/printable-invoice.scss +50 -0
  144. package/src/left-panel-link.component.tsx +41 -0
  145. package/src/left-panel-link.test.tsx +38 -0
  146. package/src/metrics-cards/card.component.tsx +11 -0
  147. package/src/metrics-cards/card.scss +20 -0
  148. package/src/metrics-cards/metrics-cards.component.tsx +42 -0
  149. package/src/metrics-cards/metrics-cards.scss +12 -0
  150. package/src/metrics-cards/metrics-cards.test.tsx +41 -0
  151. package/src/metrics-cards/metrics.resource.ts +45 -0
  152. package/src/modal/require-payment-modal.component.tsx +81 -0
  153. package/src/modal/require-payment.scss +6 -0
  154. package/src/root.component.tsx +19 -0
  155. package/src/root.scss +30 -0
  156. package/src/routes.json +79 -0
  157. package/src/setup-tests.ts +13 -0
  158. package/src/types/index.ts +167 -0
  159. package/test-helpers.tsx +23 -0
  160. package/translations/am.json +107 -0
  161. package/translations/en.json +107 -0
  162. package/translations/es.json +107 -0
  163. package/translations/fr.json +107 -0
  164. package/translations/he.json +107 -0
  165. package/translations/km.json +107 -0
  166. package/tsconfig.json +16 -0
  167. package/webpack.config.js +1 -0
@@ -0,0 +1,42 @@
1
+ import React, { useMemo } from 'react';
2
+ import { InlineLoading } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { ErrorState } from '@openmrs/esm-patient-common-lib';
5
+ import { useBillableServices } from '../billable-service.resource';
6
+ import Card from '../../metrics-cards/card.component';
7
+ import styles from '../../metrics-cards/metrics-cards.scss';
8
+ import { ExtensionSlot } from '@openmrs/esm-framework';
9
+
10
+ export default function ServiceMetrics() {
11
+ const { t } = useTranslation();
12
+ const { isLoading, error } = useBillableServices();
13
+
14
+ const cards = useMemo(
15
+ () => [
16
+ { title: 'Cash Revenue', count: '--' },
17
+ { title: 'Insurance Revenue', count: '--' },
18
+ { title: 'Pending Claims', count: '--' },
19
+ ],
20
+ [],
21
+ );
22
+
23
+ if (isLoading) {
24
+ return (
25
+ <section className={styles.container}>
26
+ <InlineLoading status="active" iconDescription="Loading" description="Loading service metrics..." />
27
+ </section>
28
+ );
29
+ }
30
+
31
+ if (error) {
32
+ return <ErrorState headerTitle={t('serviceMetrics', 'Service Metrics')} error={error} />;
33
+ }
34
+ return (
35
+ <section className={styles.container}>
36
+ {cards.map((card) => (
37
+ <Card key={card.title} title={card.title} count={card.count} />
38
+ ))}
39
+ <ExtensionSlot name="billing-home-tiles-slot" />
40
+ </section>
41
+ );
42
+ }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import BillableServicesCardLink from './billable-services-admin-card-link.component';
4
+
5
+ describe('BillableServicesCardLink', () => {
6
+ test('should render billable services admin link', () => {
7
+ renderBillableServicesCardLink();
8
+ const manageBillableServicesText = screen.getByText('Manage billable services');
9
+ expect(manageBillableServicesText).toHaveClass('heading');
10
+
11
+ const billiableText = screen.getByText('Billable Services');
12
+ expect(billiableText).toHaveClass('content');
13
+
14
+ const billiableServiceLink = screen.getByRole('link', { name: /Billable Services/i });
15
+ expect(billiableServiceLink).toHaveAttribute('href', '/spa/billable-services');
16
+ });
17
+ });
18
+
19
+ function renderBillableServicesCardLink() {
20
+ render(<BillableServicesCardLink />);
21
+ }
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Layer, ClickableTile } from '@carbon/react';
4
+ import { ArrowRight } from '@carbon/react/icons';
5
+
6
+ const BillableServicesCardLink: React.FC = () => {
7
+ const { t } = useTranslation();
8
+ const header = t('manageBillableServices', 'Manage billable services');
9
+
10
+ return (
11
+ <Layer>
12
+ <ClickableTile href={`${window.spaBase}/billable-services`} target="_blank" rel="noopener noreferrer">
13
+ <div>
14
+ <div className="heading">{header}</div>
15
+ <div className="content">{t('billableServices', 'Billable Services')}</div>
16
+ </div>
17
+ <div className="iconWrapper">
18
+ <ArrowRight size={16} />
19
+ </div>
20
+ </ClickableTile>
21
+ </Layer>
22
+ );
23
+ };
24
+
25
+ export default BillableServicesCardLink;
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import BillingHeader from '../billing-header/billing-header.component';
4
+ import MetricsCards from '../metrics-cards/metrics-cards.component';
5
+ import BillsTable from '../bills-table/bills-table.component';
6
+ import styles from './billing-dashboard.scss';
7
+
8
+ export function BillingDashboard() {
9
+ const { t } = useTranslation();
10
+
11
+ return (
12
+ <main className={styles.container}>
13
+ <BillingHeader title={t('home', 'Home')} />
14
+ <MetricsCards />
15
+ <section className={styles.billsTableContainer}>
16
+ <BillsTable />
17
+ </section>
18
+ </main>
19
+ );
20
+ }
@@ -0,0 +1,27 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .container {
6
+ height: calc(100vh - 3rem);
7
+ }
8
+
9
+ .billsTableContainer {
10
+ margin: 2rem 1rem;
11
+ }
12
+
13
+ .illo {
14
+ margin-top: layout.$spacing-05;
15
+ }
16
+
17
+ .content {
18
+ @include type.type-style('heading-compact-01');
19
+ color: colors.$gray-70;
20
+ margin-top: layout.$spacing-05;
21
+ }
22
+
23
+ .tile {
24
+ border: 1px solid colors.$gray-20;
25
+ padding: 1.5rem 0;
26
+ text-align: center;
27
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { screen, render } from '@testing-library/react';
3
+ import { BillingDashboard } from './billing-dashboard.component';
4
+
5
+ test('renders an empty state when there are no billing records', () => {
6
+ renderBillingDashboard();
7
+
8
+ expect(screen.getByTitle(/billing module illustration/i)).toBeInTheDocument();
9
+ });
10
+
11
+ function renderBillingDashboard() {
12
+ render(<BillingDashboard />);
13
+ }
@@ -0,0 +1,131 @@
1
+ import React, { useCallback, useState } from 'react';
2
+ import { Dropdown, InlineLoading, InlineNotification } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { showSnackbar } from '@openmrs/esm-framework';
5
+ import { useCashPoint, useBillableItems, createPatientBill } from './billing-form.resource';
6
+ import VisitAttributesForm from './visit-attributes/visit-attributes-form.component';
7
+ import styles from './billing-checkin-form.scss';
8
+
9
+ const DEFAULT_PRICE = 500.00001;
10
+ const PENDING_PAYMENT_STATUS = 'PENDING';
11
+
12
+ type BillingCheckInFormProps = {
13
+ patientUuid: string;
14
+ setBillingInfo: (state) => void;
15
+ };
16
+
17
+ const BillingCheckInForm: React.FC<BillingCheckInFormProps> = ({ patientUuid, setBillingInfo }) => {
18
+ const { t } = useTranslation();
19
+ const { cashPoints, isLoading: isLoadingCashPoints, error: cashError } = useCashPoint();
20
+ const { lineItems, isLoading: isLoadingLineItems, error: lineError } = useBillableItems();
21
+ const [attributes, setAttributes] = useState([]);
22
+ const [paymentMethod, setPaymentMethod] = useState<any>();
23
+ let lineList = [];
24
+
25
+ const shouldBillPatient =
26
+ attributes.find((item) => item.attributeType === 'caf2124f-00a9-4620-a250-efd8535afd6d')?.value ===
27
+ '1c30ee58-82d4-4ea4-a8c1-4bf2f9dfc8cf';
28
+
29
+ const handleCreateBill = useCallback(
30
+ (createBillPayload) => {
31
+ shouldBillPatient &&
32
+ createPatientBill(createBillPayload).then(
33
+ () => {
34
+ showSnackbar({ title: 'Patient Bill', subtitle: 'Patient has been billed successfully', kind: 'success' });
35
+ },
36
+ (error) => {
37
+ showSnackbar({
38
+ title: 'Patient Bill Error',
39
+ subtitle: 'An error has occurred while creating patient bill',
40
+ kind: 'error',
41
+ });
42
+ },
43
+ );
44
+ },
45
+ [shouldBillPatient],
46
+ );
47
+
48
+ const handleBillingService = ({ selectedItem }) => {
49
+ const cashPointUuid = cashPoints?.[0]?.uuid ?? '';
50
+ const itemUuid = selectedItem?.uuid ?? '';
51
+
52
+ // should default to first price if check returns empty. todo - update backend to return default price
53
+ const priceForPaymentMode =
54
+ selectedItem.servicePrices.find((p) => p.paymentMode?.uuid === paymentMethod) || selectedItem?.servicePrices[0];
55
+
56
+ const createBillPayload = {
57
+ lineItems: [
58
+ {
59
+ billableService: itemUuid,
60
+ quantity: 1,
61
+ price: priceForPaymentMode ? priceForPaymentMode.price : '0.000',
62
+ priceName: 'Default',
63
+ priceUuid: priceForPaymentMode ? priceForPaymentMode.uuid : '',
64
+ lineItemOrder: 0,
65
+ paymentStatus: PENDING_PAYMENT_STATUS,
66
+ },
67
+ ],
68
+ cashPoint: cashPointUuid,
69
+ patient: patientUuid,
70
+ status: PENDING_PAYMENT_STATUS,
71
+ payments: [],
72
+ };
73
+
74
+ setBillingInfo({ createBillPayload, handleCreateBill: () => handleCreateBill(createBillPayload), attributes });
75
+ };
76
+
77
+ if (isLoadingLineItems || isLoadingCashPoints) {
78
+ return (
79
+ <InlineLoading
80
+ status="active"
81
+ iconDescription={t('loading', 'Loading')}
82
+ description={t('loadingBillingServices', 'Loading billing services...')}
83
+ />
84
+ );
85
+ }
86
+
87
+ if (paymentMethod) {
88
+ lineList = [];
89
+ lineList = lineItems.filter((e) =>
90
+ e.servicePrices.some((p) => p.paymentMode && p.paymentMode.uuid === paymentMethod),
91
+ );
92
+ }
93
+
94
+ const setServicePrice = (prices) => {
95
+ const matchingPrice = prices.find((p) => p.paymentMode?.uuid === paymentMethod);
96
+ return matchingPrice ? `(${matchingPrice.name}:${matchingPrice.price})` : '';
97
+ };
98
+
99
+ if (cashError || lineError) {
100
+ return (
101
+ <InlineNotification
102
+ kind="error"
103
+ lowContrast
104
+ title={t('billErrorService', 'Bill service error')}
105
+ subtitle={t('errorLoadingBillServices', 'Error loading bill services')}
106
+ />
107
+ );
108
+ }
109
+
110
+ return (
111
+ <section className={styles.sectionContainer}>
112
+ <VisitAttributesForm setAttributes={setAttributes} setPaymentMethod={setPaymentMethod} />
113
+ {shouldBillPatient && (
114
+ <>
115
+ <div className={styles.sectionTitle}>{t('billing', 'Billing')}</div>
116
+ <div className={styles.sectionField}></div>
117
+ <Dropdown
118
+ label={t('selectBillableService', 'Select a billable service...')}
119
+ onChange={handleBillingService}
120
+ id="billable-items"
121
+ items={lineList}
122
+ itemToString={(item) => (item ? `${item.name} ${setServicePrice(item.servicePrices)}` : '')}
123
+ titleText={t('billableService', 'Billable service')}
124
+ />
125
+ </>
126
+ )}
127
+ </section>
128
+ );
129
+ };
130
+
131
+ export default React.memo(BillingCheckInForm);
@@ -0,0 +1,13 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@carbon/colors';
4
+
5
+ .sectionContainer {
6
+ margin: 0 layout.$spacing-03;
7
+ }
8
+
9
+ .sectionTitle {
10
+ @include type.type-style('heading-compact-02');
11
+ color: colors.$gray-70;
12
+ margin: 0 0 layout.$spacing-03 0;
13
+ }
@@ -0,0 +1,134 @@
1
+ import React from 'react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { screen, render } from '@testing-library/react';
4
+ import { useBillableItems, useCashPoint, createPatientBill, usePaymentMethods } from './billing-form.resource';
5
+ import BillingCheckInForm from './billing-checkin-form.component';
6
+
7
+ const mockCashPoints = [
8
+ {
9
+ uuid: '54065383-b4d4-42d2-af4d-d250a1fd2590',
10
+ name: 'Cashier 2',
11
+ description: '',
12
+ retired: false,
13
+ },
14
+ ];
15
+
16
+ const mockBillableItems = [
17
+ {
18
+ uuid: 'b37dddd6-4490-4bf7-b694-43bf19d04059',
19
+ conceptUuid: '1926AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
20
+ conceptName: 'Consultation billable item',
21
+ hasExpiration: false,
22
+ preferredVendorUuid: '359006e7-2669-4204-aee8-27462514b10a',
23
+ preferredVendorName: 'Consolt',
24
+ categoryUuid: '6469ff7e-f8c7-42d6-bff3-ac9605ec99df',
25
+ categoryName: 'Non Drug',
26
+ commonName: 'Consultation',
27
+ acronym: 'CONSULT',
28
+ },
29
+ {
30
+ uuid: 'b47dddd6-4490-4bf7-b694-43bf19d04059',
31
+ conceptUuid: '1926AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
32
+ conceptName: 'Lab Testing billable item',
33
+ hasExpiration: false,
34
+ preferredVendorUuid: '359006e7-2669-4204-aee8-27462514b10a',
35
+ preferredVendorName: 'Consolt',
36
+ categoryUuid: '6469ff7e-f8c7-42d6-bff3-ac9605ec99df',
37
+ categoryName: 'Non Drug',
38
+ commonName: 'Lab Testing',
39
+ acronym: 'CONSULT',
40
+ },
41
+ ];
42
+
43
+ const mockUseCashPoint = useCashPoint as jest.MockedFunction<typeof useCashPoint>;
44
+ const mockUseBillableItems = useBillableItems as jest.MockedFunction<typeof useBillableItems>;
45
+ const mockCreatePatientBill = createPatientBill as jest.MockedFunction<typeof createPatientBill>;
46
+ const mockusePaymentMethods = usePaymentMethods as jest.MockedFunction<typeof usePaymentMethods>;
47
+
48
+ jest.mock('./billing-form.resource', () => ({
49
+ useBillableItems: jest.fn(),
50
+ useCashPoint: jest.fn(),
51
+ createPatientBill: jest.fn(),
52
+ }));
53
+
54
+ const testProps = { patientUuid: 'some-patient-uuid', setBillingInfo: jest.fn() };
55
+
56
+ xdescribe('BillingCheckInForm', () => {
57
+ beforeEach(() => {
58
+ jest.resetAllMocks();
59
+ });
60
+
61
+ test('should show the loading spinner while retrieving data', () => {
62
+ mockUseBillableItems.mockReturnValueOnce({ lineItems: [], isLoading: true, error: null });
63
+ mockUseCashPoint.mockReturnValueOnce({ cashPoints: [], isLoading: true, error: null });
64
+ renderBillingCheckinForm();
65
+
66
+ expect(screen.getByText(/Loading billing services.../)).toBeInTheDocument();
67
+ });
68
+
69
+ test('should show error state when an error occurs while fetching data', () => {
70
+ const error = new Error('Internal server error');
71
+ mockUseBillableItems.mockReturnValueOnce({ lineItems: [], isLoading: false, error });
72
+ mockUseCashPoint.mockReturnValueOnce({ cashPoints: [], isLoading: false, error });
73
+ renderBillingCheckinForm();
74
+
75
+ expect(screen.getByText('Bill service error')).toBeInTheDocument();
76
+ expect(screen.getByText('Error loading bill services')).toBeInTheDocument();
77
+ });
78
+
79
+ test('should render the form correctly and generate the required payload', async () => {
80
+ const user = userEvent.setup();
81
+ mockUseCashPoint.mockReturnValue({ cashPoints: [], isLoading: false, error: null });
82
+ mockUseBillableItems.mockReturnValue({ lineItems: mockBillableItems, isLoading: false, error: null });
83
+ renderBillingCheckinForm();
84
+
85
+ const paymentTypeSelect = screen.getByRole('group', { name: 'Payment Details' });
86
+ expect(paymentTypeSelect).toBeInTheDocument();
87
+
88
+ const paymentTypeRadio = screen.getByRole('radio', { name: 'Paying' });
89
+ expect(paymentTypeRadio).toBeInTheDocument();
90
+ await user.click(paymentTypeRadio);
91
+
92
+ const billiableSelect = screen.getByRole('combobox', { name: 'Billable service' });
93
+ expect(billiableSelect).toBeInTheDocument();
94
+ await user.click(screen.getByRole('combobox', { name: 'Billable service' }));
95
+
96
+ await user.click(screen.getByText('Lab Testing'));
97
+
98
+ expect(testProps.setBillingInfo).toHaveBeenCalled();
99
+ expect(testProps.setBillingInfo).toHaveBeenCalledWith({
100
+ createBillPayload: {
101
+ lineItems: [
102
+ {
103
+ item: 'b47dddd6-4490-4bf7-b694-43bf19d04059',
104
+ quantity: 1,
105
+ price: 500.00001,
106
+ priceName: 'Default',
107
+ priceUuid: '',
108
+ lineItemOrder: 0,
109
+ paymentStatus: 'PENDING',
110
+ },
111
+ ],
112
+ cashPoint: '',
113
+ patient: 'some-patient-uuid',
114
+ status: 'PENDING',
115
+ payments: [],
116
+ },
117
+ handleCreateBill: expect.anything(),
118
+ attributes: [
119
+ {
120
+ attributeType: 'caf2124f-00a9-4620-a250-efd8535afd6d',
121
+ value: '1c30ee58-82d4-4ea4-a8c1-4bf2f9dfc8cf',
122
+ },
123
+ {
124
+ attributeType: '919b51c9-8e2e-468f-8354-181bf3e55786',
125
+ value: true,
126
+ },
127
+ ],
128
+ });
129
+ });
130
+ });
131
+
132
+ function renderBillingCheckinForm() {
133
+ return render(<BillingCheckInForm {...testProps} />);
134
+ }
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+ import { RadioButtonGroup, RadioButton } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import styles from './billing-form.scss';
5
+
6
+ type BillingFormProps = {
7
+ patientUuid: string;
8
+ };
9
+
10
+ const BillingForm: React.FC<BillingFormProps> = ({ patientUuid }) => {
11
+ const { t } = useTranslation();
12
+ return (
13
+ <div className={styles.billingFormContainer}>
14
+ <RadioButtonGroup
15
+ legendText={t('selectCategory', 'Select category')}
16
+ name="radio-button-group"
17
+ defaultSelected="radio-1">
18
+ <RadioButton labelText={t('drug', 'Drug')} value="radio-1" id="radio-1" />
19
+ <RadioButton labelText={t('nonDrug', 'Non drug')} value="radio-2" id="radio-2" />
20
+ </RadioButtonGroup>
21
+ </div>
22
+ );
23
+ };
24
+
25
+ export default BillingForm;
@@ -0,0 +1,31 @@
1
+ import useSWR from 'swr';
2
+ import { type OpenmrsResource, openmrsFetch } from '@openmrs/esm-framework';
3
+
4
+ export const useBillableItems = () => {
5
+ const url = `/ws/rest/v1/cashier/billableService?v=custom:(uuid,name,shortName,serviceStatus,serviceType:(display),servicePrices:(uuid,name,price,paymentMode))`;
6
+ const { data, isLoading, error } = useSWR<{ data: { results: Array<OpenmrsResource> } }>(url, openmrsFetch);
7
+ return {
8
+ lineItems: data?.data?.results ?? [],
9
+ isLoading,
10
+ error,
11
+ };
12
+ };
13
+
14
+ export const useCashPoint = () => {
15
+ const url = `/ws/rest/v1/cashier/cashPoint`;
16
+ const { data, isLoading, error } = useSWR<{ data: { results: Array<OpenmrsResource> } }>(url, openmrsFetch);
17
+
18
+ return { isLoading, error, cashPoints: data?.data?.results ?? [] };
19
+ };
20
+
21
+ export const createPatientBill = (payload) => {
22
+ const postUrl = `/ws/rest/v1/cashier/bill`;
23
+ return openmrsFetch(postUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload });
24
+ };
25
+
26
+ export const usePaymentMethods = () => {
27
+ const url = `/ws/rest/v1/cashier/paymentMode`;
28
+ const { data, isLoading, error } = useSWR<{ data: { results: Array<OpenmrsResource> } }>(url, openmrsFetch);
29
+
30
+ return { isLoading, error, paymentModes: data?.data?.results ?? [] };
31
+ };
@@ -0,0 +1,5 @@
1
+ @use '@carbon/layout';
2
+
3
+ .billingFormContainer {
4
+ padding: layout.$spacing-05;
5
+ }
@@ -0,0 +1,173 @@
1
+ import React from 'react';
2
+ import { z } from 'zod';
3
+ import { zodResolver } from '@hookform/resolvers/zod';
4
+ import { Controller, useForm } from 'react-hook-form';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { TextInput, InlineLoading, ComboBox, RadioButtonGroup, RadioButton } from '@carbon/react';
7
+ import { usePaymentMethods } from '../billing-form.resource';
8
+ import styles from './visit-attributes-form.scss';
9
+
10
+ type VisitAttributesFormProps = {
11
+ setAttributes: (state) => void;
12
+ setPaymentMethod?: (value: any) => void;
13
+ };
14
+
15
+ type VisitAttributesFormValue = {
16
+ paymentDetails: string;
17
+ paymentMethods: string;
18
+ insuranceScheme: string;
19
+ policyNumber: string;
20
+ patientCategory: string;
21
+ };
22
+
23
+ const visitAttributesFormSchema = z.object({
24
+ paymentDetails: z.string(),
25
+ paymentMethods: z.string(),
26
+ insuranceSchema: z.string(),
27
+ policyNumber: z.string(),
28
+ patientCategory: z.string(),
29
+ });
30
+
31
+ const VisitAttributesForm: React.FC<VisitAttributesFormProps> = ({ setAttributes, setPaymentMethod }) => {
32
+ const { t } = useTranslation();
33
+ const { control, getValues, watch } = useForm<VisitAttributesFormValue>({
34
+ mode: 'all',
35
+ defaultValues: {},
36
+ resolver: zodResolver(visitAttributesFormSchema),
37
+ });
38
+ const [paymentDetails, paymentMethods, insuranceSchema, policyNumber, patientCategory] = watch([
39
+ 'paymentDetails',
40
+ 'paymentMethods',
41
+ 'insuranceScheme',
42
+ 'policyNumber',
43
+ 'patientCategory',
44
+ ]);
45
+
46
+ const { paymentModes, isLoading: isLoadingPaymentModes } = usePaymentMethods();
47
+ React.useEffect(() => {
48
+ setAttributes(createVisitAttributesPayload());
49
+ }, [paymentDetails, paymentMethods, insuranceSchema, policyNumber, patientCategory]);
50
+
51
+ const createVisitAttributesPayload = () => {
52
+ const { patientCategory, paymentMethods, policyNumber, paymentDetails } = getValues();
53
+ setPaymentMethod(paymentMethods);
54
+ const formPayload = [
55
+ { uuid: 'caf2124f-00a9-4620-a250-efd8535afd6d', value: paymentDetails },
56
+ { uuid: 'c39b684c-250f-4781-a157-d6ad7353bc90', value: paymentMethods },
57
+ { uuid: '0f4f3306-f01b-43c6-af5b-fdb60015cb02', value: policyNumber },
58
+ { uuid: '2d0fa959-6780-41f1-85b1-402045935068', value: insuranceSchema },
59
+ { uuid: '3b9dfac8-9e4d-11ee-8c90-0242ac120002', value: patientCategory },
60
+ { uuid: '919b51c9-8e2e-468f-8354-181bf3e55786', value: true },
61
+ ];
62
+ const visitAttributesPayload = formPayload.filter(
63
+ (item) => item.value !== undefined && item.value !== null && item.value !== '',
64
+ );
65
+ return Object.entries(visitAttributesPayload).map(([key, value]) => ({
66
+ attributeType: value.uuid,
67
+ value: value.value,
68
+ }));
69
+ };
70
+
71
+ if (isLoadingPaymentModes) {
72
+ return (
73
+ <InlineLoading
74
+ status="active"
75
+ iconDescription={t('loadingDescription', 'Loading')}
76
+ description={t('loading', 'Loading data...')}
77
+ />
78
+ );
79
+ }
80
+
81
+ return (
82
+ <section>
83
+ <div className={styles.sectionTitle}>{t('paymentDetails', 'Payment Details')}</div>
84
+ <Controller
85
+ name="paymentDetails"
86
+ control={control}
87
+ render={({ field }) => (
88
+ <RadioButtonGroup
89
+ onChange={(selected) => field.onChange(selected)}
90
+ orientation="vertical"
91
+ legendText={t('paymentDetails', 'Payment Details')}
92
+ name="payment-details-group">
93
+ <RadioButton labelText="Paying" value="1c30ee58-82d4-4ea4-a8c1-4bf2f9dfc8cf" id="radio-1" />
94
+ <RadioButton labelText="Non paying" value="a28d7929-050a-4249-a61a-551e9b8cc102" id="radio-2" />
95
+ </RadioButtonGroup>
96
+ )}
97
+ />
98
+
99
+ {paymentDetails === '1c30ee58-82d4-4ea4-a8c1-4bf2f9dfc8cf' && (
100
+ <Controller
101
+ control={control}
102
+ name="paymentMethods"
103
+ render={({ field }) => (
104
+ <ComboBox
105
+ className={styles.sectionField}
106
+ onChange={({ selectedItem }) => field.onChange(selectedItem?.uuid)}
107
+ id="paymentMethods"
108
+ items={paymentModes}
109
+ itemToString={(item) => (item ? item.name : '')}
110
+ titleText={t('paymentMethods', 'Payment methods')}
111
+ placeholder={t('selectPaymentMethod', 'Select payment method')}
112
+ />
113
+ )}
114
+ />
115
+ )}
116
+
117
+ {paymentMethods === 'beac329b-f1dc-4a33-9e7c-d95821a137a6' &&
118
+ paymentDetails === '1c30ee58-82d4-4ea4-a8c1-4bf2f9dfc8cf' && (
119
+ <>
120
+ <Controller
121
+ control={control}
122
+ name="insuranceScheme"
123
+ render={({ field }) => (
124
+ <TextInput
125
+ className={styles.sectionField}
126
+ onChange={(e) => field.onChange(e.target.value)}
127
+ id="insurance-scheme"
128
+ type="text"
129
+ labelText={t('insuranceScheme', 'Insurance scheme')}
130
+ />
131
+ )}
132
+ />
133
+ <Controller
134
+ control={control}
135
+ name="policyNumber"
136
+ render={({ field }) => (
137
+ <TextInput
138
+ className={styles.sectionField}
139
+ onChange={(e) => field.onChange(e.target.value)}
140
+ {...field}
141
+ id="policy-number"
142
+ type="text"
143
+ labelText={t('policyNumber', 'Policy number')}
144
+ />
145
+ )}
146
+ />
147
+ </>
148
+ )}
149
+
150
+ {paymentDetails === 'a28d7929-050a-4249-a61a-551e9b8cc102' && (
151
+ <Controller
152
+ control={control}
153
+ name="patientCategory"
154
+ render={({ field }) => (
155
+ <ComboBox
156
+ className={styles.sectionField}
157
+ onChange={({ selectedItem }) => field.onChange(selectedItem?.uuid)}
158
+ id="patientCategory"
159
+ items={[
160
+ { text: 'Child under 5', uuid: '2d61b762-6e32-4e2e-811f-ac72cbd3600a' },
161
+ { text: 'Student', uuid: '159465AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' },
162
+ ]}
163
+ itemToString={(item) => (item ? item.text : '')}
164
+ titleText={t('patientCategory', 'Patient category')}
165
+ />
166
+ )}
167
+ />
168
+ )}
169
+ </section>
170
+ );
171
+ };
172
+
173
+ export default VisitAttributesForm;