* }>, * meta: array{header_row:int, delimiter:string, parsed_rows:int, skipped_rows:int} * } */ public function parse(string $content): array { $content = str_replace(["\r\n", "\r"], "\n", $content); $lines = explode("\n", $content); $delimiter = ','; $headerIndex = null; $scanMax = min(count($lines), 100); for ($i = 0; $i < $scanMax; $i++) { $line = trim((string) $lines[$i]); if ($line === '') { continue; } if ( stripos($line, 'Data contabile') !== false && stripos($line, 'Importo') !== false && stripos($line, 'Descrizione') !== false ) { $headerIndex = $i; if (substr_count($line, ';') > substr_count($line, ',')) { $delimiter = ';'; } break; } } if ($headerIndex === null) { throw new \RuntimeException('Formato CSV Banco BPM non riconosciuto: header con "Data contabile", "Importo" e "Descrizione" non trovato'); } $header = str_getcsv((string) $lines[$headerIndex], $delimiter); $header = array_map(fn($h) => $this->normHeader((string) $h), $header); $hmap = []; foreach ($header as $idx => $h) { if ($h === '') { continue; } if (!array_key_exists($h, $hmap)) { $hmap[$h] = (int) $idx; } } $idxData = $hmap['datacontabile'] ?? ($hmap['data'] ?? null); $idxValuta = $hmap['datavaluta'] ?? ($hmap['valuta'] ?? null); $idxDescr = $hmap['descrizione'] ?? null; $idxImporto = $hmap['importo'] ?? null; $idxCat = $hmap['categoriasottocategoria'] ?? ($hmap['categoria'] ?? null); $idxBanca = $hmap['banca'] ?? null; $idxRapporto = $hmap['rapporto'] ?? null; $idxRagSoc = $hmap['ragionesociale'] ?? null; if ($idxData === null || $idxDescr === null || $idxImporto === null) { throw new \RuntimeException('Formato CSV Banco BPM non riconosciuto: colonne richieste mancanti'); } $rows = []; $skipped = 0; for ($i = $headerIndex + 1; $i < count($lines); $i++) { $line = trim((string) $lines[$i]); if ($line === '') { continue; } $cols = str_getcsv($line, $delimiter); if (count($cols) <= max($idxData, $idxDescr, $idxImporto)) { $skipped++; continue; } $dateRaw = trim((string) ($cols[$idxData] ?? '')); $valutaRaw = $idxValuta !== null ? trim((string) ($cols[$idxValuta] ?? '')) : ''; $rawDescr = trim((string) ($cols[$idxDescr] ?? '')); $importoRaw = trim((string) ($cols[$idxImporto] ?? '')); $categoria = $idxCat !== null ? trim((string) ($cols[$idxCat] ?? '')) : null; $bancaRaw = $idxBanca !== null ? trim((string) ($cols[$idxBanca] ?? '')) : ''; $rapportoRaw = $idxRapporto !== null ? trim((string) ($cols[$idxRapporto] ?? '')) : ''; $ragSocRaw = $idxRagSoc !== null ? trim((string) ($cols[$idxRagSoc] ?? '')) : ''; if ($dateRaw === '' || $importoRaw === '') { $skipped++; continue; } $date = $this->parseDate($dateRaw); if (! $date) { $skipped++; continue; } $valuta = $valutaRaw !== '' ? $this->parseDate($valutaRaw) : null; $importo = $this->parseAmount($importoRaw); if ($importo === null) { $skipped++; continue; } // Elabora la semantica della riga BPM $analyzed = $this->analyzeBpmMovement($rawDescr, $categoria, $bancaRaw, $rapportoRaw, $ragSocRaw, $importo); $rows[] = [ 'data' => $date, 'valuta' => $valuta, 'descrizione' => $analyzed['descrizione_breve'], 'descrizione_estesa' => $analyzed['descrizione_estesa'], 'importo' => $importo, 'causale' => $analyzed['causale'], 'raw_line' => $line, 'match_data' => $analyzed['match_data'], ]; } return [ 'rows' => $rows, 'meta' => [ 'header_row' => $headerIndex, 'delimiter' => $delimiter, 'parsed_rows' => count($rows), 'skipped_rows' => $skipped, ], ]; } /** * @return array{ * descrizione_breve: string, * descrizione_estesa: string, * causale: string, * match_data: array * } */ public function analyzeBpmMovement( string $rawDescr, ?string $categoria, string $bancaRaw = '', string $rapportoRaw = '', string $ragSocRaw = '', float $importo = 0.0, ): array { $meta = [ 'banca_source' => 'banco_bpm', ]; if ($categoria !== null && $categoria !== '') { $meta['categoria_banca'] = $categoria; } if (preg_match('/^(\d{5})\s*-\s*(.+)$/', trim($bancaRaw), $mB)) { $meta['cod_abi'] = trim($mB[1]); $meta['banca_nome'] = trim($mB[2]); } if (preg_match('/^(\d{5})\s*-\s*(\d+)/', trim($rapportoRaw), $mR)) { $meta['cod_cab'] = trim($mR[1]); $meta['conto_num'] = trim($mR[2]); } if ($ragSocRaw !== '') { $meta['ragione_sociale_estratto'] = trim($ragSocRaw); } $d = trim($rawDescr); $descBreve = $d; $descEstesa = $d; $causale = 'GENERICO'; // 1. VOSTRA DISPOSIZIONE (Bonifico fornitore / Disposizione uscita) if (preg_match('/^VOSTRA DISPOSIZIONE\s*-\s*(?:VS\.DISP\.\s*RIF\.\s*([^\s]+))?\s*FAVORE\s+(.+?)(?:\s*-\s*ADD\.TOT)?\s*-\s*(.+)$/i', $d, $m)) { $meta['tipo'] = 'disposizione_bonifico'; $meta['rif_disp'] = trim($m[1] ?? ''); $beneficiario = trim($m[2] ?? ''); $dettaglio = trim($m[3] ?? ''); $meta['beneficiario'] = $beneficiario; $meta['dettaglio'] = $dettaglio; if (preg_match('/(?:SALDO|SAKDO)?\s*FT\s*(?:N\.?|NUM\.?)?\s*(.+?)\s+DEL\s+([0-9\/\.\-]+)/i', $dettaglio, $mFt)) { $meta['rif_fattura'] = trim($mFt[1]); $meta['data_fattura'] = trim($mFt[2]); } $causale = 'BONIFICO'; $descBreve = 'Disposizione a favore di ' . $beneficiario . ($dettaglio !== '' ? (' - ' . $dettaglio) : ''); $descEstesa = 'Disposizione bonifico a favore di ' . $beneficiario . (!empty($meta['rif_disp']) ? (' · Rif. ' . $meta['rif_disp']) : '') . ($dettaglio !== '' ? (' · ' . $dettaglio) : ''); } // 2. BONIF. VS. FAVORE / BON URG/ISTANT (Bonifico in entrata) elseif (preg_match('/^BON(?:IF)?\.?\s*(?:URG\/ISTANT\s*)?VS\.?\s*(?:FAVORE|F)?\s*-\s*BON\.DA\s+(.+?)(?:\s*-\s*(.+))?$/i', $d, $m)) { $isUrg = stripos($d, 'URG') !== false; $meta['tipo'] = $isUrg ? 'bonifico_urgente_entrata' : 'bonifico_entrata'; $mittente = trim($m[1]); $dettaglio = trim($m[2] ?? ''); $meta['mittente'] = $mittente; if ($dettaglio !== '') { $meta['dettaglio'] = $dettaglio; } $causale = 'BONIFICO'; $descBreve = ($isUrg ? 'Bonifico urgente da ' : 'Bonifico da ') . $mittente . ($dettaglio !== '' ? (' - ' . $dettaglio) : ''); $descEstesa = ($isUrg ? 'Bonifico urgente/istantaneo a Vostro favore da ' : 'Bonifico a Vostro favore da ') . $mittente . ($dettaglio !== '' ? (' · Dettaglio: ' . $dettaglio) : ''); } // 3. COMMISSIONI SU BONIFICI elseif (preg_match('/^COMM\.SU BONIFICI\s*-\s*NS RIF\.\s*([^\s]+)\s*(.*)$/i', $d, $m)) { $meta['tipo'] = 'commissioni_bonifico'; $rif = trim($m[1]); $meta['rif_disp'] = $rif; $causale = 'COMMISSIONI'; $descBreve = 'Commissioni su bonifico (Rif. ' . $rif . ')'; $descEstesa = 'Commissioni e spese su bonifico · Rif. ' . $rif; } // 4. ADDEBITO DIRETTO SDD elseif (preg_match('/^ADDEBITO DIRETTO SDD\s*-\s*SDD CORE:\s*([^\s]+)\s+(.+)$/i', $d, $m)) { $meta['tipo'] = 'addebito_sdd'; $sddCode = trim($m[1]); $fornitore = trim($m[2]); $meta['codice_sdd'] = $sddCode; $meta['beneficiario'] = $fornitore; $causale = 'SDD'; $descBreve = 'Addebito SDD ' . $fornitore; $descEstesa = 'Addebito diretto SDD CORE: ' . $sddCode . ' · Fornitore: ' . $fornitore; } // 5. PAG. UTENZE VARIE / BOLLETTINI CBILL elseif (preg_match('/^(?:PAG\.\s*UTENZE VARIE|COMMISSIONI)\s*-\s*(?:BOLL\.CBILL\s+(.+?)(?:\s*\(?PROFIL CBILL|\s*CBILL|\s*-|\s*$)|Bollettino\s+(.+?)\s+INCASSO UTE\s+Rif\.([^\s]+)\s+Nop\.([^\s]+))/i', $d, $m)) { $isComm = stripos($d, 'COMMISSIONI') === 0; $meta['tipo'] = $isComm ? 'commissioni_bollettino' : 'pagamento_utenze'; $ente = trim(!empty($m[1]) ? $m[1] : ($m[2] ?? '')); $meta['beneficiario'] = $ente; if (preg_match('/(?:PROFIL CBILL|CBILL)\s*([0-9]{10,})/i', $d, $mC)) { $meta['codice_cbill'] = trim($mC[1]); } if (!empty($m[3])) { $meta['rif_bollettino'] = trim($m[3]); } if (!empty($m[4])) { $meta['nop_bollettino'] = trim($m[4]); } $causale = $isComm ? 'COMMISSIONI' : 'CBILL'; if ($isComm) { $descBreve = 'Commissioni bollettino ' . $ente; $descEstesa = 'Commissioni pagamento bollettino · Ente: ' . $ente . (!empty($meta['codice_cbill']) ? (' · CBILL: ' . $meta['codice_cbill']) : '') . (!empty($meta['rif_bollettino']) ? (' · Rif: ' . $meta['rif_bollettino']) : ''); } else { $descBreve = 'Pagamento utenze ' . (!empty($meta['codice_cbill']) ? 'CBILL ' : '') . $ente; $descEstesa = 'Pagamento utenze bollettino · Ente: ' . $ente . (!empty($meta['codice_cbill']) ? (' · Codice CBILL: ' . $meta['codice_cbill']) : '') . (!empty($meta['rif_bollettino']) ? (' · Rif: ' . $meta['rif_bollettino']) : ''); } } // 6. I24 AGENZIA ENTRATE (F24 Telematico) elseif (preg_match('/^I24 AGENZIA ENTRATE\s*-\s*PAG\.TO TELEMATICO\s*-\s*DATA INCASSO\s*([0-9\/]+)\s*(.+)$/i', $d, $m)) { $meta['tipo'] = 'f24_agenzia_entrate'; $meta['beneficiario'] = 'AGENZIA DELLE ENTRATE'; $dataIncasso = trim($m[1]); $proto = trim($m[2]); $meta['data_incasso'] = $dataIncasso; $meta['protocollo_f24'] = $proto; $causale = 'F24'; $descBreve = 'F24 Telematico Agenzia Entrate (Incasso ' . $dataIncasso . ')'; $descEstesa = 'Modello F24 telematico Agenzia delle Entrate · Data incasso: ' . $dataIncasso . ' · Protocollo: ' . $proto; } // 7. INT. E COMP. (Interessi e Competenze) elseif (preg_match('/^INT\.\s*E COMP\.\s*-\s*(.+)$/i', $d, $m)) { $meta['tipo'] = 'interessi_competenze'; $dettaglio = trim($m[1]); $meta['dettaglio'] = $dettaglio; $isCred = stripos($dettaglio, 'CREDITORI') !== false; $causale = $isCred ? 'INTERESSI' : 'COMPETENZE'; $descBreve = $isCred ? 'Interessi creditori di conto' : 'Competenze e spese tenuta conto'; $descEstesa = 'Interessi e competenze bancarie · ' . $dettaglio; } // 8. DEBIT PAGAMENTO (POS / Carta) elseif (preg_match('/^DEBIT PAGAMENTO\s*-\s*CARTA\*(\d+)-([0-9:]+)-(.+)$/i', $d, $m)) { $meta['tipo'] = 'pagamento_pos'; $carta = trim($m[1]); $ora = trim($m[2]); $esercente = trim($m[3]); $meta['carta'] = $carta; $meta['ora'] = $ora; $meta['beneficiario'] = $esercente; $causale = 'POS'; $descBreve = 'Pagamento POS Carta *' . $carta . ' - ' . $esercente; $descEstesa = 'Pagamento con carta di debito *' . $carta . ' · Ora: ' . $ora . ' · Esercente: ' . $esercente; } // 9. DEBIT PRELIEVO ATM elseif (preg_match('/^DEBIT PREL(?:IEVO|\.NS)? ATM\s*-\s*CARTA\*(\d+)-([0-9:]+)-(.+)$/i', $d, $m)) { $meta['tipo'] = 'prelievo_atm'; $carta = trim($m[1]); $ora = trim($m[2]); $sportello = trim($m[3]); $meta['carta'] = $carta; $meta['ora'] = $ora; $meta['dettaglio'] = $sportello; $causale = 'PRELIEVO'; $descBreve = 'Prelievo ATM Carta *' . $carta . ' - ' . $sportello; $descEstesa = 'Prelievo contanti ATM Carta *' . $carta . ' · Ora: ' . $ora . ' · Sportello: ' . $sportello; } // 10. IMP. BOLLO CC/LR elseif (preg_match('/^IMP\.\s*BOLLO CC\/LR\s*-\s*(.+)$/i', $d, $m)) { $meta['tipo'] = 'imposta_bollo'; $periodo = trim($m[1]); $meta['dettaglio'] = $periodo; $causale = 'BOLLO'; $descBreve = 'Imposta di bollo c/c (' . $periodo . ')'; $descEstesa = 'Imposta di bollo conto corrente / libretto · Periodo: ' . $periodo; } // 11. SPESE E CANONE CARTA elseif (preg_match('/^(?:CANONE|RATEO CANONE|EMISS\/ATTIV) CARTA\s*-\s*(.+)$/i', $d, $m)) { $meta['tipo'] = 'spese_carta'; $dettaglio = trim($m[1]); $meta['dettaglio'] = $dettaglio; $causale = 'CARTA'; $descBreve = 'Spese / canone carta (' . $dettaglio . ')'; $descEstesa = 'Spese di gestione e canone carta di debito · ' . $dettaglio; } // 12. STORNO SCRITTURE elseif (preg_match('/^STORNO SCRITTURE\s*-\s*(.+)$/i', $d, $m)) { $meta['tipo'] = 'storno_scritture'; $dettaglio = trim($m[1]); $meta['dettaglio'] = $dettaglio; $causale = 'STORNO'; $descBreve = 'Storno scritture bancarie - ' . $dettaglio; $descEstesa = 'Storno operazione contabile · ' . $dettaglio; } // 13. VERSAMENTO CONTANTI elseif (preg_match('/^VERS\.\s*CONTANTI\s*-\s*(.+)$/i', $d, $m)) { $meta['tipo'] = 'versamento_contanti'; $causale = 'VERSAMENTO'; $descBreve = 'Versamento contanti in cassa / sportello'; $descEstesa = 'Versamento contanti in cassa / sportello · ' . trim($m[1]); } else { // Default fallback if ($categoria !== null && $categoria !== '') { $causale = mb_substr($categoria, 0, 20); } } return [ 'descrizione_breve' => mb_substr($descBreve, 0, 255), 'descrizione_estesa' => $descEstesa, 'causale' => $causale, 'match_data' => $meta, ]; } private function normHeader(string $value): string { $v = mb_strtolower(trim($value)); return (string) preg_replace('/[^a-z0-9]+/i', '', $v); } private function parseDate(string $raw): ?Carbon { $raw = trim($raw); if ($raw === '') { return null; } foreach (['d/m/Y', 'Y-m-d', 'd-m-Y', 'd.m.Y'] as $fmt) { try { $dt = Carbon::createFromFormat($fmt, $raw); if ($dt && $dt->year >= 1990 && $dt->year <= 2100) { return $dt->startOfDay(); } } catch (\Throwable) {} } return null; } private function parseAmount(string $raw): ?float { $raw = trim($raw); if ($raw === '') { return null; } $raw = str_replace('€', '', $raw); $raw = trim($raw); if (str_contains($raw, ',') && str_contains($raw, '.')) { $raw = str_replace('.', '', $raw); $raw = str_replace(',', '.', $raw); } elseif (str_contains($raw, ',')) { $raw = str_replace(',', '.', $raw); } $raw = preg_replace('/[^\d\.\+\-]/', '', $raw); if ($raw === '' || !is_numeric($raw)) { return null; } return round((float) $raw, 2); } }