Skip to content

Read the Expensify Card monthly settlement date instead of guessing it - #100974

Merged
arosiclair merged 10 commits into
mainfrom
claude-fixExpensifyCardMonthlySettlementDate
Sep 15, 2026
Merged

arosiclair merged 10 commits into
mainfrom
claude-fixExpensifyCardMonthlySettlementDate

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

Fixes how we parse monthlySettlementDate which is a day of the month not a date timestamp

Fixed Issues

$ #100912

Tests

  1. Sign in to an account that is an admin of a workspace with the Expensify Card feature enabled and a Monthly settlement frequency card.
  2. Go to Settings > Workspaces > [workspace] > Expensify Card > Settings.
  3. Verify the Settlement frequency row title reads Monthly and the supporting hint text below it reads Expensify cards will settle on the <day> of each month. (e.g. Expensify cards will settle on the 27th of each month.)
  • Verify that no errors appear in the JS console

Offline tests

None

QA Steps

Same as Tests

AI Tests

Test coverage added by this PR:

  • tests/unit/CardUtilsTest.ts — toMonthlySettlementDate resolves every day 1-31 to its own day, and returns undefined for 0, 32, a non-integer, and a Unix timestamp.
  • tests/ui/WorkspaceCardSettingsPageTest.tsx — the page renders the real day for a monthlySettlementDate of 10, and renders no hint when the value is not a day of the month.
  • tests/actions/CardSettlementFrequencyTest.ts (new) — updateSettlementFrequency writes 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 the number type.

Run locally after merging main and applying review feedback — all passed:

  • npm run typecheck
  • npm run lint on the changed files, npm run spell-changed, oxfmt
  • tests/actions/CardSettlementFrequencyTest.ts, tests/unit/CardUtilsTest.ts, tests/ui/WorkspaceCardSettingsPageTest.tsx — 535 tests passed
  • tests/unit/TravelBillingTest.ts, tests/unit/TravelBillingUtilsTest.ts, tests/ui/WorkspaceTravelBillingSectionTest.tsx — 78 tests passed

This 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 monthlySettlementDate value in the payload for one of the three affected workspaces would close that gap.

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

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>
@MelvinBot
MelvinBot requested a review from a team September 11, 2026 14:17
@melvin-bot melvin-bot Bot added Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web labels Sep 11, 2026
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I created this PR from a branch on Expensify/App because I couldn't find a fork of this repo I can push to under linhvovan29546.

To take ownership of the branch so you can push updates to it, run:

git remote add upstream https://github.com/Expensify/App.git
git fetch upstream claude-fixExpensifyCardMonthlySettlementDate
git checkout -b claude-fixExpensifyCardMonthlySettlementDate upstream/claude-fixExpensifyCardMonthlySettlementDate
git push -u origin claude-fixExpensifyCardMonthlySettlementDate

Then close this PR and open a new one from your fork.

@codecov

codecov Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/libs/CardUtils.ts 85.79% <100.00%> (+0.09%) ⬆️
src/libs/TravelBillingUtils.ts 92.59% <ø> (ø)
src/libs/actions/Card.ts 2.69% <100.00%> (+2.69%) ⬆️
...counting/reconciliation/CardReconciliationPage.tsx 0.00% <ø> (ø)
...kspace/expensifyCard/WorkspaceCardSettingsPage.tsx 93.33% <100.00%> (+0.15%) ⬆️
.../WorkspaceTravelBillingSettlementFrequencyPage.tsx 0.00% <ø> (ø)
src/libs/actions/TravelBilling.ts 58.58% <75.00%> (ø)
... and 5 files with indirect coverage changes

@MelvinBot

Copy link
Copy Markdown
Contributor Author

There's nothing to fix in the code — the only failing check is Check independent approval, which fails because this PR has zero approvals so far, not because of anything in the diff. It clears itself once an eligible reviewer approves.

Every other check on the latest run passes: typecheck, ESLint check, Oxfmt check, spellcheck, all 8 test jobs, Storybook tests, Bun tests, React Compiler Compliance, Check for unused styles, both perf-test jobs, both codecov reports, and all four build jobs.

Evidence

From the Check independent approval job log:

Insufficient independent peer review {
  approvers: [],
  independentApprovers: [],
##[error]Expensify/App#100974 does not have enough independent eligible reviewer approvals.

scripts/verifyPeerReview.ts ran against this PR and found an empty approver list. That's a review-state gate, not a build or test failure — no commit can satisfy it.

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.


view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Web test steps verification results: ❌ not run

I could not produce screenshots or recordings, for two separate reasons:

  1. There are no documented testing steps to run. The ### Tests and ### QA Steps sections of this PR are still the // TODO: **The human co-author** must fill out... placeholders. The only checkbox in either is "Verify that no errors appear in the JS console."
  2. Browser control was unavailable in this run. Web prep failed (no dev SSL cert / web backend), so NewDot could not be driven in a browser at all.

So the evidence below comes from CI and static analysis only — not from a browser.

What I did verify

CI runs the tests that encode the issue's repro, and they pass. All 8 test (job N) shards are green on a33e4e4, including the 10 new getMonthlySettlementDate unit tests and the 3 new WorkspaceCardSettingsPage UI tests added by this PR. typecheck, ESLint check, Oxfmt check, spellcheck, React Compiler Compliance, and Storybook tests also pass. The only failing check is Check independent approval, which is not a code failure.

The UI test that most directly maps to the issue writes monthlySettlementDate: 10 into Onyx and asserts the page renders "Expensify cards will settle on the 10th of each month." — that is the issue's repro, at the component level.

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. new Date(10) is 10 milliseconds after the Unix epoch, which is 31 Dec 1969 anywhere west of UTC — the same wrong day for every workspace, regardless of the real value. That the symptom is identical across three different real dates is consistent with the day-of-month-as-number hypothesis, and inconsistent with an actual date being sent and mis-formatted.

Code trace
  • src/libs/CardUtils.ts:1520 — getMonthlySettlementDate narrows the value at runtime: passes a valid Date through, reads a 1–2 digit number or numeric string as a day of the month anchored to January (so every day 1–31 exists), parses the DB datetime and date-only formats in local time, then falls back to new Date() for ISO 8601. parse is already imported at CardUtils.ts:53.
  • The explicit-parse-then-fall-back shape matches the existing isLastScrapePastDismissThreshold handling of lastScrape, so it is not a new pattern.
  • WorkspaceCardSettingsPage.tsx:60-65 — the hint now renders only when a real day resolves, which also protects format() from throwing on an invalid date.
  • WorkspaceSettlementFrequencyPage.tsx:81 passes the result into updateSettlementFrequency, whose 4th parameter is typed currentFrequency?: Date — the types line up, and typecheck confirms it.
  • Frequency selection itself is unaffected: WorkspaceCardsListLabel.tsx:167 and TravelBillingUtils.ts:146 only test truthiness of the raw value, which the widened type does not change.

What only a browser or the backend can settle

The 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:

  • Check the OpenPolicyExpensifyCardsPage (or equivalent) response payload for one of the three affected workspaces and confirm what monthlySettlementDate holds.
  • If it turns out the backend sends a real month-end date rather than a day of the month, this PR hardens the display but the wrong day will persist and a backend fix is still needed.

Worth noting for whoever picks this up: getMonthlySettlementDate deliberately returns undefined for a Unix timestamp (see the 1706353253 case in the unit tests), because a seconds timestamp and a milliseconds one are indistinguishable. If the backend sends a timestamp, the hint disappears rather than showing a wrong date — safe, but it would look like a regression to a tester who doesn't know that.

Once the Tests section is filled in and web prep is working, re-run me and I'll capture the screenshots.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

⚠️ A setup step failed before I could start. This is usually a temporary issue — try again in a few minutes, and if it persists post in #melvinbot-ai.

Run details

@linhvovan29546

Copy link
Copy Markdown
Contributor

@MelvinBot Since monthlySettlementDate is always a number the day of the month, I think we only need to handle the number type.

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>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done — dropped the string handling in 632481b. You're right that the backend only ever sends a day-of-month number, and all the string parsing was speculative: I wrote it because I couldn't confirm the payload shape from this repo, not because I had evidence for it.

One thing I did not drop: the Date branch. It isn't about the backend — we write a Date into this Onyx key ourselves, in three places:

All three use new Date() purely as a truthy "settles monthly" marker, since TravelBillingUtils.ts:146 and the settings page derive the frequency from truthiness alone. So between the optimistic write and the backend response, the value in Onyx genuinely is a Date. Typing it number fails typecheck at those three call sites, and dropping the runtime branch would blank the hint during that window.

So the type is now Date | number and the helper is down to two branches:

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
  • CardUtils.ts:1520 — removed the numeric-string regex, both parse() calls (DB datetime and date-only formats) and the new Date() ISO fallback. 37 lines down to 21.
  • ExpensifyCardSettings.ts:23 — Date | string | number → Date | number.
  • Tests: dropped the four string cases. Replaced the datetime UI test with the 1st-of-the-month boundary (Workspace B in the issue), and the unreadable-value test now uses a Unix timestamp instead of 'unparseable'.

I kept the 1–31 range guard. Without it a stray value rolls over — new Date(2026, 0, 45) is 14 February — so the guard is what makes "no hint rather than a wrong date" actually hold.

Verified locally on 632481b: npm run typecheck passes; npm test on CardUtilsTest, WorkspaceCardSettingsPageTest, TravelBillingTest and TravelBillingUtilsTest passes (583 tests); npm run lint passes on the four changed files. I could not run cspell in this environment — CI covers it.

Open question, your call: the optimistic new Date() writes are arguably the real wart here. They make the page show today as the settlement date for a moment after you switch to Monthly, which is its own small bug. If those three sites wrote something that isn't a date at all, monthlySettlementDate could be a plain number and the Date branch would disappear entirely. That's a wider change than this PR, so I left it alone — say the word if you want it in scope.

@MelvinBot

MelvinBot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor Author

No code fix is needed — nothing in the diff is failing. On the current head (632481bd) the only red check is Check independent approval, which fails because the PR has zero approvals. It clears when an eligible reviewer approves.

test (job 6) and test (job 7) looked stuck for about 45 minutes, but that was a runner stall, not your code — both have since finished green. Every other check passes.

Evidence

The peer-review gate — from the Check independent approval job log:

Insufficient independent peer review {
  approvers: [],
  independentApprovers: [],
##[error]Expensify/App#100974 does not have enough independent eligible reviewer approvals.

scripts/verifyPeerReview.ts found an empty approver list. That's review state, not build state — no commit can satisfy it.

The slow test jobs — jobs 6 and 7 sat in the Setup Node step from 14:57 onward on Blacksmith runner blacksmith-8vcpu-ubuntu-2404. Jest never started, so no test could have been the cause. Both eventually reported success.

While they were hanging I ran the same two shards locally against 632481bd:

  • npm test -- --shard=6/8 → 177 suites passed, 2,774 tests
  • npm test -- --shard=7/8 → 178 suites passed, 2,632 tests

That includes the card-related suites in those shards (tests/actions/PolicyExpensifyCardTest.ts, tests/unit/ExpensifyCardFeedSelectorUtilsTest.ts, tests/unit/hooks/useAssignCard.test.ts).

Not verified: the runtime behavior of the change in a browser. The PR's Tests and QA Steps sections are still TODO placeholders, so there were no documented steps to run — those need filling in before review.


view run · no recording available

@linhvovan29546

Copy link
Copy Markdown
Contributor

@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>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done — monthlySettlementDate is now a day-of-month number everywhere, in af2809a.

You were right that the two shapes were the problem, not just the parsing. The field was Date | number because the backend sends a day of the month but our own optimistic writes stored a Date, so every reader had to narrow it. Now there is one shape.

The optimistic writes changed, not just the type. They stored new Date() purely as a truthy "settles monthly" marker; they now store new Date().getDate(). That displays identically — format(new Date(), 'do') and getMonthlySettlementDate(new Date().getDate()) both resolve to today's day — so this is a type change, not a behavior change. Sites updated: Card.ts:667, TravelBilling.ts:93, TravelBilling.ts:313, and the placeholder in CardReconciliationPage.tsx:65.

getMonthlySettlementDate is now display-only, with one production caller. Its Date branch is gone. Both settlement-frequency pages now pass the raw value through as the rollback value instead of converting it first, which dropped the helper from those two files entirely.

Everything else that had to follow the type
  • ExpensifyCardSettings.ts:23 — monthlySettlementDate?: number, and previousMonthlySettlementDate?: number on line 29, since it holds the same value for rollback.
  • Three signatures that took a Date: updateSettlementFrequency, updateTravelBillingSettlementFrequency, and clearTravelBillingSettlementFrequencyErrors.
  • A stale doc comment in TravelBillingUtils.ts:137 that described the value as a Date.
  • Test fixtures in WorkspaceTravelBillingSectionTest, TravelBillingUtilsTest, and TravelBillingTest. In TravelBillingTest the expected optimistic value is now derived as mockDate.getDate() rather than hard-coded, because the day is read in local time and new Date('2024-05-20') is UTC midnight — hard-coding 20 would fail in any timezone west of UTC.
  • Dropped two unit tests that only existed to cover the Date branch, and one page test that became a duplicate once the value was always a number.

Checks: typecheck passes. The five affected suites pass — 603 tests. ESLint and cspell are clean on the changed lines (ESLint still reports pre-existing grandfathered errors on untouched lines in CardUtils.ts, Card.ts, TravelBilling.ts, and CardReconciliationPage.tsx). React Compiler compliance passes on the three changed pages. lint-changed and spell-changed can't resolve the base ref in this environment, so I ran ESLint and cspell against the changed files directly.

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: updateSettlementFrequency's fourth parameter is named currentFrequency but holds a settlement date, not a frequency. It's pre-existing and renaming it is cosmetic, so I didn't fold it into this change — happy to if you want it.

@MelvinBot

MelvinBot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor Author

typecheck is broken on main, not by this PR — and a fix is already open as #100978 by mountiny. Nothing to push here. Once that merges, re-run checks on this PR and typecheck will go green.

The one error is in a file this PR doesn't touch:

src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteInvoiceItemPreferenceSelectPage.tsx(1,20):
error TS2307: Cannot find module '@components/ButtonComposed' or its corresponding type declarations.

Two PRs that were each green on their own collided when both landed:

PR Merged What it did
1 #100000 Sep 10, 12:22 UTC Renamed src/components/ButtonComposed → src/components/Button
2 #99239 Sep 11, 15:13 UTC Branched before the rename, so it still imports @components/ButtonComposed

The second PR's stale import path had nothing left to resolve to, so main went red 36 minutes before this PR's typecheck ran.

Verification

The PR branch alone is clean. I ran npx tsc --noEmit -p tsconfig.json on the PR head (af2809a) — exit code 0, no output.

The break comes from main. CI typechecks the merge ref, not the PR head. On refs/pull/100974/merge, DynamicNetSuiteInvoiceItemPreferenceSelectPage.tsx:1 reads import Button from '@components/ButtonComposed';, while git ls-tree on that same ref returns zero entries matching src/components/ButtonComposed. tsconfig.json:21 maps @components/* to ./src/components/*, and no .d.ts declares the module — so it cannot resolve. The PR head does not contain that version of the file at all.

Timeline. The earlier run on this PR at 14:17 UTC had typecheck passing; the 15:49 UTC run failed. #99239 merged at 15:13 UTC, in between.

Scope. That netsuite page is the only file on main importing the old path, which matches the single-error output and the one-line fix in #100978.

I did not push anything: the fix belongs on main, and putting it in this PR would mix an unrelated change into a settlement-date PR.

Separately, Check independent approval is still failing because this PR has no approvals yet (approvers: []). That clears when an eligible reviewer approves — no commit can satisfy it.

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>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Fixed — merged main into the branch in 5b700a8, which picks up the typecheck fix.

The typecheck failure was never caused by this PR. It came from a broken import on main:

src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteInvoiceItemPreferenceSelectPage.tsx(1,20):
error TS2307: Cannot find module '@components/ButtonComposed' or its corresponding type declarations.

That file isn't in this PR's diff. #100978 fixed it on main at 15:54 UTC — five minutes after the failing run started at 15:49, so the branch was still built against the broken main. Merging main in resolves it.

Verification

Run locally on the merged branch:

  • npm run typecheck — passed (all five tsconfigs)
  • npm test -- tests/unit/CardUtilsTest.ts tests/unit/TravelBillingTest.ts tests/unit/TravelBillingUtilsTest.ts tests/ui/WorkspaceCardSettingsPageTest.tsx tests/ui/WorkspaceTravelBillingSectionTest.tsx — 603 passed, 5 suites
  • npm run lint on the eight changed source files — clean

The merge had no conflicts.

The other failing check, Check independent approval, is a review gate rather than a build failure — it reports an empty approver list and clears once an eligible reviewer approves. No commit can satisfy it.

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.

@MelvinBot

MelvinBot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Still nothing to push. On the current head (5b700a8) all 34 checks pass, and the single failure is Check independent approval — a review-state gate that fails because this PR has no approvals yet. No commit can satisfy it; it clears when an eligible reviewer approves.

The earlier typecheck failure is gone — merging main in 5b700a8 picked up the fix for the broken @components/ButtonComposed import on main.

Evidence

From the Check independent approval job log on 5b700a8:

Insufficient independent peer review {
  approvers: [],
  independentApprovers: [],

scripts/verifyPeerReview.ts ran against this PR and found an empty approver list.

Everything else is green, including typecheck, ESLint check, Oxfmt check, spellcheck, all 8 test jobs, Bun tests, Storybook tests, React Compiler Compliance, Check for unused styles, both perf-test jobs, both codecov reports, and all four build jobs.

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

@linhvovan29546

Copy link
Copy Markdown
Contributor

@MelvinBot Please merge main branch and resolve conflict, then add the test for Card.ts::updateSettlementFrequency and CardReconciliationPage.tsx

MelvinBot and others added 2 commits September 12, 2026 02:33
…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

Copy link
Copy Markdown
Contributor Author

⚠️ MelvinBot CI Fix Loop Detected

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

Copy link
Copy Markdown
Contributor Author

⚠️ MelvinBot CI Fix Loop Detected

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.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done — main is merged, the conflict is resolved, and both test files are in. Pushed as 924c702, and the PR is back to MERGEABLE.

Merge

One conflict, in tests/unit/CardUtilsTest.ts: both sides appended a new describe block at the end of the file — this branch added getMonthlySettlementDate, main added getWalletProviderNameKey. Kept both, no assertions changed on either side.

The local checkout was a depth-1 shallow clone, so the first git merge failed with "refusing to merge unrelated histories." Deepening the clone gave a real merge base and the merge went through normally.

Tests added

tests/actions/CardSettlementFrequencyTest.ts — 7 tests for updateSettlementFrequency. The one that guards this PR's fix asserts the optimistic write is a plain day-of-month number and equals new Date().getDate(), so a Date can't creep back into Card.ts:660. The rest cover the day staying in the 1–31 range the settings page can render, switching to daily clearing the date, success keeping the optimistic day, failure rolling back to the previous day, and the request sending only the frequency (never the day — the backend picks it).

tests/ui/CardReconciliationPageTest.tsx — 5 tests, a new file; the page had no coverage at all. One exercises the fallback placeholder feed at CardReconciliationPage.tsx:65 that this PR changed, by rendering with no card feed in Onyx. The others cover the continuous reconciliation toggle, the reconciliation account row appearing and hiding with the toggle, and the auto-sync explainer.

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 Date going back in there is typecheck against the narrowed monthlySettlementDate?: number. The new test covers the path the placeholder is used on, not the value itself.

A pre-existing gap the new tests surfaced

updateSettlementFrequency doesn't roll back when a daily workspace fails to switch to monthly. There's no previous day, so currentFrequency is undefined and Card.ts:663 merges {monthlySettlementDate: undefined}. Onyx drops undefined keys rather than clearing them, so the optimistic day survives the failed request and the page stays on "Monthly" with a day the backend never agreed to. Clearing it would need null, the way the daily branch already does.

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 toBeUndefined() if you fix it. Happy to include the one-line fix here if you'd rather not leave it.

AI tests

  • npm run typecheck — passed
  • npm run lint-changed — passed
  • npm run spell-changed — passed (15 files, 0 issues)
  • npm run react-compiler-compliance-check check <the four pages> — passed (check-changed still can't resolve the base ref in this environment, so the files were passed explicitly)
  • npm test -- tests/actions/CardSettlementFrequencyTest.ts tests/ui/CardReconciliationPageTest.tsx tests/unit/CardUtilsTest.ts tests/ui/WorkspaceCardSettingsPageTest.tsx — passed, 540 tests

Not run: the full npm test suite and the Storybook smoke test. I also left the AI Tests section in the PR description alone rather than rewrite the whole body around it — the results above supersede it.

@linhvovan29546

Copy link
Copy Markdown
Contributor

@MelvinBot Please update the PR description to match the latest change on the PR

@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Updated the PR description to match the current head (924c702).

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:

Old description said Actually true now
Types monthlySettlementDate as Date | string | number Typed as number only — the day of the month, 1-31
getMonthlySettlementDate parses numeric strings, the Expensify DB datetime format, and ISO 8601 All string parsing was removed. It only validates a 1-31 integer and anchors it to January
"Uses the same helper on the two settlement-frequency pages" WorkspaceSettlementFrequencyPage.tsx is no longer touched at all
"The exact value the backend sends could not be confirmed, so the helper covers every shape" The PR now commits to day-of-month everywhere, so that hedge no longer describes the code
— Four optimistic writes now store new Date().getDate() instead of a whole Date — not mentioned at all before
— previousMonthlySettlementDate retyped to number — not mentioned before
— Two new test files (tests/actions/CardSettlementFrequencyTest.ts, tests/ui/CardReconciliationPageTest.tsx) — not mentioned before

I left the Tests and QA Steps sections as the TODO placeholders — those are yours to fill in, not mine.

Two checks are failing

Oxfmt check — caused by this PR, needs a one-line fix. tests/actions/CardSettlementFrequencyTest.ts (a file this PR adds) is not formatted. Run npm run fmt and commit. I did not push this myself since you asked only for a description update — say the word and I'll do it.

test (job 4) — not caused by this PR. It fails on three cases in tests/navigation/NavigateTests.tsx, all Expected: "Workspaces_List" / Received: undefined. This PR touches no navigation code. That file was last changed on main by ea9470cb (PR #100387, merged 2026-09-11 18:17Z), and this branch picked it up in the main merge at 4cedfac0. The test.yml run on main at 7e3040ec is also failing, so it is broken upstream.

The other seven Jest shards, typecheck, ESLint check, spellcheck, React Compiler Compliance, Storybook tests, and Bun tests all pass.

One thing worth a second look

The 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 new Date(10) gives west of UTC). It is a strong inference, but it is still an inference.

That matters more now than it did before, because retyping the field to number is no longer just a display fix — it changes what four optimistic writes store. If the backend actually sends something else, those writes are now wrong in a new way. Checking monthlySettlementDate in the OpenPolicyExpensifyCardsPage payload for one of the three affected workspaces would settle it in a minute.

@melvin-bot
melvin-bot Bot requested a review from arosiclair September 12, 2026 09:55
@linhvovan29546

linhvovan29546 commented Sep 13, 2026 •

Copy link
Copy Markdown
Contributor

@arosiclair To make sure the bug is resolved, could you please supportal the user account as well?
https://expensify.slack.com/archives/C049HHMV9SM/p1789081429798599

Comment thread src/libs/actions/Card.ts Outdated
Comment thread src/libs/CardUtils.ts Outdated
Comment thread src/libs/CardUtils.ts Outdated
Comment thread tests/actions/CardSettlementFrequencyTest.ts Outdated
Comment thread tests/ui/CardReconciliationPageTest.tsx Outdated
@linhvovan29546

Copy link
Copy Markdown
Contributor

@MelvinBot Please merge the main branch and address the feedbacks above.

MelvinBot and others added 2 commits September 14, 2026 13:37
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>
@MelvinBot
MelvinBot requested a review from a team as a code owner September 14, 2026 13:42
@melvin-bot

melvin-bot Bot commented Sep 14, 2026

Copy link
Copy Markdown

@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]

@melvin-bot
melvin-bot Bot removed the request for review from a team September 14, 2026 13:42
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Merged main and addressed the review feedback. Pushed as 12cf89cfea0.

Feedback What changed
Card.ts — rename the rollback param currentFrequency → currentMonthlySettlementDate, and the failureValue read renamed with it
CardUtils.ts — "second part goes without saying" Doc comment on toMonthlySettlementDate is just the one-line summary now
CardUtils.ts — drop the inline comment Removed
CardSettlementFrequencyTest.ts — use getOnyxValue Swapped the hand-rolled Onyx.connect helper for tests/utils/getOnyxValue, which also dropped the ExpensifyCardSettings and OnyxEntry imports
CardReconciliationPageTest.tsx — unrelated Deleted
CardReconciliationPage.tsx — remove vs. fix the date Left as-is: linhvovan29546 confirmed the page never reads monthlySettlementDate or isMonthlySettlementAllowed, so they're redundant here

main merged cleanly — no conflicts.

Local checks after the merge, all passing:

  • npm run typecheck
  • npm run lint on the changed files, npm run spell-changed, oxfmt --write (no reformatting needed)
  • tests/actions/CardSettlementFrequencyTest.ts, tests/unit/CardUtilsTest.ts, tests/ui/WorkspaceCardSettingsPageTest.tsx — 535 tests passed
  • tests/unit/TravelBillingTest.ts, tests/unit/TravelBillingUtilsTest.ts, tests/ui/WorkspaceTravelBillingSectionTest.tsx — 78 tests passed

I also updated the AI Tests section of the PR description, which still listed the deleted test file and a stale CI status.

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 200. Separately, the console snippet in the test steps isn't something the web driver can run (arbitrary page scripting is out of scope), so steps 2 and 3 would need a real workspace with monthly settlement rather than an Onyx merge. The UI test tests/ui/WorkspaceCardSettingsPageTest.tsx still covers the same assertion — it renders the real day for a monthlySettlementDate of 10 and no hint when the value isn't a day of the month — but that's Jest, not a browser. Manual verification on the settings page is still worth doing before merge.

@linhvovan29546 linhvovan29546 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arosiclair Could you please check again? Thanks!

@joekaufmanexpensify joekaufmanexpensify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good for product.

@arosiclair arosiclair left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@arosiclair

Copy link
Copy Markdown
Contributor

@linhvovan29546 checklist

@linhvovan29546

Copy link
Copy Markdown
Contributor

@linhvovan29546 checklist

Reviewer checklist here #100974 (comment)

@arosiclair

Copy link
Copy Markdown
Contributor

Right forgot that you can't edit the OP. I updated it

@arosiclair
arosiclair merged commit 5540b61 into main Sep 15, 2026
50 of 54 checks passed
@arosiclair
arosiclair deleted the claude-fixExpensifyCardMonthlySettlementDate branch September 15, 2026 14:26
@github-actions

Copy link
Copy Markdown
Contributor

🚧 arosiclair has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/arosiclair in version: 9.4.80-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 No help site changes are required — I did not create a docs PR.

This PR fixes a bug: monthlySettlementDate is a day of the month (1–31), and the app was passing it to new Date(), which read it as milliseconds since the epoch. The user-facing behavior it restores is already documented correctly on the help site, so there is nothing to update.

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 checked

I verified each claim in that section against the merged code and the English strings:

Article claim Code Match
Row is labeled Settlement frequency settlementFrequency: 'Settlement frequency' — src/languages/en.ts:6373 ✅
Hint reads Expensify cards will settle on the 27th of each month. monthlySettlementDate: (date) => \Expensify cards will settle on the ${date} of each month.`` — src/languages/en.ts:6376 ✅
Hint shows only when frequency is Monthly; none on Daily src/pages/workspace/expensifyCard/WorkspaceCardSettingsPage.tsx:60-65 ✅
The day is reference only and cannot be chosen No UI writes a specific day — updateSettlementFrequency only writes today's day of the month when switching to Monthly (src/libs/actions/Card.ts:660) ✅

Other settlement-related articles I reviewed and ruled out:

  • Expensify Classic card articles (Expensify-Card-Statements.md, Admin-Card-Settings-and-Features.md, Expensify-Card-Reconciliation.md) — Classic UI, untouched by this PR.
  • Enable Consolidated Travel Billing on a Workspace — mentions settlement frequency generally ("for example, monthly") and never states a settlement day, so the Travel Billing type changes in this PR do not affect it.
  • Set Up and Manage the Expensify Card - UK/EU — covers the settlement account only, not the settlement day.

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.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/luacmartins in version: 9.4.80-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 failure ❌

Bundle Size Analysis (Sentry):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants