@kenyaemr/esm-billing-app 5.4.2-pre.2548 → 5.4.2-pre.2553

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":"accountingDashboardLink","name":"accounting-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"accounting","slot":"accounting-dashboard-slot","title":"Accounting"}},{"name":"billing-dashboard","component":"billingDashboard","slot":"accounting-dashboard-slot"},{"component":"benefitsPackageDashboardLink","name":"benefits-package-dashboard-link","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","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":"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":"order-action-button","component":"orderActionButton","slots":["prescription-action-button-slot","imaging-orders-action","procedure-orders-action","tests-ordered-actions-slot"],"order":0},{"component":"claimsManagementOverviewDashboardLink","name":"claims-management-overview-link","slots":["claims-management-dashboard-link-slot"]},{"component":"preAuthRequestsDashboardLink","name":"preauthrequest-overview-link","slots":["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"},{"name":"patient-info-sha-status","component":"patientBannerShaStatus","slot":"patient-banner-tags-slot"}],"workspaces":[{"name":"create-bill-workspace","component":"createBillWorkspace","title":"Create Bill Workspace","type":"other-form"},{"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"},{"name":"add-deposit-workspace","component":"addDepositWorkspace","title":"Add Deposit","type":"other-form"},{"name":"deposit-transaction-workspace","component":"depositTransactionWorkspace","title":"Deposit Transaction","type":"other-form"},{"name":"payment-workspace","component":"paymentWorkspace","title":"Payment Workspace","type":"other-form"}],"modals":[{"name":"create-payment-point","component":"createPaymentPoint"},{"name":"clock-out-modal","component":"clockOut"},{"name":"bulk-import-billable-services-modal","component":"bulkImportBillableServicesModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"manage-claim-request-modal","component":"manageClaimRequestModal"},{"name":"clock-in-modal","component":"clockIn"},{"name":"create-bill-item-modal","component":"createBillItemModal"},{"name":"delete-deposit-modal","component":"deleteDepositModal"},{"name":"reverse-transaction-modal","component":"reverseTransactionModal"},{"name":"print-preview-modal","component":"printPreviewModal"},{"name":"bill-action-modal","component":"billActionModal"},{"name":"require-billing-modal","component":"requirePaymentModal"}],"version":"5.4.2-pre.2548"}
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":"accountingDashboardLink","name":"accounting-dashboard-link","slot":"homepage-dashboard-slot","meta":{"name":"accounting","slot":"accounting-dashboard-slot","title":"Accounting"}},{"name":"billing-dashboard","component":"billingDashboard","slot":"accounting-dashboard-slot"},{"component":"benefitsPackageDashboardLink","name":"benefits-package-dashboard-link","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","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":"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":"order-action-button","component":"orderActionButton","slots":["prescription-action-button-slot","imaging-orders-action","procedure-orders-action","tests-ordered-actions-slot"],"order":0},{"component":"claimsManagementOverviewDashboardLink","name":"claims-management-overview-link","slots":["claims-management-dashboard-link-slot"]},{"component":"preAuthRequestsDashboardLink","name":"preauthrequest-overview-link","slots":["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"},{"name":"patient-info-sha-status","component":"patientBannerShaStatus","slot":"patient-banner-tags-slot"}],"workspaces":[{"name":"create-bill-workspace","component":"createBillWorkspace","title":"Create Bill Workspace","type":"other-form"},{"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"},{"name":"add-deposit-workspace","component":"addDepositWorkspace","title":"Add Deposit","type":"other-form"},{"name":"deposit-transaction-workspace","component":"depositTransactionWorkspace","title":"Deposit Transaction","type":"other-form"},{"name":"payment-workspace","component":"paymentWorkspace","title":"Payment Workspace","type":"other-form"}],"modals":[{"name":"create-payment-point","component":"createPaymentPoint"},{"name":"clock-out-modal","component":"clockOut"},{"name":"bulk-import-billable-services-modal","component":"bulkImportBillableServicesModal"},{"name":"delete-payment-mode-modal","component":"deletePaymentModeModal"},{"name":"manage-claim-request-modal","component":"manageClaimRequestModal"},{"name":"clock-in-modal","component":"clockIn"},{"name":"create-bill-item-modal","component":"createBillItemModal"},{"name":"delete-deposit-modal","component":"deleteDepositModal"},{"name":"reverse-transaction-modal","component":"reverseTransactionModal"},{"name":"print-preview-modal","component":"printPreviewModal"},{"name":"bill-action-modal","component":"billActionModal"},{"name":"require-billing-modal","component":"requirePaymentModal"}],"version":"5.4.2-pre.2553"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kenyaemr/esm-billing-app",
3
- "version": "5.4.2-pre.2548",
3
+ "version": "5.4.2-pre.2553",
4
4
  "description": "Billing app for KenyaEMR",
5
5
  "keywords": [
6
6
  "openmrs"
@@ -14,8 +14,8 @@ import {
14
14
  ComboBox,
15
15
  } from '@carbon/react';
16
16
  import { zodResolver } from '@hookform/resolvers/zod';
17
- import { navigate, showSnackbar, toOmrsIsoString, useConfig, useSession } from '@openmrs/esm-framework';
18
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
17
+ import { navigate, showModal, showSnackbar, toOmrsIsoString, useConfig, useSession } from '@openmrs/esm-framework';
18
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
19
19
  import { Controller, FormProvider, useForm } from 'react-hook-form';
20
20
  import { useTranslation } from 'react-i18next';
21
21
  import { useParams } from 'react-router-dom';
@@ -25,7 +25,7 @@ import { BillingConfig } from '../../../config-schema';
25
25
  import { useSystemSetting } from '../../../hooks/getMflCode';
26
26
  import usePatientDiagnosis from '../../../hooks/usePatientDiagnosis';
27
27
  import useProvider from '../../../hooks/useProvider';
28
- import { LineItem, MappedBill } from '../../../types';
28
+ import { ClaimSummary, LineItem, MappedBill, OTPVerificationModalOptions } from '../../../types';
29
29
  import ClaimExplanationAndJusificationInput from './claims-explanation-and-justification-form-input.component';
30
30
  import { processClaims, SHAPackagesAndInterventionVisitAttribute, useVisit } from './claims-form.resource';
31
31
  import useProviderList from '../../../hooks/useProviderList';
@@ -33,6 +33,8 @@ import useProviderList from '../../../hooks/useProviderList';
33
33
  import styles from './claims-form.scss';
34
34
  import debounce from 'lodash-es/debounce';
35
35
  import { formatDateTime } from '../../utils';
36
+ import { otpManager } from '../../../hooks/useOTP';
37
+ import { usePhoneNumberAttribute } from '../../../hooks/usePhoneNumber';
36
38
 
37
39
  type ClaimsFormProps = {
38
40
  bill: MappedBill;
@@ -65,6 +67,12 @@ const ClaimsFormSchema = z.object({
65
67
  provider: z.string().min(1, { message: 'Provider is required' }),
66
68
  });
67
69
 
70
+ enum OTPState {
71
+ NOT_STARTED = 'not_started',
72
+ REQUESTED = 'requested',
73
+ VERIFIED = 'verified',
74
+ }
75
+
68
76
  const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
69
77
  const { t } = useTranslation();
70
78
  const { mflCodeValue } = useSystemSetting('facility.mflcode');
@@ -74,33 +82,22 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
74
82
  const {
75
83
  currentProvider: { uuid: providerUuid },
76
84
  } = useSession();
77
- const { providerLoading: providerLoading, provider, error: providerError } = useProvider(providerUuid);
85
+ const { providerLoading, provider, error: providerError } = useProvider(providerUuid);
78
86
  const { visitAttributeTypes } = useConfig<BillingConfig>();
79
87
  const { providers, providersLoading } = useProviderList();
88
+ const { phoneNumber } = usePhoneNumberAttribute(patientUuid);
80
89
 
81
- const packagesAndinterventions = useMemo(() => {
82
- if (recentVisit) {
83
- const values = recentVisit.attributes?.find(
84
- (attr) => attr.attributeType.uuid === visitAttributeTypes.shaBenefitPackagesAndInterventions,
85
- )?.value;
86
- if (values) {
87
- const payload: SHAPackagesAndInterventionVisitAttribute = JSON.parse(values);
88
- return payload;
89
- }
90
- }
91
- return null;
92
- }, [recentVisit, visitAttributeTypes]);
93
-
94
- const encounterUuid = recentVisit?.encounters[0]?.uuid;
95
- const visitTypeUuid = recentVisit?.visitType.uuid;
96
- const [loading, setLoading] = useState(false);
90
+ const [otpState, setOtpState] = useState<OTPState>(OTPState.NOT_STARTED);
91
+ const [pendingClaimData, setPendingClaimData] = useState<z.infer<typeof ClaimsFormSchema> | null>(null);
97
92
  const [formInitialized, setFormInitialized] = useState(false);
98
93
  const [validationEnabled, setValidationEnabled] = useState(false);
94
+ const [loading, setLoading] = useState(false);
95
+ const [currentOtpPhoneNumber, setCurrentOtpPhoneNumber] = useState<string>('');
99
96
 
100
- const handleNavigateToBillingOptions = () =>
101
- navigate({
102
- to: window.getOpenmrsSpaBase() + `home/billing/patient/${patientUuid}/${billUuid}`,
103
- });
97
+ const currentPhoneRef = useRef<string>('');
98
+
99
+ const patientName = `${bill.patientName}`;
100
+ const otpExpiryMinutes = 5;
104
101
 
105
102
  const form = useForm<z.infer<typeof ClaimsFormSchema>>({
106
103
  mode: 'onTouched',
@@ -122,17 +119,14 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
122
119
  const {
123
120
  control,
124
121
  handleSubmit,
125
- formState: { errors, isValid, isDirty, touchedFields },
122
+ formState: { errors, isValid, touchedFields },
126
123
  setValue,
127
- reset,
128
124
  trigger,
129
125
  watch,
130
126
  } = form;
131
127
 
132
128
  const packages = watch('packages');
133
129
  const interventions = watch('interventions');
134
- const claimExplanation = watch('claimExplanation');
135
- const claimJustification = watch('claimJustification');
136
130
 
137
131
  const debouncedValidation = useCallback(
138
132
  debounce(() => {
@@ -140,7 +134,6 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
140
134
  trigger();
141
135
  }
142
136
  }, 500),
143
- // eslint-disable-line react-hooks/exhaustive-deps
144
137
  [formInitialized, trigger],
145
138
  );
146
139
 
@@ -157,8 +150,24 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
157
150
  facility: `${recentVisit?.location?.display || ''} - ${mflCodeValue || ''}`,
158
151
  treatmentStart: formatDateTime(recentVisit?.startDatetime || ''),
159
152
  treatmentEnd: formatDateTime(recentVisit?.stopDatetime || ''),
160
- packages: packagesAndinterventions?.packages ?? [],
161
- interventions: packagesAndinterventions?.interventions ?? [],
153
+ packages: recentVisit?.attributes?.find(
154
+ (attr) => attr.attributeType.uuid === visitAttributeTypes.shaBenefitPackagesAndInterventions,
155
+ )?.value
156
+ ? JSON.parse(
157
+ recentVisit.attributes.find(
158
+ (attr) => attr.attributeType.uuid === visitAttributeTypes.shaBenefitPackagesAndInterventions,
159
+ ).value,
160
+ ).packages
161
+ : [],
162
+ interventions: recentVisit?.attributes?.find(
163
+ (attr) => attr.attributeType.uuid === visitAttributeTypes.shaBenefitPackagesAndInterventions,
164
+ )?.value
165
+ ? JSON.parse(
166
+ recentVisit.attributes.find(
167
+ (attr) => attr.attributeType.uuid === visitAttributeTypes.shaBenefitPackagesAndInterventions,
168
+ ).value,
169
+ ).interventions
170
+ : [],
162
171
  provider: providerUuid,
163
172
  };
164
173
 
@@ -176,21 +185,87 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
176
185
  mflCodeValue,
177
186
  setValue,
178
187
  provider,
179
- packagesAndinterventions,
180
188
  visitLoading,
181
189
  diagnosisLoading,
182
190
  providerLoading,
183
191
  providerUuid,
192
+ visitAttributeTypes,
184
193
  ]);
185
194
 
186
- const onSubmit = async (data: z.infer<typeof ClaimsFormSchema>) => {
187
- setValidationEnabled(true);
188
- const isFormValid = await trigger();
189
- if (!isFormValid) {
190
- return;
191
- }
195
+ const generateClaimSummary = (data: z.infer<typeof ClaimsFormSchema>): ClaimSummary => {
196
+ const billServiceNames = selectedLineItems.map((item) => item.billableService);
197
+ const services = billServiceNames.map((service) => service.replace(/^[a-f0-9-]+:/, '').trim());
198
+ const totalAmount = selectedLineItems.reduce((sum, item) => sum + item.price * item.quantity, 0);
199
+
200
+ return {
201
+ totalAmount,
202
+ facility: recentVisit?.location?.display || '',
203
+ totalItems: selectedLineItems.length,
204
+ services: services.join(', '),
205
+ startDate: data.treatmentStart,
206
+ endDate: data.treatmentEnd,
207
+ };
208
+ };
209
+
210
+ const createDynamicOTPHandlers = useCallback(
211
+ (initialPhone: string) => {
212
+ currentPhoneRef.current = initialPhone;
213
+
214
+ return {
215
+ onRequestOtp: async (phoneNumber: string): Promise<void> => {
216
+ if (currentPhoneRef.current && currentPhoneRef.current !== phoneNumber) {
217
+ otpManager.transferOTP(currentPhoneRef.current, phoneNumber);
218
+ }
219
+
220
+ setCurrentOtpPhoneNumber(phoneNumber);
221
+ currentPhoneRef.current = phoneNumber;
222
+
223
+ const currentFormData = form.getValues();
224
+ if (!currentFormData || !selectedLineItems?.length) {
225
+ throw new Error('No claim data available for OTP request');
226
+ }
227
+
228
+ const claimSummary = generateClaimSummary(currentFormData);
229
+ await otpManager.requestOTP(phoneNumber, patientName, claimSummary, otpExpiryMinutes);
230
+ },
231
+ onVerify: async (otp: string): Promise<void> => {
232
+ const phoneForVerification = currentPhoneRef.current;
233
+
234
+ if (!phoneForVerification) {
235
+ throw new Error('No phone number available for verification');
236
+ }
192
237
 
238
+ const isValid = await otpManager.verifyOTP(phoneForVerification, otp);
239
+ if (!isValid) {
240
+ throw new Error('OTP verification failed');
241
+ }
242
+ },
243
+ };
244
+ },
245
+ [patientName, form, selectedLineItems, otpExpiryMinutes],
246
+ );
247
+
248
+ const launchOtpVerificationModal = (props: OTPVerificationModalOptions) => {
249
+ const dispose = showModal('otp-verification-modal', {
250
+ ...props,
251
+ onClose: () => {
252
+ if (otpState === OTPState.REQUESTED) {
253
+ setOtpState(OTPState.NOT_STARTED);
254
+ }
255
+ dispose();
256
+ },
257
+ size: 'xs',
258
+ });
259
+ return dispose;
260
+ };
261
+
262
+ const handleOTPVerificationSuccess = async (): Promise<void> => {
263
+ setOtpState(OTPState.VERIFIED);
264
+ };
265
+
266
+ const processClaim = async (data: z.infer<typeof ClaimsFormSchema>) => {
193
267
  setLoading(true);
268
+
194
269
  const providedItems = selectedLineItems.reduce((acc, item) => {
195
270
  acc[item.uuid] = {
196
271
  items: [
@@ -216,12 +291,12 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
216
291
  diagnoses: data.diagnoses,
217
292
  paidInFacility: true,
218
293
  patient: patientUuid,
219
- visitType: visitTypeUuid,
294
+ visitType: recentVisit?.visitType?.uuid,
220
295
  guaranteeId: 'G-001',
221
296
  claimCode: 'C-001',
222
297
  provider: data.provider,
223
- visitUuid: recentVisit.uuid,
224
- encounterUuid: encounterUuid,
298
+ visitUuid: recentVisit?.uuid,
299
+ encounterUuid: recentVisit?.encounters?.[0]?.uuid,
225
300
  use: 'claim',
226
301
  insurer: 'SHA',
227
302
  billNumber: billUuid,
@@ -231,20 +306,27 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
231
306
 
232
307
  try {
233
308
  await processClaims(payload);
309
+
234
310
  showSnackbar({
235
311
  kind: 'success',
236
312
  title: t('processClaim', 'Process Claim'),
237
- subtitle: t('sendClaim', 'Claim sent successfully'),
238
- timeoutInMs: 3000,
313
+ subtitle: t('claimProcessedSuccessfully', 'Claim processed and sent successfully'),
314
+ timeoutInMs: 4000,
239
315
  isLowContrast: true,
240
316
  });
317
+
318
+ setOtpState(OTPState.NOT_STARTED);
319
+ setPendingClaimData(null);
320
+ setCurrentOtpPhoneNumber('');
321
+ currentPhoneRef.current = '';
322
+ otpManager.clearAllOTPs();
323
+
241
324
  setTimeout(() => {
242
325
  navigate({
243
326
  to: window.getOpenmrsSpaBase() + 'home/billing/',
244
327
  });
245
328
  }, 1000);
246
329
  } catch (err) {
247
- console.error(err);
248
330
  showSnackbar({
249
331
  kind: 'error',
250
332
  title: t('claimError', 'Claim Error'),
@@ -257,6 +339,78 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
257
339
  }
258
340
  };
259
341
 
342
+ const handleInitiateOTPVerification = async (data: z.infer<typeof ClaimsFormSchema>) => {
343
+ setValidationEnabled(true);
344
+ const isFormValid = await trigger();
345
+ if (!isFormValid) {
346
+ return;
347
+ }
348
+
349
+ if (!phoneNumber) {
350
+ showSnackbar({
351
+ kind: 'error',
352
+ title: t('noPhoneNumber', 'No Phone Number'),
353
+ subtitle: t(
354
+ 'noPhoneNumberMessage',
355
+ 'No phone number found for this patient. Please update patient information.',
356
+ ),
357
+ timeoutInMs: 4000,
358
+ isLowContrast: false,
359
+ });
360
+ return;
361
+ }
362
+
363
+ setPendingClaimData(data);
364
+ setOtpState(OTPState.REQUESTED);
365
+ setCurrentOtpPhoneNumber(phoneNumber);
366
+ currentPhoneRef.current = phoneNumber;
367
+
368
+ const dynamicHandlers = createDynamicOTPHandlers(phoneNumber);
369
+
370
+ setTimeout(() => {
371
+ launchOtpVerificationModal({
372
+ otpLength: 5,
373
+ obscureText: false,
374
+ phoneNumber: phoneNumber,
375
+ expiryMinutes: otpExpiryMinutes,
376
+ onRequestOtp: dynamicHandlers.onRequestOtp,
377
+ onVerify: dynamicHandlers.onVerify,
378
+ onVerificationSuccess: handleOTPVerificationSuccess,
379
+ });
380
+ }, 0);
381
+ };
382
+
383
+ const handleProcessVerifiedClaim = async () => {
384
+ if (pendingClaimData && otpState === OTPState.VERIFIED) {
385
+ await processClaim(pendingClaimData);
386
+ }
387
+ };
388
+
389
+ const handleReopenOTPModal = () => {
390
+ const dynamicHandlers = createDynamicOTPHandlers(phoneNumber);
391
+
392
+ launchOtpVerificationModal({
393
+ otpLength: 5,
394
+ obscureText: false,
395
+ phoneNumber: currentOtpPhoneNumber || phoneNumber,
396
+ expiryMinutes: otpExpiryMinutes,
397
+ onRequestOtp: dynamicHandlers.onRequestOtp,
398
+ onVerify: dynamicHandlers.onVerify,
399
+ onVerificationSuccess: handleOTPVerificationSuccess,
400
+ });
401
+ };
402
+
403
+ const handleDiscardClaim = () => {
404
+ setOtpState(OTPState.NOT_STARTED);
405
+ setPendingClaimData(null);
406
+ setCurrentOtpPhoneNumber('');
407
+ currentPhoneRef.current = '';
408
+ otpManager.clearAllOTPs();
409
+ navigate({
410
+ to: window.getOpenmrsSpaBase() + `home/billing/patient/${patientUuid}/${billUuid}`,
411
+ });
412
+ };
413
+
260
414
  if (visitLoading || diagnosisLoading || providerLoading) {
261
415
  return (
262
416
  <Layer className={styles.loading}>
@@ -288,9 +442,12 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
288
442
  return validationEnabled && errors[fieldName] && (touchedFields[fieldName] || formInitialized);
289
443
  };
290
444
 
445
+ const isFormValid = isValid && packages?.length > 0 && interventions?.length > 0 && selectedLineItems?.length > 0;
446
+ const displayPhoneNumber = currentOtpPhoneNumber || phoneNumber;
447
+
291
448
  return (
292
449
  <FormProvider {...form}>
293
- <Form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
450
+ <Form className={styles.form}>
294
451
  {!selectedLineItems?.length && (
295
452
  <div className={styles.notificationContainer}>
296
453
  <InlineNotification
@@ -303,6 +460,49 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
303
460
  />
304
461
  </div>
305
462
  )}
463
+
464
+ {!displayPhoneNumber && (
465
+ <div className={styles.notificationContainer}>
466
+ <InlineNotification
467
+ kind="warning"
468
+ title={t('noPhoneNumber', 'No Phone Number')}
469
+ subtitle={t(
470
+ 'noPhoneNumberFound',
471
+ 'No phone number found for this patient. OTP verification will not be available.',
472
+ )}
473
+ hideCloseButton={true}
474
+ lowContrast={true}
475
+ className={styles.notification}
476
+ />
477
+ </div>
478
+ )}
479
+
480
+ {otpState === OTPState.REQUESTED && (
481
+ <div className={styles.notificationContainer}>
482
+ <InlineNotification
483
+ kind="info"
484
+ title={t('otpVerificationPending', 'OTP Verification Pending')}
485
+ subtitle={t('otpVerificationPendingMessage', 'Please complete OTP verification to process the claim')}
486
+ hideCloseButton={true}
487
+ lowContrast={true}
488
+ className={styles.notification}
489
+ />
490
+ </div>
491
+ )}
492
+
493
+ {otpState === OTPState.VERIFIED && (
494
+ <div className={styles.notificationContainer}>
495
+ <InlineNotification
496
+ kind="success"
497
+ title={t('otpVerified', 'OTP Verified')}
498
+ subtitle={t('otpVerifiedReadyToProcess', 'OTP has been verified. Click "Process Claim" to submit.')}
499
+ hideCloseButton={true}
500
+ lowContrast={true}
501
+ className={styles.notification}
502
+ />
503
+ </div>
504
+ )}
505
+
306
506
  <Stack gap={4} className={styles.grid}>
307
507
  <span className={styles.claimFormTitle}>{t('formTitle', 'Fill in the form details')}</span>
308
508
  <Row className={styles.formClaimRow}>
@@ -440,27 +640,45 @@ const ClaimsForm: React.FC<ClaimsFormProps> = ({ bill, selectedLineItems }) => {
440
640
  validationEnabled={validationEnabled}
441
641
  onInteraction={() => setValidationEnabled(true)}
442
642
  />
643
+
443
644
  <ButtonSet className={styles.buttonSet}>
444
- <Button className={styles.button} kind="secondary" onClick={handleNavigateToBillingOptions}>
645
+ <Button className={styles.button} kind="secondary" onClick={handleDiscardClaim}>
445
646
  {t('discardClaim', 'Discard Claim')}
446
647
  </Button>
447
- <Button
448
- className={styles.button}
449
- kind="primary"
450
- type="submit"
451
- onClick={() => setValidationEnabled(true)}
452
- disabled={
453
- loading || !isValid || !packages?.length || !interventions?.length || !selectedLineItems?.length
454
- }
455
- tooltipPosition="top"
456
- tooltipAlignment="center"
457
- renderIcon={loading ? InlineLoading : undefined}>
458
- {loading ? (
459
- <InlineLoading description={t('processing', 'Processing...')} />
460
- ) : (
461
- <>{t('processClaim', 'Process Claim')}</>
462
- )}
463
- </Button>
648
+
649
+ {otpState === OTPState.NOT_STARTED && (
650
+ <Button
651
+ className={styles.button}
652
+ kind="primary"
653
+ onClick={handleSubmit(handleInitiateOTPVerification)}
654
+ disabled={!isFormValid || !displayPhoneNumber}
655
+ tooltipPosition="top"
656
+ tooltipAlignment="center">
657
+ {t('sendOtp', 'Send OTP')}
658
+ </Button>
659
+ )}
660
+
661
+ {otpState === OTPState.REQUESTED && (
662
+ <Button className={styles.button} kind="ghost" onClick={handleReopenOTPModal}>
663
+ {t('enterOtp', 'Enter OTP')}
664
+ </Button>
665
+ )}
666
+
667
+ {otpState === OTPState.VERIFIED && (
668
+ <Button
669
+ className={styles.button}
670
+ kind="primary"
671
+ onClick={handleProcessVerifiedClaim}
672
+ disabled={!pendingClaimData || loading}
673
+ tooltipPosition="top"
674
+ tooltipAlignment="center">
675
+ {loading ? (
676
+ <InlineLoading description={t('processing', 'Processing claim...')} />
677
+ ) : (
678
+ t('processClaim', 'Process Claim')
679
+ )}
680
+ </Button>
681
+ )}
464
682
  </ButtonSet>
465
683
  </Stack>
466
684
  </Form>
@@ -35,6 +35,7 @@ export interface BillingConfig {
35
35
  duration: number;
36
36
  };
37
37
  localeCurrencyMapping: Record<string, string>;
38
+ phoneNumberAttributeTypeUUID: string;
38
39
  }
39
40
 
40
41
  export const configSchema: ConfigSchema = {
@@ -68,6 +69,11 @@ export const configSchema: ConfigSchema = {
68
69
  _description: 'The base url that will be used to make any backend calls related to mpesa.',
69
70
  _default: 'https://billing.kenyahmis.org',
70
71
  },
72
+ phoneNumberAttributeTypeUUID: {
73
+ _type: Type.String,
74
+ _description: 'The person attribute type uuid for phone number',
75
+ _default: '78b4630d-3446-4db2-b570-2e553231a589',
76
+ },
71
77
  hieBaseUrl: {
72
78
  _type: Type.String,
73
79
  _description: 'HIE Base URL for getting interventions and benefit packages',