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

6236 lines
282 KiB
PHP

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Throwable;
class ImportGesconFullPipeline extends Command
{
protected $signature = 'gescon:import-full
{--stabile= : Codice stabile (cartella es. 0021)}
{--anno= : Anno gestione da importare (es. 2024)}
{--path= : Path base contenente i file singolo_anno.mdb}
{--mdb= : Percorso esplicito a Stabili.mdb per arricchimento metadati}
{--mdb-condomin= : Percorso esplicito al file MDB contenente la tabella condomin (singolo_anno.mdb)}
{--split-mode=palazzine : Strategia divisione UI: none|palazzine|stabili-per-scala}
{--scala= : Filtra per scala/palazzina specifica}
{--piano-min= : Filtra piano minimo normalizzato}
{--piano-max= : Filtra piano massimo normalizzato}
{--only-pertinenze= : Solo pertinenze (any|cantina|box|posto_auto|soffitta|locale_tecnico)}
{--dry-run : Simula senza scrivere sulle tabelle dominio}
{--limit= : Limita numero record per debug}
{--reset-temp : Svuota staging prima di importare}
{--solo= : Step singolo: unita|relazioni|soggetti|diritti|millesimi|voci|operazioni|rate|incassi|giroconti|straord|addebiti|detrazioni|tutto}
{--no-views : Non ricrea le viste QA}
{--force-mapping : Applica mapping anche sovrascrivendo valori esistenti}
{--fill-missing-millesimi : Crea righe mancanti a zero per ogni unita/tabella (uso eccezionale)}
{--amministratore-id= : Forza l\'assegnazione dello stabile a questo amministratore}
';
protected $description = 'Pipeline orchestrata di import completo da archivio Gescon (MDB singolo anno) verso schema dominio.';
private array $stepsOrder = [
'stabili',
'unita',
'relazioni',
'soggetti',
'diritti',
'millesimi',
'voci',
// Importa conti bancari per stabile dalle tabelle legacy (Anagr_casse)
'banche',
'operazioni',
'rate',
'incassi',
'giroconti',
'straord',
'addebiti',
'detrazioni',
];
private array $legacyYearMap = [];
private bool $legacyYearMapLoaded = false;
private function requestedLegacyYear(): ?string
{
$anno = $this->option('anno');
if ($anno === null) {
return null;
}
$value = trim((string) $anno);
if ($value === '') {
return null;
}
return sprintf('%04d', (int) $value);
}
private function scopeCondominQuery($query)
{
$legacyYear = $this->requestedLegacyYear();
if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('condomin', 'legacy_year')) {
$query->where('legacy_year', $legacyYear);
}
return $query;
}
private array $stagingLatestLegacyYearCache = [];
private function resolveLatestStagingLegacyYear(string $table, ?string $stabile = null): ?string
{
$cacheKey = $table . '|' . ($stabile ?? '*');
if (array_key_exists($cacheKey, $this->stagingLatestLegacyYearCache)) {
return $this->stagingLatestLegacyYearCache[$cacheKey];
}
if (! Schema::connection('gescon_import')->hasTable($table)) {
return $this->stagingLatestLegacyYearCache[$cacheKey] = null;
}
if (! Schema::connection('gescon_import')->hasColumn($table, 'legacy_year')) {
return $this->stagingLatestLegacyYearCache[$cacheKey] = $this->requestedLegacyYear();
}
$query = DB::connection('gescon_import')->table($table)
->whereNotNull('legacy_year')
->where('legacy_year', '!=', '');
if ($stabile !== null && $stabile !== '' && Schema::connection('gescon_import')->hasColumn($table, 'cod_stabile')) {
$query->where('cod_stabile', $stabile);
}
$value = $query->orderByDesc('legacy_year')->value('legacy_year');
return $this->stagingLatestLegacyYearCache[$cacheKey] = ($value !== null ? trim((string) $value) : null);
}
private function currentSnapshotCondominQuery()
{
$query = DB::connection('gescon_import')->table('condomin');
if ($this->option('stabile')) {
$query->where('cod_stabile', $this->option('stabile'));
}
if ($this->option('scala')) {
$query->where('scala', $this->option('scala'));
}
$latestYear = $this->resolveLatestStagingLegacyYear('condomin', (string) ($this->option('stabile') ?? ''));
if ($latestYear !== null && Schema::connection('gescon_import')->hasColumn('condomin', 'legacy_year')) {
$query->where('legacy_year', $latestYear);
}
return $query;
}
private function currentSnapshotComproprietariQuery()
{
$query = DB::connection('gescon_import')->table('comproprietari');
if ($this->option('stabile')) {
$query->where('cod_stabile', $this->option('stabile'));
}
$latestYear = $this->resolveLatestStagingLegacyYear('comproprietari', (string) ($this->option('stabile') ?? ''))
?: $this->resolveLatestStagingLegacyYear('condomin', (string) ($this->option('stabile') ?? ''));
if ($latestYear !== null && Schema::connection('gescon_import')->hasColumn('comproprietari', 'legacy_year')) {
$query->where('legacy_year', $latestYear);
}
return $query;
}
// Mapping campi configurato via UI (per-utente/per-stabile). Caricato on-demand.
private const BOOLEAN_TARGET_KEYS = [
'stabile.ascensore',
'stabile.presenza_ascensore',
'stabile.riscaldamento_centralizzato',
'stabile.acqua_centralizzata',
'stabile.gas_centralizzato',
'stabile.servizio_portineria',
'stabile.videocitofono',
'stabile.antenna_tv_centralizzata',
'stabile.internet_condominiale',
'stabile.piano_seminterrato',
'stabile.piano_sottotetto',
];
private array $fieldMapping = [];
private array $banksMapping = [];
private array $manualFieldValues = [];
private bool $forceMapping = false;
private string $splitMode = 'palazzine';
// Cache per risoluzione conto bancario di default per stabile
private array $defaultContoCache = [];
// Legacy row cache per stabili (usata da mapField migliorata)
private ?array $stabileLegacyRow = null;
private function stepRelazioni(?int $limit = null): int
{
$params = [
'--stabile' => (string) $this->option('stabile'),
'--limit' => $limit ?? 10000,
];
if (! $this->option('dry-run')) {
$params['--apply'] = true;
}
$exit = $this->call('gescon:materialize-persone-relazioni', $params);
if ($exit !== 0) {
throw new \RuntimeException('Materializzazione persone/relazioni non riuscita.');
}
return (int) DB::table('unita_immobiliare_nominativi as uin')
->join('unita_immobiliari as ui', 'ui.id', '=', 'uin.unita_immobiliare_id')
->join('stabili as s', 's.id', '=', 'ui.stabile_id')
->when($this->option('stabile'), function ($query): void {
$stabile = trim((string) $this->option('stabile'));
$trimmed = ltrim($stabile, '0');
$query->where(function ($builder) use ($stabile, $trimmed): void {
$builder->where('s.codice_stabile', $stabile);
if ($trimmed !== '' && $trimmed !== $stabile) {
$builder->orWhere('s.codice_stabile', $trimmed);
}
});
})
->count();
}
public function handle(): int
{
if (! $this->option('stabile')) {
$this->error('Devi specificare --stabile=<codice cartella> (es. --stabile=0001).');
return 1;
}
$solo = $this->option('solo') ?: 'tutto';
$dry = (bool) $this->option('dry-run');
$limit = $this->option('limit') ? (int) $this->option('limit') : null;
$this->isDryRun = $dry;
$this->forceMapping = (bool) $this->option('force-mapping');
$this->splitMode = in_array($this->option('split-mode') ?? 'palazzine', ['none', 'palazzine', 'stabili-per-scala'], true)
? (string) $this->option('split-mode')
: 'palazzine';
$this->stabileId = $this->resolveStabileId();
$steps = $solo === 'tutto' ? $this->stepsOrder : array_intersect($this->stepsOrder, [$solo]);
if (empty($steps)) {
$this->error('Nessuno step valido.');
return 1;
}
$this->info('🚀 Avvio pipeline import Gescon');
$this->line('Steps: ' . implode(' -> ', $steps) . ($dry ? ' (DRY RUN)' : ''));
// Carica mapping se disponibile per lo stabile richiesto
$this->loadMappingIfAny();
// Preflight: assicura dati minimi di staging per lo stabile (condomin) se fornito un MDB esplicito
$this->preflightStaging();
foreach ($steps as $step) {
$method = 'step' . ucfirst($step);
if (! method_exists($this, $method)) {
$this->warn("Skip step sconosciuto: {$step}");
continue;
}
$this->newLine();
$this->line("▶️ Step {$step}...");
if ($dry) {
DB::beginTransaction();
}
try {
$count = $this->{$method}($limit);
$this->info("{$step}: {$count} record");
if ($dry && DB::transactionLevel() > 0) {
DB::rollBack();
}
} catch (Throwable $e) {
if ($dry && DB::transactionLevel() > 0) {
DB::rollBack();
}
$this->error("Errore step {$step}: " . $e->getMessage());
return 1;
}
}
if (! $this->option('no-views')) {
$this->callSilently('migrate', ['--path' => 'database/migrations/2025_08_14_120000_create_gescon_domain_tables_and_views.php']);
$this->info('🔄 Viste QA aggiornate.');
}
$this->info('✅ Pipeline completata.');
return 0;
}
private function stepStabili(?int $limit): int
{
// Crea stabili di dominio partendo da staging.stabili (se presente) o da condomin.cod_stabile
$created = 0;
$updated = 0;
$stagingHasStabili = Schema::connection('gescon_import')->hasTable('stabili');
$src = $stagingHasStabili
? DB::connection('gescon_import')->table('stabili')
: (Schema::connection('gescon_import')->hasTable('condomin') ? DB::connection('gescon_import')->table('condomin')->select('cod_stabile as cod_stabile')->distinct() : null);
if (! $src) {
// Fallback: se non c'è staging e abbiamo --mdb e --stabile, prova a creare lo stabile dai metadati di Stabili.mdb
$codRichiesto = $this->option('stabile');
$mdbPath = $this->option('mdb')
?: ($this->option('path') ? rtrim($this->option('path'), '/') . '/Stabili.mdb' : null);
if ($codRichiesto && $mdbPath && is_file($mdbPath) && Schema::hasTable('stabili')) {
$created += $this->createStabileFromMdbIfNeeded($mdbPath, $codRichiesto);
return $created;
}
return 0;
}
if ($this->option('stabile')) {
$src->where('cod_stabile', $this->option('stabile'));
}
if ($limit) {
$src->limit($limit);
}
foreach ($src->get() as $row) {
$cod = $row->cod_stabile ?? null;
if (! $cod) {
continue;
}
if (! Schema::hasTable('stabili')) {
continue;
}
$col = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione';
$exists = DB::table('stabili')->where($col, $cod)->first();
$amminId = null;
if (Schema::hasColumn('stabili', 'amministratore_id') && Schema::hasTable('amministratori')) {
$optAdm = $this->option('amministratore-id');
if ($optAdm) {
$existsAdm = DB::table('amministratori')->where('id', (int) $optAdm)->exists();
$amminId = $existsAdm ? (int) $optAdm : null;
}
if (! $amminId && $exists && isset($exists->amministratore_id)) {
$amminId = (int) $exists->amministratore_id ?: null;
}
if (! $amminId && ! $exists) {
$amminId = DB::table('amministratori')->value('id') ?? null;
}
}
$payload = [
$col => $cod,
'denominazione' => $this->mapField('stabile.denominazione', $cod),
'indirizzo' => $this->mapField('stabile.indirizzo', 'ND'),
'citta' => 'ND',
'cap' => $this->mapField('stabile.cap', '00000'),
'provincia' => $this->mapField('stabile.provincia', 'XX'),
'amministratore_id' => $amminId,
'attivo' => true,
'updated_at' => now(),
];
if (! $exists) {
$payload['created_at'] = now();
// Tag provenienza per visibilità in UI
$payload['note'] = trim((string) ($payload['note'] ?? ''));
$payload['note'] = ($payload['note'] ? ($payload['note'] . ' ') : '') . '[GESCON]';
$this->dynamicInsert('stabili', $payload);
$created++;
} else {
// Upsert non distruttivo: aggiorna solo se vuoti e se mapping prevede un valore
$updates = [];
foreach (['denominazione', 'indirizzo', 'cap', 'provincia'] as $k) {
if (! empty($payload[$k])) {
if (empty($exists->$k)) {
$updates[$k] = $payload[$k];
}
}
}
// Se richiesto esplicitamente, riassegna amministratore dello stabile esistente
if ($amminId && Schema::hasColumn('stabili', 'amministratore_id')) {
$cur = (int) ($exists->amministratore_id ?? 0);
if ($cur !== $amminId) {
if ($this->isDryRun) {
if ($this->output->isVerbose()) {
$this->line(" (dry-run) riassegnazione stabile {$cod} → amministratore #{$amminId}");
}
} else {
DB::table('stabili')->where('id', $exists->id)->update([
'amministratore_id' => $amminId,
'updated_at' => now(),
]);
$updated++;
$this->line(" -> riassegnato stabile {$cod} all'amministratore #{$amminId}");
}
}
}
if (! empty($updates)) {
$updates['updated_at'] = now();
// Aggiungi tag GESCON se manca
if (Schema::hasColumn('stabili', 'note')) {
$note = (string) ($exists->note ?? '');
if (stripos($note, 'GESCON') === false) {
$updates['note'] = trim($note . ' [GESCON]');
}
}
if ($this->isDryRun) {
if ($this->output->isVerbose()) {
$this->line(' (dry-run) aggiornamento stabili skip');
}
} else {
DB::table('stabili')->where('id', $exists->id)->update($updates);
$updated++;
}
}
}
}
// Aggiorna metadati da Stabili.mdb se disponibile (nome_directory corrisponde a --stabile, es. 0021)
$mdbPath = $this->resolveStabiliMdbPath((string) ($this->option('path') ?: ''));
$codRichiesto = $this->option('stabile');
if ($codRichiesto && is_file($mdbPath)) {
$row = $this->mdbStabiliFindByDirectory($mdbPath, $codRichiesto);
if ($row) {
$col = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione';
// Match robusto: gestisci eventuali zeri iniziali nel codice
$stab = DB::table('stabili')->where($col, $codRichiesto)->first();
if (! $stab && $col === 'codice_stabile') {
$alt = ltrim($codRichiesto, '0');
if ($alt !== '' && $alt !== $codRichiesto) {
$stab = DB::table('stabili')->where($col, $alt)->first();
}
}
if ($stab) {
// Separa CF condominio (11 cifre) e CF amministratore (16 alfanumerico tipico persona fisica)
$cfCondo = $row['cf_condominio'] ?? (isset($row['codice_fiscale']) && preg_match('/^\d{11}$/', (string) $row['codice_fiscale']) ? $row['codice_fiscale'] : null);
$cfAmm = $row['cf_amministratore'] ?? (isset($row['cin']) && preg_match('/^(?=.*[A-Z])[A-Z0-9]{16}$/i', (string) $row['cin']) ? $row['cin'] : null);
// Base update dai valori MDB
$update = [
'denominazione' => $row['denominazione'] ?? $codRichiesto,
'indirizzo' => $row['indirizzo'] ?? null,
'cap' => $row['cap'] ?? null,
'citta' => $row['citta'] ?? null,
'provincia' => $row['provincia'] ?? null,
'codice_fiscale' => $cfCondo ?? null,
'updated_at' => now(),
];
if ($cfAmm && Schema::hasColumn('stabili', 'cod_fisc_amministratore')) {
$update['cod_fisc_amministratore'] = $cfAmm;
}
// Applica mapping se configurato (rispetta --force-mapping per overwrite)
$update = $this->applyStabileMapping($update, $row, $stab);
$update = $this->applyLegacyStabileDerivedFields($update, $row, $stab);
$cols = Schema::getColumnListing('stabili');
$update = array_intersect_key($update, array_flip($cols));
// Non inserire chiavi con valore NULL per evitare violazioni NOT NULL
$update = array_filter($update, function ($v) {
return $v !== null;
});
// Verbose log: evidenzia cambio CF (o potenziale in dry-run)
if ($this->output->isVerbose()) {
$oldCf = $stab->codice_fiscale ?? null;
$newCf = $update['codice_fiscale'] ?? null;
if ($newCf && $newCf !== $oldCf) {
$this->line(sprintf(
' -> CF %s: %s => %s%s%s',
$codRichiesto,
$oldCf !== null && $oldCf !== '' ? $oldCf : 'NULL',
$newCf,
$this->option('dry-run') ? ' (DRY-RUN)' : '',
$this->forceMapping ? ' [forced]' : ''
));
}
if (isset($update['cod_fisc_amministratore'])) {
$oldCfa = $stab->cod_fisc_amministratore ?? null;
$newCfa = $update['cod_fisc_amministratore'];
if ($newCfa && $newCfa !== $oldCfa) {
$this->line(sprintf(
' -> CF Amm. %s: %s => %s%s%s',
$codRichiesto,
$oldCfa !== null && $oldCfa !== '' ? $oldCfa : 'NULL',
$newCfa,
$this->option('dry-run') ? ' (DRY-RUN)' : '',
$this->forceMapping ? ' [forced]' : ''
));
}
}
}
if (! empty($update)) {
if (Schema::hasColumn('stabili', 'note')) {
$note = DB::table('stabili')->where('id', $stab->id)->value('note');
if (stripos((string) $note, 'GESCON') === false) {
$update['note'] = trim((string) $note . ' [GESCON]');
}
}
$this->safeUpdateStabile($stab->id, $update);
if (! $this->isDryRun) {
$updated++;
}
}
}
}
}
// Se disponibile la tabella staging.stabili, importa CF e Catasto
if ($codRichiesto && Schema::connection('gescon_import')->hasTable('stabili')) {
$stage = DB::connection('gescon_import')->table('stabili')->where('cod_stabile', $codRichiesto)->first();
if ($stage && Schema::hasTable('stabili')) {
$stabCol = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione';
$stab = DB::table('stabili')->where($stabCol, $codRichiesto)->first();
if (! $stab && $stabCol === 'codice_stabile') {
$alt = ltrim($codRichiesto, '0');
if ($alt !== '' && $alt !== $codRichiesto) {
$stab = DB::table('stabili')->where($stabCol, $alt)->first();
}
}
if ($stab) {
$cols = Schema::getColumnListing('stabili');
$map = [];
// CF condominio
$map['codice_fiscale'] = $stage->codice_fiscale ?? ($stage->codice_fiscale_cond ?? ($stage->cf ?? ($stage->cf_condominio ?? null)));
// CF amministratore (se presente in staging)
$map['cod_fisc_amministratore'] = $stage->cod_fisc_amministratore ?? ($stage->cf_amministratore ?? null);
// Dati catasto (normalizza verso colonne effettive)
$foglioVal = $stage->foglio ?? ($stage->catasto_foglio ?? null);
if ($foglioVal !== null) {
if (in_array('foglio', $cols, true)) {
$map['foglio'] = $foglioVal;
} elseif (in_array('foglio_catasto', $cols, true)) {
$map['foglio_catasto'] = $foglioVal;
}
}
$partVal = $stage->particella ?? ($stage->catasto_particella ?? ($stage->mappale ?? null));
if ($partVal !== null) {
if (in_array('mappale', $cols, true)) {
$map['mappale'] = $partVal;
} elseif (in_array('particella_catasto', $cols, true)) {
$map['particella_catasto'] = $partVal;
}
}
$subVal = $stage->subalterno ?? ($stage->catasto_sub ?? null);
if ($subVal !== null && in_array('subalterno', $cols, true)) {
$map['subalterno'] = $subVal;
}
// Categoria catastale (se presente in staging)
$catVal = $stage->categoria ?? ($stage->categoria_catastale ?? null);
if ($catVal !== null) {
if (in_array('categoria', $cols, true)) {
$map['categoria'] = $catVal;
} elseif (in_array('categoria_catastale', $cols, true)) {
$map['categoria_catastale'] = $catVal;
}
}
// Filtra solo colonne esistenti e valori non null
$update = [];
foreach ($map as $k => $v) {
if ($v !== null && in_array($k, $cols, true)) {
$update[$k] = $v;
}
}
// Applica mapping UI anche sui dati staging (consente override esplicito CF quando forzato)
$update = $this->applyStabileMapping($update, (array) $stage, $stab);
if (! empty($update)) {
// Verbose log: evidenzia cambio CF da staging
if ($this->output->isVerbose()) {
$oldCf = $stab->codice_fiscale ?? null;
$newCf = $update['codice_fiscale'] ?? null;
if ($newCf && $newCf !== $oldCf) {
$this->line(sprintf(
' -> CF %s (staging): %s => %s%s%s',
$codRichiesto,
$oldCf !== null && $oldCf !== '' ? $oldCf : 'NULL',
$newCf,
$this->option('dry-run') ? ' (DRY-RUN)' : '',
$this->forceMapping ? ' [forced]' : ''
));
}
}
$update['updated_at'] = now();
$this->safeUpdateStabile($stab->id, $update);
if (! $this->isDryRun) {
$updated++;
}
}
}
}
}
if ($created > 0 || $updated > 0) {
$this->line(" → stabili creati: {$created}, aggiornati: {$updated}");
}
return $created + $updated;
}
private function stepUnita(?int $limit): int
{
// Fonte: gescon_import.condomin (elenco soggetti/unità)
if (! Schema::connection('gescon_import')->hasTable('condomin')) {
return 0;
}
$count = 0;
$updated = 0;
$hasDetailedSchema = Schema::hasColumn('unita_immobiliari', 'codice_unita');
$parentStabile = null;
if (Schema::hasTable('stabili') && $this->option('stabile')) {
$stabileCol = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione';
$parentStabile = DB::table('stabili')->where($stabileCol, $this->option('stabile'))->first();
if (! $parentStabile && $stabileCol === 'codice_stabile') {
$alt = ltrim($this->option('stabile'), '0');
if ($alt !== '' && $alt !== $this->option('stabile')) {
$parentStabile = DB::table('stabili')->where($stabileCol, $alt)->first();
}
}
}
$pertinenzeToLink = [];
$seenCondIds = [];
$processed = 0;
$batch = $limit ? min(max(1, $limit), 1000) : 1000;
$offset = 0;
while (true) {
$q = $this->currentSnapshotCondominQuery();
$q->limit($batch)->offset($offset);
$rows = $q->get();
if ($rows->isEmpty()) {
break;
}
$normalizePartyName = static function (?string $value): string {
$value = Str::upper(trim((string) $value));
$value = preg_replace('/\s+/u', ' ', $value ?? '');
return $value ?? '';
};
foreach ($rows as $r) {
$legacyCondId = null;
if (isset($r->cod_cond)) {
$legacyCondId = trim((string) $r->cod_cond);
} elseif (isset($r->id_cond)) {
$legacyCondId = trim((string) $r->id_cond);
}
if ($legacyCondId !== null && $legacyCondId !== '') {
if (isset($seenCondIds[$legacyCondId])) {
continue;
}
$seenCondIds[$legacyCondId] = true;
}
// risolvi stabile_id per riga (multi-stabili)
$rowStabileId = null;
if (Schema::hasTable('stabili')) {
$stabileCol = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione';
$stRow = DB::table('stabili')->where($stabileCol, $r->cod_stabile ?? $this->option('stabile'))->first();
$rowStabileId = $stRow->id ?? $this->stabileId;
// Split per scala: crea/usa stabile per scala
$scalaCur = trim((string) ($r->scala ?? 'A')) ?: 'A';
if ($this->splitMode === 'stabili-per-scala' && $parentStabile) {
[$childId, $childCode] = $this->ensureStabilePerScala($parentStabile, $scalaCur);
if ($childId) {
$rowStabileId = $childId;
// opzionale: potremmo voler usare childCode in codice_unita per coerenza
}
}
} else {
$rowStabileId = $this->stabileId;
}
$interno = isset($r->interno) ? trim((string) $r->interno) : null;
if ($interno === '') {
$interno = null;
}
$sub = isset($r->catasto_sub) ? trim((string) $r->catasto_sub) : (isset($r->sub) ? trim((string) $r->sub) : $interno);
if ($sub === '') {
$sub = null;
}
// Riconosci pertinenze nel campo INT o note: cantina, box/garage, posto auto, soffitta/solaio, locale tecnico
$internoStr = is_string($interno) ? trim($interno) : (string) $interno;
$ptype = null;
$pcode = null; // tipo pertinenza e codice interno normalizzato
if ($internoStr !== '') {
if (preg_match('/^CAN\s*(\d+)$/i', preg_replace('/\s+/', ' ', $internoStr), $m)) {
$ptype = 'cantina';
$pcode = 'CAN' . $m[1];
} elseif (preg_match('/^(BOX|GAR|GARAGE)\s*(\d+)?$/i', preg_replace('/\s+/', ' ', $internoStr), $m)) {
$ptype = 'box';
$pcode = 'BOX' . ($m[2] ?? '');
} elseif (preg_match('/^(PA|POSTO\s*AUTO)\s*(\d+)?$/i', preg_replace('/\s+/', ' ', $internoStr), $m)) {
$ptype = 'posto_auto';
$pcode = 'PA' . ($m[2] ?? '');
} elseif (preg_match('/^(SOF|SOFFITTA|SOLAIO)\s*(\d+)?$/i', preg_replace('/\s+/', ' ', $internoStr), $m)) {
$ptype = 'soffitta';
$pcode = 'SOF' . ($m[2] ?? '');
} elseif (preg_match('/^(LT|LOCALE\s*TECNICO)\s*(\d+)?$/i', preg_replace('/\s+/', ' ', $internoStr), $m)) {
$ptype = 'locale_tecnico';
$pcode = 'LT' . ($m[2] ?? '');
}
}
$scalaVal = trim((string) ($r->scala ?? 'A')) ?: 'A';
// Piano: normalizza (R=0, T=0, 1=1, -1 o S= -1)
$pianoVal = 0;
if (isset($r->piano)) {
$pv = is_string($r->piano) ? strtoupper(trim($r->piano)) : (string) $r->piano;
if ($pv === 'T' || $pv === 'PT' || $pv === 'R' || $pv === 'PR' || $pv === '') {
$pianoVal = 0;
} elseif ($pv === 'S' || $pv === 'PS' || $pv === '-1') {
$pianoVal = -1;
} elseif (is_numeric($pv)) {
$pianoVal = (int) $pv;
}
}
$normalizedInterno = $ptype && $pcode ? $pcode : $interno;
if ($normalizedInterno !== null) {
$normalizedInterno = trim((string) $normalizedInterno);
if ($normalizedInterno === '' || (! $ptype && preg_match('/^0+$/', $normalizedInterno))) {
$normalizedInterno = null;
}
}
if ($hasDetailedSchema) {
$exists = null;
if ($legacyCondId !== null && $legacyCondId !== '' && Schema::hasColumn('unita_immobiliari', 'legacy_cond_id')) {
$exists = DB::table('unita_immobiliari')
->where('stabile_id', $rowStabileId)
->where('legacy_cond_id', $legacyCondId)
->first();
}
if ($normalizedInterno === null && ! $exists) {
$this->warn(sprintf(
'Unita saltata per stabile %s cod_cond %s: interno mancante nello snapshot corrente autorevole.',
(string) ($r->cod_stabile ?? $this->option('stabile') ?? '-'),
$legacyCondId !== null && $legacyCondId !== '' ? $legacyCondId : '-'
));
continue;
}
// Codice unita coerente: STAB-SCALA-INT (per pertinenze: STAB-SCALA-CAN<n>/BOX<n>/...)
$intPart = $normalizedInterno;
$codiceBaseStab = ($r->cod_stabile ?? $this->option('stabile')) ?: 'S0';
$codice = $intPart !== null
? $this->buildLegacyUnitCode($codiceBaseStab, $scalaVal ?: 'A', (string) $intPart, $legacyCondId)
: (string) ($exists->codice_unita ?? '');
// Filtri post-normalizzazione: pertinenze e piano min/max
if ($this->option('only-pertinenze')) {
$opt = strtolower((string) $this->option('only-pertinenze'));
$isPert = (bool) $ptype;
$ok = $opt === 'any' ? $isPert : ($isPert && $ptype === $opt);
if (! $ok) {
continue;
}
}
$pMin = $this->option('piano-min') !== null ? (int) $this->option('piano-min') : null;
$pMax = $this->option('piano-max') !== null ? (int) $this->option('piano-max') : null;
if ($pMin !== null && $pianoVal < $pMin) {
continue;
}
if ($pMax !== null && $pianoVal > $pMax) {
continue;
}
if (! $exists && $codice !== '') {
$exists = DB::table('unita_immobiliari')->where('codice_unita', $codice)->first();
if ($exists && $legacyCondId !== null && $legacyCondId !== '' && Schema::hasColumn('unita_immobiliari', 'legacy_cond_id')) {
if (! empty($exists->legacy_cond_id) && (string) $exists->legacy_cond_id !== (string) $legacyCondId) {
$codice = $this->buildLegacyUnitCode($codiceBaseStab, $scalaVal ?: 'A', (string) $intPart, 'C' . $legacyCondId);
$exists = DB::table('unita_immobiliari')->where('codice_unita', $codice)->first();
}
}
}
if ($exists) {
// Aggiorna palazzina/scala se richiesto
$patch = [];
if ($legacyCondId !== null && $legacyCondId !== '' && Schema::hasColumn('unita_immobiliari', 'legacy_cond_id')) {
if (empty($exists->legacy_cond_id) || (string) $exists->legacy_cond_id !== (string) $legacyCondId) {
$patch['legacy_cond_id'] = $legacyCondId;
}
}
if ($codice !== '' && Schema::hasColumn('unita_immobiliari', 'codice_unita') && (string) ($exists->codice_unita ?? '') !== $codice) {
$codeOwner = DB::table('unita_immobiliari')
->where('codice_unita', $codice)
->where('id', '!=', $exists->id)
->first();
if (! $codeOwner) {
$patch['codice_unita'] = $codice;
}
}
if ($normalizedInterno !== null && Schema::hasColumn('unita_immobiliari', 'interno') && (string) ($exists->interno ?? '') !== $normalizedInterno) {
$patch['interno'] = $normalizedInterno;
}
if (Schema::hasColumn('unita_immobiliari', 'subalterno') && (string) ($exists->subalterno ?? '') !== (string) ($sub ?? '')) {
$patch['subalterno'] = $sub;
}
if (Schema::hasColumn('unita_immobiliari', 'piano') && (int) ($exists->piano ?? 0) !== $pianoVal) {
$patch['piano'] = $pianoVal;
}
if (Schema::hasColumn('unita_immobiliari', 'denominazione')) {
$denominazione = trim(($r->cognome ?? '') . ' ' . ($r->nome ?? '')) ?: null;
if ($denominazione !== null && (string) ($exists->denominazione ?? '') !== $denominazione) {
$patch['denominazione'] = $denominazione;
}
}
if (Schema::hasColumn('unita_immobiliari', 'tipo_unita') && (string) ($exists->tipo_unita ?? '') !== ($ptype ?: 'abitazione')) {
$patch['tipo_unita'] = $ptype ?: 'abitazione';
}
if (Schema::hasColumn('unita_immobiliari', 'attiva') && ! (bool) ($exists->attiva ?? false)) {
$patch['attiva'] = true;
}
if (Schema::hasColumn('unita_immobiliari', 'deleted_at') && ! empty($exists->deleted_at)) {
$patch['deleted_at'] = null;
}
if ($this->splitMode === 'palazzine') {
if (Schema::hasColumn('unita_immobiliari', 'palazzina')) {
if (empty($exists->palazzina) || $exists->palazzina !== $scalaVal) {
$patch['palazzina'] = $scalaVal;
}
}
if (Schema::hasColumn('unita_immobiliari', 'scala') && (empty($exists->scala) || $exists->scala !== $scalaVal)) {
$patch['scala'] = $scalaVal;
}
}
if (! empty($patch)) {
$patch['updated_at'] = now();
if (! $this->isDryRun) {
DB::table('unita_immobiliari')->where('id', $exists->id)->update($patch);
}
$updated++;
}
if (! $this->isDryRun && $exists->id) {
$this->upsertUnitaNominativiFromLegacy((int) $exists->id, (int) $rowStabileId, $legacyCondId, $r);
}
continue;
}
$data = [
'stabile_id' => $rowStabileId,
'palazzina_id' => null,
'codice_unita' => $codice,
'denominazione' => trim(($r->cognome ?? '') . ' ' . ($r->nome ?? '')) ?: null,
'descrizione' => null,
'palazzina' => $this->splitMode === 'palazzine' ? $scalaVal : null,
'scala' => $scalaVal,
'piano' => $pianoVal,
'interno' => $normalizedInterno,
'subalterno' => $sub,
'categoria_catastale' => null,
'classe' => null,
'consistenza' => null,
'rendita_catastale' => null,
'millesimi_generali' => 0,
'legacy_cond_id' => $legacyCondId,
'tipo_unita' => $ptype ?: 'abitazione',
'stato_conservazione' => 'buono',
'stato_occupazione' => 'occupata_proprietario',
'attiva' => true,
'unita_demo' => false,
'created_at' => now(),
'updated_at' => now(),
];
$this->dynamicInsert('unita_immobiliari', $data);
$unitaInserted = DB::table('unita_immobiliari')
->where('stabile_id', $rowStabileId)
->when(Schema::hasColumn('unita_immobiliari', 'legacy_cond_id') && $legacyCondId !== null && $legacyCondId !== '', function ($q) use ($legacyCondId) {
$q->where('legacy_cond_id', $legacyCondId);
})
->when($legacyCondId === null || $legacyCondId === '', function ($q) use ($codice) {
$q->where('codice_unita', $codice);
})
->orderByDesc('id')
->first();
if (! $this->isDryRun && $unitaInserted?->id) {
$this->upsertUnitaNominativiFromLegacy((int) $unitaInserted->id, (int) $rowStabileId, $legacyCondId, $r);
}
// Se è pertinenza, memorizza relazione da collegare in un secondo pass
if ($ptype) {
$pertinenzeToLink[] = [
'stabile_id' => $rowStabileId,
'scala' => $r->scala ?? 'A',
'interno' => (string) ($pcode ?: $interno),
'tipo' => $ptype,
'proprietario_cognome' => $r->cognome ?? null,
'proprietario_nome' => $r->nome ?? null,
];
}
} else { // schema semplice (anagrafiche base)
$criteria = ['stabile_id' => $this->stabileId];
if ($interno !== null) {
$criteria['interno'] = (string) $interno;
}
$exists = DB::table('unita_immobiliari')->where($criteria)->first();
if ($exists) {
// Aggiorna palazzina/scala se in split palazzine
if ($this->splitMode === 'palazzine') {
$patch = [];
if (Schema::hasColumn('unita_immobiliari', 'palazzina') && (empty($exists->palazzina) || $exists->palazzina !== $scalaVal)) {
$patch['palazzina'] = $scalaVal;
}
if (Schema::hasColumn('unita_immobiliari', 'scala') && (empty($exists->scala) || $exists->scala !== $scalaVal)) {
$patch['scala'] = $scalaVal;
}
if (! empty($patch)) {
$patch['updated_at'] = now();
if (! $this->isDryRun) {
DB::table('unita_immobiliari')->where('id', $exists->id)->update($patch);
}
$updated++;
}
}
continue;
}
$data = [
'stabile_id' => $rowStabileId,
'interno' => (string) $interno,
'scala' => $scalaVal,
'piano' => $pianoVal,
'subalterno' => (string) $sub,
'categoria_catastale' => null,
'superficie' => null,
'vani' => null,
'indirizzo' => $r->indirizzo_corrispondenza ?? null,
'note' => null,
'created_at' => now(),
'updated_at' => now(),
];
$this->dynamicInsert('unita_immobiliari', $data);
}
$count++;
$processed++;
if ($limit && $processed >= $limit) {
break 2;
}
}
$offset += $batch;
}
// Cleanup: rimuovi unità non presenti in condomin (evita "alieni")
if (! $this->isDryRun && $this->option('stabile')) {
$condRows = $this->currentSnapshotCondominQuery()
->select('scala', 'interno', 'cod_cond')
->get();
$condSet = [];
foreach ($condRows as $c) {
$internoStr = is_string($c->interno ?? null) ? trim((string) $c->interno) : (string) ($c->interno ?? '');
if ($internoStr !== '' && preg_match('/APP\.?\s*COND\.?/i', $internoStr)) {
continue;
}
if (isset($c->cod_cond)) {
$condSet[(string) $c->cod_cond] = true;
}
$key = strtoupper(trim((string) $c->scala)) . '|' . strtoupper(trim((string) $c->interno));
$condSet[$key] = true;
}
if (! empty($condSet)) {
$unitRows = DB::table('unita_immobiliari')
->where('stabile_id', $this->stabileId)
->whereNull('deleted_at')
->select('id', 'scala', 'interno', 'legacy_cond_id')
->get();
$alienIds = [];
foreach ($unitRows as $u) {
if (Schema::hasColumn('unita_immobiliari', 'legacy_cond_id') && ! empty($u->legacy_cond_id)) {
if (array_key_exists((string) $u->legacy_cond_id, $condSet)) {
continue;
}
} else {
$key = strtoupper(trim((string) $u->scala)) . '|' . strtoupper(trim((string) $u->interno));
if (array_key_exists($key, $condSet)) {
continue;
}
}
$alienIds[] = (int) $u->id;
}
if (! empty($alienIds)) {
$now = now();
DB::table('unita_immobiliari')->whereIn('id', $alienIds)->update([
'attiva' => 0,
'deleted_at' => $now,
'updated_at' => $now,
]);
if (Schema::hasTable('dettaglio_millesimi')) {
DB::table('dettaglio_millesimi')->whereIn('unita_immobiliare_id', $alienIds)->delete();
}
if (Schema::hasTable('dettaglio_importi_tabella')) {
DB::table('dettaglio_importi_tabella')->whereIn('unita_immobiliare_id', $alienIds)->delete();
}
}
}
}
// Collega pertinenze alle unità principali (cumulo) se tabella disponibile
if (! empty($pertinenzeToLink) && Schema::hasTable('unita_pertinenze')) {
foreach ($pertinenzeToLink as $p) {
// Heuristic: cerca unità principale con stesso stabile e scala; usa primo match non pertinenza
$main = DB::table('unita_immobiliari')
->where('stabile_id', $p['stabile_id'])
->where('scala', $p['scala'])
->whereNotIn('tipo_unita', ['cantina', 'box', 'garage', 'posto_auto', 'soffitta', 'locale_tecnico'])
->orderBy('id')
->first();
$cant = DB::table('unita_immobiliari')
->where('stabile_id', $p['stabile_id'])
->where('scala', $p['scala'])
->where('interno', $p['interno'])
->first();
if ($main && $cant) {
$this->dynamicInsert('unita_pertinenze', [
'stabile_id' => $p['stabile_id'],
'unita_principale_id' => $main->id,
'unita_accessoria_id' => $cant->id,
'tipo' => $p['tipo'] ?? 'pertinenza',
'cumulo' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
}
if ($updated > 0) {
$this->line(" → unita aggiornate: {$updated}");
}
return $count + $updated;
}
private function stepSoggetti(?int $limit): int
{
// Deriva da proprietari/inquilini nei campi condomin
if (! Schema::connection('gescon_import')->hasTable('condomin')) {
return 0;
}
$stabileId = $this->resolveStabileId();
$count = 0;
$updated = 0;
// Se nel mapping sono presenti codici cassa target (es. CCB, CDL), assicurali nelle casse
try {
if (! empty($this->banksMapping) && Schema::hasTable('casse') && $stabileId) {
foreach ($this->banksMapping as $bm) {
if (! is_array($bm)) {
continue;
}
$code = $this->resolveMappingCodiceCassa($bm);
if ($code) {
$exists = DB::table('casse')->where('stabile_id', $stabileId)->where('cod_cassa', $code)->first();
if (! $exists) {
DB::table('casse')->insert([
'tenant_id' => null,
'stabile_id' => $stabileId,
'cod_cassa' => $code,
'descrizione' => 'Cassa ' . $code,
'tipo' => ($code === 'CON') ? 'cassa_contanti' : (($code === 'CDL') ? 'altro' : 'conto_bancario_locale'),
'iban' => null,
'intestazione_conto' => null,
'attiva' => true,
'meta' => json_encode(['from_mapping' => true]),
'created_at' => now(),
'updated_at' => now(),
]);
}
}
}
}
} catch (\Throwable $e) {
}
$processed = 0;
$batch = $limit ? min(max(1, $limit), 1000) : 1000;
$offset = 0;
while (true) {
$q = $this->currentSnapshotCondominQuery();
$q->limit($batch)->offset($offset);
$rows = $q->get();
if ($rows->isEmpty()) {
break;
}
foreach ($rows as $r) {
$cf = trim((string) ($r->codice_fiscale ?? '')) ?: null;
if ($cf) {
$cf = strtoupper($cf);
}
$email = trim((string) ($r->email ?? '')) ?: null;
$nome = $r->nome ?? null;
$cognome = $r->cognome ?? null;
// Unique/dedup: preferisci CF -> email -> nome+cognome normalizzati (no uso old_id per evitare duplicati per UI)
$useDbTrigger = (bool) env('GESCON_USE_DB_TRIGGER_CODES', false);
$exists = $this->findExistingSoggetto($cf, $email, $nome, $cognome);
if ($exists) {
// Upsert non distruttivo: completa campi mancanti
$patch = [];
foreach (
[
'nome' => $nome,
'cognome' => $cognome,
'codice_fiscale' => $cf,
'email' => $email,
'telefono' => $r->telefono ?? null,
'indirizzo' => $r->indirizzo_corrispondenza ?? null,
] as $k => $v
) {
if (! empty($v) && (empty($exists->$k) || $exists->$k === 'ND')) {
$patch[$k] = $v;
}
}
if (! empty($patch)) {
$patch['updated_at'] = now();
DB::table('soggetti')->where('id', $exists->id)->update($patch);
$updated++;
}
continue;
}
// Tipo soggetto derivato da flag proprietario/inquilino
$tipo = null;
if (property_exists($r, 'inquilino') && $r->inquilino) {
$tipo = 'inquilino';
} elseif (property_exists($r, 'proprietario') && $r->proprietario) {
$tipo = 'proprietario';
}
$data = [
'old_id' => $this->safeOldId($r->cod_cond ?? null),
'nome' => $nome,
'cognome' => $cognome,
'ragione_sociale' => null,
'codice_fiscale' => $cf,
'partita_iva' => null,
'email' => $email,
'telefono' => $r->telefono ?? null,
'indirizzo' => $r->indirizzo_corrispondenza ?? null,
'cap' => null,
'citta' => null,
'provincia' => null,
'tipo' => $tipo ?? 'proprietario',
'attivo' => Schema::hasColumn('soggetti', 'attivo') ? true : null,
'created_at' => now(),
'updated_at' => now(),
];
if (! $useDbTrigger) {
$baseKey = $this->normalizeNameKey($nome, $cognome) ?: ($email ?: 'ND');
$code = $this->generateStableCondKey('S', $r->cod_stabile ?? 'S0', $baseKey);
// Ensure uniqueness by retrying with cond code salt
$tries = 0;
while (Schema::hasTable('soggetti') && DB::table('soggetti')->where('codice_univoco', $code)->exists() && $tries < 3) {
$salt = ($r->cod_cond ?? '') . '-' . ($tries + 1);
$code = $this->generateStableCondKey('S', $r->cod_stabile ?? 'S0', $baseKey . '|' . $salt);
$tries++;
}
$data['codice_univoco'] = $code;
}
// Final guard: skip if same unique code already exists with same name/email
if (Schema::hasTable('soggetti') && isset($data['codice_univoco'])) {
$ex = DB::table('soggetti')->where('codice_univoco', $data['codice_univoco'])->first();
if ($ex) {
continue;
}
}
$this->dynamicInsert('soggetti', $data);
$count++;
$processed++;
if ($limit && $processed >= $limit) {
break 2;
}
}
$offset += $batch;
}
// Dedup dopo inserimento: unisci per CF e per email
$this->dedupSoggettiBy('codice_fiscale');
$this->dedupSoggettiBy('email');
return $count + $updated;
}
private function stepDiritti(?int $limit): int
{
if (! Schema::connection('gescon_import')->hasTable('condomin')) {
return 0;
}
if (! Schema::hasTable('proprieta') || ! Schema::hasTable('unita_immobiliari') || ! Schema::hasTable('soggetti')) {
return 0;
}
$created = 0;
$updated = 0;
$skipped = 0;
$processed = 0;
$batch = $limit ? min(max(1, $limit), 1000) : 1000;
$offset = 0;
$normalizePartyName = static function (?string $value): string {
$value = Str::upper(trim((string) $value));
$value = preg_replace('/\s+/u', ' ', $value ?? '');
return $value ?? '';
};
while (true) {
$q = $this->currentSnapshotCondominQuery();
$q->limit($batch)->offset($offset);
$rows = $q->get();
if ($rows->isEmpty()) {
break;
}
foreach ($rows as $r) {
// Resolve unità
$unita = $this->findUnitaByStagingRow($r);
if (! $unita) {
$skipped++;
continue;
}
$tasks = [];
$ownerCf = trim((string) ($r->codice_fiscale ?? $r->cond_cod_fisc ?? '')) ?: null;
$ownerEmail = trim((string) ($r->email ?? $r->e_mail_condomino ?? '')) ?: null;
$ownerNome = isset($r->nome) ? trim((string) $r->nome) : null;
$ownerCognome = isset($r->cognome) ? trim((string) $r->cognome) : null;
$ownerNome = $ownerNome !== '' ? $ownerNome : null;
$ownerCognome = $ownerCognome !== '' ? $ownerCognome : null;
if (($r->proprietario ?? 0) || $ownerCf || $ownerEmail || $ownerNome || $ownerCognome) {
$tasks[] = [
'tipo' => 'proprieta',
'nome' => $ownerNome,
'cognome' => $ownerCognome,
'cf' => $ownerCf,
'email' => $ownerEmail,
'telefono' => trim((string) ($r->telefono ?? $r->tel1 ?? '')) ?: null,
'percentuale_possesso' => isset($r->proprietario_diritto_percentuale) && is_numeric($r->proprietario_diritto_percentuale)
? (float) $r->proprietario_diritto_percentuale
: (isset($r->perc_diritto_reale) && is_numeric($r->perc_diritto_reale) ? (float) $r->perc_diritto_reale : null),
'percentuale_detrazione' => isset($r->perc_detrazione) && is_numeric($r->perc_detrazione)
? (float) $r->perc_detrazione
: null,
'data_inizio' => $this->normalizeLegacyDate($r->proprietario_subentrato_dal ?? $r->subentrato_dal ?? null),
'data_fine' => $this->normalizeLegacyDate($r->proprietario_attivo_fino_al ?? $r->attivo_fino_al ?? null),
'legacy_suffix' => 'owner',
'soggetto_tipo' => 'proprietario',
];
}
$tenantName = trim((string) ($r->inquil_nome ?? $r->inquilino_denominazione ?? '')) ?: null;
$tenantCf = trim((string) ($r->inquil_cod_fisc ?? '')) ?: null;
$tenantEmail = trim((string) ($r->e_mail_inquilino ?? '')) ?: null;
$tenantPhone = trim((string) ($r->inquil_tel1 ?? '')) ?: null;
if (($r->inquilino ?? 0) || $tenantName || $tenantCf || $tenantEmail || $tenantPhone) {
$tasks[] = [
'tipo' => 'locazione',
'nome' => $tenantName,
'cognome' => null,
'cf' => $tenantCf,
'email' => $tenantEmail,
'telefono' => $tenantPhone,
'percentuale_possesso' => null,
'percentuale_detrazione' => null,
'data_inizio' => $this->normalizeLegacyDate($r->inquil_dal ?? $r->inquilino_subentrato_dal ?? null),
'data_fine' => $this->normalizeLegacyDate($r->inquil_al ?? $r->inquilino_attivo_fino_al ?? null),
'legacy_suffix' => 'tenant',
'soggetto_tipo' => 'inquilino',
];
}
if ($tasks === []) {
$skipped++;
continue;
}
foreach ($tasks as $task) {
$sog = $this->findExistingSoggetto($task['cf'], $task['email'], $task['nome'], $task['cognome']);
if ($sog && $task['tipo'] === 'locazione' && ! empty($task['nome'])) {
$matchedName = trim((string) (($sog->cognome ?? '') . ' ' . ($sog->nome ?? '')));
if ($matchedName === '' && ! empty($sog->ragione_sociale)) {
$matchedName = trim((string) $sog->ragione_sociale);
}
$tenantName = trim((string) (($task['cognome'] ?? '') . ' ' . ($task['nome'] ?? '')));
$sameUnitOwner = DB::table('proprieta')
->where('unita_immobiliare_id', $unita->id)
->where('soggetto_id', $sog->id)
->where('tipo_diritto', 'proprieta')
->exists();
if ($sameUnitOwner && $normalizePartyName($matchedName) !== $normalizePartyName($tenantName)) {
$sog = null;
}
}
if (! $sog) {
$payload = [
'nome' => $task['nome'],
'cognome' => $task['cognome'],
'codice_fiscale' => $task['cf'],
'email' => $task['email'],
'telefono' => $task['telefono'],
'tipo' => $task['soggetto_tipo'],
'created_at' => now(),
'updated_at' => now(),
];
if (Schema::hasColumn('soggetti', 'codice_univoco')) {
$baseKey = $this->normalizeNameKey($payload['nome'] ?? '', $payload['cognome'] ?? '')
?: (($payload['codice_fiscale'] ?? null) ?: (($payload['email'] ?? null) ?: 'ND'));
$code = $this->generateStableCondKey('S', $r->cod_stabile ?? 'S0', $baseKey);
$tries = 0;
while (DB::table('soggetti')->where('codice_univoco', $code)->exists() && $tries < 5) {
$salt = ($r->cod_cond ?? '') . '-' . ($task['legacy_suffix'] ?? 'rel') . '-' . ($tries + 1);
$code = $this->generateStableCondKey('S', $r->cod_stabile ?? 'S0', $baseKey . '|' . $salt);
$tries++;
}
$payload['codice_univoco'] = $code;
}
if (isset($payload['codice_univoco'])) {
$existingByCode = DB::table('soggetti')->where('codice_univoco', $payload['codice_univoco'])->first();
if ($existingByCode) {
$sog = $existingByCode;
}
}
if (! $sog) {
$sid = DB::table('soggetti')->insertGetId($payload);
$sog = DB::table('soggetti')->where('id', $sid)->first();
}
}
if (! $sog) {
$skipped++;
continue;
}
$tipo = (string) $task['tipo'];
if ($tipo === 'locazione') {
$legacyWrongLocazione = DB::table('proprieta')
->where('unita_immobiliare_id', $unita->id)
->where('tipo_diritto', 'locazione')
->where('soggetto_id', '!=', $sog->id)
->whereIn('soggetto_id', function ($query) use ($unita) {
$query->select('soggetto_id')
->from('proprieta')
->where('unita_immobiliare_id', $unita->id)
->where('tipo_diritto', 'proprieta');
})
->orderBy('id')
->first();
if ($legacyWrongLocazione) {
$this->moveProprietaRowToSoggetto(
(int) $legacyWrongLocazione->id,
(int) $sog->id,
$task['data_inizio'] ?? null,
$task['data_fine'] ?? null
);
$updated++;
continue;
}
}
$exists = DB::table('proprieta')
->where('unita_immobiliare_id', $unita->id)
->where('soggetto_id', $sog->id)
->where('tipo_diritto', $tipo)
->first();
if ($exists) {
$patch = [];
if ($tipo === 'proprieta' && $task['percentuale_possesso'] !== null && (float) ($exists->percentuale_possesso ?? 0) !== (float) $task['percentuale_possesso']) {
$patch['percentuale_possesso'] = $task['percentuale_possesso'];
}
if ($tipo === 'proprieta' && Schema::hasColumn('proprieta', 'percentuale_detrazione') && $task['percentuale_detrazione'] !== null && (float) ($exists->percentuale_detrazione ?? 0) !== (float) $task['percentuale_detrazione']) {
$patch['percentuale_detrazione'] = $task['percentuale_detrazione'];
}
if (($task['data_inizio'] ?? null) && (string) ($exists->data_inizio ?? '') !== (string) $task['data_inizio']) {
$patch['data_inizio'] = $task['data_inizio'];
}
if (array_key_exists('data_fine', $task) && (string) ($exists->data_fine ?? '') !== (string) ($task['data_fine'] ?? '')) {
$patch['data_fine'] = $task['data_fine'];
}
if ($patch !== []) {
$patch['updated_at'] = now();
DB::table('proprieta')->where('id', $exists->id)->update($patch);
$updated++;
} else {
$skipped++;
}
} else {
DB::table('proprieta')->insert([
'unita_immobiliare_id' => $unita->id,
'soggetto_id' => $sog->id,
'tipo_diritto' => $tipo,
'percentuale_possesso' => $tipo === 'proprieta' ? $task['percentuale_possesso'] : null,
'percentuale_detrazione' => ($tipo === 'proprieta' && Schema::hasColumn('proprieta', 'percentuale_detrazione')) ? $task['percentuale_detrazione'] : null,
'data_inizio' => $task['data_inizio'],
'data_fine' => $task['data_fine'],
'created_at' => now(),
'updated_at' => now(),
]);
$created++;
}
}
$processed++;
if ($limit && $processed >= $limit) {
break 2;
}
}
$offset += $batch;
}
// Import comproprietari (se presenti)
if (Schema::connection('gescon_import')->hasTable('comproprietari')) {
$q = $this->currentSnapshotComproprietariQuery();
$comprRows = $q->limit(50000)->get();
foreach ($comprRows as $cr) {
$legacyCond = trim((string) ($cr->id_cond ?? ''));
if ($legacyCond === '') {
$skipped++;
continue;
}
$unita = null;
if (Schema::hasColumn('unita_immobiliari', 'legacy_cond_id')) {
$unita = DB::table('unita_immobiliari')
->where('stabile_id', $this->stabileId)
->whereNull('deleted_at')
->where('legacy_cond_id', $legacyCond)
->first();
}
if (! $unita) {
$fake = (object) [
'scala' => $cr->ex_scala ?? null,
'interno' => $cr->ex_int ?? null,
'cod_cond' => $legacyCond,
];
$unita = $this->findUnitaByStagingRow($fake);
}
if (! $unita) {
$skipped++;
continue;
}
$nom = trim((string) ($cr->nom_cond ?? ''));
$nome = null;
$cognome = null;
if ($nom !== '') {
if (str_contains($nom, ',')) {
[$cognome, $nome] = array_map('trim', explode(',', $nom, 2));
} else {
$parts = preg_split('/\s+/', $nom);
$cognome = $parts[0] ?? null;
$nome = isset($parts[1]) ? implode(' ', array_slice($parts, 1)) : null;
}
}
$cf = trim((string) ($cr->cond_cod_fisc ?? '')) ?: null;
$email = trim((string) ($cr->e_mail_condomino ?? '')) ?: null;
$sog = $this->findExistingSoggetto($cf, $email, $nome, $cognome);
if (! $sog) {
$payload = [
'nome' => $nome,
'cognome' => $cognome,
'codice_fiscale' => $cf,
'email' => $email,
'tipo' => 'proprietario',
'created_at' => now(),
'updated_at' => now(),
];
if (Schema::hasColumn('soggetti', 'codice_univoco')) {
$baseKey = $this->normalizeNameKey($payload['nome'] ?? '', $payload['cognome'] ?? '') ?: (($payload['email'] ?? null) ?: 'ND');
$code = $this->generateStableCondKey('S', $cr->cod_stabile ?? 'S0', $baseKey);
$tries = 0;
while (DB::table('soggetti')->where('codice_univoco', $code)->exists() && $tries < 5) {
$salt = ($legacyCond ?? '') . '-' . ($tries + 1);
$code = $this->generateStableCondKey('S', $cr->cod_stabile ?? 'S0', $baseKey . '|' . $salt);
$tries++;
}
$payload['codice_univoco'] = $code;
}
if (isset($payload['codice_univoco'])) {
$existingByCode = DB::table('soggetti')->where('codice_univoco', $payload['codice_univoco'])->first();
if ($existingByCode) {
$sog = $existingByCode;
}
}
if (! $sog) {
$sid = DB::table('soggetti')->insertGetId($payload);
$sog = DB::table('soggetti')->where('id', $sid)->first();
}
}
if (! $sog) {
$skipped++;
continue;
}
$percentuale = isset($cr->perc_diritto_reale) && is_numeric($cr->perc_diritto_reale)
? (float) $cr->perc_diritto_reale
: null;
$exists = DB::table('proprieta')
->where('unita_immobiliare_id', $unita->id)
->where('soggetto_id', $sog->id)
->first();
if ($exists) {
$update = [];
if (($exists->tipo_diritto ?? '') !== 'proprieta') {
$update['tipo_diritto'] = 'proprieta';
}
if ($percentuale !== null && (float) ($exists->percentuale_possesso ?? 0) !== $percentuale) {
$update['percentuale_possesso'] = $percentuale;
}
if ($update !== []) {
$update['updated_at'] = now();
DB::table('proprieta')->where('id', $exists->id)->update($update);
$updated++;
} else {
$skipped++;
}
} else {
DB::table('proprieta')->insert([
'unita_immobiliare_id' => $unita->id,
'soggetto_id' => $sog->id,
'tipo_diritto' => 'proprieta',
'percentuale_possesso' => $percentuale,
'data_inizio' => null,
'data_fine' => null,
'created_at' => now(),
'updated_at' => now(),
]);
$created++;
}
}
}
$this->line(" → diritti inseriti: $created, aggiornati: $updated, ignorati: $skipped");
return $created + $updated;
}
private function stepMillesimi(?int $limit): int
{
$headersCreated = 0;
$detailsInserted = 0;
$detailsUpdated = 0;
$nordAvailable = Schema::connection('gescon_import')->hasTable('dett_tab') && Schema::connection('gescon_import')->hasColumn('dett_tab', 'nord');
// 1) Prepara elenco intestazioni tabella da importare (preferisci tabelle_millesimali, altrimenti deriva da dett_tab)
$headerRows = collect();
if (Schema::connection('gescon_import')->hasTable('tabelle_millesimali')) {
$q = DB::connection('gescon_import')->table('tabelle_millesimali');
if ($this->option('stabile')) {
$q->where('cod_stabile', $this->option('stabile'));
}
if ($limit) {
$q->limit($limit);
}
$headerRows = $q->get();
}
if ($headerRows->isEmpty() && Schema::connection('gescon_import')->hasTable('dett_tab')) {
$q = DB::connection('gescon_import')->table('dett_tab as d');
if (Schema::connection('gescon_import')->hasTable('condomin')) {
$q = $q->join('condomin as c', 'c.cod_cond', '=', 'd.id_cond');
if ($this->option('stabile')) {
$q->where('c.cod_stabile', $this->option('stabile'));
}
}
$headerRows = $q->select('d.cod_tab as cod_tabella', 'd.legacy_year')->distinct()->get();
}
if ($headerRows->isEmpty()) {
return 0;
}
// 2) Calcola importi preventivo/consuntivo per tabella/anno (se disponibili)
$importiMap = [];
$nordMap = [];
$totMmMap = [];
if (Schema::connection('gescon_import')->hasTable('dett_tab')) {
$q = DB::connection('gescon_import')->table('dett_tab as d')
->select(
'd.cod_tab',
'd.legacy_year',
DB::raw('SUM(d.prev) as tot_prev'),
DB::raw('SUM(d.prev_euro) as tot_prev_euro'),
DB::raw('SUM(d.cons) as tot_cons'),
DB::raw('SUM(d.cons_euro) as tot_cons_euro'),
DB::raw('SUM(d.ex_cons_euro) as tot_ex_cons_euro')
);
if (Schema::connection('gescon_import')->hasTable('condomin')) {
$q = $q->join('condomin as c', 'c.cod_cond', '=', 'd.id_cond');
if ($this->option('stabile')) {
$q->where('c.cod_stabile', $this->option('stabile'));
}
}
$q->groupBy('d.cod_tab', 'd.legacy_year');
foreach ($q->get() as $row) {
$key = strtoupper(trim((string) ($row->cod_tab ?? ''))) . '|' . (string) ($row->legacy_year ?? '');
$importiMap[$key] = $row;
}
$mmSub = DB::connection('gescon_import')->table('dett_tab')
->select('cod_tab', 'legacy_year', 'id_cond', DB::raw('MAX(mm) as mm'))
->groupBy('cod_tab', 'legacy_year', 'id_cond');
$mmQuery = DB::connection('gescon_import')->query()->fromSub($mmSub, 't')
->select('cod_tab', 'legacy_year', DB::raw('SUM(mm) as tot_mm'))
->groupBy('cod_tab', 'legacy_year');
foreach ($mmQuery->get() as $row) {
$key = strtoupper(trim((string) ($row->cod_tab ?? ''))) . '|' . (string) ($row->legacy_year ?? '');
$totMmMap[$key] = is_numeric($row->tot_mm ?? null) ? (float) $row->tot_mm : null;
}
if ($nordAvailable) {
$nq = DB::connection('gescon_import')->table('dett_tab as d')
->select('d.cod_tab', 'd.legacy_year', DB::raw('MIN(d.nord) as nord'));
if (Schema::connection('gescon_import')->hasTable('condomin')) {
$nq = $nq->join('condomin as c', 'c.cod_cond', '=', 'd.id_cond');
if ($this->option('stabile')) {
$nq->where('c.cod_stabile', $this->option('stabile'));
}
}
$nq->groupBy('d.cod_tab', 'd.legacy_year');
foreach ($nq->get() as $row) {
$key = strtoupper(trim((string) ($row->cod_tab ?? ''))) . '|' . (string) ($row->legacy_year ?? '');
$nordMap[$key] = is_numeric($row->nord ?? null) ? (int) $row->nord : null;
}
}
}
// 3) Crea/aggiorna intestazioni su dominio usando colonne effettive (codice_tabella/nome_tabella)
$tableCols = Schema::getColumnListing('tabelle_millesimali');
foreach ($headerRows as $h) {
$code = trim((string) ($h->cod_tabella ?? $h->cod_tab ?? ''));
if ($code === '') {
continue;
}
$code = strtoupper($code);
$legacyYear = isset($h->legacy_year) ? trim((string) $h->legacy_year) : null;
$annoGestione = Schema::hasColumn('tabelle_millesimali', 'anno_gestione')
? $this->resolveAnnoFromLegacyYear($legacyYear)
: null;
$rawCalcolo = $h->tipo_calcolo ?? $h->calcolo ?? null;
$calcolo = $this->normalizeLegacyCalcolo($rawCalcolo);
$calcoloDb = in_array($calcolo, ['millesimi', 'parti', 'fisso'], true) ? $calcolo : 'millesimi';
$tipoLegacy = $this->normalizeLegacyTipo($h->tipologia ?? $h->tipo ?? null);
$tipoTabella = $this->resolveTipoTabellaFromLegacy($tipoLegacy, $calcolo, $code);
$ordine = null;
foreach (['ordine_visualizzazione', 'nord', 'ordinamento'] as $orderKey) {
if (isset($h->{$orderKey}) && is_numeric($h->{$orderKey})) {
$ordine = (int) $h->{$orderKey};
break;
}
}
if ($ordine === null) {
$ordine = $nordMap[$code . '|' . (string) ($legacyYear ?? '')] ?? null;
}
$meta = [];
$importKey = $code . '|' . (string) ($legacyYear ?? '');
$importi = $importiMap[$importKey] ?? null;
$meta['legacy_year'] = $legacyYear;
if ($tipoLegacy) {
$meta['tipo'] = $tipoLegacy;
}
if ($rawCalcolo) {
$meta['calcolo'] = $rawCalcolo;
}
if ($importi) {
$meta['tot_prev'] = is_numeric($importi->tot_prev ?? null) ? (float) $importi->tot_prev : null;
$meta['tot_prev_euro'] = is_numeric($importi->tot_prev_euro ?? null) ? (float) $importi->tot_prev_euro : null;
$meta['tot_cons'] = is_numeric($importi->tot_cons ?? null) ? (float) $importi->tot_cons : null;
$meta['tot_cons_euro'] = is_numeric($importi->tot_cons_euro ?? null) ? (float) $importi->tot_cons_euro : null;
$meta['tot_ex_cons_euro'] = is_numeric($importi->tot_ex_cons_euro ?? null) ? (float) $importi->tot_ex_cons_euro : null;
}
$existsQuery = DB::table('tabelle_millesimali');
if (Schema::hasColumn('tabelle_millesimali', 'stabile_id')) {
$existsQuery->where('stabile_id', $this->stabileId);
}
if (Schema::hasColumn('tabelle_millesimali', 'codice_tabella')) {
$existsQuery->where('codice_tabella', $code);
} elseif (Schema::hasColumn('tabelle_millesimali', 'nome_tabella')) {
$existsQuery->where('nome_tabella', $code);
} elseif (Schema::hasColumn('tabelle_millesimali', 'denominazione')) {
$existsQuery->where('denominazione', 'like', "%{$code}%");
}
if (Schema::hasColumn('tabelle_millesimali', 'anno_gestione')) {
if ($annoGestione) {
$existsQuery->where('anno_gestione', (int) $annoGestione);
} else {
$existsQuery->whereNull('anno_gestione');
}
}
$exists = $existsQuery->first();
if (! $exists) {
$fallback = DB::table('tabelle_millesimali');
if (Schema::hasColumn('tabelle_millesimali', 'stabile_id')) {
$fallback->where('stabile_id', $this->stabileId);
}
if (Schema::hasColumn('tabelle_millesimali', 'codice_tabella')) {
$fallback->where('codice_tabella', $code);
} elseif (Schema::hasColumn('tabelle_millesimali', 'nome_tabella')) {
$fallback->where('nome_tabella', $code);
} elseif (Schema::hasColumn('tabelle_millesimali', 'denominazione')) {
$fallback->where('denominazione', 'like', "%{$code}%");
}
$exists = $fallback->first();
}
$totaleMillesimi = 1000.0;
if (isset($h->totale_millesimi) && is_numeric($h->totale_millesimi)) {
$tm = (float) $h->totale_millesimi;
if ($tm <= 999999.9999) {
$totaleMillesimi = $tm;
}
}
if ($totaleMillesimi <= 0) {
$mmKey = $code . '|' . (string) ($legacyYear ?? '');
$mmFallback = $totMmMap[$mmKey] ?? null;
if (is_numeric($mmFallback) && $mmFallback > 0 && $mmFallback <= 999999.9999) {
$totaleMillesimi = (float) $mmFallback;
}
}
$payload = [
'stabile_id' => $this->stabileId,
'codice_tabella' => $code,
'nome_tabella' => $code,
'denominazione' => trim((string) ($h->denominazione ?? $h->descrizione ?? '')) ?: ('Tab ' . $code),
'descrizione' => trim((string) ($h->descrizione ?? $h->descr ?? '')) ?: null,
'tipo_tabella' => $tipoTabella ?: 'custom',
'tipo_calcolo' => $calcoloDb,
'totale_millesimi' => $totaleMillesimi,
'attiva' => true,
'legacy_codice' => $code,
'meta_legacy' => $meta,
];
if ($ordine !== null) {
$payload['ordine_visualizzazione'] = $ordine;
$payload['nord'] = $ordine;
$payload['ordinamento'] = $ordine;
}
if (Schema::hasColumn('tabelle_millesimali', 'anno_gestione') && $annoGestione) {
$payload['anno_gestione'] = (int) $annoGestione;
}
$filtered = array_intersect_key($payload, array_flip($tableCols));
if (Schema::hasColumn('tabelle_millesimali', 'meta_legacy')) {
$filtered['meta_legacy'] = json_encode($meta);
}
if ($exists) {
if (Schema::hasColumn('tabelle_millesimali', 'meta_legacy')) {
$currentMeta = is_array($exists->meta_legacy)
? $exists->meta_legacy
: (is_string($exists->meta_legacy) ? json_decode($exists->meta_legacy, true) : []);
$currentMeta = is_array($currentMeta) ? $currentMeta : [];
$filtered['meta_legacy'] = json_encode(array_merge($currentMeta, $meta));
}
if (Schema::hasColumn('tabelle_millesimali', 'updated_at')) {
$filtered['updated_at'] = now();
}
if (! empty($filtered)) {
DB::table('tabelle_millesimali')->where('id', $exists->id)->update($filtered);
}
} else {
if (Schema::hasColumn('tabelle_millesimali', 'created_at')) {
$filtered['created_at'] = now();
}
if (Schema::hasColumn('tabelle_millesimali', 'updated_at')) {
$filtered['updated_at'] = now();
}
$this->dynamicInsert('tabelle_millesimali', $filtered);
$headersCreated++;
}
}
// 3) Inserisci le righe (dettagli) se presenti in staging
if (! Schema::connection('gescon_import')->hasTable('dett_tab')) {
return $headersCreated;
}
$detailsTable = Schema::hasTable('dettaglio_millesimi')
? 'dettaglio_millesimi'
: (Schema::hasTable('tabelle_millesimali_righe') ? 'tabelle_millesimali_righe' : null);
if (! $detailsTable) {
return $headersCreated;
}
$detailsCols = Schema::getColumnListing($detailsTable);
$hasRuoloLegacy = ($detailsTable === 'dettaglio_millesimi') && in_array('ruolo_legacy', $detailsCols, true);
$src = DB::connection('gescon_import')->table('dett_tab as d');
if (Schema::connection('gescon_import')->hasTable('condomin')) {
$src = $src->join('condomin as c', 'c.cod_cond', '=', 'd.id_cond');
if ($this->option('stabile')) {
$src->where('c.cod_stabile', $this->option('stabile'));
}
}
if ($limit) {
$src->limit($limit);
}
$canImporti = Schema::hasTable('dettaglio_importi_tabella');
$importiUpdated = 0;
$importiInserted = 0;
$importiSkipped = 0;
foreach ($src->get() as $r) {
// Trova la tabella dominio per codice
$scalaRow = isset($r->scala) ? (string) $r->scala : null;
$targetStabileId = $this->resolveStabileIdForScala($scalaRow);
$legacyYear = isset($r->legacy_year) ? trim((string) $r->legacy_year) : null;
$annoGestione = Schema::hasColumn('tabelle_millesimali', 'anno_gestione')
? $this->resolveAnnoFromLegacyYear($legacyYear)
: null;
if (Schema::hasColumn('tabelle_millesimali', 'codice_tabella')) {
$tab = DB::table('tabelle_millesimali')
->where('codice_tabella', $r->cod_tab)
->where('stabile_id', $targetStabileId)
->when(Schema::hasColumn('tabelle_millesimali', 'anno_gestione'), function ($q) use ($annoGestione) {
if ($annoGestione) {
$q->where('anno_gestione', (int) $annoGestione);
} else {
$q->whereNull('anno_gestione');
}
})
->first();
} elseif (Schema::hasColumn('tabelle_millesimali', 'nome_tabella')) {
$tab = DB::table('tabelle_millesimali')
->where('nome_tabella', $r->cod_tab)
->where('stabile_id', $targetStabileId)
->when(Schema::hasColumn('tabelle_millesimali', 'anno_gestione'), function ($q) use ($annoGestione) {
if ($annoGestione) {
$q->where('anno_gestione', (int) $annoGestione);
} else {
$q->whereNull('anno_gestione');
}
})
->first();
} else {
$tab = null;
}
if (! $tab) {
$fallback = DB::table('tabelle_millesimali')->where('stabile_id', $targetStabileId);
if (Schema::hasColumn('tabelle_millesimali', 'codice_tabella')) {
$fallback->where('codice_tabella', $r->cod_tab);
} elseif (Schema::hasColumn('tabelle_millesimali', 'nome_tabella')) {
$fallback->where('nome_tabella', $r->cod_tab);
}
$tab = $fallback->first();
}
// Crea intestazione on-demand se mancante per questo stabile
if (! $tab) {
$payloadH = [
'stabile_id' => $targetStabileId,
'codice_tabella' => $r->cod_tab,
'nome_tabella' => $r->cod_tab,
'denominazione' => 'Tab ' . $r->cod_tab,
'tipo_tabella' => 'custom',
'totale_millesimi' => 1000,
'attiva' => true,
'created_at' => now(),
'updated_at' => now(),
];
if (Schema::hasColumn('tabelle_millesimali', 'anno_gestione') && $annoGestione) {
$payloadH['anno_gestione'] = (int) $annoGestione;
}
$this->dynamicInsert('tabelle_millesimali', $payloadH);
$tab = DB::table('tabelle_millesimali')
->where('stabile_id', $targetStabileId)
->where(function ($q) use ($r) {
if (Schema::hasColumn('tabelle_millesimali', 'codice_tabella')) {
$q->orWhere('codice_tabella', $r->cod_tab);
}
if (Schema::hasColumn('tabelle_millesimali', 'nome_tabella')) {
$q->orWhere('nome_tabella', $r->cod_tab);
}
})
->when(Schema::hasColumn('tabelle_millesimali', 'anno_gestione'), function ($q) use ($annoGestione) {
if ($annoGestione) {
$q->where('anno_gestione', (int) $annoGestione);
} else {
$q->whereNull('anno_gestione');
}
})
->first();
$headersCreated++;
}
if (! $tab) {
continue;
}
// Best-effort: risolvi unita_immobiliare per legacy_cond_id (id_cond), fallback scala + interno
$unitaId = null;
if (Schema::hasTable('unita_immobiliari')) {
if (Schema::hasColumn('unita_immobiliari', 'legacy_cond_id') && isset($r->id_cond)) {
$legacyCond = trim((string) $r->id_cond);
if ($legacyCond !== '') {
$u = DB::table('unita_immobiliari')
->where('stabile_id', $targetStabileId)
->whereNull('deleted_at')
->where('legacy_cond_id', $legacyCond)
->first();
$unitaId = $u->id ?? null;
}
}
if (! $unitaId) {
$crit = [];
if (Schema::hasColumn('unita_immobiliari', 'stabile_id')) {
$crit['stabile_id'] = $targetStabileId;
}
if (Schema::hasColumn('unita_immobiliari', 'scala') && isset($r->scala)) {
$crit['scala'] = $r->scala;
}
if (Schema::hasColumn('unita_immobiliari', 'interno')) {
// Normalizza interno: CANxx per cantine
$int = isset($r->interno) ? (string) $r->interno : null;
if (is_string($int) && preg_match('/^\s*CAN\s*(\d+)\s*$/i', $int, $m)) {
$int = 'CAN' . $m[1];
}
if ($int !== null) {
$crit['interno'] = $int;
}
}
if (! empty($crit)) {
$u = DB::table('unita_immobiliari')
->whereNull('deleted_at')
->where($crit)
->first();
$unitaId = $u->id ?? null;
}
}
}
if (! $unitaId) {
continue;
}
$mm = (float) ($r->mm ?? 0);
if ($mm < 0) {
$mm = 0;
}
$roleLegacy = null;
if (isset($r->cond_inquil)) {
$roleLegacy = strtoupper(trim((string) $r->cond_inquil));
if ($roleLegacy === '') {
$roleLegacy = null;
}
}
// NORD (ordine legacy per colonne/righe, utile per ordinamento rendering)
$nord = null;
if ($nordAvailable && isset($r->nord)) {
// Normalizza come intero
$n = is_numeric($r->nord) ? (int) $r->nord : null;
$nord = $n !== null ? $n : null;
}
if ($detailsTable === 'dettaglio_millesimi') {
// Upsert by (tabella, unita): ignora ruolo C/I e usa il massimo mm
$exists = DB::table('dettaglio_millesimi')
->where('tabella_millesimale_id', $tab->id)
->where('unita_immobiliare_id', $unitaId)
->first();
if ($exists) {
$currentMm = is_numeric($exists->millesimi ?? null) ? (float) $exists->millesimi : 0.0;
$upd = [
'millesimi' => max($currentMm, $mm),
'updated_at' => now(),
];
// Se la tabella ha una colonna per ordine (es. posizione, ordinale, nord), prova ad aggiornarla
if ($nord !== null) {
foreach (['nord', 'ordine', 'posizione', 'ordinale'] as $c) {
if (in_array($c, $detailsCols, true)) {
$upd[$c] = $nord;
break;
}
}
}
DB::table('dettaglio_millesimi')
->where('id', $exists->id)
->update($upd);
$detailsUpdated++;
} else {
$payload = [
'tabella_millesimale_id' => $tab->id,
'unita_immobiliare_id' => $unitaId,
'millesimi' => $mm,
'note' => null,
'created_at' => now(),
'updated_at' => now(),
];
if ($nord !== null) {
foreach (['nord', 'ordine', 'posizione', 'ordinale'] as $c) {
if (in_array($c, $detailsCols, true)) {
$payload[$c] = $nord;
break;
}
}
}
$this->dynamicInsert('dettaglio_millesimi', $payload);
$detailsInserted++;
}
} else { // tabelle_millesimali_righe (fallback compatibilità vecchio schema)
$exists = DB::table('tabelle_millesimali_righe')
->where('tabella_millesimale_id', $tab->id)
->where('unita_immobiliare_id', $unitaId)
->first();
if ($exists) {
$upd = [
'millesimi' => (float) ($exists->millesimi ?? 0) + $mm,
'valore_prev' => $r->prev ?? $exists->valore_prev,
'valore_cons' => $r->cons ?? $exists->valore_cons,
'updated_at' => now(),
];
if ($nord !== null) {
$cols = Schema::getColumnListing('tabelle_millesimali_righe');
foreach (['nord', 'ordine', 'posizione', 'ordinale'] as $c) {
if (in_array($c, $cols, true)) {
$upd[$c] = $nord;
break;
}
}
}
DB::table('tabelle_millesimali_righe')
->where('id', $exists->id)
->update($upd);
$detailsUpdated++;
} else {
$payload = [
'tabella_millesimale_id' => $tab->id,
'unita_immobiliare_id' => $unitaId,
'ruolo' => $r->cond_inquil ?? null,
'millesimi' => $mm,
'valore_prev' => $r->prev ?? null,
'valore_cons' => $r->cons ?? null,
'created_at' => now(),
'updated_at' => now(),
];
if ($nord !== null) {
$cols = Schema::getColumnListing('tabelle_millesimali_righe');
foreach (['nord', 'ordine', 'posizione', 'ordinale'] as $c) {
if (in_array($c, $cols, true)) {
$payload[$c] = $nord;
break;
}
}
}
$this->dynamicInsert('tabelle_millesimali_righe', $payload);
$detailsInserted++;
}
}
if ($canImporti && $unitaId && $tab) {
$legacyYear = '0000';
if (isset($r->legacy_year) && is_string($r->legacy_year) && trim($r->legacy_year) !== '') {
$legacyYear = trim($r->legacy_year);
}
$role = null;
if (isset($r->cond_inquil) && is_string($r->cond_inquil)) {
$role = strtoupper(trim($r->cond_inquil));
if ($role === '') {
$role = null;
}
}
$payloadImporti = [
'millesimi' => $mm,
'prev_euro' => isset($r->prev_euro) && is_numeric($r->prev_euro) ? (float) $r->prev_euro : null,
'cons_euro' => isset($r->cons_euro) && is_numeric($r->cons_euro) ? (float) $r->cons_euro : null,
'ex_cons_euro' => isset($r->ex_cons_euro) && is_numeric($r->ex_cons_euro) ? (float) $r->ex_cons_euro : null,
'n_stra' => isset($r->n_stra) && is_numeric($r->n_stra) ? (int) $r->n_stra : null,
'unico' => isset($r->unico) ? (int) $r->unico : null,
'proviene_ors' => isset($r->proviene_ors) ? (string) $r->proviene_ors : null,
'proviene_n_stra' => isset($r->proviene_n_stra) && is_numeric($r->proviene_n_stra) ? (int) $r->proviene_n_stra : null,
'proviene_eserc' => isset($r->proviene_eserc) && is_numeric($r->proviene_eserc) ? (int) $r->proviene_eserc : null,
'legacy_payload' => isset($r->payload) ? (array) $r->payload : null,
'updated_at' => now(),
];
$match = [
'tabella_millesimale_id' => $tab->id,
'unita_immobiliare_id' => $unitaId,
'ruolo_legacy' => $role,
'legacy_year' => $legacyYear,
];
$exists = DB::table('dettaglio_importi_tabella')->where($match)->first();
if ($exists) {
DB::table('dettaglio_importi_tabella')->where('id', $exists->id)->update($payloadImporti);
$importiUpdated++;
} else {
$payloadImporti = array_merge($match, $payloadImporti, [
'created_at' => now(),
]);
$this->dynamicInsert('dettaglio_importi_tabella', $payloadImporti);
$importiInserted++;
}
} else {
if ($canImporti) {
$importiSkipped++;
}
}
}
$affected = $detailsInserted + $detailsUpdated;
// 4) Completa righe mancanti solo se richiesto esplicitamente.
// Di default evitiamo placeholder a zero che spezzano il collegamento reale
// tra millesimi, nominativi e unita immobiliari.
if ($this->option('fill-missing-millesimi') && $detailsTable === 'dettaglio_millesimi' && Schema::hasTable('unita_immobiliari')) {
$detailCols = Schema::getColumnListing('dettaglio_millesimi');
$unitaIds = DB::table('unita_immobiliari')
->where('stabile_id', $this->stabileId)
->whereNull('deleted_at')
->pluck('id')
->map(fn($v) => (int) $v)
->filter(fn($v) => $v > 0)
->values()
->all();
if (! empty($unitaIds)) {
$tabIds = DB::table('tabelle_millesimali')
->where('stabile_id', $this->stabileId)
->pluck('id')
->map(fn($v) => (int) $v)
->all();
foreach ($tabIds as $tabId) {
$existing = DB::table('dettaglio_millesimi')
->where('tabella_millesimale_id', $tabId)
->pluck('unita_immobiliare_id')
->map(fn($v) => (int) $v)
->all();
$missing = array_values(array_diff($unitaIds, $existing));
if ($missing === []) {
continue;
}
$now = now();
$rows = [];
foreach ($missing as $uid) {
if (! $uid) {
continue;
}
$row = [
'tabella_millesimale_id' => $tabId,
'unita_immobiliare_id' => $uid,
'millesimi' => 0,
'note' => null,
'created_at' => $now,
'updated_at' => $now,
];
if (in_array('partecipa', $detailCols, true)) {
$row['partecipa'] = 0;
}
$rows[] = $row;
}
foreach (array_chunk($rows, 500) as $chunk) {
DB::table('dettaglio_millesimi')->insert($chunk);
}
}
}
}
if ($canImporti && ($importiInserted + $importiUpdated) > 0) {
$this->line(" → importi tabella: inseriti {$importiInserted}, aggiornati {$importiUpdated}, saltati {$importiSkipped}");
}
if ($affected > 0) {
$this->line(" → intestazioni: $headersCreated, dettagli inseriti: $detailsInserted, aggiornati: $detailsUpdated");
}
return $affected ?: $headersCreated;
}
private function inferStraordinariaSequence(mixed ...$candidates): ?int
{
foreach ($candidates as $candidate) {
$value = strtoupper(trim((string) ($candidate ?? '')));
if ($value === '') {
continue;
}
if (preg_match('/([0-9]{4})$/', $value, $match)) {
$sequence = (int) substr($match[1], -2);
if ($sequence > 0) {
return $sequence;
}
}
if (preg_match('/(?:^|[^0-9])([0-9]{2})$/', $value, $match)) {
$sequence = (int) $match[1];
if ($sequence > 0) {
return $sequence;
}
}
}
return null;
}
private function buildScopedLegacyCode(?string $legacyCode, ?int $annoGestione, ?int $numeroStraordinaria = null, ?string $fallbackPrefix = null): string
{
$base = trim((string) $legacyCode);
if ($base === '') {
$base = trim((string) ($fallbackPrefix ?: 'LEGACY'));
}
if (! $annoGestione) {
return $base;
}
if ($numeroStraordinaria !== null) {
return sprintf('%s@%d-%02d', $base, $annoGestione, $numeroStraordinaria);
}
return sprintf('%s@%d', $base, $annoGestione);
}
private function isFiscalStraordinaria(object $row): bool
{
return $this->normalizeLegacyPartyName($row->descriz_ccp ?? null) === 'DETRAZ.FISC.';
}
private function sumStraordinariaImporto(object $row): ?float
{
$sum = 0.0;
$found = false;
foreach (range(1, 12) as $idx) {
$field = 'rata_' . $idx;
if (isset($row->{$field}) && is_numeric($row->{$field})) {
$sum += (float) $row->{$field};
$found = true;
}
}
return $found ? $sum : null;
}
private function stepVoci(?int $limit): int
{
$stabileId = $this->resolveStabileId();
$src = DB::connection('gescon_import')->table('voc_spe');
if ($limit) {
$src->limit($limit);
}
$count = 0;
$updated = 0;
foreach ($src->get() as $v) {
$cod = $v->cod ?? null;
if (! $cod) {
continue;
}
$legacyYear = isset($v->legacy_year) ? (string) $v->legacy_year : null;
$annoGestione = $this->resolveLegacySnapshotYear($legacyYear, $v);
$tipoLegacy = match ($v->v_ors ?? $v->V_ORS ?? '') {
'R' => 'riscaldamento',
'S' => 'straordinaria',
default => 'ordinaria'
};
$numeroStraordinaria = $tipoLegacy === 'straordinaria'
? $this->inferStraordinariaSequence($v->tabella ?? null, $v->cod ?? null, $v->descriz ?? null)
: null;
$codiceDominio = $tipoLegacy === 'straordinaria'
? $this->buildScopedLegacyCode((string) $cod, $annoGestione, $numeroStraordinaria, 'VOCSPE')
: (string) $cod;
$exists = null;
if (Schema::hasColumn('voci_spesa', 'codice')) {
$q = DB::table('voci_spesa')->where('codice', $codiceDominio);
if ($stabileId && Schema::hasColumn('voci_spesa', 'stabile_id')) {
$q->where('stabile_id', $stabileId);
}
$gestioneId = $this->resolveGestioneContabileIdForStabile($stabileId, $legacyYear, $tipoLegacy, $numeroStraordinaria);
if ($gestioneId && Schema::hasColumn('voci_spesa', 'gestione_contabile_id')) {
$q->where('gestione_contabile_id', $gestioneId);
}
$exists = $q->orderBy('id')->first();
if (! $exists) {
$exists = DB::table('voci_spesa')
->where('codice', $codiceDominio)
->when($stabileId && Schema::hasColumn('voci_spesa', 'stabile_id'), function ($qq) use ($stabileId) {
$qq->where('stabile_id', $stabileId);
})
->orderBy('id')
->first();
}
}
$tabellaId = null;
if (Schema::hasTable('tabelle_millesimali') && ! empty($v->tabella)) {
$tabellaQuery = DB::table('tabelle_millesimali')
->where('stabile_id', $stabileId)
->where(function ($q) use ($v) {
if (Schema::hasColumn('tabelle_millesimali', 'codice_tabella')) {
$q->orWhere('codice_tabella', $v->tabella);
}
if (Schema::hasColumn('tabelle_millesimali', 'nome_tabella')) {
$q->orWhere('nome_tabella', $v->tabella);
}
});
if ($annoGestione && Schema::hasColumn('tabelle_millesimali', 'anno_gestione')) {
$tabellaQuery->where('anno_gestione', $annoGestione);
}
$tabellaId = $tabellaQuery->value('id');
if (! $tabellaId && $annoGestione && Schema::hasColumn('tabelle_millesimali', 'anno_gestione')) {
$tabellaId = DB::table('tabelle_millesimali')
->where('stabile_id', $stabileId)
->where(function ($q) use ($v) {
if (Schema::hasColumn('tabelle_millesimali', 'codice_tabella')) {
$q->orWhere('codice_tabella', $v->tabella);
}
if (Schema::hasColumn('tabelle_millesimali', 'nome_tabella')) {
$q->orWhere('nome_tabella', $v->tabella);
}
})
->value('id');
}
}
$gestioneId = $this->resolveGestioneContabileIdForStabile($stabileId, $legacyYear, $tipoLegacy, $numeroStraordinaria);
$data = [
'stabile_id' => (Schema::hasColumn('voci_spesa', 'stabile_id') ? $stabileId : null),
'gestione_contabile_id' => Schema::hasColumn('voci_spesa', 'gestione_contabile_id') ? $gestioneId : null,
'codice' => $codiceDominio,
'legacy_codice' => Schema::hasColumn('voci_spesa', 'legacy_codice') ? $cod : null,
'descrizione' => $v->descriz ?? '',
'categoria' => (Schema::hasColumn('voci_spesa', 'categoria') ? $tipoLegacy : null),
'tipo_gestione' => (Schema::hasColumn('voci_spesa', 'tipo_gestione') ? $tipoLegacy : null),
'tipo' => $tipoLegacy,
'tabella_millesimale_default_id' => (Schema::hasColumn('voci_spesa', 'tabella_millesimale_default_id') ? $tabellaId : null),
'importo_default' => Schema::hasColumn('voci_spesa', 'importo_default')
? (isset($v->preventivo_euro) && is_numeric($v->preventivo_euro) ? (float) $v->preventivo_euro : (isset($v->importo_euro) && is_numeric($v->importo_euro) ? (float) $v->importo_euro : null))
: null,
'importo_consuntivo' => Schema::hasColumn('voci_spesa', 'importo_consuntivo')
? (isset($v->consuntivo_euro) && is_numeric($v->consuntivo_euro) ? (float) $v->consuntivo_euro : null)
: null,
'percentuale_condomino' => Schema::hasColumn('voci_spesa', 'percentuale_condomino')
? (isset($v->perc_proprietario) && is_numeric($v->perc_proprietario) ? (float) $v->perc_proprietario : 100.0)
: null,
'percentuale_inquilino' => Schema::hasColumn('voci_spesa', 'percentuale_inquilino')
? (isset($v->perc_inquilino) && is_numeric($v->perc_inquilino) ? (float) $v->perc_inquilino : 0.0)
: null,
'note' => $v->note ?? null,
'created_at' => now(),
'updated_at' => now(),
];
if ($exists) {
$patch = [];
foreach (['tabella_millesimale_default_id', 'importo_default', 'importo_consuntivo', 'percentuale_condomino', 'percentuale_inquilino', 'legacy_codice', 'gestione_contabile_id'] as $k) {
if (array_key_exists($k, $data) && $data[$k] !== null && (empty($exists->$k) && $exists->$k !== 0)) {
$patch[$k] = $data[$k];
}
}
if (! empty($patch)) {
$patch['updated_at'] = now();
DB::table('voci_spesa')->where('id', $exists->id)->update($patch);
$updated++;
}
continue;
}
$this->dynamicInsert('voci_spesa', $data);
$count++;
}
if ($updated > 0) {
$this->line(" → voci aggiornate: {$updated}");
}
return $count + $updated;
}
private function stepBanche(?int $limit): int
{
// Importa conti bancari per lo stabile corrente leggendo Anagr_casse da generale_stabile.mdb
if (! Schema::hasTable('dati_bancari')) {
return 0;
}
$stabileCode = (string) ($this->option('stabile') ?? '');
if ($stabileCode === '') {
return 0;
}
// Risolvi stabile_id
$stabileId = $this->resolveStabileId();
// Determina root archivi: usa --path come radice (default noto)
$root = rtrim((string) ($this->option('path') ?: '/mnt/gescon-archives/gescon'), '/');
$stableDir = $root . '/' . $stabileCode;
$generale = $stableDir . '/generale_stabile.mdb';
$rows = [];
// 0) Preferisci staging anag_casse se presente
$stagingTable = null;
foreach (['anag_casse', 'Anag_casse', 'anagr_casse', 'Anagr_casse'] as $t) {
if (Schema::connection('gescon_import')->hasTable($t)) {
$stagingTable = $t;
break;
}
}
if ($stagingTable) {
$q = DB::connection('gescon_import')->table($stagingTable);
if ($stabileCode && Schema::connection('gescon_import')->hasColumn($stagingTable, 'cod_stabile')) {
$q->where('cod_stabile', $stabileCode);
}
if ($limit) {
$q->limit($limit);
}
$rows = $q->get()->map(function ($r): array {
$out = [];
foreach ((array) $r as $k => $v) {
$out[strtolower((string) $k)] = $v;
}
return $out;
})->all();
}
if (empty($rows) && is_file($generale) && is_readable($generale)) {
$rows = $this->mdbExportToRows($generale, ['Anagr_casse', 'anagr_casse', 'ANAGR_CASSE']);
}
// Fallback: cerca Anagr_casse nei singolo_anno.mdb (può contenere info ridondanti)
if (empty($rows) && is_dir($stableDir)) {
foreach ((array) glob($stableDir . '/*/singolo_anno.mdb') as $mdb) {
$part = $this->mdbExportToRows($mdb, ['Anagr_casse', 'anagr_casse', 'ANAGR_CASSE']);
if (! empty($part)) {
$rows = array_merge($rows, $part);
}
if ($limit && count($rows) >= $limit) {
break;
}
}
}
if ($stabileId) {
$configRaw = DB::table('stabili')->where('id', $stabileId)->value('configurazione_avanzata');
$configRows = $this->buildBancheRowsFromStabileConfig($configRaw);
if ($configRows !== []) {
$rows = $this->mergeLegacyBancheRows($rows, $configRows);
}
}
if (empty($rows)) {
return 0;
}
// Applica mapping personalizzato (se presente) per individuare le colonne legacy
$mapLegacy = [
'nome_banca' => ['banca', 'istituto', 'nome_banca', 'denominazione', 'denom_banca'],
'iban' => ['iban', 'iban_conto', 'iban1'],
'swift' => ['swift', 'bic', 'bic_swift'],
'conto' => ['n_conto', 'numero_conto', 'conto'],
'intestatario' => ['intestatario', 'intestazione', 'titolare'],
'cod_cassa' => ['cod_cassa', 'codcassa', 'codice_cassa', 'modalita_pagamento', 'codice'],
'descrizione' => ['descrizione', 'descr', 'denominazione', 'note'],
];
if (! empty($this->banksMapping) && is_array($this->banksMapping)) {
// banksMapping formato: [{legacy:{...}, target:{...}}, ...] → usa chiavi legacy quando compilate
foreach ($this->banksMapping as $bm) {
if (! is_array($bm)) {
continue;
}
foreach (['nome_banca', 'iban', 'swift', 'conto', 'intestatario', 'cod_cassa'] as $k) {
$legacyField = $this->resolveMappingLegacyField($bm, $k);
if ($legacyField) {
$lk = strtolower(trim($legacyField));
if (! isset($mapLegacy[$k])) {
$mapLegacy[$k] = [];
}
array_unshift($mapLegacy[$k], $lk);
}
}
}
}
$now = now();
// Verifica se esiste già un conto marcato come "nostro" per questo stabile
$nostroAlready = false;
if (Schema::hasTable('dati_bancari') && $stabileId) {
try {
$nostroAlready = DB::table('dati_bancari')
->where('stabile_id', $stabileId)
->where('is_nostro_conto', true)
->exists();
} catch (\Throwable $e) {
$nostroAlready = false;
}
}
$created = 0;
$updated = 0;
$seenKeys = [];
foreach ($rows as $r) {
// Costruisci payload best-effort cercando tra alias
$getFirst = function (array $aliases) use ($r) {
foreach ($aliases as $a) {
if (array_key_exists($a, $r) && trim((string) $r[$a]) !== '') {
return trim((string) $r[$a]);
}
}
return null;
};
$denom = $getFirst($mapLegacy['nome_banca']);
$iban = strtoupper(preg_replace('/\s+/', '', (string) ($getFirst($mapLegacy['iban']) ?? '')));
$swift = strtoupper((string) ($getFirst($mapLegacy['swift']) ?? ''));
$numero = $getFirst($mapLegacy['conto']);
$intest = $getFirst($mapLegacy['intestatario']);
$codCassa = $getFirst($mapLegacy['cod_cassa']);
$descr = $getFirst($mapLegacy['descrizione']);
if (! $denom && ! $iban && ! $numero && ! $codCassa && ! $descr) {
continue;
}
// riga non informativa
// Chiave dedup per stabile: IBAN preferito, altrimenti numero conto
$key = $iban ?: ($numero ? ('N:' . $numero) : ($codCassa ? ('C:' . strtoupper(trim($codCassa))) : null));
if (! $key) {
continue;
}
if (isset($seenKeys[$key])) {
continue;
}
// evita duplicati da diverse sorgenti
$seenKeys[$key] = true;
// Caso 1: IBAN presente → tratta come conto bancario (dati_bancari)
if (! $iban) {
// Nessun IBAN → registra come cassa generica in tabella 'casse'
if (Schema::hasTable('casse')) {
$code = strtoupper(trim((string) ($codCassa ?: '')));
if ($code === '') {
// fallback generico evitando collisioni: GEN, GEN1, ...
$base = 'GEN';
$code = $base;
$idx = 1;
while (DB::table('casse')->where('stabile_id', $stabileId)->where('cod_cassa', $code)->exists()) {
$code = $base . (++$idx);
}
}
$existsC = DB::table('casse')->where('stabile_id', $stabileId)->where('cod_cassa', $code)->first();
if (! $existsC) {
DB::table('casse')->insert([
'tenant_id' => null,
'stabile_id' => $stabileId,
'cod_cassa' => $code,
'descrizione' => ($descr ?: ($denom ?: 'Cassa generica')),
'tipo' => ($code === 'CON') ? 'cassa_contanti' : 'altro',
'iban' => null,
'intestazione_conto' => $intest ?: null,
'attiva' => true,
'meta' => json_encode(['from' => 'Anagr_casse', 'numero' => $numero]),
'created_at' => $now,
'updated_at' => $now,
]);
$created++;
} else {
$upd = [];
if ($descr && (empty($existsC->descrizione) || $existsC->descrizione === 'ND')) {
$upd['descrizione'] = $descr;
}
if (! empty($upd)) {
$upd['updated_at'] = $now;
DB::table('casse')->where('id', $existsC->id)->update($upd);
$updated++;
}
}
}
// Per la cassa contanti "CON" crea/aggiorna anche un DatiBancari di tipo cassa per renderlo visibile nell'UI
$codeUp = strtoupper(trim((string) $codCassa));
if ($codeUp === 'CON' && Schema::hasTable('dati_bancari') && $stabileId) {
$existingDb = DB::table('dati_bancari')
->where('stabile_id', $stabileId)
->where(function ($q) {
$q->whereNull('iban')->orWhere('iban', '');
})
->where(function ($q) use ($codeUp) {
$q->where('legacy_cod_cassa', $codeUp)
->orWhere('numero_conto', $codeUp);
})
->first();
$payloadCassa = [
'stabile_id' => $stabileId,
'contatto_id' => null,
'tipo_conto' => 'cassa',
'denominazione_banca' => $denom ?: ($descr ?: 'Cassa contanti'),
'numero_conto' => $codeUp,
'iban' => null,
'legacy_cod_cassa' => $codeUp,
'abi' => null,
'cab' => null,
'cin' => null,
'bic_swift' => null,
'intestazione_conto' => $intest ?: null,
'data_saldo_iniziale' => $now->toDateString(),
'saldo_iniziale' => 0,
'valuta' => 'EUR',
'stato_conto' => 'attivo',
'is_nostro_conto' => ! $nostroAlready,
'note' => '[GESCON] Cassa contanti',
'updated_at' => $now,
];
if ($existingDb) {
DB::table('dati_bancari')->where('id', $existingDb->id)->update($payloadCassa);
$updated++;
} else {
$payloadCassa['created_at'] = $now;
DB::table('dati_bancari')->insert($payloadCassa);
$created++;
$nostroAlready = $nostroAlready || $payloadCassa['is_nostro_conto'];
}
}
// Non creare dati_bancari in assenza di IBAN, passa alla prossima riga
if ($limit && ($created + $updated) >= $limit) {
break;
}
continue;
}
// Cerca esistente per stabile_id + IBAN/numero
$exists = null;
if ($iban) {
$exists = DB::table('dati_bancari')
->where('stabile_id', $stabileId)
->whereRaw('REPLACE(UPPER(iban)," ","") = ?', [$iban])
->first();
}
if (! $exists && $numero) {
$exists = DB::table('dati_bancari')
->where('stabile_id', $stabileId)
->where('numero_conto', $numero)
->first();
}
$payload = [
'stabile_id' => $stabileId,
'contatto_id' => null,
'tipo_conto' => 'corrente',
'denominazione_banca' => $denom ?: 'Banca',
'numero_conto' => $numero,
'iban' => $iban ?: null,
'legacy_cod_cassa' => $codCassa ?: null,
'abi' => null,
'cab' => null,
'cin' => null,
'bic_swift' => $swift ?: null,
'intestazione_conto' => $intest ?: null,
'data_saldo_iniziale' => $now->toDateString(),
'saldo_iniziale' => 0,
'valuta' => 'EUR',
'stato_conto' => 'attivo',
'is_nostro_conto' => false,
'note' => '[GESCON]',
'created_at' => $now,
'updated_at' => $now,
];
if (! $nostroAlready) {
// Marca il primo che inseriamo come nostro conto
$payload['is_nostro_conto'] = true;
$nostroAlready = true;
}
if ($exists) {
// Aggiorna campi vuoti
$upd = [];
foreach (['denominazione_banca', 'numero_conto', 'iban', 'bic_swift', 'intestazione_conto', 'legacy_cod_cassa'] as $c) {
$newV = $payload[$c] ?? null;
$oldV = $exists->$c ?? null;
if ($newV && (! $oldV || $oldV === 'ND')) {
$upd[$c] = $newV;
}
}
if (! empty($upd)) {
$upd['updated_at'] = $now;
if (! $this->isDryRun) {
DB::table('dati_bancari')->where('id', $exists->id)->update($upd);
}
$updated++;
}
continue;
}
$this->dynamicInsert('dati_bancari', $payload);
// Reperisci il record appena inserito e applica eventuale codice cassa dal mapping
try {
$inserted = null;
if ($iban) {
$inserted = DB::table('dati_bancari')
->where('stabile_id', $stabileId)
->whereRaw('REPLACE(UPPER(iban)," ","") = ?', [$iban])
->orderByDesc('id')
->first();
}
if (! $inserted && $numero) {
$inserted = DB::table('dati_bancari')
->where('stabile_id', $stabileId)
->where('numero_conto', $numero)
->orderByDesc('id')
->first();
}
if ($inserted && ! empty($this->banksMapping)) {
$assigned = null;
foreach ($this->banksMapping as $bm) {
if (! is_array($bm)) {
continue;
}
$targetCode = $this->resolveMappingCodiceCassa($bm);
if (! $targetCode) {
continue;
}
$legacyCode = $bm['legacy_cod_cassa'] ?? $this->resolveMappingLegacyField($bm, 'cod_cassa');
$legacyCode = $legacyCode ? strtoupper(trim((string) $legacyCode)) : null;
$currentLegacyCode = isset($inserted->legacy_cod_cassa) ? strtoupper(trim((string) $inserted->legacy_cod_cassa)) : null;
if ($legacyCode && $currentLegacyCode && $legacyCode === $currentLegacyCode) {
$assigned = $targetCode;
break;
}
// Compatibilità formato precedente: confronta IBAN/numero reali se disponibili
$legacyIban = isset($bm['iban']) ? strtoupper(preg_replace('/\s+/', '', (string) $bm['iban'])) : null;
$legacyNum = isset($bm['conto']) ? (string) $bm['conto'] : null;
if ($legacyIban && $iban && $legacyIban === $iban) {
$assigned = $targetCode;
break;
}
if ($legacyNum && $numero && $legacyNum === (string) $numero) {
$assigned = $targetCode;
break;
}
}
if ($assigned) {
DB::table('dati_bancari')->where('id', $inserted->id)->update([
'legacy_cod_cassa' => $assigned,
'updated_at' => now(),
]);
}
}
} catch (\Throwable $e) {
}
$created++;
if ($limit && ($created + $updated) >= $limit) {
break;
}
}
if (($created + $updated) > 0) {
$this->line(" → conti bancari creati: {$created}, aggiornati: {$updated}");
}
// Assicura casse di base per lo stabile: CON (cassa contanti) sempre presente, CDL (cassa lavoro) opzionale
try {
if (Schema::hasTable('casse') && $stabileId) {
// CON: Cassa contanti, sempre
$existsCon = DB::table('casse')->where('stabile_id', $stabileId)->where('cod_cassa', 'CON')->first();
if (! $existsCon) {
DB::table('casse')->insert([
'tenant_id' => null,
'stabile_id' => $stabileId,
'cod_cassa' => 'CON',
'descrizione' => 'Cassa contanti',
'tipo' => 'cassa_contanti',
'iban' => null,
'intestazione_conto' => null,
'attiva' => true,
'meta' => json_encode(['seeded' => true]),
'created_at' => now(),
'updated_at' => now(),
]);
}
// CDL: Cassa di lavoro (crea se non esiste; opzionale ma utile)
$existsCdl = DB::table('casse')->where('stabile_id', $stabileId)->where('cod_cassa', 'CDL')->first();
if (! $existsCdl) {
DB::table('casse')->insert([
'tenant_id' => null,
'stabile_id' => $stabileId,
'cod_cassa' => 'CDL',
'descrizione' => 'Cassa di lavoro',
'tipo' => 'altro',
'iban' => null,
'intestazione_conto' => null,
'attiva' => true,
'meta' => json_encode(['seeded' => true]),
'created_at' => now(),
'updated_at' => now(),
]);
}
}
} catch (\Throwable $e) {
// non bloccare la pipeline per errori di seeding casse
}
return $created + $updated;
}
private function stepOperazioni(?int $limit): int
{
if (! Schema::connection('gescon_import')->hasTable('operazioni')) {
return 0;
}
$src = DB::connection('gescon_import')->table('operazioni');
if ($limit) {
$src->limit($limit);
}
$count = 0;
foreach ($src->get() as $o) {
$legacy = $o->id_operaz ?? null;
if (! $legacy) {
continue;
}
// Se la tabella dominio non esiste, in dry-run consideriamo solo il conteggio e saltiamo le scritture
if (! Schema::hasTable('operazioni_contabili')) {
$count++;
continue;
}
$exists = DB::table('operazioni_contabili')->where('legacy_id', $legacy)->first();
if ($exists) {
continue;
}
$data = [
'tenant_id' => null,
'gestione_id' => $this->resolveGestioneId($o->anno ?? null, $o->compet ?? null, $o->gestione ?? null),
'legacy_id' => $legacy,
'descrizione' => $o->note ?? ($o->natura ?? 'Operazione'),
'data_operazione' => $o->dt_spe ?? now()->toDateString(),
'compet' => in_array($o->compet ?? 'C', ['C', 'P']) ? $o->compet : 'C',
'natura2' => $o->natura2 ?? null,
'n_stra' => $o->n_stra ?? 0,
'protocollo_numero' => $this->nextProtocolNumber(),
'protocollo_completo' => null,
'dare' => (float) ($o->importo_spese ?? 0),
'avere' => (float) ($o->importo_entrate ?? 0),
'conto_contabile' => $o->cod_spe ?? null,
'stato_operazione' => 'confermata',
'created_at' => now(),
'updated_at' => now(),
];
// Collega conto bancario se la colonna esiste nello schema
if (Schema::hasColumn('operazioni_contabili', 'conto_bancario_id')) {
$contoId = null;
// 1) prova via cod_cassa della riga (se presente in staging operazioni)
$codCassa = $o->cod_cassa ?? $o->modalita_pagamento ?? null;
if ($codCassa) {
$contoId = $this->resolveContoByCodCassa($this->stabileId, (string) $codCassa);
}
// 2) fallback al default per stabile
if (! $contoId) {
$contoId = $this->resolveDefaultContoBancarioId($this->stabileId);
}
if ($contoId) {
$data['conto_bancario_id'] = $contoId;
}
}
// Se previsto, collega cassa (CON/CDL/altre) quando non c'è un conto bancario
if (Schema::hasColumn('operazioni_contabili', 'cassa_id') && empty($data['conto_bancario_id'] ?? null)) {
$codCassa = $o->cod_cassa ?? $o->modalita_pagamento ?? null;
if ($codCassa) {
$cassaId = $this->resolveCassaByCodCassa($this->stabileId, (string) $codCassa);
if ($cassaId) {
$data['cassa_id'] = $cassaId;
}
}
}
$this->dynamicInsert('operazioni_contabili', $data);
$count++;
}
return $count;
}
private function stepRate(?int $limit): int
{
if (Schema::connection('gescon_import')->hasTable('rate_emissioni_dettaglio') && Schema::hasTable('rate_emesse')) {
return $this->importRateEmesseDaEmissioniDettaglio($limit);
}
if (! Schema::connection('gescon_import')->hasTable('rate')) {
return 0;
}
$src = DB::connection('gescon_import')->table('rate as r');
// Filtro per stabile: se rate.cod_stabile non esiste, prova join con condomin usando r.cod_cond o r.id_cond
if ($this->option('stabile')) {
$stab = $this->option('stabile');
if (Schema::connection('gescon_import')->hasColumn('rate', 'cod_stabile')) {
$src->where('r.cod_stabile', $stab);
} elseif (
Schema::connection('gescon_import')->hasTable('condomin') &&
(Schema::connection('gescon_import')->hasColumn('rate', 'cod_cond') || Schema::connection('gescon_import')->hasColumn('rate', 'id_cond')) &&
Schema::connection('gescon_import')->hasColumn('condomin', 'cod_stabile')
) {
$joinLeft = Schema::connection('gescon_import')->hasColumn('rate', 'cod_cond') ? 'cod_cond' : 'id_cond';
$src = $src->join('condomin as c', 'c.' . $joinLeft, '=', 'r.' . $joinLeft)
->where('c.cod_stabile', $stab)
->select('r.*');
}
}
if ($limit) {
$src->limit($limit);
}
$count = 0;
foreach ($src->get() as $r) {
$legacy = $r->id ?? ($r->n_rata ?? null) ?? ($r->numero_rata ?? null) ?? ($r->n_emissione ?? null);
if (! $legacy) {
// Fallback deterministico: hash di campi tipici
$key = implode('|', [
(string) ($r->cod_stabile ?? $this->option('stabile') ?? ''),
(string) ($r->cod_cond ?? $r->id_cond ?? ''),
(string) ($r->data_emissione ?? ''),
(string) ($r->importo ?? $r->importo_euro ?? ''),
(string) ($r->causale ?? ''),
]);
$legacy = hexdec(substr(hash('crc32b', $key), 0, 8));
}
if (! $legacy) {
continue;
}
if (Schema::hasColumn('rate_emesse', 'legacy_id')) { // tabella evoluta con legacy
$exists = DB::table('rate_emesse')->where('legacy_id', $legacy)->first();
if ($exists) {
continue;
}
$data = [
'legacy_id' => $legacy,
'periodo_anno' => $r->data_emissione ? (int) date('Y', strtotime($r->data_emissione)) : null,
'periodo_mese' => $r->data_emissione ? (int) date('n', strtotime($r->data_emissione)) : null,
'importo' => $r->importo ?? 0,
'descrizione' => $r->causale ?? null,
'competenza_da' => $r->data_emissione ?? null,
'competenza_a' => $r->data_scadenza ?? null,
'created_at' => now(),
'updated_at' => now(),
];
$this->dynamicInsert('rate_emesse', $data);
} else {
// Fallback: tabella rate_emesse originale (richiede molti campi); skip per ora
continue;
}
$count++;
}
return $count;
}
private function importRateEmesseDaEmissioniDettaglio(?int $limit): int
{
if (! Schema::connection('gescon_import')->hasTable('rate_emissioni_dettaglio')) {
return 0;
}
if (! Schema::connection('gescon_import')->hasTable('rate_emissioni')) {
return 0;
}
$stab = $this->option('stabile');
$src = DB::connection('gescon_import')->table('rate_emissioni_dettaglio as d');
if ($stab) {
$src->where('d.cod_stabile', $stab);
}
if ($limit) {
$src->limit($limit);
}
$emissioni = DB::connection('gescon_import')->table('rate_emissioni')
->when($stab, fn($q) => $q->where('cod_stabile', $stab))
->get()
->groupBy(fn($r) => (string) $r->numero_emissione);
$piani = [];
$count = 0;
$cumuloCache = [];
$hasCondomin = Schema::connection('gescon_import')->hasTable('condomin');
foreach ($src->get() as $row) {
$codStabile = (string) ($row->cod_stabile ?? $stab ?? '');
$numeroEmissione = (int) ($row->numero_emissione ?? 0);
if ($codStabile === '' || $numeroEmissione <= 0) {
continue;
}
$codCond = (string) ($row->cod_cond ?? '');
$unitaId = $this->resolveUnitaIdFromCodCond($codStabile, $codCond);
if (! $unitaId) {
continue;
}
$soggettoId = $this->resolveSoggettoResponsabileId($unitaId, (string) ($row->tipo_soggetto ?? ''));
if (! $soggettoId) {
continue;
}
if (! isset($piani[$numeroEmissione])) {
$piani[$numeroEmissione] = $this->ensurePianoRateizzazionePerEmissione($codStabile, $numeroEmissione, $emissioni);
}
$pianoId = $piani[$numeroEmissione];
if (! $pianoId) {
continue;
}
$importo = is_numeric($row->importo_dovuto_euro ?? null)
? (float) $row->importo_dovuto_euro
: (is_numeric($row->importo_dovuto ?? null) ? (float) $row->importo_dovuto : 0.0);
$numeroRata = is_numeric($row->numero_ricevuta ?? null)
? (int) $row->numero_ricevuta
: (is_numeric($row->numero_mese ?? null) ? (int) $row->numero_mese : 1);
$dataEmissione = $row->data_emissione ?? null;
$dataScadenza = $row->data_scadenza ?? null;
$emissioneRows = $emissioni[(string) $numeroEmissione] ?? null;
$emissioneRow = $emissioneRows ? $emissioneRows->first() : null;
if (! $dataEmissione && $emissioneRow?->data_emissione) {
$dataEmissione = $emissioneRow->data_emissione;
}
if (! $dataScadenza && $emissioneRow?->data_scadenza) {
$dataScadenza = $emissioneRow->data_scadenza;
}
$numeroRataEff = $numeroRata;
$condInq = trim((string) ($row->cond_inq ?? $row->tipo_soggetto ?? ''));
$raggruppamento = trim((string) ($row->raggruppamento ?? ''));
if ($raggruppamento === '' && $hasCondomin && $codCond !== '') {
$cacheKey = $codStabile . '|' . $codCond;
if (! array_key_exists($cacheKey, $cumuloCache)) {
$cumuloCache[$cacheKey] = DB::connection('gescon_import')
->table('condomin')
->where('cod_stabile', $codStabile)
->where('cod_cond', $codCond)
->first(['cumulo_cond', 'cumulo_inq']);
}
$cumuloRow = $cumuloCache[$cacheKey];
if ($cumuloRow) {
$raggruppamento = trim((string) (
strtoupper($condInq) === 'I' ? ($cumuloRow->cumulo_inq ?? '') : ($cumuloRow->cumulo_cond ?? '')
));
}
}
$noteParts = [
'n_emissione=' . $numeroEmissione,
'anno_emissione=' . (string) ($row->anno_emissione ?? ''),
'cod_cond_src=' . $codCond,
'o_r_s=' . (string) ($row->tipo_quota ?? $row->tipo_soggetto ?? ''),
'n_mese=' . (string) ($row->numero_mese ?? ''),
$condInq !== '' ? 'cond_inq=' . $condInq : '',
$raggruppamento !== '' ? 'raggruppamento=' . $raggruppamento : '',
];
$existing = DB::table('rate_emesse')
->where('piano_rateizzazione_id', $pianoId)
->where('unita_immobiliare_id', $unitaId)
->where('soggetto_responsabile_id', $soggettoId)
->where('numero_rata_progressivo', $numeroRataEff)
->first();
if ($existing) {
$descr = (string) ($row->descrizione ?? $emissioneRow?->descrizione ?? '');
$descrizione = (string) ($existing->descrizione ?? '');
if ($descr !== '' && $descrizione !== '' && stripos($descrizione, $descr) === false) {
$descrizione = trim($descrizione . ' | ' . $descr);
} elseif ($descrizione === '' && $descr !== '') {
$descrizione = $descr;
}
$noteAgg = implode('|', array_filter($noteParts, fn($v) => $v !== ''));
$note = (string) ($existing->note ?? '');
if ($note === '') {
$note = $noteAgg;
} elseif ($noteAgg !== '' && stripos($note, $noteAgg) === false) {
$note = trim($note . ' | ' . $noteAgg);
}
DB::table('rate_emesse')
->where('id', $existing->id)
->update([
'descrizione' => $descrizione,
'importo_originario_unita' => (float) ($existing->importo_originario_unita ?? 0) + $importo,
'importo_addebitato_soggetto' => (float) ($existing->importo_addebitato_soggetto ?? 0) + $importo,
'note' => $note,
'updated_at' => now(),
]);
continue;
}
// Evita duplicati: stessa emissione, unità, soggetto, rata e date
$noteNeedle = '%n_emissione=' . $numeroEmissione . '%'
. 'cod_cond_src=' . $codCond . '%'
. 'o_r_s=' . (string) ($row->tipo_quota ?? $row->tipo_soggetto ?? '') . '%'
. 'n_mese=' . (string) ($row->numero_mese ?? '') . '%';
$dupExists = DB::table('rate_emesse')
->where('piano_rateizzazione_id', $pianoId)
->where('unita_immobiliare_id', $unitaId)
->where('soggetto_responsabile_id', $soggettoId)
->where('numero_rata_progressivo', $numeroRataEff)
->whereDate('data_emissione', $dataEmissione ?? now()->toDateString())
->whereDate('data_scadenza', $dataScadenza ?? ($dataEmissione ?? now()->toDateString()))
->where('note', 'like', $noteNeedle)
->exists();
if ($dupExists) {
continue;
}
$payload = [
'piano_rateizzazione_id' => $pianoId,
'unita_immobiliare_id' => $unitaId,
'soggetto_responsabile_id' => $soggettoId,
'numero_rata_progressivo' => $numeroRataEff,
'descrizione' => $row->descrizione ?? ($emissioneRow?->descrizione ?? null),
'importo_originario_unita' => $importo,
'percentuale_addebito_soggetto' => 100.0,
'importo_addebitato_soggetto' => $importo,
'data_emissione' => $dataEmissione ?? now()->toDateString(),
'data_scadenza' => $dataScadenza ?? ($dataEmissione ?? now()->toDateString()),
'stato_rata' => 'EMESSA',
'data_ultimo_pagamento' => null,
'importo_pagato' => 0,
'note' => implode('|', array_filter($noteParts, fn($v) => $v !== '')),
'created_at' => now(),
'updated_at' => now(),
];
$this->dynamicInsert('rate_emesse', $payload);
$count++;
}
return $count;
}
private function resolveUnitaIdFromCodCond(string $codStabile, string $codCond): ?int
{
$codCond = trim($codCond);
if ($codCond === '') {
return null;
}
$stabileId = $this->stabileId;
if (Schema::hasTable('stabili')) {
$stabileCol = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione';
$stRow = DB::table('stabili')->where($stabileCol, $codStabile)->first();
if ($stRow) {
$stabileId = $stRow->id;
}
}
if (! $stabileId) {
return null;
}
$cond = null;
if (Schema::connection('gescon_import')->hasTable('condomin')) {
$cond = $this->scopeCondominQuery(DB::connection('gescon_import')->table('condomin'))
->where('cod_stabile', $codStabile)
->where('cod_cond', $codCond)
->first(['scala', 'interno', 'codice_fiscale', 'email', 'nome', 'cognome']);
}
$scala = $cond?->scala ? trim((string) $cond->scala) : 'A';
$interno = $cond?->interno ? trim((string) $cond->interno) : '';
if ($interno === '') {
$interno = $codCond;
}
$unita = DB::table('unita_immobiliari')
->where('stabile_id', $stabileId)
->where('scala', $scala)
->where('interno', $interno)
->first(['id']);
if ($unita?->id) {
return (int) $unita->id;
}
$unita = DB::table('unita_immobiliari')
->where('stabile_id', $stabileId)
->where('interno', $interno)
->orderBy('id')
->first(['id']);
if ($unita?->id) {
return (int) $unita->id;
}
if ($cond) {
$cf = trim((string) ($cond->codice_fiscale ?? '')) ?: null;
$email = trim((string) ($cond->email ?? '')) ?: null;
$nome = $cond->nome ?? null;
$cognome = $cond->cognome ?? null;
$sog = $this->findExistingSoggetto($cf, $email, $nome, $cognome);
if ($sog && Schema::hasTable('proprieta')) {
$row = DB::table('proprieta')
->where('soggetto_id', $sog->id)
->whereIn('tipo_diritto', ['proprieta', 'locazione'])
->orderByRaw("CASE WHEN tipo_diritto = 'proprieta' THEN 0 ELSE 1 END")
->orderByDesc('percentuale_possesso')
->orderByDesc('id')
->first(['unita_immobiliare_id']);
if ($row?->unita_immobiliare_id) {
return (int) $row->unita_immobiliare_id;
}
}
if (Schema::hasTable('unita_immobiliari')) {
$codice = $codStabile . '-' . $scala . '-' . $interno;
$existing = DB::table('unita_immobiliari')
->where('stabile_id', $stabileId)
->where('codice_unita', $codice)
->first(['id']);
if (! $existing) {
$uid = DB::table('unita_immobiliari')->insertGetId([
'stabile_id' => $stabileId,
'codice_unita' => $codice,
'scala' => $scala,
'interno' => $interno,
'denominazione' => trim((string) ($nome ?? '') . ' ' . (string) ($cognome ?? '')) ?: null,
'tipo_unita' => 'abitazione',
'stato_occupazione' => 'occupata_proprietario',
'attiva' => true,
'created_at' => now(),
'updated_at' => now(),
]);
$existing = $uid ? (object) ['id' => $uid] : null;
}
if ($existing && Schema::hasTable('soggetti')) {
if (! $sog) {
$payload = [
'old_id' => $this->safeOldId($codCond),
'nome' => $nome,
'cognome' => $cognome,
'codice_fiscale' => $cf,
'email' => $email,
'tipo' => 'proprietario',
'created_at' => now(),
'updated_at' => now(),
];
if (Schema::hasColumn('soggetti', 'codice_univoco')) {
$baseKey = $this->normalizeNameKey($nome ?? '', $cognome ?? '') ?: ($email ?: 'ND');
$code = $this->generateStableCondKey('S', $codStabile ?: 'S0', $baseKey);
$tries = 0;
while (DB::table('soggetti')->where('codice_univoco', $code)->exists() && $tries < 5) {
$salt = ($codCond ?? '') . '-' . ($tries + 1);
$code = $this->generateStableCondKey('S', $codStabile ?: 'S0', $baseKey . '|' . $salt);
$tries++;
}
$payload['codice_univoco'] = $code;
}
$sid = DB::table('soggetti')->insertGetId($payload);
$sog = $sid ? (object) ['id' => $sid] : null;
}
if ($sog && Schema::hasTable('proprieta')) {
$existsProp = DB::table('proprieta')
->where('unita_immobiliare_id', $existing->id)
->where('soggetto_id', $sog->id)
->first(['id']);
if (! $existsProp) {
DB::table('proprieta')->insert([
'unita_immobiliare_id' => $existing->id,
'soggetto_id' => $sog->id,
'tipo_diritto' => 'proprieta',
'created_at' => now(),
'updated_at' => now(),
]);
}
}
}
if ($existing?->id) {
return (int) $existing->id;
}
}
}
$codice = $codStabile . '-' . $scala . '-' . $interno;
$unita = DB::table('unita_immobiliari')
->where('stabile_id', $stabileId)
->where('codice_unita', $codice)
->first(['id']);
if ($unita?->id) {
return (int) $unita->id;
}
if (! $cond && Schema::hasTable('unita_immobiliari')) {
$scala = 'A';
$interno = $codCond;
$codice = $codStabile . '-' . $scala . '-' . $interno;
$uid = DB::table('unita_immobiliari')->insertGetId([
'stabile_id' => $stabileId,
'codice_unita' => $codice,
'scala' => $scala,
'interno' => $interno,
'denominazione' => 'Unità legacy ' . $codCond,
'tipo_unita' => 'abitazione',
'stato_occupazione' => 'occupata_proprietario',
'attiva' => true,
'created_at' => now(),
'updated_at' => now(),
]);
if ($uid) {
if (Schema::hasTable('soggetti')) {
$payload = [
'old_id' => $this->safeOldId($codCond),
'nome' => 'Condomino',
'cognome' => $codCond,
'tipo' => 'proprietario',
'created_at' => now(),
'updated_at' => now(),
];
if (Schema::hasColumn('soggetti', 'codice_univoco')) {
$baseKey = 'COND-' . $codCond;
$code = $this->generateStableCondKey('S', $codStabile ?: 'S0', $baseKey);
$tries = 0;
while (DB::table('soggetti')->where('codice_univoco', $code)->exists() && $tries < 5) {
$code = $this->generateStableCondKey('S', $codStabile ?: 'S0', $baseKey . '|' . ($tries + 1));
$tries++;
}
$payload['codice_univoco'] = $code;
}
$sid = DB::table('soggetti')->insertGetId($payload);
if ($sid && Schema::hasTable('proprieta')) {
DB::table('proprieta')->insert([
'unita_immobiliare_id' => $uid,
'soggetto_id' => $sid,
'tipo_diritto' => 'proprieta',
'created_at' => now(),
'updated_at' => now(),
]);
}
}
return (int) $uid;
}
}
return null;
}
private function resolveSoggettoResponsabileId(int $unitaId, string $tipoSoggetto = ''): ?int
{
if (! Schema::hasTable('proprieta')) {
return null;
}
$tipoSoggetto = strtoupper(trim($tipoSoggetto));
$prefer = $tipoSoggetto === 'I' ? 'locazione' : 'proprieta';
$row = DB::table('proprieta')
->where('unita_immobiliare_id', $unitaId)
->orderByRaw("CASE WHEN tipo_diritto = ? THEN 0 ELSE 1 END", [$prefer])
->orderByDesc('percentuale_possesso')
->orderByDesc('id')
->first(['soggetto_id']);
return $row?->soggetto_id ? (int) $row->soggetto_id : null;
}
private function ensurePianoRateizzazionePerEmissione(string $codStabile, int $numeroEmissione, $emissioni): ?int
{
if (! Schema::hasTable('piano_rateizzazione')) {
return null;
}
$stabileId = $this->stabileId;
if (Schema::hasTable('stabili')) {
$stabileCol = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione';
$stRow = DB::table('stabili')->where($stabileCol, $codStabile)->first();
if ($stRow) {
$stabileId = $stRow->id;
}
}
if (! $stabileId) {
return null;
}
$noteKey = 'emissione=' . $numeroEmissione;
$exists = DB::table('piano_rateizzazione')
->where('stabile_id', $stabileId)
->where('note', 'like', '%' . $noteKey . '%')
->first(['id']);
if ($exists?->id) {
return (int) $exists->id;
}
$emissioneRows = $emissioni[(string) $numeroEmissione] ?? null;
$emissioneRow = $emissioneRows ? $emissioneRows->first() : null;
$descr = $emissioneRow?->descrizione ?: ('Emissione ' . $numeroEmissione);
$dataInizio = $emissioneRow?->data_emissione ?? now()->toDateString();
$dataFine = $emissioneRow?->data_scadenza ?? $dataInizio;
$totale = 0.0;
if (Schema::connection('gescon_import')->hasTable('rate_emissioni_dettaglio')) {
$totale = (float) DB::connection('gescon_import')->table('rate_emissioni_dettaglio')
->where('cod_stabile', $codStabile)
->where('numero_emissione', $numeroEmissione)
->sum(DB::raw('COALESCE(importo_dovuto_euro, importo_dovuto, 0)'));
}
$payload = [
'stabile_id' => $stabileId,
'unita_immobiliare_id' => null,
'descrizione' => $descr,
'importo_totale' => $totale,
'numero_rate' => 1,
'importo_rata' => $totale,
'data_inizio' => $dataInizio,
'data_fine' => $dataFine,
'frequenza' => 'MENSILE',
'stato' => 'ATTIVO',
'importo_pagato' => 0,
'importo_residuo' => $totale,
'note' => 'emissione=' . $numeroEmissione . '|cod_stabile=' . $codStabile,
'created_at' => now(),
'updated_at' => now(),
];
if (! $this->isDryRun) {
$id = DB::table('piano_rateizzazione')->insertGetId($payload);
return is_numeric($id) ? (int) $id : null;
}
return null;
}
private function stepIncassi(?int $limit): int
{
if (! Schema::connection('gescon_import')->hasTable('incassi')) {
return 0;
}
$src = DB::connection('gescon_import')->table('incassi');
if ($this->option('stabile')) {
$src->where('cod_stabile', $this->option('stabile'));
}
if ($limit) {
$src->limit($limit);
}
$count = 0;
foreach ($src->get() as $i) {
$legacy = $i->id ?? null;
if (! $legacy) {
continue;
}
if (! Schema::hasTable('incassi')) {
continue;
}
$legacyCol = Schema::hasColumn('incassi', 'legacy_id') ? 'legacy_id' : (Schema::hasColumn('incassi', 'id_incasso_gescon') ? 'id_incasso_gescon' : null);
if (! $legacyCol) {
continue;
}
$existsQuery = DB::table('incassi')->where($legacyCol, $legacy);
if ($this->stabileId && Schema::hasColumn('incassi', 'condominio_id')) {
$existsQuery->where('condominio_id', $this->stabileId);
}
$exists = $existsQuery->first();
if ($exists) {
continue;
}
$data = [];
$data[$legacyCol] = $legacy;
$codStabile = (string) ($i->cod_stabile ?? ($this->option('stabile') ?? ''));
$codCond = (string) ($i->cod_cond ?? '');
$condInq = (string) ($i->cond_inq ?? $i->cond_inquil ?? '');
$unitaId = $codCond !== '' ? $this->resolveUnitaIdFromCodCond($codStabile, $codCond) : null;
$soggettoId = $unitaId ? $this->resolveSoggettoResponsabileId($unitaId, $condInq) : null;
if (Schema::hasColumn('incassi', 'data_incasso')) {
$data['data_incasso'] = $i->data_incasso ?? $i->dt_empag ?? now()->toDateString();
$data['importo'] = $i->importo_pagato_euro ?? $i->importo_euro ?? $i->importo ?? 0;
$data['descrizione'] = $i->descrizione ?? $i->note ?? null;
$data['riferimento_orig'] = $i->riferimento_pagamento ?? null;
$data['ruolo_quota'] = $condInq !== '' ? $condInq : null;
if (Schema::hasColumn('incassi', 'cod_cond_gescon')) {
$data['cod_cond_gescon'] = $codCond !== '' ? $codCond : null;
}
if (Schema::hasColumn('incassi', 'cond_inquil')) {
$data['cond_inquil'] = $condInq !== '' ? $condInq : null;
}
if (Schema::hasColumn('incassi', 'n_mese')) {
$data['n_mese'] = $i->n_mese ?? $i->n_rata_riferimento ?? null;
}
if (Schema::hasColumn('incassi', 'o_r_s')) {
$data['o_r_s'] = $i->o_r_s ?? null;
}
if (Schema::hasColumn('incassi', 'anno_rif')) {
$data['anno_rif'] = $i->anno_rif ?? null;
}
if (Schema::hasColumn('incassi', 'dt_empag')) {
$data['dt_empag'] = $i->dt_empag ?? $i->data_incasso ?? null;
}
if (Schema::hasColumn('incassi', 'importo_pagato')) {
$data['importo_pagato'] = $i->importo_pagato_euro ?? $i->importo_euro ?? $i->importo ?? 0;
}
if (Schema::hasColumn('incassi', 'importo_pagato_euro')) {
$data['importo_pagato_euro'] = $i->importo_pagato_euro ?? $i->importo_euro ?? $i->importo ?? 0;
}
if (Schema::hasColumn('incassi', 'n_riferimento')) {
$data['n_riferimento'] = $i->n_riferimento ?? $i->riferimento_pagamento ?? null;
}
if (Schema::hasColumn('incassi', 'cod_cassa_gescon')) {
$data['cod_cassa_gescon'] = $i->cod_cassa ?? $i->modalita_pagamento ?? null;
}
if (Schema::hasColumn('incassi', 'proviene_n_stra') && ! empty($i->n_stra)) {
$data['proviene_n_stra'] = $i->n_stra;
}
if (Schema::hasColumn('incassi', 'condomino_id') && $soggettoId) {
$data['condomino_id'] = $soggettoId;
}
// collega eventuale conto per incasso se colonna esiste
if (Schema::hasColumn('incassi', 'conto_bancario_id')) {
$contoId = null;
$codCassa = $i->cod_cassa ?? $i->modalita_pagamento ?? null;
if ($codCassa) {
$contoId = $this->resolveContoByCodCassa($this->stabileId, (string) $codCassa);
}
if (! $contoId) {
$contoId = $this->resolveDefaultContoBancarioId($this->stabileId);
}
if ($contoId) {
$data['conto_bancario_id'] = $contoId;
}
}
// collega cassa per incasso se disponibile e nessun conto bancario è stato risolto
if (Schema::hasColumn('incassi', 'cassa_id') && empty($data['conto_bancario_id'] ?? null)) {
$codCassa = $i->cod_cassa ?? $i->modalita_pagamento ?? null;
if ($codCassa) {
$cassaId = $this->resolveCassaByCodCassa($this->stabileId, (string) $codCassa);
if ($cassaId) {
$data['cassa_id'] = $cassaId;
if (Schema::hasColumn('incassi', 'cod_cassa_legacy')) {
$data['cod_cassa_legacy'] = (string) $codCassa;
}
}
}
}
} else { // schema complesso (nuova tabella incassi)
$data = array_merge($data, [
'tenant_id' => 'T1',
'gestione_id' => null,
'condominio_id' => $this->stabileId ?? 1,
'conto_bancario_id' => (function () use ($i) {
$cod = $i->cod_cassa ?? $i->modalita_pagamento ?? null;
$id = $cod ? $this->resolveContoByCodCassa($this->stabileId, (string) $cod) : null;
return $id ?: ($this->resolveDefaultContoBancarioId($this->stabileId) ?? null);
})(),
'cod_cond_gescon' => $i->cod_cond ?? $i->cod_cond ?? null,
'cond_inquil' => $i->cond_inq ?? null,
'n_riferimento' => $i->riferimento_pagamento ?? null,
'anno_rif' => $i->data_incasso ? (int) date('Y', strtotime($i->data_incasso)) : 0,
'n_mese' => $i->n_mese ?? null,
'o_r_s' => null,
'importo_pagato' => $i->importo ?? 0,
'importo_pagato_euro' => $i->importo_euro ?? 0,
'dt_empag' => $i->data_incasso ?? null,
'descrizione' => $i->note ?? null,
'cod_cassa_gescon' => null,
'created_at' => now(),
'updated_at' => now(),
]);
// Se lo schema esteso prevedesse cassa_id, potremmo valorizzarlo qui se conto bancario non risolto
}
$data['created_at'] = $data['created_at'] ?? now();
$data['updated_at'] = now();
$this->dynamicInsert('incassi', $data);
$count++;
}
return $count;
}
/**
* Risolve un conto bancario per stabile utilizzando il legacy cod_cassa (o codice cassa equivalente)
*/
private function resolveContoByCodCassa(?int $stabileId, string $codCassa): ?int
{
if (! $stabileId || ! Schema::hasTable('dati_bancari')) {
return null;
}
$cod = strtoupper(trim($codCassa));
// Normalizza numeri conto da formati esterni (es. "CC N.000104951005" → "000104951005")
$normNumero = null;
if ($cod !== '') {
if (preg_match('/([0-9]{6,})/', $cod, $m)) {
$normNumero = ltrim($m[1], '0');
}
}
if ($cod === '') {
return null;
}
$row = DB::table('dati_bancari')
->where('stabile_id', $stabileId)
->where(function ($q) use ($cod) {
$q->where('legacy_cod_cassa', $cod)
->orWhere('numero_conto', $cod)
->orWhereRaw('REPLACE(UPPER(iban)," ","") = ?', [$cod]);
})
->orderByDesc('is_nostro_conto')
->orderByDesc('updated_at')
->first();
if (! $row && $normNumero) {
$row = DB::table('dati_bancari')
->where('stabile_id', $stabileId)
->where(function ($q) use ($normNumero) {
$q->where('numero_conto', $normNumero)
->orWhere('numero_conto', str_pad($normNumero, 12, '0', STR_PAD_LEFT));
})
->orderByDesc('is_nostro_conto')
->orderByDesc('updated_at')
->first();
}
return $row?->id ? (int) $row->id : null;
}
/**
* Risolve ID cassa (tabella casse) per cod_cassa legacy (es. CON, CCB, CDL, altri codici locali)
*/
private function resolveCassaByCodCassa(?int $stabileId, string $codCassa): ?int
{
if (! $stabileId || ! Schema::hasTable('casse')) {
return null;
}
$cod = strtoupper(trim($codCassa));
if ($cod === '') {
return null;
}
$row = DB::table('casse')
->where('stabile_id', $stabileId)
->where('cod_cassa', $cod)
->first();
// Mappature comuni: "CCB" potrebbe indicare il conto corrente bancario associato all'IBAN; in tal caso priorità al conto_bancario, ma lasciamo anche una cassa con stesso codice se creata
if (! $row) {
return null;
}
return (int) $row->id;
}
private function stepGiroconti(?int $limit): int
{
if (Schema::connection('gescon_import')->hasTable('Giri_conti')) {
$src = DB::connection('gescon_import')->table('Giri_conti');
} elseif (Schema::connection('gescon_import')->hasTable('giri_conti')) {
$src = DB::connection('gescon_import')->table('giri_conti');
} else {
return 0;
}
if ($limit) {
$src->limit($limit);
}
$count = 0;
foreach ($src->get() as $g) {
$rif = $g->riferimento ?? null;
if (! $rif) {
continue;
}
$exists = DB::table('movimenti_interni')->where('riferimento', $rif)->first();
if ($exists) {
continue;
}
$data = [
'legacy_id' => null,
'riferimento' => $rif,
'data' => $g->Data_giroconto ?? now()->toDateString(),
'descrizione' => $g->Descrizione ?? '',
'importo' => $g->Importo ?? 0,
'conto_from' => $g->Cod_uscita ?? '',
'conto_to' => $g->Cod_entrata ?? '',
'tipo' => $g->tipo_riga ?? null,
'created_at' => now(),
'updated_at' => now(),
];
$this->dynamicInsert('movimenti_interni', $data);
$count++;
}
return $count;
}
private function stepStraord(?int $limit): int
{
if (! Schema::connection('gescon_import')->hasTable('straordinarie')) {
return 0;
}
$src = DB::connection('gescon_import')->table('straordinarie');
if ($this->option('stabile') && Schema::connection('gescon_import')->hasColumn('straordinarie', 'cod_stabile')) {
$src->where('cod_stabile', $this->option('stabile'));
}
if ($limit) {
$src->limit($limit);
}
$count = 0;
foreach ($src->get() as $s) {
$legacy = $s->id_stra ?? null;
if (! $legacy) {
continue;
}
$legacyYear = isset($s->legacy_year) ? (string) $s->legacy_year : null;
$annoGestione = $this->resolveLegacySnapshotYear($legacyYear, $s);
$numeroStraordinaria = $this->inferStraordinariaSequence($s->codice ?? null, $s->descriz_prev_cons ?? null, $legacy);
$gestioneId = $this->resolveGestioneContabileIdForStabile($this->resolveStabileId(), $legacyYear, 'S', $numeroStraordinaria);
$codiceDominio = $this->buildScopedLegacyCode((string) ($s->codice ?? ''), $annoGestione, $numeroStraordinaria, 'STRA');
$exists = DB::table('lavori_straordinari')
->when($gestioneId, fn($q) => $q->where('gestione_id', $gestioneId))
->where('codice', $codiceDominio)
->first();
if ($exists) {
continue;
}
$isFiscal = $this->isFiscalStraordinaria($s);
$descrizione = trim((string) ($s->descriz_prev_cons ?? ''));
if ($descrizione === '') {
$descrizione = trim((string) ($s->descriz_ccp ?? '')) ?: ('Straordinaria ' . $legacy);
}
$data = [
'legacy_id' => $legacy,
'gestione_id' => $gestioneId,
'codice' => $codiceDominio,
'descrizione' => $descrizione,
'numero_rate' => $s->num_rate ?? 0,
'data_inizio' => isset($s->inizio_anno, $s->inizio_mese) ? sprintf('%04d-%02d-01', $s->inizio_anno, $s->inizio_mese) : null,
'natura' => $this->normalizeStraordinariaNatura($s->natura ?? null),
'gruppo' => $s->aggregazione ?? null,
'periodicita' => $s->periodicita ?? null,
'note_json' => json_encode([
'legacy_code' => $s->codice ?? null,
'legacy_year' => $legacyYear,
'anno_gestione' => $annoGestione,
'numero_straordinaria' => $numeroStraordinaria,
'descriz_ccp' => $s->descriz_ccp ?? null,
'is_fiscal_declaration' => $isFiscal,
'fiscal_declaration_key' => $isFiscal && $annoGestione ? sprintf('%d-%02d', $annoGestione, (int) ($numeroStraordinaria ?? 1)) : null,
]),
'importo_previsto' => $this->sumStraordinariaImporto($s),
'chiuso' => false,
'created_at' => now(),
'updated_at' => now(),
];
$this->dynamicInsert('lavori_straordinari', $data);
$count++;
}
return $count;
}
private function normalizeStraordinariaNatura($value): string
{
$normalized = strtoupper(trim((string) $value));
if (in_array($normalized, ['E', 'ENTRATA', 'ENTRATE'], true)) {
return 'entrata';
}
return 'spesa';
}
private function stepAddebiti(?int $limit): int
{
if (! Schema::connection('gescon_import')->hasTable('dett_pers')) {
return 0;
}
$src = DB::connection('gescon_import')->table('dett_pers as d');
if (Schema::connection('gescon_import')->hasTable('condomin')) {
$src = $src->leftJoin('condomin as c', 'c.cod_cond', '=', 'd.id_cond')
->addSelect('d.*', 'c.cod_stabile', 'c.scala', 'c.interno', 'c.cod_cond');
if ($this->option('stabile')) {
$src->where('c.cod_stabile', $this->option('stabile'));
}
}
if ($limit) {
$src->limit($limit);
}
$count = 0;
foreach ($src->get() as $d) {
$legacySeed = implode('|', [
(string) ($d->legacy_year ?? ''),
(string) ($d->id ?? ''),
(string) ($d->id_cond ?? ''),
(string) ($d->n_spe ?? ''),
(string) ($d->n_stra ?? ''),
(string) ($d->tipo_gestione ?? ''),
]);
$legacyUnsigned = sprintf('%u', crc32($legacySeed));
$legacy = (int) ($legacyUnsigned % 2147483647);
if ($legacy <= 0) {
$legacy = (int) $legacyUnsigned;
}
$exists = DB::table('addebiti_personalizzati')->where('legacy_id', $legacy)->first();
if ($exists) {
continue;
}
$unitaId = null;
if (Schema::hasTable('unita_immobiliari')) {
$unita = $this->findUnitaByStagingRow($d);
$unitaId = $unita->id ?? null;
}
$tabellaId = null;
if (Schema::hasTable('tabelle_millesimali') && ! empty($d->tabella)) {
$tabellaId = DB::table('tabelle_millesimali')
->where('stabile_id', $this->resolveStabileIdForScala($d->scala ?? null))
->where(function ($q) use ($d) {
if (Schema::hasColumn('tabelle_millesimali', 'codice_tabella')) {
$q->orWhere('codice_tabella', $d->tabella);
}
if (Schema::hasColumn('tabelle_millesimali', 'nome_tabella')) {
$q->orWhere('nome_tabella', $d->tabella);
}
})
->value('id');
}
$ruolo = null;
if (isset($d->cond_inq)) {
$ruolo = strtoupper(trim((string) $d->cond_inq));
$ruolo = ($ruolo === 'I') ? 'I' : 'P';
}
$gestioneId = null;
if (Schema::hasTable('gestioni_contabili') && Schema::hasColumn('addebiti_personalizzati', 'gestione_id')) {
$gestioneId = $this->resolveGestioneContabileIdForStabile(
$this->resolveStabileIdForScala($d->scala ?? null),
isset($d->legacy_year) ? (string) $d->legacy_year : null,
$d->tipo_gestione ?? null,
isset($d->n_stra) ? (int) $d->n_stra : null
);
}
$data = [
'legacy_id' => $legacy,
'gestione_id' => $gestioneId,
'unita_immobiliare_id' => $unitaId,
'tipo_gestione' => $d->tipo_gestione ?? null,
'numero_straordinaria' => $d->n_stra ?? 0,
'codice_spesa' => $d->n_spe ?? null,
'natura2' => $d->natura2 ?? null,
'importo' => $d->importo ?? 0,
'ruolo' => $ruolo,
'tabella_millesimale_id' => $tabellaId,
'is_unico' => ($d->unico ?? 0) ? true : false,
'meta' => json_encode([
'legacy_year' => $d->legacy_year ?? null,
'legacy_file' => $d->legacy_file ?? null,
'legacy_source_id' => $d->id ?? null,
'id_cond' => $d->id_cond ?? null,
'tabella' => $d->tabella ?? null,
'payload' => $d->payload ?? null,
]),
'created_at' => now(),
'updated_at' => now(),
];
$this->dynamicInsert('addebiti_personalizzati', $data);
$count++;
}
return $count;
}
private function stepDetrazioni(?int $limit): int
{
if (! Schema::connection('gescon_import')->hasTable('detrazioni_fiscali')) {
return 0;
}
$src = DB::connection('gescon_import')->table('detrazioni_fiscali');
if ($this->option('stabile') && Schema::connection('gescon_import')->hasColumn('detrazioni_fiscali', 'cod_stabile')) {
$src->where('cod_stabile', $this->option('stabile'));
}
if ($limit) {
$src->limit($limit);
}
$count = 0;
foreach ($src->get() as $d) {
$legacy = $d->id ?? null;
if (! $legacy) {
continue;
}
$exists = DB::table('detrazioni_fiscali_dom')->where('legacy_rif', $legacy)->first();
if ($exists) {
continue;
}
$data = [
'legacy_rif' => $legacy,
'descrizione' => $d->tipo_detrazione ?? '',
'tipologia' => $d->tipo_detrazione ?? null,
'importo' => $d->importo_euro ?? 0,
'cessione_flag' => false,
'importi_json' => json_encode([
'tot' => $d->importo_detraibile ?? null,
'bon' => null,
'non_bon' => null,
]),
'created_at' => now(),
'updated_at' => now(),
];
$this->dynamicInsert('detrazioni_fiscali_dom', $data);
$count++;
}
return $count;
}
private function stub(string $table, ?int $limit): int
{
// Placeholder: sostituire con logica di lettura da MDB / staging
$exists = DB::table($table)->count();
return $limit ? min($exists, $limit) : $exists;
}
private function preflightStaging(): void
{
try {
$stab = $this->option('stabile');
if (! $stab) {
return;
}
$legacyYearOpt = $this->option('anno') ? sprintf('%04d', (int) $this->option('anno')) : null;
$authoritativeCondomin = $this->resolveCondominMdbForStabile($stab, null);
$authoritativeLegacyYear = (string) ($authoritativeCondomin['legacy_year'] ?? '');
// Autoderiva --mdb-condomin se non fornito, preferendo l'anno richiesto e poi l'ultima annualità disponibile.
if (! $this->option('mdb-condomin')) {
$resolved = $this->resolveCondominMdbForStabile($stab, $legacyYearOpt);
if ($resolved !== null) {
try {
$this->input->setOption('mdb-condomin', $resolved['path']);
} catch (\Throwable $e) {
}
if ($this->output->isVerbose()) {
$this->line(sprintf(
' ↪︎ MDB condomin risolto da anni: %s (dir=%s, anno_o=%s, anno_r=%s)',
$resolved['path'],
(string) ($resolved['legacy_year'] ?? '-'),
(string) ($resolved['anno_o'] ?? '-'),
(string) ($resolved['anno_r'] ?? '-')
));
}
if (! empty($resolved['legacy_year'])) {
$legacyYearOpt = (string) $resolved['legacy_year'];
} elseif ($legacyYearOpt === null && ! empty($resolved['anno_o'])) {
$legacyYearOpt = (string) $resolved['anno_o'];
}
}
}
// Se manca la tabella o non ha righe per lo stabile, e abbiamo un MDB esplicito per condomin, caricalo
$hasCondTable = Schema::connection('gescon_import')->hasTable('condomin');
if (! $hasCondTable) {
return;
}
$needsLoad = $this->stagingTableNeedsLoadForStabile('condomin', $stab, $legacyYearOpt);
if ($needsLoad) {
$mdbCondomin = $this->option('mdb-condomin');
if ($mdbCondomin && is_file($mdbCondomin)) {
$this->line(" ↪︎ Precarico staging condomin da MDB per stabile {$stab}...");
// Propaga limit se presente
$args = [
'--mdb' => $mdbCondomin,
'--table' => 'condomin',
'--stabile' => $stab,
];
if ($legacyYearOpt) {
$args['--legacy-year'] = (string) $legacyYearOpt;
}
if ($this->option('limit')) {
$args['--limit'] = (string) $this->option('limit');
}
$this->callSilently('gescon:load-mdb', $args);
}
}
if ($authoritativeLegacyYear !== '' && $authoritativeLegacyYear !== $legacyYearOpt) {
$authoritativePath = (string) ($authoritativeCondomin['path'] ?? '');
if ($authoritativePath !== '' && is_file($authoritativePath)) {
foreach (['condomin', 'comproprietari'] as $table) {
if (! Schema::connection('gescon_import')->hasTable($table)) {
continue;
}
if (! $this->stagingTableNeedsLoadForStabile($table, $stab, $authoritativeLegacyYear)) {
continue;
}
$this->callSilently('gescon:load-mdb', [
'--mdb' => $authoritativePath,
'--table' => $table,
'--stabile' => $stab,
'--legacy-year' => $authoritativeLegacyYear,
]);
}
}
}
// Nota: partizionamento logico di condomin (unità, proprietari, inquilini) avviene negli step senza creare VIEW.
// Carica tabelle aggiuntive utili (millesimi, voci) se vuote per lo stabile
$mdbCondomin = $this->option('mdb-condomin');
if ($mdbCondomin && is_file($mdbCondomin)) {
// dett_tab
if (Schema::connection('gescon_import')->hasTable('dett_tab')) {
$need = $this->stagingTableNeedsLoadForStabile('dett_tab', $stab, $legacyYearOpt);
if ($need) {
$this->callSilently('gescon:load-mdb', [
'--mdb' => $mdbCondomin,
'--table' => 'dett_tab',
'--stabile' => $stab,
'--legacy-year' => $legacyYearOpt,
]);
}
}
// tabelle_millesimali
if (Schema::connection('gescon_import')->hasTable('tabelle_millesimali')) {
$need = $this->stagingTableNeedsLoadForStabile('tabelle_millesimali', $stab, $legacyYearOpt);
if ($need) {
$this->callSilently('gescon:load-mdb', [
'--mdb' => $mdbCondomin,
'--table' => 'tabelle_millesimali',
'--stabile' => $stab,
'--legacy-year' => $legacyYearOpt,
]);
}
}
// voc_spe (voci spesa)
if (Schema::connection('gescon_import')->hasTable('voc_spe')) {
$need = $this->stagingTableNeedsLoadForStabile('voc_spe', $stab, $legacyYearOpt);
if ($need) {
$this->callSilently('gescon:load-mdb', [
'--mdb' => $mdbCondomin,
'--table' => 'voc_spe',
'--stabile' => $stab,
'--legacy-year' => $legacyYearOpt,
]);
}
}
// comproprietari
if (Schema::connection('gescon_import')->hasTable('comproprietari')) {
$need = $this->stagingTableNeedsLoadForStabile('comproprietari', $stab, $legacyYearOpt);
if ($need) {
$this->callSilently('gescon:load-mdb', [
'--mdb' => $mdbCondomin,
'--table' => 'comproprietari',
'--stabile' => $stab,
'--legacy-year' => $legacyYearOpt,
]);
}
}
// Cre_Deb_preced (crediti/debiti pregressi)
if (Schema::connection('gescon_import')->hasTable('cre_deb_preced')) {
$need = $this->stagingTableNeedsLoadForStabile('cre_deb_preced', $stab, $legacyYearOpt);
if ($need) {
$this->callSilently('gescon:load-mdb', [
'--mdb' => $mdbCondomin,
'--table' => 'cre_deb_preced',
'--stabile' => $stab,
'--legacy-year' => $legacyYearOpt,
]);
}
}
// Altre tabelle gestionali principali: operazioni, rate, incassi, giroconti, straordinarie, addebiti personalizzati, detrazioni
$more = [
'operazioni',
'rate',
'incassi',
'giri_conti',
'straordinarie',
'dett_pers',
'detrazioni_fiscali',
];
foreach ($more as $tb) {
if (Schema::connection('gescon_import')->hasTable($tb)) {
$need = $this->stagingTableNeedsLoadForStabile($tb, $stab, $legacyYearOpt);
if ($need) {
$this->callSilently('gescon:load-mdb', [
'--mdb' => $mdbCondomin,
'--table' => $tb,
'--stabile' => $stab,
'--legacy-year' => $legacyYearOpt,
]);
}
}
}
}
} catch (\Throwable $e) {
// Non bloccare la pipeline se il precaricamento fallisce
if ($this->output->isVerbose()) {
$this->warn('Preflight staging fallito: ' . $e->getMessage());
}
}
}
private function resolveCondominMdbForStabile(string $stab, ?string $legacyYear = null): ?array
{
$root = rtrim((string) ($this->option('path') ?: '/mnt/gescon-archives/gescon'), '/');
$stableDir = $root . '/' . $stab;
if (! is_dir($stableDir)) {
return null;
}
if ($legacyYear) {
$preferred = $stableDir . '/' . $legacyYear . '/singolo_anno.mdb';
if (is_file($preferred)) {
return [
'path' => $preferred,
'legacy_year' => $legacyYear,
'anno_o' => ctype_digit($legacyYear) && strlen($legacyYear) === 4 ? $legacyYear : null,
'anno_r' => ctype_digit($legacyYear) && strlen($legacyYear) === 4 ? $legacyYear : null,
];
}
}
$anniRows = [];
$generale = $stableDir . '/generale_stabile.mdb';
foreach ($this->mdbExportToRows($generale, ['anni', 'Anni', 'ANNI']) as $row) {
$dir = trim((string) ($row['nome_dir'] ?? $row['nome_dir '] ?? ''));
if ($dir === '') {
continue;
}
$candidatePath = $stableDir . '/' . $dir . '/singolo_anno.mdb';
if (! is_file($candidatePath)) {
continue;
}
$annoO = trim((string) ($row['anno_o'] ?? $row['anno'] ?? $row['anno_gestione'] ?? ''));
$annoR = trim((string) ($row['anno_r'] ?? ''));
$anniRows[] = [
'path' => $candidatePath,
'legacy_year' => $dir,
'anno_o' => $annoO !== '' ? $annoO : null,
'anno_r' => $annoR !== '' ? $annoR : null,
];
}
if ($legacyYear) {
foreach ($anniRows as $candidate) {
if ($legacyYear === ($candidate['anno_o'] ?? null) || $legacyYear === ($candidate['anno_r'] ?? null)) {
return $candidate;
}
}
}
if ($anniRows !== []) {
usort($anniRows, static function (array $left, array $right): int {
$leftAnnoO = is_numeric($left['anno_o'] ?? null) ? (int) $left['anno_o'] : -1;
$rightAnnoO = is_numeric($right['anno_o'] ?? null) ? (int) $right['anno_o'] : -1;
if ($leftAnnoO !== $rightAnnoO) {
return $rightAnnoO <=> $leftAnnoO;
}
$leftAnnoR = is_numeric($left['anno_r'] ?? null) ? (int) $left['anno_r'] : -1;
$rightAnnoR = is_numeric($right['anno_r'] ?? null) ? (int) $right['anno_r'] : -1;
if ($leftAnnoR !== $rightAnnoR) {
return $rightAnnoR <=> $leftAnnoR;
}
return strcmp((string) ($right['legacy_year'] ?? ''), (string) ($left['legacy_year'] ?? ''));
});
return $anniRows[0];
}
$candidates = [];
foreach ((array) glob($stableDir . '/*/singolo_anno.mdb') as $candidate) {
if (! is_file($candidate)) {
continue;
}
$dirName = basename(dirname($candidate));
$sortKey = ctype_digit($dirName) ? (int) $dirName : -1;
$candidates[] = [
'path' => $candidate,
'legacy_year' => $dirName !== '' ? $dirName : null,
'anno_o' => null,
'anno_r' => null,
'sort_key' => $sortKey,
];
}
if ($candidates === []) {
return null;
}
usort($candidates, static function (array $left, array $right): int {
if ($left['sort_key'] === $right['sort_key']) {
return strcmp((string) $right['legacy_year'], (string) $left['legacy_year']);
}
return $right['sort_key'] <=> $left['sort_key'];
});
return Arr::except($candidates[0], ['sort_key']);
}
private function stagingTableNeedsLoadForStabile(string $table, string $stab, ?string $legacyYear = null): bool
{
if (! Schema::connection('gescon_import')->hasTable($table)) {
return false;
}
$conn = DB::connection('gescon_import');
if (Schema::connection('gescon_import')->hasColumn($table, 'cod_stabile')) {
$query = $conn->table($table)->where('cod_stabile', $stab);
if ($legacyYear !== null && $legacyYear !== '' && Schema::connection('gescon_import')->hasColumn($table, 'legacy_year')) {
$query->where('legacy_year', $legacyYear);
}
return ! $query->exists();
}
return $conn->table($table)->count() === 0;
}
private function createStabileFromMdbIfNeeded(string $mdbPath, string $codRichiesto): int
{
$col = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : (Schema::hasColumn('stabili', 'denominazione') ? 'denominazione' : null);
if (! $col) {
return 0;
}
$exists = DB::table('stabili')->where($col, $codRichiesto)->first();
if (! $exists && $col === 'codice_stabile') {
$alt = ltrim($codRichiesto, '0');
if ($alt !== '' && $alt !== $codRichiesto) {
$exists = DB::table('stabili')->where($col, $alt)->first();
}
}
if ($exists) {
return 0;
}
$row = $this->mdbStabiliFindByDirectory($mdbPath, $codRichiesto);
if (! $row) {
return 0;
}
$payload = [
$col => $codRichiesto,
'denominazione' => $row['denominazione'] ?? $codRichiesto,
'indirizzo' => $row['indirizzo'] ?? 'ND',
'citta' => $row['citta'] ?? 'ND',
'cap' => $row['cap'] ?? '00000',
'provincia' => $row['provincia'] ?? 'XX',
'codice_fiscale' => $row['cf_condominio'] ?? null,
'created_at' => now(),
'updated_at' => now(),
];
// Tag provenienza
if (Schema::hasColumn('stabili', 'note')) {
$payload['note'] = '[GESCON]';
}
// Inserisci (no-op in dry-run)
$this->dynamicInsert('stabili', $payload);
// Conta comunque come creato per feedback
if ($this->output->isVerbose()) {
$this->line(" -> creato stabile da MDB: {$codRichiesto}" . ($this->isDryRun ? ' (DRY-RUN)' : ''));
}
return 1;
}
// === Helper contestuali ===
private ?int $stabileId = null;
private int $protocolCounter = 0;
private bool $isDryRun = false;
private array $scalaStabileCache = [];
private function normalizeNameKey(?string $nome, ?string $cognome): string
{
$full = trim(strtoupper(trim((string) ($cognome ?? '')) . ' ' . trim((string) ($nome ?? ''))));
$full = preg_replace('/\s+/', ' ', $full);
$full = preg_replace('/[^A-Z0-9 ]/', '', $full);
return trim($full);
}
private function findExistingSoggetto(?string $cf, ?string $email, ?string $nome, ?string $cognome)
{
$q = DB::table('soggetti');
if ($cf) {
$found = (clone $q)->where('codice_fiscale', $cf)->first();
if ($found) {
return $found;
}
}
if ($email) {
$found = DB::table('soggetti')->where('email', $email)->first();
if ($found) {
return $found;
}
}
$key = $this->normalizeNameKey($nome, $cognome);
if ($key) {
$found = DB::table('soggetti')
->whereRaw("TRIM(UPPER(CONCAT(TRIM(COALESCE(cognome,'')),' ',TRIM(COALESCE(nome,''))))) = ?", [$key])
->first();
if ($found) {
return $found;
}
}
return null;
}
private function dedupSoggettiBy(string $field): void
{
if (! Schema::hasTable('soggetti') || ! in_array($field, Schema::getColumnListing('soggetti'))) {
return;
}
$dups = DB::table('soggetti')
->select($field, DB::raw('COUNT(*) as c'))
->whereNotNull($field)
->where($field, '!=', '')
->groupBy($field)
->having('c', '>', 1)
->get();
foreach ($dups as $d) {
$rows = DB::table('soggetti')->where($field, $d->$field)->orderBy('id')->get();
$keep = $rows->first();
foreach ($rows->skip(1) as $rm) {
// Riassegna proprieta
if (Schema::hasTable('proprieta')) {
$this->mergeProprietaForDuplicateSoggetto((int) $rm->id, (int) $keep->id);
}
// Unisci eventuali campi mancanti
$update = [];
foreach (['nome', 'cognome', 'email', 'telefono', 'indirizzo', 'codice_fiscale', 'tipo'] as $f) {
if (empty($keep->$f) && ! empty($rm->$f)) {
$update[$f] = $rm->$f;
}
}
if (! empty($update)) {
DB::table('soggetti')->where('id', $keep->id)->update($update);
}
// Elimina duplicato
DB::table('soggetti')->where('id', $rm->id)->delete();
}
}
}
private function mergeProprietaForDuplicateSoggetto(int $removeId, int $keepId): void
{
$rows = DB::table('proprieta')
->where('soggetto_id', $removeId)
->orderBy('id')
->get();
foreach ($rows as $row) {
$this->moveProprietaRowToSoggetto((int) $row->id, $keepId);
}
}
private function moveProprietaRowToSoggetto(int $rowId, int $targetSoggettoId, ?string $forcedDataInizio = null, ?string $forcedDataFine = null): void
{
$row = DB::table('proprieta')->where('id', $rowId)->first();
if (! $row) {
return;
}
$existing = DB::table('proprieta')
->where('soggetto_id', $targetSoggettoId)
->where('unita_immobiliare_id', $row->unita_immobiliare_id)
->where('tipo_diritto', $row->tipo_diritto)
->where('id', '!=', $rowId)
->first();
if ($existing) {
$patch = [];
if (empty($existing->percentuale_possesso) && ! empty($row->percentuale_possesso)) {
$patch['percentuale_possesso'] = $row->percentuale_possesso;
}
if (empty($existing->percentuale_detrazione) && ! empty($row->percentuale_detrazione)) {
$patch['percentuale_detrazione'] = $row->percentuale_detrazione;
}
$existingStart = $this->normalizeLegacyDate($existing->data_inizio ?? null);
$rowStart = $forcedDataInizio ?? $this->normalizeLegacyDate($row->data_inizio ?? null);
if ($rowStart && (! $existingStart || $rowStart < $existingStart)) {
$patch['data_inizio'] = $rowStart;
}
$existingEnd = $this->normalizeLegacyDate($existing->data_fine ?? null);
$rowEnd = $forcedDataFine ?? $this->normalizeLegacyDate($row->data_fine ?? null);
if ($rowEnd && (! $existingEnd || $rowEnd > $existingEnd)) {
$patch['data_fine'] = $rowEnd;
}
if ($patch !== []) {
$patch['updated_at'] = now();
DB::table('proprieta')->where('id', $existing->id)->update($patch);
}
DB::table('proprieta')->where('id', $rowId)->delete();
return;
}
$patch = [
'soggetto_id' => $targetSoggettoId,
'updated_at' => now(),
];
if ($forcedDataInizio !== null) {
$patch['data_inizio'] = $forcedDataInizio;
}
if ($forcedDataFine !== null) {
$patch['data_fine'] = $forcedDataFine;
}
DB::table('proprieta')->where('id', $rowId)->update($patch);
}
private function findUnitaByStagingRow($r)
{
if (! Schema::hasTable('unita_immobiliari')) {
return null;
}
$stabileId = $this->stabileId;
if ($stabileId !== null && Schema::hasColumn('unita_immobiliari', 'stabile_id')) {
$legacyCond = trim((string) ($r->cod_cond ?? $r->legacy_cond_id ?? $r->id_cond ?? ''));
if ($legacyCond !== '' && Schema::hasColumn('unita_immobiliari', 'legacy_cond_id')) {
$match = DB::table('unita_immobiliari')
->where('stabile_id', $stabileId)
->when(Schema::hasColumn('unita_immobiliari', 'deleted_at'), fn($q) => $q->whereNull('deleted_at'))
->where('legacy_cond_id', $legacyCond)
->first();
if ($match) {
return $match;
}
}
}
$crit = [];
if ($stabileId !== null && Schema::hasColumn('unita_immobiliari', 'stabile_id')) {
$crit['stabile_id'] = $stabileId;
}
if (Schema::hasColumn('unita_immobiliari', 'scala') && isset($r->scala)) {
$crit['scala'] = $r->scala;
}
if (Schema::hasColumn('unita_immobiliari', 'interno')) {
$int = isset($r->interno) ? (string) $r->interno : null;
if (is_string($int) && preg_match('/^\s*CAN\s*(\d+)\s*$/i', $int, $m)) {
$int = 'CAN' . $m[1];
}
if ($int !== null) {
$int = trim($int);
}
if ($int !== null && $int !== '' && ! preg_match('/^0+$/', $int)) {
$crit['interno'] = $int;
}
}
if (! empty($crit)) {
return DB::table('unita_immobiliari')
->where($crit)
->when(Schema::hasColumn('unita_immobiliari', 'deleted_at'), fn($q) => $q->whereNull('deleted_at'))
->first();
}
return null;
}
private function queryMdbSingle(string $mdbPath, string $sql): ?array
{
$cmd = 'mdb-sql ' . escapeshellarg($mdbPath) . ' <<< ' . escapeshellarg($sql);
$out = shell_exec($cmd);
if (! $out) {
return null;
}
$lines = array_values(array_filter(array_map('trim', explode("\n", $out))));
if (count($lines) < 3) {
return null;
}
// Try to detect header and data rows (very naive parser)
$headers = [];
$data = [];
foreach ($lines as $i => $line) {
if ($i === 0 || stripos($line, '\t') !== false) {
$headers = array_map('trim', preg_split('/\s+/', $line));
break;
}
}
// Fallback: use space-split headers might be unreliable; try tab-split on last 2 lines
$last = end($lines);
$prev = prev($lines);
if (! $headers && $prev) {
$headers = array_map('trim', preg_split('/\t+/', $prev));
$data = array_map('trim', preg_split('/\t+/', $last));
} else {
$data = array_map('trim', preg_split('/\s+/', $last));
}
if (! $headers || ! $data || count($headers) !== count($data)) {
return null;
}
return array_combine($headers, $data);
}
private function mdbStabiliFindByDirectory(string $mdbPath, string $directoryCode): ?array
{
// Robust export: pipe delimiter and quoted fields to preserve newlines within fields
$table = 'Stabili';
$tmp = tempnam(sys_get_temp_dir(), 'mdbcsv_');
$cmd = sprintf(
'mdb-export -D %s -d %s -q %s -R "\\n" %s %s > %s 2>/dev/null',
escapeshellarg('%Y-%m-%d'),
escapeshellarg('|'),
escapeshellarg('"'),
escapeshellarg($mdbPath),
escapeshellarg($table),
escapeshellarg($tmp)
);
@shell_exec($cmd);
if (! is_file($tmp) || filesize($tmp) === 0) {
@unlink($tmp);
return null;
}
$fh = fopen($tmp, 'r');
if (! $fh) {
@unlink($tmp);
return null;
}
$headers = fgetcsv($fh, 0, '|');
if (! $headers) {
fclose($fh);
@unlink($tmp);
return null;
}
$headers = array_map(fn($h) => strtolower(trim((string) $h)), $headers);
$code = trim($directoryCode);
while (($cols = fgetcsv($fh, 0, '|')) !== false) {
if ($cols === [null] || $cols === false) {
continue;
}
$countH = count($headers);
if (count($cols) < $countH) {
$cols = array_pad($cols, $countH, null);
}
if (count($cols) > $countH) {
$cols = array_slice($cols, 0, $countH);
}
$row = @array_combine($headers, $cols);
if (! $row) {
continue;
}
$nomeDir = $row['nome_directory'] ?? ($row['codice_directory'] ?? ($row['internet_cod_stab'] ?? ($row['cod_stabile'] ?? null)));
if (! $nomeDir || trim((string) $nomeDir) !== $code) {
continue;
}
// Prefer direct columns if exist
$cfCondo = $row['cf_condominio'] ?? $row['cod_fisc'] ?? null;
$cfAmm = $row['cin'] ?? $row['cod_fisc_amministratore'] ?? null;
// Heuristics over all values to fill missing
$allText = strtoupper(implode(' ', array_map(fn($v) => (string) $v, $row)));
if (! $cfCondo && preg_match('/(\d\D*){11}/', preg_replace('/\D+/', '', $allText))) {
// Extract contiguous 11 digits
if (preg_match('/\d{11}/', $allText, $m)) {
$cfCondo = $m[0];
}
}
if (! $cfAmm) {
if (preg_match('/(?=.*[A-Z])[A-Z0-9]{16}/', $allText, $m)) {
$cfAmm = $m[0];
}
}
$den = $row['denominazione'] ?? ($row['fe_denominazione'] ?? null);
$indir = $row['indirizzo'] ?? ($row['indir_autorizzazione'] ?? ($row['indir_autorizzazione_2'] ?? null));
$cap = $row['cap'] ?? null;
$citta = $row['citta'] ?? null;
$pr = $row['pr'] ?? ($row['catasto_pr'] ?? null);
fclose($fh);
@unlink($tmp);
return array_merge($row, [
'denominazione' => $den,
'indirizzo' => $indir,
'cap' => $cap,
'citta' => $citta,
'provincia' => $pr,
// Provide both when possible
'cf_condominio' => $cfCondo,
'cf_amministratore' => $cfAmm,
// Keep a generic for mapping
'codice_fiscale' => $cfCondo ?? $cfAmm,
'cin' => $cfAmm,
]);
}
fclose($fh);
@unlink($tmp);
return null;
}
private function resolveStabiliMdbPath(string $basePath = ''): ?string
{
$candidates = [];
$explicit = trim((string) ($this->option('mdb') ?? ''));
if ($explicit !== '') {
$candidates[] = $explicit;
}
$basePath = trim($basePath);
if ($basePath !== '') {
$normalizedBase = rtrim($basePath, '/');
$candidates[] = $normalizedBase . '/dbc/Stabili.mdb';
$candidates[] = $normalizedBase . '/dbc/stabili.mdb';
$candidates[] = $normalizedBase . '/Stabili.mdb';
$candidates[] = $normalizedBase . '/stabili.mdb';
}
$candidates[] = '/mnt/gescon-archives/gescon/dbc/Stabili.mdb';
$candidates[] = '/mnt/gescon-archives/gescon/Stabili.mdb';
foreach (array_values(array_unique(array_filter($candidates))) as $candidate) {
if (is_file($candidate) && is_readable($candidate)) {
return $candidate;
}
}
return $explicit !== '' ? $explicit : null;
}
private function applyLegacyStabileDerivedFields(array $update, array $legacyRow, $existing): array
{
$ordMonths = $this->extractLegacyRateMonths($legacyRow, 'ord_rata_');
$risMonths = $this->extractLegacyRateMonths($legacyRow, 'ris_rata_');
if ($ordMonths !== [] && Schema::hasColumn('stabili', 'rate_ordinarie_mesi')) {
$current = $this->decodeJsonArray($existing->rate_ordinarie_mesi ?? null);
if ($this->forceMapping || $current === []) {
$update['rate_ordinarie_mesi'] = json_encode($ordMonths);
}
}
if ($risMonths !== [] && Schema::hasColumn('stabili', 'rate_riscaldamento_mesi')) {
$current = $this->decodeJsonArray($existing->rate_riscaldamento_mesi ?? null);
if ($this->forceMapping || $current === []) {
$update['rate_riscaldamento_mesi'] = json_encode($risMonths);
}
}
$ibanBanca = $this->normalizeIbanValue($legacyRow['iban_banca'] ?? ($legacyRow['iban_principale'] ?? null));
$ibanPosta = $this->normalizeIbanValue($legacyRow['iban_posta'] ?? null);
$bancaPrincipale = trim((string) ($legacyRow['banca'] ?? ($legacyRow['denominazione_banca'] ?? '')));
$bancaSecondaria = trim((string) ($legacyRow['banca2'] ?? ''));
if ($bancaSecondaria === '' && ($ibanPosta || ! empty($legacyRow['num_ccp'] ?? null))) {
$bancaSecondaria = 'Conto postale';
}
if ($ibanBanca && Schema::hasColumn('stabili', 'iban_principale') && ($this->forceMapping || empty($existing->iban_principale))) {
$update['iban_principale'] = $ibanBanca;
}
if ($bancaPrincipale !== '' && Schema::hasColumn('stabili', 'banca_principale') && ($this->forceMapping || empty($existing->banca_principale))) {
$update['banca_principale'] = $bancaPrincipale;
}
if ($ibanPosta && Schema::hasColumn('stabili', 'iban_secondario') && ($this->forceMapping || empty($existing->iban_secondario))) {
$update['iban_secondario'] = $ibanPosta;
}
if ($bancaSecondaria !== '' && Schema::hasColumn('stabili', 'banca_secondaria') && ($this->forceMapping || empty($existing->banca_secondaria))) {
$update['banca_secondaria'] = $bancaSecondaria;
}
if (Schema::hasColumn('stabili', 'iban_condominio') && ($this->forceMapping || empty($existing->iban_condominio))) {
$ibanCondominio = $ibanBanca ?: $ibanPosta;
if ($ibanCondominio) {
$update['iban_condominio'] = $ibanCondominio;
}
}
return $update;
}
private function extractLegacyRateMonths(array $legacyRow, string $prefix): array
{
$months = [];
foreach (range(1, 12) as $month) {
$key = $prefix . $month;
if (! array_key_exists($key, $legacyRow)) {
continue;
}
$value = strtolower(trim((string) $legacyRow[$key]));
if ($value !== '' && ! in_array($value, ['0', 'false', 'no', 'off', 'n'], true)) {
$months[] = $month;
}
}
return $months;
}
private function decodeJsonArray(mixed $value): array
{
if (is_array($value)) {
return array_values(array_map('intval', $value));
}
if (! is_string($value) || trim($value) === '') {
return [];
}
$decoded = json_decode($value, true);
if (! is_array($decoded)) {
return [];
}
return array_values(array_map('intval', $decoded));
}
private function normalizeIbanValue(mixed $value): ?string
{
if (! is_string($value)) {
return null;
}
$normalized = strtoupper(preg_replace('/\s+/', '', $value));
return $normalized !== '' ? $normalized : null;
}
private function buildBancheRowsFromStabileConfig(mixed $configRaw): array
{
if (is_string($configRaw) && trim($configRaw) !== '') {
$configRaw = json_decode($configRaw, true);
}
if (! is_array($configRaw)) {
return [];
}
$banca = is_array($configRaw['banca'] ?? null) ? $configRaw['banca'] : [];
if ($banca === []) {
return [];
}
$rows = [];
$ibanBanca = $this->normalizeIbanValue($banca['iban_banca'] ?? null);
if ($ibanBanca) {
$rows[] = [
'banca' => trim((string) ($banca['banca'] ?? 'Banca condominio')),
'iban' => $ibanBanca,
'cod_cassa' => 'CCB',
'intestatario' => trim((string) ($banca['intestaz_ccp'] ?? '')),
];
}
$ibanPosta = $this->normalizeIbanValue($banca['iban_posta'] ?? null);
$numeroPosta = trim((string) ($banca['num_ccp'] ?? ''));
if ($ibanPosta || $numeroPosta !== '') {
$rows[] = [
'banca' => trim((string) ($banca['banca2'] ?? 'Conto postale')),
'iban' => $ibanPosta,
'n_conto' => $numeroPosta,
'cod_cassa' => 'CCP',
'intestatario' => trim((string) ($banca['intestaz_ccp'] ?? '')),
];
}
return $rows;
}
private function mergeLegacyBancheRows(array $rows, array $configRows): array
{
$seen = [];
foreach ($rows as $row) {
if (! is_array($row)) {
continue;
}
$iban = $this->normalizeIbanValue($row['iban'] ?? ($row['iban_banca'] ?? ($row['iban_posta'] ?? null)));
$numero = trim((string) ($row['n_conto'] ?? ($row['numero_conto'] ?? '')));
$codCassa = strtoupper(trim((string) ($row['cod_cassa'] ?? '')));
$key = $iban ?: ($numero !== '' ? 'N:' . $numero : ($codCassa !== '' ? 'C:' . $codCassa : null));
if ($key) {
$seen[$key] = true;
}
}
foreach ($configRows as $row) {
if (! is_array($row)) {
continue;
}
$iban = $this->normalizeIbanValue($row['iban'] ?? null);
$numero = trim((string) ($row['n_conto'] ?? ($row['numero_conto'] ?? '')));
$codCassa = strtoupper(trim((string) ($row['cod_cassa'] ?? '')));
$key = $iban ?: ($numero !== '' ? 'N:' . $numero : ($codCassa !== '' ? 'C:' . $codCassa : null));
if (! $key || isset($seen[$key])) {
continue;
}
$rows[] = $row;
$seen[$key] = true;
}
return $rows;
}
private function resolveStabileId(): ?int
{
$cod = $this->option('stabile');
if (! $cod) {
return null;
}
if (! Schema::hasTable('stabili')) {
return null;
}
$col = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : (Schema::hasColumn('stabili', 'denominazione') ? 'denominazione' : null);
if (! $col) {
return null;
}
$row = DB::table('stabili')->where($col, $cod)->first();
if (! $row && $col === 'codice_stabile') {
$alt = ltrim($cod, '0');
if ($alt !== '' && $alt !== $cod) {
$row = DB::table('stabili')->where($col, $alt)->first();
}
}
// Se esiste ma è richiesto un amministratore specifico, aggiorna l'owner (solo se cambia)
if ($row) {
$optAdm = $this->option('amministratore-id');
if ($optAdm && Schema::hasColumn('stabili', 'amministratore_id')) {
$target = (int) $optAdm;
if ((int) ($row->amministratore_id ?? 0) !== $target && ! $this->option('dry-run')) {
DB::table('stabili')->where('id', $row->id)->update([
'amministratore_id' => $target,
'updated_at' => now(),
]);
}
}
return $row->id;
}
// In dry-run do not create anything, just return null and let steps continue.
if ($this->option('dry-run')) {
return null;
}
// Ensure minimal amministratore if schema requires it
$amminId = null;
if (Schema::hasColumn('stabili', 'amministratore_id')) {
// Preferisci un amministratore passato via opzione
$optAdm = $this->option('amministratore-id');
if ($optAdm && DB::table('amministratori')->where('id', (int) $optAdm)->exists()) {
$amminId = (int) $optAdm;
} else {
$ammin = DB::table('amministratori')->first();
if (! $ammin) {
// Ensure there is at least one user
$user = DB::table('users')->first();
if (! $user) {
$userId = DB::table('users')->insertGetId([
'name' => 'Admin Seed',
'email' => 'admin-seed@example.local',
'password' => bcrypt('password'),
'created_at' => now(),
'updated_at' => now(),
]);
} else {
$userId = $user->id;
}
$amminId = DB::table('amministratori')->insertGetId([
'nome' => 'Admin',
'cognome' => 'Seed',
'user_id' => $userId,
'denominazione_studio' => 'Seed Studio',
'codice_univoco' => $this->generateCode('A', 8),
'created_at' => now(),
'updated_at' => now(),
]);
} else {
$amminId = $ammin->id;
}
}
}
// Create minimal stabile row compatible with either schema
$data = [
$col => $cod,
'denominazione' => $cod,
'indirizzo' => 'ND',
'citta' => 'ND',
'cap' => '00000',
'provincia' => 'XX',
'attivo' => true,
'amministratore_id' => $amminId,
'created_at' => now(),
'updated_at' => now(),
];
$this->dynamicInsert('stabili', $data);
$row = DB::table('stabili')->where($col, $cod)->first();
return $row?->id;
}
private function generateCode(string $prefix, int $len = 6): string
{
return $prefix . str_pad((string) random_int(0, 10 ** $len - 1), $len, '0', STR_PAD_LEFT);
}
private function generateStableCondKey(string $prefix, $stabile, $cond, int $len = 8): string
{
$len = max(1, $len);
$prefix = (string) $prefix;
$usable = max(1, $len - strlen($prefix));
$raw = (string) $stabile . '|' . (string) $cond;
// Use CRC32B for a short, deterministic hex digest; uppercase for consistency
$digest = strtoupper(substr(hash('crc32b', $raw), 0, $usable));
// Ensure exact length
if (strlen($digest) < $usable) {
$digest = str_pad($digest, $usable, '0');
}
return $prefix . $digest;
}
private function buildLegacyUnitCode(string $stabileCode, string $scala, string $interno, ?string $salt = null): string
{
$maxLen = 20;
$stablePart = $this->sanitizeUnitCodeSegment($stabileCode, 4) ?: 'S0';
$scalaPart = $this->sanitizeUnitCodeSegment($scala, 4) ?: 'A';
$internoPart = $this->sanitizeUnitCodeSegment($interno, 32) ?: 'INT';
$base = $stablePart . '-' . $scalaPart . '-' . $internoPart;
if (strlen($base) <= $maxLen) {
return $base;
}
$hash = strtoupper(substr(hash('crc32b', implode('|', [$stabileCode, $scala, $interno, (string) $salt])), 0, 4));
$reserved = strlen($stablePart) + strlen($scalaPart) + strlen($hash) + 3;
$availableInterno = max(1, $maxLen - $reserved);
$internoCompact = substr($internoPart, 0, $availableInterno);
return $stablePart . '-' . $scalaPart . '-' . $internoCompact . '-' . $hash;
}
private function sanitizeUnitCodeSegment(?string $value, int $maxLen): string
{
$normalized = strtoupper(trim((string) $value));
$normalized = preg_replace('/[^A-Z0-9]+/u', '', $normalized) ?? '';
return substr($normalized, 0, max(1, $maxLen));
}
private function nextProtocolNumber(): int
{
return ++$this->protocolCounter;
}
private function safeOldId($old)
{
if (! $old) {
return null;
}
// If already used by another soggetto, skip to avoid unique violation
$exists = Schema::hasTable('soggetti') ? DB::table('soggetti')->where('old_id', $old)->exists() : false;
return $exists ? null : $old;
}
private function resolveAnnoFromLegacyYear(?string $legacyYear): ?string
{
$legacyYear = is_string($legacyYear) ? trim($legacyYear) : '';
if ($legacyYear === '') {
return null;
}
if (preg_match('/^\d{4}$/', $legacyYear) && (int) $legacyYear >= 1900) {
return $legacyYear;
}
if (preg_match('/(19|20)\d{2}/', $legacyYear, $match)) {
return $match[0];
}
if (! $this->legacyYearMapLoaded) {
$this->legacyYearMapLoaded = true;
$this->legacyYearMap = [];
$stabileCode = (string) ($this->option('stabile') ?? '');
$root = rtrim((string) ($this->option('path') ?: '/mnt/gescon-archives/gescon'), '/');
if ($stabileCode !== '') {
$generale = $root . '/' . $stabileCode . '/generale_stabile.mdb';
$rows = $this->mdbExportToRows($generale, ['anni', 'Anni', 'ANNI']);
foreach ($rows as $row) {
$dir = trim((string) ($row['nome_dir'] ?? $row['nome_dir '] ?? ''));
$anno = trim((string) ($row['anno_o'] ?? $row['anno'] ?? $row['anno_gestione'] ?? ''));
if ($dir !== '' && $anno !== '') {
if (preg_match('/(19|20)\d{2}/', $anno, $match)) {
$anno = $match[0];
}
$this->legacyYearMap[$dir] = $anno;
}
}
}
}
return $this->legacyYearMap[$legacyYear] ?? null;
}
private function normalizeLegacyDate(?string $value): ?string
{
if (! is_string($value)) {
return null;
}
$value = trim($value);
if ($value === '') {
return null;
}
$ts = strtotime($value);
if ($ts === false) {
return null;
}
return date('Y-m-d', $ts);
}
private function resolveLegacySnapshotYear(?string $legacyYear, ?object $row = null): ?int
{
$resolved = $this->resolveAnnoFromLegacyYear($legacyYear);
if ($resolved !== null && is_numeric($resolved)) {
return (int) $resolved;
}
if ($row) {
foreach (['inizio_anno', 'anno_gestione', 'anno'] as $field) {
$value = trim((string) ($row->{$field} ?? ''));
if ($value !== '' && preg_match('/\d{4}/', $value, $m)) {
return (int) $m[0];
}
}
foreach (['subentrato_dal', 'attivo_fino_al', 'inquil_dal', 'inquil_al'] as $field) {
$normalized = $this->normalizeLegacyDate($row->{$field} ?? null);
if ($normalized) {
return (int) substr($normalized, 0, 4);
}
}
}
$optAnno = trim((string) ($this->option('anno') ?? ''));
if ($optAnno !== '' && is_numeric($optAnno)) {
return (int) $optAnno;
}
return null;
}
private function startOfYear(?int $year): ?string
{
return $year ? sprintf('%04d-01-01', $year) : null;
}
private function previousDay(?string $date): ?string
{
if (! $date) {
return null;
}
$ts = strtotime($date . ' -1 day');
return $ts !== false ? date('Y-m-d', $ts) : null;
}
private function normalizeLegacyPartyName(?string $value): string
{
$value = Str::upper(trim((string) $value));
$value = preg_replace('/\s+/u', ' ', $value ?? '');
return trim((string) ($value ?? ''));
}
private function extractLegacySnapshotParty(object $row, string $role, ?string $defaultLegacyCondId = null): array
{
$legacyYear = isset($row->legacy_year) ? trim((string) $row->legacy_year) : null;
$snapshotYear = $this->resolveLegacySnapshotYear($legacyYear, $row);
if ($role === 'C') {
$name = trim((string) ($row->proprietario_denominazione ?? $row->nom_cond ?? ''));
if ($name === '') {
$name = trim((string) (($row->cognome ?? '') . ' ' . ($row->nome ?? '')));
}
return [
'ruolo' => 'C',
'legacy_year' => $legacyYear,
'snapshot_year' => $snapshotYear,
'legacy_cond_id' => trim((string) ($row->cod_cond ?? $defaultLegacyCondId ?? '')) ?: $defaultLegacyCondId,
'nominativo' => $name !== '' ? $name : null,
'data_inizio' => $this->normalizeLegacyDate($row->proprietario_subentrato_dal ?? $row->subentrato_dal ?? null),
'data_fine' => $this->normalizeLegacyDate($row->proprietario_attivo_fino_al ?? $row->attivo_fino_al ?? null),
'percentuale' => isset($row->proprietario_diritto_percentuale) && is_numeric($row->proprietario_diritto_percentuale)
? (float) $row->proprietario_diritto_percentuale
: (isset($row->perc_diritto_reale) && is_numeric($row->perc_diritto_reale) ? (float) $row->perc_diritto_reale : null),
];
}
$tenantName = trim((string) ($row->inquilino_denominazione ?? $row->inquil_nome ?? $row->inquil ?? ''));
return [
'ruolo' => 'I',
'legacy_year' => $legacyYear,
'snapshot_year' => $snapshotYear,
'legacy_cond_id' => trim((string) ($row->cod_cond ?? $defaultLegacyCondId ?? '')) ?: $defaultLegacyCondId,
'nominativo' => $tenantName !== '' ? $tenantName : null,
'data_inizio' => $this->normalizeLegacyDate($row->inquil_dal ?? $row->inquilino_subentrato_dal ?? null),
'data_fine' => $this->normalizeLegacyDate($row->inquil_al ?? $row->inquilino_attivo_fino_al ?? null),
'percentuale' => isset($row->inquilino_diritto_percentuale) && is_numeric($row->inquilino_diritto_percentuale)
? (float) $row->inquilino_diritto_percentuale
: null,
];
}
private function loadLegacyCondominHistoryRows(object $row, ?string $legacyCondId): array
{
$codStabile = trim((string) ($row->cod_stabile ?? ''));
$codCond = trim((string) ($row->cod_cond ?? $legacyCondId ?? ''));
if ($codStabile === '' || $codCond === '') {
return [$row];
}
$historyRows = DB::connection('gescon_import')
->table('condomin')
->where('cod_stabile', $codStabile)
->where('cod_cond', $codCond)
->orderByDesc('legacy_year')
->orderByDesc('id')
->get()
->all();
if ($historyRows === []) {
return [$row];
}
$byYear = [];
foreach ($historyRows as $historyRow) {
$snapshotYear = $this->resolveLegacySnapshotYear(isset($historyRow->legacy_year) ? (string) $historyRow->legacy_year : null, $historyRow);
$bucket = $snapshotYear ? (string) $snapshotYear : ('legacy:' . (string) ($historyRow->legacy_year ?? '0'));
if (! isset($byYear[$bucket])) {
$byYear[$bucket] = $historyRow;
}
}
$historyRows = array_values($byYear);
usort($historyRows, function (object $a, object $b): int {
$yearA = $this->resolveLegacySnapshotYear(isset($a->legacy_year) ? (string) $a->legacy_year : null, $a) ?? 0;
$yearB = $this->resolveLegacySnapshotYear(isset($b->legacy_year) ? (string) $b->legacy_year : null, $b) ?? 0;
if ($yearA !== $yearB) {
return $yearB <=> $yearA;
}
return ((int) ($b->id ?? 0)) <=> ((int) ($a->id ?? 0));
});
return $historyRows;
}
private function buildRoleHistoryPayloads(array $historyRows, string $role, ?string $defaultLegacyCondId = null): array
{
$snapshots = [];
foreach ($historyRows as $historyRow) {
$snapshot = $this->extractLegacySnapshotParty($historyRow, $role, $defaultLegacyCondId);
if (! $snapshot['nominativo'] && ! $snapshot['data_inizio'] && ! $snapshot['data_fine']) {
continue;
}
$snapshots[] = $snapshot;
}
if ($snapshots === []) {
return [];
}
$payloads = [];
$current = null;
foreach ($snapshots as $snapshot) {
$snapshotNameKey = $this->normalizeLegacyPartyName($snapshot['nominativo'] ?? null);
$snapshotStart = $snapshot['data_inizio'] ?? $this->startOfYear($snapshot['snapshot_year']);
$snapshotEnd = $snapshot['data_fine'];
if ($current === null) {
$current = [
'ruolo' => $role,
'nominativo' => $snapshot['nominativo'],
'data_inizio' => $snapshotStart,
'data_fine' => $snapshotEnd,
'percentuale' => $snapshot['percentuale'],
'legacy_cond_id' => $snapshot['legacy_cond_id'] ?? $defaultLegacyCondId,
'legacy_years' => array_values(array_filter([$snapshot['legacy_year']])),
];
continue;
}
$currentNameKey = $this->normalizeLegacyPartyName($current['nominativo'] ?? null);
$sameName = $snapshotNameKey !== '' && $snapshotNameKey === $currentNameKey;
$samePercent = $snapshot['percentuale'] === null || $current['percentuale'] === null
|| abs((float) $snapshot['percentuale'] - (float) $current['percentuale']) < 0.0005;
if ($sameName && $samePercent) {
$current['data_inizio'] = $snapshot['data_inizio'] ?? $this->startOfYear($snapshot['snapshot_year']) ?? $current['data_inizio'];
$current['percentuale'] = $current['percentuale'] ?? $snapshot['percentuale'];
if (! empty($snapshot['legacy_year'])) {
$current['legacy_years'][] = $snapshot['legacy_year'];
}
continue;
}
$payloads[] = $current;
$current = [
'ruolo' => $role,
'nominativo' => $snapshot['nominativo'],
'data_inizio' => $snapshotStart,
'data_fine' => $snapshotEnd ?: $this->previousDay($current['data_inizio']),
'percentuale' => $snapshot['percentuale'],
'legacy_cond_id' => $snapshot['legacy_cond_id'] ?? $defaultLegacyCondId,
'legacy_years' => array_values(array_filter([$snapshot['legacy_year']])),
];
}
if ($current !== null) {
$payloads[] = $current;
}
return $payloads;
}
private function loadLegacyComproprietariHistoryRows(object $row, ?string $legacyCondId): array
{
if (! Schema::connection('gescon_import')->hasTable('comproprietari')) {
return [];
}
$codStabile = trim((string) ($row->cod_stabile ?? ''));
$idCondRaw = trim((string) ($legacyCondId ?? $row->cod_cond ?? ''));
if ($codStabile === '' || $idCondRaw === '') {
return [];
}
$query = DB::connection('gescon_import')
->table('comproprietari')
->where('cod_stabile', $codStabile)
->orderByDesc('legacy_year')
->orderByDesc('id');
if (is_numeric($idCondRaw)) {
$query->where('id_cond', (int) $idCondRaw);
} else {
$query->where('id_cond', $idCondRaw);
}
$historyRows = $query->get()->all();
if ($historyRows === []) {
return [];
}
$byYear = [];
foreach ($historyRows as $historyRow) {
$snapshotYear = $this->resolveLegacySnapshotYear(isset($historyRow->legacy_year) ? (string) $historyRow->legacy_year : null, $historyRow);
$bucket = $snapshotYear ? (string) $snapshotYear : ('legacy:' . (string) ($historyRow->legacy_year ?? '0'));
$nameKey = $this->normalizeLegacyPartyName($historyRow->nom_cond ?? null);
$rightKey = $this->normalizeLegacyPartyName($historyRow->diritto_reale ?? $historyRow->descriz ?? null);
$groupKey = $bucket . '|' . $nameKey . '|' . $rightKey;
if (! isset($byYear[$groupKey])) {
$byYear[$groupKey] = $historyRow;
}
}
$historyRows = array_values($byYear);
usort($historyRows, function (object $a, object $b): int {
$yearA = $this->resolveLegacySnapshotYear(isset($a->legacy_year) ? (string) $a->legacy_year : null, $a) ?? 0;
$yearB = $this->resolveLegacySnapshotYear(isset($b->legacy_year) ? (string) $b->legacy_year : null, $b) ?? 0;
if ($yearA !== $yearB) {
return $yearB <=> $yearA;
}
return ((int) ($b->id ?? 0)) <=> ((int) ($a->id ?? 0));
});
return $historyRows;
}
private function buildComproprietariHistoryPayloads(array $historyRows, ?string $defaultLegacyCondId = null): array
{
if ($historyRows === []) {
return [];
}
$snapshots = [];
foreach ($historyRows as $historyRow) {
$legacyYear = isset($historyRow->legacy_year) ? trim((string) $historyRow->legacy_year) : null;
$snapshotYear = $this->resolveLegacySnapshotYear($legacyYear, $historyRow);
$name = trim((string) ($historyRow->nom_cond ?? ''));
$rightCode = trim((string) ($historyRow->diritto_reale ?? ''));
$rightLabel = trim((string) ($historyRow->descriz ?? '')) ?: ($rightCode !== '' ? $rightCode : 'Comproprietario');
$percentuale = isset($historyRow->perc_diritto_reale) && is_numeric($historyRow->perc_diritto_reale)
? (float) $historyRow->perc_diritto_reale
: null;
if ($name === '') {
continue;
}
$snapshots[] = [
'ruolo' => 'C',
'legacy_year' => $legacyYear,
'snapshot_year' => $snapshotYear,
'legacy_cond_id' => trim((string) ($historyRow->id_cond ?? $defaultLegacyCondId ?? '')) ?: $defaultLegacyCondId,
'nominativo' => $name,
'data_inizio' => $this->startOfYear($snapshotYear),
'data_fine' => null,
'percentuale' => $percentuale,
'diritto_reale' => $rightCode !== '' ? $rightCode : null,
'diritto_label' => $rightLabel,
'id_compr' => isset($historyRow->id_compr) ? (int) $historyRow->id_compr : null,
];
}
if ($snapshots === []) {
return [];
}
$payloads = [];
$current = null;
foreach ($snapshots as $snapshot) {
$snapshotNameKey = $this->normalizeLegacyPartyName($snapshot['nominativo'] ?? null);
$snapshotRightKey = $this->normalizeLegacyPartyName($snapshot['diritto_reale'] ?? $snapshot['diritto_label'] ?? null);
$snapshotStart = $snapshot['data_inizio'] ?? $this->startOfYear($snapshot['snapshot_year']);
if ($current === null) {
$current = $snapshot + [
'legacy_years' => array_values(array_filter([$snapshot['legacy_year']])),
];
continue;
}
$currentNameKey = $this->normalizeLegacyPartyName($current['nominativo'] ?? null);
$currentRightKey = $this->normalizeLegacyPartyName($current['diritto_reale'] ?? $current['diritto_label'] ?? null);
$sameSeries = ! empty($snapshot['id_compr']) && ! empty($current['id_compr'])
&& (int) $snapshot['id_compr'] === (int) $current['id_compr'];
$sameName = $snapshotNameKey !== '' && $snapshotNameKey === $currentNameKey;
$sameRight = $snapshotRightKey === $currentRightKey;
$samePercent = $snapshot['percentuale'] === null || $current['percentuale'] === null
|| abs((float) $snapshot['percentuale'] - (float) $current['percentuale']) < 0.0005;
if (($sameName && $sameRight && $samePercent) || ($sameSeries && $sameRight)) {
$current['data_inizio'] = $snapshotStart ?? $current['data_inizio'];
$current['percentuale'] = $current['percentuale'] ?? $snapshot['percentuale'];
if (! empty($snapshot['legacy_year'])) {
$current['legacy_years'][] = $snapshot['legacy_year'];
}
continue;
}
$payloads[] = $current;
$current = $snapshot + [
'data_fine' => $this->previousDay($current['data_inizio'] ?? null),
'legacy_years' => array_values(array_filter([$snapshot['legacy_year']])),
];
}
if ($current !== null) {
$payloads[] = $current;
}
return $payloads;
}
private function upsertUnitaNominativiFromLegacy(int $unitaId, int $stabileId, ?string $legacyCondId, object $row): void
{
if (! Schema::hasTable('unita_immobiliare_nominativi')) {
return;
}
$historyRowsCondomin = $this->loadLegacyCondominHistoryRows($row, $legacyCondId);
$historyRowsComproprietari = $this->loadLegacyComproprietariHistoryRows($row, $legacyCondId);
$payloadsCondomin = array_merge(
$this->buildRoleHistoryPayloads($historyRowsCondomin, 'C', $legacyCondId),
$this->buildRoleHistoryPayloads($historyRowsCondomin, 'I', $legacyCondId)
);
$payloadsComproprietari = $this->buildComproprietariHistoryPayloads($historyRowsComproprietari, $legacyCondId);
DB::table('unita_immobiliare_nominativi')
->where('unita_immobiliare_id', $unitaId)
->where('stabile_id', $stabileId)
->whereIn('fonte', ['legacy_condomin', 'legacy_comproprietari'])
->delete();
foreach ($payloadsCondomin as $payload) {
DB::table('unita_immobiliare_nominativi')->insert([
'unita_immobiliare_id' => $unitaId,
'stabile_id' => $stabileId,
'legacy_cond_id' => $payload['legacy_cond_id'] ?? $legacyCondId,
'ruolo' => $payload['ruolo'],
'nominativo' => $payload['nominativo'],
'data_inizio' => $payload['data_inizio'],
'data_fine' => $payload['data_fine'],
'percentuale' => $payload['percentuale'],
'fonte' => 'legacy_condomin',
'legacy_payload' => json_encode([
'cod_cond' => $payload['legacy_cond_id'] ?? $legacyCondId,
'legacy_years' => array_values(array_unique($payload['legacy_years'] ?? [])),
'history_mode' => 'latest-year-backfill',
]),
'created_at' => now(),
'updated_at' => now(),
]);
}
foreach ($payloadsComproprietari as $payload) {
DB::table('unita_immobiliare_nominativi')->insert([
'unita_immobiliare_id' => $unitaId,
'stabile_id' => $stabileId,
'legacy_cond_id' => $payload['legacy_cond_id'] ?? $legacyCondId,
'ruolo' => 'C',
'nominativo' => $payload['nominativo'],
'data_inizio' => $payload['data_inizio'],
'data_fine' => $payload['data_fine'],
'percentuale' => $payload['percentuale'],
'fonte' => 'legacy_comproprietari',
'legacy_payload' => json_encode([
'cod_cond' => $payload['legacy_cond_id'] ?? $legacyCondId,
'id_compr' => $payload['id_compr'] ?? null,
'diritto_reale' => $payload['diritto_reale'] ?? null,
'diritto_label' => $payload['diritto_label'] ?? null,
'legacy_years' => array_values(array_unique($payload['legacy_years'] ?? [])),
'history_mode' => 'comproprietari-series',
]),
'created_at' => now(),
'updated_at' => now(),
]);
}
}
private function normalizeLegacyCalcolo(?string $raw): ?string
{
$v = strtolower(trim((string) ($raw ?? '')));
if ($v === '') {
return null;
}
return match ($v) {
'm' => 'millesimi',
'a', 'c' => 'a_consumo',
'x' => 'conguagli',
'p' => 'personali',
default => $v,
};
}
private function normalizeLegacyTipo(?string $raw): ?string
{
$v = strtoupper(trim((string) ($raw ?? '')));
if ($v === '') {
return null;
}
return match ($v) {
'O', 'ORD', 'ORDINARIA' => 'O',
'R', 'RISC', 'RISCALDAMENTO' => 'R',
'S', 'STRA', 'STRAORDINARIA' => 'S',
default => $v,
};
}
private function resolveTipoTabellaFromLegacy(?string $tipoLegacy, ?string $calcolo, ?string $codice): ?string
{
$c = strtolower(trim((string) ($calcolo ?? '')));
$code = strtoupper(trim((string) ($codice ?? '')));
if ($code === 'ACQUA' || in_array($c, ['a_consumo', 'acqua', 'consumo', 'c', 'a'], true)) {
return 'custom';
}
return match ($tipoLegacy) {
'R' => 'riscaldamento',
default => 'custom',
};
}
private function resolveGestioneContabileIdForStabile(?int $stabileId, ?string $legacyYear, ?string $tipoLegacy, ?int $numeroStraordinaria = null): ?int
{
if (! Schema::hasTable('gestioni_contabili')) {
return null;
}
$anno = $this->resolveAnnoFromLegacyYear($legacyYear) ?: (is_string($this->option('anno')) ? (string) $this->option('anno') : null);
if (! $anno || ! is_numeric($anno)) {
return null;
}
$tipoLegacy = is_string($tipoLegacy) ? strtoupper(trim($tipoLegacy)) : null;
$tipo = match ($tipoLegacy) {
'R', 'RISCALDAMENTO' => 'riscaldamento',
'S', 'STRAORDINARIA' => 'straordinaria',
default => 'ordinaria',
};
$q = DB::table('gestioni_contabili')
->where('anno_gestione', (int) $anno)
->where('tipo_gestione', $tipo);
if ($stabileId && Schema::hasColumn('gestioni_contabili', 'stabile_id')) {
$q->where('stabile_id', $stabileId);
}
if ($tipo === 'straordinaria' && $numeroStraordinaria !== null && Schema::hasColumn('gestioni_contabili', 'numero_straordinaria')) {
$q->where('numero_straordinaria', $numeroStraordinaria);
}
$match = $q->orderByDesc('gestione_attiva')->orderByDesc('id')->first();
if ($match) {
return $match->id;
}
if ($this->option('dry-run')) {
return null;
}
$payload = [
'tenant_id' => 'T1',
'stabile_id' => $stabileId,
'anno_gestione' => (int) $anno,
'tipo_gestione' => $tipo,
'numero_straordinaria' => $tipo === 'straordinaria' ? ($numeroStraordinaria ?? 1) : null,
'denominazione' => strtoupper(substr($tipo, 0, 1)) . $anno,
'data_inizio' => date('Y-01-01', strtotime($anno . '-01-01')),
'data_fine' => date('Y-12-31', strtotime($anno . '-12-31')),
'stato' => 'aperta',
'protocollo_prefix' => strtoupper(substr($tipo, 0, 1)) . $anno . ($tipo === 'straordinaria' ? '-' . str_pad((string) ($numeroStraordinaria ?? 1), 2, '0', STR_PAD_LEFT) : ''),
'ultimo_protocollo' => 0,
'created_at' => now(),
'updated_at' => now(),
];
$cols = Schema::getColumnListing('gestioni_contabili');
$insert = array_intersect_key($payload, array_flip($cols));
DB::table('gestioni_contabili')->insert($insert);
$match = DB::table('gestioni_contabili')
->where('anno_gestione', (int) $anno)
->where('tipo_gestione', $tipo)
->when($stabileId && Schema::hasColumn('gestioni_contabili', 'stabile_id'), fn($qq) => $qq->where('stabile_id', $stabileId))
->orderByDesc('id')
->first();
return $match?->id;
}
private function resolveGestioneId($anno, $compet, $tipo): ?int
{
if (! Schema::hasTable('gestioni_contabili')) {
return null;
}
// Normalize tipo from O/R/S to descriptive if needed
$map = ['O' => 'ordinaria', 'R' => 'riscaldamento', 'S' => 'straordinaria'];
$tipo = $map[$tipo ?? 'O'] ?? ($tipo ?: 'ordinaria');
$anno = $anno ?: date('Y');
$row = DB::table('gestioni_contabili')
->where(['anno_gestione' => (int) $anno, 'tipo_gestione' => $tipo])
->first();
if ($row) {
return $row->id;
}
// In dry-run, avoid creating missing gestione
if ($this->option('dry-run')) {
return null;
}
// Create minimal compatible record satisfying not-null constraints
$data = [
'tenant_id' => 'T1',
'anno_gestione' => (int) $anno,
'tipo_gestione' => $tipo,
'numero_straordinaria' => null,
'denominazione' => strtoupper(substr($tipo, 0, 1)) . $anno,
'data_inizio' => date('Y-01-01', strtotime($anno . '-01-01')),
'data_fine' => date('Y-12-31', strtotime($anno . '-12-31')),
'stato' => 'aperta',
'protocollo_prefix' => strtoupper(substr($tipo, 0, 1)) . $anno,
'ultimo_protocollo' => 0,
'created_at' => now(),
'updated_at' => now(),
];
DB::table('gestioni_contabili')->insert($data);
$row = DB::table('gestioni_contabili')->where(['anno_gestione' => (int) $anno, 'tipo_gestione' => $tipo])->first();
return $row?->id;
}
private function dynamicInsert(string $table, array $data): void
{
if (! Schema::hasTable($table)) {
return;
}
// silently skip
if ($this->isDryRun) {
return;
}
// skip writes in dry-run
$cols = Schema::getColumnListing($table);
$filtered = array_intersect_key($data, array_flip($cols));
if (! isset($filtered['created_at']) && in_array('created_at', $cols)) {
$filtered['created_at'] = now();
}
if (! isset($filtered['updated_at']) && in_array('updated_at', $cols)) {
$filtered['updated_at'] = now();
}
if (! empty($filtered)) {
DB::table($table)->insert($filtered);
}
}
/**
* Risolve l'ID del conto bancario di default per uno stabile.
* Preferisce il conto con IBAN che coincide con l'IBAN dello stabile,
* altrimenti il primo conto attivo per lo stabile.
*/
private function resolveDefaultContoBancarioId(?int $stabileId): ?int
{
if (! $stabileId || ! Schema::hasTable('dati_bancari')) {
return null;
}
if (isset($this->defaultContoCache[$stabileId])) {
return $this->defaultContoCache[$stabileId];
}
// 1) Prova match per IBAN tra stabile e dati_bancari
$ibanStabile = $this->getIbanForStabile($stabileId);
if ($ibanStabile) {
$row = DB::table('dati_bancari')
->where('stabile_id', $stabileId)
->whereRaw('REPLACE(UPPER(iban)," ","") = ?', [$ibanStabile])
->orderByDesc('updated_at')
->first();
if ($row) {
return $this->defaultContoCache[$stabileId] = (int) $row->id;
}
}
// 2) Fallback: primo conto attivo per stabile
$row = DB::table('dati_bancari')
->where('stabile_id', $stabileId)
->when(Schema::hasColumn('dati_bancari', 'stato_conto'), function ($q) {
$q->where('stato_conto', 'attivo');
})
->orderByDesc('updated_at')
->first();
if ($row) {
return $this->defaultContoCache[$stabileId] = (int) $row->id;
}
// 3) Ultimo fallback: qualsiasi conto dello stabile
$row = DB::table('dati_bancari')
->where('stabile_id', $stabileId)
->orderByDesc('updated_at')
->first();
return $this->defaultContoCache[$stabileId] = ($row ? (int) $row->id : null);
}
/**
* Recupera IBAN normalizzato per lo stabile dalle colonne disponibili.
*/
private function getIbanForStabile(?int $stabileId): ?string
{
if (! $stabileId || ! Schema::hasTable('stabili')) {
return null;
}
$cols = Schema::getColumnListing('stabili');
$candidate = null;
foreach (['iban_condominio', 'iban_principale', 'iban'] as $col) {
if (in_array($col, $cols, true)) {
$val = DB::table('stabili')->where('id', $stabileId)->value($col);
if ($val) {
$candidate = $val;
break;
}
}
}
if (! $candidate) {
return null;
}
$iban = strtoupper(preg_replace('/\s+/', '', (string) $candidate));
return $iban ?: null;
}
private function ensureStabilePerScala($parentStabile, string $scala): array
{
// Ritorna [childId, childCode]
if (! Schema::hasTable('stabili')) {
return [null, null];
}
$col = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione';
$parentCode = $col === 'codice_stabile' ? ($parentStabile->codice_stabile ?? null) : ($parentStabile->denominazione ?? null);
if (! $parentCode) {
return [$parentStabile->id ?? null, $parentCode];
}
$childCode = rtrim($parentCode, '-') . '-' . strtoupper($scala ?: 'A');
if (isset($this->scalaStabileCache[$childCode])) {
return $this->scalaStabileCache[$childCode];
}
$child = DB::table('stabili')->where($col, $childCode)->first();
if (! $child && ! $this->isDryRun) {
// Crea minimale ereditando alcuni campi
$payload = [
$col => $childCode,
'denominazione' => ($parentStabile->denominazione ?? $childCode) . ' - Scala ' . strtoupper($scala ?: 'A'),
'indirizzo' => $parentStabile->indirizzo ?? 'ND',
'citta' => $parentStabile->citta ?? 'ND',
'cap' => $parentStabile->cap ?? '00000',
'provincia' => $parentStabile->provincia ?? 'XX',
'amministratore_id' => $parentStabile->amministratore_id ?? null,
'attivo' => true,
'created_at' => now(),
'updated_at' => now(),
];
if (Schema::hasColumn('stabili', 'note')) {
$payload['note'] = trim((string) ($parentStabile->note ?? '')) . ' [GESCON SCALA]';
}
$this->dynamicInsert('stabili', $payload);
$child = DB::table('stabili')->where($col, $childCode)->first();
}
$res = [$child->id ?? null, $childCode];
$this->scalaStabileCache[$childCode] = $res;
return $res;
}
private function resolveStabileIdForScala(?string $scala): ?int
{
if ($this->splitMode !== 'stabili-per-scala') {
return $this->stabileId;
}
$scala = trim((string) ($scala ?? 'A')) ?: 'A';
if (! Schema::hasTable('stabili')) {
return $this->stabileId;
}
$col = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione';
$parentCode = $this->option('stabile');
if (! $parentCode) {
return $this->stabileId;
}
$childCode = rtrim($parentCode, '-') . '-' . strtoupper($scala);
$row = DB::table('stabili')->where($col, $childCode)->first();
if ($row) {
return $row->id;
}
return $this->stabileId;
}
private function safeUpdateStabile(int $id, array $update): void
{
if (empty($update)) {
return;
}
if ($this->isDryRun) {
if ($this->output->isVerbose()) {
$this->line(" (dry-run) update stabili skip for id {$id}");
}
return;
}
// Gestisci unicità codice_fiscale con riassegnazione se --force-mapping
if (isset($update['codice_fiscale']) && Schema::hasColumn('stabili', 'codice_fiscale')) {
$cf = trim((string) $update['codice_fiscale']);
if ($cf !== '') {
$other = DB::table('stabili')->where('codice_fiscale', $cf)->where('id', '!=', $id)->first();
if ($other) {
if ($this->forceMapping) {
// Libera il CF dall'altro stabile
try {
DB::table('stabili')->where('id', $other->id)->update(['codice_fiscale' => null, 'updated_at' => now()]);
} catch (\Throwable $e) {
// Fallback a 'ND' se NOT NULL
DB::table('stabili')->where('id', $other->id)->update(['codice_fiscale' => 'ND', 'updated_at' => now()]);
}
if ($this->output->isVerbose()) {
$this->line(sprintf(' -> riassegnazione CF: liberato da stabile #%d per assegnare a #%d', $other->id, $id));
}
} else {
// Evita violazione: non aggiornare CF se già in uso
unset($update['codice_fiscale']);
if ($this->output->isVerbose()) {
$this->line(' -> CF già in uso da altro stabile: aggiornamento CF ignorato (no force).');
}
}
}
}
}
if (! empty($update)) {
DB::table('stabili')->where('id', $id)->update($update);
}
}
private function applyStabileMapping(array $update, array $legacyRow, $existing): array
{
try {
$aliases = [
'comune' => 'citta',
'codice_fiscale' => 'codice_fiscale',
'cod_fisc_amministratore' => 'cod_fisc_amministratore',
];
$columnMap = [
// Identità e indirizzo
'stabile.denominazione' => 'denominazione',
'stabile.indirizzo' => 'indirizzo',
'stabile.cap' => 'cap',
'stabile.citta' => 'citta',
'stabile.provincia' => 'provincia',
'stabile.comune_cod' => 'citta', // best-effort legacy code
'stabile.codice_fiscale' => 'codice_fiscale',
// Catasto (scegli colonne presenti)
'stabile.foglio' => (Schema::hasColumn('stabili', 'foglio') ? 'foglio' : (Schema::hasColumn('stabili', 'foglio_catasto') ? 'foglio_catasto' : 'foglio')),
'stabile.foglio_catasto' => (Schema::hasColumn('stabili', 'foglio_catasto') ? 'foglio_catasto' : (Schema::hasColumn('stabili', 'foglio') ? 'foglio' : 'foglio_catasto')),
'stabile.particella' => (Schema::hasColumn('stabili', 'particella') ? 'particella' : (Schema::hasColumn('stabili', 'particella_catasto') ? 'particella_catasto' : (Schema::hasColumn('stabili', 'mappale') ? 'mappale' : 'particella'))),
'stabile.particella_catasto' => (Schema::hasColumn('stabili', 'particella_catasto') ? 'particella_catasto' : (Schema::hasColumn('stabili', 'particella') ? 'particella' : (Schema::hasColumn('stabili', 'mappale') ? 'mappale' : 'particella_catasto'))),
'stabile.mappale' => (Schema::hasColumn('stabili', 'mappale') ? 'mappale' : (Schema::hasColumn('stabili', 'particella') ? 'particella' : (Schema::hasColumn('stabili', 'particella_catasto') ? 'particella_catasto' : 'mappale'))),
'stabile.subalterno' => 'subalterno',
'stabile.categoria' => (Schema::hasColumn('stabili', 'categoria') ? 'categoria' : (Schema::hasColumn('stabili', 'categoria_catastale') ? 'categoria_catastale' : 'categoria')),
'stabile.categoria_catastale' => (Schema::hasColumn('stabili', 'categoria_catastale') ? 'categoria_catastale' : (Schema::hasColumn('stabili', 'categoria') ? 'categoria' : 'categoria_catastale')),
'stabile.classe' => (Schema::hasColumn('stabili', 'classe_catastale') ? 'classe_catastale' : (Schema::hasColumn('stabili', 'classe') ? 'classe' : 'classe_catastale')),
'stabile.consistenza' => (Schema::hasColumn('stabili', 'consistenza_catastale') ? 'consistenza_catastale' : (Schema::hasColumn('stabili', 'consistenza') ? 'consistenza' : 'consistenza_catastale')),
'stabile.rendita_catastale' => 'rendita_catastale',
'stabile.superficie_catastale' => 'superficie_catastale',
'stabile.codice_comune_catasto' => 'codice_comune_catasto',
// Caratteristiche edificio
'stabile.numero_palazzine' => 'numero_palazzine',
'stabile.numero_scale_per_palazzina' => 'numero_scale_per_palazzina',
'stabile.numero_piani' => 'numero_piani',
'stabile.piano_seminterrato' => 'piano_seminterrato',
'stabile.piano_sottotetto' => 'piano_sottotetto',
'stabile.ascensore' => 'presenza_ascensore',
'stabile.presenza_ascensore' => 'presenza_ascensore',
'stabile.cortile_giardino' => 'cortile_giardino',
'stabile.superficie_cortile' => 'superficie_cortile',
'stabile.riscaldamento_centralizzato' => 'riscaldamento_centralizzato',
'stabile.acqua_centralizzata' => 'acqua_centralizzata',
'stabile.gas_centralizzato' => 'gas_centralizzato',
'stabile.servizio_portineria' => 'servizio_portineria',
'stabile.orari_portineria' => 'orari_portineria',
'stabile.videocitofono' => 'videocitofono',
'stabile.antenna_tv_centralizzata' => 'antenna_tv_centralizzata',
'stabile.internet_condominiale' => 'internet_condominiale',
// Avanzate / stato
'stabile.configurazione_avanzata' => 'configurazione_avanzata',
'stabile.ultima_generazione_unita' => 'ultima_generazione_unita',
'stabile.stato' => 'stato',
'stabile.registro_note' => 'registro_note',
// Amministratore (1: campi su stabile, storicizzazione arriverà dopo)
'stabile.cod_fisc_amministratore' => 'cod_fisc_amministratore',
'stabile.data_nomina_amministratore' => 'data_nomina',
'stabile.data_scadenza_mandato' => 'scadenza_mandato',
];
$inverse = [];
if (! empty($this->fieldMapping) && is_array($this->fieldMapping)) {
foreach ($this->fieldMapping as $legacyKey => $target) {
if (! is_string($legacyKey) || ! is_string($target)) {
continue;
}
$inverse[$target] = $legacyKey;
}
}
if (! empty($inverse)) {
foreach ($columnMap as $tKey => $colName) {
if (! isset($inverse[$tKey])) {
continue;
}
$legacyKey = (string) $inverse[$tKey];
$lk = strtolower($legacyKey);
$lk = $aliases[$lk] ?? $lk;
if ($lk === 'codice_fiscale') {
$val = $legacyRow['codice_fiscale'] ?? ($legacyRow['cin'] ?? ($legacyRow['cod_fiscale'] ?? ($legacyRow['cf'] ?? ($legacyRow['cf_condominio'] ?? null))));
} elseif ($lk === 'cod_fisc_amministratore') {
$val = $legacyRow['cf_amministratore'] ?? ($legacyRow['cod_fisc_amministratore'] ?? ($legacyRow['cin'] ?? null));
} elseif ($tKey === 'banca.iban') {
$val = $legacyRow['iban'] ?? ($legacyRow['iban_condominio'] ?? null);
} else {
$val = is_array($legacyRow) && array_key_exists($lk, $legacyRow) ? $legacyRow[$lk] : null;
}
if ($val === null || $val === '') {
continue;
}
if (in_array($tKey, self::BOOLEAN_TARGET_KEYS, true)) {
$valStr = is_string($val) ? trim(strtolower($val)) : $val;
$val = in_array($valStr, ['1', 'y', 'yes', 'si', 'sì', 'true', 'vero', 1, true], true) ? 1 : 0;
}
$current = $existing->$colName ?? null;
if ($this->forceMapping || empty($current) || $current === 'ND') {
$update[$colName] = $val;
}
}
}
if (! empty($this->manualFieldValues)) {
foreach ($this->manualFieldValues as $target => $rawValue) {
if (! is_string($target)) {
continue;
}
$manualValue = $this->getManualValueForTarget($target);
if ($manualValue === null) {
continue;
}
$colName = $columnMap[$target] ?? null;
if (! $colName && strpos($target, 'stabile.') === 0) {
$candidate = substr($target, strlen('stabile.'));
if ($candidate !== '' && Schema::hasColumn('stabili', $candidate)) {
$colName = $candidate;
}
}
if (! $colName) {
continue;
}
$current = $existing->$colName ?? null;
if ($this->forceMapping || empty($current) || $current === 'ND') {
$update[$colName] = $manualValue;
}
}
}
if (! empty($update)) {
$update['updated_at'] = now();
}
return $update;
} catch (\Throwable $e) {
if ($this->output->isVerbose()) {
$this->warn('Mapping stabili ignorato per errore: ' . $e->getMessage());
}
return $update;
}
}
private function resolveMappingCodiceCassa(array $mapping): ?string
{
$target = isset($mapping['target']) && is_array($mapping['target']) ? $mapping['target'] : [];
$legacy = isset($mapping['legacy']) && is_array($mapping['legacy']) ? $mapping['legacy'] : [];
$candidates = [
$target['codice_cassa'] ?? null,
$mapping['target_cod_cassa'] ?? null,
$mapping['target_cod_cassa_select'] ?? null,
$mapping['codice_legacy'] ?? null,
$legacy['cod_cassa'] ?? null,
$mapping['legacy_cod_cassa'] ?? null,
$mapping['codice_cassa'] ?? null,
];
foreach ($candidates as $candidate) {
if (is_string($candidate) && trim($candidate) !== '') {
return strtoupper(trim($candidate));
}
}
return null;
}
private function resolveMappingLegacyField(array $mapping, string $field): ?string
{
$legacy = isset($mapping['legacy']) && is_array($mapping['legacy']) ? $mapping['legacy'] : [];
$candidates = [
$legacy[$field] ?? null,
$mapping['legacy_' . $field] ?? null,
];
foreach ($candidates as $candidate) {
if (is_string($candidate) && trim($candidate) !== '') {
return trim($candidate);
}
}
return null;
}
private function loadMappingIfAny(): void
{
try {
$this->fieldMapping = [];
$this->banksMapping = [];
$this->manualFieldValues = [];
if (! Schema::hasTable('gescon_import_mappings')) {
return;
}
$legacy = $this->option('stabile');
$globalRow = DB::table('gescon_import_mappings')
->whereNull('legacy_code')
->orWhere('legacy_code', '*')
->orderByDesc('updated_at')
->first();
if ($globalRow && isset($globalRow->meta)) {
$globalMeta = is_array($globalRow->meta) ? $globalRow->meta : json_decode($globalRow->meta, true);
if (is_array($globalMeta)) {
if (isset($globalMeta['mapping']) && is_array($globalMeta['mapping'])) {
$this->fieldMapping = $globalMeta['mapping'];
}
if (isset($globalMeta['banche_mapping']) && is_array($globalMeta['banche_mapping'])) {
$this->banksMapping = $globalMeta['banche_mapping'];
}
if (isset($globalMeta['manual_target_values']) && is_array($globalMeta['manual_target_values'])) {
$this->manualFieldValues = $globalMeta['manual_target_values'];
}
}
}
if ($legacy) {
$row = DB::table('gescon_import_mappings')
->where('legacy_code', $legacy)
->orderByDesc('updated_at')
->first();
if ($row && isset($row->meta)) {
$meta = is_array($row->meta) ? $row->meta : json_decode($row->meta, true);
if (is_array($meta)) {
if (isset($meta['mapping']) && is_array($meta['mapping'])) {
$this->fieldMapping = $meta['mapping'];
}
if (isset($meta['banche_mapping']) && is_array($meta['banche_mapping'])) {
$this->banksMapping = $meta['banche_mapping'];
}
if (isset($meta['manual_target_values']) && is_array($meta['manual_target_values'])) {
$this->manualFieldValues = array_replace(
$this->manualFieldValues,
$meta['manual_target_values']
);
}
}
}
}
} catch (\Throwable $e) {
// Ignora errori mapping
}
}
private function getManualValueForTarget(string $targetKey)
{
if (empty($this->manualFieldValues) || ! array_key_exists($targetKey, $this->manualFieldValues)) {
return null;
}
$value = $this->manualFieldValues[$targetKey];
if (is_string($value)) {
$value = trim($value);
}
if ($value === '' || $value === null) {
return null;
}
if (in_array($targetKey, self::BOOLEAN_TARGET_KEYS, true)) {
if (is_bool($value)) {
return $value ? 1 : 0;
}
if (is_numeric($value)) {
return (int) $value === 1 ? 1 : 0;
}
$normalized = strtolower((string) $value);
$truthy = ['1', 'y', 'yes', 'si', 'sì', 'true', 'vero', 'on'];
return in_array($normalized, $truthy, true) ? 1 : 0;
}
return $value;
}
private function mapField(string $targetKey, $default = null)
{
$manualValue = $this->getManualValueForTarget($targetKey);
if ($manualValue !== null) {
return $manualValue;
}
// Migliorato: se mapping presente e targetKey è stabile.*, tenta di leggere dal record legacy cache
if (empty($this->fieldMapping) || strpos($targetKey, 'stabile.') !== 0) {
return $default;
}
// Carica record legacy una sola volta
if ($this->stabileLegacyRow === null) {
$this->stabileLegacyRow = $this->loadStabileLegacyRow();
}
if (! $this->stabileLegacyRow) {
return $default;
}
$legacyKeyMatched = null;
foreach ($this->fieldMapping as $legacyKey => $target) {
if ($target === $targetKey) {
$legacyKeyMatched = $legacyKey;
break;
}
}
if (! $legacyKeyMatched) {
return $default;
}
$row = $this->stabileLegacyRow;
$targetAttr = substr($targetKey, strlen('stabile.'));
// Mappa candidate column names per attributi target
$candidatesMap = [
'denominazione' => ['denominazione', 'nome', 'ragione_sociale', 'descrizione', 'descr', 'nome_condominio'],
'indirizzo' => ['indirizzo', 'via', 'indir', 'address'],
'numero_civico' => ['numero_civico', 'civico', 'n_civico', 'nciv'],
'cap' => ['cap', 'cap_condominio', 'zip'],
'comune_cod' => ['comune', 'comune_cod', 'citta', 'città', 'localita', 'loc'],
'provincia' => ['provincia', 'prov', 'pr'],
'foglio' => ['foglio', 'ac_foglio', 'foglio_catasto'],
'particella' => ['particella', 'mappale', 'ac_particella', 'part'],
'subalterno' => ['subalterno', 'sub', 'ac_sub', 'ac_subalterno'],
'categoria_catastale' => ['categoria_catastale', 'categoria', 'cat_categoria'],
'classe' => ['classe', 'cat_classe'],
'consistenza' => ['consistenza', 'cat_consistenza'],
'rendita_catastale' => ['rendita_catastale', 'rendita', 'cat_rendita'],
'cod_fisc_amministratore' => ['cod_fisc_amministratore', 'cf_amministratore', 'amministratore_cf', 'cf_amm', 'cod_fiscale_amm'],
];
$value = $default;
if (isset($candidatesMap[$targetAttr])) {
foreach ($candidatesMap[$targetAttr] as $cand) {
if (isset($row[$cand]) && $row[$cand] !== '') {
$value = $row[$cand];
break;
}
}
} else {
// fallback: prova legacyKey directly
if (isset($row[$legacyKeyMatched]) && $row[$legacyKeyMatched] !== '') {
$value = $row[$legacyKeyMatched];
}
}
return $value ?? $default;
}
private function loadStabileLegacyRow(): ?array
{
try {
$code = $this->option('stabile');
if (! $code) {
return null;
}
// 1) Staging stabili
if (Schema::connection('gescon_import')->hasTable('stabili')) {
$r = DB::connection('gescon_import')->table('stabili')->where('cod_stabile', $code)->first();
if ($r) {
return (array) $r;
}
}
// 2) Staging condomin
if (Schema::connection('gescon_import')->hasTable('condomin')) {
$r = $this->scopeCondominQuery(DB::connection('gescon_import')->table('condomin'))
->where('cod_stabile', $code)
->first();
if ($r) {
return (array) $r;
}
}
// 3) MDB fallback (usa stessa logica di mdbExportToRows ridotta)
$base = $this->option('path');
if ($base) {
$mdb = rtrim($base, '/') . '/Stabili.mdb';
if (is_file($mdb) && is_readable($mdb)) {
$rows = $this->mdbExportToRows($mdb, ['Stabili', 'stabili', 'Condomin', 'condomin']);
foreach ($rows as $row) {
foreach (['cod_stabile', 'codice', 'codice_stabile', 'stabile', 'cod'] as $cf) {
if (isset($row[$cf]) && trim((string) $row[$cf]) === $code) {
return $row;
}
}
}
}
}
} catch (\Throwable $e) {
// ignore
}
return null;
}
/**
* Esporta una tabella MDB in array di righe associative (headers normalizzati lower-case).
*/
private function mdbExportToRows(string $mdbPath, array $tableCandidates): array
{
$binTables = trim((string) @shell_exec('command -v mdb-tables')) ?: '/usr/bin/mdb-tables';
$binExport = trim((string) @shell_exec('command -v mdb-export')) ?: '/usr/bin/mdb-export';
if (! is_file($mdbPath) || ! is_readable($mdbPath)) {
return [];
}
$tables = @shell_exec(sprintf('%s -1 %s 2>/dev/null', escapeshellarg($binTables), escapeshellarg($mdbPath))) ?: '';
$table = null;
if ($tables) {
$names = array_filter(preg_split('/\r?\n/', trim($tables)));
foreach ($tableCandidates as $cand) {
foreach ($names as $n) {
if (strcasecmp($n, $cand) === 0) {
$table = $n;
break 2;
}
}
}
if (! $table) {
foreach ($tableCandidates as $cand) {
foreach ($names as $n) {
if (stripos($n, $cand) !== false) {
$table = $n;
break 2;
}
}
}
}
}
if (! $table) {
return [];
}
$tmp = tempnam(sys_get_temp_dir(), 'mdbcsv_');
$cmd = sprintf(
'%s -D %s -d %s -q %s -R "\\n" %s %s > %s 2>/dev/null',
escapeshellarg($binExport),
escapeshellarg('%Y-%m-%d'),
escapeshellarg('|'),
escapeshellarg('"'),
escapeshellarg($mdbPath),
escapeshellarg($table),
escapeshellarg($tmp)
);
@shell_exec($cmd);
if (! is_file($tmp) || filesize($tmp) === 0) {
@unlink($tmp);
return [];
}
$fh = fopen($tmp, 'r');
if (! $fh) {
@unlink($tmp);
return [];
}
$headers = fgetcsv($fh, 0, '|');
if (! $headers) {
fclose($fh);
@unlink($tmp);
return [];
}
$headers = array_map(fn($h) => strtolower(trim((string) $h)), $headers);
$rows = [];
while (($cols = fgetcsv($fh, 0, '|')) !== false) {
if ($cols === [null] || $cols === false) {
continue;
}
$cols = array_map(fn($v) => is_string($v) ? trim($v) : $v, $cols);
$cnt = count($headers);
if (count($cols) < $cnt) {
$cols = array_pad($cols, $cnt, null);
}
if (count($cols) > $cnt) {
$cols = array_slice($cols, 0, $cnt);
}
$row = @array_combine($headers, $cols) ?: [];
if (! empty($row)) {
$rows[] = $row;
}
}
fclose($fh);
@unlink($tmp);
return $rows;
}
}