Repository navigation
Conversation
Add an optional Paid date column for expense reports. The date comes from the latest payment action that was not cancelled, since the Search response has no paid date on the report. Share that rule with the Paid by column.
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
|
@Expensify/design adding an optional Paid column to the Spend > Reports table, matching the existing Approved and First approved columns: same header style and date format, and the same fixed width as First approved. It's off by default and enabled from Display > Edit columns. Videos are in the description. Anything you'd like changed? |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ab766b70bf
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Paid had a fixed wide width, so it didn't line up with Approved. It now uses the narrow width like the other date columns and widens only when a paid date is from a past year and needs to show the year.
|
Good catch @shawnborton. |
|
Great, thank you! |
|
Looking very good! |
|
@FitseTLT this is ready for review. The HOLD is only for merging, until the backend change from #103650 (comment) is live. Design has already reviewed the UI changes. |
|
@FitseTLT the backend change is live, so this is off hold and ready for review. Videos are updated, including sorting by Paid. |
garrettmknight
left a comment
There was a problem hiding this comment.
Looks good from a product perspective.
|
We support Approved for expenses too why is Paid limited to expense reports @sam5235 doesn't the BE support it. |
|
This PR follows the issue scope, which is the reports table. Extending it to expenses is fairly straightforward. Expense rows already take @FitseTLT @garrettmknight should I add it to the expenses table in this PR? I'll check that the backend supports it on expenses in the meantime. |
Checked the |
|
|
||
| if (!ctx.shouldShowYearPaidReport && report && isPaidDateFromAPastYear(report, actions)) { | ||
| ctx.shouldShowYearPaidReport = true; | ||
| } |
There was a problem hiding this comment.
Why do we add this different from approved? you are totally depending on actions data instead of the snapshots paid field which the spend page api is now returning.
There was a problem hiding this comment.
Good point, I have changed it now.
The width now reads report.paid directly, like Approved reads report.approved, and isPaidDateFromAPastYear is removed. The cell still uses getPaidDate, so an offline payment shows right away, same as getApprovedDate.
| approved?: string; | ||
|
|
||
| /** The date the report was paid */ | ||
| paid?: string; |
There was a problem hiding this comment.
I still don't see paid field populated by the BE in reports
There was a problem hiding this comment.
The BE only returns paid when the query asks for it, when the Paid column is selected (columns:…,paid in the query) or when sorting by Paid. Otherwise the key isn't sent at all. It's also only in Search responses, not OpenReport/OpenApp.
To check, compare the Search response for type:expense-report columns:date,title,paid with plain type:expense-report. In the first one every report has paid (blank when unpaid), and in the second there's none.
| created: unknown; | ||
| submitted: unknown; | ||
| approved: unknown; | ||
| paid: unknown; |
There was a problem hiding this comment.
It's required by the type check at the bottom of this file testReportKeys, every key on Report must be listed here or typecheck fails. paid comes on the report_ entries of the Search snapshot, which are typed as Report (SearchResultDataType), so it has to be on Report and therefore here.
I think it's the same as debitedAmount/creditedAmount, which are also Search-only report fields.
| created: report.created, | ||
| submitted: report.submitted, | ||
| approved: report.approved, | ||
| paid: report.paid, |
There was a problem hiding this comment.
this also might not be needed.
There was a problem hiding this comment.
It's needed for the type check.
The return object satisfies Record<keyof StableReport, unknown>, so every Report key must be returned or added to ExcludedFields. The doc comment above says new fields go in the return object, and only fields that change on every message/read go in ExcludedFields. paid only changes on payment, so it sits next to approved, same as debitedAmount.
Yes apply |
|
@FitseTLT thanks for the review! All comments are addressed and Paid is now on the Expenses table too. The description, tests and videos are updated. Ready for another look. |
|
BTW I saw that on mobile paid sortBy option is included for |
|
There is a report that I took Lines 2717 to 2722 in 09246c6 When a report is marked as received more than once, Paid shows the latest marking instead of the first — |
Fixed it! A received marking also creates Right after marking, ✅ Received payment (payments off): A marked, then B, then A again. Sorted by Paid, A stays before B right after marking, after a cache clear and after opening A. Payment.off.workflow.mp4✅ Real payment: D paid, then C, then D's payment cancelled and D paid again. D uses its second payment, so C stays before D. Real.Payment.workflow.mp4 |
I believe this already happens on |
Confirmed this on
So the Expenses Sort by on mobile not picking up the desktop columns is existing behavior on Reports on main branchmain-branch-reports.mp4Expenses on main branchmain-branch-expenses.mp4Happy to open a separate issue for the Expenses one if you think it's worth fixing. |
|
You're theory doesn't work for me I added paid in Reports in web but it is not available in mobile. 2026-10-23.18-45-35.mp4 |
|
Okay it works after signing out/in but the theory applies for Expenses tab too. So you should update the test steps accordingly. |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppAndroid: mWeb ChromeiOS: HybridAppiOS: mWeb SafariMacOS: Chrome / Safari2026-10-23.19-54-56.mp4 |
|
@JS00001 the sortBy column options for mobile is totally random. there is a theory by @sam5235 that the columns open in desktop appears if sign out and in for mobile but that's not consistent from my testing. In my testing IOS web aligned with the theory but for android and android web it didn't there is no way it would be platform specific I think there is some problem in BE because there is nothing responsible for that in FE. So we can investigate it separately but I don't think we should hold this pr for it. WDYT |
I agree with this one. Upon further investigation in the apps with the same account, my earlier explanation doesn't capture the full picture of how Sort by behaves on mobile:
This is the same on It might be worth agreeing on a clearer, predictable rule for when a column and Sort by selection made in one session is saved beyond the local cache and picked up by the user's other sessions (on sign-in, cache clear, or switching tabs). That would make this behavior easier to rely on in implementation and testing. |
👍🏻 Thanks, I have updated the test steps in the PR description. |

Explanation of Change
The Spend tables can show when a report was submitted and approved, but not when it was paid. This adds an optional Paid column to the Reports table and the Expenses table.
TABLE_COLUMNS.PAIDis added to the expense report columns after First approved, and to the expense columns after Approved (COLUMN_AVAILABILITY, Search only). It is off by default and uses the existing "Paid" label (search.filters.paid).getPaidDateuses the report'spaiddate from the Search response, or the latest payment action's date if that is newer. The action covers paying from Search (including offline), before the next search refreshesreport.paid. It is blank unless the report status is Paid, and a payment that was later cancelled, dequeued or bounced does not count. A payment the submitter marked as received does count, because the report was still paid on that date.paidis added to theReporttype and registered inwhitelistedReportKeys,DebugUtilsandgetStableReportSelector, next toapproved(same asdebitedAmountin [Payment due @huult] [NO QA] Add Amount debited and Amount reimbursed filters, columns and sorting #97708, also a Search-only report field).getLastPaidAction(Paid by) now live ingetPaymentAction(latest or earliest valid payment, ignoring cancelled ones) andisPaymentMarkedAsReceived, used by both Paid and Paid by. Paid by's behaviour is unchanged and covered by its existing tests. Paid takes the earliest "received" marking, matching the backend, and the latest real payment.paiddate is from a past year (shouldShowYearPaid, read fromreport.paidlike Approved readsreport.approved).getTableMinWidthcounts it as a date column.expenseReportColumnNamesToSortingPropertyandtransactionColumnNamesToSortingProperty, so the header is sortable and Paid shows in "Sort by" when the column is enabled.Backend change (details) is live on staging and production. Search returns
paidon each report when the query asks for it (the Paid column is selected, or sorting by Paid) and acceptssortBy:paid, for bothtype:expense-reportandtype:expense.Areas that could be affected: the Paid by column (shared helper), the Reports and Expenses table headers and rows on wide layouts, and the Sort by list.
Unrelated console error: while testing I saw
The result of getSnapshot should be cached to avoid an infinite loopfromReportActionsListItemRendereron web and iOS. It also appears on the sign-in screen before any report code runs, and this PR doesn't add or change any store subscriptions, so it isn't caused by this change. Happy to open a separate issue for it.Unrelated bug: if a report is approved and then paid in the same session, its Approved date goes blank until reload. It happens on main too:
getApprovedDateonly uses the APPROVED action while the status is Approved, and approving doesn't setreport.approvedoptimistically.Fixed Issues
$ #103650
PROPOSAL: #103650 (comment)
Tests
Precondition: an account that is admin of a workspace, with paid expense reports paid on at least two different days, and one approved expense report that has not been paid yet.
Reports table
Expenses table
Narrow layout
Edit columns is only available on wide layouts, so mobile uses columns from a saved search created on web.
Offline tests
QA Steps
Same as tests.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, I verified the components usingAvatarare working as expected)npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android.Native.mp4
e5e9e-448d-4fff-920d-07494ce01649
Android: mWeb Chrome
Android.mWeb.mp4
iOS: Native
IOS.Native.mp4
iOS: mWeb Safari
IOS.mWeb.Safari.mp4
MacOS: Chrome / Safari
MacOS.Chrome.mov