fix(reports): surface disconnected accounts in the account filter - #104
Conversation
Disconnecting an account soft-deletes it but leaves its transactions live, and every aggregate keeps counting them. The Reports account filter was built from notDeleted(accounts), so those transactions had no entry to filter by and no way to be drilled into -- silent inclusion with no control, which the issue calls the worst of both. Keep the history in the totals. It is real spending that happened, and on the reporting household the disconnected accounts cover Feb-May with their live replacements starting May 31, so excluding them would drop a contiguous period rather than remove a duplicate. What was missing was visibility, so: - getReportFilterAccounts() returns live AND soft-deleted accounts, each flagged, with its transaction count and date span. - The account popover splits into "Active" and "Disconnected" groups. Disconnected rows carry their span, which is what distinguishes a superseded account from a duplicated one. - formatTxnSpan() states the year once within a year and on both ends across one, so a range is never ambiguous. Aggregates are unchanged -- they already honour the accountIds filter, so ticking a box now does the right thing. Everything is ticked by default, so existing totals do not move. Closes #87.
2cdd47a to
ae51a6b
Compare
Mutation gate: baseline comparisonRan Stryker on
This PR raises mutation coverage and still fails the gate. Every one of the 15 mutants
Also: the first run mutated a file this PR doesn't touchIt reported three files — That stale-branch behaviour is worth folding into #103 — |
|
Rebase confirmed: the gate now mutates the two files this PR actually changes ( Per-file numbers are unchanged from the baseline comparison above: Merging with All other checks green — |
Closes #87.
Decision
The issue left the semantics open — cascade the soft-delete, or keep the transactions and make them filterable. Keeping them, because the history is real spending: on the reporting household the three disconnected accounts cover Feb–May and their live replacements start 2026-05-31, with no overlapping rows. Excluding them would drop a contiguous period, not remove a duplicate.
What was actually missing is what the issue names — "the problem is attribution and control, not duplication."
Mock reviewed before implementation: https://claude.ai/code/artifact/11f44c49-c082-4f98-ae4c-179ba283f949
Change
getReportFilterAccounts()(src/queries/accounts.ts) — returns live and soft-deleted accounts, each withdisconnected,txnCount,firstTxnDate,lastTxnDate. Live accounts sort first.getAccounts()is untouched, since the accounts page and net-worth queries should keep excluding deleted rows.Account popover (
report-filter-bar.tsx) — splits intoActiveandDisconnectedgroups, the latter only rendering when there is something in it. Disconnected rows are muted and carry their date span.formatTxnSpan()(date-utils.ts) —Feb 11 – May 11, 2026within a year,Nov 20, 2025 – Jan 8, 2026across one.What does not change
Aggregates are untouched. They already honour the
accountIdsfilter, so ticking a box now does the right thing without threading a new flag through 13 query sites. Everything is ticked by default, so no existing total moves — this adds control, it does not silently restate anyone's numbers.Two implementation notes
LEFT JOINrepeats the household predicate on the joined side:scopedQueryonly constrains the driving table, so relying on it alone would have let another household's transactions into the counts. There is a regression test for exactly that.count(*)over aLEFT JOINcounts the null-filled row, so an account with no transactions would report1. Guarded, and tested.Known gap (not in scope)
The dashboard has no account filter, so it keeps including these transactions with no way to opt out. Consistent with "the history is real", but worth a follow-up — say the word and I'll file it.
Tests
8 new, written red first:
accounts-queries.test.ts): flagging, sort order, date span, the empty-account zero-count guard, household isolationdate-utils.test.ts): within-year, cross-year, single-day spansFull suite green: 759 passed / 113 files. Typecheck, lint, and
pnpm buildall clean.mutation (diff)gate may report red — see #103. It mutates whole changed files, and since #100 the DB halves report as no-coverage, so it fails on pre-existing debt regardless of the diff. Compare against amainbaseline before treating it as a real finding.🤖 Generated with Claude Code