import { NextRequest, NextResponse } from "next/server"; import { prisma, queryRaw } from "@/lib/db"; import { assignTransactionsToTrip, canAccessTransactions } from "@/lib/queries"; import { getCurrentUser } from "@/lib/auth"; import { applyRuleActions, captureSnapshot } from "@/lib/rule-actions"; import type { Actions } from "@/lib/rules"; 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 { action, ids, category, merchant_normalized, splits, tag_id } = body as { action: string; ids: number[]; category?: string; merchant_normalized?: string; splits?: { participant_id: number; share_percent: number }[]; tag_id?: number; }; if (!ids || !Array.isArray(ids) || ids.length === 0) { return NextResponse.json({ error: "ids required" }, { status: 400 }); } if (!(await canAccessTransactions(user.id, ids.map(Number)))) { return NextResponse.json({ error: "Not found" }, { status: 404 }); } if (action === "categorize" && category) { const ops = ids.map((id) => prisma.transaction_overrides.upsert({ where: { transaction_id: id }, update: { category_override: category, updated_at: new Date() }, create: { transaction_id: id, category_override: category }, }) ); await prisma.$transaction(ops); return NextResponse.json({ updated: ids.length }); } if (action === "normalize" && merchant_normalized) { const ops = ids.map((id) => prisma.transaction_overrides.upsert({ where: { transaction_id: id }, update: { merchant_normalized, updated_at: new Date() }, create: { transaction_id: id, merchant_normalized }, }) ); await prisma.$transaction(ops); return NextResponse.json({ updated: ids.length }); } if (action === "split" && Array.isArray(splits) && splits.length > 0) { const total = splits.reduce((s, x) => s + x.share_percent, 0); if (Math.abs(total - 100) > 0.01) { return NextResponse.json({ error: "Shares must sum to 100%" }, { status: 400 }); } await prisma.$transaction( ids.flatMap((id) => [ prisma.transaction_splits.deleteMany({ where: { transaction_id: id } }), prisma.transaction_splits.createMany({ data: splits.map((s) => ({ transaction_id: id, participant_id: s.participant_id, share_percent: s.share_percent, })), }), ]) ); return NextResponse.json({ updated: ids.length }); } if ((action === "tag" || action === "untag") && tag_id) { if (action === "tag") { for (const id of ids) { await queryRaw( `INSERT INTO transaction_tags (transaction_id, tag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, [id, tag_id] ); } } else { await queryRaw( `DELETE FROM transaction_tags WHERE transaction_id = ANY($1::int[]) AND tag_id = $2`, [ids, tag_id] ); } return NextResponse.json({ updated: ids.length }); } // Quick action: fire a saved rule's actions at the current selection. The // selection replaces the rule's conditions, which are not evaluated at all. // Recorded as a rule_apply_run so it can be reverted like any other run. if (action === "apply_rule") { const { rule_id } = body as { rule_id?: number }; if (!rule_id) return NextResponse.json({ error: "rule_id required" }, { status: 400 }); const rules = await queryRaw<{ id: number; name: string; actions: unknown }>( `SELECT id, name, actions FROM rules WHERE id = $1 AND owner_id = $2`, [Number(rule_id), user.id] ); if (!rules.length) return NextResponse.json({ error: "Rule not found" }, { status: 404 }); const actions = (typeof rules[0].actions === "string" ? JSON.parse(rules[0].actions) : rules[0].actions) as Actions; const snapshot = await captureSnapshot(ids.map(Number)); for (const id of ids) { await applyRuleActions(Number(id), actions); } const run = await queryRaw<{ id: number }>( `INSERT INTO rule_apply_runs (owner_id, split_from, matched, transactions_affected, snapshot, rule_id, rule_name, source) VALUES ($1, NULL, $2, $3, $4, $5, $6, 'selection') RETURNING id`, [user.id, ids.length, ids.length, JSON.stringify(snapshot), rules[0].id, rules[0].name] ); return NextResponse.json({ updated: ids.length, run_id: run[0].id, rule: rules[0].name }); } if (action === "assign_trip") { const { trip_id } = body as { ids: number[]; trip_id: number | null }; await assignTransactionsToTrip(trip_id, ids); return NextResponse.json({ updated: ids.length }); } return NextResponse.json({ error: "Invalid action" }, { status: 400 }); }