diff --git a/app/api/payments/create/route.ts b/app/api/payments/create/route.ts new file mode 100644 index 0000000..7dd354e --- /dev/null +++ b/app/api/payments/create/route.ts @@ -0,0 +1,145 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getPb } from "../../../lib/pb"; + +const BATCHES = "JEI_payment_batches"; +const INVOICES = "JEI_invoices"; +const ALLOWED_MIME = new Set(["application/pdf", "image/jpeg", "image/png"]); +const MAX_POP_BYTES = 8 * 1024 * 1024; + +export const dynamic = "force-dynamic"; + +export async function POST(req: NextRequest) { + const pb = getPb(); + let form: FormData; + try { + form = await req.formData(); + } catch (e: any) { + return NextResponse.json({ error: `Invalid form-data: ${e?.message || e}` }, { status: 400 }); + } + + const batchRef = String(form.get("batch_ref") || "").trim(); + const paymentDate = String(form.get("payment_date") || "").trim(); + const invoiceIdsRaw = String(form.get("invoice_ids") || ""); + const popFile = form.get("pop_file"); + + // 1. invoice_ids + let invoiceIds: string[]; + try { + const parsed = JSON.parse(invoiceIdsRaw); + if (!Array.isArray(parsed) || parsed.length === 0 || !parsed.every((x) => typeof x === "string")) { + throw new Error("invoice_ids must be a non-empty JSON array of strings"); + } + invoiceIds = parsed; + } catch (e: any) { + return NextResponse.json({ error: e?.message || "Invalid invoice_ids" }, { status: 400 }); + } + + // 2. batch_ref + if (!/^[A-Za-z0-9_-]{1,50}$/.test(batchRef)) { + return NextResponse.json( + { error: "batch_ref must match ^[A-Za-z0-9_-]{1,50}$" }, + { status: 400 }, + ); + } + + // 3. payment_date + if (!/^\d{4}-\d{2}-\d{2}$/.test(paymentDate)) { + return NextResponse.json( + { error: "payment_date must be YYYY-MM-DD" }, + { status: 400 }, + ); + } + + // 4. pop_file + if (!(popFile instanceof File)) { + return NextResponse.json({ error: "pop_file is required" }, { status: 400 }); + } + if (!ALLOWED_MIME.has(popFile.type)) { + return NextResponse.json( + { error: `pop_file type must be PDF, JPG or PNG (got ${popFile.type})` }, + { status: 400 }, + ); + } + if (popFile.size > MAX_POP_BYTES) { + return NextResponse.json( + { error: `pop_file exceeds 8 MB (got ${popFile.size} bytes)` }, + { status: 400 }, + ); + } + + // 5. batch_ref uniqueness + try { + const dupe = await pb.collection(BATCHES).getList(1, 1, { + filter: `batch_ref="${batchRef.replace(/"/g, '\\"')}"`, + }); + if (dupe.totalItems > 0) { + return NextResponse.json( + { error: `Batch ref "${batchRef}" already exists` }, + { status: 409 }, + ); + } + } catch (e: any) { + return NextResponse.json( + { error: `batch_ref uniqueness check failed: ${e?.message || e}` }, + { status: 500 }, + ); + } + + // 6. Re-validate each invoice is still unpaid + const conflicts: string[] = []; + const validIds: string[] = []; + for (const id of invoiceIds) { + try { + const inv = await pb.collection(INVOICES).getOne(id); + if (inv.payment_batch && inv.payment_batch !== "") { + conflicts.push(id); + } else { + validIds.push(id); + } + } catch (e: any) { + conflicts.push(id); // missing or 404 → treat as conflict + } + } + if (conflicts.length > 0) { + return NextResponse.json( + { error: `These invoices are already paid or missing: ${conflicts.join(", ")}` }, + { status: 409 }, + ); + } + + // 7. Create the batch + let batchId: string; + try { + const created = await pb.collection(BATCHES).create({ + batch_ref: batchRef, + payment_date: paymentDate, + pop_file: popFile, + }); + batchId = created.id; + } catch (e: any) { + return NextResponse.json( + { error: `Failed to create batch: ${e?.message || e}` }, + { status: 500 }, + ); + } + + // 8. PATCH each invoice (best-effort, parallel) + const patchErrors: string[] = []; + await Promise.all( + validIds.map(async (id) => { + try { + await pb.collection(INVOICES).update(id, { payment_batch: batchId }); + } catch (e: any) { + patchErrors.push(`${id}: ${e?.message || e}`); + } + }), + ); + + return NextResponse.json({ + ok: true, + batchId, + batchRef, + invoiceCount: validIds.length, + patchErrors: patchErrors.length > 0 ? patchErrors : undefined, + }); +}