Skip to content

Show Mark as paid on approved reports with nothing to reimburse - #102347

Merged
mollfpr merged 12 commits into
Expensify:mainfrom
apeyada:fix-102215
Oct 8, 2026
Merged

mollfpr merged 12 commits into
Expensify:mainfrom
apeyada:fix-102215

Conversation

@apeyada

@apeyada apeyada commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

An approved expense report with nothing left to reimburse had no Pay button anywhere in New Expensify, so it couldn't be closed out. Examples are a $50 and a -$50 expense, a single $0 expense, or a $0 report with only non-reimbursable expenses. Classic offers Mark as paid for these reports.

Pay is controlled by three checks, and each required non-zero reimbursable spend:

  • isPrimaryPayAction: report header and More menu
  • ReportPreviewActionUtils.canPay: chat report preview
  • canIOUBePaid: whether Pay can be used, plus the LHN badge and the Search row

This PR adds hasSettledZeroReimbursableSpend to ReportUtils and uses it in all three checks. It returns true only when all of these are true:

  • it is an expense report with zero reimbursable spend
  • the report total is loaded and not pending
  • the report has at least one expense that isn't pending delete
  • no expense is still scanning, failed to scan, or is a pending Expensify Card charge

canIOUBePaid only uses it for pay-elsewhere, so these reports get Mark as paid and never a real payment.

Nothing is owed on these reports, so they follow the existing non-reimbursable-only behavior: paying is optional and they aren't pushed to the payer. A new ReportUtils.isPayOptional helper is true when every expense is non-reimbursable or the reimbursable spend is settled at $0. It replaces hasOnlyNonReimbursableTransactions in the three places that already skipped non-reimbursable-only reports:

  • the LHN PAY badge (getBadgeFromIOUReport)
  • the Reports row's primary action (SearchUIUtils), which falls back to View
  • the Ready to pay to-do (TodosUtils)

The Submit button's "expense has no final value yet" condition is moved into TransactionUtils.isExpenseValueUnsettled. isTransactionSubmittable now calls it, so the Submit and Pay rules stay in sync.

$0 reimbursable spend (what this PR changes)

LHN badge, Reports row and Ready to pay are the same in prod and this PR, except where noted.

# Case Header & preview (prod) Header & preview (this PR) LHN badge Reports row Todo: Ready to pay
1 Reimbursable expenses cancel out (+$50 / -$50), total $0 No Pay Pay $0.00 > Mark as paid ❌ View ❌
2 One $0.00 reimbursable expense No Pay Pay $0.00 > Mark as paid ❌ View ❌
3 Reimbursable expenses cancel out, plus a $100 non-reimbursable expense (total $100) No Pay Pay > Mark as paid ❌ View ❌
4 Mixed: +$50 reimbursable and -$50 non-reimbursable (total $0; matches Classic) No Pay Pay $0.00 > Mark as paid ❌ View ❌
5 Non-reimbursable expenses cancel out, or one $0.00 non-reimbursable expense No Pay Pay $0.00 > Mark as paid ❌ View ❌
6 Any $0 case above that also has a scanning or failed-scan expense No Pay No Pay ❌ View ❌
7 #1–#5 on a closed (Done) report, with payments turned on after it closed No Pay Pay $0.00 > Mark as paid ❌ View ❌

Non-zero reimbursable spend (this PR doesn't change these)

# Case LHN badge (prod) LHN badge (this PR) Reports row (prod) Reports row (this PR) Todo: Ready to pay (prod) Todo: Ready to pay (this PR)
8 Negative reimbursable total ✅ ✅ Pay Pay ✅ ✅
9 Only non-reimbursable expenses, non-zero total ❌ ❌ View View ❌ ❌
10 Some, but not all, expenses held ✅ ✅ View View ✅ ✅
11 Accounting export failed ✅ ✅ Pay Pay ❌ ❌
12 Admin who isn't the reimburser, manual reimbursement ❌ ❌ Pay Pay ❌ ❌
13 Closed, or submitted with approvals off, and still waiting on a bank account ❌ ❌ View View ✅ ✅
14 One-expense report whose expense is paid at the end (pay-at-end) ❌ ❌ View View ✅ ✅

Fixed Issues

$ #102215
PROPOSAL: #102215 (comment)

Tests

Preconditions

  • A Collect or Control workspace with Add approvals on and Make or track payments on. Connect a business bank account if you can, so real payment options appear.
  • Account A is a member who submits expenses. Account B is the workspace admin, approver and payer.
  • "Reimbursable off" means turning off the Reimbursable toggle on a cash expense, which makes it non-reimbursable.

How to run each test: as A, create the report described and submit it. As B, approve it. Then as B, check these places:

  • the report header
  • the report preview in the workspace chat
  • the report's row in Reports > All reports on a wide screen
  • the report in the LHN (PAY badge)
  • Reports > To-do > Ready to pay

The test lists what each place should show. Where it says Mark as paid, also click Pay > Mark as paid once and verify the report moves to Paid.

1. Positive reimbursable total (should not change)

  • Report: one $50 reimbursable expense.
  • Verify: Pay $50.00 with the usual payment options in the header, preview and Reports row. The report shows a PAY badge in the LHN and is listed in Ready to pay.

2. Negative reimbursable total (should not change)

  • Report: one -$50 reimbursable expense.
  • Verify: only Mark as paid in the header, preview and Reports row. The report shows a PAY badge in the LHN and is listed in Ready to pay.

3. Reimbursable expenses cancel out, report total $0 (the reported bug)

  • Report: a $50 and a -$50 expense, both reimbursable.
  • Verify: Pay $0.00, whose only option is Mark as paid, in the header and preview. The Reports row shows View, there's no PAY badge in the LHN, and the report isn't listed in Ready to pay.

4. A single $0 reimbursable expense

  • Report: one $0.00 reimbursable expense with a merchant.
  • Verify: same as test 3.

5. Positive reimbursable plus non-reimbursable spend (should not change)

  • Report: a $50 reimbursable expense and a $30 expense with Reimbursable off.
  • Verify: Pay for the $50 reimbursable amount, with the usual payment options.

6. Negative reimbursable plus non-reimbursable spend (should not change)

  • Report: a -$50 reimbursable expense and a $30 expense with Reimbursable off.
  • Verify: only Mark as paid.

7. Reimbursable expenses cancel out, non-reimbursable spend remains

  • Report: a $50 and a -$50 reimbursable expense, plus a $100 expense with Reimbursable off.
  • Verify: the report total is $100.00, and Pay is available in the header and preview with Mark as paid as its only option. The Reports row shows View, there's no PAY badge in the LHN, and the report isn't listed in Ready to pay.

8. Reimbursable expense offset by a non-reimbursable credit, report total $0

  • Report: a $50 reimbursable expense and a -$50 expense with Reimbursable off.
  • Verify: only Mark as paid in the header and preview, and never a real payment. This matches Expensify Classic. The Reports row shows View, there's no PAY badge in the LHN, and the report isn't listed in Ready to pay.

9. Only non-reimbursable spend (should not change)

  • Report: one $50 expense with Reimbursable off.
  • Verify: Pay > Mark as paid is available in the header and preview. The Reports row shows View, there's no PAY badge in the LHN, and the report isn't listed in Ready to pay.

10. Non-reimbursable expenses cancel out, report total $0

  • Report: a $50 and a -$50 expense, both with Reimbursable off.
  • Verify: same as test 9.

11. An expense still scanning or failed to scan

  • Report: a $50 and a -$50 reimbursable expense, plus a scanned receipt that SmartScan hasn't finished or that failed and has no amount.

  • Verify: no Pay button in the header, preview or Reports row, no PAY badge in the LHN, and the report isn't listed in Ready to pay.

  • Verify that no errors appear in the JS console

Offline tests

Same as Tests

QA Steps

Same as Tests

  • 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
case1.mov
case2.mov
case3.mov
case5.mov
case6.mov
case7.mov
case8.mov
case9.mov
case10.mov

@codecov

codecov Bot commented Sep 26, 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/useSearchBulkActions.ts 79.94% <100.00%> (+0.04%) ⬆️
src/libs/ReportPrimaryActionUtils.ts 95.10% <100.00%> (+0.06%) ⬆️
src/libs/ReportUtils.ts 88.05% <100.00%> (+0.04%) ⬆️
src/libs/TodosUtils.ts 97.14% <100.00%> (ø)
src/libs/TransactionUtils/index.ts 95.18% <100.00%> (+0.01%) ⬆️
src/libs/TransactionUtils/violations.ts 96.50% <100.00%> (ø)
src/libs/actions/IOU/ReportWorkflow.ts 82.98% <100.00%> (-1.80%) ⬇️
src/libs/ReportPreviewActionUtils.ts 90.98% <80.00%> (-0.62%) ⬇️
src/libs/SearchUIUtils.ts 72.45% <50.00%> (ø)
... and 35 files with indirect coverage changes

@apeyada
apeyada marked this pull request as ready for review September 26, 2026 22:25
@apeyada
apeyada requested review from a team as code owners September 26, 2026 22:25
@melvin-bot
melvin-bot Bot requested review from joekaufmanexpensify and mkhutornyi and removed request for a team September 26, 2026 22:25
@melvin-bot

melvin-bot Bot commented Sep 26, 2026

Copy link
Copy Markdown

@mkhutornyi 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]

@MelvinBot

Copy link
Copy Markdown
Contributor

Review

The header, report preview, and More menu look correct. For the new $0 cases they show only Mark as paid. I found one bug in the Search row and one smaller gap.

1. Search row can offer real bank payment on a $0 report (should fix)

The Search row decides to show Pay using one set of transactions, then picks the payment options using another:

  • SearchUIUtils.ts:2177 uses the search snapshot's transactions. So hasSettledZeroReimbursableSpend returns true and the row gets Pay.
  • PayActionCell.tsx:48 reads transactions from live Onyx. If the payer never opened the report on this device, that list is []. [] doesn't fall back to Onyx, so the helper returns false and shouldOnlyShowElsewhere becomes false (L80-82).
  • With onlyShowPayElsewhere=false, SettlementButton lists business bank accounts (SettlementButton/index.tsx:277-348).
  • The guard change to amount === undefined (L87) removes the only client check that dropped a $0 row payment. A VBBA PayMoneyRequest for $0 can now be sent.

To reproduce: as the payer, on a fresh session, go straight to Reports > All reports without opening the $50/-$50 report. The test steps approve the report first, which loads its transactions into Onyx, so they won't catch this.

Suggested fix: pass the row's snapshot transactions into PayActionCell's canIOUBePaid calls. As a backstop, keep refusing non-ELSEWHERE payment types when amount === 0.

Bulk pay has the same mismatch. useSearchBulkActions.ts:783-806 passes undefined transactions (so it uses the Onyx fallback), while the list of payable reports comes from the snapshot.

2. Scanning check depends on iouRequestType (minor)

ReportUtils.ts:4808 passes isScanningTransaction. That function only matches when iouRequestType === 'scan' (TransactionUtils/index.ts:452). If a scanning receipt arrives without that field, the report counts as settled and shows Mark as paid before the amount is known. The default isScanning checks the receipt state directly and would be safer. The tests cover only SCAN_FAILED, not SCANREADY or SCANNING.

Checked and working
  • Header, preview, More menu, and selection mode compute the Pay decision and the options from the same transactions. They show only Mark as paid, labeled "Pay $0.00".
  • The LHN PAY badge is suppressed for non-reimbursable-only reports (tests 8 and 9). It shows for the $50/-$50 case, which matches how negative reports already behave.
  • The isTransactionSubmittable and hasOnlyNonReimbursableTransactions refactors don't change behavior.
  • Reports with only pending-delete expenses, or a pending total, fail closed.
  • Test 7 ($50 reimbursable, -$50 non-reimbursable) uses the new helper. getMoneyRequestSpendBreakdown returns all zeros when the report total is 0.

@MelvinBot

Copy link
Copy Markdown
Contributor

Regressions

I found three regressions from existing behavior. All three affect approved or closed expense reports with $0 reimbursable spend, which never offered Pay before this PR.

1. Pay replaces Export as the one-click action (should fix or get sign-off)

On a policy with an accounting connection, a $0 report used to show Export. It now shows Pay, because Pay is checked before Export:

To reproduce: on a QBO or Xero policy with auto-sync off, approve a $50 / -$50 report as an admin who is also the payer.

2. Bulk pay drops to Mark as paid only

In Reports, select a $100 report that can be paid by bank plus any $0 report. Before this PR, the $0 report wasn't payable, so all bank options showed. Now .some(...) at useSearchBulkActions.ts:775-806 sees the $0 report as pay-elsewhere only. Only Mark as paid is then offered for both reports, so the $100 can't be paid by bank. This happens when the $0 report's transactions are loaded in Onyx. When they aren't, you get the bank-options bug from my earlier review instead.

3. It reverses a previous deploy-blocker fix

The two tests this PR flips were added by [cp stag] fix: Hide pay button for non-reimbursable transactions report with 0 amount. That PR fixed [Due for payment 2026-04-14] [Due for payment 2026-04-14] Reports- Nothing happens after clicking bank account when paying non-reimbursable 0.00 expense. The header and preview avoid that symptom now because they offer only Mark as paid. The Search row mismatch from my earlier review brings it back: bank options on a $0 report, and a $0 VBBA payment is sent now that PayActionCell no longer drops amount: 0. Please get sign-off from that PR's reviewers before reversing it.

Also changed (likely intended, worth confirming)
  • LHN and to-dos: $0 reports with cancelling cash expenses now show a PAY badge, a green dot, and a Pay to-do (ReportWorkflow.ts:361-368, TodosUtils.ts:150-163). This includes existing unpaid $0 reports, and Submit & Close reports that already read "Done".
  • Card expenses plus a $0 cash expense: these reports now get Pay, a badge, and a to-do. Before, Pay was blocked because not every expense was non-reimbursable.
  • Unchanged: open reports, submitted reports on policies with approvals, IOUs, invoices, real bank payment checks, and the Unapprove, Cancel payment, Reopen, and Mark as exported actions.

I found these by tracing the code against the PR's parent commit. I did not run the app, because these flows need an accounting connection and a business bank account.

@MelvinBot

Copy link
Copy Markdown
Contributor

Review of the latest commit (7aa261c)

The new isPayOptional fixes the LHN badge, the Pay to-do and the Reports row. A $0 report no longer shows Pay as the row's button, so the bank-options bug from my first review no longer happens in Reports. Four items still need attention.

1. The Expenses view still shows Pay with bank options on a single $0 expense (should fix)

The PAY exclusion was added only for report rows. Expense rows still pick their button without it (SearchUIUtils.ts:1929 and L2944).

Scenario: an approved report with a single $0 expense, viewed in the Expenses tab.

  1. getActions passes [transaction] to canIOUBePaid, so hasSettledZeroReimbursableSpend returns true and the row shows Pay.
  2. PayActionCell.tsx:48 reads transactions from Onyx. If the report was never opened, that list is [], so onlyShowPayElsewhere is false and business bank accounts are offered.
  3. amount is getReimbursableTotal(report) = 0 (TransactionItemRowWide.tsx:372). The guard now allows 0 (PayActionCell.tsx:87), so a $0 bank payment is sent.

Suggested fix: apply the same isPayOptional PAY exclusion to expense rows. As a backstop, in confirmPayment, drop any payment type other than ELSEWHERE when amount === 0.

2. Bulk pay is unchanged

useSearchBulkActions.ts:783-806 still decides "Mark as paid only" from Onyx transactions (undefined). The list of payable reports comes from the search results instead. The two problems from the earlier comments still apply:

  • Selecting a $100 report plus a $0 report offers only Mark as paid for both, when the $0 report's transactions are loaded.
  • The same selection offers bank options for the $0 report when they aren't loaded.

3. Pay still replaces Export in the header and the preview

The commit fixed this for the Reports row only. On a policy with an accounting connection, the header (ReportPrimaryActionUtils.ts:570-588) and the chat preview (ReportPreviewActionUtils.ts:291-295) check Pay before Export. The preview has no More menu, so Export can't be reached there. Please check with product whether Pay or Export should come first for these $0 reports.

4. Scanning check depends on iouRequestType (minor, unchanged)

ReportUtils.ts:4808 still passes isScanningTransaction, which only matches scans marked with iouRequestType === 'scan'. The default isScanning checks the receipt state directly and would be safer.

Checked in the new commit
  • The LHN badge (getBadgeFromIOUReport), the Pay to-do, and the Search report-row button all use isPayOptional, so they now agree.
  • isPayOptional(iouReport) in the badge path falls back to Onyx transactions, the same as the canIOUBePaid call beside it, so the two stay consistent.
  • The new isPayOptional unit tests cover cancelling expenses, only non-reimbursable expenses, reimbursable spend that is owed, and a report with no expenses.

I traced these in the code and did not run the app.

@MelvinBot

Copy link
Copy Markdown
Contributor

Regressions at 7aa261c

I found two new regressions beyond my earlier comments. All four earlier items (the Expenses-tab row, bulk pay, Pay ahead of Export, and the scanning check) still apply at this commit.

1. Expenses tab now sends $0 bank payments for existing non-reimbursable reports (should fix)

This affects reports that worked the same way before this PR, for example a report with one $50 company-card expense.

  • Before: the Expenses-tab row already showed Pay, because expense rows skip the Pay exclusion (SearchUIUtils.ts:2311). The row passes amount={getReimbursableTotal(report)} (TransactionItemRowWide.tsx:372), which is 0 for a non-reimbursable report. The old !amount guard dropped every click.
  • Now: the amount === undefined guard (PayActionCell.tsx:87) never fires for expense rows, so the click goes through. If the report's transactions aren't in Onyx, the menu includes business bank accounts, and a VBBA PayMoneyRequest for $0 is sent.

This is the flow from Reports- Nothing happens after clicking bank account when paying non-reimbursable 0.00 expense. The difference is that it now sends a real request instead of doing nothing.

Suggested fix: in confirmPayment, drop any payment type other than ELSEWHERE when amount === 0. That also covers the $0 expense-row case from my last review.

2. Test 7 records $50 as paid while the button says $0.00

Test 7 has a $50 reimbursable expense and a -$50 non-reimbursable credit, so the report total is $0. getMoneyRequestSpendBreakdown returns all zeros whenever the report total is 0 (ReportUtils.ts:4767-4790). The new helper therefore treats the report as having nothing to reimburse, and the button offers Pay $0.00 > Mark as paid.

Clicking it uses getReimbursableTotal instead (PayMoneyRequest.ts:261). The optimistic PAY action and the API amount (L518) both use $50. The employee's $50 is recorded as "paid elsewhere" while the UI said $0.00.

The PR says Mark as paid is intended for this case. Please confirm that recording $50 is also intended. If it isn't, check getReimbursableTotal(report) === 0 in the helper instead of the zeroed breakdown. Current tests cover only two reimbursable expenses with nonReimbursableTotal: 0.

Lower severity
  • Closed and no-approval reports also get Pay. isReportFinished includes closed reports and reports submitted with approvals off (ReportPrimaryActionUtils.ts:260). So existing closed $0 reports, including Submit & Close ones already marked Done, now show Pay in the header and preview.
  • Closed $0 report rows in Reports. They no longer return DONE early (SearchUIUtils.ts:2221-2223). The row shows View, and the report counts as payable in bulk pay. The existing "Done for a closed report with a zero balance" test still passes only because its fixture has no transactions.
  • All-held $0 reports. The header now shows Remove hold (ReportPrimaryActionUtils.ts:553) where it used to show Export.
  • Unverified: a $0 invoice row can now call payInvoice for $0. I didn't confirm that a $0 invoice can be created.
Checked and unchanged
  • For reports that don't total $0, isPayOptional never hides a badge, Search row Pay, or Pay to-do that main showed.
  • Callers that don't pass onlyShowPayElsewhere=true are unchanged, and so are IOUs.
  • Reports with totals missing from Search, reports with no expenses, and expenses pending delete all stay without Pay.

I traced these in the code at 7aa261c. I did not run the app or the unit tests.

@apeyada

apeyada commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor Author

While testing, I found multiple bugs happening on production. If needed, I can help to fix these separately as followup.

Bug 1. Expenses tab: Pay does nothing on a non-reimbursable-only expense

  1. Create a $50 expense with Reimbursable off.
  2. Submit and approve it.
  3. In Spend > Expenses, turn on the Action column in the column settings.
  4. In Spend > Expenses, the expense row shows Pay button. Click Pay > Mark as paid.

Expected: the report is marked as paid, or the row shows View like its Reports row does
Actual: nothing happens

bug1.mov

Bug 2. Bulk pay shows bank options when an non-reimbursable $0 expense is selected

  1. Connect a business bank account.
  2. Create, submit and approve a $50 reimbursable expense.
  3. Create, submit and approve a $0 non-reimbursable expense.
  4. In Spend > Reports, select both reports and open the bulk Pay menu

Expected: No bank option
Actual: Bank option shows despite non-reimbursable expense is not payable

bug2.mov

Bug 3. Report breakdown hidden when a reimbursable and a non-reimbursable expense cancel out

  1. Create a report with a $50 reimbursable expense and a -$50 expense with Reimbursable off, then open it.

Expected: Reimbursable $50.00 and Non-reimbursable -$50.00 breakdown shows
Actual: Total $0.00, with no breakdown

bug3.mov

Bug4. Green dot 🟢 shows when a reimbursable and a non-reimbursable expense cancel out

  1. Create a report with a $50 reimbursable expense and a -$50 expense with Reimbursable off.
  2. Submit and approve
  3. Observer LHN badge

Expected: Either show Pay button or hide 🟢 (depending on product decision)
Actual: 🟢 shows

bug4.mov

@mkhutornyi

mkhutornyi commented Sep 28, 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
Android: mWeb Chrome
iOS: HybridApp
iOS: mWeb Safari
MacOS: Chrome / Safari
web1.mov
web2.mov
web3.mov

@joekaufmanexpensify joekaufmanexpensify 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.

Good for product. I like that we are reusing the existing pattern from paying non-reimbursable expenses where we do not show the pay action on the spend page.

@mkhutornyi

Copy link
Copy Markdown
Contributor

@apeyada Please fix conflict

@apeyada

apeyada commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Conflicts fixed

@mkhutornyi

Copy link
Copy Markdown
Contributor

Re: #102347 (comment)

Should we create followup issues for them separately?
cc: @mollfpr

@melvin-bot
melvin-bot Bot requested a review from mollfpr September 28, 2026 19:09
mollfpr
mollfpr previously approved these changes Oct 5, 2026

@mollfpr mollfpr 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.

Thanks @apeyada! Approving this! I'll create follow up issues for:

  1. Expenses tab: Pay does nothing on a non-reimbursable-only expense
  2. Bulk pay shows bank options when an non-reimbursable $0 expense is selected
  3. Report breakdown hidden when a reimbursable and a non-reimbursable expense cancel out

@apeyada let me know if the conflict is resolved!

@apeyada

apeyada commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

tests failing on main. I will merge main again once it's fixed

@apeyada

apeyada commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

@apeyada let me know if the conflict is resolved!

conflict is resolved. thanks

@mollfpr

mollfpr commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

@apeyada Quick update on bug 2 (bulk pay showing bank options for a $0 non-reimbursable expense). We're going to fix it in this PR: when a $0 non-reimbursable report is selected together with a payable one, the bank option will be hidden for the whole selection, same as what already happens today when the non-reimbursable report isn't $0.

We'll open a follow-up issue separately for better, per-report handling of this case instead of hiding the bank option for the whole selection, so look out for that one too.

For bug 1 and the report-level bulk action behavior, we'll also handle those as follow-up issues.

@apeyada

apeyada commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

@apeyada Quick update on bug 2 (bulk pay showing bank options for a $0 non-reimbursable expense). We're going to fix it in this PR: when a $0 non-reimbursable report is selected together with a payable one, the bank option will be hidden for the whole selection, same as what already happens today when the non-reimbursable report isn't $0.

I fixed it now. Thanks

this branch:

this.branch.mp4

production:

production has bug of showing bank option when bulk pay includes non-reimbursable-only (either zero or non-zero) expense

production.mov

@mkhutornyi mkhutornyi 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

@melvin-bot
melvin-bot Bot requested a review from mollfpr October 7, 2026 22:59

@mollfpr mollfpr 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! Thanks @apeyada!

@mollfpr
mollfpr merged commit 07d3cc0 into Expensify:main Oct 8, 2026
35 of 37 checks passed
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

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

@OSBotify

OSBotify commented Oct 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 commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/mollfpr in version: 9.5.6-0 🚀

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

@MelvinBot

Copy link
Copy Markdown
Contributor

No help site update is needed. This bug fix lets admins close approved $0 reports with Pay > Mark as paid, which is what users already expect, and no article says these reports can't be paid.

What I checked
  • The closest sentence is in Pay-Expenses.md:134: "Reports containing only non-reimbursable expenses (e.g., company card charges) cannot be paid via ACH through Expensify. Instead, select Mark as paid from the Pay button dropdown to close the report." It is still correct.
  • Reimbursement-Payment-Methods.md says non-reimbursable-only reports offer only Pay elsewhere. It is also still correct.
  • The PR also keeps $0 reports out of the PAY badge, the Search row's Pay action, and the Ready to pay to-do. These follow existing non-reimbursable behavior, and no article describes them.

@IuliiaHerets

Copy link
Copy Markdown

Hi, @mkhutornyi. Could you please share more detailed steps?

cc @mollfpr @joekaufmanexpensify

@mkhutornyi

Copy link
Copy Markdown
Contributor

@IuliiaHerets can you pleas take example where any step is unclear?

@joekaufmanexpensify

joekaufmanexpensify commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

@IuliiaHerets here is a rewritten version with more defined steps to follow:

QA Steps

Accounts used in every test

  • Account A: workspace member who submits expenses.
  • Account B: workspace admin, approver and payer.
  • Workspace: Collect or Control, with Add approvals and Make or track payments on.
  • "Reimbursable off" means turning off the Reimbursable toggle on a cash expense.

Where to check (as Account B, after A submits and B approves)

  • Header: the report header.
  • Preview: the report preview in the workspace chat.
  • Row: the report's row in Reports > All reports (wide screen).
  • LHN: the report in the LHN (PAY badge).
  • To-do: Reports > To-do > Ready to pay.

1. One positive reimbursable expense (no change expected)

Prereq: business bank account connected to the workspace.

  1. As A, create a report with one $50 reimbursable expense and submit it.
  2. As B, approve the report.
  3. Verify:
    • Verify Header, Preview and Row show Pay $50.00 with payment options including the business bank account.
    • Verify LHN shows a PAY badge.
    • Verify To-do lists the report.

2. One negative reimbursable expense (no change expected)

Prereq: none.

  1. As A, create a report with one -$50 reimbursable expense and submit it.
  2. As B, approve the report.
  3. Verify:
    • Verify Header, Preview and Row offer only Mark as paid.
    • Verify LHN shows a PAY badge.
    • Verify To-do lists the report.
  4. In the header, click Pay > Mark as paid.
  5. Verify the report moves to Paid.

3. Reimbursable expenses cancel out, total $0 (the reported bug)

Prereq: none.

  1. As A, create a report with a $50 and a -$50 expense, both reimbursable. Submit it.
  2. As B, approve the report.
  3. Verify:
    • Verify Header and Preview show Pay $0.00 with Mark as paid as the only option.
    • Verify Row shows View.
    • Verify LHN has no PAY badge.
    • Verify To-do does not list the report.
  4. In the header, click Pay > Mark as paid.
  5. Verify the report moves to Paid.

4. One $0 reimbursable expense

Prereq: none.

  1. As A, create a report with one $0.00 reimbursable expense that has a merchant. Submit it.
  2. As B, approve the report.
  3. Verify:
    • Verify Header and Preview show Pay $0.00 with Mark as paid as the only option.
    • Verify Row shows View.
    • Verify LHN has no PAY badge.
    • Verify To-do does not list the report.
  4. In the header, click Pay > Mark as paid.
  5. Verify the report moves to Paid.

5. Positive reimbursable plus non-reimbursable (no change expected)

Prereq: business bank account connected to the workspace.

  1. As A, create a report with a $50 reimbursable expense and a $30 expense with Reimbursable off. Submit it.
  2. As B, approve the report.
  3. Verify:
    • Verify Header and Preview show Pay for the $50 reimbursable amount with the usual payment options.

6. Negative reimbursable plus non-reimbursable (no change expected)

Prereq: none.

  1. As A, create a report with a -$50 reimbursable expense and a $30 expense with Reimbursable off. Submit it.
  2. As B, approve the report.
  3. Verify:
    • Verify Header and Preview offer only Mark as paid.

7. Reimbursable expenses cancel out, non-reimbursable spend remains

Prereq: none.

  1. As A, create a report with a $50 reimbursable expense, a -$50 reimbursable expense, and a $100 expense with Reimbursable off. Submit it.
  2. As B, approve the report.
  3. Verify:
    • Verify the report total is $100.00.
    • Verify Header and Preview show Pay with Mark as paid as the only option.
    • Verify Row shows View.
    • Verify LHN has no PAY badge.
    • Verify To-do does not list the report.
  4. In the header, click Pay > Mark as paid.
  5. Verify the report moves to Paid.

8. Reimbursable expense offset by a non-reimbursable credit, total $0

Prereq: business bank account connected to the workspace (to confirm no bank option appears).

  1. As A, create a report with a $50 reimbursable expense and a -$50 expense with Reimbursable off. Submit it.
  2. As B, approve the report.
  3. Verify:
    • Verify Header and Preview offer only Mark as paid, with no bank payment option. This matches Expensify Classic.
    • Verify Row shows View.
    • Verify LHN has no PAY badge.
    • Verify To-do does not list the report.
  4. In the header, click Pay > Mark as paid.
  5. Verify the report moves to Paid.

9. One non-reimbursable expense (no change expected)

Prereq: none.

  1. As A, create a report with one $50 expense with Reimbursable off. Submit it.
  2. As B, approve the report.
  3. Verify:
    • Verify Header and Preview offer Pay > Mark as paid.
    • Verify Row shows View.
    • Verify LHN has no PAY badge.
    • Verify To-do does not list the report.
  4. In the header, click Pay > Mark as paid.
  5. Verify the report moves to Paid.

10. Non-reimbursable expenses cancel out, total $0

Prereq: none.

  1. As A, create a report with a $50 and a -$50 expense, both with Reimbursable off. Submit it.
  2. As B, approve the report.
  3. Verify:
    • Verify Header and Preview show Pay $0.00 with Mark as paid as the only option.
    • Verify Row shows View.
    • Verify LHN has no PAY badge.
    • Verify To-do does not list the report.
  4. In the header, click Pay > Mark as paid.
  5. Verify the report moves to Paid.

11. Expense still scanning or failed to scan

Prereq: a receipt to scan.

  1. As A, create a report with a $50 and a -$50 reimbursable expense.
  2. Add a scanned receipt expense. Use one that SmartScan hasn't finished, or one that failed and has no amount.
  3. Submit the report.
  4. As B, approve the report.
  5. Verify:
    • Verify Header, Preview and Row show no Pay button.
    • Verify LHN has no PAY badge.
    • Verify To-do does not list the report.

12. Bulk pay with a $0 non-reimbursable report

Prereq: business bank account connected to the workspace.

  1. As A, create and submit two reports:
    • Report 1: one $50 reimbursable expense.
    • Report 2: one $0.00 expense with Reimbursable off.
  2. As B, approve both reports.
  3. Go to Reports > All reports, select both reports, and open the bulk Pay menu.
  4. Verify:
    • Verify no business bank account option is shown for the selection.

13. Closed report with payments turned on after it closed

Prereq: a workspace where a report can be closed before payments are enabled. Confirm the exact setup with the PR author before testing.

  1. As A, submit a report from one of the Test 3 to Test 10 cases (for example, a $50 and a -$50 reimbursable expense) and let it close.
  2. As the workspace admin, turn on Make or track payments.
  3. As B, open the report.
  4. Verify:
    • Verify Header and Preview show Pay $0.00 with Mark as paid as the only option.
    • Verify Row shows View.
    • Verify LHN has no PAY badge.
    • Verify To-do does not list the report.

@OSBotify

OSBotify commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/puneetlath in version: 9.5.6-6 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 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.

7 participants