error('Tabella stabili non disponibile.'); return self::FAILURE; } [$stabileId, $codiceStabile] = $this->resolveStabileScope(); if (! $stabileId || $codiceStabile === '') { $this->error('Serve --stabile=CODICE oppure --stabile_id=ID valido.'); return self::FAILURE; } $legacyYear = $this->normalizeOption($this->option('anno')); $showMissing = max(1, (int) ($this->option('show-missing') ?: 20)); $this->info('QA millesimi/spese per stabile ' . $codiceStabile . ' (#' . $stabileId . ')' . ($legacyYear ? ' anno ' . $legacyYear : '')); $activeUnitsQuery = DB::table('unita_immobiliari') ->where('stabile_id', $stabileId) ->whereNull('deleted_at'); $activeUnits = (clone $activeUnitsQuery)->count(); $unitsWithNominativi = 0; if (Schema::hasTable('unita_immobiliare_nominativi')) { $unitsWithNominativi = DB::table('unita_immobiliare_nominativi as uin') ->join('unita_immobiliari as ui', 'ui.id', '=', 'uin.unita_immobiliare_id') ->where('ui.stabile_id', $stabileId) ->whereNull('ui.deleted_at') ->distinct('uin.unita_immobiliare_id') ->count('uin.unita_immobiliare_id'); } $unitsWithoutNominativi = max(0, $activeUnits - $unitsWithNominativi); $tabelleCount = Schema::hasTable('tabelle_millesimali') ? DB::table('tabelle_millesimali')->where('stabile_id', $stabileId)->count() : 0; $detailRows = 0; $detailZeroRows = 0; $detailZeroOnUnitsWithoutNominativi = 0; $tablesWithoutPositiveRows = collect(); if (Schema::hasTable('dettaglio_millesimi') && Schema::hasTable('tabelle_millesimali')) { $detailBase = DB::table('dettaglio_millesimi as dm') ->join('tabelle_millesimali as tm', 'tm.id', '=', 'dm.tabella_millesimale_id') ->where('tm.stabile_id', $stabileId); $detailRows = (clone $detailBase)->count(); $detailZeroRows = (clone $detailBase) ->where(function ($q): void { $q->whereNull('dm.millesimi')->orWhere('dm.millesimi', '<=', 0); }) ->count(); if (Schema::hasTable('unita_immobiliare_nominativi')) { $detailZeroOnUnitsWithoutNominativi = (clone $detailBase) ->join('unita_immobiliari as ui', 'ui.id', '=', 'dm.unita_immobiliare_id') ->where(function ($q): void { $q->whereNull('dm.millesimi')->orWhere('dm.millesimi', '<=', 0); }) ->whereNotExists(function ($sub): void { $sub->selectRaw('1') ->from('unita_immobiliare_nominativi as uin') ->whereColumn('uin.unita_immobiliare_id', 'dm.unita_immobiliare_id'); }) ->count(); } $tablesWithoutPositiveRows = DB::table('tabelle_millesimali as tm') ->leftJoin('dettaglio_millesimi as dm', function ($join): void { $join->on('dm.tabella_millesimale_id', '=', 'tm.id') ->where('dm.millesimi', '>', 0); }) ->where('tm.stabile_id', $stabileId) ->groupBy('tm.id', 'tm.codice_tabella', 'tm.nome_tabella', 'tm.denominazione') ->havingRaw('COUNT(dm.id) = 0') ->selectRaw('COALESCE(tm.codice_tabella, tm.nome_tabella, tm.denominazione, CAST(tm.id AS CHAR)) as codice') ->pluck('codice'); } $vociCount = Schema::hasTable('voci_spesa') ? DB::table('voci_spesa')->where('stabile_id', $stabileId)->count() : 0; $vociSenzaTabella = Schema::hasTable('voci_spesa') ? DB::table('voci_spesa') ->where('stabile_id', $stabileId) ->where(function ($q) : void { $q->whereNull('tabella_millesimale_default_id')->orWhere('tabella_millesimale_default_id', 0); }) ->count() : 0; $vociConTabellaOrfana = (Schema::hasTable('voci_spesa') && Schema::hasTable('tabelle_millesimali')) ? DB::table('voci_spesa as vs') ->leftJoin('tabelle_millesimali as tm', 'tm.id', '=', 'vs.tabella_millesimale_default_id') ->where('vs.stabile_id', $stabileId) ->whereNotNull('vs.tabella_millesimale_default_id') ->whereNull('tm.id') ->count() : 0; $staging = $this->collectStagingMetrics($stabileId, $codiceStabile, $legacyYear); $this->line('Unita attive: ' . $activeUnits); $this->line('Unita con nominativi: ' . $unitsWithNominativi); $this->line('Unita senza nominativi: ' . $unitsWithoutNominativi); $this->line('Tabelle millesimali dominio: ' . $tabelleCount); $this->line('Righe dettaglio millesimi: ' . $detailRows); $this->line('Righe dettaglio a zero/null: ' . $detailZeroRows); $this->line('Righe a zero su unita senza nominativi: ' . $detailZeroOnUnitsWithoutNominativi); $this->line('Voci spesa dominio: ' . $vociCount); $this->line('Voci senza tabella default: ' . $vociSenzaTabella); $this->line('Voci con tabella orfana: ' . $vociConTabellaOrfana); if ($staging['available']) { $this->line('Staging tabelle legacy: ' . $staging['headers_count'] . ' intestazioni, ' . $staging['detail_codes_count'] . ' codici con dettagli'); $this->line('Staging voci legacy: ' . $staging['voci_count']); $this->line('Staging straordinarie legacy: ' . $staging['straord_count']); $this->line('Staging frazionamenti: Fraz_gen=' . $staging['fraz_gen_count'] . ', fraz_dett=' . $staging['fraz_dett_count']); } else { $this->warn('Connessione/tabelle staging non disponibili per il confronto completo.'); } $exitCode = self::SUCCESS; if ($tablesWithoutPositiveRows->isNotEmpty()) { $exitCode = self::INVALID; $this->warn('Tabelle senza alcuna riga millesimale positiva: ' . $tablesWithoutPositiveRows->take($showMissing)->implode(', ')); } if ($staging['available']) { if (! empty($staging['missing_table_codes'])) { $exitCode = self::INVALID; $this->warn('Codici tabella presenti in staging ma mancanti nel dominio: ' . implode(', ', array_slice($staging['missing_table_codes'], 0, $showMissing))); } if (! empty($staging['missing_voci_codes'])) { $exitCode = self::INVALID; $this->warn('Codici voce presenti in staging ma mancanti nel dominio: ' . implode(', ', array_slice($staging['missing_voci_codes'], 0, $showMissing))); } if ($staging['fraz_gen_count'] > 0 || $staging['fraz_dett_count'] > 0) { $this->warn('Archivio frazionamenti legacy presente: verificare Z01/Z02 prima del via libera contabile.'); if (($staging['z_fraz_voci_missing'] ?? []) !== []) { $exitCode = self::INVALID; $this->warn('Voci Z con frazionamento presenti in staging ma non allineate nel dominio: ' . implode(', ', array_slice($staging['z_fraz_voci_missing'], 0, $showMissing))); } } } if ($unitsWithoutNominativi > 0 || $detailZeroOnUnitsWithoutNominativi > 0 || $vociConTabellaOrfana > 0) { $exitCode = self::INVALID; } if ($exitCode === self::SUCCESS) { $this->info('QA completato senza anomalie bloccanti.'); } else { $this->warn('QA completato con anomalie da correggere prima del riallineamento definitivo.'); } return $exitCode; } private function resolveStabileScope(): array { $stabileId = (int) ($this->option('stabile_id') ?: 0); if ($stabileId > 0) { $codice = DB::table('stabili')->where('id', $stabileId)->value('codice_stabile'); return [$stabileId, $this->normalizeOption($codice) ?? '']; } $codiceInput = $this->normalizeCode((string) ($this->option('stabile') ?? '')); if ($codiceInput === '') { return [0, '']; } $trimmed = ltrim($codiceInput, '0'); $stabile = DB::table('stabili') ->where(function ($q) use ($codiceInput, $trimmed): void { $q->where('codice_stabile', $codiceInput); if ($trimmed !== '' && $trimmed !== $codiceInput) { $q->orWhere('codice_stabile', $trimmed); } }) ->select('id', 'codice_stabile') ->first(); if (! $stabile) { return [0, '']; } return [(int) $stabile->id, (string) $stabile->codice_stabile]; } private function collectStagingMetrics(int $stabileId, string $codiceStabile, ?string $legacyYear): array { $result = [ 'available' => false, 'headers_count' => 0, 'detail_codes_count' => 0, 'voci_count' => 0, 'straord_count' => 0, 'fraz_gen_count' => 0, 'fraz_dett_count' => 0, 'missing_table_codes' => [], 'missing_voci_codes' => [], 'z_fraz_voci_missing' => [], ]; if (! config('database.connections.gescon_import')) { return $result; } $result['available'] = true; $code = $this->normalizeCode($codiceStabile); $domainTableCodes = Schema::hasTable('tabelle_millesimali') ? DB::table('tabelle_millesimali') ->where('stabile_id', $stabileId) ->selectRaw('UPPER(TRIM(COALESCE(codice_tabella, nome_tabella, denominazione))) as codice') ->pluck('codice') ->filter() ->unique() ->values() ->all() : []; $domainVoceCodes = Schema::hasTable('voci_spesa') ? DB::table('voci_spesa') ->where('stabile_id', $stabileId) ->selectRaw('UPPER(TRIM(codice)) as codice') ->pluck('codice') ->filter() ->unique() ->values() ->all() : []; if (Schema::hasTable('voci_spesa') && Schema::hasColumn('voci_spesa', 'legacy_codice')) { $domainLegacyVoceCodes = DB::table('voci_spesa') ->where('stabile_id', $stabileId) ->selectRaw('UPPER(TRIM(legacy_codice)) as codice') ->pluck('codice') ->filter() ->unique() ->values() ->all(); $domainVoceCodes = array_values(array_unique(array_merge($domainVoceCodes, $domainLegacyVoceCodes))); } if (Schema::connection('gescon_import')->hasTable('tabelle_millesimali')) { $headersQuery = DB::connection('gescon_import')->table('tabelle_millesimali'); if (Schema::connection('gescon_import')->hasColumn('tabelle_millesimali', 'cod_stabile')) { $headersQuery->where('cod_stabile', $code); } if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('tabelle_millesimali', 'legacy_year')) { $headersQuery->where('legacy_year', $legacyYear); } $headerCodes = $headersQuery ->selectRaw('UPPER(TRIM(COALESCE(cod_tabella, denominazione, descrizione))) as codice') ->pluck('codice') ->filter() ->unique() ->values() ->all(); $result['headers_count'] = count($headerCodes); $result['missing_table_codes'] = array_values(array_diff($headerCodes, $domainTableCodes)); } if (Schema::connection('gescon_import')->hasTable('dett_tab')) { $detailQuery = DB::connection('gescon_import')->table('dett_tab as d'); if (Schema::connection('gescon_import')->hasTable('condomin')) { $detailQuery->join('condomin as c', 'c.cod_cond', '=', 'd.id_cond') ->where('c.cod_stabile', $code); if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('condomin', 'legacy_year')) { $detailQuery->where('c.legacy_year', $legacyYear); } } if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('dett_tab', 'legacy_year')) { $detailQuery->where('d.legacy_year', $legacyYear); } $detailCodes = $detailQuery ->selectRaw('UPPER(TRIM(d.cod_tab)) as codice') ->pluck('codice') ->filter() ->unique() ->values() ->all(); $result['detail_codes_count'] = count($detailCodes); if ($result['missing_table_codes'] === []) { $result['missing_table_codes'] = array_values(array_diff($detailCodes, $domainTableCodes)); } } if (Schema::connection('gescon_import')->hasTable('voc_spe')) { $vociQuery = DB::connection('gescon_import')->table('voc_spe'); if (Schema::connection('gescon_import')->hasColumn('voc_spe', 'cod_stabile')) { $vociQuery->where('cod_stabile', $code); } if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('voc_spe', 'legacy_year')) { $vociQuery->where('legacy_year', $legacyYear); } $vocCodes = $vociQuery ->selectRaw('UPPER(TRIM(cod)) as codice') ->pluck('codice') ->filter() ->unique() ->values() ->all(); $result['voci_count'] = count($vocCodes); $result['missing_voci_codes'] = array_values(array_diff($vocCodes, $domainVoceCodes)); $zFractionsQuery = DB::connection('gescon_import')->table('voc_spe') ->where('cod_stabile', $code); if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('voc_spe', 'legacy_year')) { $zFractionsQuery->where('legacy_year', $legacyYear); } $zFractions = $zFractionsQuery ->selectRaw('UPPER(TRIM(cod)) as codice') ->whereIn('cod', ['Z01', 'Z02']) ->pluck('codice') ->filter() ->unique() ->values() ->all(); $result['z_fraz_voci_missing'] = array_values(array_diff($zFractions, $domainVoceCodes)); } if (Schema::connection('gescon_import')->hasTable('straordinarie')) { $straordQuery = DB::connection('gescon_import')->table('straordinarie'); if (Schema::connection('gescon_import')->hasColumn('straordinarie', 'cod_stabile')) { $straordQuery->where('cod_stabile', $code); } if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('straordinarie', 'legacy_year')) { $straordQuery->where('legacy_year', $legacyYear); } $result['straord_count'] = $straordQuery->count(); } if (Schema::connection('gescon_import')->hasTable('fraz_gen')) { $query = DB::connection('gescon_import')->table('fraz_gen'); if (Schema::connection('gescon_import')->hasColumn('fraz_gen', 'cod_stabile')) { $query->where('cod_stabile', $code); } if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('fraz_gen', 'legacy_year')) { $query->where('legacy_year', $legacyYear); } $result['fraz_gen_count'] = $query->count(); } if (Schema::connection('gescon_import')->hasTable('fraz_dett')) { $query = DB::connection('gescon_import')->table('fraz_dett'); if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('fraz_dett', 'legacy_year')) { $query->where('legacy_year', $legacyYear); } $result['fraz_dett_count'] = $query->count(); } return $result; } private function normalizeOption(mixed $value): ?string { if (! is_string($value) && ! is_numeric($value)) { return null; } $normalized = trim((string) $value); return $normalized === '' ? null : $normalized; } private function normalizeCode(string $code): string { $code = trim($code); if ($code === '') { return ''; } if (is_numeric($code)) { return str_pad((string) ((int) $code), 4, '0', STR_PAD_LEFT); } return strtoupper($code); } }