145 lines
4.1 KiB
TypeScript
145 lines
4.1 KiB
TypeScript
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,
|
|
});
|
|
}
|