200 lines
6.2 KiB
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;
|
|
}
|
|
}
|