import { NextRequest, NextResponse } from "next/server"; import { getCurrentUser } from "@/lib/auth"; import { getTransactions } from "@/lib/queries"; import { queryRaw } from "@/lib/db"; import { completeSplit } from "@/lib/splits"; import { isPaymentMethod } from "@/lib/payment-methods"; export async function GET(req: NextRequest) { const user = await getCurrentUser(req); if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 403 }); const sp = req.nextUrl.searchParams; const parseArr = (key: string) => { const v = sp.get(key); return v ? v.split(",").filter(Boolean) : undefined; }; const result = await getTransactions(user.id, { from: sp.get("from") || undefined, to: sp.get("to") || undefined, categories: parseArr("categories"), exclude_categories: parseArr("exclude_categories"), bank_names: parseArr("bank_names"), tag_ids: parseArr("tag_ids"), transaction_types: parseArr("transaction_types"), search: sp.get("search") || undefined, statement_id: sp.get("statement_id") || undefined, sort_by: sp.get("sort_by") || undefined, sort_dir: sp.get("sort_dir") || undefined, limit: sp.get("limit") ? Number(sp.get("limit")) : undefined, offset: sp.get("offset") ? Number(sp.get("offset")) : undefined, amount_min: sp.get("amount_min") ? Number(sp.get("amount_min")) : undefined, amount_max: sp.get("amount_max") ? Number(sp.get("amount_max")) : undefined, has_split: sp.get("has_split") || undefined, trip_id: sp.get("trip_id") || undefined, trip_all_rows: sp.get("trip_all_rows") === "1" || undefined, }); return NextResponse.json(result); } 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() as { date: string; description: string; amount: number; transaction_type?: string; merchant_normalized?: string; category?: string; payment_method?: string; splits?: { participant_id: number; share_percent: number }[]; }; if (!body.date || !body.description || body.amount == null) { return NextResponse.json({ error: "date, description, amount are required" }, { status: 400 }); } // Validated here rather than left to the CHECK constraint, which surfaces as // a 500 with a Postgres error rather than a 400 naming the field. if (body.payment_method && !isPaymentMethod(body.payment_method)) { return NextResponse.json({ error: "Invalid payment_method" }, { status: 400 }); } // Insert manual transaction with no statement (statement_id = NULL, owner_id set directly) const txRows = await queryRaw<{ id: number }>( `INSERT INTO transactions (statement_id, owner_id, transaction_date, description, amount, transaction_type, merchant_normalized, category, payment_method, row_index) VALUES (NULL, $1, $2, $3, $4, $5, $6, $7, $8, ( SELECT COALESCE(MAX(row_index), -1) + 1 FROM transactions WHERE owner_id = $1 AND statement_id IS NULL )) RETURNING id`, [ user.id, body.date, body.description, body.amount, body.transaction_type || "debit", body.merchant_normalized || null, body.category || null, body.payment_method || null, ] ); const transactionId = txRows[0].id; // Insert splits if provided if (body.splits?.length) { for (const s of body.splits) { await queryRaw( `INSERT INTO transaction_splits (transaction_id, participant_id, share_percent) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING`, [transactionId, s.participant_id, s.share_percent] ); } // The form lets you name just the other person and shows the total in amber // when it is under 100 — which is how "Lawn Mowing, Sonu 50%" was saved with // the other half nowhere. Fill in the payer's share rather than refusing: // naming only the other person is a reasonable thing to mean. await completeSplit(transactionId); } return NextResponse.json({ id: transactionId }, { status: 201 }); }