netgescon-day0/app/Services/Arera/AreraOperatoriImportService.php

346 lines
14 KiB
PHP
Executable File

<?php
namespace App\Services\Arera;
use App\Models\AreraNaturaGiuridica;
use App\Models\AreraOperatore;
use App\Models\AreraSettoreAttivita;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use PhpOffice\PhpSpreadsheet\IOFactory;
use ZipArchive;
class AreraOperatoriImportService
{
/**
* @return array{rows:int, imported:int, updated:int, skipped:int, sectors:int, errors:array<int,string>, files:int}
*/
public function importZipFromUrl(string $url): array
{
@set_time_limit(0);
$url = trim($url);
if ($url === '' || ! preg_match('/^https?:\/\//i', $url)) {
throw new \InvalidArgumentException('URL ARERA non valido.');
}
$host = parse_url($url, PHP_URL_HOST);
if (! is_string($host) || ! str_ends_with(strtolower($host), 'arera.it')) {
throw new \InvalidArgumentException('Per sicurezza sono accettati solo URL del dominio arera.it.');
}
$response = Http::timeout(180)->get($url);
if (! $response->successful()) {
throw new \RuntimeException('Download ARERA fallito: HTTP ' . $response->status());
}
$bytes = $response->body();
if (! is_string($bytes) || $bytes === '') {
throw new \RuntimeException('Download ARERA vuoto.');
}
$zipPath = tempnam(sys_get_temp_dir(), 'arera_zip_');
if (! is_string($zipPath) || $zipPath === '') {
throw new \RuntimeException('Impossibile creare file temporaneo ZIP.');
}
file_put_contents($zipPath, $bytes);
try {
return $this->importZipFile($zipPath, basename((string) parse_url($url, PHP_URL_PATH)) ?: 'arera.zip');
} finally {
@unlink($zipPath);
}
}
/**
* @return array{rows:int, imported:int, updated:int, skipped:int, sectors:int, errors:array<int,string>, files:int}
*/
public function importZipFile(string $zipPath, string $sourceFilename = ''): array
{
@set_time_limit(0);
if (! class_exists(ZipArchive::class)) {
throw new \RuntimeException('Estensione ZIP non disponibile sul server.');
}
$zip = new ZipArchive();
if ($zip->open($zipPath) !== true) {
throw new \RuntimeException('Impossibile aprire lo ZIP ARERA.');
}
$summary = ['rows' => 0, 'imported' => 0, 'updated' => 0, 'skipped' => 0, 'sectors' => 0, 'errors' => [], 'files' => 0];
try {
for ($index = 0; $index < $zip->numFiles; $index++) {
$name = (string) $zip->getNameIndex($index);
$lower = strtolower($name);
if (! str_ends_with($lower, '.xlsx') && ! str_ends_with($lower, '.xls')) {
continue;
}
$contents = $zip->getFromIndex($index);
if (! is_string($contents) || $contents === '') {
$summary['errors'][] = 'File Excel vuoto nello ZIP: ' . $name;
continue;
}
$xlsxPath = tempnam(sys_get_temp_dir(), 'arera_xlsx_');
if (! is_string($xlsxPath) || $xlsxPath === '') {
$summary['errors'][] = 'Impossibile creare temporaneo per ' . $name;
continue;
}
file_put_contents($xlsxPath, $contents);
try {
$result = $this->importXlsx($xlsxPath, $sourceFilename !== '' ? ($sourceFilename . '::' . basename($name)) : basename($name));
$summary['rows'] += $result['rows'];
$summary['imported'] += $result['imported'];
$summary['updated'] += $result['updated'];
$summary['skipped'] += $result['skipped'];
$summary['sectors'] += $result['sectors'];
$summary['errors'] = array_slice(array_merge($summary['errors'], $result['errors']), 0, 40);
$summary['files']++;
} finally {
@unlink($xlsxPath);
}
}
} finally {
$zip->close();
}
if ($summary['files'] === 0) {
throw new \RuntimeException('Nessun file Excel trovato nello ZIP ARERA.');
}
return $summary;
}
/**
* @return array{rows:int, imported:int, updated:int, skipped:int, sectors:int, errors:array<int,string>}
*/
public function importXlsx(string $path, string $sourceFilename = ''): array
{
@set_time_limit(0);
$spreadsheet = IOFactory::load($path);
$sheet = $spreadsheet->getActiveSheet();
$rows = $sheet->toArray(null, true, true, true);
if ($rows === []) {
return ['rows' => 0, 'imported' => 0, 'updated' => 0, 'skipped' => 0, 'sectors' => 0, 'errors' => ['File vuoto']];
}
$headerRow = array_shift($rows) ?: [];
$headers = [];
foreach ($headerRow as $column => $label) {
$normalized = $this->normalizeHeader((string) $label);
if ($normalized !== '') {
$headers[$column] = $normalized;
}
}
$imported = 0;
$updated = 0;
$skipped = 0;
$sectorIds = [];
$errors = [];
foreach ($rows as $index => $row) {
$data = [];
foreach ($headers as $column => $key) {
$data[$key] = trim((string) ($row[$column] ?? ''));
}
$societa = $this->cleanText($data['societa'] ?? $data['ragione_sociale'] ?? '');
$piva = $this->normalizeVat($data['partita_iva'] ?? '');
if ($societa === '' && $piva === '') {
$skipped++;
continue;
}
if ($piva === '') {
$errors[] = 'Riga ' . ((int) $index + 1) . ': P.IVA mancante per ' . ($societa ?: 'operatore senza nome');
$skipped++;
continue;
}
$natura = $this->cleanText($data['natura_giuridica'] ?? '');
$naturaId = null;
if ($natura !== '') {
$naturaId = (int) AreraNaturaGiuridica::query()->firstOrCreate(['nome' => $natura])->id;
}
$settori = $this->splitMultiValue($data['settori_attivita'] ?? '');
if ($settori === []) {
$sourceLower = mb_strtolower($sourceFilename);
if (str_contains($sourceLower, 'export-gas-vend')) {
$settori = ['Gas vendita'];
} elseif (str_contains($sourceLower, 'export-mercato-vend')) {
$settori = ['Mercato vendita'];
}
}
$settoreSync = [];
foreach ($settori as $settore) {
$settoreModel = AreraSettoreAttivita::query()->firstOrCreate(
['slug' => $this->sectorSlug($settore)],
['nome' => $settore]
);
$settoreSync[] = (int) $settoreModel->id;
$sectorIds[(int) $settoreModel->id] = true;
}
$sedeLegaleRaw = (string) ($data['sede_legale'] ?? '');
if ($sedeLegaleRaw === '' && ! empty($data['indirizzo'])) {
$comuneSedeLegale = $this->cleanText($data['comune_sede_legale'] ?? '');
$sedeLegaleRaw = $this->cleanText((string) $data['indirizzo'] . ($comuneSedeLegale !== '' ? (' - ' . $comuneSedeLegale) : ''));
}
$sedeLegale = $this->parseAddress($sedeLegaleRaw);
$sedeOperativa = $this->parseAddress($data['sede_operativa'] ?? '');
$payload = [
'societa' => $societa,
'id_soggetto' => $this->cleanText($data['id_soggetto'] ?? ''),
'natura_giuridica_id' => $naturaId,
'partita_iva' => $this->cleanText($data['partita_iva'] ?? ''),
'partita_iva_normalizzata' => $piva,
'gruppo_societario' => $this->nullIfEmpty($data['gruppo_societario'] ?? ''),
'sito_internet' => $this->normalizeWebsite($data['sito_internet'] ?? ''),
'sede_legale_raw' => $this->nullIfEmpty($sedeLegaleRaw),
'sede_legale_indirizzo' => $sedeLegale['indirizzo'],
'sede_legale_cap' => $sedeLegale['cap'],
'sede_legale_citta' => $sedeLegale['citta'],
'sede_legale_provincia' => $sedeLegale['provincia'],
'sede_operativa_raw' => $this->nullIfEmpty($data['sede_operativa'] ?? ''),
'sede_operativa_indirizzo' => $sedeOperativa['indirizzo'],
'sede_operativa_cap' => $sedeOperativa['cap'],
'sede_operativa_citta' => $sedeOperativa['citta'],
'sede_operativa_provincia' => $sedeOperativa['provincia'],
'contatti_cliente' => $this->splitMultiValue($data['contatti_cliente'] ?? ''),
'contatti_cliente2' => $this->splitMultiValue($data['contatti_clienti2'] ?? ''),
'settori_raw' => $settori,
'extra_search_tokens' => array_values(array_unique(array_filter(array_merge(
$settori,
$this->splitMultiValue($data['contatti_cliente'] ?? ''),
$this->splitMultiValue($data['contatti_clienti2'] ?? ''),
[$natura, $data['gruppo_societario'] ?? '']
)))),
'source_filename' => $sourceFilename !== '' ? basename($sourceFilename) : null,
'source_imported_at' => now(),
];
$existing = AreraOperatore::query()->where('partita_iva_normalizzata', $piva)->first();
$operatore = AreraOperatore::query()->updateOrCreate(
['partita_iva_normalizzata' => $piva],
$payload
);
if ($settoreSync !== []) {
$operatore->settori()->sync($settoreSync);
}
$existing ? $updated++ : $imported++;
}
return [
'rows' => count($rows),
'imported' => $imported,
'updated' => $updated,
'skipped' => $skipped,
'sectors' => count($sectorIds),
'errors' => array_slice($errors, 0, 20),
];
}
private function normalizeHeader(string $value): string
{
$value = Str::of($value)->lower()->ascii()->replaceMatches('/[^a-z0-9]+/', '_')->trim('_')->toString();
return match ($value) {
'societa', 'societa_' => 'societa',
'ragione_sociale' => 'ragione_sociale',
'id_soggetto' => 'id_soggetto',
'natura_giuridica' => 'natura_giuridica',
'partita_iva' => 'partita_iva',
'gruppo_societario' => 'gruppo_societario',
'sito_internet' => 'sito_internet',
'sito_web' => 'sito_internet',
'sede_legale' => 'sede_legale',
'comune_sede_legale' => 'comune_sede_legale',
'indirizzo' => 'indirizzo',
'sede_operativa' => 'sede_operativa',
'settori_attivita' => 'settori_attivita',
'contatti_cliente' => 'contatti_cliente',
'contatti_clienti2', 'contatti_cliente2' => 'contatti_clienti2',
default => $value,
};
}
private function cleanText(string $value): string
{
return trim(preg_replace('/\s+/', ' ', $value) ?? $value);
}
private function nullIfEmpty(string $value): ?string
{
$value = $this->cleanText($value);
return $value !== '' ? $value : null;
}
private function normalizeVat(string $value): string
{
return preg_replace('/\D+/', '', $value) ?? '';
}
/** @return list<string> */
private function splitMultiValue(string $value): array
{
return array_values(array_unique(array_filter(array_map(
fn(string $part): string => $this->cleanText($part),
preg_split('/;|\R/', $value) ?: []
))));
}
private function normalizeWebsite(string $value): ?string
{
$value = $this->cleanText($value);
if ($value === '') {
return null;
}
return preg_match('/^https?:\/\//i', $value) ? $value : 'https://' . $value;
}
private function sectorSlug(string $value): string
{
$base = Str::slug($value);
if ($base === '') {
$base = 'settore';
}
$hash = substr(sha1($value), 0, 10);
$prefix = Str::limit($base, 175, '');
return $prefix . '-' . $hash;
}
/** @return array{indirizzo:?string, cap:?string, citta:?string, provincia:?string} */
private function parseAddress(string $value): array
{
$value = $this->cleanText($value);
$out = ['indirizzo' => null, 'cap' => null, 'citta' => null, 'provincia' => null];
if ($value === '') {
return $out;
}
$parts = array_map('trim', explode(' - ', $value, 2));
$out['indirizzo'] = $parts[0] !== '' ? $parts[0] : null;
$tail = $parts[1] ?? '';
if ($tail !== '' && preg_match('/^(\d{5})\s+(.+?)(?:\s+\(([^)]+)\))?$/u', $tail, $matches)) {
$out['cap'] = $matches[1];
$out['citta'] = trim($matches[2]);
$out['provincia'] = isset($matches[3]) ? trim($matches[3]) : null;
}
return $out;
}
}