From e548659515c730dd0fe3609afd685d52c2e035f8 Mon Sep 17 00:00:00 2001 From: "Aimane Chnaif (via MelvinBot)" Date: Fri, 22 May 2026 18:03:41 +0000 Subject: [PATCH 1/5] Add PAY badge fallback for p2p IOUs when IOU report not yet loaded in Onyx When logging in, the IOU report may not be loaded in Onyx yet, causing getIOUReportActionWithBadge to fail to determine the PAY badge (since canIOUBePaid bails when iouReport is empty). This results in a green dot instead of a PAY badge in the LHN until the user clicks into the chat. Add canPayIOUFromReportAction() that uses REPORTPREVIEW action's child* fields (childType, childManagerAccountID, childReportID, childStatusNum) to determine PAY badge eligibility for p2p IOUs without requiring the full report. Used as a fallback when the IOU report isn't in Onyx. Co-authored-by: Aimane Chnaif --- src/libs/actions/IOU/ReportWorkflow.ts | 31 +++++- tests/actions/IOUTest/ReportWorkflowTest.ts | 101 ++++++++++++++++++++ 2 files changed, 128 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU/ReportWorkflow.ts b/src/libs/actions/IOU/ReportWorkflow.ts index 6ad5d1c5d248..9475280885a0 100644 --- a/src/libs/actions/IOU/ReportWorkflow.ts +++ b/src/libs/actions/IOU/ReportWorkflow.ts @@ -306,6 +306,20 @@ function getBadgeFromIOUReport( return undefined; } +/** + * Determines if a p2p IOU can be paid by the current user using only the REPORTPREVIEW action's + * child* fields, without requiring the full IOU report to be loaded in Onyx. This is used as a + * fallback when the IOU report hasn't been fetched yet (e.g. right after login). + */ +function canPayIOUFromReportAction(action: ReportAction, chatReport: OnyxEntry, currentUserAccountID: number): boolean { + return ( + action.childType === CONST.REPORT.TYPE.IOU && + action.childReportID === chatReport?.iouReportID && + action.childManagerAccountID === currentUserAccountID && + action.childStatusNum !== CONST.REPORT.STATUS_NUM.REIMBURSED + ); +} + function getIOUReportActionWithBadge( chatReport: OnyxEntry, policy: OnyxEntry, @@ -325,12 +339,21 @@ function getIOUReportActionWithBadge( } const iouReport = getReportOrDraftReport(action.childReportID); const badge = getBadgeFromIOUReport(iouReport, chatReport, policy, reportMetadata, invoiceReceiverPolicy, currentUserLogin, currentUserAccountID); - if (!badge) { + if (badge) { + if (!earliestAction || isOlderReportAction(action, earliestAction)) { + earliestAction = action; + actionBadge = badge; + } continue; } - if (!earliestAction || isOlderReportAction(action, earliestAction)) { - earliestAction = action; - actionBadge = badge; + + // Fallback for p2p IOUs when the IOU report isn't loaded in Onyx yet (e.g. right after login). + // Use the REPORTPREVIEW action's child* fields to determine PAY badge without the full report. + if (!iouReport && chatReport?.hasOutstandingChildRequest && canPayIOUFromReportAction(action, chatReport, currentUserAccountID)) { + if (!earliestAction || isOlderReportAction(action, earliestAction)) { + earliestAction = action; + actionBadge = CONST.REPORT.ACTION_BADGE.PAY; + } } } diff --git a/tests/actions/IOUTest/ReportWorkflowTest.ts b/tests/actions/IOUTest/ReportWorkflowTest.ts index b95064183008..a13b65ac679a 100644 --- a/tests/actions/IOUTest/ReportWorkflowTest.ts +++ b/tests/actions/IOUTest/ReportWorkflowTest.ts @@ -3655,6 +3655,107 @@ describe('actions/IOU/ReportWorkflow', () => { expect(result.reportAction).toBeUndefined(); expect(result.actionBadge).toBeUndefined(); }); + + it('should return PAY badge for p2p IOU when IOU report is not loaded in Onyx', async () => { + const chatReportID = '800'; + const iouReportID = '801'; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID)), + reportID: chatReportID, + iouReportID, + hasOutstandingChildRequest: true, + }; + + // Do NOT set the IOU report in Onyx — simulates fresh login where it hasn't loaded yet + + const reportPreviewAction = { + reportActionID: '802', + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + created: '2024-08-08 19:00:00.000', + childReportID: iouReportID, + childType: CONST.REPORT.TYPE.IOU, + childManagerAccountID: RORY_ACCOUNT_ID, + childOwnerAccountID: CARLOS_ACCOUNT_ID, + childStatusNum: CONST.REPORT.STATUS_NUM.OPEN, + message: [{type: 'TEXT', text: 'IOU preview'}], + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, { + [reportPreviewAction.reportActionID]: reportPreviewAction, + }); + await waitForBatchedUpdates(); + + const result = getIOUReportActionWithBadge(fakeChatReport, null, {}, undefined, RORY_EMAIL, RORY_ACCOUNT_ID); + expect(result.reportAction).toMatchObject(reportPreviewAction); + expect(result.actionBadge).toBe(CONST.REPORT.ACTION_BADGE.PAY); + }); + + it('should NOT return PAY badge fallback when IOU is already settled', async () => { + const chatReportID = '810'; + const iouReportID = '811'; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID)), + reportID: chatReportID, + iouReportID, + hasOutstandingChildRequest: true, + }; + + const reportPreviewAction = { + reportActionID: '812', + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + created: '2024-08-08 19:00:00.000', + childReportID: iouReportID, + childType: CONST.REPORT.TYPE.IOU, + childManagerAccountID: RORY_ACCOUNT_ID, + childOwnerAccountID: CARLOS_ACCOUNT_ID, + childStatusNum: CONST.REPORT.STATUS_NUM.REIMBURSED, + message: [{type: 'TEXT', text: 'IOU preview'}], + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, { + [reportPreviewAction.reportActionID]: reportPreviewAction, + }); + await waitForBatchedUpdates(); + + const result = getIOUReportActionWithBadge(fakeChatReport, null, {}, undefined, RORY_EMAIL, RORY_ACCOUNT_ID); + expect(result.reportAction).toBeUndefined(); + expect(result.actionBadge).toBeUndefined(); + }); + + it('should NOT return PAY badge fallback when current user is not the payer', async () => { + const chatReportID = '820'; + const iouReportID = '821'; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID)), + reportID: chatReportID, + iouReportID, + hasOutstandingChildRequest: true, + }; + + const reportPreviewAction = { + reportActionID: '822', + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + created: '2024-08-08 19:00:00.000', + childReportID: iouReportID, + childType: CONST.REPORT.TYPE.IOU, + childManagerAccountID: CARLOS_ACCOUNT_ID, // Someone else is the payer + childOwnerAccountID: RORY_ACCOUNT_ID, + childStatusNum: CONST.REPORT.STATUS_NUM.OPEN, + message: [{type: 'TEXT', text: 'IOU preview'}], + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, { + [reportPreviewAction.reportActionID]: reportPreviewAction, + }); + await waitForBatchedUpdates(); + + const result = getIOUReportActionWithBadge(fakeChatReport, null, {}, undefined, RORY_EMAIL, RORY_ACCOUNT_ID); + expect(result.reportAction).toBeUndefined(); + expect(result.actionBadge).toBeUndefined(); + }); }); describe('getBadgeFromIOUReport', () => { From d6dcde4fcf3cc89f9bc987eb1090c4efa2db6aae Mon Sep 17 00:00:00 2001 From: "Aimane Chnaif (via MelvinBot)" Date: Fri, 22 May 2026 18:10:36 +0000 Subject: [PATCH 2/5] Fix typecheck errors in PAY badge fallback tests - Pass `undefined` as second arg to `createRandomReport()` (requires chatType param) - Use `undefined` instead of `null` for policy param in `getIOUReportActionWithBadge()` calls Co-authored-by: Aimane Chnaif --- tests/actions/IOUTest/ReportWorkflowTest.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/actions/IOUTest/ReportWorkflowTest.ts b/tests/actions/IOUTest/ReportWorkflowTest.ts index a13b65ac679a..01de01f90443 100644 --- a/tests/actions/IOUTest/ReportWorkflowTest.ts +++ b/tests/actions/IOUTest/ReportWorkflowTest.ts @@ -3661,7 +3661,7 @@ describe('actions/IOU/ReportWorkflow', () => { const iouReportID = '801'; const fakeChatReport: Report = { - ...createRandomReport(Number(chatReportID)), + ...createRandomReport(Number(chatReportID), undefined), reportID: chatReportID, iouReportID, hasOutstandingChildRequest: true, @@ -3686,7 +3686,7 @@ describe('actions/IOU/ReportWorkflow', () => { }); await waitForBatchedUpdates(); - const result = getIOUReportActionWithBadge(fakeChatReport, null, {}, undefined, RORY_EMAIL, RORY_ACCOUNT_ID); + const result = getIOUReportActionWithBadge(fakeChatReport, undefined, {}, undefined, RORY_EMAIL, RORY_ACCOUNT_ID); expect(result.reportAction).toMatchObject(reportPreviewAction); expect(result.actionBadge).toBe(CONST.REPORT.ACTION_BADGE.PAY); }); @@ -3696,7 +3696,7 @@ describe('actions/IOU/ReportWorkflow', () => { const iouReportID = '811'; const fakeChatReport: Report = { - ...createRandomReport(Number(chatReportID)), + ...createRandomReport(Number(chatReportID), undefined), reportID: chatReportID, iouReportID, hasOutstandingChildRequest: true, @@ -3719,7 +3719,7 @@ describe('actions/IOU/ReportWorkflow', () => { }); await waitForBatchedUpdates(); - const result = getIOUReportActionWithBadge(fakeChatReport, null, {}, undefined, RORY_EMAIL, RORY_ACCOUNT_ID); + const result = getIOUReportActionWithBadge(fakeChatReport, undefined, {}, undefined, RORY_EMAIL, RORY_ACCOUNT_ID); expect(result.reportAction).toBeUndefined(); expect(result.actionBadge).toBeUndefined(); }); @@ -3729,7 +3729,7 @@ describe('actions/IOU/ReportWorkflow', () => { const iouReportID = '821'; const fakeChatReport: Report = { - ...createRandomReport(Number(chatReportID)), + ...createRandomReport(Number(chatReportID), undefined), reportID: chatReportID, iouReportID, hasOutstandingChildRequest: true, @@ -3752,7 +3752,7 @@ describe('actions/IOU/ReportWorkflow', () => { }); await waitForBatchedUpdates(); - const result = getIOUReportActionWithBadge(fakeChatReport, null, {}, undefined, RORY_EMAIL, RORY_ACCOUNT_ID); + const result = getIOUReportActionWithBadge(fakeChatReport, undefined, {}, undefined, RORY_EMAIL, RORY_ACCOUNT_ID); expect(result.reportAction).toBeUndefined(); expect(result.actionBadge).toBeUndefined(); }); From e995c5abd4a35a251130a8dc51bca34da1f37c51 Mon Sep 17 00:00:00 2001 From: "Aimane Chnaif (via MelvinBot)" Date: Fri, 22 May 2026 18:16:15 +0000 Subject: [PATCH 3/5] Refactor: early-return when iouReport is missing to skip getBadgeFromIOUReport Co-authored-by: Aimane Chnaif --- src/libs/actions/IOU/ReportWorkflow.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/IOU/ReportWorkflow.ts b/src/libs/actions/IOU/ReportWorkflow.ts index 9475280885a0..c6fe74547add 100644 --- a/src/libs/actions/IOU/ReportWorkflow.ts +++ b/src/libs/actions/IOU/ReportWorkflow.ts @@ -338,21 +338,24 @@ function getIOUReportActionWithBadge( continue; } const iouReport = getReportOrDraftReport(action.childReportID); - const badge = getBadgeFromIOUReport(iouReport, chatReport, policy, reportMetadata, invoiceReceiverPolicy, currentUserLogin, currentUserAccountID); - if (badge) { - if (!earliestAction || isOlderReportAction(action, earliestAction)) { - earliestAction = action; - actionBadge = badge; + + if (!iouReport) { + // Fallback for p2p IOUs when the IOU report isn't loaded in Onyx yet (e.g. right after login). + // Use the REPORTPREVIEW action's child* fields to determine PAY badge without the full report. + if (chatReport?.hasOutstandingChildRequest && canPayIOUFromReportAction(action, chatReport, currentUserAccountID)) { + if (!earliestAction || isOlderReportAction(action, earliestAction)) { + earliestAction = action; + actionBadge = CONST.REPORT.ACTION_BADGE.PAY; + } } continue; } - // Fallback for p2p IOUs when the IOU report isn't loaded in Onyx yet (e.g. right after login). - // Use the REPORTPREVIEW action's child* fields to determine PAY badge without the full report. - if (!iouReport && chatReport?.hasOutstandingChildRequest && canPayIOUFromReportAction(action, chatReport, currentUserAccountID)) { + const badge = getBadgeFromIOUReport(iouReport, chatReport, policy, reportMetadata, invoiceReceiverPolicy, currentUserLogin, currentUserAccountID); + if (badge) { if (!earliestAction || isOlderReportAction(action, earliestAction)) { earliestAction = action; - actionBadge = CONST.REPORT.ACTION_BADGE.PAY; + actionBadge = badge; } } } From 504ccaf073f53ebc4bca86809f19c75d8b25eef5 Mon Sep 17 00:00:00 2001 From: "Aimane Chnaif (via MelvinBot)" Date: Fri, 22 May 2026 19:06:20 +0000 Subject: [PATCH 4/5] Add archived chat check to canPayIOUFromReportAction fallback Look up reportNameValuePairs for the chat report and check isArchivedReport before showing the PAY badge in the fallback path, matching the real path in canIOUBePaid. Add unit test. Co-authored-by: Aimane Chnaif --- src/libs/actions/IOU/ReportWorkflow.ts | 4 ++- tests/actions/IOUTest/ReportWorkflowTest.ts | 39 +++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU/ReportWorkflow.ts b/src/libs/actions/IOU/ReportWorkflow.ts index c6fe74547add..90a177875374 100644 --- a/src/libs/actions/IOU/ReportWorkflow.ts +++ b/src/libs/actions/IOU/ReportWorkflow.ts @@ -312,11 +312,13 @@ function getBadgeFromIOUReport( * fallback when the IOU report hasn't been fetched yet (e.g. right after login). */ function canPayIOUFromReportAction(action: ReportAction, chatReport: OnyxEntry, currentUserAccountID: number): boolean { + const reportNameValuePairs = getAllReportNameValuePairs()?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${chatReport?.reportID}`]; return ( action.childType === CONST.REPORT.TYPE.IOU && action.childReportID === chatReport?.iouReportID && action.childManagerAccountID === currentUserAccountID && - action.childStatusNum !== CONST.REPORT.STATUS_NUM.REIMBURSED + action.childStatusNum !== CONST.REPORT.STATUS_NUM.REIMBURSED && + !isArchivedReport(reportNameValuePairs) ); } diff --git a/tests/actions/IOUTest/ReportWorkflowTest.ts b/tests/actions/IOUTest/ReportWorkflowTest.ts index 01de01f90443..fc68b1cca22d 100644 --- a/tests/actions/IOUTest/ReportWorkflowTest.ts +++ b/tests/actions/IOUTest/ReportWorkflowTest.ts @@ -3756,6 +3756,45 @@ describe('actions/IOU/ReportWorkflow', () => { expect(result.reportAction).toBeUndefined(); expect(result.actionBadge).toBeUndefined(); }); + + it('should NOT return PAY badge fallback when chat report is archived', async () => { + const chatReportID = '830'; + const iouReportID = '831'; + + const fakeChatReport: Report = { + ...createRandomReport(Number(chatReportID), undefined), + reportID: chatReportID, + iouReportID, + hasOutstandingChildRequest: true, + }; + + const reportPreviewAction = { + reportActionID: '832', + actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, + created: '2024-08-08 19:00:00.000', + childReportID: iouReportID, + childType: CONST.REPORT.TYPE.IOU, + childManagerAccountID: RORY_ACCOUNT_ID, + childOwnerAccountID: CARLOS_ACCOUNT_ID, + childStatusNum: CONST.REPORT.STATUS_NUM.OPEN, + message: [{type: 'TEXT', text: 'IOU preview'}], + }; + + const archivedRNVP: ReportNameValuePairs = { + private_isArchived: new Date().toISOString(), + }; + + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, { + [reportPreviewAction.reportActionID]: reportPreviewAction, + }); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${chatReportID}`, archivedRNVP); + await waitForBatchedUpdates(); + + const result = getIOUReportActionWithBadge(fakeChatReport, undefined, {}, undefined, RORY_EMAIL, RORY_ACCOUNT_ID); + expect(result.reportAction).toBeUndefined(); + expect(result.actionBadge).toBeUndefined(); + }); }); describe('getBadgeFromIOUReport', () => { From 3c1d8efea5275163f04fe95c916aee5dec172a95 Mon Sep 17 00:00:00 2001 From: "Aimane Chnaif (via MelvinBot)" Date: Sat, 23 May 2026 03:26:00 +0000 Subject: [PATCH 5/5] Remove redundant isArchivedReport check from canPayIOUFromReportAction childType === IOU already ensures this is a p2p IOU in a DM chat, and DM chats cannot be archived. The archived check was defensive but unnecessary. Co-authored-by: Aimane Chnaif --- src/libs/actions/IOU/ReportWorkflow.ts | 4 +-- tests/actions/IOUTest/ReportWorkflowTest.ts | 39 --------------------- 2 files changed, 1 insertion(+), 42 deletions(-) diff --git a/src/libs/actions/IOU/ReportWorkflow.ts b/src/libs/actions/IOU/ReportWorkflow.ts index 90a177875374..c6fe74547add 100644 --- a/src/libs/actions/IOU/ReportWorkflow.ts +++ b/src/libs/actions/IOU/ReportWorkflow.ts @@ -312,13 +312,11 @@ function getBadgeFromIOUReport( * fallback when the IOU report hasn't been fetched yet (e.g. right after login). */ function canPayIOUFromReportAction(action: ReportAction, chatReport: OnyxEntry, currentUserAccountID: number): boolean { - const reportNameValuePairs = getAllReportNameValuePairs()?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${chatReport?.reportID}`]; return ( action.childType === CONST.REPORT.TYPE.IOU && action.childReportID === chatReport?.iouReportID && action.childManagerAccountID === currentUserAccountID && - action.childStatusNum !== CONST.REPORT.STATUS_NUM.REIMBURSED && - !isArchivedReport(reportNameValuePairs) + action.childStatusNum !== CONST.REPORT.STATUS_NUM.REIMBURSED ); } diff --git a/tests/actions/IOUTest/ReportWorkflowTest.ts b/tests/actions/IOUTest/ReportWorkflowTest.ts index fc68b1cca22d..01de01f90443 100644 --- a/tests/actions/IOUTest/ReportWorkflowTest.ts +++ b/tests/actions/IOUTest/ReportWorkflowTest.ts @@ -3756,45 +3756,6 @@ describe('actions/IOU/ReportWorkflow', () => { expect(result.reportAction).toBeUndefined(); expect(result.actionBadge).toBeUndefined(); }); - - it('should NOT return PAY badge fallback when chat report is archived', async () => { - const chatReportID = '830'; - const iouReportID = '831'; - - const fakeChatReport: Report = { - ...createRandomReport(Number(chatReportID), undefined), - reportID: chatReportID, - iouReportID, - hasOutstandingChildRequest: true, - }; - - const reportPreviewAction = { - reportActionID: '832', - actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW, - created: '2024-08-08 19:00:00.000', - childReportID: iouReportID, - childType: CONST.REPORT.TYPE.IOU, - childManagerAccountID: RORY_ACCOUNT_ID, - childOwnerAccountID: CARLOS_ACCOUNT_ID, - childStatusNum: CONST.REPORT.STATUS_NUM.OPEN, - message: [{type: 'TEXT', text: 'IOU preview'}], - }; - - const archivedRNVP: ReportNameValuePairs = { - private_isArchived: new Date().toISOString(), - }; - - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, fakeChatReport); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, { - [reportPreviewAction.reportActionID]: reportPreviewAction, - }); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${chatReportID}`, archivedRNVP); - await waitForBatchedUpdates(); - - const result = getIOUReportActionWithBadge(fakeChatReport, undefined, {}, undefined, RORY_EMAIL, RORY_ACCOUNT_ID); - expect(result.reportAction).toBeUndefined(); - expect(result.actionBadge).toBeUndefined(); - }); }); describe('getBadgeFromIOUReport', () => {