diff --git a/src/components/MoneyReportHeaderPrimaryAction/useConfirmApproval.ts b/src/components/MoneyReportHeaderPrimaryAction/useConfirmApproval.ts index 8b58f2baf574..5e2129a52dd9 100644 --- a/src/components/MoneyReportHeaderPrimaryAction/useConfirmApproval.ts +++ b/src/components/MoneyReportHeaderPrimaryAction/useConfirmApproval.ts @@ -4,8 +4,9 @@ import {useMoneyReportHeaderModals} from '@components/MoneyReportHeaderModalsCon import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; +import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; -import {hasHeldExpenses as hasHeldExpensesReportUtils, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; +import {hasHeldExpensesFromTransactions as hasHeldExpensesReportUtils, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; import {approveMoneyRequest} from '@userActions/IOU/ReportWorkflow'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -26,10 +27,11 @@ function useConfirmApproval(reportID: string | undefined, startApprovedAnimation const [ownerBillingGracePeriodEnd] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END); const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector}); + const {transactions: reportTransactions} = useTransactionsAndViolationsForReport(moneyRequestReport?.reportID); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); const hasViolations = hasViolationsReportUtils(moneyRequestReport?.reportID, allTransactionViolations, accountID, email ?? ''); - const isAnyTransactionOnHold = hasHeldExpensesReportUtils(moneyRequestReport?.reportID); + const isAnyTransactionOnHold = hasHeldExpensesReportUtils(Object.values(reportTransactions)); const confirmApproval = () => { if (isDelegateAccessRestricted) { diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 39564b8985a7..c9b11553fb4b 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -611,11 +611,9 @@ function dismissDuplicateTransactionViolation({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`, value: { - ...transaction, comment: { - ...transaction?.comment, dismissedViolations: { - duplicatedTransaction: { + [CONST.VIOLATIONS.DUPLICATED_TRANSACTION]: { [dismissedPersonalDetails.login ?? '']: getUnixTime(new Date()), }, }, @@ -635,7 +633,11 @@ function dismissDuplicateTransactionViolation({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`, value: { - ...transaction, + comment: { + dismissedViolations: { + [CONST.VIOLATIONS.DUPLICATED_TRANSACTION]: transaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.DUPLICATED_TRANSACTION] ?? null, + }, + }, }, })); diff --git a/tests/unit/TransactionTest.ts b/tests/unit/TransactionTest.ts index b915b776f75e..c95ecf6730d9 100644 --- a/tests/unit/TransactionTest.ts +++ b/tests/unit/TransactionTest.ts @@ -1784,6 +1784,87 @@ describe('Transaction', () => { expect(Object.keys(reportActions ?? {}).length).toBe(0); }); + it('should preserve an existing hold when optimistic dismissal is built from a stale transaction snapshot', async () => { + const transactionID = 'dismissTxnHold'; + const threadReportID = 'threadDismissHold'; + const transactionKey = `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}` as const; + const testerEmail = 'tester@example.com'; + const mockViolations: TransactionViolation[] = [{name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, type: 'warning'}]; + + mockFetch.pause(); + + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, mockViolations); + + const transactionInOnyx = generateTransaction({ + transactionID, + reportID: FAKE_OLD_REPORT_ID, + comment: { + hold: 'holdReportActionID', + dismissedViolations: { + [CONST.VIOLATIONS.SMARTSCAN_FAILED]: { + owner: 123, + }, + }, + }, + }); + await Onyx.merge(transactionKey, transactionInOnyx); + + const staleTransaction = { + ...transactionInOnyx, + comment: { + dismissedViolations: { + [CONST.VIOLATIONS.SMARTSCAN_FAILED]: { + owner: 123, + }, + }, + hold: undefined, + }, + } as Transaction; + + const iouAction = { + reportActionID: rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + childReportID: threadReportID, + actorAccountID: CURRENT_USER_ID, + created: DateUtils.getDBTime(), + originalMessage: { + IOUReportID: FAKE_OLD_REPORT_ID, + IOUTransactionID: transactionID, + amount: transactionInOnyx.amount, + currency: transactionInOnyx.currency, + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${FAKE_OLD_REPORT_ID}`, {[iouAction.reportActionID]: iouAction}); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`, {}); + + mockFetch.fail(); + + dismissDuplicateTransactionViolation({ + transactionIDs: [transactionID], + dismissedPersonalDetails: {login: testerEmail, accountID: CURRENT_USER_ID}, + expenseReport: newReport, + policy: undefined, + isASAPSubmitBetaEnabled: false, + allTransactions: {[transactionKey]: staleTransaction}, + }); + await waitForBatchedUpdates(); + + const optimisticTransaction = await getOnyxValue(transactionKey); + const duplicateDismissals = optimisticTransaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.DUPLICATED_TRANSACTION]; + expect(optimisticTransaction?.comment?.hold).toBe('holdReportActionID'); + expect(optimisticTransaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.SMARTSCAN_FAILED]).toEqual({owner: 123}); + expect(duplicateDismissals?.[testerEmail]).toEqual(expect.any(Number)); + + await mockFetch.resume(); + await waitForBatchedUpdates(); + + const revertedTransaction = await getOnyxValue(transactionKey); + expect(revertedTransaction?.comment?.hold).toBe('holdReportActionID'); + expect(revertedTransaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.SMARTSCAN_FAILED]).toEqual({owner: 123}); + expect(revertedTransaction?.comment?.dismissedViolations?.[CONST.VIOLATIONS.DUPLICATED_TRANSACTION]).toBeUndefined(); + }); + it('should not modify Onyx data when tag list does not exist at given index (empty violations array)', async () => { const transactionID = 'dismissTxn3'; const threadReportID = 'threadDismiss3';