diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index 37d87194cf43d..c50cfecec5ce9 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -90,6 +90,9 @@ type MoneyRequestReportListProps = { /** List of transactions belonging to this report */ transactions?: OnyxTypes.Transaction[]; + /** Whether there is a pending delete transaction */ + hasPendingDeletionTransaction?: boolean; + /** List of transactions that arrived when the report was open */ newTransactions: OnyxTypes.Transaction[]; @@ -122,6 +125,7 @@ function MoneyRequestReportActionsList({ violations, hasNewerActions, hasOlderActions, + hasPendingDeletionTransaction, showReportActionsLoadingState, }: MoneyRequestReportListProps) { const styles = useThemeStyles(); @@ -746,6 +750,7 @@ function MoneyRequestReportActionsList({ report={report} transactions={transactions} newTransactions={newTransactions} + hasPendingDeletionTransaction={hasPendingDeletionTransaction} reportActions={reportActions} violations={violations} hasComments={reportHasComments} diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index 0e6ff716fcd66..6507d2a2d113d 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -51,6 +51,9 @@ type MoneyRequestReportTransactionListProps = { /** List of transactions belonging to one report */ transactions: OnyxTypes.Transaction[]; + /** Whether there is a pending delete transaction */ + hasPendingDeletionTransaction?: boolean; + /** List of transactions that arrived when the report was open */ newTransactions: OnyxTypes.Transaction[]; @@ -122,6 +125,7 @@ function MoneyRequestReportTransactionList({ violations, hasComments, isLoadingInitialReportActions: isLoadingReportActions, + hasPendingDeletionTransaction = false, scrollToNewTransaction, }: MoneyRequestReportTransactionListProps) { useCopySelectionHelper(); @@ -141,8 +145,8 @@ function MoneyRequestReportTransactionList({ const session = useSession(); const hasPendingAction = useMemo(() => { - return transactions.some(getTransactionPendingAction); - }, [transactions]); + return hasPendingDeletionTransaction || transactions.some(getTransactionPendingAction); + }, [hasPendingDeletionTransaction, transactions]); const {selectedTransactionIDs, setSelectedTransactions, clearSelectedTransactions} = useSearchContext(); const isMobileSelectionModeEnabled = useMobileSelectionMode(); diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx index 4946ee1f3991b..03153537356f9 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx @@ -106,6 +106,7 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe }, [unfilteredReportActions]); const {transactions: reportTransactions, violations: allReportViolations} = useTransactionsAndViolationsForReport(reportID); + const hasPendingDeletionTransaction = Object.values(reportTransactions ?? {}).some((transaction) => transaction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); const transactions = useMemo(() => getAllNonDeletedTransactions(reportTransactions, reportActions), [reportTransactions, reportActions]); const visibleTransactions = transactions?.filter((transaction) => isOffline || transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); @@ -227,6 +228,7 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe report={report} policy={policy} transactions={visibleTransactions} + hasPendingDeletionTransaction={hasPendingDeletionTransaction} newTransactions={newTransactions} reportActions={reportActions} violations={allReportViolations} diff --git a/src/hooks/useSelectedTransactionsActions.ts b/src/hooks/useSelectedTransactionsActions.ts index 818fecf5c8527..285760952b479 100644 --- a/src/hooks/useSelectedTransactionsActions.ts +++ b/src/hooks/useSelectedTransactionsActions.ts @@ -107,7 +107,7 @@ function useSelectedTransactionsActions({ return; } - deleteMoneyRequest(transactionID, action, duplicateTransactions, duplicateTransactionViolations, false, deletedTransactionIDs); + deleteMoneyRequest(transactionID, action, duplicateTransactions, duplicateTransactionViolations, false, deletedTransactionIDs, selectedTransactionIDs); deletedTransactionIDs.push(transactionID); }); clearSelectedTransactions(true); diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index f7f87589b2c21..08ce01af4d3cc 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -93,6 +93,7 @@ function updateIOUOwnerAndTotal>( isDeleting = false, isUpdating = false, isOnHold = false, + unHeldAmount = amount, ): TReport { // For the update case, we have calculated the diff amount in the calculateDiffAmount function so there is no need to compare currencies here if ((currency !== iouReport?.currency && !isUpdating) || !iouReport) { @@ -109,12 +110,12 @@ function updateIOUOwnerAndTotal>( if (actorAccountID === iouReport.ownerAccountID) { iouReportUpdate.total += isDeleting ? -amount : amount; if (!isOnHold) { - iouReportUpdate.unheldTotal += isDeleting ? -amount : amount; + iouReportUpdate.unheldTotal += isDeleting ? -unHeldAmount : unHeldAmount; } } else { iouReportUpdate.total += isDeleting ? amount : -amount; if (!isOnHold) { - iouReportUpdate.unheldTotal += isDeleting ? amount : -amount; + iouReportUpdate.unheldTotal += isDeleting ? unHeldAmount : -unHeldAmount; } } diff --git a/src/libs/MoneyRequestReportUtils.ts b/src/libs/MoneyRequestReportUtils.ts index 0d920f2fc3d69..82f771ff8d939 100644 --- a/src/libs/MoneyRequestReportUtils.ts +++ b/src/libs/MoneyRequestReportUtils.ts @@ -76,6 +76,11 @@ function getAllNonDeletedTransactions(transactions: OnyxCollection, if (!transaction) { return false; } + + if (transaction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return true; + } + const action = getIOUActionForTransactionID(reportActions, transaction.transactionID); return !isDeletedParentAction(action) && (reportActions.length === 0 || !isDeletedAction(action)); }); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index ec6d2b1458def..3623469d4d8b3 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7807,7 +7807,13 @@ function updateMoneyRequestAmountAndCurrency({ * @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, shouldRemoveIOUTransactionID = true, transactionIDsPendingDeletion?: string[]) { +function prepareToCleanUpMoneyRequest( + transactionID: string, + reportAction: OnyxTypes.ReportAction, + shouldRemoveIOUTransactionID = true, + transactionIDsPendingDeletion?: string[], + selectedTransactionIDs?: string[], +) { // STEP 1: Get all collections we're updating const iouReportID = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction)?.IOUReportID : undefined; const iouReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`] ?? null; @@ -7862,38 +7868,46 @@ function prepareToCleanUpMoneyRequest(transactionID: string, reportAction: OnyxT 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; - if (iouReport && isExpenseReport(iouReport)) { + + 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) { + 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 - const amountDiff = getAmount(transaction, true); updatedIOUReport.total += amountDiff; if (!transaction?.reimbursable && typeof updatedIOUReport.nonReimbursableTotal === 'number') { - updatedIOUReport.nonReimbursableTotal += amountDiff; + 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 += amountDiff; + updatedIOUReport.unheldTotal += unheldAmountDiff; } if (!transaction?.reimbursable && typeof updatedIOUReport.unheldNonReimbursableTotal === 'number') { - updatedIOUReport.unheldNonReimbursableTotal += amountDiff; + 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 = updateIOUOwnerAndTotal( - iouReport, - reportAction.actorAccountID ?? CONST.DEFAULT_NUMBER_ID, - getAmount(transaction, false), - currency, - true, - false, - isTransactionOnHold, - ); + updatedIOUReport = + iouReport && !canEditTotal + ? {...iouReport} + : updateIOUOwnerAndTotal(iouReport, reportAction.actorAccountID ?? CONST.DEFAULT_NUMBER_ID, amountDiff, currency, true, false, isTransactionOnHold, unheldAmountDiff); } if (updatedIOUReport) { @@ -8175,6 +8189,7 @@ function deleteMoneyRequest( violations: OnyxCollection, isSingleTransactionView = false, transactionIDsPendingDeletion?: string[], + selectedTransactionIDs?: string[], ) { if (!transactionID) { return; @@ -8194,7 +8209,7 @@ function deleteMoneyRequest( transactionViolations, iouReport, reportPreviewAction, - } = prepareToCleanUpMoneyRequest(transactionID, reportAction, false, transactionIDsPendingDeletion); + } = prepareToCleanUpMoneyRequest(transactionID, reportAction, false, transactionIDsPendingDeletion, selectedTransactionIDs); const urlToNavigateBack = getNavigationUrlOnMoneyRequestDelete(transactionID, reportAction, isSingleTransactionView); diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index e6cb4ae68b33b..53f62b4e76ea6 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -312,6 +312,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { // OpenReport will be called each time the user scrolls up the report a bit, clicks on report preview, and then goes back. const isLinkedMessagePageReady = isLinkedMessageAvailable && (reportActions.length - indexOfLinkedMessage >= CONST.REPORT.MIN_INITIAL_REPORT_ACTION_COUNT || doesCreatedActionExists()); const {transactions: allReportTransactions, violations: allReportViolations} = useTransactionsAndViolationsForReport(reportIDFromRoute); + const hasPendingDeletionTransaction = Object.values(allReportTransactions ?? {}).some((transaction) => transaction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); const reportTransactions = useMemo(() => getAllNonDeletedTransactions(allReportTransactions, reportActions), [allReportTransactions, reportActions]); // wrapping in useMemo because this is array operation and can cause performance issues @@ -882,6 +883,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { {!!report && shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? ( { }); }); + describe('bulk deleteMoneyRequest', () => { + it('update IOU report total properly for bulk deletion of expenses', async () => { + const expenseReport: Report = { + ...createRandomReport(11), + 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(transaction1.transactionID, moneyRequestAction1, {}, {}, undefined, [], selectedTransactionIDs); + deleteMoneyRequest(transaction2.transactionID, moneyRequestAction2, {}, {}, undefined, [transaction1.transactionID], selectedTransactionIDs); + + 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('submitReport', () => { it('correctly submits a report', () => { const amount = 10000;