If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!
Version Number:
Reproducible in staging?: Needs Reproduction (Unable to reproduce)
Reproducible in production?: Needs reproduction
If this was caught during regression testing, add the test name, ID and link from BrowserStack:
Email or phone of affected tester (no customers):
Logs: https://stackoverflow.com/c/expensify/questions/4856
Expensify/Expensify Issue URL:
Issue reported by: @rafecolton
Slack conversation (hyperlinked to channel name): #Product
Action Performed:
- Create a report with multiple card expenses
- Ensure at least one expense has multiple violations
- Put that expense on hold
- Approve the report
Expected Result:
User should be prompted with options:
Approve all expenses, OR
Approve only unheld expenses and move held expenses to a new report
Actual Result:
Held expense is:
Automatically unheld
Approved without prompt
MelvinBot response:
I investigated the approval flow for held expenses. Here's what I found:
TL;DR: The most likely cause is a client-side race condition where another violation action on the same expense overwrites the hold field in Onyx state before the approval check runs. When the client doesn't detect the hold, it sends full=true to the server, which auto-unholds everything — no prompt shown.
How the flow works:
1. When you hold an expense, putOnHold sets transaction.comment.hold via a safe Onyx deep-merge
2. When you approve, the client checks hasHeldExpenses() which reads transaction.comment.hold from Onyx
3. If held expenses are detected → hold menu modal shown (approve non-held only vs approve all)
4. If not detected → approveMoneyRequest is called with full=true hardcoded
5. Server receives full=true → handleHeldTransactions auto-unholds everything. The server defaults full to true when not explicitly set.
Why "multiple violations on the same expense" matters:
Several code paths that deal with violations spread the full transaction.comment object into optimistic Onyx updates (e.g., dismissDuplicateTransactionViolation). This captures a snapshot of the comment at call time. If the hold was set concurrently or the snapshot was taken before the hold Onyx merge was processed, the spread overwrites comment.hold with a stale value (null/empty), making hasHeldExpenses() return false on the next check.
Card vs manual: No difference — the hold/approval logic treats both identically.
To confirm: We'd need the specific reportID to check server logs for whether ApproveMoneyRequest received full=true and whether "Held transactions found, unholding them" was logged. If rafecolton can share the reportID, I can check VictoriaLogs to verify the exact sequence of events.
Workaround:
Unknown
Platforms:
Select the officially supported platforms where the issue was reproduced:
Screenshots/Videos
View all open jobs on GitHub
Issue Owner
Current Issue Owner: @huult
Upwork Automation - Do Not Edit
- Upwork Job URL: https://www.upwork.com/jobs/~022050604818847892860
- Upwork Job ID: 2050604818847892860
- Last Price Increase: 2026-05-09
If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!
Version Number:
Reproducible in staging?: Needs Reproduction (Unable to reproduce)
Reproducible in production?: Needs reproduction
If this was caught during regression testing, add the test name, ID and link from BrowserStack:
Email or phone of affected tester (no customers):
Logs: https://stackoverflow.com/c/expensify/questions/4856
Expensify/Expensify Issue URL:
Issue reported by: @rafecolton
Slack conversation (hyperlinked to channel name): #Product
Action Performed:
Expected Result:
User should be prompted with options:
Approve all expenses, OR
Approve only unheld expenses and move held expenses to a new report
Actual Result:
Held expense is:
Automatically unheld
Approved without prompt
MelvinBot response:
I investigated the approval flow for held expenses. Here's what I found:
TL;DR: The most likely cause is a client-side race condition where another violation action on the same expense overwrites the hold field in Onyx state before the approval check runs. When the client doesn't detect the hold, it sends full=true to the server, which auto-unholds everything — no prompt shown.
How the flow works:
1. When you hold an expense, putOnHold sets transaction.comment.hold via a safe Onyx deep-merge
2. When you approve, the client checks hasHeldExpenses() which reads transaction.comment.hold from Onyx
3. If held expenses are detected → hold menu modal shown (approve non-held only vs approve all)
4. If not detected → approveMoneyRequest is called with full=true hardcoded
5. Server receives full=true → handleHeldTransactions auto-unholds everything. The server defaults full to true when not explicitly set.
Why "multiple violations on the same expense" matters:
Several code paths that deal with violations spread the full transaction.comment object into optimistic Onyx updates (e.g., dismissDuplicateTransactionViolation). This captures a snapshot of the comment at call time. If the hold was set concurrently or the snapshot was taken before the hold Onyx merge was processed, the spread overwrites comment.hold with a stale value (null/empty), making hasHeldExpenses() return false on the next check.
Card vs manual: No difference — the hold/approval logic treats both identically.
To confirm: We'd need the specific reportID to check server logs for whether ApproveMoneyRequest received full=true and whether "Held transactions found, unholding them" was logged. If rafecolton can share the reportID, I can check VictoriaLogs to verify the exact sequence of events.
Workaround:
Unknown
Platforms:
Select the officially supported platforms where the issue was reproduced:
Screenshots/Videos
View all open jobs on GitHub
Issue Owner
Current Issue Owner: @huultUpwork Automation - Do Not Edit