netgescon-day0/app/Services/GesconImport/Steps/HandlesUnitaStep.php

624 lines
28 KiB
PHP

<?php
namespace App\Services\GesconImport\Steps;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
trait HandlesUnitaStep
{
protected array $tipologiaCache = [];
protected array $tipologiaMetaCache = [];
private function normalizeScalaInternoForImport(string $scala, ?string $interno): array
{
$scala = strtoupper(trim($scala));
$interno = $interno !== null ? trim($interno) : null;
if ($interno === null || $interno === '') {
return [$scala, $interno];
}
$m = null;
if (preg_match('/^([A-Z])\s*[-\/]\s*(.+)$/i', $interno, $m) || preg_match('/^([A-Z])\s+(\d.*)$/i', $interno, $m)) {
$prefix = strtoupper($m[1]);
$rest = trim((string)$m[2]);
if ($rest !== '' && ($scala === '' || strcasecmp($scala, $prefix) === 0)) {
return [$prefix, $rest];
}
}
return [$scala, $interno];
}
private function stepUnita(?int $limit): int
{
// Fonte: gescon_import.condomin (elenco soggetti/unità)
if (!Schema::connection('gescon_import')->hasTable('condomin')) {
return 0;
}
$count = 0;
$updated = 0;
$hasDetailedSchema = Schema::hasColumn('unita_immobiliari', 'codice_unita');
$hasTipologiaColumn = Schema::hasColumn('unita_immobiliari', 'tipologia_id');
$parentStabile = null;
if (Schema::hasTable('stabili')) {
if ($this->targetStabileId !== null) {
$parentStabile = DB::table('stabili')->where('id', $this->targetStabileId)->first();
} elseif ($this->option('stabile')) {
$stabileCol = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione';
$parentStabile = DB::table('stabili')->where($stabileCol, $this->option('stabile'))->first();
if (!$parentStabile && $stabileCol === 'codice_stabile') {
$alt = ltrim($this->option('stabile'), '0');
if ($alt !== '' && $alt !== $this->option('stabile')) {
$parentStabile = DB::table('stabili')->where($stabileCol, $alt)->first();
}
}
}
}
$pertinenzeToLink = [];
$processed = 0;
$batch = $limit ? min(max(1, $limit), 1000) : 1000;
$offset = 0;
while (true) {
$q = DB::connection('gescon_import')->table('condomin');
if ($this->option('stabile')) {
$q->where('cod_stabile', $this->option('stabile'));
}
if ($this->option('scala')) {
$q->where('scala', $this->option('scala'));
}
$q->limit($batch)->offset($offset);
$rows = $q->get();
if ($rows->isEmpty()) {
break;
}
foreach ($rows as $r) {
// risolvi stabile_id per riga (multi-stabili)
$scalaVal = trim((string)($r->scala ?? ''));
$scalaVal = $scalaVal !== '' ? $scalaVal : 'A';
$rowStabileId = $this->stabileId;
$rowStabileCode = $this->targetStabileCode;
if ($this->targetStabileId !== null) {
$rowStabileId = $this->targetStabileId;
if ($this->splitMode === 'stabili-per-scala' && $parentStabile) {
[$childId, $childCode] = $this->ensureStabilePerScala($parentStabile, $scalaVal);
if ($childId) {
$rowStabileId = $childId;
$rowStabileCode = $childCode ?? $rowStabileCode;
}
}
} else {
if (Schema::hasTable('stabili')) {
$stabileCol = Schema::hasColumn('stabili', 'codice_stabile') ? 'codice_stabile' : 'denominazione';
$stRow = DB::table('stabili')->where($stabileCol, $r->cod_stabile ?? $this->option('stabile'))->first();
$rowStabileId = $stRow->id ?? $this->stabileId;
if ($stRow) {
$rowStabileCode = $stRow->codice_stabile ?? $stRow->denominazione ?? $rowStabileCode;
}
if ($this->splitMode === 'stabili-per-scala' && $parentStabile) {
[$childId, $childCode] = $this->ensureStabilePerScala($parentStabile, $scalaVal);
if ($childId) {
$rowStabileId = $childId;
$rowStabileCode = $childCode ?? $rowStabileCode;
}
}
} else {
$rowStabileCode = $rowStabileCode ?? ($r->cod_stabile ?? $this->option('stabile'));
}
}
if (!$rowStabileId) {
continue;
}
$interno = isset($r->interno) ? (string)$r->interno : null;
[$scalaVal, $interno] = $this->normalizeScalaInternoForImport($scalaVal, $interno);
$sub = $r->sub ?? $interno;
// Riconosci pertinenze nel campo INT o note: cantina, box/garage, posto auto, soffitta/solaio, locale tecnico
$internoStr = is_string($interno) ? trim($interno) : (string)$interno;
$ptype = null;
$pcode = null; // tipo pertinenza e codice interno normalizzato
if ($internoStr !== '') {
if (preg_match('/^CAN\s*(\d+)$/i', preg_replace('/\s+/', ' ', $internoStr), $m)) {
$ptype = 'cantina';
$pcode = 'CAN' . $m[1];
} elseif (preg_match('/^(BOX|GAR|GARAGE)\s*(\d+)?$/i', preg_replace('/\s+/', ' ', $internoStr), $m)) {
$ptype = 'box';
$pcode = 'BOX' . ($m[2] ?? '');
} elseif (preg_match('/^(PA|POSTO\s*AUTO)\s*(\d+)?$/i', preg_replace('/\s+/', ' ', $internoStr), $m)) {
$ptype = 'posto_auto';
$pcode = 'PA' . ($m[2] ?? '');
} elseif (preg_match('/^(SOF|SOFFITTA|SOLAIO)\s*(\d+)?$/i', preg_replace('/\s+/', ' ', $internoStr), $m)) {
$ptype = 'soffitta';
$pcode = 'SOF' . ($m[2] ?? '');
} elseif (preg_match('/^(LT|LOCALE\s*TECNICO)\s*(\d+)?$/i', preg_replace('/\s+/', ' ', $internoStr), $m)) {
$ptype = 'locale_tecnico';
$pcode = 'LT' . ($m[2] ?? '');
}
}
// Piano: normalizza (R=0, T=0, 1=1, -1 o S= -1)
$pianoVal = 0;
if (isset($r->piano)) {
$pv = is_string($r->piano) ? strtoupper(trim($r->piano)) : (string)$r->piano;
if ($pv === 'T' || $pv === 'PT' || $pv === 'R' || $pv === 'PR' || $pv === '') {
$pianoVal = 0;
} elseif ($pv === 'S' || $pv === 'PS' || $pv === '-1') {
$pianoVal = -1;
} elseif (is_numeric($pv)) {
$pianoVal = (int)$pv;
}
}
if ($hasDetailedSchema) {
$normNumeric = function ($v): ?string {
if ($v === null) {
return null;
}
$s = trim((string) $v);
if ($s === '') {
return null;
}
$s = str_replace(' ', '', $s);
if (str_contains($s, ',') && str_contains($s, '.')) {
$s = str_replace('.', '', $s);
}
$s = str_replace(',', '.', $s);
return is_numeric($s) ? $s : null;
};
$catSez = property_exists($r, 'catasto_sez_urbana') ? (string) ($r->catasto_sez_urbana ?? '') : '';
$catFoglio = property_exists($r, 'catasto_foglio') ? (string) ($r->catasto_foglio ?? '') : '';
$catPart = property_exists($r, 'catasto_particella') ? (string) ($r->catasto_particella ?? '') : '';
$catSub = property_exists($r, 'catasto_sub') ? (string) ($r->catasto_sub ?? '') : '';
$catCat = property_exists($r, 'catasto_categoria') ? (string) ($r->catasto_categoria ?? '') : '';
$catClasse = property_exists($r, 'catasto_classe') ? (string) ($r->catasto_classe ?? '') : '';
$catCons = property_exists($r, 'catasto_consistenza') ? $normNumeric($r->catasto_consistenza ?? null) : null;
$catSup = property_exists($r, 'catasto_superfice') ? $normNumeric($r->catasto_superfice ?? null) : null;
$catRend = property_exists($r, 'catasto_rendita') ? $normNumeric($r->catasto_rendita ?? null) : null;
// Codice unita coerente: STAB-SCALA-INT (per cantine: STAB-SCALA-CAN<n>)
$intPart = $ptype && $pcode ? $pcode : (($interno ?? null) ?: '0');
$codiceBaseStab = $rowStabileCode ?? ($r->cod_stabile ?? $this->option('stabile')) ?: 'S0';
// Se stabili-per-scala, includi comunque la scala nel codice per leggibilità
$codice = $codiceBaseStab . '-' . ($scalaVal ?: 'A') . '-' . $intPart;
// Filtri post-normalizzazione: pertinenze e piano min/max
if ($this->option('only-pertinenze')) {
$opt = strtolower((string)$this->option('only-pertinenze'));
$isPert = (bool)$ptype;
$ok = $opt === 'any' ? $isPert : ($isPert && $ptype === $opt);
if (!$ok) {
continue;
}
}
$pMin = $this->option('piano-min') !== null ? (int)$this->option('piano-min') : null;
$pMax = $this->option('piano-max') !== null ? (int)$this->option('piano-max') : null;
if ($pMin !== null && $pianoVal < $pMin) {
continue;
}
if ($pMax !== null && $pianoVal > $pMax) {
continue;
}
$exists = DB::table('unita_immobiliari')->where('codice_unita', $codice)->first();
if ($exists) {
// Aggiorna palazzina/scala se richiesto
$patch = [];
if ($this->splitMode === 'palazzine') {
if (Schema::hasColumn('unita_immobiliari', 'palazzina')) {
if (empty($exists->palazzina) || $exists->palazzina !== $scalaVal) {
$patch['palazzina'] = $scalaVal;
}
}
if (Schema::hasColumn('unita_immobiliari', 'scala') && (empty($exists->scala) || $exists->scala !== $scalaVal)) {
$patch['scala'] = $scalaVal;
}
}
if (!empty($patch)) {
$patch['updated_at'] = now();
if (!$this->isDryRun) {
DB::table('unita_immobiliari')->where('id', $exists->id)->update($patch);
}
$updated++;
}
continue;
}
$ownerName = trim(($r->cognome ?? '') . ' ' . ($r->nome ?? '')) ?: null;
$tipologiaDef = $this->detectTipologiaFromRow($r, $ptype, $internoStr);
$tipologiaId = $hasTipologiaColumn ? $this->resolveTipologiaIdFor($tipologiaDef) : null;
$denominazione = $ownerName;
if (($tipologiaDef['is_condominiale'] ?? false) && !empty($tipologiaDef['nome'])) {
$denominazione = $tipologiaDef['nome'];
}
$tipoUnitaRaw = $tipologiaDef['tipo_unita'] ?? ($ptype ?: 'abitazione');
$data = [
'stabile_id' => $rowStabileId,
'palazzina_id' => null,
'codice_unita' => $codice,
'denominazione' => $denominazione,
'descrizione' => null,
'palazzina' => $this->splitMode === 'palazzine' ? $scalaVal : null,
'scala' => $scalaVal,
'piano' => $pianoVal,
'interno' => (string)($ptype && $pcode ? $pcode : $interno),
'subalterno' => (string)$sub,
'categoria_catastale' => null,
'classe' => null,
'consistenza' => null,
'rendita_catastale' => null,
'millesimi_generali' => 0,
'tipo_unita' => $this->normalizeTipoUnita($tipoUnitaRaw),
'tipologia_id' => $tipologiaId,
'stato_conservazione' => 'buono',
'stato_occupazione' => 'occupata_proprietario',
'attiva' => true,
'unita_demo' => false,
'created_at' => now(),
'updated_at' => now()
];
if (!$hasTipologiaColumn) {
unset($data['tipologia_id']);
}
$this->dynamicInsert('unita_immobiliari', $data);
// Se è pertinenza, memorizza relazione da collegare in un secondo pass
if ($ptype) {
$pertinenzeToLink[] = [
'stabile_id' => $rowStabileId,
'scala' => $r->scala ?? 'A',
'interno' => (string)($pcode ?: $interno),
'tipo' => $tipologiaDef['tipo_unita'] ?? $ptype,
'owner_signature' => $ownerName,
];
}
} else {
// schema semplice (anagrafiche base)
$criteria = ['stabile_id' => $this->stabileId];
if ($interno !== null) {
$criteria['interno'] = (string)$interno;
}
$exists = DB::table('unita_immobiliari')->where($criteria)->first();
if ($exists) {
// Aggiorna palazzina/scala se in split palazzine
if ($this->splitMode === 'palazzine') {
$patch = [];
if (Schema::hasColumn('unita_immobiliari', 'palazzina') && (empty($exists->palazzina) || $exists->palazzina !== $scalaVal)) {
$patch['palazzina'] = $scalaVal;
}
if (Schema::hasColumn('unita_immobiliari', 'scala') && (empty($exists->scala) || $exists->scala !== $scalaVal)) {
$patch['scala'] = $scalaVal;
}
if (!empty($patch)) {
$patch['updated_at'] = now();
if (!$this->isDryRun) {
DB::table('unita_immobiliari')->where('id', $exists->id)->update($patch);
}
$updated++;
}
}
continue;
}
$data = [
'stabile_id' => $rowStabileId,
'interno' => (string)$interno,
'scala' => $scalaVal,
'piano' => $pianoVal,
'subalterno' => (string)$sub,
'categoria_catastale' => null,
'superficie' => null,
'vani' => null,
'indirizzo' => $r->indirizzo_corrispondenza ?? null,
'note' => null,
'created_at' => now(),
'updated_at' => now()
];
$this->dynamicInsert('unita_immobiliari', $data);
}
$count++;
$processed++;
if ($limit && $processed >= $limit) {
break 2;
}
}
$offset += $batch;
}
// Collega pertinenze alle unità principali (cumulo) se tabella disponibile
if (!empty($pertinenzeToLink) && Schema::hasTable('unita_pertinenze')) {
foreach ($pertinenzeToLink as $p) {
$mainCandidates = DB::table('unita_immobiliari')
->where('stabile_id', $p['stabile_id'])
->where('scala', $p['scala'])
->orderBy('id')
->get();
$main = $mainCandidates->first(function ($unit) use ($p) {
if ($this->isUnitPertinenza($unit) || $this->isUnitCondominiale($unit)) {
return false;
}
if (!empty($p['owner_signature'])) {
$normalized = Str::lower(trim($unit->denominazione ?? ''));
return $normalized === Str::lower(trim($p['owner_signature']));
}
return true;
});
if (!$main) {
$main = $mainCandidates->first(function ($unit) {
return !$this->isUnitPertinenza($unit) && !$this->isUnitCondominiale($unit);
});
}
$cant = DB::table('unita_immobiliari')
->where('stabile_id', $p['stabile_id'])
->where('scala', $p['scala'])
->where('interno', $p['interno'])
->first();
if ($main && $cant) {
$this->dynamicInsert('unita_pertinenze', [
'stabile_id' => $p['stabile_id'],
'unita_principale_id' => $main->id,
'unita_accessoria_id' => $cant->id,
'tipo' => $p['tipo'] ?? 'pertinenza',
'cumulo' => true,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
}
if ($updated > 0) {
$this->line(" → unita aggiornate: {$updated}");
}
return $count + $updated;
}
private function detectTipologiaFromRow(object $row, ?string $pertinenzaType, ?string $interno): array
{
$defaults = $this->tipologiaDefaults();
$interno = $interno ?? '';
$descriptorSources = [
$interno,
$row->note_unita ?? null,
$row->note ?? null,
$row->destinazione ?? null,
$row->descrizione ?? null,
];
$descriptor = Str::upper(trim(implode(' ', array_filter(array_map(fn($v) => is_string($v) ? $v : '', $descriptorSources)))));
$owner = Str::upper(trim(($row->cognome ?? '') . ' ' . ($row->nome ?? '')));
if ($pertinenzaType) {
$map = [
'cantina' => 'CANT',
'box' => 'BOX',
'garage' => 'BOX',
'posto_auto' => 'POSTO_AUTO',
'soffitta' => 'SOFF',
'locale_tecnico' => 'LOCALE_TEC',
];
$code = $map[$pertinenzaType] ?? null;
if ($code && isset($defaults[$code])) {
return array_merge(
$defaults[$code],
['tipo_unita' => $pertinenzaType]
);
}
}
if ($this->looksLikeCondominio($owner) || $this->looksLikeCondominio($descriptor)) {
foreach (
[
'PORTINERIA' => 'CONDO_PORTINERIA',
'BICI' => 'CONDO_BICI',
'CICLO' => 'CONDO_BICI',
'LAVAND' => 'CONDO_LAVANDERIA',
'PISCINA' => 'CONDO_PISCINA',
] as $needle => $code
) {
if (Str::contains($descriptor, $needle)) {
return array_merge(
$defaults[$code] ?? ['code' => $code, 'nome' => Str::headline(Str::lower($needle)), 'categoria' => 'condominiale', 'is_condominiale' => true],
['tipo_unita' => 'condominiale']
);
}
}
$code = 'CONDO_GENERIC';
return array_merge(
$defaults[$code] ?? ['code' => $code, 'nome' => 'Area Condominiale', 'categoria' => 'condominiale', 'is_condominiale' => true],
['tipo_unita' => 'condominiale']
);
}
foreach (
[
'UFF' => ['UFF', ['UFFICIO', 'STUDIO']],
'NEGOZIO' => ['NEGOZIO', ['NEGOZIO', 'BOTTEGA', 'COMMERCIALE']],
] as $code => [$label, $needles]
) {
foreach ($needles as $needle) {
if (Str::contains($descriptor, $needle)) {
return array_merge(
$defaults[$code] ?? ['code' => $code, 'nome' => $label, 'categoria' => 'commerciale'],
['tipo_unita' => Str::lower($label)]
);
}
}
}
$code = 'APP';
return array_merge(
$defaults[$code] ?? ['code' => $code, 'nome' => 'Appartamento', 'categoria' => 'residenziale'],
['tipo_unita' => 'abitazione']
);
}
private function normalizeTipoUnita(?string $value): string
{
$allowed = [
'abitazione',
'studio_professionale',
'attivita_commerciale',
'magazzino_deposito',
'box_garage',
'cantina',
'soffitta',
'spazio_comune_redditizio',
'locale_tecnico',
'altro',
];
$normalized = Str::snake(Str::lower(trim((string)$value)), '_');
if ($normalized === '' || $normalized === '_') {
return 'abitazione';
}
$map = [
'condominiale' => 'spazio_comune_redditizio',
'spazio_condominiale' => 'spazio_comune_redditizio',
'area_condominiale' => 'spazio_comune_redditizio',
'ufficio' => 'studio_professionale',
'studio' => 'studio_professionale',
'studio_medico' => 'studio_professionale',
'negozio' => 'attivita_commerciale',
'locale_commerciale' => 'attivita_commerciale',
'attivita' => 'attivita_commerciale',
'locale_vendita' => 'attivita_commerciale',
'magazzino' => 'magazzino_deposito',
'deposito' => 'magazzino_deposito',
'box' => 'box_garage',
'garage' => 'box_garage',
'posto_auto' => 'box_garage',
'parcheggio' => 'box_garage',
'solaio' => 'soffitta',
'locale_condominiale' => 'spazio_comune_redditizio',
'locale_tecnico' => 'locale_tecnico',
'centrale_termica' => 'locale_tecnico',
];
$normalized = $map[$normalized] ?? $normalized;
if (!in_array($normalized, $allowed, true)) {
return 'altro';
}
return $normalized;
}
private function resolveTipologiaIdFor(array $definition): ?int
{
if (!Schema::hasTable('unita_tipologie')) {
return null;
}
$code = strtoupper($definition['code'] ?? '');
if ($code === '') {
return null;
}
if (array_key_exists($code, $this->tipologiaCache)) {
return $this->tipologiaCache[$code];
}
$row = DB::table('unita_tipologie')->where('codice', $code)->first();
if ($row) {
$this->tipologiaCache[$code] = (int)$row->id;
$this->tipologiaMetaCache[(int)$row->id] = $row;
return (int)$row->id;
}
if ($this->isDryRun) {
$this->tipologiaCache[$code] = null;
return null;
}
$payload = [
'codice' => $code,
'nome' => $definition['nome'] ?? $code,
'categoria' => $definition['categoria'] ?? null,
'is_pertinenza' => (bool)($definition['is_pertinenza'] ?? false),
'is_condominiale' => (bool)($definition['is_condominiale'] ?? false),
'meta' => $definition['meta'] ?? null,
'created_at' => now(),
'updated_at' => now(),
];
$id = DB::table('unita_tipologie')->insertGetId($payload);
$row = (object)array_merge($payload, ['id' => $id]);
$this->tipologiaCache[$code] = $id;
$this->tipologiaMetaCache[$id] = $row;
return $id;
}
private function loadTipologiaMeta(?int $id): ?object
{
if (!$id || $id <= 0) {
return null;
}
if (isset($this->tipologiaMetaCache[$id])) {
return $this->tipologiaMetaCache[$id];
}
if (!Schema::hasTable('unita_tipologie')) {
return null;
}
$row = DB::table('unita_tipologie')->where('id', $id)->first();
if ($row) {
$this->tipologiaMetaCache[$id] = $row;
}
return $row;
}
private function isUnitPertinenza(object $unit): bool
{
$tipo = $unit->tipo_unita ?? null;
if (in_array($tipo, ['cantina', 'box', 'garage', 'posto_auto', 'soffitta', 'locale_tecnico'], true)) {
return true;
}
$meta = $this->loadTipologiaMeta($unit->tipologia_id ?? null);
return (bool)($meta->is_pertinenza ?? false);
}
private function isUnitCondominiale(object $unit): bool
{
$tipo = $unit->tipo_unita ?? null;
if ($tipo === 'condominiale') {
return true;
}
$meta = $this->loadTipologiaMeta($unit->tipologia_id ?? null);
return (bool)($meta->is_condominiale ?? false);
}
private function tipologiaDefaults(): array
{
return [
'APP' => ['code' => 'APP', 'nome' => 'Appartamento', 'categoria' => 'residenziale'],
'UFF' => ['code' => 'UFF', 'nome' => 'Ufficio', 'categoria' => 'commerciale'],
'NEGOZIO' => ['code' => 'NEGOZIO', 'nome' => 'Negozio', 'categoria' => 'commerciale'],
'CANT' => ['code' => 'CANT', 'nome' => 'Cantina', 'categoria' => 'pertinenza', 'is_pertinenza' => true],
'BOX' => ['code' => 'BOX', 'nome' => 'Box', 'categoria' => 'pertinenza', 'is_pertinenza' => true],
'POSTO_AUTO' => ['code' => 'POSTO_AUTO', 'nome' => 'Posto Auto', 'categoria' => 'pertinenza', 'is_pertinenza' => true],
'SOFF' => ['code' => 'SOFF', 'nome' => 'Soffitta', 'categoria' => 'pertinenza', 'is_pertinenza' => true],
'LOCALE_TEC' => ['code' => 'LOCALE_TEC', 'nome' => 'Locale Tecnico', 'categoria' => 'pertinenza', 'is_pertinenza' => true],
'CONDO_PORTINERIA' => ['code' => 'CONDO_PORTINERIA', 'nome' => 'Portineria Condominiale', 'categoria' => 'condominiale', 'is_condominiale' => true],
'CONDO_BICI' => ['code' => 'CONDO_BICI', 'nome' => 'Locale Bici', 'categoria' => 'condominiale', 'is_condominiale' => true],
'CONDO_LAVANDERIA' => ['code' => 'CONDO_LAVANDERIA', 'nome' => 'Lavanderia Condominiale', 'categoria' => 'condominiale', 'is_condominiale' => true],
'CONDO_PISCINA' => ['code' => 'CONDO_PISCINA', 'nome' => 'Piscina Condominiale', 'categoria' => 'condominiale', 'is_condominiale' => true],
'CONDO_GENERIC' => ['code' => 'CONDO_GENERIC', 'nome' => 'Area Condominiale', 'categoria' => 'condominiale', 'is_condominiale' => true],
];
}
private function looksLikeCondominio(string $value): bool
{
$value = trim($value);
if ($value === '') {
return false;
}
return (bool) preg_match('/\bCOND(?:OMIN|\.)/u', $value);
}
}