netgescon-day0/app/Services/SisterCatastoService.php

559 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Services;
use App\Models\Amministratore;
use App\Models\FeCassettoServiceConfig;
use App\Models\Stabile;
use App\Models\UnitaImmobiliare;
use App\Models\StabileServizioLettura;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\DB;
use Exception;
use ZipArchive;
class SisterCatastoService
{
public static function scomponeIndirizzoAde(string $indirizzoCompleto, string $pianoStringa = ''): array
{
$via = '';
$scala = '';
$interno = '';
$piano = '';
$pianoLivello = '';
$pianoFisico = '';
if (preg_match('/Piano\s+(.+)$/i', $indirizzoCompleto, $m)) {
$piano = trim($m[1]);
}
if (preg_match('/Interno\s+(\d+)/i', $indirizzoCompleto, $m)) {
$interno = trim($m[1]);
}
if (preg_match('/Scala\s+([A-Z])/i', $indirizzoCompleto, $m)) {
$scala = trim($m[1]);
}
$parti = preg_split('/(Scala|Interno|Piano)/i', $indirizzoCompleto);
if (isset($parti[0])) {
$via = trim(rtrim(trim($parti[0]), ','));
}
if (empty($piano) && !empty($pianoStringa)) {
$piano = trim(str_ireplace('Piano', '', $pianoStringa));
}
if (!empty($piano)) {
if (preg_match('/(\d+|S\d+|T|PT|Terra)\s*-\s*(\d+|S\d+|T|PT|Terra)/i', $piano, $m)) {
$val1 = strtoupper(trim($m[1]));
$val2 = strtoupper(trim($m[2]));
if (str_starts_with($val1, 'S')) {
$pianoLivello = $val1;
$pianoFisico = $val2;
} elseif (str_starts_with($val2, 'S')) {
$pianoLivello = $val2;
$pianoFisico = $val1;
} else {
$pianoLivello = $val1;
$pianoFisico = $val2;
}
} else {
$pUpper = strtoupper(trim($piano));
if (str_starts_with($pUpper, 'S')) {
$pianoLivello = $pUpper;
$pianoFisico = '';
} else {
$pianoLivello = '';
$pianoFisico = $pUpper;
}
}
}
return [
'catasto_via' => $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 allesercizio 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 unattività industriale e non suscettibili di destinazione diversa senza radicali trasformazioni',
'D08' => 'D/8 - Fabbricati costruiti o adattati per le speciali esigenze di unattività 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 luso dellorologio comunale',
'E07' => 'E/7 - Fabbricati destinati allesercizio 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 only passwordScript and base_url from FeCassettoServiceConfig,
* with an optional fallback to the active administrator (ID=4).
*/
protected function resolveCredentials(?int $stabileId): array
{
$cfg = FeCassettoServiceConfig::query()->first();
$passwordScript = $cfg ? trim((string)$cfg->password_script) : '';
$baseUrl = $cfg ? trim((string)$cfg->base_url) : 'https://thenetworksolution.it/FattureCorrispettivi/ScaricaFatture/CassettoFiscale.php';
$appUrl = config('app.url') ?: 'http://localhost';
// Fallback opzionale sull'amministratore con ID = 4 se vuota
if (empty($passwordScript)) {
$admin = Amministratore::find(4) ?: Amministratore::first();
if ($admin) {
$passwordScript = trim((string)$admin->fe_cassetto_password_script);
}
}
if (empty($passwordScript)) {
$passwordScript = 'demo';
}
return [
'passwordScript' => $passwordScript,
'base_url' => $baseUrl,
'app_url' => $appUrl,
];
}
/**
* 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);
$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 = str_replace('ScaricaFatture/CassettoFiscale.php', 'sister/mashImmobili.php', $creds['base_url']);
$url = str_replace('FattureCorrispettivi/sister/', 'sister/', $url);
$response = Http::asForm()->post($url, $params);
if ($response->failed()) {
throw new Exception("Errore chiamata Sister API: " . $response->body());
}
return $response->json() ?? [];
}
/**
* Metodo B: Ricerca Intestati di Immobile
*/
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);
$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 !== '') {
$params['sezUrb'] = $sezUrb;
}
$url = str_replace('ScaricaFatture/CassettoFiscale.php', 'sister/mashIntestati.php', $creds['base_url']);
$url = str_replace('FattureCorrispettivi/sister/', 'sister/', $url);
$response = Http::asForm()->post($url, $params);
if ($response->failed()) {
throw new Exception("Errore chiamata Sister API: " . $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 !== '') {
$params['sezUrb'] = $sezUrb;
}
$url = str_replace('ScaricaFatture/CassettoFiscale.php', 'sister/mashVisuraI.php', $creds['base_url']);
$url = str_replace('FattureCorrispettivi/sister/', 'sister/', $url);
$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 = str_replace('ScaricaFatture/CassettoFiscale.php', 'sister/elencoRichieste.php', $creds['base_url']);
$url = str_replace('FattureCorrispettivi/sister/', 'sister/', $url);
$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 = str_replace('ScaricaFatture/CassettoFiscale.php', 'sister/comuni.php', $creds['base_url']);
$url = str_replace('FattureCorrispettivi/sister/', 'sister/', $url);
try {
$response = Http::get($url, [
'uffprovinciale' => $uffProvinciale
]);
if ($response->successful()) {
$comuni = $response->json();
if (is_array($comuni)) {
foreach ($comuni as $c) {
if (strcasecmp(trim($c['comune'] ?? ''), trim($cittaName)) === 0) {
$cod = $c['codice'] ?? '';
$sez = $c['sezioni'] ?? '0';
$prov = $c['prov'] ?? '0';
return "{$cod}#{$cittaName}#{$sez}#{$prov}";
}
}
}
}
} catch (Exception $e) {}
// Fallback robusto
$comuneCod = (strtoupper($cittaName) === 'ROMA') ? 'H501' : 'H501';
return "{$comuneCod}#{$cittaName}#0#0";
}
}