Track an account without counting it - #35
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Details
Three pieces, each usable on its own.
"balances": falseon 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.validateSourcerejects 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_allonly ever adds, so a profile carried forward from before a column removal kept that column. A leftoverNOT NULLcolumn 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_COLUMNSmirrors the existing_ADDITIVE_COLUMNSand 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
nullkeeps today's behavior and only an explicittrue/falseacts. The API contract was regenerated.Privacy Checklist
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: falsebeing omitted from the staged balances entirely (not emitted as a null row), and an explicitbalances: truestill 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