feat(loans): principal/interest split so repayments stop distorting spend
ci / lint-test (push) Successful in 41s
ci / lint-test (push) Successful in 41s
A loan repayment is not an expense. A $3,000 mortgage repayment is roughly $1,200 of principal (equity — a balance-sheet move) and $1,800 of interest (the only part that is genuinely spend). Migration 0014: - transactions.principal_amount / interest_amount, populated only when the lender itemises the split on the repayment row - statements.interest_rate, scheduled_repayment, repayment_frequency, redraw_available, loan_term_months - normalize_repayment_frequency() + trigger, so "Fortnightly", "Bi-Weekly" and "Every 2 weeks" all land on 'fortnightly' Two statement shapes are handled. Where the loan statement lists repayments and "Interest Charged" as separate rows (the common Australian case), transaction_type already does the work. Where a lender itemises the split on the repayment row, that row is typed 'payment' and would be skipped entirely — losing the interest. New SPEND_ROWS / SPEND_BASE fragments in analytics-sql.ts count such rows at interest_amount instead of amount. Adds the loan_interest category (+ colour, and the missing fees colour). Verified against the live DB with a synthetic ANZ home loan statement: a $3,000 itemised repayment plus a $10 service fee moved April spend by exactly $1,810, with the $1,200 principal excluded and still retained on the row. Test data removed and the figure confirmed back at its original value.
This commit is contained in:
@@ -152,6 +152,37 @@ Card'`) and the DB normalises it on write. The raw extracted value is preserved
|
||||
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.
|
||||
|
||||
### Loans
|
||||
|
||||
A loan repayment is **not** an expense. It is part principal (equity, a
|
||||
balance-sheet move) and part interest (the only part that is spend). Migration
|
||||
0014 adds:
|
||||
|
||||
- `transactions.principal_amount` / `interest_amount` — populated only when the
|
||||
lender itemises the split on the repayment row itself
|
||||
- `statements.interest_rate`, `scheduled_repayment`, `repayment_frequency`,
|
||||
`redraw_available`, `loan_term_months`
|
||||
|
||||
Two statement shapes, both handled:
|
||||
|
||||
1. **Separate rows** (the common Australian case) — the loan statement lists
|
||||
repayments and "Interest Charged" separately. `transaction_type` alone is
|
||||
enough: `interest` rows count as spend, `payment` rows don't. No split columns
|
||||
needed.
|
||||
2. **Itemised repayment row** — some lenders print principal and interest on the
|
||||
repayment line. That row is typed `payment`, so it would be skipped entirely
|
||||
and its interest lost. The `SPEND_ROWS` / `SPEND_BASE` fragments in
|
||||
`analytics-sql.ts` handle it: a row with a non-null `interest_amount` counts
|
||||
as spend, valued at `interest_amount` rather than `amount`.
|
||||
|
||||
The N8N `Parse Gemini Result` node only accepts a split when both parts are
|
||||
present *and* they sum to the row amount (±2c) — a half-extracted split would
|
||||
silently misreport spend, so it is discarded rather than trusted.
|
||||
|
||||
Loan interest uses the `loan_interest` category; principal repayments use
|
||||
`investment` (excluded from spend, surfaced on the investments line in monthly
|
||||
analytics).
|
||||
|
||||
### 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