netgescon-day0/app/Filament/Widgets/GoogleWorkspaceOverview.php

874 lines
30 KiB
PHP

<?php
namespace App\Filament\Widgets;
use App\Filament\Pages\Gescon\RubricaUniversaleArchivio;
use App\Filament\Pages\Strumenti\Calendario;
use App\Filament\Pages\Supporto\TicketGestione;
use App\Filament\Pages\Supporto\TicketMobile;
use App\Models\Amministratore;
use App\Models\RubricaUniversale;
use App\Models\Stabile;
use App\Models\Ticket;
use App\Models\User;
use App\Support\StabileContext;
use Filament\Notifications\Notification;
use Filament\Widgets\Widget;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
class GoogleWorkspaceOverview extends Widget
{
protected string $view = 'filament.widgets.google-workspace-overview';
protected int|string|array $columnSpan = 'full';
public bool $googleConnected = false;
public string $connectionLabel = 'Non collegato';
public string $calendarId = 'primary';
/** @var array<int, array<string, string>> */
public array $calendarEvents = [];
/** @var array<int, array<string, string>> */
public array $contacts = [];
/** @var array<int, array<string, string|int>> */
public array $openTickets = [];
/** @var array<int, string> */
public array $driveTemplateFolders = [];
public ?string $errorMessage = null;
public bool $showTechnicalBoxes = true;
public ?string $rubricaSyncCommandPreview = null;
public ?string $rubricaSyncLastOutput = null;
public ?string $rubricaSyncLastRunLabel = null;
public function mount(): void
{
$amministratore = $this->resolveAmministratore();
if (! $amministratore instanceof Amministratore) {
$this->errorMessage = 'Amministratore non disponibile nella sessione corrente.';
return;
}
$google = Arr::get($amministratore->impostazioni ?? [], 'google', []);
$oauth = Arr::get($google, 'oauth', []);
$this->googleConnected = (bool) Arr::get($oauth, 'connected', false);
$email = (string) Arr::get($oauth, 'email', '');
$this->connectionLabel = $email !== '' ? $email : ($this->googleConnected ? 'Collegato' : 'Non collegato');
$this->calendarId = (string) Arr::get($google, 'calendar_id', 'primary') ?: 'primary';
$this->rubricaSyncCommandPreview = 'php artisan google:sync-rubrica --admin-id=' . (int) $amministratore->id . ' --max-pages=20';
if (! $this->googleConnected) {
$this->errorMessage = 'Google non e collegato. Vai su Scheda amministratore per completare il collegamento.';
return;
}
$token = $this->resolveAccessToken($amministratore, $google, $oauth);
if ($token === null) {
return;
}
$this->loadCalendarEvents($token, $this->calendarId);
$this->loadContactsPreview($token);
$this->loadOpenTickets();
$this->loadDriveTemplateFolders();
}
public function getCalendarioUrl(): string
{
return Calendario::getUrl(panel: 'admin-filament');
}
public function getRubricaUrl(): string
{
return RubricaUniversaleArchivio::getUrl(panel: 'admin-filament');
}
public function getTicketApertiUrl(): string
{
return TicketMobile::getUrl(panel: 'admin-filament');
}
public function getTicketGestioneUrl(int $ticketId): string
{
return TicketGestione::getUrl(['ticket' => $ticketId, 'tab' => 'scheda'], panel: 'admin-filament');
}
public function previewImportContactsToRubrica(): void
{
$this->runRubricaSync(true);
}
public function importContactsToRubrica(): void
{
$this->runRubricaSync(false);
}
private function runRubricaSync(bool $dryRun): void
{
$amministratore = $this->resolveAmministratore();
if (! $amministratore instanceof Amministratore) {
Notification::make()
->title('Import rubrica non disponibile')
->warning()
->body('Amministratore non disponibile nella sessione corrente.')
->send();
return;
}
$google = Arr::get($amministratore->impostazioni ?? [], 'google', []);
$oauth = Arr::get($google, 'oauth', []);
$token = $this->resolveAccessToken($amministratore, $google, $oauth);
if ($token === null) {
Notification::make()
->title($dryRun ? 'Preview rubrica non disponibile' : 'Import rubrica non riuscito')
->warning()
->body($this->errorMessage ?? 'Token Google non disponibile.')
->send();
return;
}
$params = [
'--admin-id' => [(string) $amministratore->id],
'--max-pages' => 20,
];
if ($dryRun) {
$params['--dry-run'] = true;
}
$exit = Artisan::call('google:sync-rubrica', $params);
$output = trim((string) Artisan::output());
$this->rubricaSyncLastRunLabel = now()->format('d/m/Y H:i:s') . ($dryRun ? ' · dry-run' : ' · apply');
$this->rubricaSyncLastOutput = $output !== '' ? $output : 'Nessun output disponibile.';
$this->rubricaSyncCommandPreview = 'php artisan google:sync-rubrica --admin-id=' . (int) $amministratore->id . ' --max-pages=20' . ($dryRun ? ' --dry-run' : '');
if (! $dryRun) {
$this->loadContactsPreview($token);
}
Notification::make()
->title($exit === 0
? ($dryRun ? 'Preview import rubrica completata' : 'Import rubrica completato')
: ($dryRun ? 'Preview import rubrica con errori' : 'Import rubrica con errori'))
->body((string) collect(preg_split('/\R/', $this->rubricaSyncLastOutput) ?: [])->filter()->last())
->{$exit === 0 ? 'success' : 'warning'}()
->send();
}
public function exportRubricaToGoogle(): void
{
$amministratore = $this->resolveAmministratore();
if (! $amministratore instanceof Amministratore) {
Notification::make()
->title('Export Google non disponibile')
->warning()
->body('Amministratore non disponibile nella sessione corrente.')
->send();
return;
}
$google = Arr::get($amministratore->impostazioni ?? [], 'google', []);
$oauth = Arr::get($google, 'oauth', []);
$token = $this->resolveAccessToken($amministratore, $google, $oauth);
if ($token === null) {
Notification::make()
->title('Export Google non riuscito')
->warning()
->body($this->errorMessage ?? 'Token Google non disponibile.')
->send();
return;
}
$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, 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()) {
Notification::make()
->title('Export Google interrotto')
->warning()
->body($this->googleErrorBody($response, 'Impossibile leggere i contatti Google esistenti.'))
->send();
return;
}
$connections = $response->json('connections');
if (! is_array($connections) || count($connections) === 0) {
break;
}
foreach ($connections as $person) {
$email = trim((string) Arr::get($person, 'emailAddresses.0.value', ''));
$phone = trim((string) Arr::get($person, 'phoneNumbers.0.value', ''));
if ($email !== '') {
$existingEmails[strtolower($email)] = true;
}
if ($phone !== '') {
$normalized = $this->normalizePhone($phone);
if ($normalized !== '') {
$existingPhones[$normalized] = true;
}
}
}
$nextPageToken = $response->json('nextPageToken');
if (! is_string($nextPageToken) || $nextPageToken === '') {
break;
}
}
$records = RubricaUniversale::query()
->where(function ($q): void {
$q->whereNotNull('email')->orWhereNotNull('telefono_cellulare');
})
->where('stato', 'attivo')
->orderByDesc('updated_at')
->limit(300)
->get();
$created = 0;
$skipped = 0;
$errors = 0;
foreach ($records as $record) {
$email = trim((string) ($record->email ?? ''));
$phone = trim((string) ($record->telefono_cellulare ?? ''));
$phoneNormalized = $this->normalizePhone($phone);
if (
($email !== '' && isset($existingEmails[strtolower($email)]))
|| ($phoneNormalized !== '' && isset($existingPhones[$phoneNormalized]))
) {
$skipped++;
continue;
}
[$givenName, $familyName, $fullName] = $this->buildContactNameParts(
(string) ($record->nome ?? ''),
(string) ($record->cognome ?? ''),
(string) ($record->ragione_sociale ?? '')
);
$payload = [
'names' => [
[
'displayName' => $fullName,
'givenName' => $givenName,
'familyName' => $familyName,
],
],
];
if ($email !== '') {
$payload['emailAddresses'] = [['value' => $email]];
}
if ($phone !== '') {
$payload['phoneNumbers'] = [['value' => $phone]];
}
$createResponse = Http::withToken($token)
->acceptJson()
->post('https://people.googleapis.com/v1/people:createContact', $payload);
if ($createResponse->status() === 401) {
$token = $this->resolveAccessToken($amministratore, $google, $oauth, true) ?? $token;
$createResponse = Http::withToken($token)
->acceptJson()
->post('https://people.googleapis.com/v1/people:createContact', $payload);
}
if (! $createResponse->successful()) {
$errors++;
continue;
}
$created++;
if ($email !== '') {
$existingEmails[strtolower($email)] = true;
}
if ($phoneNormalized !== '') {
$existingPhones[$phoneNormalized] = true;
}
}
$this->loadContactsPreview($token);
Notification::make()
->title('Export Rubrica -> Google completato')
->success()
->body("Creati {$created} contatti, saltati {$skipped}, errori {$errors}.")
->send();
}
public function provaSincronizzazioneCalendarioTicket(): void
{
$this->sincronizzaTicketSuGoogleCalendar(true);
}
public function sincronizzaCalendarioTicket(): void
{
$this->sincronizzaTicketSuGoogleCalendar(false);
}
private function resolveAccessToken(Amministratore $amministratore, array $google, array $oauth, 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 === '') {
$this->errorMessage = 'Token Google non disponibile. Ricollega l account Google.';
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 === []) {
$this->errorMessage = 'Client ID/Secret Google mancanti. Controlla Scheda amministratore.';
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->errorMessage = 'Refresh token Google non riuscito: ' . $lastError;
return null;
}
$newAccessToken = trim((string) Arr::get($payload, 'access_token', ''));
if ($newAccessToken === '') {
$this->errorMessage = 'Google ha risposto senza access token valido.';
return null;
}
$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 loadCalendarEvents(string $token, string $calendarId): void
{
$response = Http::withToken($token)
->acceptJson()
->get('https://www.googleapis.com/calendar/v3/calendars/' . rawurlencode($calendarId) . '/events', [
'singleEvents' => 'true',
'orderBy' => 'startTime',
'timeMin' => Carbon::now()->toIso8601String(),
'maxResults' => 8,
]);
if (! $response->successful() && $calendarId !== 'primary') {
$this->calendarId = 'primary';
$response = Http::withToken($token)
->acceptJson()
->get('https://www.googleapis.com/calendar/v3/calendars/primary/events', [
'singleEvents' => 'true',
'orderBy' => 'startTime',
'timeMin' => Carbon::now()->toIso8601String(),
'maxResults' => 8,
]);
}
if (! $response->successful()) {
$this->errorMessage = 'Calendario Google non disponibile al momento. Verifica calendar_id e permessi OAuth.';
return;
}
$items = $response->json('items');
if (! is_array($items)) {
return;
}
$events = [];
foreach ($items as $item) {
$start = (string) (Arr::get($item, 'start.dateTime') ?: Arr::get($item, 'start.date') ?: '');
$summary = (string) Arr::get($item, 'summary', 'Evento senza titolo');
$location = (string) Arr::get($item, 'location', '');
$events[] = [
'title' => $summary,
'start' => $start,
'location' => $location,
];
}
$this->calendarEvents = $events;
}
private function sincronizzaTicketSuGoogleCalendar(bool $dryRun): void
{
$amministratore = $this->resolveAmministratore();
if (! $amministratore instanceof Amministratore) {
Notification::make()->title('Sync calendario non disponibile')->warning()->send();
return;
}
$google = Arr::get($amministratore->impostazioni ?? [], 'google', []);
$oauth = Arr::get($google, 'oauth', []);
$token = $this->resolveAccessToken($amministratore, $google, $oauth);
if ($token === null) {
Notification::make()->title('Sync calendario non riuscito')->warning()->body($this->errorMessage ?? 'Token Google non disponibile.')->send();
return;
}
$stabile = StabileContext::getActiveStabile(Auth::user());
if (! $stabile instanceof Stabile) {
Notification::make()->title('Sync calendario non riuscito')->warning()->body('Seleziona prima uno stabile attivo.')->send();
return;
}
$tickets = Ticket::query()
->where('stabile_id', $stabile->id)
->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione'])
->orderByDesc('created_at')
->limit(30)
->get();
$created = 0;
$updated = 0;
$errors = 0;
$calendarId = (string) ($this->calendarId ?: 'primary');
foreach ($tickets as $ticket) {
$payload = $this->buildCalendarEventPayload($ticket, (string) ($stabile->denominazione ?? ('Stabile #' . $stabile->id)));
$event = $this->findCalendarEventByTicketId($token, $calendarId, (int) $ticket->id);
if ($event === null) {
if ($dryRun) {
$created++;
continue;
}
$createResponse = Http::withToken($token)
->acceptJson()
->post('https://www.googleapis.com/calendar/v3/calendars/' . rawurlencode($calendarId) . '/events', $payload);
if (! $createResponse->successful()) {
$errors++;
continue;
}
$created++;
continue;
}
if ($dryRun) {
$updated++;
continue;
}
$eventId = trim((string) Arr::get($event, 'id', ''));
if ($eventId === '') {
$errors++;
continue;
}
$updateResponse = Http::withToken($token)
->acceptJson()
->patch('https://www.googleapis.com/calendar/v3/calendars/' . rawurlencode($calendarId) . '/events/' . rawurlencode($eventId), $payload);
if (! $updateResponse->successful()) {
$errors++;
continue;
}
$updated++;
}
$mode = $dryRun ? 'Prova sync' : 'Sync calendario';
Notification::make()
->title($mode . ' completata')
->success()
->body("Creazioni: {$created}, aggiornamenti: {$updated}, errori: {$errors}.")
->send();
if (! $dryRun) {
$this->loadCalendarEvents($token, $calendarId);
}
}
private function findCalendarEventByTicketId(string $token, string $calendarId, int $ticketId): ?array
{
$response = Http::withToken($token)
->acceptJson()
->get('https://www.googleapis.com/calendar/v3/calendars/' . rawurlencode($calendarId) . '/events', [
'privateExtendedProperty' => 'netgescon_ticket_id=' . $ticketId,
'maxResults' => 1,
]);
if (! $response->successful()) {
return null;
}
$items = $response->json('items');
if (! is_array($items) || count($items) === 0) {
return null;
}
$first = $items[0];
return is_array($first) ? $first : null;
}
private function buildCalendarEventPayload(Ticket $ticket, string $stabileName): array
{
$startAt = Carbon::parse($ticket->data_apertura ?? $ticket->created_at ?? now());
$endAt = (clone $startAt)->addMinutes(30);
return [
'summary' => 'Ticket #' . (int) $ticket->id . ' - ' . (string) ($ticket->titolo ?? 'Segnalazione'),
'description' => implode("\n", [
'Stabile: ' . $stabileName,
'Stato: ' . (string) ($ticket->stato ?? '-'),
'Priorita: ' . (string) ($ticket->priorita ?? '-'),
'Luogo: ' . (string) ($ticket->luogo_intervento ?? '-'),
'',
(string) ($ticket->descrizione ?? ''),
]),
'start' => [
'dateTime' => $startAt->toIso8601String(),
'timeZone' => 'Europe/Rome',
],
'end' => [
'dateTime' => $endAt->toIso8601String(),
'timeZone' => 'Europe/Rome',
],
'extendedProperties' => [
'private' => [
'netgescon_ticket_id' => (string) $ticket->id,
],
],
];
}
private function loadContactsPreview(string $token): void
{
$response = Http::withToken($token)
->acceptJson()
->get('https://people.googleapis.com/v1/people/me/connections', [
'personFields' => 'names,emailAddresses,phoneNumbers',
'pageSize' => 8,
]);
if (! $response->successful()) {
return;
}
$connections = $response->json('connections');
if (! is_array($connections)) {
return;
}
$contacts = [];
foreach ($connections as $person) {
$name = (string) Arr::get($person, 'names.0.displayName', 'Contatto');
$email = (string) Arr::get($person, 'emailAddresses.0.value', '');
$phone = (string) Arr::get($person, 'phoneNumbers.0.value', '');
$contacts[] = [
'name' => $name,
'email' => $email,
'phone' => $phone,
];
}
$this->contacts = $contacts;
}
private function loadOpenTickets(): void
{
$user = Auth::user();
if (! $user instanceof User) {
$this->openTickets = [];
return;
}
$stabileIds = StabileContext::accessibleStabili($user)
->pluck('id')
->map(fn($value) => (int) $value)
->filter(fn(int $value) => $value > 0)
->values()
->all();
if ($stabileIds === []) {
$this->openTickets = [];
return;
}
$items = Ticket::query()
->with('stabile:id,denominazione')
->whereIn('stabile_id', $stabileIds)
->aperti()
->orderByDesc('created_at')
->limit(8)
->get();
$tickets = [];
foreach ($items as $ticket) {
$tickets[] = [
'id' => (int) $ticket->id,
'titolo' => (string) ($ticket->titolo ?? 'Ticket senza titolo'),
'stato' => (string) ($ticket->stato ?? '-'),
'priorita' => (string) ($ticket->priorita ?? '-'),
'stabile' => (string) ($ticket->stabile->denominazione ?? 'N/A'),
];
}
$this->openTickets = $tickets;
}
private function normalizePhone(string $phone): string
{
return preg_replace('/[^0-9+]/', '', trim($phone)) ?? '';
}
/**
* @return array{0:string,1:string,2:string}
*/
private function buildContactNameParts(string $nomeRaw, string $cognomeRaw, string $ragioneSociale): array
{
$nome = trim($nomeRaw);
$cognome = trim($cognomeRaw);
if ($cognome === '' && str_contains($nome, ' ')) {
$parts = preg_split('/\s+/', $nome) ?: [];
if (count($parts) > 1) {
$nome = trim((string) array_shift($parts));
$cognome = trim(implode(' ', $parts));
}
}
$display = trim($nome . ' ' . $cognome);
if ($display === '') {
$display = trim($ragioneSociale);
}
if ($display === '') {
$display = 'Contatto NetGescon';
}
return [$nome, $cognome, $display];
}
private function loadDriveTemplateFolders(): void
{
$items = config('netgescon.google.drive_template_folders', []);
$yearRoot = trim((string) config('netgescon.google.drive_template_year_root', ''));
$yearModel = trim((string) config('netgescon.google.drive_template_year_model', ''));
$folders = is_array($items)
? array_values(array_filter(array_map('strval', $items), fn(string $v) : bool => trim($v) !== ''))
: [];
if ($yearRoot !== '' && $yearModel !== '') {
$baseFolders = $folders;
foreach ($baseFolders as $folder) {
$folders[] = $yearRoot . '/' . $yearModel . '/' . $folder;
}
}
$this->driveTemplateFolders = array_values(array_unique($folders));
}
private function googleErrorBody($response, string $fallback): string
{
try {
$status = (int) $response->status();
$message = (string) Arr::get($response->json(), 'error.message', '');
if ($message !== '') {
return $fallback . " (HTTP {$status}: {$message})";
}
return $fallback . " (HTTP {$status})";
} catch (\Throwable) {
return $fallback;
}
}
private function resolveAmministratore(): ?Amministratore
{
$user = Auth::user();
if (! $user instanceof User) {
return null;
}
$amministratore = $user->amministratore;
if (! $amministratore instanceof Amministratore && $user->hasAnyRole(['super-admin', 'admin'])) {
$stabile = StabileContext::getActiveStabile($user);
if ($stabile instanceof Stabile && $stabile->amministratore instanceof Amministratore) {
$amministratore = $stabile->amministratore;
}
}
return $amministratore instanceof Amministratore ? $amministratore : null;
}
/**
* @return array{0:string,1:string}
*/
private function splitDisplayName(string $displayName, string $fallbackEmail): array
{
$displayName = trim($displayName);
if ($displayName === '') {
$local = trim((string) strtok($fallbackEmail, '@'));
return [$local !== '' ? $local : 'Contatto', ''];
}
$parts = preg_split('/\s+/', $displayName) ?: [];
if (count($parts) <= 1) {
return [$parts[0] ?? $displayName, ''];
}
$nome = array_shift($parts) ?: '';
$cognome = trim(implode(' ', $parts));
return [$nome, $cognome];
}
private function inferCategoria(string $displayName, string $email): string
{
$haystack = strtolower(trim($displayName . ' ' . $email));
if ($haystack === '') {
return 'altro';
}
if (str_contains($haystack, 'assicur') || str_contains($haystack, 'insurance')) {
return 'assicurazione';
}
if (str_contains($haystack, 'banca') || str_contains($haystack, 'bank') || str_contains($haystack, 'unicredit') || str_contains($haystack, 'intesa')) {
return 'banca';
}
if (str_contains($haystack, 'condominio') || str_contains($haystack, 'condom')) {
return 'condomino';
}
if (
str_contains($haystack, 'srl')
|| str_contains($haystack, 'spa')
|| str_contains($haystack, 'sas')
|| str_contains($haystack, 'snc')
|| str_contains($haystack, 'impianti')
|| str_contains($haystack, 'servizi')
|| str_contains($haystack, 'edil')
|| str_contains($haystack, 'idraul')
|| str_contains($haystack, 'elettric')
) {
return 'fornitore';
}
return 'cliente';
}
private function inferTipoContatto(string $displayName, string $email): string
{
$haystack = strtolower(trim($displayName . ' ' . $email));
if (
str_contains($haystack, 'srl')
|| str_contains($haystack, 'spa')
|| str_contains($haystack, 'sas')
|| str_contains($haystack, 'snc')
|| str_contains($haystack, 'stp')
|| str_contains($haystack, 'studio')
|| str_contains($haystack, 'impresa')
) {
return 'persona_giuridica';
}
return 'persona_fisica';
}
}