netgescon-day0/docs/91-database/02-tabelle-collegate/soggetti.md

35 KiB

👤 TABELLA SOGGETTI

Anagrafica persone e aziende collegate agli stabili


🔑 CONNESSIONE DATABASE

Credenziali NetGescon:

# Connessione MySQL
mysql -u netgescon_user -p netgescon
# Password: NetGescon2024!

# Test connessione Laravel
cd /var/www/netgescon
php artisan tinker
DB::connection()->getPdo();

🎯 CARATTERISTICHE TABELLA

Scopo:

Anagrafica unificata di proprietari, inquilini, fornitori e altri soggetti collegati agli stabili

Principio:

Collegamento via stabile - ogni soggetto appartiene a uno stabile → isolamento automatico per amministratore


🗄️ STRUTTURA TABELLA SOGGETTI

CREATE TABLE soggetti (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    stabile_id BIGINT UNSIGNED NOT NULL,              -- FK verso stabili
    
    -- Tipo soggetto
    tipo_soggetto ENUM('persona_fisica', 'persona_giuridica') DEFAULT 'persona_fisica',
    categoria_soggetto ENUM('proprietario', 'inquilino', 'fornitore', 'amministratore_precedente', 'altro') DEFAULT 'proprietario',
    
    -- Dati anagrafici persona fisica
    nome VARCHAR(255) NULL,
    cognome VARCHAR(255) NULL,
    codice_fiscale VARCHAR(16) NULL,
    data_nascita DATE NULL,
    luogo_nascita VARCHAR(255) NULL,
    
    -- Dati persona giuridica
    ragione_sociale VARCHAR(255) NULL,
    partita_iva VARCHAR(11) NULL,
    codice_fiscale_azienda VARCHAR(16) NULL,
    forma_giuridica VARCHAR(100) NULL,               -- SRL, SPA, SNC, ecc
    
    -- Contatti
    telefono VARCHAR(20) NULL,
    cellulare VARCHAR(20) NULL,
    email VARCHAR(255) NULL,
    pec VARCHAR(255) NULL,
    
    -- Indirizzo residenza/sede
    indirizzo_residenza TEXT NULL,
    citta_residenza VARCHAR(255) NULL,
    cap_residenza VARCHAR(10) NULL,
    provincia_residenza VARCHAR(2) NULL,
    
    -- Indirizzo domicilio (se diverso)
    indirizzo_domicilio TEXT NULL,
    citta_domicilio VARCHAR(255) NULL,
    cap_domicilio VARCHAR(10) NULL,
    provincia_domicilio VARCHAR(2) NULL,
    
    -- Dati bancari
    iban VARCHAR(34) NULL,
    banca VARCHAR(255) NULL,
    
    -- Configurazioni
    is_active BOOLEAN DEFAULT TRUE,
    note TEXT NULL,
    privacy_consenso BOOLEAN DEFAULT FALSE,
    data_consenso_privacy TIMESTAMP NULL,
    configurazioni JSON NULL,
    
    -- Audit
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    -- Foreign Keys
    FOREIGN KEY (stabile_id) REFERENCES stabili(id) ON DELETE CASCADE,
    
    -- Indici
    INDEX idx_stabile_id (stabile_id),
    INDEX idx_tipo_soggetto (tipo_soggetto),
    INDEX idx_categoria_soggetto (categoria_soggetto),
    INDEX idx_codice_fiscale (codice_fiscale),
    INDEX idx_partita_iva (partita_iva),
    INDEX idx_email (email),
    INDEX idx_active (is_active),
    
    -- Indici composti
    INDEX idx_stabile_categoria (stabile_id, categoria_soggetto),
    INDEX idx_nome_cognome (nome, cognome),
    INDEX idx_ragione_sociale (ragione_sociale)
);

🔗 TABELLA PIVOT UNITA-SOGGETTI

CREATE TABLE unita_soggetti (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    unita_immobiliare_id BIGINT UNSIGNED NOT NULL,
    soggetto_id BIGINT UNSIGNED NOT NULL,
    
    -- Tipo rapporto
    tipo_rapporto ENUM('proprietario', 'inquilino', 'usufruttuario', 'nudo_proprietario', 'amministratore') DEFAULT 'proprietario',
    
    -- Quote e date
    quota_proprieta DECIMAL(5, 2) DEFAULT 100.00,    -- Percentuale proprietà
    data_inizio DATE NOT NULL,
    data_fine DATE NULL,
    
    -- Contratto inquilino
    canone_mensile DECIMAL(8, 2) NULL,               -- Solo per inquilini
    deposito_cauzionale DECIMAL(8, 2) NULL,
    tipo_contratto ENUM('libero', 'concordato', 'transitorio', 'studenti') NULL,
    durata_contratto_mesi INT NULL,
    
    -- Stato
    is_active BOOLEAN DEFAULT TRUE,
    note TEXT NULL,
    
    -- Audit
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    
    -- Foreign Keys
    FOREIGN KEY (unita_immobiliare_id) REFERENCES unita_immobiliari(id) ON DELETE CASCADE,
    FOREIGN KEY (soggetto_id) REFERENCES soggetti(id) ON DELETE CASCADE,
    
    -- Indici
    INDEX idx_unita_immobiliare_id (unita_immobiliare_id),
    INDEX idx_soggetto_id (soggetto_id),
    INDEX idx_tipo_rapporto (tipo_rapporto),
    INDEX idx_active (is_active),
    INDEX idx_data_inizio (data_inizio),
    INDEX idx_data_fine (data_fine),
    
    -- Indici composti
    INDEX idx_unita_soggetto (unita_immobiliare_id, soggetto_id),
    INDEX idx_unita_rapporto (unita_immobiliare_id, tipo_rapporto),
    
    -- Vincolo: un soggetto può avere un solo rapporto attivo per unità
    UNIQUE KEY unique_unita_soggetto_attivo (unita_immobiliare_id, soggetto_id, is_active)
);

🏗️ MIGRATION LARAVEL

File: database/migrations/2024_08_05_000040_create_soggetti_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('soggetti', function (Blueprint $table) {
            $table->id();
            $table->foreignId('stabile_id')
                  ->constrained('stabili')
                  ->onDelete('cascade');
                  
            // Tipo soggetto
            $table->enum('tipo_soggetto', ['persona_fisica', 'persona_giuridica'])
                  ->default('persona_fisica');
            $table->enum('categoria_soggetto', [
                'proprietario', 'inquilino', 'fornitore', 'amministratore_precedente', 'altro'
            ])->default('proprietario');
            
            // Dati anagrafici persona fisica
            $table->string('nome')->nullable();
            $table->string('cognome')->nullable();
            $table->string('codice_fiscale', 16)->nullable();
            $table->date('data_nascita')->nullable();
            $table->string('luogo_nascita')->nullable();
            
            // Dati persona giuridica
            $table->string('ragione_sociale')->nullable();
            $table->string('partita_iva', 11)->nullable();
            $table->string('codice_fiscale_azienda', 16)->nullable();
            $table->string('forma_giuridica', 100)->nullable();
            
            // Contatti
            $table->string('telefono', 20)->nullable();
            $table->string('cellulare', 20)->nullable();
            $table->string('email')->nullable();
            $table->string('pec')->nullable();
            
            // Indirizzo residenza/sede
            $table->text('indirizzo_residenza')->nullable();
            $table->string('citta_residenza')->nullable();
            $table->string('cap_residenza', 10)->nullable();
            $table->string('provincia_residenza', 2)->nullable();
            
            // Indirizzo domicilio
            $table->text('indirizzo_domicilio')->nullable();
            $table->string('citta_domicilio')->nullable();
            $table->string('cap_domicilio', 10)->nullable();
            $table->string('provincia_domicilio', 2)->nullable();
            
            // Dati bancari
            $table->string('iban', 34)->nullable();
            $table->string('banca')->nullable();
            
            // Configurazioni
            $table->boolean('is_active')->default(true);
            $table->text('note')->nullable();
            $table->boolean('privacy_consenso')->default(false);
            $table->timestamp('data_consenso_privacy')->nullable();
            $table->json('configurazioni')->nullable();
            
            $table->timestamps();
            
            // Indici
            $table->index(['stabile_id']);
            $table->index(['tipo_soggetto']);
            $table->index(['categoria_soggetto']);
            $table->index(['codice_fiscale']);
            $table->index(['partita_iva']);
            $table->index(['email']);
            $table->index(['is_active']);
            
            // Indici composti
            $table->index(['stabile_id', 'categoria_soggetto'], 'idx_stabile_categoria');
            $table->index(['nome', 'cognome'], 'idx_nome_cognome');
            $table->index(['ragione_sociale']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('soggetti');
    }
};

File: database/migrations/2024_08_05_000041_create_unita_soggetti_table.php

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('unita_soggetti', function (Blueprint $table) {
            $table->id();
            $table->foreignId('unita_immobiliare_id')
                  ->constrained('unita_immobiliari')
                  ->onDelete('cascade');
            $table->foreignId('soggetto_id')
                  ->constrained('soggetti')
                  ->onDelete('cascade');
                  
            // Tipo rapporto
            $table->enum('tipo_rapporto', [
                'proprietario', 'inquilino', 'usufruttuario', 'nudo_proprietario', 'amministratore'
            ])->default('proprietario');
            
            // Quote e date
            $table->decimal('quota_proprieta', 5, 2)->default(100.00);
            $table->date('data_inizio');
            $table->date('data_fine')->nullable();
            
            // Contratto inquilino
            $table->decimal('canone_mensile', 8, 2)->nullable();
            $table->decimal('deposito_cauzionale', 8, 2)->nullable();
            $table->enum('tipo_contratto', ['libero', 'concordato', 'transitorio', 'studenti'])
                  ->nullable();
            $table->integer('durata_contratto_mesi')->nullable();
            
            // Stato
            $table->boolean('is_active')->default(true);
            $table->text('note')->nullable();
            
            $table->timestamps();
            
            // Indici
            $table->index(['unita_immobiliare_id']);
            $table->index(['soggetto_id']);
            $table->index(['tipo_rapporto']);
            $table->index(['is_active']);
            $table->index(['data_inizio']);
            $table->index(['data_fine']);
            
            // Indici composti
            $table->index(['unita_immobiliare_id', 'soggetto_id'], 'idx_unita_soggetto');
            $table->index(['unita_immobiliare_id', 'tipo_rapporto'], 'idx_unita_rapporto');
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('unita_soggetti');
    }
};

📊 MODEL ELOQUENT

File: app/Models/Soggetto.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Builder;

class Soggetto extends Model
{
    protected $fillable = [
        'stabile_id',
        'tipo_soggetto',
        'categoria_soggetto',
        'nome',
        'cognome',
        'codice_fiscale',
        'data_nascita',
        'luogo_nascita',
        'ragione_sociale',
        'partita_iva',
        'codice_fiscale_azienda',
        'forma_giuridica',
        'telefono',
        'cellulare',
        'email',
        'pec',
        'indirizzo_residenza',
        'citta_residenza',
        'cap_residenza',
        'provincia_residenza',
        'indirizzo_domicilio',
        'citta_domicilio',
        'cap_domicilio',
        'provincia_domicilio',
        'iban',
        'banca',
        'is_active',
        'note',
        'privacy_consenso',
        'data_consenso_privacy',
        'configurazioni',
    ];

    protected $casts = [
        'data_nascita' => 'date',
        'is_active' => 'boolean',
        'privacy_consenso' => 'boolean',
        'data_consenso_privacy' => 'datetime',
        'configurazioni' => 'array',
    ];

    // Relazioni
    public function stabile(): BelongsTo
    {
        return $this->belongsTo(Stabile::class);
    }

    public function unitaImmobiliari(): BelongsToMany
    {
        return $this->belongsToMany(UnitaImmobiliare::class, 'unita_soggetti')
                    ->withPivot([
                        'tipo_rapporto',
                        'quota_proprieta',
                        'data_inizio',
                        'data_fine',
                        'canone_mensile',
                        'deposito_cauzionale',
                        'tipo_contratto',
                        'durata_contratto_mesi',
                        'is_active',
                        'note'
                    ])
                    ->withTimestamps();
    }

    public function unitaProprieta(): BelongsToMany
    {
        return $this->unitaImmobiliari()->wherePivot('tipo_rapporto', 'proprietario');
    }

    public function unitaInquilinoIn(): BelongsToMany
    {
        return $this->unitaImmobiliari()->wherePivot('tipo_rapporto', 'inquilino');
    }

    public function contabilitaRegistrazioni(): HasMany
    {
        return $this->hasMany(ContabilitaRegistrazione::class);
    }

    public function documenti(): HasMany
    {
        return $this->hasMany(Documento::class);
    }

    // Scope automatico per amministratore tramite stabile
    protected static function booted(): void
    {
        static::addGlobalScope('amministratore', function (Builder $builder) {
            $user = auth()->user();
            if ($user && $user->amministratore_id) {
                $builder->whereHas('stabile', function ($query) use ($user) {
                    $query->where('amministratore_id', $user->amministratore_id);
                });
            }
        });
    }

    // Scope
    public function scopeActive(Builder $query): Builder
    {
        return $query->where('is_active', true);
    }

    public function scopeByTipo(Builder $query, string $tipo): Builder
    {
        return $query->where('tipo_soggetto', $tipo);
    }

    public function scopeByCategoria(Builder $query, string $categoria): Builder
    {
        return $query->where('categoria_soggetto', $categoria);
    }

    public function scopeProprietari(Builder $query): Builder
    {
        return $query->where('categoria_soggetto', 'proprietario');
    }

    public function scopeInquilini(Builder $query): Builder
    {
        return $query->where('categoria_soggetto', 'inquilino');
    }

    public function scopeFornitori(Builder $query): Builder
    {
        return $query->where('categoria_soggetto', 'fornitore');
    }

    public function scopePersoneFisiche(Builder $query): Builder
    {
        return $query->where('tipo_soggetto', 'persona_fisica');
    }

    public function scopePersoneGiuridiche(Builder $query): Builder
    {
        return $query->where('tipo_soggetto', 'persona_giuridica');
    }

    public function scopeSearch(Builder $query, string $term): Builder
    {
        return $query->where(function ($q) use ($term) {
            $q->where('nome', 'like', "%{$term}%")
              ->orWhere('cognome', 'like', "%{$term}%")
              ->orWhere('ragione_sociale', 'like', "%{$term}%")
              ->orWhere('codice_fiscale', 'like', "%{$term}%")
              ->orWhere('email', 'like', "%{$term}%");
        });
    }

    // Accessors
    public function getDenominazioneComputedAttribute(): string
    {
        if ($this->tipo_soggetto === 'persona_fisica') {
            return trim("{$this->nome} {$this->cognome}");
        } else {
            return $this->ragione_sociale ?: 'Azienda senza nome';
        }
    }

    public function getCodiceIdentificativoAttribute(): string
    {
        return $this->codice_fiscale ?: $this->codice_fiscale_azienda ?: $this->partita_iva ?: '';
    }

    public function getIndirizzoCompletoAttribute(): string
    {
        $indirizzo = $this->indirizzo_residenza ?: $this->indirizzo_domicilio;
        $citta = $this->citta_residenza ?: $this->citta_domicilio;
        $cap = $this->cap_residenza ?: $this->cap_domicilio;
        $provincia = $this->provincia_residenza ?: $this->provincia_domicilio;

        $parts = array_filter([
            $indirizzo,
            $cap,
            $citta,
            $provincia ? "({$provincia})" : null,
        ]);

        return implode(', ', $parts);
    }

    public function getContattoPreferenzialeAttribute(): string
    {
        return $this->cellulare ?: $this->telefono ?: $this->email ?: '';
    }

    public function getIsPersonaFisicaAttribute(): bool
    {
        return $this->tipo_soggetto === 'persona_fisica';
    }

    public function getIsPersonaGiuridicaAttribute(): bool
    {
        return $this->tipo_soggetto === 'persona_giuridica';
    }

    public function getIsProprietarioAttribute(): bool
    {
        return $this->categoria_soggetto === 'proprietario';
    }

    public function getIsInquilinoAttribute(): bool
    {
        return $this->categoria_soggetto === 'inquilino';
    }

    public function getIsFornitoreAttribute(): bool
    {
        return $this->categoria_soggetto === 'fornitore';
    }

    public function getEtaAttribute(): ?int
    {
        if (!$this->data_nascita) {
            return null;
        }
        
        return $this->data_nascita->age;
    }

    // Helper Methods
    public function hasPrivacyConsenso(): bool
    {
        return $this->privacy_consenso && $this->data_consenso_privacy;
    }

    public function aggiornaPrivacyConsenso(bool $consenso = true): void
    {
        $this->update([
            'privacy_consenso' => $consenso,
            'data_consenso_privacy' => $consenso ? now() : null,
        ]);
    }

    public function getUnitaAttive(): \Illuminate\Database\Eloquent\Collection
    {
        return $this->unitaImmobiliari()
            ->wherePivot('is_active', true)
            ->wherePivot('data_fine', null)
            ->get();
    }

    public function getUnitaProprietaAttive(): \Illuminate\Database\Eloquent\Collection
    {
        return $this->unitaProprieta()
            ->wherePivot('is_active', true)
            ->wherePivot('data_fine', null)
            ->get();
    }

    public function getContrattiAffittoAttivi(): \Illuminate\Database\Eloquent\Collection
    {
        return $this->unitaInquilinoIn()
            ->wherePivot('is_active', true)
            ->wherePivot('data_fine', null)
            ->get();
    }

    public function calcolaTotaleCanoniMensili(): float
    {
        return $this->unitaInquilinoIn()
            ->wherePivot('is_active', true)
            ->wherePivot('data_fine', null)
            ->sum('unita_soggetti.canone_mensile') ?: 0.0;
    }

    public function calcolaTotaleMillesimi(): float
    {
        $unitaAttive = $this->getUnitaProprietaAttive();
        
        return $unitaAttive->sum(function ($unita) {
            $quota = $unita->pivot->quota_proprieta / 100;
            return $unita->millesimi_generali * $quota;
        });
    }

    public function associaUnita(UnitaImmobiliare $unita, array $datiRapporto): void
    {
        $datiPivot = array_merge([
            'tipo_rapporto' => 'proprietario',
            'quota_proprieta' => 100.00,
            'data_inizio' => now()->toDateString(),
            'is_active' => true,
        ], $datiRapporto);

        $this->unitaImmobiliari()->attach($unita->id, $datiPivot);
        
        // Aggiorna stato unità
        $unita->aggiornaStatoOccupazione(
            $datiPivot['tipo_rapporto'] === 'proprietario' 
                ? 'occupata_proprietario' 
                : 'occupata_inquilino'
        );
    }

    public function rimuoviUnita(UnitaImmobiliare $unita): void
    {
        $this->unitaImmobiliari()->updateExistingPivot($unita->id, [
            'is_active' => false,
            'data_fine' => now()->toDateString()
        ]);
        
        // Controlla se l'unità ha altri soggetti attivi
        $altriSoggettiAttivi = $unita->soggetti()
            ->wherePivot('is_active', true)
            ->wherePivot('id', '!=', $this->id)
            ->exists();
            
        if (!$altriSoggettiAttivi) {
            $unita->aggiornaStatoOccupazione('libera');
        }
    }

    public function canDelete(): bool
    {
        return $this->unitaImmobiliari()->count() === 0 &&
               $this->contabilitaRegistrazioni()->count() === 0;
    }

    // Observer Events
    protected static function boot(): void
    {
        parent::boot();

        static::creating(function ($soggetto) {
            // Converti codice fiscale in maiuscolo
            if ($soggetto->codice_fiscale) {
                $soggetto->codice_fiscale = strtoupper($soggetto->codice_fiscale);
            }
            
            if ($soggetto->codice_fiscale_azienda) {
                $soggetto->codice_fiscale_azienda = strtoupper($soggetto->codice_fiscale_azienda);
            }
            
            // Normalizza email
            if ($soggetto->email) {
                $soggetto->email = strtolower($soggetto->email);
            }
            
            if ($soggetto->pec) {
                $soggetto->pec = strtolower($soggetto->pec);
            }
        });

        static::updating(function ($soggetto) {
            // Stesse normalizzazioni per update
            if ($soggetto->isDirty('codice_fiscale') && $soggetto->codice_fiscale) {
                $soggetto->codice_fiscale = strtoupper($soggetto->codice_fiscale);
            }
            
            if ($soggetto->isDirty('email') && $soggetto->email) {
                $soggetto->email = strtolower($soggetto->email);
            }
        });
    }
}

🌱 SEEDER DATI DEMO

File: database/seeders/SoggettiSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Soggetto;
use App\Models\UnitaImmobiliare;
use App\Models\Stabile;
use Faker\Factory as Faker;

class SoggettiSeeder extends Seeder
{
    public function run(): void
    {
        $faker = Faker::create('it_IT');
        $stabili = Stabile::all();
        
        foreach ($stabili as $stabile) {
            $this->createSoggettiPerStabile($stabile, $faker);
        }

        $totalSoggetti = Soggetto::count();
        $this->command->info("✅ Creati {$totalSoggetti} soggetti per tutti gli stabili");
        
        // Mostra riassunto per amministratore
        $stabili->groupBy('amministratore_id')->each(function ($stabiliGroup) {
            $admin = $stabiliGroup->first()->amministratore;
            $soggettiCount = Soggetto::whereIn('stabile_id', $stabiliGroup->pluck('id'))->count();
            $this->command->info("Admin {$admin->codice_univoco}: {$soggettiCount} soggetti");
        });
    }
    
    private function createSoggettiPerStabile(Stabile $stabile, $faker): void
    {
        $unita = $stabile->unitaImmobiliari;
        
        foreach ($unita as $unita) {
            $this->createSoggettiPerUnita($unita, $faker);
        }
        
        // Aggiungi alcuni fornitori per lo stabile
        $this->createFornitori($stabile, $faker);
    }
    
    private function createSoggettiPerUnita(UnitaImmobiliare $unita, $faker): void
    {
        // Crea proprietario
        $proprietario = $this->createProprietario($unita, $faker);
        
        // 30% probabilità di avere inquilino
        if (rand(1, 100) <= 30) {
            $this->createInquilino($unita, $faker);
        }
        
        // Se è una coppia, 60% probabilità di secondo proprietario
        if (rand(1, 100) <= 60) {
            $this->createCoProprietario($unita, $faker, $proprietario);
        }
    }
    
    private function createProprietario(UnitaImmobiliare $unita, $faker): Soggetto
    {
        $isPersonaGiuridica = rand(1, 100) <= 15; // 15% aziende
        
        if ($isPersonaGiuridica) {
            $soggetto = Soggetto::create([
                'stabile_id' => $unita->stabile_id,
                'tipo_soggetto' => 'persona_giuridica',
                'categoria_soggetto' => 'proprietario',
                'ragione_sociale' => $faker->company,
                'partita_iva' => $this->generatePartitaIva(),
                'codice_fiscale_azienda' => $this->generateCodiceFiscaleAzienda(),
                'forma_giuridica' => $faker->randomElement(['SRL', 'SPA', 'SNC', 'SAS']),
                'telefono' => $faker->phoneNumber,
                'email' => $faker->companyEmail,
                'pec' => str_replace('@', '@pec.', $faker->companyEmail),
                'indirizzo_residenza' => $faker->streetAddress,
                'citta_residenza' => $faker->city,
                'cap_residenza' => $faker->postcode,
                'provincia_residenza' => $faker->stateAbbr,
                'iban' => $faker->iban('IT'),
                'banca' => $faker->randomElement(['Intesa Sanpaolo', 'UniCredit', 'BNL', 'Monte Paschi']),
                'privacy_consenso' => true,
                'data_consenso_privacy' => now(),
                'is_active' => true,
            ]);
        } else {
            $soggetto = Soggetto::create([
                'stabile_id' => $unita->stabile_id,
                'tipo_soggetto' => 'persona_fisica',
                'categoria_soggetto' => 'proprietario',
                'nome' => $faker->firstName,
                'cognome' => $faker->lastName,
                'codice_fiscale' => $this->generateCodiceFiscale(),
                'data_nascita' => $faker->dateTimeBetween('-80 years', '-25 years')->format('Y-m-d'),
                'luogo_nascita' => $faker->city,
                'telefono' => $faker->phoneNumber,
                'cellulare' => $faker->mobileNumber,
                'email' => $faker->email,
                'indirizzo_residenza' => $faker->streetAddress,
                'citta_residenza' => $faker->city,
                'cap_residenza' => $faker->postcode,
                'provincia_residenza' => $faker->stateAbbr,
                'iban' => $faker->iban('IT'),
                'banca' => $faker->randomElement(['Intesa Sanpaolo', 'UniCredit', 'BNL', 'Monte Paschi']),
                'privacy_consenso' => true,
                'data_consenso_privacy' => now(),
                'is_active' => true,
            ]);
        }
        
        // Associa alla unità come proprietario
        $soggetto->associaUnita($unita, [
            'tipo_rapporto' => 'proprietario',
            'quota_proprieta' => 100.00,
            'data_inizio' => $faker->dateTimeBetween('-5 years', 'now')->format('Y-m-d'),
        ]);
        
        return $soggetto;
    }
    
    private function createCoProprietario(UnitaImmobiliare $unita, $faker, Soggetto $proprietarioPrincipale): void
    {
        $soggetto = Soggetto::create([
            'stabile_id' => $unita->stabile_id,
            'tipo_soggetto' => 'persona_fisica',
            'categoria_soggetto' => 'proprietario',
            'nome' => $faker->firstName,
            'cognome' => $proprietarioPrincipale->cognome, // Stesso cognome (coniuge)
            'codice_fiscale' => $this->generateCodiceFiscale(),
            'data_nascita' => $faker->dateTimeBetween('-80 years', '-25 years')->format('Y-m-d'),
            'luogo_nascita' => $faker->city,
            'telefono' => $proprietarioPrincipale->telefono, // Stesso telefono
            'cellulare' => $faker->mobileNumber,
            'email' => $faker->email,
            'indirizzo_residenza' => $proprietarioPrincipale->indirizzo_residenza,
            'citta_residenza' => $proprietarioPrincipale->citta_residenza,
            'cap_residenza' => $proprietarioPrincipale->cap_residenza,
            'provincia_residenza' => $proprietarioPrincipale->provincia_residenza,
            'privacy_consenso' => true,
            'data_consenso_privacy' => now(),
            'is_active' => true,
        ]);
        
        // Aggiorna quote proprietà
        $unita->soggetti()->updateExistingPivot($proprietarioPrincipale->id, [
            'quota_proprieta' => 50.00
        ]);
        
        // Associa il co-proprietario
        $soggetto->associaUnita($unita, [
            'tipo_rapporto' => 'proprietario',
            'quota_proprieta' => 50.00,
            'data_inizio' => $faker->dateTimeBetween('-5 years', 'now')->format('Y-m-d'),
        ]);
    }
    
    private function createInquilino(UnitaImmobiliare $unita, $faker): void
    {
        $soggetto = Soggetto::create([
            'stabile_id' => $unita->stabile_id,
            'tipo_soggetto' => 'persona_fisica',
            'categoria_soggetto' => 'inquilino',
            'nome' => $faker->firstName,
            'cognome' => $faker->lastName,
            'codice_fiscale' => $this->generateCodiceFiscale(),
            'data_nascita' => $faker->dateTimeBetween('-65 years', '-20 years')->format('Y-m-d'),
            'luogo_nascita' => $faker->city,
            'telefono' => $faker->phoneNumber,
            'cellulare' => $faker->mobileNumber,
            'email' => $faker->email,
            'indirizzo_residenza' => $faker->streetAddress,
            'citta_residenza' => $faker->city,
            'cap_residenza' => $faker->postcode,
            'provincia_residenza' => $faker->stateAbbr,
            'privacy_consenso' => true,
            'data_consenso_privacy' => now(),
            'is_active' => true,
        ]);
        
        // Calcola canone basato su superficie
        $canone = $unita->superficie_totale ? ($unita->superficie_totale * rand(8, 15)) : rand(500, 1200);
        
        $soggetto->associaUnita($unita, [
            'tipo_rapporto' => 'inquilino',
            'quota_proprieta' => 0.00,
            'data_inizio' => $faker->dateTimeBetween('-2 years', 'now')->format('Y-m-d'),
            'canone_mensile' => $canone,
            'deposito_cauzionale' => $canone * rand(2, 3),
            'tipo_contratto' => $faker->randomElement(['libero', 'concordato', 'transitorio']),
            'durata_contratto_mesi' => $faker->randomElement([48, 60, 18, 24]),
        ]);
    }
    
    private function createFornitori(Stabile $stabile, $faker): void
    {
        $fornitori = [
            ['Ditta Pulizie Verde', 'pulizie'],
            ['Elettricista Rossi', 'elettrico'],
            ['Idraulico Bianchi', 'idraulico'],
            ['Giardinaggio Fiori', 'giardinaggio'],
            ['Ascensori Veloci SRL', 'ascensori'],
        ];
        
        foreach ($fornitori as [$nome, $tipo]) {
            Soggetto::create([
                'stabile_id' => $stabile->id,
                'tipo_soggetto' => 'persona_giuridica',
                'categoria_soggetto' => 'fornitore',
                'ragione_sociale' => $nome,
                'partita_iva' => $this->generatePartitaIva(),
                'telefono' => $faker->phoneNumber,
                'email' => strtolower(str_replace(' ', '', $nome)) . '@example.com',
                'indirizzo_residenza' => $faker->streetAddress,
                'citta_residenza' => $faker->city,
                'cap_residenza' => $faker->postcode,
                'provincia_residenza' => $faker->stateAbbr,
                'configurazioni' => ['tipo_servizio' => $tipo],
                'is_active' => true,
            ]);
        }
    }
    
    private function generateCodiceFiscale(): string
    {
        $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        return substr(str_shuffle($chars), 0, 16);
    }
    
    private function generateCodiceFiscaleAzienda(): string
    {
        return $this->generateCodiceFiscale();
    }
    
    private function generatePartitaIva(): string
    {
        return str_pad(rand(10000000000, 99999999999), 11, '0', STR_PAD_LEFT);
    }
}

🔍 QUERY PATTERNS

Dashboard Soggetti:

// Controller - Overview soggetti per stabile
public function dashboard()
{
    $stats = [
        'proprietari_totali' => Soggetto::proprietari()->count(),
        'inquilini_totali' => Soggetti::inquilini()->count(),
        'fornitori_totali' => Soggetto::fornitori()->count(),
        'persone_fisiche' => Soggetto::personeFisiche()->count(),
        'persone_giuridiche' => Soggetto::personeGiuridiche()->count(),
    ];
    
    // Soggetti con più unità
    $soggettiMultipli = Soggetto::has('unitaImmobiliari', '>', 1)
        ->with(['unitaImmobiliari'])
        ->limit(10)
        ->get();
    
    return view('soggetti.dashboard', compact('stats', 'soggettiMultipli'));
}

Ricerca Avanzata:

// Controller - Ricerca soggetti con filtri
public function search(Request $request)
{
    $query = Soggetto::with(['stabile', 'unitaImmobiliari']);
    
    if ($request->filled('search')) {
        $query->search($request->search);
    }
    
    if ($request->filled('categoria')) {
        $query->byCategoria($request->categoria);
    }
    
    if ($request->filled('tipo')) {
        $query->byTipo($request->tipo);
    }
    
    if ($request->filled('stabile_id')) {
        $query->where('stabile_id', $request->stabile_id);
    }
    
    $soggetti = $query->orderBy('cognome')
                     ->orderBy('nome')
                     ->orderBy('ragione_sociale')
                     ->paginate(20);
    
    return view('soggetti.search', compact('soggetti'));
}

🎯 UTILIZZO PATTERN

Nel Controller:

// Proprietari di uno stabile
$proprietari = Soggetto::proprietari()
    ->where('stabile_id', $stabile->id)
    ->with('unitaProprietaAttive')
    ->get();

// Inquilini con contratti attivi
$inquilini = Soggetto::inquilini()
    ->whereHas('unitaInquilinoIn', function($q) {
        $q->wherePivot('is_active', true);
    })
    ->get();

// Totale canoni mensili
$totaleCanoni = Soggetto::inquilini()
    ->get()
    ->sum(function($soggetto) {
        return $soggetto->calcolaTotaleCanoniMensili();
    });

Nei Blade Templates:

{{-- Card soggetto --}}
@foreach($soggetti as $soggetto)
    <div class="card">
        <div class="flex justify-between items-start">
            <div>
                <h3>{{ $soggetto->denominazione_computed }}</h3>
                <p class="text-sm text-gray-600">
                    {{ ucfirst($soggetto->categoria_soggetto) }} • 
                    {{ $soggetto->is_persona_fisica ? 'Persona fisica' : 'Persona giuridica' }}
                </p>
                <p class="text-sm">{{ $soggetto->indirizzo_completo }}</p>
                <p class="text-sm">{{ $soggetto->contatto_preferenziale }}</p>
            </div>
            
            <div class="text-right">
                @if($soggetto->is_proprietario)
                    <span class="badge badge-green">Proprietario</span>
                    <p class="text-xs mt-1">{{ $soggetto->calcolaTotaleMillesimi() }}/1000</p>
                @elseif($soggetto->is_inquilino)
                    <span class="badge badge-blue">Inquilino</span>
                    <p class="text-xs mt-1">€{{ number_format($soggetto->calcolaTotaleCanoniMensili(), 2) }}/mese</p>
                @endif
            </div>
        </div>
        
        <div class="mt-3">
            <p class="text-sm font-medium">Unità collegate:</p>
            @foreach($soggetto->getUnitaAttive() as $unita)
                <span class="inline-block bg-gray-100 rounded px-2 py-1 text-xs mr-1 mb-1">
                    {{ $unita->interno }} ({{ $unita->pivot->quota_proprieta }}%)
                </span>
            @endforeach
        </div>
    </div>
@endforeach

🎯 TABELLE SOGGETTI + UNITA-SOGGETTI COMPLETATE
Con anagrafica completa, relazioni many-to-many e gestione contratti

📖 Hai ora la struttura completa: amministratori → stabili → gestioni → unità → soggetti
Prossimo step consigliato: contabilita.md per movimenti finanziari collegati alle gestioni