Skip to content

WIP Fix: Hide ineligible reports from move expenses list (issue #70423)#89079

Open
abzokhattab wants to merge 2 commits intoExpensify:mainfrom
abzokhattab:akhattab/fix-70423-attempt4
Open

WIP Fix: Hide ineligible reports from move expenses list (issue #70423)#89079
abzokhattab wants to merge 2 commits intoExpensify:mainfrom
abzokhattab:akhattab/fix-70423-attempt4

Conversation

@abzokhattab
Copy link
Copy Markdown
Contributor

@abzokhattab abzokhattab commented Apr 28, 2026

Explanation of Change

Restores the intent of #70423 — when a destination report is ineligible to receive moved expenses, it should not appear in the picker — while specifically guarding against the three deploy blockers that defeated the previous two attempts (PR #78766#81608, PR #84832#88424 / #88425).

The rule we are enforcing: an expense report is ineligible as a move-expenses destination when its workspace has Instant Submit enabled AND Submit & Close (no approvers in the workflow) AND the report contains only non-reimbursable transactions. Moving expenses to such a report fails server-side with a 403 and the user is left with a silent failure.

Where the filter lives: useOutstandingReports. This is the same shape PR #84832 took, because it is the only place that lets both consumers — the picker (IOURequestEditReportCommon) and the confirmation page (ReportField) — see a consistent filtered list. The previous attempt at the picker layer (PR #78766) is what produced the "Report field opens a blank page" deploy blocker (#81608).

How each previous regression is prevented:

Deploy blocker What broke before What this PR adds
#81608 — Report field opens a blank page when all reports contain non-reimbursable expense Attempt 2's filter only ran inside the picker. ReportField.tsx still saw the unfiltered list, so shouldReportBeEditable stayed true and the user navigated into an empty picker. Filter applied in useOutstandingReports, so ReportField.shouldReportBeEditable becomes false when the list is empty and the field becomes non-interactive.
#88424 — Missing "Remove from report" option for non-reimbursable expense in retracted report Attempt 3 unconditionally filtered every ineligible report, including the user's own retracted source report. Once outstandingReports.length === 0, shouldShowNotFoundPage flipped true and the SelectionList footer (which hosts "Remove from report") was unmounted. New guard inside filterEligibleReports: if (selectedReportID && report.reportID === selectedReportID) return true;. The current source report always stays in the list, so the picker renders and "Remove from report" remains accessible.
#88425 — New report appears briefly after creating new report from self DM createReportForPolicy writes an optimistic empty report and then runs setTransactionReport on a deferred microtask. During that gap, hasOnlyNonReimbursableTransactions flipped from false (zero transactions) to true (one non-reimbursable transaction), making the new report disappear from the picker after a frame. New guard inside isReportIneligibleForMoveExpenses itself: if pendingFields.createReport === ADD we return false. Optimistically-created reports survive the filter until the server confirms creation and the pending field clears.

The admin/manager bypass downstream in IOURequestEditReportCommon.reportOptions becomes a no-op for this case because the ineligible reports are already gone before the bypass runs — admins/managers no longer see destinations the backend would reject either.

Fixed Issues

$ #70423
PROPOSAL: #70423 (comment)

Tests

Original bug (#70423):

  1. Create a workspace
  2. In Workflows: enable Instant submission, disable Approvals, disable Payments
  3. Create a reimbursable expense, retract the report so it reopens as a draft
  4. Create a non-reimbursable expense, retract that report too
  5. Open the report containing the reimbursable expense → click More > Move expenses
  6. Verify: the non-reimbursable-only report from step 4 does NOT appear in the destination list

Regression test for #81608 (deploy-blocker hardening):

  1. Same workspace setup as above
  2. Create two non-reimbursable expenses in two reports, retract both
  3. + > Create expense > Manual, enter amount, Next
  4. On the confirmation page, locate the Report field
  5. Verify: the field is disabled (no chevron, not interactive) — instead of opening a blank picker

Regression test for #88424 (deploy-blocker hardening):

  1. Same workspace setup
  2. Create a single non-reimbursable expense
  3. Open the report → More > Retract
  4. Click the Report field
  5. Verify: the picker opens with the source report visible AND the "Remove from report" option is shown at the bottom of the list

Regression test for #88425 (deploy-blocker hardening):

  1. Same workspace setup, with no open reports on the workspace
  2. Go to Self DM
  3. Create a non-reimbursable expense
  4. Open the expense → click the Report field
  5. Click Create report
  6. Click the Report field again
  7. Verify (step 5): the new report does not flicker in and out
  8. Verify (step 6): the newly created report is visible in the picker
  • Verify that no errors appear in the JS console

Offline tests

Same as Tests. The filter operates on Onyx-resident data, so behavior is identical offline. The pendingFields.createReport === ADD guard ensures optimistic-create flows continue to work correctly while offline (the report only clears the pending field when the server confirms, and until then it stays visible).

QA Steps

Same as Tests above, on staging.

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
    • MacOS: Desktop
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I verified there are no new alerts related to the canBeMissing param for useOnyx
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Local verification

Check Result
npx prettier --write All edited files formatted
npm run lint (./scripts/lint.sh --quiet) Lint done — 0 errors
npm run typecheck-tsgo no errors
npm run react-compiler-compliance-check check-changed React Compiler compliance passed (164 changed React files)
npx jest tests/unit/hooks/useOutstandingReports.test.ts 8 / 8 passed
npx jest tests/unit/ReportUtilsTest.ts -t "isReportIneligibleForMoveExpenses" 9 / 9 passed (8 existing + 1 new)
npx jest tests/ui/IOURequestEditReportCommonTest.tsx 2 / 2 passed (no UI regressions)

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

…on guards

Restores Expensify#70423's intent (don't show non-reimbursable-only reports in the move
expenses list when the workspace has instant submit + no approvers) while
guarding against the three deploy blockers that defeated the previous attempts:

- Expensify#81608: filter lives in the hook so ReportField on the confirmation page sees
  the same filtered list and correctly disables itself when no destination is
  eligible, instead of opening a blank picker.
- Expensify#88424: the currently-selected source report always survives the filter so
  outstandingReports.length stays >= 1, shouldShowNotFoundPage stays false, and
  the "Remove from report" footer renders for retracted reports.
- Expensify#88425: optimistically created reports (pendingFields.createReport === ADD)
  bypass isReportIneligibleForMoveExpenses so the new report doesn't flicker
  in/out during the create -> deferred-microtask move window.
@abzokhattab
Copy link
Copy Markdown
Contributor Author

@codex review

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/hooks/useOutstandingReports.ts 93.33% <100.00%> (+3.33%) ⬆️
src/libs/ReportUtils.ts 82.48% <100.00%> (+<0.01%) ⬆️
... and 11 files with indirect coverage changes

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3c917ddbb3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/libs/ReportUtils.ts
Comment on lines +2738 to +2739
if (moneyRequestReport?.pendingFields?.createReport === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) {
return false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict create-report bypass to truly pending reports

The new early return for pendingFields.createReport === ADD treats every optimistic report as eligible, but failed creates keep that pending field set: createNewReport failure data only sets errorFields.createReport and never clears pendingFields.createReport (src/libs/actions/Report/index.ts:3762-3767). In an instant-submit/no-approver workspace, a failed optimistic report with only non-reimbursable transactions will now stay in the destination list indefinitely and can be selected again even though backend move operations are still rejected. The bypass should be limited to in-flight creates (for example, no create error present) or the pending field must be cleared on failure.

Useful? React with 👍 / 👎.

@abzokhattab abzokhattab marked this pull request as ready for review April 29, 2026 23:13
@abzokhattab abzokhattab requested review from a team as code owners April 29, 2026 23:13
@melvin-bot melvin-bot Bot requested a review from huult April 29, 2026 23:13
@melvin-bot
Copy link
Copy Markdown

melvin-bot Bot commented Apr 29, 2026

@huult Please 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]

@melvin-bot melvin-bot Bot requested review from trjExpensify and removed request for a team April 29, 2026 23:13
@abzokhattab abzokhattab changed the title Fix: Hide ineligible reports from move expenses list (issue #70423) WIP Fix: Hide ineligible reports from move expenses list (issue #70423) Apr 29, 2026
for (const txn of transactions) {
// eslint-disable-next-line no-await-in-loop
await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${txn.transactionID}`, txn);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CONSISTENCY-5 (docs)

The eslint-disable-next-line no-await-in-loop directive lacks an accompanying comment explaining why the rule is being suppressed.

Add a justification comment, for example:

// eslint-disable-next-line no-await-in-loop -- Onyx.merge calls must be sequential to avoid race conditions during test data setup
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report);

Reviewed at: 6de392f | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

}

describe('useOutstandingReports', () => {
beforeAll(() => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CONSISTENCY-5 (docs)

The eslint-disable-next-line no-await-in-loop directive lacks an accompanying comment explaining why the rule is being suppressed.

Add a justification comment, for example:

// eslint-disable-next-line no-await-in-loop -- Onyx.merge calls must be sequential to avoid race conditions during test data setup
await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${txn.transactionID}`, txn);

Reviewed at: 6de392f | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

@abzokhattab
Copy link
Copy Markdown
Contributor Author

all regressions are mentioned in the description @huult

@abzokhattab
Copy link
Copy Markdown
Contributor Author

@MelvinBot tackled the ai generated comments

@abzokhattab
Copy link
Copy Markdown
Contributor Author

@MelvinBot review

@abzokhattab
Copy link
Copy Markdown
Contributor Author

@codex review

@MelvinBot
Copy link
Copy Markdown
Contributor

👍 Noted — thanks for addressing those, abzokhattab. All CI checks are currently passing on this PR. Let me know if you need anything else!

@MelvinBot
Copy link
Copy Markdown
Contributor

Code Review

The approach is sound — filtering in useOutstandingReports is the right place so both consumers (ReportField and IOURequestEditReportCommon) see a consistent list. The regression guards for #88424 and #88425 are well-targeted. Two items worth addressing before this leaves WIP:


1. Performance: duplicate full-collection policy subscription

useOutstandingReports.ts:16 adds useOnyx(ONYXKEYS.COLLECTION.POLICY), but useMappedPolicies on line 15 already subscribes to the same collection internally. Any mutation to any policy now triggers a re-render of every component using this hook.

Suggestion: Instead of a second full subscription, look up the policy for each candidate report from the data already available via useMappedPolicies, or use a selector to subscribe only to the fields isReportIneligibleForMoveExpenses needs (autoReporting, autoReportingFrequency, approvalMode).


2. Stale pendingFields.createReport after failed server create

The guard at ReportUtils.ts:2738 treats any report with pendingFields.createReport === ADD as eligible. However, Report/index.ts failure data (~line 3762) only sets errorFields.createReport and never clears pendingFields.createReport. If the server rejects the create, the report stays permanently eligible in the picker.

This is a pre-existing gap (not introduced by this PR), but the PR now depends on this field for correctness. Consider tightening the guard:

if (
    moneyRequestReport?.pendingFields?.createReport === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD &&
    !moneyRequestReport?.errorFields?.createReport
) {
    return false;
}

This way, failed creates with an error indicator fall through to the normal eligibility check.


3. Test gaps (minor)

The 8 hook tests and the new ReportUtilsTest case are well-structured with clear GWT comments and good regression annotations. Two gaps:

  • Cross-policy path untested: All hook tests pass a specific POLICY_ID. The branch that iterates all policies (when selectedPolicyID is undefined / personalPolicyID / self-DM) has zero coverage. At least one test with undefined as selectedPolicyID and reports across two policies would catch regressions there.
  • Stale pending field scenario: No test verifies what happens when pendingFields.createReport === ADD but errorFields.createReport is also set (failed create). Adding one would lock in the fix from item Fix spaces #2.

Everything else looks correct — the selectedReportID bypass for the source report, the filter placement covering both return paths, and the test assertions all align with the PR description. All CI checks pass. Good work on the thorough deploy-blocker analysis in the PR body.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants