diff --git a/CLAUDE.md b/CLAUDE.md index 3742628..8138ffe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,14 +76,25 @@ docker exec postgres-personal psql -U personal -d personal < prisma/migrations/< ### Key Tables - `statements` — one row per billing period per bank account -- `transactions` — line items; `statement_id` is nullable (NULL = manual entry) +- `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`. @@ -111,6 +122,14 @@ Two files only: - 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. diff --git a/README.md b/README.md index 67f5044..88fdb60 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,8 @@ One row per line item within a statement. Cascade-deleted when the parent statem | `foreign_currency_code` | text | Foreign currency code (e.g. `USD`) | | `category` | text | AI-assigned category (see category taxonomy below) | | `row_index` | int | Position in statement — used for deduplication | +| `reconciled_with_id` | int FK → `transactions` (nullable) | Links a manually-entered transaction to its matching statement transaction after reconciliation | +| `created_at` | timestamptz | When the row was inserted — the "import date". For reconciled transactions the UI shows the original manual/CSV `created_at`, not the statement's | **Deduplication**: unique index on `(statement_id, transaction_date, description, amount, row_index)`. @@ -171,6 +173,55 @@ Saved auto-categorisation rules. Applied in bulk via the Rules page. --- +### `split_payments` +Records of actual cash settlements between participants. + +| Column | Type | Description | +|--------|------|-------------| +| `from_participant_id` | int FK → `participants` | Who paid | +| `to_participant_id` | int FK → `participants` | Who received | +| `amount` | numeric | Amount settled | +| `payment_date` | date | Date of settlement | +| `notes` | text | Optional note (e.g. "bank transfer") | +| `linked_transaction_id` | int FK → `transactions` (nullable) | If the payment was itself a transaction | + +--- + +### `expense_metadata` +Enrichment records for non-statement expenses (email receipts, manual entries). Linked to a `transaction` if one exists; otherwise a standalone record awaiting reconciliation. + +| Column | Type | Description | +|--------|------|-------------| +| `transaction_id` | int FK → `transactions` (unique, nullable) | Linked transaction; NULL until reconciled | +| `source` | text | Origin: `email`, `manual` | +| `paperless_doc_id` | int | Paperless-NGX document ID | +| `payment_method` | text | `credit_card`, `debit_card`, `paypal`, `afterpay`, `cash`, etc. | +| `payment_method_detail` | text | Card last-4 or provider detail | +| `order_reference` | text | Order/confirmation number | +| `line_items` | jsonb | Array of `{description, qty, unit_price, total}` | +| `merchant_normalized` | text | Canonical merchant for matching | +| `amount` / `transaction_date` | numeric / date | Used for reconciliation matching when `transaction_id IS NULL` | +| `extraction_model` | text | AI model used (`gemini-2.5-flash`) | + +Partial index on `(merchant_normalized, transaction_date) WHERE transaction_id IS NULL` powers reconciliation queries. + +--- + +### `rule_apply_runs` +Audit log of bulk rule-apply operations. Each run captures which transactions were affected and a full snapshot for revert support. + +| Column | Type | Description | +|--------|------|-------------| +| `owner_id` | int FK → `participants` | | +| `applied_at` | timestamptz | When the run executed | +| `split_from` | date | Optional date filter used for this run | +| `matched` | int | Number of rules matched | +| `transactions_affected` | int | Number of transactions changed | +| `reverted_at` | timestamptz | Set when run was reverted | +| `snapshot` | jsonb | Pre-run state of all affected transactions | + +--- + ### `budgets` Monthly spend targets per category. Stored but currently unused in the UI (replaced by the analytics/insights views). @@ -205,16 +256,19 @@ All routes require authentication via `X-Forwarded-User` header (set by Traefik) |--------|-------|-------------| | GET | `/api/statements` | All statements for current user | | GET / PATCH | `/api/statements/[id]` | Get statement; PATCH to reassign owner (also writes `account_owner_mappings`) | -| GET | `/api/transactions` | Paginated transactions with filters: `from`, `to`, `category`, `merchant`, `statement_id`, `search`, `sort`, `dir` | +| GET | `/api/transactions` | Paginated transactions. Filters: `from`, `to`, `categories`, `bank_names`, `tag_ids`, `transaction_types`, `search`, `statement_id`, `amount_min`, `amount_max`, `has_split` (`yes`/`no`). Sort: `sort_by` (`transaction_date`\|`amount`\|`created_at`), `sort_dir` (`asc`\|`desc`) | +| POST | `/api/transactions` | Create a manual transaction (no statement) | | GET / PATCH | `/api/transactions/[id]` | Get transaction; PATCH to upsert override (category, merchant, notes) | | GET / POST | `/api/transactions/[id]/splits` | List or create splits on a transaction | | GET / POST | `/api/transactions/[id]/tags` | List or apply tags to a transaction | | POST | `/api/transactions/bulk` | Bulk update category/merchant across multiple transactions | +| POST | `/api/transactions/reconcile` | Link manual transactions to statement transactions; copies overrides, tags, splits across | | GET | `/api/analytics/monthly` | Split-adjusted monthly spend by category + income + investments. Params: `months` (1–24, default 6) | | GET | `/api/analytics/subscriptions` | Recurring charge detection — merchants with ≥3 occurrences at consistent intervals | | GET | `/api/analytics/fees` | Fees and interest from statement summaries + individual fee/interest transactions | -| GET | `/api/shared-transactions` | Transactions that have active splits | +| GET | `/api/shared-transactions` | Transactions with active splits; sorted client-side by date/imported/amount in the UI | | POST | `/api/splits/settle` | Mark a split as settled | +| GET / POST | `/api/split-payments` | List or record cash settlements between participants | | GET / POST | `/api/participants` | List participants; POST to create (with optional `email`) | | GET | `/api/participants/[id]/balance` | Net balance owed by/to a specific participant | | GET | `/api/participants/balances` | All participant balances | @@ -292,6 +346,7 @@ docker exec postgres-personal psql -U personal -d personal \ > `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. +> `reconciled_with_id` on transactions, `expense_metadata`, `rule_apply_runs`, `split_payments` were added directly and are covered by the Prisma schema but lack individual migration files. ---