feat(insights): committed/discretionary chart, recurring charge detection, fees & interest audit

This commit is contained in:
2026-03-09 23:04:52 +11:00
parent 7379437cc3
commit c1d031511a
5 changed files with 535 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
import { NextRequest, NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth";
import { queryRaw } from "@/lib/db";
export async function GET(req: NextRequest) {
const user = await getCurrentUser(req);
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 });
// Statement-level fees and interest (aggregated by Gemini from the PDF)
const stmtRows = await queryRaw<{
bank_name: string;
fees: string;
interest: string;
}>(
`SELECT
bank_name,
SUM(COALESCE(fees_charged, 0))::numeric(12,2) AS fees,
SUM(COALESCE(interest_charged, 0))::numeric(12,2) AS interest
FROM statements
WHERE owner_id = $1
GROUP BY bank_name
HAVING SUM(COALESCE(fees_charged, 0)) + SUM(COALESCE(interest_charged, 0)) > 0
ORDER BY (SUM(COALESCE(fees_charged, 0)) + SUM(COALESCE(interest_charged, 0))) DESC`,
[user.id]
);
// Transaction-level fee and interest line items (split-adjusted)
const txnRows = await queryRaw<{
id: number;
transaction_date: string;
description: string;
merchant_name: string | null;
transaction_type: string;
my_amount: string;
bank_name: string;
}>(
`SELECT
t.id,
t.transaction_date,
t.description,
t.merchant_name,
t.transaction_type,
CASE
WHEN ts.share_percent IS NOT NULL THEN COALESCE(t.amount_aud, t.amount) * ts.share_percent / 100
ELSE COALESCE(t.amount_aud, t.amount)
END::numeric(12,2) AS my_amount,
s.bank_name
FROM transactions t
LEFT JOIN transaction_splits ts ON ts.transaction_id = t.id AND ts.participant_id = $1
JOIN statements s ON s.id = t.statement_id
WHERE s.owner_id = $1
AND t.transaction_type IN ('fee', 'interest')
ORDER BY t.transaction_date DESC`,
[user.id]
);
const by_bank = stmtRows.map((r) => ({
bank_name: r.bank_name,
fees: Number(r.fees),
interest: Number(r.interest),
total: Number(r.fees) + Number(r.interest),
}));
const transactions = txnRows.map((r) => ({
...r,
my_amount: Number(r.my_amount),
}));
// Totals from statement-level data (more complete — Gemini reads the statement summary)
const total_fees = by_bank.reduce((s, r) => s + r.fees, 0);
const total_interest = by_bank.reduce((s, r) => s + r.interest, 0);
return NextResponse.json({ by_bank, transactions, total_fees, total_interest });
}