@kenyaemr/esm-billing-app 5.3.8-pre.1599 → 5.3.8-pre.1608

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.
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.0.0"},"pages":[{"component":"billableServicesHome","route":"billable-services"},{"component":"requirePaymentModal","routeRegex":"^patient/.+/chart","online":true,"offline":false}],"extensions":[{"component":"billingDashboardLink","name":"billing-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"billing","title":"billing","slot":"billing-dashboard-slot"}},{"component":"benefitsPackageDashboardLink","name":"benefits-package-dashboard-link","slot":"patient-chart-dashboard-slot","meta":{"name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot","path":"insurance-benefits","columns":1,"columnSpan":1},"featureFlag":"healthInformationExchange"},{"component":"benefitsPackage","name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"component":"benefitsEligibilyRequestForm","name":"benefits-eligibility-request-form"},{"component":"benefitsPreAuthForm","name":"benefits-pre-auth-form"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing","layoutMode":"anchored"}},{"name":"billing-check-in-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm"},{"name":"require-billing-modal","component":"requirePaymentModal"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"initiate-payment-modal","component":"initiatePaymentDialog"},{"name":"delete-billableservice-modal","component":"deleteBillableServiceModal"},{"name":"refund-bill-modal","component":"refundBillModal"},{"name":"delete-bill-modal","component":"deleteBillModal"},{"name":"lab-order-billable-item","component":"labOrder","slot":"top-of-lab-order-form-slot"},{"name":"procedure-order-billable-item","component":"procedureOrder","slot":"top-of-procedure-order-form-slot"},{"name":"imaging-order-billable-item","component":"imagingOrder","slot":"top-of-imaging-order-form-slot"},{"name":"price-info-order","component":"priceInfoOrder"},{"name":"drug-order-billable-item","component":"drugOrder","slot":"medication-info-slot"},{"name":"billing-test-order-action","component":"testOrderAction","slot":"tests-ordered-actions-slot","order":0},{"component":"billingOverviewLink","name":"billing-overview-link","slot":"billing-dashboard-link-slot"},{"component":"paymentHistoryLink","name":"payment-history-link","slot":"billing-dashboard-link-slot"},{"component":"paymentPointsLink","name":"payment-points-link","slot":"billing-dashboard-link-slot"},{"component":"paymentModesLink","name":"payment-modes-link","slot":"billing-dashboard-link-slot"},{"component":"billManagerLink","name":"bill-manager-link","slot":"billing-dashboard-link-slot"},{"component":"chargeableItemsLink","name":"chargeable-items-link","slot":"billing-dashboard-link-slot"},{"component":"billableExemptionsLink","name":"billable-exemptions-link","slot":"billing-dashboard-link-slot"},{"component":"claimsManagementSideNavGroup","name":"claims-management-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"claims-management","title":"Claims management Overview","slot":"case-management-slot"},"featureFlag":"healthInformationExchange"},{"component":"claimsManagementOverviewDashboardLink","name":"claims-management-overview-link","slot":"claims-management-dashboard-link-slot"},{"component":"preAuthRequestsDashboardLink","name":"preauthrequest-overview-link","slot":"claims-management-dashboard-link-slot"},{"component":"claimsOverview","name":"claims-overview-dashboard-link","slot":"claims-management-overview-slot"},{"component":"waiveBillActionButton","name":"waive-bill-action-button","slot":"bill-actions-slot"},{"component":"deleteBillActionButton","name":"delete-bill-action-button","slot":"bill-actions-slot"},{"component":"refundLineItem","name":"refund-line-item","slot":"bill-actions-overflow-menu-slot"},{"name":"edit-line-item","component":"editLineItem","slot":"bill-actions-overflow-menu-slot"},{"name":"cancel-line-item","component":"cancelLineItem","slot":"bill-actions-overflow-menu-slot"}],"workspaces":[{"name":"waive-bill-form","component":"waiveBillForm","title":"Waive Bill Form","type":"other-form"},{"name":"edit-bill-form","component":"editBillForm","title":"Edit Bill Form","type":"other-form"},{"name":"billable-service-form","component":"addServiceForm","title":"Create Charge Item Form","type":"other-form"},{"name":"commodity-form","component":"addCommodityForm","title":"Create Charge Item Form","type":"other-form"},{"name":"billing-form","component":"billingForm","title":"Billing Form","type":"other-form","width":"extra-wide"},{"name":"payment-mode-workspace","component":"paymentModeWorkspace","title":"Payment Mode Workspace","type":"other-form"},{"name":"cancel-bill-workspace","component":"cancelBillWorkspace","title":"Cancel Bill Workspace","type":"other-form"}],"modals":[{"name":"create-payment-point","component":"createPaymentPoint"},{"name":"clock-in-modal","component":"clockIn"},{"name":"clock-out-modal","component":"clockOut"},{"name":"bulk-import-billable-services-modal","component":"bulkImportBillableServicesModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"retry-claim-request-modal","component":"retryClaimRequestModal"},{"name":"paid-bill-receipt-print-preview-modal","component":"paidBillReceiptPrintPreviewModal"}],"version":"5.3.8-pre.1599"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.0.0"},"pages":[{"component":"billableServicesHome","route":"billable-services"},{"component":"requirePaymentModal","routeRegex":"^patient/.+/chart","online":true,"offline":false}],"extensions":[{"component":"billingDashboardLink","name":"billing-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"billing","title":"billing","slot":"billing-dashboard-slot"}},{"component":"benefitsPackageDashboardLink","name":"benefits-package-dashboard-link","slot":"patient-chart-dashboard-slot","meta":{"name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot","path":"insurance-benefits","columns":1,"columnSpan":1},"featureFlag":"healthInformationExchange"},{"component":"benefitsPackage","name":"benefits-package","slot":"patient-chart-benefits-dashboard-slot"},{"component":"root","name":"billing-dashboard-root","slot":"billing-dashboard-slot"},{"component":"benefitsEligibilyRequestForm","name":"benefits-eligibility-request-form"},{"component":"benefitsPreAuthForm","name":"benefits-pre-auth-form"},{"name":"billing-patient-summary","component":"billingPatientSummary","slot":"patient-chart-billing-dashboard-slot","order":10,"meta":{"columnSpan":4}},{"name":"billing-summary-dashboard-link","component":"billingSummaryDashboardLink","slot":"patient-chart-dashboard-slot","order":11,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-billing-dashboard-slot","path":"Billing","layoutMode":"anchored"}},{"name":"billing-check-in-form","slot":"extra-visit-attribute-slot","component":"billingCheckInForm"},{"name":"require-billing-modal","component":"requirePaymentModal"},{"name":"patient-banner-billing-tags","component":"visitAttributeTags","slot":"patient-banner-tags-slot","order":2},{"name":"initiate-payment-modal","component":"initiatePaymentDialog"},{"name":"delete-billableservice-modal","component":"deleteBillableServiceModal"},{"name":"refund-bill-modal","component":"refundBillModal"},{"name":"delete-bill-modal","component":"deleteBillModal"},{"name":"lab-order-billable-item","component":"labOrder","slot":"top-of-lab-order-form-slot"},{"name":"procedure-order-billable-item","component":"procedureOrder","slot":"top-of-procedure-order-form-slot"},{"name":"imaging-order-billable-item","component":"imagingOrder","slot":"top-of-imaging-order-form-slot"},{"name":"price-info-order","component":"priceInfoOrder"},{"name":"drug-order-billable-item","component":"drugOrder","slot":"medication-info-slot"},{"name":"billing-test-order-action","component":"testOrderAction","slot":"tests-ordered-actions-slot","order":0},{"component":"billingOverviewLink","name":"billing-overview-link","slot":"billing-dashboard-link-slot"},{"component":"paymentHistoryLink","name":"payment-history-link","slot":"billing-dashboard-link-slot"},{"component":"paymentPointsLink","name":"payment-points-link","slot":"billing-dashboard-link-slot"},{"component":"paymentModesLink","name":"payment-modes-link","slot":"billing-dashboard-link-slot"},{"component":"billManagerLink","name":"bill-manager-link","slot":"billing-dashboard-link-slot"},{"component":"chargeableItemsLink","name":"chargeable-items-link","slot":"billing-dashboard-link-slot"},{"component":"billableExemptionsLink","name":"billable-exemptions-link","slot":"billing-dashboard-link-slot"},{"component":"claimsManagementSideNavGroup","name":"claims-management-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"claims-management","title":"Claims management Overview","slot":"case-management-slot"},"featureFlag":"healthInformationExchange"},{"component":"claimsManagementOverviewDashboardLink","name":"claims-management-overview-link","slot":"claims-management-dashboard-link-slot"},{"component":"preAuthRequestsDashboardLink","name":"preauthrequest-overview-link","slot":"claims-management-dashboard-link-slot"},{"component":"claimsOverview","name":"claims-overview-dashboard-link","slot":"claims-management-overview-slot"},{"component":"waiveBillActionButton","name":"waive-bill-action-button","slot":"bill-actions-slot"},{"component":"deleteBillActionButton","name":"delete-bill-action-button","slot":"bill-actions-slot"},{"component":"refundLineItem","name":"refund-line-item","slot":"bill-actions-overflow-menu-slot"},{"name":"edit-line-item","component":"editLineItem","slot":"bill-actions-overflow-menu-slot"},{"name":"cancel-line-item","component":"cancelLineItem","slot":"bill-actions-overflow-menu-slot"}],"workspaces":[{"name":"waive-bill-form","component":"waiveBillForm","title":"Waive Bill Form","type":"other-form"},{"name":"edit-bill-form","component":"editBillForm","title":"Edit Bill Form","type":"other-form"},{"name":"billable-service-form","component":"addServiceForm","title":"Create Charge Item Form","type":"other-form"},{"name":"commodity-form","component":"addCommodityForm","title":"Create Charge Item Form","type":"other-form"},{"name":"billing-form","component":"billingForm","title":"Billing Form","type":"other-form","width":"extra-wide"},{"name":"payment-mode-workspace","component":"paymentModeWorkspace","title":"Payment Mode Workspace","type":"other-form"},{"name":"cancel-bill-workspace","component":"cancelBillWorkspace","title":"Cancel Bill Workspace","type":"other-form"}],"modals":[{"name":"create-payment-point","component":"createPaymentPoint"},{"name":"clock-in-modal","component":"clockIn"},{"name":"clock-out-modal","component":"clockOut"},{"name":"bulk-import-billable-services-modal","component":"bulkImportBillableServicesModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"retry-claim-request-modal","component":"retryClaimRequestModal"},{"name":"paid-bill-receipt-print-preview-modal","component":"paidBillReceiptPrintPreviewModal"}],"version":"5.3.8-pre.1608"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kenyaemr/esm-billing-app",
3
- "version": "5.3.8-pre.1599",
3
+ "version": "5.3.8-pre.1608",
4
4
  "description": "Billing app for KenyaEMR",
5
5
  "browser": "dist/kenyaemr-esm-billing-app.js",
6
6
  "main": "src/index.ts",
@@ -59,6 +59,7 @@ export const PaymentHistoryTable = ({
59
59
  return {
60
60
  ...row,
61
61
  totalAmount: convertToCurrency(row.payments.reduce((acc, payment) => acc + payment.amountTendered, 0)),
62
+ referenceCodes: row.payments.map(({ attributes }) => attributes.map(({ value }) => value).join(' ')).join(', '),
62
63
  };
63
64
  });
64
65
 
@@ -71,18 +72,18 @@ export const PaymentHistoryTable = ({
71
72
  });
72
73
  const data = dataForExport.map((row: (typeof transformedRows)[0]) => {
73
74
  return {
75
+ 'Receipt Number': row.receiptNumber,
74
76
  'Patient ID': row.identifier,
75
77
  'Patient Name': row.patientName,
76
- 'Receipt Number': row.receiptNumber,
77
- 'Total Amount': row.lineItems.reduce((acc, item) => acc + item.price, 0),
78
- 'Payment Mode': row.payments.map((payment: (typeof row.payments)[0]) => payment.instanceType.name).join(', '),
79
- 'Payment Date': dayjs(row.payments[0].dateCreated).format('DD-MM-YYYY'),
80
- 'Payment Amount': row.payments.reduce((acc, payment) => acc + payment.amountTendered, 0),
78
+ 'Mode of Payment': row.payments
79
+ .map((payment: (typeof row.payments)[0]) => payment.instanceType.name)
80
+ .join(', '),
81
+ 'Total Amount Due': row.lineItems.reduce((acc, item) => acc + item.price, 0),
82
+ 'Date of Payment': dayjs(row.payments[0].dateCreated).format('DD-MM-YYYY'),
83
+ 'Total Amount Paid': row.payments.reduce((acc, payment) => acc + payment.amountTendered, 0),
81
84
  'Reason/Reference': row.payments
82
- .map((payment: (typeof row.payments)[0]) =>
83
- payment.attributes.map((attribute: (typeof payment.attributes)[0]) => attribute.attributeType.name),
84
- )
85
- .join(' '),
85
+ .map(({ attributes }) => attributes.map(({ value }) => value).join(' '))
86
+ .join(', '),
86
87
  };
87
88
  });
88
89
 
@@ -28,6 +28,10 @@ export interface BillingConfig {
28
28
  emergencyPriorityConceptUuid: string;
29
29
  };
30
30
  paymentMethodsUuidsThatShouldNotShowPrompt: Array<string>;
31
+ promptDuration: {
32
+ enable: boolean;
33
+ duration: number;
34
+ };
31
35
  }
32
36
 
33
37
  export const configSchema: ConfigSchema = {
@@ -182,4 +186,13 @@ export const configSchema: ConfigSchema = {
182
186
  },
183
187
  _default: ['beac329b-f1dc-4a33-9e7c-d95821a137a6'],
184
188
  },
189
+ promptDuration: {
190
+ _type: Type.Object,
191
+ _description:
192
+ 'The duration in hours for the prompt to be shown, if the duration is less than this, the prompt will be shown',
193
+ _default: {
194
+ enable: true,
195
+ duration: 24,
196
+ },
197
+ },
185
198
  };
@@ -65,24 +65,19 @@ export const getGender = (gender: string, t) => {
65
65
 
66
66
  /**
67
67
  * Extracts and returns the substring after the first colon (:) in the input string.
68
- * The input string is expected to be in the format "uuid:string".
68
+ * If there's no colon or the input is invalid, returns the original string.
69
+ * The input string is typically in the format "uuid:string".
69
70
  *
70
71
  * @param {string} input - The input string from which the substring is to be extracted.
71
- * @returns {string} The substring found after the first colon in the input string.
72
+ * @returns {string} The substring found after the first colon, or the original string if no colon is present.
72
73
  */
73
74
  export function extractString(input: string): string {
74
- const parts = input
75
- .split(' ')
76
- .map((s) => s.split(':')[1])
77
- .filter((s) => Boolean(s));
78
-
79
- const firstTwoBillableServices = parts.slice(0, 2);
80
-
81
- if (parts.length <= 2) {
82
- return firstTwoBillableServices.join(', ');
75
+ if (!input || typeof input !== 'string') {
76
+ return '';
83
77
  }
84
78
 
85
- return `${firstTwoBillableServices.join(', ')} & ${parts.length - 2} other services`;
79
+ const parts = input.split(':');
80
+ return parts.length > 1 ? parts[1] : input;
86
81
  }
87
82
 
88
83
  // cleans the provider display name
@@ -77,7 +77,7 @@ const PromptPaymentModal: React.FC<PromptPaymentModalProps> = () => {
77
77
  <StructuredListBody>
78
78
  {lineItems.map((lineItem) => {
79
79
  return (
80
- <StructuredListRow>
80
+ <StructuredListRow key={lineItem.uuid}>
81
81
  <StructuredListCell>{extractString(lineItem.billableService || lineItem.item)}</StructuredListCell>
82
82
  <StructuredListCell>{lineItem.quantity}</StructuredListCell>
83
83
  <StructuredListCell>{convertToCurrency(lineItem.price)}</StructuredListCell>
@@ -0,0 +1,200 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import PromptPaymentModal from './prompt-payment-modal.component';
4
+ import { useBillingPrompt } from './prompt-payment.resource';
5
+ import { navigate, useConfig } from '@openmrs/esm-framework';
6
+ import { PaymentStatus } from '../types';
7
+ import userEvent from '@testing-library/user-event';
8
+
9
+ const mockNavigate = navigate as jest.MockedFunction<typeof navigate>;
10
+
11
+ const mockMappedBill = [
12
+ {
13
+ uuid: '123e4567-e89b-12d3-a456-426614174000',
14
+ id: 1,
15
+ patientUuid: 'patient-uuid-123',
16
+ patientName: 'John Doe',
17
+ cashPointUuid: 'cashpoint-uuid-123',
18
+ cashPointName: 'Main Reception',
19
+ cashPointLocation: 'Hospital Wing A',
20
+ cashier: {
21
+ uuid: 'cashier-uuid-123',
22
+ display: 'Dr. Jane Smith',
23
+ links: [
24
+ {
25
+ rel: 'self',
26
+ uri: 'http://example.com/provider/cashier-uuid-123',
27
+ resourceAlias: 'provider',
28
+ },
29
+ ],
30
+ },
31
+ receiptNumber: 'REC-2023-001',
32
+ status: PaymentStatus.PENDING,
33
+ identifier: 'BILL-001',
34
+ dateCreated: '2023-08-15T10:30:00.000Z',
35
+ dateCreatedUnformatted: '2023-08-15',
36
+ lineItems: [
37
+ {
38
+ uuid: 'lineitem-uuid-123',
39
+ display: 'Consultation Fee',
40
+ voided: false,
41
+ voidReason: null,
42
+ item: 'Consultation',
43
+ billableService: 'some-uuid:General Consultation',
44
+ quantity: 1,
45
+ price: 50.0,
46
+ priceName: 'Standard Price',
47
+ priceUuid: 'price-uuid-123',
48
+ lineItemOrder: 1,
49
+ resourceVersion: '1.0',
50
+ paymentStatus: 'PENDING',
51
+ itemOrServiceConceptUuid: 'concept-uuid-123',
52
+ serviceTypeUuid: 'service-type-uuid-123',
53
+ order: {
54
+ uuid: 'order-uuid-123',
55
+ },
56
+ },
57
+ ],
58
+ billingService: 'Outpatient Services',
59
+ payments: [
60
+ {
61
+ uuid: 'payment-uuid-123',
62
+ instanceType: {
63
+ uuid: 'instance-type-uuid-123',
64
+ name: 'Cash Payment',
65
+ description: 'Standard cash payment',
66
+ retired: false,
67
+ },
68
+ attributes: [],
69
+ amount: 50.0,
70
+ amountTendered: 50.0,
71
+ dateCreated: 1692093000000, // Unix timestamp for 2023-08-15T10:30:00.000Z
72
+ voided: false,
73
+ resourceVersion: '1.0',
74
+ },
75
+ ],
76
+ totalAmount: 50.0,
77
+ tenderedAmount: 50.0,
78
+ display: 'Bill #BILL-001',
79
+ referenceCodes: 'REF-001',
80
+ adjustmentReason: undefined,
81
+ },
82
+ ];
83
+
84
+ const mockUseBillingPrompt = useBillingPrompt as jest.MockedFunction<typeof useBillingPrompt>;
85
+ const mockUseConfig = useConfig as jest.MockedFunction<typeof useConfig>;
86
+ jest.mock('@openmrs/esm-patient-common-lib', () => ({
87
+ getPatientUuidFromStore: jest.fn(() => 'patient-uuid'),
88
+ }));
89
+
90
+ jest.mock('@openmrs/esm-framework', () => ({
91
+ useConfig: jest.fn(),
92
+ navigate: jest.fn(),
93
+ }));
94
+
95
+ jest.mock('./prompt-payment.resource', () => ({
96
+ useBillingPrompt: jest.fn(),
97
+ }));
98
+
99
+ describe('<PromptPaymentModal />', () => {
100
+ beforeEach(() => {
101
+ jest.resetAllMocks();
102
+ });
103
+
104
+ test('should show the prompt payment modal, when `shouldShowBillingPrompt` is true and `enforceBillPayment` is true', async () => {
105
+ const user = userEvent.setup();
106
+ mockUseBillingPrompt.mockReturnValue({
107
+ shouldShowBillingPrompt: true,
108
+ isLoading: false,
109
+ bills: mockMappedBill,
110
+ currentVisit: null,
111
+ error: null,
112
+ });
113
+ mockUseConfig.mockReturnValue({
114
+ enforceBillPayment: true,
115
+ });
116
+ render(<PromptPaymentModal />);
117
+ expect(screen.getByText('Patient Billing Alert')).toBeInTheDocument();
118
+ expect(
119
+ screen.getByText('The current patient has pending bill. Advice patient to settle bill.'),
120
+ ).toBeInTheDocument();
121
+ expect(screen.getByText('Navigate back')).toBeInTheDocument();
122
+ // check if the structured list is rendered
123
+ const structuredListHeaders = ['Item', 'Quantity', 'Unit price', 'Total'];
124
+ structuredListHeaders.forEach((header) => {
125
+ expect(screen.getByText(header)).toBeInTheDocument();
126
+ });
127
+
128
+ // clicking cancel button should close the modal
129
+ const cancelButton = screen.getByText('Cancel');
130
+ await user.click(cancelButton);
131
+ expect(mockNavigate).toHaveBeenCalledWith({ to: `\${openmrsSpaBase}/home` });
132
+ // clicking proceed to care button should close the modal
133
+ const navigateBackButton = screen.getByText('Navigate back');
134
+ await user.click(navigateBackButton);
135
+ expect(mockNavigate).toHaveBeenCalledWith({ to: `\${openmrsSpaBase}/home` });
136
+ });
137
+
138
+ test('should show the prompt payment modal, when `shouldShowBillingPrompt` is true and `enforceBillPayment` is false and not navigate back', async () => {
139
+ const user = userEvent.setup();
140
+ mockUseBillingPrompt.mockReturnValue({
141
+ shouldShowBillingPrompt: true,
142
+ isLoading: false,
143
+ bills: mockMappedBill,
144
+ currentVisit: null,
145
+ error: null,
146
+ });
147
+ mockUseConfig.mockReturnValue({
148
+ enforceBillPayment: false,
149
+ });
150
+
151
+ render(<PromptPaymentModal />);
152
+ expect(screen.getByText('Patient Billing Alert')).toBeInTheDocument();
153
+ expect(
154
+ screen.getByText('The current patient has pending bill. Advice patient to settle bill.'),
155
+ ).toBeInTheDocument();
156
+ expect(screen.getByText('Cancel')).toBeInTheDocument();
157
+ expect(screen.getByText('Proceed to care')).toBeInTheDocument();
158
+
159
+ // clicking proceed to care button should close the modal
160
+ const proceedToCareButton = screen.getByText('Proceed to care');
161
+ await user.click(proceedToCareButton);
162
+ expect(mockNavigate).not.toHaveBeenCalled();
163
+
164
+ // clicking cancel button should close the modal
165
+ const cancelButton = screen.getByText('Cancel');
166
+ await user.click(cancelButton);
167
+ expect(mockNavigate).toHaveBeenCalledWith({ to: `\${openmrsSpaBase}/home` });
168
+ });
169
+
170
+ test('should show the loading state when `isLoading` is true', () => {
171
+ mockUseBillingPrompt.mockReturnValue({
172
+ shouldShowBillingPrompt: true,
173
+ isLoading: true,
174
+ bills: mockMappedBill,
175
+ currentVisit: null,
176
+ error: null,
177
+ });
178
+ mockUseConfig.mockReturnValue({
179
+ enforceBillPayment: true,
180
+ });
181
+ render(<PromptPaymentModal />);
182
+ expect(screen.getByText('Billing status')).toBeInTheDocument();
183
+ expect(screen.getByText('Verifying patient bills')).toBeInTheDocument();
184
+ });
185
+
186
+ test('should not render the modal when `shouldShowBillingPrompt` is false', () => {
187
+ mockUseBillingPrompt.mockReturnValue({
188
+ shouldShowBillingPrompt: false,
189
+ isLoading: false,
190
+ bills: mockMappedBill,
191
+ currentVisit: null,
192
+ error: null,
193
+ });
194
+ mockUseConfig.mockReturnValue({
195
+ enforceBillPayment: true,
196
+ });
197
+ render(<PromptPaymentModal />);
198
+ expect(screen.queryByText('Patient Billing Alert')).not.toBeInTheDocument();
199
+ });
200
+ });
@@ -4,6 +4,7 @@ import useSWR from 'swr';
4
4
  import { mapBillProperties } from '../billing.resource';
5
5
  import { BillingConfig } from '../config-schema';
6
6
  import { BillingPromptType, MappedBill, PatientInvoice } from '../types';
7
+ import dayjs from 'dayjs';
7
8
 
8
9
  interface BillingPromptResult {
9
10
  shouldShowBillingPrompt: boolean;
@@ -11,6 +12,12 @@ interface BillingPromptResult {
11
12
  error: Error | null;
12
13
  currentVisit: Visit | null;
13
14
  bills: Array<MappedBill>;
15
+ billingDuration?: {
16
+ isWithinPromptDuration: boolean;
17
+ hoursSinceLastBill: number;
18
+ lastDateBilled: Date;
19
+ mostRecentBill: MappedBill;
20
+ };
14
21
  }
15
22
 
16
23
  // Constants
@@ -80,7 +87,7 @@ const hasOnlyOrderBills = (bills: Array<MappedBill>): boolean => {
80
87
  * 1. The current visit is not an inpatient visit
81
88
  * 2. The patient has a positive bill balance
82
89
  * 3. The payment method is not in the excluded payment methods list
83
- * 4. For patient-chart, prompt is not shown if the line items in the bill are only orders
90
+ * 4. For patient-chart, prompt is not shown if the line items in the bill are only orders and the billing duration is within the prompt duration
84
91
  *
85
92
  * @param patientUuid - The UUID of the patient to check billing status for
86
93
  * @returns {BillingPromptResult} An object containing:
@@ -89,6 +96,7 @@ const hasOnlyOrderBills = (bills: Array<MappedBill>): boolean => {
89
96
  * - error: any error that occurred during data fetching
90
97
  * - currentVisit: the current visit object or null
91
98
  * - bills: array of the patient's bills
99
+ * - billingDuration: an object containing billing duration information
92
100
  */
93
101
  export const useBillingPrompt = (
94
102
  patientUuid: string,
@@ -100,28 +108,42 @@ export const useBillingPrompt = (
100
108
 
101
109
  const { paymentMethodsUuidsThatShouldNotShowPrompt, inPatientVisitTypeUuid } = config;
102
110
 
111
+ const hasLoaded = isLoadingBills && isLoadingVisit;
103
112
  const isExcludedPaymentMethod = checkPaymentMethodExclusion(currentVisit, paymentMethodsUuidsThatShouldNotShowPrompt);
104
-
105
113
  const patientBillBalance = calculateBillBalance(bills);
106
114
  const hasOnlyOrders = hasOnlyOrderBills(bills);
115
+ const billingDuration = checkBillingDuration(bills, config);
107
116
 
108
117
  if (promptType === 'patient-chart' && hasOnlyOrders) {
109
118
  return {
110
119
  shouldShowBillingPrompt: false,
111
- isLoading: isLoadingBills || isLoadingVisit,
120
+ isLoading: hasLoaded,
121
+ error,
122
+ currentVisit,
123
+ bills,
124
+ billingDuration,
125
+ };
126
+ }
127
+
128
+ if (promptType === 'patient-chart' && config.promptDuration.enable && !billingDuration.isWithinPromptDuration) {
129
+ return {
130
+ shouldShowBillingPrompt: false,
131
+ isLoading: hasLoaded,
112
132
  error,
113
133
  currentVisit,
114
134
  bills,
135
+ billingDuration,
115
136
  };
116
137
  }
117
138
 
118
139
  return {
119
140
  shouldShowBillingPrompt:
120
141
  !isExcludedPaymentMethod && shouldShowPrompt(currentVisit, patientBillBalance, inPatientVisitTypeUuid),
121
- isLoading: isLoadingBills || isLoadingVisit,
142
+ isLoading: hasLoaded,
122
143
  error,
123
144
  currentVisit,
124
145
  bills,
146
+ billingDuration,
125
147
  };
126
148
  };
127
149
 
@@ -160,3 +182,56 @@ export const usePatientBills = (patientUuid: string) => {
160
182
  mutate,
161
183
  };
162
184
  };
185
+
186
+ /**
187
+ * Checks if bills are within the configured prompt duration and provides billing timing information
188
+ *
189
+ * @param {Array<MappedBill>} bills - Array of patient bills containing dateCreated timestamps
190
+ * @param {Object} config - Configuration object containing prompt duration settings
191
+ * @param {number} config.promptDuration - Maximum number of hours to show prompt after bill creation
192
+ *
193
+ * @returns {Object} Billing duration information
194
+ * @returns {boolean} returns.isWithinPromptDuration - Whether the most recent bill is within prompt duration
195
+ * @returns {number} returns.hoursSinceLastBill - Hours elapsed since most recent bill
196
+ * @returns {string} returns.lastDateBilled - ISO timestamp of most recent bill
197
+ * @returns {MappedBill} returns.mostRecentBill - The most recent bill object
198
+ *
199
+ * @throws {Error} When bills array is empty or promptDuration is not configured
200
+ *
201
+ * @example
202
+ * const bills = [{ dateCreated: '2023-01-01T10:00:00Z' }];
203
+ * const config = { promptDuration: 24 };
204
+ * const result = checkBillingDuration(bills, config);
205
+ * // Returns: {
206
+ * // isWithinPromptDuration: true,
207
+ * // hoursSinceLastBill: 5,
208
+ * // lastDateBilled: '2023-01-01T10:00:00Z',
209
+ * // mostRecentBill: { dateCreated: '2023-01-01T10:00:00Z' }
210
+ * // }
211
+ */
212
+ const checkBillingDuration = (bills: Array<MappedBill> = [], config: BillingConfig) => {
213
+ if (!config?.promptDuration?.enable) {
214
+ return {
215
+ isWithinPromptDuration: false,
216
+ hoursSinceLastBill: 0,
217
+ lastDateBilled: new Date(),
218
+ mostRecentBill: null,
219
+ };
220
+ }
221
+
222
+ const sortedBills = [...bills].sort((a, b) => dayjs(b.dateCreated).diff(dayjs(a.dateCreated)));
223
+
224
+ const mostRecentBill = sortedBills[0];
225
+ const lastDateBilled = new Date(mostRecentBill?.dateCreatedUnformatted);
226
+ const currentDate = new Date();
227
+
228
+ // Calculate hours since last bill
229
+ const hoursSinceLastBill = dayjs(currentDate).diff(dayjs(lastDateBilled), 'hour');
230
+
231
+ return {
232
+ isWithinPromptDuration: hoursSinceLastBill <= config.promptDuration.duration,
233
+ hoursSinceLastBill,
234
+ lastDateBilled,
235
+ mostRecentBill,
236
+ };
237
+ };
@@ -97,6 +97,7 @@ interface AttributeType {
97
97
  foreignKey?: string | null;
98
98
  regExp?: string | null;
99
99
  required: boolean;
100
+ value?: string;
100
101
  }
101
102
 
102
103
  interface Attribute {