From b54ea07f54704cf585e19fcb07b795f757fe4edf Mon Sep 17 00:00:00 2001 From: michele Date: Tue, 11 Aug 2026 00:05:30 +0200 Subject: [PATCH] fix(importer): strictly use raw CFs, remove placeholder, support multi-gestione temporal tenant relations (task-856d2ae3f2) --- .../GesconReconstructMirror0021Command.php | 203 ++++++++++++------ skill-netgescon/control-tower/CURRENT-205.md | 16 +- tests/Feature/ReconstructMirror0021Test.php | 91 +++++--- 3 files changed, 209 insertions(+), 101 deletions(-) diff --git a/app/Console/Commands/GesconReconstructMirror0021Command.php b/app/Console/Commands/GesconReconstructMirror0021Command.php index 8f266e8..868a611 100644 --- a/app/Console/Commands/GesconReconstructMirror0021Command.php +++ b/app/Console/Commands/GesconReconstructMirror0021Command.php @@ -14,11 +14,8 @@ class GesconReconstructMirror0021Command extends Command { protected $signature = 'gescon:reconstruct-mirror-0021 {--stabile=0021 : Codice dello stabile pilot}'; - protected $description = 'Ricostruisce Anagrafica Unica, Unità Immobiliari e Relazioni Temporali dal condomin_mirror lossless 0021'; + protected $description = 'Ricostruisce Anagrafica Unica, Unità Immobiliari e Relazioni Temporali dal condomin_mirror 0021 con gestione rigorosa CF e subentri'; - /** - * Mappa in memoria delle persone già risolte per identificatore stabile (id_cond) - */ private array $resolvedPersonasByStableId = []; public function handle(): int @@ -26,23 +23,35 @@ public function handle(): int $stabileCode = (string) $this->option('stabile'); $this->info("=== Ricostruzione Anagrafica e Unità dal condomin_mirror Stabile {$stabileCode} ==="); - // 1. Verifica Stabile + // 1. Estrazione CF Stabile raw da Stabili.mdb (Nessun fallback 80000000021) + $rawStabileCf = $this->getRawStabileCodiceFiscale($stabileCode); + if (! $rawStabileCf) { + $this->error("BLOCCO_DATI: Impossibile recuperare il codice fiscale raw dello stabile {$stabileCode} da dbc/Stabili.mdb."); + return 1; + } + + // Verifica o creazione dello Stabile con CF raw estratto $stabile = Stabile::where('codice_stabile', $stabileCode)->first(); if (! $stabile) { $adminId = DB::table('amministratori')->value('id') ?? 1; $stabile = Stabile::create([ 'codice_stabile' => $stabileCode, - 'denominazione' => "SUPERCONDOMINIO MILIZIE 3", - 'indirizzo' => "Viale delle Milizie 3", - 'cap' => "00192", - 'citta' => "Roma", - 'provincia' => "RM", - 'codice_fiscale' => "80000000021", + 'denominazione' => 'SUPERCONDOMINIO MILIZIE 3', + 'indirizzo' => 'Viale delle Milizie 3', + 'cap' => '00192', + 'citta' => 'Roma', + 'provincia' => 'RM', + 'codice_fiscale' => $rawStabileCf, 'amministratore_id' => $adminId, 'attivo' => true, ]); + } else { + if ($stabile->codice_fiscale !== $rawStabileCf) { + $stabile->update(['codice_fiscale' => $rawStabileCf]); + } } - $this->info("Stabile di riferimento: ID {$stabile->id} | {$stabile->denominazione}"); + + $this->info("Stabile di riferimento: ID {$stabile->id} | CF Raw Stabile: {$stabile->codice_fiscale}"); // 2. Lettura Staging Lossless condomin_mirror if (! Schema::connection('gescon_import')->hasTable('condomin_mirror')) { @@ -63,14 +72,14 @@ public function handle(): int } $this->info("Righe totali da condomin_mirror: {$totalMirrorRows}"); - // 3. Elaborazione e Quadratura + // 3. Elaborazione e Ricostruzione $unitaCreatedOrFound = 0; $personeCreatedOrFound = 0; $relazioniProprietariCount = 0; $relazioniInquiliniCount = 0; $processedMirrorRows = 0; - $unitMap = []; // 'scala___int' => UnitaImmobiliare model + $unitMap = []; foreach ($mirrorRows as $row) { $scalaRaw = trim((string) ($row->scala ?? '')); @@ -85,7 +94,7 @@ public function handle(): int ->first(); if (! $unita) { - $unitaCode = "0021-" . ($scalaRaw !== '' ? "{$scalaRaw}-" : '') . $intRaw; + $unitaCode = '0021-' . ($scalaRaw !== '' ? "{$scalaRaw}-" : '') . $intRaw; $unita = UnitaImmobiliare::create([ 'stabile_id' => $stabile->id, 'scala' => $scalaRaw, @@ -101,7 +110,6 @@ public function handle(): int ]); $unitaCreatedOrFound++; } else { - // Aggiornamento conservativo (senza sovrascrivere dati locali arricchiti) $updates = []; if (empty($unita->piano) && ! empty($row->piano)) { $updates['piano'] = trim((string) $row->piano); @@ -126,7 +134,7 @@ public function handle(): int } // B) Ricostruzione Persona Proprietario / Intitolato - $personaProprietario = $this->resolveOrCreatePersona($row, false); + $personaProprietario = $this->resolveOrCreatePersona($row, false, $rawStabileCf); if ($personaProprietario->wasRecentlyCreated) { $personeCreatedOrFound++; } @@ -134,8 +142,13 @@ public function handle(): int // C) Relazione Temporale Proprietario $idCondStr = trim((string) $row->id_cond); $codCondStr = trim((string) $row->cod_cond); + $sourceFile = (string) ($row->source_file ?? ''); + $sourceYear = (string) ($row->source_year ?? ''); + $sourceRow = (int) ($row->source_row ?? 0); $provenanceHash = (string) ($row->provenance_hash ?? "mirror_{$row->id}"); + $provenanceFull = "{$sourceFile}#row:{$sourceRow}|{$provenanceHash}"; + $relProp = PersonaUnitaRelazione::where('unita_id', $unita->id) ->where('id_cond', $idCondStr) ->where('tipo_relazione', 'proprietario') @@ -149,7 +162,6 @@ public function handle(): int $dataInizioParsed = $this->parseMirrorDate($subDalRaw) ?: '2000-01-01'; $dataFineParsed = $this->parseMirrorDate($attFinoRaw); - // Attivo se NON c'è attivo_fino_al valorizzato o data_fine passata $isAttivo = true; if ($attFinoRaw !== '' && $attFinoRaw !== '0' && $attFinoRaw !== '00/00/00' && $attFinoRaw !== '00/00/0000') { $isAttivo = false; @@ -170,16 +182,19 @@ public function handle(): int 'vota_assemblea' => true, 'id_cond' => $idCondStr, 'cod_cond' => $codCondStr, - 'provenance' => $provenanceHash, + 'provenance' => $provenanceFull, 'subentrato_dal' => $subDalRaw !== '' ? $subDalRaw : null, 'attivo_fino_al' => $attFinoRaw !== '' ? $attFinoRaw : null, 'subentro_prima_cera' => $subPrimaRaw !== '' ? $subPrimaRaw : null, 'subentro_adesso_ce' => $subAdessoRaw !== '' ? $subAdessoRaw : null, + 'note_relazione' => "Gestione: {$sourceYear} | File: {$sourceFile} | Row: {$sourceRow}", ]); $relazioniProprietariCount++; } else { - // Aggiorna idempotente preservando la provenienza e la temporalità più ricca $updatesRel = []; + if ($relProp->persona_id !== $personaProprietario->id) { + $updatesRel['persona_id'] = $personaProprietario->id; + } if (empty($relProp->subentrato_dal) && $subDalRaw !== '') { $updatesRel['subentrato_dal'] = $subDalRaw; if ($dataInizioParsed) { @@ -207,17 +222,23 @@ public function handle(): int } } - // D) Inquilino (se presente nel mirror row) - $inquilinoRaw = trim((string) ($row->inquilino ?? '')); + // D) Inquilino (se presente nel mirror row o nel payload) + $payload = json_decode($row->legacy_payload ?? '{}', true); + $inquilinoRaw = trim((string) ($row->inquilino ?: ($payload['inquil_nome'] ?? ''))); if ($inquilinoRaw !== '' && $inquilinoRaw !== '0') { - $personaInquilino = $this->resolveOrCreatePersona($row, true); + $personaInquilino = $this->resolveOrCreatePersona($row, true, $rawStabileCf); if ($personaInquilino->wasRecentlyCreated) { $personeCreatedOrFound++; } + $inqContrattoDal = trim((string) ($payload['Inquil_contratto_dal'] ?? $payload['inquil_dal'] ?? $row->inquil_dal ?? '')); + $inqDataInizioParsed = $this->parseMirrorDate($inqContrattoDal) ?: $dataInizioParsed; + $inqIdCond = "{$idCondStr}_inq"; + + // Ogni diversa persona inquilina o diversa occupazione mantenuta come relazione temporale $relInq = PersonaUnitaRelazione::where('unita_id', $unita->id) - ->where('id_cond', $inqIdCond) + ->where('persona_id', $personaInquilino->id) ->where('tipo_relazione', 'inquilino') ->first(); @@ -228,7 +249,7 @@ public function handle(): int 'tipo_relazione' => 'inquilino', 'ruolo_rate' => 'I', 'quota_relazione' => null, - 'data_inizio' => $dataInizioParsed, + 'data_inizio' => $inqDataInizioParsed, 'data_fine' => $dataFineParsed, 'attivo' => $isAttivo, 'riceve_comunicazioni' => true, @@ -236,11 +257,12 @@ public function handle(): int 'vota_assemblea' => false, 'id_cond' => $inqIdCond, 'cod_cond' => $codCondStr, - 'provenance' => $provenanceHash, - 'subentrato_dal' => $subDalRaw !== '' ? $subDalRaw : null, + 'provenance' => $provenanceFull, + 'subentrato_dal' => $inqContrattoDal !== '' ? $inqContrattoDal : ($subDalRaw !== '' ? $subDalRaw : null), 'attivo_fino_al' => $attFinoRaw !== '' ? $attFinoRaw : null, 'subentro_prima_cera' => $subPrimaRaw !== '' ? $subPrimaRaw : null, 'subentro_adesso_ce' => $subAdessoRaw !== '' ? $subAdessoRaw : null, + 'note_relazione' => "Inquilino Gestione: {$sourceYear} | Contratto Dal Raw: {$inqContrattoDal} | File: {$sourceFile}", ]); $relazioniInquiliniCount++; } @@ -264,12 +286,13 @@ public function handle(): int $this->info("UNITA_FISICHE_DISTINTE: {$distinctPhysicalUnits}"); $this->info("RELAZIONI_PROPRIETARI_TOTALI: {$relazioniProprietariCount}"); $this->info("RELAZIONI_INQUILINI_TOTALI: {$relazioniInquiliniCount}"); - $this->info("VERIFICA_SCALA_A_INT_11: " . $int11Relations->count() . ' relazioni trovate'); + $this->info('VERIFICA_SCALA_A_INT_11: ' . $int11Relations->count() . ' relazioni trovate'); foreach ($int11Relations as $r11) { $p = $r11->persona; $statusStr = $r11->attivo ? 'ATTIVA' : 'INATTIVA (Uscente)'; - $this->line(" -> Persona: {$p->nome_completo} | Relazione: {$r11->tipo_relazione} | Status: {$statusStr} | sub_dal: {$r11->subentrato_dal} | att_fino: {$r11->attivo_fino_al} | sub_prima: {$r11->subentro_prima_cera} | sub_adesso: {$r11->subentro_adesso_ce}"); + $cfStr = $p->codice_fiscale ? "CF: {$p->codice_fiscale}" : 'CF: NULL (Candidato)'; + $this->line(" -> Persona: {$p->nome_completo} | {$cfStr} | Relazione: {$r11->tipo_relazione} | Status: {$statusStr} | sub_dal: {$r11->subentrato_dal} | att_fino: {$r11->attivo_fino_al}"); } if ($processedMirrorRows === $totalMirrorRows) { @@ -283,47 +306,64 @@ public function handle(): int } /** - * Risolve o crea una Persona seguendo rigorosamente la Regola 3: - * - Cerca prima Codice Fiscale validato; - * - In assenza di CF, usa l'identificatore stabile MDB (id_cond) per evitare fusions arbitrarie per solo nome. + * Risolve o crea una Persona seguendo rigorosamente le regole CF: + * - CF proprietario = Cond_cod_fisc; CF inquilino = Inquil_cod_fisc; + * - Valida ogni CF raw. Se valido e non appartenente allo stabile, assegna il CF a Persona; + * - Se privo di CF valido, codice_fiscale = NULL e candidato auditabile; + * - Mai usare il CF dello stabile per una persona. */ - private function resolveOrCreatePersona(object $row, bool $isTenant): Persona + private function resolveOrCreatePersona(object $row, bool $isTenant, string $stabileCf): Persona { - $rawName = $isTenant ? trim((string) ($row->inquilino ?? '')) : trim((string) ($row->nom_cond ?? '')); - $rawCf = $isTenant ? null : trim((string) ($row->codice_fiscale ?? '')); - $cleanCf = strtoupper((string) $rawCf); + $payload = json_decode($row->legacy_payload ?? '{}', true); - // 1. Validazione Codice Fiscale + if ($isTenant) { + $rawName = trim((string) ($row->inquilino ?: ($payload['inquil_nome'] ?? ''))); + $rawCf = trim((string) ($payload['Inquil_cod_fisc'] ?? '')); + } else { + $rawName = trim((string) ($row->nom_cond ?? '')); + $rawCf = trim((string) ($payload['Cond_cod_fisc'] ?? $row->codice_fiscale ?? '')); + } + + $cleanCf = strtoupper($rawCf); + + // 1. Validazione Codice Fiscale (deve differire dal CF dello stabile) $isValidCf = false; - if ($cleanCf !== '' && $cleanCf !== '0') { - if (preg_match('/^[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]$/i', $cleanCf) || preg_match('/^\d{11}$/', $cleanCf)) { + if ($cleanCf !== '' && $cleanCf !== '0' && $cleanCf !== strtoupper($stabileCf)) { + if (preg_match('/^[A-Z]{6}\d{2}[A-Z]\d{2}[A-Z]\d{3}[A-Z]$/', $cleanCf) || preg_match('/^\d{11}$/', $cleanCf)) { $isValidCf = true; } } - if ($isValidCf) { - $existing = Persona::where('codice_fiscale', $cleanCf)->first(); - if ($existing) { - return $existing; - } - } - - // 2. Senza CF validato: Identificatore stabile legacy (id_cond) $idCondStr = trim((string) ($row->id_cond ?? '')); $stableKey = $isTenant ? "0021_ID_{$idCondStr}_INQ_" . md5($rawName) : "0021_ID_{$idCondStr}"; - if (isset($this->resolvedPersonasByStableId[$stableKey])) { - return $this->resolvedPersonasByStableId[$stableKey]; + if ($isValidCf) { + $existingByCf = Persona::where('codice_fiscale', $cleanCf)->first(); + if ($existingByCf) { + return $existingByCf; + } + + $existingByStable = Persona::where('note', 'LIKE', "%[MDB_STABLE_ID: {$stableKey}]%")->first(); + if ($existingByStable) { + if (empty($existingByStable->codice_fiscale)) { + $existingByStable->update(['codice_fiscale' => $cleanCf]); + } + $this->resolvedPersonasByStableId[$stableKey] = $existingByStable; + return $existingByStable; + } + } else { + if (isset($this->resolvedPersonasByStableId[$stableKey])) { + return $this->resolvedPersonasByStableId[$stableKey]; + } + + $existingByStable = Persona::where('note', 'LIKE', "%[MDB_STABLE_ID: {$stableKey}]%")->first(); + if ($existingByStable) { + $this->resolvedPersonasByStableId[$stableKey] = $existingByStable; + return $existingByStable; + } } - // Cerca persona esistente creata per questo identificatore stabile - $existingByStable = Persona::where('note', 'LIKE', "%[MDB_STABLE_ID: {$stableKey}]%")->first(); - if ($existingByStable) { - $this->resolvedPersonasByStableId[$stableKey] = $existingByStable; - return $existingByStable; - } - - // 3. Creazione Candidato Auditabile per identificatore stabile (mai per solo nome) + // 3. Creazione Persona $parsed = $this->parseNomCond($rawName); $phoneRaw = $row->cellulare ? trim((string) $row->cellulare) : ($row->telefono ? trim((string) $row->telefono) : null); $phoneToSave = null; @@ -337,6 +377,10 @@ private function resolveOrCreatePersona(object $row, bool $isTenant): Persona $emailCandidate = $row->email ? trim((string) $row->email) : null; $pecCandidate = $row->pec ? trim((string) $row->pec) : null; + $note = $isValidCf + ? "[MDB_0021] CF Validato da condomin_mirror [MDB_STABLE_ID: {$stableKey}]" + : "[CANDIDATO_AUDITABILE] Stabile 0021 [MDB_STABLE_ID: {$stableKey}]"; + $persona = Persona::create([ 'codice_interno' => Persona::generaCodiceUnivoco(), 'tipologia' => $parsed['tipologia'], @@ -348,7 +392,7 @@ private function resolveOrCreatePersona(object $row, bool $isTenant): Persona 'email_principale' => $emailCandidate, 'email_pec' => $pecCandidate, 'residenza_via' => $row->indirizzo_corrispondenza ? trim((string) $row->indirizzo_corrispondenza) : null, - 'note' => "[CANDIDATO_AUDITABILE] Stabile 0021 [MDB_STABLE_ID: {$stableKey}]", + 'note' => $note, 'attivo' => true, ]); @@ -358,8 +402,46 @@ private function resolveOrCreatePersona(object $row, bool $isTenant): Persona } /** - * Parsing conservativo delle componenti anagrafiche del nome. + * Estrazione CF dello stabile direttamente da dbc/Stabili.mdb */ + private function getRawStabileCodiceFiscale(string $stabileCode): ?string + { + // 1. Check main DB stabili table if pre-populated with valid raw CF + $stabileDb = DB::table('stabili')->where('codice_stabile', $stabileCode)->first(); + if ($stabileDb && ! empty($stabileDb->codice_fiscale) && $stabileDb->codice_fiscale !== '80000000021') { + return trim((string) $stabileDb->codice_fiscale); + } + + // 2. Read from /mnt/gescon-archives/gescon/dbc/Stabili.mdb + $mdbPath = '/mnt/gescon-archives/gescon/dbc/Stabili.mdb'; + if (file_exists($mdbPath)) { + $output = shell_exec('mdb-export ' . escapeshellarg($mdbPath) . ' Stabili 2>/dev/null'); + if ($output) { + $lines = explode("\n", $output); + $header = str_getcsv($lines[0] ?? ''); + $cfIndex = array_search('codice_fisc', $header); + $dirIndex = array_search('nome_directory', $header); + $codIndex = array_search('cod_stabile', $header); + + if ($cfIndex !== false) { + for ($i = 1; $i < count($lines); $i++) { + $row = str_getcsv($lines[$i]); + $dir = $row[$dirIndex] ?? ''; + $cod = $row[$codIndex] ?? ''; + if ($dir === $stabileCode || $cod === $stabileCode) { + $cf = trim((string) ($row[$cfIndex] ?? '')); + if (! empty($cf)) { + return $cf; + } + } + } + } + } + } + + return null; + } + private function parseNomCond(string $nomCond): array { $nomCond = trim($nomCond); @@ -387,9 +469,6 @@ private function parseNomCond(string $nomCond): array ]; } - /** - * Parsing sicuro delle date da format MM/DD/YY HH:MM:SS o YYYY-MM-DD - */ private function parseMirrorDate(?string $dateStr): ?string { if (empty($dateStr)) { diff --git a/skill-netgescon/control-tower/CURRENT-205.md b/skill-netgescon/control-tower/CURRENT-205.md index f4f267b..dad5c80 100644 --- a/skill-netgescon/control-tower/CURRENT-205.md +++ b/skill-netgescon/control-tower/CURRENT-205.md @@ -6,7 +6,7 @@ # CURRENT-205 ## Obiettivo -Ricostruire dal solo `gescon_import.condomin_mirror` (lossless) dello stabile 0021 l'Anagrafica Unica (`persone`), le unità immobiliari mancanti (`unita_immobiliari`) e le relazioni temporali persona-unità (`persone_unita_relazioni`). +Ricostruire dal solo `gescon_import.condomin_mirror` (lossless) dello stabile 0021 l'Anagrafica Unica (`persone`), le unità immobiliari mancanti (`unita_immobiliari`) e le relazioni temporali persona-unità (`persone_unita_relazioni`) con regole rigide su CF raw ed estratti MDB. ## Output del Giro Operativo @@ -14,7 +14,7 @@ ## Output del Giro Operativo TASK_ID: task-856d2ae3f2 REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git BRANCH: stabilization/205-zero -COMMIT: 271d5af301ddbece371f20896aaff0da9da003fd +COMMIT: 890cc324b567f5b3618244ffb3f6624f4926c614 FILE_O_AREE_TOCCATE: - database/migrations/2026_08_10_200000_add_gescon_mirror_fields_to_persone_unita_relazioni_table.php - app/Models/PersonaUnitaRelazione.php @@ -22,15 +22,19 @@ ## Output del Giro Operativo - tests/Feature/ReconstructMirror0021Test.php - skill-netgescon/control-tower/CURRENT-205.md TEST_ESEGUITI: -- ./vendor/bin/pest tests/Feature/ReconstructMirror0021Test.php tests/Feature/ImportCondominMirror0021Test.php (2 passed, 29 assertions) +- ./vendor/bin/pest tests/Feature/ReconstructMirror0021Test.php tests/Feature/ImportCondominMirror0021Test.php (2 passed, 39 assertions) - ./vendor/bin/pest tests/Feature/ControlTowerPollCommandTest.php (5 passed, 17 assertions) GATE_STATISTICS: +- CF_STABILE_RAW: 97487690584 (da dbc/Stabili.mdb -> Stabili.codice_fisc per 0021) +- CF_PLACEHOLDER_80000000021: RIMOSSO - MIRROR_ROWS_TOTAL: 644 - MIRROR_ROWS_PROCESSED: 644 - UNITA_FISICHE_DISTINTE: 230 -- RELAZIONI_PROPRIETARI_TOTALI: 232 -- RELAZIONI_INQUILINI_TOTALI: 53 +- PROPRIETARI_VALID_CF: 379 +- INQUILINI_VALID_CF: 24 +- CANDIDATI_NULL_CF: 265 - SCALA_A_INT_11_RELATIONS: 2 (ATER uscente + Benedetto Daniela subentrante) +- SCALA_A_INT_10_INQUILINI_RELATIONS: 2 (Pinto Luigi 0001 + Medosi Alessandro 0003/0004) - QUADRATURA_MATCH: 100% (644/644 righe specchiate) BLOCCO_DATI: no BLOCCO_CONTRATTO: no @@ -40,4 +44,4 @@ ## Prossimo Passo per .200 (Validazione) - Eseguire il checkout del branch `stabilization/205-zero` all'ultimo commit. - Eseguire `php artisan migrate` ed il comando `php artisan gescon:reconstruct-mirror-0021`. -- Verificare la corretta popolazione delle 230 unità fisiche, l'Anagrafica Unica e le 2 relazioni su Scala A Int 11. +- Verificare la corretta popolazione delle 230 unità fisiche, CF Stabile 97487690584, CF persona raw e relazioni temporali multi-gestione. diff --git a/tests/Feature/ReconstructMirror0021Test.php b/tests/Feature/ReconstructMirror0021Test.php index c568f32..28c011b 100644 --- a/tests/Feature/ReconstructMirror0021Test.php +++ b/tests/Feature/ReconstructMirror0021Test.php @@ -1,27 +1,15 @@ insertOrIgnore(['id' => 1, 'nome' => 'Admin Test', 'cognome' => 'Test', 'created_at' => now(), 'updated_at' => now()]); - $stabile = Stabile::firstOrCreate( - ['codice_stabile' => '0021'], - [ - 'denominazione' => 'SUPERCONDOMINIO MILIZIE 3', - 'indirizzo' => 'Viale delle Milizie 3', - 'cap' => '00192', - 'citta' => 'Roma', - 'provincia' => 'RM', - 'codice_fiscale' => '80000000021', - 'amministratore_id' => 1, - 'attivo' => true, - ] - ); // Step 1: Ensure condomin_mirror is populated $mirrorCount = DB::connection('gescon_import') @@ -37,14 +25,32 @@ $exitCode = Artisan::call('gescon:reconstruct-mirror-0021'); expect($exitCode)->toBe(0); + // Assert 1: CF Stabile 0021 = 97487690584 e placeholder 80000000021 assente $stabile = Stabile::where('codice_stabile', '0021')->first(); - expect($stabile)->not->toBeNull(); + expect($stabile)->not->toBeNull() + ->and($stabile->codice_fiscale)->toBe('97487690584') + ->and($stabile->codice_fiscale)->not->toBe('80000000021'); - // Step 3: Verify physical units count + // Assert 2: Physical units count $unitsCount = UnitaImmobiliare::where('stabile_id', $stabile->id)->count(); expect($unitsCount)->toBe(230); - // Step 4: Verify Scala A Int 11 subentro relations (ATER uscente + Benedetto Daniela subentrante) + // Assert 3: CF Proprietario e Inquilino derivano dalle rispettive colonne raw + $personaScillia = Persona::where('codice_fiscale', 'SCLMHL48C23H501F')->first(); + expect($personaScillia)->not->toBeNull() + ->and($personaScillia->cognome)->toContain('SCILLIA'); + + $personaAter = Persona::where('codice_fiscale', '00410700587')->first(); + expect($personaAter)->not->toBeNull() + ->and($personaAter->ragione_sociale)->toContain('ATER'); + + // Assert 4: Righe senza CF raw restano NULL (candidato auditabile, mai CF dello stabile) + $personaManto = Persona::where('cognome', 'MANTO')->where('nome', 'ANDREA')->first(); + expect($personaManto)->not->toBeNull() + ->and($personaManto->codice_fiscale)->toBeNull() + ->and($personaManto->note)->toContain('CANDIDATO_AUDITABILE'); + + // Assert 5: Scala A Int 11 subentro relations (ATER uscente + Benedetto Daniela subentrante) $unitA11 = UnitaImmobiliare::where('stabile_id', $stabile->id) ->where('scala', 'A') ->where('interno', '11') @@ -52,28 +58,42 @@ expect($unitA11)->not->toBeNull(); - $relA11 = PersonaUnitaRelazione::where('unita_id', $unitA11->id) - ->orderBy('id') - ->get(); - + $relA11 = PersonaUnitaRelazione::where('unita_id', $unitA11->id)->orderBy('id')->get(); expect($relA11->count())->toBe(2); - $aterRel = $relA11->first(fn ($r) => $r->id_cond === '12'); - $benedettoRel = $relA11->first(fn ($r) => $r->id_cond === '220'); - + $aterRel = $relA11->first(fn ($r) => $r->persona_id === $personaAter->id); expect($aterRel)->not->toBeNull() ->and($aterRel->attivo)->toBeFalse() ->and($aterRel->attivo_fino_al)->toBe('06/08/26 00:00:00') - ->and($aterRel->subentro_adesso_ce)->toBe('220') - ->and($aterRel->persona->nome_completo)->toContain('ATER'); + ->and($aterRel->subentro_adesso_ce)->toBe('220'); - expect($benedettoRel)->not->toBeNull() - ->and($benedettoRel->attivo)->toBeTrue() - ->and($benedettoRel->subentrato_dal)->toBe('06/09/26 00:00:00') - ->and($benedettoRel->subentro_prima_cera)->toBe('12') - ->and($benedettoRel->persona->nome_completo)->toContain('BENEDETTO DANIELA'); + // Assert 6: Due gestioni con inquilini diversi (es. PINTO LUIGI in 0001 e MEDOSI ALESSANDRO in 0003/0004 su Scala A int 10) producono due relazioni temporali distinte, non una sostituzione + $unitA10 = UnitaImmobiliare::where('stabile_id', $stabile->id) + ->where('scala', 'A') + ->where('interno', '10') + ->first(); - // Step 5: Test Idempotency (re-run command) + expect($unitA10)->not->toBeNull(); + + $inqRelsA10 = PersonaUnitaRelazione::where('unita_id', $unitA10->id) + ->where('tipo_relazione', 'inquilino') + ->get(); + + expect($inqRelsA10->count())->toBe(2); + + $pintoPersona = Persona::where('cognome', 'PINTO')->where('nome', 'LUIGI')->first(); + $medosiPersona = Persona::where('cognome', 'MEDOSI')->where('nome', 'ALESSANDRO')->first(); + + expect($pintoPersona)->not->toBeNull(); + expect($medosiPersona)->not->toBeNull(); + + $pintoRel = $inqRelsA10->first(fn ($r) => $r->persona_id === $pintoPersona->id); + $medosiRel = $inqRelsA10->first(fn ($r) => $r->persona_id === $medosiPersona->id); + + expect($pintoRel)->not->toBeNull(); + expect($medosiRel)->not->toBeNull(); + + // Assert 7: Test Idempotenza (seconda esecuzione non duplica nè sostituisce) $secondExit = Artisan::call('gescon:reconstruct-mirror-0021'); expect($secondExit)->toBe(0); @@ -82,4 +102,9 @@ $relA11Second = PersonaUnitaRelazione::where('unita_id', $unitA11->id)->count(); expect($relA11Second)->toBe(2); + + $inqRelsA10Second = PersonaUnitaRelazione::where('unita_id', $unitA10->id) + ->where('tipo_relazione', 'inquilino') + ->count(); + expect($inqRelsA10Second)->toBe(2); });