From bb84f06c87f737b7fec97e35c79ff7503dee1b5a Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 10 Feb 2026 09:01:55 -0800 Subject: [PATCH 1/3] Fix Mark as paid button not showing for IOU receiver The workspace membership check added in #81303 to prevent the Pay button from showing after a user leaves a workspace was too broad. It blocked all reports with a policyID, including IOU reports (personal 1:1 expenses) that inherit a policyID from the chat report. The receiver of such an IOU may not have access to the sender's workspace policy, causing isPayer to incorrectly return false. Skip the workspace membership check for IOU reports since they are personal expenses where the payer should always be able to pay if they are the manager, regardless of workspace membership. Co-authored-by: Cursor --- src/libs/ReportUtils.ts | 4 +++- tests/unit/ReportUtilsTest.ts | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f6dbc7c255ae..07a15aed874a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2749,7 +2749,9 @@ function isPayer( // If the report belongs to a workspace, verify the user is still a member // When a user leaves a workspace, they may no longer have access to the policy data, // or the policy.role/employeeList may be stale - if (iouReport?.policyID && iouReport.policyID !== CONST.POLICY.ID_FAKE) { + // Skip this check for IOU reports (personal 1:1 expenses) since they can inherit a policyID + // from the chat report but the payer may not be a member of that workspace + if (!isIOUReport(iouReport) && iouReport?.policyID && iouReport.policyID !== CONST.POLICY.ID_FAKE) { // No policy data means user likely left the workspace if (!policy) { return false; diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index d1a3f59fcd3c..bca56f3c65fa 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -6004,6 +6004,24 @@ describe('ReportUtils', () => { // User should not be a payer even if role says admin, because they're not in employeeList expect(isPayer(currentUserAccountID, currentUserEmail, reportForNonMember, undefined, policyWithoutCurrentUser, false)).toBe(false); }); + + it('should return true for IOU report manager even when policy is not available', async () => { + // Simulate an IOU report that inherited a policyID from the chat report, + // but the receiver (payer) does not have access to that workspace policy + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}sender-workspace`, null); + + const iouReportWithPolicyID: Report = { + ...createRandomReport(8, undefined), + type: CONST.REPORT.TYPE.IOU, + stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, + statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, + policyID: 'sender-workspace', + managerID: currentUserAccountID, + }; + + // IOU receiver (payer/manager) should still be able to pay even without access to the policy + expect(isPayer(currentUserAccountID, currentUserEmail, iouReportWithPolicyID, undefined, undefined, false)).toBe(true); + }); }); describe('buildReportNameFromParticipantNames', () => { beforeAll(async () => { From 61a9ffb67cb081fb5bfa7f65f49ff5c34dd96818 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 10 Feb 2026 09:36:27 -0800 Subject: [PATCH 2/3] Fix lint --- tests/unit/ReportUtilsTest.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index bca56f3c65fa..13b98f713b9e 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -25,6 +25,9 @@ import Navigation from '@libs/Navigation/Navigation'; // eslint-disable-next-line no-restricted-syntax import * as PolicyUtils from '@libs/PolicyUtils'; import {getOriginalMessage, getReportAction, isWhisperAction} from '@libs/ReportActionsUtils'; + +// Testing only so it's okay to import computeReportName +// eslint-disable-next-line no-restricted-imports import {buildReportNameFromParticipantNames, computeReportName as computeReportNameOriginal, getGroupChatName, getPolicyExpenseChatName, getReportName} from '@libs/ReportNameUtils'; import type {OptionData} from '@libs/ReportUtils'; import { From 1e2a349db7e84403ed12ee8b26d42a0ed556416d Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 10 Feb 2026 09:36:58 -0800 Subject: [PATCH 3/3] Style fix --- tests/unit/ReportUtilsTest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 13b98f713b9e..0b8755f97bc4 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -25,7 +25,6 @@ import Navigation from '@libs/Navigation/Navigation'; // eslint-disable-next-line no-restricted-syntax import * as PolicyUtils from '@libs/PolicyUtils'; import {getOriginalMessage, getReportAction, isWhisperAction} from '@libs/ReportActionsUtils'; - // Testing only so it's okay to import computeReportName // eslint-disable-next-line no-restricted-imports import {buildReportNameFromParticipantNames, computeReportName as computeReportNameOriginal, getGroupChatName, getPolicyExpenseChatName, getReportName} from '@libs/ReportNameUtils';