option('stabile'); $this->info("=== Ricostruzione Anagrafica e Unità dal condomin_mirror Stabile {$stabileCode} ==="); // 1. Estrazione CF Stabile raw da Stabili.mdb (Nessun fallback 80000000021) $rawStabileCf = $this->getRawStabileCodiceFiscale($stabileCode); if (! $rawStabileCf) { $this->error("BLOCCO_DATI: Impossibile recuperare il codice fiscale raw dello stabile {$stabileCode} da dbc/Stabili.mdb."); return 1; } // Verifica o creazione dello Stabile con CF raw estratto $stabile = Stabile::where('codice_stabile', $stabileCode)->first(); if (! $stabile) { $adminId = DB::table('amministratori')->value('id') ?? 1; $stabile = Stabile::create([ 'codice_stabile' => $stabileCode, 'denominazione' => 'SUPERCONDOMINIO MILIZIE 3', 'indirizzo' => 'Viale delle Milizie 3', 'cap' => '00192', 'citta' => 'Roma', 'provincia' => 'RM', 'codice_fiscale' => $rawStabileCf, 'amministratore_id' => $adminId, 'attivo' => true, ]); } else { if ($stabile->codice_fiscale !== $rawStabileCf) { $stabile->update(['codice_fiscale' => $rawStabileCf]); } } $this->info("Stabile di riferimento: ID {$stabile->id} | CF Raw Stabile: {$stabile->codice_fiscale}"); // 2. Lettura Staging Lossless condomin_mirror if (! Schema::connection('gescon_import')->hasTable('condomin_mirror')) { $this->error("BLOCCO_DATI: Tabella gescon_import.condomin_mirror non esistente."); return 1; } $mirrorRows = DB::connection('gescon_import') ->table('condomin_mirror') ->where('cod_stabile', $stabileCode) ->orderBy('id') ->get(); $totalMirrorRows = $mirrorRows->count(); if ($totalMirrorRows === 0) { $this->error("BLOCCO_DATI: Nessuna riga presente in condomin_mirror per lo stabile {$stabileCode}."); return 1; } $this->info("Righe totali da condomin_mirror: {$totalMirrorRows}"); // 3. Elaborazione e Ricostruzione $unitaCreatedOrFound = 0; $personeCreatedOrFound = 0; $relazioniProprietariCount = 0; $relazioniInquiliniCount = 0; $processedMirrorRows = 0; $unitMap = []; foreach ($mirrorRows as $row) { $scalaRaw = trim((string) ($row->scala ?? '')); $intRaw = trim((string) ($row->interno ?? '')); $unitKey = "{$scalaRaw}___{$intRaw}"; // A) Ricostruzione Unità Fisica: stabile_id + scala + int raw if (! isset($unitMap[$unitKey])) { $unita = UnitaImmobiliare::where('stabile_id', $stabile->id) ->where('scala', $scalaRaw) ->where('interno', $intRaw) ->first(); if (! $unita) { $unitaCode = '0021-' . ($scalaRaw !== '' ? "{$scalaRaw}-" : '') . $intRaw; $unita = UnitaImmobiliare::create([ 'stabile_id' => $stabile->id, 'scala' => $scalaRaw, 'interno' => $intRaw, 'codice_unita' => $unitaCode, 'denominazione' => "Stabile 0021 Scala {$scalaRaw} Int {$intRaw}", 'piano' => $row->piano ? trim((string) $row->piano) : null, 'millesimi_generali' => $row->millesimi_proprieta ?: null, 'millesimi_proprieta' => $row->millesimi_proprieta ?: null, 'millesimi_riscaldamento' => $row->millesimi_riscaldamento ?: null, 'millesimi_ascensore' => $row->millesimi_ascensore ?: null, 'stato' => 'attiva', ]); $unitaCreatedOrFound++; } else { $updates = []; if (empty($unita->piano) && ! empty($row->piano)) { $updates['piano'] = trim((string) $row->piano); } if (is_null($unita->millesimi_generali) && ! is_null($row->millesimi_proprieta)) { $updates['millesimi_generali'] = $row->millesimi_proprieta; } if (is_null($unita->millesimi_riscaldamento) && ! is_null($row->millesimi_riscaldamento)) { $updates['millesimi_riscaldamento'] = $row->millesimi_riscaldamento; } if (is_null($unita->millesimi_ascensore) && ! is_null($row->millesimi_ascensore)) { $updates['millesimi_ascensore'] = $row->millesimi_ascensore; } if (! empty($updates)) { $unita->update($updates); } $unitaCreatedOrFound++; } $unitMap[$unitKey] = $unita; } else { $unita = $unitMap[$unitKey]; } // B) Ricostruzione Persona Proprietario / Intitolato $personaProprietario = $this->resolveOrCreatePersona($row, false, $rawStabileCf); if ($personaProprietario->wasRecentlyCreated) { $personeCreatedOrFound++; } // C) Relazione Temporale Proprietario $idCondStr = trim((string) $row->id_cond); $codCondStr = trim((string) $row->cod_cond); $sourceFile = (string) ($row->source_file ?? ''); $sourceYear = (string) ($row->source_year ?? ''); $sourceRow = (int) ($row->source_row ?? 0); $provenanceHash = (string) ($row->provenance_hash ?? "mirror_{$row->id}"); $provenanceFull = "{$sourceFile}#row:{$sourceRow}|{$provenanceHash}"; $relProp = PersonaUnitaRelazione::where('unita_id', $unita->id) ->where('id_cond', $idCondStr) ->where('tipo_relazione', 'proprietario') ->first(); $subDalRaw = trim((string) ($row->subentrato_dal ?? '')); $attFinoRaw = trim((string) ($row->attivo_fino_al ?? '')); $subPrimaRaw = trim((string) ($row->subentro_prima_cera ?? '')); $subAdessoRaw = trim((string) ($row->subentro_adesso_ce ?? '')); $dataInizioParsed = $this->parseMirrorDate($subDalRaw) ?: '2000-01-01'; $dataFineParsed = $this->parseMirrorDate($attFinoRaw); $isAttivo = true; if ($attFinoRaw !== '' && $attFinoRaw !== '0' && $attFinoRaw !== '00/00/00' && $attFinoRaw !== '00/00/0000') { $isAttivo = false; } if (! $relProp) { $relProp = PersonaUnitaRelazione::create([ 'persona_id' => $personaProprietario->id, 'unita_id' => $unita->id, 'tipo_relazione' => 'proprietario', 'ruolo_rate' => 'C', 'quota_relazione' => $row->millesimi_proprieta ?: 100.00, 'data_inizio' => $dataInizioParsed, 'data_fine' => $dataFineParsed, 'attivo' => $isAttivo, 'riceve_comunicazioni' => true, 'riceve_convocazioni' => true, 'vota_assemblea' => true, 'id_cond' => $idCondStr, 'cod_cond' => $codCondStr, 'provenance' => $provenanceFull, 'subentrato_dal' => $subDalRaw !== '' ? $subDalRaw : null, 'attivo_fino_al' => $attFinoRaw !== '' ? $attFinoRaw : null, 'subentro_prima_cera' => $subPrimaRaw !== '' ? $subPrimaRaw : null, 'subentro_adesso_ce' => $subAdessoRaw !== '' ? $subAdessoRaw : null, 'note_relazione' => "Gestione: {$sourceYear} | File: {$sourceFile} | Row: {$sourceRow}", ]); $relazioniProprietariCount++; } else { $updatesRel = []; if ($relProp->persona_id !== $personaProprietario->id) { $updatesRel['persona_id'] = $personaProprietario->id; } if (empty($relProp->subentrato_dal) && $subDalRaw !== '') { $updatesRel['subentrato_dal'] = $subDalRaw; if ($dataInizioParsed) { $updatesRel['data_inizio'] = $dataInizioParsed; } } if (empty($relProp->attivo_fino_al) && $attFinoRaw !== '') { $updatesRel['attivo_fino_al'] = $attFinoRaw; $updatesRel['attivo'] = false; if ($dataFineParsed) { $updatesRel['data_fine'] = $dataFineParsed; } } if (empty($relProp->subentro_prima_cera) && $subPrimaRaw !== '') { $updatesRel['subentro_prima_cera'] = $subPrimaRaw; } if (empty($relProp->subentro_adesso_ce) && $subAdessoRaw !== '') { $updatesRel['subentro_adesso_ce'] = $subAdessoRaw; } if (! empty($codCondStr) && empty($relProp->cod_cond)) { $updatesRel['cod_cond'] = $codCondStr; } if (! empty($updatesRel)) { $relProp->update($updatesRel); } } // D) Inquilino (se presente nel mirror row o nel payload) $payload = json_decode($row->legacy_payload ?? '{}', true); $inquilinoRaw = trim((string) ($row->inquilino ?: ($payload['inquil_nome'] ?? ''))); if ($inquilinoRaw !== '' && $inquilinoRaw !== '0') { $personaInquilino = $this->resolveOrCreatePersona($row, true, $rawStabileCf); if ($personaInquilino->wasRecentlyCreated) { $personeCreatedOrFound++; } $inqContrattoDal = trim((string) ($payload['Inquil_contratto_dal'] ?? $payload['inquil_dal'] ?? $row->inquil_dal ?? '')); $inqDataInizioParsed = $this->parseMirrorDate($inqContrattoDal) ?: $dataInizioParsed; $inqIdCond = "{$idCondStr}_inq"; // Ogni diversa persona inquilina o diversa occupazione mantenuta come relazione temporale $relInq = PersonaUnitaRelazione::where('unita_id', $unita->id) ->where('persona_id', $personaInquilino->id) ->where('tipo_relazione', 'inquilino') ->first(); if (! $relInq) { $relInq = PersonaUnitaRelazione::create([ 'persona_id' => $personaInquilino->id, 'unita_id' => $unita->id, 'tipo_relazione' => 'inquilino', 'ruolo_rate' => 'I', 'quota_relazione' => null, 'data_inizio' => $inqDataInizioParsed, 'data_fine' => $dataFineParsed, 'attivo' => $isAttivo, 'riceve_comunicazioni' => true, 'riceve_convocazioni' => false, 'vota_assemblea' => false, 'id_cond' => $inqIdCond, 'cod_cond' => $codCondStr, 'provenance' => $provenanceFull, 'subentrato_dal' => $inqContrattoDal !== '' ? $inqContrattoDal : ($subDalRaw !== '' ? $subDalRaw : null), 'attivo_fino_al' => $attFinoRaw !== '' ? $attFinoRaw : null, 'subentro_prima_cera' => $subPrimaRaw !== '' ? $subPrimaRaw : null, 'subentro_adesso_ce' => $subAdessoRaw !== '' ? $subAdessoRaw : null, 'note_relazione' => "Inquilino Gestione: {$sourceYear} | Contratto Dal Raw: {$inqContrattoDal} | File: {$sourceFile}", ]); $relazioniInquiliniCount++; } } $processedMirrorRows++; } // 4. Stampa Quadratura Completa $distinctPhysicalUnits = UnitaImmobiliare::where('stabile_id', $stabile->id)->count(); $int11Relations = PersonaUnitaRelazione::whereHas('unitaImmobiliare', function ($q) use ($stabile) { $q->where('stabile_id', $stabile->id) ->where('scala', 'A') ->where('interno', '11'); })->get(); $this->info('=== ESITO QUADRATURA 0021 ==='); $this->info("RIGHE_MIRROR_TOTALI: {$totalMirrorRows}"); $this->info("RIGHE_ELABORATE: {$processedMirrorRows}"); $this->info("UNITA_FISICHE_DISTINTE: {$distinctPhysicalUnits}"); $this->info("RELAZIONI_PROPRIETARI_TOTALI: {$relazioniProprietariCount}"); $this->info("RELAZIONI_INQUILINI_TOTALI: {$relazioniInquiliniCount}"); $this->info('VERIFICA_SCALA_A_INT_11: ' . $int11Relations->count() . ' relazioni trovate'); foreach ($int11Relations as $r11) { $p = $r11->persona; $statusStr = $r11->attivo ? 'ATTIVA' : 'INATTIVA (Uscente)'; $cfStr = $p->codice_fiscale ? "CF: {$p->codice_fiscale}" : 'CF: NULL (Candidato)'; $this->line(" -> Persona: {$p->nome_completo} | {$cfStr} | Relazione: {$r11->tipo_relazione} | Status: {$statusStr} | sub_dal: {$r11->subentrato_dal} | att_fino: {$r11->attivo_fino_al}"); } if ($processedMirrorRows === $totalMirrorRows) { $this->info("QUADRATURA_ESITO: PERFETTA (100% delle {$totalMirrorRows} righe specchiate)"); } else { $this->error("QUADRATURA_ESITO: DISCREPANZA ({$processedMirrorRows}/{$totalMirrorRows} righe)"); return 1; } return 0; } /** * Risolve o crea una Persona seguendo rigorosamente le regole CF: * - CF proprietario = Cond_cod_fisc; CF inquilino = Inquil_cod_fisc; * - Valida ogni CF raw. Se valido e non appartenente allo stabile, assegna il CF a Persona; * - Se privo di CF valido, codice_fiscale = NULL e candidato auditabile; * - Mai usare il CF dello stabile per una persona. */ private function resolveOrCreatePersona(object $row, bool $isTenant, string $stabileCf): Persona { $payload = json_decode($row->legacy_payload ?? '{}', true); if ($isTenant) { $rawName = trim((string) ($row->inquilino ?: ($payload['inquil_nome'] ?? ''))); $rawCf = trim((string) ($payload['Inquil_cod_fisc'] ?? '')); } else { $rawName = trim((string) ($row->nom_cond ?? '')); $rawCf = trim((string) ($payload['Cond_cod_fisc'] ?? $row->codice_fiscale ?? '')); } $cleanCf = strtoupper($rawCf); // 1. Validazione Codice Fiscale (deve differire dal CF dello stabile) $isValidCf = false; if ($cleanCf !== '' && $cleanCf !== '0' && $cleanCf !== strtoupper($stabileCf)) { if (preg_match('/^[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]$/', $cleanCf) || preg_match('/^\d{11}$/', $cleanCf)) { $isValidCf = true; } } $idCondStr = trim((string) ($row->id_cond ?? '')); $stableKey = $isTenant ? "0021_ID_{$idCondStr}_INQ_" . md5($rawName) : "0021_ID_{$idCondStr}"; if ($isValidCf) { $existingByCf = Persona::where('codice_fiscale', $cleanCf)->first(); if ($existingByCf) { return $existingByCf; } $existingByStable = Persona::where('note', 'LIKE', "%[MDB_STABLE_ID: {$stableKey}]%")->first(); if ($existingByStable) { if (empty($existingByStable->codice_fiscale)) { $existingByStable->update(['codice_fiscale' => $cleanCf]); } $this->resolvedPersonasByStableId[$stableKey] = $existingByStable; return $existingByStable; } } else { if (isset($this->resolvedPersonasByStableId[$stableKey])) { return $this->resolvedPersonasByStableId[$stableKey]; } $existingByStable = Persona::where('note', 'LIKE', "%[MDB_STABLE_ID: {$stableKey}]%")->first(); if ($existingByStable) { $this->resolvedPersonasByStableId[$stableKey] = $existingByStable; return $existingByStable; } } // 3. Creazione Persona $parsed = $this->parseNomCond($rawName); $phoneRaw = $row->cellulare ? trim((string) $row->cellulare) : ($row->telefono ? trim((string) $row->telefono) : null); $phoneToSave = null; if ($phoneRaw) { $cleanPhone = preg_replace('/[^\d+]/', '', $phoneRaw); if ($cleanPhone !== '' && ! Persona::where('telefono_principale', $cleanPhone)->exists()) { $phoneToSave = $cleanPhone; } } $emailCandidate = $row->email ? trim((string) $row->email) : null; $pecCandidate = $row->pec ? trim((string) $row->pec) : null; $note = $isValidCf ? "[MDB_0021] CF Validato da condomin_mirror [MDB_STABLE_ID: {$stableKey}]" : "[CANDIDATO_AUDITABILE] Stabile 0021 [MDB_STABLE_ID: {$stableKey}]"; $persona = Persona::create([ 'codice_interno' => Persona::generaCodiceUnivoco(), 'tipologia' => $parsed['tipologia'], 'cognome' => $parsed['cognome'], 'nome' => $parsed['nome'], 'ragione_sociale' => $parsed['ragione_sociale'], 'codice_fiscale' => $isValidCf ? $cleanCf : null, 'telefono_principale' => $phoneToSave, 'email_principale' => $emailCandidate, 'email_pec' => $pecCandidate, 'residenza_via' => $row->indirizzo_corrispondenza ? trim((string) $row->indirizzo_corrispondenza) : null, 'note' => $note, 'attivo' => true, ]); $this->resolvedPersonasByStableId[$stableKey] = $persona; return $persona; } /** * Estrazione CF dello stabile direttamente da dbc/Stabili.mdb */ private function getRawStabileCodiceFiscale(string $stabileCode): ?string { // 1. Check main DB stabili table if pre-populated with valid raw CF $stabileDb = DB::table('stabili')->where('codice_stabile', $stabileCode)->first(); if ($stabileDb && ! empty($stabileDb->codice_fiscale) && $stabileDb->codice_fiscale !== '80000000021') { return trim((string) $stabileDb->codice_fiscale); } // 2. Read from /mnt/gescon-archives/gescon/dbc/Stabili.mdb $mdbPath = '/mnt/gescon-archives/gescon/dbc/Stabili.mdb'; if (file_exists($mdbPath)) { $output = shell_exec('mdb-export ' . escapeshellarg($mdbPath) . ' Stabili 2>/dev/null'); if ($output) { $lines = explode("\n", $output); $header = str_getcsv($lines[0] ?? ''); $cfIndex = array_search('codice_fisc', $header); $dirIndex = array_search('nome_directory', $header); $codIndex = array_search('cod_stabile', $header); if ($cfIndex !== false) { for ($i = 1; $i < count($lines); $i++) { $row = str_getcsv($lines[$i]); $dir = $row[$dirIndex] ?? ''; $cod = $row[$codIndex] ?? ''; if ($dir === $stabileCode || $cod === $stabileCode) { $cf = trim((string) ($row[$cfIndex] ?? '')); if (! empty($cf)) { return $cf; } } } } } } return null; } private function parseNomCond(string $nomCond): array { $nomCond = trim($nomCond); $upper = mb_strtoupper($nomCond); $companyKeywords = ['ATER', 'SRL', 'S.R.L.', 'SPA', 'S.P.A.', 'SAS', 'S.A.S.', 'SNC', 'S.N.C.', 'CONDOMINI', 'EREDI', 'C/O', 'SOC.', 'SOCIETA', 'SOCIETÀ', 'DOMUS']; foreach ($companyKeywords as $kw) { if (str_contains($upper, $kw)) { return [ 'tipologia' => 'giuridica', 'cognome' => $nomCond, 'nome' => '', 'ragione_sociale' => $nomCond, ]; } } $parts = preg_split('/\s+/', $nomCond, 2); return [ 'tipologia' => 'fisica', 'cognome' => $parts[0] ?? $nomCond, 'nome' => $parts[1] ?? '', 'ragione_sociale' => null, ]; } private function parseMirrorDate(?string $dateStr): ?string { if (empty($dateStr)) { return null; } $dateStr = trim($dateStr); if ($dateStr === '0' || $dateStr === '00/00/00' || $dateStr === '00/00/0000') { return null; } try { $parts = explode(' ', $dateStr); $datePart = $parts[0]; if (preg_match('/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})$/', $datePart, $m)) { $month = (int) $m[1]; $day = (int) $m[2]; $year = (int) $m[3]; if ($year < 100) { $year += ($year > 50 ? 1900 : 2000); } if ($month > 12 && $day <= 12) { $tmp = $month; $month = $day; $day = $tmp; } return sprintf('%04d-%02d-%02d', $year, $month, $day); } $dt = new \DateTime($dateStr); return $dt->format('Y-m-d'); } catch (\Throwable $e) { return null; } } }