espace-paie-odentas/app/api/odentas-sign/requests/[id]/positions/route.ts

266 lines
8.6 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { verifySignatureSession } from '@/lib/odentas-sign/jwt';
import { supabaseAdmin } from '@/lib/odentas-sign/supabase';
import { getPresignedDownloadUrl } from '@/lib/odentas-sign/s3';
import {
extractPlaceholdersWithPdfParse,
countPdfPagesFromBytes,
estimatePositionsFromPlaceholders,
estimatePositionsFromPlaceholdersUsingText,
extractPlaceholdersWithPdfium,
extractPrecisePositionsFromPdf,
} from '@/lib/odentas-sign/placeholders';
// Force Node.js runtime
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
/**
* GET /api/odentas-sign/requests/:id/positions
* Récupère les positions de signature pour toutes les parties
*/
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const requestId = params.id;
// Vérifier le token JWT
const authHeader = request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return NextResponse.json(
{ error: 'Token manquant ou invalide' },
{ status: 401 }
);
}
const token = authHeader.split(' ')[1];
const payload = verifySignatureSession(token);
if (!payload || payload.requestId !== requestId) {
return NextResponse.json(
{ error: 'Token invalide ou expiré' },
{ status: 401 }
);
}
// Récupérer toutes les positions de signature existantes
const { data: positions, error } = await supabaseAdmin
.from('sign_positions')
.select('page, x, y, w, h, role')
.eq('request_id', requestId)
.order('role');
console.log('[POSITIONS API] Positions en DB:', {
requestId,
count: positions?.length || 0,
hasError: !!error,
positions,
});
if (error) {
console.error('Erreur DB lors de la récupération des positions:', error);
return NextResponse.json(
{ error: 'Erreur lors de la récupération des positions' },
{ status: 500 }
);
}
// Si positions déjà présentes, renvoyer directement
if (positions && positions.length > 0) {
console.log('[POSITIONS API] ✅ Positions trouvées en DB, renvoi direct');
const transformedPositions = positions.map((p) => ({
page: p.page,
x: p.x,
y: p.y,
width: p.w,
height: p.h,
role: p.role,
}));
return NextResponse.json({ positions: transformedPositions });
}
console.log('[POSITIONS API] ❌ Aucune position en DB, extraction depuis le PDF...');
// Pas de positions en DB: tenter d'extraire depuis le PDF via placeholders
// 1) Récupérer la clé S3 du PDF source
const { data: signRequest, error: requestErr } = await supabaseAdmin
.from('sign_requests')
.select('source_s3_key')
.eq('id', requestId)
.single();
console.log('[POSITIONS API] Sign request récupéré:', {
hasData: !!signRequest,
source_s3_key: signRequest?.source_s3_key,
hasError: !!requestErr,
});
if (requestErr || !signRequest?.source_s3_key) {
console.error('Impossible de récupérer sign_request pour extraction:', requestErr);
return NextResponse.json({ positions: [] });
}
// 2) Générer une URL présignée et télécharger le PDF
const pdfUrl = await getPresignedDownloadUrl(signRequest.source_s3_key, 300);
console.log('[POSITIONS API] URL présignée générée, téléchargement du PDF...');
const resp = await fetch(pdfUrl);
if (!resp.ok) {
console.error('Téléchargement du PDF échoué:', resp.status, pdfUrl);
return NextResponse.json({ positions: [] });
}
const arrayBuf = await resp.arrayBuffer();
const bytes = Buffer.from(arrayBuf);
console.log('[POSITIONS API] PDF téléchargé:', {
size: bytes.length,
sizeKB: Math.round(bytes.length / 1024),
});
// 1) Tentative d'extraction précise via pdf-lib (Tm/Td) → pourcentages
console.log('[POSITIONS] Tentative extraction PRÉCISE (pdf-lib) ...');
let precise = await extractPrecisePositionsFromPdf(bytes);
// 2) Fallback DocuSeal-like (Pdfium) si rien
if (!precise || precise.length === 0) {
console.warn('[POSITIONS] Aucun résultat précis, fallback PDFIUM ...');
precise = await extractPlaceholdersWithPdfium(bytes);
}
if (!precise || precise.length === 0) {
console.warn('[POSITIONS] ❌ Aucun placeholder trouvé (pdf-lib + Pdfium). Tentative fallback via pdf-parse + estimation');
const { placeholders, text, numPages } = await extractPlaceholdersWithPdfParse(bytes);
const pageCount = numPages || countPdfPagesFromBytes(bytes);
// Utiliser l'estimation basée sur le TEXTE pour une meilleure position verticale et page
const estimated = estimatePositionsFromPlaceholdersUsingText(placeholders, text, pageCount);
precise = estimated.map((p) => ({
role: p.role,
label: p.label,
page: p.page,
x: p.x,
y: p.y,
width: p.width,
height: p.height,
text: p.label,
}));
if (!precise || precise.length === 0) {
console.warn('[POSITIONS] ❌ Aucun placeholder détecté avec les méthodes disponibles');
return NextResponse.json({ positions: [] });
}
}
console.log(`[POSITIONS] ✅ Extraction réussie: ${precise.length} position(s) trouvée(s)`);
// Persister en DB pour cette demande (meilleure UX aux prochains chargements)
try {
const rows = precise.map((pos) => ({
request_id: requestId,
role: pos.role,
page: pos.page,
x: pos.x, // POURCENTAGES (%)
y: pos.y, // POURCENTAGES (%)
w: pos.width, // POURCENTAGES (%)
h: pos.height, // POURCENTAGES (%)
kind: 'signature',
label: pos.label,
}));
const { error: insertErr } = await supabaseAdmin.from('sign_positions').insert(rows);
if (insertErr) {
console.warn('Insertion positions estimées échouée (non bloquant):', insertErr);
}
} catch (e) {
console.warn('Erreur lors de la persistance des positions estimées:', e);
}
// 6) Retourner au format attendu par le front
const transformedPositions = precise.map((p) => ({
page: p.page,
x: p.x,
y: p.y,
width: p.width,
height: p.height,
role: p.role,
}));
return NextResponse.json({ positions: transformedPositions });
} catch (error) {
console.error('Erreur lors de la récupération des positions:', error);
return NextResponse.json(
{ error: 'Erreur serveur lors de la récupération des positions' },
{ status: 500 }
);
}
}
/**
* POST /api/odentas-sign/requests/:id/positions
* Upsert des positions détectées côté client (mm)
*/
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const requestId = params.id;
// Vérifier le token JWT
const authHeader = request.headers.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return NextResponse.json(
{ error: 'Token manquant ou invalide' },
{ status: 401 }
);
}
const token = authHeader.split(' ')[1];
const payload = verifySignatureSession(token);
if (!payload || payload.requestId !== requestId) {
return NextResponse.json(
{ error: 'Token invalide ou expiré' },
{ status: 401 }
);
}
const body = await request.json();
const positions = Array.isArray(body?.positions) ? body.positions : [];
if (positions.length === 0) {
return NextResponse.json({ success: false, message: 'Aucune position fournie' }, { status: 400 });
}
// Nettoyer les positions existantes de type signature pour cette requête
await supabaseAdmin
.from('sign_positions')
.delete()
.eq('request_id', requestId)
.eq('kind', 'signature');
// Insérer les nouvelles positions (mm)
const rows = positions.map((p: any) => ({
request_id: requestId,
role: p.role,
page: p.page,
x: p.x,
y: p.y,
w: p.w,
h: p.h,
kind: p.kind || 'signature',
label: p.label || null,
}));
const { error: insertErr } = await supabaseAdmin
.from('sign_positions')
.insert(rows);
if (insertErr) {
console.error('Erreur insertion positions (POST):', insertErr);
return NextResponse.json({ error: 'Insertion des positions échouée' }, { status: 500 });
}
return NextResponse.json({ success: true, inserted: rows.length });
} catch (error) {
console.error('Erreur POST positions:', error);
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 });
}
}