['categoria' => 'energia_elettrica', 'conto_mastro' => 'SPESE_ENERGIA', 'nome' => 'Enel Energia'], 'servizio elettrico' => ['categoria' => 'energia_elettrica', 'conto_mastro' => 'SPESE_ENERGIA', 'nome' => 'Servizio Elettrico Nazionale'], 'plenitude' => ['categoria' => 'riscaldamento', 'conto_mastro' => 'SPESE_RISCALDAMENTO', 'nome' => 'Eni Plenitude'], 'eni' => ['categoria' => 'riscaldamento', 'conto_mastro' => 'SPESE_RISCALDAMENTO', 'nome' => 'Eni'], 'italgas' => ['categoria' => 'riscaldamento', 'conto_mastro' => 'SPESE_RISCALDAMENTO', 'nome' => 'Italgas'], 'publiacqua' => ['categoria' => 'acqua', 'conto_mastro' => 'SPESE_ACQUA', 'nome' => 'Publiacqua'], 'acea' => ['categoria' => 'acqua', 'conto_mastro' => 'SPESE_ACQUA', 'nome' => 'Acea'], 'sorgenia' => ['categoria' => 'energia_elettrica', 'conto_mastro' => 'SPESE_ENERGIA', 'nome' => 'Sorgenia'], 'otis' => ['categoria' => 'ascensori', 'conto_mastro' => 'SPESE_ASCENSORI', 'nome' => 'Otis Servizi'], 'schindler' => ['categoria' => 'ascensori', 'conto_mastro' => 'SPESE_ASCENSORI', 'nome' => 'Schindler'], ]; /** * Esegue il parsing del file PDF specificato * * @param string $filePath * @return array */ public function parsePdf(string $filePath): array { if (!file_exists($filePath)) { Log::warning("[FatturaParser] File non trovato: $filePath"); return []; } $output = []; $exitCode = 0; $cmd = "/usr/bin/pdftotext -layout " . escapeshellarg($filePath) . " -"; exec($cmd, $output, $exitCode); if ($exitCode !== 0) { Log::error("[FatturaParser] Errore nell'esecuzione di pdftotext (Codice: $exitCode) per: $filePath"); return []; } $text = implode("\n", $output); return $this->parseText($text); } /** * Riconoscimento del fornitore a partire dal testo (P.IVA, CF, o Denominazione) */ public function detectFornitore(string $text): array { $textLower = strtolower($text); foreach ($this->fornitoriDizionario as $key => $info) { if (strpos($textLower, $key) !== false) { return $info; } } if (preg_match('/\bpartita\s*iva\b\s*[:\-]?\s*([0-9]{11})/i', $text, $m)) { return [ 'categoria' => 'altro', 'conto_mastro' => '3010', 'nome' => 'Fornitore P.IVA ' . $m[1] ]; } return [ 'categoria' => 'altro', 'conto_mastro' => '3010', 'nome' => 'Fornitore Sconosciuto' ]; } /** * Estrazione del codice CBILL standard di 18 caratteri * e codice fornitore a 5 caratteri. */ public function extractCbill18(string $text): array { $cbill = null; $codFornitore = null; if (preg_match('/(?:cbill|avviso|codice\s*avviso|cod\.\s*avviso)\b\s*(?:[a-z\(\)]*\s*)*[:\-]?\s*([0-9\s]{18,24})/i', $text, $m)) { $cleaned = preg_replace('/\s+/', '', $m[1]); if (strlen($cleaned) === 18) { $cbill = $cleaned; } } if (!$cbill && preg_match('/\b([0-9]{18})\b/', $text, $m)) { $cbill = $m[1]; } if (!$cbill && preg_match('/\b([0-9\s]{18,24})\b/', $text, $m)) { $cleaned = preg_replace('/\s+/', '', $m[1]); if (strlen($cleaned) === 18) { $cbill = $cleaned; } } if (preg_match('/(?:codice\s*ente|sia|cod\.\s*cbill)\b\s*(?:[a-z\(\)]*\s*)*[:\-]?\s*([A-Z0-9]{5})\b/i', $text, $m)) { $codFornitore = strtoupper($m[1]); } if (!$codFornitore && preg_match('/\b([A-Z0-9]{5})\b/', $text, $m)) { $val = $m[1]; if (!in_array($val, ['CBILL', 'PAGOP', 'POSTE'], true) && !is_numeric($val)) { $codFornitore = $val; } } return [ 'cbill' => $cbill, 'codice_fornitore' => $codFornitore, ]; } /** * Parsing del testo estratto dal PDF * * @param string $text * @return array */ public function parseText(string $text): array { $textNorm = str_replace(["\r\n", "\r"], "\n", $text); $fornitoreInfo = $this->detectFornitore($textNorm); $cbillData = $this->extractCbill18($textNorm); $data = [ 'fornitore_nome' => $fornitoreInfo['nome'], 'partita_iva' => null, 'codice_cliente' => null, 'pod' => null, 'pdr' => null, 'matricola_contatore' => null, 'codice_utenza' => null, 'numero_contratto' => null, 'cbill' => $cbillData['cbill'], 'codice_avviso' => $cbillData['cbill'], 'codice_fornitore_ente' => $cbillData['codice_fornitore'], 'data_inizio_periodo' => null, 'data_fine_periodo' => null, 'periodi_conteggiati' => null, 'tipo_lettura' => 'effettiva', 'quantita_consumata' => null, 'unita_misura' => null, 'importo_monetario' => null, 'conto_mastro' => $fornitoreInfo['conto_mastro'], 'categoria' => $fornitoreInfo['categoria'], ]; // Partita IVA if (preg_match('/\bpartita\s*iva\b\s*[:\-]?\s*([0-9]{11})/i', $textNorm, $m)) { $data['partita_iva'] = $m[1]; } // Codice Cliente if (preg_match('/(?:codice|cod\.|n\.|n°)\s*cliente\b\s*(?:[a-z\(\)]*\s*)*[:\-]\s*([A-Z0-9\-]{5,})/is', $textNorm, $m)) { $data['codice_cliente'] = trim($m[1]); } // Codice Utenza if (preg_match('/(?:codice|cod\.)\s*utenza\b\s*(?:[a-z\(\)]*\s*)*[:\-]\s*([A-Z0-9\-]{5,})/is', $textNorm, $m)) { $data['codice_utenza'] = trim($m[1]); } // Numero Contratto if (preg_match('/(?:codice|cod\.|numero|num\.|n\.|n°)\s*contratto\b\s*(?:[a-z\(\)]*\s*)*[:\-]\s*([A-Z0-9\-]{5,})/is', $textNorm, $m)) { $data['numero_contratto'] = trim($m[1]); } // POD (Luce) if (preg_match('/([A-Z]{2}[0-9]{3}[A-Z][0-9]{8})/i', $textNorm, $m)) { $data['pod'] = strtoupper(trim($m[1])); $data['categoria'] = 'energia_elettrica'; $data['conto_mastro'] = 'SPESE_ENERGIA'; } // PDR (Gas) if (preg_match('/(?:pdr|punto\s+di\s+riconsegna)\b\s*(?:[a-z\(\)]*\s*)*[:\-]?\s*([0-9]{14})/is', $textNorm, $m)) { $data['pdr'] = trim($m[1]); if ($data['categoria'] !== 'riscaldamento') { $data['categoria'] = 'gas'; } $data['conto_mastro'] = 'SPESE_RISCALDAMENTO'; } // Matricola Contatore if (preg_match('/(?:matricola\s+contatore|matricola|seriale|n°?\s*contatore|contatore\s*n°?|contatore)\b\s*(?:[a-z\(\)]*\s*)*[:\-]\s*([A-Z0-9\-]{4,})/is', $textNorm, $m)) { $data['matricola_contatore'] = trim($m[1]); } // Helper per parsing float in formato europeo (1.234,56 -> 1234.56) $parseFloat = function ($str) { $str = trim($str); if (strpos($str, ',') !== false && strpos($str, '.') !== false) { if (strpos($str, '.') < strpos($str, ',')) { $str = str_replace('.', '', $str); $str = str_replace(',', '.', $str); } else { $str = str_replace(',', '', $str); } } elseif (strpos($str, ',') !== false) { $str = str_replace(',', '.', $str); } return (float) $str; }; // Date Periodo if (preg_match('/dal\s+(\d{2}\/\d{2}\/\d{4})\s+al\s+(\d{2}\/\d{2}\/\d{4})/iu', $textNorm, $m)) { $data['data_inizio_periodo'] = $this->parseIsoDate($m[1]); $data['data_fine_periodo'] = $this->parseIsoDate($m[2]); } // Tipo Lettura $textLower = strtolower($textNorm); if (strpos($textLower, 'stima') !== false || strpos($textLower, 'stim') !== false) { $data['tipo_lettura'] = 'stimata'; } // 5. Lettura Precedente if (preg_match('/(?:lettura\s*(?:precedente|prec\.?)|lett\.?\s*prec\.?|prec\.?)\s*(?:[a-z\(\)]*\s*)*[:\-]?\s*([0-9]+(?:[\.,][0-9]+)*)/is', $textNorm, $m)) { $data['lettura_precedente'] = $parseFloat($m[1]); } // 6. Lettura Attuale if (preg_match('/(?:lettura\s*(?:attuale|att\.?)|lett\.?\s*att\.?|att\.?)\s*(?:[a-z\(\)]*\s*)*[:\-]?\s*([0-9]+(?:[\.,][0-9]+)*)/is', $textNorm, $m)) { $data['lettura_attuale'] = $parseFloat($m[1]); } // 7. Consumo if (preg_match('/(?:consumo|volume|m3|kwh|cons\.\s*rilevato|cons\.\s*periodo)\s*(?:[a-z\(\)]*\s*)*[:\-]?\s*([0-9]+(?:[\.,][0-9]+)*)/is', $textNorm, $m)) { $data['quantita_consumata'] = $parseFloat($m[1]); } if (preg_match('/(mc|kwh)/i', $textNorm, $m)) { $data['unita_misura'] = strtolower($m[1]); } // Importo monetario if (preg_match('/(?:totale\s*da\s*pagare|totale\s*bolletta|totale\s*fattura|importo\s*da\s*pagare|da\s*pagare|totale\s*dovuto)\s*(?:[a-z\(\)]*\s*)*[:\-]?\s*([0-9]+(?:[\.,][0-9]{2}))/is', $textNorm, $m)) { $data['importo_monetario'] = $parseFloat($m[1]); } return $data; } /** * Normalizzazione dei float */ protected function parseFloatIt(string $value): ?float { $s = trim($value); if ($s === '') { return null; } $s = str_replace(' ', '', $s); if (strpos($s, ',') !== false && strpos($s, '.') !== false) { if (strpos($s, '.') < strpos($s, ',')) { $s = str_replace('.', '', $s); $s = str_replace(',', '.', $s); } else { $s = str_replace(',', '', $s); } } elseif (strpos($s, ',') !== false) { $s = str_replace(',', '.', $s); } return (float) $s; } /** * Normalizzazione date */ protected function parseIsoDate(string $dmy): ?string { try { return Carbon::createFromFormat('d/m/Y', trim($dmy))->format('Y-m-d'); } catch (\Throwable) { return null; } } }