1365 lines
62 KiB
PHP
1365 lines
62 KiB
PHP
<?php
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\RubricaRuolo;
|
|
use App\Models\RubricaUniversale;
|
|
use App\Models\Stabile;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\Config;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
class GesconAutoSyncAnagrafiche extends Command
|
|
{
|
|
protected $signature = 'gescon:auto-sync-anagrafiche
|
|
{--stabile= : Codice stabile legacy (es. 0021) per filtrare condomin.cod_stabile}
|
|
{--limit=5000 : Max righe condomin da processare}
|
|
{--only=both : Quali soggetti importare (proprietario|inquilino|both)}
|
|
{--link-ruolo : Crea/aggiorna anche rubrica_ruoli collegando allo stabile di dominio}
|
|
{--include-comproprietari : Importa anche gescon_import.comproprietari (se presente) e crea ruoli collegati alle unità}
|
|
{--period-start= : Data inizio periodo (Y-m-d o formati legacy) da usare come fallback per data_inizio}
|
|
{--period-end= : Data fine periodo (Y-m-d o formati legacy) da usare come fallback per data_fine (vuoto = aperto)}
|
|
{--legacy-anno-dir= : Codice cartella annuale legacy (es. 0003) da salvare su rubrica_ruoli}
|
|
{--legacy-anno-o= : Anno ordinario (es. 2025) da salvare su rubrica_ruoli}
|
|
{--apply : Esegue realmente l\'upsert. Senza flag è solo dry-run}';
|
|
|
|
protected $description = 'Sincronizza contatti da gescon_import.condomin verso Rubrica Universale (o dry-run).';
|
|
|
|
public function handle(): int
|
|
{
|
|
$apply = (bool) $this->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<string, array{id:int, amministratore_id:int, codice_stabile:string}> $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 ?? $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;
|
|
}
|
|
|
|
$orphan = RubricaUniversale::query()
|
|
->whereNull('amministratore_id')
|
|
->where(function ($q) use ($cf, $piva) {
|
|
if ($cf) {
|
|
$q->orWhere('codice_fiscale', $cf);
|
|
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 ($orphan) {
|
|
return $orphan;
|
|
}
|
|
}
|
|
|
|
$email = $this->normalizeEmail($data['email'] ?? null);
|
|
if ($email) {
|
|
$found = RubricaUniversale::query()
|
|
->where('amministratore_id', $amministratoreId)
|
|
->where('email', $email)
|
|
->first();
|
|
if ($found) {
|
|
return $found;
|
|
}
|
|
|
|
$orphan = RubricaUniversale::query()
|
|
->whereNull('amministratore_id')
|
|
->where('email', $email)
|
|
->orderBy('id')
|
|
->first();
|
|
if ($orphan) {
|
|
return $orphan;
|
|
}
|
|
}
|
|
|
|
$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;
|
|
}
|
|
|
|
$orphan = RubricaUniversale::query()
|
|
->whereNull('amministratore_id')
|
|
->where(function ($q) use ($phones) {
|
|
foreach ($phones as $p) {
|
|
$q->orWhere('telefono_ufficio', $p)
|
|
->orWhere('telefono_cellulare', $p)
|
|
->orWhere('telefono_casa', $p);
|
|
}
|
|
})
|
|
->orderBy('id')
|
|
->first();
|
|
if ($orphan) {
|
|
return $orphan;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|