Skip to content

[$175] Pay is shown when one of the reports contains only nonreimbursable expense during bulk select #104388

Open

Description

@applause-bot

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


Version Number: 9.5.14-0
Reproducible in staging?: Yes
Reproducible in production?: N/A - new feature, doesn't exist in prod
If this was caught during regression testing, add the test name, ID and link from BrowserStack: #100569
Email or phone of affected tester (no customers): josdjojnsosi@gmail.com
Issue reported by: Applause Internal Team
Bug source: Exploratory - Significant User Experience Deterioration
Device used: Mac 26.5 / Chrome
App Component: Search

Action Performed:

Precondition:

  • Go to workspace settings > Workflows and enable Prevent marking non-reimbursable reports as paid
  1. Go to staging.new.expensify.com
  2. Go to workspace chat.
  3. Create a reimbursable expense.
  4. Submit and approve the report.
  5. Create a non-reimbursable expense.
  6. Submit and approve the report.
  7. Go to Spend > Reports.
  8. Bulk select both reports.

Expected Result:

Pay button will not be shown when one of the reports cannot be paid.

Actual Result:

Pay button is shown when one of the reports containing only non-reimbursable expense cannot be paid.

Workaround:

Unknown

Platforms:

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

Screenshots/Videos

Bug7290497_1792432809153.1.mp4

View all open jobs on GitHub

Applause Internal Information
Exported by: Julia Ponikarchuk
Bug ID: 7290497

Upwork Automation - Do Not Edit

Activity

  1. applause-bot commented on Oct 19, 2026

    @applause-bot
    Author

    While Applause doesn't embed attachments for this bug, here are the links for quick view:
    https://platform.applause.com/services/links/v1/external/e08fe4d5977a106d49b391837850fde37fda07d1dd72e80ff2b9d7e168686692

  2. added
    DeployBlockerCashThis issue or pull request should block deployment
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 19, 2026
  3. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    You have been assigned to this deploy blocker because you recently merged this PR: #100569

  4. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    @stitesExpensify @FitseTLT you have been assigned to this deploy blocker because you reviewed the offending PR: #100569

  5. github-actions commented on Oct 19, 2026

    @github-actions
    Contributor

    👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:

    1. Identify the pull request that introduced this issue and revert it.
    2. Find someone who can quickly fix the issue.
    3. Fix the issue yourself.
  6. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    @s77rt you have been assigned to this deploy blocker because you authored the offending PR: #100569

  7. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    💬 A slack conversation has been started in #expensify-open-source

  8. MelvinBot commented on Oct 19, 2026

    @MelvinBot
    Contributor

    Deploy Blocker Analysis

    Verdict: This looks like expected behavior, not a regression from Add workspace setting to control marking non-reimbursable reports as paid. Bulk Pay is designed to show when at least one selected report is payable. It then pays only the payable reports. I recommend demoting or closing this issue after QA runs the two checks below. I don't recommend a revert.

    Why Pay shows: Fix bulk Pay/Mark as Paid disappearing for whole-report selections changed bulk Pay to use only the payable reports instead of hiding Pay. That PR merged on 2026-09-08 and is already in production. getPayOption filters the selection to canPay reports and shows Pay if any remain (src/libs/actions/Search.ts:2423-2438). A unit test covers this behavior (tests/unit/SearchActionsTest.ts:690-699). You can see the same result in production today: select one approved report and one report still waiting for approval.

    QA check: The new PR is only at fault if the non-reimbursable report is still treated as payable. Please confirm both of these:

    1. With both reports selected, the Pay total equals only the reimbursable report's amount. After you pay, the non-reimbursable report stays unpaid.
    2. When you select only the non-reimbursable report, its row shows View and the bulk menu has no Pay option.

    If either check fails, the likely cause is that the search results don't include preventPayoutNonReimbursableReports for the policy. The fix would be a small follow-up PR, not a revert.

    Investigation details

    The new PR adds !(policy?.preventPayoutNonReimbursableReports && hasOnlyNonReimbursableTransactions(...)) to all three pay checks:

    Search rows get their action from canIOUBePaid (src/libs/SearchUIUtils.ts:2219-2248). Each row's canPay comes from that action (src/libs/SearchUIUtils.ts:2355). As a result, the non-reimbursable report should have canPay: false.

    The bulk payment and the displayed total also use only the payable subset (src/hooks/useSearchBulkActions.ts:805-807, src/hooks/useSearchBulkActions.ts:2586-2590). The partial-Pay behavior came from commit e54017a, "Degrade bulk Pay to the payable subset instead of hiding it."

    The issue's expected result assumes every selected report must be payable. That conflicts with the current bulk Pay design. This analysis is based on reading the code. I didn't test it in the app.


    view run

  9. 5 remaining items

  10. s77rt commented on Oct 19, 2026

    @s77rt
    Member

    Let's get this fixed and make sure we don't reintroduce the original issue #93699 (hiding pay buttons when it should not be hidden i.e. when all reports are payable)

  11. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 19, 2026
  12. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    Current assignee @FitseTLT is eligible for the External assigner, not assigning anyone new.

  13. changed the title [-]Pay is shown when one of the reports contains only nonreimbursable expense during bulk select[/-] [+][$175] Pay is shown when one of the reports contains only nonreimbursable expense during bulk select[/+] on Oct 19, 2026
  14. melvin-bot commented on Oct 19, 2026

    @melvin-bot
  15. FitseTLT commented on Oct 19, 2026

    @FitseTLT
    Contributor

    @MelvinBot any proposal you have got

  16. janiya2 commented on Oct 19, 2026

    @janiya2

    Contributor details
    Your Expensify account email: janithad2@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/janithad

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When selecting multiple expense reports in Search/Reports view where one or more selected reports cannot be paid (e.g. contains only non-reimbursable expenses, or is pending approval, or view-only), the Bulk Actions menu still displays the "Pay" option instead of hiding it.

    This creates an inconsistency with the "Approve" bulk action (which requires selectedReports.every(report => report.canApprove)) and allows users to trigger payment flow when not all selected reports are payable.

    What is the root cause of that problem?

    In src/libs/actions/Search.ts, getPayOption filters selectedReports to payableReports = selectedReports.filter((report) => report.canPay) and evaluates:

    const shouldShowBulkPayOption =
        selectedReports.length > 0
            ? payableReports.length > 0 &&
              payableReports.every(
                  (report) =>
                      getSelectedReportType(report) === getSelectedReportType(firstPayableReport) &&
                      shouldShowBulkOptionForRemainingTransactions(selectedTransactions, selectedReportIDs, transactionKeys),
              )
    ...

    Because payableReports.every(...) was introduced in PR #94075 to degrade bulk pay to the payable subset, it only requires at least one report to be payable (payableReports.length > 0) rather than requiring all selected reports to be payable.

    Consequently, when a non-reimbursable (or unpayable) report is selected alongside a payable report, shouldShowBulkPayOption returns true, causing the "Pay" bulk action to appear unexpectedly.

    What changes do you think we should make in order to solve the problem?

    In src/libs/actions/Search.ts, update getPayOption so that for bulk report selection, every selected report must be payable (report.canPay), making it consistent with canApprove and transaction-level selections (every(action === PAY)):

    --- a/src/libs/actions/Search.ts
    +++ b/src/libs/actions/Search.ts
    @@ -2424,14 +2424,15 @@ function getPayOption(
         const firstPayableReport = payableReports.at(0);
         const getSelectedReportType = (report: SelectedReports | undefined) => report?.type ?? getReportType(report);
         const hasLastPaymentMethod =
             selectedReports.length > 0
    -            ? payableReports.every((report) => !!getLastPolicyPaymentMethod(report.policyID, personalPolicyID, lastPaymentMethods))
    +            ? selectedReports.every((report) => !!getLastPolicyPaymentMethod(report.policyID, personalPolicyID, lastPaymentMethods))
                 : transactionKeys.every((transactionIDKey) => !!getLastPolicyPaymentMethod(selectedTransactions[transactionIDKey].policyID, personalPolicyID, lastPaymentMethods));
     
         const shouldShowBulkPayOption =
             selectedReports.length > 0
    -            ? payableReports.length > 0 &&
    -              payableReports.every(
    +            ? selectedReports.every(
                       (report) =>
    +                      report.canPay &&
                           getSelectedReportType(report) === getSelectedReportType(firstPayableReport) &&
                           shouldShowBulkOptionForRemainingTransactions(selectedTransactions, selectedReportIDs, transactionKeys),
                   )

    Also, update the unit test in tests/unit/SearchActionsTest.ts to assert that mixing payable and non-payable reports disables the bulk pay button:

    --- a/tests/unit/SearchActionsTest.ts
    +++ b/tests/unit/SearchActionsTest.ts
    @@ -687,13 +687,9 @@ describe('Search - getPayOption', () => {
             expect(getShouldEnableBulkPayOption([makeSelectedReport('1', true), makeSelectedReport('2', true)])).toBe(true);
         });
     
    -    it('keeps bulk pay enabled when only some of the selected reports are payable', () => {
    -        const shouldEnableBulkPayOption = getShouldEnableBulkPayOption([makeSelectedReport('1', true), makeSelectedReport('2', true), makeSelectedReport('3', false)]);
    -        expect(shouldEnableBulkPayOption).toBe(true);
    -    });
    +    it('disables bulk pay when only some of the selected reports are payable', () => {
    +        const shouldEnableBulkPayOption = getShouldEnableBulkPayOption([makeSelectedReport('1', true), makeSelectedReport('2', true), makeSelectedReport('3', false)]);
    +        expect(shouldEnableBulkPayOption).toBe(false);
         });

    This ensures:

    1. When all selected reports are payable, the Pay button is shown (maintaining the fix from [Due for payment 2026-09-17] [$250] Bulk Pay / Mark as Paid options missing when selecting multiple reports (across workspaces/currencies) #93699).
    2. When any selected report cannot be paid (such as a report with only non-reimbursable expenses), the Pay option is hidden.
    3. Behavior is strictly aligned with the "Approve" bulk action (selectedReports.every(report => report.canApprove)).

    What alternative solutions did you consider?

    Handling the condition inside useSearchBulkActions.ts by checking selectedReports.every(report => report.canPay) before rendering payButtonOption. However, keeping the validation centralized inside getPayOption in Search.ts is cleaner, adheres to existing architecture, and directly protects all callers and unit tests.

  17. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  18. FitseTLT commented on Oct 19, 2026

    @FitseTLT
    Contributor

    @janiya2 do not post proposal before help wanted label is applied

  19. MelvinBot commented on Oct 19, 2026

    @MelvinBot
    Contributor

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When you bulk select reports in Search and at least one can't be paid, the Pay option still shows. It then pays only the payable reports. Approve works differently: it shows only when every selected report can be approved.

    What is the root cause of that problem?

    In Fix bulk Pay/Mark as Paid disappearing for whole-report selections, commit e54017a ("Degrade bulk Pay to the payable subset instead of hiding it") changed getPayOption. It now filters the selection down to canPay reports and shows Pay if any report remains. It no longer requires every selected report to be payable (src/libs/actions/Search.ts:2423-2438).

    That commit wasn't needed to fix [Due for payment 2026-09-17] [$250] Bulk Pay / Mark as Paid options missing when selecting multiple reports (across workspaces/currencies). The same PR fixed that issue by deriving selectedPolicyIDs from the selected reports (src/hooks/useSearchBulkActions.ts:824-827). It also made two other fixes: it reads the report type from the search snapshot, and it lets Mark as Paid show without a report or policy ID.

    What changes do you think we should make in order to solve the problem?

    1. In getPayOption, restore the all-or-nothing check for whole-report selections. Show Pay only if every selected report has canPay, all reports share the first report's type, and shouldShowBulkOptionForRemainingTransactions passes. Compute hasLastPaymentMethod over all selected reports, not just the payable ones. Keep getSelectedReportType so the snapshot fix stays.
    2. In useSearchBulkActions, gate Pay on isAnyTransactionOnHold instead of isAnyPayableTransactionOnHold, the same way Approve does (src/hooks/useSearchBulkActions.ts:2590 compared with src/hooks/useSearchBulkActions.ts:2733-2740). After that, remove the unused isAnyPayableTransactionOnHold.
    3. Update the unit tests:
      • Flip "keeps bulk pay enabled when only some of the selected reports are payable" (tests/unit/SearchActionsTest.ts:690) so it expects Pay to be hidden.
      • Add a test for a payable report plus an unapproved report, which should also hide Pay.
      • Keep the existing test that expects Pay to show when all selected reports are payable, so the bug from that earlier issue doesn't come back.

    The payableSelectedReports filter in useSearchBulkActions can stay. Once every selected report must be payable, that filter has no effect on manual selections. It's still useful for Select all.

    What alternative solutions did you explore? (Optional)

    You could also apply the all-or-nothing rule to Select all, which currently shows Pay if any loaded report is payable (src/hooks/useSearchBulkActions.ts:2588-2589). I left it out because this issue covers manual selection only, and that Select all behavior was a deliberate part of the earlier fix. s77rt and FitseTLT, let me know if you want it included.

    The core change in step 1 matches the earlier proposal from janiya2. This proposal adds the hold-check alignment and states which earlier fixes to keep. I haven't tested it in the app.


    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/104388) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run

  20. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    ⚠️ Looks like this issue was linked to a Deploy Blocker here

    If you are the assigned CME please investigate whether the linked PR caused a regression and leave a comment with the results.

    If a regression has occurred and you are the assigned CM follow the instructions here.

    If this regression could have been avoided please consider also proposing a recommendation to the PR checklist so that we can avoid it in the future.

  21. stitesExpensify commented on Oct 22, 2026

    @stitesExpensify
    Contributor

    @allgandalf are you taking this one on since you authored #94075 ?

    #104388 (comment)

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

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2EngineeringExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions