Skip to content

[$250] Pay report dead-ends with generic error when cached report total is stale ("amount changed") #93659

Description

@MelvinBot

Problem

When a payer clicks Pay on a report, the App sends the Onyx-cached report total it last rendered. If that cached total is even slightly stale (e.g. a few cents of drift after a currency exchange-rate re-conversion on a foreign-currency expense), it no longer matches the server-computed reimbursable total. The backend correctly rejects the payment with an "amount changed" error to prevent paying an amount the user didn't actually see and consent to.

The problem is what the App does next:

  1. It shows a generic error — Unexpected error. Please try again later. (src/languages/en.ts:1536, via iou.error.other at src/libs/actions/IOU/PayMoneyRequest.ts:397).
  2. Its failure data rolls Onyx back to the same stale report snapshot (src/libs/actions/IOU/PayMoneyRequest.ts:401-407).

So clicking Pay again sends the same stale total and hits the same wall. The user is stuck in a dead-end loop with no indication of what's wrong or how to recover. The only current workaround is to manually reload the report so the client picks up the corrected total — which a customer should never need to know to do.

The cached total is read here: src/libs/actions/IOU/PayMoneyRequest.ts:250-257.

Expected behavior

Clicking Pay should succeed (or self-recover) without the user needing to manually reload the report. The backend consent guardrail is correct and should stay — the fix is for the client to stop dead-ending on a stale total.

Actual behavior

Payment fails with a generic "Unexpected error. Please try again later." Retrying without a manual page reload reproduces the same error every time.

Proposed direction

Make the client auto-recover from a stale total instead of dead-ending. When PayMoneyRequest fails specifically because the displayed amount no longer matches the server total, the App should re-fetch the report to refresh the cached total, then re-confirm with the corrected amount (or silently retry within a trivial tolerance) — no manual reload required.

There is clean prior art for this shape: the Reauthentication middleware already intercepts a specific backend response code, transparently fixes the underlying state, and re-runs the original request. A "stale total" recovery should follow the same pattern.

Implementation notes

Three changes, in order of importance:

  1. Auto-recover (core fix) — in PayMoneyRequest.ts, detect the "amount changed" failure, trigger an OpenReport/reconnect to pull the live total, then re-confirm and retry. Today the failure data blindly restores the same stale iouReport snapshot (lines 401-407), which guarantees the next attempt fails too.

  2. Don't clobber a server-pushed total — confirm whether the failure response already includes a refreshed report.total in its onyxData. If it does, the generic failure path is overwriting it with the stale value; honoring it may largely resolve the issue on its own.

  3. Meaningful error copy (fallback) — if auto-retry isn't pursued, at minimum replace the generic iou.error.other text with something actionable ("The amount on this report changed — review the new total and pay again"). This still requires user action, so it's the weakest option and shouldn't be the only change.

Scope sizing: a quick log query on how often PayMoneyRequest hits this "amount changed" rejection across all users (not just one report) would size the impact and confirm FX-rate timing is the dominant trigger.

Platform

Web (and any platform sharing this Onyx flow).


Originated from an internal Concierge escalation (Expensify/Expensify#648321). The backend guardrail is working as designed; this is a frontend recovery/UX defect in App.

Upwork Automation - Do Not Edit

Metadata

Metadata

Assignees

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorHelp WantedApply this label when an issue is open to proposals by contributors

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions