feat(staging): implement append-only condomin_mirror table and gescon:import-mirror-0021 command for pilot 0021

This commit is contained in:
michele 2026-08-10 10:17:22 +02:00
parent e5c8617d7d
commit 4c99f5e87c
4 changed files with 354 additions and 5 deletions

View File

@ -0,0 +1,214 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Process\Process;
class ImportCondominMirror0021 extends Command
{
protected $signature = 'gescon:import-mirror-0021
{--stabile=0021 : Codice dello stabile pilot}
{--refresh : Svuota la tabella mirror prima di importare}';
protected $description = 'Importa in modalità append-only lossless la tabella condomin dello stabile 0021 nella staging mirror (condomin_mirror)';
public function handle(): int
{
$stabileCode = str_pad((string) $this->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;
}
}

View File

@ -0,0 +1,66 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
protected $connection = 'gescon_import';
public function up(): void
{
if (Schema::connection('gescon_import')->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');
}
};

View File

@ -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

View File

@ -0,0 +1,60 @@
<?php
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
test('gescon:import-mirror-0021 imports 644 rows append-only without collisions or duplicates', function () {
// Step 1: Initial import with refresh
$exitCode = Artisan::call('gescon:import-mirror-0021', ['--refresh' => 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');
});