Skip to content

[Due for payment 2026-10-15] Extend admin bulk edit to expenses on approved and paid reports #101253

Open

Description

@MelvinBot

Proposal

Extend admin bulk edit to expenses on approved and paid reports.

Background

  • Expensify imports the chart of accounts from a connected accounting system and turns each GL account into a workspace category. A category is identified by its name. When someone codes an expense to a category, the expense records that name, and the name is what we use at export to work out which GL account the expense belongs to. Tags work the same way.
  • When an accountant renames a GL account in their accounting system, the next sync renames the matching Expensify category in place. The old name disappears from the category list and the new name takes its place, still pointing at the same underlying account. Expenses already coded to the old name keep the old name, get an Out of policy violation, and the mismatched name prevents them from exporting.
  • Today, admins can edit an expense individually in all states, including after a report is approved or paid. Admins can only bulk edit coding on draft or outstanding reports, to prevent accidental changes to records that should normally stay as they are.

Problem

When a workspace syncs categories from a connected accounting system and expenses on approved or paid reports are coded to those categories, if the accountant renames one of those GL accounts, then every affected expense is blocked from exporting. The spend never reaches the general ledger and the books stay incomplete.

Fixing this today means editing every affected expense one at a time. For a customer who just hit this, that is thousands of expenses.

Solution

Allow admins to bulk edit expenses on approved and paid reports.

Admins can already make this change one expense at a time, so this grants no additional access they don't have today. It only removes an inconsistency between the individual and bulk flows.

To prevent unintentional bulk changes, when any selected expense is on an approved or paid report, show a warning modal before the change commits:

3 of the 12 expenses you selected are on approved or paid reports. You're about to edit finalized expenses. Are you sure?

Notes

  • This is already possible in Expensify Classic, so New Expensify should match.
  • The warning modal should only appear when the selection actually includes an approved or paid expense, and should name the count of finalized expenses in the selection.

Reported in Slack.

Issue OwnerCurrent Issue Owner: @Eskalifer1

Activity

  1. heyjennahay commented on Sep 15, 2026

    @heyjennahay
    Contributor

    @MelvinBot can you please outline what is required to implement this?

  2. MelvinBot commented on Sep 15, 2026

    @MelvinBot
    ContributorAuthor

    Proposal

    What is the root cause of that problem?

    One guard. src/libs/ReportUtils.ts:5273-5277 makes canEditMultipleTransactions return false as soon as any selected expense sits on an approved or settled report, so the Edit multiple option disappears from the selection menu.

    Nothing below that guard needs new permission logic:

    • Per-field permission already handles finalized expenses. On an approved or paid report, canEditFieldOfMoneyRequest blocks the restricted fields (amount, currency, merchant, date, receipt, distance, reimbursable, report, billable) and allows the coding fields — category, tag, description, tax, attendees. That is exactly the renamed-GL-account case.
    • Role scoping is already correct. For anyone who is not an admin or manager, canEditMoneyRequest returns false on approved/settled/closed reports, so removing the guard does not widen access beyond admins and managers. No new admin check is needed.
    • The API is the same one the single-expense edit uses. Bulk edit fires one UpdateMoneyRequest per expense at src/libs/actions/IOU/BulkEdit.ts:175, so no new endpoint is expected.

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

    1. Drop the guard — remove the isApproved || isSettled early return in canEditMultipleTransactions. The loop's existing per-transaction editability check takes over. Both call sites pick it up for free: search bulk actions at useSearchBulkActions.ts:2253 and the report page at useSelectedTransactionsActions.ts:290.

    2. Add the warning modal — a ConfirmModal in save() at SearchEditMultiplePage.tsx:162. Count finalized expenses from the existing selectedTransactionContexts using isReportApproved/isSettled, show the modal only when that count is above zero, and run updateMultipleMoneyRequests on confirm. Add the copy as a count-taking key next to search.bulkActions.editMultiple* in en.ts:9679, plus the other 10 locale files, with singular and plural forms.

    3. Disable Billable and Reimbursable when the selection is mixed — this is the one real gap. Both are restricted fields, but their rows at SearchEditMultiplePage.tsx:323-339 carry no disabled flag — they are gated only on policy config. Once approved expenses can be selected, an admin could set Billable and have it silently dropped for the finalized ones by the per-field filter at BulkEdit.ts:281-289. Add disabled: isFieldDisabledForAnyTransaction(...) to both, the same way amount, merchant, date and tax already do it at lines 115-122.

    4. Tests — tests/unit/canEditMultipleTransactionsTest.ts:258 asserts today's behavior and must be inverted. Add cases for a paid report, for a non-admin (still false), for the modal count, and for the two newly disabled rows.

    Two things to confirm before building
    • Backend volume. The command is the same one admins already use on a single approved expense, so permission should be fine, but bulk edit queues one UpdateMoneyRequest per expense. The customer case in this issue is thousands of expenses — worth a backend check that the queue handles that shape before shipping.
    • Beta. Both entry points are still behind CONST.BETAS.BULK_EDIT, so this ships to beta users first.

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


    view run

  3. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Sep 16, 2026
  4. melvin-bot commented on Sep 16, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @daledah (External)

  5. melvin-bot commented on Sep 16, 2026

    @melvin-bot

    Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.

  6. daledah commented on Sep 18, 2026

    @daledah
    Contributor

    working on this issue now

  7. daledah commented on Sep 18, 2026

    @daledah
    Contributor

    @MelvinBot, the direction is right and the root cause is correct. We can discuss some improvements in the PR.

    @MelvinBot implement this

  8. MelvinBot commented on Sep 18, 2026

    @MelvinBot
    ContributorAuthor

    🤖 Draft PR created: #101553

    Please mark it as "Ready for review" when it is ready for review.

    Implemented all four parts of the approved proposal:

    1. Dropped the guard — removed the isApproved || isSettled early return in canEditMultipleTransactions, so the loop's existing per-transaction editability check decides instead. Both entry points pick it up unchanged.
    2. Added the confirmation — saving a bulk edit whose selection includes an approved or paid expense now asks first, naming the finalized count ("3 of the 12 expenses you selected are on approved or paid reports…"). Skipped entirely when the count is zero. Copy added to en.ts plus all 10 other locales with singular and plural forms.
    3. Disabled Billable and Reimbursable on mixed selections, using the same isFieldDisabledForAnyTransaction check amount, merchant, date and tax already use.
    4. Tests — inverted the two assertions that pinned the old behavior and added non-admin cases for both approved and reimbursed reports, so the role scoping is covered rather than assumed.

    Two deviations from the proposal, both minor:

    • The proposal pointed at ConfirmModal. That component is deprecated and lint-changed rejects it, so the modal uses the useConfirmModal hook instead. Same UI, no local modal state.
    • The count copy takes {count, total} inline rather than a new type in params.ts, matching the existing common.durationDays precedent.
    Verification

    Typecheck, lint-changed, spell-changed, and the React Compiler compliance check all pass. Unit tests: 21/21 in canEditMultipleTransactionsTest, 1373/1373 in ReportUtilsTest, 1847/1847 across the Search and bulk-action hook suites, plus the translate and bulk-edit utils suites.

    Not verified: the UI itself. The browser session I was handed rendered blank after CI's own onboarding replay failed, and reloading isn't a permitted recovery step — the dev server was healthy, so this was session infrastructure, not the build. The confirmation dialog and the disabled Billable/Reimbursable rows still need a human pass, and the Tests/QA sections are left for you.

    Also still open from the proposal: bulk edit enqueues one UpdateMoneyRequest per expense, and the customer case here ran to thousands. Worth a backend check on that shape before shipping. Both entry points remain behind the bulkEdit beta.

    I don't have push access to your fork, so the PR is from an upstream branch — instructions for taking it over are on the PR. Happy to discuss the improvements you had in mind there.

  9. 48 remaining items

  10. Eskalifer1 commented on Oct 8, 2026

    @Eskalifer1
    Contributor

    Not sure why @francoisl was unassigned...

    Just assigning myself since i reviewed this pr: #102720

  11. francoisl commented on Oct 8, 2026

    @francoisl
    Contributor

    Because it needs an internal employee for the final review and to merge the PR, and so we also assign the employee to the linked issue

  12. Eskalifer1 commented on Oct 8, 2026

    @Eskalifer1
    Contributor

    yea, What I mean is, when I assigned myself, you were unassigned, but I assigned you again, so no problem!

  13. Eskalifer1 commented on Oct 13, 2026

    @Eskalifer1
    Contributor

    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 - follow-up to Allow admins to bulk edit expenses on approved and paid reports #101553

    • [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:

    Test:

    1. Enable the bulkEdit beta and sign in as an admin of a Collect or Control workspace with Billable enabled.
    2. Create 2 expenses, submit and approve the report holding them.
    3. Open one expense and verify the Billable toggle is still editable (unchanged behaviour).
    4. Go back to the report, select both expenses and choose Edit multiple.
    5. Verify Billable is now editable rather than greyed out
    6. Set Billable to Yes, tap Save, confirm the Edit finalized expenses? modal, and verify both expenses update.

    Do we agree 👍 or 👎

  14. Eskalifer1 commented on Oct 13, 2026

    @Eskalifer1
    Contributor

    Note about payment
    As far as I understand, two PRs were created as part of this task:

    1. Allow admins to bulk edit expenses on approved and paid reports #101553 - implemented by Melvin, and the C+ was @daledah
    2. Align billable editability between single and bulk expense edit #102720 - implemented by @daledah, and I was the C+

    The second PR was a follow-up to the first.

    Since I only reviewed the second PR, I should receive the standard payment ($175). (I'm getting paid via Upwork)

    @daledah, could you please describe the work you completed so Matt can understand what the payment amount should be? Thank you!

  15. added
    Awaiting PaymentAuto-added when associated PR is deployed to production
    and removed on Oct 14, 2026
  16. melvin-bot commented on Oct 14, 2026

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  17. daledah commented on Oct 15, 2026

    @daledah
    Contributor

    @Eskalifer1 agreed on the split, you reviewed #102720, so $175 for that review is right. Here's my side.

    Price increase request

    Mandatory

    Proposed amount: $350 - $175 as C+ on #101553 + $175 as the author of #102720.

    Why the work exceeded the issue: I held two different roles, but the payment summary only counts one. The $175 listed covers my C+ review of #101553. It does not cover #102720, where I was the contributor, not the reviewer.

    #102720 wasn't in the original scope. After #101553 deployed, @trjExpensify comment that Billable is editable on an approved report one expense at a time but missing from Edit multiple - a separate root cause that predates #101553 (BILLABLE was already in restrictedFields)

    Regressions caused by this PR: None. No deploy blocker and no regression issue against either PR.

    Multiple PRs: #101553 (C+) and #102720 (author).

    cc @mallenexpensify @trjExpensify

  18. mallenexpensify commented on Oct 16, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor: @Eskalifer1 due $175 via Upwork
    Contributor+: @daledah due $350 via NewDot

    @Eskalifer1 can you please accept the job below? Please reply here and tag me once you have.
    https://www.upwork.com/jobs/~022111160965840563145

    @daledah your request makes sense to me, cc @trjExpensify @francoisl in case you disagree.

  19. Eskalifer1 commented on Oct 16, 2026

    @Eskalifer1
    Contributor

    Hi @mallenexpensify, i have accepted, thank you!

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

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionDailyKSv2ExternalAdded to denote the issue can be worked on by a contributorImprovementItem broken or needs improvement.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions