Repository navigation
[Due for payment 2026-10-30] [CFI] [$500] Allow any workspace admin to pay reports #95750
Copy link
Copy link
Open
Labels
Awaiting PaymentAuto-added when associated PR is deployed to productionAuto-added when associated PR is deployed to productionDailyKSv2KSv2ExternalAdded to denote the issue can be worked on by a contributorAdded to denote the issue can be worked on by a contributorNewFeatureSomething to build that is a new item.Something to build that is a new item.
Description
Coming from this Slack thread, @sakluger flagged a classic/NewDot parity gap regarding payments.
Details
- When no bank account is connected to a workspace, any workspace already sees the option to pay/mark as paid in NewDot. This part works correctly today and needs no changes.
- The gap: when a bank account is connected to a workspace, NewDot only shows the pay button to the admin set as the workspace payer. Classic shows it to any workspace admin.
- Proposal: any workspace admin should see the option to pay (via any VBA they have access to) or manually mark as paid, regardless of whether a bank account is connected to the workspace.
- In the event the workspace doesn't have a VBA, they should be prompted to add one in situ when they try to pay via business account (the existing payment button logic).
- An admin adding/paying with a bank account that's not the current workspace bank account shouldn't change the workspace bank account, same as now.
- We should not actively prompt admins that aren't the workspace payer to pay via badges/GBRs. That stays as-is (per https://github.com/Expensify/Expensify/issues/637864).
- One note: an admin that isn't the workspace payer who tries to pay a report via ACH in Classic sees an error, despite being able to mark as paid. I don't think we should go this direction in NewDot, since we already allow the admin set as the workspace payer to select the bank account to use at the time of payment in both NewDot and Classic. It feels more consistent to also allow the non-payer admin to select the bank account they want to use or mark as paid. Curious if anyone disagrees?
- I don't think this issue should require any backend changes. Let me know if anyone feels differently.
- This issue is only about who can see and use the pay button. It shouldn't change which options appear or how it behaves for a given report (e.g., non-reimbursable-only reports showing "Mark as Paid" only in the report detail view, not the report preview or the Spend page).
Non-payer workspace admin view when workspace has bank account - NewDot
Non-payer workspace admin view when workspace has bank account - OldDot
Non-payer workspace admin view when workspace has no bank account - NewDot
Non-payer workspace admin view when workspace has no bank account - OldDot
Upwork Automation - Do Not Edit
- Upwork Job URL: https://www.upwork.com/jobs/~022075307768398315166
- Upwork Job ID: 2075307768398315166
- Last Price Increase: 2026-07-09
Issue Owner
Current Issue Owner: @ikevin127Reactions are currently unavailable
Metadata
Metadata
Labels
Awaiting PaymentAuto-added when associated PR is deployed to productionAuto-added when associated PR is deployed to productionDailyKSv2KSv2ExternalAdded to denote the issue can be worked on by a contributorAdded to denote the issue can be worked on by a contributorNewFeatureSomething to build that is a new item.Something to build that is a new item.
Type
Projects
- StatusShow more project fieldsHIGH
Activity
melvin-bot commentedon Jul 9, 2026
Triggered auto assignment to Design team member for new feature review - @dannymcclain (
NewFeature)joekaufmanexpensify commentedon Jul 9, 2026
@dannymcclain I don't think we need any mocks here. We're just showing the existing pay button to a broader group of admins. Let me know if you feel differently. Going ahead with soliciting proposals for now. 👍
joekaufmanexpensify commentedon Jul 9, 2026
@MelvinBot please submit a proposal for this issue following Expensify's normal proposal guidelines.
MelvinBot commentedon Jul 9, 2026
Proposal
What is the root cause of that problem?
The pay button is gated on being the single designated workspace payer, not on being an admin.
In
isPayer, once a workspace has a designated reimburser — which is exactly the case when a bank account is connected (policy.achAccount.reimburser) — only that reimburser is treated as a payer. Other admins qualify only if they are a sharee of that specific VBA (auto reimbursement) or not at all (manual):The designated-payer gate
reimburserEmail = policy.reimburser ?? policy.achAccount?.reimburser ?? …— ReportUtils.ts:2853REIMBURSEMENT_YES):isReimburser || (isAdmin && hasAccessToBankAccount)— ReportUtils.ts:2864-2869REIMBURSEMENT_MANUAL):isReimburseronly — ReportUtils.ts:2873There is already a "any admin / Payments Admin can pay" fallback layered on top of
isPayer, but it is scoped toREIMBURSEMENT_MANUALonly, so it doesn't cover a bank-connected workspace that reimburses automatically:The fallback is manual-only in all three pay-button surfaces
canIOUBePaid—isReportPayer || (reimbursementChoice === MANUAL && canMemberWrite(…, WORKFLOWS_PAYMENTS))ReportPreviewActionUtils.ts:130-132— sameMANUAL-only conditionReportPrimaryActionUtils.ts:228-232— same, additionally behind acanNonPayerAdminPayflagisPolicyPayer, which also returnstrueonly whencurrentUserLogin === reimburserEmailNet effect: on a workspace with a bank account connected + automatic reimbursement, a non-payer workspace admin or Payments Admin sees no pay button — the OldDot/NewDot parity gap.
What changes do you think we should make in order to solve the problem?
Separate "who can pay" (button visibility — should be broad) from "who is the designated payer" (prompting — stays narrow).
canIOUBePaid,ReportPreviewActionUtils,ReportPrimaryActionUtils) so it applies whenever payments are enabled, not just manual reimbursement — i.e. gate onarePaymentsEnabled(policy)instead ofreimbursementChoice === REIMBURSEMENT_MANUAL. Any workspace admin or Payments Admin (canMemberWrite(policy, login, WORKFLOWS_PAYMENTS)) then sees the pay button regardless of being the designated payer or a VBA sharee.isPolicyPayerso the Search/Reports-page pay button matches.isPayeritself unchanged. It also feeds the PAY badge (getBadgeFromIOUReport) and next-step/GBR messaging (NextStepUtils). Keeping it gated on the designated payer ensures non-payer admins still see the button but are not actively prompted via badges/GBRs — matching the issue requirement and Expensify/Expensify#637864.No backend changes needed. A non-payer admin without a VBA who taps "pay via business account" hits the existing add-bank-account flow in situ; "mark as paid" already works via the pay-elsewhere path.
What alternative solutions did you explore? (Optional)
Broadening
isPayerdirectly (one edit) — rejected, because it is shared by the badge/GBR/next-step logic, so it would start actively prompting non-payer admins to pay, which the issue explicitly rules out.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/95750)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
joekaufmanexpensify commentedon Jul 9, 2026
Opening up to other proposals now so C+ can decide between Melvin and others.
aswin-s commentedon Jul 9, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On NewDot Web, when a workspace has a connected business bank account, the pay/mark-as-paid button is only shown to the admin designated as the workspace payer (reimburser). Classic shows it to any workspace admin — and now, with the newly added Payments Admin role (PR #94018), any Payments Admin should also see it. Expected: any workspace admin or payments admin can see and use the pay button (choosing a VBA they have access to, being prompted to add one, or marking as paid), regardless of whether the workspace has a bank account connected.
What is the root cause of that problem?
isPayerin src/libs/ReportUtils.ts:2820 definesisAdminaspolicy?.role === CONST.POLICY.ROLE.ADMIN(line 2835), which excludes the newPAYMENTS_ADMINrole added in PR #94018. Additionally, in the auto-reimbursement branch (lines 2864–2869), the function returnsisReimburser || (isAdmin && hasAccessToBankAccount)— so a non-reimburser workspace/payments admin who isn't on the connected bank account'sshareeslist returnsfalse, hiding the pay button. Classic instead lets any admin see the button and pick a VBA (or be prompted to add one) at pay time.What changes do you think we should make in order to solve the problem?
isAdminto also treatCONST.POLICY.ROLE.PAYMENTS_ADMINas an admin for payment authorization (e.g.[ROLE.ADMIN, ROLE.PAYMENTS_ADMIN].includes(policy?.role)when policy type isn't PERSONAL).hasAccessToBankAccountgate here so the return becomesisReimburser || isAdmin. The in-situ VBA prompt / bank-picker at pay time handles the "no access to current VBA" case per the issue.trueforisAdminin addition toisReimburser, so any admin can mark-as-paid or pay through their accessible VBA.PayPrimaryAction.tsx,PayActionButton.tsx,PayActionCell.tsx,MoneyReportHeaderSecondaryActions.tsx,MoneyReportHeaderSelectionDropdown.tsx,SearchUIUtils.ts,useSearchBulkActions.ts,useSelectionModeReportActions.ts,AnimatedSettlementButton.tsx,MoneyReportHeaderModals.tsx,actions/IOU/ReportWorkflow.ts) pass the current user's policy throughisPayerunchanged — no per-call-site changes needed since the gate lives in one function.What alternative solutions did you explore? (Optional)
lorretheboy commentedon Jul 9, 2026
Proposal
What is the root cause of that problem?
The pay-button gates already have an escape that lets any admin/payments admin pay when they aren't the designated payer, but it's hard-coded to manual reimbursement only. Connecting a bank account flips the policy's
reimbursementChoicetoreimburseYesand setsachAccount.reimburser(the workspace payer), which kills both paths at once:isPayerin its auto-reimbursement branch only returns true for the designated reimburser, or an admin who is a sharee of the workspace's exact bank account:App/src/libs/ReportUtils.ts
Lines 2863 to 2873 in b794ad3
canIOUBePaidrequiresreimbursementChoice === REIMBURSEMENT_MANUAL, which is no longer true once a bank account is connected:App/src/libs/actions/IOU/ReportWorkflow.ts
Lines 223 to 226 in b794ad3
The same manual-only escape is duplicated in the primary-action and report-preview gates:
App/src/libs/ReportPrimaryActionUtils.ts
Lines 227 to 232 in b794ad3
App/src/libs/ReportPreviewActionUtils.ts
Lines 129 to 132 in b794ad3
So: bank account connected → choice becomes
reimburseYes→ the manual-only escape stops applying → onlyisPayergates the button → only the workspace payer (or a bank-account sharee) sees Pay. Classic shows it to any workspace/payments admin. Note this isn't a regression from PR #94018 — that PR only added the Payments Admin role/UI; the manual-only restriction predates it, and payments admins already satisfy the escape'scanMemberWrite(..., WORKFLOWS_PAYMENTS)check via their permission bundle.What changes do you think we should make in order to solve the problem?
Broaden the non-payer-admin escape so it applies whenever reimbursement is enabled (manual or direct), instead of manual only. Since the identical expression is duplicated in three places, extract it into a shared helper in
PolicyUtils(next toisPolicyPayer), roughly:and use it in the three gates above (
canIOUBePaid,isPrimaryPayAction, and the report-previewcanPay) in place of the inline manual-only check.canMemberWrite(..., WORKFLOWS_PAYMENTS)is write-all for admins and explicitly granted to payments admins, so both roles are covered.Deliberately do not change
isPayer(orisPolicyPayer). Those still drive the LHN Pay badge, GBR (hasOutstandingChildRequest), next-step copy, and Search pay suggestions — e.g. the badge separately requiresisReportPayerbefore callingcanIOUBePaid:App/src/libs/actions/IOU/ReportWorkflow.ts
Lines 303 to 306 in b794ad3
so non-payer admins get the button but are not actively prompted via badges/GBRs, as the issue requires. Payment itself needs no changes: the settlement button already lists only bank accounts the user has access to, prompts them to add a VBA (KYC wall) if they have none, and paying from a different VBA doesn't change the workspace bank account.
What alternative solutions did you explore? (Optional)
Making
isPayer's auto-reimbursement branch return true for any admin/payments admin (canPayOnPolicy). Rejected becauseisPayeralso feeds badges, GBRs, and next-step messaging, which must stay payer-only per the issue.150 remaining items
lorretheboy commentedon Oct 25, 2026
@flodnv Yes, the changes in #103239 cover Payments Admins who aren’t the designated payer
melvin-bot commentedon Oct 27, 2026
@ikevin127 Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!
ikevin127 commentedon Oct 28, 2026
Contributor+ Checklist:
[Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.
Link to the comment on the PR: N/A - Result of the original design, NewDot has only shown the Pay button to the designated workspace payer whenever a bank account is connected, so letting non-payer admins pay is a new parity feature rather than a regression.
Link to the comment on the Issue: N/A
[Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.
Link to discussion: N/A
[Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.
Regression Test Proposal
Precondition:
Do we agree 👍 or 👎.
ikevin127 commentedon Oct 28, 2026
@chuckdries Given that this has landed on 3rd PR, and you said after 1st PR revert here:
then we had 2nd PR rework which had 1 regression reported (not sure whether this was BE related or a FE oversight).
Finally, we landed the 3rd rework PR and no regressions reported so far. Question is: do we get the full $500 or some penalty for that 2nd rework PR regression ? IMO 50% penalty seems unfair given the amount of work involved (3 PRs reviewed) to land this feature.
Please let me know what you think as payment date approaches 🙌
chuckdries commentedon Oct 28, 2026
We ended up solving the regression on the 2nd PR on the frontend. That said, I agree that the regression penalty feels unfair in this case since it was more due to confusion about requirements than a code oversight. In the future, let's all try to raise questions we're uncertain about at design or review time (speaking to myself as much as anyone else)!
melvin-bot commentedon Oct 29, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)