diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b5284ed --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,104 @@ +# money + +Statement-driven personal finance tracker. A data directory holds one folder per +account; statements dropped into those folders are parsed into a rebuildable +SQLite index, categorised by ordered glob rules, and browsed in a Bubble Tea TUI. + +Read `README.md` first — it is the user-facing reference for every config key. +This file covers what the code assumes and why. + +## Layout + +``` +cmd/money/main.go subcommands; the TUI is the default +internal/config rules.toml, account.toml, XDG config, data-root resolution +internal/model Account, Transaction, amount formatting, description normalisation +internal/glob the `*` / `?` matcher used by rules (linear time, no backtracking) +internal/parser Parser interface + registry; csv, cmd, nlb, revolut, traderepublic +internal/store SQLite index (modernc.org/sqlite, no cgo) +internal/importer directory walk, dedupe, balance checks +internal/rules applies ordered rules to rule_* columns only +internal/report per-tag aggregation, transfers excluded +internal/tui Bubble Tea models +``` + +## Invariants + +**Rule verdicts and manual edits never share a column.** `rule_tag` / +`rule_transfer` are rewritten wholesale on every retag; `manual_tag` / +`manual_transfer` are only ever written by the user. Effective values are +`COALESCE(manual_*, rule_*)`. This is what makes `money retag` safe to run at +any time, and it is the first thing to preserve when touching the schema or the +rules engine. Covered by `TestManualTagSurvivesRetag`. + +**Statements are the source of truth; the index is disposable.** Deleting +`.money/index.db` and re-importing must reproduce everything except manual tags +and manual transfer marks. + +**Dedupe is by fingerprint**: `sha256(date | amount | normalised description | +ordinal)`, where the ordinal distinguishes identical lines *within one +statement*. Two identical purchases on one day both survive; the same line in an +overlapping statement does not duplicate. Unchanged files are skipped by +checksum before parsing at all — so an import with nothing new finishes in +milliseconds. That is the checksum skip working, not a failure. + +**Money is `int64` minor units**, never a float. Per-account currency, no +conversion, and totals are never summed across currencies. + +**First matching rule wins**, so transfer rules belong above general tag rules. +A rule setting several of `match` / `counterparty` / `type` requires all of them. + +## Adding a bank parser + +Implement `parser.Parser` and call `parser.Register` from an `init`. Nothing +else changes; the name becomes usable in an `account.toml`. Use +`parser.ParseAmount` rather than hand-rolling decimal handling — it copes with +`1.234,56`, trailing minus, parenthesised negatives, currency codes and both +the ASCII hyphen and U+2212. + +Keep PDF text extraction separate from parsing: the bank parsers expose a pure +`parseXText(text string, digits int)` so they can be tested against captured +`pdftotext` output without a PDF fixture. `pdftotext -layout` (poppler-utils) is +a runtime dependency of `nlb` and `traderepublic`; no Go library reconstructs +column layout as well. + +Do not hardcode absolute column positions from a sample PDF. `pdftotext` +compresses runs of spaces, so columns shift with font and page size — derive +positions from the header line (`traderepublic`) or from the line being parsed +(`nlb`). + +## Verifying + +``` +go build ./... && go test ./... && go vet ./... && gofmt -l . +``` + +For end-to-end checks, build a throwaway data root under the scratchpad rather +than touching real data: + +``` +money --root /tmp/.../demo import +money --root /tmp/.../demo import # must report 0 new +money --root /tmp/.../demo ls --wide +``` + +### Driving the TUI in tests + +Prefer feeding `tea.Msg` values to `Model.Update` directly — that is how every +existing TUI test works, and it covers the keymap without a terminal. + +If a real terminal is genuinely needed, note that piping into `script` does not +deliver keystrokes. Use a pty and answer the two capability queries Bubble Tea +sends on startup, or the program blocks before its first render: + +- `ESC]11;?` (background colour) → reply `ESC]11;rgb:1e1e/1e1e/1e1e ESC\` +- `ESC[6n` (cursor position) → reply `ESC[1;1R` + +Also delete the index first, or the import you are trying to observe will be +skipped by checksum and finish instantly. + +## Conventions + +Comments explain why, not what. Errors name the file and the offending row or +key so a bad statement is actionable. Per-file import failures are reported and +the run continues; only a broken data root aborts.