docs: update README and add CLAUDE.md for finance app
This commit is contained in:
@@ -0,0 +1,118 @@
|
|||||||
|
# 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<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:
|
||||||
|
```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/<name>/migration.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Tables
|
||||||
|
|
||||||
|
- `statements` — one row per billing period per bank account
|
||||||
|
- `transactions` — line items; `statement_id` is nullable (NULL = manual entry)
|
||||||
|
- `transaction_overrides` — user corrections to AI-extracted data (category, merchant, notes)
|
||||||
|
- `transaction_splits` — shared expense tracking (participant, share_percent, settled)
|
||||||
|
- `transaction_tags` — many-to-many join to `tags`
|
||||||
|
- `rules` — auto-categorisation rules (JSONB conditions + actions)
|
||||||
|
- `participants` — people; `id=1` is "Me" (the primary user)
|
||||||
|
- `account_owner_mappings` — persists bank+account → owner assignments
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -58,7 +58,8 @@ One row per line item within a statement. Cascade-deleted when the parent statem
|
|||||||
| Column | Type | Description |
|
| Column | Type | Description |
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| `id` | int | Primary key |
|
| `id` | int | Primary key |
|
||||||
| `statement_id` | int FK → `statements` | Parent statement |
|
| `statement_id` | int FK → `statements` (nullable) | Parent statement; NULL for manually-entered transactions |
|
||||||
|
| `owner_id` | int FK → `participants` (nullable) | Owner for manual transactions (no statement); statement-linked transactions derive owner from `statements.owner_id` |
|
||||||
| `transaction_date` | date | Date of transaction |
|
| `transaction_date` | date | Date of transaction |
|
||||||
| `description` | text | Raw description from the statement |
|
| `description` | text | Raw description from the statement |
|
||||||
| `amount` | numeric | Original amount in statement currency |
|
| `amount` | numeric | Original amount in statement currency |
|
||||||
@@ -164,8 +165,9 @@ Saved auto-categorisation rules. Applied in bulk via the Rules page.
|
|||||||
| `enabled` | bool | |
|
| `enabled` | bool | |
|
||||||
| `priority` | int | Higher priority rules run first |
|
| `priority` | int | Higher priority rules run first |
|
||||||
|
|
||||||
**Condition fields**: `merchant_normalized`, `description`, `category`, `bank_name`, `amount`
|
**Condition fields**: `merchant_normalized`, `description`, `category`, `bank_name`, `amount`, `transaction_type`
|
||||||
**Condition operators**: `contains`, `equals`, `starts_with`, `gt`, `lt`, `not_equals`
|
**Condition operators**: `contains`, `equals`, `starts_with`, `gt`, `lt`, `not_equals`
|
||||||
|
**Actions**: `set_category`, `set_merchant`, `add_tag_ids`, `apply_split`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -289,6 +291,26 @@ docker exec postgres-personal psql -U personal -d personal \
|
|||||||
| `0007_cashflow` | `amount_aud`, `exchange_rate_to_aud` on transactions; `exchange_rate_to_aud` on statements |
|
| `0007_cashflow` | `amount_aud`, `exchange_rate_to_aud` on transactions; `exchange_rate_to_aud` on statements |
|
||||||
|
|
||||||
> `paperless_doc_id` on statements and the `uq_statements_paperless_doc_id` index were added directly (not tracked in a migration file).
|
> `paperless_doc_id` on statements and the `uq_statements_paperless_doc_id` index were added directly (not tracked in a migration file).
|
||||||
|
> `owner_id` on transactions and `statement_id` made nullable were applied directly (March 2026) to support manual transaction entry without a fake statement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Known Gaps / TODOs
|
||||||
|
|
||||||
|
### Payment Provider tracking
|
||||||
|
|
||||||
|
Currently `merchant_normalized` conflates the *payment provider* with the *merchant*. Transactions processed through PayPal, Afterpay, Zip, Alipay, etc. end up with the provider as the merchant when the real merchant can't be recovered.
|
||||||
|
|
||||||
|
**What's been done so far:**
|
||||||
|
- PayPal entries that embed the merchant name (e.g. `PAYPAL *BUNNINGSGRO`) were cleaned up — the real merchant was extracted during the March 2026 consolidation pass.
|
||||||
|
- Pure PayPal/Afterpay/Zip entries where the merchant is unrecoverable were left as-is.
|
||||||
|
- A one-time SQL consolidation pass normalised ~50 merchant name variant groups (March 2026).
|
||||||
|
|
||||||
|
**Remaining work:**
|
||||||
|
1. **DB migration**: `ALTER TABLE transactions ADD COLUMN payment_provider text` and same on `transaction_overrides`.
|
||||||
|
2. **Gemini prompt**: add `payment_provider` to the `responseSchema` so the AI extracts it separately (`"PayPal"`, `"Afterpay"`, `"Zip"`, `null`, etc.) — the raw bank description usually contains enough signal.
|
||||||
|
3. **Backfill**: for existing transactions, derive `payment_provider` from `merchant_name` patterns (`PAYPAL *`, `AFTERPAY`, `ZIP/ZIPPAY`, `BPAY`).
|
||||||
|
4. **App**: surface `payment_provider` as a filter/column in the transactions view; exclude payment providers from merchant analytics so they don't inflate the merchant list.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user