tenantId = $tenantId; $this->stats = [ 'conti_bancari' => ['imported' => 0, 'errors' => 0], 'incassi' => ['imported' => 0, 'errors' => 0], 'movimenti_bancari' => ['imported' => 0, 'errors' => 0], 'riconciliazioni' => ['automatiche' => 0, 'manuali' => 0, 'sospese' => 0] ]; } /** * Import completo flusso finanziario */ public function importCompleteFinancialFlow(array $filePaths): array { Log::info("Starting complete financial flow import for tenant: {$this->tenantId}"); DB::beginTransaction(); try { // 1. Import conti bancari if (isset($filePaths['anagr_casse'])) { $this->importContiBancari($filePaths['anagr_casse']); } // 2. Import incassi if (isset($filePaths['incassi'])) { $this->importIncassi($filePaths['incassi']); } // 3. Import movimenti bancari if (isset($filePaths['unicredit_wri'])) { $this->importMovimentiBancariUnicredit($filePaths['unicredit_wri']); } // 4. Riconciliazione automatica $this->executeAutoReconciliation(); DB::commit(); Log::info("Financial flow import completed successfully", $this->stats); return [ 'success' => true, 'message' => 'Import flusso finanziario completato', 'stats' => $this->stats ]; } catch (\Exception $e) { DB::rollBack(); Log::error("Financial flow import failed: " . $e->getMessage(), [ 'tenant_id' => $this->tenantId, 'trace' => $e->getTraceAsString() ]); return [ 'success' => false, 'message' => 'Errore durante import: ' . $e->getMessage(), 'stats' => $this->stats ]; } } /** * Import conti bancari da Anagr_casse.csv */ public function importContiBancari(string $filePath): void { Log::info("Importing conti bancari from: {$filePath}"); $csvData = $this->readCsvFile($filePath); foreach ($csvData as $row) { try { $contoBancario = ContoBancario::updateOrCreate([ 'tenant_id' => $this->tenantId, 'codice' => $row['Codice'] ], [ 'id_cassa_gescon' => $row['Id_cassa'] ?? null, 'descrizione' => $row['Descrizione'] ?? '', 'saldo_iniziale' => (float)($row['Saldo_iniziale_EURO'] ?? 0), 'saldo_corrente' => (float)($row['Saldo_iniziale_EURO'] ?? 0), 'tipo_conto' => $this->determinaTipoConto($row['Descrizione'] ?? ''), 'banca' => $this->estraiBanca($row['Descrizione'] ?? ''), 'numero_conto' => $this->estraiNumeroConto($row['Descrizione'] ?? ''), 'attivo' => true, 'formato_file' => $this->determinaFormatoFile($row['Descrizione'] ?? ''), 'metadati_gescon' => json_encode($row) ]); $this->stats['conti_bancari']['imported']++; } catch (\Exception $e) { Log::error("Error importing conto bancario", [ 'row' => $row, 'error' => $e->getMessage() ]); $this->stats['conti_bancari']['errors']++; } } } /** * Import incassi da incassi.csv */ public function importIncassi(string $filePath): void { Log::info("Importing incassi from: {$filePath}"); $csvData = $this->readCsvFile($filePath); foreach ($csvData as $row) { try { // Skip se importo = 0 if ((float)($row['importo_pagato_euro'] ?? 0) == 0) { continue; } // Trova condominio $condominio = $this->findCondominio($row['cod_cond'] ?? ''); if (!$condominio) { Log::warning("Condominio not found for incasso", ['cod_cond' => $row['cod_cond'] ?? '']); continue; } // Trova conto bancario $contoBancario = ContoBancario::where('tenant_id', $this->tenantId) ->where('codice', $row['cod_cassa'] ?? '') ->first(); if (!$contoBancario) { Log::warning("Conto bancario not found for incasso", ['cod_cassa' => $row['cod_cassa'] ?? '']); continue; } // Trova condomino (se possibile) $condomino = $this->findCondomino($row['cod_cond'] ?? '', $row['cond_inquil'] ?? ''); $incasso = Incasso::create([ 'tenant_id' => $this->tenantId, 'condominio_id' => $condominio->id, 'conto_bancario_id' => $contoBancario->id, 'condomino_id' => $condomino?->id, 'id_incasso_gescon' => $row['ID_incasso'] ?? null, 'cod_cond_gescon' => $row['cod_cond'] ?? '', 'cond_inquil' => $row['cond_inquil'] ?? '', 'n_riferimento' => $row['n_riferimento'] ?? '', 'anno_rif' => (int)($row['anno_rif'] ?? 0), 'da_ricev_diretto' => $row['da_ricev_diretto'] ?? '', 'n_ricevuta' => (int)($row['n_ricevuta'] ?? 0), 'posiz_riga' => (int)($row['posiz_riga'] ?? 0), 'anno_ricev' => $row['anno_ricev'] ?? '', 'n_mese' => $row['n_mese'] ?? '', 'o_r_s' => $row['o_r_s'] ?? '', 'importo_pagato' => (float)($row['importo_pagato'] ?? 0), 'importo_pagato_euro' => (float)($row['importo_pagato_euro'] ?? 0), 'd_p_e' => $row['d_p_e'] ?? '', 'dt_empag' => $this->parseGesconDate($row['dt_empag'] ?? ''), 'data_pagamento' => $this->parseGesconDate($row['dt_empag'] ?? ''), 'descrizione' => $row['descrizione'] ?? '', 'cod_cassa_gescon' => $row['cod_cassa'] ?? '', 'totale' => (int)($row['Totale'] ?? 0), 'str_orig' => (int)($row['str_orig'] ?? 0), 'linea_sep_stp' => $row['linea_sep_stp'] ?? '', 'proviene_ors' => $row['Proviene_ORS'] ?? '', 'proviene_n_stra' => (int)($row['Proviene_n_stra'] ?? 0), 'proviene_eserc' => (int)($row['Proviene_Eserc'] ?? 0), 'tipo_incasso' => $this->determinaTipoIncasso($row['o_r_s'] ?? ''), 'stato_riconciliazione' => 'sospesa', 'metadati_gescon' => json_encode($row) ]); $this->stats['incassi']['imported']++; } catch (\Exception $e) { Log::error("Error importing incasso", [ 'row' => $row, 'error' => $e->getMessage() ]); $this->stats['incassi']['errors']++; } } } /** * Import movimenti bancari da file Unicredit WRI */ public function importMovimentiBancariUnicredit(string $filePath): void { Log::info("Importing movimenti bancari from Unicredit WRI: {$filePath}"); // Trova conto bancario Unicredit $contoBancario = ContoBancario::where('tenant_id', $this->tenantId) ->where('banca', 'LIKE', '%UNICREDIT%') ->first(); if (!$contoBancario) { throw new \Exception("Conto bancario Unicredit non trovato"); } $fileContent = file_get_contents($filePath); $lines = explode("\n", $fileContent); // Trova l'inizio dei dati CSV $csvStartLine = null; foreach ($lines as $index => $line) { if (strpos($line, 'Data;Valuta;Descrizione;Euro;Caus.') !== false) { $csvStartLine = $index + 1; break; } } if (!$csvStartLine) { throw new \Exception("Formato file Unicredit WRI non riconosciuto"); } // Processa i movimenti for ($i = $csvStartLine; $i < count($lines); $i++) { $line = trim($lines[$i]); if (empty($line) || !preg_match('/^\d{2}\/\d{2}\/\d{4}/', $line)) { continue; } try { $parts = explode(';', $line); if (count($parts) < 5) continue; $dataOp = Carbon::createFromFormat('d/m/Y', $parts[0]); $dataValuta = Carbon::createFromFormat('d/m/Y', $parts[1]); $descrizione = $parts[2]; $importo = (float)str_replace(',', '.', $parts[3]); $causale = $parts[4]; // Genera hash per evitare duplicati $hash = hash('sha256', $this->tenantId . $line); // Pattern matching $patternData = $this->executePatternMatching($descrizione); $movimento = MovimentoBancario::updateOrCreate([ 'tenant_id' => $this->tenantId, 'hash_movimento' => $hash ], [ 'conto_bancario_id' => $contoBancario->id, 'data_operazione' => $dataOp, 'data_valuta' => $dataValuta, 'importo' => abs($importo), 'tipo_movimento' => $importo > 0 ? 'entrata' : 'uscita', 'descrizione_completa' => $descrizione, 'causale_bancaria' => $causale, 'causale_descrizione' => $this->getCausaleDescrizione($causale), 'controparte_nome' => $patternData['nome_estratto'], 'pattern_scala' => $patternData['scala'], 'pattern_interno' => $patternData['interno'], 'pattern_nome_estratto' => $patternData['nome_estratto'], 'pattern_keywords' => json_encode($patternData['keywords']), 'file_origine' => basename($filePath), 'formato_file' => 'WRI', 'riga_file' => $i + 1, 'unicredit_raw_line' => $line, 'stato_riconciliazione' => 'sospesa', 'metadati_import' => json_encode([ 'import_date' => now(), 'source_file' => $filePath, 'line_number' => $i + 1 ]) ]); $this->stats['movimenti_bancari']['imported']++; } catch (\Exception $e) { Log::error("Error importing movimento bancario", [ 'line' => $line, 'line_number' => $i + 1, 'error' => $e->getMessage() ]); $this->stats['movimenti_bancari']['errors']++; } } } /** * Esegue riconciliazione automatica */ public function executeAutoReconciliation(): void { Log::info("Starting auto-reconciliation"); // Trova movimenti in entrata non riconciliati $movimentiEntrata = MovimentoBancario::where('tenant_id', $this->tenantId) ->where('tipo_movimento', 'entrata') ->where('stato_riconciliazione', 'sospesa') ->get(); foreach ($movimentiEntrata as $movimento) { $this->tentaRiconciliazioneMovimento($movimento); } } /** * Tenta riconciliazione singolo movimento */ private function tentaRiconciliazioneMovimento(MovimentoBancario $movimento): void { // Cerca incassi candidati per matching $candidati = Incasso::where('tenant_id', $this->tenantId) ->where('stato_riconciliazione', 'sospesa') ->where('conto_bancario_id', $movimento->conto_bancario_id) ->get(); $bestMatch = null; $bestScore = 0; foreach ($candidati as $incasso) { $score = $this->calcolaScoreMatching($movimento, $incasso); if ($score > $bestScore && $score >= 70) { // Soglia minima 70% $bestMatch = $incasso; $bestScore = $score; } } if ($bestMatch) { // Riconciliazione automatica $movimento->update([ 'incasso_id' => $bestMatch->id, 'stato_riconciliazione' => 'automatica', 'confidenza_match' => $bestScore ]); $bestMatch->update([ 'movimento_bancario_id' => $movimento->id, 'stato_riconciliazione' => 'automatica', 'confidenza_match' => $bestScore, 'scostamento_importo' => abs($movimento->importo - $bestMatch->importo_pagato_euro), 'giorni_scostamento' => abs($movimento->data_operazione->diffInDays($bestMatch->data_pagamento)) ]); $this->stats['riconciliazioni']['automatiche']++; } else { $this->stats['riconciliazioni']['sospese']++; } } /** * Calcola score matching movimento-incasso */ private function calcolaScoreMatching(MovimentoBancario $movimento, Incasso $incasso): float { $score = 0; // 1. Matching importo (40 punti max) $diffImporto = abs($movimento->importo - $incasso->importo_pagato_euro); if ($diffImporto == 0) { $score += 40; } elseif ($diffImporto <= 1) { $score += 35; } elseif ($diffImporto <= 5) { $score += 25; } elseif ($diffImporto <= 10) { $score += 15; } // 2. Matching temporale (30 punti max) if ($movimento->data_operazione && $incasso->data_pagamento) { $diffGiorni = abs($movimento->data_operazione->diffInDays($incasso->data_pagamento)); if ($diffGiorni == 0) { $score += 30; } elseif ($diffGiorni <= 3) { $score += 25; } elseif ($diffGiorni <= 7) { $score += 20; } elseif ($diffGiorni <= 30) { $score += 10; } } // 3. Matching nome/UI (30 punti max) if ($movimento->pattern_scala && $movimento->pattern_interno) { // Cerca condomino con stessa scala/interno $condomino = Condomino::where('tenant_id', $this->tenantId) ->where('scala', $movimento->pattern_scala) ->where('interno', $movimento->pattern_interno) ->first(); if ($condomino && $incasso->condomino_id == $condomino->id) { $score += 30; } } return min($score, 100); // Max 100% } /** * Pattern matching per movimenti bancari */ private function executePatternMatching(string $descrizione): array { $result = [ 'scala' => null, 'interno' => null, 'nome_estratto' => null, 'keywords' => [] ]; // Pattern per Scala-Interno: ([A-D])\s*-?\s*(\d+) if (preg_match('/([A-D])\s*-?\s*(\d+)/', $descrizione, $matches)) { $result['scala'] = $matches[1]; $result['interno'] = $matches[2]; $result['keywords'][] = 'scala_interno'; } // Pattern per nome: BONIFICO SEPA DA\s+([A-Z\s]+)\s+PER if (preg_match('/BONIFICO SEPA DA\s+([A-Z\s]+)\s+PER/', $descrizione, $matches)) { $result['nome_estratto'] = trim($matches[1]); $result['keywords'][] = 'bonifico_sepa'; } // Pattern per condominio if (preg_match('/(RATA|RATE|COND|CONDOMINIO)/i', $descrizione)) { $result['keywords'][] = 'rata_condominiale'; } return $result; } // Utility methods private function readCsvFile(string $filePath): array { $data = []; if (($handle = fopen($filePath, "r")) !== FALSE) { $headers = fgetcsv($handle); while (($row = fgetcsv($handle)) !== FALSE) { $data[] = array_combine($headers, $row); } fclose($handle); } return $data; } private function determinaTipoConto(string $descrizione): string { if (stripos($descrizione, 'UNICREDIT') !== false || stripos($descrizione, 'CC') !== false) { return 'bancario'; } elseif (stripos($descrizione, 'CASSA') !== false || stripos($descrizione, 'CONTANTI') !== false) { return 'contanti'; } return 'bancario'; } private function estraiBanca(string $descrizione): ?string { if (stripos($descrizione, 'UNICREDIT') !== false) { return 'UNICREDIT'; } return null; } private function estraiNumeroConto(string $descrizione): ?string { if (preg_match('/N\.(\d+)/', $descrizione, $matches)) { return $matches[1]; } return null; } private function determinaFormatoFile(string $descrizione): ?string { if (stripos($descrizione, 'UNICREDIT') !== false) { return 'WRI'; } return null; } private function parseGesconDate(?string $date): ?Carbon { if (!$date) return null; try { // Formato: "07/15/14 00:00:00" -> MM/dd/yy return Carbon::createFromFormat('m/d/y H:i:s', $date); } catch (\Exception $e) { return null; } } private function determinaTipoIncasso(string $ors): string { switch (strtoupper($ors)) { case 'O': return 'rata_ordinaria'; case 'R': return 'riscaldamento'; case 'S': return 'rata_straordinaria'; default: return 'altro'; } } private function getCausaleDescrizione(string $causale): string { $causali = [ '048' => 'BONIFICO ENTRATA', '219' => 'IMPOSTA BOLLO', '008' => 'ADDEBITO DIRETTO', '043' => 'ACCREDITO STIPENDIO', // Aggiungi altre causali note ]; return $causali[$causale] ?? 'CAUSALE ' . $causale; } private function findCondominio(string $codCond): ?Condominio { return Condominio::where('tenant_id', $this->tenantId) ->where('cod_condominio_gescon', $codCond) ->first(); } private function findCondomino(string $codCond, string $condInquil): ?Condomino { return Condomino::where('tenant_id', $this->tenantId) ->where('cod_condominio_gescon', $codCond) ->where('inquilino_proprietario', $condInquil) ->first(); } public function getStats(): array { return $this->stats; } }