Skip to content

fix(reports): surface disconnected accounts in the account filter - #104

Merged
KenTaniguchi-R merged 1 commit into
mainfrom
fix/87-disconnected-account-filter
Aug 30, 2026
Merged

fix(reports): surface disconnected accounts in the account filter#104
KenTaniguchi-R merged 1 commit into
mainfrom
fix/87-disconnected-account-filter

Conversation

@KenTaniguchi-R

Copy link
Copy Markdown
Owner

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 with disconnected, 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 into Active and Disconnected groups, 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, 2026 within a year, Nov 20, 2025 – Jan 8, 2026 across one.

What does not change

Aggregates are untouched. They already honour the accountIds filter, 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

  • The LEFT JOIN repeats the household predicate on the joined side: scopedQuery only 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 a LEFT JOIN counts the null-filled row, so an account with no transactions would report 1. 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:

  • 5 integration (accounts-queries.test.ts): flagging, sort order, date span, the empty-account zero-count guard, household isolation
  • 3 unit (date-utils.test.ts): within-year, cross-year, single-day spans

Full suite green: 759 passed / 113 files. Typecheck, lint, and pnpm build all clean.

⚠️ The 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 a main baseline before treating it as a real finding.

🤖 Generated with Claude Code

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.
@KenTaniguchi-R
KenTaniguchi-R force-pushed the fix/87-disconnected-account-filter branch from 2cdd47a to ae51a6b Compare August 30, 2026 00:42
@KenTaniguchi-R

Copy link
Copy Markdown
Owner Author

Mutation gate: baseline comparison

Ran Stryker on origin/main (which has neither getReportFilterAccounts nor formatTxnSpan) over the same two files, so this is a true before/after.

file main this PR delta
date-utils.ts 41.67% — 50 killed, 15 survived, 55 no-cov 48.15% — 65 killed, 15 survived, 55 no-cov +6.48pp, +15 killed, zero new survivors
accounts.ts 0.00% — 0 killed, 0 survived, 44 no-cov 0.00% — 0 killed, 0 survived, 57 no-cov unchanged

This PR raises mutation coverage and still fails the gate. Every one of the 15 mutants formatTxnSpan introduced is killed by its 3 new tests — the survivor count did not move.

accounts.ts sits at 0.00% with zero mutants covered — not one killed, not one survived, before or after. getReportFilterAccounts has 5 new integration tests and the file has 11 in total, but vitest.stryker.config.ts excludes tests/integration/ wholesale, so a query module has no reachable tests during mutation by construction. This is #103 in its purest form.

Also: the first run mutated a file this PR doesn't touch

It reported three files — date-utils.ts, accounts.ts, and src/lib/simplefin/sync.ts. sync.ts appeared only because this branch was cut before #102 merged, so git diff origin/main HEAD saw the old sync.ts as a change. Rebased onto main; the diff is now the two files this PR actually changes.

That stale-branch behaviour is worth folding into #103mutate-diff.sh diffs against a moving origin/main, so any branch that falls behind drags unrelated files (and their legacy debt) into the gate.

@KenTaniguchi-R

Copy link
Copy Markdown
Owner Author

Rebase confirmed: the gate now mutates the two files this PR actually changes (date-utils.ts, accounts.ts) — sync.ts is gone from the scope, and the overall score rose 31.70% → 33.85% purely from dropping that unrelated debt.

Per-file numbers are unchanged from the baseline comparison above: date-utils.ts 41.67% → 48.15%, accounts.ts 0.00% → 0.00% (zero mutants covered either way).

Merging with mutation (diff) red on the same basis as #102: the gate is measuring pre-existing and structurally-unreachable debt, not this diff. Tracked in #103.

All other checks green — typecheck · lint · test, CodeQL, Analyze, GitGuardian.

@KenTaniguchi-R
KenTaniguchi-R merged commit f02ab5e into main Aug 30, 2026
4 of 5 checks passed
@KenTaniguchi-R
KenTaniguchi-R deleted the fix/87-disconnected-account-filter branch August 30, 2026 00:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Disconnecting an account soft-deletes it but leaves its transactions in every report, unfilterable

1 participant