diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 17849830b8dd2..c18d03c4d1580 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -114,7 +114,6 @@ import { canApproveIOU, cancelPayment, canIOUBePaid as canIOUBePaidAction, - getNavigationUrlOnMoneyRequestDelete, payInvoice, payMoneyRequest, reopenReport, @@ -123,6 +122,7 @@ import { submitReport, unapproveExpenseReport, } from '@userActions/IOU'; +import {getNavigationUrlOnMoneyRequestDelete} from '@userActions/IOU/DeleteMoneyRequest'; import {setDeleteTransactionNavigateBackUrl} from '@userActions/Report'; import {markPendingRTERTransactionsAsCash} from '@userActions/Transaction'; import CONST from '@src/CONST'; diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index 1288ee259a488..f2678c433a5b2 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -62,7 +62,7 @@ import ViolationsUtils, {filterReceiptViolations} from '@libs/Violations/Violati import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; import {clearAllRelatedReportActionErrors} from '@userActions/ClearReportActionErrors'; -import {cleanUpMoneyRequest} from '@userActions/IOU'; +import {cleanUpMoneyRequest} from '@userActions/IOU/DeleteMoneyRequest'; import {replaceReceipt} from '@userActions/IOU/Receipt'; import {addAttachmentWithComment, navigateToConciergeChatAndDeleteReport} from '@userActions/Report'; import {clearError, getLastModifiedExpense, revert} from '@userActions/Transaction'; diff --git a/src/hooks/useDeleteTransactions.ts b/src/hooks/useDeleteTransactions.ts index 50ff0926e61a5..9f16064bd6861 100644 --- a/src/hooks/useDeleteTransactions.ts +++ b/src/hooks/useDeleteTransactions.ts @@ -2,7 +2,8 @@ import passthroughPolicyTagListSelector from '@selectors/PolicyTagList'; import {useCallback} from 'react'; import type {OnyxCollection} from 'react-native-onyx'; import {useSearchStateContext} from '@components/Search/SearchContext'; -import {deleteMoneyRequest, getIOURequestPolicyID} from '@libs/actions/IOU'; +import {getIOURequestPolicyID} from '@libs/actions/IOU'; +import {deleteMoneyRequest} from '@libs/actions/IOU/DeleteMoneyRequest'; import {getIOUActionForTransactions} from '@libs/actions/IOU/Duplicate'; import {initSplitExpenseItemData, updateSplitTransactions} from '@libs/actions/IOU/Split'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; diff --git a/src/libs/actions/IOU/DeleteMoneyRequest.ts b/src/libs/actions/IOU/DeleteMoneyRequest.ts new file mode 100644 index 0000000000000..499b55bbc7f1d --- /dev/null +++ b/src/libs/actions/IOU/DeleteMoneyRequest.ts @@ -0,0 +1,917 @@ +import cloneDeep from 'lodash/cloneDeep'; +import {InteractionManager} from 'react-native'; +import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import * as API from '@libs/API'; +import type {DeleteMoneyRequestParams} from '@libs/API/parameters'; +import {WRITE_COMMANDS} from '@libs/API/types'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; +import DateUtils from '@libs/DateUtils'; +import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; +import {updateIOUOwnerAndTotal} from '@libs/IOUUtils'; +import * as Localize from '@libs/Localize'; +import Navigation from '@libs/Navigation/Navigation'; +import {getLastVisibleAction, getLastVisibleMessage, getOriginalMessage, getReportActionMessage, isDeletedAction, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import { + canUserPerformWriteAction as canUserPerformWriteActionReportUtils, + getOutstandingChildRequest, + getPersonalDetailsForAccountID, + getReportTransactions, + hasNonReimbursableTransactions as hasNonReimbursableTransactionsReportUtils, + hasOutstandingChildRequest, + isArchivedReport, + isExpenseReport, + updateOptimisticParentReportAction, +} from '@libs/ReportUtils'; +import {getAmount, getCurrency, isOnHold, removeTransactionFromDuplicateTransactionViolation} from '@libs/TransactionUtils'; +import {clearByKey as clearPdfByOnyxKey} from '@userActions/CachedPDFPaths'; +import {clearAllRelatedReportActionErrors} from '@userActions/ClearReportActionErrors'; +import {optimisticReportLastData} from '@userActions/Report'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Route} from '@src/ROUTES'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; +import type ReportAction from '@src/types/onyx/ReportAction'; +import {getAllReportActionsFromIOU, getAllReportNameValuePairs, getAllReports, getAllTransactions, getAllTransactionViolations, getReportPreviewAction} from '.'; + +type DeleteMoneyRequestFunctionParams = { + transactionID: string | undefined; + reportAction: OnyxTypes.ReportAction; + transactions: OnyxCollection; + violations: OnyxCollection; + iouReport: OnyxEntry; + chatReport: OnyxEntry; + isChatIOUReportArchived?: boolean | undefined; + isSingleTransactionView?: boolean; + transactionIDsPendingDeletion?: string[]; + selectedTransactionIDs?: string[]; + allTransactionViolationsParam: OnyxCollection; + currentUserAccountID: number; + currentUserEmail: string; +}; + +/** + * + * @param transactionID - The transactionID of IOU + * @param reportAction - The reportAction of the transaction in the IOU report + * @return the url to navigate back once the money request is deleted + */ +function prepareToCleanUpMoneyRequest( + transactionID: string, + reportAction: OnyxTypes.ReportAction, + iouReport: OnyxEntry, + chatReport: OnyxEntry, + isChatReportArchived: boolean | undefined, + shouldRemoveIOUTransactionID = true, + transactionIDsPendingDeletion?: string[], + selectedTransactionIDs?: string[], +) { + const allTransactions = getAllTransactions(); + const allTransactionViolations = getAllTransactionViolations(); + const allReports = getAllReports(); + const allReportActions = getAllReportActionsFromIOU(); + + // STEP 1: Get all collections we're updating + const iouReportID = iouReport?.reportID; + const reportPreviewAction = getReportPreviewAction(iouReport?.chatReportID, iouReport?.reportID); + const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + const isTransactionOnHold = isOnHold(transaction); + const transactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; + const transactionThreadID = reportAction.childReportID; + let transactionThread = null; + if (transactionThreadID) { + transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`] ?? null; + } + + // STEP 2: Decide if we need to: + // 1. Delete the transactionThread - delete if there are no visible comments in the thread + // 2. Update the moneyRequestPreview to show [Deleted expense] - update if the transactionThread exists AND it isn't being deleted + // The current state is that we want to get rid of the [Deleted expense] breadcrumb, + // so we never want to display it if transactionThreadID is present. + const shouldDeleteTransactionThread = !!transactionThreadID; + + // STEP 3: Update the IOU reportAction and decide if the iouReport should be deleted. We delete the iouReport if there are no visible comments left in the report. + const updatedReportAction = { + [reportAction.reportActionID]: { + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + previousMessage: reportAction.message, + message: [ + { + type: 'COMMENT', + html: '', + text: '', + isEdited: true, + isDeletedParentAction: shouldDeleteTransactionThread, + }, + ], + originalMessage: { + IOUTransactionID: shouldRemoveIOUTransactionID ? null : transactionID, + }, + errors: null, + }, + } as Record>; + + let canUserPerformWriteAction = true; + if (chatReport) { + canUserPerformWriteAction = !!canUserPerformWriteActionReportUtils(chatReport, isChatReportArchived); + } + // If we are deleting the last transaction on a report, then delete the report too + const shouldDeleteIOUReport = getReportTransactions(iouReportID).filter((trans) => !transactionIDsPendingDeletion?.includes(trans.transactionID)).length === 1; + + const iouReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`]; + if (shouldDeleteIOUReport) { + for (const [reportActionID, reportActionData] of Object.entries(iouReportActions ?? {})) { + if ( + reportAction.reportActionID === reportActionID || + reportActionData.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || + reportActionData.actionName === 'CREATED' || + !reportActionData.message || + isDeletedAction(reportActionData) + ) { + continue; + } + + updatedReportAction[reportActionID] = { + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + previousMessage: reportAction.message, + message: [ + { + type: 'COMMENT', + html: '', + text: '', + isEdited: true, + }, + ], + errors: null, + }; + } + } + // STEP 4: Update the iouReport and reportPreview with new totals and messages if it wasn't deleted + let updatedIOUReport; + const currency = getCurrency(transaction); + const updatedReportPreviewAction: Partial> = cloneDeep(reportPreviewAction ?? {}); + updatedReportPreviewAction.pendingAction = shouldDeleteIOUReport ? CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; + + const transactionPendingDelete = transactionIDsPendingDeletion?.map((id) => allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]); + const selectedTransactions = selectedTransactionIDs?.map((id) => allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]); + const canEditTotal = !selectedTransactions?.some((trans) => getCurrency(trans) !== iouReport?.currency); + const isExpenseReportType = isExpenseReport(iouReport); + const amountDiff = getAmount(transaction, isExpenseReportType) + (transactionPendingDelete?.reduce((prev, curr) => prev + getAmount(curr, isExpenseReportType), 0) ?? 0); + const unheldAmountDiff = + getAmount(transaction, isExpenseReportType) + (transactionPendingDelete?.reduce((prev, curr) => prev + (!isOnHold(curr) ? getAmount(curr, isExpenseReportType) : 0), 0) ?? 0); + + if (iouReport && isExpenseReportType) { + updatedIOUReport = {...iouReport}; + + if (typeof updatedIOUReport.total === 'number' && currency === iouReport?.currency && canEditTotal) { + // Because of the Expense reports are stored as negative values, we add the total from the amount + updatedIOUReport.total += amountDiff; + + if (!transaction?.reimbursable && typeof updatedIOUReport.nonReimbursableTotal === 'number') { + const nonReimbursableAmountDiff = + getAmount(transaction, true) + (transactionPendingDelete?.reduce((prev, curr) => prev + (!curr?.reimbursable ? getAmount(curr, true) : 0), 0) ?? 0); + updatedIOUReport.nonReimbursableTotal += nonReimbursableAmountDiff; + } + + if (!isTransactionOnHold) { + if (typeof updatedIOUReport.unheldTotal === 'number') { + updatedIOUReport.unheldTotal += unheldAmountDiff; + } + + if (!transaction?.reimbursable && typeof updatedIOUReport.unheldNonReimbursableTotal === 'number') { + const unheldNonReimbursableAmountDiff = + getAmount(transaction, true) + + (transactionPendingDelete?.reduce((prev, curr) => prev + (!isOnHold(curr) && !curr?.reimbursable ? getAmount(curr, true) : 0), 0) ?? 0); + updatedIOUReport.unheldNonReimbursableTotal += unheldNonReimbursableAmountDiff; + } + } + } + } else { + updatedIOUReport = + iouReport && !canEditTotal + ? {...iouReport} + : updateIOUOwnerAndTotal(iouReport, reportAction.actorAccountID ?? CONST.DEFAULT_NUMBER_ID, amountDiff, currency, true, false, isTransactionOnHold, unheldAmountDiff); + } + + if (updatedIOUReport) { + const lastVisibleAction = getLastVisibleAction(iouReport?.reportID, canUserPerformWriteAction, updatedReportAction); + const iouReportLastMessageText = getLastVisibleMessage(iouReport?.reportID, canUserPerformWriteAction, updatedReportAction).lastMessageText; + updatedIOUReport.lastMessageText = iouReportLastMessageText; + updatedIOUReport.lastVisibleActionCreated = lastVisibleAction?.created; + } + + const hasNonReimbursableTransactions = hasNonReimbursableTransactionsReportUtils(iouReport?.reportID); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const messageText = Localize.translateLocal( + hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', + convertToDisplayString(updatedIOUReport?.total, updatedIOUReport?.currency), + getPersonalDetailsForAccountID(updatedIOUReport?.managerID ?? CONST.DEFAULT_NUMBER_ID).login ?? '', + ); + + if (getReportActionMessage(updatedReportPreviewAction)) { + if (Array.isArray(updatedReportPreviewAction?.message)) { + const message = updatedReportPreviewAction.message.at(0); + if (message) { + message.text = messageText; + message.html = messageText; + message.deleted = shouldDeleteIOUReport ? DateUtils.getDBTime() : ''; + } + } else if (!Array.isArray(updatedReportPreviewAction.message) && updatedReportPreviewAction.message) { + updatedReportPreviewAction.message.text = messageText; + updatedReportPreviewAction.message.deleted = shouldDeleteIOUReport ? DateUtils.getDBTime() : ''; + } + } + + if (updatedReportPreviewAction && reportPreviewAction?.childMoneyRequestCount && reportPreviewAction?.childMoneyRequestCount > 0) { + updatedReportPreviewAction.childMoneyRequestCount = reportPreviewAction.childMoneyRequestCount - 1; + } + + return { + shouldDeleteTransactionThread, + shouldDeleteIOUReport, + updatedReportAction, + updatedIOUReport, + updatedReportPreviewAction, + transactionThreadID, + transactionThread, + transaction, + transactionViolations, + reportPreviewAction, + iouReportActions, + }; +} + +/** + * Calculate the URL to navigate to after a money request deletion + * @param transactionID - The ID of the money request being deleted + * @param reportAction - The report action associated with the money request + * @param isSingleTransactionView - whether we are in the transaction thread report + * @returns The URL to navigate to + */ +function getNavigationUrlOnMoneyRequestDelete( + transactionID: string | undefined, + reportAction: OnyxTypes.ReportAction, + iouReport: OnyxEntry, + chatReport: OnyxEntry, + isChatReportArchived: boolean | undefined, + isSingleTransactionView = false, +): Route | undefined { + if (!transactionID) { + return undefined; + } + + const {shouldDeleteTransactionThread, shouldDeleteIOUReport} = prepareToCleanUpMoneyRequest(transactionID, reportAction, iouReport, chatReport, isChatReportArchived); + + // Determine which report to navigate back to + if (iouReport && isSingleTransactionView && shouldDeleteTransactionThread && !shouldDeleteIOUReport) { + return ROUTES.REPORT_WITH_ID.getRoute(iouReport.reportID); + } + + if (iouReport?.chatReportID && shouldDeleteIOUReport) { + return ROUTES.REPORT_WITH_ID.getRoute(iouReport.chatReportID); + } + + return undefined; +} + +/** + * + * @param transactionID - The transactionID of IOU + * @param reportAction - The reportAction of the transaction in the IOU report + * @param isSingleTransactionView - whether we are in the transaction thread report + * @return the url to navigate back once the money request is deleted + */ +function cleanUpMoneyRequest( + transactionID: string, + reportAction: OnyxTypes.ReportAction, + reportID: string, + iouReport: OnyxEntry, + chatReport: OnyxEntry, + isChatIOUReportArchived: boolean | undefined, + originalReportID: string | undefined, + isSingleTransactionView = false, +) { + const {shouldDeleteTransactionThread, shouldDeleteIOUReport, updatedReportAction, updatedIOUReport, updatedReportPreviewAction, transactionThreadID, reportPreviewAction} = + prepareToCleanUpMoneyRequest(transactionID, reportAction, iouReport, chatReport, isChatIOUReportArchived, false); + + const urlToNavigateBack = getNavigationUrlOnMoneyRequestDelete(transactionID, reportAction, iouReport, chatReport, isChatIOUReportArchived, isSingleTransactionView); + // build Onyx data + + // Onyx operations to delete the transaction, update the IOU report action and chat report action + const reportActionsOnyxUpdates: Array> = []; + const onyxUpdates: Array< + OnyxUpdate + > = [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: null, + }, + ]; + if (shouldDeleteIOUReport) { + onyxUpdates.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + value: { + [reportAction.reportActionID]: shouldDeleteIOUReport + ? null + : { + pendingAction: null, + }, + }, + }); + } else { + reportActionsOnyxUpdates.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + value: { + [reportAction.reportActionID]: shouldDeleteIOUReport + ? null + : { + pendingAction: null, + }, + }, + }); + } + + if (reportPreviewAction?.reportActionID) { + reportActionsOnyxUpdates.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [reportPreviewAction.reportActionID]: { + ...updatedReportPreviewAction, + pendingAction: null, + errors: null, + }, + }, + }); + } + + // added the operation to delete associated transaction violations + onyxUpdates.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: null, + }); + + // added the operation to delete transaction thread + if (shouldDeleteTransactionThread) { + onyxUpdates.push( + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadID}`, + value: null, + }, + ); + } + + if (shouldDeleteIOUReport) { + onyxUpdates.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + value: updatedReportAction, + }); + } else { + // added operations to update IOU report and chat report + reportActionsOnyxUpdates.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + value: updatedReportAction, + }); + } + onyxUpdates.push( + // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, + value: updatedIOUReport, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + value: getOutstandingChildRequest(updatedIOUReport), + }, + ); + + if (!shouldDeleteIOUReport && updatedReportPreviewAction.childMoneyRequestCount === 0) { + onyxUpdates.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + value: { + hasOutstandingChildRequest: false, + }, + }); + } + + if (shouldDeleteIOUReport) { + let canUserPerformWriteAction = true; + if (chatReport) { + canUserPerformWriteAction = !!canUserPerformWriteActionReportUtils(chatReport, isChatIOUReportArchived); + } + + const lastMessageText = getLastVisibleMessage( + iouReport?.chatReportID, + canUserPerformWriteAction, + reportPreviewAction?.reportActionID ? {[reportPreviewAction.reportActionID]: null} : {}, + )?.lastMessageText; + const lastVisibleActionCreated = getLastVisibleAction( + iouReport?.chatReportID, + canUserPerformWriteAction, + reportPreviewAction?.reportActionID ? {[reportPreviewAction.reportActionID]: null} : {}, + )?.created; + + onyxUpdates.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + value: { + hasOutstandingChildRequest: false, + iouReportID: null, + lastMessageText, + lastVisibleActionCreated, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, + value: null, + }, + ); + } + + if (!shouldDeleteIOUReport) { + clearAllRelatedReportActionErrors(reportID, reportAction, originalReportID); + } + + // First, update the reportActions to ensure related actions are not displayed. + Onyx.update(reportActionsOnyxUpdates).then(() => { + Navigation.goBack(urlToNavigateBack); + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + if (shouldDeleteIOUReport) { + clearAllRelatedReportActionErrors(reportID, reportAction, originalReportID); + } + // After navigation, update the remaining data. + Onyx.update(onyxUpdates); + }); + }); +} + +/** + * @param transactionThreadID - The transaction thread reportID of the transaction + * @param shouldDeleteTransactionThread - Flag indicating whether the transactionThread should be optimistically deleted + * @param reportAction - The IOU action of the transaction + * @return Returns Onyx data including information about deleting the transactionThread and updating the child comment count for the preview report action + */ +function getCleanUpTransactionThreadReportOnyxData({ + transactionThreadID, + shouldDeleteTransactionThread, + reportAction, + isChatIOUReportArchived, + updatedReportPreviewAction, + shouldAddUpdatedReportPreviewActionToOnyxData = true, + currentUserAccountID, +}: { + transactionThreadID?: string; + shouldDeleteTransactionThread: boolean; + reportAction?: ReportAction; + isChatIOUReportArchived?: boolean; + updatedReportPreviewAction?: ReportAction; + shouldAddUpdatedReportPreviewActionToOnyxData?: boolean; + currentUserAccountID: number; +}) { + const allReports = getAllReports(); + const allReportActions = getAllReportActionsFromIOU(); + const allReportNameValuePairs = getAllReportNameValuePairs(); + + const optimisticData: Array> = []; + const successData: Array> = []; + const failureData: Array> = []; + + if (shouldDeleteTransactionThread) { + let transactionThread = null; + let transactionThreadReportActions = null; + if (transactionThreadID) { + transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`] ?? null; + transactionThreadReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadID}`] ?? null; + } + + optimisticData.push( + // Use merge instead of set to avoid deleting the report too quickly, which could cause a brief "not found" page to appear. + // The remaining parts of the report object will be removed after the API call is successful. + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, + value: { + reportID: null, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + participants: { + [currentUserAccountID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + }, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadID}`, + value: null, + }, + ); + if (transactionThread) { + successData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, + value: null, + }); + } + failureData.push( + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, + value: transactionThread ?? null, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadID}`, + value: transactionThreadReportActions, + }, + ); + } + + // Update the child comment visible count for reportPreviewAction. + const iouReportID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUReportID : undefined; + const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]; + const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReport?.chatReportID}`]; + const originalReportPreviewAction = getReportPreviewAction(chatReport?.reportID, iouReport?.reportID) ?? undefined; + let reportPreviewAction = updatedReportPreviewAction ?? originalReportPreviewAction; + if ( + originalReportPreviewAction?.reportActionID && + reportPreviewAction?.reportActionID && + reportPreviewAction?.childVisibleActionCount && + reportPreviewAction?.childVisibleActionCount > 0 && + reportAction?.childVisibleActionCount && + reportAction?.childVisibleActionCount > 0 + ) { + let canUserPerformWriteAction = true; + if (chatReport) { + const reportNameValuePairs = allReportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${iouReport?.reportID}`]; + const isArchivedExpenseReport = isArchivedReport(reportNameValuePairs); + + canUserPerformWriteAction = !!canUserPerformWriteActionReportUtils(chatReport, isChatIOUReportArchived ?? isArchivedExpenseReport); + } + const lastVisibleAction = getLastVisibleAction(iouReportID, canUserPerformWriteAction); + + const {childVisibleActionCount, childCommenterCount, childLastVisibleActionCreated, childOldestFourAccountIDs} = updateOptimisticParentReportAction( + reportPreviewAction, + lastVisibleAction?.childLastVisibleActionCreated ?? lastVisibleAction?.created ?? '', + CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + reportAction.childVisibleActionCount, + ); + + if (shouldAddUpdatedReportPreviewActionToOnyxData) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [reportPreviewAction.reportActionID]: { + childVisibleActionCount, + childCommenterCount, + childLastVisibleActionCreated, + childOldestFourAccountIDs, + }, + }, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [reportPreviewAction.reportActionID]: { + childVisibleActionCount: originalReportPreviewAction.childVisibleActionCount, + childCommenterCount: originalReportPreviewAction.childCommenterCount, + childLastVisibleActionCreated: originalReportPreviewAction.childLastVisibleActionCreated, + childOldestFourAccountIDs: originalReportPreviewAction.childOldestFourAccountIDs, + }, + }, + }); + } + + reportPreviewAction = { + reportActionID: originalReportPreviewAction.reportActionID, + childVisibleActionCount, + childCommenterCount, + childLastVisibleActionCreated, + childOldestFourAccountIDs, + } as ReportAction; + } + + return { + optimisticData, + successData, + failureData, + updatedReportPreviewAction: reportPreviewAction, + }; +} + +/** + * + * @param transactionID - The transactionID of IOU + * @param reportAction - The reportAction of the transaction in the IOU report + * @param isSingleTransactionView - whether we are in the transaction thread report + * @return the url to navigate back once the money request is deleted + */ +function deleteMoneyRequest({ + transactionID, + reportAction, + transactions, + violations, + iouReport, + chatReport, + isChatIOUReportArchived, + isSingleTransactionView = false, + transactionIDsPendingDeletion, + selectedTransactionIDs, + allTransactionViolationsParam, + currentUserAccountID, + currentUserEmail, +}: DeleteMoneyRequestFunctionParams) { + if (!transactionID) { + return; + } + + // STEP 1: Calculate and prepare the data + const { + shouldDeleteTransactionThread, + shouldDeleteIOUReport, + updatedReportAction, + updatedIOUReport, + updatedReportPreviewAction, + transactionThreadID, + transaction, + transactionViolations, + reportPreviewAction, + iouReportActions, + } = prepareToCleanUpMoneyRequest(transactionID, reportAction, iouReport, chatReport, isChatIOUReportArchived, false, transactionIDsPendingDeletion, selectedTransactionIDs); + + const urlToNavigateBack = getNavigationUrlOnMoneyRequestDelete(transactionID, reportAction, iouReport, chatReport, isChatIOUReportArchived, isSingleTransactionView); + + // STEP 2: Build Onyx data + // The logic mostly resembles the cleanUpMoneyRequest function + const optimisticData: Array< + OnyxUpdate + > = [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: {...transaction, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, + }, + ]; + + optimisticData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: null, + }); + + const failureData: Array< + OnyxUpdate + > = [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: {...transaction, pendingAction: null}, + }, + ]; + + removeTransactionFromDuplicateTransactionViolation({optimisticData, failureData}, transactionID, transactions, violations); + + optimisticData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + value: updatedReportAction, + }, + // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, + value: updatedIOUReport, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + value: getOutstandingChildRequest(updatedIOUReport), + }, + ); + + if (reportPreviewAction?.reportActionID) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: {[reportPreviewAction.reportActionID]: updatedReportPreviewAction}, + }); + } + + if (chatReport && updatedIOUReport && !shouldDeleteIOUReport && updatedReportPreviewAction?.childMoneyRequestCount === 0) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + value: { + hasOutstandingChildRequest: hasOutstandingChildRequest(chatReport, updatedIOUReport, currentUserEmail, currentUserAccountID, allTransactionViolationsParam, undefined), + }, + }); + } + + if (shouldDeleteIOUReport) { + let canUserPerformWriteAction = true; + if (chatReport) { + canUserPerformWriteAction = !!canUserPerformWriteActionReportUtils(chatReport, isChatIOUReportArchived); + } + + const optimisticReportActions = reportPreviewAction?.reportActionID ? {[reportPreviewAction.reportActionID]: null} : {}; + const optimisticLastReportData = optimisticReportLastData(iouReport?.chatReportID ?? String(CONST.DEFAULT_NUMBER_ID), optimisticReportActions, canUserPerformWriteAction); + + if (chatReport) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + value: { + hasOutstandingChildRequest: hasOutstandingChildRequest(chatReport, iouReport?.reportID, currentUserEmail, currentUserAccountID, allTransactionViolationsParam, undefined), + iouReportID: null, + ...optimisticLastReportData, + }, + }); + } + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, + value: { + reportID: null, + pendingFields: { + preview: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + }, + }, + }); + } + + const cleanUpTransactionThreadReportOnyxData = getCleanUpTransactionThreadReportOnyxData({ + shouldDeleteTransactionThread, + transactionThreadID, + reportAction, + isChatIOUReportArchived, + currentUserAccountID, + }); + optimisticData.push(...cleanUpTransactionThreadReportOnyxData.optimisticData); + + const successData: Array> = [ + shouldDeleteIOUReport + ? { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + value: null, + } + : { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + value: { + [reportAction.reportActionID]: { + pendingAction: null, + }, + }, + }, + ]; + + if (reportPreviewAction?.reportActionID) { + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [reportPreviewAction.reportActionID]: { + pendingAction: null, + errors: null, + }, + }, + }); + } + + // Ensure that any remaining data is removed upon successful completion, even if the server sends a report removal response. + // This is done to prevent the removal update from lingering in the applyHTTPSOnyxUpdates function. + successData.push(...cleanUpTransactionThreadReportOnyxData.successData); + + if (shouldDeleteIOUReport) { + successData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, + value: null, + }); + } + + successData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: null, + }); + + failureData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, + value: transactionViolations ?? null, + }); + + failureData.push(...cleanUpTransactionThreadReportOnyxData.failureData); + + const errorKey = DateUtils.getMicroseconds(); + + const originalReportActionsUpdate = {} as Record>; + if (shouldDeleteIOUReport) { + for (const action of Object.values(iouReportActions ?? {})) { + if (action.reportActionID === reportAction.reportActionID) { + continue; + } + originalReportActionsUpdate[action.reportActionID] = { + pendingAction: action.pendingAction ?? null, + message: action.message, + }; + } + } + failureData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + value: { + ...originalReportActionsUpdate, + [reportAction.reportActionID]: { + ...reportAction, + pendingAction: null, + errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericDeleteFailureMessage', errorKey), + }, + }, + }, + // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 + shouldDeleteIOUReport + ? { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, + value: iouReport, + } + : { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, + value: iouReport, + }, + ); + + if (reportPreviewAction?.reportActionID) { + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, + value: { + [reportPreviewAction.reportActionID]: { + ...reportPreviewAction, + pendingAction: null, + errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericDeleteFailureMessage', errorKey), + }, + }, + }); + } + + if (chatReport && shouldDeleteIOUReport) { + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, + value: chatReport, + }); + } + + if (!shouldDeleteIOUReport && updatedReportPreviewAction?.childMoneyRequestCount === 0) { + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, + value: { + hasOutstandingChildRequest: true, + }, + }); + } + + const parameters: DeleteMoneyRequestParams = { + transactionID, + reportActionID: reportAction.reportActionID, + }; + + // STEP 3: Make the API request + API.write(WRITE_COMMANDS.DELETE_MONEY_REQUEST, parameters, {optimisticData, successData, failureData}); + clearPdfByOnyxKey(transactionID); + + return urlToNavigateBack; +} + +export {cleanUpMoneyRequest, deleteMoneyRequest, getNavigationUrlOnMoneyRequestDelete, getCleanUpTransactionThreadReportOnyxData}; diff --git a/src/libs/actions/IOU/Duplicate.ts b/src/libs/actions/IOU/Duplicate.ts index c7cb36b7b74f2..436a7d8da47e0 100644 --- a/src/libs/actions/IOU/Duplicate.ts +++ b/src/libs/actions/IOU/Duplicate.ts @@ -48,11 +48,11 @@ import { getAllReports, getAllTransactions, getAllTransactionViolations, - getCleanUpTransactionThreadReportOnyxData, getCurrentUserEmail, getMoneyRequestParticipantsFromReport, getUserAccountID, } from '.'; +import {getCleanUpTransactionThreadReportOnyxData} from './DeleteMoneyRequest'; import type {PerDiemExpenseInformation} from './PerDiem'; import {submitPerDiemExpense} from './PerDiem'; import type {CreateTrackExpenseParams} from './TrackExpense'; diff --git a/src/libs/actions/IOU/Split.ts b/src/libs/actions/IOU/Split.ts index 5608c776a8ada..95ce6db0ffec4 100644 --- a/src/libs/actions/IOU/Split.ts +++ b/src/libs/actions/IOU/Split.ts @@ -89,6 +89,7 @@ import type RecentlyUsedTags from '@src/types/onyx/RecentlyUsedTags'; import type {OnyxData} from '@src/types/onyx/Request'; import type {SplitShares, TransactionChanges, TransactionCustomUnit} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {getCleanUpTransactionThreadReportOnyxData} from './DeleteMoneyRequest'; import { buildMinimalTransactionForFormula, buildOnyxDataForMoneyRequest, @@ -97,7 +98,6 @@ import { getAllPersonalDetails, getAllReports, getAllTransactions, - getCleanUpTransactionThreadReportOnyxData, getMoneyRequestInformation, getMoneyRequestParticipantsFromReport, getOrCreateOptimisticSplitChatReport, diff --git a/src/libs/actions/IOU/TrackExpense.ts b/src/libs/actions/IOU/TrackExpense.ts index 65d0cc761299f..17194390d8052 100644 --- a/src/libs/actions/IOU/TrackExpense.ts +++ b/src/libs/actions/IOU/TrackExpense.ts @@ -100,18 +100,16 @@ import type ReportAction from '@src/types/onyx/ReportAction'; import type {OnyxData} from '@src/types/onyx/Request'; import type {Receipt, ReceiptSource} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {deleteMoneyRequest, getCleanUpTransactionThreadReportOnyxData, getNavigationUrlOnMoneyRequestDelete} from './DeleteMoneyRequest'; import type {BuildOnyxDataForMoneyRequestKeys, ReplaceReceipt, RequestMoneyInformation, StartSplitBilActionParams} from './index'; import { buildMinimalTransactionForFormula, - deleteMoneyRequest, getAllReports, getAllTransactionDrafts, getAllTransactions, getAllTransactionViolations, - getCleanUpTransactionThreadReportOnyxData, getCurrentUserEmail, getMoneyRequestInformation, - getNavigationUrlOnMoneyRequestDelete, getReceiptError, getReportPreviewAction, getSearchOnyxUpdate, diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 66541dbfb16d7..ccb640c651879 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -1,7 +1,6 @@ /* eslint-disable max-lines */ import {format} from 'date-fns'; import {fastMerge} from 'expensify-common'; -import cloneDeep from 'lodash/cloneDeep'; // eslint-disable-next-line you-dont-need-lodash-underscore/union-by import lodashUnionBy from 'lodash/unionBy'; import {InteractionManager} from 'react-native'; @@ -17,7 +16,6 @@ import type { ApproveMoneyRequestParams, AssignReportToMeParams, CreateDistanceRequestParams, - DeleteMoneyRequestParams, MarkTransactionViolationAsResolvedParams, PayInvoiceParams, PayMoneyRequestParams, @@ -31,7 +29,7 @@ import type { UpdateMoneyRequestParams, } from '@libs/API/parameters'; import {WRITE_COMMANDS} from '@libs/API/types'; -import {convertToBackendAmount, convertToDisplayString, getCurrencyDecimals} from '@libs/CurrencyUtils'; +import {convertToBackendAmount, getCurrencyDecimals} from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; import {registerDeferredWrite} from '@libs/deferredLayoutWrite'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; @@ -74,11 +72,8 @@ import { getAllReportActions, getIOUActionForReportID, getIOUActionForTransactionID, - getLastVisibleAction, - getLastVisibleMessage, getOriginalMessage, getReportActionHtml, - getReportActionMessage, getReportActionText, hasPendingDEWApprove, isCreatedAction, @@ -114,7 +109,6 @@ import { canBeAutoReimbursed, canEditFieldOfMoneyRequest, canSubmitAndIsAwaitingForCurrentUser, - canUserPerformWriteAction as canUserPerformWriteActionReportUtils, findSelfDMReportID, generateReportID, getAllHeldTransactions as getAllHeldTransactionsReportUtils, @@ -125,13 +119,11 @@ import { getNextApproverAccountID, getOutstandingChildRequest, getParsedComment, - getPersonalDetailsForAccountID, getReportNotificationPreference, getReportOrDraftReport, getReportTransactions, getTransactionDetails, hasHeldExpenses as hasHeldExpensesReportUtils, - hasNonReimbursableTransactions as hasNonReimbursableTransactionsReportUtils, hasOnlyNonReimbursableTransactions, hasOutstandingChildRequest, hasViolations as hasViolationsReportUtils, @@ -165,7 +157,6 @@ import { populateOptimisticReportFormula, shouldCreateNewMoneyRequestReport as shouldCreateNewMoneyRequestReportReportUtils, shouldEnableNegative, - updateOptimisticParentReportAction, updateReportPreview, } from '@libs/ReportUtils'; import {buildCannedSearchQuery, buildSearchQueryJSON, buildSearchQueryString, getCurrentSearchQueryJSON} from '@libs/SearchQueryUtils'; @@ -201,16 +192,13 @@ import { isScanning, isScanRequest as isScanRequestTransactionUtils, isTimeRequest as isTimeRequestTransactionUtils, - removeTransactionFromDuplicateTransactionViolation, } from '@libs/TransactionUtils'; import type {AvatarSource} from '@libs/UserAvatarUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; -import {clearByKey as clearPdfByOnyxKey} from '@userActions/CachedPDFPaths'; -import {clearAllRelatedReportActionErrors} from '@userActions/ClearReportActionErrors'; import {buildPolicyData, generatePolicyID} from '@userActions/Policy/Policy'; import type {BuildPolicyDataKeys} from '@userActions/Policy/Policy'; import {buildOptimisticPolicyRecentlyUsedTags} from '@userActions/Policy/Tag'; -import {completeOnboarding, createTransactionThreadReport, notifyNewAction, optimisticReportLastData} from '@userActions/Report'; +import {completeOnboarding, createTransactionThreadReport, notifyNewAction} from '@userActions/Report'; import {mergeTransactionIdsHighlightOnSearchRoute, sanitizeWaypointsForAPI, stringifyWaypointsForAPI} from '@userActions/Transaction'; import {getRemoveDraftTransactionsByIDsData, removeDraftTransaction, removeDraftTransactionsByIDs} from '@userActions/TransactionEdit'; import {getOnboardingMessages} from '@userActions/Welcome/OnboardingFlow'; @@ -610,22 +598,6 @@ type GetSearchOnyxUpdateParams = { transactionThreadReportID: string | undefined; }; -type DeleteMoneyRequestFunctionParams = { - transactionID: string | undefined; - reportAction: OnyxTypes.ReportAction; - transactions: OnyxCollection; - violations: OnyxCollection; - iouReport: OnyxEntry; - chatReport: OnyxEntry; - isChatIOUReportArchived?: boolean | undefined; - isSingleTransactionView?: boolean; - transactionIDsPendingDeletion?: string[]; - selectedTransactionIDs?: string[]; - allTransactionViolationsParam: OnyxCollection; - currentUserAccountID: number; - currentUserEmail: string; -}; - type PayMoneyRequestFunctionParams = { paymentType: PaymentMethodType; chatReport: OnyxTypes.Report; @@ -809,6 +781,10 @@ function getAllReportActionsFromIOU(): OnyxCollection { return allReportActions; } +function getAllReportNameValuePairs(): OnyxCollection { + return allReportNameValuePairs; +} + function getAllTransactionDrafts(): NonNullable> { return allTransactionDrafts; } @@ -4554,860 +4530,6 @@ function createDistanceRequest(distanceRequestInformation: CreateDistanceRequest return {iouReport: distanceIouReport}; } -/** - * - * @param transactionID - The transactionID of IOU - * @param reportAction - The reportAction of the transaction in the IOU report - * @return the url to navigate back once the money request is deleted - */ -function prepareToCleanUpMoneyRequest( - transactionID: string, - reportAction: OnyxTypes.ReportAction, - iouReport: OnyxEntry, - chatReport: OnyxEntry, - isChatReportArchived: boolean | undefined, - shouldRemoveIOUTransactionID = true, - transactionIDsPendingDeletion?: string[], - selectedTransactionIDs?: string[], -) { - // STEP 1: Get all collections we're updating - const iouReportID = iouReport?.reportID; - const reportPreviewAction = getReportPreviewAction(iouReport?.chatReportID, iouReport?.reportID); - const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - const isTransactionOnHold = isOnHold(transaction); - const transactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]; - const transactionThreadID = reportAction.childReportID; - let transactionThread = null; - if (transactionThreadID) { - transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`] ?? null; - } - - // STEP 2: Decide if we need to: - // 1. Delete the transactionThread - delete if there are no visible comments in the thread - // 2. Update the moneyRequestPreview to show [Deleted expense] - update if the transactionThread exists AND it isn't being deleted - // The current state is that we want to get rid of the [Deleted expense] breadcrumb, - // so we never want to display it if transactionThreadID is present. - const shouldDeleteTransactionThread = !!transactionThreadID; - - // STEP 3: Update the IOU reportAction and decide if the iouReport should be deleted. We delete the iouReport if there are no visible comments left in the report. - const updatedReportAction = { - [reportAction.reportActionID]: { - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - previousMessage: reportAction.message, - message: [ - { - type: 'COMMENT', - html: '', - text: '', - isEdited: true, - isDeletedParentAction: shouldDeleteTransactionThread, - }, - ], - originalMessage: { - IOUTransactionID: shouldRemoveIOUTransactionID ? null : transactionID, - }, - errors: null, - }, - } as Record>; - - let canUserPerformWriteAction = true; - if (chatReport) { - canUserPerformWriteAction = !!canUserPerformWriteActionReportUtils(chatReport, isChatReportArchived); - } - // If we are deleting the last transaction on a report, then delete the report too - const shouldDeleteIOUReport = getReportTransactions(iouReportID).filter((trans) => !transactionIDsPendingDeletion?.includes(trans.transactionID)).length === 1; - - const iouReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`]; - if (shouldDeleteIOUReport) { - for (const [reportActionID, reportActionData] of Object.entries(iouReportActions ?? {})) { - if ( - reportAction.reportActionID === reportActionID || - reportActionData.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || - reportActionData.actionName === 'CREATED' || - !reportActionData.message || - isDeletedAction(reportActionData) - ) { - continue; - } - - updatedReportAction[reportActionID] = { - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - previousMessage: reportAction.message, - message: [ - { - type: 'COMMENT', - html: '', - text: '', - isEdited: true, - }, - ], - errors: null, - }; - } - } - // STEP 4: Update the iouReport and reportPreview with new totals and messages if it wasn't deleted - let updatedIOUReport; - const currency = getCurrency(transaction); - const updatedReportPreviewAction: Partial> = cloneDeep(reportPreviewAction ?? {}); - updatedReportPreviewAction.pendingAction = shouldDeleteIOUReport ? CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; - - const transactionPendingDelete = transactionIDsPendingDeletion?.map((id) => allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]); - const selectedTransactions = selectedTransactionIDs?.map((id) => allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]); - const canEditTotal = !selectedTransactions?.some((trans) => getCurrency(trans) !== iouReport?.currency); - const isExpenseReportType = isExpenseReport(iouReport); - const amountDiff = getAmount(transaction, isExpenseReportType) + (transactionPendingDelete?.reduce((prev, curr) => prev + getAmount(curr, isExpenseReportType), 0) ?? 0); - const unheldAmountDiff = - getAmount(transaction, isExpenseReportType) + (transactionPendingDelete?.reduce((prev, curr) => prev + (!isOnHold(curr) ? getAmount(curr, isExpenseReportType) : 0), 0) ?? 0); - - if (iouReport && isExpenseReportType) { - updatedIOUReport = {...iouReport}; - - if (typeof updatedIOUReport.total === 'number' && currency === iouReport?.currency && canEditTotal) { - // Because of the Expense reports are stored as negative values, we add the total from the amount - updatedIOUReport.total += amountDiff; - - if (!transaction?.reimbursable && typeof updatedIOUReport.nonReimbursableTotal === 'number') { - const nonReimbursableAmountDiff = - getAmount(transaction, true) + (transactionPendingDelete?.reduce((prev, curr) => prev + (!curr?.reimbursable ? getAmount(curr, true) : 0), 0) ?? 0); - updatedIOUReport.nonReimbursableTotal += nonReimbursableAmountDiff; - } - - if (!isTransactionOnHold) { - if (typeof updatedIOUReport.unheldTotal === 'number') { - updatedIOUReport.unheldTotal += unheldAmountDiff; - } - - if (!transaction?.reimbursable && typeof updatedIOUReport.unheldNonReimbursableTotal === 'number') { - const unheldNonReimbursableAmountDiff = - getAmount(transaction, true) + - (transactionPendingDelete?.reduce((prev, curr) => prev + (!isOnHold(curr) && !curr?.reimbursable ? getAmount(curr, true) : 0), 0) ?? 0); - updatedIOUReport.unheldNonReimbursableTotal += unheldNonReimbursableAmountDiff; - } - } - } - } else { - updatedIOUReport = - iouReport && !canEditTotal - ? {...iouReport} - : updateIOUOwnerAndTotal(iouReport, reportAction.actorAccountID ?? CONST.DEFAULT_NUMBER_ID, amountDiff, currency, true, false, isTransactionOnHold, unheldAmountDiff); - } - - if (updatedIOUReport) { - const lastVisibleAction = getLastVisibleAction(iouReport?.reportID, canUserPerformWriteAction, updatedReportAction); - const iouReportLastMessageText = getLastVisibleMessage(iouReport?.reportID, canUserPerformWriteAction, updatedReportAction).lastMessageText; - updatedIOUReport.lastMessageText = iouReportLastMessageText; - updatedIOUReport.lastVisibleActionCreated = lastVisibleAction?.created; - } - - const hasNonReimbursableTransactions = hasNonReimbursableTransactionsReportUtils(iouReport?.reportID); - // eslint-disable-next-line @typescript-eslint/no-deprecated - const messageText = Localize.translateLocal( - hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', - convertToDisplayString(updatedIOUReport?.total, updatedIOUReport?.currency), - getPersonalDetailsForAccountID(updatedIOUReport?.managerID ?? CONST.DEFAULT_NUMBER_ID).login ?? '', - ); - - if (getReportActionMessage(updatedReportPreviewAction)) { - if (Array.isArray(updatedReportPreviewAction?.message)) { - const message = updatedReportPreviewAction.message.at(0); - if (message) { - message.text = messageText; - message.html = messageText; - message.deleted = shouldDeleteIOUReport ? DateUtils.getDBTime() : ''; - } - } else if (!Array.isArray(updatedReportPreviewAction.message) && updatedReportPreviewAction.message) { - updatedReportPreviewAction.message.text = messageText; - updatedReportPreviewAction.message.deleted = shouldDeleteIOUReport ? DateUtils.getDBTime() : ''; - } - } - - if (updatedReportPreviewAction && reportPreviewAction?.childMoneyRequestCount && reportPreviewAction?.childMoneyRequestCount > 0) { - updatedReportPreviewAction.childMoneyRequestCount = reportPreviewAction.childMoneyRequestCount - 1; - } - - return { - shouldDeleteTransactionThread, - shouldDeleteIOUReport, - updatedReportAction, - updatedIOUReport, - updatedReportPreviewAction, - transactionThreadID, - transactionThread, - transaction, - transactionViolations, - reportPreviewAction, - iouReportActions, - }; -} - -/** - * Calculate the URL to navigate to after a money request deletion - * @param transactionID - The ID of the money request being deleted - * @param reportAction - The report action associated with the money request - * @param isSingleTransactionView - whether we are in the transaction thread report - * @returns The URL to navigate to - */ -function getNavigationUrlOnMoneyRequestDelete( - transactionID: string | undefined, - reportAction: OnyxTypes.ReportAction, - iouReport: OnyxEntry, - chatReport: OnyxEntry, - isChatReportArchived: boolean | undefined, - isSingleTransactionView = false, -): Route | undefined { - if (!transactionID) { - return undefined; - } - - const {shouldDeleteTransactionThread, shouldDeleteIOUReport} = prepareToCleanUpMoneyRequest(transactionID, reportAction, iouReport, chatReport, isChatReportArchived); - - // Determine which report to navigate back to - if (iouReport && isSingleTransactionView && shouldDeleteTransactionThread && !shouldDeleteIOUReport) { - return ROUTES.REPORT_WITH_ID.getRoute(iouReport.reportID); - } - - if (iouReport?.chatReportID && shouldDeleteIOUReport) { - return ROUTES.REPORT_WITH_ID.getRoute(iouReport.chatReportID); - } - - return undefined; -} - -/** - * - * @param transactionID - The transactionID of IOU - * @param reportAction - The reportAction of the transaction in the IOU report - * @param isSingleTransactionView - whether we are in the transaction thread report - * @return the url to navigate back once the money request is deleted - */ -function cleanUpMoneyRequest( - transactionID: string, - reportAction: OnyxTypes.ReportAction, - reportID: string, - iouReport: OnyxEntry, - chatReport: OnyxEntry, - isChatIOUReportArchived: boolean | undefined, - originalReportID: string | undefined, - isSingleTransactionView = false, -) { - const {shouldDeleteTransactionThread, shouldDeleteIOUReport, updatedReportAction, updatedIOUReport, updatedReportPreviewAction, transactionThreadID, reportPreviewAction} = - prepareToCleanUpMoneyRequest(transactionID, reportAction, iouReport, chatReport, isChatIOUReportArchived, false); - - const urlToNavigateBack = getNavigationUrlOnMoneyRequestDelete(transactionID, reportAction, iouReport, chatReport, isChatIOUReportArchived, isSingleTransactionView); - // build Onyx data - - // Onyx operations to delete the transaction, update the IOU report action and chat report action - const reportActionsOnyxUpdates: Array> = []; - const onyxUpdates: Array< - OnyxUpdate - > = [ - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: null, - }, - ]; - if (shouldDeleteIOUReport) { - onyxUpdates.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, - value: { - [reportAction.reportActionID]: shouldDeleteIOUReport - ? null - : { - pendingAction: null, - }, - }, - }); - } else { - reportActionsOnyxUpdates.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, - value: { - [reportAction.reportActionID]: shouldDeleteIOUReport - ? null - : { - pendingAction: null, - }, - }, - }); - } - - if (reportPreviewAction?.reportActionID) { - reportActionsOnyxUpdates.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, - value: { - [reportPreviewAction.reportActionID]: { - ...updatedReportPreviewAction, - pendingAction: null, - errors: null, - }, - }, - }); - } - - // added the operation to delete associated transaction violations - onyxUpdates.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: null, - }); - - // added the operation to delete transaction thread - if (shouldDeleteTransactionThread) { - onyxUpdates.push( - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, - value: null, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadID}`, - value: null, - }, - ); - } - - if (shouldDeleteIOUReport) { - onyxUpdates.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, - value: updatedReportAction, - }); - } else { - // added operations to update IOU report and chat report - reportActionsOnyxUpdates.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, - value: updatedReportAction, - }); - } - onyxUpdates.push( - // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, - value: updatedIOUReport, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, - value: getOutstandingChildRequest(updatedIOUReport), - }, - ); - - if (!shouldDeleteIOUReport && updatedReportPreviewAction.childMoneyRequestCount === 0) { - onyxUpdates.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, - value: { - hasOutstandingChildRequest: false, - }, - }); - } - - if (shouldDeleteIOUReport) { - let canUserPerformWriteAction = true; - if (chatReport) { - canUserPerformWriteAction = !!canUserPerformWriteActionReportUtils(chatReport, isChatIOUReportArchived); - } - - const lastMessageText = getLastVisibleMessage( - iouReport?.chatReportID, - canUserPerformWriteAction, - reportPreviewAction?.reportActionID ? {[reportPreviewAction.reportActionID]: null} : {}, - )?.lastMessageText; - const lastVisibleActionCreated = getLastVisibleAction( - iouReport?.chatReportID, - canUserPerformWriteAction, - reportPreviewAction?.reportActionID ? {[reportPreviewAction.reportActionID]: null} : {}, - )?.created; - - onyxUpdates.push( - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, - value: { - hasOutstandingChildRequest: false, - iouReportID: null, - lastMessageText, - lastVisibleActionCreated, - }, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, - value: null, - }, - ); - } - - if (!shouldDeleteIOUReport) { - clearAllRelatedReportActionErrors(reportID, reportAction, originalReportID); - } - - // First, update the reportActions to ensure related actions are not displayed. - Onyx.update(reportActionsOnyxUpdates).then(() => { - Navigation.goBack(urlToNavigateBack); - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - if (shouldDeleteIOUReport) { - clearAllRelatedReportActionErrors(reportID, reportAction, originalReportID); - } - // After navigation, update the remaining data. - Onyx.update(onyxUpdates); - }); - }); -} - -/** - * @param transactionThreadID - The transaction thread reportID of the transaction - * @param shouldDeleteTransactionThread - Flag indicating whether the transactionThread should be optimistically deleted - * @param reportAction - The IOU action of the transaction - * @return Returns Onyx data including information about deleting the transactionThread and updating the child comment count for the preview report action - */ -function getCleanUpTransactionThreadReportOnyxData({ - transactionThreadID, - shouldDeleteTransactionThread, - reportAction, - isChatIOUReportArchived, - updatedReportPreviewAction, - shouldAddUpdatedReportPreviewActionToOnyxData = true, - currentUserAccountID, -}: { - transactionThreadID?: string; - shouldDeleteTransactionThread: boolean; - reportAction?: ReportAction; - isChatIOUReportArchived?: boolean; - updatedReportPreviewAction?: ReportAction; - shouldAddUpdatedReportPreviewActionToOnyxData?: boolean; - currentUserAccountID: number; -}) { - const optimisticData: Array> = []; - const successData: Array> = []; - const failureData: Array> = []; - - if (shouldDeleteTransactionThread) { - let transactionThread = null; - let transactionThreadReportActions = null; - if (transactionThreadID) { - transactionThread = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`] ?? null; - transactionThreadReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadID}`] ?? null; - } - - optimisticData.push( - // Use merge instead of set to avoid deleting the report too quickly, which could cause a brief "not found" page to appear. - // The remaining parts of the report object will be removed after the API call is successful. - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, - value: { - reportID: null, - stateNum: CONST.REPORT.STATE_NUM.APPROVED, - statusNum: CONST.REPORT.STATUS_NUM.CLOSED, - participants: { - [currentUserAccountID]: { - notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, - }, - }, - }, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadID}`, - value: null, - }, - ); - if (transactionThread) { - successData.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, - value: null, - }); - } - failureData.push( - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${transactionThreadID}`, - value: transactionThread ?? null, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadID}`, - value: transactionThreadReportActions, - }, - ); - } - - // Update the child comment visible count for reportPreviewAction. - const iouReportID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUReportID : undefined; - const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`]; - const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReport?.chatReportID}`]; - const originalReportPreviewAction = getReportPreviewAction(chatReport?.reportID, iouReport?.reportID) ?? undefined; - let reportPreviewAction = updatedReportPreviewAction ?? originalReportPreviewAction; - if ( - originalReportPreviewAction?.reportActionID && - reportPreviewAction?.reportActionID && - reportPreviewAction?.childVisibleActionCount && - reportPreviewAction?.childVisibleActionCount > 0 && - reportAction?.childVisibleActionCount && - reportAction?.childVisibleActionCount > 0 - ) { - let canUserPerformWriteAction = true; - if (chatReport) { - const reportNameValuePairs = allReportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${iouReport?.reportID}`]; - const isArchivedExpenseReport = isArchivedReport(reportNameValuePairs); - - canUserPerformWriteAction = !!canUserPerformWriteActionReportUtils(chatReport, isChatIOUReportArchived ?? isArchivedExpenseReport); - } - const lastVisibleAction = getLastVisibleAction(iouReportID, canUserPerformWriteAction); - - const {childVisibleActionCount, childCommenterCount, childLastVisibleActionCreated, childOldestFourAccountIDs} = updateOptimisticParentReportAction( - reportPreviewAction, - lastVisibleAction?.childLastVisibleActionCreated ?? lastVisibleAction?.created ?? '', - CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - reportAction.childVisibleActionCount, - ); - - if (shouldAddUpdatedReportPreviewActionToOnyxData) { - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, - value: { - [reportPreviewAction.reportActionID]: { - childVisibleActionCount, - childCommenterCount, - childLastVisibleActionCreated, - childOldestFourAccountIDs, - }, - }, - }); - - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, - value: { - [reportPreviewAction.reportActionID]: { - childVisibleActionCount: originalReportPreviewAction.childVisibleActionCount, - childCommenterCount: originalReportPreviewAction.childCommenterCount, - childLastVisibleActionCreated: originalReportPreviewAction.childLastVisibleActionCreated, - childOldestFourAccountIDs: originalReportPreviewAction.childOldestFourAccountIDs, - }, - }, - }); - } - - reportPreviewAction = { - reportActionID: originalReportPreviewAction.reportActionID, - childVisibleActionCount, - childCommenterCount, - childLastVisibleActionCreated, - childOldestFourAccountIDs, - } as ReportAction; - } - - return { - optimisticData, - successData, - failureData, - updatedReportPreviewAction: reportPreviewAction, - }; -} - -/** - * - * @param transactionID - The transactionID of IOU - * @param reportAction - The reportAction of the transaction in the IOU report - * @param isSingleTransactionView - whether we are in the transaction thread report - * @return the url to navigate back once the money request is deleted - */ -function deleteMoneyRequest({ - transactionID, - reportAction, - transactions, - violations, - iouReport, - chatReport, - isChatIOUReportArchived, - isSingleTransactionView = false, - transactionIDsPendingDeletion, - selectedTransactionIDs, - allTransactionViolationsParam, - currentUserAccountID, - currentUserEmail, -}: DeleteMoneyRequestFunctionParams) { - if (!transactionID) { - return; - } - - // STEP 1: Calculate and prepare the data - const { - shouldDeleteTransactionThread, - shouldDeleteIOUReport, - updatedReportAction, - updatedIOUReport, - updatedReportPreviewAction, - transactionThreadID, - transaction, - transactionViolations, - reportPreviewAction, - iouReportActions, - } = prepareToCleanUpMoneyRequest(transactionID, reportAction, iouReport, chatReport, isChatIOUReportArchived, false, transactionIDsPendingDeletion, selectedTransactionIDs); - - const urlToNavigateBack = getNavigationUrlOnMoneyRequestDelete(transactionID, reportAction, iouReport, chatReport, isChatIOUReportArchived, isSingleTransactionView); - - // STEP 2: Build Onyx data - // The logic mostly resembles the cleanUpMoneyRequest function - const optimisticData: Array< - OnyxUpdate - > = [ - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: {...transaction, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, - }, - ]; - - optimisticData.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: null, - }); - - const failureData: Array< - OnyxUpdate - > = [ - { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: {...transaction, pendingAction: null}, - }, - ]; - - removeTransactionFromDuplicateTransactionViolation({optimisticData, failureData}, transactionID, transactions, violations); - - optimisticData.push( - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, - value: updatedReportAction, - }, - // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, - value: updatedIOUReport, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, - value: getOutstandingChildRequest(updatedIOUReport), - }, - ); - - if (reportPreviewAction?.reportActionID) { - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, - value: {[reportPreviewAction.reportActionID]: updatedReportPreviewAction}, - }); - } - - if (chatReport && updatedIOUReport && !shouldDeleteIOUReport && updatedReportPreviewAction?.childMoneyRequestCount === 0) { - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, - value: { - hasOutstandingChildRequest: hasOutstandingChildRequest(chatReport, updatedIOUReport, currentUserEmail, currentUserAccountID, allTransactionViolationsParam, undefined), - }, - }); - } - - if (shouldDeleteIOUReport) { - let canUserPerformWriteAction = true; - if (chatReport) { - canUserPerformWriteAction = !!canUserPerformWriteActionReportUtils(chatReport, isChatIOUReportArchived); - } - - const optimisticReportActions = reportPreviewAction?.reportActionID ? {[reportPreviewAction.reportActionID]: null} : {}; - const optimisticLastReportData = optimisticReportLastData(iouReport?.chatReportID ?? String(CONST.DEFAULT_NUMBER_ID), optimisticReportActions, canUserPerformWriteAction); - - if (chatReport) { - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, - value: { - hasOutstandingChildRequest: hasOutstandingChildRequest(chatReport, iouReport?.reportID, currentUserEmail, currentUserAccountID, allTransactionViolationsParam, undefined), - iouReportID: null, - ...optimisticLastReportData, - }, - }); - } - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, - value: { - reportID: null, - pendingFields: { - preview: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - }, - }, - }); - } - - const cleanUpTransactionThreadReportOnyxData = getCleanUpTransactionThreadReportOnyxData({ - shouldDeleteTransactionThread, - transactionThreadID, - reportAction, - isChatIOUReportArchived, - currentUserAccountID, - }); - optimisticData.push(...cleanUpTransactionThreadReportOnyxData.optimisticData); - - const successData: Array> = [ - shouldDeleteIOUReport - ? { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, - value: null, - } - : { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, - value: { - [reportAction.reportActionID]: { - pendingAction: null, - }, - }, - }, - ]; - - if (reportPreviewAction?.reportActionID) { - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, - value: { - [reportPreviewAction.reportActionID]: { - pendingAction: null, - errors: null, - }, - }, - }); - } - - // Ensure that any remaining data is removed upon successful completion, even if the server sends a report removal response. - // This is done to prevent the removal update from lingering in the applyHTTPSOnyxUpdates function. - successData.push(...cleanUpTransactionThreadReportOnyxData.successData); - - if (shouldDeleteIOUReport) { - successData.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, - value: null, - }); - } - - successData.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: null, - }); - - failureData.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, - value: transactionViolations ?? null, - }); - - failureData.push(...cleanUpTransactionThreadReportOnyxData.failureData); - - const errorKey = DateUtils.getMicroseconds(); - - const originalReportActionsUpdate = {} as Record>; - if (shouldDeleteIOUReport) { - for (const action of Object.values(iouReportActions ?? {})) { - if (action.reportActionID === reportAction.reportActionID) { - continue; - } - originalReportActionsUpdate[action.reportActionID] = { - pendingAction: action.pendingAction ?? null, - message: action.message, - }; - } - } - failureData.push( - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, - value: { - ...originalReportActionsUpdate, - [reportAction.reportActionID]: { - ...reportAction, - pendingAction: null, - errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericDeleteFailureMessage', errorKey), - }, - }, - }, - // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 - shouldDeleteIOUReport - ? { - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, - value: iouReport, - } - : { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, - value: iouReport, - }, - ); - - if (reportPreviewAction?.reportActionID) { - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, - value: { - [reportPreviewAction.reportActionID]: { - ...reportPreviewAction, - pendingAction: null, - errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericDeleteFailureMessage', errorKey), - }, - }, - }); - } - - if (chatReport && shouldDeleteIOUReport) { - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, - value: chatReport, - }); - } - - if (!shouldDeleteIOUReport && updatedReportPreviewAction?.childMoneyRequestCount === 0) { - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, - value: { - hasOutstandingChildRequest: true, - }, - }); - } - - const parameters: DeleteMoneyRequestParams = { - transactionID, - reportActionID: reportAction.reportActionID, - }; - - // STEP 3: Make the API request - API.write(WRITE_COMMANDS.DELETE_MONEY_REQUEST, parameters, {optimisticData, successData, failureData}); - clearPdfByOnyxKey(transactionID); - - return urlToNavigateBack; -} - type OptimisticHoldReportExpenseActionID = { optimisticReportActionID: string; oldReportActionID: string; @@ -10140,11 +9262,9 @@ export { cancelPayment, canIOUBePaid, canCancelPayment, - cleanUpMoneyRequest, clearMoneyRequest, createDistanceRequest, createDraftTransaction, - deleteMoneyRequest, getIOURequestPolicyID, getReportOriginalCreationTimestamp, initMoneyRequest, @@ -10189,7 +9309,6 @@ export { updateLastLocationPermissionPrompt, shouldOptimisticallyUpdateSearch, getIOUReportActionWithBadge, - getNavigationUrlOnMoneyRequestDelete, canSubmitReport, calculateDiffAmount, dismissRejectUseExplanation, @@ -10218,6 +9337,7 @@ export { getAllTransactionViolations, getAllReports, getAllReportActionsFromIOU, + getAllReportNameValuePairs, getAllTransactionDrafts, getCurrentUserEmail, getUserAccountID, @@ -10226,7 +9346,6 @@ export { getPolicyTags, setMoneyRequestTimeRate, setMoneyRequestTimeCount, - getCleanUpTransactionThreadReportOnyxData, handleNavigateAfterExpenseCreate, highlightTransactionOnSearchRouteIfNeeded, buildMinimalTransactionForFormula, diff --git a/src/libs/actions/MergeTransaction.ts b/src/libs/actions/MergeTransaction.ts index e530946429f00..590ac068872de 100644 --- a/src/libs/actions/MergeTransaction.ts +++ b/src/libs/actions/MergeTransaction.ts @@ -33,8 +33,9 @@ import {isDistanceRequest, isTransactionPendingDelete} from '@src/libs/Transacti import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {CardList, MergeTransaction, Policy, PolicyCategories, PolicyTagLists, Report, ReportNextStepDeprecated, Transaction, TransactionViolations} from '@src/types/onyx'; -import {getCleanUpTransactionThreadReportOnyxData, getUpdateMoneyRequestParams, getUpdateTrackExpenseParams} from './IOU'; +import {getUpdateMoneyRequestParams, getUpdateTrackExpenseParams} from './IOU'; import type {UpdateMoneyRequestData, UpdateMoneyRequestDataKeys} from './IOU'; +import {getCleanUpTransactionThreadReportOnyxData} from './IOU/DeleteMoneyRequest'; import {getDeleteTrackExpenseInformation} from './IOU/TrackExpense'; /** diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index f130f6b8c15ff..ab144664896d6 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -78,8 +78,9 @@ import type Nullable from '@src/types/utils/Nullable'; import type PrefixedRecord from '@src/types/utils/PrefixedRecord'; import SafeString from '@src/utils/SafeString'; import {setPersonalBankAccountContinueKYCOnSuccess} from './BankAccounts'; -import {deleteMoneyRequest, getReportPreviewAction, prepareRejectMoneyRequestData, rejectMoneyRequest} from './IOU'; +import {getReportPreviewAction, prepareRejectMoneyRequestData, rejectMoneyRequest} from './IOU'; import type {RejectMoneyRequestData} from './IOU'; +import {deleteMoneyRequest} from './IOU/DeleteMoneyRequest'; import {isCurrencySupportedForGlobalReimbursement} from './Policy/Policy'; import {deleteAppReport, setOptimisticTransactionThread} from './Report'; import {saveLastSearchParams} from './ReportNavigation'; diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index e64a853fef2c5..ee14ab6af361a 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -110,7 +110,7 @@ import { } from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import {isDemoTransaction} from '@libs/TransactionUtils'; -import {getNavigationUrlOnMoneyRequestDelete} from '@userActions/IOU'; +import {getNavigationUrlOnMoneyRequestDelete} from '@userActions/IOU/DeleteMoneyRequest'; import {deleteTrackExpense, getNavigationUrlAfterTrackExpenseDelete} from '@userActions/IOU/TrackExpense'; import { clearAvatarErrors, diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index ca19344d56b79..893d2b64c1c47 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -58,7 +58,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {cleanUpMoneyRequest} from '@libs/actions/IOU'; +import {cleanUpMoneyRequest} from '@libs/actions/IOU/DeleteMoneyRequest'; import {resolveSuggestedFollowup} from '@libs/actions/Report/SuggestedFollowup'; import {isPersonalCardBrokenConnection} from '@libs/CardUtils'; import {isChronosOOOListAction} from '@libs/ChronosUtils'; diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 736bc4a89629e..4a8a81b19b08a 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -21,10 +21,8 @@ import { clearBulkEditDraftTransaction, completePaymentOnboarding, createDistanceRequest, - deleteMoneyRequest, getIOUReportActionWithBadge, getReportOriginalCreationTimestamp, - getReportPreviewAction, handleNavigateAfterExpenseCreate, initBulkEditDraftTransaction, initMoneyRequest, @@ -54,10 +52,9 @@ import { import {putOnHold} from '@libs/actions/IOU/Hold'; import {completeSplitBill, splitBill, startSplitBill, updateSplitTransactionsFromSplitExpensesFlow} from '@libs/actions/IOU/Split'; import {requestMoney, trackExpense} from '@libs/actions/IOU/TrackExpense'; -import {updateMoneyRequestAmountAndCurrency} from '@libs/actions/IOU/UpdateMoneyRequest'; import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; import {createWorkspace, deleteWorkspace, generatePolicyID, setWorkspaceApprovalMode} from '@libs/actions/Policy/Policy'; -import {addComment, createNewReport, deleteReport, notifyNewAction, openReport} from '@libs/actions/Report'; +import {createNewReport, deleteReport, notifyNewAction} from '@libs/actions/Report'; import {subscribeToUserEvents} from '@libs/actions/User'; import type {ApiCommand} from '@libs/API/types'; import {WRITE_COMMANDS} from '@libs/API/types'; @@ -66,7 +63,6 @@ import isReportTopmostSplitNavigator from '@libs/Navigation/helpers/isReportTopm import Navigation from '@libs/Navigation/Navigation'; import {rand64} from '@libs/NumberUtils'; import {getManagerMcTestParticipant} from '@libs/OptionsListUtils'; -import {getLoginsByAccountIDs} from '@libs/PersonalDetailsUtils'; // eslint-disable-next-line no-restricted-syntax import type * as PolicyUtils from '@libs/PolicyUtils'; import { @@ -74,21 +70,12 @@ import { getIOUActionForReportID, getOriginalMessage, getReportActionHtml, - getReportActionMessage, getReportActionText, isActionableTrackExpense, isActionOfType, isMoneyRequestAction, } from '@libs/ReportActionsUtils'; -import type {OptimisticChatReport} from '@libs/ReportUtils'; -import { - buildOptimisticIOUReport, - buildOptimisticIOUReportAction, - buildTransactionThread, - createDraftTransactionAndNavigateToParticipantSelector, - getReportOrDraftReport, - isIOUReport, -} from '@libs/ReportUtils'; +import {buildOptimisticIOUReport, buildOptimisticIOUReportAction, createDraftTransactionAndNavigateToParticipantSelector, getReportOrDraftReport} from '@libs/ReportUtils'; import {buildOptimisticTransaction} from '@libs/TransactionUtils'; import type {IOUAction} from '@src/CONST'; import CONST from '@src/CONST'; @@ -122,7 +109,6 @@ import createRandomReportAction from '../utils/collections/reportActions'; import {createRandomReport} from '../utils/collections/reports'; import createRandomTransaction from '../utils/collections/transaction'; import getOnyxValue from '../utils/getOnyxValue'; -import PusherHelper from '../utils/PusherHelper'; import type {MockFetch} from '../utils/TestHelper'; import {getGlobalFetchMock, getOnyxData, localeCompare, setPersonalDetails, signInWithTestUser, translateLocal} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -220,11 +206,6 @@ const VIT_EMAIL = 'vit@expensifail.com'; const VIT_ACCOUNT_ID = 4; const VIT_PARTICIPANT: Participant = {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS, role: 'member'}; -const TEST_INTRO_SELECTED: IntroSelected = { - choice: CONST.ONBOARDING_CHOICES.SUBMIT, - isInviteOnboardingComplete: false, -}; - const getTransactionAndExpenseReports = (reportID: string) => { const transactionReport = getReportOrDraftReport(reportID); const parentTransactionReport = getReportOrDraftReport(transactionReport?.parentReportID); @@ -6432,1615 +6413,6 @@ describe('actions/IOU', () => { }); }); - describe('deleteMoneyRequest', () => { - const amount = 10000; - const comment = 'Send me money please'; - let chatReport: OnyxEntry; - let iouReport: OnyxEntry; - let createIOUAction: OnyxEntry>; - let transaction: OnyxEntry; - let thread: OptimisticChatReport; - const TEST_USER_ACCOUNT_ID = 1; - const TEST_USER_LOGIN = 'test@test.com'; - let IOU_REPORT_ID: string | undefined; - let IOU_REPORT: OnyxEntry; - let reportActionID; - const REPORT_ACTION: OnyxEntry = { - actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, - actorAccountID: TEST_USER_ACCOUNT_ID, - automatic: false, - avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png', - message: [{type: 'COMMENT', html: 'Testing a comment', text: 'Testing a comment', translationKey: ''}], - person: [{type: 'TEXT', style: 'strong', text: 'Test User'}], - shouldShow: true, - created: DateUtils.getDBTime(), - reportActionID: '1', - originalMessage: { - html: '', - whisperedTo: [], - }, - }; - - let reportActions: OnyxCollection; - - beforeEach(async () => { - // Given mocks are cleared and helpers are set up - jest.clearAllMocks(); - PusherHelper.setup(); - - // Given a test user is signed in with Onyx setup and some initial data - await signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); - subscribeToUserEvents(TEST_USER_ACCOUNT_ID, undefined); - await waitForBatchedUpdates(); - await setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID); - - // When a submit IOU expense is made - requestMoney({ - report: chatReport, - participantParams: { - payeeEmail: TEST_USER_LOGIN, - payeeAccountID: TEST_USER_ACCOUNT_ID, - participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, - }, - transactionParams: { - amount, - attendees: [], - currency: CONST.CURRENCY.USD, - created: '', - merchant: '', - comment, - }, - shouldGenerateTransactionThreadReport: true, - isASAPSubmitBetaEnabled: false, - currentUserAccountIDParam: 123, - currentUserEmailParam: 'existing@example.com', - transactionViolations: {}, - policyRecentlyUsedCurrencies: [], - existingTransactionDraft: undefined, - draftTransactionIDs: [], - isSelfTourViewed: false, - quickAction: undefined, - betas: [CONST.BETAS.ALL], - personalDetails: {}, - }); - await waitForBatchedUpdates(); - - // When fetching all reports from Onyx - const allReports = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (reports) => { - Onyx.disconnect(connection); - resolve(reports); - }, - }); - }); - - // Then we should have exactly 3 reports - expect(Object.values(allReports ?? {}).length).toBe(3); - - // Then one of them should be a chat report with relevant properties - chatReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.CHAT); - expect(chatReport).toBeTruthy(); - expect(chatReport).toHaveProperty('reportID'); - expect(chatReport).toHaveProperty('iouReportID'); - - // Then one of them should be an IOU report with relevant properties - iouReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU); - expect(iouReport).toBeTruthy(); - expect(iouReport).toHaveProperty('reportID'); - expect(iouReport).toHaveProperty('chatReportID'); - - // Then their IDs should reference each other - expect(chatReport?.iouReportID).toBe(iouReport?.reportID); - expect(iouReport?.chatReportID).toBe(chatReport?.reportID); - - // Storing IOU Report ID for further reference - IOU_REPORT_ID = chatReport?.iouReportID; - IOU_REPORT = iouReport; - - await waitForBatchedUpdates(); - - // When fetching all report actions from Onyx - const allReportActions = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (actions) => { - Onyx.disconnect(connection); - resolve(actions); - }, - }); - }); - - // Then we should find an IOU action with specific properties - const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; - createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find((reportAction): reportAction is ReportAction => - isMoneyRequestAction(reportAction), - ); - expect(createIOUAction).toBeTruthy(); - expect(createIOUAction && getOriginalMessage(createIOUAction)?.IOUReportID).toBe(iouReport?.reportID); - - // When fetching all transactions from Onyx - let allTransactions: OnyxCollection; - await getOnyxData({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (val) => { - allTransactions = val; - }, - }); - - // Then we should find a specific transaction with relevant properties - transaction = Object.values(allTransactions ?? {}).find((t) => t); - expect(transaction).toBeTruthy(); - expect(transaction?.amount).toBe(amount); - expect(transaction?.reportID).toBe(iouReport?.reportID); - expect(createIOUAction && getOriginalMessage(createIOUAction)?.IOUTransactionID).toBe(transaction?.transactionID); - }); - - afterEach(PusherHelper.teardown); - - it('delete an expense (IOU Action and transaction) successfully', async () => { - // Given the fetch operations are paused and an expense is initiated - mockFetch?.pause?.(); - - if (transaction && createIOUAction) { - // When the expense is deleted - deleteMoneyRequest({ - transactionID: transaction?.transactionID, - reportAction: createIOUAction, - transactions: {}, - violations: {}, - iouReport, - chatReport, - isChatIOUReportArchived: true, - allTransactionViolationsParam: {}, - currentUserAccountID: TEST_USER_ACCOUNT_ID, - currentUserEmail: TEST_USER_LOGIN, - }); - } - await waitForBatchedUpdates(); - - // Then we check if the IOU report action is removed from the report actions collection - let reportActionsForReport = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, - waitForCollectionCallback: false, - callback: (actionsForReport) => { - Onyx.disconnect(connection); - resolve(actionsForReport); - }, - }); - }); - - createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction): reportAction is ReportAction => - isMoneyRequestAction(reportAction), - ); - // Then the IOU Action should be truthy for offline support. - expect(createIOUAction).toBeTruthy(); - - // Then we check if the transaction is removed from the transactions collection - const t = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`, - waitForCollectionCallback: false, - callback: (transactionResult) => { - Onyx.disconnect(connection); - resolve(transactionResult); - }, - }); - }); - - expect(t).toBeTruthy(); - expect(t?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); - - // Given fetch operations are resumed - mockFetch?.resume?.(); - await waitForBatchedUpdates(); - - // Then we recheck the IOU report action from the report actions collection - reportActionsForReport = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, - waitForCollectionCallback: false, - callback: (actionsForReport) => { - Onyx.disconnect(connection); - resolve(actionsForReport); - }, - }); - }); - - createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction): reportAction is ReportAction => - isMoneyRequestAction(reportAction), - ); - expect(createIOUAction).toBeFalsy(); - - // Then we recheck the transaction from the transactions collection - const tr = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`, - waitForCollectionCallback: false, - callback: (transactionResult) => { - Onyx.disconnect(connection); - resolve(transactionResult); - }, - }); - }); - - expect(tr).toBeFalsy(); - }); - - it('delete the IOU report when there are no expenses left in the IOU report', async () => { - // Given an IOU report and a paused fetch state - mockFetch?.pause?.(); - - if (transaction && createIOUAction) { - // When the IOU expense is deleted - deleteMoneyRequest({ - transactionID: transaction?.transactionID, - reportAction: createIOUAction, - transactions: {}, - violations: {}, - iouReport, - chatReport, - isChatIOUReportArchived: true, - allTransactionViolationsParam: {}, - currentUserAccountID: TEST_USER_ACCOUNT_ID, - currentUserEmail: TEST_USER_LOGIN, - }); - } - await waitForBatchedUpdates(); - - let report = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, - waitForCollectionCallback: false, - callback: (res) => { - Onyx.disconnect(connection); - resolve(res); - }, - }); - }); - - // Then the report should be truthy for offline support - expect(report).toBeTruthy(); - - // Given the resumed fetch state - mockFetch?.resume?.(); - await waitForBatchedUpdates(); - - report = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, - waitForCollectionCallback: false, - callback: (res) => { - Onyx.disconnect(connection); - resolve(res); - }, - }); - }); - - // Then the report should be falsy so that there is no trace of the expense. - expect(report).toBeFalsy(); - }); - - it('does not delete the IOU report when there are expenses left in the IOU report', async () => { - // Given multiple expenses on an IOU report - requestMoney({ - report: chatReport, - participantParams: { - payeeEmail: TEST_USER_LOGIN, - payeeAccountID: TEST_USER_ACCOUNT_ID, - participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, - }, - transactionParams: { - amount, - attendees: [], - currency: CONST.CURRENCY.USD, - created: '', - merchant: '', - comment, - }, - shouldGenerateTransactionThreadReport: true, - isASAPSubmitBetaEnabled: false, - currentUserAccountIDParam: 123, - currentUserEmailParam: 'existing@example.com', - transactionViolations: {}, - policyRecentlyUsedCurrencies: [], - existingTransactionDraft: undefined, - draftTransactionIDs: [], - isSelfTourViewed: false, - quickAction: undefined, - betas: [CONST.BETAS.ALL], - personalDetails: {}, - }); - - await waitForBatchedUpdates(); - - // When we attempt to delete an expense from the IOU report - mockFetch?.pause?.(); - if (transaction && createIOUAction) { - deleteMoneyRequest({ - transactionID: transaction?.transactionID, - reportAction: createIOUAction, - transactions: {}, - violations: {}, - iouReport, - chatReport, - allTransactionViolationsParam: {}, - currentUserAccountID: TEST_USER_ACCOUNT_ID, - currentUserEmail: TEST_USER_LOGIN, - }); - } - await waitForBatchedUpdates(); - - // Then expect that the IOU report still exists - let allReports = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (reports) => { - Onyx.disconnect(connection); - resolve(reports); - }, - }); - }); - - await waitForBatchedUpdates(); - - iouReport = Object.values(allReports ?? {}).find((report) => isIOUReport(report)); - expect(iouReport).toBeTruthy(); - expect(iouReport).toHaveProperty('reportID'); - expect(iouReport).toHaveProperty('chatReportID'); - - // Given the resumed fetch state - await mockFetch?.resume?.(); - - allReports = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (reports) => { - Onyx.disconnect(connection); - resolve(reports); - }, - }); - }); - // Then expect that the IOU report still exists - iouReport = Object.values(allReports ?? {}).find((report) => isIOUReport(report)); - expect(iouReport).toBeTruthy(); - expect(iouReport).toHaveProperty('reportID'); - expect(iouReport).toHaveProperty('chatReportID'); - }); - - it('delete the transaction thread if there are no visible comments in the thread', async () => { - // Given all promises are resolved - await waitForBatchedUpdates(); - jest.advanceTimersByTime(10); - - // Given a transaction thread - thread = buildTransactionThread(createIOUAction, iouReport); - - expect(thread.participants).toStrictEqual({[CARLOS_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, role: CONST.REPORT.ROLE.ADMIN}}); - - Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, - callback: (val) => (reportActions = val), - }); - - await waitForBatchedUpdates(); - - jest.advanceTimersByTime(10); - - // Given User logins from the participant accounts - const participantAccountIDs = Object.keys(thread.participants ?? {}).map(Number); - const userLogins = getLoginsByAccountIDs(participantAccountIDs); - - // When Opening a thread report with the given details - openReport({ - reportID: thread.reportID, - introSelected: TEST_INTRO_SELECTED, - betas: undefined, - participantLoginList: userLogins, - newReportObject: thread, - parentReportActionID: createIOUAction?.reportActionID, - }); - await waitForBatchedUpdates(); - - // Then The iou action has the transaction report id as a child report ID - const allReportActions = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (actions) => { - Onyx.disconnect(connection); - resolve(actions); - }, - }); - }); - const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; - createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find((reportAction): reportAction is ReportAction => - isMoneyRequestAction(reportAction), - ); - expect(createIOUAction?.childReportID).toBe(thread.reportID); - - await waitForBatchedUpdates(); - - // Given Fetch is paused and timers have advanced - mockFetch?.pause?.(); - jest.advanceTimersByTime(10); - - if (transaction && createIOUAction) { - // When Deleting an expense - deleteMoneyRequest({ - transactionID: transaction?.transactionID, - reportAction: createIOUAction, - transactions: {}, - violations: {}, - iouReport, - chatReport, - allTransactionViolationsParam: {}, - currentUserAccountID: TEST_USER_ACCOUNT_ID, - currentUserEmail: TEST_USER_LOGIN, - }); - } - await waitForBatchedUpdates(); - - // Then The report for the given thread ID does not exist - let report = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, - waitForCollectionCallback: false, - callback: (reportData) => { - Onyx.disconnect(connection); - resolve(reportData); - }, - }); - }); - - expect(report?.reportID).toBeFalsy(); - mockFetch?.resume?.(); - - // Then After resuming fetch, the report for the given thread ID still does not exist - report = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, - waitForCollectionCallback: false, - callback: (reportData) => { - Onyx.disconnect(connection); - resolve(reportData); - }, - }); - }); - - expect(report?.reportID).toBeFalsy(); - }); - - it('delete the transaction thread if there are only changelogs (i.e. MODIFIED_EXPENSE actions) in the thread', async () => { - // Given all promises are resolved - await waitForBatchedUpdates(); - jest.advanceTimersByTime(10); - - // Given a transaction thread - thread = buildTransactionThread(createIOUAction, iouReport); - - Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, - callback: (val) => (reportActions = val), - }); - - await waitForBatchedUpdates(); - - jest.advanceTimersByTime(10); - - // Given User logins from the participant accounts - const participantAccountIDs = Object.keys(thread.participants ?? {}).map(Number); - const userLogins = getLoginsByAccountIDs(participantAccountIDs); - - // When Opening a thread report with the given details - openReport({ - reportID: thread.reportID, - introSelected: TEST_INTRO_SELECTED, - betas: undefined, - participantLoginList: userLogins, - newReportObject: thread, - parentReportActionID: createIOUAction?.reportActionID, - }); - await waitForBatchedUpdates(); - - // Then The iou action has the transaction report id as a child report ID - const allReportActions = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (actions) => { - Onyx.disconnect(connection); - resolve(actions); - }, - }); - }); - const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; - createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find((reportAction): reportAction is ReportAction => - isMoneyRequestAction(reportAction), - ); - - expect(createIOUAction?.childReportID).toBe(thread.reportID); - - await waitForBatchedUpdates(); - - jest.advanceTimersByTime(10); - if (transaction && createIOUAction) { - updateMoneyRequestAmountAndCurrency({ - transactionID: transaction.transactionID, - transactions: {}, - transactionThreadReport: thread, - parentReport: iouReport, - transactionViolations: {}, - amount: 20000, - currency: CONST.CURRENCY.USD, - taxAmount: 0, - taxCode: '', - taxValue: '', - policy: { - id: '123', - role: CONST.POLICY.ROLE.USER, - type: CONST.POLICY.TYPE.TEAM, - name: '', - owner: '', - outputCurrency: '', - isPolicyExpenseChatEnabled: false, - }, - policyTagList: {}, - policyCategories: {}, - currentUserAccountIDParam: 123, - currentUserEmailParam: 'existing@example.com', - isASAPSubmitBetaEnabled: false, - policyRecentlyUsedCurrencies: [], - parentReportNextStep: undefined, - }); - } - await waitForBatchedUpdates(); - - // Verify there are two actions (created + changelog) - expect(Object.values(reportActions ?? {}).length).toBe(2); - - // Fetch the updated IOU Action from Onyx - await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, - waitForCollectionCallback: false, - callback: (reportActionsForReport) => { - Onyx.disconnect(connection); - createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction): reportAction is ReportAction => - isMoneyRequestAction(reportAction), - ); - resolve(); - }, - }); - }); - - if (transaction && createIOUAction) { - // When Deleting an expense - deleteMoneyRequest({ - transactionID: transaction?.transactionID, - reportAction: createIOUAction, - transactions: {}, - violations: {}, - iouReport, - chatReport, - allTransactionViolationsParam: {}, - currentUserAccountID: TEST_USER_ACCOUNT_ID, - currentUserEmail: TEST_USER_LOGIN, - }); - } - await waitForBatchedUpdates(); - - // Then, the report for the given thread ID does not exist - const report = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, - waitForCollectionCallback: false, - callback: (reportData) => { - Onyx.disconnect(connection); - resolve(reportData); - }, - }); - }); - - expect(report?.reportID).toBeFalsy(); - }); - - it('should delete the transaction thread regardless of whether there are visible comments in the thread.', async () => { - // Given initial environment is set up - await waitForBatchedUpdates(); - - // Given a transaction thread - thread = buildTransactionThread(createIOUAction, iouReport); - - expect(thread.participants).toEqual({[CARLOS_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, role: CONST.REPORT.ROLE.ADMIN}}); - - const participantAccountIDs = Object.keys(thread.participants ?? {}).map(Number); - const userLogins = getLoginsByAccountIDs(participantAccountIDs); - jest.advanceTimersByTime(10); - openReport({ - reportID: thread.reportID, - introSelected: TEST_INTRO_SELECTED, - betas: undefined, - participantLoginList: userLogins, - newReportObject: thread, - parentReportActionID: createIOUAction?.reportActionID, - }); - await waitForBatchedUpdates(); - - Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, - callback: (val) => (reportActions = val), - }); - await waitForBatchedUpdates(); - - await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, - callback: (report) => { - Onyx.disconnect(connection); - expect(report).toBeTruthy(); - resolve(); - }, - }); - }); - - jest.advanceTimersByTime(10); - - // When a comment is added - addComment({ - report: thread, - notifyReportID: thread.reportID, - ancestors: [], - text: 'Testing a comment', - timezoneParam: CONST.DEFAULT_TIME_ZONE, - currentUserAccountID: RORY_ACCOUNT_ID, - }); - await waitForBatchedUpdates(); - - // Then comment details should match the expected report action - const resultAction = Object.values(reportActions ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT); - reportActionID = resultAction?.reportActionID; - expect(resultAction?.message).toEqual(REPORT_ACTION.message); - expect(resultAction?.person).toEqual(REPORT_ACTION.person); - - await waitForBatchedUpdates(); - - // Then the report should have 2 actions - expect(Object.values(reportActions ?? {}).length).toBe(2); - const resultActionAfter = reportActionID ? reportActions?.[reportActionID] : undefined; - expect(resultActionAfter?.pendingAction).toBeUndefined(); - - mockFetch?.pause?.(); - - const allReportActions = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (actions) => { - Onyx.disconnect(connection); - resolve(actions); - }, - }); - }); - - const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; - createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find( - (reportAction): reportAction is ReportAction => reportAction.reportActionID === createIOUAction?.reportActionID, - ); - - if (transaction && createIOUAction) { - // When deleting expense - deleteMoneyRequest({ - transactionID: transaction?.transactionID, - reportAction: createIOUAction, - transactions: {}, - violations: {}, - iouReport, - chatReport, - allTransactionViolationsParam: {}, - currentUserAccountID: TEST_USER_ACCOUNT_ID, - currentUserEmail: TEST_USER_LOGIN, - }); - } - await waitForBatchedUpdates(); - - // Then the transaction thread report should be ready to be deleted - await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, - waitForCollectionCallback: false, - callback: (report) => { - Onyx.disconnect(connection); - expect(report?.reportID).toBeFalsy(); - resolve(); - }, - }); - }); - - // When fetch resumes - // Then the transaction thread report should be deleted - mockFetch?.resume?.(); - await waitForBatchedUpdates(); - - // Then the transaction thread report should be deleted - await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, - waitForCollectionCallback: false, - callback: (report) => { - Onyx.disconnect(connection); - expect(report).toBeFalsy(); - resolve(); - }, - }); - }); - }); - - it('update the moneyRequestPreview to show [Deleted expense] when appropriate', async () => { - await waitForBatchedUpdates(); - - // Given a thread report - - jest.advanceTimersByTime(10); - thread = buildTransactionThread(createIOUAction, iouReport); - - expect(thread.participants).toStrictEqual({[CARLOS_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, role: CONST.REPORT.ROLE.ADMIN}}); - - Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, - callback: (val) => (reportActions = val), - }); - await waitForBatchedUpdates(); - - jest.advanceTimersByTime(10); - const participantAccountIDs = Object.keys(thread.participants ?? {}).map(Number); - const userLogins = getLoginsByAccountIDs(participantAccountIDs); - openReport({ - reportID: thread.reportID, - introSelected: TEST_INTRO_SELECTED, - betas: undefined, - participantLoginList: userLogins, - newReportObject: thread, - parentReportActionID: createIOUAction?.reportActionID, - }); - - await waitForBatchedUpdates(); - - const allReportActions = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (actions) => { - Onyx.disconnect(connection); - resolve(actions); - }, - }); - }); - - const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; - createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find((reportAction): reportAction is ReportAction => - isMoneyRequestAction(reportAction), - ); - expect(createIOUAction?.childReportID).toBe(thread.reportID); - - await waitForBatchedUpdates(); - - // Given an added comment to the thread report - - jest.advanceTimersByTime(10); - - addComment({ - report: thread, - notifyReportID: thread.reportID, - ancestors: [], - text: 'Testing a comment', - timezoneParam: CONST.DEFAULT_TIME_ZONE, - currentUserAccountID: RORY_ACCOUNT_ID, - }); - await waitForBatchedUpdates(); - - // Fetch the updated IOU Action from Onyx due to addition of comment to transaction thread. - // This needs to be fetched as `deleteMoneyRequest` depends on `childVisibleActionCount` in `createIOUAction`. - await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, - waitForCollectionCallback: false, - callback: (reportActionsForReport) => { - Onyx.disconnect(connection); - createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction): reportAction is ReportAction => - isMoneyRequestAction(reportAction), - ); - resolve(); - }, - }); - }); - - let resultAction = Object.values(reportActions ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT); - reportActionID = resultAction?.reportActionID; - - expect(resultAction?.message).toEqual(REPORT_ACTION.message); - expect(resultAction?.person).toEqual(REPORT_ACTION.person); - expect(resultAction?.pendingAction).toBeUndefined(); - - await waitForBatchedUpdates(); - - // Verify there are three actions (created + addcomment) and our optimistic comment has been removed - expect(Object.values(reportActions ?? {}).length).toBe(2); - - let resultActionAfterUpdate = reportActionID ? reportActions?.[reportActionID] : undefined; - - // Verify that our action is no longer in the loading state - expect(resultActionAfterUpdate?.pendingAction).toBeUndefined(); - - await waitForBatchedUpdates(); - - // Given an added comment to the IOU report - - Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${IOU_REPORT_ID}`, - callback: (val) => (reportActions = val), - }); - await waitForBatchedUpdates(); - - jest.advanceTimersByTime(10); - - if (IOU_REPORT_ID) { - addComment({ - report: IOU_REPORT, - notifyReportID: IOU_REPORT_ID, - ancestors: [], - text: 'Testing a comment', - timezoneParam: CONST.DEFAULT_TIME_ZONE, - currentUserAccountID: RORY_ACCOUNT_ID, - }); - } - await waitForBatchedUpdates(); - - resultAction = Object.values(reportActions ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT); - reportActionID = resultAction?.reportActionID; - - expect(resultAction?.message).toEqual(REPORT_ACTION.message); - expect(resultAction?.person).toEqual(REPORT_ACTION.person); - expect(resultAction?.pendingAction).toBeUndefined(); - - await waitForBatchedUpdates(); - - // Verify there are three actions (created + iou + addcomment) and our optimistic comment has been removed - expect(Object.values(reportActions ?? {}).length).toBe(3); - - resultActionAfterUpdate = reportActionID ? reportActions?.[reportActionID] : undefined; - - // Verify that our action is no longer in the loading state - expect(resultActionAfterUpdate?.pendingAction).toBeUndefined(); - - mockFetch?.pause?.(); - if (transaction && createIOUAction) { - // When we delete the expense - deleteMoneyRequest({ - transactionID: transaction.transactionID, - reportAction: createIOUAction, - transactions: {}, - violations: {}, - iouReport, - chatReport, - isChatIOUReportArchived: undefined, - allTransactionViolationsParam: {}, - currentUserAccountID: TEST_USER_ACCOUNT_ID, - currentUserEmail: TEST_USER_LOGIN, - }); - } - await waitForBatchedUpdates(); - - // Then we expect the moneyRequestPreview to show [Deleted expense] - - await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, - waitForCollectionCallback: false, - callback: (reportActionsForReport) => { - Onyx.disconnect(connection); - createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction): reportAction is ReportAction => - isMoneyRequestAction(reportAction), - ); - expect(getReportActionMessage(createIOUAction)?.isDeletedParentAction).toBeTruthy(); - resolve(); - }, - }); - }); - - // When we resume fetch - mockFetch?.resume?.(); - - // Then we expect the moneyRequestPreview to show [Deleted expense] - - await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, - waitForCollectionCallback: false, - callback: (reportActionsForReport) => { - Onyx.disconnect(connection); - createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction): reportAction is ReportAction => - isMoneyRequestAction(reportAction), - ); - expect(getReportActionMessage(createIOUAction)?.isDeletedParentAction).toBeTruthy(); - resolve(); - }, - }); - }); - }); - - it('update IOU report and reportPreview with new totals and messages if the IOU report is not deleted', async () => { - await waitForBatchedUpdates(); - Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, - callback: (val) => (iouReport = val), - }); - await waitForBatchedUpdates(); - - // Given a second expense in addition to the first one - - jest.advanceTimersByTime(10); - const amount2 = 20000; - const comment2 = 'Send me money please 2'; - if (chatReport) { - requestMoney({ - report: chatReport, - participantParams: { - payeeEmail: TEST_USER_LOGIN, - payeeAccountID: TEST_USER_ACCOUNT_ID, - participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, - }, - transactionParams: { - amount: amount2, - attendees: [], - currency: CONST.CURRENCY.USD, - created: '', - merchant: '', - comment: comment2, - }, - shouldGenerateTransactionThreadReport: true, - isASAPSubmitBetaEnabled: false, - currentUserAccountIDParam: 123, - currentUserEmailParam: 'existing@example.com', - transactionViolations: {}, - policyRecentlyUsedCurrencies: [], - existingTransactionDraft: undefined, - draftTransactionIDs: [], - isSelfTourViewed: false, - quickAction: undefined, - betas: [CONST.BETAS.ALL], - personalDetails: {}, - }); - } - - await waitForBatchedUpdates(); - - // Then we expect the IOU report and reportPreview to update with new totals - - expect(iouReport).toBeTruthy(); - expect(iouReport).toHaveProperty('reportID'); - expect(iouReport).toHaveProperty('chatReportID'); - expect(iouReport?.total).toBe(30000); - - const iouPreview = chatReport?.reportID && iouReport?.reportID ? getReportPreviewAction(chatReport.reportID, iouReport.reportID) : undefined; - expect(iouPreview).toBeTruthy(); - expect(getReportActionText(iouPreview)).toBe('rory@expensifail.com owes $300.00'); - - // When we delete the first expense - mockFetch?.pause?.(); - jest.advanceTimersByTime(10); - if (transaction && createIOUAction) { - deleteMoneyRequest({ - transactionID: transaction.transactionID, - reportAction: createIOUAction, - transactions: {}, - violations: {}, - iouReport, - chatReport, - isChatIOUReportArchived: undefined, - allTransactionViolationsParam: {}, - currentUserAccountID: TEST_USER_ACCOUNT_ID, - currentUserEmail: TEST_USER_LOGIN, - }); - } - await waitForBatchedUpdates(); - - // Then we expect the IOU report and reportPreview to update with new totals - - expect(iouReport).toBeTruthy(); - expect(iouReport).toHaveProperty('reportID'); - expect(iouReport).toHaveProperty('chatReportID'); - expect(iouReport?.total).toBe(20000); - - // When we resume - mockFetch?.resume?.(); - - // Then we expect the IOU report and reportPreview to update with new totals - expect(iouReport).toBeTruthy(); - expect(iouReport).toHaveProperty('reportID'); - expect(iouReport).toHaveProperty('chatReportID'); - expect(iouReport?.total).toBe(20000); - }); - - it('navigate the user correctly to the iou Report when appropriate', async () => { - // Given multiple expenses on an IOU report - requestMoney({ - report: chatReport, - participantParams: { - payeeEmail: TEST_USER_LOGIN, - payeeAccountID: TEST_USER_ACCOUNT_ID, - participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, - }, - transactionParams: { - amount, - attendees: [], - currency: CONST.CURRENCY.USD, - created: '', - merchant: '', - comment, - }, - shouldGenerateTransactionThreadReport: true, - isASAPSubmitBetaEnabled: false, - currentUserAccountIDParam: 123, - currentUserEmailParam: 'existing@example.com', - transactionViolations: {}, - policyRecentlyUsedCurrencies: [], - existingTransactionDraft: undefined, - draftTransactionIDs: [], - isSelfTourViewed: false, - quickAction: undefined, - betas: [CONST.BETAS.ALL], - personalDetails: {}, - }); - await waitForBatchedUpdates(); - - // Given a thread report - jest.advanceTimersByTime(10); - thread = buildTransactionThread(createIOUAction, iouReport); - - expect(thread.participants).toStrictEqual({[CARLOS_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, role: CONST.REPORT.ROLE.ADMIN}}); - - jest.advanceTimersByTime(10); - const participantAccountIDs = Object.keys(thread.participants ?? {}).map(Number); - const userLogins = getLoginsByAccountIDs(participantAccountIDs); - openReport({ - reportID: thread.reportID, - introSelected: TEST_INTRO_SELECTED, - betas: undefined, - participantLoginList: userLogins, - newReportObject: thread, - parentReportActionID: createIOUAction?.reportActionID, - }); - await waitForBatchedUpdates(); - - const allReportActions = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (actions) => { - Onyx.disconnect(connection); - resolve(actions); - }, - }); - }); - - const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; - createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find((reportAction): reportAction is ReportAction => - isMoneyRequestAction(reportAction), - ); - expect(createIOUAction?.childReportID).toBe(thread.reportID); - - // When we delete the expense, we should not delete the IOU report - mockFetch?.pause?.(); - - let navigateToAfterDelete; - if (transaction && createIOUAction) { - navigateToAfterDelete = deleteMoneyRequest({ - transactionID: transaction.transactionID, - reportAction: createIOUAction, - transactions: {}, - violations: {}, - iouReport, - chatReport, - isSingleTransactionView: true, - allTransactionViolationsParam: {}, - currentUserAccountID: TEST_USER_ACCOUNT_ID, - currentUserEmail: TEST_USER_LOGIN, - }); - } - - let allReports = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (reports) => { - Onyx.disconnect(connection); - resolve(reports); - }, - }); - }); - - iouReport = Object.values(allReports ?? {}).find((report) => isIOUReport(report)); - expect(iouReport).toBeTruthy(); - expect(iouReport).toHaveProperty('reportID'); - expect(iouReport).toHaveProperty('chatReportID'); - - await mockFetch?.resume?.(); - - allReports = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (reports) => { - Onyx.disconnect(connection); - resolve(reports); - }, - }); - }); - - iouReport = Object.values(allReports ?? {}).find((report) => isIOUReport(report)); - expect(iouReport).toBeTruthy(); - expect(iouReport).toHaveProperty('reportID'); - expect(iouReport).toHaveProperty('chatReportID'); - - // Then we expect to navigate to the iou report - expect(IOU_REPORT_ID).not.toBeUndefined(); - if (IOU_REPORT_ID) { - expect(navigateToAfterDelete).toEqual(ROUTES.REPORT_WITH_ID.getRoute(IOU_REPORT_ID)); - } - }); - - it('navigate the user correctly to the chat Report when appropriate', () => { - let navigateToAfterDelete; - if (transaction && createIOUAction) { - // When we delete the expense and we should delete the IOU report - navigateToAfterDelete = deleteMoneyRequest({ - transactionID: transaction.transactionID, - reportAction: createIOUAction, - transactions: {}, - violations: {}, - iouReport, - chatReport, - allTransactionViolationsParam: {}, - currentUserAccountID: TEST_USER_ACCOUNT_ID, - currentUserEmail: TEST_USER_LOGIN, - }); - } - // Then we expect to navigate to the chat report - expect(chatReport?.reportID).not.toBeUndefined(); - - if (chatReport?.reportID) { - expect(navigateToAfterDelete).toEqual(ROUTES.REPORT_WITH_ID.getRoute(chatReport?.reportID)); - } - }); - - it('update reportPreview with childVisibleActionCount if the IOU report is not deleted', async () => { - await waitForBatchedUpdates(); - Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, - callback: (val) => (iouReport = val), - }); - await waitForBatchedUpdates(); - - // Given a second expense in addition to the first one - - jest.advanceTimersByTime(10); - const amount2 = 20000; - const comment2 = 'Send me money please 2'; - if (chatReport) { - requestMoney({ - report: chatReport, - participantParams: { - payeeEmail: TEST_USER_LOGIN, - payeeAccountID: TEST_USER_ACCOUNT_ID, - participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, - }, - transactionParams: { - amount: amount2, - attendees: [], - currency: CONST.CURRENCY.USD, - created: '', - merchant: '', - comment: comment2, - }, - shouldGenerateTransactionThreadReport: true, - isASAPSubmitBetaEnabled: false, - transactionViolations: {}, - currentUserAccountIDParam: 123, - currentUserEmailParam: 'existing@example.com', - policyRecentlyUsedCurrencies: [], - quickAction: undefined, - isSelfTourViewed: false, - existingTransactionDraft: undefined, - draftTransactionIDs: [], - betas: [CONST.BETAS.ALL], - personalDetails: {}, - }); - } - - await waitForBatchedUpdates(); - - // Then we expect the IOU report and reportPreview to update with new totals - - expect(iouReport).toBeTruthy(); - expect(iouReport).toHaveProperty('reportID'); - expect(iouReport).toHaveProperty('chatReportID'); - expect(iouReport?.total).toBe(30000); - - await waitForBatchedUpdates(); - - // Given a transaction thread - thread = buildTransactionThread(createIOUAction, iouReport); - - expect(thread.participants).toEqual({[CARLOS_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, role: CONST.REPORT.ROLE.ADMIN}}); - - const participantAccountIDs = Object.keys(thread.participants ?? {}).map(Number); - const userLogins = getLoginsByAccountIDs(participantAccountIDs); - jest.advanceTimersByTime(10); - openReport({ - reportID: thread.reportID, - introSelected: TEST_INTRO_SELECTED, - betas: undefined, - participantLoginList: userLogins, - newReportObject: thread, - parentReportActionID: createIOUAction?.reportActionID, - }); - await waitForBatchedUpdates(); - - Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, - callback: (val) => (reportActions = val), - }); - await waitForBatchedUpdates(); - - await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, - callback: (report) => { - Onyx.disconnect(connection); - expect(report).toBeTruthy(); - resolve(); - }, - }); - }); - - jest.advanceTimersByTime(10); - - // When a comment is added - let iouPreview = getReportPreviewAction(chatReport?.reportID, iouReport?.reportID); - const ancestors = []; - ancestors.push(...(iouReport && createIOUAction ? [{report: iouReport, reportAction: createIOUAction, shouldDisplayNewMarker: false}] : [])); - ancestors.push(...(chatReport && iouPreview ? [{report: chatReport, reportAction: iouPreview, shouldDisplayNewMarker: false}] : [])); - addComment({ - report: thread, - notifyReportID: thread.reportID, - ancestors, - text: 'Testing a comment', - timezoneParam: CONST.DEFAULT_TIME_ZONE, - currentUserAccountID: CARLOS_ACCOUNT_ID, - }); - await waitForBatchedUpdates(); - - // Then comment details should match the expected report action - const resultAction = Object.values(reportActions ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT); - reportActionID = resultAction?.reportActionID; - expect(resultAction?.message).toEqual(REPORT_ACTION.message); - expect(resultAction?.person).toEqual(REPORT_ACTION.person); - - await waitForBatchedUpdates(); - - // Then the childVisibleActionCount of createIOUAction and iouPreview should be increased by 1 - const allReportActions = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - waitForCollectionCallback: true, - callback: (actions) => { - Onyx.disconnect(connection); - resolve(actions); - }, - }); - }); - - const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; - createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find( - (reportAction): reportAction is ReportAction => - isMoneyRequestAction(reportAction) && reportAction.reportActionID === createIOUAction?.reportActionID, - ); - expect(createIOUAction).toBeTruthy(); - expect(createIOUAction?.childVisibleActionCount).toEqual(1); - expect(createIOUAction?.childCommenterCount).toEqual(1); - - iouPreview = getReportPreviewAction(chatReport?.reportID, iouReport?.reportID); - expect(iouPreview).toBeTruthy(); - expect(iouPreview?.childVisibleActionCount).toEqual(1); - expect(iouPreview?.childCommenterCount).toEqual(1); - - // When we delete the first expense - mockFetch?.pause?.(); - jest.advanceTimersByTime(10); - if (transaction && createIOUAction) { - deleteMoneyRequest({ - transactionID: transaction.transactionID, - reportAction: createIOUAction, - transactions: {}, - violations: {}, - iouReport, - chatReport, - allTransactionViolationsParam: {}, - currentUserAccountID: TEST_USER_ACCOUNT_ID, - currentUserEmail: TEST_USER_LOGIN, - }); - } - - await waitForBatchedUpdates(); - - // Then we expect the reportPreview to update with new childVisibleActionCount - - iouPreview = getReportPreviewAction(chatReport?.reportID, iouReport?.reportID); - expect(iouPreview).toBeTruthy(); - expect(iouPreview?.childVisibleActionCount).toEqual(0); - expect(iouPreview?.childCommenterCount).toEqual(0); - - // When we resume - mockFetch?.resume?.(); - await waitForBatchedUpdates(); - - // Then we expect the reportPreview to update with new childVisibleActionCount - iouPreview = getReportPreviewAction(chatReport?.reportID, iouReport?.reportID); - expect(iouPreview).toBeTruthy(); - expect(iouPreview?.childVisibleActionCount).toEqual(0); - expect(iouPreview?.childCommenterCount).toEqual(0); - }); - }); - - describe('bulk deleteMoneyRequest', () => { - const TEST_USER_ACCOUNT_ID = 1; - const TEST_USER_LOGIN = 'test@email.com'; - - it('update IOU report total properly for bulk deletion of expenses', async () => { - const expenseReport: Report = { - ...createRandomReport(11, undefined), - type: CONST.REPORT.TYPE.EXPENSE, - total: 30, - currency: CONST.CURRENCY.USD, - unheldTotal: 20, - unheldNonReimbursableTotal: 20, - }; - const transaction1: Transaction = { - ...createRandomTransaction(1), - amount: 10, - comment: {hold: '123'}, - currency: CONST.CURRENCY.USD, - reportID: expenseReport.reportID, - reimbursable: true, - }; - const moneyRequestAction1: ReportAction = { - ...createRandomReportAction(1), - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, - childReportID: '1', - originalMessage: { - IOUReportID: expenseReport.reportID, - amount: transaction1.amount, - currency: transaction1.currency, - type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, - }, - message: undefined, - previousMessage: undefined, - }; - const transaction2: Transaction = {...createRandomTransaction(2), amount: 10, currency: CONST.CURRENCY.USD, reportID: expenseReport.reportID, reimbursable: false}; - const moneyRequestAction2: ReportAction = { - ...createRandomReportAction(2), - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, - childReportID: '2', - originalMessage: { - IOUReportID: expenseReport.reportID, - amount: transaction2.amount, - currency: transaction2.currency, - type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, - }, - message: undefined, - previousMessage: undefined, - }; - const transaction3: Transaction = {...createRandomTransaction(3), amount: 10, currency: CONST.CURRENCY.USD, reportID: expenseReport.reportID, reimbursable: false}; - - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, transaction1); - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`, transaction2); - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction3.transactionID}`, transaction3); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); - - const selectedTransactionIDs = [transaction1.transactionID, transaction2.transactionID]; - deleteMoneyRequest({ - transactionID: transaction1.transactionID, - reportAction: moneyRequestAction1, - transactions: {}, - violations: {}, - iouReport: expenseReport, - chatReport: expenseReport, - transactionIDsPendingDeletion: [], - selectedTransactionIDs, - allTransactionViolationsParam: {}, - currentUserAccountID: TEST_USER_ACCOUNT_ID, - currentUserEmail: TEST_USER_LOGIN, - }); - deleteMoneyRequest({ - transactionID: transaction2.transactionID, - reportAction: moneyRequestAction2, - transactions: {}, - violations: {}, - iouReport: expenseReport, - chatReport: expenseReport, - transactionIDsPendingDeletion: [transaction1.transactionID], - selectedTransactionIDs, - allTransactionViolationsParam: {}, - currentUserAccountID: TEST_USER_ACCOUNT_ID, - currentUserEmail: TEST_USER_LOGIN, - }); - await waitForBatchedUpdates(); - - const report = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, - callback: (val) => { - Onyx.disconnect(connection); - resolve(val); - }, - }); - }); - - expect(report?.total).toBe(10); - expect(report?.unheldTotal).toBe(10); - expect(report?.unheldNonReimbursableTotal).toBe(10); - }); - }); - - describe('deleteMoneyRequest with allTransactionViolationsParam', () => { - const TEST_USER_ACCOUNT_ID = 1; - const TEST_USER_LOGIN = 'test@email.com'; - it('should pass transaction violations to hasOutstandingChildRequest correctly', async () => { - // Given an expense report with a transaction - const expenseReport: Report = { - ...createRandomReport(20, undefined), - type: CONST.REPORT.TYPE.EXPENSE, - total: 100, - currency: CONST.CURRENCY.USD, - }; - - const transaction1: Transaction = { - ...createRandomTransaction(20), - amount: 100, - currency: CONST.CURRENCY.USD, - reportID: expenseReport.reportID, - reimbursable: true, - }; - - const moneyRequestAction1: ReportAction = { - ...createRandomReportAction(20), - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, - childReportID: '20', - originalMessage: { - IOUReportID: expenseReport.reportID, - amount: transaction1.amount, - currency: transaction1.currency, - type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, - }, - message: undefined, - previousMessage: undefined, - }; - - // When we set up the transaction and report in Onyx - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, transaction1); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); - - // And we call deleteMoneyRequest with transaction violations - const transactionViolations = { - [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction1.transactionID}`]: [ - { - name: CONST.VIOLATIONS.AUTO_REPORTED_REJECTED_EXPENSE, - type: CONST.VIOLATION_TYPES.VIOLATION, - }, - ], - }; - - deleteMoneyRequest({ - transactionID: transaction1.transactionID, - reportAction: moneyRequestAction1, - transactions: {}, - violations: {}, - iouReport: expenseReport, - chatReport: expenseReport, - allTransactionViolationsParam: transactionViolations, - currentUserAccountID: TEST_USER_ACCOUNT_ID, - currentUserEmail: TEST_USER_LOGIN, - }); - - await waitForBatchedUpdates(); - - // Then the transaction should be deleted - const deletedTransaction = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, - callback: (val) => { - Onyx.disconnect(connection); - resolve(val); - }, - }); - }); - - expect(deletedTransaction).toBeUndefined(); - }); - - it('should handle empty transaction violations correctly', async () => { - // Given an expense report with a transaction - const expenseReport: Report = { - ...createRandomReport(21, undefined), - type: CONST.REPORT.TYPE.EXPENSE, - total: 50, - currency: CONST.CURRENCY.USD, - }; - - const transaction1: Transaction = { - ...createRandomTransaction(21), - amount: 50, - currency: CONST.CURRENCY.USD, - reportID: expenseReport.reportID, - reimbursable: true, - }; - - const moneyRequestAction1: ReportAction = { - ...createRandomReportAction(21), - actionName: CONST.REPORT.ACTIONS.TYPE.IOU, - childReportID: '21', - originalMessage: { - IOUReportID: expenseReport.reportID, - amount: transaction1.amount, - currency: transaction1.currency, - type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, - }, - message: undefined, - previousMessage: undefined, - }; - - // When we set up the transaction and report in Onyx - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, transaction1); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); - - // And we call deleteMoneyRequest with empty transaction violations - deleteMoneyRequest({ - transactionID: transaction1.transactionID, - reportAction: moneyRequestAction1, - transactions: {}, - violations: {}, - iouReport: expenseReport, - chatReport: expenseReport, - allTransactionViolationsParam: {}, - currentUserAccountID: TEST_USER_ACCOUNT_ID, - currentUserEmail: TEST_USER_LOGIN, - }); - - await waitForBatchedUpdates(); - - // Then the transaction should be deleted - const deletedTransaction = await new Promise>((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, - callback: (val) => { - Onyx.disconnect(connection); - resolve(val); - }, - }); - }); - - expect(deletedTransaction).toBeUndefined(); - }); - }); - describe('submitReport', () => { it('correctly submits a report', () => { const amount = 10000; diff --git a/tests/actions/IOUTest/DeleteMoneyRequestTest.ts b/tests/actions/IOUTest/DeleteMoneyRequestTest.ts new file mode 100644 index 0000000000000..1e93d8a005a7b --- /dev/null +++ b/tests/actions/IOUTest/DeleteMoneyRequestTest.ts @@ -0,0 +1,1723 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import Onyx from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {getReportPreviewAction} from '@libs/actions/IOU'; +import {deleteMoneyRequest} from '@libs/actions/IOU/DeleteMoneyRequest'; +import {requestMoney} from '@libs/actions/IOU/TrackExpense'; +import {updateMoneyRequestAmountAndCurrency} from '@libs/actions/IOU/UpdateMoneyRequest'; +import initOnyxDerivedValues from '@libs/actions/OnyxDerived'; +import {addComment, openReport} from '@libs/actions/Report'; +import {subscribeToUserEvents} from '@libs/actions/User'; +import {getLoginsByAccountIDs} from '@libs/PersonalDetailsUtils'; +import {getOriginalMessage, getReportActionMessage, getReportActionText, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import type {OptimisticChatReport} from '@libs/ReportUtils'; +import {buildTransactionThread, isIOUReport} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import IntlStore from '@src/languages/IntlStore'; +import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; +import DateUtils from '@src/libs/DateUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {IntroSelected, Report} from '@src/types/onyx'; +import type ReportAction from '@src/types/onyx/ReportAction'; +import type {ReportActions} from '@src/types/onyx/ReportAction'; +import type Transaction from '@src/types/onyx/Transaction'; +import createRandomReportAction from '../../utils/collections/reportActions'; +import {createRandomReport} from '../../utils/collections/reports'; +import createRandomTransaction from '../../utils/collections/transaction'; +import PusherHelper from '../../utils/PusherHelper'; +import type {MockFetch} from '../../utils/TestHelper'; +import {getGlobalFetchMock, getOnyxData, setPersonalDetails, signInWithTestUser} from '../../utils/TestHelper'; +import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; + +const topMostReportID = '23423423'; +jest.mock('@src/libs/Navigation/Navigation', () => ({ + navigate: jest.fn(), + dismissModal: jest.fn(), + dismissToPreviousRHP: jest.fn(), + dismissToSuperWideRHP: jest.fn(), + navigateBackToLastSuperWideRHPScreen: jest.fn(), + dismissModalWithReport: jest.fn(), + goBack: jest.fn(), + getTopmostReportId: jest.fn(() => topMostReportID), + setNavigationActionToMicrotaskQueue: jest.fn(), + removeScreenByKey: jest.fn(), + isNavigationReady: jest.fn(() => Promise.resolve()), + getReportRouteByID: jest.fn(), + getActiveRouteWithoutParams: jest.fn(), + getActiveRoute: jest.fn(), + navigationRef: { + getRootState: jest.fn(), + isReady: jest.fn(() => true), + }, +})); + +jest.mock('@react-navigation/native'); + +jest.mock('@src/libs/actions/Report', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const originalModule = jest.requireActual('@src/libs/actions/Report'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + ...originalModule, + notifyNewAction: jest.fn(), + }; +}); +jest.mock('@libs/Navigation/helpers/isSearchTopmostFullScreenRoute', () => jest.fn()); +jest.mock('@libs/Navigation/helpers/isReportTopmostSplitNavigator', () => jest.fn()); +jest.mock('@libs/deferredLayoutWrite', () => ({ + registerDeferredWrite: (_key: string, callback: () => void) => callback(), + flushDeferredWrite: jest.fn(), + cancelDeferredWrite: jest.fn(), + hasDeferredWrite: () => false, + getOptimisticWatchKey: () => undefined, +})); +jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}}))); + +const TEST_INTRO_SELECTED: IntroSelected = { + choice: CONST.ONBOARDING_CHOICES.SUBMIT, + isInviteOnboardingComplete: false, +}; + +const RORY_EMAIL = 'rory@expensifail.com'; +const RORY_ACCOUNT_ID = 3; +const CARLOS_ACCOUNT_ID = 1; + +OnyxUpdateManager(); + +describe('actions/IOU/DeleteMoneyRequest', () => { + let mockFetch: MockFetch; + + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + initialKeyStates: { + [ONYXKEYS.SESSION]: {accountID: RORY_ACCOUNT_ID, email: RORY_EMAIL}, + [ONYXKEYS.PERSONAL_DETAILS_LIST]: {[RORY_ACCOUNT_ID]: {accountID: RORY_ACCOUNT_ID, login: RORY_EMAIL}}, + }, + }); + initOnyxDerivedValues(); + IntlStore.load(CONST.LOCALES.EN); + return waitForBatchedUpdates(); + }); + + beforeEach(() => { + global.fetch = getGlobalFetchMock(); + mockFetch = fetch as MockFetch; + return Onyx.clear().then(waitForBatchedUpdates); + }); + + afterEach(() => { + mockFetch?.mockClear(); + }); + + describe('deleteMoneyRequest', () => { + const amount = 10000; + const comment = 'Send me money please'; + let chatReport: OnyxEntry; + let iouReport: OnyxEntry; + let createIOUAction: OnyxEntry>; + let transaction: OnyxEntry; + let thread: OptimisticChatReport; + const TEST_USER_ACCOUNT_ID = 1; + const TEST_USER_LOGIN = 'test@test.com'; + let IOU_REPORT_ID: string | undefined; + let IOU_REPORT: OnyxEntry; + let reportActionID; + const REPORT_ACTION: OnyxEntry = { + actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, + actorAccountID: TEST_USER_ACCOUNT_ID, + automatic: false, + avatar: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/avatar_3.png', + message: [{type: 'COMMENT', html: 'Testing a comment', text: 'Testing a comment', translationKey: ''}], + person: [{type: 'TEXT', style: 'strong', text: 'Test User'}], + shouldShow: true, + created: DateUtils.getDBTime(), + reportActionID: '1', + originalMessage: { + html: '', + whisperedTo: [], + }, + }; + + let reportActions: OnyxCollection; + + beforeEach(async () => { + // Given mocks are cleared and helpers are set up + jest.clearAllMocks(); + PusherHelper.setup(); + + // Given a test user is signed in with Onyx setup and some initial data + await signInWithTestUser(TEST_USER_ACCOUNT_ID, TEST_USER_LOGIN); + subscribeToUserEvents(TEST_USER_ACCOUNT_ID, undefined); + await waitForBatchedUpdates(); + await setPersonalDetails(TEST_USER_LOGIN, TEST_USER_ACCOUNT_ID); + + // When a submit IOU expense is made + requestMoney({ + report: chatReport, + participantParams: { + payeeEmail: TEST_USER_LOGIN, + payeeAccountID: TEST_USER_ACCOUNT_ID, + participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, + }, + transactionParams: { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant: '', + comment, + }, + shouldGenerateTransactionThreadReport: true, + isASAPSubmitBetaEnabled: false, + currentUserAccountIDParam: 123, + currentUserEmailParam: 'existing@example.com', + transactionViolations: {}, + policyRecentlyUsedCurrencies: [], + existingTransactionDraft: undefined, + draftTransactionIDs: [], + isSelfTourViewed: false, + quickAction: undefined, + betas: [CONST.BETAS.ALL], + personalDetails: {}, + }); + await waitForBatchedUpdates(); + + // When fetching all reports from Onyx + const allReports = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (reports) => { + Onyx.disconnect(connection); + resolve(reports); + }, + }); + }); + + // Then we should have exactly 3 reports + expect(Object.values(allReports ?? {}).length).toBe(3); + + // Then one of them should be a chat report with relevant properties + chatReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.CHAT); + expect(chatReport).toBeTruthy(); + expect(chatReport).toHaveProperty('reportID'); + expect(chatReport).toHaveProperty('iouReportID'); + + // Then one of them should be an IOU report with relevant properties + iouReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.IOU); + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + + // Then their IDs should reference each other + expect(chatReport?.iouReportID).toBe(iouReport?.reportID); + expect(iouReport?.chatReportID).toBe(chatReport?.reportID); + + // Storing IOU Report ID for further reference + IOU_REPORT_ID = chatReport?.iouReportID; + IOU_REPORT = iouReport; + + await waitForBatchedUpdates(); + + // When fetching all report actions from Onyx + const allReportActions = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (actions) => { + Onyx.disconnect(connection); + resolve(actions); + }, + }); + }); + + // Then we should find an IOU action with specific properties + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; + createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find((reportAction): reportAction is ReportAction => + isMoneyRequestAction(reportAction), + ); + expect(createIOUAction).toBeTruthy(); + expect(createIOUAction && getOriginalMessage(createIOUAction)?.IOUReportID).toBe(iouReport?.reportID); + + // When fetching all transactions from Onyx + let allTransactions: OnyxCollection; + await getOnyxData({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (val) => { + allTransactions = val; + }, + }); + + // Then we should find a specific transaction with relevant properties + transaction = Object.values(allTransactions ?? {}).find((t) => t); + expect(transaction).toBeTruthy(); + expect(transaction?.amount).toBe(amount); + expect(transaction?.reportID).toBe(iouReport?.reportID); + expect(createIOUAction && getOriginalMessage(createIOUAction)?.IOUTransactionID).toBe(transaction?.transactionID); + }); + + afterEach(PusherHelper.teardown); + + it('delete an expense (IOU Action and transaction) successfully', async () => { + // Given the fetch operations are paused and an expense is initiated + mockFetch?.pause?.(); + + if (transaction && createIOUAction) { + // When the expense is deleted + deleteMoneyRequest({ + transactionID: transaction?.transactionID, + reportAction: createIOUAction, + transactions: {}, + violations: {}, + iouReport, + chatReport, + isChatIOUReportArchived: true, + allTransactionViolationsParam: {}, + currentUserAccountID: TEST_USER_ACCOUNT_ID, + currentUserEmail: TEST_USER_LOGIN, + }); + } + await waitForBatchedUpdates(); + + // Then we check if the IOU report action is removed from the report actions collection + let reportActionsForReport = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (actionsForReport) => { + Onyx.disconnect(connection); + resolve(actionsForReport); + }, + }); + }); + + createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction): reportAction is ReportAction => + isMoneyRequestAction(reportAction), + ); + // Then the IOU Action should be truthy for offline support. + expect(createIOUAction).toBeTruthy(); + + // Then we check if the transaction is removed from the transactions collection + const t = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`, + waitForCollectionCallback: false, + callback: (transactionResult) => { + Onyx.disconnect(connection); + resolve(transactionResult); + }, + }); + }); + + expect(t).toBeTruthy(); + expect(t?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + + // Given fetch operations are resumed + mockFetch?.resume?.(); + await waitForBatchedUpdates(); + + // Then we recheck the IOU report action from the report actions collection + reportActionsForReport = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (actionsForReport) => { + Onyx.disconnect(connection); + resolve(actionsForReport); + }, + }); + }); + + createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction): reportAction is ReportAction => + isMoneyRequestAction(reportAction), + ); + expect(createIOUAction).toBeFalsy(); + + // Then we recheck the transaction from the transactions collection + const tr = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`, + waitForCollectionCallback: false, + callback: (transactionResult) => { + Onyx.disconnect(connection); + resolve(transactionResult); + }, + }); + }); + + expect(tr).toBeFalsy(); + }); + + it('delete the IOU report when there are no expenses left in the IOU report', async () => { + // Given an IOU report and a paused fetch state + mockFetch?.pause?.(); + + if (transaction && createIOUAction) { + // When the IOU expense is deleted + deleteMoneyRequest({ + transactionID: transaction?.transactionID, + reportAction: createIOUAction, + transactions: {}, + violations: {}, + iouReport, + chatReport, + isChatIOUReportArchived: true, + allTransactionViolationsParam: {}, + currentUserAccountID: TEST_USER_ACCOUNT_ID, + currentUserEmail: TEST_USER_LOGIN, + }); + } + await waitForBatchedUpdates(); + + let report = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (res) => { + Onyx.disconnect(connection); + resolve(res); + }, + }); + }); + + // Then the report should be truthy for offline support + expect(report).toBeTruthy(); + + // Given the resumed fetch state + mockFetch?.resume?.(); + await waitForBatchedUpdates(); + + report = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (res) => { + Onyx.disconnect(connection); + resolve(res); + }, + }); + }); + + // Then the report should be falsy so that there is no trace of the expense. + expect(report).toBeFalsy(); + }); + + it('does not delete the IOU report when there are expenses left in the IOU report', async () => { + // Given multiple expenses on an IOU report + requestMoney({ + report: chatReport, + participantParams: { + payeeEmail: TEST_USER_LOGIN, + payeeAccountID: TEST_USER_ACCOUNT_ID, + participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, + }, + transactionParams: { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant: '', + comment, + }, + shouldGenerateTransactionThreadReport: true, + isASAPSubmitBetaEnabled: false, + currentUserAccountIDParam: 123, + currentUserEmailParam: 'existing@example.com', + transactionViolations: {}, + policyRecentlyUsedCurrencies: [], + existingTransactionDraft: undefined, + draftTransactionIDs: [], + isSelfTourViewed: false, + quickAction: undefined, + betas: [CONST.BETAS.ALL], + personalDetails: {}, + }); + + await waitForBatchedUpdates(); + + // When we attempt to delete an expense from the IOU report + mockFetch?.pause?.(); + if (transaction && createIOUAction) { + deleteMoneyRequest({ + transactionID: transaction?.transactionID, + reportAction: createIOUAction, + transactions: {}, + violations: {}, + iouReport, + chatReport, + allTransactionViolationsParam: {}, + currentUserAccountID: TEST_USER_ACCOUNT_ID, + currentUserEmail: TEST_USER_LOGIN, + }); + } + await waitForBatchedUpdates(); + + // Then expect that the IOU report still exists + let allReports = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (reports) => { + Onyx.disconnect(connection); + resolve(reports); + }, + }); + }); + + await waitForBatchedUpdates(); + + iouReport = Object.values(allReports ?? {}).find((report) => isIOUReport(report)); + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + + // Given the resumed fetch state + await mockFetch?.resume?.(); + + allReports = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (reports) => { + Onyx.disconnect(connection); + resolve(reports); + }, + }); + }); + // Then expect that the IOU report still exists + iouReport = Object.values(allReports ?? {}).find((report) => isIOUReport(report)); + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + }); + + it('delete the transaction thread if there are no visible comments in the thread', async () => { + // Given all promises are resolved + await waitForBatchedUpdates(); + jest.advanceTimersByTime(10); + + // Given a transaction thread + thread = buildTransactionThread(createIOUAction, iouReport); + + expect(thread.participants).toStrictEqual({[CARLOS_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, role: CONST.REPORT.ROLE.ADMIN}}); + + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, + callback: (val) => (reportActions = val), + }); + + await waitForBatchedUpdates(); + + jest.advanceTimersByTime(10); + + // Given User logins from the participant accounts + const participantAccountIDs = Object.keys(thread.participants ?? {}).map(Number); + const userLogins = getLoginsByAccountIDs(participantAccountIDs); + + // When Opening a thread report with the given details + openReport({ + reportID: thread.reportID, + introSelected: TEST_INTRO_SELECTED, + betas: undefined, + participantLoginList: userLogins, + newReportObject: thread, + parentReportActionID: createIOUAction?.reportActionID, + }); + await waitForBatchedUpdates(); + + // Then The iou action has the transaction report id as a child report ID + const allReportActions = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (actions) => { + Onyx.disconnect(connection); + resolve(actions); + }, + }); + }); + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; + createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find((reportAction): reportAction is ReportAction => + isMoneyRequestAction(reportAction), + ); + expect(createIOUAction?.childReportID).toBe(thread.reportID); + + await waitForBatchedUpdates(); + + // Given Fetch is paused and timers have advanced + mockFetch?.pause?.(); + jest.advanceTimersByTime(10); + + if (transaction && createIOUAction) { + // When Deleting an expense + deleteMoneyRequest({ + transactionID: transaction?.transactionID, + reportAction: createIOUAction, + transactions: {}, + violations: {}, + iouReport, + chatReport, + allTransactionViolationsParam: {}, + currentUserAccountID: TEST_USER_ACCOUNT_ID, + currentUserEmail: TEST_USER_LOGIN, + }); + } + await waitForBatchedUpdates(); + + // Then The report for the given thread ID does not exist + let report = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, + waitForCollectionCallback: false, + callback: (reportData) => { + Onyx.disconnect(connection); + resolve(reportData); + }, + }); + }); + + expect(report?.reportID).toBeFalsy(); + mockFetch?.resume?.(); + + // Then After resuming fetch, the report for the given thread ID still does not exist + report = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, + waitForCollectionCallback: false, + callback: (reportData) => { + Onyx.disconnect(connection); + resolve(reportData); + }, + }); + }); + + expect(report?.reportID).toBeFalsy(); + }); + + it('delete the transaction thread if there are only changelogs (i.e. MODIFIED_EXPENSE actions) in the thread', async () => { + // Given all promises are resolved + await waitForBatchedUpdates(); + jest.advanceTimersByTime(10); + + // Given a transaction thread + thread = buildTransactionThread(createIOUAction, iouReport); + + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, + callback: (val) => (reportActions = val), + }); + + await waitForBatchedUpdates(); + + jest.advanceTimersByTime(10); + + // Given User logins from the participant accounts + const participantAccountIDs = Object.keys(thread.participants ?? {}).map(Number); + const userLogins = getLoginsByAccountIDs(participantAccountIDs); + + // When Opening a thread report with the given details + openReport({ + reportID: thread.reportID, + introSelected: TEST_INTRO_SELECTED, + betas: undefined, + participantLoginList: userLogins, + newReportObject: thread, + parentReportActionID: createIOUAction?.reportActionID, + }); + await waitForBatchedUpdates(); + + // Then The iou action has the transaction report id as a child report ID + const allReportActions = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (actions) => { + Onyx.disconnect(connection); + resolve(actions); + }, + }); + }); + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; + createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find((reportAction): reportAction is ReportAction => + isMoneyRequestAction(reportAction), + ); + + expect(createIOUAction?.childReportID).toBe(thread.reportID); + + await waitForBatchedUpdates(); + + jest.advanceTimersByTime(10); + if (transaction && createIOUAction) { + updateMoneyRequestAmountAndCurrency({ + transactionID: transaction.transactionID, + transactions: {}, + transactionThreadReport: thread, + parentReport: iouReport, + transactionViolations: {}, + amount: 20000, + currency: CONST.CURRENCY.USD, + taxAmount: 0, + taxCode: '', + taxValue: '', + policy: { + id: '123', + role: CONST.POLICY.ROLE.USER, + type: CONST.POLICY.TYPE.TEAM, + name: '', + owner: '', + outputCurrency: '', + isPolicyExpenseChatEnabled: false, + }, + policyTagList: {}, + policyCategories: {}, + currentUserAccountIDParam: 123, + currentUserEmailParam: 'existing@example.com', + isASAPSubmitBetaEnabled: false, + policyRecentlyUsedCurrencies: [], + parentReportNextStep: undefined, + }); + } + await waitForBatchedUpdates(); + + // Verify there are two actions (created + changelog) + expect(Object.values(reportActions ?? {}).length).toBe(2); + + // Fetch the updated IOU Action from Onyx + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForReport) => { + Onyx.disconnect(connection); + createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction): reportAction is ReportAction => + isMoneyRequestAction(reportAction), + ); + resolve(); + }, + }); + }); + + if (transaction && createIOUAction) { + // When Deleting an expense + deleteMoneyRequest({ + transactionID: transaction?.transactionID, + reportAction: createIOUAction, + transactions: {}, + violations: {}, + iouReport, + chatReport, + allTransactionViolationsParam: {}, + currentUserAccountID: TEST_USER_ACCOUNT_ID, + currentUserEmail: TEST_USER_LOGIN, + }); + } + await waitForBatchedUpdates(); + + // Then, the report for the given thread ID does not exist + const report = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, + waitForCollectionCallback: false, + callback: (reportData) => { + Onyx.disconnect(connection); + resolve(reportData); + }, + }); + }); + + expect(report?.reportID).toBeFalsy(); + }); + + it('should delete the transaction thread regardless of whether there are visible comments in the thread.', async () => { + // Given initial environment is set up + await waitForBatchedUpdates(); + + // Given a transaction thread + thread = buildTransactionThread(createIOUAction, iouReport); + + expect(thread.participants).toEqual({[CARLOS_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, role: CONST.REPORT.ROLE.ADMIN}}); + + const participantAccountIDs = Object.keys(thread.participants ?? {}).map(Number); + const userLogins = getLoginsByAccountIDs(participantAccountIDs); + jest.advanceTimersByTime(10); + openReport({ + reportID: thread.reportID, + introSelected: TEST_INTRO_SELECTED, + betas: undefined, + participantLoginList: userLogins, + newReportObject: thread, + parentReportActionID: createIOUAction?.reportActionID, + }); + await waitForBatchedUpdates(); + + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, + callback: (val) => (reportActions = val), + }); + await waitForBatchedUpdates(); + + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, + callback: (report) => { + Onyx.disconnect(connection); + expect(report).toBeTruthy(); + resolve(); + }, + }); + }); + + jest.advanceTimersByTime(10); + + // When a comment is added + addComment({ + report: thread, + notifyReportID: thread.reportID, + ancestors: [], + text: 'Testing a comment', + timezoneParam: CONST.DEFAULT_TIME_ZONE, + currentUserAccountID: RORY_ACCOUNT_ID, + }); + await waitForBatchedUpdates(); + + // Then comment details should match the expected report action + const resultAction = Object.values(reportActions ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT); + reportActionID = resultAction?.reportActionID; + expect(resultAction?.message).toEqual(REPORT_ACTION.message); + expect(resultAction?.person).toEqual(REPORT_ACTION.person); + + await waitForBatchedUpdates(); + + // Then the report should have 2 actions + expect(Object.values(reportActions ?? {}).length).toBe(2); + const resultActionAfter = reportActionID ? reportActions?.[reportActionID] : undefined; + expect(resultActionAfter?.pendingAction).toBeUndefined(); + + mockFetch?.pause?.(); + + const allReportActions = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (actions) => { + Onyx.disconnect(connection); + resolve(actions); + }, + }); + }); + + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; + createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find( + (reportAction): reportAction is ReportAction => reportAction.reportActionID === createIOUAction?.reportActionID, + ); + + if (transaction && createIOUAction) { + // When deleting expense + deleteMoneyRequest({ + transactionID: transaction?.transactionID, + reportAction: createIOUAction, + transactions: {}, + violations: {}, + iouReport, + chatReport, + allTransactionViolationsParam: {}, + currentUserAccountID: TEST_USER_ACCOUNT_ID, + currentUserEmail: TEST_USER_LOGIN, + }); + } + await waitForBatchedUpdates(); + + // Then the transaction thread report should be ready to be deleted + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, + waitForCollectionCallback: false, + callback: (report) => { + Onyx.disconnect(connection); + expect(report?.reportID).toBeFalsy(); + resolve(); + }, + }); + }); + + // When fetch resumes + // Then the transaction thread report should be deleted + mockFetch?.resume?.(); + await waitForBatchedUpdates(); + + // Then the transaction thread report should be deleted + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, + waitForCollectionCallback: false, + callback: (report) => { + Onyx.disconnect(connection); + expect(report).toBeFalsy(); + resolve(); + }, + }); + }); + }); + + it('update the moneyRequestPreview to show [Deleted expense] when appropriate', async () => { + await waitForBatchedUpdates(); + + // Given a thread report + + jest.advanceTimersByTime(10); + thread = buildTransactionThread(createIOUAction, iouReport); + + expect(thread.participants).toStrictEqual({[CARLOS_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, role: CONST.REPORT.ROLE.ADMIN}}); + + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, + callback: (val) => (reportActions = val), + }); + await waitForBatchedUpdates(); + + jest.advanceTimersByTime(10); + const participantAccountIDs = Object.keys(thread.participants ?? {}).map(Number); + const userLogins = getLoginsByAccountIDs(participantAccountIDs); + openReport({ + reportID: thread.reportID, + introSelected: TEST_INTRO_SELECTED, + betas: undefined, + participantLoginList: userLogins, + newReportObject: thread, + parentReportActionID: createIOUAction?.reportActionID, + }); + + await waitForBatchedUpdates(); + + const allReportActions = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (actions) => { + Onyx.disconnect(connection); + resolve(actions); + }, + }); + }); + + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; + createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find((reportAction): reportAction is ReportAction => + isMoneyRequestAction(reportAction), + ); + expect(createIOUAction?.childReportID).toBe(thread.reportID); + + await waitForBatchedUpdates(); + + // Given an added comment to the thread report + + jest.advanceTimersByTime(10); + + addComment({ + report: thread, + notifyReportID: thread.reportID, + ancestors: [], + text: 'Testing a comment', + timezoneParam: CONST.DEFAULT_TIME_ZONE, + currentUserAccountID: RORY_ACCOUNT_ID, + }); + await waitForBatchedUpdates(); + + // Fetch the updated IOU Action from Onyx due to addition of comment to transaction thread. + // This needs to be fetched as `deleteMoneyRequest` depends on `childVisibleActionCount` in `createIOUAction`. + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForReport) => { + Onyx.disconnect(connection); + createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction): reportAction is ReportAction => + isMoneyRequestAction(reportAction), + ); + resolve(); + }, + }); + }); + + let resultAction = Object.values(reportActions ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT); + reportActionID = resultAction?.reportActionID; + + expect(resultAction?.message).toEqual(REPORT_ACTION.message); + expect(resultAction?.person).toEqual(REPORT_ACTION.person); + expect(resultAction?.pendingAction).toBeUndefined(); + + await waitForBatchedUpdates(); + + // Verify there are three actions (created + addcomment) and our optimistic comment has been removed + expect(Object.values(reportActions ?? {}).length).toBe(2); + + let resultActionAfterUpdate = reportActionID ? reportActions?.[reportActionID] : undefined; + + // Verify that our action is no longer in the loading state + expect(resultActionAfterUpdate?.pendingAction).toBeUndefined(); + + await waitForBatchedUpdates(); + + // Given an added comment to the IOU report + + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${IOU_REPORT_ID}`, + callback: (val) => (reportActions = val), + }); + await waitForBatchedUpdates(); + + jest.advanceTimersByTime(10); + + if (IOU_REPORT_ID) { + addComment({ + report: IOU_REPORT, + notifyReportID: IOU_REPORT_ID, + ancestors: [], + text: 'Testing a comment', + timezoneParam: CONST.DEFAULT_TIME_ZONE, + currentUserAccountID: RORY_ACCOUNT_ID, + }); + } + await waitForBatchedUpdates(); + + resultAction = Object.values(reportActions ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT); + reportActionID = resultAction?.reportActionID; + + expect(resultAction?.message).toEqual(REPORT_ACTION.message); + expect(resultAction?.person).toEqual(REPORT_ACTION.person); + expect(resultAction?.pendingAction).toBeUndefined(); + + await waitForBatchedUpdates(); + + // Verify there are three actions (created + iou + addcomment) and our optimistic comment has been removed + expect(Object.values(reportActions ?? {}).length).toBe(3); + + resultActionAfterUpdate = reportActionID ? reportActions?.[reportActionID] : undefined; + + // Verify that our action is no longer in the loading state + expect(resultActionAfterUpdate?.pendingAction).toBeUndefined(); + + mockFetch?.pause?.(); + if (transaction && createIOUAction) { + // When we delete the expense + deleteMoneyRequest({ + transactionID: transaction.transactionID, + reportAction: createIOUAction, + transactions: {}, + violations: {}, + iouReport, + chatReport, + isChatIOUReportArchived: undefined, + allTransactionViolationsParam: {}, + currentUserAccountID: TEST_USER_ACCOUNT_ID, + currentUserEmail: TEST_USER_LOGIN, + }); + } + await waitForBatchedUpdates(); + + // Then we expect the moneyRequestPreview to show [Deleted expense] + + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForReport) => { + Onyx.disconnect(connection); + createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction): reportAction is ReportAction => + isMoneyRequestAction(reportAction), + ); + expect(getReportActionMessage(createIOUAction)?.isDeletedParentAction).toBeTruthy(); + resolve(); + }, + }); + }); + + // When we resume fetch + mockFetch?.resume?.(); + + // Then we expect the moneyRequestPreview to show [Deleted expense] + + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, + waitForCollectionCallback: false, + callback: (reportActionsForReport) => { + Onyx.disconnect(connection); + createIOUAction = Object.values(reportActionsForReport ?? {}).find((reportAction): reportAction is ReportAction => + isMoneyRequestAction(reportAction), + ); + expect(getReportActionMessage(createIOUAction)?.isDeletedParentAction).toBeTruthy(); + resolve(); + }, + }); + }); + }); + + it('update IOU report and reportPreview with new totals and messages if the IOU report is not deleted', async () => { + await waitForBatchedUpdates(); + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, + callback: (val) => (iouReport = val), + }); + await waitForBatchedUpdates(); + + // Given a second expense in addition to the first one + + jest.advanceTimersByTime(10); + const amount2 = 20000; + const comment2 = 'Send me money please 2'; + if (chatReport) { + requestMoney({ + report: chatReport, + participantParams: { + payeeEmail: TEST_USER_LOGIN, + payeeAccountID: TEST_USER_ACCOUNT_ID, + participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, + }, + transactionParams: { + amount: amount2, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant: '', + comment: comment2, + }, + shouldGenerateTransactionThreadReport: true, + isASAPSubmitBetaEnabled: false, + currentUserAccountIDParam: 123, + currentUserEmailParam: 'existing@example.com', + transactionViolations: {}, + policyRecentlyUsedCurrencies: [], + existingTransactionDraft: undefined, + draftTransactionIDs: [], + isSelfTourViewed: false, + quickAction: undefined, + betas: [CONST.BETAS.ALL], + personalDetails: {}, + }); + } + + await waitForBatchedUpdates(); + + // Then we expect the IOU report and reportPreview to update with new totals + + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + expect(iouReport?.total).toBe(30000); + + const iouPreview = chatReport?.reportID && iouReport?.reportID ? getReportPreviewAction(chatReport.reportID, iouReport.reportID) : undefined; + expect(iouPreview).toBeTruthy(); + expect(getReportActionText(iouPreview)).toBe('rory@expensifail.com owes $300.00'); + + // When we delete the first expense + mockFetch?.pause?.(); + jest.advanceTimersByTime(10); + if (transaction && createIOUAction) { + deleteMoneyRequest({ + transactionID: transaction.transactionID, + reportAction: createIOUAction, + transactions: {}, + violations: {}, + iouReport, + chatReport, + isChatIOUReportArchived: undefined, + allTransactionViolationsParam: {}, + currentUserAccountID: TEST_USER_ACCOUNT_ID, + currentUserEmail: TEST_USER_LOGIN, + }); + } + await waitForBatchedUpdates(); + + // Then we expect the IOU report and reportPreview to update with new totals + + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + expect(iouReport?.total).toBe(20000); + + // When we resume + mockFetch?.resume?.(); + + // Then we expect the IOU report and reportPreview to update with new totals + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + expect(iouReport?.total).toBe(20000); + }); + + it('navigate the user correctly to the iou Report when appropriate', async () => { + // Given multiple expenses on an IOU report + requestMoney({ + report: chatReport, + participantParams: { + payeeEmail: TEST_USER_LOGIN, + payeeAccountID: TEST_USER_ACCOUNT_ID, + participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, + }, + transactionParams: { + amount, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant: '', + comment, + }, + shouldGenerateTransactionThreadReport: true, + isASAPSubmitBetaEnabled: false, + currentUserAccountIDParam: 123, + currentUserEmailParam: 'existing@example.com', + transactionViolations: {}, + policyRecentlyUsedCurrencies: [], + existingTransactionDraft: undefined, + draftTransactionIDs: [], + isSelfTourViewed: false, + quickAction: undefined, + betas: [CONST.BETAS.ALL], + personalDetails: {}, + }); + await waitForBatchedUpdates(); + + // Given a thread report + jest.advanceTimersByTime(10); + thread = buildTransactionThread(createIOUAction, iouReport); + + expect(thread.participants).toStrictEqual({[CARLOS_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, role: CONST.REPORT.ROLE.ADMIN}}); + + jest.advanceTimersByTime(10); + const participantAccountIDs = Object.keys(thread.participants ?? {}).map(Number); + const userLogins = getLoginsByAccountIDs(participantAccountIDs); + openReport({ + reportID: thread.reportID, + introSelected: TEST_INTRO_SELECTED, + betas: undefined, + participantLoginList: userLogins, + newReportObject: thread, + parentReportActionID: createIOUAction?.reportActionID, + }); + await waitForBatchedUpdates(); + + const allReportActions = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (actions) => { + Onyx.disconnect(connection); + resolve(actions); + }, + }); + }); + + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; + createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find((reportAction): reportAction is ReportAction => + isMoneyRequestAction(reportAction), + ); + expect(createIOUAction?.childReportID).toBe(thread.reportID); + + // When we delete the expense, we should not delete the IOU report + mockFetch?.pause?.(); + + let navigateToAfterDelete; + if (transaction && createIOUAction) { + navigateToAfterDelete = deleteMoneyRequest({ + transactionID: transaction.transactionID, + reportAction: createIOUAction, + transactions: {}, + violations: {}, + iouReport, + chatReport, + isSingleTransactionView: true, + allTransactionViolationsParam: {}, + currentUserAccountID: TEST_USER_ACCOUNT_ID, + currentUserEmail: TEST_USER_LOGIN, + }); + } + + let allReports = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (reports) => { + Onyx.disconnect(connection); + resolve(reports); + }, + }); + }); + + iouReport = Object.values(allReports ?? {}).find((report) => isIOUReport(report)); + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + + await mockFetch?.resume?.(); + + allReports = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (reports) => { + Onyx.disconnect(connection); + resolve(reports); + }, + }); + }); + + iouReport = Object.values(allReports ?? {}).find((report) => isIOUReport(report)); + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + + // Then we expect to navigate to the iou report + expect(IOU_REPORT_ID).not.toBeUndefined(); + if (IOU_REPORT_ID) { + expect(navigateToAfterDelete).toEqual(ROUTES.REPORT_WITH_ID.getRoute(IOU_REPORT_ID)); + } + }); + + it('navigate the user correctly to the chat Report when appropriate', () => { + let navigateToAfterDelete; + if (transaction && createIOUAction) { + // When we delete the expense and we should delete the IOU report + navigateToAfterDelete = deleteMoneyRequest({ + transactionID: transaction.transactionID, + reportAction: createIOUAction, + transactions: {}, + violations: {}, + iouReport, + chatReport, + allTransactionViolationsParam: {}, + currentUserAccountID: TEST_USER_ACCOUNT_ID, + currentUserEmail: TEST_USER_LOGIN, + }); + } + // Then we expect to navigate to the chat report + expect(chatReport?.reportID).not.toBeUndefined(); + + if (chatReport?.reportID) { + expect(navigateToAfterDelete).toEqual(ROUTES.REPORT_WITH_ID.getRoute(chatReport?.reportID)); + } + }); + + it('update reportPreview with childVisibleActionCount if the IOU report is not deleted', async () => { + await waitForBatchedUpdates(); + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, + callback: (val) => (iouReport = val), + }); + await waitForBatchedUpdates(); + + // Given a second expense in addition to the first one + + jest.advanceTimersByTime(10); + const amount2 = 20000; + const comment2 = 'Send me money please 2'; + if (chatReport) { + requestMoney({ + report: chatReport, + participantParams: { + payeeEmail: TEST_USER_LOGIN, + payeeAccountID: TEST_USER_ACCOUNT_ID, + participant: {login: RORY_EMAIL, accountID: RORY_ACCOUNT_ID}, + }, + transactionParams: { + amount: amount2, + attendees: [], + currency: CONST.CURRENCY.USD, + created: '', + merchant: '', + comment: comment2, + }, + shouldGenerateTransactionThreadReport: true, + isASAPSubmitBetaEnabled: false, + transactionViolations: {}, + currentUserAccountIDParam: 123, + currentUserEmailParam: 'existing@example.com', + policyRecentlyUsedCurrencies: [], + quickAction: undefined, + isSelfTourViewed: false, + existingTransactionDraft: undefined, + draftTransactionIDs: [], + betas: [CONST.BETAS.ALL], + personalDetails: {}, + }); + } + + await waitForBatchedUpdates(); + + // Then we expect the IOU report and reportPreview to update with new totals + + expect(iouReport).toBeTruthy(); + expect(iouReport).toHaveProperty('reportID'); + expect(iouReport).toHaveProperty('chatReportID'); + expect(iouReport?.total).toBe(30000); + + await waitForBatchedUpdates(); + + // Given a transaction thread + thread = buildTransactionThread(createIOUAction, iouReport); + + expect(thread.participants).toEqual({[CARLOS_ACCOUNT_ID]: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, role: CONST.REPORT.ROLE.ADMIN}}); + + const participantAccountIDs = Object.keys(thread.participants ?? {}).map(Number); + const userLogins = getLoginsByAccountIDs(participantAccountIDs); + jest.advanceTimersByTime(10); + openReport({ + reportID: thread.reportID, + introSelected: TEST_INTRO_SELECTED, + betas: undefined, + participantLoginList: userLogins, + newReportObject: thread, + parentReportActionID: createIOUAction?.reportActionID, + }); + await waitForBatchedUpdates(); + + Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${thread.reportID}`, + callback: (val) => (reportActions = val), + }); + await waitForBatchedUpdates(); + + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${thread.reportID}`, + callback: (report) => { + Onyx.disconnect(connection); + expect(report).toBeTruthy(); + resolve(); + }, + }); + }); + + jest.advanceTimersByTime(10); + + // When a comment is added + let iouPreview = getReportPreviewAction(chatReport?.reportID, iouReport?.reportID); + const ancestors = []; + ancestors.push(...(iouReport && createIOUAction ? [{report: iouReport, reportAction: createIOUAction, shouldDisplayNewMarker: false}] : [])); + ancestors.push(...(chatReport && iouPreview ? [{report: chatReport, reportAction: iouPreview, shouldDisplayNewMarker: false}] : [])); + addComment({ + report: thread, + notifyReportID: thread.reportID, + ancestors, + text: 'Testing a comment', + timezoneParam: CONST.DEFAULT_TIME_ZONE, + currentUserAccountID: CARLOS_ACCOUNT_ID, + }); + await waitForBatchedUpdates(); + + // Then comment details should match the expected report action + const resultAction = Object.values(reportActions ?? {}).find((reportAction) => reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT); + reportActionID = resultAction?.reportActionID; + expect(resultAction?.message).toEqual(REPORT_ACTION.message); + expect(resultAction?.person).toEqual(REPORT_ACTION.person); + + await waitForBatchedUpdates(); + + // Then the childVisibleActionCount of createIOUAction and iouPreview should be increased by 1 + const allReportActions = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (actions) => { + Onyx.disconnect(connection); + resolve(actions); + }, + }); + }); + + const reportActionsForIOUReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.iouReportID}`]; + createIOUAction = Object.values(reportActionsForIOUReport ?? {}).find( + (reportAction): reportAction is ReportAction => + isMoneyRequestAction(reportAction as ReportAction) && (reportAction as ReportAction).reportActionID === createIOUAction?.reportActionID, + ); + expect(createIOUAction).toBeTruthy(); + expect(createIOUAction?.childVisibleActionCount).toEqual(1); + expect(createIOUAction?.childCommenterCount).toEqual(1); + + iouPreview = getReportPreviewAction(chatReport?.reportID, iouReport?.reportID); + expect(iouPreview).toBeTruthy(); + expect(iouPreview?.childVisibleActionCount).toEqual(1); + expect(iouPreview?.childCommenterCount).toEqual(1); + + // When we delete the first expense + mockFetch?.pause?.(); + jest.advanceTimersByTime(10); + if (transaction && createIOUAction) { + deleteMoneyRequest({ + transactionID: transaction.transactionID, + reportAction: createIOUAction, + transactions: {}, + violations: {}, + iouReport, + chatReport, + allTransactionViolationsParam: {}, + currentUserAccountID: TEST_USER_ACCOUNT_ID, + currentUserEmail: TEST_USER_LOGIN, + }); + } + + await waitForBatchedUpdates(); + + // Then we expect the reportPreview to update with new childVisibleActionCount + + iouPreview = getReportPreviewAction(chatReport?.reportID, iouReport?.reportID); + expect(iouPreview).toBeTruthy(); + expect(iouPreview?.childVisibleActionCount).toEqual(0); + expect(iouPreview?.childCommenterCount).toEqual(0); + + // When we resume + mockFetch?.resume?.(); + await waitForBatchedUpdates(); + + // Then we expect the reportPreview to update with new childVisibleActionCount + iouPreview = getReportPreviewAction(chatReport?.reportID, iouReport?.reportID); + expect(iouPreview).toBeTruthy(); + expect(iouPreview?.childVisibleActionCount).toEqual(0); + expect(iouPreview?.childCommenterCount).toEqual(0); + }); + }); + + describe('bulk deleteMoneyRequest', () => { + const TEST_USER_ACCOUNT_ID = 1; + const TEST_USER_LOGIN = 'test@email.com'; + + it('update IOU report total properly for bulk deletion of expenses', async () => { + const expenseReport: Report = { + ...createRandomReport(11, undefined), + type: CONST.REPORT.TYPE.EXPENSE, + total: 30, + currency: CONST.CURRENCY.USD, + unheldTotal: 20, + unheldNonReimbursableTotal: 20, + }; + const transaction1: Transaction = { + ...createRandomTransaction(1), + amount: 10, + comment: {hold: '123'}, + currency: CONST.CURRENCY.USD, + reportID: expenseReport.reportID, + reimbursable: true, + }; + const moneyRequestAction1: ReportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + childReportID: '1', + originalMessage: { + IOUReportID: expenseReport.reportID, + amount: transaction1.amount, + currency: transaction1.currency, + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + message: undefined, + previousMessage: undefined, + }; + const transaction2: Transaction = {...createRandomTransaction(2), amount: 10, currency: CONST.CURRENCY.USD, reportID: expenseReport.reportID, reimbursable: false}; + const moneyRequestAction2: ReportAction = { + ...createRandomReportAction(2), + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + childReportID: '2', + originalMessage: { + IOUReportID: expenseReport.reportID, + amount: transaction2.amount, + currency: transaction2.currency, + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + message: undefined, + previousMessage: undefined, + }; + const transaction3: Transaction = {...createRandomTransaction(3), amount: 10, currency: CONST.CURRENCY.USD, reportID: expenseReport.reportID, reimbursable: false}; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, transaction1); + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction2.transactionID}`, transaction2); + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction3.transactionID}`, transaction3); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); + + const selectedTransactionIDs = [transaction1.transactionID, transaction2.transactionID]; + deleteMoneyRequest({ + transactionID: transaction1.transactionID, + reportAction: moneyRequestAction1, + transactions: {}, + violations: {}, + iouReport: expenseReport, + chatReport: expenseReport, + transactionIDsPendingDeletion: [], + selectedTransactionIDs, + allTransactionViolationsParam: {}, + currentUserAccountID: TEST_USER_ACCOUNT_ID, + currentUserEmail: TEST_USER_LOGIN, + }); + deleteMoneyRequest({ + transactionID: transaction2.transactionID, + reportAction: moneyRequestAction2, + transactions: {}, + violations: {}, + iouReport: expenseReport, + chatReport: expenseReport, + transactionIDsPendingDeletion: [transaction1.transactionID], + selectedTransactionIDs, + allTransactionViolationsParam: {}, + currentUserAccountID: TEST_USER_ACCOUNT_ID, + currentUserEmail: TEST_USER_LOGIN, + }); + await waitForBatchedUpdates(); + + const report = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, + callback: (val) => { + Onyx.disconnect(connection); + resolve(val); + }, + }); + }); + + expect(report?.total).toBe(10); + expect(report?.unheldTotal).toBe(10); + expect(report?.unheldNonReimbursableTotal).toBe(10); + }); + }); + + describe('deleteMoneyRequest with allTransactionViolationsParam', () => { + const TEST_USER_ACCOUNT_ID = 1; + const TEST_USER_LOGIN = 'test@email.com'; + it('should pass transaction violations to hasOutstandingChildRequest correctly', async () => { + // Given an expense report with a transaction + const expenseReport: Report = { + ...createRandomReport(20, undefined), + type: CONST.REPORT.TYPE.EXPENSE, + total: 100, + currency: CONST.CURRENCY.USD, + }; + + const transaction1: Transaction = { + ...createRandomTransaction(20), + amount: 100, + currency: CONST.CURRENCY.USD, + reportID: expenseReport.reportID, + reimbursable: true, + }; + + const moneyRequestAction1: ReportAction = { + ...createRandomReportAction(20), + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + childReportID: '20', + originalMessage: { + IOUReportID: expenseReport.reportID, + amount: transaction1.amount, + currency: transaction1.currency, + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + message: undefined, + previousMessage: undefined, + }; + + // When we set up the transaction and report in Onyx + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, transaction1); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); + + // And we call deleteMoneyRequest with transaction violations + const transactionViolations = { + [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction1.transactionID}`]: [ + { + name: CONST.VIOLATIONS.AUTO_REPORTED_REJECTED_EXPENSE, + type: CONST.VIOLATION_TYPES.VIOLATION, + }, + ], + }; + + deleteMoneyRequest({ + transactionID: transaction1.transactionID, + reportAction: moneyRequestAction1, + transactions: {}, + violations: {}, + iouReport: expenseReport, + chatReport: expenseReport, + allTransactionViolationsParam: transactionViolations, + currentUserAccountID: TEST_USER_ACCOUNT_ID, + currentUserEmail: TEST_USER_LOGIN, + }); + + await waitForBatchedUpdates(); + + // Then the transaction should be deleted + const deletedTransaction = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, + callback: (val) => { + Onyx.disconnect(connection); + resolve(val); + }, + }); + }); + + expect(deletedTransaction).toBeUndefined(); + }); + + it('should handle empty transaction violations correctly', async () => { + // Given an expense report with a transaction + const expenseReport: Report = { + ...createRandomReport(21, undefined), + type: CONST.REPORT.TYPE.EXPENSE, + total: 50, + currency: CONST.CURRENCY.USD, + }; + + const transaction1: Transaction = { + ...createRandomTransaction(21), + amount: 50, + currency: CONST.CURRENCY.USD, + reportID: expenseReport.reportID, + reimbursable: true, + }; + + const moneyRequestAction1: ReportAction = { + ...createRandomReportAction(21), + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + childReportID: '21', + originalMessage: { + IOUReportID: expenseReport.reportID, + amount: transaction1.amount, + currency: transaction1.currency, + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + message: undefined, + previousMessage: undefined, + }; + + // When we set up the transaction and report in Onyx + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, transaction1); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); + + // And we call deleteMoneyRequest with empty transaction violations + deleteMoneyRequest({ + transactionID: transaction1.transactionID, + reportAction: moneyRequestAction1, + transactions: {}, + violations: {}, + iouReport: expenseReport, + chatReport: expenseReport, + allTransactionViolationsParam: {}, + currentUserAccountID: TEST_USER_ACCOUNT_ID, + currentUserEmail: TEST_USER_LOGIN, + }); + + await waitForBatchedUpdates(); + + // Then the transaction should be deleted + const deletedTransaction = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`, + callback: (val) => { + Onyx.disconnect(connection); + resolve(val); + }, + }); + }); + + expect(deletedTransaction).toBeUndefined(); + }); + }); +});