NetGescon).'; public function handle(): int { $dryRun = (bool) $this->option('dry-run'); $maxPages = max(1, (int) $this->option('max-pages')); $filterAdminIds = collect((array) $this->option('admin-id')) ->map(fn($v) => (int) $v) ->filter(fn(int $v) => $v > 0) ->values() ->all(); $query = Amministratore::query(); if ($filterAdminIds !== []) { $query->whereIn('id', $filterAdminIds); } $amministratori = $query->get(); if ($amministratori->isEmpty()) { $this->warn('Nessun amministratore trovato per i filtri richiesti.'); return self::SUCCESS; } $totalCreated = 0; $totalUpdated = 0; $totalSkipped = 0; foreach ($amministratori as $amministratore) { $google = Arr::get($amministratore->impostazioni ?? [], 'google', []); $oauth = Arr::get($google, 'oauth', []); $connected = (bool) Arr::get($oauth, 'connected', false); if (! $connected) { $this->line("Admin {$amministratore->id}: Google non collegato, salto."); continue; } $token = $this->resolveAccessToken($amministratore, $google, $oauth, $dryRun); if ($token === null) { $this->warn("Admin {$amministratore->id}: token non disponibile, salto."); continue; } $adminUserId = (int) ($amministratore->user_id ?? 0); $created = 0; $updated = 0; $skipped = 0; $nextPageToken = null; for ($page = 0; $page < $maxPages; $page++) { $response = Http::withToken($token) ->acceptJson() ->get('https://people.googleapis.com/v1/people/me/connections', [ 'personFields' => 'names,emailAddresses,phoneNumbers', 'pageSize' => 200, 'pageToken' => $nextPageToken, ]); if ($response->status() === 401) { $token = $this->resolveAccessToken($amministratore, $google, $oauth, $dryRun, true) ?? $token; $response = Http::withToken($token) ->acceptJson() ->get('https://people.googleapis.com/v1/people/me/connections', [ 'personFields' => 'names,emailAddresses,phoneNumbers', 'pageSize' => 200, 'pageToken' => $nextPageToken, ]); } if (! $response->successful()) { $this->warn("Admin {$amministratore->id}: errore People API ({$response->status()})."); break; } $connections = $response->json('connections'); if (! is_array($connections) || count($connections) === 0) { break; } foreach ($connections as $person) { $name = trim((string) Arr::get($person, 'names.0.displayName', '')); $email = trim((string) Arr::get($person, 'emailAddresses.0.value', '')); $phone = trim((string) Arr::get($person, 'phoneNumbers.0.value', '')); $resourceName = trim((string) Arr::get($person, 'resourceName', '')); [$phoneE164, $phonePrefix, $phoneNational] = PhoneNumber::splitItaly($phone); $phoneNormalized = PhoneNumber::normalizeForMatch((string) ($phoneE164 ?? $phone)); if ($name === '' && $email === '' && $phoneE164 === null) { $skipped++; continue; } [$nome, $cognome] = $this->splitDisplayName($name, $email); $categoria = $this->inferCategoria($name, $email); $tipoContatto = $this->inferTipoContatto($name, $email); $today = now()->toDateString(); $existing = RubricaUniversale::withTrashed() ->where(function ($q) use ($email, $phone, $phoneNormalized): void { if ($email !== '') { $q->orWhere('email', $email); } if ($phone !== '' || $phoneNormalized !== '') { if ($phone !== '') { $q->orWhere('telefono_cellulare', $phone) ->orWhere('telefono_ufficio', $phone) ->orWhere('telefono_casa', $phone); } if ($phoneNormalized !== '') { $digits = preg_replace('/\D+/', '', $phoneNormalized) ?: ''; if ($digits !== '') { $needle = '%' . $digits . '%'; $q->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_cellulare, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle]) ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_ufficio, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle]) ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_casa, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle]); } } } }) ->first(); if ($existing instanceof RubricaUniversale) { $isGoogleOrigin = str_contains(strtolower((string) ($existing->note ?? '')), 'origine contatto: google workspace'); $changed = false; if ($this->shouldSyncField($existing->nome, $nome, $isGoogleOrigin)) { $existing->nome = $nome; $changed = true; } if ($this->shouldSyncField($existing->cognome, $cognome, $isGoogleOrigin)) { $existing->cognome = $cognome; $changed = true; } if ($this->shouldSyncField($existing->email, $email, $isGoogleOrigin)) { $existing->email = $email; $changed = true; } if ($this->shouldSyncField($existing->telefono_cellulare, $phoneE164, $isGoogleOrigin)) { $existing->telefono_cellulare = $phoneE164; $changed = true; } if ($this->shouldSyncField($existing->prefisso_cellulare, $phonePrefix, $isGoogleOrigin)) { $existing->prefisso_cellulare = $phonePrefix; $changed = true; } if ($this->shouldSyncField($existing->telefono_cellulare_nazionale, $phoneNational, $isGoogleOrigin)) { $existing->telefono_cellulare_nazionale = $phoneNational; $changed = true; } if ($resourceName !== '' && $this->shouldSyncField($existing->google_person_resource_name, $resourceName, true)) { $existing->google_person_resource_name = $resourceName; $changed = true; } if (($existing->categoria ?? '') === '' || $existing->categoria === 'altro') { $existing->categoria = $categoria; $changed = true; } if (($existing->tipo_contatto ?? '') === '') { $existing->tipo_contatto = $tipoContatto; $changed = true; } if (($existing->note ?? '') === '') { $existing->note = 'Origine contatto: Google Workspace'; $changed = true; } if ($changed) { if (! $dryRun) { if (method_exists($existing, 'trashed') && $existing->trashed()) { $existing->restore(); } $existing->data_ultima_modifica = $today; if ($adminUserId > 0) { $existing->modificato_da = $adminUserId; } $existing->save(); } $updated++; } else { $skipped++; } continue; } $created++; if ($dryRun) { continue; } RubricaUniversale::create([ 'nome' => $nome, 'cognome' => $cognome, 'tipo_contatto' => $tipoContatto, 'telefono_cellulare' => $phoneE164, 'prefisso_cellulare' => $phonePrefix, 'telefono_cellulare_nazionale' => $phoneNational, 'email' => $email !== '' ? $email : null, 'google_person_resource_name' => $resourceName !== '' ? $resourceName : null, 'categoria' => $categoria, 'stato' => 'attivo', 'note' => 'Origine contatto: Google Workspace', 'data_inserimento' => $today, 'data_ultima_modifica' => $today, 'creato_da' => $adminUserId > 0 ? $adminUserId : null, 'modificato_da' => $adminUserId > 0 ? $adminUserId : null, ]); } $nextPageToken = $response->json('nextPageToken'); if (! is_string($nextPageToken) || $nextPageToken === '') { break; } } $totalCreated += $created; $totalUpdated += $updated; $totalSkipped += $skipped; $mode = $dryRun ? 'dry-run' : 'apply'; $this->info("Admin {$amministratore->id} ({$mode}): creati {$created}, aggiornati {$updated}, invariati/scartati {$skipped}."); } $this->newLine(); $this->info("Totale: creati {$totalCreated}, aggiornati {$totalUpdated}, invariati/scartati {$totalSkipped}."); return self::SUCCESS; } private function resolveAccessToken(Amministratore $amministratore, array $google, array $oauth, bool $dryRun, bool $forceRefresh = false): ?string { $accessToken = trim((string) Arr::get($oauth, 'access_token', '')); $refreshToken = trim((string) Arr::get($oauth, 'refresh_token', '')); if (! $forceRefresh && $accessToken !== '') { return $accessToken; } if ($refreshToken === '') { return null; } $settingsClientId = trim((string) Arr::get($google, 'client_id', '')); $settingsClientSecret = trim((string) Arr::get($google, 'client_secret', '')); $configClientId = trim((string) config('services.google.client_id')); $configClientSecret = trim((string) config('services.google.client_secret')); $credentialPairs = []; if ($settingsClientId !== '' && $settingsClientSecret !== '') { $credentialPairs[] = [$settingsClientId, $settingsClientSecret, 'settings']; } if ( $configClientId !== '' && $configClientSecret !== '' && ($configClientId !== $settingsClientId || $configClientSecret !== $settingsClientSecret) ) { $credentialPairs[] = [$configClientId, $configClientSecret, 'config']; } if ($credentialPairs === []) { return null; } $payload = null; $lastError = null; foreach ($credentialPairs as [$clientId, $clientSecret, $source]) { $response = Http::asForm()->post('https://oauth2.googleapis.com/token', [ 'client_id' => $clientId, 'client_secret' => $clientSecret, 'refresh_token' => $refreshToken, 'grant_type' => 'refresh_token', ]); if ($response->successful()) { $payload = $response->json(); break; } $message = (string) (Arr::get($response->json(), 'error_description') ?: Arr::get($response->json(), 'error.message') ?: 'refresh_failed'); $lastError = "{$source}:{$response->status()} {$message}"; } if (! is_array($payload)) { $this->warn("Admin {$amministratore->id}: refresh token fallito ({$lastError})."); return null; } $newAccessToken = trim((string) Arr::get($payload, 'access_token', '')); if ($newAccessToken === '') { return null; } if (! $dryRun) { $impostazioni = $amministratore->impostazioni ?? []; $impostazioniGoogle = Arr::get($impostazioni, 'google', []); $impostazioniOauth = Arr::get($impostazioniGoogle, 'oauth', []); $impostazioniOauth['access_token'] = $newAccessToken; $impostazioniOauth['expires_in'] = (int) Arr::get($payload, 'expires_in', 3600); $impostazioniOauth['refreshed_at'] = Carbon::now()->toDateTimeString(); if (isset($payload['refresh_token']) && is_string($payload['refresh_token']) && $payload['refresh_token'] !== '') { $impostazioniOauth['refresh_token'] = $payload['refresh_token']; } $impostazioniGoogle['oauth'] = $impostazioniOauth; $impostazioni['google'] = $impostazioniGoogle; $amministratore->impostazioni = $impostazioni; $amministratore->save(); } return $newAccessToken; } private function splitDisplayName(string $displayName, string $email): array { $source = trim($displayName); if ($source === '' && $email !== '') { $source = trim((string) preg_replace('/@.*/', '', $email)); $source = str_replace(['.', '_', '-'], ' ', $source); } $source = preg_replace('/\s+/', ' ', trim($source)); if ($source === '') { return [null, null]; } $parts = explode(' ', $source); if (count($parts) === 1) { return [$parts[0], null]; } $nome = array_shift($parts); $cognome = implode(' ', $parts); return [$nome ?: null, $cognome ?: null]; } private function inferCategoria(string $name, string $email): string { $haystack = strtolower(trim($name . ' ' . $email)); if (str_contains($haystack, 'condominio') || str_contains($haystack, 'residence')) { return 'cliente'; } if (str_contains($haystack, 'srl') || str_contains($haystack, 'spa') || str_contains($haystack, 'snc') || str_contains($haystack, 'sas')) { return 'fornitore'; } return 'altro'; } private function inferTipoContatto(string $name, string $email): string { $haystack = strtolower(trim($name . ' ' . $email)); if (str_contains($haystack, 'srl') || str_contains($haystack, 'spa') || str_contains($haystack, 'snc') || str_contains($haystack, 'sas')) { return 'persona_giuridica'; } return 'persona_fisica'; } private function shouldSyncField(?string $current, ?string $incoming, bool $isGoogleOrigin): bool { $incoming = trim((string) $incoming); if ($incoming === '') { return false; } $current = trim((string) $current); if ($current === '') { return true; } if (! $isGoogleOrigin) { return false; } return mb_strtolower($current) !== mb_strtolower($incoming); } }