Google).'; public function handle(): int { $dryRun = (bool) $this->option('dry-run'); $limit = max(1, (int) $this->option('limit')); $categorie = collect((array) $this->option('categoria')) ->map(fn ($v) => trim((string) $v)) ->filter(fn (string $v) => $v !== '') ->values() ->all(); $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; $totalErrors = 0; foreach ($amministratori as $amministratore) { $google = Arr::get($amministratore->impostazioni ?? [], 'google', []); $oauth = Arr::get($google, 'oauth', []); if (! (bool) Arr::get($oauth, 'connected', false)) { $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; } [$existingEmails, $existingPhones, $token] = $this->loadGoogleContactIndex($amministratore, $google, $oauth, $token, $dryRun); $records = RubricaUniversale::query() ->where(function ($q): void { $q->whereNotNull('email')->orWhereNotNull('telefono_cellulare'); }) ->where('stato', 'attivo') ->when($categorie !== [], function ($q) use ($categorie): void { $q->whereIn('categoria', $categorie); }) ->orderByDesc('updated_at') ->limit($limit) ->get(); $created = 0; $updated = 0; $skipped = 0; $errors = 0; foreach ($records as $record) { $emails = $this->emailsForGoogle($record); $phones = $this->phonesForGoogle($record); $email = $emails[0]['value'] ?? ''; $phone = $phones[0]['value'] ?? ''; $phoneNormalized = $this->normalizePhone($phone); [$givenName, $familyName, $fullName] = $this->buildGoogleNameParts($record); $payload = [ 'names' => [[ 'displayName' => $fullName, 'givenName' => $givenName, 'familyName' => $familyName, ]], ]; if ($emails !== []) { $payload['emailAddresses'] = $emails; } if ($phones !== []) { $payload['phoneNumbers'] = $phones; } $resourceName = null; if (! empty($record->google_person_resource_name)) { $resourceName = (string) $record->google_person_resource_name; } elseif ($email !== '' && isset($existingEmails[strtolower($email)])) { $resourceName = $existingEmails[strtolower($email)]; } elseif ($phoneNormalized !== '' && isset($existingPhones[$phoneNormalized])) { $resourceName = $existingPhones[$phoneNormalized]; } if ($dryRun) { if ($resourceName !== null) { $updated++; } elseif ($email === '' && $phoneNormalized === '') { $skipped++; } else { $created++; } if ($email !== '') { $existingEmails[strtolower($email)] = $resourceName ?? 'dry-run'; } if ($phoneNormalized !== '') { $existingPhones[$phoneNormalized] = $resourceName ?? 'dry-run'; } continue; } if ($resourceName !== null) { $personResponse = Http::withToken($token) ->acceptJson() ->get("https://people.googleapis.com/v1/{$resourceName}", [ 'personFields' => 'metadata', ]); if ($personResponse->status() === 401) { $token = $this->resolveAccessToken($amministratore, $google, $oauth, false, true) ?? $token; $personResponse = Http::withToken($token) ->acceptJson() ->get("https://people.googleapis.com/v1/{$resourceName}", [ 'personFields' => 'metadata', ]); } if (! $personResponse->successful()) { $errors++; continue; } $etag = (string) Arr::get($personResponse->json(), 'etag', ''); if ($etag === '') { $etag = (string) Arr::get($personResponse->json(), 'metadata.sources.0.etag', ''); } $updatePayload = $payload; $updatePayload['resourceName'] = $resourceName; if ($etag !== '') { $updatePayload['etag'] = $etag; } $updateResponse = Http::withToken($token) ->acceptJson() ->patch("https://people.googleapis.com/v1/{$resourceName}:updateContact?updatePersonFields=names,emailAddresses,phoneNumbers", $updatePayload); if ($updateResponse->status() === 401) { $token = $this->resolveAccessToken($amministratore, $google, $oauth, false, true) ?? $token; $updateResponse = Http::withToken($token) ->acceptJson() ->patch("https://people.googleapis.com/v1/{$resourceName}:updateContact?updatePersonFields=names,emailAddresses,phoneNumbers", $updatePayload); } if (! $updateResponse->successful()) { $errors++; continue; } if ($record->google_person_resource_name !== $resourceName) { $record->google_person_resource_name = $resourceName; $record->save(); } $updated++; if ($email !== '') { $existingEmails[strtolower($email)] = $resourceName; } if ($phoneNormalized !== '') { $existingPhones[$phoneNormalized] = $resourceName; } continue; } $createResponse = Http::withToken($token) ->acceptJson() ->post('https://people.googleapis.com/v1/people:createContact', $payload); if ($createResponse->status() === 401) { $token = $this->resolveAccessToken($amministratore, $google, $oauth, false, true) ?? $token; $createResponse = Http::withToken($token) ->acceptJson() ->post('https://people.googleapis.com/v1/people:createContact', $payload); } if (! $createResponse->successful()) { $errors++; continue; } $created++; $createdResource = (string) Arr::get($createResponse->json(), 'resourceName', ''); if ($createdResource !== '' && $record->google_person_resource_name !== $createdResource) { $record->google_person_resource_name = $createdResource; $record->save(); } if ($email !== '') { $existingEmails[strtolower($email)] = $createdResource !== '' ? $createdResource : 'created'; } if ($phoneNormalized !== '') { $existingPhones[$phoneNormalized] = $createdResource !== '' ? $createdResource : 'created'; } } $totalCreated += $created; $totalUpdated += $updated; $totalSkipped += $skipped; $totalErrors += $errors; $mode = $dryRun ? 'dry-run' : 'apply'; $this->info("Admin {$amministratore->id} ({$mode}): creati {$created}, aggiornati {$updated}, saltati {$skipped}, errori {$errors}."); } $this->newLine(); $this->info("Totale push: creati {$totalCreated}, aggiornati {$totalUpdated}, saltati {$totalSkipped}, errori {$totalErrors}."); return self::SUCCESS; } /** * @return array{0: array, 1: array, 2: string} */ private function loadGoogleContactIndex(Amministratore $amministratore, array $google, array $oauth, string $token, bool $dryRun): array { $existingEmails = []; $existingPhones = []; $nextPageToken = null; for ($page = 0; $page < 5; $page++) { $response = Http::withToken($token) ->acceptJson() ->get('https://people.googleapis.com/v1/people/me/connections', [ 'personFields' => 'emailAddresses,phoneNumbers', 'pageSize' => 500, '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' => 'emailAddresses,phoneNumbers', 'pageSize' => 500, 'pageToken' => $nextPageToken, ]); } if (! $response->successful()) { break; } $connections = $response->json('connections'); if (! is_array($connections) || count($connections) === 0) { break; } foreach ($connections as $person) { $resourceName = trim((string) Arr::get($person, 'resourceName', '')); foreach ((array) Arr::get($person, 'emailAddresses', []) as $emailRow) { $email = trim((string) Arr::get($emailRow, 'value', '')); if ($email !== '' && $resourceName !== '') { $existingEmails[strtolower($email)] = $resourceName; } } foreach ((array) Arr::get($person, 'phoneNumbers', []) as $phoneRow) { $phone = trim((string) Arr::get($phoneRow, 'value', '')); if ($phone !== '' && $resourceName !== '') { $normalized = $this->normalizePhone($phone); if ($normalized !== '') { $existingPhones[$normalized] = $resourceName; } } } } $nextPageToken = $response->json('nextPageToken'); if (! is_string($nextPageToken) || $nextPageToken === '') { break; } } return [$existingEmails, $existingPhones, $token]; } 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 normalizePhone(string $phone): string { return preg_replace('/[^0-9+]/', '', trim($phone)) ?? ''; } private function mobileForGoogle(RubricaUniversale $record): ?string { $candidate = trim((string) ($record->telefono_cellulare ?? '')); if ($candidate === '') { $national = trim((string) ($record->telefono_cellulare_nazionale ?? '')); if ($national !== '') { $prefix = trim((string) ($record->prefisso_cellulare ?? '+39')); $candidate = $prefix . $national; } } return PhoneNumber::toE164Italy($candidate); } /** @return array> */ private function emailsForGoogle(RubricaUniversale $record): array { $emails = []; $seen = []; $push = function (?string $value, string $type, string $formattedType) use (&$emails, &$seen): void { $email = strtolower(trim((string) $value)); if ($email === '' || isset($seen[$email])) { return; } $seen[$email] = true; $emails[] = [ 'value' => $email, 'type' => $type, 'formattedType' => $formattedType, ]; }; foreach ($this->splitEmailValues((string) ($record->email ?? '')) as $email) { $push($email, 'work', 'Email'); } foreach ($this->splitEmailValues((string) ($record->pec ?? '')) as $email) { $push($email, 'other', 'PEC'); } if (Schema::hasTable('rubrica_contatti_canali')) { $channels = RubricaContattoCanale::query() ->where('rubrica_id', (int) $record->id) ->whereIn('tipo', ['email', 'pec']) ->orderByDesc('is_principale') ->orderBy('id') ->get(['tipo', 'valore']); foreach ($channels as $channel) { $push((string) ($channel->valore ?? ''), (string) ($channel->tipo ?? '') === 'pec' ? 'other' : 'work', (string) ($channel->tipo ?? '') === 'pec' ? 'PEC' : 'Email'); } } return $emails; } /** @return array> */ private function phonesForGoogle(RubricaUniversale $record): array { $phones = []; $seen = []; $push = function (?string $value, string $type, string $formattedType) use (&$phones, &$seen): void { $phone = trim((string) $value); if ($phone === '') { return; } $normalized = $this->normalizePhone($phone); if ($normalized === '' || isset($seen[$normalized])) { return; } $seen[$normalized] = true; $phones[] = [ 'value' => $phone, 'type' => $type, 'formattedType' => $formattedType, ]; }; foreach ($this->splitPhoneValues((string) ($record->telefono_cellulare ?? '')) as $phone) { $push(PhoneNumber::toE164Italy($phone) ?? $phone, 'mobile', 'Cellulare'); } if (($primaryMobile = $this->mobileForGoogle($record)) !== null) { $push($primaryMobile, 'mobile', 'Cellulare'); } foreach ($this->splitPhoneValues((string) ($record->telefono_ufficio ?? '')) as $phone) { $push($phone, 'work', 'Ufficio'); } foreach ($this->splitPhoneValues((string) ($record->telefono_casa ?? '')) as $phone) { $push($phone, 'home', 'Casa'); } if (Schema::hasTable('rubrica_contatti_canali')) { $channels = RubricaContattoCanale::query() ->where('rubrica_id', (int) $record->id) ->whereIn('tipo', ['telefono', 'cellulare']) ->orderByDesc('is_principale') ->orderBy('id') ->get(['tipo', 'valore']); foreach ($channels as $channel) { $isMobile = (string) ($channel->tipo ?? '') === 'cellulare'; $value = $isMobile ? (PhoneNumber::toE164Italy((string) ($channel->valore ?? '')) ?? (string) ($channel->valore ?? '')) : (string) ($channel->valore ?? ''); $push($value, $isMobile ? 'mobile' : 'work', $isMobile ? 'Cellulare' : 'Telefono'); } } return $phones; } /** @return array */ private function splitEmailValues(string $value): array { $parts = preg_split('/[;,\n\r]+/', trim($value)) ?: []; return array_values(array_filter(array_map(function (string $part): ?string { $email = strtolower(trim($part)); return filter_var($email, FILTER_VALIDATE_EMAIL) ? $email : null; }, $parts))); } /** @return array */ private function splitPhoneValues(string $value): array { $parts = preg_split('/[;,\n\r\/|]+/', trim($value)) ?: []; return array_values(array_filter(array_map(function (string $part): ?string { $clean = preg_replace('/[^0-9+]/', '', trim($part)); return is_string($clean) && $clean !== '' ? $clean : null; }, $parts))); } /** * @return array{0:string,1:string,2:string} */ private function buildGoogleNameParts(RubricaUniversale $record): array { $nome = trim((string) ($record->nome ?? '')); $cognome = trim((string) ($record->cognome ?? '')); // Legacy imports can contain "Nome Cognome" inside nome with empty cognome. if ($cognome === '' && str_contains($nome, ' ')) { $parts = preg_split('/\s+/', $nome) ?: []; if (count($parts) > 1) { $nome = trim((string) array_shift($parts)); $cognome = trim(implode(' ', $parts)); } } $displayName = trim($nome . ' ' . $cognome); if ($displayName === '') { $displayName = trim((string) ($record->ragione_sociale ?? '')); } if ($displayName === '') { $displayName = 'Contatto NetGescon'; } return [$nome, $cognome, $displayName]; } }