# 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 ```bash # 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()` — 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: ```sql 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: ```sql COALESCE(o.merchant_normalized, t.merchant_normalized, t.merchant_name) -- merchant COALESCE(o.category_override, t.category) -- category ``` ## Database ```bash # 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//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//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: ```bash 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.