} */ public function parse(string $content): array { $content = str_replace(["\r\n", "\r"], "\n", $content); $lines = array_values(array_filter(explode("\n", $content), fn($l) => trim((string) $l) !== '')); if (! empty($lines)) { $lines[0] = preg_replace('/^\xEF\xBB\xBF/', '', (string) $lines[0]) ?? (string) $lines[0]; } $iban = null; foreach ($lines as $line) { if (preg_match('/\bIT\s*[0-9A-Z ]{20,}\b/i', (string) $line, $m)) { $iban = strtoupper(preg_replace('/\s+/', '', $m[0])); break; } } $headerIndex = null; $delimiter = ';'; $map = []; foreach ($lines as $i => $line) { $line = preg_replace('/^\xEF\xBB\xBF/', '', (string) $line) ?? (string) $line; $candidate = trim($line); // Heuristica: header contiene "data" e "import". $low = mb_strtolower($candidate); if (! str_contains($low, 'data') || ! (str_contains($low, 'import') || str_contains($low, 'dare') || str_contains($low, 'avere'))) { continue; } $delim = str_contains($candidate, ';') ? ';' : (str_contains($candidate, ',') ? ',' : null); if (! $delim) { continue; } $cols = array_map(fn($v) => $this->normHeader($v), str_getcsv($candidate, $delim)); $idxData = $this->findFirstIndex($cols, ['dataoperazione', 'dataop', 'datacontabile', 'data']); $idxValuta = $this->findFirstIndex($cols, ['datavaluta', 'valuta', 'dataval']); // "descrizione" spesso è "descrizione" o "causale" o "descrizioneoperazione". $idxDescr = $this->findFirstIndex($cols, ['descrizione', 'descrizioneoperazione', 'causale', 'causaleabi', 'dettaglio']); // Alcuni export Intesa includono una colonna separata di descrizione estesa. $idxDescrEstesa = $this->findFirstIndex($cols, ['descrizioneoperazioneestesa', 'descrizioneestesa', 'dettagliooperazione', 'dettagliooperazioneesteso']); // Importo può essere una colonna unica o 2 colonne dare/avere. $idxImporto = $this->findFirstIndex($cols, ['importo', 'importoeuro', 'euro', 'importooperazione']); $idxDare = $this->findFirstIndex($cols, ['dare', 'importodare']); $idxAvere = $this->findFirstIndex($cols, ['avere', 'importoavere']); if ($idxData === null || ($idxImporto === null && ($idxDare === null && $idxAvere === null)) || $idxDescr === null) { continue; } $headerIndex = $i; $delimiter = $delim; $map = [ 'data' => $idxData, 'valuta' => $idxValuta, 'descrizione' => $idxDescr, 'descrizione_estesa' => $idxDescrEstesa, 'importo' => $idxImporto, 'dare' => $idxDare, 'avere' => $idxAvere, ]; break; } if ($headerIndex === null) { throw new \RuntimeException('Formato Intesa non riconosciuto: header CSV non trovato'); } $rows = []; for ($i = $headerIndex + 1; $i < count($lines); $i++) { $raw = (string) $lines[$i]; $parts = str_getcsv($raw, $delimiter); $parts = array_map(static fn($v) => trim((string) $v), $parts); $data = $this->parseDate($parts[$map['data']] ?? null); if (! $data) { continue; } $valuta = null; if ($map['valuta'] !== null) { $valuta = $this->parseDate($parts[$map['valuta']] ?? null); } $descrizione = trim((string) ($parts[$map['descrizione']] ?? '')); if ($descrizione === '') { continue; } $descrizioneEstesa = null; if (isset($map['descrizione_estesa']) && $map['descrizione_estesa'] !== null) { $descrizioneEstesa = trim((string) ($parts[$map['descrizione_estesa']] ?? '')); if ($descrizioneEstesa === '') { $descrizioneEstesa = null; } } $importo = null; if ($map['importo'] !== null) { $importo = $this->parseMoney($parts[$map['importo']] ?? null); } else { $dare = $map['dare'] !== null ? $this->parseMoney($parts[$map['dare']] ?? null) : null; $avere = $map['avere'] !== null ? $this->parseMoney($parts[$map['avere']] ?? null) : null; if ($dare !== null && $dare != 0.0) { $importo = -abs($dare); } elseif ($avere !== null && $avere != 0.0) { $importo = abs($avere); } } if ($importo === null) { continue; } $rows[] = [ 'data' => $data, 'valuta' => $valuta, 'descrizione' => $descrizione, 'descrizione_estesa' => $descrizioneEstesa, 'importo' => $importo, 'causale' => null, 'raw_line' => $raw, ]; } return ['iban' => $iban, 'rows' => $rows]; } private function normHeader(string $value): string { $value = trim(mb_strtolower($value)); $value = str_replace([' ', "\t", "\u{00A0}"], '', $value); $value = str_replace(['à', 'á', 'â', 'ä'], 'a', $value); $value = str_replace(['è', 'é', 'ê', 'ë'], 'e', $value); $value = str_replace(['ì', 'í', 'î', 'ï'], 'i', $value); $value = str_replace(['ò', 'ó', 'ô', 'ö'], 'o', $value); $value = str_replace(['ù', 'ú', 'û', 'ü'], 'u', $value); $value = preg_replace('/[^a-z0-9]/', '', $value) ?? $value; return $value; } /** * @param array $cols * @param array $candidates */ private function findFirstIndex(array $cols, array $candidates): ?int { foreach ($candidates as $c) { foreach ($cols as $idx => $col) { if ($col === $c) { return (int) $idx; } } } return null; } private function parseDate(?string $value): ?Carbon { $value = trim((string) $value); if ($value === '') { return null; } try { return Carbon::createFromFormat('d/m/Y', $value)->startOfDay(); } catch (\Throwable) { return null; } } private function parseMoney(?string $value): ?float { $value = trim((string) $value); if ($value === '') { return null; } $value = str_replace(["\u{00A0}", "\t", ' '], '', $value); $value = str_ireplace(['EUR', 'EURO', '€'], '', $value); $isParenNegative = str_starts_with($value, '(') && str_ends_with($value, ')'); if ($isParenNegative) { $value = substr($value, 1, -1); } $value = preg_replace('/[^0-9,\.\-\+]/', '', $value) ?? $value; $value = str_replace('.', '', $value); $value = str_replace(',', '.', $value); if ($isParenNegative && ! str_starts_with($value, '-')) { $value = '-' . $value; } if (! is_numeric($value)) { return null; } return (float) $value; } }