diff --git a/src/libs/ReportPreviewActionUtils.ts b/src/libs/ReportPreviewActionUtils.ts index bc13ac1d91977..38ade57e848e2 100644 --- a/src/libs/ReportPreviewActionUtils.ts +++ b/src/libs/ReportPreviewActionUtils.ts @@ -20,7 +20,7 @@ import { isReportApproved, isSettled, } from './ReportUtils'; -import {hasSubmissionBlockingViolations, isPending, isScanning} from './TransactionUtils'; +import {hasSmartScanFailedWithMissingFields, hasSubmissionBlockingViolations, isPending, isScanning} from './TransactionUtils'; function canSubmit( report: Report, @@ -47,6 +47,10 @@ function canSubmit( const isAnyReceiptBeingScanned = transactions?.some((transaction) => isScanning(transaction)); + if (hasSmartScanFailedWithMissingFields(transactions ?? [], report)) { + return false; + } + if (transactions?.some((transaction) => hasSubmissionBlockingViolations(transaction, violations, currentUserEmail, currentUserAccountID, report, policy))) { return false; } diff --git a/src/libs/ReportPrimaryActionUtils.ts b/src/libs/ReportPrimaryActionUtils.ts index 2bea12909a348..2cf13ba5e067c 100644 --- a/src/libs/ReportPrimaryActionUtils.ts +++ b/src/libs/ReportPrimaryActionUtils.ts @@ -49,6 +49,7 @@ import { allHavePendingRTERViolation, getTransactionViolations, hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils, + hasSmartScanFailedWithMissingFields, hasSubmissionBlockingViolations, isDuplicate, isOnHold as isOnHoldTransactionUtils, @@ -121,6 +122,10 @@ function isSubmitAction( return false; } + if (hasSmartScanFailedWithMissingFields(reportTransactions ?? [], report)) { + return false; + } + if (violations && currentUserEmail && currentUserAccountID !== undefined) { if (reportTransactions.some((transaction) => hasSubmissionBlockingViolations(transaction, violations, currentUserEmail, currentUserAccountID, report, policy))) { return false; diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index fb115669cb11e..127be89f34f26 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -68,6 +68,7 @@ import { allHavePendingRTERViolation, getOriginalTransactionWithSplitInfo, hasReceipt as hasReceiptTransactionUtils, + hasSmartScanFailedWithMissingFields, hasSubmissionBlockingViolations, isDuplicate, isManagedCardTransaction as isManagedCardTransactionTransactionUtils, @@ -210,6 +211,10 @@ function isSubmitAction({ return false; } + if (hasSmartScanFailedWithMissingFields(reportTransactions ?? [], report)) { + return false; + } + if (violations && currentUserLogin && currentUserAccountID !== undefined) { if (reportTransactions.some((transaction) => hasSubmissionBlockingViolations(transaction, violations, currentUserLogin, currentUserAccountID, report, policy))) { return false; diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 53e5e85daffe7..3d7a74509afd6 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -2760,6 +2760,15 @@ function shouldReuseInitialTransaction( return !isMultiScanEnabled || (transactions.length === 1 && (!initialTransaction.receipt?.source || initialTransaction.receipt?.isTestReceipt === true)); } +/** + * Check if the transaction has a smartscan failed with missing fields before violation is written + */ +function hasSmartScanFailedWithMissingFields(transactions: Transaction[], report: OnyxEntry): boolean { + return transactions.some( + (transaction) => isScanRequest(transaction) && transaction?.receipt?.state === CONST.IOU.RECEIPT_STATE.SCAN_FAILED && hasMissingSmartscanFields(transaction, report), + ); +} + export { buildOptimisticTransaction, calculateTaxAmount, @@ -2897,6 +2906,7 @@ export { isTimeRequest, getExpenseTypeTranslationKey, isDistanceTypeRequest, + hasSmartScanFailedWithMissingFields, }; export type {TransactionChanges}; diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index f6e92b2b67641..50c5a7344a36d 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -211,6 +211,7 @@ import { getWaypoints, hasAnyTransactionWithoutRTERViolation, hasDuplicateTransactions, + hasSmartScanFailedWithMissingFields, hasSubmissionBlockingViolations, isCustomUnitRateIDForP2P, isDistanceRequest as isDistanceRequestTransactionUtils, @@ -10550,6 +10551,7 @@ function canSubmitReport( hasTransactionWithoutRTERViolation && !isReportArchived && !hasAnySubmissionBlockingViolations && + !hasSmartScanFailedWithMissingFields(transactions, report) && transactions.length > 0 ); } diff --git a/tests/actions/ReportPreviewActionUtilsTest.ts b/tests/actions/ReportPreviewActionUtilsTest.ts index 4c1b2621ecf74..8adb35182477a 100644 --- a/tests/actions/ReportPreviewActionUtilsTest.ts +++ b/tests/actions/ReportPreviewActionUtilsTest.ts @@ -276,6 +276,51 @@ describe('getReportPreviewAction', () => { ).toBe(CONST.REPORT.REPORT_PREVIEW_ACTIONS.VIEW); }); + it('canSubmit should return false for expense preview when smartscan failed with missing fields (before violation is written)', async () => { + const TRANSACTION_ID = 'TRANSACTION_ID'; + const report: Report = { + ...createRandomReport(REPORT_ID, undefined), + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + isWaitingOnBankAccount: false, + }; + + const policy = createRandomPolicy(0); + policy.autoReportingFrequency = CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE; + policy.type = CONST.POLICY.TYPE.CORPORATE; + if (policy.harvesting) { + policy.harvesting.enabled = false; + } + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + const transaction = { + transactionID: TRANSACTION_ID, + reportID: `${REPORT_ID}`, + iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, + receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED}, + merchant: '', + } as unknown as Transaction; + + const {result: isReportArchived} = renderHook(() => useReportIsArchived(report?.parentReportID)); + + await waitForBatchedUpdatesWithAct(); + + expect( + getReportPreviewAction({ + isReportArchived: isReportArchived.current, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + currentUserLogin: CURRENT_USER_EMAIL, + report, + policy, + transactions: [transaction], + bankAccountList: {}, + reportMetadata: undefined, + }), + ).toBe(CONST.REPORT.REPORT_PREVIEW_ACTIONS.VIEW); + }); + describe('canApprove', () => { it('should return true for report being processed', async () => { const report = { diff --git a/tests/unit/IOUUtilsTest.ts b/tests/unit/IOUUtilsTest.ts index 81066b2fc6458..0412407efa4b1 100644 --- a/tests/unit/IOUUtilsTest.ts +++ b/tests/unit/IOUUtilsTest.ts @@ -594,6 +594,39 @@ describe('canSubmitReport', () => { const {result: isReportArchived} = renderHook(() => useReportIsArchived(report?.reportID)); expect(canSubmitReport(report, policy, [], undefined, isReportArchived.current, '', currentUserAccountID)).toBe(false); }); + + it('returns false when SmartScan failed with missing fields before violation is written', async () => { + await Onyx.merge(ONYXKEYS.SESSION, {accountID: currentUserAccountID}); + const policy: Policy = { + ...createRandomPolicy(8), + ownerAccountID: currentUserAccountID, + areRulesEnabled: true, + preventSelfApproval: false, + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE, + harvesting: {enabled: false}, + }; + const report: Report = { + ...createRandomReport(8, undefined), + type: CONST.REPORT.TYPE.EXPENSE, + managerID: currentUserAccountID, + ownerAccountID: currentUserAccountID, + policyID: policy.id, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + }; + + const transaction: Transaction = { + ...createRandomTransaction(1), + reportID: report.reportID, + iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, + receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED}, + merchant: 'Coffee', + created: '', + amount: 100, + }; + + expect(canSubmitReport(report, policy, [transaction], undefined, false, '', currentUserAccountID)).toBe(false); + }); }); describe('Check valid amount for IOU/Expense request', () => { diff --git a/tests/unit/ReportPrimaryActionUtilsTest.ts b/tests/unit/ReportPrimaryActionUtilsTest.ts index 82b8b953f0341..a227b69dc40b0 100644 --- a/tests/unit/ReportPrimaryActionUtilsTest.ts +++ b/tests/unit/ReportPrimaryActionUtilsTest.ts @@ -931,6 +931,42 @@ describe('getPrimaryAction', () => { ).toBe(''); }); + it('should not return SUBMIT when smartscan failed with missing fields before violation is written', async () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: CURRENT_USER_ACCOUNT_ID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + } as unknown as Report; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + const policy = { + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE, + }; + const TRANSACTION_ID = 'TRANSACTION_ID'; + const transaction = { + transactionID: TRANSACTION_ID, + reportID: `${REPORT_ID}`, + iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, + receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED}, + merchant: '', + } as unknown as Transaction; + + expect( + getReportPrimaryAction({ + currentUserLogin: CURRENT_USER_EMAIL, + currentUserAccountID: CURRENT_USER_ACCOUNT_ID, + report, + chatReport, + reportTransactions: [transaction], + violations: {}, + bankAccountList: {}, + policy: policy as Policy, + isChatReportArchived: false, + }), + ).toBe(''); + }); + it('should return an empty string for invoice report when the chat report is archived', async () => { // Given the invoice data const {policy, convertedInvoiceChat: invoiceChatReport}: InvoiceTestData = InvoiceData; diff --git a/tests/unit/ReportSecondaryActionUtilsTest.ts b/tests/unit/ReportSecondaryActionUtilsTest.ts index 2a03ce4c6df2b..4ae9df9cfef53 100644 --- a/tests/unit/ReportSecondaryActionUtilsTest.ts +++ b/tests/unit/ReportSecondaryActionUtilsTest.ts @@ -425,6 +425,46 @@ describe('getSecondaryAction', () => { expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.SUBMIT)).toBe(false); }); + it('should not include SUBMIT option when smartscan failed with missing fields before violation is written', async () => { + const report = { + reportID: REPORT_ID, + type: CONST.REPORT.TYPE.EXPENSE, + ownerAccountID: EMPLOYEE_ACCOUNT_ID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + total: 10, + } as unknown as Report; + const policy = { + autoReportingFrequency: CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT, + harvesting: { + enabled: true, + }, + } as unknown as Policy; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report); + + const TRANSACTION_ID = 'TRANSACTION_ID'; + const transaction = { + transactionID: TRANSACTION_ID, + reportID: `${REPORT_ID}`, + iouRequestType: CONST.IOU.REQUEST_TYPE.SCAN, + receipt: {state: CONST.IOU.RECEIPT_STATE.SCAN_FAILED}, + merchant: '', + } as unknown as Transaction; + + const result = getSecondaryReportActions({ + currentUserLogin: EMPLOYEE_EMAIL, + currentUserAccountID: EMPLOYEE_ACCOUNT_ID, + report, + chatReport, + reportTransactions: [transaction], + originalTransaction: {} as Transaction, + violations: {}, + bankAccountList: {}, + policy, + }); + expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.SUBMIT)).toBe(false); + }); + it('includes APPROVE option for approver and report with duplicates', async () => { const report = { reportID: REPORT_ID,