434 lines
17 KiB
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);
|
|
}
|
|
}
|
|
|