fix(trips): stop reporting a settlement breakdown that cannot be computed
ci / lint-test (push) Successful in 35s

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.
This commit is contained in:
2026-07-26 16:13:20 +10:00
parent ba87ff86e7
commit 3f04cbd5e7
3 changed files with 22 additions and 56 deletions
+11 -5
View File
@@ -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<TripRow[]> {
@@ -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