netgescon-day0/app/Console/Commands/ScanAndImportFeFilesCommand.php

293 lines
11 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\FatturaElettronica;
use App\Models\OperazioneContabile;
use App\Models\Stabile;
use App\Services\FattureElettroniche\FatturaElettronicaImporter;
use App\Services\FattureElettroniche\FatturaElettronicaXmlParser;
use App\Services\FattureElettroniche\P7mExtractor;
use App\Services\TenantArchivePathService;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;
class ScanAndImportFeFilesCommand extends Command
{
protected $signature = 'gescon:scan-import-fe
{--dir= : Directory specifica da scansionare}
{--stabile= : Filtra per codice stabile (es. 0013, 0021)}
{--dry-run : Esegui solo simulazione senza copiare né scrivere su DB}';
protected $description = 'Scansiona l\'intero archivio di backup, assegna le FE agli stabili tramite Codice Fiscale, copia i file nelle rispettive cartelle e importa nel DB';
public function handle(
FatturaElettronicaXmlParser $parser,
FatturaElettronicaImporter $importer,
P7mExtractor $p7mExtractor,
TenantArchivePathService $pathService
): int {
$this->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;
}
}