Skip to content

Fix bulk Pay/Mark as Paid disappearing for whole-report selections - #94075

Merged
luacmartins merged 15 commits into
mainfrom
claude-bulkPayMarkAsPaidPolicyIDs
Sep 8, 2026
Merged

luacmartins merged 15 commits into
mainfrom
claude-bulkPayMarkAsPaidPolicyIDs

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Jun 19, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

When multiple whole reports are selected in the Search/Reports view, the bulk action menu intermittently dropped both Pay and Mark as Paid, leaving only Export — even when the selected reports shared the same currency and month.

Root cause: selectedPolicyIDs was derived only from selectedTransactions, never from selectedReports. When the user selects whole reports (no individual transaction rows), selectedTransactions is empty, so selectedPolicyIDs was empty and selectedPolicyID was undefined. useBulkPayOptions short-circuits to bulkPayButtonOptions = undefined when selectedPolicyID is missing, and the menu gate … && !!bulkPayButtonOptions?.length then removed the entire Pay group — including its Mark as Paid submenu item. (selectedReportID already falls back to selectedReportIDs, but selectedPolicyID did not — this gap was introduced alongside the cross-workspace bulk-pay change in #87148.)

The fix derives selectedPolicyIDs from selectedReports as well as selectedTransactions, so whole-report selections produce a valid selectedPolicyID and useBulkPayOptions can build the native Pay options (and, for multi-currency selections, the Mark as Paid / Pay Elsewhere fallback) instead of returning undefined.

This implements item 2 of the approved proposal — the regression that explains the dominant reported symptom (options disappearing even for same-currency/same-month selections). Proposal items 1 (offering a standalone Mark as Paid that is fully decoupled from the native Pay submenu) and 3 (degrading rather than removing the group when one selected report has canPay === false) change bulk-pay eligibility semantics and warrant dedicated multi-workspace QA, so they are intentionally left out of this PR and called out as follow-ups for the reviewer.

Fixed Issues

$ #93699
PROPOSAL: #93699 (comment)

Tests

  1. Open Search → Reports (Expenses) with reports across one or more workspaces that you can pay.
  2. Select multiple whole reports (use the row checkboxes, not individual transactions) that share the same currency and month.
  3. Verify the bulk action menu offers Pay and Mark as Paid (not only Export).
  4. Repeat with reports spanning multiple currencies and verify Mark as Paid remains available.
  • Verify that no errors appear in the JS console

Offline tests

  • Verify that no errors appear in the JS console

QA Steps

  1. Open Search → Reports (Expenses) with reports across one or more workspaces that you can pay.
  2. Select multiple whole reports (use the row checkboxes, not individual transactions) that share the same currency and month.
  3. Verify the bulk action menu offers Pay and Mark as Paid (not only Export).
  4. Repeat with reports spanning multiple currencies and verify Mark as Paid remains available.
  • 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
  • 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 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 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)
  • 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)
  • 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.
  • 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.

Screenshots/Videos

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

…s Paid options appear for whole-report selections

Co-authored-by: Eugene Voloshchak <eVoloshchak@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team June 19, 2026 16:50
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I created this PR from an upstream branch since I don't have push access to your fork.

To take ownership of this branch and be able to push updates, run:

git remote add upstream https://github.com/Expensify/App.git
git fetch upstream claude-bulkPayMarkAsPaidPolicyIDs
git checkout -b claude-bulkPayMarkAsPaidPolicyIDs upstream/claude-bulkPayMarkAsPaidPolicyIDs
git push -u origin claude-bulkPayMarkAsPaidPolicyIDs

Then you can close this PR and open a new one from your fork.

@codecov

codecov Bot commented Jun 19, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/hooks/useBulkPayOptions.ts 64.86% <100.00%> (+1.35%) ⬆️
src/hooks/useSearchBulkActions.ts 72.17% <100.00%> (+0.33%) ⬆️
src/libs/API/types.ts 100.00% <ø> (ø)
src/components/Search/SearchBulkActionsButton.tsx 90.00% <75.00%> (-0.91%) ⬇️
src/libs/actions/Search.ts 57.96% <77.77%> (+3.76%) ⬆️
... and 13 files with indirect coverage changes

@eVoloshchak

eVoloshchak commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible 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 checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (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 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)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • 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 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.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this 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.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
screen-20260622-185330-1782147197521.mp4
Android: mWeb Chrome
screen-20260622-185204-1782147111599.mp4
iOS: HybridApp
Screen.Recording.2026-06-22.at.18.49.22.mov
iOS: mWeb Safari
Screen.Recording.2026-06-22.at.18.49.51.mov
MacOS: Chrome / Safari
Screen.Recording.2026-08-31.at.10.30.35.mov

@eVoloshchak eVoloshchak left a comment

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.

LGTM!

@eVoloshchak

Copy link
Copy Markdown
Contributor

Validated this writing on all of the platforms using the tests from Tests section.
Screenshots/Videos for all of the platforms are here
@MelvinBot, populate the QA Steps section with Tests, remove the placeholders, check off every checkbox in the PR author checklist and mark this PR as ready for review

@MelvinBot
MelvinBot marked this pull request as ready for review June 22, 2026 16:59
@MelvinBot
MelvinBot requested a review from a team as a code owner June 22, 2026 16:59
@melvin-bot
melvin-bot Bot requested review from eVoloshchak and trjExpensify and removed request for a team June 22, 2026 17:00
@melvin-bot

melvin-bot Bot commented Jun 22, 2026

Copy link
Copy Markdown

@eVoloshchak 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 removed the request for review from a team June 22, 2026 17:00

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

Copy link
Copy Markdown

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: a4fea404a4

ℹ️ 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".

),
],
[selectedTransactions],
() => [...new Set([...Object.values(selectedTransactions).map((transaction) => transaction.policyID), ...selectedReports.map((report) => report.policyID)].filter(Boolean))],

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 Align the selected policy with the selected report

When a whole-report selection spans more than one workspace and the selected transaction entries are missing policyID (the case this patch is fixing), this appends policy IDs in selectedReports order while SearchBulkActionsButton still chooses currentSelectedReportID from selectedTransactionReportIDs. Those arrays are derived from different sources/orderings, so selecting reports in a different order (or numeric transaction-key ordering) can pair one report's iouReport with another workspace's policy for KYC/add-bank-account and restriction checks, routing payment setup to the wrong workspace. Derive the policy from the same report ID used for the current selected report instead of just appending report policies.

Useful? React with 👍 / 👎.

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.

@MelvinBot, make changes according to this review comment

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.

@MelvinBot Could you please check codex's comment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Checked it — Codex is right, and this PR is what made it reachable. Fixed in 9b1f3cb.

The problem. SearchBulkActionsButton.tsx:107-108 picks the policy and the report from two separately ordered and separately filtered arrays:

const currentSelectedPolicyID = selectedPolicyIDs?.at(0);
const currentSelectedReportID = selectedTransactionReportIDs?.at(0) ?? selectedReportIDs?.at(0);

selectedReports is built in display order, Object.values(selectedTransactions) in click order, and the two arrays apply different filters — so index 0 is not a shared anchor. Both values are then consumed together: currentPolicy feeds handleBulkPayItemSelected (:151) → triggerKYCFlow and shouldRestrictUserBillableActions, and currentSelectedPolicyID is the policyID in addBankAccountRoute next to iouReport={selectedIOUReport} (:210-215). Mismatch = KYC / add-bank-account / restricted-action routed to the wrong workspace.

Why this PR made it live. Before, selectedPolicyIDs came from transactions only, so in exactly this scenario (report selection, transaction entries with no policyID) it was [] and currentSelectedPolicyID was undefined — no policy, no misrouting. This PR added ...selectedReports.map((report) => report.policyID), so it now resolves to a real policy that may belong to a different report than currentSelectedReportID.

The fix — anchor the policy to the report actually being acted on, keeping the old value as a last-resort fallback so the bug this PR fixes stays fixed:

const currentSelectedPolicyID =
    selectedIOUReport?.policyID ??
    searchData?.[`${ONYXKEYS.COLLECTION.REPORT}${currentSelectedReportID}`]?.policyID ??
    selectedReports.find((report) => report.reportID === currentSelectedReportID)?.policyID ??
    Object.values(selectedTransactions).find((transaction) => transaction.reportID === currentSelectedReportID)?.policyID ??
    selectedPolicyIDs?.at(0);

The snapshot fallback matters because this component renders outside SearchScopeProvider, so the live Onyx read misses reports that exist only in the search snapshot.

Scope, and two things worth knowing

Blast radius is payment setup, not payment execution. The actual payment loop is already correctly paired — useSearchBulkActions.ts:1357-1400 uses item.policyID and item.reportID from the same item. So the bug decided which workspace's bank-account/KYC screen you landed on, not which reports got paid.

useBulkPayOptions was never affected. payScopedPolicyIDs.at(0) and selectedBulkPayReportID can also mismatch, but selectedPolicyID is only a truthiness guard there (useBulkPayOptions.ts:146) — it's never used to look up a policy or build a route. I checked every reference. So the payScoped* work added earlier in this PR re-scoped the Pay menu only; it did not touch the pairing Codex flagged, which is why the concern survived.

Verification: npm run typecheck passed; eslint on the changed file 0 errors (2 pre-existing seatbelt warnings); react-compiler-compliance-check exit 0; oxfmt produced no changes; 10 bulk-action suites 189/189 passed.

Test gap I did not close: the existing SearchBulkActionsButtonTest.tsx only exercises the all-matching count label through a helper — it never renders the component with Onyx, so there's no scaffolding to assert the policy/report pairing without building it from scratch. I left that rather than bolt on a fragile test. Worth adding if you want a regression guard on this.


view run

@trjExpensify trjExpensify left a comment

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.

@eVoloshchak confirming a couple of things...

On this:

Proposal items 1 (offering a standalone Mark as Paid that is fully decoupled from the native Pay submenu)

... confirming that we are not pulling "Mark as paid" out of the Pay submenu, but just in certain cases we'll still show "Pay > Mark as paid" when it's available and in-app payment is not?

Secondly, related to this slack thread, confirming whether "Select all" will now show this manual bulk pay option, whereas today on "Select all on this page" does?

@eVoloshchak

Copy link
Copy Markdown
Contributor

confirming that we are not pulling "Mark as paid" out of the Pay submenu, but just in certain cases we'll still show "Pay > Mark as paid" when it's available and in-app payment is not?

@trjExpensify, that is correct

Secondly, related to this slack thread, confirming whether "Select all" will now show this manual bulk pay option, whereas today on "Select all on this page" does?

As it stands now, it will not, that's a separate issue. But we can handle it here if it's needed (I suspect additional BE work will be needed, but not 100% sure)

@trjExpensify

Copy link
Copy Markdown
Contributor

@trjExpensify, that is correct

Okay cool, great... good to know. 👍

As it stands now, it will not, that's a separate issue. But we can handle it here if it's needed (I suspect additional BE work will be needed, but not 100% sure)

@luacmartins best to separate this out then, I guess?

@luacmartins

Copy link
Copy Markdown
Contributor

Yea, if we're not addressing that here. It'd require BE changes, so we can look for an external BE contributor to work on this

@trjExpensify

Copy link
Copy Markdown
Contributor

What BE changes would we need, perhaps @allgandalf would be up for helping with that?

@luacmartins

Copy link
Copy Markdown
Contributor

I think we'd need to pass a flag like isPayingAllReports or something like that that would run the query on all matching reports (for the pay query) and then pay them all.

That said, we just removed the capability from the server looping over these reports since it was making the command go to the blocking commit, so I suspect a "pay all" would also lead to stability issues 🤔 We should probably discuss in engineering chat how we wanna do this

@luacmartins

Copy link
Copy Markdown
Contributor

@allgandalf

Copy link
Copy Markdown
Contributor

What BE changes would we need, perhaps @allgandalf would be up for helping with that?

Sure thing! Please assign me to the issue if my help is needed 🙇

@luacmartins
luacmartins requested a review from allgandalf June 29, 2026 18:06
@luacmartins

Copy link
Copy Markdown
Contributor

Thanks @allgandalf. All yours!

@allgandalf

Copy link
Copy Markdown
Contributor

@luacmartins can you summarise the discussions happened on slack, i don't have access to that chat

@luacmartins

Copy link
Copy Markdown
Contributor

Yea, this is the plan:

Follow the pattern in https://github.com/Expensify/Auth/pull/22402
TLDR:

  1. Auth command queries all reports, calls CreateJobs, creating 1 unique job per report to be paid, each spaced out 1-2 seconds
  2. The job is unique per report, calls the appropriate auth command

This way if 1 report out of 100 fails, it doesn't fail the whole batch, and it's easier to retry

@MelvinBot

Copy link
Copy Markdown
Contributor Author

No new changes this time — this request re-fired on the batch I already handled, and the work is already on the branch.

All ten "remove this comment" threads from your last review pass were answered in d8dd7be (nine comments removed, one skipped). I re-verified against origin/claude-bulkPayMarkAsPaidPolicyIDs and the removed text is gone — no second push was needed, so I didn't make one.

What I did do: closed out five older threads of yours that had never gotten a reply.

  • Four "make this comment shorter" threads (lines 719, 1351, 2187, 2211) are moot — those comments were deleted outright, not shortened.
  • The fifth, on src/libs/actions/Search.ts:1955-1958, is the last comment still standing from any of your passes. It's the JSDoc on queueBulkPayReports. I replied there with a proposed one-line version and the option to delete it instead — pick either and I'll push it.

Still open: the thread anchored to useSearchBulkActions.ts lines 595–596 is the one comment I couldn't act on. Its anchor lands on code, not a comment (details here). My guess is the two-liner above payScopedPolicyIDs — confirm and I'll remove it.


view run

@huult

huult commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

@MelvinBot #94075 (review) could you check this comment?

Resolve currentSelectedPolicyID from the report currentSelectedReportID points at
instead of taking the first entry of the independently ordered selectedPolicyIDs
array, so a cross-workspace selection can't route KYC and the add-bank-account
flow to a different workspace than the report being paid.

Co-authored-by: huult <huult@users.noreply.github.com>
@huult

huult commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Screen.Recording.2026-09-03.at.15.12.58.mov
@trjExpensify @luacmartins @allgandalf Could you help review this attachment? Thanks!

@trjExpensify what do you think? thank you 😄

Comment on lines +109 to +112
// Resolve the policy from the report that is actually being paid. selectedPolicyIDs is ordered and filtered
// independently of currentSelectedReportID, so on a selection spanning workspaces its first entry can belong to a
// different workspace than this report, which would send the KYC wall and the add-bank-account flow to the wrong
// workspace. Fall back to the first selected policy only when the report itself yields nothing.

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.

@MelvinBot could you remove this comment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed — pushed in 83b17c0.

Dropped the four lines above currentSelectedPolicyID ("Resolve the policy from the report that is actually being paid…"). Comment-only deletion, no code changed.

Verification
  • npm run typecheck — passed
  • eslint on the changed file — 0 errors (2 pre-existing seatbelt warnings, unchanged)
  • npm run react-compiler-compliance-check — passed for this file
  • oxfmt — ran repo-wide, produced no changes
  • Jest — 61/61 passed across SearchBulkActionsButtonTest, useSearchBulkActionsTest, useSearchBulkActionsPayTest, SearchActionsTest

Co-authored-by: huult <huult@users.noreply.github.com>
* so this covers reports beyond the currently loaded page(s) when "Select all" is checked in Search.
*/
function queueBulkPayReports(jsonQuery: string) {
write(WRITE_COMMANDS.QUEUE_BULK_PAY_REPORTS, {jsonQuery});

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.

@MelvinBot Why are we using the QUEUE_BULK_PAY_REPORTS API? Where did you get this information from?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I didn't pick this command name — it was given to me. allgandalf supplied it and eVoloshchak instructed me to implement against it.

The chain:

  1. allgandalf, Aug 25 — "the command name is QueueBulkPayReports and the param it takes is jsonQuery"
  2. eVoloshchak, Aug 30 — "@MelvinBot, implement this: On Search → Reports (Expenses), show manual bulk pay option when 'Select all' is checked. It should call the new API method, the command name is QueueBulkPayReports and the param it takes is jsonQuery. Make sure to account for pagination, there could be more than one page of selected reports."

That instruction is also why select-all bulk pay is in this PR at all — it's outside the original proposal. eVoloshchak had earlier flagged it as a separate issue "we can handle here if needed", then decided to fold it in.

Backend side, per earlier comments on this PR:

  • luacmartins, Jun 29 described the design — an Auth command queries all matching reports and calls CreateJobs to create one job per report, spaced 1–2s apart, so a single failure doesn't sink the batch.
  • trjExpensify, Jul 28 named the implementing PRs: Expensify/Auth#22681 and Expensify/Web-Expensify#54319.

One thing you should confirm before merging

I have not verified the backend command exists. My access is limited to Expensify/App, so I can't open those two backend PRs or grep for the command — I'm relaying what was stated in this thread, not something I checked. QueueBulkPayReports isn't on App main; this PR adds the client half only. If those backend PRs haven't shipped, select-all Pay will fail at runtime with no client-side signal.

Worth a direct check with allgandalf or eVoloshchak that both have deployed.

Where it lands in this PR

Introduced in c3bb029 ("Add manual bulk pay for Select-all via QueueBulkPayReports"):

It now fires for any "Select all", not only multi-page selections — which resolves the concern eVoloshchak raised on Aug 31 about it being skipped when the selection fits on one page.

@huult huult left a comment

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.

Screen.Recording.2026-09-06.at.10.48.41.mov

LGTM.

@allgandalf

Copy link
Copy Markdown
Contributor

@MelvinBot can you please resolve merge conflicts ? @luacmartins i think the PR is now ready for your review

@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 I resolved and verified the merge locally, but I'm not allowed to push it — see the note at the bottom. The resolution is a two-word change, so it's fast to apply by hand.

The conflict

Merging main (bf4bd3f) into this branch conflicts in one file: src/hooks/useSearchBulkActions.ts. The other five changed files (SearchBulkActionsButton.tsx, libs/API/parameters/index.ts, libs/API/types.ts, libs/actions/Search.ts, tests/unit/SearchActionsTest.ts) all auto-merge cleanly.

It's a pure adjacency collision — the two sides don't overlap semantically:

The resolution — keep both sides

// Reconciliation - All Expenses reports on card spend, so it is offered whenever the current user is a workspace admin or card
// admin of a workspace with a card product enabled, regardless of which rows are selected.
const hasCardEnabledAdminPolicy = useMemo(() => Object.values(policies ?? {}).some((policy) => isAdminOfCardEnabledPolicy(policy)), [policies]);

const selectedBulkCurrency = payScopedReports.at(0)?.currency ?? Object.values(selectedTransactions).at(0)?.currency;
const totalFormattedAmount = getTotalFormattedAmount(convertToDisplayString, payScopedReports, selectedTransactions, selectedBulkCurrency);

Take main's new hasCardEnabledAdminPolicy block verbatim, and keep this PR's selectedReports → payScopedReports swap on the last two lines. That swap is the point of the PR, so it must survive; hasCardEnabledAdminPolicy is unrelated new work from main, so it must survive too. No judgement call beyond that.

Verification I ran on the resolved merge
  • npm run typecheck — passed (all 5 tsconfigs).
  • Jest, 8 suites / 168 tests, all passing: SearchActionsTest, useSearchBulkActions{,Pay,Export,Delete,Duplicate,DownloadPDF,DownloadReceipts}Test. (The act(...) warnings in the output are pre-existing noise.)
  • npm run fmt (oxfmt) — produced zero changes, so formatting is already correct.
  • eslint src/hooks/useSearchBulkActions.ts — 10 @typescript-eslint/no-unsafe-type-assertion errors, which is exactly the count grandfathered for this file in config/eslint/eslint.seatbelt.tsv:479. No new violations, so the ratchet is satisfied.
  • react-compiler-compliance-check — reports errors on this file (babel at lines 449/2052, oxc at 507/739/1805). All of those lines sit outside this PR's diff against main, so they're pre-existing and not a regression. This matches what I found on an earlier pass.
  • import {getConnectedIntegration, isAdminOfCardEnabledPolicy, isSubmitPolicy} from '@libs/PolicyUtils' merged in cleanly, and hasCardEnabledAdminPolicy is consumed in both places main expects it.

I can only make code changes when directed by the PR author, an assignee, or an existing co-author. To have me implement this, the PR assignee or author should reply with a request tagging me.

Concretely: this PR's assignee is eVoloshchak and its existing co-authors are eVoloshchak and huult — a reply from any of them tagging me is enough, and I'll push the merge commit above. Alternatively, add allgandalf to the PR assignees and re-run the request.


view run

@allgandalf allgandalf self-assigned this Sep 8, 2026
@allgandalf

Copy link
Copy Markdown
Contributor

@MelvinBot i'm the assignee now, can you please resolve merge conflicts, thanks
!

…aidPolicyIDs

Resolved one conflict in src/hooks/useSearchBulkActions.ts. main added hasCardEnabledAdminPolicy directly above selectedBulkCurrency, while this branch changed selectedBulkCurrency and totalFormattedAmount to read from payScopedReports instead of selectedReports. Kept both sides.

Co-authored-by: Gandalf <allgandalf@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Merge conflicts resolved — pushed as 0cd2ff9. The PR now reports MERGEABLE. CI is still running on the merge; the only check that has failed so far is Check independent approval, which is a governance gate, not a code failure. Already green on the merge: React Compiler Compliance, Oxfmt check, Storybook tests, Bun tests, knip, Check for unused styles.

Merged main (bf4bd3f) in with git merge — all 14 original commits are untouched, no rebase or squash.

The one conflict

src/hooks/useSearchBulkActions.ts. The other five changed files auto-merged cleanly.

It was a pure adjacency collision, so I kept both sides:

  • main added hasCardEnabledAdminPolicy directly above selectedBulkCurrency — kept verbatim.
  • This PR changed selectedBulkCurrency / totalFormattedAmount to read from payScopedReports instead of selectedReports — kept, since that scoping is the PR.

Net result vs main in that file is unchanged from before the merge (101 changed lines, same hunks), so nothing of yours was dropped.

AI Tests

Check Result
npm run typecheck ✅ passed, all 5 tsconfigs
Jest (13 suites, Search + bulk actions) ✅ 722/722 passed
npm run fmt (oxfmt) ✅ zero changes
eslint on the resolved file ✅ 10 errors = the 10 grandfathered in eslint.seatbelt.tsv:479 — ratchet satisfied, no new violations
react-compiler-compliance-check ✅ no regression — see below
Compiler-compliance baseline comparison

The check fails on this file, but it fails identically on main, so the merge introduces nothing. I confirmed by swapping in main's copy of the file and re-running:

  • merged tree: 10 babel errors, 9 oxc errors
  • main baseline: 10 babel errors, 9 oxc errors

Same counts, same lines (babel 449/2052, oxc 507/739/1805) — all outside this PR's diff.

Suites run

SearchActionsTest, useSearchBulkActions{,Pay,Export,Delete,Duplicate,DownloadPDF,DownloadReceipts}Test, Search/SearchBulkActionsButtonTest, Search/SearchSelectionFooterTest, Search/SearchSelectionProviderTest, Search/SearchUIUtilsTest, Search/ExpenseReportSearchViewTest.

What I did not run locally: the full npm run lint and full npm test sweeps, and the Storybook smoke test — each exceeds the time budget for one run, and against this merge base they'd mostly be re-testing main's own 1,579 commits rather than the six lines I resolved. CI is covering all three on the merge (Storybook already passed). npm run lint-changed silently no-ops in this checkout, so I ran eslint on the resolved file directly and checked it against the seatbelt instead.

I also didn't do a browser pass — your Tests steps need payable reports across multiple workspaces, which a fresh test account doesn't have. Say the word if you want me to attempt one.


view run

@allgandalf

Copy link
Copy Markdown
Contributor

@luacmartins back to you 👍

@luacmartins luacmartins left a comment

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.

LGTM

@luacmartins
luacmartins merged commit d45300f into main Sep 8, 2026
42 of 43 checks passed
@luacmartins
luacmartins deleted the claude-bulkPayMarkAsPaidPolicyIDs branch September 8, 2026 20:37
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

🚧 luacmartins has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

OSBotify commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify OSBotify mentioned this pull request Sep 9, 2026
83 tasks done
@OSBotify

OSBotify commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/luacmartins in version: 9.4.75-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Yes — help site changes are required. Draft PR: #100748

This PR changed bulk Pay behavior in the Reports view in three ways that the help site either didn't cover or actively contradicted.

Change in this PR Help site state before
Select all now offers Pay (new QueueBulkPayReports command pages through every matching report, including unloaded pages) Undocumented — the article only described ticking individual rows
getPayOption no longer requires every selected report to be payable; Pay shows if at least one is, and only payable reports get paid. The held-expense gate narrowed to payable reports only Undocumented
Bulk Pay dropdown option is Mark as paid; the bulk-actions button renders as <count> selected Article said select Selected, then Pay elsewhere — neither matches the UI

All edits landed in docs/articles/new-expensify/wallet-and-payments/Reimbursement-Payment-Methods.md — the only article that documents bulk pay.

How each change maps to the diff, and what I could not verify

Select all → Pay elsewhere only. useSearchBulkActions.ts passes onlyShowPayElsewhere: onlyShowPayElsewhere || areAllMatchingItemsSelected, so a full-match selection can only ever produce the Mark as paid option — direct reimbursement from a business bank account is never offered. The new section documents that constraint and the Select all vs Select all on this page distinction.

Partial selections. getPayOption in Search.ts now gates on payableReports.length > 0 && payableReports.every(...) instead of selectedReports.every(report => report.canPay && ...), and itemsToPay is built from payableSelectedReports. isAnyPayableTransactionOnHold scopes the hold check to reports actually being paid. The new section states that unpayable reports are skipped, that the total reflects only payable reports, and that all payable reports must share a report type.

Label verification. Confirmed against src/languages/en.ts: iou.payElsewhere → Mark as paid (the Pay Elsewhere option's text comes from translate('iou.payElsewhere', '') in SettlementButtonUtils.ts), workspace.common.selected → <count> selected, search.exportAll.selectAllOnThisPage → Select all on this page, and SearchSelectAllMenu.tsx uses workspace.people.selectAll → Select all for the second menu item. Mark as paid also matches the wording already used in the Pay-Expenses article.

Verification limits — please double-check these. The labels above come from the source strings, not the running app. The web tester confirmed the left nav (Spend with Expenses and Reports sub-tabs) but could not reach the bulk-pay UI: the test account has no workspaces and Reports sits behind a Collect-plan upgrade wall, so no report rows existed to select. The Select all / Mark as paid strings and the partial-selection behavior are unverified against a real workspace.

Left alone deliberately. The single-report section of the same article still says Pay elsewhere where the New Expensify dropdown reads Mark as paid. That inaccuracy predates this PR, so I didn't widen the change — flagged in the docs PR body in case you want it fixed there.

Guidelines applied. Both new headings are task-based and searchable, only ## is used, button and tab names are bolded in sentence case, navigation uses the unified web/mobile sentence, and the single screenshot suggestion is a non-rendered <!-- SCREENSHOT: --> comment with a stated purpose per docs/HELP_AUTHORING_GUIDELINES.md §8.


@eVoloshchak, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR Ready for review

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/mountiny in version: 9.4.75-3 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 failure ❌
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/mountiny in version: 9.4.75-3 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 failure ❌
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

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.

9 participants