Skip to content

[Due for payment 2026-09-25] Expense-Empty report can be downloaded via reconciliation in workspace chat #100701

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?: N/A - new feature, doesn't exist in prod
If this was caught during regression testing, add the test name, ID and link from BrowserStack: #99309
Email or phone of affected tester (no customers): dreamnitethedragon+vegst@gmail.com
Issue reported by: Applause Internal Team
Bug source: Exploratory - Significant User Experience Deterioration
Device used: Redmi note 10s android 13
App Component: User Settings

Action Performed:

  1. Launch app
  2. Open workspace chat
  3. Create empty report
  4. Tap more - expense - reconciliation option - note download completes
  5. Go to Spend - Report
  6. Long press and select report - export - reconciliation option
  7. Note download fail message is shown

Expected Result:

Empty report must not be able to be downloaded via reconciliation in workspace chat like in spend - report.

Actual Result:

Empty report can be downloaded via reconciliation in workspace chat but show failed in spend - report.

Workaround:

Unknown

Platforms:

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

Screenshots/Videos

Bug7254926_1788950603355.Screenrecorder-2026-09-09-16-08-33-703.mp4

View all open jobs on GitHub

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

Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

  1. 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/9bb1cc460457794b09b9ee525b3c842392bcae81527ef4225e63bed8266aa742

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

    @melvin-bot

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

  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

    @NikkiWines @ZhenjaHorbach you have been assigned to this deploy blocker because you reviewed the offending PR: #99309

  6. melvin-bot commented on Sep 9, 2026

    @melvin-bot

    @dukenv0307 you have been assigned to this deploy blocker because you authored the offending PR: #99309

  7. melvin-bot commented on Sep 9, 2026

    @melvin-bot

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

  8. MelvinBot commented on Sep 9, 2026

    @MelvinBot
    Contributor

    🔍 Investigation Summary

    Classification: Frontend bug
    Causing PR: #99309 - "Add "Reconciliation - All Expenses" export option for NewDot" by @dukenv0307 (High confidence for the new symptom, see caveat below)
    Related Issues: #99172 (the issue #99309 implemented)

    Recommendation: ROLL FORWARD

    The new Reconciliation - All Expenses template leaked out of the Search export menu (its intended scope) into the report header's More > Export submenu, which — unlike Search — has no empty-report guard. The fix is a ~6-line guard in a single hook, so a targeted fix is much cheaper and lower-risk than reverting an approved feature PR.

    Assigned: @dukenv0307 (PR author), @NikkiWines and @ZhenjaHorbach (approving reviewers) — already assigned by Melvin, no change needed
    Labels: No changes. DeployBlockerCash is correct and is kept (App-side fix). DeployBlocker was not present, so nothing was removed.

    📋 Detailed Analysis

    Evidence

    1. #99309 is the only PR on the staging checklist (#100624) that touches reconciliation export. Grepping the checklist body for reconcil/export returns only #99309 (checklist line 45). Merged 2026-09-08T14:50:58Z, immediately before this v9.4.74 report — timeline matches, and the reporter confirms the option does not exist in production.

    2. #99309 added the template behind a defaulted parameter of a shared function, so every caller that omits the argument silently opts in:

    src/libs/actions/Search.ts#L2007-L2016 — getExportTemplates(..., includeReconciliationAllExpenses = isAdminOfCardEnabledPolicy(policy))
    src/libs/actions/Search.ts#L2048-L2053 — where the template is pushed
    src/libs/PolicyUtils.ts#L820-L825 — the new isAdminOfCardEnabledPolicy helper

    getExportTemplates has five callers. Only the Search bulk-actions hook was updated to pass the new flag explicitly (useSearchBulkActions.ts#L1832-L1842). The report-level callers were not, so they inherited the template:

    The PR description scopes the change to "the Search export menu … including from Card statements". The report header menu was neither intended nor tested — the PR's tests cover only getExportTemplates and useSearchBulkActions (tests/unit/SearchActionsTest.ts, tests/unit/hooks/useSearchBulkActionsExportTest.ts), with no coverage of useExportActions.

    3. Production vs staging: staging-only for the reported symptom, as the reporter states. Caveat below on the underlying guard gap.

    Verification

    Confirmed the "download failed" behaviour the reporter saw in Spend > Report is a client-side guard, and that it exists in exactly one of the two flows:

    Guard present (Search / Spend > Report) — useSearchBulkActions.ts#L846-L862:

    const hasOnlyEmptyReports = selectedReports.length > 0 && emptyReports.length === selectedReports.length;
    
    if (hasOnlyEmptyReports) {
        setEmptyReportsCount(emptyReports.length);
        setIsDownloadErrorModalVisible(true);
        return;
    }

    emptyReports is derived from fullReport.transactionCount === 0. That state renders the modal at SearchBulkActionsButton.tsx#L295-L303 with the copy common.downloadFailedEmptyReportDescription → "You can't export an empty report." (en.ts#L212-L215). This is exactly the message in step 7 of the repro.

    Guard absent (workspace chat report header) — useExportActions.ts#L125-L150. Its beginExportWithTemplate only checks isOffline and !moneyRequestReport, then queues the export with transactionIDList: transactionIDs — an empty array for an empty report:

    const beginExportWithTemplate = (templateName, templateType, transactionIDList, exportName, policyID) => {
        if (isOffline) { showOfflineModal(); return; }
        if (!moneyRequestReport) { return; }
        queueExportSearchWithTemplate({...}, true);

    Both flows then hit the same QueueExportSearchWithTemplate command (Search.ts#L1925-L1975), so the only thing producing the divergence in behaviour is the missing client guard.

    Also confirmed the export entry point itself is ungated: SECONDARY_ACTIONS.EXPORT is pushed unconditionally at ReportSecondaryActionUtils.ts#L1137, with no transaction-count check.

    Root Cause

    Two layers:

    1. Regression from Add "Reconciliation - All Expenses" export option for NewDot #99309 (the reported symptom). Adding includeReconciliationAllExpenses as a parameter whose default is isAdminOfCardEnabledPolicy(policy) — rather than false with explicit opt-in — turned the new template on for every existing getExportTemplates caller. The report header and transaction-selection export menus picked it up for free, outside the PR's intended and tested scope.

    2. Pre-existing gap it exposed. The empty-report guard was only ever implemented in useSearchBulkActions, never in useExportActions. So on an empty report the header menu will happily queue any template export (Expense level export, Report level export, custom templates), not just reconciliation. That part is not new in Add "Reconciliation - All Expenses" export option for NewDot #99309 — it just became visible because reconciliation is the template most obviously wrong to run against an empty report. Worth a separate follow-up issue.

    There is a second, product-level question for @trjExpensify / @NikkiWines: Reconciliation - All Expenses reports on workspace-wide card spend, which is why Search computes eligibility across all policies rather than from the selection. Scoping it to a single report via reportIDList: [reportID] is semantically odd regardless of whether the report is empty, so it may not belong in the single-report export menu at all.

    Suggested fix

    Primary (unblocks the deploy), in src/hooks/useExportActions.ts — add the empty-report guard to beginExportWithTemplate, mirroring Search:

    if ((moneyRequestReport.transactionCount ?? transactionIDs.length) === 0) {
        // Match the Search export flow: an empty report has nothing to export
        showDownloadErrorModal();
        return;
    }

    transactionCount is on the Report type (Report.ts#L171). To match the Search copy exactly, showDownloadErrorModal should take an optional prompt so it can use common.downloadFailedEmptyReportDescription instead of the generic common.downloadFailedDescription.

    Optional, if product decides the workspace-wide template shouldn't appear in single-report menus: pass includeReconciliationAllExpenses: false explicitly from useExportActions.ts#L74-L83 and useSelectedTransactionsActions.ts#L396-L404.

    Revert is not recommended: #99309 spans 22 files including 11 language files and three test files, the feature itself works as designed in Search and Card statements, and the defect is one missing guard in an adjacent hook.

    💡 Suggested AI code review rule

    This bug looks like a recurring, mechanically detectable pattern, so it may be worth a coding-standards rule so the AI reviewer catches the next one. Proposed for Expensify/App:

    • Rule: CONSISTENCY-18 Opt new behavior in explicitly at every call site
    • Condition: When a PR adds a new parameter to a shared, multi-caller function, flag it if the default is anything other than the behavior-preserving value (e.g. = someHelper(x) or = a === b rather than = false). Default it off and pass it explicitly at the call sites that want it, so untouched callers keep their current behavior.
    • Why: A defaulted-on parameter silently enables the new behavior in every caller the PR never opened or tested. Here it pushed a workspace-wide card reconciliation export template into the single-report export menus, which lack the empty-report guard the Search menu has — #100701.
    • Search pattern: in the diff, ^\+\s+\w+\s*=\s*(?!(true|false|undefined|null|\[\]|\{\}|['"0-9]))inside an exported function's parameter list; thengrep -rn "(" src/` and check whether every call site was updated in the same PR.
    • Offending code (❌): src/libs/actions/Search.ts#L2007-L2016 and the four call sites that were left untouched (one, two)
    • Fix (✅): default to false and pass explicitly, as useSearchBulkActions.ts#L1832-L1842 already does

    The full rule file, with the ❌/✅ code inline in Expensify/App's coding-standards format, would go in Expensify/App itself. If it's worth adding, a maintainer can open it there as a draft PR.

  9. 15 remaining items

  10. NikkiWines commented on Sep 23, 2026

    @NikkiWines
    Contributor

    i think it's a separate issue since this behavior existed for other templates as well

  11. ZhenjaHorbach commented on Sep 23, 2026

    @ZhenjaHorbach
    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: NA
      Link to the comment on the Issue: NA

    It's more like a new feature

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

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

    Precondition:

    Test:

    1. Open App
    2. Open workspace chat
    3. Create an empty report
    4. Tap More - Export
    5. Click any kind of export
    6. Verify that: An empty report must not be able to be downloaded, and EmptyReportDownloadErrorModal should be shown

    Do we agree 👍 or 👎

  12. added
    Awaiting PaymentAuto-added when associated PR is deployed to production
    and removed on Sep 23, 2026
  13. melvin-bot commented on Sep 24, 2026

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  14. melvin-bot commented on Sep 24, 2026

    @melvin-bot

    Payment Summary

    Resolving PRs:

    BugZero Checklist (@mallenexpensify)

    • I have confirmed assignees, roles, and Upwork contracts look correct
    • I have paid out Upwork contracts / manual NewDot requests
    • [BugZero Assignee] I have created a GH issue for creating/updating the regression test once above steps have been agreed upon
  15. mallenexpensify commented on Sep 26, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor+: @ZhenjaHorbach due $250 via NewDot

    @ZhenjaHorbach can you use the exact C+ checklist? I have automation to key off ## Regression Test Proposal in the comment. Thx.

  16. garrettmknight commented on Nov 5, 2026

    @garrettmknight
    Contributor

    $250 approved for @ZhenjaHorbach

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 productionBugSomething is broken. Auto assigns a BugZero manager.DailyKSv2EngineeringImprovementItem broken or needs improvement.

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions