From 8be73276b157c03df37c58261ac2898616d8a8a1 Mon Sep 17 00:00:00 2001 From: "truph01 (via MelvinBot)" Date: Fri, 6 Mar 2026 13:14:01 +0000 Subject: [PATCH 1/4] Block restricted field edits on closed expense reports When workflows are disabled, expense reports transition to CLOSED state but the restricted-field guard in canEditFieldOfMoneyRequest only checked for settled and approved reports, not closed ones. This allowed admin/manager users to see the delete receipt button on closed reports, leading to an unexpected error when the backend rejected the operation. Add isClosedReport check alongside the existing isSettled and isReportIDApproved checks to prevent editing restricted fields on closed reports. Co-authored-by: truph01 --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 648da7d3af762..ac6185783ff59 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4869,7 +4869,7 @@ function canEditFieldOfMoneyRequest( const moneyRequestReport = report ?? (iouMessage?.IOUReportID ? (getReport(iouMessage?.IOUReportID, allReports) ?? ({} as Report)) : ({} as Report)); const transaction = linkedTransaction ?? allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${iouMessage?.IOUTransactionID}`] ?? ({} as Transaction); - if (isSettled(String(moneyRequestReport.reportID)) || isReportIDApproved(String(moneyRequestReport.reportID))) { + if (isSettled(String(moneyRequestReport.reportID)) || isReportIDApproved(String(moneyRequestReport.reportID)) || isClosedReport(moneyRequestReport)) { return false; } From cbbbc8c834e8940dda9f75bc8453de84c484d3d2 Mon Sep 17 00:00:00 2001 From: "truph01 (via MelvinBot)" Date: Mon, 16 Mar 2026 09:58:34 +0000 Subject: [PATCH 2/4] Narrow closed-report guard to RECEIPT field only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of blocking all restricted field edits on closed reports, only block RECEIPT edits — matching the original issue scope. Co-authored-by: truph01 --- src/libs/ReportUtils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e6b8d2a697d70..15b64af5b362f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4888,7 +4888,11 @@ function canEditFieldOfMoneyRequest( const moneyRequestReport = report ?? (iouMessage?.IOUReportID ? (getReport(iouMessage?.IOUReportID, allReports) ?? ({} as Report)) : ({} as Report)); const transaction = linkedTransaction ?? deprecatedAllTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${iouMessage?.IOUTransactionID}`] ?? ({} as Transaction); - if (isSettled(String(moneyRequestReport.reportID)) || isReportIDApproved(String(moneyRequestReport.reportID)) || isClosedReport(moneyRequestReport)) { + if (isSettled(String(moneyRequestReport.reportID)) || isReportIDApproved(String(moneyRequestReport.reportID))) { + return false; + } + + if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT && isClosedReport(moneyRequestReport)) { return false; } From b067a444f31448ee4c7636cd4d070e96888c7d11 Mon Sep 17 00:00:00 2001 From: "truph01 (via MelvinBot)" Date: Mon, 16 Mar 2026 10:10:14 +0000 Subject: [PATCH 3/4] Move isClosedReport check into RECEIPT return block Co-authored-by: truph01 --- src/libs/ReportUtils.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 15b64af5b362f..de9c0fcf91781 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4892,10 +4892,6 @@ function canEditFieldOfMoneyRequest( return false; } - if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT && isClosedReport(moneyRequestReport)) { - return false; - } - if ((fieldToEdit === CONST.EDIT_REQUEST_FIELD.AMOUNT || fieldToEdit === CONST.EDIT_REQUEST_FIELD.CURRENCY) && isCardTransactionTransactionUtils(transaction)) { return false; } @@ -4928,6 +4924,7 @@ function canEditFieldOfMoneyRequest( if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) { return ( + !isClosedReport(moneyRequestReport) && !isInvoiceReport(moneyRequestReport) && !isReceiptBeingScanned(transaction) && !isPerDiemRequest(transaction) && From e4256d89effdb2eefed2290e1cd1fe047e6eac69 Mon Sep 17 00:00:00 2001 From: "truph01 (via MelvinBot)" Date: Mon, 16 Mar 2026 10:19:39 +0000 Subject: [PATCH 4/4] Add unit tests for receipt field editing on closed reports Tests that canEditFieldOfMoneyRequest returns false for the RECEIPT field when the expense report is closed, and true when open. Co-authored-by: truph01 --- tests/unit/canEditFieldOfMoneyRequestTest.ts | 103 +++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/tests/unit/canEditFieldOfMoneyRequestTest.ts b/tests/unit/canEditFieldOfMoneyRequestTest.ts index 0631ade1f5a9e..a7768afb910a8 100644 --- a/tests/unit/canEditFieldOfMoneyRequestTest.ts +++ b/tests/unit/canEditFieldOfMoneyRequestTest.ts @@ -297,4 +297,107 @@ describe('canEditFieldOfMoneyRequest', () => { }); }); }); + + describe('receipt field', () => { + const RECEIPT_IOU_REPORT_ID = '5001'; + const RECEIPT_IOU_TRANSACTION_ID = '5002'; + const RECEIPT_AMOUNT = 100; + const receiptPolicyID = '5003'; + + const randomReportAction = createRandomReportAction(501); + const adminPolicy = {...createRandomPolicy(Number(receiptPolicyID), CONST.POLICY.TYPE.TEAM), role: CONST.POLICY.ROLE.ADMIN}; + + const reportAction = { + ...randomReportAction, + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + actorAccountID: currentUserAccountID, + childStateNum: CONST.REPORT.STATE_NUM.OPEN, + childStatusNum: CONST.REPORT.STATUS_NUM.OPEN, + originalMessage: { + // eslint-disable-next-line @typescript-eslint/no-deprecated + ...randomReportAction.originalMessage, + IOUReportID: RECEIPT_IOU_REPORT_ID, + IOUTransactionID: RECEIPT_IOU_TRANSACTION_ID, + type: CONST.IOU.ACTION.CREATE, + amount: RECEIPT_AMOUNT, + currency: CONST.CURRENCY.USD, + }, + }; + + const moneyRequestTransaction = { + ...createRandomTransaction(Number(RECEIPT_IOU_TRANSACTION_ID)), + reportID: RECEIPT_IOU_REPORT_ID, + transactionID: RECEIPT_IOU_TRANSACTION_ID, + amount: RECEIPT_AMOUNT, + managedCard: false, + status: CONST.TRANSACTION.STATUS.POSTED, + }; + + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + + Onyx.multiSet({ + [ONYXKEYS.SESSION]: {email: currentUserEmail, accountID: currentUserAccountID}, + }); + initOnyxDerivedValues(); + + return waitForBatchedUpdates(); + }); + + beforeEach(() => { + const policyCollectionDataSet = toCollectionDataSet(ONYXKEYS.COLLECTION.POLICY, [adminPolicy], (current) => current.id); + Onyx.multiSet({ + [`${ONYXKEYS.COLLECTION.TRANSACTION}${RECEIPT_IOU_TRANSACTION_ID}`]: moneyRequestTransaction, + ...policyCollectionDataSet, + }); + return waitForBatchedUpdates(); + }); + + afterEach(() => { + Onyx.clear(); + return waitForBatchedUpdates(); + }); + + it('should return false for receipt field when the expense report is closed', async () => { + // Given a closed expense report where the current user is an admin + const closedExpenseReport = { + ...createExpenseReport(Number(RECEIPT_IOU_REPORT_ID)), + policyID: receiptPolicyID, + ownerAccountID: currentUserAccountID, + managerID: secondUserAccountID, + stateNum: CONST.REPORT.STATE_NUM.APPROVED, + statusNum: CONST.REPORT.STATUS_NUM.CLOSED, + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${RECEIPT_IOU_REPORT_ID}`, closedExpenseReport); + await waitForBatchedUpdates(); + + // When the admin tries to edit the receipt field + const canEditReceipt = canEditFieldOfMoneyRequest(reportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT); + + // Then they should not be able to edit the receipt on a closed report + expect(canEditReceipt).toBe(false); + }); + + it('should return true for receipt field when the expense report is open', async () => { + // Given an open expense report where the current user is an admin + const openExpenseReport = { + ...createExpenseReport(Number(RECEIPT_IOU_REPORT_ID)), + policyID: receiptPolicyID, + ownerAccountID: currentUserAccountID, + managerID: secondUserAccountID, + stateNum: CONST.REPORT.STATE_NUM.OPEN, + statusNum: CONST.REPORT.STATUS_NUM.OPEN, + }; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${RECEIPT_IOU_REPORT_ID}`, openExpenseReport); + await waitForBatchedUpdates(); + + // When the admin tries to edit the receipt field + const canEditReceipt = canEditFieldOfMoneyRequest(reportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT); + + // Then they should be able to edit the receipt on an open report + expect(canEditReceipt).toBe(true); + }); + }); });