where('stabile_id', $stabileId) ->where(function ($q) { $q->where('iban', self::IBAN_GERMANICO) ->orWhere('legacy_cod_cassa', 'CCB') ->orWhere('numero_conto', '233378'); }) ->first(); if ($conto) { return $conto; } return DatiBancari::create([ 'stabile_id' => $stabileId, 'tipo_conto' => 'corrente', 'denominazione_banca' => 'Banca del Fucino', 'numero_conto' => '233378', 'legacy_cod_cassa' => 'CCB', 'iban' => self::IBAN_GERMANICO, 'abi' => '03124', 'cab' => '03203', 'cin' => 'W', 'intestazione_conto' => 'CONDOMINIO VIA GERMANICO 96', 'data_saldo_iniziale' => '2022-10-01', 'saldo_iniziale' => 3433.49, 'valuta' => 'EUR', 'stato_conto' => 'attivo', 'is_nostro_conto' => true, 'note' => '[GESCON Germanico 96 CCB]', ]); } /** * Esegue sincronizzazione completa: importazione dai file .xls e riconciliazione contabile. */ public function syncAndReconcileAll(?string $dir = null, int $stabileId = self::STABILE_ID): array { $importRes = $this->importFromFiles($dir, $stabileId); $recRes = $this->reconcile($stabileId); return array_merge($importRes, $recRes); } /** * Importa tutti i file .xls della banca nella tabella contabilita_movimenti_banca. * * @return array{total_files: int, raw_rows: int, imported: int, duplicates: int} */ public function importFromFiles(?string $dir = null, int $stabileId = self::STABILE_ID): array { $dir = $dir ?: self::DEFAULT_BANCA_DIR; if (!is_dir($dir)) { return ['total_files' => 0, 'raw_rows' => 0, 'imported' => 0, 'duplicates' => 0, 'error' => "Directory {$dir} non trovata"]; } $conto = $this->ensureContoBancario($stabileId); $files = glob($dir . '/*.xls'); sort($files); $totalFiles = count($files); $rawRows = 0; $imported = 0; $duplicates = 0; foreach ($files as $filePath) { $fileName = basename($filePath); try { $parsed = $this->parser->parseXlsx($filePath); } catch (\Throwable $e) { Log::warning("Errore nel parsing del file bancario {$fileName}: " . $e->getMessage()); continue; } foreach ($parsed['rows'] as $r) { $rawRows++; $data = $r['data'] instanceof Carbon ? $r['data'] : Carbon::parse($r['data']); $valuta = !empty($r['valuta']) ? ($r['valuta'] instanceof Carbon ? $r['valuta'] : Carbon::parse($r['valuta'])) : null; $importo = round((float) $r['importo'], 2); $descrizione = trim((string) ($r['descrizione'] ?? '')); // Calcolo hash univoco di riga $hashBase = implode('|', [ $stabileId, $data->format('Y-m-d'), $valuta ? $valuta->format('Y-m-d') : '', number_format($importo, 2, '.', ''), $descrizione, ]); $rowHash = hash('sha256', $hashBase); $exists = MovimentoBanca::query() ->where('stabile_id', $stabileId) ->where('row_hash', $rowHash) ->exists(); if ($exists) { $duplicates++; continue; } // Estrazione dati strutturati dalla descrizione bancaria $mittente = null; $beneficiario = null; $cro = null; $note = null; $tipoOperazione = 'altro'; if ($importo > 0) { $tipoOperazione = 'incasso'; if (preg_match('/BONIFICO A VOSTRO FAVORE\s+([A-Z0-9\.\s\,\&\-\'\/\(\)]+?)\s+(?:Data|Coord|Banca|Cro|Note|Id)/i', $descrizione, $m)) { $mittente = trim($m[1]); } elseif (preg_match('/BONIFICO.*?FAVORE\s+([A-Z0-9\.\s\,\&\-\'\/\(\)]+)/i', $descrizione, $m)) { $mittente = trim($m[1]); } elseif (preg_match('/VERSAMENTO CONTANTE\s+([A-Z0-9\.\s\,\&\-\'\/\(\)]+)/i', $descrizione, $m)) { $mittente = trim($m[1]); } } else { $tipoOperazione = 'spesa'; if (preg_match('/ADDEBITO BONIFICO DA HOME BANKING\s+([A-Z0-9\.\s\,\&\-\'\/\(\)]+?)\s+(?:Bonifico|Data|Coord|Banca|Cro|Note|Id)/i', $descrizione, $m)) { $beneficiario = trim($m[1]); } elseif (preg_match('/ADDEBITO DIRETTO CORE RCUR.*?Prg\.Car\.:\s*\d+\s+([A-Z0-9\.\s\,\&\-\'\/\(\)]+?)\s+-\s+/i', $descrizione, $m)) { $beneficiario = trim($m[1]); } elseif (preg_match('/PAGAMENTO POS.*?C\/O\s+([A-Z0-9\.\s\,\&\-\'\/\(\)]+)/i', $descrizione, $m)) { $beneficiario = trim($m[1]); } elseif (str_contains($descrizione, 'COMM.')) { $beneficiario = 'Banca del Fucino (Commissioni)'; } } if (preg_match('/Cro:\s*([A-Za-z0-9]+)/i', $descrizione, $m)) { $cro = trim($m[1]); } if (preg_match('/Note:\s*(.+?)(?:Id\.Operazione|$)/i', $descrizione, $m)) { $note = trim($m[1]); } MovimentoBanca::create([ 'stabile_id' => $stabileId, 'conto_id' => $conto->id, 'iban' => self::IBAN_GERMANICO, 'data' => $data, 'valuta' => $valuta, 'descrizione' => $descrizione, 'descrizione_estesa' => $note ?: $descrizione, 'importo' => $importo, 'causale' => $r['causale'] ?? null, 'tipo_operazione' => $tipoOperazione, 'mittente' => $mittente, 'beneficiario' => $beneficiario, 'rif_disposizione' => $cro, 'banca_nome' => 'Banca del Fucino', 'source_file' => $fileName, 'raw_line' => $r['raw_line'] ?? null, 'row_hash' => $rowHash, 'stato_riconciliazione' => 'da_riconciliare', ]); $imported++; } } return [ 'total_files' => $totalFiles, 'raw_rows' => $rawRows, 'imported' => $imported, 'duplicates' => $duplicates, ]; } /** * Riconcilia tutti i movimenti bancari con gli Incassi Gescon (Inc_da_ec e incassi) * e con le Spese (Operazioni). * * @return array{total_movimenti: int, riconciliati_incassi: int, riconciliati_spese: int, totale_riconciliati: int, percentuale: float} */ public function reconcile(int $stabileId = self::STABILE_ID): array { $genMdb = "/mnt/gescon-archives/gescon/" . self::COD_STABILE . "/generale_stabile.mdb"; $incDaEc = $this->gesconService->runMdbExport($genMdb, 'Inc_da_ec'); // Mappa delle unità per scala e interno e legacy_cond_id $unitaList = UnitaImmobiliare::query()->where('stabile_id', $stabileId)->get(); $unitaByScInt = []; $unitaByLegacyId = []; foreach ($unitaList as $u) { $key = strtoupper(trim((string)$u->scala)) . '|' . trim((string)$u->interno); $unitaByScInt[$key] = $u; if (!empty($u->legacy_cond_id)) { $unitaByLegacyId[trim((string)$u->legacy_cond_id)] = $u; } } // Movimenti bancari dello stabile $movimenti = MovimentoBanca::query()->where('stabile_id', $stabileId)->get(); $totalMovimenti = $movimenti->count(); $riconciliatiIncassi = 0; $riconciliatiSpese = 0; // Indicizzazione Inc_da_ec per importo $ecByAmount = []; foreach ($incDaEc as $ec) { $amt = number_format(round((float)($ec['Importo'] ?? 0), 2), 2, '.', ''); $ecByAmount[$amt][] = $ec; } // Spese consolidate Gescon dagli archivi annuali (0914, 0913, 0912, 0909) $speseArchivio = []; foreach (['0914', '0913', '0912', '0909', '0005', '0004'] as $yrDir) { $singoloMdb = "/mnt/gescon-archives/gescon/" . self::COD_STABILE . "/{$yrDir}/singolo_anno.mdb"; if (!file_exists($singoloMdb)) continue; $ops = $this->gesconService->runMdbExport($singoloMdb, 'Operazioni'); foreach ($ops as $op) { $amt = round((float)($op['importo_euro'] ?? 0), 2); if ($amt > 0) { $dt = !empty($op['dt_spe']) ? date('Y-m-d', strtotime($op['dt_spe'])) : null; $speseArchivio[] = [ 'anno_dir' => $yrDir, 'id_operaz' => $op['id_operaz'] ?? null, 'data' => $dt, 'importo' => $amt, 'beneficiario' => trim((string)($op['benef'] ?? '')), 'cod_forn' => $op['cod_for'] ?? null, 'num_fat' => $op['num_fat'] ?? null, ]; } } } foreach ($movimenti as $mov) { $importo = (float) $mov->importo; $dataMov = $mov->data ? $mov->data->format('Y-m-d') : null; $desc = strtoupper($mov->descrizione); // 1. Riconciliazione INCASSI (importo > 0) if ($importo > 0) { $amtKey = number_format($importo, 2, '.', ''); $candidates = $ecByAmount[$amtKey] ?? []; $bestMatch = null; foreach ($candidates as $ec) { $ecDateRaw = $ec['Data_pag'] ?? ''; $ecDate = $ecDateRaw ? date('Y-m-d', strtotime($ecDateRaw)) : null; // Match esatto o per data ravvicinata (+/- 14 giorni) o per nome $nomeCond = strtoupper(trim((string)($ec['Nome_condomino'] ?? ''))); $cognomeTokens = array_filter(explode(' ', $nomeCond)); $nameMatched = false; foreach ($cognomeTokens as $tok) { if (strlen($tok) >= 4 && str_contains($desc, $tok)) { $nameMatched = true; break; } } $daysDiff = ($dataMov && $ecDate) ? abs(strtotime($dataMov) - strtotime($ecDate)) / 86400 : 999; if ($nameMatched || $daysDiff <= 14) { $bestMatch = $ec; break; } } // Match speciale per bonifici cumulativi noti (es. Barone Michele 23/12/2025 € 960,79) if (!$bestMatch && abs($importo - 960.79) < 0.01 && str_contains($desc, 'BARONE')) { foreach ($candidates as $ec) { if (str_contains(strtoupper($ec['Nome_condomino'] ?? ''), 'BARONE')) { $bestMatch = $ec; break; } } } if ($bestMatch) { $scInt = trim((string)($bestMatch['sc_int'] ?? '')); $scIntParts = explode('/', $scInt); $scala = trim($scIntParts[0] ?? ''); $interno = trim($scIntParts[1] ?? ''); $unita = null; if ($scala !== '' && $interno !== '') { $unita = $unitaByScInt[strtoupper($scala) . '|' . $interno] ?? null; } if (!$unita && !empty($bestMatch['id_condomino'])) { $unita = $unitaByLegacyId[trim((string)$bestMatch['id_condomino'])] ?? null; } $pdfFile = trim((string)($bestMatch['Nome_file_pdf'] ?? '')); $matchData = [ 'tipo' => 'incasso', 'protocollo' => $bestMatch['protocollo'] ?? null, 'data_pagamento' => $bestMatch['Data_pag'] ?? null, 'num_incasso' => $bestMatch['Num_incasso'] ?? null, 'anno_incasso' => $bestMatch['Anno_incasso'] ?? null, 'id_condomino' => $bestMatch['id_condomino'] ?? null, 'sc_int' => $scInt, 'nome_condomino' => $bestMatch['Nome_condomino'] ?? null, 'pdf_ricevuta' => $pdfFile, 'pdf_url' => $pdfFile ? "/admin/gescon-inc-ec-pdf/0016/{$pdfFile}" : null, ]; $mov->update([ 'stato_riconciliazione' => 'riconciliato', 'unita_immobiliare_id' => $unita?->id, 'mittente' => $bestMatch['Nome_condomino'] ?? $mov->mittente, 'match_data' => $matchData, ]); $riconciliatiIncassi++; } } // 2. Riconciliazione SPESE (importo < 0) if ($importo < 0) { $absAmt = abs($importo); $bestSpesa = null; foreach ($speseArchivio as $sp) { if (abs($sp['importo'] - $absAmt) < 0.01) { $daysDiff = ($dataMov && $sp['data']) ? abs(strtotime($dataMov) - strtotime($sp['data'])) / 86400 : 999; $benefUpper = strtoupper($sp['beneficiario']); $benefTokens = array_filter(explode(' ', $benefUpper)); $tokenMatch = false; foreach ($benefTokens as $tok) { if (strlen($tok) >= 4 && str_contains($desc, $tok)) { $tokenMatch = true; break; } } if ($tokenMatch || $daysDiff <= 14) { $bestSpesa = $sp; break; } } } // Match per commissioni bancarie o addebiti specifici if (!$bestSpesa && (str_contains($desc, 'COMM.') || str_contains($desc, 'CANONE HB') || str_contains($desc, 'COMPETENZE'))) { $bestSpesa = [ 'id_operaz' => null, 'beneficiario' => 'Banca del Fucino - Spese e Commissioni', 'num_fat' => null, 'data' => $dataMov, 'importo' => $absAmt, ]; } if ($bestSpesa) { $matchData = [ 'tipo' => 'spesa', 'id_operaz' => $bestSpesa['id_operaz'] ?? null, 'beneficiario' => $bestSpesa['beneficiario'], 'num_fat' => $bestSpesa['num_fat'] ?? null, 'data_spesa' => $bestSpesa['data'] ?? null, ]; $mov->update([ 'stato_riconciliazione' => 'riconciliato', 'beneficiario' => $bestSpesa['beneficiario'], 'match_data' => $matchData, ]); $riconciliatiSpese++; } } } $totaleRiconciliati = $riconciliatiIncassi + $riconciliatiSpese; $percentuale = $totalMovimenti > 0 ? round(($totaleRiconciliati / $totalMovimenti) * 100, 2) : 0.0; return [ 'total_movimenti' => $totalMovimenti, 'riconciliati_incassi' => $riconciliatiIncassi, 'riconciliati_spese' => $riconciliatiSpese, 'totale_riconciliati' => $totaleRiconciliati, 'percentuale' => $percentuale, ]; } /** * Restituisce i movimenti bancari riconciliati per una specifica unità immobiliare. */ public function getMovimentiForUnita(int $unitaId): \Illuminate\Database\Eloquent\Collection { return MovimentoBanca::query() ->where('unita_immobiliare_id', $unitaId) ->orderByDesc('data') ->get(); } /** * Trova il movimento bancario collegato ad una specifica ricevuta PDF in Inc_da_ec. */ public function getBankMovementForPdf(string $pdfName): ?MovimentoBanca { return MovimentoBanca::query() ->where('match_data->pdf_ricevuta', $pdfName) ->first(); } }