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

846 lines
31 KiB
PHP
Executable File

<?php
namespace App\Console\Commands;
use App\Models\StabileServizio;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class ImportGesconAcquaLegacyCommand extends Command
{
protected $signature = 'gescon:import-acqua-legacy
{--stabile= : Codice stabile legacy/cartella (es. 0023)}
{--stabile-id= : ID stabile locale (opzionale)}
{--mdb= : Percorso esplicito al generale_stabile.mdb}
{--base-path=/mnt/gescon-archives/gescon : Base path archivi GESCON}
{--acqua-years= : Anni acqua da importare (csv, es. 2024 oppure 2024,2025)}
{--purge-non-target-water : Elimina letture legacy acqua fuori anni target}
{--open-years=2024,2025 : Anni da mantenere visibili/aperti}
{--no-close-gestioni : Non chiudere le gestioni precedenti agli open-years}
{--dry-run : Simula senza scrivere}
{--limit= : Limita righe acqua_dett per debug}';
protected $description = 'Importa acqua legacy (anni, acqua_gen, acqua_fatture, acqua_dett) in servizi/letture per stabile e chiude gli anni storici.';
private bool $dryRun = false;
private ?string $tariffeSkipReason = null;
public function handle(): int
{
$this->dryRun = (bool) $this->option('dry-run');
$stabile = trim((string) ($this->option('stabile') ?? ''));
$stabileIdOption = (int) ($this->option('stabile-id') ?: 0);
if ($stabile === '' && $stabileIdOption <= 0) {
$this->error('Passa almeno --stabile=0023 oppure --stabile-id=<id>.');
return self::FAILURE;
}
$stabileRow = $this->resolveStabile($stabile, $stabileIdOption);
if (! $stabileRow) {
$this->error('Stabile locale non trovato. Verifica codice/id e anagrafica stabili.');
return self::FAILURE;
}
$stabileId = (int) $stabileRow->id;
$stabileCode = $stabile !== '' ? $stabile : trim((string) ($stabileRow->codice_stabile ?? ''));
$mdb = $this->resolveMdbPath($stabileCode);
if (! is_file($mdb)) {
$this->error('File MDB non trovato: ' . $mdb);
return self::FAILURE;
}
$bin = trim((string) @shell_exec('command -v mdb-export'));
if ($bin === '') {
$this->error('mdb-export non disponibile (installa mdbtools).');
return self::FAILURE;
}
$this->info('Stabile locale: #' . $stabileId . ' [' . ($stabileRow->codice_stabile ?? 'n/d') . '] ' . ($stabileRow->denominazione ?? ''));
$this->line('MDB: ' . $mdb . ($this->dryRun ? ' (DRY RUN)' : ''));
$anniRows = $this->exportTable($mdb, ['anni', 'Anni', 'ANNI']);
$genRowsAll = $this->exportTable($mdb, ['acqua_gen', 'Acqua_Gen', 'ACQUA_GEN']);
$fattRowsAll = $this->exportTable($mdb, ['acqua_fatture', 'Acqua_Fatture', 'ACQUA_FATTURE']);
$dettRowsAll = $this->exportTable($mdb, ['acqua_dett', 'Acqua_Dett', 'ACQUA_DETT']);
$targetAcquaYears = $this->parseYearsCsv((string) ($this->option('acqua-years') ?? ''));
$allowedRifUte = [];
$genRows = $genRowsAll;
if (! empty($targetAcquaYears)) {
$genRows = array_values(array_filter($genRowsAll, function (array $row) use ($targetAcquaYears): bool {
$year = $this->extractYear(trim((string) ($row['rif_anno'] ?? '')));
return $year !== null && in_array((int) $year, $targetAcquaYears, true);
}));
foreach ($genRows as $row) {
$rif = trim((string) ($row['rif_ute'] ?? ''));
if ($rif !== '') {
$allowedRifUte[$rif] = true;
}
}
} else {
foreach ($genRows as $row) {
$rif = trim((string) ($row['rif_ute'] ?? ''));
if ($rif !== '') {
$allowedRifUte[$rif] = true;
}
}
}
$fattRows = array_values(array_filter($fattRowsAll, function (array $row) use ($allowedRifUte): bool {
$rif = trim((string) ($row['rif_ute'] ?? ''));
return $rif !== '' && isset($allowedRifUte[$rif]);
}));
$dettRows = array_values(array_filter($dettRowsAll, function (array $row) use ($allowedRifUte): bool {
$rif = trim((string) ($row['rif_ute'] ?? ''));
return $rif !== '' && isset($allowedRifUte[$rif]);
}));
if (empty($dettRows)) {
$this->warn('Nessun record in acqua_dett: nulla da importare.');
return self::SUCCESS;
}
$limit = (int) ($this->option('limit') ?: 0);
if ($limit > 0) {
$dettRows = array_slice($dettRows, 0, $limit);
}
$servizio = $this->ensureAcquaServizio($stabileId, $stabileCode);
if (! $servizio) {
$this->error('Impossibile creare/risolvere il servizio acqua.');
return self::FAILURE;
}
$this->line('Servizio acqua: #' . (int) $servizio->id . ' ' . ($servizio->nome ?? 'acqua'));
$purgedWaterRows = 0;
if ((bool) $this->option('purge-non-target-water') && ! empty($allowedRifUte)) {
$purgedWaterRows = $this->purgeLegacyLettureOutsideRifUte($stabileId, (int) $servizio->id, array_keys($allowedRifUte));
}
$genByRif = $this->groupByKey($genRows, 'rif_ute');
$fattByRif = $this->groupByKey($fattRows, 'rif_ute');
$unitMaps = $this->buildUnitaMaps($stabileId);
$lettureStats = $this->importLetture(
$stabileId,
(int) $servizio->id,
$dettRows,
$genByRif,
$fattByRif,
$unitMaps['byLegacyCond'] ?? [],
$unitMaps['byScalaInterno'] ?? []
);
$tariffeStats = $this->importTariffeDaAcquaGen($stabileId, (int) $servizio->id, $genRows, $fattByRif);
$closedCount = 0;
if (! (bool) $this->option('no-close-gestioni')) {
$closedCount = $this->closeHistoricGestioni($stabileId, (string) ($this->option('open-years') ?? '2024,2025'));
}
$this->newLine();
$this->info('Import ACQUA completato');
$this->line('- anni (legacy): ' . count($anniRows));
$this->line('- acqua_gen: ' . count($genRows));
$this->line('- acqua_fatture: ' . count($fattRows));
$this->line('- acqua_dett processate: ' . count($dettRows));
if (! empty($targetAcquaYears)) {
$this->line('- anni acqua target: ' . implode(', ', $targetAcquaYears));
}
$this->line('- letture create: ' . $lettureStats['created']);
$this->line('- letture aggiornate: ' . $lettureStats['updated']);
$this->line('- letture purge fuori target: ' . $purgedWaterRows);
$this->line('- letture con unita risolta: ' . $lettureStats['matched_unita']);
$this->line('- letture senza match unita: ' . $lettureStats['unmatched_unita']);
$this->line('- nominativi C/I sincronizzati: ' . $lettureStats['nominativi_synced']);
$this->line('- tariffe create: ' . $tariffeStats['created']);
$this->line('- tariffe aggiornate: ' . $tariffeStats['updated']);
if ($this->tariffeSkipReason) {
$this->line('- tariffe skip: ' . $this->tariffeSkipReason);
}
$this->line('- gestioni storiche chiuse (< open years): ' . $closedCount);
return self::SUCCESS;
}
private function resolveStabile(string $stabileCode, int $stabileId): ?object
{
if (! Schema::hasTable('stabili')) {
return null;
}
if ($stabileId > 0) {
return DB::table('stabili')->where('id', $stabileId)->first();
}
$candidates = [];
if ($stabileCode !== '') {
$candidates[] = $stabileCode;
$trim = ltrim($stabileCode, '0');
if ($trim !== '') {
$candidates[] = $trim;
}
if (is_numeric($stabileCode)) {
$int = (int) $stabileCode;
$candidates[] = (string) $int;
$candidates[] = str_pad((string) $int, 4, '0', STR_PAD_LEFT);
}
}
$candidates = array_values(array_unique(array_filter($candidates, fn($v) => trim((string) $v) !== '')));
$query = DB::table('stabili');
$query->where(function ($q) use ($candidates): void {
foreach ($candidates as $candidate) {
$q->orWhere('codice_stabile', $candidate)
->orWhere('cod_stabile', $candidate)
->orWhere('old_id', $candidate);
}
});
return $query->orderByDesc('id')->first();
}
private function resolveMdbPath(string $stabileCode): string
{
$explicit = trim((string) ($this->option('mdb') ?? ''));
if ($explicit !== '') {
return $explicit;
}
$base = rtrim((string) ($this->option('base-path') ?? '/mnt/gescon-archives/gescon'), '/');
return $base . '/' . $stabileCode . '/generale_stabile.mdb';
}
private function exportTable(string $mdb, array $tableCandidates): array
{
foreach ($tableCandidates as $table) {
$cmd = "mdb-export -D '%Y-%m-%d %H:%M:%S' -d '|' -q '" . '"' . "' -R '\\n' "
. escapeshellarg($mdb)
. ' '
. escapeshellarg($table);
$csv = $this->runShell($cmd);
if (! is_string($csv) || trim($csv) === '') {
continue;
}
$rows = $this->parseDelimited($csv, '|');
if (! empty($rows)) {
return $rows;
}
}
return [];
}
private function runShell(string $cmd): ?string
{
$out = shell_exec($cmd . ' 2>/dev/null');
if (! is_string($out) || trim($out) === '') {
return null;
}
return $out;
}
private function parseDelimited(string $text, string $sep): array
{
$lines = preg_split('/\r?\n/', trim($text));
if (! is_array($lines) || count($lines) === 0) {
return [];
}
$headers = str_getcsv((string) array_shift($lines), $sep);
$headers = array_map(fn($h) => strtolower(trim((string) $h)), $headers);
$rows = [];
foreach ($lines as $line) {
if (trim((string) $line) === '') {
continue;
}
$cols = str_getcsv((string) $line, $sep);
if (count($cols) < count($headers)) {
$cols = array_pad($cols, count($headers), null);
}
if (count($cols) > count($headers)) {
$cols = array_slice($cols, 0, count($headers));
}
$row = @array_combine($headers, $cols);
if (! is_array($row)) {
continue;
}
$rows[] = $row;
}
return $rows;
}
private function ensureAcquaServizio(int $stabileId, string $stabileCode): ?StabileServizio
{
$servizio = StabileServizio::query()
->where('stabile_id', $stabileId)
->where('tipo', 'acqua')
->orderByDesc('attivo')
->orderByDesc('id')
->first();
if ($servizio) {
return $servizio;
}
if ($this->dryRun) {
return new StabileServizio([
'id' => 0,
'stabile_id' => $stabileId,
'tipo' => 'acqua',
'nome' => 'Utenza acqua legacy ' . $stabileCode,
'attivo' => true,
]);
}
return StabileServizio::query()->create([
'stabile_id' => $stabileId,
'tipo' => 'acqua',
'nome' => 'Utenza acqua legacy ' . $stabileCode,
'attivo' => true,
'meta' => [
'canale_acquisizione' => 'import_file',
'sorgente' => 'generale_stabile.mdb',
],
]);
}
private function groupByKey(array $rows, string $key): array
{
$out = [];
foreach ($rows as $row) {
$k = trim((string) ($row[strtolower($key)] ?? ''));
if ($k === '') {
continue;
}
$out[$k][] = $row;
}
return $out;
}
private function buildUnitaMaps(int $stabileId): array
{
if (! Schema::hasTable('unita_immobiliari')) {
return ['byLegacyCond' => [], 'byScalaInterno' => []];
}
$rows = DB::table('unita_immobiliari')
->where('stabile_id', $stabileId)
->select(['id', 'legacy_cond_id', 'scala', 'interno'])
->get();
$byLegacyCond = [];
$byScalaInterno = [];
foreach ($rows as $row) {
$id = (int) ($row->id ?? 0);
if ($id <= 0) {
continue;
}
$legacy = trim((string) ($row->legacy_cond_id ?? ''));
if ($legacy !== '' && ! isset($byLegacyCond[$legacy])) {
$byLegacyCond[$legacy] = $id;
}
$scala = $this->normalizeToken((string) ($row->scala ?? ''));
$interno = $this->normalizeToken((string) ($row->interno ?? ''));
if ($scala !== '' || $interno !== '') {
$byScalaInterno[$scala . '|' . $interno] = $id;
}
}
return ['byLegacyCond' => $byLegacyCond, 'byScalaInterno' => $byScalaInterno];
}
private function importLetture(
int $stabileId,
int $servizioId,
array $dettRows,
array $genByRif,
array $fattByRif,
array $unitaByLegacyCond,
array $unitaByScalaInterno
): array {
if (! Schema::hasTable('stabile_servizio_letture')) {
return ['created' => 0, 'updated' => 0, 'matched_unita' => 0, 'unmatched_unita' => 0, 'nominativi_synced' => 0];
}
$tableCols = Schema::getColumnListing('stabile_servizio_letture');
$colsSet = array_flip($tableCols);
$created = 0;
$updated = 0;
$matchedUnita = 0;
$unmatchedUnita = 0;
$nominativiSynced = 0;
foreach ($dettRows as $row) {
$rifUte = trim((string) ($row['rif_ute'] ?? ''));
if ($rifUte === '') {
continue;
}
$legacyCondId = trim((string) ($row['id_cond'] ?? $row['t_cod_cond'] ?? ''));
$ruolo = strtoupper(trim((string) ($row['cond_inquil'] ?? '')));
if (! in_array($ruolo, ['C', 'I'], true)) {
$ruolo = 'C';
}
$scala = $this->normalizeToken((string) ($row['t_scala'] ?? ''));
$interno = $this->normalizeToken((string) ($row['t_interno'] ?? ''));
$unitaId = null;
if ($legacyCondId !== '' && isset($unitaByLegacyCond[$legacyCondId])) {
$unitaId = (int) $unitaByLegacyCond[$legacyCondId];
} elseif (isset($unitaByScalaInterno[$scala . '|' . $interno])) {
$unitaId = (int) $unitaByScalaInterno[$scala . '|' . $interno];
}
if ($unitaId) {
$matchedUnita++;
} else {
$unmatchedUnita++;
}
$genRef = $genByRif[$rifUte][0] ?? [];
$rifAnno = trim((string) ($genRef['rif_anno'] ?? ''));
$year = $this->extractYear($rifAnno);
$periodoDal = $year ? sprintf('%04d-01-01', $year) : null;
$periodoAl = $year ? sprintf('%04d-12-31', $year) : null;
$letturaInizio = $this->firstMoney($row, ['lett_vecchia_1', 'lett_vecchia_2', 'lett_vecchia_3']);
$letturaFine = $this->firstMoney($row, ['lett_nuova_1', 'lett_nuova_2', 'lett_nuova_3']);
$consumo = $this->money($row['consumo'] ?? null);
if (abs($consumo) < 0.00001) {
$consumo = 0.0;
foreach (['cons_1', 'cons_2', 'cons_3', 'cons_4', 'cons_5'] as $consKey) {
$consumo += $this->money($row[$consKey] ?? null);
}
}
$importoTotale = 0.0;
foreach ((array) ($fattByRif[$rifUte] ?? []) as $fattRow) {
$importoTotale += $this->money($fattRow['importo_ft'] ?? null);
}
if (abs($importoTotale) < 0.00001) {
$importoTotale = $this->money($genRef['di_cui_prop_consumi_euro'] ?? $genRef['di_cui_prop_consumi'] ?? null);
}
$tipoUtenza = trim((string) ($row['tipo_utenza'] ?? ''));
$nominativo = trim((string) ($row['t_nome'] ?? ''));
$ref = 'LEGACY:' . $rifUte . ':COND:' . ($legacyCondId !== '' ? $legacyCondId : 'NA') . ':' . $ruolo;
$match = DB::table('stabile_servizio_letture')
->where('stabile_id', $stabileId)
->where('stabile_servizio_id', $servizioId)
->where('riferimento_acquisizione', $ref)
->first(['id']);
$payload = [
'stabile_id' => $stabileId,
'stabile_servizio_id' => $servizioId,
'unita_immobiliare_id' => $unitaId,
'periodo_dal' => $periodoDal,
'periodo_al' => $periodoAl,
'tipologia_lettura' => trim((string) ($row['tipo_lettura'] ?? '')) ?: null,
'canale_acquisizione' => 'import_file',
'riferimento_acquisizione' => $ref,
'lettura_inizio' => $letturaInizio,
'lettura_fine' => $letturaFine,
'consumo_valore' => $consumo !== 0.0 ? $consumo : null,
'consumo_unita' => 'mc',
'importo_totale' => $importoTotale !== 0.0 ? $importoTotale : null,
'raw' => json_encode([
'source' => 'legacy_generale_stabile_mdb',
'rif_ute' => $rifUte,
'rif_anno' => $rifAnno,
'legacy_cond_id' => $legacyCondId,
'ruolo_legacy' => $ruolo,
'tipo_utenza' => $tipoUtenza !== '' ? $tipoUtenza : null,
'nome_legacy' => $nominativo !== '' ? $nominativo : null,
't_scala' => trim((string) ($row['t_scala'] ?? '')),
't_interno' => trim((string) ($row['t_interno'] ?? '')),
'n_persone' => trim((string) ($row['n_persone'] ?? '')),
'relativa_a' => trim((string) ($row['relativa_a'] ?? '')),
'acqua_gen' => $genRef,
], JSON_UNESCAPED_UNICODE),
'updated_at' => now(),
];
$payload = array_intersect_key($payload, $colsSet);
if ($this->dryRun) {
if ($match) {
$updated++;
} else {
$created++;
}
} else {
if ($match) {
DB::table('stabile_servizio_letture')->where('id', (int) $match->id)->update($payload);
$updated++;
} else {
$payload['created_at'] = now();
DB::table('stabile_servizio_letture')->insert($payload);
$created++;
}
}
if ($unitaId && $nominativo !== '') {
$nominativiSynced += $this->syncNominativoRuolo(
$stabileId,
$unitaId,
$legacyCondId,
$ruolo,
$nominativo,
$rifUte,
$tipoUtenza
);
}
}
return [
'created' => $created,
'updated' => $updated,
'matched_unita' => $matchedUnita,
'unmatched_unita' => $unmatchedUnita,
'nominativi_synced' => $nominativiSynced,
];
}
private function syncNominativoRuolo(
int $stabileId,
int $unitaId,
string $legacyCondId,
string $ruolo,
string $nominativo,
string $rifUte,
string $tipoUtenza
): int {
if (! Schema::hasTable('unita_immobiliare_nominativi')) {
return 0;
}
$cols = array_flip(Schema::getColumnListing('unita_immobiliare_nominativi'));
$where = [
'unita_immobiliare_id' => $unitaId,
'stabile_id' => $stabileId,
'legacy_cond_id' => $legacyCondId !== '' ? $legacyCondId : null,
'ruolo' => $ruolo,
'nominativo' => $nominativo,
];
$exists = DB::table('unita_immobiliare_nominativi')->where($where)->first(['id']);
$payload = [
'unita_immobiliare_id' => $unitaId,
'stabile_id' => $stabileId,
'legacy_cond_id' => $legacyCondId !== '' ? $legacyCondId : null,
'ruolo' => $ruolo,
'nominativo' => $nominativo,
'fonte' => 'legacy_acqua_dett',
'legacy_payload' => json_encode([
'rif_ute' => $rifUte,
'tipo_utenza' => $tipoUtenza !== '' ? $tipoUtenza : null,
], JSON_UNESCAPED_UNICODE),
'updated_at' => now(),
];
$payload = array_intersect_key($payload, $cols);
if ($this->dryRun) {
return $exists ? 0 : 1;
}
if ($exists) {
DB::table('unita_immobiliare_nominativi')->where('id', (int) $exists->id)->update($payload);
return 0;
}
$payload['created_at'] = now();
DB::table('unita_immobiliare_nominativi')->insert($payload);
return 1;
}
private function importTariffeDaAcquaGen(int $stabileId, int $servizioId, array $genRows, array $fattByRif): array
{
if (! Schema::hasTable('stabile_servizio_tariffe') || empty($genRows)) {
return ['created' => 0, 'updated' => 0];
}
// In alcuni ambienti la tabella richiede FK obbligatoria verso fatture_elettroniche.
// I dati legacy acqua_gen non hanno questo riferimento diretto: evitiamo hard-fail.
if (Schema::hasColumn('stabile_servizio_tariffe', 'fattura_elettronica_id')) {
$this->tariffeSkipReason = 'richiesta fattura_elettronica_id obbligatoria (non disponibile su sorgente legacy acqua_gen)';
return ['created' => 0, 'updated' => 0];
}
$cols = array_flip(Schema::getColumnListing('stabile_servizio_tariffe'));
$created = 0;
$updated = 0;
foreach ($genRows as $row) {
$rifUte = trim((string) ($row['rif_ute'] ?? ''));
if ($rifUte === '') {
continue;
}
$rifAnno = trim((string) ($row['rif_anno'] ?? ''));
$year = $this->extractYear($rifAnno);
$decorrenza = $year ? sprintf('%04d-01-01', $year) : null;
$totFatture = 0.0;
foreach ((array) ($fattByRif[$rifUte] ?? []) as $fattRow) {
$totFatture += $this->money($fattRow['importo_ft'] ?? null);
}
$componenti = [
'ripart_precedente' => $this->money($row['ripart_precedente'] ?? null),
'di_cui_prop_consumi_euro' => $this->money($row['di_cui_prop_consumi_euro'] ?? $row['di_cui_prop_consumi'] ?? null),
'di_cui_millesimale_euro' => $this->money($row['di_cui_millesimale_euro'] ?? $row['di_cui_millesimale'] ?? null),
'di_cui_parti_uguali_euro' => $this->money($row['di_cui_parti_uguali_euro'] ?? $row['di_cui_parti_uguali'] ?? null),
'totale_fatture_rif_ute' => $totFatture,
'max_fascia' => trim((string) ($row['max_fascia'] ?? '')),
'criterio_tariffazione' => trim((string) ($row['criterio_tariffazione'] ?? '')),
'n_mesi' => trim((string) ($row['n_mesi'] ?? '')),
'tariffa_periodo' => trim((string) ($row['tariffa_periodo'] ?? '')),
];
$keyWhere = [
'stabile_id' => $stabileId,
'stabile_servizio_id' => $servizioId,
'profilo_tariffario' => 'LEGACY-RIF-' . $rifUte,
];
$existing = DB::table('stabile_servizio_tariffe')->where($keyWhere)->first(['id']);
$payload = [
'stabile_id' => $stabileId,
'stabile_servizio_id' => $servizioId,
'data_fattura' => $decorrenza,
'decorrenza_tariffe' => $decorrenza,
'profilo_tariffario' => 'LEGACY-RIF-' . $rifUte,
'delibera_ato' => trim((string) ($row['nom_ute'] ?? '')) ?: null,
'tariffe_componenti' => json_encode($componenti, JSON_UNESCAPED_UNICODE),
'payload' => json_encode([
'source' => 'legacy_generale_stabile_mdb',
'rif_ute' => $rifUte,
'acqua_gen' => $row,
'acqua_fatture' => $fattByRif[$rifUte] ?? [],
], JSON_UNESCAPED_UNICODE),
'updated_at' => now(),
];
$payload = array_intersect_key($payload, $cols);
if ($this->dryRun) {
if ($existing) {
$updated++;
} else {
$created++;
}
continue;
}
if ($existing) {
DB::table('stabile_servizio_tariffe')->where('id', (int) $existing->id)->update($payload);
$updated++;
} else {
$payload['created_at'] = now();
DB::table('stabile_servizio_tariffe')->insert($payload);
$created++;
}
}
return ['created' => $created, 'updated' => $updated];
}
private function closeHistoricGestioni(int $stabileId, string $openYearsRaw): int
{
if (! Schema::hasTable('gestioni_contabili')) {
return 0;
}
$openYears = array_values(array_filter(array_map(function ($y) {
$v = trim((string) $y);
return preg_match('/^\d{4}$/', $v) ? (int) $v : null;
}, explode(',', $openYearsRaw))));
if (empty($openYears)) {
return 0;
}
$minOpen = min($openYears);
$query = DB::table('gestioni_contabili')
->where('anno_gestione', '<', $minOpen)
->when(Schema::hasColumn('gestioni_contabili', 'stabile_id'), fn($q) => $q->where('stabile_id', $stabileId));
$rows = $query->get(['id']);
if ($rows->isEmpty()) {
return 0;
}
if ($this->dryRun) {
return $rows->count();
}
$update = ['updated_at' => now()];
if (Schema::hasColumn('gestioni_contabili', 'stato')) {
$update['stato'] = 'chiusa';
}
if (Schema::hasColumn('gestioni_contabili', 'gestione_attiva')) {
$update['gestione_attiva'] = false;
}
DB::table('gestioni_contabili')
->whereIn('id', $rows->pluck('id')->all())
->update($update);
return $rows->count();
}
/** @return array<int> */
private function parseYearsCsv(string $value): array
{
$years = [];
foreach (explode(',', $value) as $raw) {
$v = trim($raw);
if (! preg_match('/^\d{4}$/', $v)) {
continue;
}
$years[] = (int) $v;
}
$years = array_values(array_unique($years));
sort($years);
return $years;
}
/** @param array<int, string> $allowedRifUte */
private function purgeLegacyLettureOutsideRifUte(int $stabileId, int $servizioId, array $allowedRifUte): int
{
if (! Schema::hasTable('stabile_servizio_letture')) {
return 0;
}
$allowed = array_fill_keys($allowedRifUte, true);
$rows = DB::table('stabile_servizio_letture')
->where('stabile_id', $stabileId)
->where('stabile_servizio_id', $servizioId)
->where('canale_acquisizione', 'import_file')
->where('riferimento_acquisizione', 'like', 'LEGACY:%')
->get(['id', 'riferimento_acquisizione']);
$toDeleteIds = [];
foreach ($rows as $row) {
$ref = (string) ($row->riferimento_acquisizione ?? '');
if (! preg_match('/^LEGACY:([^:]+):COND:/', $ref, $m)) {
continue;
}
$rif = trim((string) ($m[1] ?? ''));
if ($rif === '' || isset($allowed[$rif])) {
continue;
}
$toDeleteIds[] = (int) $row->id;
}
if (empty($toDeleteIds)) {
return 0;
}
if ($this->dryRun) {
return count($toDeleteIds);
}
DB::table('stabile_servizio_letture')->whereIn('id', $toDeleteIds)->delete();
return count($toDeleteIds);
}
private function firstMoney(array $row, array $keys): ?float
{
foreach ($keys as $key) {
$v = $this->money($row[$key] ?? null);
if (abs($v) > 0.00001) {
return $v;
}
}
return null;
}
private function extractYear(string $value): ?int
{
if (preg_match('/(19|20)\d{2}/', $value, $m)) {
return (int) $m[0];
}
return null;
}
private function normalizeToken(string $value): string
{
$v = strtoupper(trim($value));
$v = preg_replace('/\s+/', '', $v) ?? $v;
return $v;
}
private function money($value): float
{
if ($value === null || $value === '') {
return 0.0;
}
$s = trim((string) $value);
if ($s === '') {
return 0.0;
}
$s = str_replace([" ", "\u{00A0}"], '', $s);
if (preg_match('/^-?\d{1,3}(?:\.\d{3})+(?:,\d+)?$/', $s)) {
$s = str_replace('.', '', $s);
$s = str_replace(',', '.', $s);
return (float) $s;
}
if (preg_match('/^-?\d{1,3}(?:,\d{3})+(?:\.\d+)?$/', $s)) {
$s = str_replace(',', '', $s);
return (float) $s;
}
if (str_contains($s, ',') && ! str_contains($s, '.')) {
$s = str_replace(',', '.', $s);
} elseif (str_contains($s, ',') && str_contains($s, '.')) {
$s = str_replace('.', '', $s);
$s = str_replace(',', '.', $s);
}
$s = preg_replace('/[^0-9\-\.]/', '', $s) ?? '';
if ($s === '' || $s === '-' || $s === '.') {
return 0.0;
}
return (float) $s;
}
}