fix(anagrafiche): import comproprietari from MDB, link Francioni/Tordini to Stabile 0016 B/4 and replicate canonical contacts (task-d095b0e23b)

This commit is contained in:
michele 2026-08-27 12:35:30 +02:00
parent 1036e76593
commit 2a873958ca
5 changed files with 189 additions and 45 deletions

View File

@ -158,6 +158,97 @@ public function handle(): int
$perFileCounts[$key]['imported']++;
}
$unitMapByIdCond = [];
$unitMapByCodCond = [];
foreach ($rows as $r) {
$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;
if ($idCond) {
$unitMapByIdCond[$idCond] = ['scala' => $rawScala, 'interno' => $rawInterno, 'piano' => $rawPiano];
}
if ($codCond) {
$unitMapByCodCond[$codCond] = ['scala' => $rawScala, 'interno' => $rawInterno, 'piano' => $rawPiano];
}
}
// Extract and mirror comproprietari if present in MDB
$compRows = $this->extractMdbTableRows($cfg['path'], 'comproprietari');
$compNum = 0;
foreach ($compRows as $c) {
$compNum++;
$provHash = hash('sha256', "{$ammCode}|{$stabileCode}|{$cfg['rel']}|{$cfg['year']}|comproprietari|{$compNum}");
$cIdCond = isset($c['id_cond']) ? (string)$c['id_cond'] : null;
$cCodCond = isset($c['ex_cod_cond']) ? (string)$c['ex_cod_cond'] : (isset($c['cod_cond']) ? (string)$c['cod_cond'] : null);
$unitInfo = ($cIdCond && isset($unitMapByIdCond[$cIdCond]))
? $unitMapByIdCond[$cIdCond]
: (($cCodCond && isset($unitMapByCodCond[$cCodCond])) ? $unitMapByCodCond[$cCodCond] : null);
$rawScala = $unitInfo['scala'] ?? (isset($c['ex_scala']) ? (string)$c['ex_scala'] : '');
$rawInterno = $unitInfo['interno'] ?? (isset($c['ex_int']) ? (string)$c['ex_int'] : '');
$rawPiano = $unitInfo['piano'] ?? '';
$nomCond = isset($c['nom_cond']) ? (string)$c['nom_cond'] : (isset($c['Detraz_cess_nome']) ? (string)$c['Detraz_cess_nome'] : null);
$codFisc = isset($c['Cond_cod_fisc']) ? (string)$c['Cond_cod_fisc'] : (isset($c['Detraz_cess_CF']) ? (string)$c['Detraz_cess_CF'] : null);
$tel = isset($c['tel1']) ? (string)$c['tel1'] : null;
$cell = isset($c['Cell_cond']) ? (string)$c['Cell_cond'] : null;
$email = isset($c['E_mail_condomino']) ? (string)$c['E_mail_condomino'] : null;
$pec = isset($c['PEC_compr']) ? (string)$c['PEC_compr'] : null;
$dirReale= isset($c['Diritto_reale']) && trim((string)$c['Diritto_reale']) !== '' ? (string)$c['Diritto_reale'] : (isset($c['Descriz']) ? (string)$c['Descriz'] : 'Comproprietà');
$percDir = isset($c['Perc_Diritto_reale']) && is_numeric($c['Perc_Diritto_reale']) ? (string)$c['Perc_Diritto_reale'] : null;
$recordComp = [
'amministratore_code' => $ammCode,
'cod_stabile' => $stabileCode,
'source_file' => $cfg['rel'],
'source_year' => $cfg['year'],
'source_table' => 'comproprietari',
'source_row' => $compNum,
'provenance_hash' => $provHash,
'id_cond' => $cIdCond,
'cod_cond' => $cCodCond,
'scala' => trim($rawScala) !== '' ? trim($rawScala) : null,
'interno' => trim($rawInterno) !== '' ? trim($rawInterno) : null,
'piano' => trim($rawPiano) !== '' ? trim($rawPiano) : null,
'cognome' => null,
'nome' => null,
'nom_cond' => $nomCond,
'codice_fiscale' => $codFisc,
'telefono' => $tel,
'cellulare' => $cell,
'email' => $email,
'pec' => $pec,
'millesimi_proprieta' => null,
'millesimi_riscaldamento' => null,
'millesimi_ascensore' => null,
'proprietario' => $nomCond,
'inquilino' => null,
'subentro_prima_cera' => null,
'subentro_adesso_ce' => null,
'subentrato_dal' => null,
'attivo_fino_al' => null,
'inquil_dal' => null,
'inquil_al' => null,
'diritto_reale' => $dirReale,
'diritto_godimento' => null,
'perc_diritto_reale' => $percDir,
'legacy_payload' => json_encode($c, JSON_UNESCAPED_UNICODE),
'created_at' => now(),
'updated_at' => now(),
];
DB::connection('gescon_import')->table('condomin_mirror')->updateOrInsert(
['provenance_hash' => $provHash],
$recordComp
);
}
}
$stagingRows = DB::connection('gescon_import')->table('condomin_mirror')->where('cod_stabile', $stabileCode)->count();
@ -188,7 +279,12 @@ public function handle(): int
private function extractMdbCondominRows(string $mdbPath): array
{
$proc = new Process(['mdb-export', $mdbPath, 'condomin']);
return $this->extractMdbTableRows($mdbPath, 'condomin');
}
private function extractMdbTableRows(string $mdbPath, string $tableName): array
{
$proc = new Process(['mdb-export', $mdbPath, $tableName]);
$proc->run();
if (! $proc->isSuccessful()) {

View File

@ -473,11 +473,9 @@ protected function buildDomainConsolidatedQuery(int $stabileId): Builder
$ownerNameSub = DB::table('persone_unita_relazioni as pur')
->join('persone as p', 'p.id', '=', 'pur.persona_id')
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
->where('pur.tipo_relazione', 'proprietario')
->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario'])
->where('pur.attivo', true)
->orderByDesc('pur.id')
->selectRaw("TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''), ' ', COALESCE(p.ragione_sociale, '')))")
->limit(1);
->selectRaw("GROUP_CONCAT(DISTINCT TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''), ' ', COALESCE(p.ragione_sociale, ''))) SEPARATOR ' / ')");
$tenantNameSub = DB::table('persone_unita_relazioni as pur')
->join('persone as p', 'p.id', '=', 'pur.persona_id')
@ -491,11 +489,9 @@ protected function buildDomainConsolidatedQuery(int $stabileId): Builder
$ownerCellSub = DB::table('persone_unita_relazioni as pur')
->join('persone as p', 'p.id', '=', 'pur.persona_id')
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
->where('pur.tipo_relazione', 'proprietario')
->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario'])
->where('pur.attivo', true)
->orderByDesc('pur.id')
->selectRaw("COALESCE(p.telefono_principale, '')")
->limit(1);
->selectRaw("GROUP_CONCAT(DISTINCT COALESCE(p.telefono_principale, '') SEPARATOR ' / ')");
$tenantCellSub = DB::table('persone_unita_relazioni as pur')
->join('persone as p', 'p.id', '=', 'pur.persona_id')
@ -509,11 +505,9 @@ protected function buildDomainConsolidatedQuery(int $stabileId): Builder
$ownerCfSub = DB::table('persone_unita_relazioni as pur')
->join('persone as p', 'p.id', '=', 'pur.persona_id')
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
->where('pur.tipo_relazione', 'proprietario')
->whereIn('pur.tipo_relazione', ['proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario'])
->where('pur.attivo', true)
->orderByDesc('pur.id')
->selectRaw("COALESCE(p.codice_fiscale, '')")
->limit(1);
->selectRaw("GROUP_CONCAT(DISTINCT COALESCE(p.codice_fiscale, '') SEPARATOR ' / ')");
$tenantCfSub = DB::table('persone_unita_relazioni as pur')
->join('persone as p', 'p.id', '=', 'pur.persona_id')

View File

@ -1385,24 +1385,34 @@ private function populateNominativiStoriciFallback(): void
$owner = trim((string) ($m->nom_cond ?? $m->proprietario ?? ''));
if ($owner !== '') {
$fallbackList[] = [
'ruolo' => 'Proprietario',
'nominativo' => $owner,
'periodo' => $yLabel,
'percentuale' => '100,00%',
'fonte' => 'Archivio storico legacy',
'detail' => ! empty($m->codice_fiscale) ? ('CF: ' . $m->codice_fiscale) : null,
'persona_id' => null,
'nome' => $owner,
'codice_fiscale'=> ! empty($m->codice_fiscale) ? $m->codice_fiscale : null,
'tipo_relazione'=> 'proprietario',
'ruolo_label' => 'Proprietario (' . $yLabel . ')',
'quota' => 100.0,
'quota_label' => '100,00',
'data_inizio' => $yLabel,
'data_fine' => null,
'attivo' => false,
'fonte' => 'Archivio storico legacy',
];
}
$inquil = trim((string) ($m->inquilino ?? $m->inquil_nome ?? ''));
if ($inquil !== '') {
$fallbackList[] = [
'ruolo' => 'Inquilino',
'nominativo' => $inquil,
'periodo' => $yLabel,
'percentuale' => '100,00%',
'fonte' => 'Archivio storico legacy',
'detail' => null,
'persona_id' => null,
'nome' => $inquil,
'codice_fiscale'=> ! empty($m->inquil_cod_fisc) ? $m->inquil_cod_fisc : null,
'tipo_relazione'=> 'inquilino',
'ruolo_label' => 'Inquilino (' . $yLabel . ')',
'quota' => 100.0,
'quota_label' => '100,00',
'data_inizio' => $yLabel,
'data_fine' => null,
'attivo' => false,
'fonte' => 'Archivio storico legacy',
];
}
}
@ -3243,6 +3253,7 @@ protected function hydrateRelazioni(): void
if (! $this->mostraStorico) {
$relazioniStoriche = $proprietari->filter(fn($r) => ! $isRelazioneCorrenteNelAnno($r, $activeAnno))
->concat($inquilini->filter(fn($r) => ! $isRelazioneCorrenteNelAnno($r, $activeAnno)))
->concat($relazioniMapped->filter(fn($r) => ! ($r['attivo'] ?? false)))
->unique(fn(array $r) => $this->relationIdentityKey($r))
->values()
->all();
@ -3252,6 +3263,41 @@ protected function hydrateRelazioni(): void
}
$legacyRow = $this->getLegacyCondominRow();
$legacyOwnerName = trim((string) ($legacyRow?->nom_cond ?? $legacyRow?->proprietario ?? ''));
if ($legacyOwnerName !== '') {
$ownerNameUpper = strtoupper($legacyOwnerName);
$matchedOwner = $proprietari->filter(function (array $p) use ($ownerNameUpper): bool {
$nome = strtoupper(trim((string) ($p['nome'] ?? '')));
return $nome !== '' && (str_contains($nome, $ownerNameUpper) || str_contains($ownerNameUpper, $nome));
})->values();
if ($matchedOwner->isNotEmpty()) {
$unmatchedOwners = $proprietari->reject(function (array $p) use ($ownerNameUpper): bool {
$nome = strtoupper(trim((string) ($p['nome'] ?? '')));
return $nome !== '' && (str_contains($nome, $ownerNameUpper) || str_contains($ownerNameUpper, $nome));
})->values()->all();
$proprietari = $matchedOwner;
$relazioniStoriche = array_values(array_unique(array_merge($relazioniStoriche, $unmatchedOwners), SORT_REGULAR));
} else {
$legacyOwnerRecord = [
'persona_id' => null,
'nome' => $legacyOwnerName,
'codice_fiscale'=> trim((string) ($legacyRow?->cond_cod_fisc ?? $legacyRow?->codice_fiscale ?? '')),
'tipo_relazione'=> 'proprietario',
'ruolo_label' => 'Proprietario 100%',
'quota' => 100.0,
'quota_label' => '100,00',
'data_inizio' => '01/01/' . $activeAnno,
'data_fine' => null,
'attivo' => true,
'fonte' => 'legacy_condomin_mirror',
];
$relazioniStoriche = array_values(array_unique(array_merge($relazioniStoriche, $proprietari->all()), SORT_REGULAR));
$proprietari = collect([$legacyOwnerRecord]);
}
}
$legacyInqName = trim((string) ($legacyRow?->inquil_nome ?? $legacyRow?->inquilino ?? ''));
if ($legacyInqName !== '') {
$inqNameUpper = strtoupper($legacyInqName);
@ -3291,11 +3337,19 @@ protected function hydrateRelazioni(): void
}
}
$activeOwnerNames = $proprietari->pluck('nome')->map(fn($n) => strtoupper(trim((string) $n)))->all();
$fallbackHist = (array) ($this->populateNominativiStoriciFallback() ?? []);
$relazioniStoriche = collect(array_merge($relazioniStoriche, $fallbackHist))
->reject(fn($r) => in_array(strtoupper(trim((string) ($r['nome'] ?? ''))), $activeOwnerNames, true))
->unique(fn(array $r) => $this->relationIdentityKey($r))
->values()
->all();
$this->relazioniPerTipo = [
'proprietari' => $proprietari->values()->all(),
'inquilini' => $inqArr,
'altri' => $altri->values()->all(),
'storico' => $relazioniStoriche,
'storico' => ! empty($relazioniStoriche) ? $relazioniStoriche : (array) ($this->nominativiStorici ?? []),
];
}

View File

@ -8,7 +8,7 @@ # ASCII Wireframe - Nominativi e Anagrafica Unica per Stabile
+-----------------------------------------------------------------------------------------------------------------------+
| CONDOMINI > NOMINATIVI E ANAGRAFICA UNICA |
| |
| Stabile Attivo: [ 0013 - OTTAVIANO 105 (ID 18) v ] Anno Gestione: [ 2026 (0015 - Attiva) v ] [ Mostra Storico ]|
| Stabile Attivo: [ 0016 - GERMANICO 96 (ID 19) v ] Anno Gestione: [ 0914 (Anno 2026 - Attiva) v ] |
| |
| +-----------------------------------------------------------------------------------------------------------------+ |
| | FILTRI E RICERCA ANAGRAFICA CANONICA | |
@ -20,18 +20,22 @@ # ASCII Wireframe - Nominativi e Anagrafica Unica per Stabile
| +-----------------------------------------------------------------------------------------------------------------+ |
| |
| +-----------------------------------------------------------------------------------------------------------------+ |
| | ANAGRAFICA UNICA E RUOLI CONSOLIDATI [ + Nuovo Nominativo ] | |
| | ANAGRAFICA UNICA E RUOLI CONSOLIDATI (Stabile 0016 - GERMANICO 96) [ + Nuovo Nominativo ] | |
| +-----------------------------------------------------------------------------------------------------------------+ |
| | NOMINATIVO CANONICO | CF / P.IVA | RUOLO & QUOTA | UNITA IMMOBILIARE | PERIODO / GESTIONE | AZIONI | |
| +--------------------------+------------------+-------------------+-------------------+--------------------+--------+ |
| | [NOME NOMINATIVO REAL] | [CODICE FISCALE] | Proprietario 100% | Scala A - Int. 1 | Dal 01/01/2026 | [Apri] | |
| | [Anagrafica 000000XX] | | (Piena proprietà) | (ID XX) | (Gestione 2026) | [Edit] | |
| | FRANCIONI CLAUDIA / | FRNCLD49P51H501B | Comproprietà 50% | Scala B - Int. 4 | Dal 01/01/2000 | [Apri] | |
| | BARONE MICHELE | BRNMHL69M11H501K | (Piena proprieta) | (ID 313) | (Gestione 0914) | [Edit] | |
| | [Email: cecilia.tordini@gmail.com] | | | | | |
| +--------------------------+------------------+-------------------+-------------------+--------------------+--------+ |
| | [NOME INQUILINO REAL] | [CODICE FISCALE] | Inquilino 100% | Scala A - Int. 2 | Dal 01/01/2025 | [Apri] | |
| | [Anagrafica 000000YY] | | (Locatario) | (ID YY) | (Gestione 2025-26) | [Edit] | |
| | SAVELLI FABIO | SVLFBA58H27H501P | Comproprietà 50% | Scala A - Int. 25 | Dal 01/01/2000 | [Apri] | |
| | [Anagrafica 000000SF] | | | (ID 334) | (Gestione 0914) | [Edit] | |
| +--------------------------+------------------+-------------------+-------------------+--------------------+--------+ |
| | [NOME STORICO REAL] | [CODICE FISCALE] | Inquilino 100% | Scala A - Int. 2 | 01/01/2024-31/12/24| [Apri] | |
| | [Anagrafica 000000ZZ] | | (Locatario stor.) | (ID YY) | (Gestione 2024) | [Edit] | |
| | BINI FLORIANA / | BNIFRN76M50H501V | Nuda proprietà | Scala A - Int. 59 | Dal 01/01/2000 | [Apri] | |
| | BINI VANESSA | BNIVSS79M46H501K | | (ID 368) | (Gestione 0914) | [Edit] | |
| +--------------------------+------------------+-------------------+-------------------+--------------------+--------+ |
| | GRILLO CAMILLO / | GRLCLL41R14H501X | Usufrutto / Nuda | Scala A - Int. 33 | Dal 01/01/2000 | [Apri] | |
| | GRILLO FRANCESCO | GRLFNC77D03H501R | | (ID 342) | (Gestione 0914) | [Edit] | |
| +-----------------------------------------------------------------------------------------------------------------+ |
+-----------------------------------------------------------------------------------------------------------------------+
```

View File

@ -13,18 +13,14 @@
beforeEach(function () {
DB::table('amministratori')->insertOrIgnore(['id' => 1, 'nome' => 'Admin Test', 'cognome' => 'Test', 'created_at' => now(), 'updated_at' => now()]);
$stabile = Stabile::where('codice_stabile', '0021')->first();
if (! $stabile) {
$mirrorCount = DB::connection('gescon_import')
->table('condomin_mirror')
->where('cod_stabile', '0021')
->count();
if ($mirrorCount === 0) {
Artisan::call('gescon:import-mirror-0021');
}
Artisan::call('gescon:reconstruct-mirror-0021');
$stabile = Stabile::where('codice_stabile', '0021')->first();
$mirrorCount = DB::connection('gescon_import')
->table('condomin_mirror')
->where('cod_stabile', '0021')
->count();
if ($mirrorCount === 0) {
Artisan::call('gescon:import-mirror-0021');
}
Artisan::call('gescon:reconstruct-mirror-0021');
});
test('gescon:bonifica-duplicati-benedetto command is 100% idempotent across two runs', function () {