diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 9ff584e3a882..7eacacec6678 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -38,7 +38,18 @@ import { isMoneyRequestReport, isSettled, } from './ReportUtils'; -import {getAmount as getTransactionAmount, getCreated as getTransactionCreatedDate, getMerchant as getTransactionMerchant, isExpensifyCardTransaction, isPending} from './TransactionUtils'; +import { + getMerchant, + getAmount as getTransactionAmount, + getCreated as getTransactionCreatedDate, + getMerchant as getTransactionMerchant, + isAmountMissing, + isExpensifyCardTransaction, + isPartialMerchant, + isPending, + isReceiptBeingScanned, + isScanRequest, +} from './TransactionUtils'; const columnNamesToSortingProperty = { [CONST.SEARCH.TABLE_COLUMNS.TO]: 'formattedTo' as const, @@ -347,10 +358,14 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr if (canIOUBePaid(report, chatReport, policy, allReportTransactions, false, chatReportRNVP, invoiceReceiverPolicy) && !hasOnlyHeldExpenses(report.reportID, allReportTransactions)) { return CONST.SEARCH.ACTION_TYPES.PAY; } - const hasOnlyPendingTransactions = allReportTransactions.length > 0 && allReportTransactions.every((t) => isExpensifyCardTransaction(t) && isPending(t)); + const hasOnlyPendingCardOrScanningTransactions = + allReportTransactions.length > 0 && + allReportTransactions.every( + (t) => (isExpensifyCardTransaction(t) && isPending(t)) || (isPartialMerchant(getMerchant(t)) && isAmountMissing(t)) || (isScanRequest(t) && isReceiptBeingScanned(t)), + ); const isAllowedToApproveExpenseReport = isAllowedToApproveExpenseReportUtils(report, undefined, policy); - if (canApproveIOU(report, policy) && isAllowedToApproveExpenseReport && !hasOnlyPendingTransactions) { + if (canApproveIOU(report, policy) && isAllowedToApproveExpenseReport && !hasOnlyPendingCardOrScanningTransactions) { return CONST.SEARCH.ACTION_TYPES.APPROVE; } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 3a93e929b681..466b27876a91 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -8039,20 +8039,21 @@ function canApproveIOU( const iouSettled = isSettled(iouReport?.reportID); const reportNameValuePairs = chatReportRNVP ?? getReportNameValuePairs(iouReport?.reportID); const isArchivedExpenseReport = isArchivedReport(reportNameValuePairs); - let isTransactionBeingScanned = false; const reportTransactions = getReportTransactions(iouReport?.reportID); - for (const transaction of reportTransactions) { - const hasReceipt = hasReceiptTransactionUtils(transaction); - const isReceiptBeingScanned = isReceiptBeingScannedTransactionUtils(transaction); - - // If transaction has receipt (scan) and its receipt is being scanned, we shouldn't be able to Approve - if (hasReceipt && isReceiptBeingScanned) { - isTransactionBeingScanned = true; - } + const hasOnlyPendingCardOrScanningTransactions = + reportTransactions.length > 0 && + reportTransactions.every( + (transaction) => + (isExpensifyCardTransaction(transaction) && isPending(transaction)) || + (isPartialMerchant(getMerchant(transaction)) && isAmountMissing(transaction)) || + (isScanRequestTransactionUtils(transaction) && isReceiptBeingScannedTransactionUtils(transaction)), + ); + if (hasOnlyPendingCardOrScanningTransactions) { + return false; } const isPayAtEndExpenseReport = isPayAtEndExpenseReportReportUtils(iouReport?.reportID, reportTransactions); - return isCurrentUserManager && !isOpenExpenseReport && !isApproved && !iouSettled && !isArchivedExpenseReport && !isTransactionBeingScanned && !isPayAtEndExpenseReport; + return reportTransactions.length > 0 && isCurrentUserManager && !isOpenExpenseReport && !isApproved && !iouSettled && !isArchivedExpenseReport && !isPayAtEndExpenseReport; } function canIOUBePaid( diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 7f75d31baec2..50ea7040f1e1 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -3,6 +3,7 @@ import isEqual from 'lodash/isEqual'; import type {OnyxCollection, OnyxEntry, OnyxInputValue} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import { + canApproveIOU, cancelPayment, deleteMoneyRequest, payMoneyRequest, @@ -4472,4 +4473,180 @@ describe('actions/IOU', () => { }); }); }); + + describe('canApproveIOU', () => { + it('should return false if we have only pending card transactions', async () => { + const policyID = '2'; + const reportID = '1'; + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + }; + const fakeReport: Report = { + ...createRandomReport(Number(reportID)), + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + }; + const fakeTransaction1: Transaction = { + ...createRandomTransaction(0), + reportID, + bank: CONST.EXPENSIFY_CARD.BANK, + status: CONST.TRANSACTION.STATUS.PENDING, + }; + const fakeTransaction2: Transaction = { + ...createRandomTransaction(1), + reportID, + bank: CONST.EXPENSIFY_CARD.BANK, + status: CONST.TRANSACTION.STATUS.PENDING, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${fakeReport.reportID}`, fakeReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction1.transactionID}`, fakeTransaction1); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction2.transactionID}`, fakeTransaction2); + + await waitForBatchedUpdates(); + + expect(canApproveIOU(fakeReport, fakePolicy)).toBeFalsy(); + }); + it('should return false if we have only scan failure transactions', async () => { + const policyID = '2'; + const reportID = '1'; + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + }; + const fakeReport: Report = { + ...createRandomReport(Number(reportID)), + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + managerID: RORY_ACCOUNT_ID, + }; + const fakeTransaction1: Transaction = { + ...createRandomTransaction(0), + reportID, + amount: 0, + modifiedAmount: 0, + receipt: { + state: CONST.IOU.RECEIPT_STATE.SCANFAILED, + }, + merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, + modifiedMerchant: undefined, + }; + const fakeTransaction2: Transaction = { + ...createRandomTransaction(1), + reportID, + amount: 0, + modifiedAmount: 0, + receipt: { + state: CONST.IOU.RECEIPT_STATE.SCANFAILED, + }, + merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, + modifiedMerchant: undefined, + }; + + await Onyx.set(ONYXKEYS.COLLECTION.REPORT, { + [`${ONYXKEYS.COLLECTION.REPORT}${fakeReport.reportID}`]: fakeReport, + }); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${fakeReport.reportID}`, fakeReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction1.transactionID}`, fakeTransaction1); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction2.transactionID}`, fakeTransaction2); + + await waitForBatchedUpdates(); + + expect(canApproveIOU(fakeReport, fakePolicy)).toBeFalsy(); + }); + it('should return false if all transactions are pending card or scan failure transaction', async () => { + const policyID = '2'; + const reportID = '1'; + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + }; + const fakeReport: Report = { + ...createRandomReport(Number(reportID)), + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + managerID: RORY_ACCOUNT_ID, + }; + const fakeTransaction1: Transaction = { + ...createRandomTransaction(0), + reportID, + bank: CONST.EXPENSIFY_CARD.BANK, + status: CONST.TRANSACTION.STATUS.PENDING, + }; + const fakeTransaction2: Transaction = { + ...createRandomTransaction(1), + reportID, + amount: 0, + modifiedAmount: 0, + receipt: { + state: CONST.IOU.RECEIPT_STATE.SCANFAILED, + }, + merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, + modifiedMerchant: undefined, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${fakeReport.reportID}`, fakeReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction1.transactionID}`, fakeTransaction1); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction2.transactionID}`, fakeTransaction2); + + await waitForBatchedUpdates(); + + expect(canApproveIOU(fakeReport, fakePolicy)).toBeFalsy(); + }); + it('should return true if at least one transactions is not pending card or scan failure transaction', async () => { + const policyID = '2'; + const reportID = '1'; + const fakePolicy: Policy = { + ...createRandomPolicy(Number(policyID)), + type: CONST.POLICY.TYPE.TEAM, + approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, + }; + const fakeReport: Report = { + ...createRandomReport(Number(reportID)), + type: CONST.REPORT.TYPE.EXPENSE, + policyID, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + managerID: RORY_ACCOUNT_ID, + }; + const fakeTransaction1: Transaction = { + ...createRandomTransaction(0), + reportID, + bank: CONST.EXPENSIFY_CARD.BANK, + status: CONST.TRANSACTION.STATUS.PENDING, + }; + const fakeTransaction2: Transaction = { + ...createRandomTransaction(1), + reportID, + amount: 0, + receipt: { + state: CONST.IOU.RECEIPT_STATE.SCANFAILED, + }, + merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, + modifiedMerchant: undefined, + }; + const fakeTransaction3: Transaction = { + ...createRandomTransaction(2), + reportID, + amount: 100, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${fakeReport.reportID}`, fakeReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction1.transactionID}`, fakeTransaction1); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction2.transactionID}`, fakeTransaction2); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction3.transactionID}`, fakeTransaction3); + + await waitForBatchedUpdates(); + + expect(canApproveIOU(fakeReport, fakePolicy)).toBeTruthy(); + }); + }); }); diff --git a/tests/unit/DebugUtilsTest.ts b/tests/unit/DebugUtilsTest.ts index 5c414d59c036..3e15952bb26e 100644 --- a/tests/unit/DebugUtilsTest.ts +++ b/tests/unit/DebugUtilsTest.ts @@ -1069,6 +1069,11 @@ describe('DebugUtils', () => { approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC, type: CONST.POLICY.TYPE.CORPORATE, }, + [`${ONYXKEYS.COLLECTION.TRANSACTION}1` as const]: { + amount: -100, + currency: CONST.CURRENCY.USD, + reportID: '2', + }, [ONYXKEYS.SESSION]: { accountID: 12345, },