diff --git a/src/components/AddUnreportedExpenseFooter.tsx b/src/components/AddUnreportedExpenseFooter.tsx index 58defd361630..9040b913a967 100644 --- a/src/components/AddUnreportedExpenseFooter.tsx +++ b/src/components/AddUnreportedExpenseFooter.tsx @@ -75,7 +75,7 @@ function AddUnreportedExpenseFooter({selectedIds, report, reportToConfirm, repor policy, reportNextStep, policyCategories, - allTransactionsCollection: allTransactions, + allTransactions, }); } }); diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index e5e16f086c6a..3438686e9499 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -52,18 +52,6 @@ import type TransactionState from '@src/types/utils/TransactionStateType'; import {getPolicyTagsData} from './Policy/Tag'; import {getCurrentUserAccountID} from './Report'; -const allTransactions: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - callback: (transaction, key) => { - if (!key || !transaction) { - return; - } - const transactionID = CollectionUtils.extractCollectionItemID(key); - allTransactions[transactionID] = transaction; - }, -}); - let allTransactionDrafts: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, @@ -385,19 +373,29 @@ function updateWaypoints(transactionID: string, waypoints: WaypointCollection, i }); } +type DismissDuplicateTransactionViolationProps = { + transactionIDs: string[]; + dismissedPersonalDetails: PersonalDetails; + expenseReport: OnyxEntry; + policy: OnyxEntry; + isASAPSubmitBetaEnabled: boolean; + allTransactions: OnyxCollection; +}; + /** * Dismisses the duplicate transaction violation for the provided transactionIDs * and updates the transaction to include the dismissed violation in the comment. */ -function dismissDuplicateTransactionViolation( - transactionIDs: string[], - dismissedPersonalDetails: PersonalDetails, - expenseReport: OnyxEntry, - policy: OnyxEntry, - isASAPSubmitBetaEnabled: boolean, -) { +function dismissDuplicateTransactionViolation({ + transactionIDs, + dismissedPersonalDetails, + expenseReport, + policy, + isASAPSubmitBetaEnabled, + allTransactions, +}: DismissDuplicateTransactionViolationProps) { const currentTransactionViolations = transactionIDs.map((id) => ({transactionID: id, violations: allTransactionViolation?.[id] ?? []})); - const currentTransactions = transactionIDs.map((id) => allTransactions?.[id]); + const currentTransactions = transactionIDs.map((id) => allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]); const transactionsReportActions = currentTransactions.map((transaction) => getIOUActionForReportID(transaction?.reportID, transaction?.transactionID)); const optimisticDismissedViolationReportActions = transactionsReportActions.map(() => { return buildOptimisticDismissedViolationReportAction({reason: 'manual', violationName: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}); @@ -700,7 +698,7 @@ type ChangeTransactionsReportProps = { policy?: OnyxEntry; reportNextStep?: OnyxEntry; policyCategories?: OnyxEntry; - allTransactionsCollection: OnyxCollection; + allTransactions: OnyxCollection; }; function changeTransactionsReport({ @@ -712,11 +710,11 @@ function changeTransactionsReport({ policy, reportNextStep, policyCategories, - allTransactionsCollection, + allTransactions, }: ChangeTransactionsReportProps) { const reportID = newReport?.reportID ?? CONST.REPORT.UNREPORTED_REPORT_ID; - const transactions = transactionIDs.map((id) => allTransactionsCollection?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]).filter((t): t is NonNullable => t !== undefined); + const transactions = transactionIDs.map((id) => allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]).filter((t): t is NonNullable => t !== undefined); const transactionIDToReportActionAndThreadData: Record = {}; const updatedReportTotals: Record = {}; const updatedReportNonReimbursableTotals: Record = {}; diff --git a/src/pages/NewReportWorkspaceSelectionPage.tsx b/src/pages/NewReportWorkspaceSelectionPage.tsx index 1824c5f31332..610d2aaa034b 100644 --- a/src/pages/NewReportWorkspaceSelectionPage.tsx +++ b/src/pages/NewReportWorkspaceSelectionPage.tsx @@ -139,7 +139,7 @@ function NewReportWorkspaceSelectionPage({route}: NewReportWorkspaceSelectionPag policy: policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`], reportNextStep, policyCategories: undefined, - allTransactionsCollection: allTransactions, + allTransactions, }); // eslint-disable-next-line rulesdir/no-default-id-values diff --git a/src/pages/Search/SearchTransactionsChangeReport.tsx b/src/pages/Search/SearchTransactionsChangeReport.tsx index 92808341df00..8744ffcf203e 100644 --- a/src/pages/Search/SearchTransactionsChangeReport.tsx +++ b/src/pages/Search/SearchTransactionsChangeReport.tsx @@ -86,7 +86,7 @@ function SearchTransactionsChangeReport() { policy: policyForMovingExpenses, reportNextStep, policyCategories: allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyForMovingExpensesID}`], - allTransactionsCollection: allTransactions, + allTransactions, }); clearSelectedTransactions(); }); @@ -128,7 +128,7 @@ function SearchTransactionsChangeReport() { policy: allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${item.policyID}`], reportNextStep, policyCategories: allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${item.policyID}`], - allTransactionsCollection: allTransactions, + allTransactions, }); // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { @@ -147,7 +147,7 @@ function SearchTransactionsChangeReport() { isASAPSubmitBetaEnabled, accountID: session?.accountID ?? CONST.DEFAULT_NUMBER_ID, email: session?.email ?? '', - allTransactionsCollection: allTransactions, + allTransactions, }); clearSelectedTransactions(); Navigation.goBack(); diff --git a/src/pages/TransactionDuplicate/Review.tsx b/src/pages/TransactionDuplicate/Review.tsx index d8633034acfa..d81c31a340c1 100644 --- a/src/pages/TransactionDuplicate/Review.tsx +++ b/src/pages/TransactionDuplicate/Review.tsx @@ -59,14 +59,9 @@ function TransactionDuplicateReview() { [transactionIDs], ); - const [transactions] = useOnyx( - ONYXKEYS.COLLECTION.TRANSACTION, - { - selector: transactionsSelector, - canBeMissing: true, - }, - [transactionIDs], - ); + const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: true}); + const transactions = useMemo(() => transactionsSelector(allTransactions ?? {}), [allTransactions, transactionsSelector]); + const originalTransactionIDsListRef = useRef(null); const [transactionIDsList = getEmptyArray()] = useOnyx(ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS, { canBeMissing: true, @@ -97,7 +92,14 @@ function TransactionDuplicateReview() { ); const keepAll = () => { - dismissDuplicateTransactionViolation(transactionIDs, currentPersonalDetails, expenseReport, policy, isASAPSubmitBetaEnabled); + dismissDuplicateTransactionViolation({ + transactionIDs, + dismissedPersonalDetails: currentPersonalDetails, + expenseReport, + policy, + isASAPSubmitBetaEnabled, + allTransactions, + }); Navigation.goBack(); }; diff --git a/src/pages/iou/request/step/IOURequestEditReport.tsx b/src/pages/iou/request/step/IOURequestEditReport.tsx index 03ee0e7fe4c9..eba4016703df 100644 --- a/src/pages/iou/request/step/IOURequestEditReport.tsx +++ b/src/pages/iou/request/step/IOURequestEditReport.tsx @@ -73,7 +73,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { policy: allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${item.policyID}`], reportNextStep, policyCategories: allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${item.policyID}`], - allTransactionsCollection: allTransactions, + allTransactions, }); turnOffMobileSelectionMode(); clearSelectedTransactions(true); @@ -91,7 +91,7 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) { isASAPSubmitBetaEnabled, accountID: session?.accountID ?? CONST.DEFAULT_NUMBER_ID, email: session?.email ?? '', - allTransactionsCollection: allTransactions, + allTransactions, }); if (shouldTurnOffSelectionMode) { turnOffMobileSelectionMode(); diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx index b9625fd07474..a1c50e7f9881 100644 --- a/src/pages/iou/request/step/IOURequestStepReport.tsx +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -133,7 +133,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { policy: allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${item.policyID}`], reportNextStep: undefined, policyCategories: allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${item.policyID}`], - allTransactionsCollection: allTransactions, + allTransactions, }); removeTransaction(transaction.transactionID); } @@ -175,7 +175,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { isASAPSubmitBetaEnabled, accountID: session?.accountID ?? CONST.DEFAULT_NUMBER_ID, email: session?.email ?? '', - allTransactionsCollection: allTransactions, + allTransactions, }); removeTransaction(transaction.transactionID); }); diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index b5e80b9efb93..2a750ece6905 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -8928,7 +8928,7 @@ describe('actions/IOU', () => { accountID: CARLOS_ACCOUNT_ID, email: CARLOS_EMAIL, newReport: result.current.report, - allTransactionsCollection: allTransactions, + allTransactions, }); let updatedTransaction: OnyxEntry; diff --git a/tests/unit/TransactionTest.ts b/tests/unit/TransactionTest.ts index 23599ffeed1c..9e2f37df8b6c 100644 --- a/tests/unit/TransactionTest.ts +++ b/tests/unit/TransactionTest.ts @@ -3,7 +3,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; import useOnyx from '@hooks/useOnyx'; -import {changeTransactionsReport, markAsCash, saveWaypoint} from '@libs/actions/Transaction'; +import {changeTransactionsReport, dismissDuplicateTransactionViolation, markAsCash, saveWaypoint} from '@libs/actions/Transaction'; import DateUtils from '@libs/DateUtils'; import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils'; import type {buildOptimisticNextStep} from '@libs/NextStepUtils'; @@ -125,7 +125,7 @@ describe('Transaction', () => { accountID: CURRENT_USER_ID, email: 'test@example.com', newReport: report, - allTransactionsCollection: allTransactions, + allTransactions, }); await waitForBatchedUpdates(); const reportActions = await new Promise>((resolve) => { @@ -172,7 +172,7 @@ describe('Transaction', () => { accountID: CURRENT_USER_ID, email: 'test@example.com', newReport: report, - allTransactionsCollection: allTransactions, + allTransactions, }); await waitForBatchedUpdates(); const reportActions = await new Promise>((resolve) => { @@ -234,7 +234,7 @@ describe('Transaction', () => { newReport: report, policy: undefined, reportNextStep: mockReportNextStep, - allTransactionsCollection: allTransactions, + allTransactions, }); await waitForBatchedUpdates(); @@ -298,7 +298,7 @@ describe('Transaction', () => { newReport: report, policy: undefined, reportNextStep: mockReportNextStep, - allTransactionsCollection: allTransactions, + allTransactions, }); await waitForBatchedUpdates(); @@ -350,7 +350,7 @@ describe('Transaction', () => { newReport: report, policy: undefined, reportNextStep: undefined, - allTransactionsCollection: allTransactions, + allTransactions, }); await waitForBatchedUpdates(); @@ -413,7 +413,7 @@ describe('Transaction', () => { accountID: CURRENT_USER_ID, email: 'test@example.com', newReport: report, - allTransactionsCollection: allTransactions, + allTransactions, }); await waitForBatchedUpdates(); @@ -468,7 +468,7 @@ describe('Transaction', () => { accountID: CURRENT_USER_ID, email: 'test@example.com', newReport: report, - allTransactionsCollection: allTransactions, + allTransactions, }); await waitForBatchedUpdates(); @@ -520,7 +520,7 @@ describe('Transaction', () => { accountID: customAccountID, email: customEmail, newReport: report, - allTransactionsCollection: allTransactions, + allTransactions, }); await waitForBatchedUpdates(); @@ -578,7 +578,7 @@ describe('Transaction', () => { accountID: CURRENT_USER_ID, email: 'test@example.com', newReport: expenseReport, - allTransactionsCollection: allTransactions, + allTransactions, }); await waitForBatchedUpdates(); const report = await new Promise>((resolve) => { @@ -636,7 +636,7 @@ describe('Transaction', () => { accountID: CURRENT_USER_ID, email: 'test@example.com', newReport: expenseReport, - allTransactionsCollection: allTransactions, + allTransactions, }); await waitForBatchedUpdates(); const report = await new Promise>((resolve) => { @@ -701,7 +701,7 @@ describe('Transaction', () => { accountID: CURRENT_USER_ID, email: 'test@example.com', newReport: newExpenseReport, - allTransactionsCollection: allTransactions, + allTransactions, }); await waitForBatchedUpdates(); const report = await new Promise>((resolve) => { @@ -765,7 +765,7 @@ describe('Transaction', () => { accountID: CURRENT_USER_ID, email: 'test@example.com', newReport: newExpenseReport, - allTransactionsCollection: allTransactions, + allTransactions, }); await waitForBatchedUpdates(); const report = await new Promise>((resolve) => { @@ -822,7 +822,7 @@ describe('Transaction', () => { accountID: CURRENT_USER_ID, email: 'test@example.com', newReport: fakeReport, - allTransactionsCollection: allTransactions, + allTransactions, }); await waitForBatchedUpdates(); @@ -881,7 +881,7 @@ describe('Transaction', () => { accountID: CURRENT_USER_ID, email: 'test@example.com', newReport: fakeReport, - allTransactionsCollection: allTransactions, + allTransactions, }); await waitForBatchedUpdates(); @@ -935,7 +935,7 @@ describe('Transaction', () => { policy, reportNextStep: undefined, policyCategories, - allTransactionsCollection: allTransactions, + allTransactions, }); await waitForBatchedUpdates(); @@ -1213,4 +1213,220 @@ describe('Transaction', () => { await mockFetch.resume(); }); }); + + describe('dismissDuplicateTransactionViolation', () => { + it('should optimistically remove DUPLICATED_TRANSACTION violation and add dismissed violation report action', async () => { + const transactionID = 'dismissTxn1'; + const threadReportID = 'threadDismiss1'; + const mockViolations: TransactionViolation[] = [ + {name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, type: 'warning'}, + {name: CONST.VIOLATIONS.MISSING_CATEGORY, type: 'violation'}, + ]; + + mockFetch.pause(); + + // Put violations into Onyx so module-level Onyx.connect in Transaction.ts picks them up + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, mockViolations); + + // Prepare a transaction and its IOU action so getIOUActionForReportID finds a childReportID + const transaction = generateTransaction({transactionID, reportID: FAKE_OLD_REPORT_ID}); + 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: transaction.amount, + currency: transaction.currency, + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${FAKE_OLD_REPORT_ID}`, {[iouAction.reportActionID]: iouAction}); + // Ensure transaction exists in the passed allTransactions mapping + const allTransactions = {[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction}; + + // Ensure the child report actions collection exists + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`, {}); + + // Call the function + dismissDuplicateTransactionViolation({ + transactionIDs: [transactionID], + dismissedPersonalDetails: {login: 'tester@example.com', accountID: CURRENT_USER_ID}, + expenseReport: newReport, + policy: undefined, + isASAPSubmitBetaEnabled: false, + allTransactions, + }); + await waitForBatchedUpdates(); + + const optimisticViolations = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + expect(optimisticViolations).toEqual([{name: CONST.VIOLATIONS.MISSING_CATEGORY, type: 'violation'}]); + + const reportActions = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`); + const reportActionValues = Object.values(reportActions ?? {}); + expect(reportActionValues.length).toBe(1); + expect(reportActionValues.at(0)?.actionName).toBe(CONST.REPORT.ACTIONS.TYPE.DISMISSED_VIOLATION); + + // Let the mocked API resolve and ensure optimistic state behaves as implementation expects: + // The implementation clears the optimistic dismissed-violation report action on success to avoid duplicates, + // so after the API resolves the report action should be removed while the filtered violations persist. + await mockFetch.resume(); + await waitForBatchedUpdates(); + + const finalViolations = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + expect(finalViolations).toEqual([{name: CONST.VIOLATIONS.MISSING_CATEGORY, type: 'violation'}]); + + const finalReportActions = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`); + const finalReportActionValues = Object.values(finalReportActions ?? {}); + // The optimistic dismissed violation report action is removed on successful API response to avoid duplicates + expect(finalReportActionValues.length).toBe(0); + }); + + it('should restore DUPLICATED_TRANSACTION violation and remove report action when API fails', async () => { + const transactionID = 'dismissTxn2'; + const threadReportID = 'threadDismiss2'; + const mockViolations: TransactionViolation[] = [{name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, type: 'warning'}]; + + mockFetch.pause(); + + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, mockViolations); + + const transaction = generateTransaction({transactionID, reportID: FAKE_OLD_REPORT_ID}); + 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: transaction.amount, + currency: transaction.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}`, {}); + const allTransactions = {[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction}; + + // Make the API fail + mockFetch.fail(); + + dismissDuplicateTransactionViolation({ + transactionIDs: [transactionID], + dismissedPersonalDetails: {login: 'tester@example.com', accountID: CURRENT_USER_ID}, + expenseReport: newReport, + policy: undefined, + isASAPSubmitBetaEnabled: false, + allTransactions, + }); + await waitForBatchedUpdates(); + + // Resume (the mock is set to fail) and wait for failure handling + await mockFetch.resume(); + await waitForBatchedUpdates(); + + const failureViolations = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + expect(failureViolations).toEqual(mockViolations); + + const reportActions = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`); + expect(Object.keys(reportActions ?? {}).length).toBe(0); + }); + + 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'; + const mockViolations: TransactionViolation[] = []; + + mockFetch.pause(); + + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, mockViolations); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`, {}); + + const transaction = generateTransaction({transactionID, reportID: FAKE_OLD_REPORT_ID}); + const allTransactions = {[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction}; + + dismissDuplicateTransactionViolation({ + transactionIDs: [transactionID], + dismissedPersonalDetails: {login: 'tester@example.com', accountID: CURRENT_USER_ID}, + expenseReport: newReport, + policy: undefined, + isASAPSubmitBetaEnabled: false, + allTransactions, + }); + await waitForBatchedUpdates(); + + const optimisticViolations = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + expect(optimisticViolations).toEqual([]); + + await mockFetch.resume(); + }); + + describe('integration with useOnyx', () => { + it('works when violations are read via useOnyx hook', async () => { + const transactionID = 'dismissTxn4'; + const threadReportID = 'threadDismiss4'; + const mockViolations: TransactionViolation[] = [ + {name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, type: 'warning'}, + {name: CONST.VIOLATIONS.MISSING_CATEGORY, type: 'violation'}, + ]; + + mockFetch.pause(); + + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, mockViolations); + + const transaction = generateTransaction({transactionID, reportID: FAKE_OLD_REPORT_ID}); + 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: transaction.amount, + currency: transaction.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}`, {}); + const allTransactions = {[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: transaction}; + + const {result} = renderHook(() => useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`)); + await waitFor(() => { + expect(result.current[0]).toBeDefined(); + }); + + await act(async () => { + dismissDuplicateTransactionViolation({ + transactionIDs: [transactionID], + dismissedPersonalDetails: {login: 'tester@example.com', accountID: CURRENT_USER_ID}, + expenseReport: newReport, + policy: undefined, + isASAPSubmitBetaEnabled: false, + allTransactions, + }); + await waitForBatchedUpdates(); + }); + + // The useOnyx hook should reflect the optimistic change + await waitFor(() => { + expect(result.current[0]).toEqual([{name: CONST.VIOLATIONS.MISSING_CATEGORY, type: 'violation'}]); + }); + + const reportActions = await OnyxUtils.get(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${threadReportID}`); + const reportActionValues = Object.values(reportActions ?? {}); + expect(reportActionValues.length).toBe(1); + expect(reportActionValues.at(0)?.actionName).toBe(CONST.REPORT.ACTIONS.TYPE.DISMISSED_VIOLATION); + + await mockFetch.resume(); + }); + }); + }); });