JEI_Import_Documents-App/app/api/payments/create/route.ts

146 lines
4.1 KiB
TypeScript
Raw Normal View History

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