Skip to content

Track an account without counting it - #35

Merged
ShichengRao merged 1 commit into
mainfrom
claude/net-worth-plaid-coverage-656593
Aug 13, 2026
Merged

ShichengRao merged 1 commit into
mainfrom
claude/net-worth-plaid-coverage-656593

Conversation

@ShichengRao

Copy link
Copy Markdown
Owner

Summary

Lets an account live in the ledger without being counted in the numbers.

A donor-advised fund is the clearest case. The giving is already counted as a donation when it leaves the account that funded it, so the fund's own rows — the mirrored contribution coming in, and the grants going out — would count that same money a second time. Its balance is money you no longer own, so it does not belong in net worth either. But the rows themselves are worth keeping: they are how you see when a grant actually went out.

Pairing the two sides as a transfer is the wrong tool here. The pairing holds both rows at kind=transfer, and the Sankey deliberately ignores transfers, so the donation would vanish from the chart instead of landing once on the contribution date.

Type of Change

  • Importer support
  • Bug fix
  • Tests
  • Documentation
  • Frontend
  • Backend
  • Refactor / cleanup

Details

Three pieces, each usable on its own.

"balances": false on a Plaid account mapping. The account's transactions stage as usual; no balance row is ever built for it, so it never enters the net-worth series — net worth is the sum of snapshot balance rows, so an account with no rows contributes nothing to any total, category breakdown, or snapshot. The mapping is skipped entirely rather than emitting a null row, since a null balance already means "account did not exist yet" and must not be asserted implicitly. validateSource rejects a non-boolean value instead of silently staging the balance the mapping meant to suppress — a typo there would quietly land money in net worth.

A "set: exclude from totals" rule action, alongside the category, kind, and merchant that rules already stamp. One rule matching an account — no description pattern needed — keeps every row it imports visible in the ledger and out of every total. Applied at import time and on re-apply, so it needs no per-batch attention. Every analytics and budget query already filters on Transaction.is_excluded_from_totals, so nothing downstream changed. Re-apply still skips user-categorized rows and rows already in a transfer pair, as before; the existing bulk Exclude from totals button covers those.

Dropped model columns are now dropped from existing databases. create_all only ever adds, so a profile carried forward from before a column removal kept that column. A leftover NOT NULL column with no default makes every insert into that table fail, because no model supplies a value any more — in practice, account creation returning a 500 on a long-lived profile while a freshly created one works, which is why this never surfaced in tests or on demo data. _DROPPED_COLUMNS mirrors the existing _ADDITIVE_COLUMNS and repairs affected databases on open; it is a no-op everywhere else. Seeded with the two columns already in this state (accounts.is_liquid, accounts.is_taxable), which no model or query references.

Reviewer notes: the rule column is additive and nullable, so null keeps today's behavior and only an explicit true/false acts. The API contract was regenerated.

Privacy Checklist

  • I used only synthetic test/sample data.
  • I did not include real account numbers, balances, merchants, names, addresses, or other personal details.
  • This change does not add or expand outbound network access. (No new hosts; the Plaid connector's existing calls are unchanged, and the new mapping flag only suppresses a locally staged file.)

Testing

make test (124 passed), make test-automation (20 passed), make typecheck, make api-contract-check.

New backend tests:

  • test_schema_drift.py — builds a database in the legacy shape, asserts the insert really does fail without the repair (guarding the premise), then that opening it drops the columns and restores inserts, and that the drop is a no-op on a fresh database.
  • test_rule_exclude_from_totals.py — the action returns for a matching account and not for others; rules without it leave the flag alone; re-apply stamps existing unreviewed rows; and end to end, the Sankey counts a contribution once on its own date rather than double-counting the fund's mirrored inflow and later grant.

New automation tests cover a mapping marked balances: false being omitted from the staged balances entirely (not emitted as a null row), and an explicit balances: true still being included.

Also exercised in the browser against a scratch profile: created the rule, ran re-apply, confirmed matching rows flipped to excluded with their kinds untouched and the rule row showing the new pill.

Screenshots

The rule editor gains one checkbox under "Set: merchant override", and the rule list shows an "excluded from totals" pill so a rule that only excludes does not read as doing nothing.

Related Issue

n/a

Some accounts are worth having in the ledger but not in the numbers. A
donor-advised fund is the clearest case: the giving is already counted as
a donation when it leaves the account that funded it, so the fund's own
rows — the mirrored contribution and the grants out — would count that
same money a second time, and its balance is money you no longer own.

Three pieces, each usable on its own:

- Plaid mappings take "balances": false. The account's transactions stage
  as usual; no balance row is ever built for it, so it stays out of the
  net-worth series (net worth is the sum of snapshot balance rows).
  A non-boolean value is rejected rather than silently staging the
  balance the mapping meant to suppress.

- Rules take a "set: exclude from totals" action, alongside the category,
  kind, and merchant they already stamp. One rule matching an account
  keeps every row it imports visible in the ledger and out of every
  total, on each import and on re-apply.

- Columns dropped from the models are now dropped from existing
  databases. create_all only ever adds, so a profile carried forward from
  before a removal kept the column — and a leftover NOT NULL column with
  no default made every insert into that table fail, since no model
  supplies a value any more. Fresh databases never had the column, which
  is why this only bit long-lived profiles. _DROPPED_COLUMNS mirrors the
  existing _ADDITIVE_COLUMNS and repairs them on open.

Note that pairing the two sides as a transfer is the wrong tool here: the
pairing holds both rows at kind=transfer, and the Sankey ignores
transfers, so the donation would disappear from the chart. Excluding the
fund's side keeps it counted once, on the contribution date.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ShichengRao
ShichengRao merged commit bfbe876 into main Aug 13, 2026
5 checks passed
@ShichengRao
ShichengRao deleted the claude/net-worth-plaid-coverage-656593 branch August 13, 2026 05:34
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.

1 participant