| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162 |
- const pdfParse = require('pdf-parse');
- const fs = require('fs');
- // Known suppliers with their aliases and associated charge categories
- const SUPPLIERS = [
- { name: 'EDF', patterns: [/\bedf\b/i, /\bélectricité de france\b/i], category: 'electricite' },
- { name: 'Engie', patterns: [/\bengie\b/i, /\bgdf\s*suez\b/i], category: 'gaz' },
- { name: 'TotalEnergies', patterns: [/\btotal\s*energies?\b/i, /\btotal\s*direct\s*energie\b/i], category: 'electricite' },
- { name: 'Eni', patterns: [/\beni\s/i, /\beni\.com\b/i], category: 'gaz' },
- { name: 'Veolia', patterns: [/\bveolia\b/i], category: 'eau' },
- { name: 'Suez', patterns: [/\bsuez\b/i, /\blyonnaise des eaux\b/i], category: 'eau' },
- { name: 'Saur', patterns: [/\bsaur\b/i], category: 'eau' },
- { name: 'Eau de Paris', patterns: [/\beau de paris\b/i], category: 'eau' },
- { name: 'GRDF', patterns: [/\bgrdf\b/i], category: 'gaz' },
- { name: 'Enedis', patterns: [/\benedis\b/i, /\berdf\b/i], category: 'electricite' },
- { name: 'Orange', patterns: [/\borange\b/i, /\bfrance\s*t[eé]l[eé]com\b/i], category: 'autre' },
- { name: 'SFR', patterns: [/\bsfr\b/i], category: 'autre' },
- { name: 'Free', patterns: [/\bfree\s*mobile\b/i, /\bfree\.fr\b/i, /\biliad\b/i], category: 'autre' },
- { name: 'Bouygues Telecom', patterns: [/\bbouygues\s*tel/i], category: 'autre' },
- { name: 'Axa', patterns: [/\baxa\b/i], category: 'assurance' },
- { name: 'Maif', patterns: [/\bmaif\b/i], category: 'assurance' },
- { name: 'MACIF', patterns: [/\bmacif\b/i], category: 'assurance' },
- { name: 'Groupama', patterns: [/\bgroupama\b/i], category: 'assurance' },
- { name: 'Allianz', patterns: [/\ballianz\b/i], category: 'assurance' },
- { name: 'MMA', patterns: [/\bmma\b/i], category: 'assurance' },
- { name: 'MAAF', patterns: [/\bmaaf\b/i], category: 'assurance' },
- { name: 'Generali', patterns: [/\bgenerali\b/i], category: 'assurance' },
- { name: 'Matmut', patterns: [/\bmatmut\b/i], category: 'assurance' },
- { name: 'Otis', patterns: [/\botis\b/i], category: 'ascenseur' },
- { name: 'Schindler', patterns: [/\bschindler\b/i], category: 'ascenseur' },
- { name: 'Kone', patterns: [/\bkone\b/i], category: 'ascenseur' },
- { name: 'ThyssenKrupp', patterns: [/\bthyssen\s*krupp\b/i], category: 'ascenseur' },
- ];
- // Category keywords for fallback detection
- const CATEGORY_KEYWORDS = {
- eau: [/\beau\b/i, /\bassainissement\b/i, /\bconsommation\s*d'eau\b/i],
- gaz: [/\bgaz\b/i, /\bgaz\s*naturel\b/i],
- electricite: [/\b[eé]lectricit[eé]\b/i, /\bconsommation\s*[eé]lectrique\b/i, /\bkwh\b/i],
- entretien: [/\bentretien\b/i, /\bmaintenance\b/i, /\bnettoyage\b/i, /\bravalement\b/i, /\br[eé]paration\b/i],
- ascenseur: [/\bascenseur\b/i, /\b[eé]l[eé]vateur\b/i],
- ordures: [/\bordures\b/i, /\bd[eé]chets\b/i, /\bom\b/i, /\bteom\b/i, /\btaxe.*enl[eè]vement\b/i],
- assurance: [/\bassurance\b/i, /\bprime\b/i, /\bcotisation\b/i, /\bsinistre\b/i, /\bgarantie\b/i],
- };
- // Amount patterns (French format: 1 234,56 € or 1234.56)
- const AMOUNT_PATTERNS = [
- /(?:total\s*(?:ttc|à\s*payer|net|facture|dû|du))\s*[:\s]*(\d[\d\s]*[.,]\d{2})\s*(?:€|eur)/i,
- /(?:montant\s*(?:ttc|total|à\s*payer|net|dû|du))\s*[:\s]*(\d[\d\s]*[.,]\d{2})\s*(?:€|eur)/i,
- /(?:net\s*à\s*payer)\s*[:\s]*(\d[\d\s]*[.,]\d{2})\s*(?:€|eur)/i,
- /(?:total\s*(?:ttc|à\s*payer|net|facture|dû|du))\s*[:\s]*(\d[\d\s]*[.,]\d{2})/i,
- /(?:montant\s*(?:ttc|total|à\s*payer|net|dû|du))\s*[:\s]*(\d[\d\s]*[.,]\d{2})/i,
- /(?:net\s*à\s*payer)\s*[:\s]*(\d[\d\s]*[.,]\d{2})/i,
- /(\d[\d\s]*[.,]\d{2})\s*(?:€|eur)\s*(?:ttc)/i,
- /(\d[\d\s]*[.,]\d{2})\s*(?:€|eur)/i,
- ];
- function parseAmount(raw) {
- if (!raw) return null;
- const cleaned = raw.replace(/\s/g, '').replace(',', '.');
- const val = parseFloat(cleaned);
- return isNaN(val) ? null : Math.round(val * 100) / 100;
- }
- function detectSupplier(text) {
- for (const supplier of SUPPLIERS) {
- for (const pattern of supplier.patterns) {
- if (pattern.test(text)) {
- return { name: supplier.name, category: supplier.category };
- }
- }
- }
- return null;
- }
- function detectCategory(text) {
- for (const [category, patterns] of Object.entries(CATEGORY_KEYWORDS)) {
- for (const pattern of patterns) {
- if (pattern.test(text)) return category;
- }
- }
- return 'autre';
- }
- function detectAmount(text) {
- for (const pattern of AMOUNT_PATTERNS) {
- const match = text.match(pattern);
- if (match) {
- const amount = parseAmount(match[1]);
- if (amount && amount > 0 && amount < 100000) return amount;
- }
- }
- return null;
- }
- async function analyzeFile(filePath) {
- const ext = filePath.toLowerCase().split('.').pop();
- if (ext === 'pdf') {
- return analyzePdf(filePath);
- }
- // For images (jpg, png, webp) - extract info from filename only
- return analyzeFromFilename(filePath);
- }
- async function analyzePdf(filePath) {
- const buffer = fs.readFileSync(filePath);
- let text = '';
- try {
- const data = await pdfParse(buffer);
- text = data.text || '';
- } catch (err) {
- console.warn(`Could not parse PDF ${filePath}:`, err.message);
- return analyzeFromFilename(filePath);
- }
- const supplierInfo = detectSupplier(text);
- const detectedAmount = detectAmount(text);
- const detectedCategory = supplierInfo?.category || detectCategory(text);
- const detectedSupplier = supplierInfo?.name || null;
- // Try to build a label
- let label = '';
- if (detectedSupplier) label = `Facture ${detectedSupplier}`;
- else if (detectedCategory !== 'autre') {
- const catLabels = { eau: 'Eau', gaz: 'Gaz', electricite: 'Électricité', entretien: 'Entretien', ascenseur: 'Ascenseur', ordures: 'Ordures ménagères', assurance: 'Assurance' };
- label = `Facture ${catLabels[detectedCategory] || detectedCategory}`;
- } else {
- label = 'Facture à identifier';
- }
- return {
- detected_supplier: detectedSupplier,
- detected_category: detectedCategory,
- detected_amount: detectedAmount,
- detected_label: label,
- };
- }
- function analyzeFromFilename(filePath) {
- const basename = filePath.split('/').pop().replace(/\.[^.]+$/, '').replace(/[_-]/g, ' ');
- const supplierInfo = detectSupplier(basename);
- const detectedCategory = supplierInfo?.category || detectCategory(basename) || 'autre';
- const detectedSupplier = supplierInfo?.name || null;
- // Try to find amount in filename (e.g., "EDF_150.50_2024.pdf")
- const amountMatch = basename.match(/(\d+[.,]\d{2})/);
- const detectedAmount = amountMatch ? parseAmount(amountMatch[1]) : null;
- let label = detectedSupplier ? `Facture ${detectedSupplier}` : 'Facture à identifier';
- return {
- detected_supplier: detectedSupplier,
- detected_category: detectedCategory,
- detected_amount: detectedAmount,
- detected_label: label,
- };
- }
- module.exports = { analyzeFile, detectSupplier, detectCategory, detectAmount, SUPPLIERS };
|