checkRegistryConsistency($stabileInput); if ($registryIssues !== []) { $results[] = [ 'file' => 'quality://archives/tenant-registry-ordinarie', 'module' => 'stabili', 'menu' => 'condomini', 'issues' => $registryIssues, ]; } $ordinarieIssues = $this->checkOrdinarieConsistency($stabileInput, $anno); if ($ordinarieIssues !== []) { $results[] = [ 'file' => 'quality://archives/ordinarie-audit', 'module' => 'contabilita-gescon', 'menu' => 'contabilita', 'issues' => $ordinarieIssues, ]; } return $results; } private function checkRegistryConsistency(?string $stabileInput): array { if (! Schema::hasTable('tenant_archive_registries')) { return [ $this->issue( 'tenant_registry_missing', 'Registry archivi non disponibile', 'La tabella tenant_archive_registries non esiste: impossibile allineare in modo affidabile archivio, owner e storage per stabile.', 'warning', 'Esegui la migrazione del registry e il comando netgescon:archive-registry-sync.' ), ]; } $issues = []; $registryRows = DB::table('tenant_archive_registries') ->where('archive_type', 'stabile') ->orderBy('archive_code') ->get(['archive_code', 'owner_amministratore_code', 'storage_relative_path', 'status']); $invalidCodes = $registryRows ->filter(fn($row) => ! preg_match('/^[A-Za-z0-9_-]+$/', (string) ($row->archive_code ?? ''))) ->values(); if ($invalidCodes->isNotEmpty()) { $sample = $invalidCodes->take(5)->map(fn($row) => (string) $row->archive_code)->implode(', '); $issues[] = $this->issue( 'tenant_registry_invalid_codes', 'Codici archivio stabile non validi nel registry', 'Rilevati ' . $invalidCodes->count() . ' codici archivio non canonici nel registry stabili. Esempi: ' . $sample . '.', 'error', 'Normalizza i codici stabile e rigenera il registry prima di usare i controlli operativi.' ); } $normalizedDuplicates = $registryRows ->groupBy(fn($row) => $this->normalizeArchiveCode((string) ($row->archive_code ?? ''))) ->filter(fn($group, $key) => $key !== '' && $group->count() > 1); if ($normalizedDuplicates->isNotEmpty()) { $sample = $normalizedDuplicates->take(5)->map(function ($group, $key) { $codes = collect($group)->pluck('archive_code')->implode('/'); return $key . ' => ' . $codes; })->implode('; '); $issues[] = $this->issue( 'tenant_registry_normalized_duplicates', 'Codici stabile duplicati dopo normalizzazione', 'Il registry contiene ' . $normalizedDuplicates->count() . ' gruppi di codici che collassano sullo stesso identificativo logico. Esempi: ' . $sample . '.', 'warning', 'Scegli un solo codice canonico per stabile e riallinea archivio, import e mapping legacy.' ); } if ($stabileInput !== null && trim($stabileInput) !== '') { $resolved = $this->resolveStabile(trim($stabileInput)); if ($resolved === null) { $issues[] = $this->issue( 'tenant_registry_stabile_not_found', 'Stabile richiesto non trovato', 'Nessuno stabile locale trovato per il filtro `' . trim($stabileInput) . '`.', 'warning', 'Usa codice_stabile, cod_stabile o ID locale valido prima di lanciare la QA specifica.' ); return $issues; } $candidateCodes = collect($this->buildLegacyCodeCandidates($resolved)) ->map(fn($code) => $this->normalizeArchiveCode($code)) ->filter() ->unique() ->values(); $matchingRegistry = $registryRows->filter(function ($row) use ($candidateCodes) { return $candidateCodes->contains($this->normalizeArchiveCode((string) ($row->archive_code ?? ''))); })->values(); if ($matchingRegistry->isEmpty()) { $issues[] = $this->issue( 'tenant_registry_missing_stabile_mapping', 'Stabile senza mapping registry coerente', 'Lo stabile `' . ($resolved->codice_stabile ?? trim($stabileInput)) . '` non ha un record coerente nel registry centrale.', 'error', 'Esegui netgescon:archive-registry-sync e verifica codice stabile e owner attuale.' ); } elseif ($matchingRegistry->count() > 1) { $sample = $matchingRegistry->map(fn($row) => (string) $row->archive_code . ' -> ' . (string) $row->owner_amministratore_code)->implode('; '); $issues[] = $this->issue( 'tenant_registry_multiple_mappings_stabile', 'Stabile con mapping registry multipli', 'Per lo stabile `' . ($resolved->codice_stabile ?? trim($stabileInput)) . '` risultano più mapping coerenti nel registry: ' . $sample . '.', 'warning', 'Riduci il registry a un solo record canonico per stabile dopo la pulizia dati test.' ); } } if ($issues === []) { $issues[] = $this->issue( 'tenant_registry_consistent', 'Registry archivi coerente', 'Nessuna anomalia bloccante rilevata nel registry centrale degli archivi stabili.', 'info', 'Continua a usare il registry come fonte di verità per codice, owner e storage.' ); } return $issues; } private function checkOrdinarieConsistency(?string $stabileInput, ?int $anno): array { if (! Schema::connection('gescon_import')->hasTable('operazioni')) { return [ $this->issue( 'ordinarie_operazioni_missing', 'Archivio operazioni legacy non disponibile', 'La tabella gescon_import.operazioni non è disponibile: impossibile eseguire la QA di Ordinarie.', 'critical', 'Ripristina la connessione gescon_import e la tabella operazioni.' ), ]; } $issues = []; $resolved = null; $candidateCodes = []; $label = 'globale'; if ($stabileInput !== null && trim($stabileInput) !== '') { $resolved = $this->resolveStabile(trim($stabileInput)); if ($resolved === null) { return [ $this->issue( 'ordinarie_stabile_not_found', 'Stabile QA non trovato', 'Nessuno stabile locale trovato per il filtro `' . trim($stabileInput) . '`.', 'warning', 'Usa un codice stabile valido prima di eseguire la QA mirata di Ordinarie.' ), ]; } $candidateCodes = $this->buildLegacyCodeCandidates($resolved); $label = (string) ($resolved->codice_stabile ?: $resolved->denominazione ?: trim($stabileInput)); } $baseQuery = DB::connection('gescon_import')->table('operazioni'); if ($candidateCodes !== [] && Schema::connection('gescon_import')->hasColumn('operazioni', 'cod_stabile')) { $baseQuery->whereIn('cod_stabile', $candidateCodes); } if ($anno !== null && Schema::connection('gescon_import')->hasColumn('operazioni', 'dt_spe')) { $baseQuery->whereYear('dt_spe', $anno); } $totalRows = (clone $baseQuery)->count(); if ($totalRows === 0) { return [ $this->issue( 'ordinarie_no_rows', 'Nessuna riga Ordinarie trovata', 'La QA non ha trovato righe in gescon_import.operazioni per il contesto `' . $label . '`' . ($anno ? ' e anno ' . $anno : '') . '.', 'info', 'Se atteso, controlla codice legacy stabile, anno selezionato e import operazioni.' ), ]; } $variantSummary = []; if ($candidateCodes !== [] && Schema::connection('gescon_import')->hasColumn('operazioni', 'cod_stabile')) { $variantSummary = (clone $baseQuery) ->select('cod_stabile') ->selectRaw('COUNT(*) as righe') ->groupBy('cod_stabile') ->orderByDesc('righe') ->get() ->map(fn($row) => (string) $row->cod_stabile . ': ' . (int) $row->righe) ->all(); if (count($variantSummary) > 1) { $issues[] = $this->issue( 'ordinarie_multiple_legacy_codes', 'Ordinarie distribuite su più codici legacy stabile', 'Per lo stabile `' . $label . '` le operazioni sono presenti su più codici legacy: ' . implode('; ', array_slice($variantSummary, 0, 8)) . '.', 'warning', 'Allinea il mapping legacy del stabile e scegli un solo codice operativo per filtri e viste.' ); } } $gestioneCounts = Schema::connection('gescon_import')->hasColumn('operazioni', 'gestione') ? (clone $baseQuery) ->selectRaw("COALESCE(NULLIF(TRIM(gestione), ''), '[vuoto]') as gestione_label") ->selectRaw('COUNT(*) as righe') ->groupBy('gestione_label') ->orderByDesc('righe') ->get() : collect(); $yearCounts = Schema::connection('gescon_import')->hasColumn('operazioni', 'dt_spe') ? (clone $baseQuery) ->selectRaw('YEAR(dt_spe) as anno_label') ->selectRaw('COUNT(*) as righe') ->groupBy('anno_label') ->orderByDesc('anno_label') ->get() : collect(); $topCodes = (clone $baseQuery) ->selectRaw("COALESCE(NULLIF(TRIM(cod_spe), ''), '[vuoto]') as cod_spe_label") ->selectRaw('COUNT(*) as righe') ->groupBy('cod_spe_label') ->orderByDesc('righe') ->limit(5) ->get() ->map(fn($row) => (string) $row->cod_spe_label . ': ' . (int) $row->righe) ->all(); $issues[] = $this->issue( 'ordinarie_volume_overview', 'Panoramica volume Ordinarie', 'Contesto `' . $label . '`' . ($anno ? ' anno ' . $anno : '') . ': ' . $totalRows . ' righe operative. Gestione: ' . $this->summarizeRows($gestioneCounts, 'gestione_label') . '. Anni: ' . $this->summarizeRows($yearCounts, 'anno_label') . '. Top voci: ' . implode('; ', $topCodes) . '.', $totalRows > 10000 ? 'warning' : 'info', $totalRows > 10000 ? 'Usa questo audit come base per restringere stabile/anno prima di verifiche manuali riga per riga in Filament.' : 'La distribuzione delle righe è entro un volume gestibile per analisi mirate.' ); $missingGestione = Schema::connection('gescon_import')->hasColumn('operazioni', 'gestione') ? (clone $baseQuery)->where(function ($query) { $query->whereNull('gestione')->orWhere('gestione', ''); })->count() : 0; if ($missingGestione > 0) { $issues[] = $this->issue( 'ordinarie_missing_gestione', 'Righe Ordinarie senza gestione valorizzata', 'Rilevate ' . $missingGestione . ' righe in operazioni con gestione vuota o nulla per il contesto `' . $label . '`' . ($anno ? ' anno ' . $anno : '') . '.', 'error', 'Normalizza il campo gestione nei dati legacy o aggiungi una regola univoca di fallback prima della visualizzazione Filament.' ); } if (Schema::connection('gescon_import')->hasColumn('operazioni', 'tabella')) { $missingTabella = (clone $baseQuery)->where(function ($query) { $query->whereNull('tabella')->orWhere('tabella', ''); })->count(); if ($missingTabella > 0) { $issues[] = $this->issue( 'ordinarie_missing_tabella', 'Righe Ordinarie senza tabella millesimale', 'Rilevate ' . $missingTabella . ' righe con tabella vuota o nulla nel contesto `' . $label . '`' . ($anno ? ' anno ' . $anno : '') . '.', 'warning', 'Bonifica tabella/cod_tab prima di usare consuntivi e raggruppamenti per tabella in Filament.' ); } } if (Schema::connection('gescon_import')->hasColumn('operazioni', 'dt_spe')) { $missingDate = (clone $baseQuery)->whereNull('dt_spe')->count(); if ($missingDate > 0) { $issues[] = $this->issue( 'ordinarie_missing_date', 'Righe Ordinarie senza data operazione', 'Rilevate ' . $missingDate . ' righe senza dt_spe nel contesto `' . $label . '`' . ($anno ? ' anno ' . $anno : '') . '.', 'warning', 'Compila dt_spe o definisci una regola di derivazione, altrimenti i filtri anno resteranno inaffidabili.' ); } } return $issues; } private function resolveStabile(string $input): ?object { $stabile = DB::table('stabili')->where('codice_stabile', $input)->first(); if (! $stabile && Schema::hasColumn('stabili', 'cod_stabile')) { $stabile = DB::table('stabili')->where('cod_stabile', $input)->first(); } if (! $stabile && ctype_digit($input) && ! str_starts_with($input, '0')) { $stabile = DB::table('stabili')->where('id', (int) $input)->first(); } return $stabile; } private function buildLegacyCodeCandidates(object $stabile): array { $candidates = []; foreach ([(string) ($stabile->codice_stabile ?? ''), (string) ($stabile->cod_stabile ?? ''), (string) ($stabile->old_id ?? '')] as $value) { $trimmed = trim($value); if ($trimmed === '') { continue; } $candidates[] = $trimmed; if (ctype_digit($trimmed)) { $candidates[] = ltrim($trimmed, '0'); $candidates[] = str_pad((string) ((int) $trimmed), 4, '0', STR_PAD_LEFT); $candidates[] = str_pad((string) ((int) $trimmed), 8, '0', STR_PAD_LEFT); } } return array_values(array_unique(array_filter($candidates, fn($value) => $value !== ''))); } private function summarizeRows($rows, string $labelKey): string { if ($rows instanceof \Illuminate\Support\Collection) { $rows = $rows->all(); } if (! is_array($rows) || $rows === []) { return 'n/d'; } return collect($rows) ->take(8) ->map(function ($row) use ($labelKey) { $label = (string) ($row->{$labelKey} ?? '[n/d]'); $count = (int) ($row->righe ?? 0); return $label . ': ' . $count; }) ->implode('; '); } private function normalizeArchiveCode(string $code): string { $normalized = strtoupper(trim($code)); if ($normalized === '') { return ''; } if (ctype_digit($normalized)) { $normalized = ltrim($normalized, '0'); return $normalized === '' ? '0' : $normalized; } return $normalized; } private function issue(string $rule, string $name, string $description, string $severity, string $suggestion): array { return [ 'rule' => $rule, 'name' => $name, 'description' => $description, 'severity' => $severity, 'line' => 1, 'match' => '—', 'suggestion' => $suggestion, 'context' => [], ]; } }