info('======================================================================'); $this->info(' NETGESCON: SCANSIONE, MATCHING FISCALE & IMPORTAZIONE FATTURE FE '); $this->info('======================================================================'); $isDryRun = (bool) $this->option('dry-run'); $filterStabile = $this->option('stabile'); $customDir = $this->option('dir'); // 1. Carica gli stabili canonici con Codice Fiscale $stabiliQuery = Stabile::whereNotNull('codice_fiscale')->where('codice_fiscale', '<>', ''); if ($filterStabile) { $stabiliQuery->where('codice_stabile', $filterStabile); } $stabili = $stabiliQuery->get(); if ($stabili->isEmpty()) { $this->error('Nessuno stabile canonico con Codice Fiscale configurato trovato.'); return Command::FAILURE; } $cfMap = []; foreach ($stabili as $s) { $cleanCf = strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string) $s->codice_fiscale)); if ($cleanCf !== '') { $cfMap[$cleanCf] = $s; } } $this->info("Stabili target attivi: " . count($cfMap)); foreach ($cfMap as $cf => $s) { $this->line(" - Stabile [{$s->codice_stabile}] {$s->denominazione} (CF: {$cf})"); } // 2. Determina le directory sorgente da scansionare $scanRoots = []; if ($customDir) { if (! is_dir($customDir)) { $this->error("Directory non trovata: {$customDir}"); return Command::FAILURE; } $scanRoots[] = realpath($customDir); } else { $basePath = base_path(); $candidates = [ $basePath . '/_legacy-vault', $basePath . '/Miki-Bug-workspace', $basePath . '/storage/gescon', $basePath . '/storage/app/amministratori', $basePath . '/storage/app/private/amministratori', $basePath . '/storage/app/public', ]; foreach ($candidates as $c) { if (is_dir($c)) { $scanRoots[] = realpath($c); } } } $this->info("\nCartelle sorgente incluse nella scansione: " . count($scanRoots)); // 3. Raccogli tutti i file .xml e .p7m $foundFiles = []; $excludedFolders = ['/vendor/', '/node_modules/', '/.git/', '/.gemini/', '/storage/framework/', '/bootstrap/cache/']; foreach ($scanRoots as $root) { $this->line(" Scansione: {$root}..."); $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); /** @var SplFileInfo $file */ foreach ($iterator as $file) { if (! $file->isFile()) { continue; } $path = $file->getRealPath(); $lowerPath = strtolower($path); $skip = false; foreach ($excludedFolders as $ex) { if (str_contains($lowerPath, $ex)) { $skip = true; break; } } if ($skip) { continue; } if (str_ends_with($lowerPath, '.xml') || str_ends_with($lowerPath, '.p7m')) { $foundFiles[] = $path; } } } $foundFiles = array_values(array_unique($foundFiles)); $totalFiles = count($foundFiles); $this->info("Totale file XML/P7M individuati: {$totalFiles}\n"); if ($totalFiles === 0) { $this->warn('Nessun file XML o P7M trovato.'); return Command::SUCCESS; } // 4. Scansione e parsing semantico di ciascun file $stats = [ 'scanned' => 0, 'matched' => 0, 'unmatched' => 0, 'copied' => 0, 'imported' => 0, 'duplicates' => 0, 'errors' => 0, 'by_stabile' => [], ]; $progressBar = $this->output->createProgressBar($totalFiles); $progressBar->start(); foreach ($foundFiles as $filePath) { $stats['scanned']++; $progressBar->advance(); $lowerPath = strtolower($filePath); $filename = basename($filePath); $isP7m = str_ends_with($lowerPath, '.p7m'); try { if ($isP7m) { $xml = $p7mExtractor->extractXmlFromP7m($filePath); } else { $xml = file_get_contents($filePath); } if (! is_string($xml) || trim($xml) === '') { continue; } $data = $parser->parse($xml); $destCf = strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string) ($data['destinatario_cf'] ?? ''))); $destPiva = strtoupper(preg_replace('/[^A-Z0-9]/i', '', (string) ($data['destinatario_piva'] ?? ''))); $matchedStabile = $cfMap[$destCf] ?? ($cfMap[$destPiva] ?? null); if (! $matchedStabile) { $stats['unmatched']++; continue; } $stats['matched']++; $codStabile = $matchedStabile->codice_stabile; $stats['by_stabile'][$codStabile] = ($stats['by_stabile'][$codStabile] ?? 0) + 1; if ($isDryRun) { continue; } // Determina la cartella di destinazione corretta $inboxAbsolute = $pathService->stabileAbsolutePath($matchedStabile, 'fatture_elettroniche/inbox'); if (! is_dir($inboxAbsolute)) { @mkdir($inboxAbsolute, 0755, true); } $destinationFile = $inboxAbsolute . '/' . $filename; if (! file_exists($destinationFile) && realpath($filePath) !== realpath($destinationFile)) { // Copia preservando sorgente intatta @copy($filePath, $destinationFile); $stats['copied']++; } // Importazione nel DB NetGescon $importRes = $importer->importXml( $xml, (int) $matchedStabile->id, $filename, [ 'force_stabile_id' => (int) $matchedStabile->id, 'create_fornitore_if_missing'=> true, 'auto_repair_duplicate' => true, 'p7m_path' => $isP7m ? $pathService->stabileRelativePath($matchedStabile, 'fatture_elettroniche/inbox/' . $filename) : null, ] ); if ($importRes['status'] === 'imported') { $stats['imported']++; } elseif ($importRes['status'] === 'duplicate') { $stats['duplicates']++; } else { $stats['errors']++; } } catch (\Throwable $e) { $stats['errors']++; } } $progressBar->finish(); $this->newLine(2); // 5. Riconciliazione automatica con operazioni_contabili (num_fat, dt_fat) $this->info("Riconciliazione automatica FE con operazioni contabili e registri..."); $reconciledOps = 0; foreach ($stabili as $stabile) { $gestioneIds = \App\Models\GestioneContabile::where('stabile_id', $stabile->id)->pluck('id'); if ($gestioneIds->isEmpty()) { continue; } $feRows = FatturaElettronica::where('stabile_id', $stabile->id)->get(); foreach ($feRows as $fe) { $numFat = trim((string) $fe->numero_fattura); $dtFat = $fe->data_fattura ? Carbon::parse($fe->data_fattura)->format('Y-m-d') : null; if ($numFat === '' || ! $dtFat) { continue; } $updated = OperazioneContabile::whereIn('gestione_id', $gestioneIds) ->whereNull('fe_fattura_id') ->where('num_fat', $numFat) ->whereDate('dt_fat', $dtFat) ->update(['fe_fattura_id' => $fe->id]); if ($updated > 0) { $reconciledOps += $updated; } } } $this->info("Operazioni contabili collegate a Fattura Elettronica ID: {$reconciledOps}"); // 6. Riepilogo finale $this->info('======================================================================'); $this->info(' RIEPILOGO SCANSIONE FE '); $this->info('======================================================================'); $this->table( ['Metrica', 'Valore'], [ ['File scansionati', $stats['scanned']], ['File associati a Stabili (Match CF)', $stats['matched']], ['File senza riscontro CF stabili', $stats['unmatched']], ['File copiati nelle cartelle stabili', $stats['copied']], ['Fatture importate a nuovo su DB', $stats['imported']], ['Fatture già presenti / aggiornate', $stats['duplicates']], ['Errori di lettura/parsing', $stats['errors']], ['Operazioni contabili riconciliate a FE', $reconciledOps], ] ); $this->info("\nDistribuzione fatture per Stabile:"); foreach ($stats['by_stabile'] as $cod => $count) { $stabileObj = $stabili->firstWhere('codice_stabile', $cod); $denom = $stabileObj ? $stabileObj->denominazione : ''; $this->line(" - Stabile [{$cod}] {$denom}: {$count} fatture"); } return Command::SUCCESS; } }