argument('amministratore_id'); if ($adminId <= 0) { $this->error('amministratore_id non valido'); return self::FAILURE; } $dry = (bool) $this->option('dry-run'); $deleteEmpties = (bool) $this->option('delete-empties'); if (!Schema::hasTable('rubrica_universale')) { $this->error('Tabella rubrica_universale non disponibile.'); return self::FAILURE; } $stats = [ 'groups' => 0, 'duplicates' => 0, 'merged' => 0, 'rubrica_soft_deleted' => 0, 'rubrica_empties_soft_deleted' => 0, 'moved_ruoli' => 0, 'moved_canali' => 0, 'moved_fornitori' => 0, 'moved_stabili' => 0, ]; $runner = function () use ($adminId, $dry, $deleteEmpties, &$stats): void { // Carica i contatti dell'admin (escludi già soft-deleted per evitare merge strani). $rubriche = RubricaUniversale::query() ->where('amministratore_id', $adminId) ->whereNull('deleted_at') ->get([ 'id', 'codice_univoco', 'tipo_contatto', 'ragione_sociale', 'nome', 'cognome', 'codice_fiscale', 'partita_iva', 'email', 'pec', 'telefono_ufficio', 'telefono_cellulare', 'telefono_casa', 'indirizzo', 'cap', 'citta', 'provincia', 'note', 'categoria', 'stato', ]); /** @var array> $groups */ $groups = []; foreach ($rubriche as $r) { $keys = $this->buildIdentityKeys($r); if (empty($keys)) { continue; } // Usa una chiave primaria stabile (preferisci fiscale), altrimenti email/telefono. $primary = $keys[0]; $groups[$primary] ??= []; $groups[$primary][] = $r; } foreach ($groups as $key => $items) { if (count($items) < 2) { continue; } $stats['groups']++; $stats['duplicates'] += (count($items) - 1); $master = $this->pickMaster($items); $others = array_values(array_filter($items, fn($x) => (int) $x->id !== (int) $master->id)); foreach ($others as $dup) { if ($dry) { $stats['merged']++; continue; } DB::transaction(function () use ($adminId, $master, $dup, &$stats): void { // 1) Merge campi (non sovrascrivere i valori del master, riempi solo i null/vuoti) $payload = $this->mergeRubricaPayload($master, $dup); if (!empty($payload)) { $master->fill($payload); if ($master->isDirty()) { $master->save(); } } // 2) Sposta relazioni verso master if (Schema::hasTable('rubrica_ruoli')) { $stats['moved_ruoli'] += RubricaRuolo::query() ->where('rubrica_id', (int) $dup->id) ->update(['rubrica_id' => (int) $master->id]); } if (Schema::hasTable('rubrica_contatto_canali')) { $stats['moved_canali'] += RubricaContattoCanale::query() ->where('rubrica_id', (int) $dup->id) ->update(['rubrica_id' => (int) $master->id]); } if (Schema::hasTable('fornitori')) { $stats['moved_fornitori'] += Fornitore::query() ->where('amministratore_id', $adminId) ->where('rubrica_id', (int) $dup->id) ->update(['rubrica_id' => (int) $master->id]); } if (Schema::hasTable('stabili') && Schema::hasColumn('stabili', 'rubrica_id')) { $stats['moved_stabili'] += Stabile::query() ->where('amministratore_id', $adminId) ->where('rubrica_id', (int) $dup->id) ->update(['rubrica_id' => (int) $master->id]); } // 3) Soft-delete duplicato $note = trim((string) ($dup->note ?? '')); $suffix = 'Merged into rubrica_id=' . (int) $master->id; if ($note === '') { $dup->note = $suffix; } elseif (!str_contains($note, $suffix)) { $dup->note = mb_substr($note . "\n" . $suffix, 0, 65535); } $dup->save(); $dup->delete(); $stats['rubrica_soft_deleted']++; $stats['merged']++; }); } } if (!$deleteEmpties) { return; } // Soft-delete contatti completamente vuoti e non collegati a nulla. $empties = RubricaUniversale::query() ->where('amministratore_id', $adminId) ->whereNull('deleted_at') ->get(['id', 'ragione_sociale', 'nome', 'cognome', 'codice_fiscale', 'partita_iva', 'email', 'pec', 'telefono_ufficio', 'telefono_cellulare', 'telefono_casa']); foreach ($empties as $r) { if (!$this->isRubricaEmptyIdentity($r)) { continue; } $hasRuoli = Schema::hasTable('rubrica_ruoli') ? RubricaRuolo::query()->where('rubrica_id', (int) $r->id)->exists() : false; $hasCanali = Schema::hasTable('rubrica_contatto_canali') ? RubricaContattoCanale::query()->where('rubrica_id', (int) $r->id)->exists() : false; $hasFornitori = Schema::hasTable('fornitori') ? Fornitore::query()->where('amministratore_id', $adminId)->where('rubrica_id', (int) $r->id)->exists() : false; $hasStabili = (Schema::hasTable('stabili') && Schema::hasColumn('stabili', 'rubrica_id')) ? Stabile::query()->where('amministratore_id', $adminId)->where('rubrica_id', (int) $r->id)->exists() : false; if ($hasRuoli || $hasCanali || $hasFornitori || $hasStabili) { continue; } $stats['rubrica_empties_soft_deleted']++; if (!$dry) { $r->note = trim(((string) ($r->note ?? '')) . "\n" . 'Auto-clean: empty identity'); $r->save(); $r->delete(); } } }; if ($dry) { $runner(); } else { DB::transaction($runner); } $this->info('✅ Rubrica dedup completata' . ($dry ? ' (dry-run)' : '')); $this->line(json_encode($stats, JSON_UNESCAPED_UNICODE)); return self::SUCCESS; } /** @return list */ private function buildIdentityKeys(RubricaUniversale $r): array { $keys = []; $cf = $this->normalizeCf($r->codice_fiscale); $piva = $this->normalizeCf($r->partita_iva); $fiscal = $this->normalizeFiscalIds($cf ?: $piva); if ($fiscal['cf']) { $keys[] = 'CF:' . $fiscal['cf']; } if ($fiscal['piva']) { $keys[] = 'PIVA:' . $fiscal['piva']; } $email = $this->normalizeEmail($r->email); if ($email) { $keys[] = 'EMAIL:' . $email; } $phones = array_values(array_unique(array_filter([ $this->normalizePhone($r->telefono_ufficio), $this->normalizePhone($r->telefono_cellulare), $this->normalizePhone($r->telefono_casa), ]))); foreach ($phones as $p) { $keys[] = 'TEL:' . $p; } // Ordina: fiscale > email > telefono usort($keys, function (string $a, string $b): int { $wa = str_starts_with($a, 'CF:') || str_starts_with($a, 'PIVA:') ? 0 : (str_starts_with($a, 'EMAIL:') ? 1 : 2); $wb = str_starts_with($b, 'CF:') || str_starts_with($b, 'PIVA:') ? 0 : (str_starts_with($b, 'EMAIL:') ? 1 : 2); return $wa <=> $wb; }); return $keys; } /** @param array $items */ private function pickMaster(array $items): RubricaUniversale { $best = null; $bestScore = -1; foreach ($items as $r) { $score = 0; foreach ( [ 'ragione_sociale', 'nome', 'cognome', 'codice_fiscale', 'partita_iva', 'email', 'pec', 'telefono_ufficio', 'telefono_cellulare', 'telefono_casa', 'indirizzo', 'cap', 'citta', 'provincia', ] as $f ) { $v = trim((string) ($r->{$f} ?? '')); if ($v !== '') { $score += 2; } } $note = trim((string) ($r->note ?? '')); if ($note !== '') { $score += 1; } // Preferisci record che hanno già codice_univoco (stabile) e min id per stabilità. $code = trim((string) ($r->codice_univoco ?? '')); if ($code !== '') { $score += 1; } if ($best === null || $score > $bestScore || ($score === $bestScore && (int) $r->id < (int) $best->id)) { $best = $r; $bestScore = $score; } } return $best ?? $items[0]; } private function mergeRubricaPayload(RubricaUniversale $master, RubricaUniversale $dup): array { $fields = [ 'tipo_contatto', 'ragione_sociale', 'nome', 'cognome', 'codice_fiscale', 'partita_iva', 'email', 'pec', 'telefono_ufficio', 'telefono_cellulare', 'telefono_casa', 'indirizzo', 'cap', 'citta', 'provincia', 'categoria', 'stato', ]; $payload = []; foreach ($fields as $f) { $cur = trim((string) ($master->{$f} ?? '')); $val = trim((string) ($dup->{$f} ?? '')); if ($cur === '' && $val !== '') { $payload[$f] = $dup->{$f}; } } // Note: concatena solo se il master è vuoto. $mNote = trim((string) ($master->note ?? '')); $dNote = trim((string) ($dup->note ?? '')); if ($mNote === '' && $dNote !== '') { $payload['note'] = $dup->note; } return $payload; } private function isRubricaEmptyIdentity(RubricaUniversale $r): bool { foreach (['ragione_sociale', 'nome', 'cognome', 'codice_fiscale', 'partita_iva', 'email', 'pec', 'telefono_ufficio', 'telefono_cellulare', 'telefono_casa'] as $f) { $v = trim((string) ($r->{$f} ?? '')); if ($v !== '') { return false; } } return true; } private function normalizeCf(?string $cf): ?string { $cf = strtoupper(trim((string) ($cf ?? ''))); $cf = preg_replace('/\s+/', '', $cf); return $cf !== '' ? $cf : 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; } /** @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)) { if (strlen($norm) < 11) { $norm = str_pad($norm, 11, '0', STR_PAD_LEFT); } if (strlen($norm) === 11) { return ['cf' => $norm, 'piva' => $norm]; } } return ['cf' => $norm, 'piva' => null]; } }