395 lines
17 KiB
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' => [],
|
|
];
|
|
}
|
|
}
|