option('apply'); $stabile = $this->option('stabile'); $linkRuolo = (bool) $this->option('link-ruolo'); $includeComproprietari = (bool) $this->option('include-comproprietari'); $periodStart = $this->normalizeDateToYmd($this->option('period-start')); $periodEnd = $this->normalizeDateToYmd($this->option('period-end')); $legacyAnnoDir = trim((string) ($this->option('legacy-anno-dir') ?? '')) ?: null; $legacyAnnoO = $this->option('legacy-anno-o') !== null && $this->option('legacy-anno-o') !== '' ? (int) $this->option('legacy-anno-o') : null; $only = strtolower(trim((string) ($this->option('only') ?? 'both'))); if (! in_array($only, ['proprietario', 'inquilino', 'both'], true)) { $this->warn("Valore --only non riconosciuto: {$only}. Uso 'both'."); $only = 'both'; } $limit = (int) $this->option('limit'); if ($limit < 1) { $limit = 1; } if ($limit > 20000) { $limit = 20000; } $conn = Config::get('database.connections.gescon_import') ? 'gescon_import' : DB::getDefaultConnection(); if (! Schema::connection($conn)->hasTable('condomin')) { $this->error("Tabella condomin non presente sulla connessione {$conn}"); return 1; } if (! Schema::hasTable('rubrica_universale')) { $this->error('Tabella rubrica_universale non disponibile: impossibile sincronizzare.'); return 1; } if ($linkRuolo && ! Schema::hasTable('rubrica_ruoli')) { $this->error('Tabella rubrica_ruoli non disponibile: impossibile creare ruoli.'); return 1; } /** @var array $stabiliMap */ $stabiliMap = []; foreach (Stabile::query()->get(['id', 'codice_stabile', 'amministratore_id']) as $s) { $code = trim((string) ($s->codice_stabile ?? '')); if ($code === '') { continue; } $stabiliMap[$code] = [ 'id' => (int) $s->id, 'amministratore_id' => (int) ($s->amministratore_id ?? 0), 'codice_stabile' => $code, ]; $trim = ltrim($code, '0'); if ($trim !== '' && ! isset($stabiliMap[$trim])) { $stabiliMap[$trim] = $stabiliMap[$code]; } } $query = DB::connection($conn)->table('condomin'); if ($stabile) { $query->where('cod_stabile', $stabile); } $rows = $query->orderBy('cod_cond')->limit($limit)->get(); if ($rows->isEmpty()) { $this->info('Nessun record da sincronizzare.'); return 0; } // Cache per risolvere duplicati (condomin.E_lostesso_Di) puntando al record "master". $condominById = []; foreach ($rows as $r) { $rid = (int) ($r->id ?? 0); if ($rid > 0) { $condominById[$rid] = $r; } } $condominMasterCache = []; $inserted = 0; $updated = 0; $skipped = 0; $ruoliInserted = 0; $ruoliUpdated = 0; foreach ($rows as $row) { $codStabile = trim((string) ($row->cod_stabile ?? '')); $codCondRaw = trim((string) ($row->cod_cond ?? '')); // Dedup: se E_lostesso_Di è valorizzato, usa il record master come riferimento per chiavi/identità. $masterId = (int) ($row->E_lostesso_Di ?? 0); $masterRow = null; if ($masterId > 0) { if (isset($condominById[$masterId])) { $masterRow = $condominById[$masterId]; } elseif (array_key_exists($masterId, $condominMasterCache)) { $masterRow = $condominMasterCache[$masterId]; } else { $condominMasterCache[$masterId] = DB::connection($conn)->table('condomin')->where('id', $masterId)->first(); $masterRow = $condominMasterCache[$masterId]; } } $keyRow = $masterRow ?: $row; if ($codStabile === '' || $codCondRaw === '') { $skipped++; continue; } $domain = $stabiliMap[$codStabile] ?? $stabiliMap[ltrim($codStabile, '0')] ?? null; if (! $domain) { $skipped++; continue; } $amministratoreId = (int) ($domain['amministratore_id'] ?? 0); if ($amministratoreId <= 0) { $skipped++; continue; } // In condomin, alcuni flag possono essere NULL o assenti: non assumere "true" per default. $proprietarioFlag = (bool) ($row->proprietario ?? false); $inquilinoFlag = (bool) ($row->inquilino ?? false); $cognome = trim((string) ($keyRow->cognome ?? '')) ?: null; $nome = trim((string) ($keyRow->nome ?? '')) ?: null; $cf = trim((string) ($keyRow->codice_fiscale ?? '')) ?: null; $email = $this->firstNonEmptyStr([ $keyRow->email ?? null, $keyRow->e_mail_condomino ?? null, $row->email ?? null, $row->e_mail_condomino ?? null, ], 191); $pec = $this->firstNonEmptyStr([ $keyRow->pec ?? null, $keyRow->pec_condominio ?? null, $row->pec ?? null, $row->pec_condominio ?? null, ], 191); $telefono = $this->firstNonEmptyStr([ $keyRow->telefono ?? null, $keyRow->tel_cond ?? null, $row->telefono ?? null, $row->tel_cond ?? null, ], 50); $cellulare = $this->firstNonEmptyStr([ $keyRow->cellulare ?? null, $keyRow->cell_cond ?? null, $row->cellulare ?? null, $row->cell_cond ?? null, ], 50); // NB: non usare $row->denominazione come fallback: in condomin spesso è descrittore unità (es. "Area Condominiale"). $denomOwner = trim((string) ($keyRow->proprietario_denominazione ?? '')) ?: null; // Preferenza richiesta: usa inquil_nome quando presente, fallback su inquilino_denominazione. $denomTenant = trim((string) ($row->inquil_nome ?? $row->inquilino_denominazione ?? '')) ?: null; if ($denomOwner && $this->isCondominialePlaceholder($denomOwner)) { $denomOwner = null; } if ($denomTenant && $this->isCondominialePlaceholder($denomTenant)) { $denomTenant = null; } $scala = $this->cleanShort((string) ($row->scala ?? '')); $interno = $this->cleanShort((string) ($row->interno ?? '')); $piano = $this->normalizePiano($row->piano ?? null); $tasks = []; if ($only === 'proprietario' || $only === 'both') { $ownerRight = $this->resolveRightInfo($row, 'proprietario'); $ownerPercent = $this->normalizePercentValue($ownerRight['percent'] ?? null); $candidate = [ 'denominazione' => $denomOwner, 'nome' => $nome, 'cognome' => $cognome, 'cf' => $cf, 'email' => $email, 'pec' => $pec, 'telefono' => $telefono, 'cellulare' => $cellulare, ]; if (($proprietarioFlag || ($ownerRight['custom'] ?? null)) && $this->hasRubricaIdentity($candidate)) { $tasks[] = [ 'role' => 'condomino', 'role_custom' => $ownerRight['custom'] ?? null, 'right_percent' => $ownerPercent, 'denominazione' => $denomOwner, 'nome' => $nome, 'cognome' => $cognome, 'cf' => $cf, 'email' => $email, 'pec' => $pec, 'telefono' => $telefono, 'cellulare' => $cellulare, 'indirizzo_corrispondenza' => $row->indirizzo_corrispondenza ?? null, 'indirizzo' => $row->ind ?? null, 'cap' => $row->cap ?? null, 'citta' => $row->citta ?? null, 'provincia' => $row->pr ?? null, 'presso' => $row->presso ?? null, 'data_inizio' => $row->proprietario_subentrato_dal ?? $row->subentrato_dal ?? null, 'data_fine' => $row->proprietario_attivo_fino_al ?? $row->attivo_fino_al ?? null, ]; } elseif (! $proprietarioFlag && ! $this->rowLooksLikeTenant($row) && $this->hasRubricaIdentity($candidate)) { // Anche senza flag, importa se ci sono dati anagrafici reali. $tasks[] = [ 'role' => 'condomino', 'role_custom' => $ownerRight['custom'] ?? null, 'right_percent' => $ownerPercent, 'denominazione' => $denomOwner, 'nome' => $nome, 'cognome' => $cognome, 'cf' => $cf, 'email' => $email, 'pec' => $pec, 'telefono' => $telefono, 'cellulare' => $cellulare, 'indirizzo_corrispondenza' => $row->indirizzo_corrispondenza ?? null, 'indirizzo' => $row->ind ?? null, 'cap' => $row->cap ?? null, 'citta' => $row->citta ?? null, 'provincia' => $row->pr ?? null, 'presso' => $row->presso ?? null, 'data_inizio' => $row->proprietario_subentrato_dal ?? $row->subentrato_dal ?? null, 'data_fine' => $row->proprietario_attivo_fino_al ?? $row->attivo_fino_al ?? null, ]; } } if ($only === 'inquilino' || $only === 'both') { $tenantRight = $this->resolveRightInfo($row, 'inquilino'); $tenantPercent = $this->normalizePercentValue($tenantRight['percent'] ?? null); // Tenant: preferisci nominativo da inquil_nome (già scelto a monte) e prova a splittare in nome/cognome. [$tenantCognome, $tenantNome, $tenantRagione] = $this->splitNomeCognomeOrRagione($denomTenant ?? ''); $candidate = [ 'denominazione' => $denomTenant, 'nome' => $tenantNome, 'cognome' => $tenantCognome, 'cf' => $row->inquil_cod_fisc ?? null, 'email' => $row->e_mail_inquilino ?? $keyRow->e_mail_inquilino ?? null, 'pec' => $row->pec_inquilino ?? null, 'telefono' => $row->inquil_tel1 ?? null, 'cellulare' => $row->cell_inq ?? $keyRow->cell_inq ?? null, ]; if (($inquilinoFlag || ($tenantRight['custom'] ?? null)) && $this->hasRubricaIdentity($candidate)) { $tasks[] = [ 'role' => 'inquilino', 'role_custom' => $tenantRight['custom'] ?? null, 'right_percent' => $tenantPercent, 'denominazione' => $tenantRagione ?: null, 'nome' => $tenantRagione ? null : ($tenantNome ?: null), 'cognome' => $tenantRagione ? null : ($tenantCognome ?: null), 'cf' => $row->inquil_cod_fisc ?? null, 'email' => $row->e_mail_inquilino ?? $keyRow->e_mail_inquilino ?? null, 'pec' => $row->pec_inquilino ?? null, 'telefono' => $row->inquil_tel1 ?? null, 'cellulare' => $row->cell_inq ?? $keyRow->cell_inq ?? null, 'indirizzo_corrispondenza' => $row->inquil_indir ?? null, 'indirizzo' => $row->inquil_indir ?? null, 'cap' => $row->inquil_cap ?? null, 'citta' => $row->inquil_citta ?? null, 'provincia' => $row->inquil_pr ?? null, 'presso' => $row->inquil_presso ?? null, 'data_inizio' => $row->inquilino_subentrato_dal ?? $row->inquil_dal ?? null, 'data_fine' => $row->inquilino_attivo_fino_al ?? $row->inquil_al ?? null, ]; } elseif (! $inquilinoFlag && $this->hasRubricaIdentity($candidate)) { $tasks[] = [ 'role' => 'inquilino', 'role_custom' => $tenantRight['custom'] ?? null, 'right_percent' => $tenantPercent, 'denominazione' => $tenantRagione ?: null, 'nome' => $tenantRagione ? null : ($tenantNome ?: null), 'cognome' => $tenantRagione ? null : ($tenantCognome ?: null), 'cf' => $row->inquil_cod_fisc ?? null, 'email' => $row->e_mail_inquilino ?? $keyRow->e_mail_inquilino ?? null, 'pec' => $row->pec_inquilino ?? null, 'telefono' => $row->inquil_tel1 ?? null, 'cellulare' => $row->cell_inq ?? $keyRow->cell_inq ?? null, 'indirizzo_corrispondenza' => $row->inquil_indir ?? null, 'indirizzo' => $row->inquil_indir ?? null, 'cap' => $row->inquil_cap ?? null, 'citta' => $row->inquil_citta ?? null, 'provincia' => $row->inquil_pr ?? null, 'presso' => $row->inquil_presso ?? null, 'data_inizio' => $row->inquilino_subentrato_dal ?? $row->inquil_dal ?? null, 'data_fine' => $row->inquilino_attivo_fino_al ?? $row->inquil_al ?? null, ]; } } if (empty($tasks)) { $skipped++; continue; } foreach ($tasks as $t) { $role = (string) $t['role']; $roleCustom = $this->normalizeRoleCustom($role, $t['role_custom'] ?? null); $rubricaKey = (string) (trim((string) ($keyRow->cod_cond ?? '')) ?: $codCondRaw); if ($role === 'inquilino') { // Se il CF del tenant è assente sulla riga corrente, prova a recuperarlo da altre righe condomin della stessa unità. if (empty($t['cf'])) { $hintName = $t['denominazione'] ?? trim(((string) ($t['cognome'] ?? '')) . ' ' . ((string) ($t['nome'] ?? ''))); $fallback = $this->lookupTenantIdentityFromStaging($conn, (string) $codStabile, $scala, $interno, $piano, $hintName); if (! empty($fallback['cf'])) { $t['cf'] = $fallback['cf']; } if (empty($t['email']) && ! empty($fallback['email'])) { $t['email'] = $fallback['email']; } if (empty($t['telefono']) && ! empty($fallback['telefono'])) { $t['telefono'] = $fallback['telefono']; } if (empty($t['cellulare']) && ! empty($fallback['cellulare'])) { $t['cellulare'] = $fallback['cellulare']; } } // Chiave tenant: preferisci identificativi univoci (CF/email/telefono) per evitare duplicazioni. $tenantKey = $this->normalizeIdentityKey($t['cf'] ?? null) ?: $this->normalizeIdentityKey($t['email'] ?? null) ?: $this->normalizeIdentityKey($t['telefono'] ?? null) ?: $this->normalizeIdentityKey($t['cellulare'] ?? null) ?: $this->normalizeIdentityKey($t['denominazione'] ?? null) ?: $this->normalizeIdentityKey(($t['cognome'] ?? '') . ($t['nome'] ?? '')); $rubricaKey = $tenantKey ?: (string) $codCondRaw; } $rubricaCode = $this->shortRubricaCode((string) $amministratoreId, (string) $codStabile, (string) $rubricaKey, $role); $match = ['codice_univoco' => $rubricaCode]; $ragione = trim((string) ($t['denominazione'] ?? '')) ?: null; $nomeT = trim((string) ($t['nome'] ?? '')) ?: null; $cognomeT = trim((string) ($t['cognome'] ?? '')) ?: null; if ($ragione && ($nomeT || $cognomeT)) { // Preferisci persona fisica se ho nome/cognome. $ragione = null; } $note = 'Auto-sync condomin (staging)'; $note .= " | cod_stabile={$codStabile} | cod_cond={$codCondRaw} | ruolo={$role}"; if ($roleCustom) { $note .= " | diritto={$roleCustom}"; } $fiscal = $this->normalizeFiscalIds(! empty($t['cf']) ? (string) $t['cf'] : null); $data = [ 'amministratore_id' => $amministratoreId, 'codice_univoco' => $rubricaCode, 'ragione_sociale' => $ragione ? mb_substr($ragione, 0, 255) : null, 'nome' => $ragione ? null : ($nomeT ? mb_substr($nomeT, 0, 100) : null), 'cognome' => $ragione ? null : ($cognomeT ? mb_substr($cognomeT, 0, 100) : null), 'tipo_contatto' => $ragione ? 'persona_giuridica' : 'persona_fisica', 'codice_fiscale' => $fiscal['cf'] ? mb_substr($fiscal['cf'], 0, 20) : null, 'partita_iva' => $fiscal['piva'] ? mb_substr($fiscal['piva'], 0, 20) : null, 'email' => ($emailNorm = $this->normalizeEmail(! empty($t['email']) ? (string) $t['email'] : null)) ? mb_substr($emailNorm, 0, 191) : null, 'pec' => ($pecNorm = $this->normalizeEmail(! empty($t['pec']) ? (string) $t['pec'] : null)) ? mb_substr($pecNorm, 0, 191) : null, 'telefono_ufficio' => ($telNorm = $this->normalizePhone(! empty($t['telefono']) ? (string) $t['telefono'] : null)) ? mb_substr($telNorm, 0, 50) : null, 'telefono_cellulare' => ($cellNorm = $this->normalizePhone(! empty($t['cellulare']) ? (string) $t['cellulare'] : null)) ? mb_substr($cellNorm, 0, 50) : null, 'indirizzo' => $this->firstNonEmptyStr([ $t['indirizzo'] ?? null, $t['indirizzo_corrispondenza'] ?? null, ], 255), 'cap' => $this->firstNonEmptyStr([$t['cap'] ?? null], 10), 'citta' => $this->firstNonEmptyStr([$t['citta'] ?? null], 100), 'provincia' => $this->firstNonEmptyStr([$t['provincia'] ?? null], 10), 'categoria' => 'condomino', 'stato' => 'attivo', ]; // Se non c'è identità minima, non creare record vuoti. if (! $this->hasRubricaIdentity([ 'denominazione' => $data['ragione_sociale'] ?? null, 'nome' => $data['nome'] ?? null, 'cognome' => $data['cognome'] ?? null, 'cf' => $data['codice_fiscale'] ?? null, 'email' => $data['email'] ?? null, 'pec' => $data['pec'] ?? null, 'telefono' => $data['telefono_ufficio'] ?? null, 'cellulare' => $data['telefono_cellulare'] ?? null, ])) { $skipped++; continue; } if (! $apply) { $exists = RubricaUniversale::withTrashed()->where($match)->exists(); $exists ? $updated++ : $inserted++; continue; } $record = $this->findExistingRubrica($amministratoreId, $data) ?: RubricaUniversale::withTrashed()->where($match)->first(); if ($record) { if (method_exists($record, 'trashed') && $record->trashed()) { $record->restore(); } // Se ho trovato per identità (CF/PIVA/email/telefono), NON cambiare il codice_univoco esistente. if (! empty($record->codice_univoco) && (string) $record->codice_univoco !== (string) $rubricaCode) { unset($data['codice_univoco']); } // Non sovrascrivere note manuali: aggiungi solo se vuoto. if (empty($record->note)) { $data['note'] = $note; } $record->fill($data); if ($record->isDirty()) { $record->save(); } $updated++; } else { $data['note'] = $note; RubricaUniversale::create($data); $inserted++; $record = RubricaUniversale::withTrashed()->where($match)->first(); } if ($linkRuolo && $record) { $stabileId = (int) ($domain['id'] ?? 0); if ($stabileId > 0) { $unitaId = $this->resolveUnitaImmobiliareId($stabileId, $scala, $interno, $piano); $dataInizio = $this->normalizeDateToYmd($t['data_inizio'] ?? null) ?: $periodStart; $dataFine = $this->normalizeDateToYmd($t['data_fine'] ?? null) ?: $periodEnd; $isAttivo = true; if ($dataFine) { $isAttivo = (string) $dataFine >= date('Y-m-d'); } // Dedup: per lo stesso soggetto/unità/anno deve esistere un solo ruolo. // Non includiamo ruolo_custom nel match per evitare doppioni (es. condomino vs condomino(piena_proprieta)). $ruoloMatch = [ 'rubrica_id' => (int) $record->id, 'ruolo_standard' => $role, 'stabile_id' => $stabileId, 'unita_immobiliare_id' => $unitaId, ]; // Storico per annualità: se la tabella supporta i campi legacy, includili nel match if (Schema::hasColumn('rubrica_ruoli', 'legacy_anno_dir')) { $ruoloMatch['legacy_anno_dir'] = $legacyAnnoDir; } if (Schema::hasColumn('rubrica_ruoli', 'legacy_anno_o')) { $ruoloMatch['legacy_anno_o'] = $legacyAnnoO; } // Se esiste già un ruolo con custom valorizzato, preservalo quando in input arriva vuoto. if ($roleCustom === null) { $existingCustom = RubricaRuolo::query() ->where($ruoloMatch) ->whereNotNull('ruolo_custom') ->orderByDesc('is_preferito') ->orderByDesc('id') ->value('ruolo_custom'); if (is_string($existingCustom) && trim($existingCustom) !== '') { $roleCustom = trim($existingCustom); } } $ruoloPayload = [ 'data_inizio' => $dataInizio, 'data_fine' => $dataFine, 'is_attivo' => $isAttivo, 'is_preferito' => true, 'ruolo_custom' => $roleCustom, 'legacy_anno_dir' => Schema::hasColumn('rubrica_ruoli', 'legacy_anno_dir') ? $legacyAnnoDir : null, 'legacy_anno_o' => Schema::hasColumn('rubrica_ruoli', 'legacy_anno_o') ? $legacyAnnoO : null, 'meta' => [ 'source' => 'gescon_import.condomin', 'cod_cond' => $codCondRaw, 'cod_stabile' => $codStabile, 'role' => $role, 'scala' => $scala, 'interno' => $interno, 'piano' => $piano, 'legacy_anno_dir' => $legacyAnnoDir, 'legacy_anno_o' => $legacyAnnoO, 'right_percent' => $t['right_percent'] ?? null, 'percentuale_possesso' => $t['right_percent'] ?? null, ], ]; $ruolo = RubricaRuolo::updateOrCreate($ruoloMatch, $ruoloPayload); $ruolo->wasRecentlyCreated ? $ruoliInserted++ : $ruoliUpdated++; } } } } // Import comproprietari (se richiesto) if ($includeComproprietari && Schema::connection($conn)->hasTable('comproprietari')) { $src = DB::connection($conn)->table('comproprietari'); if ($stabile) { $src->where('cod_stabile', $stabile); } $comprRows = $src->limit(50000)->get(); foreach ($comprRows as $cr) { $codStabile = trim((string) ($cr->cod_stabile ?? '')); if ($codStabile === '') { continue; } $domain = $stabiliMap[$codStabile] ?? $stabiliMap[ltrim($codStabile, '0')] ?? null; if (! $domain) { continue; } $amministratoreId = (int) ($domain['amministratore_id'] ?? 0); if ($amministratoreId <= 0) { continue; } $idCompr = (int) ($cr->id_compr ?? 0); $idCond = (int) ($cr->id_cond ?? 0); $nom = trim((string) ($cr->nom_cond ?? '')); [$cognomeT, $nomeT, $ragioneT] = $this->splitNomeCognomeOrRagione($nom); $cf = trim((string) ($cr->cond_cod_fisc ?? '')) ?: null; $email = trim((string) ($cr->e_mail_condomino ?? '')) ?: null; $pec = trim((string) ($cr->pec_compr ?? '')) ?: null; $role = 'condomino'; $roleCustomRaw = $cr->descriz ?? ($cr->diritto_reale ?? null); $roleCustom = $this->normalizeRoleCustom($role, $roleCustomRaw); $rightPercent = $this->normalizePercentValue($cr->perc_diritto_reale ?? null); // Codice deterministico per comproprietari $codKey = $idCompr > 0 ? ('COMPR' . $idCompr) : ('COND' . $idCond . '|' . ($cf ?? $email ?? $nom)); $rubricaCode = $this->shortRubricaCode((string) $amministratoreId, (string) $codStabile, (string) $codKey, $role); $match = ['codice_univoco' => $rubricaCode]; $fiscal = $this->normalizeFiscalIds($cf); $data = [ 'amministratore_id' => $amministratoreId, 'codice_univoco' => $rubricaCode, 'ragione_sociale' => $ragioneT, 'nome' => $ragioneT ? null : $nomeT, 'cognome' => $ragioneT ? null : $cognomeT, 'tipo_contatto' => $ragioneT ? 'persona_giuridica' : 'persona_fisica', 'codice_fiscale' => $fiscal['cf'] ? mb_substr($fiscal['cf'], 0, 20) : null, 'partita_iva' => $fiscal['piva'] ? mb_substr($fiscal['piva'], 0, 20) : null, 'email' => ($emailNorm = $this->normalizeEmail($email)) ? mb_substr($emailNorm, 0, 191) : null, 'pec' => ($pecNorm = $this->normalizeEmail($pec)) ? mb_substr($pecNorm, 0, 191) : null, 'categoria' => 'condomino', 'stato' => 'attivo', ]; if (! $this->hasRubricaIdentity([ 'denominazione' => $data['ragione_sociale'] ?? null, 'nome' => $data['nome'] ?? null, 'cognome' => $data['cognome'] ?? null, 'cf' => $data['codice_fiscale'] ?? null, 'email' => $data['email'] ?? null, 'pec' => $data['pec'] ?? null, 'telefono' => null, 'cellulare' => null, ])) { $skipped++; continue; } if (! $apply) { $exists = RubricaUniversale::withTrashed()->where($match)->exists(); $exists ? $updated++ : $inserted++; } else { $record = $this->findExistingRubrica($amministratoreId, $data) ?: RubricaUniversale::withTrashed()->where($match)->first(); if ($record) { if (method_exists($record, 'trashed') && $record->trashed()) { $record->restore(); } if (! empty($record->codice_univoco) && (string) $record->codice_univoco !== (string) $rubricaCode) { unset($data['codice_univoco']); } $record->fill($data); if ($record->isDirty()) { $record->save(); } $updated++; } else { $data['note'] = 'Auto-sync comproprietari (staging)'; RubricaUniversale::create($data); $inserted++; $record = RubricaUniversale::withTrashed()->where($match)->first(); } if ($linkRuolo && $record) { $stabileId = (int) ($domain['id'] ?? 0); if ($stabileId > 0) { $exScala = isset($cr->ex_scala) ? trim((string) $cr->ex_scala) : null; $exInt = isset($cr->ex_int) ? trim((string) $cr->ex_int) : null; $unitaId = $this->resolveUnitaImmobiliareIdByComproprietarioHints( $stabileId, (string) $codStabile, $exScala, $exInt, $idCond, ); $dataInizio = $periodStart; $dataFine = $periodEnd; $isAttivo = true; if ($dataFine) { $isAttivo = (string) $dataFine >= date('Y-m-d'); } $ruoloMatch = [ 'rubrica_id' => (int) $record->id, 'ruolo_standard' => $role, 'stabile_id' => $stabileId, 'unita_immobiliare_id' => $unitaId, ]; if (Schema::hasColumn('rubrica_ruoli', 'legacy_anno_dir')) { $ruoloMatch['legacy_anno_dir'] = $legacyAnnoDir; } if (Schema::hasColumn('rubrica_ruoli', 'legacy_anno_o')) { $ruoloMatch['legacy_anno_o'] = $legacyAnnoO; } if ($roleCustom === null) { $existingCustom = RubricaRuolo::query() ->where($ruoloMatch) ->whereNotNull('ruolo_custom') ->orderByDesc('is_preferito') ->orderByDesc('id') ->value('ruolo_custom'); if (is_string($existingCustom) && trim($existingCustom) !== '') { $roleCustom = trim($existingCustom); } } $ruoloPayload = [ 'data_inizio' => $dataInizio, 'data_fine' => $dataFine, 'is_attivo' => $isAttivo, 'is_preferito' => true, 'ruolo_custom' => $roleCustom, 'legacy_anno_dir' => Schema::hasColumn('rubrica_ruoli', 'legacy_anno_dir') ? $legacyAnnoDir : null, 'legacy_anno_o' => Schema::hasColumn('rubrica_ruoli', 'legacy_anno_o') ? $legacyAnnoO : null, 'meta' => [ 'source' => 'gescon_import.comproprietari', 'cod_stabile' => $codStabile, 'id_cond' => $idCond, 'id_compr' => $idCompr, 'diritto_reale' => $cr->diritto_reale ?? null, 'descriz' => $cr->descriz ?? null, 'right_percent' => $rightPercent, 'percentuale_possesso' => $rightPercent, 'legacy_anno_dir' => $legacyAnnoDir, 'legacy_anno_o' => $legacyAnnoO, ], ]; $ruolo = RubricaRuolo::updateOrCreate($ruoloMatch, $ruoloPayload); $ruolo->wasRecentlyCreated ? $ruoliInserted++ : $ruoliUpdated++; } } } } } $msg = ($apply ? 'Sync eseguito' : 'Dry-run') . " - inseriti: {$inserted}, aggiornati: {$updated}, saltati: {$skipped}"; if ($linkRuolo) { $msg .= ", ruoli+ inseriti: {$ruoliInserted}, ruoli aggiornati: {$ruoliUpdated}"; } $this->info($msg); return 0; } /** * Identità minima per evitare record Rubrica vuoti. * @param array{denominazione:mixed,nome:mixed,cognome:mixed,cf:mixed,email:mixed,pec:mixed,telefono:mixed,cellulare:mixed} $candidate */ private function hasRubricaIdentity(array $candidate): bool { foreach (['denominazione', 'nome', 'cognome', 'cf', 'email', 'pec', 'telefono', 'cellulare'] as $k) { $v = trim((string) ($candidate[$k] ?? '')); if ($v !== '') { return true; } } return false; } private function rowLooksLikeTenant(object $row): bool { // Se la riga contiene dati tenant, non deve generare un "condomino" per fallback. if ((bool) ($row->inquilino ?? false)) { return true; } $tenantFields = [ $row->inquil_nome ?? null, $row->inquilino_denominazione ?? null, $row->inquil_cod_fisc ?? null, $row->e_mail_inquilino ?? null, $row->pec_inquilino ?? null, $row->inquil_tel1 ?? null, $row->cell_inq ?? null, $row->inquil_indir ?? null, $row->inquil_citta ?? null, $row->inquil_pr ?? null, $row->inquil_cap ?? null, $row->inquil_dal ?? null, $row->inquil_al ?? null, ]; foreach ($tenantFields as $value) { if (is_string($value) && trim($value) !== '') { return true; } if ($value !== null && ! is_string($value)) { return true; } } return false; } private function normalizeIdentityKey($value): ?string { if ($value === null) { return null; } $s = trim((string) $value); if ($s === '') { return null; } $s = mb_strtoupper($s); // Solo alfanumerico: riduce varianti come "MASSIMO RENDINA" vs "MASSIMO-RENDINA". $s = preg_replace('/[^A-Z0-9]+/u', '', $s); return $s !== '' ? $s : null; } private function normalizeCf(?string $cf): ?string { $cf = strtoupper(trim((string) ($cf ?? ''))); $cf = preg_replace('/\s+/', '', $cf); return $cf !== '' ? $cf : null; } /** * Normalizza identificativi fiscali italiani. * - Se solo cifre e lunghezza <= 11: pad a 11 (P.IVA). In Italia spesso coincide con CF per persone giuridiche. * - Se 16 alfanumerici: CF. * @return array{cf:?string,piva:?string} */ private function normalizeFiscalIds(?string $raw): array { $raw = trim((string) ($raw ?? '')); if ($raw === '') { return ['cf' => null, 'piva' => null]; } $norm = $this->normalizeCf($raw); if (! $norm) { return ['cf' => null, 'piva' => null]; } if (preg_match('/^\d+$/', $norm)) { // Probabile P.IVA/CF numerico: preserva zeri iniziali. if (strlen($norm) < 11) { $norm = str_pad($norm, 11, '0', STR_PAD_LEFT); } if (strlen($norm) === 11) { return ['cf' => $norm, 'piva' => $norm]; } } // CF classico 16 caratteri. return ['cf' => $norm, 'piva' => null]; } private function normalizeEmail(?string $email): ?string { $email = strtolower(trim((string) ($email ?? ''))); return $email !== '' ? $email : null; } private function normalizePhone(?string $phone): ?string { $phone = trim((string) ($phone ?? '')); if ($phone === '') { return null; } $digits = preg_replace('/[^0-9+]/', '', $phone); $digits = trim((string) $digits); return $digits !== '' ? $digits : null; } /** * Recupera identità tenant da altre righe condomin della stessa unità, quando la riga corrente è incompleta. * @return array{cf:?string,email:?string,telefono:?string,cellulare:?string} */ private function lookupTenantIdentityFromStaging(string $conn, string $codStabile, string $scala, string $interno, mixed $piano, mixed $tenantNameHint): array { try { if (! Schema::connection($conn)->hasTable('condomin')) { return ['cf' => null, 'email' => null, 'telefono' => null, 'cellulare' => null]; } $base = DB::connection($conn)->table('condomin') ->where('cod_stabile', $codStabile); $scala = trim((string) $scala); $interno = trim((string) $interno); if ($scala !== '') { $base->where('scala', $scala); } // Primo tentativo: match stretto per interno $q = clone $base; if ($interno !== '') { $q->where('interno', $interno); } // Piano: confronta solo se valorizzato in input $pianoStr = trim((string) ($piano ?? '')); if ($pianoStr !== '') { $q->where('piano', $piano); } $hint = trim((string) ($tenantNameHint ?? '')); if ($hint !== '') { $like = '%' . str_replace(['%', '_'], ['\%', '\_'], $hint) . '%'; $q->where(function ($qq) use ($like) { $qq->where('inquil_nome', 'like', $like) ->orWhere('inquilino_denominazione', 'like', $like); }); } $row = $q->orderByDesc('inquil_cod_fisc') ->orderByDesc('e_mail_inquilino') ->orderByDesc('inquil_tel1') ->orderByDesc('cell_inq') ->first([ 'inquil_cod_fisc', 'e_mail_inquilino', 'inquil_tel1', 'cell_inq', ]); // Secondo tentativo: per pertinenze o interno formattato diversamente, ignora interno e usa solo nome+scala. if (! $row && $hint !== '') { $q2 = clone $base; $like = '%' . str_replace(['%', '_'], ['\\%', '\\_'], $hint) . '%'; $q2->where(function ($qq) use ($like) { $qq->where('inquil_nome', 'like', $like) ->orWhere('inquilino_denominazione', 'like', $like); }); $q2->whereNotNull('inquil_cod_fisc')->where('inquil_cod_fisc', '!=', ''); $row = $q2->orderByDesc('inquil_cod_fisc')->first([ 'inquil_cod_fisc', 'e_mail_inquilino', 'inquil_tel1', 'cell_inq', ]); } // Terzo tentativo: matching "forte" in PHP su nome normalizzato (anche ordine parole invertito). if (! $row && $hint !== '') { $hintKey = $this->normalizeIdentityKey($hint); $hintWords = preg_split('/\s+/', trim($hint)) ?: []; $hintRev = $hintWords && count($hintWords) >= 2 ? implode(' ', array_reverse($hintWords)) : ''; $hintRevKey = $this->normalizeIdentityKey($hintRev); $cands = (clone $base) ->whereNotNull('inquil_cod_fisc')->where('inquil_cod_fisc', '!=', '') ->limit(500) ->get([ 'inquil_nome', 'inquilino_denominazione', 'inquil_cod_fisc', 'e_mail_inquilino', 'inquil_tel1', 'cell_inq', ]); foreach ($cands as $cand) { $k1 = $this->normalizeIdentityKey($cand->inquil_nome ?? null); $k2 = $this->normalizeIdentityKey($cand->inquilino_denominazione ?? null); $match = false; $contains = function (?string $a, ?string $b): bool { if (! $a || ! $b) { return false; } return str_contains($a, $b) || str_contains($b, $a); }; if ($hintKey && ($contains($k1, $hintKey) || $contains($k2, $hintKey))) { $match = true; } if (! $match && $hintRevKey && ($contains($k1, $hintRevKey) || $contains($k2, $hintRevKey))) { $match = true; } if ($match) { $row = $cand; break; } } } if (! $row) { return ['cf' => null, 'email' => null, 'telefono' => null, 'cellulare' => null]; } return [ 'cf' => $this->normalizeCf(! empty($row->inquil_cod_fisc) ? (string) $row->inquil_cod_fisc : null), 'email' => $this->normalizeEmail(! empty($row->e_mail_inquilino) ? (string) $row->e_mail_inquilino : null), 'telefono' => $this->normalizePhone(! empty($row->inquil_tel1) ? (string) $row->inquil_tel1 : null), 'cellulare' => $this->normalizePhone(! empty($row->cell_inq) ? (string) $row->cell_inq : null), ]; } catch (\Throwable $e) { return ['cf' => null, 'email' => null, 'telefono' => null, 'cellulare' => null]; } } private function findExistingRubrica(int $amministratoreId, array $data): ?RubricaUniversale { $ids = $this->normalizeFiscalIds($data['codice_fiscale'] ?? null); $cf = $ids['cf']; $piva = $ids['piva'] ?? $this->normalizeCf($data['partita_iva'] ?? null); if ($cf || $piva) { $found = RubricaUniversale::query() ->where('amministratore_id', $amministratoreId) ->where(function ($q) use ($cf, $piva) { if ($cf) { $q->orWhere('codice_fiscale', $cf); // Fallback: alcuni import storici perdono zeri iniziali. if (preg_match('/^0+\d+$/', $cf)) { $q->orWhere('codice_fiscale', ltrim($cf, '0')); } } if ($piva) { $q->orWhere('partita_iva', $piva); if (preg_match('/^0+\d+$/', $piva)) { $q->orWhere('partita_iva', ltrim($piva, '0')); } } }) ->orderBy('id') ->first(); if ($found) { return $found; } } $email = $this->normalizeEmail($data['email'] ?? null); if ($email) { $found = RubricaUniversale::query() ->where('amministratore_id', $amministratoreId) ->where('email', $email) ->first(); if ($found) { return $found; } } $telefono = $this->normalizePhone($data['telefono_ufficio'] ?? null); $cellulare = $this->normalizePhone($data['telefono_cellulare'] ?? null); $phones = array_values(array_unique(array_filter([$telefono, $cellulare]))); if (! empty($phones)) { $found = RubricaUniversale::query() ->where('amministratore_id', $amministratoreId) ->where(function ($q) use ($phones) { foreach ($phones as $p) { $q->orWhere('telefono_ufficio', $p) ->orWhere('telefono_cellulare', $p) ->orWhere('telefono_casa', $p); } }) ->first(); if ($found) { return $found; } } return null; } private function normalizeDateToYmd(mixed $v): ?string { if ($v === null) { return null; } $s = trim((string) $v); if ($s === '') { return null; } if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $s, $m)) { $year = (int) $m[1]; if ($year >= 0 && $year <= 99) { $year = 2000 + $year; $s = sprintf('%04d-%02d-%02d', $year, (int) $m[2], (int) $m[3]); } if ($year < 1900 || $year > 2100) { return null; } return $s; } // Legacy: 01/01/25 00:00:00 oppure 01/01/2025 $dt = \DateTime::createFromFormat('m/d/y H:i:s', $s) ?: \DateTime::createFromFormat('m/d/Y H:i:s', $s) ?: \DateTime::createFromFormat('d/m/y H:i:s', $s) ?: \DateTime::createFromFormat('d/m/Y H:i:s', $s) ?: \DateTime::createFromFormat('m/d/y', $s) ?: \DateTime::createFromFormat('m/d/Y', $s) ?: \DateTime::createFromFormat('d/m/y', $s) ?: \DateTime::createFromFormat('d/m/Y', $s); if ($dt) { $out = $dt->format('Y-m-d'); if (preg_match('/^(\d{4})-/', $out, $m2)) { $year = (int) $m2[1]; if ($year < 1900 || $year > 2100) { return null; } } return $out; } $ts = @strtotime($s); if (! $ts) { return null; } $out = date('Y-m-d', $ts); if (preg_match('/^(\d{4})-/', $out, $m3)) { $year = (int) $m3[1]; if ($year < 1900 || $year > 2100) { return null; } } return $out; } /** @return array{0:?string,1:?string,2:?string} [cognome,nome,ragione_sociale] */ private function splitNomeCognomeOrRagione(?string $raw): array { $s = trim((string) ($raw ?? '')); if ($s === '') { return [null, null, null]; } // Se contiene caratteri tipici di ragione sociale o è tutto maiuscolo lungo, trattalo come ragione if (preg_match('/\b(SRL|SPA|S\.R\.L\.|S\.P\.A\.|SAS|SNC|ASSOCIAZIONE|CONDOMINIO)\b/i', $s)) { return [null, null, mb_substr($s, 0, 255)]; } $parts = array_values(array_filter(preg_split('/\s+/', $s) ?: [])); if (count($parts) >= 2) { $nome = array_pop($parts); $cognome = implode(' ', $parts); return [mb_substr($cognome, 0, 100), mb_substr($nome, 0, 100), null]; } return [null, null, mb_substr($s, 0, 255)]; } private function resolveUnitaImmobiliareIdByIdCond(int $stabileId, int $idCond): ?int { if ($stabileId <= 0 || $idCond <= 0) { return null; } if (! Schema::hasTable('unita_immobiliari')) { return null; } if (! Schema::hasColumn('unita_immobiliari', 'codice_unita')) { return null; } $cod = preg_replace('/[^A-Za-z0-9_-]/', '', (string) $idCond); if (! $cod) { return null; } $id = DB::table('unita_immobiliari') ->where('stabile_id', $stabileId) ->where('codice_unita', $cod) ->orderBy('id') ->value('id'); return $id ? (int) $id : null; } private function resolveUnitaImmobiliareIdByComproprietarioHints( int $stabileId, string $codStabile, ?string $exScala, ?string $exInt, int $idCond, ): ?int { if ($stabileId <= 0) { return null; } if (! Schema::hasTable('unita_immobiliari') || ! Schema::hasColumn('unita_immobiliari', 'codice_unita')) { return null; } $codStabile = strtoupper(trim($codStabile)); $scala = $exScala !== null ? strtoupper(trim($exScala)) : ''; $interno = $exInt !== null ? strtoupper(trim($exInt)) : ''; $clean = static function (string $s): string { return preg_replace('/[^A-Z0-9_-]+/', '', $s) ?: ''; }; $codStabile = $clean($codStabile); $scala = $clean($scala); $interno = $clean($interno); // In import unità il codice è costruito come: COD_STAB-SCALA-INT if ($codStabile !== '' && $scala !== '' && $interno !== '') { $codiceUnita = $codStabile . '-' . $scala . '-' . $interno; $id = DB::table('unita_immobiliari') ->where('stabile_id', $stabileId) ->where('codice_unita', $codiceUnita) ->orderBy('id') ->value('id'); if ($id) { return (int) $id; } } return $this->resolveUnitaImmobiliareIdByIdCond($stabileId, $idCond); } private function shortRubricaCode(string $amministratoreId, string $codStabile, string $codCond, string $role): string { // Deterministico, lunghezza 8 per la colonna CHAR(8): prefisso C + hash crc32b $input = strtoupper(trim($amministratoreId)) . '|' . strtoupper(trim($codStabile)) . '|' . strtoupper(trim($codCond)) . '|' . strtoupper(trim($role)); return substr('C' . hash('crc32b', $input), 0, 8); } private function cleanShort(string $value): ?string { $v = trim($value); return $v === '' ? null : $v; } private function firstNonEmptyStr(array $values, int $maxLen): ?string { foreach ($values as $v) { if ($v === null) { continue; } $s = trim((string) $v); if ($s === '') { continue; } return mb_substr($s, 0, $maxLen); } return null; } private function normalizePiano(mixed $raw): ?int { if ($raw === null) { return null; } $s = trim((string) $raw); if ($s === '') { return null; } $upper = strtoupper($s); if (in_array($upper, ['T', 'PT', 'P.T.', 'PIANO TERRA'], true)) { return 0; } // numerico semplice: -2, -1, 0, 1, 2... if (preg_match('/^-?\d+$/', $s)) { return (int) $s; } return null; } private function resolveUnitaImmobiliareId(int $stabileId, ?string $scala, ?string $interno, ?int $piano): ?int { if ($stabileId <= 0 || ! $scala || ! $interno) { return null; } if (! Schema::hasTable('unita_immobiliari')) { return null; } $q = DB::table('unita_immobiliari') ->where('stabile_id', $stabileId) ->where('scala', $scala) ->where('interno', $interno); if ($piano !== null && Schema::hasColumn('unita_immobiliari', 'piano')) { $q->where('piano', $piano); } $id = $q->orderBy('id')->value('id'); return $id ? (int) $id : null; } private function isCondominialePlaceholder(string $value): bool { $v = strtoupper(trim($value)); if ($v === '') { return false; } // Placeholder tipici presi dai descrittori unità / tipologie. if (str_contains($v, 'AREA CONDOMINIALE')) { return true; } if (str_contains($v, 'SPAZIO CONDOMINIALE')) { return true; } if (str_contains($v, 'PARTI COMUNI')) { return true; } return false; } /** @return array{custom:?string, percent: mixed} */ private function resolveRightInfo(object $row, string $subject): array { $subject = strtolower($subject); if ($subject === 'proprietario') { $custom = $row->proprietario_diritto_descrizione ?? $row->proprietario_diritto_valore ?? $row->diritto_reale ?? $row->diritto_godimento ?? null; $percent = $row->proprietario_diritto_percentuale ?? $row->perc_diritto_reale ?? null; } else { $custom = $row->inquilino_diritto_descrizione ?? $row->inquilino_diritto_valore ?? null; $percent = $row->inquilino_diritto_percentuale ?? null; } $custom = is_string($custom) ? trim($custom) : null; if ($custom === '') { $custom = null; } return ['custom' => $custom, 'percent' => $percent]; } private function normalizeRoleCustom(string $roleStandard, mixed $raw): ?string { $roleStandard = strtolower(trim($roleStandard)); $s = is_string($raw) ? trim($raw) : ''; if ($s === '') { return null; } $norm = strtolower($s); $norm = str_replace(['à', 'è', 'é', 'ì', 'ò', 'ù'], ['a', 'e', 'e', 'i', 'o', 'u'], $norm); $norm = preg_replace('/[^a-z0-9_\s-]+/', '', $norm) ?? $norm; $norm = str_replace([' ', '-'], '_', $norm); $norm = preg_replace('/_+/', '_', $norm) ?? $norm; $norm = trim($norm, '_'); if ($roleStandard === 'inquilino') { return $norm; } // Normalizza principali aventi diritto. return match ($norm) { 'proprietario', 'prop' => 'proprietario', 'comproprietario', 'co_proprietario' => 'comproprietario', 'nudo_proprietario', 'nuda_proprieta' => 'nudo_proprietario', 'usufruttuario', 'usufrutto' => 'usufruttuario', 'titolare' => 'titolare', default => $norm, }; } private function normalizePercentValue(mixed $raw): ?float { if ($raw === null) { return null; } if (is_string($raw)) { $raw = trim($raw); if ($raw === '') { return null; } $raw = str_replace(['%', ' '], ['', ''], $raw); $raw = str_replace(',', '.', $raw); } if (! is_numeric($raw)) { return null; } return (float) $raw; } }