false, 'error' => 'Falta el fitxer config.php' ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } /** @var array $config */ $config = require $configFile; return $config; } function kapvoe_json_input(): array { $raw = file_get_contents('php://input') ?: '{}'; $decoded = json_decode($raw, true); return is_array($decoded) ? $decoded : []; } function kapvoe_json_response(array $payload, int $status = 200): never { http_response_code($status); header('Content-Type: application/json; charset=utf-8'); echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } function kapvoe_require_post(): void { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { kapvoe_json_response(['ok' => false, 'error' => 'Mètode no permès'], 405); } } function kapvoe_validate_required(array $data, array $fields): array { $errors = []; foreach ($fields as $field) { if (!isset($data[$field]) || trim((string)$data[$field]) === '') { $errors[$field] = 'Camp obligatori'; } } return $errors; } function kapvoe_orders_csv_path(array $config): string { $dir = rtrim((string)$config['orders_storage_dir'], DIRECTORY_SEPARATOR); if (!is_dir($dir)) { mkdir($dir, 0775, true); } return $dir . DIRECTORY_SEPARATOR . 'orders.csv'; } function kapvoe_orders_csv_header(): array { return [ 'order_id', 'created_at', 'product_code', 'product_name', 'product_image_url', 'unit_price', 'quantity', 'subtotal', 'shipping_method', 'shipping_cost', 'total_amount', 'customer_name', 'address', 'postal_code', 'city', 'province', 'phone', 'email', 'analytics_session_id', 'analytics_page_url', 'analytics_referrer', 'analytics_user_agent', 'analytics_utm_source', 'analytics_utm_medium', 'analytics_utm_campaign', 'analytics_device_type', 'analytics_page_type', 'payment_status', 'stripe_session_id', 'payment_intent_id', 'stock_updated', 'stock_updated_at', 'webhook_processed_at', 'customer_email_sent', 'admin_email_sent', 'email_notifications_sent_at', ]; } function kapvoe_upgrade_orders_csv_schema(array $config): void { $path = kapvoe_orders_csv_path($config); if (!file_exists($path)) { return; } $fh = fopen($path, 'rb'); if (!$fh) { return; } $rows = []; while (($row = fgetcsv($fh, 0, ';')) !== false) { $rows[] = $row; } fclose($fh); if (!$rows) { return; } $targetHeader = kapvoe_orders_csv_header(); $currentHeader = $rows[0]; if ($currentHeader === $targetHeader) { return; } $legacyHeader = [ 'order_id', 'created_at', 'product_code', 'product_name', 'unit_price', 'quantity', 'customer_name', 'address', 'postal_code', 'city', 'province', 'phone', 'email', 'payment_status', 'stripe_session_id', 'payment_intent_id', 'stock_updated', 'stock_updated_at', 'webhook_processed_at', ]; $headerWithoutImage = [ 'order_id', 'created_at', 'product_code', 'product_name', 'unit_price', 'quantity', 'subtotal', 'shipping_method', 'shipping_cost', 'total_amount', 'customer_name', 'address', 'postal_code', 'city', 'province', 'phone', 'email', 'payment_status', 'stripe_session_id', 'payment_intent_id', 'stock_updated', 'stock_updated_at', 'webhook_processed_at', ]; $newRows = [$targetHeader]; for ($i = 1; $i < count($rows); $i++) { $row = $rows[$i]; if ($currentHeader === $legacyHeader) { if (count($row) === count($legacyHeader)) { $normalized = [ $row[0] ?? '', $row[1] ?? '', $row[2] ?? '', $row[3] ?? '', '', $row[4] ?? '', $row[5] ?? '', '', '', '', '', $row[6] ?? '', $row[7] ?? '', $row[8] ?? '', $row[9] ?? '', $row[10] ?? '', $row[11] ?? '', $row[12] ?? '', $row[13] ?? '', $row[14] ?? '', $row[15] ?? '', $row[16] ?? '', $row[17] ?? '', $row[18] ?? '', ]; } elseif (count($row) === count($legacyHeader) + 1) { $normalized = [ $row[0] ?? '', $row[1] ?? '', $row[2] ?? '', $row[3] ?? '', $row[4] ?? '', $row[5] ?? '', $row[6] ?? '', '', '', '', '', $row[7] ?? '', $row[8] ?? '', $row[9] ?? '', $row[10] ?? '', $row[11] ?? '', $row[12] ?? '', $row[13] ?? '', $row[14] ?? '', $row[15] ?? '', $row[16] ?? '', $row[17] ?? '', $row[18] ?? '', $row[19] ?? '', ]; } else { $normalized = array_pad(array_slice($row, 0, count($targetHeader)), count($targetHeader), ''); } } elseif ($currentHeader === $headerWithoutImage) { if (count($row) === count($headerWithoutImage)) { $normalized = [ $row[0] ?? '', $row[1] ?? '', $row[2] ?? '', $row[3] ?? '', '', $row[4] ?? '', $row[5] ?? '', $row[6] ?? '', $row[7] ?? '', $row[8] ?? '', $row[9] ?? '', $row[10] ?? '', $row[11] ?? '', $row[12] ?? '', $row[13] ?? '', $row[14] ?? '', $row[15] ?? '', $row[16] ?? '', $row[17] ?? '', $row[18] ?? '', $row[19] ?? '', $row[20] ?? '', $row[21] ?? '', $row[22] ?? '', ]; } elseif (count($row) === count($headerWithoutImage) + 1) { $normalized = [ $row[0] ?? '', $row[1] ?? '', $row[2] ?? '', $row[3] ?? '', $row[4] ?? '', $row[5] ?? '', $row[6] ?? '', $row[7] ?? '', $row[8] ?? '', $row[9] ?? '', $row[10] ?? '', $row[11] ?? '', $row[12] ?? '', $row[13] ?? '', $row[14] ?? '', $row[15] ?? '', $row[16] ?? '', $row[17] ?? '', $row[18] ?? '', $row[19] ?? '', $row[20] ?? '', $row[21] ?? '', $row[22] ?? '', $row[23] ?? '', ]; } else { $normalized = array_pad(array_slice($row, 0, count($targetHeader)), count($targetHeader), ''); } } else { $assoc = []; $limit = min(count($currentHeader), count($row)); for ($j = 0; $j < $limit; $j++) { $assoc[$currentHeader[$j]] = $row[$j]; } $normalized = []; foreach ($targetHeader as $column) { $normalized[] = $assoc[$column] ?? ''; } } $newRows[] = $normalized; } $fh = fopen($path, 'wb'); if (!$fh) { return; } foreach ($newRows as $row) { fputcsv($fh, $row, ';'); } fclose($fh); } function kapvoe_append_order(array $config, array $order): void { $path = kapvoe_orders_csv_path($config); kapvoe_upgrade_orders_csv_schema($config); $exists = file_exists($path); $fh = fopen($path, 'ab'); if (!$fh) { throw new RuntimeException('No s\'ha pogut obrir orders.csv'); } if (!$exists) { fputcsv($fh, kapvoe_orders_csv_header(), ';'); } $row = []; foreach (kapvoe_orders_csv_header() as $column) { $row[] = $order[$column] ?? ''; } fputcsv($fh, $row, ';'); fclose($fh); } function kapvoe_update_order_status(array $config, string $sessionId, string $newStatus, string $paymentIntent = ''): void { $path = kapvoe_orders_csv_path($config); kapvoe_upgrade_orders_csv_schema($config); if (!file_exists($path)) { return; } $rows = []; $fh = fopen($path, 'rb'); if (!$fh) { return; } while (($row = fgetcsv($fh, 0, ';')) !== false) { $rows[] = $row; } fclose($fh); if (count($rows) < 2) { return; } $header = $rows[0]; $sessionIdx = array_search('stripe_session_id', $header, true); $statusIdx = array_search('payment_status', $header, true); $intentIdx = array_search('payment_intent_id', $header, true); if ($sessionIdx === false || $statusIdx === false) { return; } for ($i = 1; $i < count($rows); $i++) { while (count($rows[$i]) < count($header)) { $rows[$i][] = ''; } if (($rows[$i][$sessionIdx] ?? '') === $sessionId) { $rows[$i][$statusIdx] = $newStatus; if ($intentIdx !== false) { $rows[$i][$intentIdx] = $paymentIntent; } break; } } $fh = fopen($path, 'wb'); if (!$fh) { return; } foreach ($rows as $row) { while (count($row) < count($header)) { $row[] = ''; } fputcsv($fh, $row, ';'); } fclose($fh); } function kapvoe_get_order_by_session_id(array $config, string $sessionId): ?array { $path = kapvoe_orders_csv_path($config); kapvoe_upgrade_orders_csv_schema($config); if (!file_exists($path)) { return null; } $fh = fopen($path, 'rb'); if (!$fh) { return null; } $header = fgetcsv($fh, 0, ';'); if (!$header) { fclose($fh); return null; } while (($row = fgetcsv($fh, 0, ';')) !== false) { while (count($row) < count($header)) { $row[] = ''; } $assoc = array_combine($header, $row); if (($assoc['stripe_session_id'] ?? '') === $sessionId) { fclose($fh); return $assoc ?: null; } } fclose($fh); return null; } function kapvoe_mark_order_stock_updated(array $config, string $sessionId): void { $path = kapvoe_orders_csv_path($config); kapvoe_upgrade_orders_csv_schema($config); if (!file_exists($path)) { return; } $rows = []; $fh = fopen($path, 'rb'); if (!$fh) { return; } while (($row = fgetcsv($fh, 0, ';')) !== false) { $rows[] = $row; } fclose($fh); if (count($rows) < 1) { return; } $header = $rows[0]; $sessionIdx = array_search('stripe_session_id', $header, true); $stockUpdatedIdx = array_search('stock_updated', $header, true); $stockUpdatedAtIdx = array_search('stock_updated_at', $header, true); $webhookProcessedIdx = array_search('webhook_processed_at', $header, true); if ($sessionIdx === false) { return; } if ($stockUpdatedIdx === false) { $header[] = 'stock_updated'; $stockUpdatedIdx = count($header) - 1; } if ($stockUpdatedAtIdx === false) { $header[] = 'stock_updated_at'; $stockUpdatedAtIdx = count($header) - 1; } if ($webhookProcessedIdx === false) { $header[] = 'webhook_processed_at'; $webhookProcessedIdx = count($header) - 1; } $rows[0] = $header; $now = date('Y-m-d H:i:s'); for ($i = 1; $i < count($rows); $i++) { while (count($rows[$i]) < count($header)) { $rows[$i][] = ''; } if (($rows[$i][$sessionIdx] ?? '') === $sessionId) { if (($rows[$i][$stockUpdatedIdx] ?? '') === '1') { return; } $rows[$i][$stockUpdatedIdx] = '1'; $rows[$i][$stockUpdatedAtIdx] = $now; $rows[$i][$webhookProcessedIdx] = $now; break; } } $fh = fopen($path, 'wb'); if (!$fh) { return; } foreach ($rows as $row) { while (count($row) < count($header)) { $row[] = ''; } fputcsv($fh, $row, ';'); } fclose($fh); } function kapvoe_format_money($value): string { return number_format((float)$value, 2, ',', '.') . ' EUR'; } function kapvoe_mail_html_escape(?string $value): string { return htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); } function kapvoe_mail_base_url(array $config): string { $successUrl = (string)($config['success_url'] ?? ''); if ($successUrl === '') { return ''; } $parts = parse_url($successUrl); if (!is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) { return ''; } $base = $parts['scheme'] . '://' . $parts['host']; if (!empty($parts['port'])) { $base .= ':' . $parts['port']; } return $base; } function kapvoe_build_order_email_html(array $config, array $order, string $sessionId, bool $forAdmin): string { $brand = (string)($config['mail_from_name'] ?? 'Blood Bros Sports'); $productCode = kapvoe_mail_html_escape((string)($order['product_code'] ?? '')); $customerName = kapvoe_mail_html_escape((string)($order['customer_name'] ?? '')); $customerEmail = kapvoe_mail_html_escape((string)($order['email'] ?? '')); $phone = kapvoe_mail_html_escape((string)($order['phone'] ?? '')); $subtotal = kapvoe_format_money($order['subtotal'] ?? $order['unit_price'] ?? '0'); $shippingCost = kapvoe_format_money($order['shipping_cost'] ?? '0'); $totalAmount = kapvoe_format_money($order['total_amount'] ?? $order['unit_price'] ?? '0'); $shippingMethod = (string)($order['shipping_method'] ?? 'pickup'); $shippingLabel = $shippingMethod === 'shipping' ? 'Enviament' : 'Entrega en persona'; $sessionIdSafe = kapvoe_mail_html_escape($sessionId); $imageUrl = trim((string)($order['product_image_url'] ?? '')); $successLink = (string)($config['success_url'] ?? ''); if ($successLink !== '' && $sessionId !== '') { $successLink .= '?session_id=' . rawurlencode($sessionId); } $deliveryBlock = $shippingMethod === 'shipping' ? '
' . '
Adreca d\'enviament
' . '
' . kapvoe_mail_html_escape((string)($order['address'] ?? '')) . '
' . kapvoe_mail_html_escape((string)($order['postal_code'] ?? '')) . ' ' . kapvoe_mail_html_escape((string)($order['city'] ?? '')) . '
' . kapvoe_mail_html_escape((string)($order['province'] ?? '')) . '
' : '
' . '
Entrega
' . '
Quedarem amb tu per WhatsApp per entregar-te les ulleres en persona a Lleida o Mollerussa.
' . '
'; $intro = $forAdmin ? 'S\'ha confirmat una nova compra al web. Tens el resum complet de la comanda a continuacio.' : 'Hem rebut la teva compra. Aqui tens un resum de la comanda confirmada.'; $badge = $forAdmin ? 'Nova comanda cobrada' : 'Compra confirmada'; $cta = $successLink !== '' ? 'Veure resum' : ''; return ' ' . kapvoe_mail_html_escape($brand) . '
' . kapvoe_mail_html_escape($brand) . '
Pagament correcte
' . kapvoe_mail_html_escape($intro) . '
' . kapvoe_mail_html_escape($badge) . '
' . ($imageUrl !== '' ? '' . $productCode . '' : '
Sense imatge
') . '
' . $productCode . '
Sessio Stripe: ' . $sessionIdSafe . '
Metode: ' . kapvoe_mail_html_escape($shippingLabel) . '
Resum economic
Subtotal producte' . kapvoe_mail_html_escape($subtotal) . '
Enviament' . kapvoe_mail_html_escape($shippingCost) . '
Total' . kapvoe_mail_html_escape($totalAmount) . '
Dades de contacte
Nom
' . $customerName . '
Telefon
' . $phone . '
Correu electronic
' . $customerEmail . '
' . $deliveryBlock . '
' . ($cta !== '' ? '
' . $cta . '
' : '') . '
'; } function kapvoe_mail_log_path(array $config): string { $dir = rtrim((string)($config['orders_storage_dir'] ?? (__DIR__ . '/data')), DIRECTORY_SEPARATOR); if (!is_dir($dir)) { @mkdir($dir, 0775, true); } return $dir . DIRECTORY_SEPARATOR . 'mail.log'; } function kapvoe_log_mail_event(array $config, string $message): void { $line = '[' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL; $path = kapvoe_mail_log_path($config); $written = @file_put_contents($path, $line, FILE_APPEND); if ($written === false) { $fallbackPath = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'kapvoe-mail.log'; @file_put_contents($fallbackPath, $line, FILE_APPEND); error_log('[KAPVOE MAIL] ' . trim($message) . ' | primary_log=' . $path . ' | fallback_log=' . $fallbackPath); return; } error_log('[KAPVOE MAIL] ' . trim($message) . ' | log=' . $path); } function kapvoe_analytics_log_path(array $config): string { $dir = rtrim((string)($config['orders_storage_dir'] ?? (__DIR__ . '/data')), DIRECTORY_SEPARATOR); if (!is_dir($dir)) { @mkdir($dir, 0775, true); } return $dir . DIRECTORY_SEPARATOR . 'analytics.log'; } function kapvoe_log_analytics_event(array $config, string $message): void { $line = '[' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL; $path = kapvoe_analytics_log_path($config); @file_put_contents($path, $line, FILE_APPEND); error_log('[KAPVOE ANALYTICS] ' . trim($message) . ' | log=' . $path); } function kapvoe_internal_notification_log_path(array $config): string { $configuredPath = trim((string)($config['internal_notification_log_path'] ?? '')); if ($configuredPath !== '') { return $configuredPath; } $dir = rtrim((string)($config['orders_storage_dir'] ?? (__DIR__ . '/data')), DIRECTORY_SEPARATOR); if (!is_dir($dir)) { @mkdir($dir, 0775, true); } return $dir . DIRECTORY_SEPARATOR . 'internal-notifications.log'; } function kapvoe_log_internal_notification(array $config, array $payload): bool { $path = kapvoe_internal_notification_log_path($config); $line = '[' . date('Y-m-d H:i:s') . '] ' . json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL; $written = @file_put_contents($path, $line, FILE_APPEND); if ($written === false) { error_log('[KAPVOE INTERNAL] No s\'ha pogut escriure el log intern: ' . $path); return false; } error_log('[KAPVOE INTERNAL] log=' . $path); return true; } function kapvoe_build_internal_notification_payload(array $order, string $sessionId): array { return [ 'event' => 'order_paid', 'session_id' => $sessionId, 'order_id' => (string)($order['order_id'] ?? ''), 'product_code' => (string)($order['product_code'] ?? ''), 'product_name' => (string)($order['product_name'] ?? ''), 'customer_name' => (string)($order['customer_name'] ?? ''), 'customer_email' => (string)($order['email'] ?? ''), 'phone' => (string)($order['phone'] ?? ''), 'shipping_method' => (string)($order['shipping_method'] ?? ''), 'total_amount' => (string)($order['total_amount'] ?? ''), 'created_at' => (string)($order['created_at'] ?? ''), 'sent_at' => date('c'), ]; } function kapvoe_send_internal_notification(array $config, array $order, string $sessionId): array { $enabled = (bool)($config['internal_notification_enabled'] ?? true); if (!$enabled) { return [ 'enabled' => false, 'sent' => false, 'channel' => 'disabled', ]; } $payload = kapvoe_build_internal_notification_payload($order, $sessionId); $webhookUrl = trim((string)($config['internal_notification_webhook_url'] ?? '')); $token = trim((string)($config['internal_notification_webhook_token'] ?? '')); $timeout = max(5, (int)($config['internal_notification_timeout'] ?? 10)); if ($webhookUrl === '') { $logged = kapvoe_log_internal_notification($config, $payload); return [ 'enabled' => true, 'sent' => $logged, 'channel' => 'log', ]; } try { $headers = []; if ($token !== '') { $headers[] = 'Authorization: Bearer ' . $token; } $result = kapvoe_post_json($webhookUrl, $headers, $payload, $timeout); $statusCode = (int)($result['status_code'] ?? 0); if ($statusCode < 200 || $statusCode >= 300) { throw new RuntimeException('Webhook intern ha respost amb HTTP ' . $statusCode); } kapvoe_log_mail_event($config, 'INTERNAL webhook OK session=' . $sessionId . ' url=' . $webhookUrl); return [ 'enabled' => true, 'sent' => true, 'channel' => 'webhook', ]; } catch (Throwable $e) { kapvoe_log_mail_event($config, 'INTERNAL webhook FAIL session=' . $sessionId . ' error=' . $e->getMessage()); $logged = kapvoe_log_internal_notification($config, $payload + ['webhook_error' => $e->getMessage()]); return [ 'enabled' => true, 'sent' => $logged, 'channel' => $logged ? 'log-fallback' : 'failed', 'error' => $e->getMessage(), ]; } } function kapvoe_encode_email_header(string $value): string { if ($value === '' || !preg_match('/[^\x20-\x7E]/', $value)) { return $value; } return '=?UTF-8?B?' . base64_encode($value) . '?='; } function kapvoe_normalize_email_text(string $htmlBody): string { $text = str_replace(['
', '
', '
'], "\n", $htmlBody); $text = html_entity_decode(strip_tags($text), ENT_QUOTES | ENT_HTML5, 'UTF-8'); $text = preg_replace("/\r\n|\r/u", "\n", $text) ?? $text; $text = preg_replace("/\n{3,}/u", "\n\n", $text) ?? $text; return trim($text); } function kapvoe_build_email_message( string $fromEmail, string $fromName, string $replyTo, string $toEmail, string $subject, string $htmlBody ): array { $plainText = kapvoe_normalize_email_text($htmlBody); $boundary = 'kapvoe_' . bin2hex(random_bytes(12)); $encodedSubject = kapvoe_encode_email_header($subject); $encodedFromName = kapvoe_encode_email_header($fromName); $headers = [ 'MIME-Version: 1.0', 'From: ' . $encodedFromName . ' <' . $fromEmail . '>', 'Reply-To: ' . $replyTo, 'To: <' . $toEmail . '>', 'Subject: ' . $encodedSubject, 'Date: ' . date(DATE_RFC2822), 'Message-ID: <' . bin2hex(random_bytes(16)) . '@' . ($_SERVER['SERVER_NAME'] ?? 'localhost') . '>', 'Content-Type: multipart/alternative; boundary="' . $boundary . '"', ]; $message = "--{$boundary}\r\n" . "Content-Type: text/plain; charset=UTF-8\r\n" . "Content-Transfer-Encoding: 8bit\r\n\r\n" . $plainText . "\r\n\r\n" . "--{$boundary}\r\n" . "Content-Type: text/html; charset=UTF-8\r\n" . "Content-Transfer-Encoding: 8bit\r\n\r\n" . $htmlBody . "\r\n\r\n" . "--{$boundary}--\r\n"; return [ 'headers' => $headers, 'message' => $message, 'subject' => $encodedSubject, ]; } function kapvoe_post_json(string $url, array $headers, array $payload, int $timeout = 15): array { if (!function_exists('curl_init')) { throw new RuntimeException('cURL no esta disponible al PHP'); } $ch = curl_init($url); if ($ch === false) { throw new RuntimeException('No s\'ha pogut inicialitzar cURL'); } curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), CURLOPT_HTTPHEADER => array_merge([ 'Content-Type: application/json', 'Accept: application/json', ], $headers), CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_TIMEOUT => $timeout, CURLOPT_CONNECTTIMEOUT => min(10, $timeout), ]); $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); $decoded = json_decode($response, true); return [ 'status_code' => $statusCode, 'body' => $response, 'json' => is_array($decoded) ? $decoded : null, ]; } function kapvoe_analytics_enabled(array $config): bool { return (bool)($config['analytics_enabled'] ?? false) && trim((string)($config['analytics_sync_url'] ?? '')) !== ''; } function kapvoe_send_analytics_payload(array $config, array $payload): bool { if (!kapvoe_analytics_enabled($config)) { return false; } $url = trim((string)($config['analytics_sync_url'] ?? '')); $token = trim((string)($config['analytics_sync_token'] ?? '')); $timeout = max(5, (int)($config['analytics_timeout'] ?? 10)); $headers = []; $requestPayload = $payload; if ($token !== '') { $headers[] = 'Authorization: Bearer ' . $token; $requestPayload['token'] = $token; } $result = kapvoe_post_json($url, $headers, $requestPayload, $timeout); $statusCode = (int)($result['status_code'] ?? 0); if ($statusCode < 200 || $statusCode >= 300) { throw new RuntimeException('Analytics HTTP ' . $statusCode); } return true; } function kapvoe_track_analytics_event(array $config, array $payload): bool { if (!kapvoe_analytics_enabled($config)) { return false; } try { $sent = kapvoe_send_analytics_payload($config, $payload); kapvoe_log_analytics_event( $config, 'event=' . (string)($payload['event_type'] ?? 'unknown') . ' session=' . (string)($payload['session_id'] ?? '') . ' order=' . (string)($payload['order_id'] ?? '') . ' sent=' . ($sent ? '1' : '0') ); return $sent; } catch (Throwable $e) { kapvoe_log_analytics_event( $config, 'ERROR event=' . (string)($payload['event_type'] ?? 'unknown') . ' session=' . (string)($payload['session_id'] ?? '') . ' order=' . (string)($payload['order_id'] ?? '') . ' ' . $e->getMessage() ); return false; } } function kapvoe_build_payment_success_event(array $order, string $sessionId): array { return [ 'timestamp' => date('c'), 'event_type' => 'payment_success', 'product_code' => (string)($order['product_code'] ?? ''), 'model' => (string)($order['product_name'] ?? ''), 'category' => (string)($order['analytics_page_type'] ?? ''), 'price' => (string)($order['unit_price'] ?? ''), 'stock' => '', 'page_url' => (string)($order['analytics_page_url'] ?? ''), 'user_agent' => (string)($order['analytics_user_agent'] ?? ''), 'session_id' => (string)($order['analytics_session_id'] ?? ''), 'event_id' => 'evt_' . bin2hex(random_bytes(8)), 'order_id' => (string)($order['order_id'] ?? ''), 'stripe_session_id' => $sessionId, 'payment_status' => (string)($order['payment_status'] ?? 'paid'), 'quantity' => (string)($order['quantity'] ?? '1'), 'shipping_method' => (string)($order['shipping_method'] ?? ''), 'total_amount' => (string)($order['total_amount'] ?? ''), 'referrer' => (string)($order['analytics_referrer'] ?? ''), 'utm_source' => (string)($order['analytics_utm_source'] ?? ''), 'utm_medium' => (string)($order['analytics_utm_medium'] ?? ''), 'utm_campaign' => (string)($order['analytics_utm_campaign'] ?? ''), 'device_type' => (string)($order['analytics_device_type'] ?? ''), 'page_type' => (string)($order['analytics_page_type'] ?? ''), ]; } function kapvoe_send_via_resend( array $config, string $fromEmail, string $fromName, string $replyTo, string $toEmail, string $subject, string $htmlBody ): bool { $apiKey = trim((string)($config['resend_api_key'] ?? '')); $apiUrl = trim((string)($config['resend_api_url'] ?? 'https://api.resend.com/emails')); $timeout = max(5, (int)($config['smtp_timeout'] ?? 15)); if ($apiKey === '') { throw new RuntimeException('Falta configurar resend_api_key'); } $payload = [ 'from' => trim($fromName) !== '' ? ($fromName . ' <' . $fromEmail . '>') : $fromEmail, 'to' => [$toEmail], 'subject' => $subject, 'html' => $htmlBody, 'text' => kapvoe_normalize_email_text($htmlBody), 'reply_to' => [$replyTo], ]; $result = kapvoe_post_json($apiUrl, [ 'Authorization: Bearer ' . $apiKey, ], $payload, $timeout); if (($result['status_code'] ?? 0) < 200 || ($result['status_code'] ?? 0) >= 300) { $json = $result['json'] ?? []; $message = ''; if (is_array($json)) { $message = (string)($json['message'] ?? $json['error'] ?? ''); } if ($message === '') { $message = (string)($result['body'] ?? 'Resposta invalida de Resend'); } throw new RuntimeException('Resend API error: ' . $message); } return true; } function kapvoe_smtp_expect($socket, array $expectedCodes, string $context): string { $response = ''; while (!feof($socket)) { $line = fgets($socket, 515); if ($line === false) { break; } $response .= $line; if (preg_match('/^\d{3} /', $line) === 1) { break; } } if ($response === '') { throw new RuntimeException('SMTP sense resposta en ' . $context); } $code = (int)substr($response, 0, 3); if (!in_array($code, $expectedCodes, true)) { throw new RuntimeException('SMTP error en ' . $context . ': ' . trim($response)); } return $response; } function kapvoe_smtp_write($socket, string $command): void { $written = fwrite($socket, $command); if ($written === false || $written < strlen($command)) { throw new RuntimeException('No s\'ha pogut escriure al servidor SMTP'); } } function kapvoe_send_via_smtp_curl( array $config, string $fromEmail, string $fromName, string $replyTo, string $toEmail, string $subject, string $htmlBody ): bool { if (!function_exists('curl_init')) { throw new RuntimeException('cURL no esta disponible al PHP'); } $host = trim((string)($config['smtp_host'] ?? '')); if ($host === '') { return false; } $port = (int)($config['smtp_port'] ?? 587); $username = trim((string)($config['smtp_username'] ?? '')); $password = (string)($config['smtp_password'] ?? ''); $encryption = strtolower(trim((string)($config['smtp_encryption'] ?? 'tls'))); $timeout = max(5, (int)($config['smtp_timeout'] ?? 15)); $allowInvalidCert = (bool)($config['smtp_allow_invalid_certificates'] ?? false); $built = kapvoe_build_email_message($fromEmail, $fromName, $replyTo, $toEmail, $subject, $htmlBody); $payload = implode("\r\n", $built['headers']) . "\r\n\r\n" . $built['message']; $urlScheme = $encryption === 'ssl' ? 'smtps' : 'smtp'; $url = $urlScheme . '://' . $host . ':' . $port; $payloadLength = strlen($payload); $offset = 0; $ch = curl_init($url); if ($ch === false) { throw new RuntimeException('No s\'ha pogut inicialitzar cURL SMTP'); } curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_UPLOAD, true); curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, min(10, $timeout)); curl_setopt($ch, CURLOPT_MAIL_FROM, '<' . $fromEmail . '>'); curl_setopt($ch, CURLOPT_MAIL_RCPT, ['<' . $toEmail . '>']); curl_setopt($ch, CURLOPT_INFILESIZE, $payloadLength); curl_setopt($ch, CURLOPT_READFUNCTION, static function ($curl, $fd, $length) use (&$payload, &$offset, $payloadLength) { if ($offset >= $payloadLength) { return ''; } $chunk = substr($payload, $offset, $length); $offset += strlen($chunk); return $chunk; }); if ($username !== '') { curl_setopt($ch, CURLOPT_USERNAME, $username); curl_setopt($ch, CURLOPT_PASSWORD, $password); } if ($encryption === 'tls') { curl_setopt($ch, CURLOPT_USE_SSL, CURLUSESSL_ALL); } elseif ($encryption === '' || $encryption === 'none') { curl_setopt($ch, CURLOPT_USE_SSL, CURLUSESSL_NONE); } else { curl_setopt($ch, CURLOPT_USE_SSL, CURLUSESSL_ALL); } if ($allowInvalidCert) { curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); } $response = curl_exec($ch); if ($response === false) { $error = curl_error($ch); curl_close($ch); throw new RuntimeException('SMTP cURL error: ' . $error); } $statusCode = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); if ($statusCode >= 400) { throw new RuntimeException('SMTP cURL ha respost amb codi ' . $statusCode); } return true; } function kapvoe_send_via_smtp( array $config, string $fromEmail, string $fromName, string $replyTo, string $toEmail, string $subject, string $htmlBody ): bool { try { return kapvoe_send_via_smtp_curl($config, $fromEmail, $fromName, $replyTo, $toEmail, $subject, $htmlBody); } catch (Throwable $curlError) { kapvoe_log_mail_event($config, 'SMTP cURL FAIL -> ' . $toEmail . ' [' . $subject . '] ' . $curlError->getMessage()); } $host = trim((string)($config['smtp_host'] ?? '')); if ($host === '') { return false; } $port = (int)($config['smtp_port'] ?? 587); $username = trim((string)($config['smtp_username'] ?? '')); $password = (string)($config['smtp_password'] ?? ''); $encryption = strtolower(trim((string)($config['smtp_encryption'] ?? 'tls'))); $timeout = max(5, (int)($config['smtp_timeout'] ?? 15)); $allowInvalidCert = (bool)($config['smtp_allow_invalid_certificates'] ?? false); $secureTransport = $encryption === 'ssl' ? 'ssl://' : ''; $remote = $secureTransport . $host . ':' . $port; $errno = 0; $errstr = ''; $contextOptions = []; if ($allowInvalidCert) { $contextOptions['ssl'] = [ 'verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true, 'SNI_enabled' => true, 'peer_name' => $host, ]; } $context = !empty($contextOptions) ? stream_context_create($contextOptions) : null; $socket = @stream_socket_client( $remote, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $context ); if (!$socket) { throw new RuntimeException('No s\'ha pogut connectar a SMTP ' . $host . ':' . $port . ' (' . $errstr . ')'); } stream_set_timeout($socket, $timeout); try { kapvoe_smtp_expect($socket, [220], 'connexio'); $clientHost = $_SERVER['SERVER_NAME'] ?? 'localhost'; kapvoe_smtp_write($socket, 'EHLO ' . $clientHost . "\r\n"); kapvoe_smtp_expect($socket, [250], 'EHLO'); if ($encryption === 'tls') { kapvoe_smtp_write($socket, "STARTTLS\r\n"); kapvoe_smtp_expect($socket, [220], 'STARTTLS'); $cryptoMethod = defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT') ? STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT : STREAM_CRYPTO_METHOD_TLS_CLIENT; $cryptoEnabled = @stream_socket_enable_crypto($socket, true, $cryptoMethod); if ($cryptoEnabled !== true) { throw new RuntimeException('No s\'ha pogut establir el canal TLS amb SMTP'); } kapvoe_smtp_write($socket, 'EHLO ' . $clientHost . "\r\n"); kapvoe_smtp_expect($socket, [250], 'EHLO post-TLS'); } if ($username !== '') { kapvoe_smtp_write($socket, "AUTH LOGIN\r\n"); kapvoe_smtp_expect($socket, [334], 'AUTH LOGIN'); kapvoe_smtp_write($socket, base64_encode($username) . "\r\n"); kapvoe_smtp_expect($socket, [334], 'SMTP username'); kapvoe_smtp_write($socket, base64_encode($password) . "\r\n"); kapvoe_smtp_expect($socket, [235], 'SMTP password'); } $built = kapvoe_build_email_message($fromEmail, $fromName, $replyTo, $toEmail, $subject, $htmlBody); $smtpPayload = implode("\r\n", $built['headers']) . "\r\n\r\n" . $built['message']; $smtpPayload = preg_replace("/(?m)^\./", '..', $smtpPayload) ?? $smtpPayload; kapvoe_smtp_write($socket, 'MAIL FROM:<' . $fromEmail . ">\r\n"); kapvoe_smtp_expect($socket, [250], 'MAIL FROM'); kapvoe_smtp_write($socket, 'RCPT TO:<' . $toEmail . ">\r\n"); kapvoe_smtp_expect($socket, [250, 251], 'RCPT TO'); kapvoe_smtp_write($socket, "DATA\r\n"); kapvoe_smtp_expect($socket, [354], 'DATA'); kapvoe_smtp_write($socket, $smtpPayload . "\r\n.\r\n"); kapvoe_smtp_expect($socket, [250], 'envio DATA'); kapvoe_smtp_write($socket, "QUIT\r\n"); return true; } finally { fclose($socket); } } function kapvoe_send_html_email(array $config, string $toEmail, string $subject, string $htmlBody): bool { $toEmail = trim($toEmail); if ($toEmail === '') { return false; } $fromEmail = trim((string)($config['mail_from_email'] ?? '')); $fromName = trim((string)($config['mail_from_name'] ?? 'Blood Bros Sports')); $replyTo = trim((string)($config['mail_reply_to'] ?? $fromEmail)); if ($fromEmail === '') { throw new RuntimeException('Falta configurar mail_from_email'); } $mailTransport = strtolower(trim((string)($config['mail_transport'] ?? 'auto'))); $smtpConfigured = (bool)($config['smtp_enabled'] ?? false) || trim((string)($config['smtp_host'] ?? '')) !== ''; $resendConfigured = trim((string)($config['resend_api_key'] ?? '')) !== ''; try { $built = kapvoe_build_email_message($fromEmail, $fromName, $replyTo, $toEmail, $subject, $htmlBody); if ($mailTransport === 'resend') { $resendSent = kapvoe_send_via_resend($config, $fromEmail, $fromName, $replyTo, $toEmail, $subject, $htmlBody); kapvoe_log_mail_event($config, 'Resend OK -> ' . $toEmail . ' [' . $subject . ']'); return $resendSent; } if ($mailTransport === 'smtp') { $smtpSent = kapvoe_send_via_smtp($config, $fromEmail, $fromName, $replyTo, $toEmail, $subject, $htmlBody); kapvoe_log_mail_event($config, 'SMTP OK -> ' . $toEmail . ' [' . $subject . ']'); return $smtpSent; } if ($mailTransport === 'auto' && $resendConfigured) { $resendSent = kapvoe_send_via_resend($config, $fromEmail, $fromName, $replyTo, $toEmail, $subject, $htmlBody); kapvoe_log_mail_event($config, 'Resend auto OK -> ' . $toEmail . ' [' . $subject . ']'); return $resendSent; } $mailSent = mail($toEmail, $built['subject'], $built['message'], implode("\r\n", $built['headers'])); if ($mailSent) { kapvoe_log_mail_event($config, 'mail() OK -> ' . $toEmail . ' [' . $subject . ']'); return true; } $lastPhpError = error_get_last(); $lastPhpErrorMessage = is_array($lastPhpError) ? (string)($lastPhpError['message'] ?? '') : ''; kapvoe_log_mail_event( $config, 'mail() FAIL -> ' . $toEmail . ' [' . $subject . ']' . ($lastPhpErrorMessage !== '' ? ' ' . $lastPhpErrorMessage : '') ); if ($smtpConfigured && ($mailTransport === 'mail' || $mailTransport === 'auto')) { $smtpSent = kapvoe_send_via_smtp($config, $fromEmail, $fromName, $replyTo, $toEmail, $subject, $htmlBody); kapvoe_log_mail_event($config, 'SMTP fallback OK -> ' . $toEmail . ' [' . $subject . ']'); return $smtpSent; } if ($resendConfigured && ($mailTransport === 'mail' || $mailTransport === 'auto')) { $resendSent = kapvoe_send_via_resend($config, $fromEmail, $fromName, $replyTo, $toEmail, $subject, $htmlBody); kapvoe_log_mail_event($config, 'Resend fallback OK -> ' . $toEmail . ' [' . $subject . ']'); return $resendSent; } return false; } catch (Throwable $e) { kapvoe_log_mail_event($config, 'ERROR -> ' . $toEmail . ' [' . $subject . '] ' . $e->getMessage()); return false; } } function kapvoe_mark_order_email_notifications(array $config, string $sessionId, bool $customerSent, bool $adminSent): void { $path = kapvoe_orders_csv_path($config); kapvoe_upgrade_orders_csv_schema($config); if (!file_exists($path)) { return; } $rows = []; $fh = fopen($path, 'rb'); if (!$fh) { return; } while (($row = fgetcsv($fh, 0, ';')) !== false) { $rows[] = $row; } fclose($fh); if (count($rows) < 2) { return; } $header = $rows[0]; $sessionIdx = array_search('stripe_session_id', $header, true); $customerIdx = array_search('customer_email_sent', $header, true); $adminIdx = array_search('admin_email_sent', $header, true); $sentAtIdx = array_search('email_notifications_sent_at', $header, true); if ($sessionIdx === false || $customerIdx === false || $adminIdx === false || $sentAtIdx === false) { return; } $now = date('Y-m-d H:i:s'); for ($i = 1; $i < count($rows); $i++) { while (count($rows[$i]) < count($header)) { $rows[$i][] = ''; } if (($rows[$i][$sessionIdx] ?? '') === $sessionId) { if ($customerSent) { $rows[$i][$customerIdx] = '1'; } if ($adminSent) { $rows[$i][$adminIdx] = '1'; } if ($customerSent || $adminSent) { $rows[$i][$sentAtIdx] = $now; } break; } } $fh = fopen($path, 'wb'); if (!$fh) { return; } foreach ($rows as $row) { while (count($row) < count($header)) { $row[] = ''; } fputcsv($fh, $row, ';'); } fclose($fh); } function kapvoe_send_order_notifications(array $config, array $order, string $sessionId): array { $customerAlreadySent = (string)($order['customer_email_sent'] ?? '') === '1'; $adminAlreadySent = (string)($order['admin_email_sent'] ?? '') === '1'; $customerSent = false; $adminSent = false; $internalResult = [ 'enabled' => false, 'sent' => false, 'channel' => 'disabled', ]; $customerEmail = trim((string)($order['email'] ?? '')); $adminEmail = trim((string)($config['admin_notification_email'] ?? '')); kapvoe_log_mail_event( $config, 'notify session=' . $sessionId . ' customer_already=' . ($customerAlreadySent ? '1' : '0') . ' admin_already=' . ($adminAlreadySent ? '1' : '0') . ' customer_email=' . ($customerEmail !== '' ? $customerEmail : '(empty)') . ' admin_email=' . ($adminEmail !== '' ? $adminEmail : '(empty)') ); if (!$customerAlreadySent && $customerEmail !== '') { $customerSubject = 'Compra confirmada - ' . (string)($order['product_code'] ?? 'Blood Bros Sports'); $customerHtml = kapvoe_build_order_email_html($config, $order, $sessionId, false); $customerSent = kapvoe_send_html_email($config, $customerEmail, $customerSubject, $customerHtml); } if (!$adminAlreadySent && $adminEmail !== '') { $adminSubject = 'Nova comanda - ' . (string)($order['product_code'] ?? 'Blood Bros Sports'); $adminHtml = kapvoe_build_order_email_html($config, $order, $sessionId, true); $adminSent = kapvoe_send_html_email($config, $adminEmail, $adminSubject, $adminHtml); } $internalResult = kapvoe_send_internal_notification($config, $order, $sessionId); if ($customerSent || $adminSent) { kapvoe_mark_order_email_notifications($config, $sessionId, $customerSent, $adminSent); } $result = [ 'customer_already_sent' => $customerAlreadySent, 'admin_already_sent' => $adminAlreadySent, 'customer_sent' => $customerSent, 'admin_sent' => $adminSent, 'internal_notification' => $internalResult, ]; kapvoe_log_mail_event( $config, 'notify_result session=' . $sessionId . ' customer_sent=' . ($customerSent ? '1' : '0') . ' admin_sent=' . ($adminSent ? '1' : '0') . ' internal_channel=' . (string)($internalResult['channel'] ?? 'unknown') . ' internal_sent=' . (!empty($internalResult['sent']) ? '1' : '0') ); return $result; } function kapvoe_create_checkout_session(array $config, array $payload): array { $secretKey = (string)($config['stripe_secret_key'] ?? ''); $currency = (string)($config['currency'] ?? 'eur'); $successUrl = (string)($config['success_url'] ?? ''); $cancelUrl = (string)($config['cancel_url'] ?? ''); if ($secretKey === '') { throw new RuntimeException('Falta stripe_secret_key'); } if ($successUrl === '' || $cancelUrl === '') { throw new RuntimeException('Falten success_url o cancel_url'); } $lineItems = [ [ 'name' => (string)($payload['product_name'] ?? 'Producte'), 'unit_amount' => (int)($payload['unit_amount_cents'] ?? 0), 'quantity' => max(1, (int)($payload['quantity'] ?? 1)), ], ]; $shippingMethod = (string)($payload['shipping_method'] ?? 'pickup'); $shippingCostCents = (int)($payload['shipping_cost_cents'] ?? 0); if ($shippingMethod === 'shipping' && $shippingCostCents > 0) { $lineItems[] = [ 'name' => 'Enviament', 'unit_amount' => $shippingCostCents, 'quantity' => 1, ]; } $postFields = [ 'mode' => 'payment', 'success_url' => $successUrl . '?session_id={CHECKOUT_SESSION_ID}', 'cancel_url' => $cancelUrl, 'customer_email' => (string)($payload['email'] ?? ''), 'metadata[order_id]' => (string)($payload['order_id'] ?? ''), 'metadata[product_code]' => (string)($payload['product_code'] ?? ''), 'metadata[shipping_method]' => $shippingMethod, ]; foreach ($lineItems as $i => $item) { $postFields["line_items[$i][price_data][currency]"] = $currency; $postFields["line_items[$i][price_data][product_data][name]"] = $item['name']; $postFields["line_items[$i][price_data][unit_amount]"] = (string)$item['unit_amount']; $postFields["line_items[$i][quantity]"] = (string)$item['quantity']; } $ch = curl_init('https://api.stripe.com/v1/checkout/sessions'); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => http_build_query($postFields), CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . $secretKey, 'Content-Type: application/x-www-form-urlencoded', ], CURLOPT_TIMEOUT => 30, CURLOPT_CONNECTTIMEOUT => 10, ]); $response = curl_exec($ch); if ($response === false) { $error = curl_error($ch); curl_close($ch); throw new RuntimeException('Error cURL Stripe: ' . $error); } $statusCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $json = json_decode($response, true); if ($statusCode < 200 || $statusCode >= 300) { $message = is_array($json) ? ($json['error']['message'] ?? 'Error Stripe desconegut') : $response; throw new RuntimeException('Stripe error: ' . $message); } if (!is_array($json) || empty($json['url'])) { throw new RuntimeException('Resposta invàlida de Stripe'); } return $json; } function kapvoe_decrement_sheet_stock(array $config, array $order, string $sessionId): array { $url = (string)($config['stock_sync_url'] ?? ''); $token = (string)($config['stock_sync_token'] ?? ''); if ($url === '' || $token === '') { throw new RuntimeException('Falta configurar stock_sync_url o stock_sync_token'); } $payload = [ 'action' => 'decrement_stock', 'token' => $token, 'product_code' => (string)($order['product_code'] ?? ''), 'quantity' => max(1, (int)($order['quantity'] ?? 1)), 'order_id' => (string)($order['order_id'] ?? ''), 'session_id' => $sessionId, ]; if ($payload['product_code'] === '') { throw new RuntimeException('La comanda no té product_code'); } if ($payload['order_id'] === '') { throw new RuntimeException('La comanda no té order_id'); } if (!function_exists('curl_init')) { throw new RuntimeException('cURL no està disponible al PHP'); } $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Accept: application/json', ], CURLOPT_TIMEOUT => 20, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, ]); $response = curl_exec($ch); if ($response === false) { $error = curl_error($ch); curl_close($ch); throw new RuntimeException("Error cURL stock sync: {$error}"); } $statusCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($statusCode < 200 || $statusCode >= 300) { throw new RuntimeException("Stock sync HTTP {$statusCode}: {$response}"); } $json = json_decode($response, true); if (!is_array($json)) { throw new RuntimeException('Resposta no JSON del stock sync: ' . $response); } if (!($json['ok'] ?? false)) { throw new RuntimeException('Stock sync error: ' . ($json['error'] ?? 'error desconegut')); } return $json; } function kapvoe_fetch_catalog_products(array $config): array { $url = (string)($config['catalog_api_url'] ?? ''); if ($url === '') { throw new RuntimeException('Falta configurar catalog_api_url'); } if (!function_exists('curl_init')) { throw new RuntimeException('cURL no està disponible al PHP'); } $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPGET => true, CURLOPT_HTTPHEADER => [ 'Accept: application/json', ], CURLOPT_TIMEOUT => 20, CURLOPT_CONNECTTIMEOUT => 10, ]); $response = curl_exec($ch); if ($response === false) { $error = curl_error($ch); curl_close($ch); throw new RuntimeException("Error cURL catàleg: {$error}"); } $statusCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($statusCode < 200 || $statusCode >= 300) { throw new RuntimeException("Catàleg HTTP {$statusCode}: {$response}"); } $json = json_decode($response, true); if (!is_array($json) || !($json['ok'] ?? false) || !isset($json['products']) || !is_array($json['products'])) { throw new RuntimeException('Resposta invàlida del catàleg'); } return $json['products']; } function kapvoe_get_catalog_product_by_code(array $config, string $productCode): array { $productCode = trim($productCode); if ($productCode === '') { throw new RuntimeException('Falta product_code'); } $products = kapvoe_fetch_catalog_products($config); foreach ($products as $product) { $code = trim((string)($product['product_code'] ?? '')); if ($code === $productCode) { return $product; } } throw new RuntimeException("No s'ha trobat el producte {$productCode} al catàleg"); } function kapvoe_verify_stripe_signature(string $payload, string $header, string $secret, int $tolerance = 300): bool { if (!$header || !$secret) { return false; } $parts = explode(',', $header); $timestamp = null; $signature = null; foreach ($parts as $part) { [$k, $v] = array_pad(explode('=', trim($part), 2), 2, null); if ($k === 't') { $timestamp = $v; } if ($k === 'v1') { $signature = $v; } } if (!$timestamp || !$signature) { return false; } if (abs(time() - (int)$timestamp) > $tolerance) { return false; } $signedPayload = $timestamp . '.' . $payload; $expected = hash_hmac('sha256', $signedPayload, $secret); return hash_equals($expected, $signature); }