Read the Expensify Card monthly settlement date instead of guessing it - #100974
Conversation
The settlement date arrives from the backend as a string or a day-of-month number, but the page passed it straight to new Date(). A bare day is read as milliseconds since the epoch, so every workspace showed the 31st west of UTC. Resolve the value in CardUtils and show nothing when no real day can be read. Co-authored-by: Linh Vo <linhvovan29546@users.noreply.github.com>
|
I created this PR from a branch on To take ownership of the branch so you can push updates to it, run: Then close this PR and open a new one from your fork. |
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
|
There's nothing to fix in the code — the only failing check is Every other check on the latest run passes: EvidenceFrom the
I could not verify the change in a browser in this environment, so the code behavior itself is still unverified beyond the unit and UI tests listed in the PR body. |
|
🤖 Web test steps verification results: ❌ not run I could not produce screenshots or recordings, for two separate reasons:
So the evidence below comes from CI and static analysis only — not from a browser. What I did verifyCI runs the tests that encode the issue's repro, and they pass. All 8 The UI test that most directly maps to the issue writes The root-cause mechanism matches the reported symptom. The reporter saw the 31st on three workspaces whose real dates were the 1st, 10th, and 10th. Code trace
What only a browser or the backend can settleThe tests prove the page renders correctly for each shape the value might take. They cannot prove which shape the backend actually sends — the PR description flags this too. So the one thing still unverified is the thing the issue is about: does an affected workspace now show its real settlement date? Two ways to close that, neither of which needs a browser:
Worth noting for whoever picks this up: Once the Tests section is filled in and web prep is working, re-run me and I'll capture the screenshots. |
|
|
|
@MelvinBot Since |
Drop the speculative string parsing from getMonthlySettlementDate. The backend sends the settlement date as a day of the month, so number is the only shape that needs converting. Date stays because our own optimistic writes store one. Co-authored-by: Linh Vo <linhvovan29546@users.noreply.github.com>
|
Done — dropped the string handling in One thing I did not drop: the
All three use So the type is now if (monthlySettlementDate instanceof Date) {
return Number.isNaN(monthlySettlementDate.getTime()) ? undefined : monthlySettlementDate;
}
if (!Number.isInteger(monthlySettlementDate) || monthlySettlementDate < 1 || monthlySettlementDate > 31) {
return undefined;
}
return new Date(new Date().getFullYear(), 0, monthlySettlementDate);Happy to go further if you'd prefer — see the open question at the bottom. What changed
I kept the 1–31 range guard. Without it a stray value rolls over — Verified locally on Open question, your call: the optimistic |
|
No code fix is needed — nothing in the diff is failing. On the current head (
EvidenceThe peer-review gate — from the
The slow test jobs — jobs 6 and 7 sat in the While they were hanging I ran the same two shards locally against
That includes the card-related suites in those shards ( Not verified: the runtime behavior of the change in a browser. The PR's view run · no recording available |
|
@MelvinBot I think we only need to handle one number type, so there should be a single source of truth. |
monthlySettlementDate was typed Date | number: the backend sends a day of the month, but our own optimistic writes stored a Date. Two shapes in one Onyx field meant every reader had to narrow it. Make the field a day-of-month number everywhere. The optimistic writes now store new Date().getDate(), which displays identically to the Date they stored before (format(new Date(), 'do') and the helper both resolve to today's day), so the only change is the type. getMonthlySettlementDate loses its Date branch and is now display-only: the two settlement-frequency pages pass the raw value straight through as the rollback value. Co-authored-by: Linh Vo <linhvovan29546@users.noreply.github.com>
|
Done — You were right that the two shapes were the problem, not just the parsing. The field was The optimistic writes changed, not just the type. They stored
Everything else that had to follow the type
Checks: I tried to verify this in the browser now that a session was available, but the session went blank before any navigation — 0 DOM nodes, no network activity, while the dev server stayed healthy and compiled the changed pages without errors. That's a session-level stall rather than anything from this branch, but it does mean the settlement-frequency screen is still unverified in a browser. One thing I left alone: |
|
The one error is in a file this PR doesn't touch: Two PRs that were each green on their own collided when both landed:
The second PR's stale import path had nothing left to resolve to, so VerificationThe PR branch alone is clean. I ran The break comes from Timeline. The earlier run on this PR at 14:17 UTC had Scope. That netsuite page is the only file on I did not push anything: the fix belongs on Separately, Every other check passes. I could not verify anything in a browser for this, but a missing-module compile error needs no runtime check. view run · no recording available |
…rdMonthlySettlementDate Co-authored-by: Linh Vo <linhvovan29546@users.noreply.github.com>
|
Fixed — merged The That file isn't in this PR's diff. #100978 fixed it on VerificationRun locally on the merged branch:
The merge had no conflicts. The other failing check, I could not verify the change in a browser in this environment, so the runtime behavior remains covered only by the unit and UI tests. |
|
Still nothing to push. On the current head ( The earlier EvidenceFrom the
Everything else is green, including I did not verify the change's behavior in a browser on this run — there were no test or build failures to reproduce, so the code behavior is still covered only by the unit and UI tests in the PR. view run · no recording available |
|
@MelvinBot Please merge main branch and resolve conflict, then add the test for Card.ts::updateSettlementFrequency and CardReconciliationPage.tsx |
…rdMonthlySettlementDate # Conflicts: # tests/unit/CardUtilsTest.ts
Covers the two call sites that were converting the settlement day with new Date(): the optimistic write in Card.ts and the placeholder feed the reconciliation page falls back to. Co-authored-by: Linh Vo <linhvovan29546@users.noreply.github.com>
|
MelvinBot has made 6 consecutive commits trying to fix CI failures. Automatic CI fix dispatches are now paused for this PR. Next steps: A human should review the recent commits and resolve the conflicting CI errors manually. |
1 similar comment
|
MelvinBot has made 6 consecutive commits trying to fix CI failures. Automatic CI fix dispatches are now paused for this PR. Next steps: A human should review the recent commits and resolve the conflicting CI errors manually. |
|
Done — main is merged, the conflict is resolved, and both test files are in. Pushed as MergeOne conflict, in The local checkout was a depth-1 shallow clone, so the first Tests added
One caveat worth stating plainly: the settlement day in that placeholder feed is never rendered, so no runtime assertion can observe it directly. What actually stops a A pre-existing gap the new tests surfaced
This predates the PR — line 663 is unchanged by it — so I left the behavior alone rather than widen the scope. The test documents it as-is and is commented to flip to AI tests
Not run: the full |
|
@MelvinBot Please update the PR description to match the latest change on the PR |
|
🤖 Updated the PR description to match the current head ( The description had been written against the first commit and the approach changed substantially across the three commits since, so the old text described a fix that is no longer here. What was stale:
I left the Two checks are failing
The other seven Jest shards, One thing worth a second lookThe description now states as fact that the backend sends a day of the month, and the type change enforces it. I could not confirm that from this repo — it is inferred from the symptom (three workspaces with three different real dates all rendering the 31st, which is what That matters more now than it did before, because retyping the field to |
|
@arosiclair To make sure the bug is resolved, could you please supportal the user account as well? |
|
@MelvinBot Please merge the main branch and address the feedbacks above. |
Co-authored-by: Linh Vo <linhvovan29546@users.noreply.github.com>
…op unrelated test - CardUtils: keep only the one-line summary on toMonthlySettlementDate and drop the inline range comment - Card.updateSettlementFrequency: rename currentFrequency to currentMonthlySettlementDate - CardSettlementFrequencyTest: read Onyx through the shared getOnyxValue helper - Remove tests/ui/CardReconciliationPageTest.tsx, which was unrelated to this fix Co-authored-by: Linh Vo <linhvovan29546@users.noreply.github.com>
|
@linhvovan29546 @arosiclair One of you needs to copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
|
Merged
Local checks after the merge, all passing:
I also updated the One thing I could not do: I tried to re-verify the settings page in a browser after the merge, but the web test session came up blank — zero DOM nodes on every snapshot, despite the dev server returning |
linhvovan29546
left a comment
There was a problem hiding this comment.
@arosiclair Could you please check again? Thanks!
joekaufmanexpensify
left a comment
There was a problem hiding this comment.
Good for product.
|
@linhvovan29546 checklist |
Reviewer checklist here #100974 (comment) |
|
Right forgot that you can't edit the OP. I updated it |
|
🚧 arosiclair has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚀 Deployed to staging by https://github.com/arosiclair in version: 9.4.80-0 🚀
|
|
🤖 No help site changes are required — I did not create a docs PR. This PR fixes a bug: The relevant article is Set Up and Manage the Expensify Card - US, section When your Expensify Cards settle each month (docs/articles/new-expensify/expensify-card/Set-Up-and-Manage-the-Expensify-Card-US.md:105-109). It was added by [No QA] Docs updates for E/A#98164 show Expensify Card monthly settlement date and still matches the code after this fix. How I checkedI verified each claim in that section against the merged code and the English strings:
Other settlement-related articles I reviewed and ruled out:
The one genuinely new behavior is defensive: when the value cannot be resolved to a day between 1 and 31, the hint is hidden rather than showing a wrong date. That is an error-state fallback with no user-facing instruction attached, so per the help authoring guidelines it does not belong in the article. I verified the labels against the source strings rather than a live workspace, because showing this hint requires an Expensify Card workspace on a Monthly settlement frequency, which a fresh test account cannot reach. @linhvovan29546, there is no help site PR to review — please confirm you agree that the existing article already reflects the behavior this PR restores. If you think something here still needs a docs update, reply with what to change and I'll open the draft PR. |
|
🚀 Deployed to production by https://github.com/luacmartins in version: 9.4.80-0 🚀
Bundle Size Analysis (Sentry): |
Explanation of Change
Fixes how we parse
monthlySettlementDatewhich is a day of the month not a date timestampFixed Issues
$ #100912
Tests
Expensify cards will settle on the <day> of each month.(e.g.Expensify cards will settle on the 27th of each month.)Offline tests
None
QA Steps
Same as Tests
AI Tests
Test coverage added by this PR:
tests/unit/CardUtilsTest.ts—toMonthlySettlementDateresolves every day 1-31 to its own day, and returnsundefinedfor0,32, a non-integer, and a Unix timestamp.tests/ui/WorkspaceCardSettingsPageTest.tsx— the page renders the real day for amonthlySettlementDateof10, and renders no hint when the value is not a day of the month.tests/actions/CardSettlementFrequencyTest.ts(new) —updateSettlementFrequencywrites a day-of-month number optimistically and rolls back to the previous day on failure.tests/unit/TravelBillingTest.ts,tests/unit/TravelBillingUtilsTest.ts,tests/ui/WorkspaceTravelBillingSectionTest.tsx— updated for thenumbertype.Run locally after merging
mainand applying review feedback — all passed:npm run typechecknpm run linton the changed files,npm run spell-changed,oxfmttests/actions/CardSettlementFrequencyTest.ts,tests/unit/CardUtilsTest.ts,tests/ui/WorkspaceCardSettingsPageTest.tsx— 535 tests passedtests/unit/TravelBillingTest.ts,tests/unit/TravelBillingUtilsTest.ts,tests/ui/WorkspaceTravelBillingSectionTest.tsx— 78 tests passedThis change has not been verified in a browser against a real workspace with a backend-supplied settlement date. The unit and UI tests prove the page renders the correct day once Onyx holds a day-of-month number; they cannot prove what the backend actually sends. Confirming the
monthlySettlementDatevalue in the payload for one of the three affected workspaces would close that gap.PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))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: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari