Skip to content

Web - Report - Drag multiple receipts later does not appear in the report #100698

Closed

Description

@applause-bot

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


Version Number: v9.4.74-0
Reproducible in staging?: Yes
Reproducible in production?: No
If this was caught during regression testing, add the test name, ID and link from BrowserStack: #98847
Email or phone of affected tester (no customers): leungbratt6+09093@gmail.com
Issue reported by: Applause Internal Team
Bug source: Pull Request QA execution
Device used: Windows 10/Chrome
App Component: Chat Report View

Action Performed:

  1. Open the New Dot app
  2. Navigate to the Workspace chat
  3. Create an empty report and drag a receipt into it
  4. Drag multiple receipts into the same report again

Expected Result:

New receipts appear as expenses in the same report like in Prod

Actual Result:

The attached receipt for the first expense has been changed to another receipt

Workaround:

Unknown

Platforms:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome Safari

Screenshots/Videos

Bug7254863_1788944991548._98847_1.1.mp4

View all open jobs on GitHub

Applause Internal Information
Exported by: Julia Ponikarchuk
Bug ID: 7254863

Activity

  1. added
    DeployBlockerCashThis issue or pull request should block deployment
    BugSomething is broken. Auto assigns a BugZero manager.
    on Sep 9, 2026
  2. applause-bot commented on Sep 9, 2026

    @applause-bot
    Author

    While Applause doesn't embed attachments for this bug, here are the links for quick view:
    https://platform.applause.com/services/links/v1/external/5d7bd4abbfe8d5aa51ae2bde33c0ac2134a8aafa0dbea8dea430e9293d01b720

  3. melvin-bot commented on Sep 9, 2026

    @melvin-bot

    You have been assigned to this deploy blocker because you recently merged this PR: #98847

  4. github-actions commented on Sep 9, 2026

    @github-actions
    Contributor

    👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:

    1. Identify the pull request that introduced this issue and revert it.
    2. Find someone who can quickly fix the issue.
    3. Fix the issue yourself.
  5. melvin-bot commented on Sep 9, 2026

    @melvin-bot

    @blimpich @ikevin127 you have been assigned to this deploy blocker because you reviewed the offending PR: #98847

  6. melvin-bot commented on Sep 9, 2026

    @melvin-bot

    💬 A slack conversation has been started in #expensify-open-source

  7. MelvinBot commented on Sep 9, 2026

    @MelvinBot
    Contributor

    🔍 Investigation Summary

    Classification: Frontend bug
    Causing PR: None found in this release. #98847 - "fix: remove unsafe assertions from receipt and settings flows" by @KJ21-ENG is refuted (Low confidence — it does not touch this code path)
    Related Issues: Deploy checklist #100624

    Recommendation: ROLL FORWARD

    The behavior comes from the report composer's "replace receipt" mode, which is byte-identical between production and staging, so a revert of #98847 (or of anything else on the checklist) cannot fix it — a small targeted change in useReceiptDrop/useShouldAddOrReplaceReceipt is needed. Please also re-verify the "Reproducible in production: No" result, since no code in this path changed this release.

    Assigned: No new assignees — no causing PR was identified at medium or higher confidence, so the existing #98847 assignment is not corroborated by code evidence.
    Labels: No changes. DeployBlockerCash kept (the fix is in Expensify/App); DeployBlocker was not present.

    📋 Detailed Analysis

    Evidence

    #98847 is not on this code path. Its only receipt-related change is to src/hooks/useReceiptScanDrop.tsx, and that hook is imported by exactly three screens — src/pages/home/HomePage.tsx, src/pages/Search/SearchPageWide.tsx and src/pages/Search/SearchPageNarrow/index.tsx (via src/components/ReceiptScanDropZone). It is the global create drop handler. Dropping onto a report goes through ReportScreen → ReportActionCompose.DropZone (ComposerDropZone) → useReceiptDrop, a different hook that #98847 never touched.

    Its actual diff there is also incapable of producing this symptom: it adds a files.every((file) => file instanceof Blob) type guard, an if (!initialTransaction) return; early exit, and replaces two as Partial<Transaction> casts. Nothing in it changes which transaction a receipt is attached to, and it always creates a brand-new draft per file exactly as before.

    Nothing else in the release touches the report drop path either. I pulled the file list of all 51 App PRs on the checklist (#100624); none of them modify useReceiptDrop.ts, useShouldAddOrReplaceReceipt.ts, ComposerDropZone.tsx, DualDropZone.tsx, useFilesValidation.tsx or MoneyRequestReportUtils.ts. The blob SHAs for all six files are identical on production and staging (e.g. useShouldAddOrReplaceReceipt.ts is 9036d2c on both), so this path ships unchanged in v9.4.74. The useShouldAddOrReplaceReceipt logic itself has not been meaningfully edited since April/May 2026.

    That makes this a pre-existing bug that surfaced during PR QA of #98847, not a regression from it. The "Reproducible in production: No" result is worth re-running, because the client code that produces the symptom is identical in production.

    Verification

    Affected files (permalinks at the #98847 merge commit 7a0eec8, identical on production):

    Confirmation that #98847 does not modify any of these: its changed files are src/hooks/useReceiptScanDrop.tsx, src/libs/actions/BankAccounts.ts, src/pages/ReimbursementAccount/USD/*, src/pages/Search/SearchSavePage.tsx, src/pages/settings/Report/ReportDetailsColumnsPage.tsx, src/pages/workspace/accounting/certinia/* and one new test file.

    Root Cause

    The report composer switches its receipt drop zone into single-receipt replace mode as soon as the report contains exactly one expense:

    // useShouldAddOrReplaceReceipt.ts:34,41
    const isSingleTransactionView = !!transaction && !!reportTransactions && reportTransactions.length === 1;
    const shouldAddOrReplaceReceipt = (isTransactionThreadView || isSingleTransactionView) && canEditReceipt;

    Step 3 of the repro (drop one receipt into the empty report) creates that one expense. On step 4 the report now has reportTransactions.length === 1, so shouldAddOrReplaceReceipt flips to true (the drop zone label even changes from "Scan receipts" to "Add receipt"/"Replace receipt"). useReceiptDrop then takes the replace branch, which discards every dropped file except the first and overwrites the existing expense's receipt:

    // useReceiptDrop.ts:117-124 (drop handler)
    if (shouldAddOrReplaceReceipt && transactionID) {
        const file = files.at(0);
        ...
        validateFiles([file], items);   // only ONE file survives
        return;
    }
    
    // useReceiptDrop.ts:57-69 (post-validation)
    if (shouldAddOrReplaceReceipt && transactionID) {
        replaceReceipt({transaction, file: files.at(0) as File, source, ...});
        return;                          // no new transactions are ever created
    }

    That is exactly the reported behavior: the first expense's receipt is swapped for one of the newly dropped receipts, and the remaining receipts never become expenses. The multi-file initMoneyRequest + buildOptimisticTransactionAndCreateDraft loop below it (useReceiptDrop.ts:72-95), which is what should create one draft transaction per dropped file, is never reached.

    Suggested targeted fix — only treat a drop as a receipt replacement when a single file is dropped, unless we are genuinely in a transaction thread (where adding expenses is not an option). Return isTransactionThreadView from useShouldAddOrReplaceReceipt and gate both branches in useReceiptDrop:

    const shouldReplaceExistingReceipt = shouldAddOrReplaceReceipt && !!transactionID && (isTransactionThreadView || files.length === 1);

    then use shouldReplaceExistingReceipt in place of shouldAddOrReplaceReceipt && transactionID at both useReceiptDrop.ts:57 and useReceiptDrop.ts:117, so a multi-file drop on a one-expense report falls through to the "create one expense per receipt" loop.

  8. MelvinBot commented on Sep 9, 2026

    @MelvinBot
    Contributor

    ✅ Independent verification of the analysis above

    I re-checked the three load-bearing claims from my investigation comment directly against the code. All three hold.

    Verdict: don't revert. #98847 is not the cause, and this is not a v9.4.74 regression.

    Root cause: once a report holds exactly one expense, the composer's drop zone flips into single-receipt replace mode. The next drop discards every file but the first and overwrites the existing expense's receipt instead of creating new expenses — exactly the reported symptom.

    Recommended fix (roll forward): only replace when the drop is genuinely single-receipt. Return isTransactionThreadView from useShouldAddOrReplaceReceipt and gate both branches of useReceiptDrop on shouldAddOrReplaceReceipt && !!transactionID && (isTransactionThreadView || files.length === 1), so a multi-file drop on a one-expense report falls through to the create-per-receipt loop.

    What I verified, and how

    1. #98847 is not on this code path. Its only receipt change is to src/hooks/useReceiptScanDrop.tsx. Grepping every importer of that hook:

    src/components/ReceiptScanDropZone/index.tsx  →  used only by:
      src/pages/home/HomePage.tsx
      src/pages/Search/SearchPageWide.tsx
      src/pages/Search/SearchPageNarrow/index.tsx
    

    No report screen imports it. The report drop instead runs ReportActionCompose → ComposerDropZone.tsx:121 → useReceiptDrop, a hook #98847 never touched.

    2. The path is byte-identical on production and staging. Blob SHAs via the GitHub API:

    File production staging
    useReceiptDrop.ts 79c7e7b 79c7e7b
    useShouldAddOrReplaceReceipt.ts 9036d2c 9036d2c

    Nothing in this release changed the code that produces the symptom, so no revert on the checklist can fix it. Please re-run the "Reproducible in production: No" check — the client code is the same in production, so I'd expect it to reproduce there too.

    3. The mechanism, read from the current files. The gate flips as soon as the report has one transaction — which step 3 of the repro creates:

    useShouldAddOrReplaceReceipt.ts:34-41

    const isSingleTransactionView = !!transaction && !!reportTransactions && reportTransactions.length === 1;
    ...
    const shouldAddOrReplaceReceipt = (isTransactionThreadView || isSingleTransactionView) && canEditReceipt;

    Then the drop handler throws away every file except the first, and the post-validation branch overwrites the existing transaction's receipt and returns — so the initMoneyRequest + buildOptimisticTransactionAndCreateDraft loop that would create one expense per file is never reached:

    useReceiptDrop.ts:117-124 — validateFiles([file], items); return;
    useReceiptDrop.ts:57-70 — replaceReceipt({transaction, file: files.at(0) as File, ...}); return;

    Live repro was attempted but could not be run

    I tried to confirm the behavior in a real browser session and could not, for two independent reasons:

    • Native OS file drag-and-drop is out of scope for the web automation tooling — there's no drag or upload primitive, and file drops require real File objects fed in from outside the page. Substituting the "+" / attach-receipt menu button would exercise a different code path, so I didn't fall back to it.
    • The browser session also never painted (blank page, 0 accessibility nodes) despite the dev server serving 200 for index.html and all three bundles.

    So the analysis above rests on code evidence only — it has not been confirmed by a live reproduction.


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


    view run · view recording

  9. KJ21-ENG commented on Sep 9, 2026

    @KJ21-ENG
    Contributor

    🔍 Investigation Summary

    Classification: Frontend bug Causing PR: None found in this release. #98847 - "fix: remove unsafe assertions from receipt and settings flows" by @KJ21-ENG is refuted (Low confidence — it does not touch this code path) Related Issues: Deploy checklist #100624

    Recommendation: ROLL FORWARD

    The behavior comes from the report composer's "replace receipt" mode, which is byte-identical between production and staging, so a revert of #98847 (or of anything else on the checklist) cannot fix it — a small targeted change in useReceiptDrop/useShouldAddOrReplaceReceipt is needed. Please also re-verify the "Reproducible in production: No" result, since no code in this path changed this release.

    Assigned: No new assignees — no causing PR was identified at medium or higher confidence, so the existing #98847 assignment is not corroborated by code evidence. Labels: No changes. DeployBlockerCash kept (the fix is in Expensify/App); DeployBlocker was not present.

    📋 Detailed Analysis

    It's not caused by #98847

    cc: @blimpich @ikevin127

  10. m-natarajan commented on Sep 9, 2026

    @m-natarajan

    Not repro in latest staging v9.4.74-1

    bandicam.2026-09-09.17-41-40-777.mp4
  11. added and removed
    DeployBlockerCashThis issue or pull request should block deployment
    on Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2Engineering

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions