Files
finance-app/src/app/api/analytics/merchants/[merchant]/route.ts
T
siddharthd ab00f8c592
ci / lint-test (push) Successful in 41s
fix(analytics): my share is what is left over, not 100%
The split-adjusted spend expression assumed a transaction with no split row for
me was entirely mine. That is wrong when a transaction is allocated fully to
someone else: I paid, they owe all of it, and there is no row for me to match.
Both branches of the CASE missed and the ELSE charged me the full amount.

24 transactions were affected, all travel bookings between 2026-01-09 and
2026-06-26, overstating my spend by $8,579.07 across every analytics view.

Adds myShare/mySplitOf to analytics-sql.ts, which fall back to
100 - (sum of everyone else's shares) instead of 100, and applies them to all
five analytics routes. Centralised for the same reason as EXCLUDE_NON_SPEND:
the expression was duplicated five times and had already drifted.
2026-07-26 10:05:09 +10:00

57 lines
1.8 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { getCurrentUser } from "@/lib/auth";
import { queryRaw } from "@/lib/db";
import { OWNER_SCOPE, STATEMENTS_JOIN, EFFECTIVE_CATEGORY, mySplitOf } from "@/lib/analytics-sql";
const MY_AMOUNT = mySplitOf(`COALESCE(t.amount_aud, t.amount)`);
export async function GET(
req: NextRequest,
{ params }: { params: Promise<{ merchant: string }> }
) {
const user = await getCurrentUser(req);
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { merchant } = await params;
const decoded = decodeURIComponent(merchant);
const transactions = await queryRaw<{
id: number;
transaction_date: string;
description: string;
amount: number;
amount_aud: number | null;
my_amount: number;
transaction_type: string;
category: string;
bank_name: string;
statement_id: number;
}>(`
SELECT
t.id,
t.transaction_date::text,
t.description,
t.amount,
t.amount_aud,
CASE
WHEN t.transaction_type IN ('refund', 'credit') THEN -${MY_AMOUNT}
ELSE ${MY_AMOUNT}
END::numeric(10,2) as my_amount,
t.transaction_type,
${EFFECTIVE_CATEGORY} as category,
COALESCE(s.bank_name, 'Manual') as bank_name,
t.statement_id
FROM transactions t
${STATEMENTS_JOIN}
LEFT JOIN transaction_overrides o ON o.transaction_id = t.id
LEFT JOIN transaction_splits ts ON ts.transaction_id = t.id AND ts.participant_id = $1
WHERE ${OWNER_SCOPE} = $1
AND t.transaction_type IN ('debit', 'fee', 'interest', 'refund', 'credit')
AND COALESCE(o.merchant_normalized, t.merchant_normalized, t.merchant_name, t.description) = $2
ORDER BY t.transaction_date DESC
LIMIT 500
`, [user.id, decoded]);
return NextResponse.json({ transactions });
}