netgescon-day0/app/Console/Commands/GoogleSyncRubricaContactsCommand.php

473 lines
20 KiB
PHP
Executable File

<?php
namespace App\Console\Commands;
use App\Models\Amministratore;
use App\Models\RubricaContattoCanale;
use App\Models\RubricaUniversale;
use App\Support\PhoneNumber;
use Illuminate\Console\Command;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Schema;
class GoogleSyncRubricaContactsCommand extends Command
{
protected $signature = 'google:sync-rubrica
{--admin-id=* : Limita la sync a uno o piu ID amministratore}
{--max-pages=20 : Numero massimo di pagine Google People da leggere per admin}
{--dry-run : Simula senza scrivere sul database}';
protected $description = 'Sincronizza contatti Google Workspace nella rubrica del gestionale (Google -> 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();
$this->syncAdditionalGoogleChannels($existing, $person);
}
$updated++;
} else {
if (! $dryRun) {
$this->syncAdditionalGoogleChannels($existing, $person);
}
$skipped++;
}
continue;
}
$created++;
if ($dryRun) {
continue;
}
$createdRubrica = 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,
]);
$this->syncAdditionalGoogleChannels($createdRubrica, $person);
}
$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);
}
private function syncAdditionalGoogleChannels(RubricaUniversale $rubrica, array $person): void
{
if (! Schema::hasTable('rubrica_contatti_canali')) {
return;
}
$emailRows = (array) Arr::get($person, 'emailAddresses', []);
foreach ($emailRows as $index => $emailRow) {
$value = strtolower(trim((string) Arr::get($emailRow, 'value', '')));
if ($value === '') {
continue;
}
$tipo = str_contains($value, '@cert.') ? 'pec' : 'email';
RubricaContattoCanale::query()->updateOrCreate(
[
'rubrica_id' => (int) $rubrica->id,
'tipo' => $tipo,
'valore' => $value,
'stabile_id' => null,
'unita_immobiliare_id' => null,
],
[
'etichetta' => $tipo === 'pec' ? 'PEC Google' : 'Email Google',
'is_principale' => $index === 0,
'meta' => ['source' => 'google_workspace'],
]
);
}
$phoneRows = (array) Arr::get($person, 'phoneNumbers', []);
foreach ($phoneRows as $index => $phoneRow) {
$raw = trim((string) Arr::get($phoneRow, 'value', ''));
if ($raw === '') {
continue;
}
$digits = PhoneNumber::normalizeDigits($raw);
if ($digits === '') {
continue;
}
$tipo = str_starts_with($digits, '3') || str_starts_with($digits, '39') ? 'cellulare' : 'telefono';
$value = $tipo === 'cellulare' ? (PhoneNumber::toE164Italy($raw) ?? $raw) : $raw;
RubricaContattoCanale::query()->updateOrCreate(
[
'rubrica_id' => (int) $rubrica->id,
'tipo' => $tipo,
'valore' => $value,
'stabile_id' => null,
'unita_immobiliare_id' => null,
],
[
'etichetta' => $tipo === 'cellulare' ? 'Cellulare Google' : 'Telefono Google',
'is_principale' => $index === 0,
'meta' => ['source' => 'google_workspace'],
]
);
}
}
}