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= (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'); $hasTipologiaColumn = Schema::hasColumn('unita_immobiliari', 'tipologia_id'); $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 $ownerDescriptor = trim((string) (($r->cognome ?? '') . ' ' . ($r->nome ?? ''))); $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] ?? ''); } } $unitClassification = $this->resolveLegacyUnitClassification($r, $internoStr, $ownerDescriptor, $ptype); if ($ptype === null && ($unitClassification['pertinenza_type'] ?? null)) { $ptype = (string) $unitClassification['pertinenza_type']; } $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 ($normalizedInterno === null) { $normalizedInterno = $this->buildLegacyFallbackInterno($legacyCondId, $unitClassification); } 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/BOX/...) $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; } } $resolvedTipoUnita = $this->normalizeLegacyTipoUnita($unitClassification['tipo_unita'] ?? ($ptype ?: 'abitazione')); if (Schema::hasColumn('unita_immobiliari', 'tipo_unita') && (string) ($exists->tipo_unita ?? '') !== $resolvedTipoUnita) { $patch['tipo_unita'] = $resolvedTipoUnita; } if (Schema::hasColumn('unita_immobiliari', 'categoria_catastale')) { $categoriaCatastale = $this->normalizeCategoriaCatastale($this->firstNonEmptyStr([ $r->catasto_categoria ?? null, $r->categoria_catastale ?? null, $unitClassification['categoria_catastale'] ?? null, ], 20)); if ($categoriaCatastale !== null && (string) ($exists->categoria_catastale ?? '') !== $categoriaCatastale) { $patch['categoria_catastale'] = $categoriaCatastale; } } if ($hasTipologiaColumn) { $tipologiaId = $this->resolveLegacyTipologiaId($unitClassification); if ($tipologiaId !== null && (int) ($exists->tipologia_id ?? 0) !== $tipologiaId) { $patch['tipologia_id'] = $tipologiaId; } } 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 (($unitClassification['is_condominiale'] ?? false) && Schema::hasColumn('unita_immobiliari', 'denominazione')) { $condoName = $this->firstNonEmptyStr([ $unitClassification['denominazione'] ?? null, $ownerDescriptor, ], 255); if ($condoName !== null && (string) ($exists->denominazione ?? '') !== $condoName) { $patch['denominazione'] = $condoName; } } 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; } $resolvedTipoUnita = $this->normalizeLegacyTipoUnita($unitClassification['tipo_unita'] ?? ($ptype ?: 'abitazione')); $categoriaCatastale = $this->normalizeCategoriaCatastale($this->firstNonEmptyStr([ $r->catasto_categoria ?? null, $r->categoria_catastale ?? null, $unitClassification['categoria_catastale'] ?? null, ], 20)); $tipologiaId = $hasTipologiaColumn ? $this->resolveLegacyTipologiaId($unitClassification) : null; $data = [ 'stabile_id' => $rowStabileId, 'palazzina_id' => null, 'codice_unita' => $codice, 'denominazione' => $this->firstNonEmptyStr([ $unitClassification['denominazione'] ?? null, $ownerDescriptor, ], 255), 'descrizione' => null, 'palazzina' => $this->splitMode === 'palazzine' ? $scalaVal : null, 'scala' => $scalaVal, 'piano' => $pianoVal, 'interno' => $normalizedInterno, 'subalterno' => $sub, 'categoria_catastale' => $categoriaCatastale, 'classe' => null, 'consistenza' => null, 'rendita_catastale' => null, 'millesimi_generali' => 0, 'legacy_cond_id' => $legacyCondId, 'tipo_unita' => $resolvedTipoUnita, 'tipologia_id' => $tipologiaId, 'stato_conservazione' => 'buono', 'stato_occupazione' => 'occupata_proprietario', 'attiva' => true, 'unita_demo' => false, 'created_at' => now(), 'updated_at' => now(), ]; if (! $hasTipologiaColumn) { unset($data['tipologia_id']); } $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, $o->n_stra ?? 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 stepAssemblee(?int $limit): int { if (! Schema::hasTable('assemblee')) { return 0; } $stabileId = $this->stabileId ?: $this->resolveStabileId(); $stabile = trim((string) ($this->option('stabile') ?? '')); if (! $stabileId || $stabile === '') { return 0; } $sources = $this->resolveAssembleeMdbSources($stabile); if ($sources === []) { return 0; } $assembleeColumns = Schema::getColumnListing('assemblee'); $canMatch = in_array('stabile_id', $assembleeColumns, true) && in_array('tipo', $assembleeColumns, true) && in_array('data_prima_convocazione', $assembleeColumns, true) && in_array('data_seconda_convocazione', $assembleeColumns, true); $processed = 0; foreach ($sources as $source) { $rows = $this->mdbExportToRows($source['path'], ['Singolo_anno_assemblee', 'singolo_anno_assemblee', 'assemblee']); if ($rows === []) { continue; } foreach ($rows as $row) { if ($limit !== null && $processed >= $limit) { return $processed; } $firstDate = $this->normalizeLegacyDateTime($row['dt_1_convoc'] ?? null, $row['ora_1_convoc'] ?? null); $secondDate = $this->normalizeLegacyDateTime($row['dt_2_convoc'] ?? null, $row['ora_2_convoc'] ?? null); if ($firstDate === null && $secondDate === null) { continue; } $tipo = $this->normalizeLegacyAssembleaType($row['ordin_straord'] ?? null); $luogo = $this->firstNonEmptyStr([ $row['luogo_2_convoc'] ?? null, $row['luogo_1_convoc'] ?? null, ], 255); $reference = $secondDate ?? $firstDate; $referenceTs = $reference ? strtotime($reference) : false; $note = $this->buildLegacyAssembleaNote($row, (string) ($source['legacy_year'] ?? '')); $payload = [ 'stabile_id' => $stabileId, 'tipo' => $tipo, 'data_assemblea' => $reference, 'data_prima_convocazione' => $firstDate, 'data_seconda_convocazione' => $secondDate, 'luogo' => $luogo, 'note' => $note, 'stato' => ($referenceTs !== false && $referenceTs < time()) ? 'svolta' : 'convocata', 'data_convocazione' => $firstDate ? substr($firstDate, 0, 10) : null, 'data_svolgimento' => ($referenceTs !== false && $referenceTs < time()) ? substr($reference, 0, 10) : null, 'created_at' => now(), 'updated_at' => now(), ]; if ($canMatch) { $match = [ 'stabile_id' => $stabileId, 'tipo' => $tipo, 'data_prima_convocazione' => $firstDate, 'data_seconda_convocazione' => $secondDate, ]; if (! $this->isDryRun) { DB::table('assemblee')->updateOrInsert($match, array_merge($payload, [ 'updated_at' => now(), ])); } } else { $this->dynamicInsert('assemblee', $payload); } $processed++; } } return $processed; } 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 resolveLegacyUnitClassification(object $row, ?string $interno, ?string $ownerDescriptor, ?string $explicitPertinenzaType = null): array { $descriptor = Str::upper(trim(implode(' ', array_filter([ $interno, $ownerDescriptor, is_string($row->note ?? null) ? $row->note : null, is_string($row->descrizione ?? null) ? $row->descrizione : null, is_string($row->destinazione ?? null) ? $row->destinazione : null, ], fn($value) => is_string($value) && trim($value) !== '')))); $mapping = $this->findLegacyUnitTypeMapping( 'condomin', 'tipo_pr', isset($row->tipo_pr) ? trim((string) $row->tipo_pr) : null, $descriptor, ); if (str_contains($descriptor, 'MAGAZZ')) { $mapping = array_merge($mapping, [ 'tipo_unita' => 'magazzino_deposito', 'tipologia_codice' => $mapping['tipologia_codice'] ?? 'MAG', 'tipologia_nome' => $mapping['tipologia_nome'] ?? 'Magazzino', 'tipologia_categoria' => $mapping['tipologia_categoria'] ?? 'deposito', 'categoria_catastale' => $mapping['categoria_catastale'] ?? 'C/2', 'fallback_interno_prefix' => $mapping['fallback_interno_prefix'] ?? 'MAG', 'denominazione' => $this->looksLikeCondominialeDescriptor($descriptor) ? 'Magazzino Condominiale' : ($mapping['denominazione'] ?? null), ]); } if (str_contains($descriptor, 'BOX') || str_contains($descriptor, 'GARAGE') || str_contains($descriptor, 'AUTORIM')) { $mapping = array_merge($mapping, [ 'tipo_unita' => 'box_garage', 'tipologia_codice' => $mapping['tipologia_codice'] ?? 'BOX', 'tipologia_nome' => $mapping['tipologia_nome'] ?? 'Box', 'tipologia_categoria' => $mapping['tipologia_categoria'] ?? 'pertinenza', 'categoria_catastale' => $mapping['categoria_catastale'] ?? 'C/6', 'is_pertinenza' => true, 'fallback_interno_prefix' => $mapping['fallback_interno_prefix'] ?? 'BOX', ]); } if (str_contains($descriptor, 'CANTIN')) { $mapping = array_merge($mapping, [ 'tipo_unita' => 'cantina', 'tipologia_codice' => $mapping['tipologia_codice'] ?? 'CANT', 'tipologia_nome' => $mapping['tipologia_nome'] ?? 'Cantina', 'tipologia_categoria' => $mapping['tipologia_categoria'] ?? 'pertinenza', 'categoria_catastale' => $mapping['categoria_catastale'] ?? 'C/2', 'is_pertinenza' => true, 'fallback_interno_prefix' => $mapping['fallback_interno_prefix'] ?? 'CAN', ]); } if ($explicitPertinenzaType !== null && $explicitPertinenzaType !== '') { $mapping['pertinenza_type'] = $explicitPertinenzaType; } elseif (($mapping['tipo_unita'] ?? null) === 'box_garage') { $mapping['pertinenza_type'] = 'box'; } elseif (($mapping['tipo_unita'] ?? null) === 'cantina') { $mapping['pertinenza_type'] = 'cantina'; } elseif (($mapping['tipo_unita'] ?? null) === 'soffitta') { $mapping['pertinenza_type'] = 'soffitta'; } elseif (($mapping['tipo_unita'] ?? null) === 'locale_tecnico') { $mapping['pertinenza_type'] = 'locale_tecnico'; } if (($mapping['is_condominiale'] ?? false) && empty($mapping['denominazione'])) { $mapping['denominazione'] = trim((string) ($ownerDescriptor ?? '')) ?: 'Unita Condominiale'; } return $mapping; } private function buildLegacyFallbackInterno(?string $legacyCondId, array $classification): ?string { $prefix = trim((string) ($classification['fallback_interno_prefix'] ?? '')); $condId = trim((string) ($legacyCondId ?? '')); if ($prefix === '') { return null; } return $condId !== '' ? strtoupper($prefix . $condId) : strtoupper($prefix); } private function findLegacyUnitTypeMapping(string $source, string $field, ?string $value, string $descriptor): array { $cacheKey = implode('|', [$source, $field, strtoupper(trim((string) $value)), $descriptor]); if (array_key_exists($cacheKey, $this->legacyUnitTypeMappings)) { return $this->legacyUnitTypeMappings[$cacheKey]; } $default = [ 'tipo_unita' => 'abitazione', 'tipologia_codice' => 'APP', 'tipologia_nome' => 'Appartamento', 'tipologia_categoria' => 'residenziale', 'categoria_catastale' => null, 'is_pertinenza' => false, 'is_condominiale' => false, 'fallback_interno_prefix' => null, 'denominazione' => null, ]; if (! Schema::hasTable('legacy_unita_tipo_mappings')) { return $this->legacyUnitTypeMappings[$cacheKey] = $default; } $rows = DB::table('legacy_unita_tipo_mappings') ->where('legacy_source', $source) ->where('legacy_field', $field) ->where(function ($query) use ($value) { if ($value !== null && trim((string) $value) !== '') { $query->where('legacy_value', strtoupper(trim((string) $value)))->orWhereNull('legacy_value'); } else { $query->whereNull('legacy_value'); } }) ->orderBy('sort_order') ->orderBy('id') ->get(); foreach ($rows as $row) { $regex = trim((string) ($row->match_regex ?? '')); if ($regex !== '' && @preg_match('/' . $regex . '/i', $descriptor) !== 1) { continue; } return $this->legacyUnitTypeMappings[$cacheKey] = array_merge($default, [ 'tipo_unita' => trim((string) ($row->tipo_unita ?? '')) ?: $default['tipo_unita'], 'tipologia_codice' => trim((string) ($row->tipologia_codice ?? '')) ?: $default['tipologia_codice'], 'tipologia_nome' => trim((string) ($row->tipologia_nome ?? '')) ?: $default['tipologia_nome'], 'tipologia_categoria' => trim((string) ($row->tipologia_categoria ?? '')) ?: $default['tipologia_categoria'], 'categoria_catastale' => trim((string) ($row->categoria_catastale ?? '')) ?: null, 'is_pertinenza' => (bool) ($row->is_pertinenza ?? false), 'is_condominiale' => (bool) ($row->is_condominiale ?? false), 'fallback_interno_prefix' => trim((string) ($row->fallback_interno_prefix ?? '')) ?: null, 'denominazione' => trim((string) ($row->descrizione ?? '')) ?: null, ]); } return $this->legacyUnitTypeMappings[$cacheKey] = $default; } private function normalizeLegacyTipoUnita(?string $value): string { $normalized = Str::snake(Str::lower(trim((string) $value)), '_'); return match ($normalized) { 'box', 'garage', 'posto_auto', 'box_garage' => 'box_garage', 'cantina' => 'cantina', 'soffitta', 'solaio' => 'soffitta', 'locale_tecnico', 'centrale_termica' => 'locale_tecnico', 'magazzino', 'deposito', 'magazzino_deposito' => 'magazzino_deposito', 'condominiale', 'spazio_comune_redditizio', 'area_condominiale' => 'spazio_comune_redditizio', 'studio', 'ufficio', 'studio_professionale' => 'studio_professionale', 'negozio', 'locale_commerciale', 'attivita_commerciale' => 'attivita_commerciale', 'abitazione', '' => 'abitazione', default => 'altro', }; } private function resolveLegacyTipologiaId(array $classification): ?int { if (! Schema::hasTable('unita_tipologie')) { return null; } $code = strtoupper(trim((string) ($classification['tipologia_codice'] ?? ''))); if ($code === '') { return null; } if (array_key_exists($code, $this->tipologiaCache)) { return $this->tipologiaCache[$code]; } $row = DB::table('unita_tipologie')->where('codice', $code)->first(); if ($row) { return $this->tipologiaCache[$code] = (int) $row->id; } if ($this->isDryRun) { return $this->tipologiaCache[$code] = null; } $id = DB::table('unita_tipologie')->insertGetId([ 'codice' => $code, 'nome' => trim((string) ($classification['tipologia_nome'] ?? $code)) ?: $code, 'categoria' => trim((string) ($classification['tipologia_categoria'] ?? '')) ?: null, 'is_pertinenza' => (bool) ($classification['is_pertinenza'] ?? false), 'is_condominiale' => (bool) ($classification['is_condominiale'] ?? false), 'meta' => json_encode([ 'source' => 'legacy_unita_tipo_mappings', 'categoria_catastale' => $classification['categoria_catastale'] ?? null, ]), 'created_at' => now(), 'updated_at' => now(), ]); return $this->tipologiaCache[$code] = (int) $id; } private function looksLikeCondominialeDescriptor(string $descriptor): bool { return str_contains($descriptor, 'CONDOMINIO') || str_contains($descriptor, 'PARTI COMUNI'); } private function normalizeCategoriaCatastale(?string $value): ?string { $value = strtoupper(trim((string) ($value ?? ''))); if ($value === '') { return null; } if (preg_match('/^([A-Z])(\d+)$/', $value, $matches) === 1) { return $matches[1] . '/' . $matches[2]; } return $value; } 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 normalizeLegacyDateTime(?string $dateValue, ?string $timeValue = null): ?string { if (! is_string($dateValue)) { return null; } $dateValue = trim($dateValue); if ($dateValue === '') { return null; } $dateToken = preg_split('/\s+/', $dateValue)[0] ?? ''; $dateToken = trim((string) $dateToken); if ($dateToken === '') { return null; } $datePart = null; foreach (['!m/d/y', '!d/m/y', '!m/d/Y', '!d/m/Y', '!Y-m-d'] as $format) { $parsed = \DateTime::createFromFormat($format, $dateToken); if ($parsed instanceof \DateTime) { $datePart = $parsed->format('Y-m-d'); break; } } if ($datePart === null) { $ts = strtotime($dateValue); if ($ts === false) { return null; } $datePart = date('Y-m-d', $ts); } return $datePart . ' ' . ($this->normalizeLegacyTime($timeValue) ?? '00:00:00'); } private function normalizeLegacyTime(?string $value): ?string { if (! is_string($value)) { return null; } $value = trim($value); if ($value === '') { return null; } $value = str_replace([',', '.'], ':', $value); $value = preg_replace('/\s+/', '', $value ?? ''); if ($value === null || $value === '') { return null; } if (preg_match('/^(\d{1,2})$/', $value, $matches)) { return sprintf('%02d:00:00', (int) $matches[1]); } if (preg_match('/^(\d{1,2}):(\d{1,2})$/', $value, $matches)) { return sprintf('%02d:%02d:00', (int) $matches[1], (int) $matches[2]); } if (preg_match('/^(\d{1,2}):(\d{1,2}):(\d{1,2})$/', $value, $matches)) { return sprintf('%02d:%02d:%02d', (int) $matches[1], (int) $matches[2], (int) $matches[3]); } return null; } private function normalizeLegacyAssembleaType(?string $value): string { $normalized = Str::lower(trim((string) $value)); return str_contains($normalized, 'stra') ? 'straordinaria' : 'ordinaria'; } private function buildLegacyAssembleaNote(array $row, string $legacyYear): ?string { $parts = []; $numero = trim((string) ($row['num_ass'] ?? '')); if ($numero !== '') { $parts[] = 'Assemblea legacy n. ' . $numero; } if ($legacyYear !== '') { $parts[] = 'Cartella legacy: ' . $legacyYear; } $noteConvocazione = trim((string) ($row['note_convocaz'] ?? '')); if ($noteConvocazione !== '') { $parts[] = 'Note convocazione: ' . $noteConvocazione; } $noteAssemblea = trim((string) ($row['note_assemblea'] ?? '')); if ($noteAssemblea !== '') { $parts[] = 'Note assemblea: ' . $noteAssemblea; } $noteInterne = trim((string) ($row['note_assemblea_int'] ?? '')); if ($noteInterne !== '') { $parts[] = 'Note interne legacy: ' . $noteInterne; } $ordineGiorno = trim((string) ($row['ordine_del_giorno'] ?? '')); if ($ordineGiorno !== '') { $parts[] = 'Ordine del giorno legacy:' . PHP_EOL . $ordineGiorno; } if ($parts === []) { return null; } return implode(PHP_EOL . PHP_EOL, $parts); } private function resolveAssembleeMdbSources(string $stabile): array { $root = rtrim((string) ($this->option('path') ?: '/mnt/gescon-archives/gescon'), '/'); $stableDir = $root . '/' . $stabile; if (! is_dir($stableDir)) { return []; } $anno = trim((string) ($this->option('anno') ?? '')); $sources = []; if ($anno !== '' && strcasecmp($anno, 'all') !== 0) { $candidates = array_unique(array_filter([ $stableDir . '/' . $anno . '/singolo_anno.mdb', is_numeric($anno) ? $stableDir . '/' . sprintf('%04d', (int) $anno) . '/singolo_anno.mdb' : null, ])); foreach ($candidates as $candidate) { if (is_string($candidate) && is_file($candidate)) { $sources[] = [ 'path' => $candidate, 'legacy_year' => basename(dirname($candidate)), ]; } } } else { foreach ((array) glob($stableDir . '/*/singolo_anno.mdb') as $candidate) { if (! is_string($candidate) || ! is_file($candidate)) { continue; } $sources[] = [ 'path' => $candidate, 'legacy_year' => basename(dirname($candidate)), ]; } } if ($sources === []) { $resolved = $this->resolveCondominMdbForStabile($stabile, is_numeric($anno) ? sprintf('%04d', (int) $anno) : null); if ($resolved !== null && ! empty($resolved['path']) && is_file((string) $resolved['path'])) { $sources[] = [ 'path' => (string) $resolved['path'], 'legacy_year' => (string) ($resolved['legacy_year'] ?? basename(dirname((string) $resolved['path']))), ]; } } usort($sources, static function (array $left, array $right): int { return strcmp((string) ($left['legacy_year'] ?? ''), (string) ($right['legacy_year'] ?? '')); }); return array_values(array_unique($sources, SORT_REGULAR)); } 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, $numeroStraordinaria = null): ?int { if (! Schema::hasTable('gestioni_contabili')) { return null; } $legacyYear = $anno !== null && $anno !== '' ? (string) $anno : null; $stabileId = $this->stabileId ?: $this->resolveStabileId(); return $this->resolveGestioneContabileIdForStabile( $stabileId, $legacyYear, $tipo !== null ? (string) $tipo : null, is_numeric($numeroStraordinaria) ? (int) $numeroStraordinaria : null, ); } 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; } private function firstNonEmptyStr(array $values, int $maxLen): ?string { foreach ($values as $v) { if ($v === null) { continue; } $s = trim((string) $v); if ($s === '') { continue; } return mb_substr($s, 0, $maxLen); } return null; } }