feat: Allow admins to mark report with non-reimbursable expenses as paid and show decision modal if user try to pay via ACH#83329
Conversation
…bursable expenses
|
Hey, I noticed you changed If you want to automatically generate translations for other locales, an Expensify employee will have to:
Alternatively, if you are an external contributor, you can run the translation script locally with your own OpenAI API key. To learn more, try running: npx ts-node ./scripts/generateTranslations.ts --helpTypically, you'd want to translate only what you changed by running |
src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Enables admins to mark expense reports that contain only non‑reimbursable expenses as paid (via “Pay elsewhere”), and shows a blocking decision modal when attempting to pay such reports via direct payment methods (e.g., ACH).
Changes:
- Show Pay actions/buttons for reports with only non‑reimbursable transactions across report header/preview and search flows.
- Block non‑reimbursable reports from being paid via direct payment methods by showing a localized DecisionModal.
- Add new i18n strings for the non‑reimbursable payment error messaging.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/pages/Search/SearchPage.tsx | Adjusts bulk-pay “only show pay elsewhere” logic to exclude only-non-reimbursable reports. |
| src/libs/actions/IOU/index.ts | Extends canIOUBePaid() to treat only-non-reimbursable reports as eligible when showing “pay elsewhere”. |
| src/libs/SearchUIUtils.ts | Adds PAY action for only-non-reimbursable reports and updates pay-elsewhere gating. |
| src/libs/ReportPrimaryActionUtils.ts | Allows primary Pay action for only-non-reimbursable expense reports. |
| src/libs/ReportPreviewActionUtils.ts | Allows preview Pay action for only-non-reimbursable expense reports. |
| src/libs/MoneyRequestReportUtils.ts | Adjusts preview button amount display for only-non-reimbursable reports. |
| src/components/SelectionListWithSections/Search/ActionCell/PayActionCell.tsx | Adds DecisionModal when attempting to pay non-reimbursable reports via direct payment methods. |
| src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx | Shows Pay button for only-non-reimbursable reports and displays DecisionModal on direct-payment attempts. |
| src/components/ProcessMoneyReportHoldMenu.tsx | Adds callback to surface the non-reimbursable direct-payment error from the hold menu flow. |
| src/components/MoneyReportHeader.tsx | Shows Pay button for only-non-reimbursable reports and displays DecisionModal on direct-payment attempts. |
| src/languages/*.ts | Adds localized strings for the new non-reimbursable payment error modal. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6d92ab3e55
ℹ️ 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".
…bleexpenses-mark-as-paid
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
|
@trjExpensify @jamesdeanexpensify Can you confirm the copy please title: "Cannot pay via direct payment" NewDot.mp4OldDot
|
|
There was a problem hiding this comment.
This is one of @joekaufmanexpensify's 👍
For this copy:
@jamesdeanexpensify I suggested a copy update here to tighten this up a bit. What do you think about the below:
Cannot pay via Expensify
The report doesn’t have reimbursable expenses. Double check the expenses, or manually mark as paid.[Got it]
|
That also works! |
|
Oh haha, I totally went straight to the review mode and didn't see that from earlier. I think what I like about the version I suggested is that the payment method is a bit clearer and linked. You can pay the report, just not via Expensify. |
|
Makes sense. What do you mean by the payment method is linked? |
|
The difference between:
AND
The first option is an indicator of the payment method (in-app via Expensify), the latter could be misconstrued as not being able to pay the report at all. |
|
🤖 Code Review (Updated) Good progress since the last review. The main feedback items have been addressed: Resolved since last review:
Remaining items (low severity, non-blocking):
Overall this PR looks ready. The core logic is sound, tests cover the new paths, copy has been approved, and the earlier review feedback has been incorporated. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ed5bbfdf9b
ℹ️ 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".
src/libs/ReportPreviewActionUtils.ts
Outdated
| const didExportFail = !isExported && hasExportError; | ||
|
|
||
| if (isExpense && isReportPayer && isPaymentsEnabled && isReportFinished && reimbursableSpend !== 0) { | ||
| if (isExpense && isReportPayer && isPaymentsEnabled && isReportFinished && (reimbursableSpend !== 0 || hasOnlyNonReimbursableTransactions(report?.reportID))) { |
There was a problem hiding this comment.
Use caller transactions when deriving preview PAY action
canPay() now depends on hasOnlyNonReimbursableTransactions(report?.reportID) but does not use the transactions that getReportPreviewAction() already receives, so it falls back to global Onyx state. If preview transactions are available before the collection is hydrated (or differ from global cache), this returns false and the PAY action is omitted for non-reimbursable-only reports. Threading the provided transactions through this check avoids stale/global-state misclassification.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
I think we should address this @samranahm @situchan. We added support for passing the transactions to hasOnlyNonReimbursableTransactions. Why don't we use it and rely on the deprecatedReportsTransactions?
There was a problem hiding this comment.
Updated the logic to pass transactions instead of relying on deprecatedReportsTransactions.
|
@cristipaval friendly bump. |
This isn't great though. @samranahm @situchan can you outline the case for it in numbered steps? CC: @joekaufmanexpensify |
|
@trjExpensify here are the steps to reproduce #83329 (comment)
We have consistent behaviour with staging. |
src/libs/MoneyRequestReportUtils.ts
Outdated
| } | ||
|
|
||
| // For reports with only non-reimbursable expenses, show total display spend for Mark as paid. | ||
| if (hasOnlyNonReimbursableTransactions(report?.reportID)) { |
There was a problem hiding this comment.
Same as here, why do we rely on the global-deprecated-cached transactions? Can't we pass them as parameters?
There was a problem hiding this comment.
We absolutely can, updated the logic.
src/libs/ReportPrimaryActionUtils.ts
Outdated
| const {reimbursableSpend} = getMoneyRequestSpendBreakdown(report); | ||
|
|
||
| if (isReportPayer && isExpenseReport && arePaymentsEnabled && isReportFinished && reimbursableSpend !== 0) { | ||
| if (isReportPayer && isExpenseReport && arePaymentsEnabled && isReportFinished && (reimbursableSpend !== 0 || hasOnlyNonReimbursableTransactions(report?.reportID))) { |
There was a problem hiding this comment.
again, we shouldn't rely on the global cached transactions since they are deprecated
Interesting. I agree that behavior seems incorrect. I'm not convinced the admin needs to confirm anything in that case. I wonder if we could just pay the unheld expenses and split the held non-reimbursable expenses to a new report. Alternatively, we could warn them what would happen if the admin proceeds, so they're aware. But IDT we should offer two pay buttons to pay the same amount... If this is on staging, we prob don't need to handle it here though, yeah? Since this PR primarily deals with non-reimbursable reports. |
|
Yeah, as it's on staging, I agree with it being out of scope of this PR.
Same here. Thinking about it though, I think the improvement comes sooner probably.
^^ at step 5, we should consider keeping held expenses back. I just tried to unhold an expense on a closed report and it outright fails:
Hitting this code block in the |
|
Ah, yeah that makes sense to me. |
|
🚧 @cristipaval has triggered a test Expensify/App build. You can view the workflow run here. |
This comment has been minimized.
This comment has been minimized.
1 similar comment
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
Thanks @samranahm! @situchan, could you quickly retest on a platform to confirm it still works as expected? 🙏 |
|
retested. works as expected Screen.Recording.2026-04-01.at.4.25.53.PM.mov |
cristipaval
left a comment
There was a problem hiding this comment.
Thank you all for working on this!
|
🚧 @cristipaval has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚀 Deployed to staging by https://github.com/cristipaval in version: 9.3.52-0 🚀
Bundle Size Analysis (Sentry): |
|
🤖 I reviewed the changes in this PR and confirmed that help site updates are needed. I've created a draft PR with the required changes: Draft PR: #86995 Changes made:
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: Then you can close the draft PR and open a new one from your fork. Please mark it as "Ready for review" when it is ready for review. |
|
@samranahm This PR is failing because of a regression issue #87018 The issue is reproducible in: Web, Android Bug7120186_1775162143393.bandicam_2026-04-02_23-34-18-889.mp4 |



Explanation of Change
Fixed Issues
$ #81721
PROPOSAL: #81721 (comment)
Tests
Precondition:
Test:
Offline tests
QA Steps
Same as test
// TODO: These must be filled out, or the issue title must include "[No QA]."
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android.Native.mp4
Android: mWeb Chrome
Android.mWeb.Chrome.mp4
iOS: Native
IOS.Native.mp4
iOS: mWeb Safari
IOS.mWeb.Safari.mp4
MacOS: Chrome / Safari
macOS.Chrome.mp4