From f60898aae63a85d0e2198050720d80afd48cd215 Mon Sep 17 00:00:00 2001 From: michele Date: Mon, 10 Aug 2026 23:45:19 +0200 Subject: [PATCH] =?UTF-8?q?feat(importer):=20reconstruct=20Anagrafica=20Un?= =?UTF-8?q?ica,=20Unit=C3=A0=20and=20Relazioni=20for=200021=20from=20condo?= =?UTF-8?q?min=5Fmirror=20(task-856d2ae3f2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GesconReconstructMirror0021Command.php | 428 ++++++++++++++++++ app/Models/PersonaUnitaRelazione.php | 7 + ...ields_to_persone_unita_relazioni_table.php | 58 +++ skill-netgescon/control-tower/CURRENT-205.md | 38 +- tests/Feature/ReconstructMirror0021Test.php | 85 ++++ 5 files changed, 598 insertions(+), 18 deletions(-) create mode 100644 app/Console/Commands/GesconReconstructMirror0021Command.php create mode 100644 database/migrations/2026_08_10_200000_add_gescon_mirror_fields_to_persone_unita_relazioni_table.php create mode 100644 tests/Feature/ReconstructMirror0021Test.php diff --git a/app/Console/Commands/GesconReconstructMirror0021Command.php b/app/Console/Commands/GesconReconstructMirror0021Command.php new file mode 100644 index 0000000..8f266e8 --- /dev/null +++ b/app/Console/Commands/GesconReconstructMirror0021Command.php @@ -0,0 +1,428 @@ +option('stabile'); + $this->info("=== Ricostruzione Anagrafica e Unità dal condomin_mirror Stabile {$stabileCode} ==="); + + // 1. Verifica Stabile + $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", + 'amministratore_id' => $adminId, + 'attivo' => true, + ]); + } + $this->info("Stabile di riferimento: ID {$stabile->id} | {$stabile->denominazione}"); + + // 2. Lettura Staging Lossless condomin_mirror + if (! Schema::connection('gescon_import')->hasTable('condomin_mirror')) { + $this->error("BLOCCO_DATI: Tabella gescon_import.condomin_mirror non esistente."); + return 1; + } + + $mirrorRows = DB::connection('gescon_import') + ->table('condomin_mirror') + ->where('cod_stabile', $stabileCode) + ->orderBy('id') + ->get(); + + $totalMirrorRows = $mirrorRows->count(); + if ($totalMirrorRows === 0) { + $this->error("BLOCCO_DATI: Nessuna riga presente in condomin_mirror per lo stabile {$stabileCode}."); + return 1; + } + $this->info("Righe totali da condomin_mirror: {$totalMirrorRows}"); + + // 3. Elaborazione e Quadratura + $unitaCreatedOrFound = 0; + $personeCreatedOrFound = 0; + $relazioniProprietariCount = 0; + $relazioniInquiliniCount = 0; + $processedMirrorRows = 0; + + $unitMap = []; // 'scala___int' => UnitaImmobiliare model + + foreach ($mirrorRows as $row) { + $scalaRaw = trim((string) ($row->scala ?? '')); + $intRaw = trim((string) ($row->interno ?? '')); + $unitKey = "{$scalaRaw}___{$intRaw}"; + + // A) Ricostruzione Unità Fisica: stabile_id + scala + int raw + if (! isset($unitMap[$unitKey])) { + $unita = UnitaImmobiliare::where('stabile_id', $stabile->id) + ->where('scala', $scalaRaw) + ->where('interno', $intRaw) + ->first(); + + if (! $unita) { + $unitaCode = "0021-" . ($scalaRaw !== '' ? "{$scalaRaw}-" : '') . $intRaw; + $unita = UnitaImmobiliare::create([ + 'stabile_id' => $stabile->id, + 'scala' => $scalaRaw, + 'interno' => $intRaw, + 'codice_unita' => $unitaCode, + 'denominazione' => "Stabile 0021 Scala {$scalaRaw} Int {$intRaw}", + 'piano' => $row->piano ? trim((string) $row->piano) : null, + 'millesimi_generali' => $row->millesimi_proprieta ?: null, + 'millesimi_proprieta' => $row->millesimi_proprieta ?: null, + 'millesimi_riscaldamento' => $row->millesimi_riscaldamento ?: null, + 'millesimi_ascensore' => $row->millesimi_ascensore ?: null, + 'stato' => 'attiva', + ]); + $unitaCreatedOrFound++; + } else { + // Aggiornamento conservativo (senza sovrascrivere dati locali arricchiti) + $updates = []; + if (empty($unita->piano) && ! empty($row->piano)) { + $updates['piano'] = trim((string) $row->piano); + } + if (is_null($unita->millesimi_generali) && ! is_null($row->millesimi_proprieta)) { + $updates['millesimi_generali'] = $row->millesimi_proprieta; + } + if (is_null($unita->millesimi_riscaldamento) && ! is_null($row->millesimi_riscaldamento)) { + $updates['millesimi_riscaldamento'] = $row->millesimi_riscaldamento; + } + if (is_null($unita->millesimi_ascensore) && ! is_null($row->millesimi_ascensore)) { + $updates['millesimi_ascensore'] = $row->millesimi_ascensore; + } + if (! empty($updates)) { + $unita->update($updates); + } + $unitaCreatedOrFound++; + } + $unitMap[$unitKey] = $unita; + } else { + $unita = $unitMap[$unitKey]; + } + + // B) Ricostruzione Persona Proprietario / Intitolato + $personaProprietario = $this->resolveOrCreatePersona($row, false); + if ($personaProprietario->wasRecentlyCreated) { + $personeCreatedOrFound++; + } + + // C) Relazione Temporale Proprietario + $idCondStr = trim((string) $row->id_cond); + $codCondStr = trim((string) $row->cod_cond); + $provenanceHash = (string) ($row->provenance_hash ?? "mirror_{$row->id}"); + + $relProp = PersonaUnitaRelazione::where('unita_id', $unita->id) + ->where('id_cond', $idCondStr) + ->where('tipo_relazione', 'proprietario') + ->first(); + + $subDalRaw = trim((string) ($row->subentrato_dal ?? '')); + $attFinoRaw = trim((string) ($row->attivo_fino_al ?? '')); + $subPrimaRaw = trim((string) ($row->subentro_prima_cera ?? '')); + $subAdessoRaw = trim((string) ($row->subentro_adesso_ce ?? '')); + + $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; + } + + if (! $relProp) { + $relProp = PersonaUnitaRelazione::create([ + 'persona_id' => $personaProprietario->id, + 'unita_id' => $unita->id, + 'tipo_relazione' => 'proprietario', + 'ruolo_rate' => 'C', + 'quota_relazione' => $row->millesimi_proprieta ?: 100.00, + 'data_inizio' => $dataInizioParsed, + 'data_fine' => $dataFineParsed, + 'attivo' => $isAttivo, + 'riceve_comunicazioni' => true, + 'riceve_convocazioni' => true, + 'vota_assemblea' => true, + 'id_cond' => $idCondStr, + 'cod_cond' => $codCondStr, + 'provenance' => $provenanceHash, + 'subentrato_dal' => $subDalRaw !== '' ? $subDalRaw : null, + 'attivo_fino_al' => $attFinoRaw !== '' ? $attFinoRaw : null, + 'subentro_prima_cera' => $subPrimaRaw !== '' ? $subPrimaRaw : null, + 'subentro_adesso_ce' => $subAdessoRaw !== '' ? $subAdessoRaw : null, + ]); + $relazioniProprietariCount++; + } else { + // Aggiorna idempotente preservando la provenienza e la temporalità più ricca + $updatesRel = []; + if (empty($relProp->subentrato_dal) && $subDalRaw !== '') { + $updatesRel['subentrato_dal'] = $subDalRaw; + if ($dataInizioParsed) { + $updatesRel['data_inizio'] = $dataInizioParsed; + } + } + if (empty($relProp->attivo_fino_al) && $attFinoRaw !== '') { + $updatesRel['attivo_fino_al'] = $attFinoRaw; + $updatesRel['attivo'] = false; + if ($dataFineParsed) { + $updatesRel['data_fine'] = $dataFineParsed; + } + } + if (empty($relProp->subentro_prima_cera) && $subPrimaRaw !== '') { + $updatesRel['subentro_prima_cera'] = $subPrimaRaw; + } + if (empty($relProp->subentro_adesso_ce) && $subAdessoRaw !== '') { + $updatesRel['subentro_adesso_ce'] = $subAdessoRaw; + } + if (! empty($codCondStr) && empty($relProp->cod_cond)) { + $updatesRel['cod_cond'] = $codCondStr; + } + if (! empty($updatesRel)) { + $relProp->update($updatesRel); + } + } + + // D) Inquilino (se presente nel mirror row) + $inquilinoRaw = trim((string) ($row->inquilino ?? '')); + if ($inquilinoRaw !== '' && $inquilinoRaw !== '0') { + $personaInquilino = $this->resolveOrCreatePersona($row, true); + if ($personaInquilino->wasRecentlyCreated) { + $personeCreatedOrFound++; + } + + $inqIdCond = "{$idCondStr}_inq"; + $relInq = PersonaUnitaRelazione::where('unita_id', $unita->id) + ->where('id_cond', $inqIdCond) + ->where('tipo_relazione', 'inquilino') + ->first(); + + if (! $relInq) { + $relInq = PersonaUnitaRelazione::create([ + 'persona_id' => $personaInquilino->id, + 'unita_id' => $unita->id, + 'tipo_relazione' => 'inquilino', + 'ruolo_rate' => 'I', + 'quota_relazione' => null, + 'data_inizio' => $dataInizioParsed, + 'data_fine' => $dataFineParsed, + 'attivo' => $isAttivo, + 'riceve_comunicazioni' => true, + 'riceve_convocazioni' => false, + 'vota_assemblea' => false, + 'id_cond' => $inqIdCond, + 'cod_cond' => $codCondStr, + 'provenance' => $provenanceHash, + 'subentrato_dal' => $subDalRaw !== '' ? $subDalRaw : null, + 'attivo_fino_al' => $attFinoRaw !== '' ? $attFinoRaw : null, + 'subentro_prima_cera' => $subPrimaRaw !== '' ? $subPrimaRaw : null, + 'subentro_adesso_ce' => $subAdessoRaw !== '' ? $subAdessoRaw : null, + ]); + $relazioniInquiliniCount++; + } + } + + $processedMirrorRows++; + } + + // 4. Stampa Quadratura Completa + $distinctPhysicalUnits = UnitaImmobiliare::where('stabile_id', $stabile->id)->count(); + + $int11Relations = PersonaUnitaRelazione::whereHas('unitaImmobiliare', function ($q) use ($stabile) { + $q->where('stabile_id', $stabile->id) + ->where('scala', 'A') + ->where('interno', '11'); + })->get(); + + $this->info('=== ESITO QUADRATURA 0021 ==='); + $this->info("RIGHE_MIRROR_TOTALI: {$totalMirrorRows}"); + $this->info("RIGHE_ELABORATE: {$processedMirrorRows}"); + $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'); + + 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}"); + } + + if ($processedMirrorRows === $totalMirrorRows) { + $this->info("QUADRATURA_ESITO: PERFETTA (100% delle {$totalMirrorRows} righe specchiate)"); + } else { + $this->error("QUADRATURA_ESITO: DISCREPANZA ({$processedMirrorRows}/{$totalMirrorRows} righe)"); + return 1; + } + + return 0; + } + + /** + * 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. + */ + private function resolveOrCreatePersona(object $row, bool $isTenant): Persona + { + $rawName = $isTenant ? trim((string) ($row->inquilino ?? '')) : trim((string) ($row->nom_cond ?? '')); + $rawCf = $isTenant ? null : trim((string) ($row->codice_fiscale ?? '')); + $cleanCf = strtoupper((string) $rawCf); + + // 1. Validazione Codice Fiscale + $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)) { + $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]; + } + + // 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) + $parsed = $this->parseNomCond($rawName); + $phoneRaw = $row->cellulare ? trim((string) $row->cellulare) : ($row->telefono ? trim((string) $row->telefono) : null); + $phoneToSave = null; + if ($phoneRaw) { + $cleanPhone = preg_replace('/[^\d+]/', '', $phoneRaw); + if ($cleanPhone !== '' && ! Persona::where('telefono_principale', $cleanPhone)->exists()) { + $phoneToSave = $cleanPhone; + } + } + + $emailCandidate = $row->email ? trim((string) $row->email) : null; + $pecCandidate = $row->pec ? trim((string) $row->pec) : null; + + $persona = Persona::create([ + 'codice_interno' => Persona::generaCodiceUnivoco(), + 'tipologia' => $parsed['tipologia'], + 'cognome' => $parsed['cognome'], + 'nome' => $parsed['nome'], + 'ragione_sociale' => $parsed['ragione_sociale'], + 'codice_fiscale' => $isValidCf ? $cleanCf : null, + 'telefono_principale' => $phoneToSave, + '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}]", + 'attivo' => true, + ]); + + $this->resolvedPersonasByStableId[$stableKey] = $persona; + + return $persona; + } + + /** + * Parsing conservativo delle componenti anagrafiche del nome. + */ + private function parseNomCond(string $nomCond): array + { + $nomCond = trim($nomCond); + $upper = mb_strtoupper($nomCond); + $companyKeywords = ['ATER', 'SRL', 'S.R.L.', 'SPA', 'S.P.A.', 'SAS', 'S.A.S.', 'SNC', 'S.N.C.', 'CONDOMINI', 'EREDI', 'C/O', 'SOC.', 'SOCIETA', 'SOCIETÀ', 'DOMUS']; + + foreach ($companyKeywords as $kw) { + if (str_contains($upper, $kw)) { + return [ + 'tipologia' => 'giuridica', + 'cognome' => $nomCond, + 'nome' => '', + 'ragione_sociale' => $nomCond, + ]; + } + } + + $parts = preg_split('/\s+/', $nomCond, 2); + + return [ + 'tipologia' => 'fisica', + 'cognome' => $parts[0] ?? $nomCond, + 'nome' => $parts[1] ?? '', + 'ragione_sociale' => null, + ]; + } + + /** + * 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)) { + return null; + } + $dateStr = trim($dateStr); + if ($dateStr === '0' || $dateStr === '00/00/00' || $dateStr === '00/00/0000') { + return null; + } + + try { + $parts = explode(' ', $dateStr); + $datePart = $parts[0]; + if (preg_match('/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})$/', $datePart, $m)) { + $month = (int) $m[1]; + $day = (int) $m[2]; + $year = (int) $m[3]; + if ($year < 100) { + $year += ($year > 50 ? 1900 : 2000); + } + if ($month > 12 && $day <= 12) { + $tmp = $month; + $month = $day; + $day = $tmp; + } + + return sprintf('%04d-%02d-%02d', $year, $month, $day); + } + $dt = new \DateTime($dateStr); + + return $dt->format('Y-m-d'); + } catch (\Throwable $e) { + return null; + } + } +} diff --git a/app/Models/PersonaUnitaRelazione.php b/app/Models/PersonaUnitaRelazione.php index f63bc44..553b034 100755 --- a/app/Models/PersonaUnitaRelazione.php +++ b/app/Models/PersonaUnitaRelazione.php @@ -24,6 +24,13 @@ class PersonaUnitaRelazione extends Model 'riceve_convocazioni', 'vota_assemblea', 'note_relazione', + 'id_cond', + 'cod_cond', + 'provenance', + 'subentrato_dal', + 'attivo_fino_al', + 'subentro_prima_cera', + 'subentro_adesso_ce', ]; protected $casts = [ diff --git a/database/migrations/2026_08_10_200000_add_gescon_mirror_fields_to_persone_unita_relazioni_table.php b/database/migrations/2026_08_10_200000_add_gescon_mirror_fields_to_persone_unita_relazioni_table.php new file mode 100644 index 0000000..3c574c8 --- /dev/null +++ b/database/migrations/2026_08_10_200000_add_gescon_mirror_fields_to_persone_unita_relazioni_table.php @@ -0,0 +1,58 @@ +string('id_cond', 50)->nullable()->index(); + } + if (! Schema::hasColumn('persone_unita_relazioni', 'cod_cond')) { + $table->string('cod_cond', 50)->nullable()->index(); + } + if (! Schema::hasColumn('persone_unita_relazioni', 'provenance')) { + $table->string('provenance', 255)->nullable(); + } + if (! Schema::hasColumn('persone_unita_relazioni', 'subentrato_dal')) { + $table->string('subentrato_dal', 50)->nullable(); + } + if (! Schema::hasColumn('persone_unita_relazioni', 'attivo_fino_al')) { + $table->string('attivo_fino_al', 50)->nullable(); + } + if (! Schema::hasColumn('persone_unita_relazioni', 'subentro_prima_cera')) { + $table->string('subentro_prima_cera', 255)->nullable(); + } + if (! Schema::hasColumn('persone_unita_relazioni', 'subentro_adesso_ce')) { + $table->string('subentro_adesso_ce', 255)->nullable(); + } + }); + } + + public function down(): void + { + if (! Schema::hasTable('persone_unita_relazioni')) { + return; + } + + Schema::table('persone_unita_relazioni', function (Blueprint $table) { + $columnsToDrop = []; + foreach (['id_cond', 'cod_cond', 'provenance', 'subentrato_dal', 'attivo_fino_al', 'subentro_prima_cera', 'subentro_adesso_ce'] as $col) { + if (Schema::hasColumn('persone_unita_relazioni', $col)) { + $columnsToDrop[] = $col; + } + } + if (! empty($columnsToDrop)) { + $table->dropColumn($columnsToDrop); + } + }); + } +}; diff --git a/skill-netgescon/control-tower/CURRENT-205.md b/skill-netgescon/control-tower/CURRENT-205.md index 05185bf..f4f267b 100644 --- a/skill-netgescon/control-tower/CURRENT-205.md +++ b/skill-netgescon/control-tower/CURRENT-205.md @@ -1,41 +1,43 @@ # CURRENT-205 -TASK_ID: task-25d2e33f8d +TASK_ID: task-856d2ae3f2 MACHINE: .205 STATO: completato ## Obiettivo -Creare una nuova tabella staging mirror lossless (`gescon_import.condomin_mirror`) e il relativo comando Artisan dedicato (`gescon:import-mirror-0021`) per lo stabile pilot 0021 in modalità append-only senza deduplica né sovrascritture degli esercizi storici. +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`). ## Output del Giro Operativo ESITO_205: riuscito -TASK_ID: task-25d2e33f8d +TASK_ID: task-856d2ae3f2 REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git BRANCH: stabilization/205-zero -COMMIT: 4c99f5e87caeaba77a3b54210071d6e195a55b2f +COMMIT: 271d5af301ddbece371f20896aaff0da9da003fd FILE_O_AREE_TOCCATE: -- database/migrations/2026_08_10_100000_create_gescon_import_condomin_mirror_table.php -- app/Console/Commands/ImportCondominMirror0021.php -- tests/Feature/ImportCondominMirror0021Test.php -- scripts/ops/antigravity-cli/run_205_followup_via_agy.sh +- database/migrations/2026_08_10_200000_add_gescon_mirror_fields_to_persone_unita_relazioni_table.php +- app/Models/PersonaUnitaRelazione.php +- app/Console/Commands/GesconReconstructMirror0021Command.php +- tests/Feature/ReconstructMirror0021Test.php +- skill-netgescon/control-tower/CURRENT-205.md TEST_ESEGUITI: -- ./vendor/bin/pest tests/Feature/ImportCondominMirror0021Test.php (1 passed, 11 assertions) +- ./vendor/bin/pest tests/Feature/ReconstructMirror0021Test.php tests/Feature/ImportCondominMirror0021Test.php (2 passed, 29 assertions) - ./vendor/bin/pest tests/Feature/ControlTowerPollCommandTest.php (5 passed, 17 assertions) -- ./scripts/ops/antigravity-cli/test_runner_and_parser.sh (6/6 passed) GATE_STATISTICS: -- MDB_ROWS_TOTAL: 644 -- STAGING_ROWS: 644 -- COLLISION_COUNT: 0 -- DUPLICATE_PROVENANCE_COUNT: 0 -- SECOND_RUN_COUNTS: 644 +- MIRROR_ROWS_TOTAL: 644 +- MIRROR_ROWS_PROCESSED: 644 +- UNITA_FISICHE_DISTINTE: 230 +- RELAZIONI_PROPRIETARI_TOTALI: 232 +- RELAZIONI_INQUILINI_TOTALI: 53 +- SCALA_A_INT_11_RELATIONS: 2 (ATER uscente + Benedetto Daniela subentrante) +- QUADRATURA_MATCH: 100% (644/644 righe specchiate) BLOCCO_DATI: no BLOCCO_CONTRATTO: no RISCHI_APERTI: nessuno ## Prossimo Passo per .200 (Validazione) -- Eseguire il checkout del branch `stabilization/205-zero` al commit `4c99f5e87caeaba77a3b54210071d6e195a55b2f`. -- Eseguire `php artisan migrate` ed il comando `php artisan gescon:import-mirror-0021`. -- Verificare la corretta popolazione di `gescon_import.condomin_mirror` (644 righe totali da 0001, 0003, 0004) e la conservazione del campo `interno` originario. +- 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. diff --git a/tests/Feature/ReconstructMirror0021Test.php b/tests/Feature/ReconstructMirror0021Test.php new file mode 100644 index 0000000..c568f32 --- /dev/null +++ b/tests/Feature/ReconstructMirror0021Test.php @@ -0,0 +1,85 @@ +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') + ->table('condomin_mirror') + ->where('cod_stabile', '0021') + ->count(); + + if ($mirrorCount === 0) { + Artisan::call('gescon:import-mirror-0021'); + } + + // Step 2: Run reconstruction command + $exitCode = Artisan::call('gescon:reconstruct-mirror-0021'); + expect($exitCode)->toBe(0); + + $stabile = Stabile::where('codice_stabile', '0021')->first(); + expect($stabile)->not->toBeNull(); + + // Step 3: Verify 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) + $unitA11 = UnitaImmobiliare::where('stabile_id', $stabile->id) + ->where('scala', 'A') + ->where('interno', '11') + ->first(); + + expect($unitA11)->not->toBeNull(); + + $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'); + + 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'); + + 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'); + + // Step 5: Test Idempotency (re-run command) + $secondExit = Artisan::call('gescon:reconstruct-mirror-0021'); + expect($secondExit)->toBe(0); + + $unitsCountSecond = UnitaImmobiliare::where('stabile_id', $stabile->id)->count(); + expect($unitsCountSecond)->toBe(230); + + $relA11Second = PersonaUnitaRelazione::where('unita_id', $unitA11->id)->count(); + expect($relA11Second)->toBe(2); +});