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, }); }