feat(statements+analytics): normalise statement_type; fix analytics scoping
ci / lint-test (push) Successful in 38s
ci / lint-test (push) Successful in 38s
Groundwork for importing bank and loan statements alongside credit cards.
statement_type was whatever free text Gemini put in account_type ('Credit Card',
'credit card', 'credit_card', 'Business Card', 'ACCESS ADVANTAGE',
'multi-currency account'). The UI coped only by doing .includes("card"), which
breaks as soon as bank and loan statements arrive.
- Migration 0013 adds normalize_statement_type() + a BEFORE INSERT/UPDATE
trigger and a CHECK constraint over credit_card|transaction|savings|loan|
offset|investment|other. The trigger means the N8N workflow keeps working
unchanged while it still sends free text. Raw value stays in account_type.
Backfilled 99 existing rows.
- src/lib/statement-types.ts mirrors the vocabulary for the UI; statements page
now filters by the real types and headlines balance vs amount due per type.
Analytics were scoped with INNER JOIN statements + s.owner_id, which silently
dropped all 180 manual/CSV transactions (statement_id IS NULL) from every
report. Switched all six routes to LEFT JOIN + COALESCE(t.owner_id, s.owner_id)
via shared fragments in src/lib/analytics-sql.ts, so the transfers/investment
exclusion that stops card-payment double counting stays consistent. Also
extended that exclusion to trip analytics, which had none.
Drive-by: /api/analytics/subscriptions was returning 500 on an unserialisable
BigInt from COUNT(*) + 1.
Verified against the live DB: monthly spend picks up the previously invisible
manual transactions (Apr 9,849.91 -> 14,286.70) and all four analytics
endpoints return 200.
This commit is contained in:
@@ -127,6 +127,31 @@ 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`
|
||||
|
||||
Analytics queries must import the fragments from `src/lib/analytics-sql.ts`
|
||||
(`STATEMENTS_JOIN`, `OWNER_SCOPE`, `EFFECTIVE_CATEGORY`, `EXCLUDE_NON_SPEND`)
|
||||
rather than hand-rolling them. Two failure modes they exist to prevent:
|
||||
|
||||
- An `INNER JOIN statements` + `WHERE s.owner_id = $1` silently drops every
|
||||
manual/CSV transaction (`statement_id IS NULL`).
|
||||
- Spend must exclude the `transfers` and `investment` categories. Once bank
|
||||
statements are imported alongside card statements, a credit-card payment
|
||||
appears twice — as a debit leaving the bank account and as the underlying
|
||||
purchases on the card statement. Excluding `transfers` is what nets it out.
|
||||
Use the `EXCLUDE_NON_SPEND` fragment: a bare `category NOT IN (...)` evaluates
|
||||
to NULL for uncategorised rows and drops them from totals.
|
||||
|
||||
### Statement types
|
||||
|
||||
`statements.statement_type` is constrained to `credit_card | transaction |
|
||||
savings | loan | offset | investment | other`. Migration 0013 added a
|
||||
`normalize_statement_type()` SQL function plus a BEFORE INSERT/UPDATE trigger, so
|
||||
the N8N workflow can keep sending raw free text (`'ACCESS ADVANTAGE'`, `'Business
|
||||
Card'`) and the DB normalises it on write. The raw extracted value is preserved in
|
||||
`account_type`.
|
||||
|
||||
The TypeScript mirror is `src/lib/statement-types.ts` — keep the list, the SQL
|
||||
function, and the CHECK constraint in sync when adding a type.
|
||||
|
||||
### Prisma
|
||||
|
||||
The schema at `prisma/schema.prisma` covers all tables. The generated client (gitignored) must be regenerated after schema changes:
|
||||
|
||||
Reference in New Issue
Block a user