option('series')); $source = (string) $this->option('source'); $dryRun = (bool) $this->option('dry-run'); $url = trim((string) ($this->option('url') ?: config('services.istat.api_url', ''))); $file = trim((string) ($this->option('file') ?: '')); if ($url === '' && $file === '') { $this->error('Specifica --url oppure --file (oppure configura ISTAT_API_URL).'); return self::FAILURE; } try { $payload = $this->loadPayload($url, $file); $rows = $this->extractRows($payload); if (empty($rows)) { $this->warn('Nessuna riga valida trovata nella sorgente.'); return self::SUCCESS; } $upserts = []; foreach ($rows as $row) { $normalized = $this->normalizeRow($row); if (! $normalized) { continue; } $upserts[] = [ 'codice_serie' => $series, 'anno' => $normalized['anno'], 'mese' => $normalized['mese'], 'indice' => $normalized['indice'], 'fonte' => $source, 'data_pubblicazione' => $normalized['data_pubblicazione'], 'payload' => $row, 'sincronizzato_il' => now(), 'created_at' => now(), 'updated_at' => now(), ]; } if (empty($upserts)) { $this->warn('Righe presenti ma nessuna convertibile in (anno,mese,indice).'); return self::SUCCESS; } if ($dryRun) { $this->info('Dry-run: ' . count($upserts) . ' righe parsate.'); $this->table(['serie', 'anno', 'mese', 'indice'], collect($upserts)->take(20)->map(fn($r) => [ $r['codice_serie'], $r['anno'], $r['mese'], $r['indice'], ])->all()); return self::SUCCESS; } IstatIndiceMensile::query()->upsert( $upserts, ['codice_serie', 'anno', 'mese'], ['indice', 'fonte', 'data_pubblicazione', 'payload', 'sincronizzato_il', 'updated_at'] ); $this->info('Sincronizzazione completata: ' . count($upserts) . ' righe upsertate.'); return self::SUCCESS; } catch (\Throwable $e) { $this->error('Errore sincronizzazione ISTAT: ' . $e->getMessage()); return self::FAILURE; } } private function loadPayload(string $url, string $file): array { if ($file !== '') { if (! is_file($file)) { throw new \RuntimeException('File non trovato: ' . $file); } $decoded = json_decode((string) file_get_contents($file), true); if (! is_array($decoded)) { throw new \RuntimeException('JSON file non valido.'); } return $decoded; } $timeout = (int) config('services.istat.timeout', 30); $resp = Http::timeout(max(5, $timeout))->get($url); if (! $resp->successful()) { throw new \RuntimeException('HTTP ' . $resp->status() . ' da sorgente ISTAT.'); } $decoded = $resp->json(); if (! is_array($decoded)) { throw new \RuntimeException('Risposta JSON non valida.'); } return $decoded; } private function extractRows(array $payload): array { if (isset($payload[0]) && is_array($payload[0])) { return $payload; } foreach (['data', 'rows', 'values', 'result'] as $k) { $v = Arr::get($payload, $k); if (is_array($v) && isset($v[0]) && is_array($v[0])) { return $v; } } return []; } private function normalizeRow(array $row): ?array { $anno = $row['anno'] ?? $row['year'] ?? null; $mese = $row['mese'] ?? $row['month'] ?? null; $indice = $row['indice'] ?? $row['value'] ?? $row['valore'] ?? null; $period = (string) ($row['periodo'] ?? $row['period'] ?? ''); if (($anno === null || $mese === null) && preg_match('/^(\d{4})[-\/](\d{1,2})$/', $period, $m)) { $anno = (int) $m[1]; $mese = (int) $m[2]; } if (! is_numeric($anno) || ! is_numeric($mese) || ! is_numeric($indice)) { return null; } $anno = (int) $anno; $mese = (int) $mese; $indice = (float) $indice; if ($anno < 1900 || $anno > 2300 || $mese < 1 || $mese > 12) { return null; } $pub = $row['data_pubblicazione'] ?? $row['published_at'] ?? null; $pubDate = null; if (is_string($pub) && trim($pub) !== '') { try { $pubDate = Carbon::parse($pub)->format('Y-m-d'); } catch (\Throwable) { $pubDate = null; } } return [ 'anno' => $anno, 'mese' => $mese, 'indice' => $indice, 'data_pubblicazione' => $pubDate, ]; } }