Skip to content

Commit b10a56a

Browse files
authored
Merge pull request #55345 from nkdengineer/fix/54996
Prevent approving in expense report only has pending card/scan failure transactions
2 parents 63afa93 + ac46491 commit b10a56a

File tree

4 files changed

+211
-13
lines changed

4 files changed

+211
-13
lines changed

src/libs/SearchUIUtils.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,18 @@ import {
3838
isMoneyRequestReport,
3939
isSettled,
4040
} from './ReportUtils';
41-
import {getAmount as getTransactionAmount, getCreated as getTransactionCreatedDate, getMerchant as getTransactionMerchant, isExpensifyCardTransaction, isPending} from './TransactionUtils';
41+
import {
42+
getMerchant,
43+
getAmount as getTransactionAmount,
44+
getCreated as getTransactionCreatedDate,
45+
getMerchant as getTransactionMerchant,
46+
isAmountMissing,
47+
isExpensifyCardTransaction,
48+
isPartialMerchant,
49+
isPending,
50+
isReceiptBeingScanned,
51+
isScanRequest,
52+
} from './TransactionUtils';
4253

4354
const columnNamesToSortingProperty = {
4455
[CONST.SEARCH.TABLE_COLUMNS.TO]: 'formattedTo' as const,
@@ -347,10 +358,14 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr
347358
if (canIOUBePaid(report, chatReport, policy, allReportTransactions, false, chatReportRNVP, invoiceReceiverPolicy) && !hasOnlyHeldExpenses(report.reportID, allReportTransactions)) {
348359
return CONST.SEARCH.ACTION_TYPES.PAY;
349360
}
350-
const hasOnlyPendingTransactions = allReportTransactions.length > 0 && allReportTransactions.every((t) => isExpensifyCardTransaction(t) && isPending(t));
361+
const hasOnlyPendingCardOrScanningTransactions =
362+
allReportTransactions.length > 0 &&
363+
allReportTransactions.every(
364+
(t) => (isExpensifyCardTransaction(t) && isPending(t)) || (isPartialMerchant(getMerchant(t)) && isAmountMissing(t)) || (isScanRequest(t) && isReceiptBeingScanned(t)),
365+
);
351366

352367
const isAllowedToApproveExpenseReport = isAllowedToApproveExpenseReportUtils(report, undefined, policy);
353-
if (canApproveIOU(report, policy) && isAllowedToApproveExpenseReport && !hasOnlyPendingTransactions) {
368+
if (canApproveIOU(report, policy) && isAllowedToApproveExpenseReport && !hasOnlyPendingCardOrScanningTransactions) {
354369
return CONST.SEARCH.ACTION_TYPES.APPROVE;
355370
}
356371

src/libs/actions/IOU.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8039,20 +8039,21 @@ function canApproveIOU(
80398039
const iouSettled = isSettled(iouReport?.reportID);
80408040
const reportNameValuePairs = chatReportRNVP ?? getReportNameValuePairs(iouReport?.reportID);
80418041
const isArchivedExpenseReport = isArchivedReport(reportNameValuePairs);
8042-
let isTransactionBeingScanned = false;
80438042
const reportTransactions = getReportTransactions(iouReport?.reportID);
8044-
for (const transaction of reportTransactions) {
8045-
const hasReceipt = hasReceiptTransactionUtils(transaction);
8046-
const isReceiptBeingScanned = isReceiptBeingScannedTransactionUtils(transaction);
8047-
8048-
// If transaction has receipt (scan) and its receipt is being scanned, we shouldn't be able to Approve
8049-
if (hasReceipt && isReceiptBeingScanned) {
8050-
isTransactionBeingScanned = true;
8051-
}
8043+
const hasOnlyPendingCardOrScanningTransactions =
8044+
reportTransactions.length > 0 &&
8045+
reportTransactions.every(
8046+
(transaction) =>
8047+
(isExpensifyCardTransaction(transaction) && isPending(transaction)) ||
8048+
(isPartialMerchant(getMerchant(transaction)) && isAmountMissing(transaction)) ||
8049+
(isScanRequestTransactionUtils(transaction) && isReceiptBeingScannedTransactionUtils(transaction)),
8050+
);
8051+
if (hasOnlyPendingCardOrScanningTransactions) {
8052+
return false;
80528053
}
80538054
const isPayAtEndExpenseReport = isPayAtEndExpenseReportReportUtils(iouReport?.reportID, reportTransactions);
80548055

8055-
return isCurrentUserManager && !isOpenExpenseReport && !isApproved && !iouSettled && !isArchivedExpenseReport && !isTransactionBeingScanned && !isPayAtEndExpenseReport;
8056+
return reportTransactions.length > 0 && isCurrentUserManager && !isOpenExpenseReport && !isApproved && !iouSettled && !isArchivedExpenseReport && !isPayAtEndExpenseReport;
80568057
}
80578058

80588059
function canIOUBePaid(

tests/actions/IOUTest.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import isEqual from 'lodash/isEqual';
33
import type {OnyxCollection, OnyxEntry, OnyxInputValue} from 'react-native-onyx';
44
import Onyx from 'react-native-onyx';
55
import {
6+
canApproveIOU,
67
cancelPayment,
78
deleteMoneyRequest,
89
payMoneyRequest,
@@ -4472,4 +4473,180 @@ describe('actions/IOU', () => {
44724473
});
44734474
});
44744475
});
4476+
4477+
describe('canApproveIOU', () => {
4478+
it('should return false if we have only pending card transactions', async () => {
4479+
const policyID = '2';
4480+
const reportID = '1';
4481+
const fakePolicy: Policy = {
4482+
...createRandomPolicy(Number(policyID)),
4483+
type: CONST.POLICY.TYPE.TEAM,
4484+
approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC,
4485+
};
4486+
const fakeReport: Report = {
4487+
...createRandomReport(Number(reportID)),
4488+
type: CONST.REPORT.TYPE.EXPENSE,
4489+
policyID,
4490+
};
4491+
const fakeTransaction1: Transaction = {
4492+
...createRandomTransaction(0),
4493+
reportID,
4494+
bank: CONST.EXPENSIFY_CARD.BANK,
4495+
status: CONST.TRANSACTION.STATUS.PENDING,
4496+
};
4497+
const fakeTransaction2: Transaction = {
4498+
...createRandomTransaction(1),
4499+
reportID,
4500+
bank: CONST.EXPENSIFY_CARD.BANK,
4501+
status: CONST.TRANSACTION.STATUS.PENDING,
4502+
};
4503+
4504+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${fakeReport.reportID}`, fakeReport);
4505+
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction1.transactionID}`, fakeTransaction1);
4506+
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction2.transactionID}`, fakeTransaction2);
4507+
4508+
await waitForBatchedUpdates();
4509+
4510+
expect(canApproveIOU(fakeReport, fakePolicy)).toBeFalsy();
4511+
});
4512+
it('should return false if we have only scan failure transactions', async () => {
4513+
const policyID = '2';
4514+
const reportID = '1';
4515+
const fakePolicy: Policy = {
4516+
...createRandomPolicy(Number(policyID)),
4517+
type: CONST.POLICY.TYPE.TEAM,
4518+
approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC,
4519+
};
4520+
const fakeReport: Report = {
4521+
...createRandomReport(Number(reportID)),
4522+
type: CONST.REPORT.TYPE.EXPENSE,
4523+
policyID,
4524+
stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
4525+
statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
4526+
managerID: RORY_ACCOUNT_ID,
4527+
};
4528+
const fakeTransaction1: Transaction = {
4529+
...createRandomTransaction(0),
4530+
reportID,
4531+
amount: 0,
4532+
modifiedAmount: 0,
4533+
receipt: {
4534+
state: CONST.IOU.RECEIPT_STATE.SCANFAILED,
4535+
},
4536+
merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT,
4537+
modifiedMerchant: undefined,
4538+
};
4539+
const fakeTransaction2: Transaction = {
4540+
...createRandomTransaction(1),
4541+
reportID,
4542+
amount: 0,
4543+
modifiedAmount: 0,
4544+
receipt: {
4545+
state: CONST.IOU.RECEIPT_STATE.SCANFAILED,
4546+
},
4547+
merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT,
4548+
modifiedMerchant: undefined,
4549+
};
4550+
4551+
await Onyx.set(ONYXKEYS.COLLECTION.REPORT, {
4552+
[`${ONYXKEYS.COLLECTION.REPORT}${fakeReport.reportID}`]: fakeReport,
4553+
});
4554+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${fakeReport.reportID}`, fakeReport);
4555+
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction1.transactionID}`, fakeTransaction1);
4556+
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction2.transactionID}`, fakeTransaction2);
4557+
4558+
await waitForBatchedUpdates();
4559+
4560+
expect(canApproveIOU(fakeReport, fakePolicy)).toBeFalsy();
4561+
});
4562+
it('should return false if all transactions are pending card or scan failure transaction', async () => {
4563+
const policyID = '2';
4564+
const reportID = '1';
4565+
const fakePolicy: Policy = {
4566+
...createRandomPolicy(Number(policyID)),
4567+
type: CONST.POLICY.TYPE.TEAM,
4568+
approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC,
4569+
};
4570+
const fakeReport: Report = {
4571+
...createRandomReport(Number(reportID)),
4572+
type: CONST.REPORT.TYPE.EXPENSE,
4573+
policyID,
4574+
stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
4575+
statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
4576+
managerID: RORY_ACCOUNT_ID,
4577+
};
4578+
const fakeTransaction1: Transaction = {
4579+
...createRandomTransaction(0),
4580+
reportID,
4581+
bank: CONST.EXPENSIFY_CARD.BANK,
4582+
status: CONST.TRANSACTION.STATUS.PENDING,
4583+
};
4584+
const fakeTransaction2: Transaction = {
4585+
...createRandomTransaction(1),
4586+
reportID,
4587+
amount: 0,
4588+
modifiedAmount: 0,
4589+
receipt: {
4590+
state: CONST.IOU.RECEIPT_STATE.SCANFAILED,
4591+
},
4592+
merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT,
4593+
modifiedMerchant: undefined,
4594+
};
4595+
4596+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${fakeReport.reportID}`, fakeReport);
4597+
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction1.transactionID}`, fakeTransaction1);
4598+
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction2.transactionID}`, fakeTransaction2);
4599+
4600+
await waitForBatchedUpdates();
4601+
4602+
expect(canApproveIOU(fakeReport, fakePolicy)).toBeFalsy();
4603+
});
4604+
it('should return true if at least one transactions is not pending card or scan failure transaction', async () => {
4605+
const policyID = '2';
4606+
const reportID = '1';
4607+
const fakePolicy: Policy = {
4608+
...createRandomPolicy(Number(policyID)),
4609+
type: CONST.POLICY.TYPE.TEAM,
4610+
approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC,
4611+
};
4612+
const fakeReport: Report = {
4613+
...createRandomReport(Number(reportID)),
4614+
type: CONST.REPORT.TYPE.EXPENSE,
4615+
policyID,
4616+
stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
4617+
statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
4618+
managerID: RORY_ACCOUNT_ID,
4619+
};
4620+
const fakeTransaction1: Transaction = {
4621+
...createRandomTransaction(0),
4622+
reportID,
4623+
bank: CONST.EXPENSIFY_CARD.BANK,
4624+
status: CONST.TRANSACTION.STATUS.PENDING,
4625+
};
4626+
const fakeTransaction2: Transaction = {
4627+
...createRandomTransaction(1),
4628+
reportID,
4629+
amount: 0,
4630+
receipt: {
4631+
state: CONST.IOU.RECEIPT_STATE.SCANFAILED,
4632+
},
4633+
merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT,
4634+
modifiedMerchant: undefined,
4635+
};
4636+
const fakeTransaction3: Transaction = {
4637+
...createRandomTransaction(2),
4638+
reportID,
4639+
amount: 100,
4640+
};
4641+
4642+
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${fakeReport.reportID}`, fakeReport);
4643+
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction1.transactionID}`, fakeTransaction1);
4644+
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction2.transactionID}`, fakeTransaction2);
4645+
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${fakeTransaction3.transactionID}`, fakeTransaction3);
4646+
4647+
await waitForBatchedUpdates();
4648+
4649+
expect(canApproveIOU(fakeReport, fakePolicy)).toBeTruthy();
4650+
});
4651+
});
44754652
});

tests/unit/DebugUtilsTest.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,11 @@ describe('DebugUtils', () => {
10691069
approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC,
10701070
type: CONST.POLICY.TYPE.CORPORATE,
10711071
},
1072+
[`${ONYXKEYS.COLLECTION.TRANSACTION}1` as const]: {
1073+
amount: -100,
1074+
currency: CONST.CURRENCY.USD,
1075+
reportID: '2',
1076+
},
10721077
[ONYXKEYS.SESSION]: {
10731078
accountID: 12345,
10741079
},

0 commit comments

Comments
 (0)