From 4c99f5e87caeaba77a3b54210071d6e195a55b2f Mon Sep 17 00:00:00 2001 From: michele Date: Mon, 10 Aug 2026 10:17:22 +0200 Subject: [PATCH] feat(staging): implement append-only condomin_mirror table and gescon:import-mirror-0021 command for pilot 0021 --- .../Commands/ImportCondominMirror0021.php | 214 ++++++++++++++++++ ...te_gescon_import_condomin_mirror_table.php | 66 ++++++ .../run_205_followup_via_agy.sh | 19 +- .../Feature/ImportCondominMirror0021Test.php | 60 +++++ 4 files changed, 354 insertions(+), 5 deletions(-) create mode 100644 app/Console/Commands/ImportCondominMirror0021.php create mode 100644 database/migrations/2026_08_10_100000_create_gescon_import_condomin_mirror_table.php create mode 100644 tests/Feature/ImportCondominMirror0021Test.php diff --git a/app/Console/Commands/ImportCondominMirror0021.php b/app/Console/Commands/ImportCondominMirror0021.php new file mode 100644 index 0000000..8307356 --- /dev/null +++ b/app/Console/Commands/ImportCondominMirror0021.php @@ -0,0 +1,214 @@ +option('stabile'), 4, '0', STR_PAD_LEFT); + $ammCode = 'HWFGITXK'; + + if ($this->option('refresh')) { + DB::connection('gescon_import')->table('condomin_mirror')->truncate(); + $this->info("Tabella gescon_import.condomin_mirror svuotata."); + } + + $baseDirs = [ + "/mnt/gescon-archives/gescon/{$stabileCode}", + storage_path("app/amministratori/{$ammCode}/legacy/{$stabileCode}") + ]; + + $targetDir = null; + foreach ($baseDirs as $dir) { + if (is_dir($dir)) { + $targetDir = $dir; + break; + } + } + + if (! $targetDir) { + $this->error("Directory MDB non trovata per lo stabile {$stabileCode}"); + return Command::FAILURE; + } + + $fileConfigs = [ + '0001' => ['year' => '0001', 'rel' => "{$stabileCode}/0001/singolo_anno.mdb", 'path' => "{$targetDir}/0001/singolo_anno.mdb"], + '0003' => ['year' => '0003', 'rel' => "{$stabileCode}/0003/singolo_anno.mdb", 'path' => "{$targetDir}/0003/singolo_anno.mdb"], + '0004' => ['year' => '0004', 'rel' => "{$stabileCode}/0004/singolo_anno.mdb", 'path' => "{$targetDir}/0004/singolo_anno.mdb"], + ]; + + $perFileCounts = []; + $totalMdbRows = 0; + + foreach ($fileConfigs as $key => $cfg) { + if (! file_exists($cfg['path'])) { + $this->warn("File non trovato: {$cfg['path']}"); + continue; + } + + $rows = $this->extractMdbCondominRows($cfg['path']); + $count = count($rows); + $totalMdbRows += $count; + $perFileCounts[$key] = [ + 'rel' => $cfg['rel'], + 'year' => $cfg['year'], + 'mdb_rows' => $count, + 'imported' => 0 + ]; + + $rowNum = 0; + foreach ($rows as $r) { + $rowNum++; + $provHash = hash('sha256', "{$ammCode}|{$stabileCode}|{$cfg['rel']}|{$cfg['year']}|condomin|{$rowNum}"); + + // Extract real MDB interno column (int or interno) without reconstructing from id_cond/cod_cond + $rawInterno = isset($r['int']) ? (string)$r['int'] : (isset($r['interno']) ? (string)$r['interno'] : ''); + $rawScala = isset($r['scala']) ? (string)$r['scala'] : ''; + $rawPiano = isset($r['piano']) ? (string)$r['piano'] : ''; + + $idCond = isset($r['id_cond']) ? (string)$r['id_cond'] : null; + $codCond = isset($r['cod_cond']) ? (string)$r['cod_cond'] : null; + $cognome = isset($r['cognome']) ? (string)$r['cognome'] : null; + $nome = isset($r['nome']) ? (string)$r['nome'] : null; + $nomCond = isset($r['nom_cond']) ? (string)$r['nom_cond'] : null; + $codFisc = isset($r['cond_cod_fisc']) ? (string)$r['cond_cod_fisc'] : (isset($r['codice_fiscale']) ? (string)$r['codice_fiscale'] : null); + $tel = isset($r['tel1']) ? (string)$r['tel1'] : (isset($r['telefono']) ? (string)$r['telefono'] : null); + $cell = isset($r['cell_cond']) ? (string)$r['cell_cond'] : (isset($r['cellulare']) ? (string)$r['cellulare'] : null); + $email = isset($r['e_mail_condomino']) ? (string)$r['e_mail_condomino'] : (isset($r['email']) ? (string)$r['email'] : null); + $pec = isset($r['pec_condomino']) ? (string)$r['pec_condomino'] : (isset($r['pec']) ? (string)$r['pec'] : null); + + $proprietario = isset($r['proprietario']) ? (string)$r['proprietario'] : null; + $inquilino = isset($r['inquilino']) ? (string)$r['inquilino'] : (isset($r['inquil_nome']) ? (string)$r['inquil_nome'] : null); + + $subPrima = isset($r['subentro_prima_cera']) ? (string)$r['subentro_prima_cera'] : null; + $subAdesso = isset($r['subentro_adesso_ce']) ? (string)$r['subentro_adesso_ce'] : null; + $subDal = isset($r['subentrato_dal']) ? (string)$r['subentrato_dal'] : null; + $attivoAl = isset($r['attivo_fino_al']) ? (string)$r['attivo_fino_al'] : null; + $inquilDal = isset($r['inquil_dal']) ? (string)$r['inquil_dal'] : null; + $inquilAl = isset($r['inquil_al']) ? (string)$r['inquil_al'] : null; + + $dirReale = isset($r['diritto_reale']) ? (string)$r['diritto_reale'] : null; + $dirGodimento = isset($r['diritto_godimento']) ? (string)$r['diritto_godimento'] : null; + $percDir = isset($r['perc_diritto_reale']) ? (string)$r['perc_diritto_reale'] : null; + + $millProp = isset($r['millesimi_proprieta']) && is_numeric($r['millesimi_proprieta']) ? (float)$r['millesimi_proprieta'] : null; + $millRisc = isset($r['millesimi_riscaldamento']) && is_numeric($r['millesimi_riscaldamento']) ? (float)$r['millesimi_riscaldamento'] : null; + $millAsc = isset($r['millesimi_ascensore']) && is_numeric($r['millesimi_ascensore']) ? (float)$r['millesimi_ascensore'] : null; + + $record = [ + 'amministratore_code' => $ammCode, + 'cod_stabile' => $stabileCode, + 'source_file' => $cfg['rel'], + 'source_year' => $cfg['year'], + 'source_table' => 'condomin', + 'source_row' => $rowNum, + 'provenance_hash' => $provHash, + 'id_cond' => $idCond, + 'cod_cond' => $codCond, + 'scala' => trim($rawScala) !== '' ? trim($rawScala) : null, + 'interno' => trim($rawInterno) !== '' ? trim($rawInterno) : null, + 'piano' => trim($rawPiano) !== '' ? trim($rawPiano) : null, + 'cognome' => $cognome, + 'nome' => $nome, + 'nom_cond' => $nomCond, + 'codice_fiscale' => $codFisc, + 'telefono' => $tel, + 'cellulare' => $cell, + 'email' => $email, + 'pec' => $pec, + 'millesimi_proprieta' => $millProp, + 'millesimi_riscaldamento' => $millRisc, + 'millesimi_ascensore' => $millAsc, + 'proprietario' => $proprietario, + 'inquilino' => $inquilino, + 'subentro_prima_cera' => $subPrima, + 'subentro_adesso_ce' => $subAdesso, + 'subentrato_dal' => $subDal, + 'attivo_fino_al' => $attivoAl, + 'inquil_dal' => $inquilDal, + 'inquil_al' => $inquilAl, + 'diritto_reale' => $dirReale, + 'diritto_godimento' => $dirGodimento, + 'perc_diritto_reale' => $percDir, + 'legacy_payload' => json_encode($r, JSON_UNESCAPED_UNICODE), + 'created_at' => now(), + 'updated_at' => now(), + ]; + + DB::connection('gescon_import')->table('condomin_mirror')->updateOrInsert( + ['provenance_hash' => $provHash], + $record + ); + + $perFileCounts[$key]['imported']++; + } + } + + $stagingRows = DB::connection('gescon_import')->table('condomin_mirror')->where('cod_stabile', $stabileCode)->count(); + + $duplicates = DB::connection('gescon_import')->select(" + SELECT provenance_hash, COUNT(*) as cnt + FROM condomin_mirror + WHERE cod_stabile = ? + GROUP BY provenance_hash + HAVING cnt > 1 + ", [$stabileCode]); + + $duplicateCount = count($duplicates); + $collisionCount = $stagingRows < $totalMdbRows ? ($totalMdbRows - $stagingRows) : 0; + + $this->info("=== FASE 1A MIRROR STAGING 0021 COMPLETATA ==="); + $this->line("MDB_ROWS_TOTAL: {$totalMdbRows}"); + $this->line("STAGING_ROWS: {$stagingRows}"); + $this->line("COLLISION_COUNT: {$collisionCount}"); + $this->line("DUPLICATE_PROVENANCE_COUNT: {$duplicateCount}"); + + foreach ($perFileCounts as $k => $info) { + $this->line("PER_FILE: {$info['rel']} | YEAR: {$info['year']} | MDB: {$info['mdb_rows']} | STAGING: {$info['imported']}"); + } + + return Command::SUCCESS; + } + + private function extractMdbCondominRows(string $mdbPath): array + { + $proc = new Process(['mdb-export', $mdbPath, 'condomin']); + $proc->run(); + + if (! $proc->isSuccessful()) { + return []; + } + + $csvContent = $proc->getOutput(); + $stream = fopen('php://memory', 'r+'); + fwrite($stream, $csvContent); + rewind($stream); + + $header = fgetcsv($stream); + if (! $header) { + fclose($stream); + return []; + } + + $rows = []; + while (($data = fgetcsv($stream)) !== false) { + if (count($data) === count($header)) { + $rows[] = array_combine($header, $data); + } + } + + fclose($stream); + return $rows; + } +} diff --git a/database/migrations/2026_08_10_100000_create_gescon_import_condomin_mirror_table.php b/database/migrations/2026_08_10_100000_create_gescon_import_condomin_mirror_table.php new file mode 100644 index 0000000..00eb815 --- /dev/null +++ b/database/migrations/2026_08_10_100000_create_gescon_import_condomin_mirror_table.php @@ -0,0 +1,66 @@ +hasTable('condomin_mirror')) { + return; + } + + Schema::connection('gescon_import')->create('condomin_mirror', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('amministratore_code', 50)->index(); + $table->string('cod_stabile', 20)->index(); + $table->string('source_file', 255)->index(); + $table->string('source_year', 20)->index(); + $table->string('source_table', 50)->default('condomin'); + $table->integer('source_row')->index(); + $table->string('provenance_hash', 64)->unique('condomin_mirror_provenance_unique'); + + // Raw columns from MDB condomin + $table->string('id_cond', 50)->nullable()->index(); + $table->string('cod_cond', 50)->nullable()->index(); + $table->string('scala', 50)->nullable(); + $table->string('interno', 50)->nullable(); // Real MDB int/interno column + $table->string('piano', 50)->nullable(); + $table->string('cognome', 255)->nullable(); + $table->string('nome', 255)->nullable(); + $table->string('nom_cond', 255)->nullable(); + $table->string('codice_fiscale', 50)->nullable(); + $table->string('telefono', 100)->nullable(); + $table->string('cellulare', 100)->nullable(); + $table->string('email', 255)->nullable(); + $table->string('pec', 255)->nullable(); + $table->string('indirizzo_corrispondenza', 255)->nullable(); + $table->decimal('millesimi_proprieta', 15, 4)->nullable(); + $table->decimal('millesimi_riscaldamento', 15, 4)->nullable(); + $table->decimal('millesimi_ascensore', 15, 4)->nullable(); + $table->string('proprietario', 255)->nullable(); + $table->string('inquilino', 255)->nullable(); + $table->string('subentro_prima_cera', 255)->nullable(); + $table->string('subentro_adesso_ce', 255)->nullable(); + $table->string('subentrato_dal', 50)->nullable(); + $table->string('attivo_fino_al', 50)->nullable(); + $table->string('inquil_dal', 50)->nullable(); + $table->string('inquil_al', 50)->nullable(); + $table->string('diritto_reale', 100)->nullable(); + $table->string('diritto_godimento', 100)->nullable(); + $table->string('perc_diritto_reale', 50)->nullable(); + + $table->json('legacy_payload')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::connection('gescon_import')->dropIfExists('condomin_mirror'); + } +}; diff --git a/scripts/ops/antigravity-cli/run_205_followup_via_agy.sh b/scripts/ops/antigravity-cli/run_205_followup_via_agy.sh index 551f17d..36001a5 100755 --- a/scripts/ops/antigravity-cli/run_205_followup_via_agy.sh +++ b/scripts/ops/antigravity-cli/run_205_followup_via_agy.sh @@ -11,6 +11,7 @@ PARSER_SCRIPT="$SCRIPT_DIR/parse_205_runner_output.py" MODEL="gemini-3.6-flash-high" MODE="${AGY_MODE:-plan}" PRINT_TIMEOUT="${AGY_PRINT_TIMEOUT:-20m}" +AGY_LOG_FILE="${AGY_LOG_FILE:-}" if command -v agy-ultra &>/dev/null; then EXEC_BIN="agy-ultra" @@ -45,13 +46,21 @@ else fi fi +CMD_ARGS=( + -p "$PROMPT" + --model "$MODEL" + --mode "$MODE" + --print-timeout "$PRINT_TIMEOUT" + --add-dir "$REPO_DIR" +) + +if [ -n "$AGY_LOG_FILE" ]; then + CMD_ARGS+=(--log-file "$AGY_LOG_FILE") +fi + set +e RAW_OUTPUT="$( - "$EXEC_BIN" -p "$PROMPT" \ - --model "$MODEL" \ - --mode "$MODE" \ - --print-timeout "$PRINT_TIMEOUT" \ - --add-dir "$REPO_DIR" 2>&1 + "$EXEC_BIN" "${CMD_ARGS[@]}" 2>&1 )" AGY_EXIT=$? set -e diff --git a/tests/Feature/ImportCondominMirror0021Test.php b/tests/Feature/ImportCondominMirror0021Test.php new file mode 100644 index 0000000..5c730d0 --- /dev/null +++ b/tests/Feature/ImportCondominMirror0021Test.php @@ -0,0 +1,60 @@ + true]); + expect($exitCode)->toBe(0); + + $stagingCount = DB::connection('gescon_import') + ->table('condomin_mirror') + ->where('cod_stabile', '0021') + ->count(); + + expect($stagingCount)->toBe(644); + + // Check count per year + $count0001 = DB::connection('gescon_import')->table('condomin_mirror')->where('cod_stabile', '0021')->where('source_year', '0001')->count(); + $count0003 = DB::connection('gescon_import')->table('condomin_mirror')->where('cod_stabile', '0021')->where('source_year', '0003')->count(); + $count0004 = DB::connection('gescon_import')->table('condomin_mirror')->where('cod_stabile', '0021')->where('source_year', '0004')->count(); + + expect($count0001)->toBe(212) + ->and($count0003)->toBe(214) + ->and($count0004)->toBe(218); + + // Check duplicates + $duplicates = DB::connection('gescon_import')->select(" + SELECT provenance_hash, COUNT(*) as cnt + FROM condomin_mirror + WHERE cod_stabile = '0021' + GROUP BY provenance_hash + HAVING cnt > 1 + "); + + expect($duplicates)->toBeEmpty(); + + // Step 2: Second run (Idempotency) + $secondExit = Artisan::call('gescon:import-mirror-0021'); + expect($secondExit)->toBe(0); + + $secondCount = DB::connection('gescon_import') + ->table('condomin_mirror') + ->where('cod_stabile', '0021') + ->count(); + + expect($secondCount)->toBe(644); + + // Step 3: Verify real MDB interno column preservation + $sample = DB::connection('gescon_import') + ->table('condomin_mirror') + ->where('cod_stabile', '0021') + ->where('source_year', '0001') + ->where('id_cond', '127') + ->first(); + + expect($sample)->not->toBeNull() + ->and($sample->scala)->toBe('C') + ->and($sample->interno)->toBe('3'); +});