346 lines
14 KiB
PHP
Executable File
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;
|
|
}
|
|
}
|