unicreditSemicolonCsvParser = $unicreditSemicolonCsvParser ?? new UnicreditSemicolonCsvParser(); $this->cedhouseCsvParser = $cedhouseCsvParser ?? new CedhouseCsvParser(); $this->mpsQifParser = $mpsQifParser ?? new MpsQifParser(); } /** * Import movimenti da CSV (separatore ;) con header "Data contabile" ecc., legato a un conto specifico. * * @return array{imported:int, duplicates:int, meta?:array} */ public function importUnicreditSemicolonCsvForConto( string $content, int $stabileId, int $contoId, ?string $sourceFile = null, ?int $gestioneId = null, bool $replace = false, ): array { $parsed = $this->unicreditSemicolonCsvParser->parse($content); $iban = null; try { $iban = DatiBancari::query()->where('id', $contoId)->value('iban'); $iban = is_string($iban) && trim($iban) !== '' ? trim($iban) : null; } catch (\Throwable) { $iban = null; } $imported = 0; $duplicates = 0; $minDateStr = null; $maxDateStr = null; if (! empty($parsed['rows'])) { $min = null; $max = null; foreach ($parsed['rows'] as $r) { $d = $r['data'] ?? null; if (! $d) { continue; } $min = $min ? ($d->lt($min) ? $d : $min) : $d; $max = $max ? ($d->gt($max) ? $d : $max) : $d; } $minDateStr = $min ? $min->toDateString() : null; $maxDateStr = $max ? $max->toDateString() : null; } DB::transaction(function () use ($parsed, $stabileId, $contoId, $iban, $sourceFile, $gestioneId, $replace, &$imported, &$duplicates) { if ($replace && ! empty($parsed['rows'])) { $minDate = null; $maxDate = null; foreach ($parsed['rows'] as $r) { $d = $r['data'] ?? null; if (! $d) { continue; } $minDate = $minDate ? ($d->lt($minDate) ? $d : $minDate) : $d; $maxDate = $maxDate ? ($d->gt($maxDate) ? $d : $maxDate) : $d; } if ($minDate && $maxDate) { MovimentoBanca::query() ->where('stabile_id', $stabileId) ->where('conto_id', $contoId) ->whereNotNull('raw_line') ->whereNull('registrazione_id') ->whereDate('data', '>=', $minDate->toDateString()) ->whereDate('data', '<=', $maxDate->toDateString()) ->delete(); } } foreach ($parsed['rows'] as $row) { $hashBase = implode('|', [ $iban ?? ('CONTO:' . $contoId), $row['data']->format('Y-m-d'), ($row['valuta']?->format('Y-m-d') ?? ''), $row['descrizione'], number_format((float) $row['importo'], 2, '.', ''), $row['causale'] ?? '', ]); $rowHash = hash('sha256', $hashBase); $exists = MovimentoBanca::query() ->where('stabile_id', $stabileId) ->where('row_hash', $rowHash) ->exists(); if ($exists) { $duplicates++; continue; } $payload = [ 'stabile_id' => $stabileId, 'conto_id' => $contoId, 'gestione_id' => $gestioneId, 'iban' => $iban, 'data' => $row['data'], 'valuta' => $row['valuta'], 'descrizione' => $row['descrizione'], 'importo' => $row['importo'], 'causale' => $row['causale'], 'source_file' => $sourceFile, 'raw_line' => $row['raw_line'], 'row_hash' => $rowHash, ]; if (Schema::hasColumn('contabilita_movimenti_banca', 'descrizione_estesa')) { $payload['descrizione_estesa'] = isset($row['descrizione_estesa']) && is_string($row['descrizione_estesa']) ? trim($row['descrizione_estesa']) : null; if (($payload['descrizione_estesa'] ?? null) === null && is_string($row['raw_line'] ?? null) && str_contains($row['raw_line'], '|')) { $parts = explode('|', (string) $row['raw_line'], 2); $fallback = trim((string) ($parts[1] ?? '')); $payload['descrizione_estesa'] = $fallback !== '' ? $fallback : null; } } if (Schema::hasColumn('contabilita_movimenti_banca', 'descrizione_estesa')) { $payload['descrizione_estesa'] = isset($row['descrizione_estesa']) && is_string($row['descrizione_estesa']) ? trim($row['descrizione_estesa']) : null; } $payload = $this->appendMatchData($payload, $row['descrizione'] ?? null, $row['raw_line'] ?? null, $row['descrizione'] ?? null); MovimentoBanca::query()->create($payload); $imported++; } }); $meta = []; if (isset($parsed['meta']) && is_array($parsed['meta'])) { $meta = $parsed['meta']; } if ($minDateStr && $maxDateStr) { $meta['min_date'] = $minDateStr; $meta['max_date'] = $maxDateStr; } return ['imported' => $imported, 'duplicates' => $duplicates, 'meta' => $meta]; } /** * Import CSV legato a un conto, scegliendo automaticamente il parser (multi-banca). * * Attualmente supporta: * - Unicredit semicolon ("Data contabile;...;Accrediti;Addebiti") * - Unicredit WRI ("Data;Valuta;Descrizione;Euro;") * - Intesa CSV (header tipici "data operazione" / "data valuta") * - CEDHOUSE ("R;NR;Data;Tipo Riga;Descrizione;Entrate;Uscite;...") * * @return array{imported:int, duplicates:int, meta?:array} */ public function importCsvAutoForConto( string $content, int $stabileId, int $contoId, ?string $sourceFile = null, ?int $gestioneId = null, bool $replace = false, ): array { $format = $this->detectCsvFormat($content); if ($format === 'unicredit_semicolon') { $res = $this->importUnicreditSemicolonCsvForConto($content, $stabileId, $contoId, $sourceFile, $gestioneId, $replace); $res['meta'] = array_merge(['format' => $format], is_array($res['meta'] ?? null) ? $res['meta'] : []); return $res; } if ($format === 'unicredit_wri') { $parsed = $this->unicreditParser->parse($content); return $this->importParsedRowsForConto( $parsed, $stabileId, $contoId, $sourceFile, $gestioneId, $replace, $format, ); } if ($format === 'intesa_csv') { $parsed = $this->intesaParser->parse($content); return $this->importParsedRowsForConto( $parsed, $stabileId, $contoId, $sourceFile, $gestioneId, $replace, $format, ); } if ($format === 'cedhouse_csv') { $parsed = $this->cedhouseCsvParser->parse($content); return $this->importParsedRowsForConto( $parsed, $stabileId, $contoId, $sourceFile, $gestioneId, $replace, $format, ); } // Fallback: prova in sequenza (utile per CSV "strani" o header varianti) $errors = []; foreach (['unicredit_wri', 'intesa_csv', 'cedhouse_csv', 'unicredit_semicolon'] as $try) { try { if ($try === 'unicredit_semicolon') { $res = $this->importUnicreditSemicolonCsvForConto($content, $stabileId, $contoId, $sourceFile, $gestioneId, $replace); $res['meta'] = array_merge(['format' => $try], is_array($res['meta'] ?? null) ? $res['meta'] : []); return $res; } $parsed = $try === 'unicredit_wri' ? $this->unicreditParser->parse($content) : ($try === 'cedhouse_csv' ? $this->cedhouseCsvParser->parse($content) : $this->intesaParser->parse($content)); return $this->importParsedRowsForConto($parsed, $stabileId, $contoId, $sourceFile, $gestioneId, $replace, $try); } catch (\Throwable $e) { $errors[] = $try . ': ' . $e->getMessage(); } } throw new \RuntimeException('Formato CSV non riconosciuto. Tentativi: ' . implode(' | ', $errors)); } /** * @return array{imported:int, duplicates:int, meta?:array} */ public function importUnicreditWriForConto( string $content, int $stabileId, int $contoId, ?string $sourceFile = null, ?int $gestioneId = null, bool $replace = false, ): array { $parsed = $this->unicreditParser->parse($content); return $this->importParsedRowsForConto( $parsed, $stabileId, $contoId, $sourceFile, $gestioneId, $replace, 'unicredit_wri', ); } /** * @return array{imported:int, duplicates:int, meta?:array} */ public function importIntesaCsvForConto( string $content, int $stabileId, int $contoId, ?string $sourceFile = null, ?int $gestioneId = null, bool $replace = false, ): array { $parsed = $this->intesaParser->parse($content); return $this->importParsedRowsForConto( $parsed, $stabileId, $contoId, $sourceFile, $gestioneId, $replace, 'intesa_csv', ); } /** * @return array{imported:int, duplicates:int, meta?:array} */ public function importMpsQifForConto( string $content, int $stabileId, int $contoId, ?string $sourceFile = null, ?int $gestioneId = null, bool $replace = false, ): array { $parsed = $this->mpsQifParser->parse($content); return $this->importParsedRowsForConto( $parsed, $stabileId, $contoId, $sourceFile, $gestioneId, $replace, 'mps_qif', ); } /** * @param array{rows?:array, meta?:array} $parsed * @return array{imported:int, duplicates:int, meta?:array} */ private function importParsedRowsForConto( array $parsed, int $stabileId, int $contoId, ?string $sourceFile, ?int $gestioneId, bool $replace, string $format, ): array { $rows = $parsed['rows'] ?? []; if (! is_array($rows) || empty($rows)) { throw new \RuntimeException('Nessuna riga trovata nel file (' . $format . ').'); } $iban = null; try { $iban = DatiBancari::query()->where('id', $contoId)->value('iban'); $iban = is_string($iban) && trim($iban) !== '' ? trim($iban) : null; } catch (\Throwable) { $iban = null; } $imported = 0; $duplicates = 0; $hasDescrizioneEstesa = false; try { $hasDescrizioneEstesa = DB::getSchemaBuilder()->hasColumn('contabilita_movimenti_banca', 'descrizione_estesa'); } catch (\Throwable) { $hasDescrizioneEstesa = false; } $minDateStr = null; $maxDateStr = null; $min = null; $max = null; foreach ($rows as $r) { $d = $r['data'] ?? null; if (! $d) { continue; } $min = $min ? ($d->lt($min) ? $d : $min) : $d; $max = $max ? ($d->gt($max) ? $d : $max) : $d; } $minDateStr = $min ? $min->toDateString() : null; $maxDateStr = $max ? $max->toDateString() : null; DB::transaction(function () use ($rows, $stabileId, $contoId, $iban, $sourceFile, $gestioneId, $replace, $hasDescrizioneEstesa, &$imported, &$duplicates) { if ($replace && ! empty($rows)) { $minDate = null; $maxDate = null; foreach ($rows as $r) { $d = $r['data'] ?? null; if (! $d) { continue; } $minDate = $minDate ? ($d->lt($minDate) ? $d : $minDate) : $d; $maxDate = $maxDate ? ($d->gt($maxDate) ? $d : $maxDate) : $d; } if ($minDate && $maxDate) { MovimentoBanca::query() ->where('stabile_id', $stabileId) ->where('conto_id', $contoId) ->whereNotNull('raw_line') ->whereNull('registrazione_id') ->whereDate('data', '>=', $minDate->toDateString()) ->whereDate('data', '<=', $maxDate->toDateString()) ->delete(); } } foreach ($rows as $row) { $hashBase = implode('|', [ $iban ?? ('CONTO:' . $contoId), $row['data']->format('Y-m-d'), ($row['valuta']?->format('Y-m-d') ?? ''), $row['descrizione'], number_format((float) $row['importo'], 2, '.', ''), $row['causale'] ?? '', ]); $rowHash = hash('sha256', $hashBase); $exists = MovimentoBanca::query() ->where('stabile_id', $stabileId) ->where('row_hash', $rowHash) ->exists(); if ($exists) { $duplicates++; continue; } $payload = [ 'stabile_id' => $stabileId, 'conto_id' => $contoId, 'gestione_id' => $gestioneId, 'iban' => $iban, 'data' => $row['data'], 'valuta' => $row['valuta'], 'descrizione' => $row['descrizione'], 'importo' => $row['importo'], 'causale' => $row['causale'], 'source_file' => $sourceFile, 'raw_line' => $row['raw_line'], 'row_hash' => $rowHash, ]; if ($hasDescrizioneEstesa) { $descEst = isset($row['descrizione_estesa']) && is_string($row['descrizione_estesa']) ? trim($row['descrizione_estesa']) : null; if (! $descEst && isset($row['raw_line']) && is_string($row['raw_line'])) { $descEst = trim($row['raw_line']); } $payload['descrizione_estesa'] = $descEst !== '' ? $descEst : null; } MovimentoBanca::query()->create($payload); $imported++; } }); $meta = []; if (isset($parsed['meta']) && is_array($parsed['meta'])) { $meta = $parsed['meta']; } $meta['format'] = $format; if ($minDateStr && $maxDateStr) { $meta['min_date'] = $minDateStr; $meta['max_date'] = $maxDateStr; } return ['imported' => $imported, 'duplicates' => $duplicates, 'meta' => $meta]; } private function detectCsvFormat(string $content): ?string { $head = mb_strtolower(substr($content, 0, 10000)); // Unicredit nuovo formato: Data contabile;...;Accrediti;Addebiti if (str_contains($head, 'data contabile') && str_contains($head, 'accrediti') && str_contains($head, 'addebiti')) { return 'unicredit_semicolon'; } // Unicredit WRI: Data;Valuta;Descrizione;Euro; if (str_contains($head, 'data;valuta;descrizione') && str_contains($head, 'euro')) { return 'unicredit_wri'; } // Intesa: header tipici if (str_contains($head, 'data operazione') && str_contains($head, 'data valuta')) { return 'intesa_csv'; } // CEDHOUSE: header tipico "R;NR;Data;Tipo Riga;Descrizione;Entrate;Uscite" if (str_contains($head, 'tipo riga') && str_contains($head, 'entrate') && str_contains($head, 'uscite')) { return 'cedhouse_csv'; } return null; } /** * @return array{imported:int, duplicates:int} */ public function importUnicreditWri( string $content, int $stabileId, ?string $sourceFile = null, ?string $forcedIban = null, ?int $gestioneId = null, bool $replace = false, ): array { $parsed = $this->unicreditParser->parse($content); $iban = $forcedIban ?: ($parsed['iban'] ?? null); $contoId = null; try { if ( is_string($iban) && trim($iban) !== '' && DB::getSchemaBuilder()->hasTable('dati_bancari') && DB::getSchemaBuilder()->hasColumn('contabilita_movimenti_banca', 'conto_id') ) { $contoId = DatiBancari::query() ->where('stabile_id', $stabileId) ->where('iban', trim($iban)) ->orderBy('id') ->value('id'); $contoId = is_numeric($contoId) ? (int) $contoId : null; } } catch (\Throwable) { $contoId = null; } $imported = 0; $duplicates = 0; DB::transaction(function () use ($parsed, $stabileId, $contoId, $iban, $sourceFile, $gestioneId, $replace, &$imported, &$duplicates) { if ($replace && ! empty($parsed['rows'])) { $minDate = null; $maxDate = null; foreach ($parsed['rows'] as $r) { $d = $r['data'] ?? null; if (! $d) { continue; } $minDate = $minDate ? ($d->lt($minDate) ? $d : $minDate) : $d; $maxDate = $maxDate ? ($d->gt($maxDate) ? $d : $maxDate) : $d; } if ($minDate && $maxDate) { // Sicurezza: elimina solo movimenti importati e non riconciliati. $del = MovimentoBanca::query() ->where('stabile_id', $stabileId) ->whereNotNull('raw_line') ->whereNull('registrazione_id') ->whereDate('data', '>=', $minDate->toDateString()) ->whereDate('data', '<=', $maxDate->toDateString()); if ($contoId !== null) { $del->where('conto_id', $contoId); } elseif (is_string($iban) && trim($iban) !== '') { $del->where('iban', trim($iban)); } $del->delete(); } } foreach ($parsed['rows'] as $row) { $hashBase = implode('|', [ $iban ?? 'NOIBAN', $row['data']->format('Y-m-d'), ($row['valuta']?->format('Y-m-d') ?? ''), $row['descrizione'], number_format((float) $row['importo'], 2, '.', ''), $row['causale'] ?? '', ]); $rowHash = hash('sha256', $hashBase); $exists = MovimentoBanca::query() ->where('stabile_id', $stabileId) ->where('row_hash', $rowHash) ->exists(); if ($exists) { $duplicates++; continue; } MovimentoBanca::query()->create([ 'stabile_id' => $stabileId, 'conto_id' => $contoId, 'gestione_id' => $gestioneId, 'iban' => $iban, 'data' => $row['data'], 'valuta' => $row['valuta'], 'descrizione' => $row['descrizione'], 'importo' => $row['importo'], 'causale' => $row['causale'], 'source_file' => $sourceFile, 'raw_line' => $row['raw_line'], 'row_hash' => $rowHash, ]); $imported++; } }); return ['imported' => $imported, 'duplicates' => $duplicates]; } /** * @return array{imported:int, duplicates:int} */ public function importIntesaCsv( string $content, int $stabileId, ?string $sourceFile = null, ?string $forcedIban = null, ?int $gestioneId = null, bool $replace = false, ): array { $parsed = $this->intesaParser->parse($content); $iban = $forcedIban ?: ($parsed['iban'] ?? null); $hasDescrizioneEstesa = false; try { $hasDescrizioneEstesa = DB::getSchemaBuilder()->hasColumn('contabilita_movimenti_banca', 'descrizione_estesa'); } catch (\Throwable) { $hasDescrizioneEstesa = false; } $contoId = null; try { if ( is_string($iban) && trim($iban) !== '' && DB::getSchemaBuilder()->hasTable('dati_bancari') && DB::getSchemaBuilder()->hasColumn('contabilita_movimenti_banca', 'conto_id') ) { $contoId = DatiBancari::query() ->where('stabile_id', $stabileId) ->where('iban', trim($iban)) ->orderBy('id') ->value('id'); $contoId = is_numeric($contoId) ? (int) $contoId : null; } } catch (\Throwable) { $contoId = null; } $imported = 0; $duplicates = 0; DB::transaction(function () use ($parsed, $stabileId, $contoId, $iban, $sourceFile, $gestioneId, $replace, $hasDescrizioneEstesa, &$imported, &$duplicates) { if ($replace && ! empty($parsed['rows'])) { $minDate = null; $maxDate = null; foreach ($parsed['rows'] as $r) { $d = $r['data'] ?? null; if (! $d) { continue; } $minDate = $minDate ? ($d->lt($minDate) ? $d : $minDate) : $d; $maxDate = $maxDate ? ($d->gt($maxDate) ? $d : $maxDate) : $d; } if ($minDate && $maxDate) { $del = MovimentoBanca::query() ->where('stabile_id', $stabileId) ->whereNotNull('raw_line') ->whereNull('registrazione_id') ->whereDate('data', '>=', $minDate->toDateString()) ->whereDate('data', '<=', $maxDate->toDateString()); if ($contoId !== null) { $del->where('conto_id', $contoId); } elseif (is_string($iban) && trim($iban) !== '') { $del->where('iban', trim($iban)); } $del->delete(); } } foreach ($parsed['rows'] as $row) { $hashBase = implode('|', [ $iban ?? 'NOIBAN', $row['data']->format('Y-m-d'), ($row['valuta']?->format('Y-m-d') ?? ''), $row['descrizione'], number_format((float) $row['importo'], 2, '.', ''), $row['causale'] ?? '', ]); $rowHash = hash('sha256', $hashBase); $exists = MovimentoBanca::query() ->where('stabile_id', $stabileId) ->where('row_hash', $rowHash) ->exists(); if ($exists) { $duplicates++; continue; } $payload = [ 'stabile_id' => $stabileId, 'conto_id' => $contoId, 'gestione_id' => $gestioneId, 'iban' => $iban, 'data' => $row['data'], 'valuta' => $row['valuta'], 'descrizione' => $row['descrizione'], 'importo' => $row['importo'], 'causale' => $row['causale'], 'source_file' => $sourceFile, 'raw_line' => $row['raw_line'], 'row_hash' => $rowHash, ]; if ($hasDescrizioneEstesa) { $payload['descrizione_estesa'] = isset($row['descrizione_estesa']) && is_string($row['descrizione_estesa']) ? trim($row['descrizione_estesa']) : null; } $payload = $this->appendMatchData($payload, $row['descrizione_estesa'] ?? null, $row['raw_line'] ?? null, $row['descrizione'] ?? null); MovimentoBanca::query()->create($payload); $imported++; } }); return ['imported' => $imported, 'duplicates' => $duplicates]; } /** * Import movimenti da XLSX, legato a un conto specifico. * * @return array{imported:int, duplicates:int, meta?:array} */ public function importExcelXlsxForConto( string $filePath, int $stabileId, int $contoId, ?string $sourceFile = null, ?int $gestioneId = null, bool $replace = false, ): array { $parsed = $this->excelParser->parseXlsx($filePath); $iban = null; try { $iban = DatiBancari::query()->where('id', $contoId)->value('iban'); $iban = is_string($iban) && trim($iban) !== '' ? trim($iban) : null; } catch (\Throwable) { $iban = null; } $imported = 0; $duplicates = 0; $minDateStr = null; $maxDateStr = null; if (! empty($parsed['rows'])) { $min = null; $max = null; foreach ($parsed['rows'] as $r) { $d = $r['data'] ?? null; if (! $d) { continue; } $min = $min ? ($d->lt($min) ? $d : $min) : $d; $max = $max ? ($d->gt($max) ? $d : $max) : $d; } $minDateStr = $min ? $min->toDateString() : null; $maxDateStr = $max ? $max->toDateString() : null; } DB::transaction(function () use ($parsed, $stabileId, $contoId, $iban, $sourceFile, $gestioneId, $replace, &$imported, &$duplicates) { if ($replace && ! empty($parsed['rows'])) { $minDate = null; $maxDate = null; foreach ($parsed['rows'] as $r) { $d = $r['data'] ?? null; if (! $d) { continue; } $minDate = $minDate ? ($d->lt($minDate) ? $d : $minDate) : $d; $maxDate = $maxDate ? ($d->gt($maxDate) ? $d : $maxDate) : $d; } if ($minDate && $maxDate) { MovimentoBanca::query() ->where('stabile_id', $stabileId) ->where('conto_id', $contoId) ->whereNotNull('raw_line') ->whereNull('registrazione_id') ->whereDate('data', '>=', $minDate->toDateString()) ->whereDate('data', '<=', $maxDate->toDateString()) ->delete(); } } foreach ($parsed['rows'] as $row) { $hashBase = implode('|', [ $iban ?? ('CONTO:' . $contoId), $row['data']->format('Y-m-d'), ($row['valuta']?->format('Y-m-d') ?? ''), $row['descrizione'], number_format((float) $row['importo'], 2, '.', ''), $row['causale'] ?? '', ]); $rowHash = hash('sha256', $hashBase); $exists = MovimentoBanca::query() ->where('stabile_id', $stabileId) ->where('row_hash', $rowHash) ->exists(); if ($exists) { $duplicates++; continue; } $payload = [ 'stabile_id' => $stabileId, 'conto_id' => $contoId, 'gestione_id' => $gestioneId, 'iban' => $iban, 'data' => $row['data'], 'valuta' => $row['valuta'], 'descrizione' => $row['descrizione'], 'importo' => $row['importo'], 'causale' => $row['causale'], 'source_file' => $sourceFile, 'raw_line' => $row['raw_line'], 'row_hash' => $rowHash, ]; if (Schema::hasColumn('contabilita_movimenti_banca', 'descrizione_estesa')) { $descEst = isset($row['descrizione_estesa']) && is_string($row['descrizione_estesa']) ? trim($row['descrizione_estesa']) : null; if (($descEst ?? '') === '' && is_string($row['raw_line'] ?? null) && str_contains((string) $row['raw_line'], '|')) { $parts = explode('|', (string) $row['raw_line'], 2); $fallback = trim((string) ($parts[1] ?? '')); $descEst = $fallback !== '' ? $fallback : null; } $payload['descrizione_estesa'] = $descEst !== '' ? $descEst : null; } $payload = $this->appendMatchData($payload, $row['descrizione_estesa'] ?? null, $row['raw_line'] ?? null, $row['descrizione'] ?? null); MovimentoBanca::query()->create($payload); $imported++; } }); $meta = []; if (isset($parsed['meta']) && is_array($parsed['meta'])) { $meta = $parsed['meta']; } if ($minDateStr && $maxDateStr) { $meta['min_date'] = $minDateStr; $meta['max_date'] = $maxDateStr; } return ['imported' => $imported, 'duplicates' => $duplicates, 'meta' => $meta]; } private function appendMatchData(array $payload, ?string $descrizioneEstesa, ?string $fallbackRaw, ?string $fallbackDescrizione): array { $hasMatch = Schema::hasColumn('contabilita_movimenti_banca', 'match_data'); $hasConfirm = Schema::hasColumn('contabilita_movimenti_banca', 'da_confermare'); $hasRubricaMitt = Schema::hasColumn('contabilita_movimenti_banca', 'rubrica_mittente_id'); $hasRubricaBen = Schema::hasColumn('contabilita_movimenti_banca', 'rubrica_beneficiario_id'); $hasFornitore = Schema::hasColumn('contabilita_movimenti_banca', 'fornitore_id'); if (! $hasMatch && ! $hasConfirm && ! $hasRubricaMitt && ! $hasRubricaBen && ! $hasFornitore) { return $payload; } $text = is_string($descrizioneEstesa) && trim($descrizioneEstesa) !== '' ? trim($descrizioneEstesa) : (is_string($fallbackRaw) && trim($fallbackRaw) !== '' ? trim($fallbackRaw) : (is_string($fallbackDescrizione) ? trim($fallbackDescrizione) : '')); if ($text === '') { return $payload; } $match = $this->parseDescrizioneEstesa($text); if ($hasMatch && ! empty($match)) { $payload['match_data'] = $match; } $mittente = $match['mittente'] ?? null; $beneficiario = $match['beneficiario'] ?? null; $rubricaMittenteId = null; $rubricaBeneficiarioId = null; $fornitoreId = null; if ($mittente) { $rubricaMittenteId = $this->matchRubricaId($mittente); $fornitoreId = $this->matchFornitoreId($mittente); } if ($beneficiario) { $rubricaBeneficiarioId = $this->matchRubricaId($beneficiario); } if ($hasRubricaMitt && $rubricaMittenteId) { $payload['rubrica_mittente_id'] = $rubricaMittenteId; } if ($hasRubricaBen && $rubricaBeneficiarioId) { $payload['rubrica_beneficiario_id'] = $rubricaBeneficiarioId; } if ($hasFornitore && $fornitoreId) { $payload['fornitore_id'] = $fornitoreId; } if ($hasConfirm) { $needs = ! empty($match) && ! $rubricaMittenteId && ! $fornitoreId; $payload['da_confermare'] = $needs; } return $payload; } private function parseDescrizioneEstesa(string $text): array { $res = []; $text = trim(preg_replace('/\s+/', ' ', $text)); if ($text === '') { return $res; } if (preg_match('/COD\.\s*DISP\.:\s*([A-Z0-9]+)/i', $text, $m)) { $res['cod_disp'] = trim($m[1]); } if (preg_match('/RIF\.?\s*([^\n]+?)(?=\s+MITT\.|\s+BENEF\.|\s+BIC\.|$)/i', $text, $m)) { $res['riferimento'] = trim($m[1]); } if (preg_match('/MITT\.:\s*([^\n]+?)(?=\s+BENEF\.|\s+BIC\.|$)/i', $text, $m)) { $res['mittente'] = trim($m[1]); } if (preg_match('/BENEF\.:\s*([^\n]+?)(?=\s+BIC\.|$)/i', $text, $m)) { $res['beneficiario'] = trim($m[1]); } if (preg_match('/BONIFICO\s+A\s+VOSTRO\s+FAVORE\s+DISPOSTO\s+DA\s*:\s*MITT\.:/i', $text)) { $res['descrizione_base'] = 'Bonifico a Vostro favore disposto da:'; } if (preg_match('/\bBONINEURVERU\b/i', $text)) { if (preg_match('/\b(\d{10,})\b/', $text, $mDisp)) { $res['cod_disp'] = trim($mDisp[1]); } if (preg_match('/\b(\d{8}-[A-Za-z0-9]+)\b/', $text, $mRef)) { $res['cod_ref'] = trim($mRef[1]); } } if (preg_match('/\bCOSPERBON\b/i', $text)) { if (preg_match('/\b(\d{8}-[A-Za-z0-9]+)\b/', $text, $mRef)) { $res['cod_ref'] = trim($mRef[1]); } if (preg_match('/\b(\d{10,})\b/', $text, $mDisp)) { $res['cod_disp'] = trim($mDisp[1]); } } if (preg_match('/\bCASH\s+NOT\s+PROVIDED\b/i', $text)) { $res['cash'] = 'NOT PROVIDED'; } elseif (preg_match('/\bCASH\s+([0-9]{3,})\b/i', $text, $m)) { $res['cash'] = trim($m[1]); } if (preg_match('/\b([A-Z])\s*0?(\d{1,3})\b/', $text, $m)) { $res['unita'] = strtoupper($m[1]) . str_pad($m[2], 2, '0', STR_PAD_LEFT); } if (preg_match('/\bordinaria\s+([^\n]+?)(?=\s+[A-Z]\s*\d{1,3}|\s+Bonifico|$)/i', $text, $m)) { $res['periodo_rate'] = trim($m[1]); } if (preg_match('/\b(Altri\s+pagamenti|Bonifici?|Addebiti?|Prelievi?|Commissioni?)\b/i', $text, $m)) { $res['payment_group'] = strtoupper(trim((string) $m[1])); } if (preg_match('/\bFav\.?\s+([^\n]+?)(?=\s+(?:Cbill|CBILL|SIA\b|Da\s+Digital\s+Banking|Di\s+Cui\s+Commissioni|T[\-\+0-9\.,]|$))/i', $text, $m)) { $provider = trim((string) $m[1]); if ($provider !== '') { $res['payment_provider'] = $provider; if (empty($res['beneficiario'])) { $res['beneficiario'] = $provider; } } } if (preg_match('/\bCbill\b\s*(?:N\.)?\s*\.?\s*([0-9]{9,})/i', $text, $m)) { $res['cbill'] = trim((string) $m[1]); } if (preg_match('/\bSIA\b[:\s]*([A-Z0-9]{3,10})/i', $text, $m)) { $res['sia'] = strtoupper(trim((string) $m[1])); } elseif (preg_match('/\b([A-Z][A-Z0-9]{3,6})\b(?=\s+Cbill\b)/i', $text, $m)) { $candidate = strtoupper(trim((string) $m[1])); if (! in_array($candidate, ['CBILL', 'FAV'], true)) { $res['sia'] = $candidate; } } if (preg_match('/Di\s+Cui\s+Commissioni:\s*([0-9\.,]+)\s*Euro/i', $text, $m)) { $commissione = $this->parseItalianDecimal($m[1] ?? null); if ($commissione !== null) { $res['commissioni_euro'] = $commissione; } } if (preg_match('/\bDa\s+Digital\s+Banking\b/i', $text)) { $res['payment_channel'] = 'digital_banking'; } $providerUpper = strtoupper(trim((string) ($res['payment_provider'] ?? $res['beneficiario'] ?? ''))); if ($providerUpper !== '') { if (str_contains($providerUpper, 'ACEA ATO2') || str_contains($providerUpper, 'ACEA ACQUA')) { $res['utility_type'] = 'acqua'; $res['suggested_voce_spesa_code'] = 'ACQ'; } elseif (str_contains($providerUpper, 'A2A')) { $res['utility_type'] = 'utenza'; } } if (empty($res) && str_contains($text, 'Bonifico a Vostro favore')) { $res['descrizione_raw'] = $text; } return $res; } private function matchRubricaId(string $name): ?int { $term = trim($name); if ($term === '') { return null; } $like = '%' . str_replace(['%', '_'], ['\%', '\_'], $term) . '%'; $rows = RubricaUniversale::query() ->where(function ($q) use ($like) { $q->where('ragione_sociale', 'like', $like) ->orWhereRaw("CONCAT(nome,' ',cognome) LIKE ?", [$like]); }) ->limit(2) ->get(['id']); return $rows->count() === 1 ? (int) $rows->first()->id : null; } private function matchFornitoreId(string $name): ?int { $term = trim($name); if ($term === '') { return null; } $like = '%' . str_replace(['%', '_'], ['\%', '\_'], $term) . '%'; $rows = Fornitore::query() ->where(function ($q) use ($like) { $q->where('ragione_sociale', 'like', $like) ->orWhereRaw("CONCAT(nome,' ',cognome) LIKE ?", [$like]); }) ->limit(2) ->get(['id']); return $rows->count() === 1 ? (int) $rows->first()->id : null; } private function parseItalianDecimal(mixed $value): ?float { $value = trim((string) $value); if ($value === '') { return null; } $value = str_replace(['.', ' '], ['', ''], $value); $value = str_replace(',', '.', $value); return is_numeric($value) ? (float) $value : null; } }