Skip to content

Commit 69fd95a

Browse files
authored
Merge pull request #90331 from nabi-ebrahimi/fix/prevent-auto-approval-held-expense-multi-violations
fix: prevent automatic unhold and approval of held expenses with multiple violations
2 parents eaaffe3 + 20434e3 commit 69fd95a

3 files changed

Lines changed: 91 additions & 6 deletions

File tree

src/components/MoneyReportHeaderPrimaryAction/useConfirmApproval.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import {useMoneyReportHeaderModals} from '@components/MoneyReportHeaderModalsCon
44
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
55
import useOnyx from '@hooks/useOnyx';
66
import usePermissions from '@hooks/usePermissions';
7+
import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport';
78
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
8-
import {hasHeldExpenses as hasHeldExpensesReportUtils, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
9+
import {hasHeldExpensesFromTransactions as hasHeldExpensesReportUtils, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
910
import {approveMoneyRequest} from '@userActions/IOU/ReportWorkflow';
1011
import CONST from '@src/CONST';
1112
import ONYXKEYS from '@src/ONYXKEYS';
@@ -26,10 +27,11 @@ function useConfirmApproval(reportID: string | undefined, startApprovedAnimation
2627
const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END);
2728
const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
2829
const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector});
30+
const {transactions: reportTransactions} = useTransactionsAndViolationsForReport(moneyRequestReport?.reportID);
2931

3032
const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
3133
const hasViolations = hasViolationsReportUtils(moneyRequestReport?.reportID, allTransactionViolations, accountID, email ?? '');
32-
const isAnyTransactionOnHold = hasHeldExpensesReportUtils(moneyRequestReport?.reportID);
34+
const isAnyTransactionOnHold = hasHeldExpensesReportUtils(Object.values(reportTransactions));
3335

3436
const confirmApproval = () => {
3537
if (isDelegateAccessRestricted) {

src/libs/actions/Transaction.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -629,11 +629,9 @@ function dismissDuplicateTransactionViolation({
629629
onyxMethod: Onyx.METHOD.MERGE,
630630
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`,
631631
value: {
632-
...transaction,
633632
comment: {
634-
...transaction?.comment,
635633
dismissedViolations: {
636-
duplicatedTransaction: {
634+
[CONST.VIOLATIONS.DUPLICATED_TRANSACTION]: {
637635
[dismissedPersonalDetails.login ?? '']: getUnixTime(new Date()),
638636
},
639637
},
@@ -653,7 +651,11 @@ function dismissDuplicateTransactionViolation({
653651
onyxMethod: Onyx.METHOD.MERGE,
654652
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`,
655653
value: {
656-
...transaction,
654+
comment: {
655+
dismissedViolations: {
656+
[CONST.VIOLATIONS.DUPLICATED_TRANSACTION]: transaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.DUPLICATED_TRANSACTION] ?? null,
657+
},
658+
},
657659
},
658660
}));
659661

tests/unit/TransactionTest.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2091,6 +2091,87 @@ describe('Transaction', () => {
20912091
expect(Object.keys(reportActions ?? {}).length).toBe(0);
20922092
});
20932093

2094+
it('should preserve an existing hold when optimistic dismissal is built from a stale transaction snapshot', async () => {
2095+
const transactionID = 'dismissTxnHold';
2096+
const threadReportID = 'threadDismissHold';
2097+
const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}` as const;
2098+
const testerEmail = 'tester@example.com';
2099+
const mockViolations: TransactionViolation[] = [{name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, type: 'warning'}];
2100+
2101+
mockFetch.pause();
2102+
2103+
await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, mockViolations);
2104+
2105+
const transactionInOnyx = generateTransaction({
2106+
transactionID,
2107+
reportID: FAKE_OLD_REPORT_ID,
2108+
comment: {
2109+
hold: 'holdReportActionID',
2110+
dismissedViolations: {
2111+
[CONST.VIOLATIONS.SMARTSCAN_FAILED]: {
2112+
owner: 123,
2113+
},
2114+
},
2115+
},
2116+
});
2117+
await Onyx.merge(transactionKey, transactionInOnyx);
2118+
2119+
const staleTransaction = {
2120+
...transactionInOnyx,
2121+
comment: {
2122+
dismissedViolations: {
2123+
[CONST.VIOLATIONS.SMARTSCAN_FAILED]: {
2124+
owner: 123,
2125+
},
2126+
},
2127+
hold: undefined,
2128+
},
2129+
} as Transaction;
2130+
2131+
const iouAction = {
2132+
reportActionID: rand64(),
2133+
actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
2134+
childReportID: threadReportID,
2135+
actorAccountID: CURRENT_USER_ID,
2136+
created: DateUtils.getDBTime(),
2137+
originalMessage: {
2138+
IOUReportID: FAKE_OLD_REPORT_ID,
2139+
IOUTransactionID: transactionID,
2140+
amount: transactionInOnyx.amount,
2141+
currency: transactionInOnyx.currency,
2142+
type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
2143+
},
2144+
};
2145+
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${FAKE_OLD_REPORT_ID}`, {[iouAction.reportActionID]: iouAction});
2146+
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`, {});
2147+
2148+
mockFetch.fail();
2149+
2150+
dismissDuplicateTransactionViolation({
2151+
transactionIDs: [transactionID],
2152+
dismissedPersonalDetails: {login: testerEmail, accountID: CURRENT_USER_ID},
2153+
expenseReport: newReport,
2154+
policy: undefined,
2155+
isASAPSubmitBetaEnabled: false,
2156+
allTransactions: {[transactionKey]: staleTransaction},
2157+
});
2158+
await waitForBatchedUpdates();
2159+
2160+
const optimisticTransaction = await getOnyxValue(transactionKey);
2161+
const duplicateDismissals = optimisticTransaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.DUPLICATED_TRANSACTION];
2162+
expect(optimisticTransaction?.comment?.hold).toBe('holdReportActionID');
2163+
expect(optimisticTransaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.SMARTSCAN_FAILED]).toEqual({owner: 123});
2164+
expect(duplicateDismissals?.[testerEmail]).toEqual(expect.any(Number));
2165+
2166+
await mockFetch.resume();
2167+
await waitForBatchedUpdates();
2168+
2169+
const revertedTransaction = await getOnyxValue(transactionKey);
2170+
expect(revertedTransaction?.comment?.hold).toBe('holdReportActionID');
2171+
expect(revertedTransaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.SMARTSCAN_FAILED]).toEqual({owner: 123});
2172+
expect(revertedTransaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.DUPLICATED_TRANSACTION]).toBeUndefined();
2173+
});
2174+
20942175
it('should not modify Onyx data when tag list does not exist at given index (empty violations array)', async () => {
20952176
const transactionID = 'dismissTxn3';
20962177
const threadReportID = 'threadDismiss3';

0 commit comments

Comments
 (0)