Skip to content

[$175] Approved date column is not populated for that expense that has a proper approved date. #104768

Open

Description

@m-natarajan

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


Version Number: 9.5.16-1
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):
Logs: https://stackoverflow.com/c/expensify/questions/4856
Expensify/Expensify Issue URL:
Issue reported by: @FitseTLT
Slack conversation (hyperlinked to channel name): #Expensify Bugs

Action Performed:

  1. Open spend > Expenses and add Approved column on the table and also filter with status Paid
  2. Find an expense that is approved and take the report id of the parent expense report.
  3. Now go to the expense report in inbox
  4. Clear cache and directly go to the inbox to open the expense report before opening spend page.
  5. Go to Spend page

Expected Result:

Approved date column populated

Actual Result:

Approved date column is empty and not populated

Workaround:

Unknown

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

2026-10-21.18-26-32.mp4
Recording.3731.mp4

View all open jobs on GitHub

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

Activity

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

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    On the Expenses view, Search builds each row from the live Onyx report when one exists, not from the Search snapshot report. Opening the report in Inbox first creates that live report.

    The live report from OpenReport doesn't carry approved. The App never sets that field itself; only the server sends it.

    getApprovedDate only falls back to the latest APPROVED report action when the report's status is APPROVED. A paid report has status REIMBURSED, so the function returns '' and the column is blank. The UI fallback transactionItem.approved ?? report?.approved doesn't help, because '' is not nullish.

    Expense-report rows are not affected because they read the snapshot report.

    This regression came from two PRs working together:

    Code references

    Not verified: I didn't confirm the OpenReport response shape. Comparing the report_<id> payload from OpenReport with the one from Search in a network trace would confirm it.

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

    In both transaction-row paths (getTransactionsSections and the transaction branch of getReportSections), make the missing date fall back to the snapshot:

    1. When the live report has no approved, use the snapshot report's approved before calling getApprovedDate.
    2. Apply the same fallback to submitted for getSubmittedDate, which has the same gap.

    getApprovedDate already returns blank for Open and Submitted reports, so a stale snapshot date can't reappear after unapproving.

    Add a unit test with a paid report: the live report has no approved, and the snapshot report has it. The Approved column should show the snapshot date.

    What alternative solutions did you explore? (Optional)

    You could also let getApprovedDate use the latest APPROVED action for REIMBURSED (and approved-then-closed) reports. Once a report is paid, the latest approval is the final one.

    On its own this is less reliable:

    • OpenReport loads only the newest page of actions.
    • Live actions replace snapshot actions entirely, so the approval action may be missing on busy reports.

    Use it only as an extra safeguard after the snapshot fallback.


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

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

    @melvin-bot

    Current assignee @FitseTLT is eligible for the External assigner, not assigning anyone new.

  5. changed the title [-]Approved date column is not populated for that expense that has a proper approved date.[/-] [+][$175] Approved date column is not populated for that expense that has a proper approved date.[/+] on Oct 22, 2026
  6. Abdallah-1551 commented on Oct 22, 2026

    @Abdallah-1551

    Proposal

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

    On Spend > Expenses, the optional Approved column is empty for an expense that was approved and has a valid approved date. It happens when the user clears the cache, opens the parent expense report from the Inbox first, and only then goes to the Spend page. The Approved date should show the same way it does when the Spend page is opened first.

    What is the root cause of that problem?

    Kimi's analysis was empty and the thread has no comments, so I could not confirm the cause against the source. This is my best reading of the repro, and the first step in the proposal is to verify it.

    • The approved date is only available in the search snapshot (SearchTransaction.approved), or in report actions that the snapshot or search response supplies. It is not a field on the normal transactions_ entry that OpenReport writes to Onyx.
    • Opening the report first puts a partial copy of the transaction, report and report actions into Onyx, from OpenReport/GetOlderActions. That copy has no approved value and may not contain the APPROVED action.
    • The Search list-building code (SearchUIUtils.getTransactionsSections / getTransactionItemCommonFormattedProperties) then appears to prefer or merge that live Onyx data over the snapshot data. The approved date is computed from data that does not have it, or is replaced by undefined. The column then renders empty.
    • With a clean cache and the Spend page opened first, only the snapshot data exists, so the column is populated. This matches the order dependence in the repro.

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

    1. Confirm the cause. After the repro, inspect snapshot_<hash>.data.transactions_<id>.approved and the local transactions_<id> in Onyx. If the snapshot has the date and the UI does not, the cause is the override described above.

    2. Make the snapshot the source of truth for approved. In SearchUIUtils (transaction item creation), do not let local Onyx data overwrite snapshot-only fields:

    // before: local data spread last, so it can clobber snapshot-only fields
    const transactionItem = {
        ...snapshotTransaction,
        ...localTransaction,
    };
    
    // after: snapshot-only fields are resolved explicitly
    const transactionItem = {
        ...localTransaction,
        ...snapshotTransaction,
        approved: snapshotTransaction.approved ?? getApprovedDateFromActions(reportActions, reportID) ?? localTransaction?.approved,
        submitted: snapshotTransaction.submitted ?? localTransaction?.submitted,
    };

    getApprovedDateFromActions is a small helper that returns the created date of the last CONST.REPORT.ACTIONS.TYPE.APPROVED action for the report. It is only a fallback for when the snapshot does not have the field.

    1. Guard Onyx updates against clearing snapshot fields. If the cause is an Onyx update from OpenReport merging approved: null/undefined into the snapshot, drop those keys before merging (or skip them in the snapshot-update code path) so an existing value is never erased by a partial payload.

    2. Make the column renderer tolerant. In the Approved column component (TransactionItemRow / DateCell), format item.approved and render an empty cell only when it is truly absent.

    What alternative solutions did you explore? (Optional)

    • Force a Search refetch whenever the Spend page mounts. This hides the symptom but costs extra network calls and flickers. It also leaves the merge/precedence bug in place.
    • Make OpenReport return approved on the transaction. This needs a backend change and a new field on the Transaction type. It is a heavier fix and is not needed if the snapshot already carries the value.
    • **Cache the approved date in
  7. melvin-bot commented on Oct 22, 2026

    @melvin-bot
  8. FitseTLT commented on Oct 22, 2026

    @FitseTLT
    Contributor

    @Abdallah-1551 donot post proposals before Help Wanted label is applied

  9. FitseTLT commented on Oct 22, 2026

    @FitseTLT
    Contributor

    @MelvinBot can u access the slack thread this bug was reported if not I will post you here the discussion we had.

  10. camesenin commented on Oct 22, 2026

    @camesenin

    Proposal

    Problem

    In Spend > Expenses, when filtering by Paid status (or looking at an expense whose parent report was approved and reimbursed), the Approved column is empty if the user cleared their cache and opened the parent expense report from the Inbox before navigating to the Spend page.

    Root Cause

    In src/libs/SearchUIUtils.ts:

    1. When building rows for the expenses table (getTransactionsSections and getReportActionsSections), SearchUIUtils computes the approved date via getApprovedDate(report, actions).
    2. In lines 1874–1875:
    const transactionItem = data[key];
    const report = getReportOrDraftReport(transactionItem.reportID) ?? data[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`];

    When the user visits the expense report from the Inbox first, OpenReport populates the live report in Onyx (reports_<id>).
    3. The server payload from OpenReport does not populate report.approved (this is only provided in Search snapshot results).
    4. getApprovedDate (lines 2723–2732):

    function getApprovedDate(reportItem: OnyxTypes.Report, actions: OnyxTypes.ReportAction[]): string {
        if (reportItem.statusNum === CONST.REPORT.STATUS_NUM.OPEN || reportItem.statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED) {
            return '';
        }
        const reportApproved = reportItem.approved ?? '';
        const actionApproved = reportItem.statusNum === CONST.REPORT.STATUS_NUM.APPROVED ? (findActionByCreated(actions, [CONST.REPORT.ACTIONS.TYPE.APPROVED], 'latest')?.created ?? '') : '';
        return reportApproved >= actionApproved ? reportApproved : actionApproved;
    }
    1. Notice line 2730:
      actionApproved is strictly gated on reportItem.statusNum === CONST.REPORT.STATUS_NUM.APPROVED.
      When a report is paid/reimbursed, reportItem.statusNum is CONST.REPORT.STATUS_NUM.REIMBURSED (or closed). Because reportItem.approved was not on the live report object from OpenReport and reportItem.statusNum !== CONST.REPORT.STATUS_NUM.APPROVED, actionApproved resolves to ''. Thus getApprovedDate returns '', leaving the Approved date column completely blank despite the report having a valid APPROVED report action.

    Furthermore, in getTransactionsSections:

    const report = getReportOrDraftReport(transactionItem.reportID) ?? data[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`];

    If the live report in Onyx lacks approved, it should also fall back to the snapshot's report data[${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}]?.approved.

    Solution

    1. In src/libs/SearchUIUtils.ts:
      • In getApprovedDate: A report that reached CONST.REPORT.STATUS_NUM.REIMBURSED (or settled/closed) was already approved prior to reimbursement. Allow findActionByCreated to retrieve the APPROVED action timestamp when statusNum === CONST.REPORT.STATUS_NUM.APPROVED || statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED.
      • Also allow fallback to snapshot report's approved timestamp if the live report does not have approved set.
    function getApprovedDate(reportItem: OnyxTypes.Report, actions: OnyxTypes.ReportAction[]): string {
        if (reportItem.statusNum === CONST.REPORT.STATUS_NUM.OPEN || reportItem.statusNum === CONST.REPORT.STATUS_NUM.SUBMITTED) {
            return '';
        }
        const reportApproved = reportItem.approved ?? '';
        const isApprovedOrSettled = reportItem.statusNum === CONST.REPORT.STATUS_NUM.APPROVED || reportItem.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED;
        const actionApproved = isApprovedOrSettled ? (findActionByCreated(actions, [CONST.REPORT.ACTIONS.TYPE.APPROVED], 'latest')?.created ?? '') : '';
        return reportApproved >= actionApproved ? reportApproved : actionApproved;
    }
    1. Additionally, in getTransactionsSections and getReportActionsSections:
      When retrieving the report object for property extraction, if the live report in Onyx does not carry approved, fall back to the snapshot report:
    const snapshotReport = data[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`];
    const report = getReportOrDraftReport(transactionItem.reportID) ?? snapshotReport;
    const effectiveReport = report && !report.approved && snapshotReport?.approved ? {...report, approved: snapshotReport.approved} : report;

    And pass effectiveReport to getApprovedDate(effectiveReport, actions).

    Alternative Solutions Explored

    • Adding approved field to OpenReport response: Requires backend modifications and would still leave offline/historical local reports without the date. Computing from the APPROVED action and Search snapshot data is already the pattern used by the client for submitted and offline state synchronization.
  11. MelvinBot commented on Oct 22, 2026

    @MelvinBot
    Contributor

    No, I can't read that Slack thread from here. Please post the discussion in this issue.

    I'll compare it with my proposal, which says the live report from OpenReport has no approved date. These details would help most:

    • Any reproduction finding that contradicts that root cause.
    • Any network trace showing whether the OpenReport response includes approved.
  12. FitseTLT commented on Oct 22, 2026

    @FitseTLT
    Contributor

    @MelvinBot

    this is what I found while reviewing #103956 but the problem seems to be from main. And as the reproduction is not straight-forward I chose to dig it myself and here is my finding.
    Bug:Approved column shows blank for reports that were genuinely approved and the BE has properly populated the approved field in snapshot data.

    Cause: the report object used to compute these columns prefers a global, app-wide live report cache over Search's own response data:

    getReportOrDraftReport(id) ?? data[report_id]

    const report = getReportOrDraftReport(transactionItem.reportID) ?? data[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`];

    const report = (getReportOrDraftReport(transactionItem.reportID) ?? data[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`]) as OnyxTypes.Report | undefined;

    Since approved is only populated on Search's response but aren't reliably present on that live onyx key, any report already cached elsewhere (report opened in inbox before spend page) in the app silently loses Search's correct value. (edited)

  13. MelvinBot commented on Oct 22, 2026

    @MelvinBot
    Contributor

    Thanks, FitseTLT. Your finding matches the root cause in my proposal. Search prefers the live report, which lacks approved, over the snapshot report, which has it.

    One detail explains why the column is blank and not just stale. getApprovedDate only falls back to the APPROVED report action when the report status is APPROVED. A paid report gets no fallback, so it returns ''.

    The fix is the same either way. At both lines you linked, fill approved and submitted from the snapshot report when the live report doesn't have them. A broader option is to layer the live report over the snapshot report. That keeps every server-only field, but it may also keep stale snapshot values, so it needs more testing.

    Code references
  14. melvin-bot commented on Oct 22, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  15. Anthgg commented on Oct 22, 2026

    @Anthgg

    Proposal

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

    On the Spend > Expenses table, when filtering by status (such as Paid) and adding the "Approved" column, opening an expense report from Inbox before opening the Spend page causes the Approved date column for that expense to be blank/unpopulated, even though the expense was legitimately approved and paid.

    What is the root cause of that problem?

    There are two interconnected root causes in src/libs/SearchUIUtils.ts:

    1. Live report preference drops server-computed snapshot metadata:
      In getTransactionsSections (line 1878) and getReportActionsSections (line 2951), the code resolves the report using:

      const report = getReportOrDraftReport(transactionItem.reportID) ?? data[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`];

      When the report has been opened in Inbox, OpenReport populates the live Onyx report report_<id>. However, OpenReport only returns standard report fields and does not include server-calculated search snapshot fields like approved or submitted. Because getReportOrDraftReport returns this live report, the fallback to data[${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}] is bypassed, and report.approved becomes undefined.

    2. getApprovedDate restricts report action fallback strictly to APPROVED status:
      In getApprovedDate (lines 2728-2733):

      const reportApproved = reportItem.approved ?? '';
      const actionApproved = reportItem.statusNum === CONST.REPORT.STATUS_NUM.APPROVED ? (findActionByCreated(actions, [CONST.REPORT.ACTIONS.TYPE.APPROVED], 'latest')?.created ?? '') : '';
      return reportApproved >= actionApproved ? reportApproved : actionApproved;

      When a report has already been reimbursed/settled (statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED), statusNum === CONST.REPORT.STATUS_NUM.APPROVED evaluates to false. If reportItem.approved was omitted or cleared by the live report, actionApproved evaluates to '', resulting in getApprovedDate returning ''.

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

    1. Merge live report with search snapshot metadata in SearchUIUtils.ts:
      In both getTransactionsSections and getReportActionsSections, preserve server-computed snapshot fields (approved, submitted) when overlaying live Onyx updates:

      const liveReport = getReportOrDraftReport(transactionItem.reportID);
      const snapshotReport = data[`${ONYXKEYS.COLLECTION.REPORT}${transactionItem.reportID}`];
      const report = liveReport
          ? {...snapshotReport, ...liveReport, approved: liveReport.approved ?? snapshotReport?.approved, submitted: liveReport.submitted ?? snapshotReport?.submitted}
          : snapshotReport;
    2. Allow approved date fallback on reimbursed/settled reports in getApprovedDate:
      In getApprovedDate (src/libs/SearchUIUtils.ts), allow reports that have reached terminal paid/reimbursed status to also reference their historical APPROVED report action when reportItem.approved is missing:

      const isApprovedOrSettled = reportItem.statusNum === CONST.REPORT.STATUS_NUM.APPROVED || reportItem.statusNum === CONST.REPORT.STATUS_NUM.REIMBURSED;
      const actionApproved = isApprovedOrSettled ? (findActionByCreated(actions, [CONST.REPORT.ACTIONS.TYPE.APPROVED], 'latest')?.created ?? '') : '';

    What alternative solutions did you explore? (Optional)

    • Modifying backend OpenReport API: Rejected because backend API contract changes take longer to deploy and do not repair existing cached client Onyx states.
    • Relying solely on APPROVED report action fallback without preserving snapshot: Rejected because Search queries with limit do not fetch full report action histories for all search items, making snapshot preservation essential.

    Contributor details:
    GitHub: @Anthgg

  16. FitseTLT commented on Oct 27, 2026

    @FitseTLT
    Contributor

    Reviewing tomorrow

  17. FitseTLT commented on Oct 29, 2026

    @FitseTLT
    Contributor

    @MelvinBot I think we should have a more wholistic solution not only specific fields can u investigate on why/when the live data was added with the snapshot as the fallback.

  18. MelvinBot commented on Oct 29, 2026

    @MelvinBot
    Contributor

    The live-first lookup was a deliberate review request, added on 2026-04-28 in Fall back to Onyx when search snapshot is missing report data. Its goal was to fix rows whose report was missing from a limited Search response. Nobody checked whether the live report has every field the snapshot has, and it doesn't. That gap causes this bug.

    Holistic fix: add one helper in SearchUIUtils and use it at every live-first site. The helper layers the live report over the snapshot report:

    function getSearchRowReport(reportID, data) {
        const liveReport = getReportOrDraftReport(reportID);
        const snapshotReport = data[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
        return liveReport && snapshotReport ? {...snapshotReport, ...liveReport} : (liveReport ?? snapshotReport);
    }
    • Live values still win, so optimistic and offline edits keep showing.
    • Snapshot values fill fields the live report never gets (approved, submitted, paid, declined), with no per-field list to maintain.
    • This also covers point 2 of the original review: "Do this consistently for all data, not only specific fields."

    Trade-off: if the server deletes a key from the live report (merges null), the stale snapshot value shows until the next Search refresh. That risk is smaller than today's, where every Search-only field is lost.

    Timeline
    1. 2025-04: Add New Task Section to Reports started the pattern in getTasksSections. Onyx came first, with the snapshot as a fallback. The PR gives no reason.
    2. 2026-03: fix: Updated task title isn't reflected in Reports > Chats in Offline and infinite loading when click on the time of the task in Reports > Chats switched getReportActionsSections from snapshot-only to live-first so offline task edits appear in Search.
    3. 2026-04: Fall back to Onyx when search snapshot is missing report data fixed [Due for payment 2026-05-12] [$250] Reports - Unable to open expense after applying "Limit Results" filter.. The first commit was snapshot-first, as the title says. luacmartins then requested live-first "like we do for todos", and commit a70696531e0 flipped all three sites before merge.
    4. 2026-06: feat: add three report custom columns to expense tables copied the pattern for custom columns.
    5. 2026-08: optimistically update some columns in offline built getApprovedDate, getSubmittedDate and getPaidDate on the live-first report. These read the report action only for APPROVED status, so a paid report shows a blank date.

    Inconsistency: Resolve reports snapshot-first in Search bulk Delete and Reject went the other way. Bulk actions use the snapshot first (Search.ts:173-181), while row-building uses the live report first.

    Sites to switch to the helper

    Leave SearchUIUtils.ts:2625 live-only. It deliberately avoids a stale snapshot thread that would create a duplicate IOU action.

    Not verified: I didn't capture an OpenReport payload. The claim that it omits approved, submitted and paid comes from this issue and [Due for payment 2026-08-25] Spend - First approver field does not show the approver after approving report offline.


    Next Steps: Reply with @MelvinBot implement this to create a draft PR, or @MelvinBot <your feedback> to refine this analysis.


    view run

  19. duongynhi000005-oss commented on Nov 1, 2026

    @duongynhi000005-oss
  20. FitseTLT commented on Nov 2, 2026

    @FitseTLT
    Contributor

    @MelvinBot implement

  21. MelvinBot commented on Nov 2, 2026

    @MelvinBot
    Contributor

    ⏱️ Ran out of time before I could finish. This usually means the task was too complex or I got stuck exploring. Please try again with a more specific request or post in #melvinbot-ai.

    Run details

  22. FitseTLT commented on Nov 3, 2026

    @FitseTLT
    Contributor
  23. MelvinBot commented on Nov 3, 2026

    @MelvinBot
    Contributor

    🤖 Draft PR created: Keep Search-only report fields when the live report is already in Onyx
    Please mark it as "Ready for review" or ask me to do that for you when it is ready for review.

    It implements the holistic fix. A new getSearchRowReport helper layers the live report over the Search snapshot report, so live values still win and Search-only fields like approved are kept.

    Details
    • Sites switched to the helper: getTransactionsSections, the transaction branch of getReportSections, the task parent report in getTaskSections, and the custom-column check in getColumnsToShow.
    • Left unchanged: getReportActionsSections (chat rows), which doesn't render Search-only report fields. Switching it would need an unsafe type assertion that lint rejects. createAndOpenSearchTransactionThread is also unchanged, because it falls back to the row's own report, not the snapshot.
    • Tests: A new unit test covers a paid report whose live report lacks approved. It fails without the change and passes with it. All 86 Search test suites pass, and typecheck, lint-changed, and spell-changed pass.
    • Not done: I didn't run the manual repro in the app. The Tests and QA sections are left for you to fill in.

    view run

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

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions