Files
finance-app/CLAUDE.md
T
siddharthd ce67e38d77 docs: update CLAUDE.md and README to reflect recent changes
- 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
2026-05-10 16:52:55 +10:00

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, database personal, user personal
  • Auth: X-Forwarded-User header (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.ts use raw SQL via queryRaw from src/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 in transactions.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 account
  • transactions — line items; statement_id is nullable (NULL = manual entry); reconciled_with_id links a manual tx to its matched statement tx
  • transaction_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 participants
  • transaction_tags — many-to-many join to tags
  • rules — auto-categorisation rules (JSONB conditions + actions)
  • rule_apply_runs — audit log of bulk rule-apply runs with full snapshot for revert
  • expense_metadata — enrichment from email receipts; transaction_id nullable until reconciled
  • participants — people; id=1 is "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_at of the original manual/CSV transaction (via LEFT 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

  1. Create src/app/api/<resource>/route.ts
  2. Always call getCurrentUser(req) first; return 403 if null
  3. Write SQL in src/lib/queries.ts using queryRaw
  4. 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 to Condition.field union, TxFields interface, and evaluateCondition() switch
  • src/app/rules/page.tsx — add to FIELDS array; add special rendering if needed (e.g. enum dropdown for transaction_type)

Modifying queries

  • All JOINs to statements must be LEFT 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.mdKnown 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.