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 };