Skip to content

feat: add Auto report option for expenses spanning multiple submitters - #99688

Merged
yuwenmemon merged 12 commits into
Expensify:mainfrom
daledah:refactor/87954-auto-report
Sep 18, 2026
Merged

yuwenmemon merged 12 commits into
Expensify:mainfrom
daledah:refactor/87954-auto-report

Conversation

@daledah

@daledah daledah commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

Fixed Issues

$ #87954
PROPOSAL:

Tests

Preconditions:

  • Log in as a workspace admin with company cards enabled.
  • Have at least 2 cardholders with unreported card expenses. Arrange them so that:
    • Cardholder A already has an open/draft expense report
    • Cardholder B has no open report at all

Steps:

  1. Go to Search → Expenses and filter to unreported card expenses.
  2. Select expenses belonging to two or more different submitters.
  3. Open the bulk action menu.
  4. Verify Move to report is now offered.
  5. Tap it.
  6. Verify the RHP shows only an Auto report row:
    • no Create report row
    • no list of individual reports
    • no search input above the list
  7. Tap Auto report.
  8. Verify the RHP closes, the selection clears, and no error appears.
  • 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 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 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

MacOS: Chrome / Safari
Screen.Recording.2026-09-02.at.18.21.22.mov

@melvin-bot

melvin-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown

Hey, I noticed you changed src/languages/en.ts in a PR from a fork. For security reasons, translations are not generated automatically for PRs from forks.

If you want to automatically generate translations for other locales, an Expensify employee will have to:

  1. Look at the code and make sure there are no malicious changes.
  2. Run the Generate static translations GitHub workflow. If you have write access and the K2 extension, you can simply click: [this button]

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 bun ./scripts/generateTranslations.ts --help

Typically, you'd want to translate only what you changed by running npx bun ./scripts/generateTranslations.ts --compare-ref main

@codecov

codecov Bot commented Aug 27, 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/CONST/index.ts 91.52% <ø> (ø)
src/hooks/useSearchBulkActions.ts 71.83% <25.00%> (+7.10%) ⬆️
src/libs/actions/Transaction.ts 79.62% <0.00%> (+0.50%) ⬆️
...es/iou/request/step/IOURequestEditReportCommon.tsx 85.71% <58.33%> (-5.47%) ⬇️
...rc/pages/Search/SearchTransactionsChangeReport.tsx 0.00% <0.00%> (ø)
... and 389 files with indirect coverage changes

@daledah
daledah marked this pull request as ready for review September 2, 2026 16:20
@daledah
daledah requested review from a team as code owners September 2, 2026 16:20
@melvin-bot
melvin-bot Bot requested review from heyjennahay, mkhutornyi and shawnborton and removed request for a team September 2, 2026 16:20
@melvin-bot

melvin-bot Bot commented Sep 2, 2026

Copy link
Copy Markdown

@shawnborton @mkhutornyi One of you needs to 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]

Comment thread src/pages/Search/SearchTransactionsChangeReport.tsx Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4c1c461bcb

ℹ️ 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".

Comment thread src/libs/actions/Transaction.ts Outdated
@mkhutornyi

mkhutornyi commented Sep 8, 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.mov
iOS: mWeb Safari
MacOS: Chrome / Safari
web.mov
web-offline.mov

@MelvinBot

MelvinBot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

🤖 Code review — the approach is sound, but three things should be settled before merge.

Blocking

1. A failed ChangeTransactionsReport is invisible to the user. autoReportTransactions calls API.write with no onyxData, so there is no failureData. autoReport then closes the RHP and clears the selection immediately. If the write fails, the admin sees no error and no rollback — the expenses just stay unreported with no explanation. "Nothing to apply optimistically" is fair for optimisticData, but failureData still needs to surface something.

2. Auto report is offered for expenses that are already on reports. The owner check was removed from useSearchBulkActions.ts:2657 unconditionally, and hasMultipleSubmitters never checks that the selection is unreported. So selecting expenses from two submitters' existing open reports now offers Auto report, which moves them into each owner's latest draft. That is outside the linked issue (reconciling unreported expenses) and outside the test plan. areAllTransactionsUnreported is already computed at SearchTransactionsChangeReport.tsx:91 — gate on it unless moving reported expenses is intended.

3. Auto report skips every guard the other two destinations run. handleSelectReport and handleCreateReport both run blockDistanceRequestIfNeeded, validatePerDiemMove, and shouldRestrictUserBillableActions; handleAutoReport runs none of them. Select a per-diem or manual-distance expense across two submitters and Auto report is the only option, so those checks never fire. The destination is server-chosen so the client can't evaluate them — which means this needs an explicit decision: hide Auto report when isPerDiemRequest / distance expenses are in the selection, or confirm the backend rejects those moves cleanly.

Worth confirming

  • reportID: '-3' — confirm ChangeTransactionsReport accepts the new sentinel before this ships, otherwise the whole flow no-ops in production.
  • transactionIDToReportActionAndThreadData: '{}' — the normal path sends client-generated moneyRequestPreviewReportActionID and thread IDs per transaction (type). Confirm the server generates them itself here, or moved expenses may land without a preview action or thread.

Nits

  • hasMultipleSubmitters returns true when two expenses from the same submitter both have an unresolvable owner (line 135). Not a regression — that case previously hid Move to report entirely — but the admin silently loses the report list and Create report.
  • The owner-counting block moved verbatim from useSearchBulkActions to SearchTransactionsChangeReport rather than into a shared util, so the two entry points can now drift. It also duplicates the loop targetOwnerAccountID already does over the same data.
  • autoReportOption uses the legacy <MenuItem /> API while createReportOption directly above uses MenuItem.Root / MenuItem.Row. Match the neighbour.
  • No unit tests, though the author checklist has "I added unit tests" checked and Codecov flags a drop on all four source files. hasMultipleSubmitters and the bulk-action gating are pure logic and cheap to cover.

Test plan

The steps cover the happy path well. Missing: the single-submitter regression (report list + Create report must still render), offline, and per-diem/distance selections. "Offline tests: Same as tests" also contradicts the new offline block — that path now shows a confirm modal and should be written out.

I reviewed this statically; I did not run the flow, since it needs a company-card workspace with two cardholders holding unreported expenses.


view run · no recording available

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 202cbd2071

ℹ️ 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".

if (selectedTransactionsKeys.length === 0) {
return;
}
autoReportTransactions(selectedTransactionsKeys);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce billing restrictions before auto-reporting

When a multi-submitter selection includes expenses from a workspace whose owner is past due, this path sends ChangeTransactionsReport without the shouldRestrictUserBillableActions check used by explicit report selection and report creation. Because Auto report can move expenses into or create draft reports, it either bypasses the billing restriction or is rejected server-side after the RHP closes and the selection is cleared; check every involved policy and route to the restricted-action screen before invoking the write.

Useful? React with 👍 / 👎.

@MelvinBot

MelvinBot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

🤖 Regression scan — the existing single-submitter paths are provably untouched. Four regressions live in the code paths this PR newly opens up.

Regressions

R1 — Expenses already on reports can now be pushed back into drafts. The owner gate was removed unconditionally at useSearchBulkActions.ts:2657, and hasMultipleSubmitters never checks that the selection is unreported. Select expenses from two submitters' existing open reports → Move to report → Auto report is the only row → each expense goes to its owner's latest draft report. Search offered no way to do that before. areAllTransactionsUnreported is already computed at line 91 — gate on it.

R2 — A failed move is now silent and unrecoverable. autoReportTransactions passes no onyxData, so there is no failureData, and autoReport closes the RHP and clears the selection before the response lands. Every other move path on this screen rolls back and surfaces the error. Skipping optimisticData is defensible; skipping failureData is not.

R3 — Auto report bypasses every pre-move guard. handleSelectReport and handleCreateReport both run blockDistanceRequestIfNeeded, validatePerDiemMove, shouldRestrictUserBillableActions, and the MAX_TRANSACTIONS check. handleAutoReport runs none. This bites hardest for per-diem and manual-distance expenses, where Auto report is the only available destination, so no check fires at all.

R4 — hasMultipleSubmitters collapses to "more than one expense" when owner metadata is missing. Verify this against a real reconciliation query. selection.ownerAccountID comes from selectionBuilders.ts:99 (item.reportAction?.actorAccountID), and the fallback getReportOrDraftReport(reportID) can never resolve for an unreported expense because its reportID is '0'. So if the search snapshot is missing the money-request action for any selected expense, hasUnknownOwner is true and the expression reduces to selectedTransactionsKeys.length > 1 — one cardholder's bulk selection gets classified as multi-submitter and silently loses both the report list and Create report.

Verified clean

  • Same/single-submitter Search selections. With hasMultipleSubmitters === false, autoReportOption is undefined, so listHeaderContent is exactly createReportOption, and reportOptions, shouldShowTextInput, and shouldShowNotFoundPage all evaluate as they did before. No behavior change.
  • Single-expense move flow. DynamicIOURequestStepReport and DynamicIOURequestEditReport don't pass hasMultipleSubmitters or autoReport, so the defaults keep that screen identical.
  • No classification drift between the two gates. The bulk-action gate and the new RHP check read the same selectedTransactions from the same context and use a character-for-character identical formula, so the RHP can't show one owner's report list for a multi-owner selection.
  • CI. Every substantive check passes — only Check independent approval and checklist fail.

This is static analysis; I did not run the flow, since it needs a company-card workspace with two cardholders holding unreported expenses.


view run · no recording available

@mkhutornyi

Copy link
Copy Markdown
Contributor

Please check AI review comments and address valid ones

@mkhutornyi

Copy link
Copy Markdown
Contributor

Should this also be supported in expense detail header menu?

Screen.Recording.2026-09-13.at.5.38.23.PM.mov

@mkhutornyi

Copy link
Copy Markdown
Contributor

Bug: cross-submitter selections now reach the move flow

Steps to reproduce

  1. Sign in as an admin of a workspace with two members, A and B.
  2. Have A and B each submit a one-expense report, so both sit in Processing awaiting first approval.
  3. Go to Search → Expenses and select A's expense and B's expense together.
  4. Open the bulk-action menu. Move to report is listed. On main, the option is absent for a cross-submitter selection.
  5. Tap it. The RHP shows a single row, Auto report: no report list, no Create report.
  6. Tap Auto report. The RHP closes, the selection clears, and no visual feedback if backend sends error.
Screen.Recording.2026-09-13.at.5.48.30.PM.mov
Screen.Recording.2026-09-13.at.5.49.53.PM.mov

Comment thread src/pages/iou/request/step/IOURequestEditReportCommon.tsx
@daledah

daledah commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

Should this also be supported in expense detail header menu?

@mkhutornyi that's intentional. Auto report only shows when expenses from 2+ submitters are selected.

@daledah

daledah commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Bug: cross-submitter selections now reach the move flow

@mkhutornyi I've updated it: Auto report now only shows when every selected expense is a company card expense
cc @s77rt

@mkhutornyi

Copy link
Copy Markdown
Contributor

Are all comments addressed?

@daledah

daledah commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

@mkhutornyi yes, please review again

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

A few product concerns.
Otherwise looks good.

Comment on lines +2721 to +2739
// Across submitters the only destination the App can offer is "Auto report". Every other mixed-owner selection
// stays hidden as before, so there is no entry into a screen that could only offer one submitter's reports to
// everybody else's expenses. Requirements:
// - every owner resolved, or the count below cannot tell one cardholder's bulk selection from a mixed one
// - every expense on a managed card, because the backend resolves each destination through the card; one
// expense without a card fails the whole request with "404 Card not found"
// - nothing whose validity depends on the destination workspace, which the backend picks: per diem rates and
// the map/GPS rules on manual and odometer distance can only be checked against a known workspace
// An expense we cannot read fails all three, so it withholds the flow rather than risking a rejected move.
const canAutoReportAcrossSubmitters =
ownerAccountIDs.size > 1 &&
!hasUnknownOwner &&
selectedTransactionsKeys.every((id) => {
const transaction = selectedTransactions[id]?.transaction ?? allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`];
if (!transaction || !isManagedCardTransaction(transaction)) {
return false;
}
return !(isPerDiemRequest(transaction) || isManualDistanceRequest(transaction) || isOdometerDistanceRequest(transaction));
});

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.

Auto report reaches expenses on Processing reports - Is this expected?

canAutoReportAcrossSubmitters checks owners, managed cards and per diem/distance, but not report state.
canChangeReport is true for an admin on any outstanding report, including Processing, so submitted company-card expenses from two employees now reach Move to report → Auto report.
On main a mixed-owner selection never reaches the move flow.

  1. As a workspace admin, have members A and B each submit a report containing one company-card expense (Processing, awaiting first approval).
  2. Go to Search → Expenses and select both expenses.
  3. Open the bulk-action menu and tap Move to report.

Should Move to report be offered (with Auto report) or not?

Comment on lines +2060 to +2064
const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.TRANSACTION>> = transactionIDs.map((transactionID) => ({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
value: {errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericEditFailureMessage')},
}));

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.

Auto report failures don't show in Search

failureData writes errors onto each transaction, but getTransactionsSections sets errors: undefined on every Search row (src/libs/SearchUIUtils.ts:2450, :3461), and report-level red brick roads don't read transaction.errors.

  1. As a workspace admin, go to Search → Expenses and select unreported card expenses from two cardholders.
  2. Tap Move to report → Auto report.
  3. Make the ChangeTransactionsReport request fail.

Expected: The affected expenses show an error in the list.
Actual: Nothing appears in Search; the expenses just stay unreported.

// "Auto report" has the backend resolve each destination through the expense's card, so one expense without a card
// fails the whole request with "404 Card not found".
const areAllManagedCardTransactions = selectedTransactionsKeys.length > 0 && transactions.length === selectedTransactionsKeys.length && transactions.every(isManagedCardTransaction);
const hasMultipleSubmitters = useMemo(() => {

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.

One cardholder's selection with unresolved owners gets no Move to report - expected?

This now ignores unresolved owners, but the entry gate at src/hooks/useSearchBulkActions.ts:2717 still counts them as a second owner, so the selection never reaches the fixed screen. Behavior is unchanged from main.

  1. As a workspace admin, go to Search → Expenses and select two unreported card expenses from one cardholder whose rows have no money-request action.
  2. Open the bulk-action menu.

Should Move to report be offered (with the cardholder's reports and Create report) or not?

Comment on lines +123 to +128
// Kept separate from `targetOwnerAccountID`, which stops at the first owner it finds. Counting needs them all.
// Only distinct resolved owners count. An owner we cannot resolve must not stand in for a second submitter: for an
// unreported expense the report lookup can never resolve one (its reportID is `0`), so a search snapshot missing
// the money-request action would otherwise file one cardholder's bulk selection as mixed and strip its report list.
// "Auto report" has the backend resolve each destination through the expense's card, so one expense without a card
// fails the whole request with "404 Card not found".

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.

Please simplify all inline comments which are too long.

@melvin-bot

melvin-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown

We did not find an internal engineer to review this PR, trying to assign a random engineer to #87954 as well as to this PR... Please reach out for help on Slack if no one gets assigned!

@melvin-bot
melvin-bot Bot requested a review from yuwenmemon September 17, 2026 14:52
@MelvinBot

Copy link
Copy Markdown
Contributor

🤖 Re-review at 0742500. Two of the three earlier blockers are genuinely resolved. One is not — the failure path still never reaches the admin — and the backend contract is still unconfirmed.

Blocking

The failureData you added doesn't render anywhere. autoReportTransactions merges errors into ${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}, but this flow never reads that collection:

So the merge creates an empty transaction_<id> stub in Onyx and the admin still sees nothing. successData's {errors: null} creates the same stubs on the happy path. Surfacing this needs a snapshot write (${ONYXKEYS.COLLECTION.SNAPSHOT}${hash} → data.transaction_<id>) plus a row that renders it — or a different mechanism entirely, given the RHP is already closed by then.

Confirm before merge

reportID: '-3' and transactionIDToReportActionAndThreadData: '{}'. Still unverified from this repo. If the backend doesn't accept the sentinel the whole feature no-ops — and per the blocker above, silently. Please link the backend change or a confirmation.

Card expenses already on other submitters' reports. The managed-card gate fixes mkhutornyi's exact repro, but only because those were non-card expenses. canEditFieldOfMoneyRequest returns true for an admin on any outstanding report, so two cardholders' card expenses sitting on Processing reports still offer Auto report and get pulled back into each owner's draft. Issue 87954 is scoped to reconciling unreported expenses, and areAllTransactionsUnreported is already computed one screen away. Gate on it, or say moving reported expenses is intended.

Remaining skipped guards. Excluding per diem / manual distance / odometer is the right call, but a managed-card expense is unlikely to be any of those, so those three checks may be dead while the real gaps stay open: shouldRestrictUserBillableActions and the MAX_TRANSACTIONS cap never run for Auto report. Also, plain map-distance expenses still skip the homeAddressRequired branch of blockDistanceRequestIfNeeded. "The backend rejects these cleanly" is a fine answer — just state it.

Nits

  • The two gates have drifted. The hook at useSearchBulkActions.ts:2730 requires !hasUnknownOwner and falls back to allTransactions; the page at SearchTransactionsChangeReport.tsx:129 does neither and instead demands every selection carry .transaction. Unreachable today (entries without .transaction get canChangeReport: false), but if the hook ever says yes and the page says no you land on a blank RHP: no rows, no header, and shouldShowNotFoundPage returns false because outstandingReports.length > 0 and selectedReport is undefined. One shared helper would remove the class of bug.
  • Owner counting is now written out three times — targetOwnerAccountID, hasMultipleSubmitters, and the hook — over the same data.
  • autoReportOption uses the legacy <MenuItem /> API while createReportOption directly above uses MenuItem.Root / MenuItem.Row.
  • Still no unit tests, though the author checklist has "I added unit tests" checked and Codecov flags a drop. hasMultipleSubmitters and canAutoReportAcrossSubmitters are pure functions of the selection and cheap to cover.
  • Test plan: "Offline tests: Same as tests" now contradicts the new offline block, which shows a confirm modal. The single-submitter regression (report list + Create report must still render) is also worth one line.
  • Scope note so the issue isn't closed early: this covers item 1 of Add Auto report option when moving expenses for multiple employees (to existing options New report + existing report). Items 2 (target the card's preferred workspace) and 3 (show cardholders' existing draft reports) are untouched.

CI is clean — only Check independent approval fails.

I reviewed this statically; I did not run the flow, since it needs a company-card workspace with two cardholders holding unreported card expenses.

@MelvinBot

Copy link
Copy Markdown
Contributor

🤖 Code quality review at 0742500. This is a quality-only pass — the correctness findings from my previous review still stand on this same commit.

Headline: one eligibility rule is written twice in two files and the two copies disagree, and one of the three new props exists only to re-ask a question the caller already answered. Extracting a single helper fixes both and makes the logic unit-testable.

Worth changing

1. areAllManagedCardTransactions shouldn't be a prop. It is read in exactly one place, IOURequestEditReportCommon.tsx:378, only to return undefined. The caller already knows the answer before it renders. Pass autoReport only when the selection is eligible and both the prop and the branch disappear. The test for this is whether removing the prop would delete only a conditional render and nothing else — here it would.

2. The eligibility formula is duplicated and the copies aren't equivalent. useSearchBulkActions.ts:2730-2739 requires resolved owners > 1, no unknown owner, and falls back to allTransactions. autoReportOption drops the unknown-owner requirement, has no fallback, and re-runs the same per diem / manual distance / odometer triple. Two gates that must agree but are written differently will drift. One exported predicate, called by both.

3. Owner counting is now written three times. targetOwnerAccountID, hasMultipleSubmitters, and the hook loop all walk the same selectedTransactions doing the same ownerAccountID ?? getReportOrDraftReport(...) resolution. ownerAccountIDs appears nowhere else in src/ outside these two files, so this duplication is new. One util returning the owner set collapses all three: targetOwnerAccountID becomes its single element, hasMultipleSubmitters becomes size > 1.

4. Two different definitions of "multiple owners" sit two lines apart and feed the same if. hasMultipleOwners treats an unresolvable owner as a second submitter; canAutoReportAcrossSubmitters on line 2731 requires strictly resolved owners > 1. Both are read at line 2741. A reader has to hold both meanings at once. Name each for the question it answers.

5. Comment style. Em dashes at IOURequestEditReportCommon.tsx:383-384 and a semicolon joining two sentences at useSearchBulkActions.ts:2725 both trip the plain-comment-style standard. Separately, the nine-line block at 2721-2729 is an essay with a bulleted "Requirements:" list wrapped around a ten-line expression. Extracting the helper in item 2 gives that prose a natural home as a short JSDoc. "It withholds the flow" and "keeps the row honest" read as anthropomorphism — say what the code does.

Verify before acting

Manual memoization. This PR adds three: hasMultipleSubmitters (useMemo), handleAutoReport (useCallback), and autoReportOption (useMemo). React Compiler is enabled, so new hand-written memos are normally removed — but that standard requires confirming the file compiles first, and I could not execute check-compiler.sh in this environment. If both files compile, drop all three. If they don't, matching the file's existing style is the right call.

Either way, one inconsistency stands on its own: areAllManagedCardTransactions is a plain const while hasMultipleSubmitters on the very next line is a useMemo, over the same data. Pick one.

Nits

  • transactionIDToReportActionAndThreadData: '{}' is a hand-written literal where the sibling call builds the same field with JSON.stringify. Use JSON.stringify({}) so it can't drift from the parameter's type.
  • successData and failureData are the same five-line map twice, differing only in the errors value. One small builder.
  • const listHeaderContent = createReportOption ?? autoReportOption is correct only because both options branch on hasMultipleSubmitters in opposite directions, so exactly one can ever be defined. That invariant is invisible at the call site.
  • autoReportOption uses the legacy <MenuItem /> API while createReportOption fifty lines above uses MenuItem.Root / MenuItem.Row.
  • The prop interface is now 25 props with two more behavior flags. Item 1 removes one. Longer term, "multiple submitters" switches four separate behaviors in this component and is arguably a distinct screen rather than a mode.
  • Removing the break from the owner loop means a 500-expense selection now runs 500 getReportOrDraftReport lookups where it used to stop at two. You can still exit once ownerAccountIDs.size > 1 && hasUnknownOwner are both settled.
  • Still no unit tests. The helper from items 2 and 3 would be a pure function of the selection — the cheapest possible thing to cover, and the strongest argument for extracting it.

The comments in this PR do explain why rather than what, which is the harder half and is genuinely well done. The issue is length and placement, not intent.

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

Product change LGTM 👍

@yuwenmemon
yuwenmemon merged commit bf26bfb into Expensify:main Sep 18, 2026
39 of 42 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

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

@OSBotify

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

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/yuwenmemon in version: 9.4.90-0 🚀

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

@MelvinBot

Copy link
Copy Markdown
Contributor

🤖 Yes — one help site article needed updating. Draft PR: Docs updates for E/A#99688 Auto report for multi-submitter expense moves (Expensify/App#101630)

Why: this PR changes what an admin sees in an already-documented flow. Getting-Started-with-the-Spend-Page.md:62-82 tells readers that Move to report offers Create report, an existing report, or Remove from report. For a selection spanning submitters, none of those appear — the panel now shows only Auto report. The article also doesn't explain that Move to report is offered for mixed-submitter selections at all, or why it stays hidden when one expense isn't on a company card.

What the docs PR adds to that one article:

  • ## How to move company card expenses that belong to more than one submitter — the three-step flow, plus what happens to each expense (added to its submitter's most recent draft report, or a new one).
  • ## What to know before using Auto report — the conditions enforced in useSearchBulkActions.ts:2721-2740 and IOURequestEditReportCommon.tsx:371-395: company card expenses only, no per diem / manual distance / odometer distance, online only, no destination choice, and errors surfacing on the expenses.
  • Two FAQ entries and matching metadata keywords.

I verified the UI labels against the live web app before writing: the Spend tab, the Move to report bulk action, and the Create report row in the right-hand panel titled Report. Auto report and its description come from iou.autoReport / iou.autoReportDescription as added here.

Articles I checked and left alone

I searched every file under docs/articles for this flow. Getting-Started-with-the-Spend-Page.md is the only one that documents Move to report or the bulk action menu. The other articles that mention unreported expenses — Using-Reports-in-New-Expensify.md, Managing-Expenses-in-a-Report.md, Create-and-Submit-Reports.md, Configure-Company-Card-Settings.md, and the rest — describe report creation, submission, or card import, and none of them describe the destination picker this PR changes. Nothing in them became wrong, so I didn't touch them.

The article already departs from HELP_AUTHORING_GUIDELINES.md in places (multiple # headings, some ### headings). I matched the surrounding structure rather than restructuring an article this PR didn't affect; my new sections use task-based ## headings per the guidelines.

@daledah, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR Ready for review

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #101767 was identified to be related to this PR.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/lakchote in version: 9.4.90-2 🚀

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