changes
This commit is contained in:
612
src/app/api/normative-analysis/route.ts
Normal file
612
src/app/api/normative-analysis/route.ts
Normal file
@@ -0,0 +1,612 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { type NormativeConfidence, type NormativeDocumentType as PrismaNormativeDocumentType, type NormativeRiskLevel, Prisma } from "@prisma/client";
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireAdminApiUser } from "@/lib/auth/admin";
|
||||
import { isLikelyPdfUrl, parseLicitationDocumentLinks, pickPrimaryLicitationDocument, pickPrimaryLicitationPdfDocument } from "@/lib/licitations/documents";
|
||||
import { type NormativeDocumentType } from "@/lib/normative-analysis/analyze";
|
||||
import { analyzeNormativeTextWithAi } from "@/lib/normative-analysis/ai-analyze";
|
||||
import { toNormativeHistoryView } from "@/lib/normative-analysis/history";
|
||||
import { analyzePdf } from "@/lib/pdf/analyzePdf";
|
||||
import { OcrFailedError, OcrUnavailableError, PdfEncryptedError, PdfNoTextDetectedError, PdfUnreadableError } from "@/lib/pdf/errors";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
const MAX_NORMATIVE_PDF_BYTES = 20 * 1024 * 1024;
|
||||
const SOURCE_FETCH_TIMEOUT_MS = 45_000;
|
||||
const STORAGE_ROOT = path.resolve(process.cwd(), "storage");
|
||||
|
||||
function isPdfFile(file: File) {
|
||||
const extension = file.name.toLowerCase().endsWith(".pdf");
|
||||
const mimeType = file.type === "application/pdf";
|
||||
return extension || mimeType;
|
||||
}
|
||||
|
||||
function hasPdfSignature(buffer: Buffer) {
|
||||
return buffer.subarray(0, 5).toString("utf8") === "%PDF-";
|
||||
}
|
||||
|
||||
function guessFileNameFromUrl(url: string) {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const segments = parsed.pathname.split("/").filter(Boolean);
|
||||
const lastSegment = segments.at(-1) ?? "";
|
||||
const decoded = decodeURIComponent(lastSegment).trim();
|
||||
if (!decoded) {
|
||||
return "documento-principal.pdf";
|
||||
}
|
||||
|
||||
return decoded.toLowerCase().endsWith(".pdf") ? decoded : `${decoded}.pdf`;
|
||||
} catch {
|
||||
return "documento-principal.pdf";
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSourcePdf(url: string) {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), SOURCE_FETCH_TIMEOUT_MS);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
redirect: "follow",
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`SOURCE_FETCH_FAILED_${response.status}`);
|
||||
}
|
||||
|
||||
const headerLength = response.headers.get("content-length");
|
||||
const sizeHint = headerLength ? Number.parseInt(headerLength, 10) : 0;
|
||||
if (Number.isFinite(sizeHint) && sizeHint > MAX_NORMATIVE_PDF_BYTES) {
|
||||
throw new Error("SOURCE_PDF_TOO_LARGE");
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
if (!buffer.byteLength) {
|
||||
throw new Error("SOURCE_PDF_EMPTY");
|
||||
}
|
||||
|
||||
if (buffer.byteLength > MAX_NORMATIVE_PDF_BYTES) {
|
||||
throw new Error("SOURCE_PDF_TOO_LARGE");
|
||||
}
|
||||
|
||||
return buffer;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
type LicitationSourcePdf = {
|
||||
fileBuffer: Buffer;
|
||||
fileName: string;
|
||||
linkedLicitationId: string;
|
||||
issuingEntity: string | null;
|
||||
};
|
||||
|
||||
type SourcePdfCandidate = {
|
||||
url: string;
|
||||
fileName: string;
|
||||
};
|
||||
|
||||
function buildSourcePdfCandidates(documentsRaw: unknown, rawSourceUrl: string | null) {
|
||||
const sourceDocuments = parseLicitationDocumentLinks(documentsRaw);
|
||||
const primaryPdf = pickPrimaryLicitationPdfDocument(sourceDocuments);
|
||||
const primaryAny = pickPrimaryLicitationDocument(sourceDocuments);
|
||||
const candidates: SourcePdfCandidate[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
function pushCandidate(url: string | null | undefined, fileName: string | null | undefined) {
|
||||
const normalizedUrl = typeof url === "string" ? url.trim() : "";
|
||||
if (!normalizedUrl || seen.has(normalizedUrl)) {
|
||||
return;
|
||||
}
|
||||
seen.add(normalizedUrl);
|
||||
candidates.push({
|
||||
url: normalizedUrl,
|
||||
fileName: (fileName?.trim() || guessFileNameFromUrl(normalizedUrl)),
|
||||
});
|
||||
}
|
||||
|
||||
pushCandidate(primaryPdf?.url, primaryPdf?.name);
|
||||
pushCandidate(primaryAny?.url, primaryAny?.name);
|
||||
|
||||
for (const document of sourceDocuments) {
|
||||
pushCandidate(document.url, document.name);
|
||||
}
|
||||
|
||||
if (rawSourceUrl) {
|
||||
const normalizedSourceUrl = rawSourceUrl.trim();
|
||||
const existingIndex = candidates.findIndex((item) => item.url === normalizedSourceUrl);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
if (isLikelyPdfUrl(normalizedSourceUrl) && existingIndex > 0) {
|
||||
const [entry] = candidates.splice(existingIndex, 1);
|
||||
candidates.unshift(entry);
|
||||
}
|
||||
} else if (isLikelyPdfUrl(normalizedSourceUrl)) {
|
||||
candidates.unshift({
|
||||
url: normalizedSourceUrl,
|
||||
fileName: guessFileNameFromUrl(normalizedSourceUrl),
|
||||
});
|
||||
} else {
|
||||
pushCandidate(normalizedSourceUrl, guessFileNameFromUrl(normalizedSourceUrl));
|
||||
}
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
async function resolveLicitationSourcePdf(sourceLicitationId: string, currentIssuingEntity: string | null): Promise<LicitationSourcePdf> {
|
||||
const sourceLicitation = await prisma.licitation.findUnique({
|
||||
where: { id: sourceLicitationId },
|
||||
select: {
|
||||
id: true,
|
||||
supplierAwarded: true,
|
||||
documents: true,
|
||||
rawSourceUrl: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!sourceLicitation) {
|
||||
throw new Error("SOURCE_LICITATION_NOT_FOUND");
|
||||
}
|
||||
|
||||
const candidates = buildSourcePdfCandidates(sourceLicitation.documents, sourceLicitation.rawSourceUrl);
|
||||
|
||||
if (candidates.length === 0) {
|
||||
throw new Error("SOURCE_LICITATION_NO_PDF");
|
||||
}
|
||||
|
||||
let sawSourceTooLarge = false;
|
||||
let sawSourceTimeout = false;
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const fileBuffer = await fetchSourcePdf(candidate.url);
|
||||
if (!hasPdfSignature(fileBuffer)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
fileBuffer,
|
||||
fileName: candidate.fileName,
|
||||
linkedLicitationId: sourceLicitation.id,
|
||||
issuingEntity: currentIssuingEntity || sourceLicitation.supplierAwarded?.trim() || null,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message === "SOURCE_PDF_TOO_LARGE") {
|
||||
sawSourceTooLarge = true;
|
||||
}
|
||||
|
||||
if (error.name === "AbortError") {
|
||||
sawSourceTimeout = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sawSourceTooLarge) {
|
||||
throw new Error("SOURCE_LICITATION_PDF_TOO_LARGE");
|
||||
}
|
||||
|
||||
if (sawSourceTimeout) {
|
||||
throw new Error("SOURCE_LICITATION_PDF_TIMEOUT");
|
||||
}
|
||||
|
||||
throw new Error("SOURCE_LICITATION_NO_VALID_PDF");
|
||||
}
|
||||
|
||||
type ProposalDocumentSource = {
|
||||
fileName: string;
|
||||
filePath: string;
|
||||
mimeType: string;
|
||||
};
|
||||
|
||||
function isPdfProposalDocument(document: ProposalDocumentSource) {
|
||||
return document.mimeType === "application/pdf" || document.fileName.toLowerCase().endsWith(".pdf");
|
||||
}
|
||||
|
||||
function pickPrimaryProposalPdfDocument(documents: ProposalDocumentSource[]) {
|
||||
return documents.find((document) => isPdfProposalDocument(document)) ?? null;
|
||||
}
|
||||
|
||||
async function readStoredProposalPdf(filePath: string) {
|
||||
const absolutePath = path.resolve(process.cwd(), filePath);
|
||||
|
||||
if (!absolutePath.startsWith(`${STORAGE_ROOT}${path.sep}`)) {
|
||||
throw new Error("PROPOSAL_PDF_INVALID_PATH");
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = await readFile(absolutePath);
|
||||
|
||||
if (!buffer.byteLength) {
|
||||
throw new Error("PROPOSAL_PDF_EMPTY");
|
||||
}
|
||||
|
||||
if (buffer.byteLength > MAX_NORMATIVE_PDF_BYTES) {
|
||||
throw new Error("PROPOSAL_PDF_TOO_LARGE");
|
||||
}
|
||||
|
||||
return buffer;
|
||||
} catch (error) {
|
||||
if (
|
||||
error &&
|
||||
typeof error === "object" &&
|
||||
"code" in error &&
|
||||
(error as NodeJS.ErrnoException).code === "ENOENT"
|
||||
) {
|
||||
throw new Error("PROPOSAL_PDF_NOT_FOUND");
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function parseDocumentType(value: unknown): NormativeDocumentType {
|
||||
if (typeof value !== "string") {
|
||||
return "OTRO";
|
||||
}
|
||||
|
||||
const normalized = value.trim().toUpperCase();
|
||||
if (normalized === "BASES_LICITACION" || normalized === "CONVOCATORIA" || normalized === "ANEXOS" || normalized === "REGLAMENTO" || normalized === "LEY") {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return "OTRO";
|
||||
}
|
||||
|
||||
function toPrismaDocumentType(value: NormativeDocumentType): PrismaNormativeDocumentType {
|
||||
if (value === "BASES_LICITACION" || value === "CONVOCATORIA" || value === "REGLAMENTO" || value === "LEY") {
|
||||
return value;
|
||||
}
|
||||
|
||||
return "OTRO";
|
||||
}
|
||||
|
||||
function toPrismaRiskLevel(value: "alto" | "medio" | "bajo"): NormativeRiskLevel {
|
||||
if (value === "alto") {
|
||||
return "ALTO";
|
||||
}
|
||||
|
||||
if (value === "medio") {
|
||||
return "MEDIO";
|
||||
}
|
||||
|
||||
return "BAJO";
|
||||
}
|
||||
|
||||
function toPrismaConfidence(value: "low" | "medium" | "high"): NormativeConfidence {
|
||||
if (value === "high") {
|
||||
return "HIGH";
|
||||
}
|
||||
|
||||
if (value === "medium") {
|
||||
return "MEDIUM";
|
||||
}
|
||||
|
||||
return "LOW";
|
||||
}
|
||||
|
||||
function mapAnalysisError(error: unknown) {
|
||||
if (error instanceof PdfEncryptedError) {
|
||||
return {
|
||||
status: 422,
|
||||
error: "El PDF esta protegido/encriptado. Sube una version sin bloqueo para extraer texto.",
|
||||
code: error.code,
|
||||
};
|
||||
}
|
||||
|
||||
if (error instanceof PdfUnreadableError) {
|
||||
return {
|
||||
status: 422,
|
||||
error: "No fue posible leer el PDF. Verifica que el archivo no este danado.",
|
||||
code: error.code,
|
||||
};
|
||||
}
|
||||
|
||||
if (error instanceof OcrUnavailableError) {
|
||||
return {
|
||||
status: 503,
|
||||
error: "No se detecto texto suficiente y OCRmyPDF no esta disponible.",
|
||||
code: error.code,
|
||||
};
|
||||
}
|
||||
|
||||
if (error instanceof PdfNoTextDetectedError) {
|
||||
return {
|
||||
status: 422,
|
||||
error: "No se detecto texto util en el PDF para analizar.",
|
||||
code: error.code,
|
||||
};
|
||||
}
|
||||
|
||||
if (error instanceof OcrFailedError) {
|
||||
return {
|
||||
status: 422,
|
||||
error: "No se detecto texto suficiente y el OCR fallo durante el procesamiento.",
|
||||
code: error.code,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 422,
|
||||
error: "No fue posible analizar el PDF.",
|
||||
code: "NORMATIVE_ANALYSIS_FAILED",
|
||||
};
|
||||
}
|
||||
|
||||
function isSchemaNotReadyError(error: unknown) {
|
||||
return error instanceof Prisma.PrismaClientKnownRequestError && (error.code === "P2021" || error.code === "P2022");
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const user = await requireAdminApiUser();
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const documentType = parseDocumentType(formData.get("documentType"));
|
||||
const issuingEntityRaw = formData.get("issuingEntity");
|
||||
let issuingEntity = typeof issuingEntityRaw === "string" ? issuingEntityRaw.trim() || null : null;
|
||||
const sourceLicitationIdRaw = formData.get("sourceLicitationId");
|
||||
const sourceLicitationId = typeof sourceLicitationIdRaw === "string" ? sourceLicitationIdRaw.trim() || null : null;
|
||||
const sourceProposalIdRaw = formData.get("sourceProposalId");
|
||||
const sourceProposalId = typeof sourceProposalIdRaw === "string" ? sourceProposalIdRaw.trim() || null : null;
|
||||
const fileInput = formData.get("file");
|
||||
const uploadedFile = fileInput instanceof File && fileInput.size > 0 ? fileInput : null;
|
||||
const useSourceDocument = String(formData.get("useSourceDocument") ?? "") === "1";
|
||||
|
||||
try {
|
||||
let fileName = "";
|
||||
let fileBuffer: Buffer | null = null;
|
||||
let linkedLicitationId: string | null = null;
|
||||
|
||||
if (uploadedFile) {
|
||||
if (uploadedFile.size > MAX_NORMATIVE_PDF_BYTES) {
|
||||
return NextResponse.json({ error: "El archivo excede el limite de 20MB." }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!isPdfFile(uploadedFile)) {
|
||||
return NextResponse.json({ error: "Solo se permiten archivos PDF." }, { status: 400 });
|
||||
}
|
||||
|
||||
fileBuffer = Buffer.from(await uploadedFile.arrayBuffer());
|
||||
fileName = uploadedFile.name;
|
||||
|
||||
if (!hasPdfSignature(fileBuffer)) {
|
||||
return NextResponse.json({ error: "El archivo no parece ser un PDF valido." }, { status: 400 });
|
||||
}
|
||||
} else if (sourceProposalId && useSourceDocument) {
|
||||
const sourceProposal = await prisma.proposal.findFirst({
|
||||
where: {
|
||||
id: sourceProposalId,
|
||||
userId: user.id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
issuingEntity: true,
|
||||
sourceLicitationId: true,
|
||||
documents: {
|
||||
orderBy: [{ createdAt: "desc" }],
|
||||
select: {
|
||||
fileName: true,
|
||||
filePath: true,
|
||||
mimeType: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!sourceProposal) {
|
||||
return NextResponse.json({ error: "No se encontro la propuesta vinculada de Modulo 5." }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!issuingEntity) {
|
||||
issuingEntity = sourceProposal.issuingEntity?.trim() || null;
|
||||
}
|
||||
|
||||
linkedLicitationId = sourceProposal.sourceLicitationId ?? null;
|
||||
const proposalPdf = pickPrimaryProposalPdfDocument(sourceProposal.documents);
|
||||
|
||||
if (proposalPdf) {
|
||||
fileBuffer = await readStoredProposalPdf(proposalPdf.filePath);
|
||||
fileName = proposalPdf.fileName;
|
||||
|
||||
if (!hasPdfSignature(fileBuffer)) {
|
||||
return NextResponse.json({ error: "El PDF principal de Modulo 5 no tiene una firma valida." }, { status: 422 });
|
||||
}
|
||||
} else if (sourceProposal.sourceLicitationId) {
|
||||
const licitationSource = await resolveLicitationSourcePdf(sourceProposal.sourceLicitationId, issuingEntity);
|
||||
fileBuffer = licitationSource.fileBuffer;
|
||||
fileName = licitationSource.fileName;
|
||||
linkedLicitationId = licitationSource.linkedLicitationId;
|
||||
issuingEntity = licitationSource.issuingEntity;
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: "La propuesta de Modulo 5 no tiene un PDF disponible para analisis automatico. Sube un PDF manualmente." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
} else if (sourceLicitationId && useSourceDocument) {
|
||||
const licitationSource = await resolveLicitationSourcePdf(sourceLicitationId, issuingEntity);
|
||||
fileBuffer = licitationSource.fileBuffer;
|
||||
fileName = licitationSource.fileName;
|
||||
linkedLicitationId = licitationSource.linkedLicitationId;
|
||||
issuingEntity = licitationSource.issuingEntity;
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: "Debes adjuntar un PDF o vincular una propuesta de Modulo 5 / oportunidad de Modulo 3 para usar su documento principal." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!fileBuffer) {
|
||||
return NextResponse.json({ error: "No fue posible obtener un documento PDF para analizar." }, { status: 400 });
|
||||
}
|
||||
|
||||
const analyzed = await analyzePdf(fileBuffer);
|
||||
const aiAnalysis = await analyzeNormativeTextWithAi({
|
||||
text: analyzed.text,
|
||||
fileName,
|
||||
documentType,
|
||||
issuingEntity,
|
||||
methodUsed: analyzed.methodUsed,
|
||||
numPages: analyzed.numPages,
|
||||
warnings: analyzed.warnings,
|
||||
});
|
||||
const result = aiAnalysis.result;
|
||||
const combinedWarnings = [
|
||||
...analyzed.warnings,
|
||||
...aiAnalysis.warnings,
|
||||
`Motor de analisis: ${aiAnalysis.engine}${aiAnalysis.model ? ` (${aiAnalysis.model})` : ""}`,
|
||||
];
|
||||
|
||||
if (!linkedLicitationId && sourceLicitationId) {
|
||||
const linkedLicitation = await prisma.licitation.findUnique({
|
||||
where: { id: sourceLicitationId },
|
||||
select: { id: true },
|
||||
});
|
||||
linkedLicitationId = linkedLicitation?.id ?? null;
|
||||
}
|
||||
|
||||
if (!linkedLicitationId && sourceProposalId) {
|
||||
const linkedProposal = await prisma.proposal.findFirst({
|
||||
where: {
|
||||
id: sourceProposalId,
|
||||
userId: user.id,
|
||||
},
|
||||
select: {
|
||||
sourceLicitationId: true,
|
||||
},
|
||||
});
|
||||
linkedLicitationId = linkedProposal?.sourceLicitationId ?? null;
|
||||
}
|
||||
|
||||
const created = await prisma.normativeAnalysisHistory.create({
|
||||
data: {
|
||||
userId: user.id,
|
||||
sourceLicitationId: linkedLicitationId,
|
||||
fileName,
|
||||
documentType: toPrismaDocumentType(documentType),
|
||||
issuingEntity,
|
||||
methodUsed: analyzed.methodUsed === "ocr" ? "OCR" : "DIRECT",
|
||||
numPages: analyzed.numPages,
|
||||
warnings: combinedWarnings,
|
||||
extractedChars: analyzed.text.length,
|
||||
confidence: toPrismaConfidence(result.confidence),
|
||||
viabilityScore: result.participationViability.score,
|
||||
riskLevel: toPrismaRiskLevel(result.risk.level),
|
||||
executiveSummary: result.executiveSummary,
|
||||
result,
|
||||
analyzedAt: new Date(),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
sourceLicitationId: true,
|
||||
fileName: true,
|
||||
documentType: true,
|
||||
issuingEntity: true,
|
||||
methodUsed: true,
|
||||
numPages: true,
|
||||
warnings: true,
|
||||
extractedChars: true,
|
||||
analyzedAt: true,
|
||||
viabilityScore: true,
|
||||
riskLevel: true,
|
||||
confidence: true,
|
||||
result: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
item: toNormativeHistoryView(created),
|
||||
});
|
||||
} catch (error) {
|
||||
if (isSchemaNotReadyError(error)) {
|
||||
return NextResponse.json(
|
||||
{ error: "La base de datos aun no tiene las tablas de Analisis Normativo. Ejecuta prisma migrate para continuar." },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message === "SOURCE_LICITATION_NOT_FOUND") {
|
||||
return NextResponse.json({ error: "No se encontro la oportunidad vinculada de Modulo 3." }, { status: 404 });
|
||||
}
|
||||
|
||||
if (error.message === "SOURCE_LICITATION_NO_PDF") {
|
||||
return NextResponse.json(
|
||||
{ error: "La oportunidad de Modulo 3 no tiene un PDF principal disponible para analisis automatico." },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (error.message === "SOURCE_LICITATION_NO_VALID_PDF") {
|
||||
return NextResponse.json(
|
||||
{ error: "No se pudo obtener un PDF valido desde los enlaces de la oportunidad en Modulo 3." },
|
||||
{ status: 422 },
|
||||
);
|
||||
}
|
||||
|
||||
if (error.message === "SOURCE_LICITATION_PDF_TOO_LARGE") {
|
||||
return NextResponse.json({ error: "Los documentos vinculados de Modulo 3 exceden el limite de 20MB." }, { status: 400 });
|
||||
}
|
||||
|
||||
if (error.message === "SOURCE_LICITATION_PDF_TIMEOUT") {
|
||||
return NextResponse.json({ error: "Se excedio el tiempo al descargar documentos vinculados de Modulo 3." }, { status: 504 });
|
||||
}
|
||||
|
||||
if (error.message === "SOURCE_LICITATION_INVALID_PDF") {
|
||||
return NextResponse.json(
|
||||
{ error: "El documento principal de Modulo 3 no tiene una firma PDF valida." },
|
||||
{ status: 422 },
|
||||
);
|
||||
}
|
||||
|
||||
if (error.message === "PROPOSAL_PDF_INVALID_PATH") {
|
||||
return NextResponse.json({ error: "El archivo de Modulo 5 no esta disponible para lectura segura." }, { status: 422 });
|
||||
}
|
||||
|
||||
if (error.message === "PROPOSAL_PDF_NOT_FOUND") {
|
||||
return NextResponse.json({ error: "No se encontro el PDF principal cargado en Modulo 5." }, { status: 422 });
|
||||
}
|
||||
|
||||
if (error.message === "PROPOSAL_PDF_EMPTY") {
|
||||
return NextResponse.json({ error: "El PDF principal de Modulo 5 esta vacio." }, { status: 422 });
|
||||
}
|
||||
|
||||
if (error.message === "PROPOSAL_PDF_TOO_LARGE") {
|
||||
return NextResponse.json({ error: "El PDF principal de Modulo 5 excede el limite de 20MB." }, { status: 400 });
|
||||
}
|
||||
|
||||
if (error.name === "AbortError") {
|
||||
return NextResponse.json({ error: "Se excedio el tiempo al descargar el PDF principal de Modulo 3." }, { status: 504 });
|
||||
}
|
||||
|
||||
if (error.message === "SOURCE_PDF_TOO_LARGE") {
|
||||
return NextResponse.json({ error: "El PDF principal de Modulo 3 excede el limite de 20MB." }, { status: 400 });
|
||||
}
|
||||
|
||||
if (error.message === "SOURCE_PDF_EMPTY") {
|
||||
return NextResponse.json({ error: "El PDF principal de Modulo 3 esta vacio." }, { status: 422 });
|
||||
}
|
||||
|
||||
if (error.message.startsWith("SOURCE_FETCH_FAILED_")) {
|
||||
return NextResponse.json({ error: "No fue posible descargar el PDF principal desde la fuente de Modulo 3." }, { status: 422 });
|
||||
}
|
||||
}
|
||||
|
||||
const mapped = mapAnalysisError(error);
|
||||
return NextResponse.json({ error: mapped.error, code: mapped.code }, { status: mapped.status });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user