true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 20, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_HTTPHEADER => [ 'Accept: application/json', 'User-Agent: KapvoePortfolioAPI/1.0' ], CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, ]); $response = curl_exec($ch); if ($response === false) { $error = curl_error($ch); curl_close($ch); throw new RuntimeException("Error cURL: {$error}"); } $statusCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($statusCode < 200 || $statusCode >= 300) { throw new RuntimeException("Resposta remota no vàlida. HTTP {$statusCode}"); } $json = json_decode($response, true); if (!is_array($json)) { throw new RuntimeException('La resposta remota no és JSON vàlid.'); } return $json; } function normalizeBool($value): bool { $v = mb_strtolower(trim((string)($value ?? ''))); return in_array($v, ['1', 'true', 'sí', 'si', 'yes', 'y'], true); } function normalizeText($value): string { return trim((string)($value ?? '')); } function normalizePriceNumber($value): ?float { if ($value === null || $value === '') { return null; } $raw = str_replace(['€', ' '], '', (string)$value); $raw = str_replace(',', '.', $raw); return is_numeric($raw) ? (float)$raw : null; } function normalizeStock($value): int { if ($value === null || $value === '') { return 0; } return max(0, (int)$value); } function normalizeProduct(array $p): array { $priceNumber = normalizePriceNumber($p['europe_price_number'] ?? $p['PREU'] ?? $p['PREU EUROPA'] ?? null); $priceText = normalizeText($p['europe_price_text'] ?? ''); if ($priceText === '' && $priceNumber !== null) { $priceText = number_format($priceNumber, 2, ',', '.') . ' €'; } $image = normalizeText($p['image_url'] ?? $p['IMAGE_URL'] ?? ''); $filename = basename($image); return [ 'product_code' => normalizeText($p['product_code'] ?? $p['Product Code'] ?? $p['PRODUCT CODE'] ?? ''), 'model' => normalizeText($p['model'] ?? $p['MODEL'] ?? ''), 'category' => normalizeText($p['category'] ?? $p['CATEGORIA'] ?? $p['CATEGORY'] ?? ''), 'colors' => normalizeText($p['colors'] ?? $p['COLORS'] ?? $p['VIDRE'] ?? ''), 'description' => normalizeText($p['description'] ?? $p['DESCRIPCIO'] ?? $p['DESCRIPTION'] ?? ''), 'image_url' => $filename ? "https://kapvoe-portfoli.treblarella.org/assets/products/$filename" : '', 'stock' => normalizeStock($p['stock'] ?? $p['STOCK'] ?? 0), 'top_vendes' => normalizeBool($p['top_vendes'] ?? $p['TOP_VENDES'] ?? false), 'europe_price_number' => $priceNumber, 'europe_price_text' => $priceText ]; } try { if (!is_dir($cacheDir)) { mkdir($cacheDir, 0775, true); } $forceRefresh = isset($_GET['refresh']) && $_GET['refresh'] === '1'; $useCache = !$forceRefresh && file_exists($cacheFile) && (time() - filemtime($cacheFile) < $cacheTtl); if ($useCache) { $cached = json_decode((string)file_get_contents($cacheFile), true); if (is_array($cached)) { respond(200, $cached); } } if ($googleScriptUrl === 'POSA_AQUI_LA_TEVA_URL_DE_GOOGLE_SCRIPT') { throw new RuntimeException('Has de configurar la URL del Google Script a api/products.php'); } $remote = fetchRemoteJson($googleScriptUrl); if (($remote['ok'] ?? null) !== true || !isset($remote['products']) || !is_array($remote['products'])) { throw new RuntimeException('El JSON remot no té el format esperat.'); } $normalizedProducts = array_map('normalizeProduct', $remote['products']); $normalizedProducts = array_values(array_filter($normalizedProducts, function (array $p): bool { return $p['product_code'] !== ''; })); $payload = [ 'ok' => true, 'source' => 'php-proxy', 'cached' => false, 'updated_at' => $remote['updated_at'] ?? date('c'), 'count' => count($normalizedProducts), 'products' => $normalizedProducts ]; file_put_contents($cacheFile, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); respond(200, $payload); } catch (Throwable $e) { if (file_exists($cacheFile)) { $cached = json_decode((string)file_get_contents($cacheFile), true); if (is_array($cached)) { $cached['cached'] = true; $cached['warning'] = 'S\'està retornant cache perquè la font remota ha fallat.'; $cached['error_detail'] = $e->getMessage(); respond(200, $cached); } } respond(500, [ 'ok' => false, 'error' => 'No s\'ha pogut obtenir el catàleg.', 'detail' => $e->getMessage() ]); }