diff --git a/src/libs/actions/IOU/DeleteMoneyRequest.ts b/src/libs/actions/IOU/DeleteMoneyRequest.ts index 357260b99431..4a081ae2f81a 100644 --- a/src/libs/actions/IOU/DeleteMoneyRequest.ts +++ b/src/libs/actions/IOU/DeleteMoneyRequest.ts @@ -42,7 +42,7 @@ import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxInputValue, OnyxUpdate} import cloneDeep from 'lodash/cloneDeep'; import Onyx from 'react-native-onyx'; -import {getAllReportActionsFromIOU, getAllReportNameValuePairs, getAllReports, getAllTransactions, getAllTransactionViolations} from '.'; +import {getAllReportActionsFromIOU, getAllReportNameValuePairs, getAllTransactions, getAllTransactionViolations} from '.'; import {getReportPreviewReportAction, maybeUpdateReportNameForFormulaTitle} from './MoneyRequestBuilder'; type PrepareToCleanUpMoneyRequestResult = { @@ -620,18 +620,31 @@ function getCleanUpTransactionThreadReportOnyxData({ updatedReportPreviewAction, shouldAddUpdatedReportPreviewActionToOnyxData = true, currentUserAccountID, + transactionThread: transactionThreadParam, + iouReport, + chatReport, transactionThreadReportActionsParam, }: { transactionThreadID?: string; shouldDeleteTransactionThread: boolean; - reportAction?: ReportAction; isChatIOUReportArchived?: boolean; updatedReportPreviewAction?: ReportAction; shouldAddUpdatedReportPreviewActionToOnyxData?: boolean; currentUserAccountID: number; - transactionThreadReportActionsParam: OnyxEntry; -}) { - const allReports = getAllReports(); + transactionThread?: OnyxEntry; + transactionThreadReportActionsParam?: OnyxEntry; +} & ( + | { + reportAction: ReportAction; + iouReport: OnyxEntry; + chatReport: OnyxEntry; + } + | { + reportAction?: undefined; + iouReport?: OnyxEntry; + chatReport?: OnyxEntry; + } +)) { const allReportNameValuePairs = getAllReportNameValuePairs(); const optimisticData: Array> = []; @@ -642,7 +655,7 @@ function getCleanUpTransactionThreadReportOnyxData({ let transactionThread = null; let transactionThreadReportActions = null; if (transactionThreadID) { - transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`] ?? null; + transactionThread = transactionThreadParam ?? null; transactionThreadReportActions = transactionThreadReportActionsParam ?? null; } @@ -692,8 +705,6 @@ function getCleanUpTransactionThreadReportOnyxData({ // Update the child comment visible count for reportPreviewAction. const iouReportID = isMoneyRequestAction(reportAction) ? reportAction?.reportID : undefined; - const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]; - const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReport?.chatReportID}`]; const originalReportPreviewAction = getReportPreviewReportAction(chatReport?.reportID, iouReport?.reportID) ?? undefined; let reportPreviewAction = updatedReportPreviewAction ?? originalReportPreviewAction; if ( @@ -943,6 +954,9 @@ function deleteMoneyRequest({ reportAction, isChatIOUReportArchived, currentUserAccountID, + transactionThread: transactionThreadReport, + iouReport, + chatReport, transactionThreadReportActionsParam: transactionThreadReportActions, }); optimisticData.push(...cleanUpTransactionThreadReportOnyxData.optimisticData); diff --git a/src/libs/actions/IOU/Duplicate.ts b/src/libs/actions/IOU/Duplicate.ts index eb886b0f9977..f7766709ec02 100644 --- a/src/libs/actions/IOU/Duplicate.ts +++ b/src/libs/actions/IOU/Duplicate.ts @@ -63,7 +63,7 @@ import type {PerDiemExpenseInformation} from './PerDiem'; import type {CreateDistanceRequestInformation} from './Split'; import type {CreateTrackExpenseParams} from './TrackExpense'; -import {getAllReports, getAllTransactions, getCurrentUserAccountIDFromSession} from '.'; +import {getAllTransactions, getCurrentUserAccountIDFromSession} from '.'; import {getCleanUpTransactionThreadReportOnyxData} from './DeleteMoneyRequest'; import {getMoneyRequestParticipantsFromReport} from './MoneyRequest'; import {submitPerDiemExpense} from './PerDiem'; @@ -179,6 +179,7 @@ type MergeDuplicatesFuncParams = MergeDuplicatesParams & { taxValue?: string; allTransactionViolations: OnyxCollection; allReportActionsList: OnyxCollection; + allReportsList: OnyxCollection; }; /** Merge several transactions into one by updating the fields of the one we want to keep and deleting the rest */ @@ -190,11 +191,11 @@ function mergeDuplicates({ taxValue, allTransactionViolations, allReportActionsList, + allReportsList, ...params }: MergeDuplicatesFuncParams) { const allParams: MergeDuplicatesParams = {...params}; const allTransactions = getAllTransactions(); - const allReports = getAllReports(); const originalSelectedTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${params.transactionID}`]; const optimisticTransactionData = buildOptimisticTransactionData({ @@ -246,7 +247,7 @@ function mergeDuplicates({ }; }); - const expenseReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${params.reportID}`]; + const expenseReport = allReportsList?.[`${ONYXKEYS.COLLECTION.REPORT}${params.reportID}`]; // Group each discarded duplicate's IOU action and amount by its own source report so the // soft-delete MERGE and total decrement target the correct keys when duplicates span reports. @@ -273,7 +274,7 @@ function mergeDuplicates({ const cleanUpTransactionThreadReportsSuccessData = []; const cleanUpTransactionThreadReportsFailureData = []; for (const [sourceReportID, {amount, reimbursableAmount, actions}] of sources) { - const sourceReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${sourceReportID}`]; + const sourceReport = allReportsList?.[`${ONYXKEYS.COLLECTION.REPORT}${sourceReportID}`]; const sourceReimbursableTotal = getReimbursableTotal(sourceReport); expenseReportOptimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -301,6 +302,10 @@ function mergeDuplicates({ let updatedReportPreviewAction; for (const [index, iouAction] of actions.entries()) { const transactionThreadID = iouAction.childReportID; + const transactionThread = allReportsList?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`]; + const iouReportID = isMoneyRequestAction(iouAction) ? iouAction?.reportID : undefined; + const iouReport = allReportsList?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]; + const chatReport = allReportsList?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReport?.chatReportID}`]; const cleanUp = getCleanUpTransactionThreadReportOnyxData({ transactionThreadID, shouldDeleteTransactionThread: !!transactionThreadID, @@ -308,6 +313,9 @@ function mergeDuplicates({ updatedReportPreviewAction, shouldAddUpdatedReportPreviewActionToOnyxData: index === actions.length - 1, currentUserAccountID, + transactionThread, + iouReport, + chatReport, transactionThreadReportActionsParam: allReportActionsList?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadID}`], }); cleanUpTransactionThreadReportsOptimisticData.push(...cleanUp.optimisticData); diff --git a/src/libs/actions/IOU/SplitTransactionUpdate.ts b/src/libs/actions/IOU/SplitTransactionUpdate.ts index 6c01d851deca..448261da7329 100644 --- a/src/libs/actions/IOU/SplitTransactionUpdate.ts +++ b/src/libs/actions/IOU/SplitTransactionUpdate.ts @@ -1419,12 +1419,15 @@ function updateSplitTransactions({ // getDeleteTrackExpenseInformation only handles deleting the transaction report thread, so we need to update the report preview action here if (originalReportPreviewAction) { + const currentActionIOUReport = allReportsList?.[`${ONYXKEYS.COLLECTION.REPORT}${currentReportAction?.reportID}`]; const cleanUpTransactionThreadReportOnyxData = getCleanUpTransactionThreadReportOnyxData({ shouldDeleteTransactionThread: false, reportAction: currentReportAction, updatedReportPreviewAction: (updatedReportPreviewAction ?? originalReportPreviewAction) as OnyxTypes.ReportAction, shouldAddUpdatedReportPreviewActionToOnyxData: false, currentUserAccountID: currentUserPersonalDetails.accountID, + iouReport: currentActionIOUReport, + chatReport: allReportsList?.[`${ONYXKEYS.COLLECTION.REPORT}${currentActionIOUReport?.chatReportID}`], // shouldDeleteTransactionThread is false, so the transaction-thread report actions are never read here. transactionThreadReportActionsParam: undefined, }); @@ -1670,6 +1673,8 @@ function updateSplitTransactions({ }, }), }; + const iouActionIOUReportID = isMoneyRequestAction(iouActionToCleanUp) ? iouActionToCleanUp.reportID : undefined; + const iouActionIOUReport = allReportsList?.[`${ONYXKEYS.COLLECTION.REPORT}${iouActionIOUReportID}`]; const {optimisticData, successData, failureData} = getCleanUpTransactionThreadReportOnyxData({ transactionThreadID: iouActionToCleanUp.childReportID, @@ -1677,6 +1682,9 @@ function updateSplitTransactions({ reportAction: iouActionToCleanUp, updatedReportPreviewAction: updatedReportPreviewAction as OnyxTypes.ReportAction, currentUserAccountID: currentUserPersonalDetails.accountID, + transactionThread: allReportsList?.[`${ONYXKEYS.COLLECTION.REPORT}${iouActionToCleanUp.childReportID}`], + iouReport: iouActionIOUReport, + chatReport: allReportsList?.[`${ONYXKEYS.COLLECTION.REPORT}${iouActionIOUReport?.chatReportID}`], transactionThreadReportActionsParam: allReportActionsList?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouActionToCleanUp.childReportID}`], }); diff --git a/src/libs/actions/IOU/TrackExpense.ts b/src/libs/actions/IOU/TrackExpense.ts index 9dc10bf44111..b34d9d31c80e 100644 --- a/src/libs/actions/IOU/TrackExpense.ts +++ b/src/libs/actions/IOU/TrackExpense.ts @@ -665,6 +665,7 @@ type GetDeleteTrackExpenseInformationParams = { actionableWhisperReportActionID?: string; resolution?: string; shouldRemoveIOUTransaction?: boolean; + transactionThread?: OnyxEntry; }; function getDeleteTrackExpenseInformation({ @@ -679,6 +680,7 @@ function getDeleteTrackExpenseInformation({ actionableWhisperReportActionID = '', resolution = '', shouldRemoveIOUTransaction = true, + transactionThread, }: GetDeleteTrackExpenseInformationParams) { // STEP 1: Get all collections we're updating const transaction = getAllTransactions()?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; @@ -753,6 +755,7 @@ function getDeleteTrackExpenseInformation({ const cleanUpTransactionThreadReportOnyxData = getCleanUpTransactionThreadReportOnyxData({ transactionThreadID, shouldDeleteTransactionThread, + transactionThread, currentUserAccountID, transactionThreadReportActionsParam: transactionThreadReportActions, }); diff --git a/src/libs/actions/MergeTransaction.ts b/src/libs/actions/MergeTransaction.ts index df5c7cbf8d29..0fc08034e808 100644 --- a/src/libs/actions/MergeTransaction.ts +++ b/src/libs/actions/MergeTransaction.ts @@ -420,6 +420,9 @@ type MergeTransactionRequestParams = { sourceIOUAction: OnyxEntry; getCurrencyDecimals: CurrencyListActionsContextType['getCurrencyDecimals']; getCurrencySymbol: CurrencyListActionsContextType['getCurrencySymbol']; + sourceIOUActionThreadReport: OnyxEntry; + sourceActionIOUReport: OnyxEntry; + sourceActionChatReport: OnyxEntry; }; /** * Merges two transactions by updating the target transaction with selected fields and deleting the source transaction. @@ -454,6 +457,9 @@ function mergeTransactionRequest({ sourceIOUAction, getCurrencyDecimals, getCurrencySymbol, + sourceIOUActionThreadReport, + sourceActionIOUReport, + sourceActionChatReport, }: MergeTransactionRequestParams) { // For both unreported expenses and expense reports, negate the display amount when storing // This preserves the user's chosen sign while following the storage convention @@ -625,6 +631,9 @@ function mergeTransactionRequest({ shouldDeleteTransactionThread, reportAction: sourceIOUAction, currentUserAccountID: currentUserAccountIDParam, + iouReport: sourceActionIOUReport, + chatReport: sourceActionChatReport, + transactionThread: sourceIOUActionThreadReport, transactionThreadReportActionsParam: sourceTransactionThreadReportActions, }); optimisticSourceReportActionData.push(...cleanUpSourceTransactionThreadReportOnyxData.optimisticData); @@ -656,6 +665,7 @@ function mergeTransactionRequest({ actionableWhisperReportActionID, resolution: CONST.REPORT.ACTIONABLE_TRACK_EXPENSE_WHISPER_RESOLUTION.NOTHING, shouldRemoveIOUTransaction: false, + transactionThread: sourceIOUActionThreadReport, }); sourceTransactionOptimisticData.push(...optimisticData); diff --git a/src/pages/TransactionDuplicate/DynamicConfirmationPage.tsx b/src/pages/TransactionDuplicate/DynamicConfirmationPage.tsx index 45e5fc66d940..438eee8b5c9e 100644 --- a/src/pages/TransactionDuplicate/DynamicConfirmationPage.tsx +++ b/src/pages/TransactionDuplicate/DynamicConfirmationPage.tsx @@ -67,6 +67,7 @@ function DynamicConfirmationPage() { const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${newTransaction?.reportID}`); const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${newTransaction?.reportID}`); const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); + const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const reportAction = Object.values(reportActions ?? {}).find( (action) => ReportActionsUtils.isMoneyRequestAction(action) && ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID === reviewDuplicates?.transactionID, ); @@ -111,7 +112,15 @@ function DynamicConfirmationPage() { // Suppress the NotFound guard for the discarded thread the server tears down on merge. const keptReportRoute = ROUTES.REPORT_WITH_ID.getRoute(mergeParams.reportID); setDeleteTransactionNavigateBackUrl(keptReportRoute); - mergeDuplicates({...mergeParams, ...taxData, currentUserAccountID, currentUserLogin: currentUserLogin ?? '', allTransactionViolations, allReportActionsList: allReportActions}); + mergeDuplicates({ + ...mergeParams, + ...taxData, + currentUserAccountID, + currentUserLogin: currentUserLogin ?? '', + allTransactionViolations, + allReportActionsList: allReportActions, + allReportsList: allReports, + }); if (isSuperWideRHPDisplayed) { Navigation.dismissToSuperWideRHP(); return; @@ -125,7 +134,7 @@ function DynamicConfirmationPage() { Navigation.dismissModal({ afterTransition: () => Navigation.navigate(keptReportRoute, {forceReplace: true}), }); - }, [childReportID, transactionsMergeParams, taxData, currentUserAccountID, currentUserLogin, isSuperWideRHPDisplayed, allTransactionViolations, allReportActions]); + }, [childReportID, transactionsMergeParams, taxData, currentUserAccountID, currentUserLogin, isSuperWideRHPDisplayed, allTransactionViolations, allReportActions, allReports]); const handleResolveDuplicates = useCallback(() => { resolveDuplicates({...transactionsMergeParams, ...taxData, transactionThreadReportIDMap, allTransactionViolations, allReportActionsList: allReportActions, delegateAccountID}); diff --git a/src/pages/TransactionMerge/DynamicConfirmationPage.tsx b/src/pages/TransactionMerge/DynamicConfirmationPage.tsx index e88b5756c7d5..f6e7628e53ea 100644 --- a/src/pages/TransactionMerge/DynamicConfirmationPage.tsx +++ b/src/pages/TransactionMerge/DynamicConfirmationPage.tsx @@ -28,7 +28,7 @@ import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTop import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {MergeTransactionNavigatorParamList} from '@libs/Navigation/types'; -import {getFilteredReportActionsForReportView, getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; +import {getFilteredReportActionsForReportView, getIOUActionForTransactionID, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {findSelfDMReportID} from '@libs/ReportUtils'; import CONST from '@src/CONST'; @@ -91,8 +91,14 @@ function DynamicConfirmationPage({route}: DynamicConfirmationPageProps) { const [sourceReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(sourceTransaction?.reportID)}`); const sourceIOUAction = sourceTransaction ? getIOUActionForTransactionID(Object.values(sourceReportActions ?? {}), sourceTransaction.transactionID) : undefined; + const selfDMSourceIOUAction = + selfDMReport?.reportID && sourceTransaction ? getIOUActionForTransactionID(Object.values(selfDMReportActions ?? {}), sourceTransaction.transactionID) : undefined; const [sourceTransactionThreadReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(sourceIOUAction?.childReportID)}`); - + const sourceThreadReportID = sourceIOUAction?.childReportID ?? selfDMSourceIOUAction?.childReportID; + const [sourceIOUActionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(sourceThreadReportID)}`); + const sourceIOUReportID = isMoneyRequestAction(sourceIOUAction) ? sourceIOUAction?.reportID : undefined; + const [sourceActionIOUReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(sourceIOUReportID)}`); + const [sourceActionChatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(sourceActionIOUReport?.chatReportID)}`); // Build the merged transaction data for display const mergedTransactionData = buildMergedTransactionData(targetTransaction, mergeTransaction); @@ -134,6 +140,9 @@ function DynamicConfirmationPage({route}: DynamicConfirmationPageProps) { reportPolicyTags, sourceTransactionThreadReportActions, sourceIOUAction, + sourceIOUActionThreadReport, + sourceActionIOUReport, + sourceActionChatReport, }); const reportIDToDismiss = reportID !== CONST.REPORT.UNREPORTED_REPORT_ID ? reportID : undefined; diff --git a/tests/actions/IOUTest/DuplicateTest.ts b/tests/actions/IOUTest/DuplicateTest.ts index dbc2462696c6..d068fd6a1a8c 100644 --- a/tests/actions/IOUTest/DuplicateTest.ts +++ b/tests/actions/IOUTest/DuplicateTest.ts @@ -229,6 +229,9 @@ describe('actions/Duplicate', () => { [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicate1ID}`]: duplicate1Violations, [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicate2ID}`]: duplicate2Violations, }, + allReportsList: { + [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: expenseReport, + }, allReportActionsList: { [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: {action456: iouAction1, action789: iouAction2}, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${childReportID}`]: {}, @@ -341,6 +344,9 @@ describe('actions/Duplicate', () => { [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: {action789: iouAction1}, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${childReportID}`]: passedInChildReportActions, }, + allReportsList: { + [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: expenseReport, + }, }); await waitForBatchedUpdates(); @@ -360,6 +366,100 @@ describe('actions/Duplicate', () => { ); }); + it('threads allReportsList into the transaction-thread cleanup instead of relying on the deprecated reports cache', async () => { + // Given: A duplicate whose expense report and transaction thread are already cached in Onyx + const reportID = 'reportReports'; + const mainTransactionID = 'mainReports'; + const duplicate1ID = 'dupReports'; + const childReportID = 'childReports'; + + const mainTransaction = createMockTransaction(mainTransactionID, reportID); + const duplicateTransaction1 = createMockTransaction(duplicate1ID, reportID, 100); + const mainViolations = createMockViolations(); + const duplicate1Violations = createMockViolations(); + + const iouAction1 = createMockIouAction(duplicate1ID, 'action789', childReportID); + + // The values already cached via the deprecated Onyx.connect-backed reports accessor — + // deliberately different from what's passed via allReportsList below. + const cachedExpenseReport = createMockReport(reportID, 500); + const cachedThreadReport = createMockReport(childReportID, 999); + + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${mainTransactionID}`, mainTransaction); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${duplicate1ID}`, duplicateTransaction1); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, cachedExpenseReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${childReportID}`, cachedThreadReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${mainTransactionID}`, mainViolations); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicate1ID}`, duplicate1Violations); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {action789: iouAction1}); + await waitForBatchedUpdates(); + + // The reports explicitly passed in, distinct from the cached ones above. + const passedExpenseReport = createMockReport(reportID, 200); + const passedThreadReport = createMockReport(childReportID, 42); + + // When: Call mergeDuplicates, passing allReportsList with DIFFERENT values for the source and child reports + mergeDuplicates({ + transactionID: mainTransactionID, + transactionIDList: [duplicate1ID], + created: '2024-01-01 12:00:00', + merchant: 'Updated Merchant', + amount: 200, + currency: CONST.CURRENCY.EUR, + category: 'Travel', + comment: 'Updated comment', + billable: true, + reimbursable: false, + tag: 'UpdatedProject', + taxCode: '', + receiptID: 123, + reportID, + currentUserLogin: RORY_EMAIL, + currentUserAccountID: RORY_ACCOUNT_ID, + allTransactionViolations: { + [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${mainTransactionID}`]: mainViolations, + [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicate1ID}`]: duplicate1Violations, + }, + allReportsList: { + [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: passedExpenseReport, + [`${ONYXKEYS.COLLECTION.REPORT}${childReportID}`]: passedThreadReport, + }, + allReportActionsList: { + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: {action789: iouAction1}, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${childReportID}`]: {}, + }, + }); + await waitForBatchedUpdates(); + + // Then: The source-report total math and the transaction-thread rollback data use the PASSED-IN reports, + // not the stale cached ones — proving mergeDuplicates used the explicit allReportsList slice instead of + // the deprecated reports cache. + expect(writeSpy).toHaveBeenCalledWith( + WRITE_COMMANDS.MERGE_DUPLICATES, + expect.anything(), + expect.objectContaining({ + optimisticData: expect.arrayContaining([ + expect.objectContaining({ + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: expect.objectContaining({total: (passedExpenseReport.total ?? 0) - duplicateTransaction1.amount}), + }), + ]), + }), + ); + expect(writeSpy).toHaveBeenCalledWith( + WRITE_COMMANDS.MERGE_DUPLICATES, + expect.anything(), + expect.objectContaining({ + failureData: expect.arrayContaining([ + expect.objectContaining({ + key: `${ONYXKEYS.COLLECTION.REPORT}${childReportID}`, + value: passedThreadReport, + }), + ]), + }), + ); + }); + it('should handle empty duplicate transaction list', async () => { // Given: Set up test data with only main transaction const reportID = 'report123'; @@ -395,6 +495,9 @@ describe('actions/Duplicate', () => { currentUserLogin: RORY_EMAIL, currentUserAccountID: RORY_ACCOUNT_ID, allTransactionViolations: {[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${mainTransactionID}`]: []}, + allReportsList: { + [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: expenseReport, + }, allReportActionsList: {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: {}}, }); await waitForBatchedUpdates(); @@ -455,6 +558,7 @@ describe('actions/Duplicate', () => { [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${mainTransactionID}`]: [], [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicate1ID}`]: [], }, + allReportsList: {}, allReportActionsList: {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: {}}, }); await waitForBatchedUpdates(); @@ -668,6 +772,15 @@ describe('actions/Duplicate', () => { reportID, }; + // Mirror what the page passes: the full REPORT collection from useOnyx(ONYXKEYS.COLLECTION.REPORT), + // including the transaction thread reports created optimistically above. + const allReportsList = { + [`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`]: chatReport, + [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: expenseReport, + [`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport1.reportID}`]: transactionThreadReport1, + [`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport2.reportID}`]: transactionThreadReport2, + }; + // When: Call mergeDuplicates mergeDuplicates({ ...mergeParams, @@ -678,6 +791,7 @@ describe('actions/Duplicate', () => { [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicate1ID}`]: duplicate1Violations, [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicate2ID}`]: duplicate2Violations, }, + allReportsList, allReportActionsList: {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: {[iouAction1ID]: iouAction1, [iouAction2ID]: iouAction2}}, }); await waitForBatchedUpdates(); @@ -785,6 +899,9 @@ describe('actions/Duplicate', () => { [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${mainTransactionID}`]: mainViolations, [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${duplicate1ID}`]: duplicate1Violations, }, + allReportsList: { + [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: expenseReport, + }, allReportActionsList: {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: {mainAction123: mainIouAction, action456: dupIouAction}}, }); await waitForBatchedUpdates(); @@ -873,6 +990,10 @@ describe('actions/Duplicate', () => { [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${mainTransactionID}`]: mainViolations, [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${crossReportDuplicateID}`]: crossDuplicateViolations, }, + allReportsList: { + [`${ONYXKEYS.COLLECTION.REPORT}${keptReportID}`]: keptReport, + [`${ONYXKEYS.COLLECTION.REPORT}${crossReportID}`]: crossReport, + }, allReportActionsList: { [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${keptReportID}`]: {actionMain: mainIouAction}, [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${crossReportID}`]: {actionCross: crossIouAction}, diff --git a/tests/actions/MergeTransactionTest.ts b/tests/actions/MergeTransactionTest.ts index 793da5bc2965..50d04fcf4814 100644 --- a/tests/actions/MergeTransactionTest.ts +++ b/tests/actions/MergeTransactionTest.ts @@ -202,6 +202,7 @@ function runCrossReportMergeToSourceReportRequest(fixtures: CrossReportMergeToSo policyTags: undefined, policyCategories: undefined, allTransactionViolations: createAllTransactionViolations(targetTransaction.transactionID, sourceTransaction.transactionID, mockViolations, mockViolations), + sourceIOUActionThreadReport: undefined, targetTransactionThreadReport: {reportID: targetReport.reportID}, targetTransactionThreadParentReport: undefined, reportPolicyTags: undefined, @@ -319,6 +320,7 @@ describe('mergeTransactionRequest', () => { mergeTransaction, targetTransaction, sourceTransaction, + sourceIOUActionThreadReport: undefined, targetTransactionThreadReport: {reportID: 'target-report-456'}, targetTransactionThreadParentReport: undefined, reportPolicyTags: undefined, @@ -441,6 +443,7 @@ describe('mergeTransactionRequest', () => { mergeTransaction, targetTransaction, sourceTransaction, + sourceIOUActionThreadReport: undefined, targetTransactionThreadReport: {reportID: targetReportID}, targetTransactionThreadParentReport: undefined, reportPolicyTags: undefined, @@ -546,6 +549,7 @@ describe('mergeTransactionRequest', () => { mergeTransaction, targetTransaction, sourceTransaction, + sourceIOUActionThreadReport: undefined, targetTransactionThreadReport: {reportID: targetExpenseReport.reportID}, targetTransactionThreadParentReport: undefined, reportPolicyTags: undefined, @@ -701,6 +705,7 @@ describe('mergeTransactionRequest', () => { mergeTransaction, targetTransaction, sourceTransaction, + sourceIOUActionThreadReport: undefined, targetTransactionThreadReport: {reportID: 'target-report-456'}, targetTransactionThreadParentReport: undefined, reportPolicyTags: undefined, @@ -811,6 +816,7 @@ describe('mergeTransactionRequest', () => { mergeTransaction, targetTransaction, sourceTransaction, + sourceIOUActionThreadReport: undefined, targetTransactionThreadReport: {reportID: 'target123'}, targetTransactionThreadParentReport: undefined, reportPolicyTags: undefined, @@ -1044,6 +1050,7 @@ describe('mergeTransactionRequest', () => { mergeTransaction, targetTransaction, sourceTransaction, + sourceIOUActionThreadReport: undefined, targetTransactionThreadReport: {reportID: 'target-report-456'}, targetTransactionThreadParentReport: undefined, reportPolicyTags: undefined, @@ -1252,6 +1259,7 @@ describe('mergeTransactionRequest', () => { mergeTransaction, targetTransaction, sourceTransaction, + sourceIOUActionThreadReport: undefined, targetTransactionThreadReport: {reportID: 'target-report-456'}, targetTransactionThreadParentReport: undefined, reportPolicyTags: undefined, @@ -1409,6 +1417,7 @@ describe('mergeTransactionRequest', () => { mergeTransaction, targetTransaction, sourceTransaction, + sourceIOUActionThreadReport: undefined, targetTransactionThreadReport: {reportID: 'target-report-456'}, targetTransactionThreadParentReport: undefined, reportPolicyTags: undefined,