$tipo === Richiesta::RICONSEGNA ? Richiesta::RICONSEGNA : Richiesta::NUOVO, 'nome' => trim($nome), 'numero_territorio' => $tipo === Richiesta::RICONSEGNA ? trim((string) $numero) : null, 'note' => trim((string) $note) !== '' ? trim((string) $note) : null, ]); // Nel registro tipo e numero, mai il nome. activity()->performedOn($richiesta) ->withProperties(array_filter(['tipo' => $richiesta->tipo, 'territorio' => $richiesta->numero_territorio])) ->log('richiesta_ricevuta'); $this->avvisa($richiesta); return $richiesta; } /** * Chi gestisce le richieste e ha un'email vera: amministratori e * assistenti (permesso richieste.gestisci) con l'indirizzo confermato. */ public function destinatari(): Collection { return User::permission('richieste.gestisci')->get() ->filter(fn (User $u) => $u->email && $u->email_verified_at !== null) ->values(); } /** * L'email parte subito e non in coda: contiene il nome scritto nella * richiesta, che nel database è cifrato e in coda starebbe in chiaro. * Senza un server di posta funzionante non si manda niente: la richiesta * resta comunque nella Home. */ private function avvisa(Richiesta $richiesta): void { if (! $this->posta->funzionante()) { return; } $destinatari = $this->destinatari(); if ($destinatari->isEmpty()) { return; } try { Notification::send($destinatari, new RichiestaRicevuta($richiesta)); } catch (\Throwable $e) { report($e); } } /** * I proclamatori attivi con un nome riconducibile a quello scritto nella * richiesta, dal più simile: uguale, iniziale, una lettera sbagliata. * I nomi sono cifrati, quindi il confronto si fa qui, non in SQL. * * @return Collection */ public function nomiSimili(string $nome, ?Collection $proclamatori = null, int $quanti = 5): Collection { $cercate = self::parole($nome); if ($cercate === []) { return collect(); } return ($proclamatori ?? Proclamatore::attivi()->get()) ->map(function (Proclamatore $p) use ($cercate) { $sue = self::parole($p->cognome . ' ' . $p->nome); $punti = 0; foreach ($cercate as $parola) { $migliore = 0; foreach ($sue as $sua) { $migliore = max($migliore, self::somiglianza($parola, $sua)); } $punti += $migliore; } return [$p, $punti]; }) ->filter(fn ($coppia) => $coppia[1] >= 2) ->sortBy([[fn ($coppia) => $coppia[1], 'desc'], [fn ($coppia) => mb_strtolower($coppia[0]->nome_completo), 'asc']]) ->take($quanti) ->map(fn ($coppia) => $coppia[0]) ->values(); } /** L'assegnazione aperta del territorio indicato in una riconsegna, se c'è. */ public function assegnazioneAperta(?string $numero): ?Assegnazione { $numero = trim((string) $numero); if ($numero === '') { return null; } $territorio = Territorio::query() ->whereRaw('LOWER(numero) = ?', [Str::lower($numero)]) ->with('assegnazioneCorrente.proclamatore') ->first(); return $territorio?->assegnazioneCorrente; } /** @return string[] parole minuscole, senza accenti né segni */ private static function parole(string $testo): array { $pulito = preg_replace('/[^a-z0-9 ]+/', ' ', Str::lower(Str::ascii($testo))); return array_values(array_filter(explode(' ', (string) $pulito), fn ($p) => strlen($p) >= 2)); } /** 3 uguale, 2 una inizia con l'altra, 1 una lettera di differenza. */ private static function somiglianza(string $a, string $b): int { if ($a === $b) { return 3; } if (min(strlen($a), strlen($b)) >= 3 && (str_starts_with($a, $b) || str_starts_with($b, $a))) { return 2; } if (min(strlen($a), strlen($b)) >= 4 && levenshtein($a, $b) <= 1) { return 1; } return 0; } }