35 KiB
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