Skip to content

Commit aa87e91

Browse files
author
Nabi Ebrahimi
committed
fix: prevent auto-unhold and approval for held expenses with multiple violations
1 parent 194f7f6 commit aa87e91

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
@@ -611,11 +611,9 @@ function dismissDuplicateTransactionViolation({
611611
onyxMethod: Onyx.METHOD.MERGE,
612612
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`,
613613
value: {
614-
...transaction,
615614
comment: {
616-
...transaction?.comment,
617615
dismissedViolations: {
618-
duplicatedTransaction: {
616+
[CONST.VIOLATIONS.DUPLICATED_TRANSACTION]: {
619617
[dismissedPersonalDetails.login ?? '']: getUnixTime(new Date()),
620618
},
621619
},
@@ -635,7 +633,11 @@ function dismissDuplicateTransactionViolation({
635633
onyxMethod: Onyx.METHOD.MERGE,
636634
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`,
637635
value: {
638-
...transaction,
636+
comment: {
637+
dismissedViolations: {
638+
[CONST.VIOLATIONS.DUPLICATED_TRANSACTION]: transaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.DUPLICATED_TRANSACTION] ?? null,
639+
},
640+
},
639641
},
640642
}));
641643

tests/unit/TransactionTest.ts

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

1787+
it('should preserve an existing hold when optimistic dismissal is built from a stale transaction snapshot', async () => {
1788+
const transactionID = 'dismissTxnHold';
1789+
const threadReportID = 'threadDismissHold';
1790+
const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}` as const;
1791+
const testerEmail = 'tester@example.com';
1792+
const mockViolations: TransactionViolation[] = [{name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, type: 'warning'}];
1793+
1794+
mockFetch.pause();
1795+
1796+
await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, mockViolations);
1797+
1798+
const transactionInOnyx = generateTransaction({
1799+
transactionID,
1800+
reportID: FAKE_OLD_REPORT_ID,
1801+
comment: {
1802+
hold: 'holdReportActionID',
1803+
dismissedViolations: {
1804+
[CONST.VIOLATIONS.SMARTSCAN_FAILED]: {
1805+
owner: 123,
1806+
},
1807+
},
1808+
},
1809+
});
1810+
await Onyx.merge(transactionKey, transactionInOnyx);
1811+
1812+
const staleTransaction = {
1813+
...transactionInOnyx,
1814+
comment: {
1815+
dismissedViolations: {
1816+
[CONST.VIOLATIONS.SMARTSCAN_FAILED]: {
1817+
owner: 123,
1818+
},
1819+
},
1820+
hold: null,
1821+
},
1822+
} as Transaction;
1823+
1824+
const iouAction = {
1825+
reportActionID: rand64(),
1826+
actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
1827+
childReportID: threadReportID,
1828+
actorAccountID: CURRENT_USER_ID,
1829+
created: DateUtils.getDBTime(),
1830+
originalMessage: {
1831+
IOUReportID: FAKE_OLD_REPORT_ID,
1832+
IOUTransactionID: transactionID,
1833+
amount: transactionInOnyx.amount,
1834+
currency: transactionInOnyx.currency,
1835+
type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
1836+
},
1837+
};
1838+
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${FAKE_OLD_REPORT_ID}`, {[iouAction.reportActionID]: iouAction});
1839+
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`, {});
1840+
1841+
mockFetch.fail();
1842+
1843+
dismissDuplicateTransactionViolation({
1844+
transactionIDs: [transactionID],
1845+
dismissedPersonalDetails: {login: testerEmail, accountID: CURRENT_USER_ID},
1846+
expenseReport: newReport,
1847+
policy: undefined,
1848+
isASAPSubmitBetaEnabled: false,
1849+
allTransactions: {[transactionKey]: staleTransaction},
1850+
});
1851+
await waitForBatchedUpdates();
1852+
1853+
const optimisticTransaction = await getOnyxValue(transactionKey);
1854+
const duplicateDismissals = optimisticTransaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.DUPLICATED_TRANSACTION];
1855+
expect(optimisticTransaction?.comment?.hold).toBe('holdReportActionID');
1856+
expect(optimisticTransaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.SMARTSCAN_FAILED]).toEqual({owner: 123});
1857+
expect(duplicateDismissals?.[testerEmail]).toEqual(expect.any(Number));
1858+
1859+
await mockFetch.resume();
1860+
await waitForBatchedUpdates();
1861+
1862+
const revertedTransaction = await getOnyxValue(transactionKey);
1863+
expect(revertedTransaction?.comment?.hold).toBe('holdReportActionID');
1864+
expect(revertedTransaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.SMARTSCAN_FAILED]).toEqual({owner: 123});
1865+
expect(revertedTransaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.DUPLICATED_TRANSACTION]).toBeUndefined();
1866+
});
1867+
17871868
it('should not modify Onyx data when tag list does not exist at given index (empty violations array)', async () => {
17881869
const transactionID = 'dismissTxn3';
17891870
const threadReportID = 'threadDismiss3';

0 commit comments

Comments
 (0)