diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index ab1eb7581e98..614c3902de6a 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -1294,83 +1294,106 @@ function changeTransactionsReport( } } - // 9. Update next step for report + // 9. Update next steps for all affected reports const destinationReportID = reportID === CONST.REPORT.UNREPORTED_REPORT_ID ? (existingSelfDMReportID ?? selfDMReport?.reportID) : reportID; - const destinationReport = destinationReportID - ? (allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${destinationReportID}`] ?? - (destinationReportID === newReport?.reportID ? newReport : undefined) ?? - (destinationReportID === selfDMReport?.reportID ? selfDMReport : undefined)) - : undefined; - const destinationTotal = (destinationReportID ? updatedReportTotals[destinationReportID] : undefined) ?? destinationReport?.total ?? newReport?.total; - const nextStepReport = { - ...destinationReport, - reportID: destinationReport?.reportID ?? destinationReportID ?? reportID, - total: destinationTotal, - }; - const hasViolations = hasViolationsReportUtils(nextStepReport?.reportID, allTransactionViolation, accountID, email ?? ''); - - // buildOptimisticNextStep is used in parallel - // eslint-disable-next-line @typescript-eslint/no-deprecated - const optimisticNextStepDeprecated = buildNextStepNew({ - report: nextStepReport, - policy, - currentUserAccountIDParam: accountID, - currentUserEmailParam: email, - hasViolations, - isASAPSubmitBetaEnabled, - predictedNextStatus: nextStepReport.statusNum ?? CONST.REPORT.STATUS_NUM.OPEN, - shouldFixViolations, - }); - const optimisticNextStep = buildOptimisticNextStep({ - report: nextStepReport, - policy, - currentUserAccountIDParam: accountID, - currentUserEmailParam: email, - hasViolations, - isASAPSubmitBetaEnabled, - predictedNextStatus: nextStepReport.statusNum ?? CONST.REPORT.STATUS_NUM.OPEN, - shouldFixViolations, - }); - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${reportID}`, - value: optimisticNextStepDeprecated, - }); - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - nextStep: optimisticNextStep, - pendingFields: { - nextStep: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + const affectedReportIDs = new Set(); + + for (const reportIDToUpdate of Object.keys(updatedReportTotals)) { + affectedReportIDs.add(reportIDToUpdate); + } + + if (destinationReportID) { + affectedReportIDs.add(destinationReportID); + } + + for (const affectedReportID of affectedReportIDs) { + const affectedReport = + allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${affectedReportID}`] ?? + (affectedReportID === newReport?.reportID ? newReport : undefined) ?? + (affectedReportID === selfDMReport?.reportID ? selfDMReport : undefined); + + if (!affectedReport) { + return; + } + + const updatedTotal = updatedReportTotals[affectedReportID] ?? affectedReport.total; + const updatedReport = { + ...affectedReport, + total: updatedTotal, + reportID: affectedReport.reportID ?? affectedReportID, + }; + + const predictedNextStatus = updatedReport.statusNum ?? CONST.REPORT.STATUS_NUM.OPEN; + + const hasViolations = hasViolationsReportUtils(updatedReport.reportID, allTransactionViolation, accountID, email ?? ''); + const isDestinationReport = affectedReportID === destinationReportID; + const shouldFixViolationsForReport = isDestinationReport ? shouldFixViolations : false; + const shouldUseUnreportedNextStepKey = reportID === CONST.REPORT.UNREPORTED_REPORT_ID && isDestinationReport; + const nextStepOnyxReportID = shouldUseUnreportedNextStepKey ? reportID : affectedReportID; + + // eslint-disable-next-line @typescript-eslint/no-deprecated + const optimisticNextStepForCollection = buildNextStepNew({ + report: updatedReport, + policy, + currentUserAccountIDParam: accountID, + currentUserEmailParam: email, + hasViolations, + isASAPSubmitBetaEnabled, + predictedNextStatus, + shouldFixViolations: shouldFixViolationsForReport, + }); + const optimisticNextStepForReport = buildOptimisticNextStep({ + report: updatedReport, + policy, + currentUserAccountIDParam: accountID, + currentUserEmailParam: email, + hasViolations, + isASAPSubmitBetaEnabled, + predictedNextStatus, + shouldFixViolations: shouldFixViolationsForReport, + }); + + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${nextStepOnyxReportID}`, + value: optimisticNextStepForCollection, + }); + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${affectedReportID}`, + value: { + nextStep: optimisticNextStepForReport, + pendingFields: { + nextStep: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, }, - }, - }); - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - pendingFields: { - nextStep: null, + }); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${affectedReportID}`, + value: { + pendingFields: { + nextStep: null, + }, }, - }, - }); - // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${reportID}`, - value: reportNextStep, - }); - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - nextStep: nextStepReport.nextStep ?? null, - pendingFields: { - nextStep: null, + }); + // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${nextStepOnyxReportID}`, + value: nextStepOnyxReportID === reportID ? reportNextStep : (affectedReport.nextStep ?? null), + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${affectedReportID}`, + value: { + nextStep: affectedReport.nextStep ?? null, + pendingFields: { + nextStep: null, + }, }, - }, - }); + }); + } const parameters: ChangeTransactionsReportParams = { transactionList: transactionIDs.join(','), diff --git a/tests/unit/TransactionTest.ts b/tests/unit/TransactionTest.ts index f39b27c3f271..5a414da08162 100644 --- a/tests/unit/TransactionTest.ts +++ b/tests/unit/TransactionTest.ts @@ -6,6 +6,7 @@ import useOnyx from '@hooks/useOnyx'; import {changeTransactionsReport, saveWaypoint} from '@libs/actions/Transaction'; import DateUtils from '@libs/DateUtils'; import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils'; +import type {buildOptimisticNextStep} from '@libs/NextStepUtils'; import {rand64} from '@libs/NumberUtils'; import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils'; import CONST from '@src/CONST'; @@ -305,6 +306,64 @@ describe('Transaction', () => { mockAPIWrite.mockRestore(); }); + it('updates the source submitted report next step without reopening when it becomes empty', async () => { + const mockAPIWrite = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + const buildOptimisticNextStepSpy = jest.spyOn(require('@libs/NextStepUtils'), 'buildOptimisticNextStep'); + + const transaction = generateTransaction({ + reportID: FAKE_OLD_REPORT_ID, + amount: -100, + currency: CONST.CURRENCY.USD, + }); + const oldIOUAction: OnyxEntry> = { + reportActionID: rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + actorAccountID: CURRENT_USER_ID, + created: DateUtils.getDBTime(), + originalMessage: { + IOUReportID: FAKE_OLD_REPORT_ID, + IOUTransactionID: transaction.transactionID, + amount: transaction.amount, + currency: transaction.currency, + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + }, + }; + const submittedReport: Report = { + ...createExpenseReport(6), + reportID: FAKE_OLD_REPORT_ID, + ownerAccountID: CURRENT_USER_ID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + currency: CONST.CURRENCY.USD, + total: -100, + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${FAKE_OLD_REPORT_ID}`, submittedReport); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${FAKE_OLD_REPORT_ID}`, {[oldIOUAction.reportActionID]: oldIOUAction}); + const report = await getReportFromUseOnyx(FAKE_NEW_REPORT_ID); + + changeTransactionsReport([transaction.transactionID], false, CURRENT_USER_ID, 'test@example.com', report); + await waitForBatchedUpdates(); + + try { + const buildOptimisticNextStepCalls = buildOptimisticNextStepSpy.mock.calls as Array<[Parameters[0]]>; + const sourceNextStepCall = buildOptimisticNextStepCalls.find(([params]) => params.report?.reportID === FAKE_OLD_REPORT_ID); + + expect(sourceNextStepCall).toBeDefined(); + expect(sourceNextStepCall?.[0].predictedNextStatus).toBe(CONST.REPORT.STATUS_NUM.SUBMITTED); + + const apiWriteCall = mockAPIWrite.mock.calls.at(0); + const optimisticData = (apiWriteCall?.[2] as {optimisticData?: Array<{key: string}>})?.optimisticData; + const sourceNextStepUpdate = optimisticData?.find((data) => data.key === `${ONYXKEYS.COLLECTION.NEXT_STEP}${FAKE_OLD_REPORT_ID}`); + + expect(sourceNextStepUpdate).toBeDefined(); + } finally { + buildOptimisticNextStepSpy.mockRestore(); + mockAPIWrite.mockRestore(); + } + }); + it('correctly handles ASAP submit beta enabled when moving transactions', async () => { const mockAPIWrite = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve());