option('stabile'); $this->info("=== Ricostruzione Anagrafica e Unità dal condomin_mirror Stabile {$stabileCode} ==="); // 1. Verifica Stabile $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' => "80000000021", 'amministratore_id' => $adminId, 'attivo' => true, ]); } $this->info("Stabile di riferimento: ID {$stabile->id} | {$stabile->denominazione}"); // 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 Quadratura $unitaCreatedOrFound = 0; $personeCreatedOrFound = 0; $relazioniProprietariCount = 0; $relazioniInquiliniCount = 0; $processedMirrorRows = 0; $unitMap = []; // 'scala___int' => UnitaImmobiliare model 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 { // Aggiornamento conservativo (senza sovrascrivere dati locali arricchiti) $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); if ($personaProprietario->wasRecentlyCreated) { $personeCreatedOrFound++; } // C) Relazione Temporale Proprietario $idCondStr = trim((string) $row->id_cond); $codCondStr = trim((string) $row->cod_cond); $provenanceHash = (string) ($row->provenance_hash ?? "mirror_{$row->id}"); $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); // Attivo se NON c'è attivo_fino_al valorizzato o data_fine passata $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' => $provenanceHash, 'subentrato_dal' => $subDalRaw !== '' ? $subDalRaw : null, 'attivo_fino_al' => $attFinoRaw !== '' ? $attFinoRaw : null, 'subentro_prima_cera' => $subPrimaRaw !== '' ? $subPrimaRaw : null, 'subentro_adesso_ce' => $subAdessoRaw !== '' ? $subAdessoRaw : null, ]); $relazioniProprietariCount++; } else { // Aggiorna idempotente preservando la provenienza e la temporalità più ricca $updatesRel = []; 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) $inquilinoRaw = trim((string) ($row->inquilino ?? '')); if ($inquilinoRaw !== '' && $inquilinoRaw !== '0') { $personaInquilino = $this->resolveOrCreatePersona($row, true); if ($personaInquilino->wasRecentlyCreated) { $personeCreatedOrFound++; } $inqIdCond = "{$idCondStr}_inq"; $relInq = PersonaUnitaRelazione::where('unita_id', $unita->id) ->where('id_cond', $inqIdCond) ->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' => $dataInizioParsed, 'data_fine' => $dataFineParsed, 'attivo' => $isAttivo, 'riceve_comunicazioni' => true, 'riceve_convocazioni' => false, 'vota_assemblea' => false, 'id_cond' => $inqIdCond, 'cod_cond' => $codCondStr, 'provenance' => $provenanceHash, 'subentrato_dal' => $subDalRaw !== '' ? $subDalRaw : null, 'attivo_fino_al' => $attFinoRaw !== '' ? $attFinoRaw : null, 'subentro_prima_cera' => $subPrimaRaw !== '' ? $subPrimaRaw : null, 'subentro_adesso_ce' => $subAdessoRaw !== '' ? $subAdessoRaw : null, ]); $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)'; $this->line(" -> Persona: {$p->nome_completo} | Relazione: {$r11->tipo_relazione} | Status: {$statusStr} | sub_dal: {$r11->subentrato_dal} | att_fino: {$r11->attivo_fino_al} | sub_prima: {$r11->subentro_prima_cera} | sub_adesso: {$r11->subentro_adesso_ce}"); } 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 la Regola 3: * - Cerca prima Codice Fiscale validato; * - In assenza di CF, usa l'identificatore stabile MDB (id_cond) per evitare fusions arbitrarie per solo nome. */ private function resolveOrCreatePersona(object $row, bool $isTenant): Persona { $rawName = $isTenant ? trim((string) ($row->inquilino ?? '')) : trim((string) ($row->nom_cond ?? '')); $rawCf = $isTenant ? null : trim((string) ($row->codice_fiscale ?? '')); $cleanCf = strtoupper((string) $rawCf); // 1. Validazione Codice Fiscale $isValidCf = false; if ($cleanCf !== '' && $cleanCf !== '0') { if (preg_match('/^[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]$/i', $cleanCf) || preg_match('/^\d{11}$/', $cleanCf)) { $isValidCf = true; } } if ($isValidCf) { $existing = Persona::where('codice_fiscale', $cleanCf)->first(); if ($existing) { return $existing; } } // 2. Senza CF validato: Identificatore stabile legacy (id_cond) $idCondStr = trim((string) ($row->id_cond ?? '')); $stableKey = $isTenant ? "0021_ID_{$idCondStr}_INQ_" . md5($rawName) : "0021_ID_{$idCondStr}"; if (isset($this->resolvedPersonasByStableId[$stableKey])) { return $this->resolvedPersonasByStableId[$stableKey]; } // Cerca persona esistente creata per questo identificatore stabile $existingByStable = Persona::where('note', 'LIKE', "%[MDB_STABLE_ID: {$stableKey}]%")->first(); if ($existingByStable) { $this->resolvedPersonasByStableId[$stableKey] = $existingByStable; return $existingByStable; } // 3. Creazione Candidato Auditabile per identificatore stabile (mai per solo nome) $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; $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' => "[CANDIDATO_AUDITABILE] Stabile 0021 [MDB_STABLE_ID: {$stableKey}]", 'attivo' => true, ]); $this->resolvedPersonasByStableId[$stableKey] = $persona; return $persona; } /** * Parsing conservativo delle componenti anagrafiche del nome. */ 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, ]; } /** * Parsing sicuro delle date da format MM/DD/YY HH:MM:SS o YYYY-MM-DD */ 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; } } }