diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 888e10bc363f..5750a191cbb3 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -4363,12 +4363,15 @@ function createWorkspaceFromIOUPayment( }); } - // To optimistically remove the GBR from the DM we need to update the hasOutstandingChildRequest param to false + // To optimistically remove the GBR from the DM we need to update the hasOutstandingChildRequest param to false. + // We also clear iouReportID so the moved IOU report is no longer resolved from this DM when a new expense is + // created there (otherwise the moved report would be reused and "reappear" in the DM preview). optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${oldChatReportID}`, value: { hasOutstandingChildRequest: false, + iouReportID: null, }, }); failureData.push({ @@ -4376,6 +4379,7 @@ function createWorkspaceFromIOUPayment( key: `${ONYXKEYS.COLLECTION.REPORT}${oldChatReportID}`, value: { hasOutstandingChildRequest: true, + iouReportID, }, }); diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index eb859463f640..54891ab73b58 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -6805,5 +6805,63 @@ describe('actions/Policy', () => { apiWriteSpy.mockRestore(); isIOUReportUsingReportSpy.mockRestore(); }); + + it('should clear iouReportID on the old DM chat optimistically and restore it on failure', async () => { + await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID}); + await waitForBatchedUpdates(); + + const employeeAccountID = 400; + const iouReportOwnerEmail = 'employee@example.com'; + const oldChatReportID = '901'; + const movedIouReportID = '900'; + + const iouReport: Report = { + ...createRandomReport(1, undefined), + reportID: movedIouReportID, + type: CONST.REPORT.TYPE.IOU, + ownerAccountID: employeeAccountID, + chatReportID: oldChatReportID, + policyID: 'oldPolicyID', + currency: CONST.CURRENCY.USD, + total: 1500, + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, iouReport); + await waitForBatchedUpdates(); + + const isIOUReportUsingReportSpy = jest.spyOn(ReportUtils, 'isIOUReportUsingReport').mockReturnValue(true); + const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + + const mockTranslate = ((key: string) => key) as unknown as Parameters[8]; + Policy.createWorkspaceFromIOUPayment(iouReport, undefined, ESH_ACCOUNT_ID, ESH_EMAIL, iouReportOwnerEmail, undefined, CONST.CURRENCY.USD, undefined, mockTranslate, {}); + await waitForBatchedUpdates(); + + const writeOptions = apiWriteSpy.mock.calls.at(0)?.at(2) as { + optimisticData?: Array<{onyxMethod?: string; key?: string; value?: Record | null}>; + failureData?: Array<{onyxMethod?: string; key?: string; value?: Record | null}>; + }; + + const oldChatKey = `${ONYXKEYS.COLLECTION.REPORT}${oldChatReportID}`; + const optimisticOldChatUpdate = (writeOptions?.optimisticData ?? []).find( + (update) => update.key === oldChatKey && (update.value as {iouReportID?: string | null} | null)?.iouReportID !== undefined, + ); + const failureOldChatUpdate = (writeOptions?.failureData ?? []).find( + (update) => update.key === oldChatKey && (update.value as {iouReportID?: string | null} | null)?.iouReportID !== undefined, + ); + + // Optimistic update should clear the dangling pointer so a fresh IOU report is built + // for the next expense in this DM (the moved report otherwise gets reused via getMoneyRequestInformation). + expect(optimisticOldChatUpdate).toBeDefined(); + expect((optimisticOldChatUpdate?.value as {iouReportID?: string | null})?.iouReportID).toBeNull(); + expect((optimisticOldChatUpdate?.value as {hasOutstandingChildRequest?: boolean})?.hasOutstandingChildRequest).toBe(false); + + // Failure rollback must restore the previous iouReportID so the DM chat is back to its prior state. + expect(failureOldChatUpdate).toBeDefined(); + expect((failureOldChatUpdate?.value as {iouReportID?: string | null})?.iouReportID).toBe(movedIouReportID); + expect((failureOldChatUpdate?.value as {hasOutstandingChildRequest?: boolean})?.hasOutstandingChildRequest).toBe(true); + + apiWriteSpy.mockRestore(); + isIOUReportUsingReportSpy.mockRestore(); + }); }); });