netgescon-day0/app/Services/Quality/OrdinarieQualityAuditService.php

395 lines
17 KiB
PHP

<?php
namespace App\Services\Quality;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class OrdinarieQualityAuditService
{
public function run(?string $stabileInput = null, ?int $anno = null): array
{
$results = [];
$registryIssues = $this->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' => [],
];
}
}