diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 4fb36d00f19a..f940f0d2f9e7 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -998,6 +998,19 @@ function changeTransactionsReport({ ...pendingFields, }; }; + const clearOptimisticPendingFields = (reportIDToUpdate: string | undefined, fieldNames: Array>) => { + if (!reportIDToUpdate || !optimisticPendingFieldsByReport[reportIDToUpdate]) { + return; + } + + for (const fieldName of fieldNames) { + delete optimisticPendingFieldsByReport[reportIDToUpdate][fieldName]; + } + + if (Object.keys(optimisticPendingFieldsByReport[reportIDToUpdate]).length === 0) { + delete optimisticPendingFieldsByReport[reportIDToUpdate]; + } + }; const clearAccumulatedReportTotals = (reportIDToUpdate: string) => { delete updatedReportTotals[reportIDToUpdate]; delete updatedReportNonReimbursableTotals[reportIDToUpdate]; @@ -1014,6 +1027,14 @@ function changeTransactionsReport({ total: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, }); }; + const clearStaleReportState = (reportIDToUpdate: string | undefined) => { + if (!reportIDToUpdate) { + return; + } + + staleReportIDs.delete(reportIDToUpdate); + clearOptimisticPendingFields(reportIDToUpdate, ['total']); + }; const getTargetReportCurrencies = (targetReportID: string) => { if (!targetReportCurrenciesByReport[targetReportID]) { targetReportCurrenciesByReport[targetReportID] = new Set( @@ -1204,22 +1225,32 @@ function changeTransactionsReport({ const {amount: transactionAmount = 0, currency: transactionCurrency} = getTransactionDetails(transaction, undefined, undefined, allowNegative) ?? {}; const resolvedTransactionCurrency = transactionCurrency ?? transaction.currency; const oldReportTotal = oldReport?.total ?? 0; - const updatedReportTotal = transactionAmount < 0 ? oldReportTotal - transactionAmount : oldReportTotal + transactionAmount; if (oldReport) { const oldReportTransactionCount = updatedReportTransactionCounts[oldReportID] ?? oldReport.transactionCount ?? 0; updatedReportTransactionCounts[oldReportID] = Math.max(0, oldReportTransactionCount - 1); - if (staleReportIDs.has(oldReportID) || oldReport.pendingFields?.total) { + const remainingTransactions = getReportTransactions(oldReportID).filter( + (reportTransaction) => reportTransaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && !transactionIDs.includes(reportTransaction.transactionID), + ); + const willBeEmpty = remainingTransactions.length === 0; + + if (willBeEmpty) { + clearStaleReportState(oldReportID); + updatedReportTotals[oldReportID] = 0; + updatedReportNonReimbursableTotals[oldReportID] = 0; + updatedReportUnheldNonReimbursableTotals[oldReportID] = 0; + } else if (staleReportIDs.has(oldReportID) || oldReport.pendingFields?.total) { markReportTotalAsStale(oldReportID); } else if (oldReport.currency === transactionCurrency) { - updatedReportTotals[oldReportID] = updatedReportTotals[oldReportID] ? updatedReportTotals[oldReportID] : updatedReportTotal; - updatedReportNonReimbursableTotals[oldReportID] = - (updatedReportNonReimbursableTotals[oldReportID] ? updatedReportNonReimbursableTotals[oldReportID] : (oldReport?.nonReimbursableTotal ?? 0)) + - (transaction?.reimbursable ? 0 : transactionAmount); - updatedReportUnheldNonReimbursableTotals[oldReportID] = - (updatedReportUnheldNonReimbursableTotals[oldReportID] ? updatedReportUnheldNonReimbursableTotals[oldReportID] : (oldReport?.unheldNonReimbursableTotal ?? 0)) + - (transaction?.reimbursable && !isOnHold(transaction) ? 0 : transactionAmount); + const currentTotal = updatedReportTotals[oldReportID] ?? oldReportTotal; + updatedReportTotals[oldReportID] = currentTotal + transactionAmount; + + const currentNonReimbursableTotal = updatedReportNonReimbursableTotals[oldReportID] ?? oldReport?.nonReimbursableTotal ?? 0; + updatedReportNonReimbursableTotals[oldReportID] = currentNonReimbursableTotal + (transaction?.reimbursable ? 0 : transactionAmount); + + const currentUnheldNonReimbursableTotal = updatedReportUnheldNonReimbursableTotals[oldReportID] ?? oldReport?.unheldNonReimbursableTotal ?? 0; + updatedReportUnheldNonReimbursableTotals[oldReportID] = currentUnheldNonReimbursableTotal + (transaction?.reimbursable && !isOnHold(transaction) ? 0 : transactionAmount); } else { markReportTotalAsStale(oldReportID); } diff --git a/tests/unit/TransactionTest.ts b/tests/unit/TransactionTest.ts index b915b776f75e..0ec5d100857f 100644 --- a/tests/unit/TransactionTest.ts +++ b/tests/unit/TransactionTest.ts @@ -693,7 +693,7 @@ describe('Transaction', () => { expect(report?.total).toBe(0); }); - it('should update the old report total when the currency is the same', async () => { + it('should reset the old report total to 0 when moving the last same-currency expense', async () => { const oldExpenseReport = { ...createRandomReport(1, undefined), total: -200, @@ -738,11 +738,11 @@ describe('Transaction', () => { }); }); - expect(report?.total).toBe(oldExpenseReport.total - transaction.amount); - expect(report?.nonReimbursableTotal).toBe(oldExpenseReport.nonReimbursableTotal - transaction.amount); + expect(report?.total).toBe(0); + expect(report?.nonReimbursableTotal).toBe(0); }); - it('should not update the old report total when the currency is different', async () => { + it('should reset the old report total to 0 when no expenses remain, even if the currency is different', async () => { const oldExpenseReport = { ...createRandomReport(1, undefined), total: -200, @@ -787,8 +787,73 @@ describe('Transaction', () => { }); }); - expect(report?.total).toBe(oldExpenseReport.total); - expect(report?.nonReimbursableTotal).toBe(oldExpenseReport.nonReimbursableTotal); + expect(report?.total).toBe(0); + expect(report?.nonReimbursableTotal).toBe(0); + }); + + it('should reset the old report total to 0 after moving all same-currency expenses to a new report', async () => { + const oldExpenseReport = { + ...createRandomReport(1, undefined), + total: -300, + nonReimbursableTotal: -300, + currency: CONST.CURRENCY.USD, + transactionCount: 2, + }; + const firstTransaction = { + ...generateTransaction({ + reportID: oldExpenseReport.reportID, + }), + amount: -100, + reimbursable: false, + currency: CONST.CURRENCY.USD, + }; + const secondTransaction = { + ...generateTransaction({ + reportID: oldExpenseReport.reportID, + }), + amount: -200, + reimbursable: false, + currency: CONST.CURRENCY.USD, + }; + const firstIOUAction = createIOUAction(firstTransaction, FAKE_OLD_REPORT_ID); + const secondIOUAction = createIOUAction(secondTransaction, FAKE_OLD_REPORT_ID); + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${firstTransaction.transactionID}`, firstTransaction); + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${secondTransaction.transactionID}`, secondTransaction); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${oldExpenseReport.reportID}`, oldExpenseReport); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldExpenseReport.reportID}`, { + [firstIOUAction.reportActionID]: firstIOUAction, + [secondIOUAction.reportActionID]: secondIOUAction, + }); + + const fakeReport = await getReportFromUseOnyx(FAKE_NEW_REPORT_ID); + const allTransactions = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${firstTransaction.transactionID}`]: firstTransaction, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${secondTransaction.transactionID}`]: secondTransaction, + }; + changeTransactionsReport({ + transactionIDs: [firstTransaction.transactionID, secondTransaction.transactionID], + isASAPSubmitBetaEnabled: false, + accountID: CURRENT_USER_ID, + email: 'test@example.com', + newReport: fakeReport, + policy: undefined, + allTransactions, + policyTagList: undefined, + }); + await waitForBatchedUpdates(); + + const report = await new Promise>((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT}${oldExpenseReport.reportID}`, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value); + }, + }); + }); + + expect(report?.total).toBe(0); + expect(report?.nonReimbursableTotal).toBe(0); }); it('should keep both reports stale and preserve the displayed totals for mixed-currency partial moves', async () => {