565 lines
22 KiB
PHP
565 lines
22 KiB
PHP
<?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 GooglePushRubricaContactsCommand extends Command
|
|
{
|
|
protected $signature = 'google:push-rubrica
|
|
{--admin-id=* : Limita la push a uno o piu ID amministratore}
|
|
{--limit=300 : Numero massimo di contatti NetGescon da valutare per admin}
|
|
{--categoria=* : Filtra per categoria rubrica (es: fornitore, condomino)}
|
|
{--dry-run : Simula senza scrivere su Google}';
|
|
|
|
protected $description = 'Sincronizza contatti NetGescon verso Google Contacts (NetGescon -> 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<string,string>, 1: array<string,string>, 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<int, array<string, string>> */
|
|
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<int, array<string, string>> */
|
|
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<int, string> */
|
|
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<int, string> */
|
|
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];
|
|
}
|
|
}
|