*/ public function ingestForFornitore(Fornitore $fornitore, int $limit = 50, bool $dryRun = false): array { $stats = [ 'products' => 0, 'images' => 0, 'documents' => 0, 'skipped_existing' => 0, 'skipped_unsupported' => 0, 'failed' => 0, ]; $products = Product::query() ->where('default_fornitore_id', (int) $fornitore->id) ->orderBy('id') ->limit(max(1, $limit)) ->get(['id', 'internal_code', 'meta']); foreach ($products as $product) { $stats['products']++; $catalog = is_array($product->meta['catalog'] ?? null) ? $product->meta['catalog'] : []; $links = is_array($catalog['private_source_links'] ?? null) ? $catalog['private_source_links'] : []; $imageResult = $this->ingestSingleAsset($product, $fornitore, (string) ($links['image_link'] ?? ''), 'image', $dryRun); $stats[$imageResult['bucket']]++; $documentResult = $this->ingestSingleAsset($product, $fornitore, (string) ($links['product_page'] ?? ''), 'document', $dryRun); $stats[$documentResult['bucket']]++; } return $stats; } /** * @return array{bucket:string} */ private function ingestSingleAsset(Product $product, Fornitore $fornitore, string $url, string $mediaType, bool $dryRun): array { $url = trim($url); if ($url === '' || ! str_starts_with($url, 'http')) { return ['bucket' => 'skipped_unsupported']; } $existing = ProductMedia::query() ->where('product_id', (int) $product->id) ->where('media_type', $mediaType) ->whereIn('disk', ['local', 'public']) ->exists(); if ($existing) { return ['bucket' => 'skipped_existing']; } if ($dryRun) { return ['bucket' => $mediaType === 'image' ? 'images' : 'documents']; } try { $response = Http::timeout(20) ->withHeaders(['User-Agent' => 'NetgesconCatalogBot/1.0']) ->get($url); } catch (\Throwable) { return ['bucket' => 'failed']; } if (! $response->successful()) { return ['bucket' => 'failed']; } $mime = strtolower(trim((string) $response->header('Content-Type'))); if ($mime !== '' && str_contains($mime, ';')) { $mime = trim((string) Str::before($mime, ';')); } if ($mediaType === 'image' && ! str_starts_with($mime, 'image/')) { return ['bucket' => 'skipped_unsupported']; } if ($mediaType === 'document' && ! str_contains($mime, 'pdf') && ! str_ends_with(Str::lower(parse_url($url, PHP_URL_PATH) ?: ''), '.pdf')) { return ['bucket' => 'skipped_unsupported']; } $extension = $this->guessExtension($url, $mime, $mediaType); $folder = $this->buildMediaFolder($fornitore, $product, $mediaType); $filename = Str::slug((string) ($product->internal_code ?? ('product-' . $product->id))) . '-' . substr(sha1($url), 0, 12) . '.' . $extension; $path = $folder . '/' . $filename; Storage::disk('public')->put($path, $response->body()); ProductMedia::query()->create([ 'product_id' => (int) $product->id, 'media_type' => $mediaType, 'disk' => 'public', 'path' => $path, 'title' => $mediaType === 'image' ? 'Immagine catalogo interna' : 'Scheda prodotto interna', 'mime_type' => $mime !== '' ? $mime : null, 'size_bytes' => strlen($response->body()), 'sort_order' => 0, 'meta' => [ 'source_url' => $url, 'source_system' => 'private_catalog_link', 'internalized' => true, ], ]); return ['bucket' => $mediaType === 'image' ? 'images' : 'documents']; } private function buildMediaFolder(Fornitore $fornitore, Product $product, string $mediaType): string { $base = ArchivioPaths::fornitoreBase($fornitore); if (! is_string($base) || $base === '') { $base = 'catalogo/fornitori/' . ($fornitore->codice_univoco ?: $fornitore->id); } return trim('catalogo-interno/' . trim($base, '/') . '/prodotti/' . ($product->internal_code ?: $product->id) . '/' . $mediaType, '/'); } private function guessExtension(string $url, string $mime, string $mediaType): string { $path = Str::lower((string) (parse_url($url, PHP_URL_PATH) ?: '')); $ext = pathinfo($path, PATHINFO_EXTENSION); if (is_string($ext) && $ext !== '') { return $ext; } return match (true) { str_contains($mime, 'png') => 'png', str_contains($mime, 'webp') => 'webp', str_contains($mime, 'jpeg'), str_contains($mime, 'jpg') => 'jpg', $mediaType === 'document' => 'pdf', default => 'bin', }; } }