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

448 lines
20 KiB
PHP
Executable File

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class NetgesconQaMillesimiSpeseCommand extends Command
{
protected $signature = 'netgescon:qa-millesimi-spese
{--stabile= : Codice stabile legacy/domino}
{--stabile_id= : ID stabile dominio}
{--anno= : Legacy year staging (es. 0001) per filtrare il confronto}
{--show-missing=20 : Massimo numero di codici mancanti da mostrare}';
protected $description = 'QA su tabelle millesimali, voci spesa e frazionamenti, evitando fallback vuoti e verificando l allineamento con nominativi/unita.';
public function handle(): int
{
if (! Schema::hasTable('stabili')) {
$this->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();
$expectedTablesWithoutPositiveRows = collect();
$unexpectedTablesWithoutPositiveRows = 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', 'tm.tipo_calcolo', 'tm.totale_millesimi')
->havingRaw('COUNT(dm.id) = 0')
->selectRaw('COALESCE(tm.codice_tabella, tm.nome_tabella, tm.denominazione, CAST(tm.id AS CHAR)) as codice')
->selectRaw('LOWER(COALESCE(tm.tipo_calcolo, "")) as tipo_calcolo')
->selectRaw('COALESCE(tm.totale_millesimi, 0) as totale_millesimi')
->get();
$expectedTablesWithoutPositiveRows = $tablesWithoutPositiveRows
->filter(fn($row) => $this->isExpectedTableWithoutPositiveRows($row))
->pluck('codice')
->values();
$unexpectedTablesWithoutPositiveRows = $tablesWithoutPositiveRows
->reject(fn($row) => $this->isExpectedTableWithoutPositiveRows($row))
->pluck('codice')
->values();
}
$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.');
}
if ($expectedTablesWithoutPositiveRows->isNotEmpty()) {
$this->line('Tabelle legacy senza righe millesimali positive ma attese: ' . $expectedTablesWithoutPositiveRows->implode(', '));
}
$exitCode = self::SUCCESS;
if ($unexpectedTablesWithoutPositiveRows->isNotEmpty()) {
$exitCode = self::INVALID;
$this->warn('Tabelle senza alcuna riga millesimale positiva: ' . $unexpectedTablesWithoutPositiveRows->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,
'detail_codes' => [],
'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);
$result['detail_codes'] = $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);
}
private function isExpectedTableWithoutPositiveRows(object $row): bool
{
$code = strtoupper(trim((string) ($row->codice ?? '')));
$tipoCalcolo = strtolower(trim((string) ($row->tipo_calcolo ?? '')));
$totaleMm = is_numeric($row->totale_millesimi ?? null) ? (float) $row->totale_millesimi : 0.0;
$staging = $this->collectStagingMetrics(
$this->resolveStabileScope()[0],
$this->resolveStabileScope()[1],
$this->normalizeOption($this->option('anno'))
);
$detailCodes = array_map(static fn($value) => strtoupper(trim((string) $value)), $staging['detail_codes'] ?? []);
if ($code !== '' && ! in_array($code, $detailCodes, true)) {
return true;
}
if (in_array($tipoCalcolo, ['a_consumo', 'personali'], true)) {
return true;
}
if ($code === 'RIPART') {
return true;
}
return $totaleMm <= 0;
}
}