netgescon-day0/app/Services/Contabilita/UnicreditSemicolonCsvParser.php

200 lines
6.2 KiB
PHP

<?php
namespace App\Services\Contabilita;
use Carbon\Carbon;
class UnicreditSemicolonCsvParser
{
/**
* Parser per export CSV (separatore ;) con righe introduttive e header tipo:
* Data contabile;Data valuta;Descrizione;Accrediti;Addebiti;...
*
* @return array{
* rows: array<int, array{data: Carbon, valuta: Carbon|null, descrizione: string, importo: float, causale: string|null, raw_line: string}>,
* 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), 300);
for ($i = 0; $i < $scanMax; $i++) {
$line = trim((string) $lines[$i]);
if ($line === '') {
continue;
}
if (
stripos($line, 'Data contabile') !== false
&& stripos($line, 'Accrediti') !== false
&& stripos($line, 'Addebiti') !== false
) {
$headerIndex = $i;
break;
}
}
if ($headerIndex === null) {
throw new \RuntimeException('Formato CSV non riconosciuto: header "Data contabile" 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'] ?? null;
$idxValuta = $hmap['datavaluta'] ?? null;
$idxDescr = $hmap['descrizione'] ?? null;
$idxAccrediti = $hmap['accrediti'] ?? null;
$idxAddebiti = $hmap['addebiti'] ?? null;
$idxDescrEstesa = $hmap['descrizioneestesa'] ?? null;
if ($idxData === null || $idxDescr === null || ($idxAccrediti === null && $idxAddebiti === null)) {
throw new \RuntimeException('Formato CSV non riconosciuto: colonne richieste mancanti');
}
$rows = [];
$skipped = 0;
for ($i = $headerIndex + 1; $i < count($lines); $i++) {
$line = (string) $lines[$i];
if (trim($line) === '') {
continue;
}
$cols = str_getcsv($line, $delimiter);
if (count($cols) < count($header)) {
$cols = array_pad($cols, count($header), null);
}
$rawData = $cols[$idxData] ?? null;
$data = $this->parseDate($rawData);
if (! $data) {
$skipped++;
continue;
}
$valuta = null;
if ($idxValuta !== null) {
$valuta = $this->parseDate($cols[$idxValuta] ?? null);
}
$descrizione = trim((string) ($cols[$idxDescr] ?? ''));
$descrEstesa = $idxDescrEstesa !== null ? trim((string) ($cols[$idxDescrEstesa] ?? '')) : '';
if ($descrizione === '' && $descrEstesa !== '') {
$descrizione = $descrEstesa;
}
if ($descrizione === '') {
$skipped++;
continue;
}
$accrediti = $idxAccrediti !== null ? $this->parseMoney($cols[$idxAccrediti] ?? null) : null;
$addebiti = $idxAddebiti !== null ? $this->parseMoney($cols[$idxAddebiti] ?? null) : null;
$importo = null;
if ($accrediti !== null && abs($accrediti) > 0.0) {
$importo = abs($accrediti);
} elseif ($addebiti !== null && abs($addebiti) > 0.0) {
$importo = -abs($addebiti);
}
if ($importo === null) {
$skipped++;
continue;
}
$rows[] = [
'data' => $data,
'valuta' => $valuta,
'descrizione' => $descrizione,
'importo' => (float) $importo,
'causale' => null,
'raw_line' => 'csv:' . ($i + 1),
];
}
return [
'rows' => $rows,
'meta' => [
'header_row' => (int) ($headerIndex + 1),
'delimiter' => $delimiter,
'parsed_rows' => (int) count($rows),
'skipped_rows' => (int) $skipped,
],
];
}
private function parseDate(mixed $value): ?Carbon
{
$s = trim((string) ($value ?? ''));
if ($s === '') {
return null;
}
foreach (['d/m/y', 'd/m/Y', 'd.m.Y', 'd.m.y', 'Y-m-d'] as $fmt) {
try {
return Carbon::createFromFormat($fmt, $s)->startOfDay();
} catch (\Throwable) {
// keep trying
}
}
return null;
}
private function parseMoney(mixed $value): ?float
{
if ($value === null) {
return null;
}
$s = trim((string) $value);
if ($s === '') {
return null;
}
$s = str_replace(["\u{00A0}", "\t", ' '], '', $s);
$s = str_ireplace(['EUR', 'EURO', '€'], '', $s);
$s = preg_replace('/[^0-9,\.\-\+]/', '', $s) ?? $s;
// formato italiano: 1.059,26
$s = str_replace('.', '', $s);
$s = str_replace(',', '.', $s);
if (! is_numeric($s)) {
return null;
}
return (float) $s;
}
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;
}
}