You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[$250] Reports marked as manually exported or paid in New Dot reappear in the Awaiting Export / Awaiting Payment queues. #95070
Version Number: 9.4.24-0 Reproducible in staging?: Needs Reproduction (Unable to reproduce mark as paid) 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:@Tmjarnesk1 Slack conversation (hyperlinked to channel name): #Expensify Bugs
Action Performed:
Opened workspace A.
Went to the QuickBooks Online workflow in NewDot.
Opened the Awaiting Export tab.
Selected multiple historical reports.
Used the bulk action dropdown above the reports.
Selected Export > Mark as manually exported.
Confirmed the reports cleared from the queue.
Refreshed / returned later and saw older reports reappear in Awaiting Export. Only a few would repopulate every so often...
Also went to the Awaiting Payment tab.
Selected May reports in bulk.
Used the bulk action to mark the reports as paid.
Also tried marking the reports as paid directly from the report itself.
Refreshed / returned later and saw the May reports still appearing in Awaiting Payment.
Expected Result:
Reports marked as manually exported should stay cleared from the Awaiting Export queue. Reports marked as paid should stay cleared from the Awaiting Payment queue.
Actual Result:
The actions appear to work temporarily, but reports reappear in the queue afterward. This is affecting 50+ historical reports for the user who is connecting to QBO for the first time and trying to clear older reports that were already captured in QuickBooks and mark older reports as paid for account cleanup.
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
The video and screenshot for the issue are in Slack thread
Root Cause (most likely backend — needs internal confirmation): The App side of both flows behaves correctly. When you bulk Mark as manually exported or Mark as paid, NewDot optimistically flips each report's state (isExportedToIntegration: true for export; stateNum=APPROVED / statusNum=REIMBURSED for pay) and removes it from the queue. But queue membership is decided by the server, not the client — the "Awaiting Export" and "Awaiting Payment" tabs are server-computed searches (action:export and action:pay). So if the backend doesn't durably persist the new state for these historical, pre-QBO-connection reports (or still recomputes them as needing export/payment), the next Search/OpenApp pull rebuilds the queue from the server and the reports reappear. That "clears, then reappears on refresh" signature — identical across both export and payment, and specific to a first-time QBO connection with 50+ old reports — points at backend persistence/recomputation, not App code.
Because the fix appears to live outside the Expensify/App repo (in the backend that handles MarkAsExported / PayMoneyRequest and computes the action:export / action:pay classifications), this needs an internal engineer with backend access — it is not an App-only change. I'm not proposing an App code fix.
Confidence: Medium — the App-side flow is verified against code; the backend revert is inferred by elimination (backend repos aren't accessible to me and there are no logs/IDs in the issue to confirm).
Investigation details & evidence
Mark as manually exported (App side is correct):
markAsManuallyExported() calls the MarkAsExported command and optimistically merges isExportedToIntegration: true onto each report — src/libs/actions/Report/index.ts:6010-6087.
The Export tab itself is a server search on action:export — src/libs/SearchUIUtils.ts:831. successData never re-asserts the flag or edits the snapshot, so the App relies entirely on the backend having persisted the export state.
Mark as paid (App side is correct):
Bulk pay loops one PayMoneyRequest / PayMoneyRequestWithWallet per report, optimistically setting statusNum=REIMBURSED, stateNum=APPROVED — src/libs/actions/IOU/PayMoneyRequest.ts:325-341.
The report is removed from the "Awaiting Payment" results only in successData (after a 200) and only when the active tab's search key is PAY — src/libs/actions/Search.ts:451. This removal is cosmetic against the current snapshot.
The "Awaiting Payment" tab is a server search on action:pay + reimbursable:yes + payer:me — src/libs/SearchUIUtils.ts:807. Membership is server-decided, so a re-pull that still returns action:pay re-adds the report.
Alternative explanations considered:
App not persisting optimistic state in successData — this is by design; if the backend persisted the state, the next pull returns it and the queue stays cleared. So reappearance necessarily requires the backend to return the old state → backend.
Silent failure on older reports (e.g. bulk pay resolving a payment method the backend can't honor for pre-connection reports, or MarkAsExported no-op'ing reports that predate the QBO connection) — also backend-side.
To confirm (backend engineer / logs): for the affected reportIDs at marking time —
Did MarkAsExported / PayMoneyRequest return 200, and did the report's export flag / statusNum actually persist server-side (vs. no-op or revert)?
After the action, does the Search backend still compute action:export / action:pay for those reports? (reportIDs/accountID are in the linked Slack thread.)
Possibly related:Expensify/App#93699 (bulk Pay / Mark as Paid option availability) — different symptom, but same bulk-pay area; likely unrelated to the reappearance itself.
Next Steps for Contributor+ / BugZero team:
This looks like a backend issue (report state not persisting / being recomputed for historical pre-QBO reports). Please route to an internal engineer with access to the MarkAsExported / PayMoneyRequest and Search action computation backend, and pull the reportIDs/accountID from the Slack thread to confirm with logs.
Reply @MelvinBot <feedback> to refine this analysis.
When a user opens the QuickBooks Online Awaiting Export tab, bulk selects historical reports and picks Export then Mark as manually exported (or marks reports as paid in the Awaiting Payment tab), NewDot writes an optimistic Onyx update that removes the reports from the queue immediately. After a refresh, the Search page pulls a fresh search snapshot from the backend, and any report whose export/payment status was not durably persisted repopulates the queue.
The export action module in Export.ts handles the export flows and their Onyx keys:
The mark-as-manually-exported bulk action clears items optimistically, but there is no successData that keeps those reports excluded from the Awaiting Export search results once the search snapshot is re-fetched. Because the optimistic Onyx entry is transient, and the search snapshot from the backend does not reflect the manually-exported state for these older reports, the reports reappear. The same pattern applies to Mark as paid: the local queue clears, but the refreshed search snapshot re-adds the reports because the paid state is not reconciled with what Search returns.
This is why only a subset repopulates each time and why the action "appears to work temporarily" then reverts: the temporary state is optimistic-only, and the authoritative search data on refresh does not carry the status change for these historical reports.
What changes do you think we should make in order to solve the problem?
In Export.ts, ensure the mark-as-manually-exported bulk command includes successData (not just optimistic data) that durably records the exported state for each report, so the reports stay excluded when the Search snapshot re-renders.
Apply the same durable successData/failureData pattern to the mark-as-paid bulk action so paid reports are reconciled against the refreshed Awaiting Payment search snapshot rather than only optimistically removed.
Because the reporter notes this affects 50+ historical reports on a first-time QBO connection and QA cannot reproduce mark as paid, confirm the backend command actually persists the status for older reports (the App-side optimistic update alone cannot survive a search snapshot refresh). If the backend does not record the status for historical reports, the fix must land there and the App change above keeps the queue consistent between refreshes.
What alternative solutions did you explore? (Optional)
Reminder: Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job.
Marking reports as manually exported / paid clears them from the Awaiting Export / Awaiting Payment queues only optimistically; the durable status those Search queries filter on isn't persisted, so the next re-fetch re-includes them.
In src/libs/actions/Report/index.ts, markAsManuallyExported (~L6007-L6083) pushes an EXPORTED report action and clears some optimistic fields, then calls API.write(WRITE_COMMANDS.MARK_AS_EXPORTED, ...). It records the export marker but doesn't move the report out of the "awaiting export" state that the QBO Awaiting Export tab's Search query filters on. There's a separate Search-context path, exportToIntegrationOnSearch in src/libs/actions/Search.ts (~L979), that is meant to update the Search SNAPSHOT; if the bulk dropdown routes manual export through the report-context markAsManuallyExported instead, the Search snapshot/backend filter state isn't durably changed. So the report disappears from the queue momentarily (optimistic) and returns on refresh because the server never excluded it from the filter. The same shape applies to mark-as-paid: the optimistic clear isn't backed by a persisted payment status the Awaiting Payment filter respects, so May reports reappear.
Because this touches the server-side export/payment status and the Search index the queues filter on, it's largely Internal/backend — which fits the "Needs Reproduction" state, and the scale (50+ historical reports) points at bulk processing/persistence rather than a single-report UI bug.
What changes do you think we should make in order to solve the problem?
Primary (backend/Internal): MARK_AS_EXPORTED (manual export) and the mark-as-paid command must durably set the report's export/payment status so the Awaiting Export / Awaiting Payment Search filters permanently exclude those reports — not just record an export action. Verify the bulk path processes all selected reports (no partial drops for large batches) so historical reports don't silently repopulate.
App-side:
Route the bulk "Mark as manually exported" / "Mark as paid" actions taken from the Search queue tabs through the Search-context actions that update the SNAPSHOT (exportToIntegrationOnSearch and the paid equivalent), so the optimistic queue removal matches the server filter semantics rather than only mutating the report.
Ensure failureData reverts the optimistic removal for any report whose command fails, so failed reports stay visible instead of appearing cleared and then "reappearing" — that mismatch is what makes this look like data loss.
What alternative solutions did you explore? (Optional)
Only strengthening the client optimistic update so the reports stay hidden, but that would mask reports the server still considers awaiting export/payment; the durable fix is persisting the status the queue filters on, with the client optimistic update and failure revert kept consistent with it.
The Awaiting Export and Awaiting Payment queues are not a stored list the App edits — they are derived from durable report state on every render, so the App can never make a "cleared" state stick on its own.
Both tabs are TODO search keys (EXPORT, PAY), and their live results are computed client-side by TodosUtils.getTodoReportsForSearchKey → reportMatchesTodoBucket:
EXPORT membership is isExportAction(...) && policy?.exporter === login; PAY membership is isPrimaryPayAction(...). Neither reads a queue flag — they read the report's persisted state:
isExportAction drops the report the instant it reads as exported (isExported → return false):
isPrimaryPayAction drops the report once it is no longer a finished-but-unpaid expense report (a REIMBURSED report fails isReportFinished and returns false at the !isProcessingReport guard):
So the clear step works because the bulk actions flip that durable state optimistically. Bulk "Mark as manually exported" goes through markAsManuallyExported (imported at
The reappearance is the same fact in reverse: membership is re-derived from authoritative state on the next OpenApp/Search pull, and the App only ever changed that state optimistically. Two things bring the report back, both requiring the write to not durably persist:
The MARK_AS_EXPORTED / PayMoneyRequest write fails or no‑ops server-side for these historical pre‑QBO reports, so the App rolls the optimistic state back via failureData (pay restores the whole prior iouReport, re-exposing the old statusNum):
The write succeeds locally but the next server pull still classifies the report as action:export/action:pay, so the predicate flips back to true.
Either way the predicate re-includes the report. That explains the exact signature — clears, then reverts on refresh; specific to 50+ old first‑time‑connection reports; only a subset repopulating (the ones whose write didn't stick). No client change can make optimistic-only state survive a refresh, so the durable failure is backend persistence of the exported action / reimbursed status for these reports.
What changes do you think we should make in order to solve the problem?
This is an Internal/backend fix, not App code. MARK_AS_EXPORTED and PayMoneyRequest/PayMoneyRequestWithWallet must durably persist the EXPORTED_TO_INTEGRATION action / statusNum=REIMBURSED for the historical pre‑connection reports, so the action:export/action:pay classification (and the equivalent isExportAction/isPrimaryPayAction predicates) permanently exclude them. Route to a backend engineer with the reportIDs/accountID from the Slack thread and verify, per report at marking time: did the command return 200, and does the exported flag / status still hold on the next Search pull (vs. no‑op or revert)?
The one App-layer change worth making is on the error/rollback path (data layer, not the predicate): when the write fails, the optimistic state is silently reverted, so the user only sees "it cleared, then came back" with no signal. Surfacing the RBR error on those reports on failureData turns a confusing reappearance into an actionable failure. It does not fix the reappearance — that stays backend.
What alternative solutions did you explore? (Optional)
Persist a client-only "manually exported"/"paid" flag in successData and exclude on it (as the other proposals suggest). Rejected: queue membership is intentionally derived from authoritative report state via reportMatchesTodoBucket, so a client flag would keep reports hidden that the server still legitimately returns as awaiting export/payment — masking real work and diverging the client from the server's action: classification. It also cannot help the reported case, where the state simply never persisted server-side.
I also checked whether the bug is that bulk "Mark as manually exported" routes through the report-context markAsManuallyExported instead of the Search-context exportToIntegrationOnSearch, which alone edits the search snapshot in successData (
// If we are on the 'Export' suggested search, remove the report from the view once the action is taken, don't wait for the view to be re-fetched via Search
). Rejected: that snapshot edit is cosmetic against the current fetch, and the to-do queue re-derives membership from durable state on the next pull regardless — so aligning the code paths would change only the momentary optimistic behavior, not the reappearance.
Please re-state the problem that we are trying to solve in this issue.
When a user bulk-marks historical reports as manually exported in the QBO Awaiting Export queue (or marks reports as paid in Awaiting Payment), the reports clear from the queue and then reappear on the next refresh — so 50+ historical reports on a first-time QBO connection cannot be permanently cleared.
What is the root cause of that problem?
The Awaiting Export / Awaiting Payment tabs are to-do suggested searches, so the visible list is not a server snapshot the App edits — it is re-derived live from durable report state on every Onyx change by useTodoSearchResults → reportMatchesTodoBucket (TodosUtils.ts:123-126), which SearchResultsProvider prefers over the snapshot whenever shouldUseLiveData is true (SearchResultsProvider.tsx:45-66). EXPORT membership is isExportAction(report, login, policy, reportActions) && policy?.exporter === login.
Inside isExportAction, the report drops out of the queue the instant isExported(...) returns true. So the fix hinges entirely on how we compute isExported — and that's where the real defect is. isExported in ReportUtils.ts:12593-12598 returns the report-level flag verbatim whenever it is defined, and only falls back to scanning the export report actions when the flag is undefined:
if(report?.isExportedToIntegration!==undefined){returnreport.isExportedToIntegration;// a stored `false` short-circuits here and never reaches the action scan}// Fallback: scan actions for a valid EXPORTED_TO_INTEGRATION (incl. markedManually), reset-invalidation aware
This is fatal for the manual-export path. markAsManuallyExported records the export as a markedManuallyEXPORTED_TO_INTEGRATIONreport action and sets isExportedToIntegration: trueonly optimistically (Report/index.ts:6138-6146); its successData re-affirms nothing but the action's pendingAction (Report/index.ts:6149-6157). For historical, pre-QBO-connection reports, the report-level isExportedToIntegration flag was never populated by any auto-export — so when the first-time connection triggers a full report reload, OpenApp merges these reports back with isExportedToIntegration: false, overwriting the optimistic true. isExported then returns that falsewithout ever consulting the valid markedManually action the backend did persist, isExportAction flips back to true, and the report re-enters the queue. That is the exact "clears, then reappears on refresh" signature — and "only a few repopulate" is precisely the subset whose flag comes back false while any report that was also auto-exported (flag true) stays gone.
History of how we got here:
Originally isExported scanned report actions only — the markedManually export action was authoritative.
PR #65782 (commit d3f6ee3, "Use BE report data instead of report actions for Report Previews") added the isExportedToIntegration flag short-circuit so report previews could trust the backend field instead of re-scanning actions. Reasonable there — but the same isExported helper feeds the Export to-do bucket, and for manual exports the flag is not the source of truth, the markedManually action is. The short-circuit silently vetoes it.
The pay half has no equivalent App defect: isPrimaryPayAction is driven purely by statusNum/stateNum (ReportPrimaryActionUtils.ts:243-252) with no flag-over-history override, so a REIMBURSED report leaves the bucket cleanly and only reappears if the server genuinely didn't persist REIMBURSED — that half is backend persistence, as others note. The export half is the one that reappears despite a correctly-persisted backend action, and that is an App bug.
What changes do you think we should make in order to solve the problem?
Primary fix (root cause): in isExported, treat only a truthy flag as authoritative; on a falsy/absent flag, fall through to the action scan (which already honors reset-invalidation via lastSuccessfulExportCreated > lastResetCreated) instead of returning false blindly:
functionisExported(reportActions,report){// A truthy backend flag is authoritative (auto-export path).// A falsy/absent flag must NOT veto a valid manual export recorded in the report actions —// historical reports marked as manually exported keep isExportedToIntegration=false while// carrying a `markedManually` EXPORTED_TO_INTEGRATION action.if(report?.isExportedToIntegration){returntrue;}// ...existing reset-aware action scan (unchanged)...}
Why this fixes it:
Manual export → refresh (the bug): flag comes back false, but the persisted markedManually action has no later reset action → action scan returns true → isExportAction false → report stays out of the queue.
Auto-export then re-open/retract: flag falseand a later REOPENED/RETRACTED/UNAPPROVED reset action → action scan returns false → report correctly re-enters the queue. No regression.
Result: reports marked as manually exported stay cleared from the Awaiting Export queue across refreshes, because the queue derivation now trusts the persisted manual-export action instead of a stale isExportedToIntegration: false flag. The residual pay-side reappearance is the backend persisting REIMBURSED for historical reports and should be confirmed with the reportIDs/accountID in the Slack thread.
What alternative solutions did you explore? (Optional)
Persist the export state in markAsManuallyExported's successData (or a new isManuallyExported flag) so the optimistic value survives. It cannot — the field is backend-owned; the first-time-connection report reload merges the server's isExportedToIntegration: false back over any client value. Only a derivation that respects the persisted action survives the refresh. Adding a second client-only flag also diverges the App from the server's action:export classification.
Reconcile the search SNAPSHOT in successData (mirror exportToIntegrationOnSearch). Misdirected for these tabs — they render liveuseTodoSearchResults data, not the snapshot (SearchResultsProvider.tsx:45-66), so a snapshot edit is cosmetic and the queue re-derives membership from durable state on the next pull regardless.
Treat the whole thing as backend-only. Incomplete — it leaves the isExported flag-over-action veto in place, so even after the backend persists the manual export action correctly, the App still re-adds these historical reports to the queue. The backend persistence matters for the pay half, but the export half will not clear durably without the derivation fix above.
⚠️@yusufdeveloper2903 Your proposal is a duplicate of an already existing proposal and has been automatically withdrawn to prevent spam. Please review the existing proposals before submitting a new one.
hungvu193 "Awaiting export" isn't inside the QuickBooks Online connection screens — it's a suggested search in the Reports view, under the Accounting section of the left-hand search menu.
To get there: Left nav → Reports → in the search list on the left, Accounting → Awaiting export. (It's a saved search for action:export, not a QBO-specific page — the reporter just reached it while cleaning up QBO reports.)
Why you might not see it: the "Awaiting export" item only renders when, for at least one workspace, you are the configured exporter and that workspace's accounting connection has no sync error — isEligibleForExportSuggestion = isExporter && !hasExportError (SearchUIUtils.ts:1163). So to reproduce, make sure your account is set as the workspace's exporter (Workspace → Accounting → set the preferred exporter to you) on a workspace with a working QBO connection.
P.S. Is everyone reading this sure this is really a near-term priority? Be brave: if you disagree, go ahead and close it out. If someone disagrees, they'll reopen it, and if they don't: one less thing to do!
Activity
MelvinBot commentedon Jul 1, 2026
Issue Analysis
Root Cause (most likely backend — needs internal confirmation): The App side of both flows behaves correctly. When you bulk Mark as manually exported or Mark as paid, NewDot optimistically flips each report's state (
isExportedToIntegration: truefor export;stateNum=APPROVED / statusNum=REIMBURSEDfor pay) and removes it from the queue. But queue membership is decided by the server, not the client — the "Awaiting Export" and "Awaiting Payment" tabs are server-computed searches (action:exportandaction:pay). So if the backend doesn't durably persist the new state for these historical, pre-QBO-connection reports (or still recomputes them as needing export/payment), the next Search/OpenApp pull rebuilds the queue from the server and the reports reappear. That "clears, then reappears on refresh" signature — identical across both export and payment, and specific to a first-time QBO connection with 50+ old reports — points at backend persistence/recomputation, not App code.Because the fix appears to live outside the
Expensify/Apprepo (in the backend that handlesMarkAsExported/PayMoneyRequestand computes theaction:export/action:payclassifications), this needs an internal engineer with backend access — it is not an App-only change. I'm not proposing an App code fix.Confidence: Medium — the App-side flow is verified against code; the backend revert is inferred by elimination (backend repos aren't accessible to me and there are no logs/IDs in the issue to confirm).
Investigation details & evidence
Mark as manually exported (App side is correct):
markAsManuallyExported()calls theMarkAsExportedcommand and optimistically mergesisExportedToIntegration: trueonto each report —src/libs/actions/Report/index.ts:6010-6087.isExported()→src/libs/ReportUtils.ts:12385andsrc/libs/TodosUtils.ts:122.action:export—src/libs/SearchUIUtils.ts:831.successDatanever re-asserts the flag or edits the snapshot, so the App relies entirely on the backend having persisted the export state.Mark as paid (App side is correct):
PayMoneyRequest/PayMoneyRequestWithWalletper report, optimistically settingstatusNum=REIMBURSED, stateNum=APPROVED—src/libs/actions/IOU/PayMoneyRequest.ts:325-341.successData(after a 200) and only when the active tab's search key is PAY —src/libs/actions/Search.ts:451. This removal is cosmetic against the current snapshot.action:pay+reimbursable:yes+payer:me—src/libs/SearchUIUtils.ts:807. Membership is server-decided, so a re-pull that still returnsaction:payre-adds the report.Alternative explanations considered:
successData— this is by design; if the backend persisted the state, the next pull returns it and the queue stays cleared. So reappearance necessarily requires the backend to return the old state → backend.MarkAsExportedno-op'ing reports that predate the QBO connection) — also backend-side.To confirm (backend engineer / logs): for the affected reportIDs at marking time —
MarkAsExported/PayMoneyRequestreturn 200, and did the report's export flag /statusNumactually persist server-side (vs. no-op or revert)?action:export/action:payfor those reports? (reportIDs/accountID are in the linked Slack thread.)Possibly related: Expensify/App#93699 (bulk Pay / Mark as Paid option availability) — different symptom, but same bulk-pay area; likely unrelated to the reappearance itself.
Next Steps for Contributor+ / BugZero team:
MarkAsExported/PayMoneyRequestand Searchactioncomputation backend, and pull the reportIDs/accountID from the Slack thread to confirm with logs.@MelvinBot <feedback>to refine this analysis.melvin-bot commentedon Jul 6, 2026
Huh... This is 4 days overdue. Who can take care of this?
melvin-bot commentedon Jul 9, 2026
6 days overdue. This is scarier than being forced to listen to Vogon poetry!
melvin-bot commentedon Jul 10, 2026
8 days overdue is a lot. Should this be a Weekly issue? If so, feel free to change it!
samranahm commentedon Jul 14, 2026
Proposal
What is the root cause of that problem?
When a user opens the QuickBooks Online Awaiting Export tab, bulk selects historical reports and picks Export then Mark as manually exported (or marks reports as paid in the Awaiting Payment tab), NewDot writes an optimistic Onyx update that removes the reports from the queue immediately. After a refresh, the Search page pulls a fresh search snapshot from the backend, and any report whose export/payment status was not durably persisted repopulates the queue.
The export action module in
Export.tshandles the export flows and their Onyx keys:App/src/libs/actions/Export.ts
Lines 11 to 12 in 20f2fef
The mark-as-manually-exported bulk action clears items optimistically, but there is no
successDatathat keeps those reports excluded from the Awaiting Export search results once the search snapshot is re-fetched. Because the optimistic Onyx entry is transient, and the search snapshot from the backend does not reflect the manually-exported state for these older reports, the reports reappear. The same pattern applies to Mark as paid: the local queue clears, but the refreshed search snapshot re-adds the reports because the paid state is not reconciled with what Search returns.This is why only a subset repopulates each time and why the action "appears to work temporarily" then reverts: the temporary state is optimistic-only, and the authoritative search data on refresh does not carry the status change for these historical reports.
What changes do you think we should make in order to solve the problem?
Export.ts, ensure the mark-as-manually-exported bulk command includessuccessData(not just optimistic data) that durably records the exported state for each report, so the reports stay excluded when the Search snapshot re-renders.App/src/libs/actions/Export.ts
Lines 11 to 12 in 20f2fef
Apply the same durable
successData/failureDatapattern to the mark-as-paid bulk action so paid reports are reconciled against the refreshed Awaiting Payment search snapshot rather than only optimistically removed.Because the reporter notes this affects 50+ historical reports on a first-time QBO connection and QA cannot reproduce mark as paid, confirm the backend command actually persists the status for older reports (the App-side optimistic update alone cannot survive a search snapshot refresh). If the backend does not record the status for historical reports, the fix must land there and the App change above keeps the queue consistent between refreshes.
What alternative solutions did you explore? (Optional)
Reminder: Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job.
David-2002065 commentedon Jul 14, 2026
Proposal
What is the root cause of that problem?
Marking reports as manually exported / paid clears them from the Awaiting Export / Awaiting Payment queues only optimistically; the durable status those Search queries filter on isn't persisted, so the next re-fetch re-includes them.
In
src/libs/actions/Report/index.ts,markAsManuallyExported(~L6007-L6083) pushes anEXPORTEDreport action and clears some optimistic fields, then callsAPI.write(WRITE_COMMANDS.MARK_AS_EXPORTED, ...). It records the export marker but doesn't move the report out of the "awaiting export" state that the QBO Awaiting Export tab's Search query filters on. There's a separate Search-context path,exportToIntegrationOnSearchinsrc/libs/actions/Search.ts(~L979), that is meant to update the SearchSNAPSHOT; if the bulk dropdown routes manual export through the report-contextmarkAsManuallyExportedinstead, the Search snapshot/backend filter state isn't durably changed. So the report disappears from the queue momentarily (optimistic) and returns on refresh because the server never excluded it from the filter. The same shape applies to mark-as-paid: the optimistic clear isn't backed by a persisted payment status the Awaiting Payment filter respects, so May reports reappear.Because this touches the server-side export/payment status and the Search index the queues filter on, it's largely Internal/backend — which fits the "Needs Reproduction" state, and the scale (50+ historical reports) points at bulk processing/persistence rather than a single-report UI bug.
What changes do you think we should make in order to solve the problem?
Primary (backend/Internal):
MARK_AS_EXPORTED(manual export) and the mark-as-paid command must durably set the report's export/payment status so the Awaiting Export / Awaiting Payment Search filters permanently exclude those reports — not just record an export action. Verify the bulk path processes all selected reports (no partial drops for large batches) so historical reports don't silently repopulate.App-side:
SNAPSHOT(exportToIntegrationOnSearchand the paid equivalent), so the optimistic queue removal matches the server filter semantics rather than only mutating the report.failureDatareverts the optimistic removal for any report whose command fails, so failed reports stay visible instead of appearing cleared and then "reappearing" — that mismatch is what makes this look like data loss.What alternative solutions did you explore? (Optional)
Only strengthening the client optimistic update so the reports stay hidden, but that would mask reports the server still considers awaiting export/payment; the durable fix is persisting the status the queue filters on, with the client optimistic update and failure revert kept consistent with it.
oqildev commentedon Jul 14, 2026
Proposal
What is the root cause of that problem?
The Awaiting Export and Awaiting Payment queues are not a stored list the App edits — they are derived from durable report state on every render, so the App can never make a "cleared" state stick on its own.
Both tabs are TODO search keys (
EXPORT,PAY), and their live results are computed client-side byTodosUtils.getTodoReportsForSearchKey→reportMatchesTodoBucket:App/src/libs/TodosUtils.ts
Lines 104 to 129 in ac6069a
EXPORT membership is
isExportAction(...) && policy?.exporter === login; PAY membership isisPrimaryPayAction(...). Neither reads a queue flag — they read the report's persisted state:isExportActiondrops the report the instant it reads as exported (isExported→return false):App/src/libs/ReportPrimaryActionUtils.ts
Lines 281 to 324 in ac6069a
isPrimaryPayActiondrops the report once it is no longer a finished-but-unpaid expense report (aREIMBURSEDreport failsisReportFinishedand returns false at the!isProcessingReportguard):App/src/libs/ReportPrimaryActionUtils.ts
Lines 206 to 259 in ac6069a
The server-side suggested searches use identical semantics (
action:export exporter:me,action:pay reimbursable:yes payer:me):App/src/libs/SearchUIUtils.ts
Lines 812 to 858 in ac6069a
So the clear step works because the bulk actions flip that durable state optimistically. Bulk "Mark as manually exported" goes through
markAsManuallyExported(imported atApp/src/hooks/useSearchBulkActions.ts
Line 15 in ac6069a
EXPORTED_TO_INTEGRATIONaction (buildOptimisticExportIntegrationAction, imported atApp/src/libs/actions/Report/index.ts
Line 111 in ac6069a
isExportedtrue → report leaves the EXPORT bucket. Bulk "Mark as paid" runspayMoneyRequest, which optimistically setsstatusNum=REIMBURSED, stateNum=APPROVED:App/src/libs/actions/IOU/PayMoneyRequest.ts
Lines 332 to 351 in ac6069a
The reappearance is the same fact in reverse: membership is re-derived from authoritative state on the next OpenApp/Search pull, and the App only ever changed that state optimistically. Two things bring the report back, both requiring the write to not durably persist:
MARK_AS_EXPORTED/PayMoneyRequestwrite fails or no‑ops server-side for these historical pre‑QBO reports, so the App rolls the optimistic state back viafailureData(pay restores the whole prioriouReport, re-exposing the oldstatusNum):App/src/libs/actions/IOU/PayMoneyRequest.ts
Lines 405 to 432 in ac6069a
action:export/action:pay, so the predicate flips back to true.Either way the predicate re-includes the report. That explains the exact signature — clears, then reverts on refresh; specific to 50+ old first‑time‑connection reports; only a subset repopulating (the ones whose write didn't stick). No client change can make optimistic-only state survive a refresh, so the durable failure is backend persistence of the exported action / reimbursed status for these reports.
What changes do you think we should make in order to solve the problem?
This is an Internal/backend fix, not App code.
MARK_AS_EXPORTEDandPayMoneyRequest/PayMoneyRequestWithWalletmust durably persist theEXPORTED_TO_INTEGRATIONaction /statusNum=REIMBURSEDfor the historical pre‑connection reports, so theaction:export/action:payclassification (and the equivalentisExportAction/isPrimaryPayActionpredicates) permanently exclude them. Route to a backend engineer with the reportIDs/accountID from the Slack thread and verify, per report at marking time: did the command return 200, and does the exported flag / status still hold on the next Search pull (vs. no‑op or revert)?The one App-layer change worth making is on the error/rollback path (data layer, not the predicate): when the write fails, the optimistic state is silently reverted, so the user only sees "it cleared, then came back" with no signal. Surfacing the RBR error on those reports on
failureDataturns a confusing reappearance into an actionable failure. It does not fix the reappearance — that stays backend.What alternative solutions did you explore? (Optional)
Persist a client-only "manually exported"/"paid" flag in
successDataand exclude on it (as the other proposals suggest). Rejected: queue membership is intentionally derived from authoritative report state viareportMatchesTodoBucket, so a client flag would keep reports hidden that the server still legitimately returns as awaiting export/payment — masking real work and diverging the client from the server'saction:classification. It also cannot help the reported case, where the state simply never persisted server-side.I also checked whether the bug is that bulk "Mark as manually exported" routes through the report-context
markAsManuallyExportedinstead of the Search-contextexportToIntegrationOnSearch, which alone edits the search snapshot insuccessData(App/src/libs/actions/Search.ts
Lines 1149 to 1158 in ac6069a
getSearchActionOnyxDataatApp/src/libs/actions/Search.ts
Lines 460 to 466 in ac6069a
yusufdeveloper2903 commentedon Jul 14, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
When a user bulk-marks historical reports as manually exported in the QBO Awaiting Export queue (or marks reports as paid in Awaiting Payment), the reports clear from the queue and then reappear on the next refresh — so 50+ historical reports on a first-time QBO connection cannot be permanently cleared.
What is the root cause of that problem?
The Awaiting Export / Awaiting Payment tabs are to-do suggested searches, so the visible list is not a server snapshot the App edits — it is re-derived live from durable report state on every Onyx change by
useTodoSearchResults→reportMatchesTodoBucket(TodosUtils.ts:123-126), whichSearchResultsProviderprefers over the snapshot whenevershouldUseLiveDatais true (SearchResultsProvider.tsx:45-66). EXPORT membership isisExportAction(report, login, policy, reportActions) && policy?.exporter === login.Inside
isExportAction, the report drops out of the queue the instantisExported(...)returns true. So the fix hinges entirely on how we computeisExported— and that's where the real defect is.isExportedinReportUtils.ts:12593-12598returns the report-level flag verbatim whenever it is defined, and only falls back to scanning the export report actions when the flag isundefined:This is fatal for the manual-export path.
markAsManuallyExportedrecords the export as amarkedManuallyEXPORTED_TO_INTEGRATIONreport action and setsisExportedToIntegration: trueonly optimistically (Report/index.ts:6138-6146); itssuccessDatare-affirms nothing but the action'spendingAction(Report/index.ts:6149-6157). For historical, pre-QBO-connection reports, the report-levelisExportedToIntegrationflag was never populated by any auto-export — so when the first-time connection triggers a full report reload,OpenAppmerges these reports back withisExportedToIntegration: false, overwriting the optimistictrue.isExportedthen returns thatfalsewithout ever consulting the validmarkedManuallyaction the backend did persist,isExportActionflips back to true, and the report re-enters the queue. That is the exact "clears, then reappears on refresh" signature — and "only a few repopulate" is precisely the subset whose flag comes backfalsewhile any report that was also auto-exported (flagtrue) stays gone.History of how we got here:
isExportedscanned report actions only — themarkedManuallyexport action was authoritative.d3f6ee3, "Use BE report data instead of report actions for Report Previews") added theisExportedToIntegrationflag short-circuit so report previews could trust the backend field instead of re-scanning actions. Reasonable there — but the sameisExportedhelper feeds the Export to-do bucket, and for manual exports the flag is not the source of truth, themarkedManuallyaction is. The short-circuit silently vetoes it.The pay half has no equivalent App defect:
isPrimaryPayActionis driven purely bystatusNum/stateNum(ReportPrimaryActionUtils.ts:243-252) with no flag-over-history override, so aREIMBURSEDreport leaves the bucket cleanly and only reappears if the server genuinely didn't persistREIMBURSED— that half is backend persistence, as others note. The export half is the one that reappears despite a correctly-persisted backend action, and that is an App bug.What changes do you think we should make in order to solve the problem?
Primary fix (root cause): in
isExported, treat only a truthy flag as authoritative; on a falsy/absent flag, fall through to the action scan (which already honors reset-invalidation vialastSuccessfulExportCreated > lastResetCreated) instead of returningfalseblindly:Why this fixes it:
false, but the persistedmarkedManuallyaction has no later reset action → action scan returnstrue→isExportActionfalse → report stays out of the queue.falseand a laterREOPENED/RETRACTED/UNAPPROVEDreset action → action scan returnsfalse→ report correctly re-enters the queue. No regression.trueexactly as before; only the falsy branch now defers to history instead of asserting "not exported."Result: reports marked as manually exported stay cleared from the Awaiting Export queue across refreshes, because the queue derivation now trusts the persisted manual-export action instead of a stale
isExportedToIntegration: falseflag. The residual pay-side reappearance is the backend persistingREIMBURSEDfor historical reports and should be confirmed with the reportIDs/accountID in the Slack thread.What alternative solutions did you explore? (Optional)
markAsManuallyExported'ssuccessData(or a newisManuallyExportedflag) so the optimistic value survives. It cannot — the field is backend-owned; the first-time-connection report reload merges the server'sisExportedToIntegration: falseback over any client value. Only a derivation that respects the persisted action survives the refresh. Adding a second client-only flag also diverges the App from the server'saction:exportclassification.SNAPSHOTinsuccessData(mirrorexportToIntegrationOnSearch). Misdirected for these tabs — they render liveuseTodoSearchResultsdata, not the snapshot (SearchResultsProvider.tsx:45-66), so a snapshot edit is cosmetic and the queue re-derives membership from durable state on the next pull regardless.isExportedflag-over-action veto in place, so even after the backend persists the manual export action correctly, the App still re-adds these historical reports to the queue. The backend persistence matters for the pay half, but the export half will not clear durably without the derivation fix above.github-actions commentedon Jul 14, 2026
36 remaining items
VictoriaExpensify commentedon Aug 7, 2026
Any update on this one @chuckdries and @situchan ?
chuckdries commentedon Aug 7, 2026
Trying to repro again today
chuckdries commentedon Aug 11, 2026
I still haven't successfully reproduced the issue. I have asked for volunteers to hand off to a BC
hungvu193 commentedon Aug 12, 2026
I have some bandwidth, so assigning this to myself and I'll try to reproduce it 👀
hungvu193 commentedon Aug 12, 2026
@MelvinBot How can I find the
Awaiting Export tab.inside QuickBooks Online workflow in NewDot.?MelvinBot commentedon Aug 12, 2026
hungvu193"Awaiting export" isn't inside the QuickBooks Online connection screens — it's a suggested search in the Reports view, under the Accounting section of the left-hand search menu.To get there: Left nav → Reports → in the search list on the left, Accounting → Awaiting export. (It's a saved search for
action:export, not a QBO-specific page — the reporter just reached it while cleaning up QBO reports.)Why you might not see it: the "Awaiting export" item only renders when, for at least one workspace, you are the configured exporter and that workspace's accounting connection has no sync error —
isEligibleForExportSuggestion = isExporter && !hasExportError(SearchUIUtils.ts:1163). So to reproduce, make sure your account is set as the workspace's exporter (Workspace → Accounting → set the preferred exporter to you) on a workspace with a working QBO connection.Code references
SearchUIUtils.ts:4835-4849Awaiting export:en.ts:9024SearchUIUtils.ts:1163hungvu193 commentedon Aug 14, 2026
I also couldn't able to reproduce this one 🤔 . Can we ask the customer if they're still able to reproduce it?
mvtglobally commentedon Aug 14, 2026
Issue not reproducible during KI retests. (First week)
melvin-bot commentedon Sep 7, 2026
This issue has not been updated in over 15 days. @chuckdries, @hungvu193, @situchan eroding to Monthly issue.
P.S. Is everyone reading this sure this is really a near-term priority? Be brave: if you disagree, go ahead and close it out. If someone disagrees, they'll reopen it, and if they don't: one less thing to do!