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

434 lines
17 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,
* descrizione_estesa: string|null,
* importo: float,
* causale: string|null,
* raw_line: string,
* match_data: array<string, mixed>
* }>,
* 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);
$idxBanca = $hmap['banca'] ?? null;
$idxRapporto = $hmap['rapporto'] ?? null;
$idxRagSoc = $hmap['ragionesociale'] ?? 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] ?? '')) : '';
$rawDescr = trim((string) ($cols[$idxDescr] ?? ''));
$importoRaw = trim((string) ($cols[$idxImporto] ?? ''));
$categoria = $idxCat !== null ? trim((string) ($cols[$idxCat] ?? '')) : null;
$bancaRaw = $idxBanca !== null ? trim((string) ($cols[$idxBanca] ?? '')) : '';
$rapportoRaw = $idxRapporto !== null ? trim((string) ($cols[$idxRapporto] ?? '')) : '';
$ragSocRaw = $idxRagSoc !== null ? trim((string) ($cols[$idxRagSoc] ?? '')) : '';
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;
}
// Elabora la semantica della riga BPM
$analyzed = $this->analyzeBpmMovement($rawDescr, $categoria, $bancaRaw, $rapportoRaw, $ragSocRaw, $importo);
$rows[] = [
'data' => $date,
'valuta' => $valuta,
'descrizione' => $analyzed['descrizione_breve'],
'descrizione_estesa' => $analyzed['descrizione_estesa'],
'importo' => $importo,
'causale' => $analyzed['causale'],
'raw_line' => $line,
'match_data' => $analyzed['match_data'],
];
}
return [
'rows' => $rows,
'meta' => [
'header_row' => $headerIndex,
'delimiter' => $delimiter,
'parsed_rows' => count($rows),
'skipped_rows' => $skipped,
],
];
}
/**
* @return array{
* descrizione_breve: string,
* descrizione_estesa: string,
* causale: string,
* match_data: array<string, mixed>
* }
*/
public function analyzeBpmMovement(
string $rawDescr,
?string $categoria,
string $bancaRaw = '',
string $rapportoRaw = '',
string $ragSocRaw = '',
float $importo = 0.0,
): array {
$meta = [
'banca_source' => 'banco_bpm',
];
if ($categoria !== null && $categoria !== '') {
$meta['categoria_banca'] = $categoria;
}
if (preg_match('/^(\d{5})\s*-\s*(.+)$/', trim($bancaRaw), $mB)) {
$meta['cod_abi'] = trim($mB[1]);
$meta['banca_nome'] = trim($mB[2]);
}
if (preg_match('/^(\d{5})\s*-\s*(\d+)/', trim($rapportoRaw), $mR)) {
$meta['cod_cab'] = trim($mR[1]);
$meta['conto_num'] = trim($mR[2]);
}
if ($ragSocRaw !== '') {
$meta['ragione_sociale_estratto'] = trim($ragSocRaw);
}
$d = trim($rawDescr);
$descBreve = $d;
$descEstesa = $d;
$causale = 'GENERICO';
// 1. VOSTRA DISPOSIZIONE (Bonifico fornitore / Disposizione uscita)
if (preg_match('/^VOSTRA DISPOSIZIONE\s*-\s*(?:VS\.DISP\.\s*RIF\.\s*([^\s]+))?\s*FAVORE\s+(.+?)(?:\s*-\s*ADD\.TOT)?\s*-\s*(.+)$/i', $d, $m)) {
$meta['tipo'] = 'disposizione_bonifico';
$meta['rif_disp'] = trim($m[1] ?? '');
$beneficiario = trim($m[2] ?? '');
$dettaglio = trim($m[3] ?? '');
$meta['beneficiario'] = $beneficiario;
$meta['dettaglio'] = $dettaglio;
if (preg_match('/(?:SALDO|SAKDO)?\s*FT\s*(?:N\.?|NUM\.?)?\s*(.+?)\s+DEL\s+([0-9\/\.\-]+)/i', $dettaglio, $mFt)) {
$meta['rif_fattura'] = trim($mFt[1]);
$meta['data_fattura'] = trim($mFt[2]);
}
$causale = 'BONIFICO';
$descBreve = 'Disposizione a favore di ' . $beneficiario . ($dettaglio !== '' ? (' - ' . $dettaglio) : '');
$descEstesa = 'Disposizione bonifico a favore di ' . $beneficiario
. (!empty($meta['rif_disp']) ? (' · Rif. ' . $meta['rif_disp']) : '')
. ($dettaglio !== '' ? (' · ' . $dettaglio) : '');
}
// 2. BONIF. VS. FAVORE / BON URG/ISTANT (Bonifico in entrata)
elseif (preg_match('/^BON(?:IF)?\.?\s*(?:URG\/ISTANT\s*)?VS\.?\s*(?:FAVORE|F)?\s*-\s*BON\.DA\s+(.+?)(?:\s*-\s*(.+))?$/i', $d, $m)) {
$isUrg = stripos($d, 'URG') !== false;
$meta['tipo'] = $isUrg ? 'bonifico_urgente_entrata' : 'bonifico_entrata';
$mittente = trim($m[1]);
$dettaglio = trim($m[2] ?? '');
$meta['mittente'] = $mittente;
if ($dettaglio !== '') {
$meta['dettaglio'] = $dettaglio;
}
$causale = 'BONIFICO';
$descBreve = ($isUrg ? 'Bonifico urgente da ' : 'Bonifico da ') . $mittente . ($dettaglio !== '' ? (' - ' . $dettaglio) : '');
$descEstesa = ($isUrg ? 'Bonifico urgente/istantaneo a Vostro favore da ' : 'Bonifico a Vostro favore da ') . $mittente
. ($dettaglio !== '' ? (' · Dettaglio: ' . $dettaglio) : '');
}
// 3. COMMISSIONI SU BONIFICI
elseif (preg_match('/^COMM\.SU BONIFICI\s*-\s*NS RIF\.\s*([^\s]+)\s*(.*)$/i', $d, $m)) {
$meta['tipo'] = 'commissioni_bonifico';
$rif = trim($m[1]);
$meta['rif_disp'] = $rif;
$causale = 'COMMISSIONI';
$descBreve = 'Commissioni su bonifico (Rif. ' . $rif . ')';
$descEstesa = 'Commissioni e spese su bonifico · Rif. ' . $rif;
}
// 4. ADDEBITO DIRETTO SDD
elseif (preg_match('/^ADDEBITO DIRETTO SDD\s*-\s*SDD CORE:\s*([^\s]+)\s+(.+)$/i', $d, $m)) {
$meta['tipo'] = 'addebito_sdd';
$sddCode = trim($m[1]);
$fornitore = trim($m[2]);
$meta['codice_sdd'] = $sddCode;
$meta['beneficiario'] = $fornitore;
$causale = 'SDD';
$descBreve = 'Addebito SDD ' . $fornitore;
$descEstesa = 'Addebito diretto SDD CORE: ' . $sddCode . ' · Fornitore: ' . $fornitore;
}
// 5. PAG. UTENZE VARIE / BOLLETTINI CBILL
elseif (preg_match('/^(?:PAG\.\s*UTENZE VARIE|COMMISSIONI)\s*-\s*(?:BOLL\.CBILL\s+(.+?)(?:\s*\(?PROFIL CBILL|\s*CBILL|\s*-|\s*$)|Bollettino\s+(.+?)\s+INCASSO UTE\s+Rif\.([^\s]+)\s+Nop\.([^\s]+))/i', $d, $m)) {
$isComm = stripos($d, 'COMMISSIONI') === 0;
$meta['tipo'] = $isComm ? 'commissioni_bollettino' : 'pagamento_utenze';
$ente = trim(!empty($m[1]) ? $m[1] : ($m[2] ?? ''));
$meta['beneficiario'] = $ente;
if (preg_match('/(?:PROFIL CBILL|CBILL)\s*([0-9]{10,})/i', $d, $mC)) {
$meta['codice_cbill'] = trim($mC[1]);
}
if (!empty($m[3])) {
$meta['rif_bollettino'] = trim($m[3]);
}
if (!empty($m[4])) {
$meta['nop_bollettino'] = trim($m[4]);
}
$causale = $isComm ? 'COMMISSIONI' : 'CBILL';
if ($isComm) {
$descBreve = 'Commissioni bollettino ' . $ente;
$descEstesa = 'Commissioni pagamento bollettino · Ente: ' . $ente
. (!empty($meta['codice_cbill']) ? (' · CBILL: ' . $meta['codice_cbill']) : '')
. (!empty($meta['rif_bollettino']) ? (' · Rif: ' . $meta['rif_bollettino']) : '');
} else {
$descBreve = 'Pagamento utenze ' . (!empty($meta['codice_cbill']) ? 'CBILL ' : '') . $ente;
$descEstesa = 'Pagamento utenze bollettino · Ente: ' . $ente
. (!empty($meta['codice_cbill']) ? (' · Codice CBILL: ' . $meta['codice_cbill']) : '')
. (!empty($meta['rif_bollettino']) ? (' · Rif: ' . $meta['rif_bollettino']) : '');
}
}
// 6. I24 AGENZIA ENTRATE (F24 Telematico)
elseif (preg_match('/^I24 AGENZIA ENTRATE\s*-\s*PAG\.TO TELEMATICO\s*-\s*DATA INCASSO\s*([0-9\/]+)\s*(.+)$/i', $d, $m)) {
$meta['tipo'] = 'f24_agenzia_entrate';
$meta['beneficiario'] = 'AGENZIA DELLE ENTRATE';
$dataIncasso = trim($m[1]);
$proto = trim($m[2]);
$meta['data_incasso'] = $dataIncasso;
$meta['protocollo_f24'] = $proto;
$causale = 'F24';
$descBreve = 'F24 Telematico Agenzia Entrate (Incasso ' . $dataIncasso . ')';
$descEstesa = 'Modello F24 telematico Agenzia delle Entrate · Data incasso: ' . $dataIncasso . ' · Protocollo: ' . $proto;
}
// 7. INT. E COMP. (Interessi e Competenze)
elseif (preg_match('/^INT\.\s*E COMP\.\s*-\s*(.+)$/i', $d, $m)) {
$meta['tipo'] = 'interessi_competenze';
$dettaglio = trim($m[1]);
$meta['dettaglio'] = $dettaglio;
$isCred = stripos($dettaglio, 'CREDITORI') !== false;
$causale = $isCred ? 'INTERESSI' : 'COMPETENZE';
$descBreve = $isCred ? 'Interessi creditori di conto' : 'Competenze e spese tenuta conto';
$descEstesa = 'Interessi e competenze bancarie · ' . $dettaglio;
}
// 8. DEBIT PAGAMENTO (POS / Carta)
elseif (preg_match('/^DEBIT PAGAMENTO\s*-\s*CARTA\*(\d+)-([0-9:]+)-(.+)$/i', $d, $m)) {
$meta['tipo'] = 'pagamento_pos';
$carta = trim($m[1]);
$ora = trim($m[2]);
$esercente = trim($m[3]);
$meta['carta'] = $carta;
$meta['ora'] = $ora;
$meta['beneficiario'] = $esercente;
$causale = 'POS';
$descBreve = 'Pagamento POS Carta *' . $carta . ' - ' . $esercente;
$descEstesa = 'Pagamento con carta di debito *' . $carta . ' · Ora: ' . $ora . ' · Esercente: ' . $esercente;
}
// 9. DEBIT PRELIEVO ATM
elseif (preg_match('/^DEBIT PREL(?:IEVO|\.NS)? ATM\s*-\s*CARTA\*(\d+)-([0-9:]+)-(.+)$/i', $d, $m)) {
$meta['tipo'] = 'prelievo_atm';
$carta = trim($m[1]);
$ora = trim($m[2]);
$sportello = trim($m[3]);
$meta['carta'] = $carta;
$meta['ora'] = $ora;
$meta['dettaglio'] = $sportello;
$causale = 'PRELIEVO';
$descBreve = 'Prelievo ATM Carta *' . $carta . ' - ' . $sportello;
$descEstesa = 'Prelievo contanti ATM Carta *' . $carta . ' · Ora: ' . $ora . ' · Sportello: ' . $sportello;
}
// 10. IMP. BOLLO CC/LR
elseif (preg_match('/^IMP\.\s*BOLLO CC\/LR\s*-\s*(.+)$/i', $d, $m)) {
$meta['tipo'] = 'imposta_bollo';
$periodo = trim($m[1]);
$meta['dettaglio'] = $periodo;
$causale = 'BOLLO';
$descBreve = 'Imposta di bollo c/c (' . $periodo . ')';
$descEstesa = 'Imposta di bollo conto corrente / libretto · Periodo: ' . $periodo;
}
// 11. SPESE E CANONE CARTA
elseif (preg_match('/^(?:CANONE|RATEO CANONE|EMISS\/ATTIV) CARTA\s*-\s*(.+)$/i', $d, $m)) {
$meta['tipo'] = 'spese_carta';
$dettaglio = trim($m[1]);
$meta['dettaglio'] = $dettaglio;
$causale = 'CARTA';
$descBreve = 'Spese / canone carta (' . $dettaglio . ')';
$descEstesa = 'Spese di gestione e canone carta di debito · ' . $dettaglio;
}
// 12. STORNO SCRITTURE
elseif (preg_match('/^STORNO SCRITTURE\s*-\s*(.+)$/i', $d, $m)) {
$meta['tipo'] = 'storno_scritture';
$dettaglio = trim($m[1]);
$meta['dettaglio'] = $dettaglio;
$causale = 'STORNO';
$descBreve = 'Storno scritture bancarie - ' . $dettaglio;
$descEstesa = 'Storno operazione contabile · ' . $dettaglio;
}
// 13. VERSAMENTO CONTANTI
elseif (preg_match('/^VERS\.\s*CONTANTI\s*-\s*(.+)$/i', $d, $m)) {
$meta['tipo'] = 'versamento_contanti';
$causale = 'VERSAMENTO';
$descBreve = 'Versamento contanti in cassa / sportello';
$descEstesa = 'Versamento contanti in cassa / sportello · ' . trim($m[1]);
} else {
// Default fallback
if ($categoria !== null && $categoria !== '') {
$causale = mb_substr($categoria, 0, 20);
}
}
return [
'descrizione_breve' => mb_substr($descBreve, 0, 255),
'descrizione_estesa' => $descEstesa,
'causale' => $causale,
'match_data' => $meta,
];
}
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);
}
}