Skip to content

[Due for payment 2026-11-03] [$175] Spend - Deleted card expenses display with no owner (From column) #103480

Open

Description

@arosiclair

Version Number: 9.5.3-4
Reproducible in staging?: Y
Reproducible in production?: Y
If this was caught during regression testing, add the test name, ID and link from BrowserStack:
Email or phone of affected tester (no customers): N/A
Logs: N/A
Expensify/Expensify Issue URL: N/A
Issue reported by: @flaviadefaria
Slack conversation (hyperlinked to channel name): N/A

Action Performed:

Setup

  1. Log in as policy admin
  2. Invite a member
  3. Workspace settings > Company cards > Add cards
  4. Proceed with United States.
  5. Proceed with Direct feed.
  6. Select Other and then Next. This opens the Plaid UI.
  7. Tap Continue as guest.
  8. Select any bank. I typically do US Bank.
  9. Enter the Plaid test credentials:
    • username: user_good
    • password: pass_good
    • If prompted for a security code, enter: credential_good.
  10. Select the Plaid credit card ending x3333
  11. Follow the remaining steps to connect the card(s) in Plaid.
  12. In the card feed settings, set Allow deleting transactions to disabled
  13. Assign the card to the member
    • Select From the beginning to import historical transactions.

Steps

  1. As the admin, select one of the card expenses in the Spend page
  2. Verify the delete option is available
  3. Delete the card expense
  4. View deleted expenses (Filters > Status > Deleted)
  5. Add the From column if it isn't already present

Expected Result:

The From column displays the owner of the card transaction

Actual Result:

The From column is empty. After undeleting, the column displays correctly

Workaround:

Can the user still use Expensify without this being fixed? Have you informed them of the workaround?

Platforms:

Select the officially supported platforms where the issue was reproduced:

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

Screenshots/Videos

Deleted Undeleted
Image Image

View all open jobs on GitHub

Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @mananjadhav

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 8, 2026
  2. MelvinBot commented on Oct 8, 2026

    @MelvinBot
    Contributor

    Issue Analysis

    Root Cause: A deleted expense has no parent report in Search. Its reportID is the trash ID -1. The App reads the From value from only two places: the expense's IOU report action, or the parent report's owner (SearchUIUtils.ts:1886-1887). The report lookup always fails for -1. The Search response for deleted expenses doesn't include the IOU action, so the App falls back to empty personal details, and the cell renders blank. After you undelete, the real report returns and its ownerAccountID fills From.

    This isn't specific to card expenses. In a web test, a deleted cash expense also showed an empty From cell.

    Recommended Fix: The fix needs a backend change, outside the App repo. Search needs to return the owner for deleted expenses. It can return either the IOU report action (with IOUTransactionID) or an owner account ID on the transaction, plus that owner's personal details. Please escalate to an internal engineer with backend access. Once the backend sends an owner field, the App can add it as a fallback in both From paths (SearchUIUtils.ts:1886 and SearchUIUtils.ts:2936).

    Investigation details

    How From is resolved (App, at 5d3a894)

    • The App looks up the report by the transaction's reportID (SearchUIUtils.ts:1852). For deleted expenses, reportID is CONST.REPORT.TRASH_REPORT_ID ('-1'), so report is undefined.
    • The App looks up the IOU action by transaction ID (SearchUIUtils.ts:1873). That map is filled only from report actions present in the Search snapshot (SearchUIUtils.ts:1509-1514).
    • fromAccountID = reportAction?.actorAccountID ?? report?.ownerAccountID resolves to undefined, so from becomes emptyPersonalDetails (account ID 0). UserInfoCell returns null for account ID 0, so the cell renders blank instead of showing "Hidden".
    • The grouped path uses only the IOU action, with no report fallback (SearchUIUtils.ts:2936).
    • An existing comment already notes that Search doesn't return the IOU action for deleted items (SearchUIUtils.ts:2532-2533).
    • The Search transaction data has no owner or account ID field the App could use instead.

    Web test (cash expense)

    I created a manual expense, submitted it to a workspace, and deleted it. Then I opened Filters > Status > Deleted and grouped by From to show the From column. The group header showed the submitter, but the expense's own From cell was empty. Date, Status, Merchant, Category, and Amount all showed normally.

    Deleted cash expense with empty From cell

    Alternatives considered

    • Card-specific behavior: The App handles card and cash expenses the same way here, and the cash test reproduced the bug.
    • App-only card fallback: The App could use cardList[transaction.cardID].accountID for card expenses. This would leave cash expenses broken. It also depends on the admin's cardList holding members' cards, which I didn't verify.
    • Missing personal details rather than a missing action: This is unlikely. In the cash test, the owner was the signed-in user, whose details are always loaded.

    Not verified: I didn't check the backend Search code or capture a raw Search response. "Search omits the IOU action for deleted expenses" comes from App behavior and the existing code comment.


    Next Steps: Escalate the backend change to an internal engineer, or reply with @MelvinBot <your feedback> to refine this analysis.


    view run · view recording

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

    @melvin-bot

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

  5. changed the title [-]Spend - Deleted card expenses display with no owner (From column)[/-] [+][$175] Spend - Deleted card expenses display with no owner (From column)[/+] on Oct 8, 2026
  6. melvin-bot commented on Oct 8, 2026

    @melvin-bot
  7. yuvrajangadsingh commented on Oct 8, 2026

    @yuvrajangadsingh
    Contributor

    withdrawing this, it went up before the issue had the Help Wanted label. will come back if the label is added.

  8. strzj1317 commented on Oct 8, 2026

    @strzj1317

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    In Workspace > Spend (and Search), when viewing deleted company card expenses (Filters > Status > Deleted), the From column displays empty rather than showing the owner / cardholder of the transaction. Once undeleted, the column displays the owner correctly.

    What is the root cause of that problem?

    In src/libs/SearchUIUtils.ts:1886-1887, getTransactionsSections resolves the from personal details using:

    const fromAccountID = reportAction?.actorAccountID ?? report?.ownerAccountID;
    const from = fromAccountID ? (personalDetailsMap.get(fromAccountID.toString()) ?? emptyPersonalDetails) : emptyPersonalDetails;

    And similarly in getTransactionsSectionsGroupedBy (SearchUIUtils.ts:2936):

    const from = reportAction?.actorAccountID ? (mergedPersonalDetails?.[reportAction.actorAccountID] ?? emptyPersonalDetails) : emptyPersonalDetails;

    When an expense is deleted:

    1. Its reportID becomes CONST.REPORT.TRASH_REPORT_ID ('-1'), so report lookup returns undefined.
    2. The Search snapshot does not include the IOU report actions for deleted transactions, so reportAction is also undefined.
    3. Consequently, fromAccountID evaluates to undefined, resolving from to emptyPersonalDetails (account ID 0), which causes UserInfoCell to render blank.

    However, the deleted transaction still retains its transactionItem.cardID.
    Furthermore, cardList (which maps each company card in the workspace to its assigned cardholder accountID) is already passed to getSections at SearchUIUtils.ts:3680 from useSearchSections.ts:65. But getSections currently drops cardList and does not forward it to getTransactionsSections (SearchUIUtils.ts:3747) or getTransactionsSectionsGroupedBy.

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

    1. In src/libs/SearchUIUtils.ts, update GetTransactionSectionsParams and GetTransactionsSectionsGroupedByParams to accept cardList and onyxPersonalDetailsList:
    type GetTransactionSectionsParams = {
        // ...existing params
        cardList?: OnyxEntry<OnyxTypes.CardList>;
        onyxPersonalDetailsList?: OnyxTypes.PersonalDetailsList;
    };
    1. In getSections (SearchUIUtils.ts:3747), forward cardList and onyxPersonalDetailsList into getTransactionsSections and getTransactionsSectionsGroupedBy:
        return getTransactionsSections({
            data,
            currentSearch,
            currentAccountID,
            currentUserEmail,
            formatPhoneNumber,
            translate,
            isActionLoadingSet,
            bankAccountList,
            rules,
            reportActions,
            queryJSON,
            isAttendeesEnabledForMovingPolicy,
            optimisticTransactionID,
            cardList,
            onyxPersonalDetailsList,
        });
    1. In getTransactionsSections (SearchUIUtils.ts:1886-1887), fall back to the card's assigned accountID for card transactions, and fallback to onyxPersonalDetailsList if personalDetailsMap does not contain the member:
    const fromAccountID = reportAction?.actorAccountID ?? report?.ownerAccountID ?? (transactionItem.cardID ? cardList?.[transactionItem.cardID]?.accountID : undefined);
    const from = fromAccountID
        ? (personalDetailsMap.get(fromAccountID.toString()) ?? onyxPersonalDetailsList?.[fromAccountID] ?? emptyPersonalDetails)
        : emptyPersonalDetails;
    1. In getTransactionsSectionsGroupedBy (SearchUIUtils.ts:2936), apply the same fallback:
    const fromAccountID = reportAction?.actorAccountID ?? report?.ownerAccountID ?? (transactionItem.cardID ? cardList?.[transactionItem.cardID]?.accountID : undefined);
    const from = fromAccountID
        ? (mergedPersonalDetails?.[fromAccountID] ?? onyxPersonalDetailsList?.[fromAccountID] ?? emptyPersonalDetails)
        : emptyPersonalDetails;

    What alternative solutions did you explore? (Optional)

    • Backend modification: Modifying the Search backend endpoint to synthesize an IOU action or include ownerAccountID on deleted transactions. This requires internal backend access and delays the fix. Since the client already has the workspace's assigned cards and cardholders in cardList via Onyx, forwarding cardList to getTransactionsSections completely and cleanly solves this on the client side with zero backend dependencies.
  9. flaviadefaria commented on Oct 9, 2026

    @flaviadefaria
    Contributor

    @mananjadhav thoughts on these proposals?

  10. ashusatyarthi-wq commented on Oct 9, 2026

    @ashusatyarthi-wq

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    In Workspace > Spend (and Search), when viewing deleted company card expenses (Filters > Status > Deleted), the From column is empty instead of displaying the cardholder/owner of the transaction. Once undeleted, the owner is displayed properly.

    What is the root cause of that problem?

    In src/libs/SearchUIUtils.ts:1887-1888 (getTransactionsSections):

    const fromAccountID = reportAction?.actorAccountID ?? report?.ownerAccountID;
    const from = fromAccountID ? (personalDetailsMap.get(fromAccountID.toString()) ?? emptyPersonalDetails) : emptyPersonalDetails;

    And similarly in getReportSections (SearchUIUtils.ts:2936):

    const from = reportAction?.actorAccountID ? (mergedPersonalDetails?.[reportAction.actorAccountID] ?? emptyPersonalDetails) : emptyPersonalDetails;

    When a company card transaction is deleted:

    1. Its reportID becomes CONST.REPORT.TRASH_REPORT_ID ('-1').
    2. There is no parent report for '-1' in data, so report resolves to undefined.
    3. The Search results snapshot does not include money request report actions for deleted transactions, so reportAction is also undefined.
    4. Therefore, fromAccountID evaluates to undefined, and from defaults to emptyPersonalDetails, rendering a blank formattedFrom cell in the table.

    Although the deleted transaction no longer has an active report or report action, it still retains its transactionItem.cardID.
    useSearchSections.ts already retrieves both cardList (personalAndWorkspaceCards) and nonPersonalAndWorkspaceCardList (nonPersonalAndWorkspaceCards) from Onyx and forwards them to getSections. However, getSections currently drops cardList, nonPersonalAndWorkspaceCardList, and onyxPersonalDetailsList instead of passing them into getTransactionsSections (and getReportSections).

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

    1. Update GetTransactionSectionsParams in src/libs/SearchUIUtils.ts

    Add cardList, nonPersonalAndWorkspaceCardList, and onyxPersonalDetailsList to GetTransactionSectionsParams and GetReportSectionsParams:

    type GetTransactionSectionsParams = {
        // ...existing params
        cardList?: OnyxEntry<OnyxTypes.CardList>;
        nonPersonalAndWorkspaceCardList?: OnyxEntry<OnyxTypes.CardList>;
        onyxPersonalDetailsList?: OnyxTypes.PersonalDetailsList;
    };

    2. Forward card lists and personal details in getSections (SearchUIUtils.ts:3788)

    In getSections, forward cardList, nonPersonalAndWorkspaceCardList, and onyxPersonalDetailsList to getTransactionsSections and getReportSections:

        if (type === CONST.SEARCH.DATA_TYPES.EXPENSE_REPORT) {
            return getReportSections({
                // ...existing params
                cardList,
                nonPersonalAndWorkspaceCardList,
                onyxPersonalDetailsList,
            });
        }
    
        // ...
        return getTransactionsSections({
            data,
            currentSearch,
            currentAccountID,
            currentUserEmail,
            formatPhoneNumber,
            translate,
            isActionLoadingSet,
            bankAccountList,
            rules,
            reportActions,
            queryJSON,
            isAttendeesEnabledForMovingPolicy,
            optimisticTransactionID,
            cardList,
            nonPersonalAndWorkspaceCardList,
            onyxPersonalDetailsList,
        });

    3. Add cardholder fallback in getTransactionsSections (SearchUIUtils.ts:1887-1888)

    If reportAction?.actorAccountID and report?.ownerAccountID are undefined, fall back to the assigned card's accountID, and look up details in personalDetailsMap with fallback to onyxPersonalDetailsList:

    const card = transactionItem.cardID ? (cardList?.[transactionItem.cardID] ?? nonPersonalAndWorkspaceCardList?.[transactionItem.cardID]) : undefined;
    const fromAccountID = reportAction?.actorAccountID ?? report?.ownerAccountID ?? card?.accountID;
    const from = fromAccountID
        ? (personalDetailsMap.get(fromAccountID.toString()) ?? onyxPersonalDetailsList?.[fromAccountID] ?? emptyPersonalDetails)
        : emptyPersonalDetails;

    4. Add cardholder fallback in getReportSections (SearchUIUtils.ts:2936)

    Apply the same cardholder fallback for individual transaction rows inside report groups:

    const card = transactionItem.cardID ? (cardList?.[transactionItem.cardID] ?? nonPersonalAndWorkspaceCardList?.[transactionItem.cardID]) : undefined;
    const fromAccountID = reportAction?.actorAccountID ?? report?.ownerAccountID ?? card?.accountID;
    const from = fromAccountID
        ? (mergedPersonalDetails?.[fromAccountID] ?? onyxPersonalDetailsList?.[fromAccountID] ?? emptyPersonalDetails)
        : emptyPersonalDetails;

    What specific scenarios should we cover in automated tests?

    In tests/unit/SearchUIUtilsTest.ts:

    1. Deleted Company Card Transaction:
      • Create a deleted transaction (reportID: '-1', cardID: '123') without a report or report action.
      • Provide cardList with card '123' assigned to accountID: 456.
      • Verify getTransactionsSections sets from.accountID to 456 and correctly populates formattedFrom.
    2. Undeleted Transaction Precedence:
      • Verify that for an active transaction with a valid reportAction or report, reportAction.actorAccountID / report.ownerAccountID takes precedence over card.accountID.
    3. Non-Personal / Workspace Card Fallback:
      • Verify that when the card is in nonPersonalAndWorkspaceCardList, from resolves to the cardholder's details correctly.
  11. ashusatyarthi-wq commented on Oct 9, 2026

    @ashusatyarthi-wq

    Contributor details
    Your Expensify account email: ashusatyarthi@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~010e53044b5828fe60

  12. 19 remaining items

  13. mananjadhav commented on Oct 13, 2026

    @mananjadhav
    Collaborator

    Reviewing the PR.

  14. mananjadhav commented on Oct 14, 2026

    @mananjadhav
    Collaborator

    @flaviadefaria @arosiclair The issue marks the platform Web only. Do we want to show From on the mobile apps too. But unlike the Wide table, the From is hidden in all deleted expenses (not just card expense).

    Image
  15. flaviadefaria commented on Oct 15, 2026

    @flaviadefaria
    Contributor

    @flaviadefaria @arosiclair The issue marks the platform Web only. Do we want to show From on the mobile apps too.

    Yes, we should make the change also on mobile.

    But unlike the Wide table, the From is hidden in all deleted expenses (not just card expense).

    I'm not 100% sure that I understood this comment. Normally when viewing expenses on mobile the profile photo/icon and their name display the merchant name. I'd expect us to use this same pattern in the Delete filter for mobile. Let me know if this makes sense or if you have any questions.

  16. mananjadhav commented on Oct 15, 2026

    @mananjadhav
    Collaborator

    @flaviadefaria Just to clarify that we want to show From in all the deleted expenses? (which currently it's not shown). I'll get it fixed.

  17. mananjadhav commented on Oct 15, 2026

    @mananjadhav
    Collaborator

    Deleted expenses now show From only when the App can still tell who they belonged to, which is card expenses. Deleting an expense removes its report, so Search no longer has the submitter. A card expense can still be matched to the cardholder, and that person is shown as From on web and mobile. Cash, distance, and other non-card expenses have nothing left to look up, so From stays blank until Search returns the owner.

    ios-delete-exp-from-user.mov
  18. flaviadefaria commented on Oct 19, 2026

    @flaviadefaria
    Contributor

    That’s fine, since admins can only delete other users’ credit card expenses. And the most important thing is that they can easily identify which expenses belonged to those users.

  19. mananjadhav commented on Oct 23, 2026

    @mananjadhav
    Collaborator

    The PR is reviewed. @arosiclair All yours.

  20. changed the title [-][$175] Spend - Deleted card expenses display with no owner (From column)[/-] [+][Due for payment 2026-11-03] [$175] Spend - Deleted card expenses display with no owner (From column)[/+] on Oct 27, 2026
  21. melvin-bot commented on Oct 27, 2026

    @melvin-bot

    @mananjadhav

    The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.23-3 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:

    If no regressions arise, payment will be issued on 2026-11-03. 🎊

    The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]

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.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions