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:
parent
39c452002a
commit
8e3daad446
235
backend/collectors/bodaccCollector.js
Normal file
235
backend/collectors/bodaccCollector.js
Normal 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`
|
||||||
|
};
|
||||||
|
}
|
||||||
430
backend/collectors/inforgreffeCollector.js
Normal file
430
backend/collectors/inforgreffeCollector.js
Normal 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
|
||||||
|
};
|
||||||
366
backend/collectors/pdfGenerator.js
Normal file
366
backend/collectors/pdfGenerator.js
Normal 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
|
||||||
|
};
|
||||||
@ -16,6 +16,11 @@ const { preprocessImageForOCR, analyzeImageMetadata } = require('./imagePreproce
|
|||||||
const { nameConfidenceBoost } = require('./nameDirectory')
|
const { nameConfidenceBoost } = require('./nameDirectory')
|
||||||
const pdf = require('pdf-parse')
|
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 app = express()
|
||||||
const PORT = process.env.PORT || 3001
|
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
|
// 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 {
|
try {
|
||||||
const { folderHash, fileHash, kind } = req.params
|
const { folderHash, fileHash, kind } = req.params
|
||||||
if (!['person', 'address', 'company'].includes(kind)) {
|
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))
|
fs.writeFileSync(statusPath, JSON.stringify(status, null, 2))
|
||||||
|
|
||||||
// Simuler une collecte asynchrone courte
|
res.json({ success: true, message: 'Enrichissement démarré' })
|
||||||
setTimeout(() => {
|
|
||||||
|
// Enrichissement asynchrone selon le type
|
||||||
|
setTimeout(async () => {
|
||||||
try {
|
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 = {
|
const done = {
|
||||||
...status,
|
kind,
|
||||||
state: 'done',
|
state: result?.success ? 'done' : 'error',
|
||||||
|
startedAt: status.startedAt,
|
||||||
finishedAt: new Date().toISOString(),
|
finishedAt: new Date().toISOString(),
|
||||||
message: 'Collecte terminée',
|
message: result?.success ? 'Collecte terminée' : 'Erreur de collecte',
|
||||||
sources: (kind === 'person')
|
sources: result?.sources ? Object.keys(result.sources) :
|
||||||
? ['bodacc_gel_avoirs']
|
(kind === 'person') ? ['bodacc_gel_avoirs'] :
|
||||||
: (kind === 'company')
|
(kind === 'company') ? ['kbis_inforgreffe', 'societe_com'] :
|
||||||
? ['kbis_inforgreffe', 'societe_com']
|
['cadastre', 'georisque', 'geofoncier'],
|
||||||
: ['cadastre', 'georisque', 'geofoncier'],
|
data: result,
|
||||||
|
pdfGenerated
|
||||||
}
|
}
|
||||||
fs.writeFileSync(statusPath, JSON.stringify(done, null, 2))
|
fs.writeFileSync(statusPath, JSON.stringify(done, null, 2))
|
||||||
// Générer un PDF minimal (texte) pour preuve de concept
|
|
||||||
try {
|
console.log(`[Enrich] Terminé pour ${kind}:`, done.state)
|
||||||
const content = `Dossier d'enrichissement\nKind: ${kind}\nFichier: ${fileHash}\nSources: ${done.sources.join(', ')}\nDate: ${new Date().toISOString()}\n`
|
|
||||||
fs.writeFileSync(pdfPath, content)
|
} catch (error) {
|
||||||
} catch {}
|
console.error(`[Enrich] Erreur enrichissement ${kind}:`, error.message)
|
||||||
} catch {}
|
|
||||||
}, 1500)
|
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) {
|
} catch (e) {
|
||||||
return res.status(500).json({ success: false, error: e?.message || String(e) })
|
return res.status(500).json({ success: false, error: e?.message || String(e) })
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user