Skip to content

[Due for payment 2026-05-26] [$250] Held Expense with Multiple Violations Is Automatically Unheld and Approved Without Prompt #89429

@m-natarajan

Description

@m-natarajan

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:

  1. Create a report with multiple card expenses
  2. Ensure at least one expense has multiple violations
  3. Put that expense on hold
  4. 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:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome / Safari

Screenshots/Videos

View all open jobs on GitHub

Issue OwnerCurrent 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

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.ExternalAdded to denote the issue can be worked on by a contributorNeeds ReproductionReproducible steps neededWeeklyKSv2

Type

No type
No fields configured for issues without a type.

Projects

Status

No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions