Move splits, tags and overrides from manual to statement side on reconcile (delete from manual after copying) instead of just copying. Add read-time filter to exclude reconciled manual transactions from balance and shared transaction queries. Also adds participant filter to shared expenses page.
92 lines
3.3 KiB
TypeScript
92 lines
3.3 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { getCurrentUser } from "@/lib/auth";
|
|
import { prisma, queryRaw } from "@/lib/db";
|
|
|
|
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 {
|
|
matches: { manual_id: number; statement_tx_id: number }[];
|
|
};
|
|
|
|
if (!Array.isArray(body.matches) || body.matches.length === 0) {
|
|
return NextResponse.json({ error: "No matches provided" }, { status: 400 });
|
|
}
|
|
|
|
// Verify all manual_ids belong to this user
|
|
const manualIds = body.matches.map((m) => m.manual_id);
|
|
const owned = await queryRaw<{ id: number }>(
|
|
`SELECT id FROM transactions WHERE id = ANY($1::int[]) AND statement_id IS NULL AND owner_id = $2`,
|
|
[manualIds, user.id]
|
|
);
|
|
if (owned.length !== manualIds.length) {
|
|
return NextResponse.json({ error: "One or more transactions not found" }, { status: 404 });
|
|
}
|
|
|
|
let reconciled = 0;
|
|
|
|
for (const { manual_id, statement_tx_id } of body.matches) {
|
|
await prisma.$transaction(async (tx) => {
|
|
// Copy overrides: manual → statement tx
|
|
const override = await tx.transaction_overrides.findUnique({
|
|
where: { transaction_id: manual_id },
|
|
});
|
|
if (override) {
|
|
await tx.transaction_overrides.upsert({
|
|
where: { transaction_id: statement_tx_id },
|
|
update: {
|
|
category_override: override.category_override,
|
|
merchant_normalized: override.merchant_normalized,
|
|
notes: override.notes,
|
|
my_share_percent: override.my_share_percent,
|
|
updated_at: new Date(),
|
|
},
|
|
create: {
|
|
transaction_id: statement_tx_id,
|
|
category_override: override.category_override,
|
|
merchant_normalized: override.merchant_normalized,
|
|
notes: override.notes,
|
|
my_share_percent: override.my_share_percent,
|
|
},
|
|
});
|
|
await tx.transaction_overrides.deleteMany({ where: { transaction_id: manual_id } });
|
|
}
|
|
|
|
// Move tags: manual → statement tx
|
|
const tags = await tx.transaction_tags.findMany({ where: { transaction_id: manual_id } });
|
|
if (tags.length) {
|
|
await tx.transaction_tags.createMany({
|
|
data: tags.map((t) => ({ transaction_id: statement_tx_id, tag_id: t.tag_id })),
|
|
skipDuplicates: true,
|
|
});
|
|
await tx.transaction_tags.deleteMany({ where: { transaction_id: manual_id } });
|
|
}
|
|
|
|
// Move splits: manual → statement tx
|
|
const splits = await tx.transaction_splits.findMany({ where: { transaction_id: manual_id } });
|
|
if (splits.length) {
|
|
await tx.transaction_splits.createMany({
|
|
data: splits.map((s) => ({
|
|
transaction_id: statement_tx_id,
|
|
participant_id: s.participant_id,
|
|
share_percent: s.share_percent,
|
|
})),
|
|
skipDuplicates: true,
|
|
});
|
|
await tx.transaction_splits.deleteMany({ where: { transaction_id: manual_id } });
|
|
}
|
|
|
|
// Mark manual tx as reconciled (link to statement tx)
|
|
await tx.$executeRawUnsafe(
|
|
`UPDATE transactions SET reconciled_with_id = $1 WHERE id = $2`,
|
|
statement_tx_id,
|
|
manual_id
|
|
);
|
|
});
|
|
reconciled++;
|
|
}
|
|
|
|
return NextResponse.json({ reconciled });
|
|
}
|