feat(staging): implement append-only condomin_mirror table and gescon:import-mirror-0021 command for pilot 0021
This commit is contained in:
parent
e5c8617d7d
commit
4c99f5e87c
214
app/Console/Commands/ImportCondominMirror0021.php
Normal file
214
app/Console/Commands/ImportCondominMirror0021.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -11,6 +11,7 @@ PARSER_SCRIPT="$SCRIPT_DIR/parse_205_runner_output.py"
|
||||||
MODEL="gemini-3.6-flash-high"
|
MODEL="gemini-3.6-flash-high"
|
||||||
MODE="${AGY_MODE:-plan}"
|
MODE="${AGY_MODE:-plan}"
|
||||||
PRINT_TIMEOUT="${AGY_PRINT_TIMEOUT:-20m}"
|
PRINT_TIMEOUT="${AGY_PRINT_TIMEOUT:-20m}"
|
||||||
|
AGY_LOG_FILE="${AGY_LOG_FILE:-}"
|
||||||
|
|
||||||
if command -v agy-ultra &>/dev/null; then
|
if command -v agy-ultra &>/dev/null; then
|
||||||
EXEC_BIN="agy-ultra"
|
EXEC_BIN="agy-ultra"
|
||||||
|
|
@ -45,13 +46,21 @@ else
|
||||||
fi
|
fi
|
||||||
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
|
set +e
|
||||||
RAW_OUTPUT="$(
|
RAW_OUTPUT="$(
|
||||||
"$EXEC_BIN" -p "$PROMPT" \
|
"$EXEC_BIN" "${CMD_ARGS[@]}" 2>&1
|
||||||
--model "$MODEL" \
|
|
||||||
--mode "$MODE" \
|
|
||||||
--print-timeout "$PRINT_TIMEOUT" \
|
|
||||||
--add-dir "$REPO_DIR" 2>&1
|
|
||||||
)"
|
)"
|
||||||
AGY_EXIT=$?
|
AGY_EXIT=$?
|
||||||
set -e
|
set -e
|
||||||
|
|
|
||||||
60
tests/Feature/ImportCondominMirror0021Test.php
Normal file
60
tests/Feature/ImportCondominMirror0021Test.php
Normal 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');
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user