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

188 lines
5.8 KiB
PHP

<?php
namespace App\Services\Contabilita;
use Carbon\Carbon;
class BpmCsvParser
{
/**
* Parser per export CSV Banco BPM con header tipo:
* "Ragione Sociale","Data contabile","Data valuta","Banca","Rapporto","Importo","Divisa","Descrizione","Categoria/sottocategoria","Hashtag"
*
* @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), 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);
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] ?? '')) : '';
$descr = trim((string) ($cols[$idxDescr] ?? ''));
$importoRaw = trim((string) ($cols[$idxImporto] ?? ''));
$categoria = $idxCat !== null ? trim((string) ($cols[$idxCat] ?? '')) : null;
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;
}
$causale = null;
if ($categoria !== null && $categoria !== '') {
$causale = mb_substr($categoria, 0, 20);
}
$rows[] = [
'data' => $date,
'valuta' => $valuta,
'descrizione' => $descr,
'importo' => $importo,
'causale' => $causale,
'raw_line' => $line,
];
}
return [
'rows' => $rows,
'meta' => [
'header_row' => $headerIndex,
'delimiter' => $delimiter,
'parsed_rows' => count($rows),
'skipped_rows' => $skipped,
],
];
}
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);
}
}