You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[CFI] [$250] Spend - Add a Paid column option to the reports table #103650
On the Spend page, the reports table lets you add Submitted and Approved date columns. It has no Paid column, so you have to open the report and read its messages to see when it was paid.
Solution
Add a Paid column option, like the Submitted and Approved columns. When enabled, it shows the timestamp of when each report was paid.
changed the title [-]Spend - Add a Paid column option to the reports table[/-][+][$175] Spend - Add a Paid column option to the reports table[/+]on Oct 9, 2026
changed the title [-][$175] Spend - Add a Paid column option to the reports table[/-][+][CFI] [$175] Spend - Add a Paid column option to the reports table[/+]on Oct 9, 2026
Edit (Oct 12): the paid date now has its own lookup instead of reusing the Paid by action, and the column uses the fixed wide size. Both from @trasnake87's proposal below, who caught that the Paid by lookup skips some payments.
Re-state problem
The Spend reports table can show Submitted and Approved date columns but has no Paid column. To see when a report was paid, you have to open it and read the messages.
Root cause
The reports view gets its column options from getCustomColumns, which returns the hand-written TYPE_CUSTOM_COLUMNS.EXPENSE_REPORT list. That list has Submitted, Approved, First approved and Paid by, but no paid date, so the column picker can't offer one.
When getReportSections builds each report row, it resolves the latest payment action for paid reports and only uses its actor for the Paid by column. Its created timestamp is never copied onto the row.
That action can't supply the date, though. getLastPaidAction is a payer lookup, so it skips payments that don't name a payer. One is the IOU PAY action that markReportPaymentReceived adds with isSubmitterMarkedPaymentReceived, while the report still moves to REIMBURSED. The other is an owner's payment followed by the owner's MARKED_REDEEMED. Leaving those out is right for Paid by, but a date taken from the same action comes out blank, or points at an older payment.
The per-report snapshot map doesn't help either. It only collects REIMBURSED, MARKED_REIMBURSED and MARK_REIMBURSED_FROM_INTEGRATION actions, never IOU PAY ones.
Add PAID: 'paid' to TABLE_COLUMNS in src/CONST/index.ts, next to PAID_BY.
Add PAID: this.TABLE_COLUMNS.PAID to TYPE_CUSTOM_COLUMNS.EXPENSE_REPORT, right after PAID_BY.
Leave COLUMN_AVAILABILITY alone. Its search entries only build the expenses list (TYPE_CUSTOM_COLUMNS.EXPENSE), not the reports one.
Add PAID to expenseReportColumnNamesToSortingProperty and getSearchColumnTranslationKey, next to the existing SUBMITTED and APPROVED entries.
2. Put the paid date on the row
Add getPaidDate(reportItem, snapshotPaidAction, actions) in SearchUIUtils.ts, next to getApprovedDate. It returns '' unless statusNum is REIMBURSED.
For a paid report it returns the created time of the newest payment action: REIMBURSED, MARKED_REIMBURSED, MARK_REIMBURSED_FROM_INTEGRATION, or an IOU action of type PAY, including the submitter-confirmed one.
It keeps the cancellation rule from getLastPaidAction, so a payment at or before the latest REIMBURSEMENT_DEQUEUED, ACH canceled or ACH bounce action doesn't count. It drops the payer checks.
It seeds from the snapshot action, like getLastPaidAction does, and scans the report's actions for the IOU PAY ones the snapshot never collects.
In the row object, add paid: getPaidDate(reportItem, lastReimbursedActionByReportID.get(reportItem.reportID), actions) next to submitted and approved.
Leave getLastPaidAction and Paid by as they are.
Add paid?: string to ExpenseReportListItemType in ListItem/types.ts.
3. Render and size it
In ExpenseReportListItemRowWide.tsx, add a [CONST.SEARCH.TABLE_COLUMNS.PAID] cell copied from the Approved one: DateCell with item.paid ?? '', showTooltip, isLargeScreenWidth, and shouldUseLocalTimeZone.
In SearchTableHeader.tsx, add a header entry after Approved using the existing search.filters.paid key ('Paid' in en.ts). Use the same key in getSearchColumnTranslationKey, so no new strings are needed.
In styles/utils/index.ts, add PAID to the fixed w102 case that First approved uses. DateCell adds the year for a past-year date, which the w62 default would clip. Approved avoids that with a shouldShowYearApproved flag, but that flag runs through Search/index.tsx and the header, which is a lot of plumbing for one column.
changed the title [-][CFI] [$175] Spend - Add a Paid column option to the reports table[/-][+][CFI] [$250] Spend - Add a Paid column option to the reports table[/+]on Oct 9, 2026
🚨 Edited by proposal-police: This proposal was edited at 2026-10-09 19:29:14 UTC.
Proposal
Please re-state the problem that we are trying to solve in this issue.
On the Spend page, the reports table offers Submitted and Approved date columns but no Paid column, so users have to open each report and read its actions to find out when it was paid.
What is the root cause of that problem?
This is a missing feature. The reports (expense report) table only knows about the columns listed in TYPE_CUSTOM_COLUMNS.EXPENSE_REPORT, and there is no paid-date entry there or in TABLE_COLUMNS:
The data we need is already computed. When building each report row, getReportSections resolves the latest valid payment action (via getLastPaidAction, which skips submitter "marked as received" actions and payments that were later dequeued, canceled or bounced) to fill the existing Paid by column. Only the actor of that action is used; its created timestamp, which is exactly the paid date, is thrown away:
What changes do you think we should make in order to solve the problem?
Add a Paid date column to the reports table, wired the same way as the Approved / First approved date columns:
Constants (src/CONST/index.ts): add PAID: 'paid' to SEARCH.TABLE_COLUMNS, and add it to TYPE_CUSTOM_COLUMNS.EXPENSE_REPORT right after PAID_BY, so it appears in the column picker next to the other approval/payment columns. It stays off by default (it is not added to the default visible columns).
Row data (getReportSections in src/libs/SearchUIUtils.ts): set a new paid field on the report list item from the created of the same lastReimbursedAction already used for Paid by, falling back to an empty string. Because that action is only resolved when the report is in the Reimbursed status and ignores canceled/bounced payments, the cell is blank for unpaid reports and for reports whose payment was reversed. Add the optional paid field to the expense report list item type in src/components/Search/SearchList/ListItem/types.ts.
Header and translation: add the column to the expense report header list in SearchTableHeader.tsx, and map it in the column-to-translation switch in SearchUIUtils.ts. The existing search.filters.paid ("Paid") string can be reused, so no new translations are needed.
Cell rendering (ExpenseReportListItemRowWide.tsx): render a DateCell for the new column showing item.paid, exactly like the First approved cell.
Width and sorting: give the column a fixed date width in getSearchColumnStyles (same as First approved, wide enough for a past-year date; like the other date columns it is not content-measured, so getSearchColumnContentToMeasure.ts needs no change), and add it to nonSortableColumns (plus isColumnSortable: false on its header). The Search API rejects sortBy:paid as an invalid query (I confirmed this on dev: clicking a sortable Paid header showed "That search isn't valid"), so the column is display-only until the backend accepts that sort key; making it sortable later only needs the column removed from that set and mapped to paid in expenseReportColumnNamesToSortingProperty.
Tests: classify the new column next to Paid by in ColumnAvailabilityTest, and assert the paid date in the existing Paid by cases of SearchUIUtilsTest (set on paid reports, blank when the payment was marked received or canceled).
What alternative solutions did you explore? (Optional)
Have the backend return a paid timestamp on each report in the search snapshot and read it directly, like the submitted / approved report fields. That requires a backend change, while the frontend already resolves the correct payment action for the Paid by column, so deriving the date from that same action keeps both columns consistent for every row and ships without backend work. A backend field would still be the way to make the column sortable server-side.
🚨 Edited by proposal-police: This proposal was edited at 2026-10-09 19:27:30 UTC.
Proposal
What is the root cause of that problem?
This is a missing Reports-table capability, not an incorrect payment write. The selectable report-column registry includes Submitted, Approved, Paid by and Paid status, but no Paid date. getCustomColumns() builds the picker from this registry, and getColumnsToShow() rejects selected columns outside it, so adding a cell alone would not make Paid selectable or reachable.
The report-row producer already resolves a payment action for Paid by, but only emits the payer identity, not its timestamp. The header and wide row likewise define Submitted and Approved without a Paid date.
Producer check: payment actions are legitimate history. The optimistic action builder supplies created; the payment flow writes that action and the report's reimbursed status, clears pending state on success, and restores the report on failure. These writes should remain unchanged; the missing state is the derived table date, not payment history.
An important distinction is that getLastPaidAction() is currently a payer resolver: it rejects cancelled payments and submitter-confirmed receipts. A receipt does not identify a payer, but the receipt flow still legitimately marks the report paid. Therefore blindly using the existing Paid-by result for Paid would leave those reports without a date.
History inspection found adjacent feature additions, not a removal of Paid: commit 7d95877 added First approver/First approved, and 0b38622 repaired Paid by to use live payment actions. This request extends that pipeline; neither commit is evidence that a Paid date previously existed. 7d95877 0b38622
What changes do you think we should make in order to solve the problem?
Add TABLE_COLUMNS.PAID = 'paid' and include it after Approved in TYPE_CUSTOM_COLUMNS.EXPENSE_REPORT only. Add its column-label mapping to getSearchColumnTranslationKey() using the existing search.filters.paid translation. Keep it opt-in: do not add it to default columns, expense columns, or report-detail expense columns. The column ID types already derive from the constants.
// Only the table column header shows the short "Created"; every other label source (Sort by, Edit columns, saved search, CSV) uses the full "Created date".
Produce a derived paid?: string on TransactionReportGroupListItemType in getReportSections(). Refactor the payment resolver to share payment-type selection, latest-action ordering and cancellation cutoffs, while making payer attribution a separate predicate. Paid takes the latest valid payment action's created when the report has reimbursed status; otherwise it is blank. Recognize REIMBURSED, MARKED_REIMBURSED, MARK_REIMBURSED_FROM_INTEGRATION and IOU/PAY. Exclude actions at or before the latest dequeued/cancelled/bounced reimbursement, failed actions and pending deletions. Include submitter-confirmed IOU/PAY or MARKED_REIMBURSED events for the date without changing the existing Paid-by exclusions. MARKED_REDEEMED is attribution context, not an additional later payment timestamp. If no qualifying timestamp exists, leave the cell blank rather than inventing one from report creation or last activity.
For the new date resolver, reconcile the complete snapshot and live payment/cancellation histories by reportActionID, with live versions (including errors and pending deletions) overriding snapshot versions before eligibility checks. Do not treat an empty or partial live list as complete history, or reduce the snapshot to one payment seed. The current fallback selects a present live array even when it is empty, and the snapshot preprocessing retains only the latest of three settled action types—not IOU/PAY or the cancellation history. For example, a snapshot-only received-payment PAY event must still supply Paid when an empty live list exists. Snapshot cancellations must also invalidate older live payments.
Apply failed/pending-deletion exclusion to cancellation events before calculating the cutoff as well as to payment events: a failed cancel restores REIMBURSED while leaving an errored dequeue action. That failed reversal must not erase the still-valid payment date. Keep this reconciliation scoped to producing Paid; preserve the existing Paid-by predicate, behavior and tests. Payment from Search still updates the date from live optimistic actions without requiring a new search response.
Register Paid in getExpenseReportHeaders() and the wide row's column-component map. Render the raw timestamp with the shared DateCell, showTooltip, isLargeScreenWidth and shouldUseLocalTimeZone, matching Submitted/Approved's date presentation. Use a fixed past-year-capable width, following First approved's existing 102px pattern, for header and row; add the same width to getTableMinWidth()'s fallback. This avoids introducing another year flag through every header and list caller.
The Edit columns picker, save handler, query serialization and direct/saved columns:paid selections use the shared registry/column pipeline. Keep Paid visible when selected even if every row is unpaid; only its cells are blank. Existing column order and required Total remain intact.
Cover payment elsewhere, reimbursement, integration-marked payments, submitter-confirmed receipts, snapshot-only history and live optimistic payments through the shared date resolver. Unpaid/cancelled reports stay blank; a later re-payment replaces the old date. Failure rollback restores the prior status, so the date clears without changing payment actions.
This change adds display/selection, not a new server sorting contract. Mark the Paid header non-sortable and add Paid to nonSortableColumns so the separate Sort by dropdown cannot offer it either. Server Paid sorting is not established by this client checkout; sorting only the loaded page would not establish correct paginated order. Existing sort choices and date filters remain unchanged.
Keep narrow report cards unchanged, as with existing optional Submitted/Approved columns. The wide table branch receives columns; the narrow card branch does not. Other search types and the individual report's expense table are outside this Reports-column request.
Observed checks: static tracing of column selection, row production, live/snapshot actions, payment success/failure writes, responsive branches and history diffs. No tests were executed.
Planned regressions:
Extend tests/unit/Search/SearchUIUtilsTest.ts: exact Paid timestamps for each supported action type; newest valid payment after cancellation; blank unpaid/no-history rows; submitter receipt date populated while Paid by remains blank; live optimistic payment absent from the snapshot; failed payment rollback; no mutation of source reports/actions.
Add partial-history fixtures: a snapshot IOU/PAY receipt with an empty live array; snapshot cancellation plus an older live payment; same-ID live error/deletion overriding a snapshot payment; failed cancellation rollback retaining the original Paid date. Exercise both snapshot-only history and uncached reports with no qualifying timestamp.
Extend tests/unit/Search/SearchColumnsPageTest.tsx: Paid available only for Reports, unchecked by default, selection/order persisted across save/reopen, and an all-unpaid table retains the selected column.
Verify the header and row appear in the same position and both header sorting and Sort by omit Paid.
Verify localized dates near midnight/year boundaries and past-year width; manually check web/desktop wide tables, resizing/scrolling, reload/deep-linked column selections, offline pay followed by success/failure, and unchanged narrow cards.
What alternative solutions did you explore? (Optional)
The smallest rival is adding a Paid cell fed by lastReimbursedAction?.created beside Paid by. It reuses an available value, but it misses submitter-confirmed receipts because the current resolver intentionally excludes them for payer attribution. Removing those exclusions outright would instead regress Paid by. Sharing the payment/cancellation scan while separating timestamp selection from payer attribution covers both contracts.
Another concrete alternative is adding a persisted Report.paid field and optimistically writing it in PayMoneyRequest.ts. That requires an agreed server field for all payment producers, cancellation/re-payment and rollback, while the current client already has timestamped payment actions and live-action subscriptions. A future canonical server timestamp would be useful for history-free results, but this proposal does not invent or persist a field unsupported by the current contract.
A page-local scan in ExpenseReportListItemRowWide.tsx would also show dates, but it bypasses the shared row-data owner and duplicates action selection per rendered row. The shared producer gives every report row one consistent date before rendering.
🤖 ProposalPolice™ is tracking duplicate proposals for this issue using an OpenAI Conversation. This comment stores that Conversation's ID and can be safely ignored.
📣 @sam5235 You have been assigned to this job!
Please apply to the Upwork job and leave a comment on the Github issue letting us know when we can expect a PR to be ready for review 🧑💻
Once you apply to this job, your Upwork ID will be stored and you will be automatically hired for future jobs!
Keep in mind: Code of Conduct | Contributing 📖
Testing it found that the backend is needed for more than sorting:
Sorting:sortBy:paid still returns 401 Invalid Query.
Display: the App reads the paid date from each report's payment action, but Search only returns those actions when Paid by is also selected. With only Paid selected, the response has the IOU create and APPROVED actions but no MARKEDREIMBURSED, so the cell is blank until the report is opened. This corrects my proposal, which said display works without backend changes; that only holds when Paid by is selected.
Suggested fix: accept sortBy:paid, and return a paid date on each report like submitted and approved, whatever columns are selected. Once you confirm the field name, I'll add it to the App (prefer report.paid, fall back to the payment actions), since a new report key needs your OK.
If that will take a while, a stopgap is for the App to also request paidBy whenever paid is selected, so the actions come back. I'd only do that if you are OK with it, since it relies on how the backend picks which actions to return.
@mallenexpensify following up on the Upwork job for this issue: https://www.upwork.com/jobs/~022108633277897041341 shows "Only invited users can find, view and apply to the job" for me, while other Expensify jobs open and accept applications normally, so it seems specific to this posting. Could you look into it?
@JS00001 the backend change is live.
Search returns paid on reports and sortBy:paid works on staging and production. The PR now reads report.paid and It adds paid to the report key whitelist.
Activity
melvin-bot commentedon Oct 9, 2026
Current assignee @FitseTLT is eligible for the External assigner, not assigning anyone new.
[-]Spend - Add a Paid column option to the reports table[/-][+][$175] Spend - Add a Paid column option to the reports table[/+]melvin-bot commentedon Oct 9, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022108633277897041341
[-][$175] Spend - Add a Paid column option to the reports table[/-][+][CFI] [$175] Spend - Add a Paid column option to the reports table[/+]yuvrajangadsingh commentedon Oct 9, 2026
Proposal
Edit (Oct 12): the paid date now has its own lookup instead of reusing the Paid by action, and the column uses the fixed wide size. Both from @trasnake87's proposal below, who caught that the Paid by lookup skips some payments.
Re-state problem
The Spend reports table can show Submitted and Approved date columns but has no Paid column. To see when a report was paid, you have to open it and read the messages.
Root cause
The reports view gets its column options from getCustomColumns, which returns the hand-written TYPE_CUSTOM_COLUMNS.EXPENSE_REPORT list. That list has Submitted, Approved, First approved and Paid by, but no paid date, so the column picker can't offer one.
Reports column list
getCustomColumns reading it
When getReportSections builds each report row, it resolves the latest payment action for paid reports and only uses its actor for the Paid by column. Its created timestamp is never copied onto the row.
Payment-action handling in SearchUIUtils.ts
That action can't supply the date, though. getLastPaidAction is a payer lookup, so it skips payments that don't name a payer. One is the IOU PAY action that markReportPaymentReceived adds with isSubmitterMarkedPaymentReceived, while the report still moves to REIMBURSED. The other is an owner's payment followed by the owner's MARKED_REDEEMED. Leaving those out is right for Paid by, but a date taken from the same action comes out blank, or points at an older payment.
getLastPaidAction
markReportPaymentReceived building that action
and setting the report to REIMBURSED
The per-report snapshot map doesn't help either. It only collects REIMBURSED, MARKED_REIMBURSED and MARK_REIMBURSED_FROM_INTEGRATION actions, never IOU PAY ones.
lastReimbursedActionByReportID collection
The row only sets submitted and approved, so a Paid cell has nothing to read.
Report-row date fields
The wide row renders its date cells from a per-column map, and PAID has no entry in it.
Wide-row column map
Changes to make
1. Register the column
2. Put the paid date on the row
3. Render and size it
First approved width
4. Tests
Alternatives
[-][CFI] [$175] Spend - Add a Paid column option to the reports table[/-][+][CFI] [$250] Spend - Add a Paid column option to the reports table[/+]wildan-m commentedon Oct 9, 2026
🚨 Edited by proposal-police: This proposal was edited at 2026-10-09 19:29:14 UTC.
Proposal
Please re-state the problem that we are trying to solve in this issue.
On the Spend page, the reports table offers Submitted and Approved date columns but no Paid column, so users have to open each report and read its actions to find out when it was paid.
What is the root cause of that problem?
This is a missing feature. The reports (expense report) table only knows about the columns listed in
TYPE_CUSTOM_COLUMNS.EXPENSE_REPORT, and there is no paid-date entry there or inTABLE_COLUMNS:App/src/CONST/index.ts
Lines 7912 to 7920 in 7155d25
App/src/CONST/index.ts
Lines 8115 to 8124 in 7155d25
The data we need is already computed. When building each report row,
getReportSectionsresolves the latest valid payment action (viagetLastPaidAction, which skips submitter "marked as received" actions and payments that were later dequeued, canceled or bounced) to fill the existing Paid by column. Only the actor of that action is used; itscreatedtimestamp, which is exactly the paid date, is thrown away:App/src/libs/SearchUIUtils.ts
Lines 2837 to 2844 in 7155d25
What changes do you think we should make in order to solve the problem?
Add a Paid date column to the reports table, wired the same way as the Approved / First approved date columns:
src/CONST/index.ts): addPAID: 'paid'toSEARCH.TABLE_COLUMNS, and add it toTYPE_CUSTOM_COLUMNS.EXPENSE_REPORTright afterPAID_BY, so it appears in the column picker next to the other approval/payment columns. It stays off by default (it is not added to the default visible columns).getReportSectionsinsrc/libs/SearchUIUtils.ts): set a newpaidfield on the report list item from thecreatedof the samelastReimbursedActionalready used for Paid by, falling back to an empty string. Because that action is only resolved when the report is in the Reimbursed status and ignores canceled/bounced payments, the cell is blank for unpaid reports and for reports whose payment was reversed. Add the optionalpaidfield to the expense report list item type insrc/components/Search/SearchList/ListItem/types.ts.SearchTableHeader.tsx, and map it in the column-to-translation switch inSearchUIUtils.ts. The existingsearch.filters.paid("Paid") string can be reused, so no new translations are needed.ExpenseReportListItemRowWide.tsx): render aDateCellfor the new column showingitem.paid, exactly like the First approved cell.getSearchColumnStyles(same as First approved, wide enough for a past-year date; like the other date columns it is not content-measured, sogetSearchColumnContentToMeasure.tsneeds no change), and add it tononSortableColumns(plusisColumnSortable: falseon its header). The Search API rejectssortBy:paidas an invalid query (I confirmed this on dev: clicking a sortable Paid header showed "That search isn't valid"), so the column is display-only until the backend accepts that sort key; making it sortable later only needs the column removed from that set and mapped topaidinexpenseReportColumnNamesToSortingProperty.ColumnAvailabilityTest, and assert the paid date in the existing Paid by cases ofSearchUIUtilsTest(set on paid reports, blank when the payment was marked received or canceled).App/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx
Lines 107 to 115 in 7155d25
App/src/libs/SearchUIUtils.ts
Lines 351 to 359 in 7155d25
The date is derived on the frontend from report actions already in the search snapshot, so no backend change is needed.
Compare branch: https://github.com/Expensify/App/compare/main...wildan-m:App:wildan/103650-paid-date-column?expand=1
What alternative solutions did you explore? (Optional)
Have the backend return a paid timestamp on each report in the search snapshot and read it directly, like the
submitted/approvedreport fields. That requires a backend change, while the frontend already resolves the correct payment action for the Paid by column, so deriving the date from that same action keeps both columns consistent for every row and ships without backend work. A backend field would still be the way to make the column sortable server-side.KJ21-ENG commentedon Oct 9, 2026
🚨 Edited by proposal-police: This proposal was edited at 2026-10-09 19:27:30 UTC.
Proposal
What is the root cause of that problem?
This is a missing Reports-table capability, not an incorrect payment write. The selectable report-column registry includes Submitted, Approved, Paid by and Paid status, but no Paid date.
getCustomColumns()builds the picker from this registry, andgetColumnsToShow()rejects selected columns outside it, so adding a cell alone would not make Paid selectable or reachable.App/src/CONST/index.ts
Lines 7912 to 7939 in d216f70
App/src/libs/SearchUIUtils.ts
Lines 4361 to 4366 in d216f70
App/src/libs/SearchUIUtils.ts
Lines 6421 to 6454 in d216f70
The report-row producer already resolves a payment action for Paid by, but only emits the payer identity, not its timestamp. The header and wide row likewise define Submitted and Approved without a Paid date.
App/src/libs/SearchUIUtils.ts
Lines 2837 to 2844 in d216f70
App/src/libs/SearchUIUtils.ts
Lines 2882 to 2892 in d216f70
App/src/components/Search/SearchTableHeader.tsx
Lines 252 to 285 in d216f70
App/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx
Lines 75 to 94 in d216f70
Producer check: payment actions are legitimate history. The optimistic action builder supplies
created; the payment flow writes that action and the report's reimbursed status, clears pending state on success, and restores the report on failure. These writes should remain unchanged; the missing state is the derived table date, not payment history.App/src/libs/ReportUtils.ts
Lines 8419 to 8439 in d216f70
App/src/libs/actions/IOU/PayMoneyRequest.ts
Lines 322 to 347 in d216f70
App/src/libs/actions/IOU/PayMoneyRequest.ts
Lines 390 to 419 in d216f70
An important distinction is that
getLastPaidAction()is currently a payer resolver: it rejects cancelled payments and submitter-confirmed receipts. A receipt does not identify a payer, but the receipt flow still legitimately marks the report paid. Therefore blindly using the existing Paid-by result for Paid would leave those reports without a date.App/src/libs/SearchUIUtils.ts
Lines 2643 to 2690 in d216f70
App/src/libs/actions/IOU/PayMoneyRequest.ts
Lines 969 to 989 in d216f70
App/src/libs/actions/IOU/PayMoneyRequest.ts
Lines 1007 to 1034 in d216f70
History inspection found adjacent feature additions, not a removal of Paid: commit 7d95877 added First approver/First approved, and 0b38622 repaired Paid by to use live payment actions. This request extends that pipeline; neither commit is evidence that a Paid date previously existed.
7d95877
0b38622
What changes do you think we should make in order to solve the problem?
Add
TABLE_COLUMNS.PAID = 'paid'and include it after Approved inTYPE_CUSTOM_COLUMNS.EXPENSE_REPORTonly. Add its column-label mapping togetSearchColumnTranslationKey()using the existingsearch.filters.paidtranslation. Keep it opt-in: do not add it to default columns, expense columns, or report-detail expense columns. The column ID types already derive from the constants.App/src/CONST/index.ts
Lines 8115 to 8124 in d216f70
App/src/components/Search/types.ts
Lines 161 to 184 in d216f70
App/src/libs/SearchUIUtils.ts
Lines 4448 to 4465 in d216f70
App/src/languages/en.ts
Lines 10346 to 10352 in d216f70
Produce a derived
paid?: stringonTransactionReportGroupListItemTypeingetReportSections(). Refactor the payment resolver to share payment-type selection, latest-action ordering and cancellation cutoffs, while making payer attribution a separate predicate. Paid takes the latest valid payment action'screatedwhen the report has reimbursed status; otherwise it is blank. Recognize REIMBURSED, MARKED_REIMBURSED, MARK_REIMBURSED_FROM_INTEGRATION and IOU/PAY. Exclude actions at or before the latest dequeued/cancelled/bounced reimbursement, failed actions and pending deletions. Include submitter-confirmed IOU/PAY or MARKED_REIMBURSED events for the date without changing the existing Paid-by exclusions. MARKED_REDEEMED is attribution context, not an additional later payment timestamp. If no qualifying timestamp exists, leave the cell blank rather than inventing one from report creation or last activity.For the new date resolver, reconcile the complete snapshot and live payment/cancellation histories by reportActionID, with live versions (including errors and pending deletions) overriding snapshot versions before eligibility checks. Do not treat an empty or partial live list as complete history, or reduce the snapshot to one payment seed. The current fallback selects a present live array even when it is empty, and the snapshot preprocessing retains only the latest of three settled action types—not IOU/PAY or the cancellation history. For example, a snapshot-only received-payment PAY event must still supply Paid when an empty live list exists. Snapshot cancellations must also invalidate older live payments.
App/src/libs/SearchUIUtils.ts
Lines 1498 to 1508 in 4d934ce
App/src/libs/SearchUIUtils.ts
Lines 1795 to 1805 in 4d934ce
App/src/libs/ReportUtils.ts
Lines 14391 to 14410 in 4d934ce
App/src/components/Search/hooks/useSearchSnapshot.ts
Lines 214 to 239 in 4d934ce
Apply failed/pending-deletion exclusion to cancellation events before calculating the cutoff as well as to payment events: a failed cancel restores REIMBURSED while leaving an errored dequeue action. That failed reversal must not erase the still-valid payment date. Keep this reconciliation scoped to producing Paid; preserve the existing Paid-by predicate, behavior and tests. Payment from Search still updates the date from live optimistic actions without requiring a new search response.
App/src/libs/ReportUtils.ts
Lines 4862 to 4865 in 4d934ce
App/src/libs/actions/IOU/PayMoneyRequest.ts
Lines 731 to 759 in 4d934ce
App/src/libs/SearchUIUtils.ts
Lines 1795 to 1805 in d216f70
App/src/libs/SearchUIUtils.ts
Lines 2643 to 2690 in d216f70
App/src/libs/SearchUIUtils.ts
Lines 2792 to 2799 in d216f70
App/src/components/Search/SearchList/ListItem/types.ts
Lines 253 to 277 in d216f70
App/tests/unit/Search/SearchUIUtilsTest.ts
Lines 7658 to 7755 in d216f70
getExpenseReportHeaders()and the wide row's column-component map. Render the raw timestamp with the sharedDateCell,showTooltip,isLargeScreenWidthandshouldUseLocalTimeZone, matching Submitted/Approved's date presentation. Use a fixed past-year-capable width, following First approved's existing 102px pattern, for header and row; add the same width togetTableMinWidth()'s fallback. This avoids introducing another year flag through every header and list caller.App/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/ExpenseReportListItemRowWide.tsx
Lines 75 to 94 in d216f70
App/src/components/Search/SearchList/ListItem/DateCell.tsx
Lines 23 to 59 in d216f70
App/src/styles/utils/index.ts
Lines 2040 to 2049 in d216f70
App/src/libs/SearchUIUtils.ts
Lines 6998 to 7038 in d216f70
Path coverage and preserved behavior:
The Edit columns picker, save handler, query serialization and direct/saved
columns:paidselections use the shared registry/column pipeline. Keep Paid visible when selected even if every row is unpaid; only its cells are blank. Existing column order and required Total remain intact.App/src/pages/Search/SearchColumnsPage.tsx
Lines 102 to 134 in d216f70
App/src/libs/SearchQueryUtils.ts
Lines 1239 to 1245 in d216f70
App/src/libs/SearchUIUtils.ts
Lines 6433 to 6477 in d216f70
Cover payment elsewhere, reimbursement, integration-marked payments, submitter-confirmed receipts, snapshot-only history and live optimistic payments through the shared date resolver. Unpaid/cancelled reports stay blank; a later re-payment replaces the old date. Failure rollback restores the prior status, so the date clears without changing payment actions.
App/src/libs/SearchUIUtils.ts
Lines 2643 to 2690 in d216f70
App/src/libs/actions/IOU/PayMoneyRequest.ts
Lines 401 to 419 in d216f70
This change adds display/selection, not a new server sorting contract. Mark the Paid header non-sortable and add Paid to
nonSortableColumnsso the separate Sort by dropdown cannot offer it either. Server Paid sorting is not established by this client checkout; sorting only the loaded page would not establish correct paginated order. Existing sort choices and date filters remain unchanged.App/src/libs/SearchUIUtils.ts
Lines 457 to 464 in d216f70
App/src/components/Search/SortableTableHeader.tsx
Lines 78 to 85 in d216f70
App/src/libs/SearchUIUtils.ts
Lines 5302 to 5309 in d216f70
App/src/libs/actions/Search.ts
Lines 1245 to 1264 in d216f70
Keep narrow report cards unchanged, as with existing optional Submitted/Approved columns. The wide table branch receives columns; the narrow card branch does not. Other search types and the individual report's expense table are outside this Reports-column request.
App/src/components/Search/SearchList/ListItem/ExpenseReportListItemRow/index.tsx
Lines 10 to 40 in d216f70
App/src/components/Search/SearchTableHeader.tsx
Lines 521 to 535 in d216f70
Test coverage:
Observed checks: static tracing of column selection, row production, live/snapshot actions, payment success/failure writes, responsive branches and history diffs. No tests were executed.
Planned regressions:
tests/unit/Search/SearchUIUtilsTest.ts: exact Paid timestamps for each supported action type; newest valid payment after cancellation; blank unpaid/no-history rows; submitter receipt date populated while Paid by remains blank; live optimistic payment absent from the snapshot; failed payment rollback; no mutation of source reports/actions.tests/unit/Search/SearchColumnsPageTest.tsx: Paid available only for Reports, unchecked by default, selection/order persisted across save/reopen, and an all-unpaid table retains the selected column.What alternative solutions did you explore? (Optional)
The smallest rival is adding a Paid cell fed by
lastReimbursedAction?.createdbeside Paid by. It reuses an available value, but it misses submitter-confirmed receipts because the current resolver intentionally excludes them for payer attribution. Removing those exclusions outright would instead regress Paid by. Sharing the payment/cancellation scan while separating timestamp selection from payer attribution covers both contracts.App/src/libs/SearchUIUtils.ts
Lines 2650 to 2689 in d216f70
App/tests/unit/Search/SearchUIUtilsTest.ts
Lines 7698 to 7729 in d216f70
Another concrete alternative is adding a persisted
Report.paidfield and optimistically writing it inPayMoneyRequest.ts. That requires an agreed server field for all payment producers, cancellation/re-payment and rollback, while the current client already has timestamped payment actions and live-action subscriptions. A future canonical server timestamp would be useful for history-free results, but this proposal does not invent or persist a field unsupported by the current contract.App/src/types/onyx/Report.ts
Lines 83 to 90 in 4d934ce
App/src/libs/actions/IOU/PayMoneyRequest.ts
Lines 322 to 351 in 4d934ce
A page-local scan in
ExpenseReportListItemRowWide.tsxwould also show dates, but it bypasses the shared row-data owner and duplicates action selection per rendered row. The shared producer gives every report row one consistent date before rendering.App/src/libs/SearchUIUtils.ts
Lines 2873 to 2903 in 4d934ce
Dip-G-Git commentedon Oct 9, 2026
github-actions commentedon Oct 9, 2026
🤖 ProposalPolice™ is tracking duplicate proposals for this issue using an OpenAI Conversation. This comment stores that Conversation's ID and can be safely ignored.
84 remaining items
melvin-bot commentedon Oct 13, 2026
📣 @sam5235 You have been assigned to this job!
Please apply to the Upwork job and leave a comment on the Github issue letting us know when we can expect a PR to be ready for review 🧑💻
Once you apply to this job, your Upwork ID will be stored and you will be automatically hired for future jobs!
Keep in mind: Code of Conduct | Contributing 📖
sam5235 commentedon Oct 13, 2026
Thanks! The PR will be ready for review within 24 hours.
@JS00001, thanks for confirming. I'll build sorting in the App now and we can test it against staging once the backend change is there.
@JS00001, the Upwork job link gives me "Only invited users can find, view and apply to the job". Could someone invite me? My profile: https://www.upwork.com/freelancers/~0123dda34004da49e6. If you're having issue with Upwork profile link, you can try inviting me using https://www.upwork.com/freelancers/samiyahamid.
JS00001 commentedon Oct 13, 2026
I'm not sure, cc @mallenexpensify do you know more about this ^ ?
sam5235 commentedon Oct 14, 2026
@JS00001 PR is up and held on the backend: #103956
Testing it found that the backend is needed for more than sorting:
sortBy:paidstill returns401 Invalid Query.IOUcreate andAPPROVEDactions but noMARKEDREIMBURSED, so the cell is blank until the report is opened. This corrects my proposal, which said display works without backend changes; that only holds when Paid by is selected.Suggested fix: accept
sortBy:paid, and return apaiddate on each report likesubmittedandapproved, whatever columns are selected. Once you confirm the field name, I'll add it to the App (preferreport.paid, fall back to the payment actions), since a new report key needs your OK.If that will take a while, a stopgap is for the App to also request
paidBywheneverpaidis selected, so the actions come back. I'd only do that if you are OK with it, since it relies on how the backend picks which actions to return.sam5235 commentedon Oct 14, 2026
@mallenexpensify following up on the Upwork job for this issue: https://www.upwork.com/jobs/~022108633277897041341 shows "Only invited users can find, view and apply to the job" for me, while other Expensify jobs open and accept applications normally, so it seems specific to this posting. Could you look into it?
upwork-job-issue.mp4
mallenexpensify commentedon Oct 18, 2026
@sam5235 can you please accept the job below? Please reply here and tag me once you have.
https://www.upwork.com/jobs/~022108633277897041341
After being hired the first time, it'll be quicker/easier in the future (assuming automation is working, we've been having issues) Thx
sam5235 commentedon Oct 18, 2026
@mallenexpensify, thanks for looking into the issue. It is resolved and I have now accepted the job offer.
sam5235 commentedon Oct 19, 2026
@JS00001 the backend change is live.
Search returns
paidon reports andsortBy:paidworks onstagingandproduction. ThePRnow readsreport.paidand It addspaidto the report key whitelist.FitseTLT commentedon Oct 19, 2026
@JS00001 Can you confirm the BE change is done? Should we proceed?
JS00001 commentedon Oct 19, 2026
Yep looks like the BE changes are done