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/Hold.ts b/src/libs/actions/IOU/Hold.ts index d2d1378cf4ab..3a8fc72dd0e0 100644 --- a/src/libs/actions/IOU/Hold.ts +++ b/src/libs/actions/IOU/Hold.ts @@ -49,20 +49,34 @@ import {getAllReports, getAllTransactions} from '.'; /** * Put expense on HOLD */ -function putOnHold( - transactionID: string, - comment: string, - initialReportID: string | undefined, - isOffline: boolean, - currentUserLogin: string, - currentUserAccountID: number, - transactionViolations: OnyxEntry, - isTrackIntentUser: boolean | undefined, - delegateAccountID: number | undefined, - ancestors: Ancestor[] = [], -) { +function putOnHold({ + transactionID, + comment, + initialReportID, + initialReport, + transactionReport, + isOffline, + currentUserLogin, + currentUserAccountID, + transactionViolations, + isTrackIntentUser, + delegateAccountID, + ancestors = [], +}: { + transactionID: string; + comment: string; + initialReportID: string | undefined; + initialReport: OnyxEntry; + transactionReport: OnyxEntry; + isOffline: boolean; + currentUserLogin: string; + currentUserAccountID: number; + transactionViolations: OnyxEntry; + isTrackIntentUser: boolean | undefined; + delegateAccountID: number | undefined; + ancestors?: Ancestor[]; +}) { const allTransactions = getAllTransactions(); - const allReports = getAllReports(); const currentTime = DateUtils.getDBTime(); const reportID = initialReportID ?? generateReportID(); @@ -71,14 +85,14 @@ function putOnHold( const newViolation = {name: CONST.VIOLATIONS.HOLD, type: CONST.VIOLATION_TYPES.VIOLATION, showInReview: true}; const updatedViolations = [...(transactionViolations ?? []), newViolation]; const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`]; + const iouReport = transactionReport; const iouAction = getIOUActionForReportID(transaction?.reportID, transactionID); let transactionThreadReport: OnyxTypes.Report; // If there is no existing transaction thread report, we should create one // This way we ensure every held request has a dedicated thread for comments if (initialReportID) { - transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${initialReportID}`] ?? ({} as OnyxTypes.Report); + transactionThreadReport = initialReport ?? ({} as OnyxTypes.Report); } else { const moneyRequestReport = getReportOrDraftReport(transaction?.reportID); transactionThreadReport = buildTransactionThread(iouAction, moneyRequestReport, currentUserAccountID, undefined, reportID); @@ -341,22 +355,50 @@ function putOnHold( Navigation.setNavigationActionToMicrotaskQueue(() => notifyNewAction(currentReportID, undefined, true)); } -function putTransactionsOnHold( - transactionsID: string[], - comment: string, - reportID: string, - isOffline: boolean, - currentUserLogin: string, - currentUserAccountID: number, - allTransactionViolations: OnyxCollection, - isTrackIntentUser: boolean | undefined, - delegateAccountID: number | undefined, - ancestors: Ancestor[] = [], -) { +function putTransactionsOnHold({ + transactionsID, + allReports, + transactionReports, + comment, + reportID, + isOffline, + currentUserLogin, + currentUserAccountID, + allTransactionViolations, + isTrackIntentUser, + delegateAccountID, + ancestors = [], +}: { + transactionsID: string[]; + allReports: OnyxCollection; + transactionReports: Record>; + comment: string; + reportID: string; + isOffline: boolean; + currentUserLogin: string; + currentUserAccountID: number; + allTransactionViolations: OnyxCollection; + isTrackIntentUser: boolean | undefined; + delegateAccountID: number | undefined; + ancestors?: Ancestor[]; +}) { for (const transactionID of transactionsID) { const {childReportID} = getIOUActionForReportID(reportID, transactionID) ?? {}; const transactionViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; - putOnHold(transactionID, comment, childReportID, isOffline, currentUserLogin, currentUserAccountID, transactionViolations, isTrackIntentUser, delegateAccountID, ancestors); + putOnHold({ + transactionID, + comment, + initialReportID: childReportID, + initialReport: allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${childReportID}`], + transactionReport: transactionReports[transactionID], + isOffline, + currentUserLogin, + currentUserAccountID, + transactionViolations, + isTrackIntentUser, + delegateAccountID, + ancestors, + }); } } 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/Search/SearchHoldReasonPage.tsx b/src/pages/Search/SearchHoldReasonPage.tsx index 1303de3f79b0..fd41d82a5ab8 100644 --- a/src/pages/Search/SearchHoldReasonPage.tsx +++ b/src/pages/Search/SearchHoldReasonPage.tsx @@ -9,6 +9,7 @@ import useDynamicBackPath from '@hooks/useDynamicBackPath'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; +import useTransactionsByID from '@hooks/useTransactionsByID'; import {clearErrorFields, clearErrors} from '@libs/actions/FormActions'; import {putOnHold, putTransactionsOnHold} from '@libs/actions/IOU/Hold'; @@ -24,6 +25,9 @@ import ONYXKEYS from '@src/ONYXKEYS'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/MoneyRequestHoldReasonForm'; +import type {Report} from '@src/types/onyx'; + +import type {OnyxEntry} from 'react-native-onyx'; import {isTrackIntentUserSelector} from '@selectors/Onboarding'; import {transactionViolationsByIDsSelector} from '@selectors/TransactionViolations'; @@ -44,9 +48,23 @@ function SearchHoldReasonPage({route}: SearchHoldReasonPageProps) { const {accountID: currentUserAccountID, login: currentUserLogin} = useCurrentUserPersonalDetails(); const delegateAccountID = useDelegateAccountID(); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const relevantTransactionIDs = useMemo(() => (isBulkHold ? selectedTransactionIDs : Object.keys(selectedTransactions)), [isBulkHold, selectedTransactionIDs, selectedTransactions]); const [selectedTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {selector: transactionViolationsByIDsSelector(relevantTransactionIDs)}); + // selectedTransactionIDs can be different from selectedtransactions so we need to use data from onyx + const [selectedTransactionsOnyx] = useTransactionsByID(selectedTransactionIDs); + const selectedTransactionReports = selectedTransactionsOnyx?.reduce( + (reportCollection, selectedTransaction) => { + if (!selectedTransaction.transactionID) { + return reportCollection; + } + // eslint-disable-next-line no-param-reassign + reportCollection[selectedTransaction.transactionID] = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${selectedTransaction?.reportID}`]; + return reportCollection; + }, + {} as Record>, + ); const {isOffline} = useNetwork(); const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, { selector: isTrackIntentUserSelector, @@ -65,36 +83,41 @@ function SearchHoldReasonPage({route}: SearchHoldReasonPageProps) { return; } if (isBulkHold) { - putTransactionsOnHold( - selectedTransactionIDs, + putTransactionsOnHold({ + transactionsID: selectedTransactionIDs, + allReports, + transactionReports: selectedTransactionReports, comment, reportID, isOffline, - currentUserLogin ?? '', + currentUserLogin: currentUserLogin ?? '', currentUserAccountID, - selectedTransactionViolations, + allTransactionViolations: selectedTransactionViolations, isTrackIntentUser, delegateAccountID, ancestors, - ); + }); clearSelectedTransactions(true); } else { const transactionIDs = Object.keys(selectedTransactions); for (const transactionID of transactionIDs) { const transactionThreadReportID = selectedTransactions[transactionID].reportAction?.childReportID; + const transactionReportID = selectedTransactions[transactionID].transaction?.reportID; const transactionViolations = selectedTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; - putOnHold( + putOnHold({ transactionID, comment, - transactionThreadReportID, + initialReportID: transactionThreadReportID, + initialReport: allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`], + transactionReport: allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionReportID}`], isOffline, - currentUserLogin ?? '', + currentUserLogin: currentUserLogin ?? '', currentUserAccountID, transactionViolations, isTrackIntentUser, delegateAccountID, ancestors, - ); + }); } clearSelectedTransactions(); } @@ -116,6 +139,8 @@ function SearchHoldReasonPage({route}: SearchHoldReasonPageProps) { selectedTransactionViolations, isTrackIntentUser, delegateAccountID, + allReports, + selectedTransactionReports, ], ); 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/src/pages/iou/DynamicHoldReasonPage.tsx b/src/pages/iou/DynamicHoldReasonPage.tsx index 95a803eae7cc..1b70e2a84e1b 100644 --- a/src/pages/iou/DynamicHoldReasonPage.tsx +++ b/src/pages/iou/DynamicHoldReasonPage.tsx @@ -45,6 +45,7 @@ function DynamicHoldReasonPage({route}: DynamicHoldReasonPageProps) { const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${holdReportID}`); const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); + const [transactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`); const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); const {isOffline} = useNetwork(); const ancestors = useAncestors(report); @@ -77,18 +78,20 @@ function DynamicHoldReasonPage({route}: DynamicHoldReasonPageProps) { return; } - putOnHold( + putOnHold({ transactionID, - values.comment, - holdReportID, + comment: values.comment, + initialReportID: holdReportID, + initialReport: report, + transactionReport, isOffline, - currentUserLogin ?? '', + currentUserLogin: currentUserLogin ?? '', currentUserAccountID, transactionViolations, isTrackIntentUser, delegateAccountID, ancestors, - ); + }); Navigation.goBack(backPath); }; 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,