From 1a92744ad9875188eb3f2f944d624d2378a017ea Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Mon, 13 Apr 2026 19:21:31 +0700 Subject: [PATCH 1/2] Clear stale iouReportID on DM chat when IOU is moved to a new workspace via pay-with-business-account cancel flow --- src/libs/actions/Policy/Policy.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 3d274fa77d61..df67d47e7554 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -4293,12 +4293,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({ @@ -4306,6 +4309,7 @@ function createWorkspaceFromIOUPayment( key: `${ONYXKEYS.COLLECTION.REPORT}${oldChatReportID}`, value: { hasOutstandingChildRequest: true, + iouReportID, }, }); From 81129179479e04ecb516e52e5bcfc89334fefc3b Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Tue, 5 May 2026 01:42:50 +0700 Subject: [PATCH 2/2] Add unit test for createWorkspaceFromIOUPayment iouReportID clearing --- tests/actions/PolicyTest.ts | 58 +++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/actions/PolicyTest.ts b/tests/actions/PolicyTest.ts index 0aa0eacbd0fa..7c2a80961cbd 100644 --- a/tests/actions/PolicyTest.ts +++ b/tests/actions/PolicyTest.ts @@ -6765,5 +6765,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(); + }); }); });