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) { $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->option('mdb') ?: ($this->option('path') ? rtrim($this->option('path'), '/') . '/Stabili.mdb' : '/mnt/gescon-archives/gescon/dbc/Stabili.mdb'); $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); $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->cf ?? ($stage->cf_condominio ?? null)); // CF amministratore (se presente in staging) $map['cod_fisc_amministratore'] = $stage->cod_fisc_amministratore ?? ($stage->cf_amministratore ?? null); // Dati catasto (normalizza verso colonne effettive) $foglioVal = $stage->foglio ?? ($stage->catasto_foglio ?? null); if ($foglioVal !== null) { if (in_array('foglio', $cols, true)) { $map['foglio'] = $foglioVal; } elseif (in_array('foglio_catasto', $cols, true)) { $map['foglio_catasto'] = $foglioVal; } } $partVal = $stage->particella ?? ($stage->catasto_particella ?? ($stage->mappale ?? null)); if ($partVal !== null) { if (in_array('mappale', $cols, true)) { $map['mappale'] = $partVal; } elseif (in_array('particella_catasto', $cols, true)) { $map['particella_catasto'] = $partVal; } } $subVal = $stage->subalterno ?? ($stage->catasto_sub ?? null); if ($subVal !== null && in_array('subalterno', $cols, true)) { $map['subalterno'] = $subVal; } // Categoria catastale (se presente in staging) $catVal = $stage->categoria ?? ($stage->categoria_catastale ?? null); if ($catVal !== null) { if (in_array('categoria', $cols, true)) { $map['categoria'] = $catVal; } elseif (in_array('categoria_catastale', $cols, true)) { $map['categoria_catastale'] = $catVal; } } // Filtra solo colonne esistenti e valori non null $update = []; foreach ($map as $k => $v) { if ($v !== null && in_array($k, $cols, true)) { $update[$k] = $v; } } // Applica mapping UI anche sui dati staging (consente override esplicito CF quando forzato) $update = $this->applyStabileMapping($update, (array)$stage, $stab); if (!empty($update)) { // Verbose log: evidenzia cambio CF da staging if ($this->output->isVerbose()) { $oldCf = $stab->codice_fiscale ?? null; $newCf = $update['codice_fiscale'] ?? null; if ($newCf && $newCf !== $oldCf) { $this->line(sprintf( ' -> CF %s (staging): %s => %s%s%s', $codRichiesto, $oldCf !== null && $oldCf !== '' ? $oldCf : 'NULL', $newCf, $this->option('dry-run') ? ' (DRY-RUN)' : '', $this->forceMapping ? ' [forced]' : '' )); } } $update['updated_at'] = now(); $this->safeUpdateStabile($stab->id, $update); if (!$this->isDryRun) { $updated++; } } } } } if ($created > 0 || $updated > 0) { $this->line(" → stabili creati: {$created}, aggiornati: {$updated}"); } return $created + $updated; } private function stepUnita(?int $limit): int { // Fonte: gescon_import.condomin (elenco soggetti/unità) if (!Schema::connection('gescon_import')->hasTable('condomin')) return 0; $count = 0; $updated = 0; $hasDetailedSchema = Schema::hasColumn('unita_immobiliari', 'codice_unita'); $parentStabile = null; if (Schema::hasTable('stabili') && $this->option('stabile')) { $stabileCol = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; $parentStabile = DB::table('stabili')->where($stabileCol, $this->option('stabile'))->first(); if (!$parentStabile && $stabileCol === 'codice_stabile') { $alt = ltrim($this->option('stabile'), '0'); if ($alt !== '' && $alt !== $this->option('stabile')) { $parentStabile = DB::table('stabili')->where($stabileCol, $alt)->first(); } } } $pertinenzeToLink = []; $seenCondIds = []; $processed = 0; $batch = $limit ? min(max(1, $limit), 1000) : 1000; $offset = 0; while (true) { $q = DB::connection('gescon_import')->table('condomin'); if ($this->option('stabile')) { $q->where('cod_stabile', $this->option('stabile')); } if ($this->option('scala')) { $q->where('scala', $this->option('scala')); } $q->limit($batch)->offset($offset); $rows = $q->get(); if ($rows->isEmpty()) break; 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 = $r->interno ?? null; $sub = $r->sub ?? $interno; // Riconosci pertinenze nel campo INT o note: cantina, box/garage, posto auto, soffitta/solaio, locale tecnico $internoStr = is_string($interno) ? trim($interno) : (string)$interno; if ($internoStr !== '' && preg_match('/APP\.?\s*COND\.?/i', $internoStr)) { continue; } $ptype = null; $pcode = null; // tipo pertinenza e codice interno normalizzato if ($internoStr !== '') { if (preg_match('/^CAN\s*(\d+)$/i', preg_replace('/\s+/', ' ', $internoStr), $m)) { $ptype = 'cantina'; $pcode = 'CAN' . $m[1]; } elseif (preg_match('/^(BOX|GAR|GARAGE)\s*(\d+)?$/i', preg_replace('/\s+/', ' ', $internoStr), $m)) { $ptype = 'box'; $pcode = 'BOX' . ($m[2] ?? ''); } elseif (preg_match('/^(PA|POSTO\s*AUTO)\s*(\d+)?$/i', preg_replace('/\s+/', ' ', $internoStr), $m)) { $ptype = 'posto_auto'; $pcode = 'PA' . ($m[2] ?? ''); } elseif (preg_match('/^(SOF|SOFFITTA|SOLAIO)\s*(\d+)?$/i', preg_replace('/\s+/', ' ', $internoStr), $m)) { $ptype = 'soffitta'; $pcode = 'SOF' . ($m[2] ?? ''); } elseif (preg_match('/^(LT|LOCALE\s*TECNICO)\s*(\d+)?$/i', preg_replace('/\s+/', ' ', $internoStr), $m)) { $ptype = 'locale_tecnico'; $pcode = 'LT' . ($m[2] ?? ''); } } $scalaVal = trim((string)($r->scala ?? 'A')) ?: 'A'; // Piano: normalizza (R=0, T=0, 1=1, -1 o S= -1) $pianoVal = 0; if (isset($r->piano)) { $pv = is_string($r->piano) ? strtoupper(trim($r->piano)) : (string)$r->piano; if ($pv === 'T' || $pv === 'PT' || $pv === 'R' || $pv === 'PR' || $pv === '') { $pianoVal = 0; } elseif ($pv === 'S' || $pv === 'PS' || $pv === '-1') { $pianoVal = -1; } elseif (is_numeric($pv)) { $pianoVal = (int)$pv; } } if ($hasDetailedSchema) { // Codice unita coerente: STAB-SCALA-INT (per cantine: STAB-SCALA-CAN) $intPart = $ptype && $pcode ? $pcode : (($interno ?? null) ?: '0'); $codiceBaseStab = ($r->cod_stabile ?? $this->option('stabile')) ?: 'S0'; // Se stabili-per-scala, includi comunque la scala nel codice per leggibilità $codice = $codiceBaseStab . '-' . ($scalaVal ?: 'A') . '-' . $intPart; // 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; } $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 (!$exists) { $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 = $codice . '-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 ($this->splitMode === 'palazzine') { if (Schema::hasColumn('unita_immobiliari', 'palazzina')) { if (empty($exists->palazzina) || $exists->palazzina !== $scalaVal) { $patch['palazzina'] = $scalaVal; } } if (Schema::hasColumn('unita_immobiliari', 'scala') && (empty($exists->scala) || $exists->scala !== $scalaVal)) { $patch['scala'] = $scalaVal; } } if (!empty($patch)) { $patch['updated_at'] = now(); if (!$this->isDryRun) { DB::table('unita_immobiliari')->where('id', $exists->id)->update($patch); } $updated++; } if (!$this->isDryRun && $exists->id) { $this->upsertUnitaNominativiFromLegacy((int) $exists->id, (int) $rowStabileId, $legacyCondId, $r); } continue; } $data = [ 'stabile_id' => $rowStabileId, 'palazzina_id' => null, 'codice_unita' => $codice, 'denominazione' => trim(($r->cognome ?? '') . ' ' . ($r->nome ?? '')) ?: null, 'descrizione' => null, 'palazzina' => $this->splitMode === 'palazzine' ? $scalaVal : null, 'scala' => $scalaVal, 'piano' => $pianoVal, 'interno' => (string)($ptype && $pcode ? $pcode : $interno), 'subalterno' => (string)$sub, 'categoria_catastale' => null, 'classe' => null, 'consistenza' => null, 'rendita_catastale' => null, 'millesimi_generali' => 0, 'legacy_cond_id' => $legacyCondId, 'tipo_unita' => $ptype ?: 'abitazione', 'stato_conservazione' => 'buono', 'stato_occupazione' => 'occupata_proprietario', 'attiva' => true, 'unita_demo' => false, 'created_at' => now(), 'updated_at' => now() ]; $this->dynamicInsert('unita_immobiliari', $data); $unitaInserted = DB::table('unita_immobiliari') ->where('stabile_id', $rowStabileId) ->when(Schema::hasColumn('unita_immobiliari', 'legacy_cond_id') && $legacyCondId !== null && $legacyCondId !== '', function ($q) use ($legacyCondId) { $q->where('legacy_cond_id', $legacyCondId); }) ->when($legacyCondId === null || $legacyCondId === '', function ($q) use ($codice) { $q->where('codice_unita', $codice); }) ->orderByDesc('id') ->first(); if (!$this->isDryRun && $unitaInserted?->id) { $this->upsertUnitaNominativiFromLegacy((int) $unitaInserted->id, (int) $rowStabileId, $legacyCondId, $r); } // Se è pertinenza, memorizza relazione da collegare in un secondo pass if ($ptype) { $pertinenzeToLink[] = [ 'stabile_id' => $rowStabileId, 'scala' => $r->scala ?? 'A', 'interno' => (string)($pcode ?: $interno), 'tipo' => $ptype, 'proprietario_cognome' => $r->cognome ?? null, 'proprietario_nome' => $r->nome ?? null, ]; } } else { // schema semplice (anagrafiche base) $criteria = ['stabile_id' => $this->stabileId]; if ($interno !== null) $criteria['interno'] = (string)$interno; $exists = DB::table('unita_immobiliari')->where($criteria)->first(); if ($exists) { // Aggiorna palazzina/scala se in split palazzine if ($this->splitMode === 'palazzine') { $patch = []; if (Schema::hasColumn('unita_immobiliari', 'palazzina') && (empty($exists->palazzina) || $exists->palazzina !== $scalaVal)) { $patch['palazzina'] = $scalaVal; } if (Schema::hasColumn('unita_immobiliari', 'scala') && (empty($exists->scala) || $exists->scala !== $scalaVal)) { $patch['scala'] = $scalaVal; } if (!empty($patch)) { $patch['updated_at'] = now(); if (!$this->isDryRun) DB::table('unita_immobiliari')->where('id', $exists->id)->update($patch); $updated++; } } continue; } $data = [ 'stabile_id' => $rowStabileId, 'interno' => (string)$interno, 'scala' => $scalaVal, 'piano' => $pianoVal, 'subalterno' => (string)$sub, 'categoria_catastale' => null, 'superficie' => null, 'vani' => null, 'indirizzo' => $r->indirizzo_corrispondenza ?? null, 'note' => null, 'created_at' => now(), 'updated_at' => now() ]; $this->dynamicInsert('unita_immobiliari', $data); } $count++; $processed++; if ($limit && $processed >= $limit) { break 2; } } $offset += $batch; } // Cleanup: rimuovi unità non presenti in condomin (evita "alieni") if (!$this->isDryRun && $this->option('stabile')) { $condRows = DB::connection('gescon_import')->table('condomin') ->where('cod_stabile', $this->option('stabile')) ->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 = DB::connection('gescon_import')->table('condomin'); if ($this->option('stabile')) { $q->where('cod_stabile', $this->option('stabile')); } if ($this->option('scala')) { $q->where('scala', $this->option('scala')); } $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; while (true) { $q = DB::connection('gescon_import')->table('condomin'); if ($this->option('stabile')) { $q->where('cod_stabile', $this->option('stabile')); } if ($this->option('scala')) { $q->where('scala', $this->option('scala')); } $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; } // Resolve soggetto (già creato in stepSoggetti) $sog = $this->findExistingSoggetto(trim($r->codice_fiscale ?? '') ?: null, trim($r->email ?? '') ?: null, $r->nome ?? null, $r->cognome ?? null); if (!$sog) { // crea minimale se proprio assente $payload = [ 'nome' => $r->nome ?? null, 'cognome' => $r->cognome ?? null, 'codice_fiscale' => (trim($r->codice_fiscale ?? '') ?: null), 'email' => (trim($r->email ?? '') ?: null), 'tipo' => ($r->inquilino ?? 0) ? 'inquilino' : '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', $r->cod_stabile ?? 'S0', $baseKey); $tries = 0; while (DB::table('soggetti')->where('codice_univoco', $code)->exists() && $tries < 5) { $salt = ($r->cod_cond ?? '') . '-' . ($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(); } } $tipo = ($r->proprietario ?? 0) ? 'proprieta' : (($r->inquilino ?? 0) ? 'locazione' : 'proprieta'); $percPossesso = isset($r->perc_diritto_reale) && is_numeric($r->perc_diritto_reale) ? (float) $r->perc_diritto_reale : null; $percDetrazione = isset($r->perc_detrazione) && is_numeric($r->perc_detrazione) ? (float) $r->perc_detrazione : null; $exists = DB::table('proprieta') ->where('unita_immobiliare_id', $unita->id) ->where('soggetto_id', $sog->id) ->first(); if ($exists) { $patch = []; if (($exists->tipo_diritto ?? '') !== $tipo) { $patch['tipo_diritto'] = $tipo; } if ($tipo === 'proprieta' && $percPossesso !== null && (float) ($exists->percentuale_possesso ?? 0) !== $percPossesso) { $patch['percentuale_possesso'] = $percPossesso; } if ($tipo === 'proprieta' && Schema::hasColumn('proprieta', 'percentuale_detrazione') && $percDetrazione !== null && (float) ($exists->percentuale_detrazione ?? 0) !== $percDetrazione) { $patch['percentuale_detrazione'] = $percDetrazione; } 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' ? $percPossesso : null, 'percentuale_detrazione' => ($tipo === 'proprieta' && Schema::hasColumn('proprieta', 'percentuale_detrazione')) ? $percDetrazione : null, 'data_inizio' => null, 'data_fine' => null, '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 = DB::connection('gescon_import')->table('comproprietari'); if ($this->option('stabile')) { $q->where('cod_stabile', $this->option('stabile')); } $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: ogni unità deve avere una riga per ogni tabella if ($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 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; $exists = null; if (Schema::hasColumn('voci_spesa', 'codice')) { $q = DB::table('voci_spesa')->where('codice', $cod); if ($stabileId && Schema::hasColumn('voci_spesa', 'stabile_id')) { $q->where('stabile_id', $stabileId); } $tipoLegacy = match ($v->v_ors ?? $v->V_ORS ?? '') { 'R' => 'riscaldamento', 'S' => 'straordinaria', default => 'ordinaria' }; $gestioneId = $this->resolveGestioneContabileIdForStabile($stabileId, $legacyYear, $tipoLegacy, null); 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', $cod) ->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)) { $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'); } $tipoLegacy = match ($v->v_ors ?? $v->V_ORS ?? '') { 'R' => 'riscaldamento', 'S' => 'straordinaria', default => 'ordinaria' }; $gestioneId = $this->resolveGestioneContabileIdForStabile($stabileId, $legacyYear, $tipoLegacy, null); $data = [ 'stabile_id' => (Schema::hasColumn('voci_spesa', 'stabile_id') ? $stabileId : null), 'gestione_contabile_id' => Schema::hasColumn('voci_spesa', 'gestione_contabile_id') ? $gestioneId : null, 'codice' => $cod, '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') ? 'ordinaria' : 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 (empty($rows)) return 0; // Applica mapping personalizzato (se presente) per individuare le colonne legacy $mapLegacy = [ 'nome_banca' => ['banca', 'istituto', 'nome_banca', 'denominazione', 'denom_banca'], 'iban' => ['iban', 'iban_conto', 'iban1'], 'swift' => ['swift', 'bic', 'bic_swift'], 'conto' => ['n_conto', 'numero_conto', 'conto'], 'intestatario' => ['intestatario', 'intestazione', 'titolare'], 'cod_cassa' => ['cod_cassa', 'codcassa', 'codice_cassa', 'modalita_pagamento', 'codice'], 'descrizione' => ['descrizione', 'descr', 'denominazione', 'note'] ]; if (!empty($this->banksMapping) && is_array($this->banksMapping)) { // banksMapping formato: [{legacy:{...}, target:{...}}, ...] → usa chiavi legacy quando compilate foreach ($this->banksMapping as $bm) { if (!is_array($bm)) { continue; } foreach (['nome_banca', 'iban', 'swift', 'conto', 'intestatario', 'cod_cassa'] as $k) { $legacyField = $this->resolveMappingLegacyField($bm, $k); if ($legacyField) { $lk = strtolower(trim($legacyField)); if (!isset($mapLegacy[$k])) { $mapLegacy[$k] = []; } array_unshift($mapLegacy[$k], $lk); } } } } $now = now(); // Verifica se esiste già un conto marcato come "nostro" per questo stabile $nostroAlready = false; if (Schema::hasTable('dati_bancari') && $stabileId) { try { $nostroAlready = DB::table('dati_bancari') ->where('stabile_id', $stabileId) ->where('is_nostro_conto', true) ->exists(); } catch (\Throwable $e) { $nostroAlready = false; } } $created = 0; $updated = 0; $seenKeys = []; foreach ($rows as $r) { // Costruisci payload best-effort cercando tra alias $getFirst = function (array $aliases) use ($r) { foreach ($aliases as $a) { if (array_key_exists($a, $r) && trim((string)$r[$a]) !== '') return trim((string)$r[$a]); } return null; }; $denom = $getFirst($mapLegacy['nome_banca']); $iban = strtoupper(preg_replace('/\s+/', '', (string)($getFirst($mapLegacy['iban']) ?? ''))); $swift = strtoupper((string)($getFirst($mapLegacy['swift']) ?? '')); $numero = $getFirst($mapLegacy['conto']); $intest = $getFirst($mapLegacy['intestatario']); $codCassa = $getFirst($mapLegacy['cod_cassa']); $descr = $getFirst($mapLegacy['descrizione']); if (!$denom && !$iban && !$numero && !$codCassa && !$descr) continue; // riga non informativa // Chiave dedup per stabile: IBAN preferito, altrimenti numero conto $key = $iban ?: ($numero ? ('N:' . $numero) : ($codCassa ? ('C:' . strtoupper(trim($codCassa))) : null)); if (!$key) continue; if (isset($seenKeys[$key])) continue; // evita duplicati da diverse sorgenti $seenKeys[$key] = true; // Caso 1: IBAN presente → tratta come conto bancario (dati_bancari) if (!$iban) { // Nessun IBAN → registra come cassa generica in tabella 'casse' if (Schema::hasTable('casse')) { $code = strtoupper(trim((string)($codCassa ?: ''))); if ($code === '') { // fallback generico evitando collisioni: GEN, GEN1, ... $base = 'GEN'; $code = $base; $idx = 1; while (DB::table('casse')->where('stabile_id', $stabileId)->where('cod_cassa', $code)->exists()) { $code = $base . (++$idx); } } $existsC = DB::table('casse')->where('stabile_id', $stabileId)->where('cod_cassa', $code)->first(); if (!$existsC) { DB::table('casse')->insert([ 'tenant_id' => null, 'stabile_id' => $stabileId, 'cod_cassa' => $code, 'descrizione' => ($descr ?: ($denom ?: 'Cassa generica')), 'tipo' => ($code === 'CON') ? 'cassa_contanti' : 'altro', 'iban' => null, 'intestazione_conto' => $intest ?: null, 'attiva' => true, 'meta' => json_encode(['from' => 'Anagr_casse', 'numero' => $numero]), 'created_at' => $now, 'updated_at' => $now, ]); $created++; } else { $upd = []; if ($descr && (empty($existsC->descrizione) || $existsC->descrizione === 'ND')) $upd['descrizione'] = $descr; if (!empty($upd)) { $upd['updated_at'] = $now; DB::table('casse')->where('id', $existsC->id)->update($upd); $updated++; } } } // Per la cassa contanti "CON" crea/aggiorna anche un DatiBancari di tipo cassa per renderlo visibile nell'UI $codeUp = strtoupper(trim((string)$codCassa)); if ($codeUp === 'CON' && Schema::hasTable('dati_bancari') && $stabileId) { $existingDb = DB::table('dati_bancari') ->where('stabile_id', $stabileId) ->where(function ($q) { $q->whereNull('iban')->orWhere('iban', ''); }) ->where(function ($q) use ($codeUp) { $q->where('legacy_cod_cassa', $codeUp) ->orWhere('numero_conto', $codeUp); }) ->first(); $payloadCassa = [ 'stabile_id' => $stabileId, 'contatto_id' => null, 'tipo_conto' => 'cassa', 'denominazione_banca' => $denom ?: ($descr ?: 'Cassa contanti'), 'numero_conto' => $codeUp, 'iban' => null, 'legacy_cod_cassa' => $codeUp, 'abi' => null, 'cab' => null, 'cin' => null, 'bic_swift' => null, 'intestazione_conto' => $intest ?: null, 'data_saldo_iniziale' => $now->toDateString(), 'saldo_iniziale' => 0, 'valuta' => 'EUR', 'stato_conto' => 'attivo', 'is_nostro_conto' => !$nostroAlready, 'note' => '[GESCON] Cassa contanti', 'updated_at' => $now, ]; if ($existingDb) { DB::table('dati_bancari')->where('id', $existingDb->id)->update($payloadCassa); $updated++; } else { $payloadCassa['created_at'] = $now; DB::table('dati_bancari')->insert($payloadCassa); $created++; $nostroAlready = $nostroAlready || $payloadCassa['is_nostro_conto']; } } // Non creare dati_bancari in assenza di IBAN, passa alla prossima riga if ($limit && ($created + $updated) >= $limit) break; continue; } // Cerca esistente per stabile_id + IBAN/numero $exists = null; if ($iban) { $exists = DB::table('dati_bancari') ->where('stabile_id', $stabileId) ->whereRaw('REPLACE(UPPER(iban)," ","") = ?', [$iban]) ->first(); } if (!$exists && $numero) { $exists = DB::table('dati_bancari') ->where('stabile_id', $stabileId) ->where('numero_conto', $numero) ->first(); } $payload = [ 'stabile_id' => $stabileId, 'contatto_id' => null, 'tipo_conto' => 'corrente', 'denominazione_banca' => $denom ?: 'Banca', 'numero_conto' => $numero, 'iban' => $iban ?: null, 'legacy_cod_cassa' => $codCassa ?: null, 'abi' => null, 'cab' => null, 'cin' => null, 'bic_swift' => $swift ?: null, 'intestazione_conto' => $intest ?: null, 'data_saldo_iniziale' => $now->toDateString(), 'saldo_iniziale' => 0, 'valuta' => 'EUR', 'stato_conto' => 'attivo', 'is_nostro_conto' => false, 'note' => '[GESCON]', 'created_at' => $now, 'updated_at' => $now, ]; if (!$nostroAlready) { // Marca il primo che inseriamo come nostro conto $payload['is_nostro_conto'] = true; $nostroAlready = true; } if ($exists) { // Aggiorna campi vuoti $upd = []; foreach (['denominazione_banca', 'numero_conto', 'iban', 'bic_swift', 'intestazione_conto', 'legacy_cod_cassa'] as $c) { $newV = $payload[$c] ?? null; $oldV = $exists->$c ?? null; if ($newV && (!$oldV || $oldV === 'ND')) $upd[$c] = $newV; } if (!empty($upd)) { $upd['updated_at'] = $now; if (!$this->isDryRun) DB::table('dati_bancari')->where('id', $exists->id)->update($upd); $updated++; } continue; } $this->dynamicInsert('dati_bancari', $payload); // Reperisci il record appena inserito e applica eventuale codice cassa dal mapping try { $inserted = null; if ($iban) { $inserted = DB::table('dati_bancari') ->where('stabile_id', $stabileId) ->whereRaw('REPLACE(UPPER(iban)," ","") = ?', [$iban]) ->orderByDesc('id') ->first(); } if (!$inserted && $numero) { $inserted = DB::table('dati_bancari') ->where('stabile_id', $stabileId) ->where('numero_conto', $numero) ->orderByDesc('id') ->first(); } if ($inserted && !empty($this->banksMapping)) { $assigned = null; foreach ($this->banksMapping as $bm) { if (!is_array($bm)) { continue; } $targetCode = $this->resolveMappingCodiceCassa($bm); if (!$targetCode) { continue; } $legacyCode = $bm['legacy_cod_cassa'] ?? $this->resolveMappingLegacyField($bm, 'cod_cassa'); $legacyCode = $legacyCode ? strtoupper(trim((string)$legacyCode)) : null; $currentLegacyCode = isset($inserted->legacy_cod_cassa) ? strtoupper(trim((string)$inserted->legacy_cod_cassa)) : null; if ($legacyCode && $currentLegacyCode && $legacyCode === $currentLegacyCode) { $assigned = $targetCode; break; } // Compatibilità formato precedente: confronta IBAN/numero reali se disponibili $legacyIban = isset($bm['iban']) ? strtoupper(preg_replace('/\s+/', '', (string)$bm['iban'])) : null; $legacyNum = isset($bm['conto']) ? (string)$bm['conto'] : null; if ($legacyIban && $iban && $legacyIban === $iban) { $assigned = $targetCode; break; } if ($legacyNum && $numero && $legacyNum === (string)$numero) { $assigned = $targetCode; break; } } if ($assigned) { DB::table('dati_bancari')->where('id', $inserted->id)->update([ 'legacy_cod_cassa' => $assigned, 'updated_at' => now(), ]); } } } catch (\Throwable $e) { } $created++; if ($limit && ($created + $updated) >= $limit) break; } if (($created + $updated) > 0) { $this->line(" → conti bancari creati: {$created}, aggiornati: {$updated}"); } // Assicura casse di base per lo stabile: CON (cassa contanti) sempre presente, CDL (cassa lavoro) opzionale try { if (Schema::hasTable('casse') && $stabileId) { // CON: Cassa contanti, sempre $existsCon = DB::table('casse')->where('stabile_id', $stabileId)->where('cod_cassa', 'CON')->first(); if (!$existsCon) { DB::table('casse')->insert([ 'tenant_id' => null, 'stabile_id' => $stabileId, 'cod_cassa' => 'CON', 'descrizione' => 'Cassa contanti', 'tipo' => 'cassa_contanti', 'iban' => null, 'intestazione_conto' => null, 'attiva' => true, 'meta' => json_encode(['seeded' => true]), 'created_at' => now(), 'updated_at' => now(), ]); } // CDL: Cassa di lavoro (crea se non esiste; opzionale ma utile) $existsCdl = DB::table('casse')->where('stabile_id', $stabileId)->where('cod_cassa', 'CDL')->first(); if (!$existsCdl) { DB::table('casse')->insert([ 'tenant_id' => null, 'stabile_id' => $stabileId, 'cod_cassa' => 'CDL', 'descrizione' => 'Cassa di lavoro', 'tipo' => 'altro', 'iban' => null, 'intestazione_conto' => null, 'attiva' => true, 'meta' => json_encode(['seeded' => true]), 'created_at' => now(), 'updated_at' => now(), ]); } } } catch (\Throwable $e) { // non bloccare la pipeline per errori di seeding casse } return $created + $updated; } private function stepOperazioni(?int $limit): int { if (!Schema::connection('gescon_import')->hasTable('operazioni')) return 0; $src = DB::connection('gescon_import')->table('operazioni'); if ($limit) $src->limit($limit); $count = 0; foreach ($src->get() as $o) { $legacy = $o->id_operaz ?? null; if (!$legacy) continue; // Se la tabella dominio non esiste, in dry-run consideriamo solo il conteggio e saltiamo le scritture if (!Schema::hasTable('operazioni_contabili')) { $count++; continue; } $exists = DB::table('operazioni_contabili')->where('legacy_id', $legacy)->first(); if ($exists) continue; $data = [ 'tenant_id' => null, 'gestione_id' => $this->resolveGestioneId($o->anno ?? null, $o->compet ?? null, $o->gestione ?? null), 'legacy_id' => $legacy, 'descrizione' => $o->note ?? ($o->natura ?? 'Operazione'), 'data_operazione' => $o->dt_spe ?? now()->toDateString(), 'compet' => in_array($o->compet ?? 'C', ['C', 'P']) ? $o->compet : 'C', 'natura2' => $o->natura2 ?? null, 'n_stra' => $o->n_stra ?? 0, 'protocollo_numero' => $this->nextProtocolNumber(), 'protocollo_completo' => null, 'dare' => (float)($o->importo_spese ?? 0), 'avere' => (float)($o->importo_entrate ?? 0), 'conto_contabile' => $o->cod_spe ?? null, 'stato_operazione' => 'confermata', 'created_at' => now(), 'updated_at' => now() ]; // Collega conto bancario se la colonna esiste nello schema if (Schema::hasColumn('operazioni_contabili', 'conto_bancario_id')) { $contoId = null; // 1) prova via cod_cassa della riga (se presente in staging operazioni) $codCassa = $o->cod_cassa ?? $o->modalita_pagamento ?? null; if ($codCassa) { $contoId = $this->resolveContoByCodCassa($this->stabileId, (string)$codCassa); } // 2) fallback al default per stabile if (!$contoId) { $contoId = $this->resolveDefaultContoBancarioId($this->stabileId); } if ($contoId) { $data['conto_bancario_id'] = $contoId; } } // Se previsto, collega cassa (CON/CDL/altre) quando non c'è un conto bancario if (Schema::hasColumn('operazioni_contabili', 'cassa_id') && empty($data['conto_bancario_id'] ?? null)) { $codCassa = $o->cod_cassa ?? $o->modalita_pagamento ?? null; if ($codCassa) { $cassaId = $this->resolveCassaByCodCassa($this->stabileId, (string)$codCassa); if ($cassaId) { $data['cassa_id'] = $cassaId; } } } $this->dynamicInsert('operazioni_contabili', $data); $count++; } return $count; } private function stepRate(?int $limit): int { if (Schema::connection('gescon_import')->hasTable('rate_emissioni_dettaglio') && Schema::hasTable('rate_emesse')) { return $this->importRateEmesseDaEmissioniDettaglio($limit); } if (!Schema::connection('gescon_import')->hasTable('rate')) return 0; $src = DB::connection('gescon_import')->table('rate as r'); // Filtro per stabile: se rate.cod_stabile non esiste, prova join con condomin usando r.cod_cond o r.id_cond if ($this->option('stabile')) { $stab = $this->option('stabile'); if (Schema::connection('gescon_import')->hasColumn('rate', 'cod_stabile')) { $src->where('r.cod_stabile', $stab); } elseif ( Schema::connection('gescon_import')->hasTable('condomin') && (Schema::connection('gescon_import')->hasColumn('rate', 'cod_cond') || Schema::connection('gescon_import')->hasColumn('rate', 'id_cond')) && Schema::connection('gescon_import')->hasColumn('condomin', 'cod_stabile') ) { $joinLeft = Schema::connection('gescon_import')->hasColumn('rate', 'cod_cond') ? 'cod_cond' : 'id_cond'; $src = $src->join('condomin as c', 'c.' . $joinLeft, '=', 'r.' . $joinLeft) ->where('c.cod_stabile', $stab) ->select('r.*'); } } if ($limit) $src->limit($limit); $count = 0; foreach ($src->get() as $r) { $legacy = $r->id ?? ($r->n_rata ?? null) ?? ($r->numero_rata ?? null) ?? ($r->n_emissione ?? null); if (!$legacy) { // Fallback deterministico: hash di campi tipici $key = implode('|', [ (string)($r->cod_stabile ?? $this->option('stabile') ?? ''), (string)($r->cod_cond ?? $r->id_cond ?? ''), (string)($r->data_emissione ?? ''), (string)($r->importo ?? $r->importo_euro ?? ''), (string)($r->causale ?? '') ]); $legacy = hexdec(substr(hash('crc32b', $key), 0, 8)); } if (!$legacy) continue; if (Schema::hasColumn('rate_emesse', 'legacy_id')) { // tabella evoluta con legacy $exists = DB::table('rate_emesse')->where('legacy_id', $legacy)->first(); if ($exists) continue; $data = [ 'legacy_id' => $legacy, 'periodo_anno' => $r->data_emissione ? (int)date('Y', strtotime($r->data_emissione)) : null, 'periodo_mese' => $r->data_emissione ? (int)date('n', strtotime($r->data_emissione)) : null, 'importo' => $r->importo ?? 0, 'descrizione' => $r->causale ?? null, 'competenza_da' => $r->data_emissione ?? null, 'competenza_a' => $r->data_scadenza ?? null, 'created_at' => now(), 'updated_at' => now() ]; $this->dynamicInsert('rate_emesse', $data); } else { // Fallback: tabella rate_emesse originale (richiede molti campi); skip per ora continue; } $count++; } return $count; } private function importRateEmesseDaEmissioniDettaglio(?int $limit): int { if (!Schema::connection('gescon_import')->hasTable('rate_emissioni_dettaglio')) return 0; if (!Schema::connection('gescon_import')->hasTable('rate_emissioni')) return 0; $stab = $this->option('stabile'); $src = DB::connection('gescon_import')->table('rate_emissioni_dettaglio as d'); if ($stab) { $src->where('d.cod_stabile', $stab); } if ($limit) $src->limit($limit); $emissioni = DB::connection('gescon_import')->table('rate_emissioni') ->when($stab, fn($q) => $q->where('cod_stabile', $stab)) ->get() ->groupBy(fn($r) => (string) $r->numero_emissione); $piani = []; $count = 0; $cumuloCache = []; $hasCondomin = Schema::connection('gescon_import')->hasTable('condomin'); foreach ($src->get() as $row) { $codStabile = (string) ($row->cod_stabile ?? $stab ?? ''); $numeroEmissione = (int) ($row->numero_emissione ?? 0); if ($codStabile === '' || $numeroEmissione <= 0) { continue; } $codCond = (string) ($row->cod_cond ?? ''); $unitaId = $this->resolveUnitaIdFromCodCond($codStabile, $codCond); if (!$unitaId) { continue; } $soggettoId = $this->resolveSoggettoResponsabileId($unitaId, (string) ($row->tipo_soggetto ?? '')); if (!$soggettoId) { continue; } if (!isset($piani[$numeroEmissione])) { $piani[$numeroEmissione] = $this->ensurePianoRateizzazionePerEmissione($codStabile, $numeroEmissione, $emissioni); } $pianoId = $piani[$numeroEmissione]; if (!$pianoId) { continue; } $importo = is_numeric($row->importo_dovuto_euro ?? null) ? (float) $row->importo_dovuto_euro : (is_numeric($row->importo_dovuto ?? null) ? (float) $row->importo_dovuto : 0.0); $numeroRata = is_numeric($row->numero_ricevuta ?? null) ? (int) $row->numero_ricevuta : (is_numeric($row->numero_mese ?? null) ? (int) $row->numero_mese : 1); $dataEmissione = $row->data_emissione ?? null; $dataScadenza = $row->data_scadenza ?? null; $emissioneRows = $emissioni[(string) $numeroEmissione] ?? null; $emissioneRow = $emissioneRows ? $emissioneRows->first() : null; if (!$dataEmissione && $emissioneRow?->data_emissione) { $dataEmissione = $emissioneRow->data_emissione; } if (!$dataScadenza && $emissioneRow?->data_scadenza) { $dataScadenza = $emissioneRow->data_scadenza; } $numeroRataEff = $numeroRata; $condInq = trim((string) ($row->cond_inq ?? $row->tipo_soggetto ?? '')); $raggruppamento = trim((string) ($row->raggruppamento ?? '')); if ($raggruppamento === '' && $hasCondomin && $codCond !== '') { $cacheKey = $codStabile . '|' . $codCond; if (!array_key_exists($cacheKey, $cumuloCache)) { $cumuloCache[$cacheKey] = DB::connection('gescon_import') ->table('condomin') ->where('cod_stabile', $codStabile) ->where('cod_cond', $codCond) ->first(['cumulo_cond', 'cumulo_inq']); } $cumuloRow = $cumuloCache[$cacheKey]; if ($cumuloRow) { $raggruppamento = trim((string) ( strtoupper($condInq) === 'I' ? ($cumuloRow->cumulo_inq ?? '') : ($cumuloRow->cumulo_cond ?? '') )); } } $noteParts = [ 'n_emissione=' . $numeroEmissione, 'anno_emissione=' . (string) ($row->anno_emissione ?? ''), 'cod_cond_src=' . $codCond, 'o_r_s=' . (string) ($row->tipo_quota ?? $row->tipo_soggetto ?? ''), 'n_mese=' . (string) ($row->numero_mese ?? ''), $condInq !== '' ? 'cond_inq=' . $condInq : '', $raggruppamento !== '' ? 'raggruppamento=' . $raggruppamento : '', ]; $existing = DB::table('rate_emesse') ->where('piano_rateizzazione_id', $pianoId) ->where('unita_immobiliare_id', $unitaId) ->where('soggetto_responsabile_id', $soggettoId) ->where('numero_rata_progressivo', $numeroRataEff) ->first(); if ($existing) { $descr = (string) ($row->descrizione ?? $emissioneRow?->descrizione ?? ''); $descrizione = (string) ($existing->descrizione ?? ''); if ($descr !== '' && $descrizione !== '' && stripos($descrizione, $descr) === false) { $descrizione = trim($descrizione . ' | ' . $descr); } elseif ($descrizione === '' && $descr !== '') { $descrizione = $descr; } $noteAgg = implode('|', array_filter($noteParts, fn($v) => $v !== '')); $note = (string) ($existing->note ?? ''); if ($note === '') { $note = $noteAgg; } elseif ($noteAgg !== '' && stripos($note, $noteAgg) === false) { $note = trim($note . ' | ' . $noteAgg); } DB::table('rate_emesse') ->where('id', $existing->id) ->update([ 'descrizione' => $descrizione, 'importo_originario_unita' => (float) ($existing->importo_originario_unita ?? 0) + $importo, 'importo_addebitato_soggetto' => (float) ($existing->importo_addebitato_soggetto ?? 0) + $importo, 'note' => $note, 'updated_at' => now(), ]); continue; } // Evita duplicati: stessa emissione, unità, soggetto, rata e date $noteNeedle = '%n_emissione=' . $numeroEmissione . '%' . 'cod_cond_src=' . $codCond . '%' . 'o_r_s=' . (string) ($row->tipo_quota ?? $row->tipo_soggetto ?? '') . '%' . 'n_mese=' . (string) ($row->numero_mese ?? '') . '%'; $dupExists = DB::table('rate_emesse') ->where('piano_rateizzazione_id', $pianoId) ->where('unita_immobiliare_id', $unitaId) ->where('soggetto_responsabile_id', $soggettoId) ->where('numero_rata_progressivo', $numeroRataEff) ->whereDate('data_emissione', $dataEmissione ?? now()->toDateString()) ->whereDate('data_scadenza', $dataScadenza ?? ($dataEmissione ?? now()->toDateString())) ->where('note', 'like', $noteNeedle) ->exists(); if ($dupExists) { continue; } $payload = [ 'piano_rateizzazione_id' => $pianoId, 'unita_immobiliare_id' => $unitaId, 'soggetto_responsabile_id' => $soggettoId, 'numero_rata_progressivo' => $numeroRataEff, 'descrizione' => $row->descrizione ?? ($emissioneRow?->descrizione ?? null), 'importo_originario_unita' => $importo, 'percentuale_addebito_soggetto' => 100.0, 'importo_addebitato_soggetto' => $importo, 'data_emissione' => $dataEmissione ?? now()->toDateString(), 'data_scadenza' => $dataScadenza ?? ($dataEmissione ?? now()->toDateString()), 'stato_rata' => 'EMESSA', 'data_ultimo_pagamento' => null, 'importo_pagato' => 0, 'note' => implode('|', array_filter($noteParts, fn($v) => $v !== '')), 'created_at' => now(), 'updated_at' => now(), ]; $this->dynamicInsert('rate_emesse', $payload); $count++; } return $count; } private function resolveUnitaIdFromCodCond(string $codStabile, string $codCond): ?int { $codCond = trim($codCond); if ($codCond === '') return null; $stabileId = $this->stabileId; if (Schema::hasTable('stabili')) { $stabileCol = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; $stRow = DB::table('stabili')->where($stabileCol, $codStabile)->first(); if ($stRow) { $stabileId = $stRow->id; } } if (!$stabileId) { return null; } $cond = null; if (Schema::connection('gescon_import')->hasTable('condomin')) { $cond = 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 ($limit) $src->limit($limit); $count = 0; foreach ($src->get() as $s) { $legacy = $s->id_stra ?? null; if (!$legacy) continue; $exists = DB::table('lavori_straordinari')->where('legacy_id', $legacy)->first(); if ($exists) continue; $data = [ 'legacy_id' => $legacy, 'codice' => $s->codice ?? ('STRA-' . $legacy), 'descrizione' => $s->descriz_prev_cons ?? '', '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' => $s->natura ?? 'spesa', 'gruppo' => $s->aggregazione ?? null, 'periodicita' => $s->periodicita ?? null, 'created_at' => now(), 'updated_at' => now() ]; $this->dynamicInsert('lavori_straordinari', $data); $count++; } return $count; } 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 ($limit) $src->limit($limit); $count = 0; foreach ($src->get() as $d) { $legacy = $d->id ?? null; if (!$legacy) continue; $exists = DB::table('detrazioni_fiscali_dom')->where('legacy_rif', $legacy)->first(); if ($exists) continue; $data = [ 'legacy_rif' => $legacy, 'descrizione' => $d->tipo_detrazione ?? '', 'tipologia' => $d->tipo_detrazione ?? null, 'importo' => $d->importo_euro ?? 0, 'cessione_flag' => false, 'importi_json' => json_encode([ 'tot' => $d->importo_detraibile ?? null, 'bon' => null, 'non_bon' => null ]), 'created_at' => now(), 'updated_at' => now() ]; $this->dynamicInsert('detrazioni_fiscali_dom', $data); $count++; } return $count; } private function stub(string $table, ?int $limit): int { // Placeholder: sostituire con logica di lettura da MDB / staging $exists = DB::table($table)->count(); return $limit ? min($exists, $limit) : $exists; } private function preflightStaging(): void { try { $stab = $this->option('stabile'); if (!$stab) return; // Autoderiva --mdb-condomin se non fornito e abbiamo --path/--anno if (!$this->option('mdb-condomin')) { $root = rtrim((string)($this->option('path') ?: '/mnt/gescon-archives/gescon'), '/'); $anno = $this->option('anno') ? sprintf('%04d', (int)$this->option('anno')) : null; if ($anno) { $cand = $root . '/' . $stab . '/' . $anno . '/singolo_anno.mdb'; if (is_file($cand)) { // Hack: set option at runtime using Symfony input if available try { $this->input->setOption('mdb-condomin', $cand); } catch (\Throwable $e) { } } } } // 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; $hasCodCol = Schema::connection('gescon_import')->hasColumn('condomin', 'cod_stabile'); $needsLoad = false; if ($hasCodCol) { $q = DB::connection('gescon_import')->table('condomin')->where('cod_stabile', $stab); $needsLoad = !$q->exists(); } else { $needsLoad = DB::connection('gescon_import')->table('condomin')->count() === 0; } $legacyYearOpt = $this->option('anno') ? sprintf('%04d', (int)$this->option('anno')) : null; 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); } } // 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 = DB::connection('gescon_import')->table('dett_tab')->count() === 0; 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 = DB::connection('gescon_import')->table('tabelle_millesimali')->count() === 0; 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 = DB::connection('gescon_import')->table('voc_spe')->count() === 0; if ($need) { $this->callSilently('gescon:load-mdb', [ '--mdb' => $mdbCondomin, '--table' => 'voc_spe', '--stabile' => $stab, '--legacy-year' => $legacyYearOpt, ]); } } // Cre_Deb_preced (crediti/debiti pregressi) if (Schema::connection('gescon_import')->hasTable('cre_deb_preced')) { if (Schema::connection('gescon_import')->hasColumn('cre_deb_preced', 'cod_stabile')) { $need = !DB::connection('gescon_import')->table('cre_deb_preced')->where('cod_stabile', $stab)->exists(); } else { $need = DB::connection('gescon_import')->table('cre_deb_preced')->count() === 0; } 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 = DB::connection('gescon_import')->table($tb)->count() === 0; 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 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("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')) { DB::table('proprieta')->where('soggetto_id', $rm->id)->update(['soggetto_id' => $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 findUnitaByStagingRow($r) { if (!Schema::hasTable('unita_immobiliari')) return null; $crit = []; if ($this->stabileId !== null && Schema::hasColumn('unita_immobiliari', 'stabile_id')) { $crit['stabile_id'] = $this->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 || trim($int) === '') && isset($r->cod_cond)) { $int = (string) $r->cod_cond; } if ($int !== null) { $crit['interno'] = $int; } } if (!empty($crit)) { return DB::table('unita_immobiliari')->where($crit)->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['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 [ '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 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 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 (!$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 !== '') { $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 upsertUnitaNominativiFromLegacy(int $unitaId, int $stabileId, ?string $legacyCondId, object $row): void { if (!Schema::hasTable('unita_immobiliare_nominativi')) { return; } $payloads = []; $ownerStart = $this->normalizeLegacyDate($row->subentrato_dal ?? $row->proprietario_subentrato_dal ?? null); $ownerEnd = $this->normalizeLegacyDate($row->attivo_fino_al ?? $row->proprietario_attivo_fino_al ?? null); $subentroPrima = trim((string) ($row->subentro_prima_cera ?? '')); $subentroAdesso = trim((string) ($row->subentro_adesso_ce ?? '')); $resolveOwnerName = function (?object $source) use ($row): ?string { $src = $source ?: $row; $name = trim((string) ($src->proprietario_denominazione ?? $src->nom_cond ?? '')); if ($name === '') { $name = trim((string) (($src->cognome ?? '') . ' ' . ($src->nome ?? ''))); } return $name !== '' ? $name : null; }; $resolveLegacyRow = function (string $idCond) use ($row): ?object { if ($idCond === '') { return null; } $query = DB::connection('gescon_import') ->table('condomin') ->where('id_cond', $idCond); if (!empty($row->cod_stabile)) { $query->where('cod_stabile', $row->cod_stabile); } if (property_exists($row, 'legacy_year') && !empty($row->legacy_year)) { $query->where('legacy_year', $row->legacy_year); } return $query->first(); }; $ownerPerc = isset($row->proprietario_diritto_percentuale) && is_numeric($row->proprietario_diritto_percentuale) ? (float) $row->proprietario_diritto_percentuale : null; if ($subentroPrima !== '') { $oldRow = $resolveLegacyRow($subentroPrima); $oldName = $resolveOwnerName($oldRow); $oldStart = $this->normalizeLegacyDate($oldRow?->subentrato_dal ?? null); $oldEnd = $ownerStart ?: $this->normalizeLegacyDate($oldRow?->attivo_fino_al ?? null); if ($oldName || $oldStart || $oldEnd) { $payloads[] = [ 'ruolo' => 'C', 'nominativo' => $oldName, 'data_inizio' => $oldStart, 'data_fine' => $oldEnd, 'percentuale' => $ownerPerc, 'legacy_cond_id' => $subentroPrima, ]; } } $newOwnerRow = $subentroAdesso !== '' ? $resolveLegacyRow($subentroAdesso) : null; $newOwnerName = $resolveOwnerName($newOwnerRow); $newOwnerStart = $ownerStart ?: $this->normalizeLegacyDate($newOwnerRow?->subentrato_dal ?? null); $newOwnerEnd = $ownerEnd ?: $this->normalizeLegacyDate($newOwnerRow?->attivo_fino_al ?? null); if (!empty($newOwnerName) || $newOwnerStart || $newOwnerEnd) { $payloads[] = [ 'ruolo' => 'C', 'nominativo' => $newOwnerName, 'data_inizio' => $newOwnerStart, 'data_fine' => $newOwnerEnd, 'percentuale' => $ownerPerc, 'legacy_cond_id' => $subentroAdesso !== '' ? $subentroAdesso : $legacyCondId, ]; } $fallbackOwnerName = $resolveOwnerName(null); if (empty($payloads) && ($fallbackOwnerName || $ownerStart || $ownerEnd)) { $payloads[] = [ 'ruolo' => 'C', 'nominativo' => $fallbackOwnerName, 'data_inizio' => $ownerStart, 'data_fine' => $ownerEnd, 'percentuale' => $ownerPerc, 'legacy_cond_id' => $legacyCondId, ]; } $tenantName = trim((string) ($row->inquilino_denominazione ?? $row->inquil_nome ?? $row->inquil ?? '')); $tenantStart = $this->normalizeLegacyDate($row->inquil_dal ?? $row->inquilino_subentrato_dal ?? null); $tenantEnd = $this->normalizeLegacyDate($row->inquil_al ?? $row->inquilino_attivo_fino_al ?? null); $tenantPerc = isset($row->inquilino_diritto_percentuale) && is_numeric($row->inquilino_diritto_percentuale) ? (float) $row->inquilino_diritto_percentuale : null; if (!empty($tenantName) || $tenantStart || $tenantEnd) { $payloads[] = [ 'ruolo' => 'I', 'nominativo' => $tenantName !== '' ? $tenantName : null, 'data_inizio' => $tenantStart, 'data_fine' => $tenantEnd, 'percentuale' => $tenantPerc, 'legacy_cond_id' => $legacyCondId, ]; } foreach ($payloads as $p) { $match = [ 'unita_immobiliare_id' => $unitaId, 'stabile_id' => $stabileId, 'legacy_cond_id' => $p['legacy_cond_id'] ?? $legacyCondId, 'ruolo' => $p['ruolo'], 'data_inizio' => $p['data_inizio'], 'data_fine' => $p['data_fine'], ]; $exists = DB::table('unita_immobiliare_nominativi')->where($match)->first(); $values = array_merge($match, [ 'nominativo' => $p['nominativo'], 'percentuale' => $p['percentuale'], 'fonte' => 'legacy_condomin', 'legacy_payload' => json_encode([ 'legacy_year' => $row->legacy_year ?? null, 'cod_cond' => $match['legacy_cond_id'], 'subentro_prima_cera' => $subentroPrima !== '' ? $subentroPrima : null, 'subentro_adesso_ce' => $subentroAdesso !== '' ? $subentroAdesso : null, ]), 'updated_at' => now(), ]); if ($exists) { DB::table('unita_immobiliare_nominativi')->where('id', $exists->id)->update($values); } else { $values['created_at'] = now(); DB::table('unita_immobiliare_nominativi')->insert($values); } } } private function normalizeLegacyCalcolo(?string $raw): ?string { $v = strtolower(trim((string) ($raw ?? ''))); if ($v === '') { return null; } return match ($v) { 'm' => 'millesimi', 'a', 'c' => 'a_consumo', 'x' => 'conguagli', 'p' => 'personali', default => $v, }; } private function normalizeLegacyTipo(?string $raw): ?string { $v = strtoupper(trim((string) ($raw ?? ''))); if ($v === '') { return null; } return match ($v) { 'O', 'ORD', 'ORDINARIA' => 'O', 'R', 'RISC', 'RISCALDAMENTO' => 'R', 'S', 'STRA', 'STRAORDINARIA' => 'S', default => $v, }; } private function resolveTipoTabellaFromLegacy(?string $tipoLegacy, ?string $calcolo, ?string $codice): ?string { $c = strtolower(trim((string) ($calcolo ?? ''))); $code = strtoupper(trim((string) ($codice ?? ''))); if ($code === 'ACQUA' || in_array($c, ['a_consumo', 'acqua', 'consumo', 'c', 'a'], true)) { return 'custom'; } return match ($tipoLegacy) { 'R' => 'riscaldamento', default => 'custom', }; } private function resolveGestioneContabileIdForStabile(?int $stabileId, ?string $legacyYear, ?string $tipoLegacy, ?int $numeroStraordinaria = null): ?int { if (!Schema::hasTable('gestioni_contabili')) { return null; } $anno = $this->resolveAnnoFromLegacyYear($legacyYear) ?: (is_string($this->option('anno')) ? (string) $this->option('anno') : null); if (!$anno || !is_numeric($anno)) { return null; } $tipoLegacy = is_string($tipoLegacy) ? strtoupper(trim($tipoLegacy)) : null; $tipo = match ($tipoLegacy) { 'R', 'RISCALDAMENTO' => 'riscaldamento', 'S', 'STRAORDINARIA' => 'straordinaria', default => 'ordinaria', }; $q = DB::table('gestioni_contabili') ->where('anno_gestione', (int) $anno) ->where('tipo_gestione', $tipo); if ($stabileId && Schema::hasColumn('gestioni_contabili', 'stabile_id')) { $q->where('stabile_id', $stabileId); } if ($tipo === 'straordinaria' && $numeroStraordinaria !== null && Schema::hasColumn('gestioni_contabili', 'numero_straordinaria')) { $q->where('numero_straordinaria', $numeroStraordinaria); } $match = $q->orderByDesc('gestione_attiva')->orderByDesc('id')->first(); if ($match) { return $match->id; } if ($this->option('dry-run')) { return null; } $payload = [ 'tenant_id' => 'T1', 'stabile_id' => $stabileId, 'anno_gestione' => (int) $anno, 'tipo_gestione' => $tipo, 'numero_straordinaria' => $tipo === 'straordinaria' ? ($numeroStraordinaria ?? 1) : null, 'denominazione' => strtoupper(substr($tipo, 0, 1)) . $anno, 'data_inizio' => date('Y-01-01', strtotime($anno . '-01-01')), 'data_fine' => date('Y-12-31', strtotime($anno . '-12-31')), 'stato' => 'aperta', 'protocollo_prefix' => strtoupper(substr($tipo, 0, 1)) . $anno . ($tipo === 'straordinaria' ? '-' . str_pad((string) ($numeroStraordinaria ?? 1), 2, '0', STR_PAD_LEFT) : ''), 'ultimo_protocollo' => 0, 'created_at' => now(), 'updated_at' => now(), ]; $cols = Schema::getColumnListing('gestioni_contabili'); $insert = array_intersect_key($payload, array_flip($cols)); DB::table('gestioni_contabili')->insert($insert); $match = DB::table('gestioni_contabili') ->where('anno_gestione', (int) $anno) ->where('tipo_gestione', $tipo) ->when($stabileId && Schema::hasColumn('gestioni_contabili', 'stabile_id'), fn($qq) => $qq->where('stabile_id', $stabileId)) ->orderByDesc('id') ->first(); return $match?->id; } private function resolveGestioneId($anno, $compet, $tipo): ?int { if (!Schema::hasTable('gestioni_contabili')) return null; // Normalize tipo from O/R/S to descriptive if needed $map = ['O' => 'ordinaria', 'R' => 'riscaldamento', 'S' => 'straordinaria']; $tipo = $map[$tipo ?? 'O'] ?? ($tipo ?: 'ordinaria'); $anno = $anno ?: date('Y'); $row = DB::table('gestioni_contabili') ->where(['anno_gestione' => (int)$anno, 'tipo_gestione' => $tipo]) ->first(); if ($row) return $row->id; // In dry-run, avoid creating missing gestione if ($this->option('dry-run')) return null; // Create minimal compatible record satisfying not-null constraints $data = [ 'tenant_id' => 'T1', 'anno_gestione' => (int)$anno, 'tipo_gestione' => $tipo, 'numero_straordinaria' => null, 'denominazione' => strtoupper(substr($tipo, 0, 1)) . $anno, 'data_inizio' => date('Y-01-01', strtotime($anno . '-01-01')), 'data_fine' => date('Y-12-31', strtotime($anno . '-12-31')), 'stato' => 'aperta', 'protocollo_prefix' => strtoupper(substr($tipo, 0, 1)) . $anno, 'ultimo_protocollo' => 0, 'created_at' => now(), 'updated_at' => now() ]; DB::table('gestioni_contabili')->insert($data); $row = DB::table('gestioni_contabili')->where(['anno_gestione' => (int)$anno, 'tipo_gestione' => $tipo])->first(); return $row?->id; } private function dynamicInsert(string $table, array $data): void { if (!Schema::hasTable($table)) return; // silently skip if ($this->isDryRun) return; // skip writes in dry-run $cols = Schema::getColumnListing($table); $filtered = array_intersect_key($data, array_flip($cols)); if (!isset($filtered['created_at']) && in_array('created_at', $cols)) $filtered['created_at'] = now(); if (!isset($filtered['updated_at']) && in_array('updated_at', $cols)) $filtered['updated_at'] = now(); if (!empty($filtered)) DB::table($table)->insert($filtered); } /** * Risolve l'ID del conto bancario di default per uno stabile. * Preferisce il conto con IBAN che coincide con l'IBAN dello stabile, * altrimenti il primo conto attivo per lo stabile. */ private function resolveDefaultContoBancarioId(?int $stabileId): ?int { if (!$stabileId || !Schema::hasTable('dati_bancari')) return null; if (isset($this->defaultContoCache[$stabileId])) return $this->defaultContoCache[$stabileId]; // 1) Prova match per IBAN tra stabile e dati_bancari $ibanStabile = $this->getIbanForStabile($stabileId); if ($ibanStabile) { $row = DB::table('dati_bancari') ->where('stabile_id', $stabileId) ->whereRaw('REPLACE(UPPER(iban)," ","") = ?', [$ibanStabile]) ->orderByDesc('updated_at') ->first(); if ($row) { return $this->defaultContoCache[$stabileId] = (int)$row->id; } } // 2) Fallback: primo conto attivo per stabile $row = DB::table('dati_bancari') ->where('stabile_id', $stabileId) ->when(Schema::hasColumn('dati_bancari', 'stato_conto'), function ($q) { $q->where('stato_conto', 'attivo'); }) ->orderByDesc('updated_at') ->first(); if ($row) { return $this->defaultContoCache[$stabileId] = (int)$row->id; } // 3) Ultimo fallback: qualsiasi conto dello stabile $row = DB::table('dati_bancari') ->where('stabile_id', $stabileId) ->orderByDesc('updated_at') ->first(); return $this->defaultContoCache[$stabileId] = ($row ? (int)$row->id : null); } /** * Recupera IBAN normalizzato per lo stabile dalle colonne disponibili. */ private function getIbanForStabile(?int $stabileId): ?string { if (!$stabileId || !Schema::hasTable('stabili')) return null; $cols = Schema::getColumnListing('stabili'); $candidate = null; foreach (['iban_condominio', 'iban_principale', 'iban'] as $col) { if (in_array($col, $cols, true)) { $val = DB::table('stabili')->where('id', $stabileId)->value($col); if ($val) { $candidate = $val; break; } } } if (!$candidate) return null; $iban = strtoupper(preg_replace('/\s+/', '', (string)$candidate)); return $iban ?: null; } private function ensureStabilePerScala($parentStabile, string $scala): array { // Ritorna [childId, childCode] if (!Schema::hasTable('stabili')) return [null, null]; $col = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; $parentCode = $col === 'codice_stabile' ? ($parentStabile->codice_stabile ?? null) : ($parentStabile->denominazione ?? null); if (!$parentCode) return [$parentStabile->id ?? null, $parentCode]; $childCode = rtrim($parentCode, '-') . '-' . strtoupper($scala ?: 'A'); if (isset($this->scalaStabileCache[$childCode])) return $this->scalaStabileCache[$childCode]; $child = DB::table('stabili')->where($col, $childCode)->first(); if (!$child && !$this->isDryRun) { // Crea minimale ereditando alcuni campi $payload = [ $col => $childCode, 'denominazione' => ($parentStabile->denominazione ?? $childCode) . ' - Scala ' . strtoupper($scala ?: 'A'), 'indirizzo' => $parentStabile->indirizzo ?? 'ND', 'citta' => $parentStabile->citta ?? 'ND', 'cap' => $parentStabile->cap ?? '00000', 'provincia' => $parentStabile->provincia ?? 'XX', 'amministratore_id' => $parentStabile->amministratore_id ?? null, 'attivo' => true, 'created_at' => now(), 'updated_at' => now(), ]; if (Schema::hasColumn('stabili', 'note')) { $payload['note'] = trim((string)($parentStabile->note ?? '')) . ' [GESCON SCALA]'; } $this->dynamicInsert('stabili', $payload); $child = DB::table('stabili')->where($col, $childCode)->first(); } $res = [$child->id ?? null, $childCode]; $this->scalaStabileCache[$childCode] = $res; return $res; } private function resolveStabileIdForScala(?string $scala): ?int { if ($this->splitMode !== 'stabili-per-scala') return $this->stabileId; $scala = trim((string)($scala ?? 'A')) ?: 'A'; if (!Schema::hasTable('stabili')) return $this->stabileId; $col = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione'; $parentCode = $this->option('stabile'); if (!$parentCode) return $this->stabileId; $childCode = rtrim($parentCode, '-') . '-' . strtoupper($scala); $row = DB::table('stabili')->where($col, $childCode)->first(); if ($row) return $row->id; return $this->stabileId; } private function safeUpdateStabile(int $id, array $update): void { if (empty($update)) return; if ($this->isDryRun) { if ($this->output->isVerbose()) { $this->line(" (dry-run) update stabili skip for id {$id}"); } return; } // Gestisci unicità codice_fiscale con riassegnazione se --force-mapping if (isset($update['codice_fiscale']) && Schema::hasColumn('stabili', 'codice_fiscale')) { $cf = trim((string)$update['codice_fiscale']); if ($cf !== '') { $other = DB::table('stabili')->where('codice_fiscale', $cf)->where('id', '!=', $id)->first(); if ($other) { if ($this->forceMapping) { // Libera il CF dall'altro stabile try { DB::table('stabili')->where('id', $other->id)->update(['codice_fiscale' => null, 'updated_at' => now()]); } catch (\Throwable $e) { // Fallback a 'ND' se NOT NULL DB::table('stabili')->where('id', $other->id)->update(['codice_fiscale' => 'ND', 'updated_at' => now()]); } if ($this->output->isVerbose()) { $this->line(sprintf(' -> riassegnazione CF: liberato da stabile #%d per assegnare a #%d', $other->id, $id)); } } else { // Evita violazione: non aggiornare CF se già in uso unset($update['codice_fiscale']); if ($this->output->isVerbose()) { $this->line(' -> CF già in uso da altro stabile: aggiornamento CF ignorato (no force).'); } } } } } if (!empty($update)) { DB::table('stabili')->where('id', $id)->update($update); } } private function applyStabileMapping(array $update, array $legacyRow, $existing): array { try { $aliases = [ 'comune' => 'citta', 'codice_fiscale' => 'codice_fiscale', 'cod_fisc_amministratore' => 'cod_fisc_amministratore', ]; $columnMap = [ // Identità e indirizzo 'stabile.denominazione' => 'denominazione', 'stabile.indirizzo' => 'indirizzo', 'stabile.cap' => 'cap', 'stabile.citta' => 'citta', 'stabile.provincia' => 'provincia', 'stabile.comune_cod' => 'citta', // best-effort legacy code 'stabile.codice_fiscale' => 'codice_fiscale', // Catasto (scegli colonne presenti) 'stabile.foglio' => (Schema::hasColumn('stabili', 'foglio') ? 'foglio' : (Schema::hasColumn('stabili', 'foglio_catasto') ? 'foglio_catasto' : 'foglio')), 'stabile.foglio_catasto' => (Schema::hasColumn('stabili', 'foglio_catasto') ? 'foglio_catasto' : (Schema::hasColumn('stabili', 'foglio') ? 'foglio' : 'foglio_catasto')), 'stabile.particella' => (Schema::hasColumn('stabili', 'particella') ? 'particella' : (Schema::hasColumn('stabili', 'particella_catasto') ? 'particella_catasto' : (Schema::hasColumn('stabili', 'mappale') ? 'mappale' : 'particella'))), 'stabile.particella_catasto' => (Schema::hasColumn('stabili', 'particella_catasto') ? 'particella_catasto' : (Schema::hasColumn('stabili', 'particella') ? 'particella' : (Schema::hasColumn('stabili', 'mappale') ? 'mappale' : 'particella_catasto'))), 'stabile.mappale' => (Schema::hasColumn('stabili', 'mappale') ? 'mappale' : (Schema::hasColumn('stabili', 'particella') ? 'particella' : (Schema::hasColumn('stabili', 'particella_catasto') ? 'particella_catasto' : 'mappale'))), 'stabile.subalterno' => 'subalterno', 'stabile.categoria' => (Schema::hasColumn('stabili', 'categoria') ? 'categoria' : (Schema::hasColumn('stabili', 'categoria_catastale') ? 'categoria_catastale' : 'categoria')), 'stabile.categoria_catastale' => (Schema::hasColumn('stabili', 'categoria_catastale') ? 'categoria_catastale' : (Schema::hasColumn('stabili', 'categoria') ? 'categoria' : 'categoria_catastale')), 'stabile.classe' => (Schema::hasColumn('stabili', 'classe_catastale') ? 'classe_catastale' : (Schema::hasColumn('stabili', 'classe') ? 'classe' : 'classe_catastale')), 'stabile.consistenza' => (Schema::hasColumn('stabili', 'consistenza_catastale') ? 'consistenza_catastale' : (Schema::hasColumn('stabili', 'consistenza') ? 'consistenza' : 'consistenza_catastale')), 'stabile.rendita_catastale' => 'rendita_catastale', 'stabile.superficie_catastale' => 'superficie_catastale', 'stabile.codice_comune_catasto' => 'codice_comune_catasto', // Caratteristiche edificio 'stabile.numero_palazzine' => 'numero_palazzine', 'stabile.numero_scale_per_palazzina' => 'numero_scale_per_palazzina', 'stabile.numero_piani' => 'numero_piani', 'stabile.piano_seminterrato' => 'piano_seminterrato', 'stabile.piano_sottotetto' => 'piano_sottotetto', 'stabile.ascensore' => 'presenza_ascensore', 'stabile.presenza_ascensore' => 'presenza_ascensore', 'stabile.cortile_giardino' => 'cortile_giardino', 'stabile.superficie_cortile' => 'superficie_cortile', 'stabile.riscaldamento_centralizzato' => 'riscaldamento_centralizzato', 'stabile.acqua_centralizzata' => 'acqua_centralizzata', 'stabile.gas_centralizzato' => 'gas_centralizzato', 'stabile.servizio_portineria' => 'servizio_portineria', 'stabile.orari_portineria' => 'orari_portineria', 'stabile.videocitofono' => 'videocitofono', 'stabile.antenna_tv_centralizzata' => 'antenna_tv_centralizzata', 'stabile.internet_condominiale' => 'internet_condominiale', // Avanzate / stato 'stabile.configurazione_avanzata' => 'configurazione_avanzata', 'stabile.ultima_generazione_unita' => 'ultima_generazione_unita', 'stabile.stato' => 'stato', 'stabile.registro_note' => 'registro_note', // Amministratore (1: campi su stabile, storicizzazione arriverà dopo) 'stabile.cod_fisc_amministratore' => 'cod_fisc_amministratore', 'stabile.data_nomina_amministratore' => 'data_nomina', 'stabile.data_scadenza_mandato' => 'scadenza_mandato', ]; $inverse = []; if (!empty($this->fieldMapping) && is_array($this->fieldMapping)) { foreach ($this->fieldMapping as $legacyKey => $target) { if (!is_string($legacyKey) || !is_string($target)) { continue; } $inverse[$target] = $legacyKey; } } if (!empty($inverse)) { foreach ($columnMap as $tKey => $colName) { if (!isset($inverse[$tKey])) { continue; } $legacyKey = (string)$inverse[$tKey]; $lk = strtolower($legacyKey); $lk = $aliases[$lk] ?? $lk; if ($lk === 'codice_fiscale') { $val = $legacyRow['codice_fiscale'] ?? ($legacyRow['cin'] ?? ($legacyRow['cod_fiscale'] ?? ($legacyRow['cf'] ?? ($legacyRow['cf_condominio'] ?? null)))); } elseif ($lk === 'cod_fisc_amministratore') { $val = $legacyRow['cf_amministratore'] ?? ($legacyRow['cod_fisc_amministratore'] ?? ($legacyRow['cin'] ?? null)); } elseif ($tKey === 'banca.iban') { $val = $legacyRow['iban'] ?? ($legacyRow['iban_condominio'] ?? null); } else { $val = is_array($legacyRow) && array_key_exists($lk, $legacyRow) ? $legacyRow[$lk] : null; } if ($val === null || $val === '') { continue; } if (in_array($tKey, self::BOOLEAN_TARGET_KEYS, true)) { $valStr = is_string($val) ? trim(strtolower($val)) : $val; $val = in_array($valStr, ['1', 'y', 'yes', 'si', 'sì', 'true', 'vero', 1, true], true) ? 1 : 0; } $current = $existing->$colName ?? null; if ($this->forceMapping || empty($current) || $current === 'ND') { $update[$colName] = $val; } } } if (!empty($this->manualFieldValues)) { foreach ($this->manualFieldValues as $target => $rawValue) { if (!is_string($target)) { continue; } $manualValue = $this->getManualValueForTarget($target); if ($manualValue === null) { continue; } $colName = $columnMap[$target] ?? null; if (!$colName && strpos($target, 'stabile.') === 0) { $candidate = substr($target, strlen('stabile.')); if ($candidate !== '' && Schema::hasColumn('stabili', $candidate)) { $colName = $candidate; } } if (!$colName) { continue; } $current = $existing->$colName ?? null; if ($this->forceMapping || empty($current) || $current === 'ND') { $update[$colName] = $manualValue; } } } if (!empty($update)) { $update['updated_at'] = now(); } return $update; } catch (\Throwable $e) { if ($this->output->isVerbose()) { $this->warn('Mapping stabili ignorato per errore: ' . $e->getMessage()); } return $update; } } private function resolveMappingCodiceCassa(array $mapping): ?string { $target = isset($mapping['target']) && is_array($mapping['target']) ? $mapping['target'] : []; $legacy = isset($mapping['legacy']) && is_array($mapping['legacy']) ? $mapping['legacy'] : []; $candidates = [ $target['codice_cassa'] ?? null, $mapping['target_cod_cassa'] ?? null, $mapping['target_cod_cassa_select'] ?? null, $mapping['codice_legacy'] ?? null, $legacy['cod_cassa'] ?? null, $mapping['legacy_cod_cassa'] ?? null, $mapping['codice_cassa'] ?? null, ]; foreach ($candidates as $candidate) { if (is_string($candidate) && trim($candidate) !== '') { return strtoupper(trim($candidate)); } } return null; } private function resolveMappingLegacyField(array $mapping, string $field): ?string { $legacy = isset($mapping['legacy']) && is_array($mapping['legacy']) ? $mapping['legacy'] : []; $candidates = [ $legacy[$field] ?? null, $mapping['legacy_' . $field] ?? null, ]; foreach ($candidates as $candidate) { if (is_string($candidate) && trim($candidate) !== '') { return trim($candidate); } } return null; } private function loadMappingIfAny(): void { try { $this->fieldMapping = []; $this->banksMapping = []; $this->manualFieldValues = []; if (!Schema::hasTable('gescon_import_mappings')) { return; } $legacy = $this->option('stabile'); $globalRow = DB::table('gescon_import_mappings') ->whereNull('legacy_code') ->orWhere('legacy_code', '*') ->orderByDesc('updated_at') ->first(); if ($globalRow && isset($globalRow->meta)) { $globalMeta = is_array($globalRow->meta) ? $globalRow->meta : json_decode($globalRow->meta, true); if (is_array($globalMeta)) { if (isset($globalMeta['mapping']) && is_array($globalMeta['mapping'])) { $this->fieldMapping = $globalMeta['mapping']; } if (isset($globalMeta['banche_mapping']) && is_array($globalMeta['banche_mapping'])) { $this->banksMapping = $globalMeta['banche_mapping']; } if (isset($globalMeta['manual_target_values']) && is_array($globalMeta['manual_target_values'])) { $this->manualFieldValues = $globalMeta['manual_target_values']; } } } if ($legacy) { $row = DB::table('gescon_import_mappings') ->where('legacy_code', $legacy) ->orderByDesc('updated_at') ->first(); if ($row && isset($row->meta)) { $meta = is_array($row->meta) ? $row->meta : json_decode($row->meta, true); if (is_array($meta)) { if (isset($meta['mapping']) && is_array($meta['mapping'])) { $this->fieldMapping = $meta['mapping']; } if (isset($meta['banche_mapping']) && is_array($meta['banche_mapping'])) { $this->banksMapping = $meta['banche_mapping']; } if (isset($meta['manual_target_values']) && is_array($meta['manual_target_values'])) { $this->manualFieldValues = array_replace( $this->manualFieldValues, $meta['manual_target_values'] ); } } } } } catch (\Throwable $e) { // Ignora errori mapping } } private function getManualValueForTarget(string $targetKey) { if (empty($this->manualFieldValues) || !array_key_exists($targetKey, $this->manualFieldValues)) { return null; } $value = $this->manualFieldValues[$targetKey]; if (is_string($value)) { $value = trim($value); } if ($value === '' || $value === null) { return null; } if (in_array($targetKey, self::BOOLEAN_TARGET_KEYS, true)) { if (is_bool($value)) { return $value ? 1 : 0; } if (is_numeric($value)) { return (int)$value === 1 ? 1 : 0; } $normalized = strtolower((string)$value); $truthy = ['1', 'y', 'yes', 'si', 'sì', 'true', 'vero', 'on']; return in_array($normalized, $truthy, true) ? 1 : 0; } return $value; } private function mapField(string $targetKey, $default = null) { $manualValue = $this->getManualValueForTarget($targetKey); if ($manualValue !== null) { return $manualValue; } // Migliorato: se mapping presente e targetKey è stabile.*, tenta di leggere dal record legacy cache if (empty($this->fieldMapping) || strpos($targetKey, 'stabile.') !== 0) return $default; // Carica record legacy una sola volta if ($this->stabileLegacyRow === null) { $this->stabileLegacyRow = $this->loadStabileLegacyRow(); } if (!$this->stabileLegacyRow) return $default; $legacyKeyMatched = null; foreach ($this->fieldMapping as $legacyKey => $target) { if ($target === $targetKey) { $legacyKeyMatched = $legacyKey; break; } } if (!$legacyKeyMatched) return $default; $row = $this->stabileLegacyRow; $targetAttr = substr($targetKey, strlen('stabile.')); // Mappa candidate column names per attributi target $candidatesMap = [ 'denominazione' => ['denominazione', 'nome', 'ragione_sociale', 'descrizione', 'descr', 'nome_condominio'], 'indirizzo' => ['indirizzo', 'via', 'indir', 'address'], 'numero_civico' => ['numero_civico', 'civico', 'n_civico', 'nciv'], 'cap' => ['cap', 'cap_condominio', 'zip'], 'comune_cod' => ['comune', 'comune_cod', 'citta', 'città', 'localita', 'loc'], 'provincia' => ['provincia', 'prov', 'pr'], 'foglio' => ['foglio', 'ac_foglio', 'foglio_catasto'], 'particella' => ['particella', 'mappale', 'ac_particella', 'part'], 'subalterno' => ['subalterno', 'sub', 'ac_sub', 'ac_subalterno'], 'categoria_catastale' => ['categoria_catastale', 'categoria', 'cat_categoria'], 'classe' => ['classe', 'cat_classe'], 'consistenza' => ['consistenza', 'cat_consistenza'], 'rendita_catastale' => ['rendita_catastale', 'rendita', 'cat_rendita'], 'cod_fisc_amministratore' => ['cod_fisc_amministratore', 'cf_amministratore', 'amministratore_cf', 'cf_amm', 'cod_fiscale_amm'], ]; $value = $default; if (isset($candidatesMap[$targetAttr])) { foreach ($candidatesMap[$targetAttr] as $cand) { if (isset($row[$cand]) && $row[$cand] !== '') { $value = $row[$cand]; break; } } } else { // fallback: prova legacyKey directly if (isset($row[$legacyKeyMatched]) && $row[$legacyKeyMatched] !== '') $value = $row[$legacyKeyMatched]; } return $value ?? $default; } private function loadStabileLegacyRow(): ?array { try { $code = $this->option('stabile'); if (!$code) return null; // 1) Staging stabili if (Schema::connection('gescon_import')->hasTable('stabili')) { $r = DB::connection('gescon_import')->table('stabili')->where('cod_stabile', $code)->first(); if ($r) return (array)$r; } // 2) Staging condomin if (Schema::connection('gescon_import')->hasTable('condomin')) { $r = 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; } }