From 3f04cbd5e7e3a05821b395de4d54d984eee7fd44 Mon Sep 17 00:00:00 2001 From: siddharthd Date: Sun, 26 Jul 2026 16:13:20 +1000 Subject: [PATCH] fix(trips): stop reporting a settlement breakdown that cannot be computed The trip view showed Total Owed / Settled / Unsettled per participant, with the last two derived from transaction_splits.settled. Nothing sets that flag - its only writer was /api/splits/settle, which no UI calls - so it is false on all 673 splits and every trip reported 100% unsettled, including trips already paid in full. Molina has paid $20,782.79 against $19,556.07 of splits and the Europe trip still showed her entire share outstanding. A correct per-trip figure is not computable either: split_payments records only from, to, amount and date, so a payment cannot be attributed to a trip. The trip view now shows each participant's share and points at Shared for what is actually owed, which is where settlement genuinely lives. Also removes /api/splits/settle. It was unreachable from the UI but live on its URL, and a single call with participant_id would mark every one of that person's splits settled - writing a flag nothing reads. Settlement will be reintroduced against settlement contexts (docs/shared-expenses-design.md). getParticipantBalances is deliberately untouched: it computes splits minus payments, which is coherent. Excluding settled splits there while still subtracting the payments that settled them would double-count. --- src/app/api/splits/settle/route.ts | 46 ------------------------------ src/app/trips/[id]/page.tsx | 16 +++++++---- src/lib/queries.ts | 16 +++++++---- 3 files changed, 22 insertions(+), 56 deletions(-) delete mode 100644 src/app/api/splits/settle/route.ts diff --git a/src/app/api/splits/settle/route.ts b/src/app/api/splits/settle/route.ts deleted file mode 100644 index bcfaa2b..0000000 --- a/src/app/api/splits/settle/route.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { queryRaw } from "@/lib/db"; -import { getCurrentUser } from "@/lib/auth"; - -// A split may be settled by the transaction's effective owner or by the -// participant the split belongs to. -const SCOPE = ` - AND EXISTS ( - SELECT 1 FROM transactions t - LEFT JOIN statements s ON s.id = t.statement_id - WHERE t.id = transaction_splits.transaction_id - AND (COALESCE(t.owner_id, s.owner_id) = $2 OR transaction_splits.participant_id = $2) - )`; - -export async function POST(req: NextRequest) { - const user = await getCurrentUser(req); - if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); - - const body = await req.json(); - const { participant_id, split_ids } = body as { - participant_id?: number; - split_ids?: number[]; - }; - - if (participant_id) { - const rows = await queryRaw<{ id: number }>( - `UPDATE transaction_splits SET settled = true, settled_at = NOW() - WHERE participant_id = $1 AND settled = false ${SCOPE} - RETURNING id`, - [participant_id, user.id] - ); - return NextResponse.json({ settled: rows.length }); - } - - if (split_ids?.length) { - const rows = await queryRaw<{ id: number }>( - `UPDATE transaction_splits SET settled = true, settled_at = NOW() - WHERE id = ANY($1::int[]) AND settled = false ${SCOPE} - RETURNING id`, - [split_ids, user.id] - ); - return NextResponse.json({ settled: rows.length }); - } - - return NextResponse.json({ error: "participant_id or split_ids required" }, { status: 400 }); -} diff --git a/src/app/trips/[id]/page.tsx b/src/app/trips/[id]/page.tsx index 5b060b5..4312623 100644 --- a/src/app/trips/[id]/page.tsx +++ b/src/app/trips/[id]/page.tsx @@ -277,7 +277,7 @@ export default function TripDetailPage({ params }: { params: Promise<{ id: strin - {["Person", "Total Owed", "Settled", "Unsettled"].map((h) => ( + {["Person", "Share of this trip"].map((h) => ( - - ))}
{p.name} ${Number(p.owed).toFixed(2)}${Number(p.settled).toFixed(2)} 0 ? "text-amber-400" : "text-zinc-600"}`}> - ${Number(p.unsettled).toFixed(2)} -
+ {/* Settled/unsettled was reported from transaction_splits.settled, + which nothing sets — so every trip showed 100% unsettled forever, + including ones already paid in full. Settlement is tracked across + the whole relationship, not per trip: payments carry no trip + attribution, so a per-trip figure cannot be computed. */} +

+ Settlement is tracked across all shared expenses, not per trip — + see Shared for + what is actually owed. +

)} diff --git a/src/lib/queries.ts b/src/lib/queries.ts index f2696b6..9f5ad28 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -753,7 +753,7 @@ export interface TripAnalytics { daily_spend: { date: string; amount: number }[]; top_merchants: { merchant: string; amount: number; count: number }[]; tag_breakdown: { tag_id: number; name: string; color: string; amount: number; count: number }[]; - participant_splits: { participant_id: number; name: string; owed: number; settled: number; unsettled: number }[]; + participant_splits: { participant_id: number; name: string; owed: number }[]; } export async function getTrips(ownerId: number): Promise { @@ -857,13 +857,19 @@ export async function getTripAnalytics(tripId: number, ownerId: number): Promise ORDER BY 4 DESC `, [tripId]), - queryRaw<{ participant_id: number; name: string; owed: number; settled: number; unsettled: number }>(` + // No settled/unsettled breakdown here. It was computed from + // transaction_splits.settled, which only /api/splits/settle writes and + // nothing in the UI calls — so it is false on all 673 splits and every trip + // reported 100% unsettled, including trips paid in full. A real per-trip + // figure is not computable either: split_payments carries no trip + // attribution, so a payment cannot be assigned to a trip. Settlement is a + // property of the whole relationship until settlement contexts exist + // (see docs/shared-expenses-design.md). + queryRaw<{ participant_id: number; name: string; owed: number }>(` SELECT p.id AS participant_id, p.name, - SUM(ts.share_percent / 100.0 * COALESCE(tx.amount_aud, tx.amount))::float AS owed, - SUM(CASE WHEN ts.settled THEN ts.share_percent / 100.0 * COALESCE(tx.amount_aud, tx.amount) ELSE 0 END)::float AS settled, - SUM(CASE WHEN NOT ts.settled THEN ts.share_percent / 100.0 * COALESCE(tx.amount_aud, tx.amount) ELSE 0 END)::float AS unsettled + SUM(ts.share_percent / 100.0 * COALESCE(tx.amount_aud, tx.amount))::float AS owed FROM transaction_overrides o JOIN transactions tx ON tx.id = o.transaction_id JOIN transaction_splits ts ON ts.transaction_id = tx.id