- Add reconciled_with_id and created_at columns to transactions table docs - Document split_payments, expense_metadata, rule_apply_runs tables - Update /api/transactions route docs with has_split filter and all sort options - Add /api/transactions/reconcile and /api/split-payments to API table - Document import date (created_at) behaviour and reconciliation caveat - Add Prisma regeneration note to CLAUDE.md - Note schema drift for tables added without migration files
6.3 KiB
CLAUDE.md
Guidance for Claude Code when working in this repository.
Project Overview
Personal finance tracker. Bank statements are ingested via an N8N workflow (in the smarthome repo at docker/automation/workflows/cc-statement-processor-paperless.json) that sends PDFs to Gemini 2.5 Flash for extraction, then inserts into PostgreSQL.
- App: Next.js 16 App Router, TypeScript, Tailwind CSS
- DB: PostgreSQL container
postgres-personal, databasepersonal, userpersonal - Auth:
X-Forwarded-Userheader (email) set by Traefik →participants.email. In dev/fallback: participant id=1 ("Me") - Runs at: port 3000 inside container, exposed on host port 4100, proxied at
https://finance.bosecamp.com
Common Commands
# Build and deploy (from smarthome repo root)
docker compose --env-file docker/common.env --env-file docker/finance/.env \
-f docker/finance/docker-compose.yml up -d --build
# IMPORTANT: docker restart does NOT pick up a new image — always use the compose command above
# DB access
docker exec postgres-personal psql -U personal -d personal
# View logs
docker logs finance -f
Architecture
Key Files
| File | Purpose |
|---|---|
src/lib/db.ts |
queryRaw<T>() — the only DB query function; uses pg directly |
src/lib/queries.ts |
All SQL query functions (no ORM); import queryRaw from @/lib/db |
src/lib/hooks.ts |
TanStack Query hooks for all API calls |
src/lib/auth.ts |
getCurrentUser() — reads X-Forwarded-User header |
src/lib/categories.ts |
Canonical category list (CATEGORIES array + formatCategory()) |
src/app/api/*/route.ts |
API route handlers |
src/components/ |
Shared UI components |
Data Flow
- All queries in
src/lib/queries.tsuse raw SQL viaqueryRawfromsrc/lib/db.ts - API routes call query functions and return
NextResponse.json() - Frontend uses hooks from
src/lib/hooks.ts(TanStack Query) — never fetches directly - Auth is always checked first in every API route:
const user = await getCurrentUser(req)
Owner Scoping
All data is scoped by owner_id. The effective owner of a transaction is:
COALESCE(t.owner_id, s.owner_id)
- Statement-linked transactions: owner comes from
statements.owner_id - Manual transactions:
statement_id IS NULL, owner stored directly intransactions.owner_id
The effective merchant and category always prefer overrides:
COALESCE(o.merchant_normalized, t.merchant_normalized, t.merchant_name) -- merchant
COALESCE(o.category_override, t.category) -- category
Database
# Schema inspection
docker exec postgres-personal psql -U personal -d personal -c "\d transactions"
# Apply a migration SQL file
docker exec postgres-personal psql -U personal -d personal < prisma/migrations/<name>/migration.sql
Key Tables
statements— one row per billing period per bank accounttransactions— line items;statement_idis nullable (NULL = manual entry);reconciled_with_idlinks a manual tx to its matched statement txtransaction_overrides— user corrections to AI-extracted data (category, merchant, notes)transaction_splits— shared expense tracking (participant, share_percent, settled)split_payments— recorded cash settlements between participantstransaction_tags— many-to-many join totagsrules— auto-categorisation rules (JSONB conditions + actions)rule_apply_runs— audit log of bulk rule-apply runs with full snapshot for revertexpense_metadata— enrichment from email receipts;transaction_idnullable until reconciledparticipants— people;id=1is "Me" (the primary user)account_owner_mappings— persists bank+account → owner assignments
Import Date (created_at)
transactions.created_at is the import timestamp (DB default now()). In the transactions and shared views, the "Imported" column shows:
- For statement transactions: when the statement was processed by N8N
- For reconciled transactions: the
created_atof the original manual/CSV transaction (viaLEFT JOIN transactions src ON src.reconciled_with_id = t.id) — so the original import date is preserved post-reconciliation
Use created_at (not transaction_date) to answer "what was added since the last settlement?". Sort by created_at is supported server-side in getTransactions and client-side in the shared view.
Rules System
Conditions are AND-evaluated. Fields: merchant_normalized, description, category, bank_name, amount, transaction_type. Operators: contains, equals, starts_with, gt, lt, not_equals. Actions: set_category, set_merchant, add_tag_ids, apply_split.
contains and equals operators are case-insensitive (both sides .toLowerCase()).
Development Patterns
Adding a new API route
- Create
src/app/api/<resource>/route.ts - Always call
getCurrentUser(req)first; return 403 if null - Write SQL in
src/lib/queries.tsusingqueryRaw - Add a TanStack Query hook in
src/lib/hooks.ts
Adding a new condition field to rules
Two files only:
src/app/api/rules/apply/route.ts— add toCondition.fieldunion,TxFieldsinterface, andevaluateCondition()switchsrc/app/rules/page.tsx— add toFIELDSarray; add special rendering if needed (e.g. enum dropdown fortransaction_type)
Modifying queries
- All JOINs to
statementsmust beLEFT JOIN(manual transactions have no statement) - Owner filter pattern:
WHERE COALESCE(t.owner_id, s.owner_id) = $1 - Bank name pattern:
COALESCE(s.bank_name, 'Manual') as bank_name
Prisma
The schema at prisma/schema.prisma covers all tables. The generated client (gitignored) must be regenerated after schema changes:
cd /mnt/m2cache/appdata/finance-app && npx prisma generate
Docker builds run npx prisma generate automatically. Do not commit src/generated/prisma/ — it is gitignored.
Known Gaps / TODOs
See README.md → Known Gaps / TODOs for full details.
Payment provider tracking: merchant_normalized currently conflates payment provider (PayPal, Afterpay, Zip) with the actual merchant. Plan: add payment_provider column, update Gemini prompt to extract it separately, backfill from merchant_name patterns, surface in UI filters.