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:
@@ -6,6 +6,8 @@ import {
|
||||
STATEMENTS_JOIN,
|
||||
EFFECTIVE_CATEGORY,
|
||||
EXCLUDE_NON_SPEND,
|
||||
SPEND_ROWS,
|
||||
SPEND_BASE,
|
||||
} from "@/lib/analytics-sql";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
@@ -34,9 +36,9 @@ export async function GET(req: NextRequest) {
|
||||
${EFFECTIVE_CATEGORY} as category,
|
||||
SUM(
|
||||
CASE
|
||||
WHEN ts.share_percent IS NOT NULL THEN COALESCE(t.amount_aud, t.amount) * ts.share_percent / 100
|
||||
WHEN o.my_share_percent IS NOT NULL THEN COALESCE(t.amount_aud, t.amount) * o.my_share_percent / 100
|
||||
ELSE COALESCE(t.amount_aud, t.amount)
|
||||
WHEN ts.share_percent IS NOT NULL THEN (${SPEND_BASE}) * ts.share_percent / 100
|
||||
WHEN o.my_share_percent IS NOT NULL THEN (${SPEND_BASE}) * o.my_share_percent / 100
|
||||
ELSE (${SPEND_BASE})
|
||||
END
|
||||
)::numeric(12,2) as total_spent,
|
||||
COUNT(*)::int as transaction_count
|
||||
@@ -45,7 +47,7 @@ export async function GET(req: NextRequest) {
|
||||
LEFT JOIN transaction_splits ts ON ts.transaction_id = t.id AND ts.participant_id = $1
|
||||
${STATEMENTS_JOIN}
|
||||
WHERE ${OWNER_SCOPE} = $1
|
||||
AND t.transaction_type IN ('debit', 'fee', 'interest')
|
||||
AND ${SPEND_ROWS}
|
||||
AND ${EXCLUDE_NON_SPEND}
|
||||
AND t.transaction_date >= $2
|
||||
AND t.transaction_date < $3
|
||||
|
||||
Reference in New Issue
Block a user