$via, 'catasto_scala' => $scala, 'catasto_interno' => $interno, 'catasto_piano' => $piano, 'catasto_piano_livello' => $pianoLivello, 'catasto_piano_fisico' => $pianoFisico ]; } public static function formattaCategoriaCatastale(string $codice): string { $codiceClean = strtoupper(trim(str_replace([' ', '/', '-', '_'], '', $codice))); if (preg_match('/^([A-F])(\d)$/', $codiceClean, $m)) { $codiceClean = $m[1] . '0' . $m[2]; } $mappa = [ 'A01' => 'A/1 - Abitazioni di tipo signorile', 'A02' => 'A/2 - Abitazioni di tipo civile', 'A03' => 'A/3 - Abitazioni di tipo economico', 'A04' => 'A/4 - Abitazioni di tipo popolare', 'A05' => 'A/5 - Abitazioni di tipo ultrapopolare', 'A06' => 'A/6 - Abitazioni di tipo rurale', 'A07' => 'A/7 - Abitazioni in villini', 'A08' => 'A/8 - Abitazioni in ville', 'A09' => 'A/9 - Castelli, palazzi di eminenti pregi artistici o storici', 'A10' => 'A/10 - Uffici e studi privati', 'A11' => 'A/11 - Abitazioni ed alloggi tipici dei luoghi', 'B01' => 'B/1 - Collegi e convitti, educandati, ricoveri, orfanotrofi, ospizi, conventi, seminari, caserme', 'B02' => 'B/2 - Case di cura ed ospedali (senza fine di lucro)', 'B03' => 'B/3 - Prigioni e riformatori', 'B04' => 'B/4 - Uffici pubblici', 'B05' => 'B/5 - Scuole e laboratori scientifici', 'B06' => 'B/6 - Biblioteche, pinacoteche, musei, gallerie, accademie che non hanno sede in edifici della categoria A/9', 'B07' => 'B/7 - Cappelle ed oratori non destinati all’esercizio pubblico del culto', 'B08' => 'B/8 - Magazzini sotterranei per depositi di derrate', 'C01' => 'C/1 - Negozi e botteghe', 'C02' => 'C/2 - Magazzini e locali di deposito', 'C03' => 'C/3 - Laboratori per arti e mestieri', 'C04' => 'C/4 - Fabbricati e locali per esercizi sportivi (senza fine di lucro)', 'C05' => 'C/5 - Stabilimenti balneari e di acque curative (senza fine di lucro)', 'C06' => 'C/6 - Stalle, scuderie, rimesse, autorimesse (senza fine di lucro)', 'C07' => 'C/7 - Tettoie chiuse o aperte', 'D01' => 'D/1 - Opifici', 'D02' => 'D/2 - Alberghi e pensioni (con fine di lucro)', 'D03' => 'D/3 - Teatri, cinematografi, sale per concerti e spettacoli e simili (con fine di lucro)', 'D04' => 'D/4 - Case di cura ed ospedali (con fine di lucro)', 'D05' => 'D/5 - Istituto di credito, cambio e assicurazione (con fine di lucro)', 'D06' => 'D/6 - Fabbricati e locali per esercizi sportivi (con fine di lucro)', 'D07' => 'D/7 - Fabbricati costruiti o adattati per le speciali esigenze di un’attività industriale e non suscettibili di destinazione diversa senza radicali trasformazioni', 'D08' => 'D/8 - Fabbricati costruiti o adattati per le speciali esigenze di un’attività commerciale e non suscettibili di destinazione diversa senza radicali trasformazioni', 'D09' => 'D/9 - Edifici galleggianti o assicurati a punti fissi del suolo, ponti privati soggetti a pedaggio', 'D10' => 'D/10 - Fabbricati per funzioni produttive connesse alle attività agricole', 'E01' => 'E/1 - Stazioni per servizi di trasporto, terrestri, marittimi ed aerei', 'E02' => 'E/2 - Ponti comunali e provinciali soggetti a pedaggio', 'E03' => 'E/3 - Costruzioni e fabbricati per speciali esigenze pubbliche', 'E04' => 'E/4 - Recinti chiusi per speciali esigenze pubbliche', 'E05' => 'E/5 - Fabbricati costituenti fortificazioni e loro dipendenze', 'E06' => 'E/6 - Fari, semafori, torri per rendere pubblico l’uso dell’orologio comunale', 'E07' => 'E/7 - Fabbricati destinati all’esercizio pubblico dei culti', 'E08' => 'E/8 - Fabbricati e costruzioni nei cimiteri, esclusi i colombari, i sepolcri e le tombe di famiglia', 'E09' => 'E/9 - Fabbricati a destinazione particolare non compresi nelle categorie precedenti della categoria E', 'F01' => 'F/1 - Area urbana', 'F02' => 'F/2 - Unità collabenti', 'F03' => 'F/3 - Unità in corso di costruzione', 'F04' => 'F/4 - Unità in corso di definizione', 'F05' => 'F/5 - Lastrico solare', 'F06' => 'F/6 - Fabbricato in attesa di dichiarazione', 'F07' => 'F/7 - Infrastrutture di reti pubbliche di comunicazione', ]; if (isset($mappa[$codiceClean])) { return $mappa[$codiceClean]; } if (preg_match('/^([A-F])0?(\d+)$/', $codiceClean, $m)) { return $m[1] . '/' . $m[2]; } return $codice; } public static function estraiCodiceCompatto(string $categoria): string { if (str_contains($categoria, '-')) { $parti = explode('-', $categoria); return trim($parti[0]); } $codiceClean = strtoupper(trim(str_replace([' ', '/', '-', '_'], '', $categoria))); if (preg_match('/^([A-F])0?(\d+)$/', $codiceClean, $m)) { return $m[1] . '/' . $m[2]; } return $categoria; } public static function pulisciRendita(mixed $val): float { if ($val === null || $val === '') { return 0.0; } $valStr = (string)$val; $clean = preg_replace('/[^\d\,\.]/', '', $valStr); if (str_contains($clean, ',') && !str_contains($clean, '.')) { $clean = str_replace(',', '.', $clean); } elseif (str_contains($clean, ',') && str_contains($clean, '.')) { if (strrpos($clean, ',') > strrpos($clean, '.')) { $clean = str_replace('.', '', $clean); $clean = str_replace(',', '.', $clean); } else { $clean = str_replace(',', '', $clean); } } return round((float)$clean, 2); } /** * Resolve credentials: extracts passwordScript and base_url from config / .env / FeCassettoServiceConfig / Amministratore. */ public function resolveCredentials(?int $stabileId = null): array { $cfg = FeCassettoServiceConfig::query()->first(); $passwordScript = $cfg ? trim((string)$cfg->password_script) : ''; $baseUrl = ($cfg && !empty($cfg->base_url)) ? trim((string)$cfg->base_url) : (string)config('services.sister.base_url', 'https://thenetworksolution.it/sister/'); $appUrl = config('app.url') ?: 'http://localhost'; if (empty($passwordScript)) { $passwordScript = trim((string)config('services.sister.password_script', env('SISTER_PASSWORD_SCRIPT', env('FE_PASSWORD_SCRIPT', '')))); } if (empty($passwordScript) && $stabileId) { $stabile = Stabile::find($stabileId); if ($stabile?->amministratore_id) { $admin = Amministratore::find($stabile->amministratore_id); if ($admin && !empty($admin->fe_cassetto_password_script)) { $passwordScript = trim((string)$admin->fe_cassetto_password_script); } } } if (empty($passwordScript)) { $admin = Amministratore::whereNotNull('fe_cassetto_password_script')->where('fe_cassetto_password_script', '<>', '')->first(); if ($admin) { $passwordScript = trim((string)$admin->fe_cassetto_password_script); } } return [ 'passwordScript' => $passwordScript, 'base_url' => $baseUrl, 'app_url' => $appUrl, ]; } /** * Costruisce l'URL completo per gli script Sister API */ public function buildSisterUrl(string $baseUrl, string $endpoint): string { if (str_contains($baseUrl, 'ScaricaFatture/CassettoFiscale.php')) { $baseUrl = str_replace('ScaricaFatture/CassettoFiscale.php', 'sister/', $baseUrl); $baseUrl = str_replace('FattureCorrispettivi/sister/', 'sister/', $baseUrl); } $cleanBase = rtrim($baseUrl, '/'); if (!str_ends_with($cleanBase, '/sister') && !str_contains($cleanBase, 'sister')) { $cleanBase .= '/sister'; } return "{$cleanBase}/" . ltrim($endpoint, '/'); } /** * Get multi-tenant storage directory for the stable. */ public function getStoragePath(int $stabileId): string { $stabile = Stabile::find($stabileId); $amministratore = $stabile?->amministratore; $adminCode = $amministratore ? trim((string)$amministratore->codice_amministratore) : 'ADM'; $adminFolder = "AMMINISTRATORE_" . str_replace(' ', '_', $adminCode); $stabileCode = $stabile ? trim((string)$stabile->codice_stabile) : 'STB'; $stabileName = $stabile ? trim((string)$stabile->denominazione) : 'STABILE'; $stabileFolder = "STABILE_" . str_replace(' ', '_', "{$stabileCode}_{$stabileName}"); $adminFolder = preg_replace('/[^a-zA-Z0-9_]/', '', $adminFolder); $stabileFolder = preg_replace('/[^a-zA-Z0-9_]/', '', $stabileFolder); $adminFolder = preg_replace('/_+/', '_', $adminFolder); $stabileFolder = preg_replace('/_+/', '_', $stabileFolder); return "{$adminFolder}/{$stabileFolder}/catasto"; } /** * Metodo A: Ricerca Immobili per Soggetto (PF / PG) */ public function ricercaImmobiliSoggetto(int $stabileId, string $cfOrPg, string $uffProvinciale, string $tipoCatasto = 'E'): array { $creds = $this->resolveCredentials($stabileId); if (empty($creds['passwordScript'])) { throw new Exception("Credenziali Sister non configurate (passwordScript mancante). Configura la licenza in SuperAdmin o nel file .env."); } $params = [ 'passwordScript' => $creds['passwordScript'], 'app_url' => $creds['app_url'], 'tipoCatasto' => $tipoCatasto, 'uffprovinciale' => $uffProvinciale, 'easy' => 1, ]; if (strlen($cfOrPg) === 16) { $params['cf'] = strtoupper($cfOrPg); } else { $params['pg'] = $cfOrPg; } $url = $this->buildSisterUrl($creds['base_url'], 'mashImmobili.php'); $response = Http::asForm()->post($url, $params); if ($response->failed()) { throw new Exception("Errore chiamata Sister API (mashImmobili): " . $response->body()); } return $response->json() ?? []; } /** * Metodo B1: Ricerca Lista Immobili / Geometria Stabile (tutti i subalterni per Foglio e Particella) */ public function ricercaGeometriaImmobile( int $stabileId, string $tipoCatasto, string $uffProvinciale, string $denomComune, ?string $foglio, ?string $particella, ?string $sezione = null, ?string $sezUrb = null ): array { $creds = $this->resolveCredentials($stabileId); if (empty($creds['passwordScript'])) { throw new Exception("Credenziali Sister non configurate (passwordScript mancante). Configura la licenza in SuperAdmin o nel file .env."); } $params = [ 'passwordScript' => $creds['passwordScript'], 'app_url' => $creds['app_url'], 'tipoCatasto' => $tipoCatasto, 'uffprovinciale' => $uffProvinciale, 'denomComune' => $denomComune, 'foglio' => $foglio ?? '', 'particella' => $particella ?? '', 'easy' => 1, ]; if ($sezione !== null && $sezione !== '') { $params['sezione'] = $sezione; } if ($sezUrb !== null && $sezUrb !== '') { $sClean = trim($sezUrb); if (!preg_match('/^[A-Z]\d{3}$/i', $sClean) && strlen($sClean) <= 3) { $params['sezUrb'] = $sClean; } } $url = $this->buildSisterUrl($creds['base_url'], 'ricercaImmobile.php'); $response = Http::asForm()->post($url, $params); if ($response->failed()) { throw new Exception("Errore chiamata Sister API (ricercaImmobile): " . $response->body()); } return $response->json() ?? []; } /** * Metodo B2: Ricerca Intestati di Immobile (mashIntestati.php) */ public function ricercaIntestatiImmobile( int $stabileId, string $tipoCatasto, string $uffProvinciale, string $denomComune, ?string $foglio, ?string $particella, ?string $subalterno = null, ?string $sezione = null, ?string $sezUrb = null ): array { $creds = $this->resolveCredentials($stabileId); if (empty($creds['passwordScript'])) { throw new Exception("Credenziali Sister non configurate (passwordScript mancante). Configura la licenza in SuperAdmin o nel file .env."); } $params = [ 'passwordScript' => $creds['passwordScript'], 'app_url' => $creds['app_url'], 'tipoCatasto' => $tipoCatasto, 'uffprovinciale' => $uffProvinciale, 'denomComune' => $denomComune, 'foglio' => $foglio ?? '', 'particella' => $particella ?? '', 'easy' => 1, ]; if ($subalterno !== null && $subalterno !== '') { $params['subalterno'] = $subalterno; } if ($sezione !== null && $sezione !== '') { $params['sezione'] = $sezione; } if ($sezUrb !== null && $sezUrb !== '') { $sClean = trim($sezUrb); if (!preg_match('/^[A-Z]\d{3}$/i', $sClean) && strlen($sClean) <= 3) { $params['sezUrb'] = $sClean; } } $url = $this->buildSisterUrl($creds['base_url'], 'mashIntestati.php'); $response = Http::asForm()->post($url, $params); if ($response->failed()) { throw new Exception("Errore chiamata Sister API (mashIntestati): " . $response->body()); } return $response->json() ?? []; } /** * Metodo C: Acquisto Visura Ufficiale (PDF / XML Firmato) * Gestisce la cache locale e il polling per i documenti in evasione. */ public function acquistaEPrelevaVisura( int $stabileId, string $tipoCatasto, string $uffProvinciale, string $denomComune, ?string $foglio, ?string $particella, ?string $subalterno = null, ?string $sezione = null, ?string $sezUrb = null, string $formato = 'PDF', string $tipoVisura = 'completa', int $cacheHours = 24 ): array { $creds = $this->resolveCredentials($stabileId); $dirPath = $this->getStoragePath($stabileId); // Genera un nome file locale univoco per la visura $subLabel = ($subalterno !== null && $subalterno !== '') ? "_sub{$subalterno}" : ""; $foglioClean = $foglio ?? 'empty'; $particellaClean = $particella ?? 'empty'; $localFilename = "visura_{$tipoCatasto}_{$foglioClean}_{$particellaClean}{$subLabel}." . strtolower($formato); $localRelativePath = "{$dirPath}/{$localFilename}"; // Verifica cache se le ore sono > 0 e il file esiste localmente if ($cacheHours > 0 && Storage::disk('public')->exists($localRelativePath)) { $lastModified = Storage::disk('public')->lastModified($localRelativePath); if (time() - $lastModified < ($cacheHours * 3600)) { return [ 'status' => 'success', 'cached' => true, 'file_path' => $localRelativePath, 'url' => Storage::disk('public')->url($localRelativePath), ]; } } $params = [ 'passwordScript' => $creds['passwordScript'], 'app_url' => $creds['app_url'], 'tipoCatasto' => $tipoCatasto, 'uffprovinciale' => $uffProvinciale, 'denomComune' => $denomComune, 'foglio' => $foglio ?? '', 'particella' => $particella ?? '', 'formato' => strtoupper($formato), 'tipoVisura' => $tipoVisura, 'cache' => $cacheHours, ]; if ($subalterno !== null && $subalterno !== '') { $params['subalterno'] = $subalterno; } if ($sezione !== null && $sezione !== '') { $params['sezione'] = $sezione; } if ($sezUrb !== null && $sezUrb !== '') { $sClean = trim($sezUrb); if (!preg_match('/^[A-Z]\d{3}$/i', $sClean) && strlen($sClean) <= 3) { $params['sezUrb'] = $sClean; } } $url = $this->buildSisterUrl($creds['base_url'], 'mashVisuraI.php'); $response = Http::asForm()->post($url, $params); if ($response->failed()) { throw new Exception("Errore chiamata Sister API per visura: " . $response->body()); } $result = $response->json(); // Se la richiesta è in evasione differita if (isset($result['idRichiesta'])) { return $this->pollRichiestaEvasione($stabileId, (int)$result['idRichiesta'], $localFilename); } if (isset($result['url'])) { $fileUrl = $result['url']; if (!str_starts_with($fileUrl, 'http')) { $fileUrl = "https://thenetworksolution.it/sister/" . ltrim($fileUrl, '/'); } return $this->scaricaEArchiviaFile($stabileId, $fileUrl, $localFilename); } throw new Exception("Risposta Sister non riconosciuta: " . $response->body()); } /** * Esegue il download del file (PDF/XML o ZIP) e lo memorizza sul Cloud dello stabile. */ protected function scaricaEArchiviaFile(int $stabileId, string $url, string $targetFilename): array { $response = Http::get($url); if ($response->failed()) { throw new Exception("Impossibile scaricare il documento catastale dall'URL: {$url}"); } $content = $response->body(); $dirPath = $this->getStoragePath($stabileId); Storage::disk('public')->makeDirectory($dirPath); $localRelativePath = "{$dirPath}/{$targetFilename}"; // Se il file scaricato è uno ZIP (es. in caso di omoimmobili), lo scompattiamo if (str_starts_with($content, "PK\x03\x04")) { $tempZipPath = tempnam(sys_get_temp_dir(), 'sister_zip_'); file_put_contents($tempZipPath, $content); $zip = new ZipArchive(); if ($zip->open($tempZipPath) === true) { $firstFile = $zip->numFiles > 0 ? $zip->getNameIndex(0) : ''; for ($i = 0; $i < $zip->numFiles; $i++) { $filename = $zip->getNameIndex($i); $fileContent = $zip->getFromIndex($i); Storage::disk('public')->put("{$dirPath}/{$filename}", $fileContent); } $zip->close(); unlink($tempZipPath); return [ 'status' => 'success', 'zip_extracted' => true, 'directory' => $dirPath, 'file_path' => "{$dirPath}/" . $firstFile, 'url' => Storage::disk('public')->url("{$dirPath}/" . $firstFile), ]; } unlink($tempZipPath); } Storage::disk('public')->put($localRelativePath, $content); return [ 'status' => 'success', 'cached' => false, 'file_path' => $localRelativePath, 'url' => Storage::disk('public')->url($localRelativePath), ]; } /** * Esegue il polling per le richieste in evasione differita. */ protected function pollRichiestaEvasione(int $stabileId, int $idRichiesta, string $targetFilename): array { $creds = $this->resolveCredentials($stabileId); $attempts = 0; $maxAttempts = 5; while ($attempts < $maxAttempts) { $attempts++; sleep(5); $params = [ 'passwordScript' => $creds['passwordScript'], 'app_url' => $creds['app_url'], 'download' => $idRichiesta, ]; $url = $this->buildSisterUrl($creds['base_url'], 'elencoRichieste.php'); $response = Http::asForm()->post($url, $params); if ($response->successful()) { $content = $response->body(); if (!str_starts_with(trim($content), '{')) { $dirPath = $this->getStoragePath($stabileId); Storage::disk('public')->makeDirectory($dirPath); $localRelativePath = "{$dirPath}/{$targetFilename}"; Storage::disk('public')->put($localRelativePath, $content); return [ 'status' => 'success', 'cached' => false, 'file_path' => $localRelativePath, 'url' => Storage::disk('public')->url($localRelativePath), ]; } $json = json_decode($content, true); if (isset($json['err']) && !str_contains($json['err'], 'in produzione') && !str_contains($json['err'], 'in evasione')) { throw new Exception("Errore nel polling Sister: " . $json['err']); } } } throw new Exception("Tempo massimo scaduto per l'evasione catastale. idRichiesta: {$idRichiesta}"); } /** * Recupera dinamicamente il parametro denomComune interrogando comuni.php. */ public function getComuneCat(int $stabileId, string $uffProvinciale, string $cittaName): string { $creds = $this->resolveCredentials($stabileId); $url = $this->buildSisterUrl($creds['base_url'], 'comuni.php'); try { $params = [ 'uffprovinciale' => $uffProvinciale, ]; if (!empty($creds['passwordScript'])) { $params['passwordScript'] = $creds['passwordScript']; } $response = Http::get($url, $params); if ($response->successful()) { $data = $response->json(); $comuni = $data['comuni'] ?? $data; if (is_array($comuni)) { foreach ($comuni as $c) { if (is_string($c)) { $parti = explode('#', $c); if (isset($parti[1]) && strcasecmp(trim($parti[1]), trim($cittaName)) === 0) { return $c; } } elseif (is_array($c) && isset($c['comune'])) { if (strcasecmp(trim($c['comune']), trim($cittaName)) === 0) { $cod = $c['codice'] ?? 'H501'; $sez = $c['sezioni'] ?? '0'; $prov = $c['prov'] ?? '0'; return "{$cod}#{$cittaName}#{$sez}#{$prov}"; } } } } } } catch (Exception $e) {} // Fallback standard deterministico $comuneCod = (strtoupper($cittaName) === 'ROMA') ? 'H501' : 'H501'; return "{$comuneCod}#{$cittaName}#0#0"; } }