188 lines
5.8 KiB
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);
|
|
}
|
|
}
|