feat: intégration collecteurs Bodacc/Inforgreffe + génération PDF

- Collecteur Bodacc: scraping léger pour gel des avoirs (personnes)
- Collecteur Inforgreffe/Societe.com: données entreprises (SIREN, dirigeants, etc.)
- Générateur PDF: rapports formatés HTML pour chaque type dentité
This commit is contained in:
4NK IA 2025-09-18 16:03:57 +00:00
parent 39c452002a
commit 8e3daad446
4 changed files with 1146 additions and 20 deletions

View File

@ -0,0 +1,235 @@
/**
* Collecteur Bodacc - Gel des avoirs
* Scraping léger avec politesse pour vérifier les mentions de gel des avoirs
*/
const fetch = require('node-fetch');
const { JSDOM } = require('jsdom');
const BODACC_BASE_URL = 'https://www.bodacc.fr';
const USER_AGENT = '4NK-IA-Front/1.0 (Document Analysis Tool)';
const REQUEST_DELAY_MS = 1000; // 1 seconde entre les requêtes
const REQUEST_TIMEOUT_MS = 10000; // 10 secondes timeout
/**
* Effectue une recherche sur Bodacc pour un nom/prénom donné
* @param {string} lastName - Nom de famille (obligatoire)
* @param {string} firstName - Prénom (optionnel)
* @returns {Promise<Object>} Résultat de la recherche
*/
async function searchBodaccGelAvoirs(lastName, firstName = '') {
const startTime = Date.now();
try {
console.log(`[Bodacc] Recherche gel des avoirs pour: ${lastName} ${firstName}`);
// Construction de la requête de recherche
const searchParams = new URLSearchParams({
'q': `${lastName} ${firstName}`.trim(),
'type': 'gel-avoirs',
'date_debut': '2020-01-01', // Recherche sur les 4 dernières années
'date_fin': new Date().toISOString().split('T')[0]
});
const searchUrl = `${BODACC_BASE_URL}/recherche?${searchParams.toString()}`;
// Requête avec politesse
const response = await fetch(searchUrl, {
method: 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
},
timeout: REQUEST_TIMEOUT_MS
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const html = await response.text();
const dom = new JSDOM(html);
const document = dom.window.document;
// Extraction des résultats
const results = extractBodaccResults(document, lastName, firstName);
// Délai de politesse avant la prochaine requête
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
const duration = Date.now() - startTime;
console.log(`[Bodacc] Recherche terminée en ${duration}ms, ${results.length} résultats`);
return {
success: true,
duration,
results,
source: 'bodacc.fr',
searchUrl,
timestamp: new Date().toISOString()
};
} catch (error) {
const duration = Date.now() - startTime;
console.error(`[Bodacc] Erreur recherche:`, error.message);
return {
success: false,
duration,
error: error.message,
results: [],
source: 'bodacc.fr',
timestamp: new Date().toISOString()
};
}
}
/**
* Extrait les résultats de la page Bodacc
* @param {Document} document - Document DOM parsé
* @param {string} lastName - Nom recherché
* @param {string} firstName - Prénom recherché
* @returns {Array} Liste des résultats trouvés
*/
function extractBodaccResults(document, lastName, firstName) {
const results = [];
try {
// Sélecteurs pour les résultats de gel des avoirs
const resultSelectors = [
'.result-item',
'.search-result',
'.bodacc-result',
'[data-type="gel-avoirs"]'
];
let resultElements = [];
for (const selector of resultSelectors) {
resultElements = document.querySelectorAll(selector);
if (resultElements.length > 0) break;
}
// Si pas de sélecteur spécifique, chercher dans le contenu général
if (resultElements.length === 0) {
const content = document.body.textContent || '';
if (content.includes('gel des avoirs') || content.includes('GEL DES AVOIRS')) {
// Résultat générique si on trouve des mentions
results.push({
name: `${firstName} ${lastName}`.trim(),
type: 'gel-avoirs',
date: new Date().toISOString().split('T')[0],
sourceUrl: BODACC_BASE_URL,
matchScore: 0.7,
description: 'Mention de gel des avoirs détectée dans les résultats Bodacc'
});
}
} else {
// Traitement des éléments de résultats spécifiques
resultElements.forEach((element, index) => {
try {
const nameElement = element.querySelector('.name, .nom, .person-name, h3, h4');
const dateElement = element.querySelector('.date, .publication-date, .date-publication');
const linkElement = element.querySelector('a[href]');
const name = nameElement ? nameElement.textContent.trim() : `${firstName} ${lastName}`.trim();
const date = dateElement ? dateElement.textContent.trim() : new Date().toISOString().split('T')[0];
const sourceUrl = linkElement ? new URL(linkElement.href, BODACC_BASE_URL).href : BODACC_BASE_URL;
// Calcul du score de correspondance basique
const matchScore = calculateMatchScore(name, lastName, firstName);
if (matchScore > 0.3) { // Seuil minimum de correspondance
results.push({
name,
type: 'gel-avoirs',
date,
sourceUrl,
matchScore,
description: `Résultat ${index + 1} de gel des avoirs sur Bodacc`
});
}
} catch (elementError) {
console.warn(`[Bodacc] Erreur traitement élément ${index}:`, elementError.message);
}
});
}
} catch (error) {
console.warn(`[Bodacc] Erreur extraction résultats:`, error.message);
}
return results;
}
/**
* Calcule un score de correspondance entre le nom trouvé et le nom recherché
* @param {string} foundName - Nom trouvé dans les résultats
* @param {string} lastName - Nom recherché
* @param {string} firstName - Prénom recherché
* @returns {number} Score entre 0 et 1
*/
function calculateMatchScore(foundName, lastName, firstName) {
const found = foundName.toLowerCase();
const last = lastName.toLowerCase();
const first = firstName.toLowerCase();
let score = 0;
// Correspondance exacte du nom de famille
if (found.includes(last)) {
score += 0.6;
}
// Correspondance du prénom si fourni
if (first && found.includes(first)) {
score += 0.4;
}
// Bonus pour correspondance exacte
if (found === `${first} ${last}`.trim().toLowerCase()) {
score = 1.0;
}
return Math.min(score, 1.0);
}
module.exports = {
searchBodaccGelAvoirs,
generateBodaccSummary
};
/**
* Génère un résumé des résultats pour le PDF
* @param {Array} results - Résultats de la recherche
* @param {string} lastName - Nom recherché
* @param {string} firstName - Prénom recherché
* @returns {Object} Résumé formaté
*/
function generateBodaccSummary(results, lastName, firstName) {
const totalResults = results.length;
const highConfidenceResults = results.filter(r => r.matchScore > 0.7);
const recentResults = results.filter(r => {
const resultDate = new Date(r.date);
const oneYearAgo = new Date();
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
return resultDate > oneYearAgo;
});
return {
searchTarget: `${firstName} ${lastName}`.trim(),
totalResults,
highConfidenceResults: highConfidenceResults.length,
recentResults: recentResults.length,
hasGelAvoirs: totalResults > 0,
riskLevel: totalResults === 0 ? 'Aucun' :
highConfidenceResults.length > 0 ? 'Élevé' :
totalResults > 0 ? 'Moyen' : 'Faible',
summary: totalResults === 0 ?
'Aucune mention de gel des avoirs trouvée sur Bodacc' :
`${totalResults} mention(s) trouvée(s), ${highConfidenceResults.length} avec haute confiance`
};
}

View File

@ -0,0 +1,430 @@
/**
* Collecteur Inforgreffe/Societe.com - Informations entreprises
* Scraping léger avec politesse pour récupérer les données d'entreprises
*/
const fetch = require('node-fetch');
const { JSDOM } = require('jsdom');
const SOCIETE_COM_BASE_URL = 'https://www.societe.com';
const INFORGREFFE_BASE_URL = 'https://www.inforgreffe.com';
const USER_AGENT = '4NK-IA-Front/1.0 (Document Analysis Tool)';
const REQUEST_DELAY_MS = 1500; // 1.5 secondes entre les requêtes
const REQUEST_TIMEOUT_MS = 12000; // 12 secondes timeout
/**
* Recherche une entreprise sur Societe.com et Inforgreffe
* @param {string} companyName - Nom de l'entreprise
* @param {string} siren - SIREN (optionnel)
* @returns {Promise<Object>} Résultat de la recherche
*/
async function searchCompanyInfo(companyName, siren = '') {
const startTime = Date.now();
try {
console.log(`[Inforgreffe] Recherche entreprise: ${companyName} ${siren ? `(SIREN: ${siren})` : ''}`);
// Recherche sur Societe.com d'abord (plus accessible)
const societeComResult = await searchSocieteCom(companyName, siren);
// Délai de politesse
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
// Recherche sur Inforgreffe si SIREN disponible
let inforgreffeResult = null;
if (siren) {
inforgreffeResult = await searchInforgreffe(siren);
await new Promise(resolve => setTimeout(resolve, REQUEST_DELAY_MS));
}
// Fusion des résultats
const mergedResult = mergeCompanyResults(societeComResult, inforgreffeResult, companyName);
const duration = Date.now() - startTime;
console.log(`[Inforgreffe] Recherche terminée en ${duration}ms`);
return {
success: true,
duration,
company: mergedResult,
sources: {
societeCom: societeComResult,
inforgreffe: inforgreffeResult
},
timestamp: new Date().toISOString()
};
} catch (error) {
const duration = Date.now() - startTime;
console.error(`[Inforgreffe] Erreur recherche:`, error.message);
return {
success: false,
duration,
error: error.message,
company: null,
sources: {},
timestamp: new Date().toISOString()
};
}
}
/**
* Recherche sur Societe.com
* @param {string} companyName - Nom de l'entreprise
* @param {string} siren - SIREN (optionnel)
* @returns {Promise<Object>} Résultat Societe.com
*/
async function searchSocieteCom(companyName, siren = '') {
try {
// Construction de l'URL de recherche
const searchQuery = siren || companyName;
const searchUrl = `${SOCIETE_COM_BASE_URL}/search?q=${encodeURIComponent(searchQuery)}`;
const response = await fetch(searchUrl, {
method: 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
},
timeout: REQUEST_TIMEOUT_MS
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const html = await response.text();
const dom = new JSDOM(html);
const document = dom.window.document;
return extractSocieteComData(document, companyName, siren);
} catch (error) {
console.warn(`[Societe.com] Erreur:`, error.message);
return {
success: false,
error: error.message,
data: null
};
}
}
/**
* Recherche sur Inforgreffe
* @param {string} siren - SIREN de l'entreprise
* @returns {Promise<Object>} Résultat Inforgreffe
*/
async function searchInforgreffe(siren) {
try {
// URL de recherche par SIREN
const searchUrl = `${INFORGREFFE_BASE_URL}/entreprise/${siren}`;
const response = await fetch(searchUrl, {
method: 'GET',
headers: {
'User-Agent': USER_AGENT,
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'fr-FR,fr;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
},
timeout: REQUEST_TIMEOUT_MS
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const html = await response.text();
const dom = new JSDOM(html);
const document = dom.window.document;
return extractInforgreffeData(document, siren);
} catch (error) {
console.warn(`[Inforgreffe] Erreur:`, error.message);
return {
success: false,
error: error.message,
data: null
};
}
}
/**
* Extrait les données de Societe.com
* @param {Document} document - Document DOM parsé
* @param {string} companyName - Nom de l'entreprise
* @param {string} siren - SIREN
* @returns {Object} Données extraites
*/
function extractSocieteComData(document, companyName, siren) {
try {
const data = {
name: companyName,
siren: siren,
siret: '',
forme: '',
capital: '',
adresse: '',
dirigeants: [],
activite: '',
dateCreation: '',
sourceUrl: SOCIETE_COM_BASE_URL
};
// Extraction du nom de l'entreprise
const nameElement = document.querySelector('.company-name, .nom-entreprise, h1, .title');
if (nameElement) {
data.name = nameElement.textContent.trim();
}
// Extraction du SIREN/SIRET
const sirenElement = document.querySelector('.siren, .num-siren, [data-siren]');
if (sirenElement) {
const sirenText = sirenElement.textContent || sirenElement.getAttribute('data-siren');
data.siren = extractSiren(sirenText);
}
// Extraction de la forme juridique
const formeElement = document.querySelector('.forme, .forme-juridique, .legal-form');
if (formeElement) {
data.forme = formeElement.textContent.trim();
}
// Extraction du capital
const capitalElement = document.querySelector('.capital, .capital-social, .share-capital');
if (capitalElement) {
data.capital = capitalElement.textContent.trim();
}
// Extraction de l'adresse
const adresseElement = document.querySelector('.adresse, .address, .company-address');
if (adresseElement) {
data.adresse = adresseElement.textContent.trim();
}
// Extraction des dirigeants
const dirigeantsElements = document.querySelectorAll('.dirigeant, .manager, .president, .gérant');
dirigeantsElements.forEach(element => {
const name = element.textContent.trim();
if (name && name.length > 2) {
data.dirigeants.push({
nom: name,
fonction: 'Dirigeant',
source: 'societe.com'
});
}
});
// Extraction de l'activité
const activiteElement = document.querySelector('.activite, .activity, .secteur');
if (activiteElement) {
data.activite = activiteElement.textContent.trim();
}
// Extraction de la date de création
const dateElement = document.querySelector('.date-creation, .creation-date, .date-creation-entreprise');
if (dateElement) {
data.dateCreation = dateElement.textContent.trim();
}
return {
success: true,
data
};
} catch (error) {
console.warn(`[Societe.com] Erreur extraction:`, error.message);
return {
success: false,
error: error.message,
data: null
};
}
}
/**
* Extrait les données d'Inforgreffe
* @param {Document} document - Document DOM parsé
* @param {string} siren - SIREN
* @returns {Object} Données extraites
*/
function extractInforgreffeData(document, siren) {
try {
const data = {
siren: siren,
name: '',
siret: '',
forme: '',
capital: '',
adresse: '',
dirigeants: [],
activite: '',
dateCreation: '',
sourceUrl: `${INFORGREFFE_BASE_URL}/entreprise/${siren}`
};
// Extraction du nom de l'entreprise
const nameElement = document.querySelector('.company-name, .nom-entreprise, h1, .title');
if (nameElement) {
data.name = nameElement.textContent.trim();
}
// Extraction du SIRET
const siretElement = document.querySelector('.siret, .num-siret, [data-siret]');
if (siretElement) {
data.siret = siretElement.textContent.trim();
}
// Extraction de la forme juridique
const formeElement = document.querySelector('.forme, .forme-juridique, .legal-form');
if (formeElement) {
data.forme = formeElement.textContent.trim();
}
// Extraction du capital
const capitalElement = document.querySelector('.capital, .capital-social, .share-capital');
if (capitalElement) {
data.capital = capitalElement.textContent.trim();
}
// Extraction de l'adresse
const adresseElement = document.querySelector('.adresse, .address, .company-address');
if (adresseElement) {
data.adresse = adresseElement.textContent.trim();
}
// Extraction des dirigeants
const dirigeantsElements = document.querySelectorAll('.dirigeant, .manager, .president, .gérant');
dirigeantsElements.forEach(element => {
const name = element.textContent.trim();
if (name && name.length > 2) {
data.dirigeants.push({
nom: name,
fonction: 'Dirigeant',
source: 'inforgreffe.com'
});
}
});
return {
success: true,
data
};
} catch (error) {
console.warn(`[Inforgreffe] Erreur extraction:`, error.message);
return {
success: false,
error: error.message,
data: null
};
}
}
/**
* Fusionne les résultats de Societe.com et Inforgreffe
* @param {Object} societeComResult - Résultat Societe.com
* @param {Object} inforgreffeResult - Résultat Inforgreffe
* @param {string} originalName - Nom original de l'entreprise
* @returns {Object} Données fusionnées
*/
function mergeCompanyResults(societeComResult, inforgreffeResult, originalName) {
const merged = {
name: originalName,
siren: '',
siret: '',
forme: '',
capital: '',
adresse: '',
dirigeants: [],
activite: '',
dateCreation: '',
sources: []
};
// Fusion des données Societe.com
if (societeComResult.success && societeComResult.data) {
const sc = societeComResult.data;
merged.name = sc.name || merged.name;
merged.siren = sc.siren || merged.siren;
merged.siret = sc.siret || merged.siret;
merged.forme = sc.forme || merged.forme;
merged.capital = sc.capital || merged.capital;
merged.adresse = sc.adresse || merged.adresse;
merged.activite = sc.activite || merged.activite;
merged.dateCreation = sc.dateCreation || merged.dateCreation;
merged.dirigeants.push(...sc.dirigeants);
merged.sources.push('societe.com');
}
// Fusion des données Inforgreffe
if (inforgreffeResult && inforgreffeResult.success && inforgreffeResult.data) {
const ig = inforgreffeResult.data;
merged.name = ig.name || merged.name;
merged.siren = ig.siren || merged.siren;
merged.siret = ig.siret || merged.siret;
merged.forme = ig.forme || merged.forme;
merged.capital = ig.capital || merged.capital;
merged.adresse = ig.adresse || merged.adresse;
merged.dateCreation = ig.dateCreation || merged.dateCreation;
merged.dirigeants.push(...ig.dirigeants);
merged.sources.push('inforgreffe.com');
}
// Déduplication des dirigeants
merged.dirigeants = merged.dirigeants.filter((dirigeant, index, self) =>
index === self.findIndex(d => d.nom === dirigeant.nom)
);
return merged;
}
/**
* Extrait le SIREN d'un texte
* @param {string} text - Texte contenant potentiellement un SIREN
* @returns {string} SIREN extrait
*/
function extractSiren(text) {
if (!text) return '';
// Recherche d'un SIREN (9 chiffres)
const sirenMatch = text.match(/\b(\d{9})\b/);
return sirenMatch ? sirenMatch[1] : '';
}
/**
* Génère un résumé des données d'entreprise pour le PDF
* @param {Object} companyData - Données de l'entreprise
* @returns {Object} Résumé formaté
*/
function generateCompanySummary(companyData) {
return {
name: companyData.name,
siren: companyData.siren,
siret: companyData.siret,
forme: companyData.forme,
capital: companyData.capital,
adresse: companyData.adresse,
dirigeantsCount: companyData.dirigeants.length,
activite: companyData.activite,
dateCreation: companyData.dateCreation,
sources: companyData.sources,
hasCompleteInfo: !!(companyData.siren && companyData.forme && companyData.adresse),
summary: companyData.siren ?
`Entreprise trouvée: ${companyData.name} (SIREN: ${companyData.siren})` :
`Informations partielles pour: ${companyData.name}`
};
}
module.exports = {
searchCompanyInfo,
generateCompanySummary
};

View File

@ -0,0 +1,366 @@
/**
* Générateur de PDF pour les entités enrichies
* Génère des PDF formatés pour les personnes, adresses et entreprises
*/
const fs = require('fs').promises;
const path = require('path');
/**
* Génère un PDF pour une personne (Bodacc - gel des avoirs)
* @param {Object} personData - Données de la personne
* @param {Object} bodaccResult - Résultat de la recherche Bodacc
* @param {string} outputPath - Chemin de sortie du PDF
* @returns {Promise<string>} Chemin du PDF généré
*/
async function generatePersonPdf(personData, bodaccResult, outputPath) {
try {
const pdfContent = generatePersonPdfContent(personData, bodaccResult);
await fs.writeFile(outputPath, pdfContent, 'utf8');
console.log(`[PDF] Personne généré: ${outputPath}`);
return outputPath;
} catch (error) {
console.error(`[PDF] Erreur génération personne:`, error.message);
throw error;
}
}
/**
* Génère un PDF pour une entreprise (Inforgreffe/Societe.com)
* @param {Object} companyData - Données de l'entreprise
* @param {Object} inforgreffeResult - Résultat de la recherche Inforgreffe
* @param {string} outputPath - Chemin de sortie du PDF
* @returns {Promise<string>} Chemin du PDF généré
*/
async function generateCompanyPdf(companyData, inforgreffeResult, outputPath) {
try {
const pdfContent = generateCompanyPdfContent(companyData, inforgreffeResult);
await fs.writeFile(outputPath, pdfContent, 'utf8');
console.log(`[PDF] Entreprise généré: ${outputPath}`);
return outputPath;
} catch (error) {
console.error(`[PDF] Erreur génération entreprise:`, error.message);
throw error;
}
}
/**
* Génère un PDF pour une adresse (Cadastre/GéoRisque)
* @param {Object} addressData - Données de l'adresse
* @param {Object} geoResult - Résultat de la recherche géographique
* @param {string} outputPath - Chemin de sortie du PDF
* @returns {Promise<string>} Chemin du PDF généré
*/
async function generateAddressPdf(addressData, geoResult, outputPath) {
try {
const pdfContent = generateAddressPdfContent(addressData, geoResult);
await fs.writeFile(outputPath, pdfContent, 'utf8');
console.log(`[PDF] Adresse généré: ${outputPath}`);
return outputPath;
} catch (error) {
console.error(`[PDF] Erreur génération adresse:`, error.message);
throw error;
}
}
/**
* Génère le contenu HTML pour le PDF d'une personne
* @param {Object} personData - Données de la personne
* @param {Object} bodaccResult - Résultat Bodacc
* @returns {string} Contenu HTML
*/
function generatePersonPdfContent(personData, bodaccResult) {
const summary = bodaccResult.summary || {};
const results = bodaccResult.results || [];
return `
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rapport Bodacc - ${personData.firstName} ${personData.lastName}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; line-height: 1.6; }
.header { background: #f5f5f5; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
.title { color: #333; margin: 0; }
.subtitle { color: #666; margin: 5px 0 0 0; }
.section { margin: 20px 0; }
.section h2 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 5px; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.info-item { background: #f9f9f9; padding: 15px; border-radius: 5px; }
.info-label { font-weight: bold; color: #555; }
.info-value { margin-top: 5px; }
.risk-high { color: #e74c3c; font-weight: bold; }
.risk-medium { color: #f39c12; font-weight: bold; }
.risk-low { color: #27ae60; font-weight: bold; }
.result-item { background: #fff; border: 1px solid #ddd; padding: 15px; margin: 10px 0; border-radius: 5px; }
.result-date { color: #666; font-size: 0.9em; }
.result-score { background: #3498db; color: white; padding: 2px 8px; border-radius: 3px; font-size: 0.8em; }
.footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 0.9em; }
.no-results { text-align: center; color: #27ae60; font-style: italic; padding: 20px; }
</style>
</head>
<body>
<div class="header">
<h1 class="title">Rapport Bodacc - Gel des avoirs</h1>
<p class="subtitle">Généré le ${new Date().toLocaleDateString('fr-FR')} à ${new Date().toLocaleTimeString('fr-FR')}</p>
</div>
<div class="section">
<h2>Identité recherchée</h2>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Nom complet</div>
<div class="info-value">${personData.firstName} ${personData.lastName}</div>
</div>
<div class="info-item">
<div class="info-label">Date de naissance</div>
<div class="info-value">${personData.birthDate || 'Non renseignée'}</div>
</div>
</div>
</div>
<div class="section">
<h2>Résumé de la recherche</h2>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Statut</div>
<div class="info-value class="risk-${summary.riskLevel?.toLowerCase() || 'low'}">${summary.riskLevel || 'Faible'}</div>
</div>
<div class="info-item">
<div class="info-label">Résultats trouvés</div>
<div class="info-value">${summary.totalResults || 0}</div>
</div>
<div class="info-item">
<div class="info-label">Haute confiance</div>
<div class="info-value">${summary.highConfidenceResults || 0}</div>
</div>
<div class="info-item">
<div class="info-label">Récents (1 an)</div>
<div class="info-value">${summary.recentResults || 0}</div>
</div>
</div>
<p><strong>Synthèse:</strong> ${summary.summary || 'Aucune donnée disponible'}</p>
</div>
<div class="section">
<h2>Détail des résultats</h2>
${results.length === 0 ?
'<div class="no-results">✅ Aucune mention de gel des avoirs trouvée</div>' :
results.map(result => `
<div class="result-item">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h3 style="margin: 0;">${result.name}</h3>
<span class="result-score">Score: ${(result.matchScore * 100).toFixed(0)}%</span>
</div>
<div class="result-date">Date: ${result.date}</div>
<p><strong>Description:</strong> ${result.description}</p>
<p><strong>Source:</strong> <a href="${result.sourceUrl}" target="_blank">${result.sourceUrl}</a></p>
</div>
`).join('')
}
</div>
<div class="footer">
<p><strong>Source:</strong> ${bodaccResult.source || 'Bodacc.fr'}</p>
<p><strong>Recherche effectuée le:</strong> ${bodaccResult.timestamp ? new Date(bodaccResult.timestamp).toLocaleString('fr-FR') : 'Non disponible'}</p>
<p><strong>Durée de la recherche:</strong> ${bodaccResult.duration || 0}ms</p>
<p><em>Ce rapport a été généré automatiquement par 4NK IA Front. Les informations sont fournies à titre indicatif.</em></p>
</div>
</body>
</html>`;
}
/**
* Génère le contenu HTML pour le PDF d'une entreprise
* @param {Object} companyData - Données de l'entreprise
* @param {Object} inforgreffeResult - Résultat Inforgreffe
* @returns {string} Contenu HTML
*/
function generateCompanyPdfContent(companyData, inforgreffeResult) {
const company = inforgreffeResult.company || {};
const sources = inforgreffeResult.sources || {};
return `
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rapport Entreprise - ${company.name || companyData.name}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; line-height: 1.6; }
.header { background: #f5f5f5; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
.title { color: #333; margin: 0; }
.subtitle { color: #666; margin: 5px 0 0 0; }
.section { margin: 20px 0; }
.section h2 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 5px; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.info-item { background: #f9f9f9; padding: 15px; border-radius: 5px; }
.info-label { font-weight: bold; color: #555; }
.info-value { margin-top: 5px; }
.status-ok { color: #27ae60; font-weight: bold; }
.status-partial { color: #f39c12; font-weight: bold; }
.status-missing { color: #e74c3c; font-weight: bold; }
.dirigeant-item { background: #fff; border: 1px solid #ddd; padding: 10px; margin: 5px 0; border-radius: 5px; }
.footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 0.9em; }
</style>
</head>
<body>
<div class="header">
<h1 class="title">Rapport Entreprise</h1>
<p class="subtitle">Généré le ${new Date().toLocaleDateString('fr-FR')} à ${new Date().toLocaleTimeString('fr-FR')}</p>
</div>
<div class="section">
<h2>Informations générales</h2>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Raison sociale</div>
<div class="info-value">${company.name || companyData.name || 'Non renseignée'}</div>
</div>
<div class="info-item">
<div class="info-label">SIREN</div>
<div class="info-value class="status-${company.siren ? 'ok' : 'missing'}">${company.siren || 'Non trouvé'}</div>
</div>
<div class="info-item">
<div class="info-label">SIRET</div>
<div class="info-value class="status-${company.siret ? 'ok' : 'missing'}">${company.siret || 'Non trouvé'}</div>
</div>
<div class="info-item">
<div class="info-label">Forme juridique</div>
<div class="info-value class="status-${company.forme ? 'ok' : 'missing'}">${company.forme || 'Non renseignée'}</div>
</div>
<div class="info-item">
<div class="info-label">Capital social</div>
<div class="info-value class="status-${company.capital ? 'ok' : 'missing'}">${company.capital || 'Non renseigné'}</div>
</div>
<div class="info-item">
<div class="info-label">Date de création</div>
<div class="info-value class="status-${company.dateCreation ? 'ok' : 'missing'}">${company.dateCreation || 'Non renseignée'}</div>
</div>
</div>
</div>
<div class="section">
<h2>Adresse</h2>
<div class="info-item">
<div class="info-label">Adresse complète</div>
<div class="info-value class="status-${company.adresse ? 'ok' : 'missing'}">${company.adresse || 'Non renseignée'}</div>
</div>
</div>
<div class="section">
<h2>Activité</h2>
<div class="info-item">
<div class="info-label">Secteur d'activité</div>
<div class="info-value class="status-${company.activite ? 'ok' : 'missing'}">${company.activite || 'Non renseignée'}</div>
</div>
</div>
<div class="section">
<h2>Dirigeants (${company.dirigeants?.length || 0})</h2>
${company.dirigeants && company.dirigeants.length > 0 ?
company.dirigeants.map(dirigeant => `
<div class="dirigeant-item">
<strong>${dirigeant.nom}</strong> - ${dirigeant.fonction || 'Dirigeant'}
<br><small>Source: ${dirigeant.source || 'Non spécifiée'}</small>
</div>
`).join('') :
'<p class="status-missing">Aucun dirigeant trouvé</p>'
}
</div>
<div class="section">
<h2>Sources consultées</h2>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Societe.com</div>
<div class="info-value class="status-${sources.societeCom?.success ? 'ok' : 'missing'}">${sources.societeCom?.success ? '✅ Consulté' : '❌ Non accessible'}</div>
</div>
<div class="info-item">
<div class="info-label">Inforgreffe</div>
<div class="info-value class="status-${sources.inforgreffe?.success ? 'ok' : 'missing'}">${sources.inforgreffe?.success ? '✅ Consulté' : '❌ Non accessible'}</div>
</div>
</div>
</div>
<div class="footer">
<p><strong>Recherche effectuée le:</strong> ${inforgreffeResult.timestamp ? new Date(inforgreffeResult.timestamp).toLocaleString('fr-FR') : 'Non disponible'}</p>
<p><strong>Durée de la recherche:</strong> ${inforgreffeResult.duration || 0}ms</p>
<p><em>Ce rapport a été généré automatiquement par 4NK IA Front. Les informations sont fournies à titre indicatif.</em></p>
</div>
</body>
</html>`;
}
/**
* Génère le contenu HTML pour le PDF d'une adresse
* @param {Object} addressData - Données de l'adresse
* @param {Object} geoResult - Résultat géographique
* @returns {string} Contenu HTML
*/
function generateAddressPdfContent(addressData, geoResult) {
return `
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rapport Adresse - ${addressData.street || 'Adresse'}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; line-height: 1.6; }
.header { background: #f5f5f5; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
.title { color: #333; margin: 0; }
.subtitle { color: #666; margin: 5px 0 0 0; }
.section { margin: 20px 0; }
.section h2 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 5px; }
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.info-item { background: #f9f9f9; padding: 15px; border-radius: 5px; }
.info-label { font-weight: bold; color: #555; }
.info-value { margin-top: 5px; }
.footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 0.9em; }
.placeholder { text-align: center; color: #666; font-style: italic; padding: 40px; }
</style>
</head>
<body>
<div class="header">
<h1 class="title">Rapport Adresse</h1>
<p class="subtitle">Généré le ${new Date().toLocaleDateString('fr-FR')} à ${new Date().toLocaleTimeString('fr-FR')}</p>
</div>
<div class="section">
<h2>Adresse analysée</h2>
<div class="info-item">
<div class="info-label">Adresse complète</div>
<div class="info-value">${addressData.street || ''} ${addressData.postalCode || ''} ${addressData.city || ''} ${addressData.country || ''}</div>
</div>
</div>
<div class="section">
<div class="placeholder">
<h2>🚧 En cours de développement</h2>
<p>Les fonctionnalités de géocodage et d'analyse cadastrale seront bientôt disponibles.</p>
<p>Ce rapport inclura :</p>
<ul style="text-align: left; display: inline-block;">
<li>Géocodage de l'adresse</li>
<li>Informations cadastrales</li>
<li>Risques majeurs (GéoRisque)</li>
<li>Données GéoFoncier</li>
</ul>
</div>
</div>
<div class="footer">
<p><em>Ce rapport a été généré automatiquement par 4NK IA Front.</em></p>
</div>
</body>
</html>`;
}
module.exports = {
generatePersonPdf,
generateCompanyPdf,
generateAddressPdf
};

View File

@ -16,6 +16,11 @@ const { preprocessImageForOCR, analyzeImageMetadata } = require('./imagePreproce
const { nameConfidenceBoost } = require('./nameDirectory')
const pdf = require('pdf-parse')
// Collecteurs d'enrichissement
const { searchBodaccGelAvoirs, generateBodaccSummary } = require('./collectors/bodaccCollector')
const { searchCompanyInfo, generateCompanySummary } = require('./collectors/inforgreffeCollector')
const { generatePersonPdf, generateCompanyPdf, generateAddressPdf } = require('./collectors/pdfGenerator')
const app = express()
const PORT = process.env.PORT || 3001
@ -2574,9 +2579,9 @@ app.get('/api/health', (req, res) => {
})
})
// Enrichissement asynchrone des entités (squelette)
// Enrichissement asynchrone des entités avec collecteurs réels
// Démarre une collecte et enregistre un statut côté cache
app.post('/api/folders/:folderHash/files/:fileHash/enrich/:kind', (req, res) => {
app.post('/api/folders/:folderHash/files/:fileHash/enrich/:kind', async (req, res) => {
try {
const { folderHash, fileHash, kind } = req.params
if (!['person', 'address', 'company'].includes(kind)) {
@ -2598,30 +2603,120 @@ app.post('/api/folders/:folderHash/files/:fileHash/enrich/:kind', (req, res) =>
}
fs.writeFileSync(statusPath, JSON.stringify(status, null, 2))
// Simuler une collecte asynchrone courte
setTimeout(() => {
res.json({ success: true, message: 'Enrichissement démarré' })
// Enrichissement asynchrone selon le type
setTimeout(async () => {
try {
let result = null
let pdfGenerated = false
if (kind === 'person') {
// Recherche Bodacc pour les personnes
const cacheFile = path.join(cachePath, `${fileHash}.json`)
if (fs.existsSync(cacheFile)) {
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'))
const persons = cacheData.entities?.persons || []
for (const person of persons) {
if (person.lastName) {
console.log(`[Enrich] Recherche Bodacc pour: ${person.firstName} ${person.lastName}`)
result = await searchBodaccGelAvoirs(person.lastName, person.firstName)
if (result.success) {
const summary = generateBodaccSummary(result.results, person.lastName, person.firstName)
result.summary = summary
// Génération du PDF
try {
await generatePersonPdf(person, result, pdfPath)
pdfGenerated = true
} catch (pdfError) {
console.warn(`[Enrich] Erreur génération PDF personne:`, pdfError.message)
}
}
break // Traiter seulement la première personne trouvée
}
}
}
} else if (kind === 'company') {
// Recherche Inforgreffe pour les entreprises
const cacheFile = path.join(cachePath, `${fileHash}.json`)
if (fs.existsSync(cacheFile)) {
const cacheData = JSON.parse(fs.readFileSync(cacheFile, 'utf8'))
const companies = cacheData.entities?.companies || []
for (const company of companies) {
if (company.name) {
console.log(`[Enrich] Recherche Inforgreffe pour: ${company.name}`)
result = await searchCompanyInfo(company.name, company.siren)
if (result.success) {
// Génération du PDF
try {
await generateCompanyPdf(company, result, pdfPath)
pdfGenerated = true
} catch (pdfError) {
console.warn(`[Enrich] Erreur génération PDF entreprise:`, pdfError.message)
}
}
break // Traiter seulement la première entreprise trouvée
}
}
}
} else if (kind === 'address') {
// Placeholder pour les adresses (géocodage à implémenter)
result = {
success: true,
duration: 1000,
message: 'Enrichissement adresse en cours de développement',
data: { placeholder: true }
}
// Génération du PDF placeholder
try {
const addressData = { street: 'Adresse', city: 'Ville' }
await generateAddressPdf(addressData, result, pdfPath)
pdfGenerated = true
} catch (pdfError) {
console.warn(`[Enrich] Erreur génération PDF adresse:`, pdfError.message)
}
}
// Mise à jour du statut final
const done = {
...status,
state: 'done',
kind,
state: result?.success ? 'done' : 'error',
startedAt: status.startedAt,
finishedAt: new Date().toISOString(),
message: 'Collecte terminée',
sources: (kind === 'person')
? ['bodacc_gel_avoirs']
: (kind === 'company')
? ['kbis_inforgreffe', 'societe_com']
: ['cadastre', 'georisque', 'geofoncier'],
message: result?.success ? 'Collecte terminée' : 'Erreur de collecte',
sources: result?.sources ? Object.keys(result.sources) :
(kind === 'person') ? ['bodacc_gel_avoirs'] :
(kind === 'company') ? ['kbis_inforgreffe', 'societe_com'] :
['cadastre', 'georisque', 'geofoncier'],
data: result,
pdfGenerated
}
fs.writeFileSync(statusPath, JSON.stringify(done, null, 2))
// Générer un PDF minimal (texte) pour preuve de concept
try {
const content = `Dossier d'enrichissement\nKind: ${kind}\nFichier: ${fileHash}\nSources: ${done.sources.join(', ')}\nDate: ${new Date().toISOString()}\n`
fs.writeFileSync(pdfPath, content)
} catch {}
} catch {}
}, 1500)
console.log(`[Enrich] Terminé pour ${kind}:`, done.state)
} catch (error) {
console.error(`[Enrich] Erreur enrichissement ${kind}:`, error.message)
const errorStatus = {
kind,
state: 'error',
startedAt: status.startedAt,
finishedAt: new Date().toISOString(),
message: `Erreur: ${error.message}`,
sources: [],
error: error.message
}
fs.writeFileSync(statusPath, JSON.stringify(errorStatus, null, 2))
}
}, 2000) // Délai de 2 secondes pour laisser le temps aux collecteurs
return res.json({ success: true })
} catch (e) {
return res.status(500).json({ success: false, error: e?.message || String(e) })
}