netgescon-day0/app/Filament/Pages/Affitti/GestioneAffitti.php

1535 lines
60 KiB
PHP

<?php
namespace App\Filament\Pages\Affitti;
use App\Models\AffittoCanoneDovuto;
use App\Models\AffittoCanonePagato;
use App\Models\AffittoClausola;
use App\Models\AffittoContrattoClausola;
use App\Models\AffittoImmobile;
use App\Models\AffittoInquilino;
use App\Models\DatiBancari;
use App\Models\DocumentoStabile;
use App\Models\RubricaUniversale;
use App\Models\Stabile;
use App\Models\StabileServizio;
use App\Models\UnitaImmobiliare;
use App\Services\Affitti\IstatRivalutazioneService;
use App\Support\StabileContext;
use BackedEnum;
use Filament\Pages\Page;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Livewire\Attributes\Url;
use Livewire\WithFileUploads;
use Symfony\Component\Process\Process;
use UnitEnum;
class GestioneAffitti extends Page
{
use WithFileUploads;
protected static ?string $navigationLabel = 'Gestione Affitti';
protected static ?string $title = 'Gestione Affitti';
protected static UnitEnum|string|null $navigationGroup = 'Stabile';
protected static ?string $slug = 'affitti/gestione';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-home-modern';
protected string $view = 'filament.pages.affitti.gestione-affitti';
#[Url( as : 'tab')]
public string $tab = 'elenco';
public string $anagraficaSection = 'anagrafica';
public string $elencoTipoFiltro = 'tutti';
public string $elencoStatoContrattoFiltro = 'attivi';
public array $affittiOptions = [];
public array $affittiList = [];
public ?int $selectedAffittoId = null;
public array $form = [];
public bool $forceAll = false;
public ?int $stabileFilterId = null;
public array $stabileOptions = [];
public array $unitaOptions = [];
public array $localiTecnici = [];
public array $rubricaOptions = [];
public array $contoOptions = [];
public ?int $selectedStabileRubricaId = null;
public ?string $selectedStabileCode = null;
public ?string $selectedStabileName = null;
public bool $selectedAffittoObsoleto = false;
public array $servizi = [];
public array $serviziCatalogo = [
'assicurazione' => 'Assicurazione',
'ascensore' => 'Ascensore',
'riscaldamento' => 'Riscaldamento',
'antenna' => 'Impianto antenna',
'acqua' => 'Ripartizione acqua',
'locale_biciclette' => 'Locale biciclette',
'altro' => 'Altro servizio',
];
public array $canoniDovuti = [];
public array $canoniPagati = [];
public array $canoniRighe = [];
public array $inquilini = [];
public array $clausoleContratto = [];
public array $clausoleCatalogo = [];
public array $contractTemplates = [];
public ?string $selectedContractTemplate = null;
public ?string $contractDraftPreview = null;
public array $totali = [
'dovuti' => 0,
'pagati' => 0,
'saldo' => 0,
];
public $documentFile;
public ?string $documentDescrizione = null;
public ?string $documentCategoria = 'contratti';
public ?string $documentDataScadenza = null;
public ?int $documentInquilinoRow = null;
public string $legacyMdbPath = '/mnt/gescon-archives/gescon/parti_comuni.mdb';
public ?string $legacyLastSyncMessage = null;
public static function canAccess(): bool
{
$user = Auth::user();
if (! $user instanceof \App\Models\User) {
return false;
}
return $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
}
public function mount(): void
{
$this->initContractTemplates();
$this->tab = in_array($this->tab, ['elenco', 'anagrafica', 'banca', 'scadenze', 'canoni', 'documenti'], true)
? $this->tab
: 'elenco';
$this->loadStabiliOptions();
if (! $this->forceAll) {
$this->stabileFilterId = $this->resolveStabileIdFromContext();
}
$this->loadAffitti();
if (! $this->selectedAffittoId && ! empty($this->affittiOptions)) {
$this->selectedAffittoId = (int) array_key_first($this->affittiOptions);
}
$this->loadUnita();
$this->loadServizi();
$this->loadLocaliTecnici();
$this->loadRubricaOptions();
$this->loadContoOptions();
$this->loadClausoleCatalogo();
$this->loadSelected();
}
public function updatedStabileFilterId(): void
{
$this->loadAffitti();
if (! $this->selectedAffittoId && ! empty($this->affittiOptions)) {
$this->selectedAffittoId = (int) array_key_first($this->affittiOptions);
}
$this->loadUnita();
$this->loadServizi();
$this->loadLocaliTecnici();
$this->loadSelected();
}
public function updatedSelectedAffittoId(): void
{
$this->loadSelected();
}
public function selectAffitto(int $id): void
{
$this->selectedAffittoId = $id;
$this->loadSelected();
$this->setTab('scadenze');
}
public function setTab(string $tab): void
{
$this->tab = in_array($tab, ['elenco', 'anagrafica', 'banca', 'scadenze', 'canoni', 'documenti'], true) ? $tab : 'elenco';
}
public function setAnagraficaSection(string $section): void
{
$this->anagraficaSection = in_array($section, ['anagrafica', 'catasto', 'inquilini'], true)
? $section
: 'anagrafica';
}
public function mergeCatastoFromUnita(bool $force = false): void
{
if (! $this->selectedAffittoId) {
return;
}
$record = AffittoImmobile::find($this->selectedAffittoId);
if (! $record) {
return;
}
$unit = $this->resolveCandidateUnita($record);
if (! $unit) {
$this->dispatch('notify', ['type' => 'warning', 'message' => 'Nessuna unità immobiliare trovata da agganciare al contratto.']);
return;
}
if (! $record->unita_immobiliare_id) {
$record->unita_immobiliare_id = $unit->id;
}
$map = [
'sezione_urbana' => $unit->sezione_urbana,
'foglio' => $unit->foglio,
'particella' => $unit->particella,
'subalterno' => $unit->subalterno,
'categoria_catastale' => $unit->categoria_catastale,
'classe_catastale' => $unit->classe,
'consistenza_catastale' => $unit->consistenza,
'rendita_catastale' => $unit->rendita_catastale,
'interno_catastale' => $unit->interno,
'piano_catastale' => $unit->piano,
];
$surface = $unit->superficie_commerciale ?? $unit->superficie_calpestabile;
if ($surface !== null) {
$map['superficie_catastale_mq'] = $surface;
}
if (! empty($unit->tipo_unita)) {
$map['destinazione_uso'] = $unit->tipo_unita;
}
foreach ($map as $field => $value) {
$current = $record->{$field};
if ($force || $this->isEmptyValue($current)) {
$record->{$field} = $value;
}
}
$record->save();
$this->loadSelected();
$this->dispatch('notify', ['type' => 'success', 'message' => 'Dati catastali uniti ai valori dell\'unità immobiliare.']);
}
public function generateContractDraft(): void
{
if (! $this->selectedAffittoId) {
return;
}
$record = AffittoImmobile::with(['stabile.amministratore', 'rubricaInquilino', 'inquilini.rubrica', 'unitaImmobiliare'])->find($this->selectedAffittoId);
if (! $record) {
return;
}
$templateKey = $this->selectedContractTemplate ?: 'abitativo_431_2';
if (! isset($this->contractTemplates[$templateKey])) {
$this->dispatch('notify', ['type' => 'warning', 'message' => 'Template contratto non disponibile.']);
return;
}
$stabile = $record->stabile;
$amministratore = $stabile?->amministratore;
$inquilino = $record->rubricaInquilino;
if (! $inquilino) {
$inquilino = $record->inquilini()->where('attivo', true)->with('rubrica')->first()?->rubrica;
}
$locatoreDenominazione = trim((string) ($stabile?->denominazione ?? 'Condominio'));
$locatoreCf = trim((string) ($stabile?->codice_fiscale ?? ''));
$locatoreIndirizzo = trim((string) ($stabile?->indirizzo_completo ?? ''));
$amministratoreNome = trim((string) (($stabile?->amministratore_nome ?: null) ?? (($amministratore?->nome ?? '') . ' ' . ($amministratore?->cognome ?? ''))));
$amministratoreCf = trim((string) (($stabile?->cod_fisc_amministratore ?: null) ?? ($amministratore?->codice_fiscale_studio ?? '')));
$conduttriceNome = trim((string) ($inquilino?->nome_completo ?? $record->nome_inquilino ?? ''));
$conduttriceCf = trim((string) ($inquilino?->codice_fiscale ?? ''));
$descUnita = trim((string) ($record->descrizione_immobile ?? 'unità immobiliare'));
$indirizzoUnita = trim((string) ($record->indirizzo_immobile ?: $locatoreIndirizzo));
$catasto = 'Foglio ' . ($record->foglio ?? '---')
. ' - Particella ' . ($record->particella ?? '---')
. ' - Sub ' . ($record->subalterno ?? '---')
. ' - Cat. ' . ($record->categoria_catastale ?? '---')
. ' con rendita catastale di EUR ' . number_format((float) ($record->rendita_catastale ?? 0), 2, ',', '.');
$dataInizio = $record->inizio_contratto ? Carbon::parse($record->inizio_contratto)->format('d/m/Y') : '---';
$dataFine = $record->data_fine_contratto ? Carbon::parse($record->data_fine_contratto)->format('d/m/Y') : '---';
$canoneMensile = number_format((float) ($record->importo_fitto ?? 0), 2, ',', '.');
$this->contractDraftPreview = "CONTRATTO DI LOCAZIONE AD USO ABITATIVO\n"
. "(Ai sensi dell'art. 2 della legge 9 dicembre 1998, n. 431)\n\n"
. "Tra\n"
. "{$locatoreDenominazione}" . ($locatoreIndirizzo !== '' ? ", {$locatoreIndirizzo}" : '')
. ($locatoreCf !== '' ? ", Cod. Fisc. {$locatoreCf}" : '')
. ", di seguito denominato locatore;\n"
. "nella persona dell'amministratore pro-tempore {$amministratoreNome}"
. ($amministratoreCf !== '' ? ", Cod. Fisc. {$amministratoreCf}" : '') . ".\n\n"
. "E\n"
. "{$conduttriceNome}" . ($conduttriceCf !== '' ? ", Cod. Fisc. {$conduttriceCf}" : '')
. ", di seguito denominata conduttrice.\n\n"
. "CONCEDE IN LOCAZIONE\n"
. "{$descUnita} posta in {$indirizzoUnita}.\n"
. "Dati catastali: {$catasto}.\n\n"
. "SI CONVIENE E STIPULA QUANTO SEGUE\n"
. "1) Durata: dal {$dataInizio} al {$dataFine}.\n"
. "2) Canone mensile: EUR {$canoneMensile}.\n"
. "3) Clausole aggiuntive: come da sezione Clausole e postille del gestionale.\n\n"
. "Luogo e data: __________________________\n"
. "Firma locatore: _________________________\n"
. "Firma conduttrice: ______________________\n";
$this->dispatch('notify', ['type' => 'success', 'message' => 'Bozza contratto generata dal template selezionato.']);
}
public function createAffitto(): void
{
$stabileId = $this->resolveStabileId();
if (! $stabileId) {
$this->dispatch('notify', ['type' => 'warning', 'message' => 'Seleziona prima uno stabile.']);
return;
}
$record = AffittoImmobile::create([
'stabile_id' => $stabileId,
'codice_stabile_legacy' => $this->resolveStabileLegacyCode($stabileId),
'codice_appartamento' => 'NEW-' . now()->format('YmdHis'),
'descrizione_immobile' => 'Nuovo immobile',
]);
$this->loadAffitti();
$this->selectedAffittoId = $record->id;
$this->loadSelected();
}
public function saveAffitto(): void
{
if (! $this->selectedAffittoId) {
return;
}
$record = AffittoImmobile::find($this->selectedAffittoId);
if (! $record) {
return;
}
$record->fill($this->form);
if (! $record->stabile_id) {
$record->stabile_id = $this->resolveStabileId();
}
if (! $record->codice_stabile_legacy) {
$record->codice_stabile_legacy = $this->resolveStabileLegacyCode($record->stabile_id);
}
$record->save();
$this->saveInquilini($record);
$this->saveClausoleContratto($record);
$this->emitCanoniAutomaticiForRecord($record);
$this->loadAffitti();
$this->loadSelected();
$this->dispatch('notify', ['type' => 'success', 'message' => 'Affitto aggiornato.']);
}
public function markContrattoObsoleto(): void
{
if (! $this->selectedAffittoId) {
return;
}
$record = AffittoImmobile::find($this->selectedAffittoId);
if (! $record) {
return;
}
$record->obsoleto = true;
$record->obsoleto_dal = now()->toDateString();
if (empty($record->motivo_obsolescenza)) {
$record->motivo_obsolescenza = 'Contratto non piu in essere';
}
$record->save();
$this->loadAffitti();
$this->loadSelected();
$this->dispatch('notify', ['type' => 'success', 'message' => 'Contratto marcato come obsoleto e nascosto dagli attivi.']);
}
public function riattivaContratto(): void
{
if (! $this->selectedAffittoId) {
return;
}
$record = AffittoImmobile::find($this->selectedAffittoId);
if (! $record) {
return;
}
$record->obsoleto = false;
$record->obsoleto_dal = null;
$record->save();
$this->loadAffitti();
$this->loadSelected();
$this->dispatch('notify', ['type' => 'success', 'message' => 'Contratto riattivato.']);
}
public function emitCanoniAutomatici(): void
{
if (! $this->selectedAffittoId) {
return;
}
$record = AffittoImmobile::find($this->selectedAffittoId);
if (! $record) {
return;
}
$created = $this->emitCanoniAutomaticiForRecord($record);
$this->loadSelected();
$this->dispatch('notify', [
'type' => 'success',
'message' => 'Emissione automatica completata: ' . $created . ' rate create/aggiornate.',
]);
}
public function addInquilino(): void
{
$this->inquilini[] = [
'id' => null,
'rubrica_inquilino_id' => null,
'nome_visualizzato' => null,
'email' => null,
'telefono' => null,
'documenti_note' => null,
'attivo' => true,
];
}
public function removeInquilino(int $index): void
{
if (! isset($this->inquilini[$index])) {
return;
}
unset($this->inquilini[$index]);
$this->inquilini = array_values($this->inquilini);
}
public function addClausolaContratto(): void
{
$this->clausoleContratto[] = [
'id' => null,
'clausola_id' => null,
'titolo' => null,
'testo' => null,
'periodicita' => null,
'importo' => null,
'ordine' => count($this->clausoleContratto) + 1,
'attivo' => true,
];
}
public function removeClausolaContratto(int $index): void
{
if (! isset($this->clausoleContratto[$index])) {
return;
}
unset($this->clausoleContratto[$index]);
$this->clausoleContratto = array_values($this->clausoleContratto);
}
public function useClausolaTemplate(int $index): void
{
if (! isset($this->clausoleContratto[$index])) {
return;
}
$clausolaId = (int) ($this->clausoleContratto[$index]['clausola_id'] ?? 0);
if ($clausolaId <= 0) {
return;
}
$template = AffittoClausola::find($clausolaId);
if (! $template) {
return;
}
$this->clausoleContratto[$index]['titolo'] = $template->titolo;
$this->clausoleContratto[$index]['testo'] = $template->testo;
$this->clausoleContratto[$index]['attivo'] = (bool) $template->attivo;
}
public function cleanCanoniDuplicates(): void
{
if (! $this->selectedAffittoId) {
return;
}
$removed = 0;
$removed += $this->deleteCanoniDuplicates(AffittoCanoneDovuto::query()->where('affitto_id', $this->selectedAffittoId)->get()->all());
$removed += $this->deleteCanoniDuplicates(AffittoCanonePagato::query()->where('affitto_id', $this->selectedAffittoId)->get()->all());
$this->loadSelected();
$this->dispatch('notify', [
'type' => 'success',
'message' => 'Pulizia completata: rimossi ' . $removed . ' duplicati canoni.',
]);
}
public function syncAffittiFromLegacy(): void
{
$mdb = trim((string) $this->legacyMdbPath);
if ($mdb === '' || ! is_file($mdb)) {
$this->dispatch('notify', ['type' => 'warning', 'message' => 'Percorso MDB non valido.']);
return;
}
$cmd = ['php', 'artisan', 'affitti:import-mdb', '--mdb=' . $mdb, '--refresh-canoni'];
$stabileLegacy = $this->resolveStabileLegacyCode($this->resolveStabileId());
if (! empty($stabileLegacy)) {
$cmd[] = '--stabile=' . $stabileLegacy;
}
$process = new Process($cmd, base_path());
$process->setTimeout(900);
$process->run();
if (! $process->isSuccessful()) {
$this->legacyLastSyncMessage = trim($process->getErrorOutput()) ?: 'Errore durante il sync affitti da legacy.';
$this->dispatch('notify', ['type' => 'danger', 'message' => 'Sync legacy fallito. Controlla output.']);
return;
}
$this->legacyLastSyncMessage = trim($process->getOutput()) ?: 'Sincronizzazione completata.';
$this->loadAffitti();
$this->loadSelected();
$this->dispatch('notify', ['type' => 'success', 'message' => 'Dati affitti aggiornati dal legacy MDB.']);
}
public function uploadDocumento(): void
{
if (! $this->selectedAffittoId || ! $this->documentFile) {
return;
}
$affitto = AffittoImmobile::find($this->selectedAffittoId);
if (! $affitto || ! $affitto->stabile_id) {
return;
}
$descrizione = $this->documentDescrizione;
if ($this->documentInquilinoRow !== null && isset($this->inquilini[$this->documentInquilinoRow])) {
$inq = $this->inquilini[$this->documentInquilinoRow];
$inqName = trim((string) ($inq['nome_visualizzato'] ?? ''));
if ($inqName === '' && ! empty($inq['rubrica_inquilino_id'])) {
$inqName = (string) (RubricaUniversale::find((int) $inq['rubrica_inquilino_id'])?->nome_completo ?? '');
}
if ($inqName !== '') {
$prefix = 'Inquilino: ' . $inqName;
$descrizione = trim($prefix . ($descrizione ? ' · ' . $descrizione : ''));
}
}
$file = $this->documentFile;
$path = $file->store('documenti_stabili/' . $affitto->stabile_id . '/affitti');
$size = $file->getSize();
$mime = $file->getMimeType();
$name = basename($path);
DocumentoStabile::create([
'stabile_id' => $affitto->stabile_id,
'nome_file' => $name,
'nome_originale' => $file->getClientOriginalName(),
'percorso_file' => $path,
'categoria' => $this->documentCategoria ?: 'contratti',
'tipo_mime' => $mime ?: 'application/octet-stream',
'dimensione' => $size ?: 0,
'descrizione' => $descrizione,
'data_scadenza' => $this->documentDataScadenza ?: null,
'tags' => 'affitti,contratto',
'pubblico' => false,
'protetto' => false,
'versione' => 1,
'downloads' => 0,
'caricato_da' => Auth::id(),
]);
$this->documentFile = null;
$this->documentDescrizione = null;
$this->documentDataScadenza = null;
$this->documentInquilinoRow = null;
$this->dispatch('notify', ['type' => 'success', 'message' => 'Documento caricato.']);
}
public function saveServizi(): void
{
$stabileId = $this->resolveStabileId();
if (! $stabileId) {
return;
}
foreach ($this->serviziCatalogo as $key => $label) {
$row = $this->servizi[$key] ?? [];
$payload = [
'stabile_id' => $stabileId,
'tipo' => $key,
'nome' => $label,
'attivo' => (bool) ($row['attivo'] ?? false),
'meta' => [
'data_scadenza' => $row['data_scadenza'] ?? null,
'note' => $row['note'] ?? null,
],
];
StabileServizio::updateOrCreate(
['stabile_id' => $stabileId, 'tipo' => $key],
$payload
);
}
$this->loadServizi();
$this->dispatch('notify', ['type' => 'success', 'message' => 'Servizi aggiornati.']);
}
private function loadAffitti(): void
{
$stabileId = $this->resolveStabileId();
$query = AffittoImmobile::query();
if ($stabileId) {
$query->where('stabile_id', $stabileId);
}
$rows = $query
->withSum('canoniDovuti as dovuti_totale', 'totale')
->withSum('canoniPagati as pagati_totale', 'totale')
->orderBy('proprietario_nome')
->orderBy('codice_appartamento')
->get();
$this->affittiOptions = $rows->mapWithKeys(function (AffittoImmobile $a) {
$label = trim(($a->codice_appartamento ?? '—') . ' · ' . ($a->descrizione_immobile ?? 'Immobiliare'));
return [$a->id => $label];
})->all();
$this->affittiList = $rows->map(fn(AffittoImmobile $a) => [
'warnings' => $this->buildAffittoWarnings($a),
'id' => $a->id,
'codice_appartamento' => $a->codice_appartamento,
'descrizione_immobile' => $a->descrizione_immobile,
'nome_inquilino' => $a->nome_inquilino,
'proprietario_nome' => $a->proprietario_nome,
'inizio_contratto' => $a->inizio_contratto,
'presa_in_carico_operativa_dal' => $a->presa_in_carico_operativa_dal,
'prossima_registrazione' => $a->prossima_registrazione,
'tipo_riga' => $a->tipo_riga,
'stabile_id' => $a->stabile_id,
'dovuti_totale' => (float) ($a->dovuti_totale ?? 0),
'pagati_totale' => (float) ($a->pagati_totale ?? 0),
'saldo_totale' => (float) ($a->dovuti_totale ?? 0) - (float) ($a->pagati_totale ?? 0),
'obsoleto' => (bool) ($a->obsoleto ?? false),
'obsoleto_dal' => $a->obsoleto_dal,
'stabile' => $a->stabile?->codice_univoco ?: ($a->stabile?->codice_stabile ?: $a->codice_stabile_legacy),
'stabile_nome' => $a->stabile?->denominazione,
])->all();
}
private function buildAffittoWarnings(AffittoImmobile $affitto): array
{
$warnings = [];
$today = Carbon::today();
if ($affitto->prossima_scadenza) {
$days = $today->diffInDays(Carbon::parse($affitto->prossima_scadenza), false);
if ($days <= 60) {
$warnings[] = $days < 0
? 'Rinnovo contratto scaduto da ' . abs($days) . ' giorni'
: 'Rinnovo contratto entro ' . $days . ' giorni';
}
}
if ($affitto->prossima_registrazione) {
$days = $today->diffInDays(Carbon::parse($affitto->prossima_registrazione), false);
if ($days <= 45) {
$warnings[] = $days < 0
? 'Registrazione AdE scaduta da ' . abs($days) . ' giorni'
: 'Registrazione AdE entro ' . $days . ' giorni';
}
}
$managementStart = $this->resolveCanoniManagementStart($affitto);
$scadutiNonPagati = AffittoCanoneDovuto::query()
->where('affitto_id', $affitto->id)
->when($managementStart, function ($query) use ($managementStart) {
$query->where(function ($inner) use ($managementStart) {
$inner->whereDate('data_scadenza', '>=', $managementStart->toDateString())
->orWhere(function ($fallback) use ($managementStart) {
$fallback->whereNull('data_scadenza')
->where(function ($monthQuery) use ($managementStart) {
$monthQuery->where('anno', '>', (int) $managementStart->year)
->orWhere(function ($sameYear) use ($managementStart) {
$sameYear->where('anno', (int) $managementStart->year)
->where('mese', '>=', (int) $managementStart->month);
});
});
});
});
})
->whereDate('data_scadenza', '<', $today->toDateString())
->sum('totale')
- AffittoCanonePagato::query()
->where('affitto_id', $affitto->id)
->when($managementStart, function ($query) use ($managementStart) {
$query->where(function ($inner) use ($managementStart) {
$inner->whereDate('data_pagamento', '>=', $managementStart->toDateString())
->orWhere(function ($fallback) use ($managementStart) {
$fallback->whereNull('data_pagamento')
->where(function ($monthQuery) use ($managementStart) {
$monthQuery->where('anno', '>', (int) $managementStart->year)
->orWhere(function ($sameYear) use ($managementStart) {
$sameYear->where('anno', (int) $managementStart->year)
->where('mese', '>=', (int) $managementStart->month);
});
});
});
});
})
->whereDate('data_pagamento', '<=', $today->toDateString())
->sum('totale');
if ($scadutiNonPagati > 0.01) {
$warnings[] = 'Canoni scaduti non pagati: EUR ' . number_format($scadutiNonPagati, 2, ',', '.');
}
return $warnings;
}
private function loadStabiliOptions(): void
{
$this->stabileOptions = Stabile::query()
->orderBy('codice_stabile')
->get()
->mapWithKeys(function (Stabile $s) {
$label = trim(($s->codice_univoco ?: $s->codice_stabile ?: ('Stabile ' . $s->id)) . ' · ' . ($s->denominazione ?? ''));
return [$s->id => $label];
})
->all();
}
private function loadUnita(): void
{
$stabileId = $this->resolveStabileId();
if (! $stabileId) {
$this->unitaOptions = [];
return;
}
$this->unitaOptions = UnitaImmobiliare::query()
->withTrashed()
->where('stabile_id', $stabileId)
->orderBy('scala')
->orderBy('piano')
->orderBy('interno')
->get()
->mapWithKeys(function (UnitaImmobiliare $u) {
$status = $u->trashed() ? ' [archiviata]' : '';
$label = trim(($u->codice_unita ?? $u->denominazione ?? ('Unità ' . $u->id)) . ' · ' . ($u->denominazione ?? 'unità') . ' · Scala ' . ($u->scala ?? '-') . ' · Int ' . ($u->interno ?? '-') . $status);
return [$u->id => $label];
})
->all();
}
private function loadLocaliTecnici(): void
{
$stabileId = $this->resolveStabileId();
if (! $stabileId) {
$this->localiTecnici = [];
return;
}
$tipi = ['locale_tecnico', 'centrale_termica', 'autorimessa', 'cantina', 'box', 'solaio', 'posto_auto', 'locale_bici'];
$this->localiTecnici = UnitaImmobiliare::query()
->where('stabile_id', $stabileId)
->whereIn('tipo_unita', $tipi)
->orderBy('scala')
->orderBy('piano')
->orderBy('interno')
->get()
->map(fn(UnitaImmobiliare $u) => [
'id' => $u->id,
'codice' => $u->codice_unita ?? $u->denominazione ?? ('Unità ' . $u->id),
'tipo' => $u->tipo_unita,
'scala' => $u->scala,
'interno' => $u->interno,
'descrizione' => $u->descrizione,
])
->all();
}
private function loadServizi(): void
{
$stabileId = $this->resolveStabileId();
$this->servizi = [];
if (! $stabileId) {
return;
}
$rows = StabileServizio::where('stabile_id', $stabileId)->get()->keyBy('tipo');
foreach ($this->serviziCatalogo as $key => $label) {
$row = $rows->get($key);
$meta = is_array($row?->meta) ? $row->meta : [];
$this->servizi[$key] = [
'attivo' => (bool) ($row?->attivo ?? false),
'data_scadenza' => $meta['data_scadenza'] ?? null,
'note' => $meta['note'] ?? null,
];
}
}
private function loadSelected(): void
{
$this->form = [
'codice_appartamento' => null,
'unita_immobiliare_id' => null,
'proprietario_nome' => null,
'proprietario_intesta' => null,
'descrizione_immobile' => null,
'indirizzo_immobile' => null,
'cap' => null,
'citta' => null,
'provincia' => null,
'nome_inquilino' => null,
'rubrica_inquilino_id' => null,
'conto_bancario_id' => null,
'rendita_catastale' => null,
'importo_fitto' => null,
'particella' => null,
'foglio' => null,
'subalterno' => null,
'destinazione_uso' => null,
'comune_catastale_codice' => null,
'comune_catastale_nome' => null,
'sezione_urbana' => null,
'categoria_catastale' => null,
'classe_catastale' => null,
'consistenza_catastale' => null,
'superficie_catastale_mq' => null,
'interno_catastale' => null,
'piano_catastale' => null,
'numero_registrazione_ade' => null,
'data_registrazione_ade' => null,
'inizio_contratto' => null,
'presa_in_carico_operativa_dal' => null,
'ultimo_rinnovo' => null,
'prossima_scadenza' => null,
'prossima_registrazione' => null,
'data_fine_contratto' => null,
'giorno_emissione_canone' => 5,
'mese_adempimenti_annuali' => 1,
'giorno_adempimenti_annuali' => 30,
'imposta_registro_annuale' => null,
'quota_imposta_inquilino_percent' => 50,
'imposta_bollo_annuale' => null,
'obsoleto' => false,
'obsoleto_dal' => null,
'motivo_obsolescenza' => null,
'note' => null,
'def_ammin' => null,
'descr_1_voce' => null,
'tipo_riga' => null,
'inte_cc' => null,
'iban' => null,
'selez' => null,
'num_ccp' => null,
'autorizz_674' => null,
];
$this->canoniDovuti = [];
$this->canoniPagati = [];
$this->canoniRighe = [];
$this->totali = ['dovuti' => 0, 'pagati' => 0, 'saldo' => 0];
$this->selectedStabileRubricaId = null;
$this->selectedStabileCode = null;
$this->selectedStabileName = null;
$this->inquilini = [];
$this->clausoleContratto = [];
if (! $this->selectedAffittoId) {
return;
}
$record = AffittoImmobile::find($this->selectedAffittoId);
if (! $record) {
return;
}
$this->ensureUnitLink($record);
$record = AffittoImmobile::find($this->selectedAffittoId);
if (! $record) {
return;
}
foreach ($this->form as $k => $v) {
$this->form[$k] = $record->{$k};
}
if ($record->stabile) {
$this->selectedStabileRubricaId = $record->stabile->rubrica_id ?? null;
$this->selectedStabileCode = $record->stabile->codice_univoco ?: ($record->stabile->codice_stabile ?? null);
$this->selectedStabileName = $record->stabile->denominazione ?? null;
}
$this->selectedAffittoObsoleto = (bool) ($record->obsoleto ?? false);
$this->emitCanoniAutomaticiForRecord($record);
$dovuti = AffittoCanoneDovuto::where('affitto_id', $record->id)->orderByDesc('anno')->orderByDesc('mese')->get();
$this->ensureRicevute($record, $dovuti);
$pagati = AffittoCanonePagato::where('affitto_id', $record->id)->orderByDesc('anno')->orderByDesc('mese')->get();
$managementStart = $this->resolveCanoniManagementStart($record);
if ($managementStart) {
$start = $managementStart->copy()->startOfMonth();
$dovuti = $dovuti->filter(function (AffittoCanoneDovuto $row) use ($start) {
$month = Carbon::create((int) $row->anno, (int) $row->mese, 1);
return $month->gte($start);
})->values();
$pagati = $pagati->filter(function (AffittoCanonePagato $row) use ($start) {
$month = Carbon::create((int) $row->anno, (int) $row->mese, 1);
return $month->gte($start);
})->values();
}
$this->canoniDovuti = $dovuti->toArray();
$this->canoniPagati = $pagati->toArray();
$this->canoniRighe = $this->buildCanoniRows($dovuti->toArray(), $pagati->toArray());
$this->inquilini = $record->inquilini()->orderBy('id')->get()->map(function (AffittoInquilino $row) {
return [
'id' => $row->id,
'rubrica_inquilino_id' => $row->rubrica_inquilino_id,
'nome_visualizzato' => $row->nome_visualizzato,
'email' => $row->email,
'telefono' => $row->telefono,
'documenti_note' => $row->documenti_note,
'attivo' => (bool) $row->attivo,
];
})->all();
if (empty($this->inquilini) && ($record->nome_inquilino || $record->rubrica_inquilino_id)) {
$this->inquilini[] = [
'id' => null,
'rubrica_inquilino_id' => $record->rubrica_inquilino_id,
'nome_visualizzato' => $record->nome_inquilino,
'email' => null,
'telefono' => null,
'documenti_note' => null,
'attivo' => true,
];
}
$this->clausoleContratto = $record->clausoleContratto()->orderBy('ordine')->orderBy('id')->get()->map(function (AffittoContrattoClausola $row) {
return [
'id' => $row->id,
'clausola_id' => $row->clausola_id,
'titolo' => $row->titolo,
'testo' => $row->testo,
'periodicita' => $row->periodicita,
'importo' => $row->importo,
'ordine' => $row->ordine,
'attivo' => (bool) $row->attivo,
];
})->all();
$sumDovuti = $dovuti->sum('totale');
$sumPagati = $pagati->sum('totale');
$this->totali = [
'dovuti' => $sumDovuti,
'pagati' => $sumPagati,
'saldo' => $sumDovuti - $sumPagati,
];
$this->selectedContractTemplate = $this->selectedContractTemplate ?: 'abitativo_431_2';
}
private function ensureUnitLink(AffittoImmobile $record): void
{
if ($record->unita_immobiliare_id) {
return;
}
$candidate = $this->resolveCandidateUnita($record);
if (! $candidate) {
return;
}
$record->unita_immobiliare_id = $candidate->id;
$record->save();
}
private function resolveCandidateUnita(AffittoImmobile $record): ?UnitaImmobiliare
{
$stabileId = $record->stabile_id ?: $this->resolveStabileId();
if (! $stabileId) {
return null;
}
$query = UnitaImmobiliare::withTrashed()->where('stabile_id', $stabileId);
if ($record->unita_immobiliare_id) {
return $query->where('id', $record->unita_immobiliare_id)->first();
}
$code = trim((string) ($record->codice_appartamento ?? ''));
$desc = trim((string) ($record->descrizione_immobile ?? ''));
if ($code !== '') {
$byCode = (clone $query)->where(function ($q) use ($code) {
$q->where('codice_unita', $code)
->orWhere('denominazione', $code)
->orWhere('interno', $code);
})->first();
if ($byCode) {
return $byCode;
}
}
if ($desc !== '') {
$byDesc = (clone $query)->where('denominazione', 'like', '%' . $desc . '%')->first();
if ($byDesc) {
return $byDesc;
}
}
return null;
}
private function isEmptyValue(mixed $value): bool
{
if ($value === null) {
return true;
}
if (is_string($value)) {
return trim($value) === '';
}
return false;
}
private function resolveStabileId(): ?int
{
if ($this->stabileFilterId) {
return $this->stabileFilterId;
}
if ($this->forceAll) {
return null;
}
return $this->resolveStabileIdFromContext();
}
private function resolveStabileIdFromContext(): ?int
{
$user = Auth::user();
if ($user instanceof \App\Models\User) {
return StabileContext::resolveActiveStabileId($user);
}
return null;
}
private function resolveStabileLegacyCode(?int $stabileId): ?string
{
if (! $stabileId) {
return null;
}
return Stabile::where('id', $stabileId)->value('codice_stabile');
}
private function loadRubricaOptions(): void
{
$this->rubricaOptions = RubricaUniversale::query()
->orderBy('ragione_sociale')
->orderBy('cognome')
->get()
->mapWithKeys(function (RubricaUniversale $r) {
$label = trim(($r->nome_completo ?? $r->ragione_sociale ?? 'Anagrafica') . ' · ' . ($r->codice_fiscale ?? $r->partita_iva ?? ''));
return [$r->id => $label];
})
->all();
}
private function loadContoOptions(): void
{
$query = DatiBancari::query()->with('contatto');
$this->contoOptions = $query->orderBy('denominazione_banca')
->get()
->mapWithKeys(function (DatiBancari $c) {
$name = $c->contatto?->nome_completo ?? $c->intestazione_conto ?? 'Conto';
$label = trim(($c->denominazione_banca ?? 'Banca') . ' · ' . ($c->identificativo_conto ?? '') . ' · ' . $name);
return [$c->id => $label];
})
->all();
}
private function loadClausoleCatalogo(): void
{
$this->clausoleCatalogo = AffittoClausola::query()
->where('attivo', true)
->orderBy('ordine')
->orderBy('titolo')
->get()
->mapWithKeys(function (AffittoClausola $c) {
$label = trim(($c->titolo ?: 'Clausola') . ' · #' . $c->id);
return [$c->id => $label];
})
->all();
}
private function initContractTemplates(): void
{
$this->contractTemplates = [
'abitativo_431_2' => 'Contratto abitativo art.2 L.431/1998 (base)',
];
}
private function saveInquilini(AffittoImmobile $record): void
{
$keepIds = [];
foreach ($this->inquilini as $row) {
$nome = trim((string) ($row['nome_visualizzato'] ?? ''));
$rubricaId = (int) ($row['rubrica_inquilino_id'] ?? 0);
if ($nome === '' && $rubricaId <= 0) {
continue;
}
$payload = [
'rubrica_inquilino_id' => $rubricaId > 0 ? $rubricaId : null,
'nome_visualizzato' => $nome !== '' ? $nome : null,
'email' => trim((string) ($row['email'] ?? '')) ?: null,
'telefono' => trim((string) ($row['telefono'] ?? '')) ?: null,
'documenti_note' => trim((string) ($row['documenti_note'] ?? '')) ?: null,
'attivo' => (bool) ($row['attivo'] ?? true),
];
$id = (int) ($row['id'] ?? 0);
if ($id > 0) {
$entity = AffittoInquilino::query()->where('affitto_id', $record->id)->where('id', $id)->first();
if ($entity) {
$entity->fill($payload)->save();
$keepIds[] = $entity->id;
continue;
}
}
$entity = $record->inquilini()->create($payload);
$keepIds[] = $entity->id;
}
$record->inquilini()->whereNotIn('id', $keepIds ?: [0])->delete();
$primary = $record->inquilini()->where('attivo', true)->orderBy('id')->first();
if ($primary) {
$record->nome_inquilino = $primary->nome_visualizzato ?: ($primary->rubrica?->nome_completo ?? $record->nome_inquilino);
$record->rubrica_inquilino_id = $primary->rubrica_inquilino_id;
$record->save();
}
}
private function saveClausoleContratto(AffittoImmobile $record): void
{
$keepIds = [];
foreach ($this->clausoleContratto as $index => $row) {
$titolo = trim((string) ($row['titolo'] ?? ''));
$testo = trim((string) ($row['testo'] ?? ''));
$clausolaId = (int) ($row['clausola_id'] ?? 0);
if ($titolo === '' && $testo === '' && $clausolaId <= 0) {
continue;
}
$payload = [
'clausola_id' => $clausolaId > 0 ? $clausolaId : null,
'titolo' => $titolo !== '' ? $titolo : null,
'testo' => $testo !== '' ? $testo : null,
'periodicita' => in_array(($row['periodicita'] ?? null), ['mensile', 'annuale', 'una_tantum'], true) ? $row['periodicita'] : null,
'importo' => is_numeric($row['importo'] ?? null) ? (float) $row['importo'] : null,
'ordine' => is_numeric($row['ordine'] ?? null) ? (int) $row['ordine'] : ($index + 1),
'attivo' => (bool) ($row['attivo'] ?? true),
];
$id = (int) ($row['id'] ?? 0);
if ($id > 0) {
$entity = AffittoContrattoClausola::query()->where('affitto_id', $record->id)->where('id', $id)->first();
if ($entity) {
$entity->fill($payload)->save();
$keepIds[] = $entity->id;
continue;
}
}
$entity = $record->clausoleContratto()->create($payload);
$keepIds[] = $entity->id;
}
$record->clausoleContratto()->whereNotIn('id', $keepIds ?: [0])->delete();
}
private function buildCanoniRows(array $dovuti, array $pagati): array
{
$today = Carbon::today();
$rows = [];
foreach ($dovuti as $r) {
$key = ($r['anno'] ?? '0') . '-' . str_pad((string) ($r['mese'] ?? '0'), 2, '0', STR_PAD_LEFT);
if (! isset($rows[$key])) {
$rows[$key] = [
'anno' => $r['anno'] ?? null,
'mese' => $r['mese'] ?? null,
'mese_descrizione' => $r['mese_descrizione'] ?? null,
'totale_dovuto' => 0.0,
'totale_pagato' => 0.0,
'data_pagamento' => null,
'data_emissione' => null,
'data_scadenza' => null,
'saldo_progressivo' => 0.0,
'stato' => 'non_emesso',
];
}
$rows[$key]['totale_dovuto'] = (float) ($r['totale'] ?? 0);
$rows[$key]['data_emissione'] = $r['data_emissione'] ?? null;
$rows[$key]['data_scadenza'] = $r['data_scadenza'] ?? null;
if (empty($rows[$key]['mese_descrizione']) && ! empty($r['mese_descrizione'])) {
$rows[$key]['mese_descrizione'] = $r['mese_descrizione'];
}
}
foreach ($pagati as $r) {
$key = ($r['anno'] ?? '0') . '-' . str_pad((string) ($r['mese'] ?? '0'), 2, '0', STR_PAD_LEFT);
if (! isset($rows[$key])) {
$rows[$key] = [
'anno' => $r['anno'] ?? null,
'mese' => $r['mese'] ?? null,
'mese_descrizione' => $r['mese_descrizione'] ?? null,
'totale_dovuto' => 0.0,
'totale_pagato' => 0.0,
'data_pagamento' => null,
'data_emissione' => null,
'data_scadenza' => null,
'saldo_progressivo' => 0.0,
'stato' => 'non_emesso',
];
}
$rows[$key]['totale_pagato'] = (float) ($r['totale'] ?? 0);
$rows[$key]['data_pagamento'] = $r['data_pagamento'] ?? null;
if (empty($rows[$key]['mese_descrizione']) && ! empty($r['mese_descrizione'])) {
$rows[$key]['mese_descrizione'] = $r['mese_descrizione'];
}
}
ksort($rows);
$progressive = 0.0;
foreach ($rows as $key => $row) {
$delta = (float) ($row['totale_dovuto'] ?? 0) - (float) ($row['totale_pagato'] ?? 0);
$progressive += $delta;
$status = 'in_regola';
$scadenza = ! empty($row['data_scadenza']) ? Carbon::parse($row['data_scadenza']) : null;
if (($row['totale_dovuto'] ?? 0) <= 0) {
$status = 'non_emesso';
} elseif ($progressive > 0.0001 && $scadenza && $scadenza->lt($today)) {
$status = 'scaduto';
} elseif ($progressive > 0.0001) {
$status = 'da_pagare';
}
$rows[$key]['saldo_progressivo'] = $progressive;
$rows[$key]['stato'] = $status;
}
return array_values(array_reverse($rows));
}
private function emitCanoniAutomaticiForRecord(AffittoImmobile $record): int
{
if ((bool) ($record->obsoleto ?? false)) {
return 0;
}
$managementStart = $this->resolveCanoniManagementStart($record);
if (! $managementStart) {
return 0;
}
$start = $managementStart->copy()->startOfMonth();
$todayMonth = Carbon::today()->startOfMonth();
$end = $todayMonth;
if (! empty($record->prossima_scadenza)) {
$scadenza = Carbon::parse($record->prossima_scadenza)->startOfMonth();
if ($scadenza->lt($end)) {
$end = $scadenza;
}
}
if (! empty($record->data_fine_contratto)) {
$fineContratto = Carbon::parse($record->data_fine_contratto)->startOfMonth();
if ($fineContratto->lt($end)) {
$end = $fineContratto;
}
}
if ($end->lt($start)) {
return 0;
}
$existing = AffittoCanoneDovuto::query()
->where('affitto_id', $record->id)
->orderBy('anno')
->orderBy('mese')
->get()
->keyBy(fn(AffittoCanoneDovuto $r) => sprintf('%04d-%02d', (int) ($r->anno ?? 0), (int) ($r->mese ?? 0)));
$clausole = $record->clausoleContratto()
->where('attivo', true)
->orderBy('ordine')
->orderBy('id')
->get();
$istatService = app(IstatRivalutazioneService::class);
$istatSeries = strtoupper((string) config('services.istat.series', 'FOI'));
$annualBase = is_numeric($record->importo_fitto) ? (float) $record->importo_fitto : 0.0;
$currentBase = round($annualBase / 12, 2);
$meseAdempimenti = (int) ($record->mese_adempimenti_annuali ?? 1);
if ($meseAdempimenti < 1 || $meseAdempimenti > 12) {
$meseAdempimenti = 1;
}
$giornoAdempimenti = (int) ($record->giorno_adempimenti_annuali ?? 30);
if ($giornoAdempimenti < 1 || $giornoAdempimenti > 31) {
$giornoAdempimenti = 30;
}
$impostaRegistroAnnuale = is_numeric($record->imposta_registro_annuale) ? (float) $record->imposta_registro_annuale : 0.0;
$quotaImpostaInquilino = is_numeric($record->quota_imposta_inquilino_percent) ? (float) $record->quota_imposta_inquilino_percent : 50.0;
$quotaImpostaInquilino = max(0.0, min(100.0, $quotaImpostaInquilino));
$impostaBolloAnnuale = is_numeric($record->imposta_bollo_annuale) ? (float) $record->imposta_bollo_annuale : 0.0;
$created = 0;
for ($cursor = $start->copy(); $cursor->lte($end); $cursor->addMonth()) {
$key = $cursor->format('Y-m');
$existingRow = $existing->get($key);
if ($existingRow) {
$fittoExisting = is_numeric($existingRow->fitto) ? (float) $existingRow->fitto : $currentBase;
$istatExisting = is_numeric($existingRow->istat_importo) ? (float) $existingRow->istat_importo : 0.0;
$currentBase = $fittoExisting + max(0.0, $istatExisting);
continue;
}
$giornoEmissione = (int) ($record->giorno_emissione_canone ?? 5);
if ($giornoEmissione < 1 || $giornoEmissione > 28) {
$giornoEmissione = 5;
}
$emissione = Carbon::create((int) $cursor->year, (int) $cursor->month, min($giornoEmissione, $cursor->daysInMonth));
$scadenza = $emissione->copy();
$istatImporto = 0.0;
$istatPercentuale = null;
$extras = [];
foreach ($clausole as $clausola) {
$periodicita = $clausola->periodicita;
$applies = false;
if ($periodicita === 'mensile') {
$applies = true;
} elseif ($periodicita === 'annuale') {
$applies = (int) $cursor->month === (int) $start->month;
} elseif ($periodicita === 'una_tantum') {
$applies = $cursor->isSameMonth($start);
}
if (! $applies) {
continue;
}
$importo = is_numeric($clausola->importo) ? (float) $clausola->importo : 0.0;
$titolo = trim((string) ($clausola->titolo ?? ''));
if (Str::contains(Str::lower($titolo), 'istat')) {
if ($importo != 0.0) {
$istatImporto += $importo;
} else {
$adeguamento = $istatService->calcolaAdeguamentoAnnuale($annualBase, $cursor, $istatSeries);
if ($adeguamento) {
$quota = $this->resolveIstatQuotaFromClause($clausola);
$calcImporto = (float) ($adeguamento['importo_adeguamento'] ?? 0) / 12;
$calcPercent = (float) ($adeguamento['variazione_percentuale'] ?? 0);
if ($calcImporto > 0) {
$istatImporto += round($calcImporto * $quota, 2);
$istatPercentuale = round($calcPercent * $quota, 4);
}
}
}
} else {
if ($importo == 0.0) {
continue;
}
$extras[] = ['titolo' => $titolo !== '' ? $titolo : 'Voce contratto', 'importo' => $importo];
}
}
if ((int) $cursor->month === $meseAdempimenti) {
$registroQuotaInquilino = round($impostaRegistroAnnuale * ($quotaImpostaInquilino / 100), 2);
if ($registroQuotaInquilino > 0) {
$extras[] = [
'titolo' => 'Imposta registro annua (quota inquilino ' . rtrim(rtrim(number_format($quotaImpostaInquilino, 2, '.', ''), '0'), '.') . '%)',
'importo' => $registroQuotaInquilino,
];
}
if ($impostaBolloAnnuale > 0) {
$extras[] = [
'titolo' => 'Imposta di bollo annua',
'importo' => round($impostaBolloAnnuale, 2),
];
}
$scadenza = Carbon::create((int) $cursor->year, (int) $cursor->month, min($giornoAdempimenti, $cursor->daysInMonth));
}
$fitto = $currentBase;
$totale = $fitto + $istatImporto;
$payload = [
'affitto_id' => $record->id,
'anno' => (int) $cursor->year,
'mese' => (int) $cursor->month,
'mese_descrizione' => $cursor->locale('it')->translatedFormat('F'),
'fitto' => $fitto,
'istat_percentuale' => $istatPercentuale,
'istat_importo' => $istatImporto > 0 ? $istatImporto : null,
'bollo' => 0,
'totale' => 0,
'data_emissione' => $emissione->toDateString(),
'data_scadenza' => $scadenza->toDateString(),
'saldo_progressivo' => 0,
];
for ($i = 1; $i <= 4; $i++) {
$payload['descrizione_' . $i] = null;
$payload['importo_' . $i] = null;
}
foreach (array_values($extras) as $idx => $extra) {
if ($idx >= 4) {
break;
}
$col = $idx + 1;
$payload['descrizione_' . $col] = $extra['titolo'];
$payload['importo_' . $col] = $extra['importo'];
$totale += (float) $extra['importo'];
}
$payload['totale'] = $totale;
AffittoCanoneDovuto::create($payload);
$created++;
if ($istatImporto > 0) {
$annualBase += ($istatImporto * 12);
$currentBase += $istatImporto;
}
}
return $created;
}
private function resolveCanoniManagementStart(AffittoImmobile $record): ?Carbon
{
$dates = [];
if (! empty($record->inizio_contratto)) {
$dates[] = Carbon::parse($record->inizio_contratto)->startOfMonth();
}
if (! empty($record->presa_in_carico_operativa_dal)) {
$dates[] = Carbon::parse($record->presa_in_carico_operativa_dal)->startOfMonth();
}
if ($record->stabile && $record->stabile->latestAmministratoreTransfer?->created_at) {
$dates[] = $record->stabile->latestAmministratoreTransfer->created_at->copy()->startOfMonth();
}
if ($dates === []) {
return null;
}
usort($dates, fn(Carbon $left, Carbon $right): int => $left->getTimestamp() <=> $right->getTimestamp());
return $dates[count($dates) - 1];
}
private function resolveIstatQuotaFromClause(AffittoContrattoClausola $clausola): float
{
$content = trim((string) ($clausola->titolo ?? '') . ' ' . (string) ($clausola->testo ?? ''));
if ($content === '') {
return 1.0;
}
if (! preg_match('/(\d{1,3}(?:[\.,]\d{1,2})?)\s*%/', $content, $m)) {
return 1.0;
}
$raw = str_replace(',', '.', $m[1]);
if (! is_numeric($raw)) {
return 1.0;
}
$pct = (float) $raw;
if ($pct <= 0) {
return 1.0;
}
return min(1.0, $pct / 100);
}
private function deleteCanoniDuplicates(array $rows): int
{
$removed = 0;
$grouped = [];
foreach ($rows as $row) {
$key = implode('|', [
(int) ($row->affitto_id ?? 0),
(int) ($row->anno ?? 0),
(int) ($row->mese ?? 0),
number_format((float) ($row->totale ?? 0), 2, '.', ''),
]);
$grouped[$key][] = $row;
}
foreach ($grouped as $items) {
if (count($items) <= 1) {
continue;
}
usort($items, fn($a, $b) => (int) $a->id <=> (int) $b->id);
$toDelete = array_slice($items, 1);
foreach ($toDelete as $dup) {
$dup->delete();
$removed++;
}
}
return $removed;
}
private function ensureRicevute(AffittoImmobile $affitto, $dovuti): void
{
foreach ($dovuti as $row) {
if (! empty($row->n_ricevuta)) {
continue;
}
$row->n_ricevuta = $row->generateNumeroRicevuta();
$row->save();
}
}
}