Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent approving in expense report only has pending card/scan failure transactions #55345

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,16 @@ 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,
} from './TransactionUtils';

const columnNamesToSortingProperty = {
[CONST.SEARCH.TABLE_COLUMNS.TO]: 'formattedTo' as const,
Expand Down Expand Up @@ -332,10 +341,11 @@ 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 hasOnlyPendingCardOrScanFailTransactions =
allReportTransactions.length > 0 && allReportTransactions.every((t) => (isExpensifyCardTransaction(t) && isPending(t)) || (isPartialMerchant(getMerchant(t)) && isAmountMissing(t)));

const isAllowedToApproveExpenseReport = isAllowedToApproveExpenseReportUtils(report, undefined, policy);
if (canApproveIOU(report, policy) && isAllowedToApproveExpenseReport && !hasOnlyPendingTransactions) {
if (canApproveIOU(report, policy) && isAllowedToApproveExpenseReport && !hasOnlyPendingCardOrScanFailTransactions) {
nkdengineer marked this conversation as resolved.
Show resolved Hide resolved
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
return CONST.SEARCH.ACTION_TYPES.APPROVE;
}

Expand Down
9 changes: 9 additions & 0 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,13 @@ import {
getTransaction,
getUpdatedTransaction,
hasReceipt as hasReceiptTransactionUtils,
isAmountMissing,
isDistanceRequest as isDistanceRequestTransactionUtils,
isExpensifyCardTransaction,
isFetchingWaypointsFromServer,
isOnHold,
isPartialMerchant,
isPending,
isPerDiemRequest as isPerDiemRequestTransactionUtils,
isReceiptBeingScanned as isReceiptBeingScannedTransactionUtils,
isScanRequest as isScanRequestTransactionUtils,
Expand Down Expand Up @@ -7759,6 +7763,11 @@ function canApproveIOU(
const isArchivedExpenseReport = isArchivedReport(iouReport, reportNameValuePairs);
let isTransactionBeingScanned = false;
const reportTransactions = getAllReportTransactions(iouReport?.reportID);
const hasOnlyPendingCardOrScanFailTransactions =
reportTransactions.length > 0 && reportTransactions.every((t) => (isExpensifyCardTransaction(t) && isPending(t)) || (isPartialMerchant(getMerchant(t)) && isAmountMissing(t)));
if (hasOnlyPendingCardOrScanFailTransactions) {
return false;
}
for (const transaction of reportTransactions) {
const hasReceipt = hasReceiptTransactionUtils(transaction);
const isReceiptBeingScanned = isReceiptBeingScannedTransactionUtils(transaction);
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
177 changes: 177 additions & 0 deletions tests/actions/IOUTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -4444,4 +4445,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();
});
});
});
Loading