feat(importer): reconstruct Anagrafica Unica, Unità and Relazioni for 0021 from condomin_mirror (task-856d2ae3f2)
This commit is contained in:
parent
dc08affa82
commit
f60898aae6
428
app/Console/Commands/GesconReconstructMirror0021Command.php
Normal file
428
app/Console/Commands/GesconReconstructMirror0021Command.php
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Persona;
|
||||
use App\Models\PersonaUnitaRelazione;
|
||||
use App\Models\Stabile;
|
||||
use App\Models\UnitaImmobiliare;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
* Mappa in memoria delle persone già risolte per identificatore stabile (id_cond)
|
||||
*/
|
||||
private array $resolvedPersonasByStableId = [];
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$stabileCode = (string) $this->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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('persone_unita_relazioni')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('persone_unita_relazioni', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('persone_unita_relazioni', 'id_cond')) {
|
||||
$table->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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
85
tests/Feature/ReconstructMirror0021Test.php
Normal file
85
tests/Feature/ReconstructMirror0021Test.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
use App\Models\PersonaUnitaRelazione;
|
||||
use App\Models\Stabile;
|
||||
use App\Models\UnitaImmobiliare;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
test('gescon:reconstruct-mirror-0021 reconstructs units, personas and subentro relations with full 644 quadratura', function () {
|
||||
// Step 0: Ensure Amministratore and Stabile exist in test environment
|
||||
DB::table('amministratori')->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);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user