diff --git a/src/app/api/compress-pdf/extreme/route.ts b/src/app/api/compress-pdf/extreme/route.ts index 91fbb04..1a9dcb3 100644 --- a/src/app/api/compress-pdf/extreme/route.ts +++ b/src/app/api/compress-pdf/extreme/route.ts @@ -1,10 +1,13 @@ import { NextRequest, NextResponse } from "next/server"; -import { writeFile, readFile, unlink, mkdir } from "fs/promises"; +import { writeFile, readFile, unlink, mkdir, stat } from "fs/promises"; +import { createWriteStream } from "fs"; import { execFile } from "child_process"; import { promisify } from "util"; import { randomUUID } from "crypto"; import { join } from "path"; import { tmpdir } from "os"; +import { Readable } from "stream"; +import { pipeline } from "stream/promises"; const execFileAsync = promisify(execFile); @@ -97,23 +100,60 @@ async function cleanup(dir: string) { export async function POST(req: NextRequest) { const tmpDir = join(tmpdir(), `pdf-extreme-${randomUUID()}`); try { - const formData = await req.formData(); - const fileBlob = formData.get("fileInput") as Blob | null; - if (!fileBlob) { + // Stream body directly to temp file — avoids req.formData() size limit + // that causes "Failed to parse body as FormData" on large files + await mkdir(tmpDir, { recursive: true }); + + const rawPath = join(tmpDir, "raw-upload"); + const inputPath = join(tmpDir, "input.pdf"); + const gsOutputPath = join(tmpDir, "gs-output.pdf"); + const finalOutputPath = join(tmpDir, "final.pdf"); + + if (!req.body) { return NextResponse.json( { error: "Lipsește fișierul PDF." }, { status: 400 }, ); } - const originalSize = fileBlob.size; - await mkdir(tmpDir, { recursive: true }); + // Write the raw multipart body to disk + const nodeStream = Readable.fromWeb(req.body as import("stream/web").ReadableStream); + await pipeline(nodeStream, createWriteStream(rawPath)); - const inputPath = join(tmpDir, "input.pdf"); - const gsOutputPath = join(tmpDir, "gs-output.pdf"); - const finalOutputPath = join(tmpDir, "final.pdf"); + // Extract the PDF from multipart: find the double CRLF after headers, + // then read until the boundary marker before the end + const rawBuf = await readFile(rawPath); + const headerEnd = rawBuf.indexOf(Buffer.from("\r\n\r\n")); + if (headerEnd === -1) { + return NextResponse.json( + { error: "Lipsește fișierul PDF." }, + { status: 400 }, + ); + } - await writeFile(inputPath, Buffer.from(await fileBlob.arrayBuffer())); + // Extract boundary from Content-Type header + const contentType = req.headers.get("content-type") || ""; + const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/); + const boundary = boundaryMatch?.[1] ?? boundaryMatch?.[2] ?? ""; + + if (!boundary) { + return NextResponse.json( + { error: "Lipsește fișierul PDF." }, + { status: 400 }, + ); + } + + // File content starts after first double CRLF, ends before closing boundary + const closingBoundary = Buffer.from(`\r\n--${boundary}`); + const fileStart = headerEnd + 4; + const fileEnd = rawBuf.indexOf(closingBoundary, fileStart); + const pdfData = fileEnd !== -1 ? rawBuf.subarray(fileStart, fileEnd) : rawBuf.subarray(fileStart); + + await writeFile(inputPath, pdfData); + const originalSize = pdfData.length; + + // Clean up raw file early to free disk space + await unlink(rawPath).catch(() => {}); // Step 1: Ghostscript — aggressive image recompression + downsampling try { diff --git a/src/app/api/registratura/route.ts b/src/app/api/registratura/route.ts index 472cd53..4094a50 100644 --- a/src/app/api/registratura/route.ts +++ b/src/app/api/registratura/route.ts @@ -14,7 +14,7 @@ import type { Prisma } from "@prisma/client"; import { v4 as uuid } from "uuid"; import { prisma } from "@/core/storage/prisma"; import { getAuthSession } from "@/core/auth"; -import { allocateSequenceNumber } from "@/modules/registratura/services/registry-service"; +import { allocateSequenceNumber, recalculateSequence } from "@/modules/registratura/services/registry-service"; import { logAuditEvent, computeEntryDiff, @@ -349,9 +349,21 @@ export async function PUT(req: NextRequest) { delete updates.createdBy; delete updates.createdByName; + // Re-allocate registry number when company or direction changes + const companyChanged = updates.company && updates.company !== existing.company; + const directionChanged = updates.direction && updates.direction !== existing.direction; + let newNumber: string | undefined; + if (companyChanged || directionChanged) { + const targetCompany = (updates.company ?? existing.company) as CompanyId; + const targetDirection = (updates.direction ?? existing.direction) as RegistryDirection; + const allocated = await allocateSequenceNumber(targetCompany, targetDirection); + newNumber = allocated.number; + } + const updated: RegistryEntry = { ...existing, ...updates, + ...(newNumber ? { number: newNumber } : {}), updatedAt: new Date().toISOString(), }; @@ -364,6 +376,14 @@ export async function PUT(req: NextRequest) { await saveEntryToDB(updated); + // Recalculate old company's counter so next entry gets the correct number + if (companyChanged || directionChanged) { + await recalculateSequence( + existing.company as CompanyId, + existing.direction as RegistryDirection, + ); + } + if (diff) { const action = updates.status === "inchis" && existing.status !== "inchis" ? "closed" as const @@ -373,11 +393,11 @@ export async function PUT(req: NextRequest) { await logAuditEvent({ entryId: id, - entryNumber: existing.number, + entryNumber: updated.number, action, actor: actor.id, actorName: actor.name, - company: existing.company, + company: updated.company, detail: { changes: diff }, }); } diff --git a/src/modules/registratura/services/registry-service.ts b/src/modules/registratura/services/registry-service.ts index bc79d08..920e296 100644 --- a/src/modules/registratura/services/registry-service.ts +++ b/src/modules/registratura/services/registry-service.ts @@ -252,6 +252,51 @@ export async function allocateSequenceNumber( }; } +/** + * Recalculate a company's sequence counter to match actual entries in the DB. + * Called when an entry is reassigned away from a company, so the counter + * reflects the real max sequence instead of staying artificially high. + */ +export async function recalculateSequence( + company: CompanyId, + direction: RegistryDirection, + year?: number, +): Promise { + const { prisma } = await import("@/core/storage/prisma"); + + const companyPrefix = REGISTRY_COMPANY_PREFIX[company]; + const typeCode = DIRECTION_TYPE_CODE[direction]; + const yr = year ?? new Date().getFullYear(); + + // Find the actual max sequence from entries in KeyValueStore + const pattern = `${companyPrefix}-${yr}-${typeCode}-%`; + const rows = await prisma.$queryRaw>` + SELECT MAX( + CAST(SUBSTRING(value::text FROM ${`${companyPrefix}-${yr}-${typeCode}-(\\d{5})`}) AS INTEGER) + ) AS "maxSeq" + FROM "KeyValueStore" + WHERE namespace = 'registratura' + AND key LIKE 'entry:%' + AND value::text LIKE ${`%"number":"${pattern}"%`} + `; + + const actualMax = rows[0]?.maxSeq ?? 0; + + // Reset the counter to the actual max (or delete if 0) + if (actualMax === 0) { + await prisma.$executeRaw` + DELETE FROM "RegistrySequence" + WHERE company = ${companyPrefix} AND year = ${yr} AND type = ${typeCode} + `; + } else { + await prisma.$executeRaw` + UPDATE "RegistrySequence" + SET "lastSeq" = ${actualMax}, "updatedAt" = NOW() + WHERE company = ${companyPrefix} AND year = ${yr} AND type = ${typeCode} + `; + } +} + // ── Number format detection + parsing ── export interface ParsedRegistryNumber {