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