ci / lint-test (push) Successful in 52s
Trips were scoped to trips.owner_id, so Sonu saw no trips at all — despite having paid for 104 of the tagged rows herself. Her own spending was invisible on the only page organised around it. A participant is now anyone with a split on, who paid for, or whose payment is scoped to, a transaction tagged to the trip. Derived, not stored. A trip_participants table was designed and rejected: the expenses already carry the fact, and two records of one fact drift apart. Deriving it also excludes Singapore + Bangkok 2026 from Sonu for free, which a table would have to be kept in sync to do. Siddharth 4 trips, Sonu 3, Molina 1. Everything about a trip is shared except delete. Both trip foreign keys are ON DELETE SET NULL, so deleting Europe 2026 untags 210 transactions and NULLs the trip scope on 6 payments — where the hand-derived Europe-first allocation lives, which nothing recomputes. That stays with the owner. Trip owed now returns both directions and nets neither. An obligation lives on a row someone else paid for, so a viewer-as-payer figure can never hold it, and Sonu's Europe read "you are owed $2,408.24" while omitting the $8,004.04 she owed. Collapsing the two into a signed net is the tempting next step and would have corrupted the scope allocation: the grouped-payment allocation cleared each trip against the one-directional gross, so redefining the debt afterwards turns $8,004.04 already allocated into an $802.75 over-allocation with household understated by the same amount. Verified byte-identical — Auckland $1,505.64, Europe Molina -$816.16, Europe Sonu $0.00, Sonu + Sunny $0.00. getTransactions gained trip_all_rows so a participant sees the whole trip. It is opt-in and not implied by trip_id, because the same endpoint backs the main transactions list and its trip filter must keep owner scoping. Participation is re-checked in SQL, so passing the flag for someone else's trip returns nothing. Payments can finally say what they settle. trip_id has existed since migration 0022 but POST never read it and GET never returned it, so every payment made in the app landed on household and the 9 trip-scoped rows were hand-written SQL. "Both" needs no new shape — one row per scope sharing a linked_transaction_id. Three write paths had no authorisation at all and were reachable by any participant: assignTransactionsToTrip checked nothing, DELETE on a payment deleted by bare id, and POST accepted any from/to pair. All three now check. Also fixes the test suite, which was pointing at postgres-pantry: container IPs move on recreation and 172.22.0.47 stopped being postgres-personal. It only failed safe because the credentials did not match — resetDB now refuses to truncate anything not named personal_test. 22 new tests, 276 passing, build clean.
23 lines
999 B
TypeScript
23 lines
999 B
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { getCurrentUser } from "@/lib/auth";
|
|
import { assignTransactionsToTrip } from "@/lib/queries";
|
|
|
|
export async function PATCH(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
|
|
const user = await getCurrentUser(req);
|
|
if (!user) return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
const { id } = await params;
|
|
const { transactionIds } = await req.json() as { transactionIds: number[] };
|
|
if (!Array.isArray(transactionIds) || !transactionIds.length) {
|
|
return NextResponse.json({ error: "transactionIds must be a non-empty array" }, { status: 400 });
|
|
}
|
|
try {
|
|
const assigned = await assignTransactionsToTrip(Number(id), transactionIds, user.id);
|
|
return NextResponse.json({ ok: true, assigned, requested: transactionIds.length });
|
|
} catch (e) {
|
|
return NextResponse.json(
|
|
{ error: e instanceof Error ? e.message : "Failed to assign" },
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
}
|