fix: prevent automatic unhold and approval of held expenses with multiple violations#90331
Conversation
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
|
@huult @NikkiWines One of you needs to copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
|
@nabi-ebrahimi Could you check why the TypeScript check failed? |
Fixed the TypeScript check. I added an explicit cast for the test-only stale transaction object because it intentionally uses |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppScreen.Recording.2026-05-14.at.12.03.19.mp4Android: mWeb ChromeScreen.Recording.2026-05-14.at.12.04.12.mp4iOS: HybridAppScreen.Recording.2026-05-14.at.12.07.07.mp4iOS: mWeb SafariScreen.Recording.2026-05-14.at.12.08.31.mp4MacOS: Chrome / SafariScreen.Recording.2026-05-14.at.11.52.05.mp4 |
| owner: 123, | ||
| }, | ||
| }, | ||
| hold: null, |
There was a problem hiding this comment.
| hold: null, | |
| hold: '', |
or
| hold: null, | |
| hold: undefined, |
We don’t need to set hold to null or update the type for it.
There was a problem hiding this comment.
Updated. I removed hold: null from the stale test fixture and switched it to undefined so the test still covers the stale snapshot case without widening the production type.
|
|
||
| /** Whether the transaction is on hold */ | ||
| hold?: string; | ||
| hold?: string | null; |
There was a problem hiding this comment.
| hold?: string | null; | |
| hold?: string; |
Should remove null
There was a problem hiding this comment.
Updated. I removed null from the hold type and kept it as hold?: string.
|
@huult, thank you for the review. I’ve addressed your feedback. The lint error appears to be unrelated to this PR. |
|
@nabi-ebrahimi could you sync with main? |
…oval-held-expense-multi-violations
@huult, thank you. I have merged the latest changes from main. |
NikkiWines
left a comment
There was a problem hiding this comment.
![]()
Holding merging pending an internal block atm - will merge once that's cleared
|
@nabi-ebrahimi, I got a notification for a comment about a regression for this PR. Is this ready to be merged? |
|
Hi @NikkiWines, thanks for checking. This is not a regression — the reported behavior is unrelated to this PR. The PR has been tested and is ready to be merged.
|
|
🚧 @NikkiWines has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚀 Deployed to staging by https://github.com/NikkiWines in version: 9.3.76-0 🚀
|
|
No help site changes required. This PR is a bug fix that corrects internal behavior to match what the help site already documents:
The fix ensures held expenses stay held when dismissing duplicate violations, and that the approval flow properly detects holds — both behaviors were already documented correctly. No new features, UI changes, or workflow changes were introduced. |
|
🚀 Deployed to production by https://github.com/roryabraham in version: 9.3.77-3 🚀
|
Explanation of Change
This change prevents held expenses from being accidentally cleared when dismissing a duplicate-transaction warning.
Previously, dismissing a duplicate violation could merge stale transaction data back into Onyx. If that stale snapshot did not include the latest hold state, an expense that was actually on hold could briefly or permanently appear unheld. The update makes the dismissal merge only the specific duplicate dismissal field instead of rewriting the whole transaction/comment object, preserving unrelated transaction state like holds.
The approval flow was also updated to check for held expenses using the report’s current transaction data instead of relying only on report-level lookup behavior. This keeps the approval blocker aligned with the actual expenses currently loaded for the report.
A regression test was added to cover the stale-snapshot case: when a duplicate dismissal fails and rolls back, the existing hold remains intact while only the duplicate dismissal state is reverted.
Fixed Issues
$ #89429
PROPOSAL: #89429 (comment)
Tests
Open the workspace chat and create an expense report with multiple expenses.
Submit the expense report.
Open the submitted report.
Select one expense using its checkbox.
From the actions menu, place the selected expense on hold.
Click Approve to approve the report.
Verify that an approval modal appears with the following options:
Offline tests
Same as Tests.
QA Steps
Same as Tests.
// TODO: These must be filled out, or the issue title must include "[No QA]."
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
Recording_20260513_005557.mp4
Android: mWeb Chrome
Recording_20260513_004903.mp4
iOS: Native
Screen.Recording.2026-05-12.at.11.07.34.PM.mov
iOS: mWeb Safari
Screen.Recording.2026-05-12.at.11.12.05.PM.mov
MacOS: Chrome / Safari
Screen.Recording.2026-05-12.at.11.03.22.PM.mov