Block restricted field edits on closed expense reports#84407
Conversation
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 <truph01@users.noreply.github.com>
|
I created this PR from an upstream branch since I don't have push access to your fork. To take ownership of this branch and be able to push updates, run: Then you can close this PR and open a new one from your fork. |
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
|
@MelvinBot This PR is ready for review |
|
Thanks |
|
@MelvinBot Mark this PR as ready for review |
|
Done! This PR has been marked as ready for review. 🚀 |
trjExpensify
left a comment
There was a problem hiding this comment.
Gotcha, so I'm right in thinking that if you natively went to Classic as an admin/approver. You wouldn't be able to delete the receipt from an expense on a closed report. Did you test that @truph01?
This gap allowed admin/manager users (who bypass the isClosedReport check in canEditMoneyRequest) to see the "Delete Receipt" button on closed reports. When they tapped it, the backend rejected the DetachReceipt API call, producing an "Unexpected error deleting this receipt" message.
|
@MelvinBot Please merge main |
…ptClosedReport Co-authored-by: truph01 <truph01@users.noreply.github.com>
|
Merged |
|
@MelvinBot We should apply the fix for |
Instead of blocking all restricted field edits on closed reports, only block RECEIPT edits — matching the original issue scope. Co-authored-by: truph01 <truph01@users.noreply.github.com>
|
Updated — the |
In OD, I am unable to delete the receipt even if it isn't a closed report. Only the rotate and crop actions are available:
|
|
@MelvinBot That condition can be added in here: Lines 4925 to 4934 in 81b04d8 What do you think? |
|
Adding That said, there may be an even more comprehensive option worth considering: adding if (isSettled(String(moneyRequestReport.reportID)) || isReportIDApproved(String(moneyRequestReport.reportID)) || isClosedReport(moneyRequestReport)) {
return false;
}This would block all restricted field edits on closed reports (AMOUNT, CURRENCY, MERCHANT, DATE, RECEIPT, DISTANCE, DISTANCE_RATE, REIMBURSABLE, REPORT) — matching how settled and approved reports are already handled. It would also make the existing REIMBURSABLE-specific The reasoning: if a report is closed, it's in a terminal state just like settled or approved. Allowing edits to some restricted fields (like AMOUNT or MERCHANT) on closed reports but not others seems inconsistent and could surface similar bugs for those fields in the future. Happy to go with whichever approach you prefer — either your suggestion (RECEIPT block) or the broader guard. |
|
@MelvinBot Let's apply this:
|
Co-authored-by: truph01 <truph01@users.noreply.github.com>
|
Done! Moved the The net diff against if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) {
return (
+ !isClosedReport(moneyRequestReport) &&
!isInvoiceReport(moneyRequestReport) &&
!isReceiptBeingScanned(transaction) &&
!isPerDiemRequest(transaction) && |
|
@MelvinBot The codecov is reduced here. Let's add/update unit test to cover the new change. |
Tests that canEditFieldOfMoneyRequest returns false for the RECEIPT field when the expense report is closed, and true when open. Co-authored-by: truph01 <truph01@users.noreply.github.com>
|
Added unit tests for the receipt field change in
Both tests set up an admin user on the policy so that All 9 tests in |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppScreen.Recording.2026-03-16.at.17.43.48.movAndroid: mWeb ChromeScreen.Recording.2026-03-16.at.17.39.53.moviOS: HybridAppScreen.Recording.2026-03-16.at.17.42.07.moviOS: mWeb SafariScreen.Recording.2026-03-16.at.17.38.12.movMacOS: Chrome / SafariScreen.Recording.2026-03-16.at.17.35.40.mov |
|
@trjExpensify Could you check my comment? |
|
🚧 @grgia has triggered a test Expensify/App build. You can view the workflow run here. |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/grgia in version: 9.3.40-0 🚀
|
|
🚀 Deployed to staging by https://github.com/grgia in version: 9.3.40-0 🚀
|

Explanation of Change
When workflows are disabled on a workspace, the policy's
approvalModeis set toOPTIONAL(Submit & Close) and expense reports transition to CLOSED state (statusNum=2). ThecanEditFieldOfMoneyRequestfunction's RECEIPT return block checked various conditions (invoice report, scanning, per-diem, distance, permissions) but did not check for closed reports.This gap allowed admin/manager users (who bypass the
isClosedReportcheck incanEditMoneyRequest) to see the "Delete Receipt" button on closed reports. When they tapped it, the backend rejected theDetachReceiptAPI call, producing an "Unexpected error deleting this receipt" message.This PR adds
!isClosedReport(moneyRequestReport)to the RECEIPT return block's condition list, keeping all RECEIPT-related logic consolidated in one place.Fixed Issues
$ #81493
PROPOSAL: #81493 (comment)
Tests
Offline tests
QA Steps
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari