448 lines
20 KiB
PHP
Executable File
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;
|
|
}
|
|
}
|