1749 lines
59 KiB
PHP
1749 lines
59 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* KAPVOE Stripe Checkout helpers
|
|
* Plain PHP + cURL. No Composer dependency required.
|
|
*/
|
|
|
|
function kapvoe_load_config(): array {
|
|
$configFile = __DIR__ . '/config.php';
|
|
if (!file_exists($configFile)) {
|
|
http_response_code(500);
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
echo json_encode([
|
|
'ok' => 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'
|
|
? '<div style="padding:10px 0;border-bottom:1px solid rgba(255,255,255,.08);">'
|
|
. '<div style="font-size:12px;color:#9fb0ca;text-transform:uppercase;letter-spacing:.12em;font-weight:700;">Adreca d\'enviament</div>'
|
|
. '<div style="margin-top:6px;font-size:15px;line-height:1.55;color:#eef4ff;">'
|
|
. kapvoe_mail_html_escape((string)($order['address'] ?? '')) . '<br>'
|
|
. kapvoe_mail_html_escape((string)($order['postal_code'] ?? '')) . ' '
|
|
. kapvoe_mail_html_escape((string)($order['city'] ?? '')) . '<br>'
|
|
. kapvoe_mail_html_escape((string)($order['province'] ?? ''))
|
|
. '</div></div>'
|
|
: '<div style="padding:10px 0;border-bottom:1px solid rgba(255,255,255,.08);">'
|
|
. '<div style="font-size:12px;color:#9fb0ca;text-transform:uppercase;letter-spacing:.12em;font-weight:700;">Entrega</div>'
|
|
. '<div style="margin-top:6px;font-size:15px;line-height:1.55;color:#eef4ff;">Quedarem amb tu per WhatsApp per entregar-te les ulleres en persona a Lleida o Mollerussa.</div>'
|
|
. '</div>';
|
|
|
|
$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 !== ''
|
|
? '<a href="' . kapvoe_mail_html_escape($successLink) . '" style="display:inline-block;padding:14px 22px;border-radius:999px;background:linear-gradient(180deg, rgba(93,154,255,.9), rgba(52,115,214,.9));color:#ffffff;text-decoration:none;font-weight:800;">Veure resum</a>'
|
|
: '';
|
|
|
|
return '<!DOCTYPE html>
|
|
<html lang="ca">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>' . kapvoe_mail_html_escape($brand) . '</title>
|
|
</head>
|
|
<body style="margin:0;padding:24px;background:#081221;font-family:Arial,Helvetica,sans-serif;color:#f4f7fb;">
|
|
<div style="max-width:960px;margin:0 auto;border-radius:32px;border:1px solid rgba(255,255,255,.10);background:linear-gradient(180deg,#172544 0%,#0f1b32 100%);box-shadow:0 16px 44px rgba(0,0,0,.34);overflow:hidden;">
|
|
<div style="padding:28px;">
|
|
<div style="display:flex;align-items:center;gap:18px;">
|
|
<div style="width:84px;height:84px;border-radius:24px;background:linear-gradient(135deg, rgba(255,255,255,.08), rgba(255,255,255,.03));border:1px solid rgba(255,255,255,.10);display:flex;align-items:center;justify-content:center;overflow:hidden;">
|
|
<img src="' . kapvoe_mail_html_escape(kapvoe_mail_base_url($config) . '/assets/logo/bloodbros-sports-logo.png') . '" alt="' . kapvoe_mail_html_escape($brand) . '" style="max-width:82%;max-height:82%;display:block;">
|
|
</div>
|
|
<div>
|
|
<div style="margin:0 0 6px;font-size:24px;line-height:1.2;font-weight:800;color:#ffffff;">Pagament correcte</div>
|
|
<div style="margin:0;font-size:16px;line-height:1.5;color:#b9c8df;">' . kapvoe_mail_html_escape($intro) . '</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin:22px 0 22px;display:inline-block;padding:10px 16px;border-radius:999px;background:rgba(40,210,103,.14);border:1px solid rgba(40,210,103,.28);color:#c8ffd9;font-weight:800;">' . kapvoe_mail_html_escape($badge) . '</div>
|
|
|
|
<div style="display:grid;gap:18px;">
|
|
<div style="display:grid;grid-template-columns:120px 1fr;gap:18px;align-items:center;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.04);border-radius:24px;padding:18px;">
|
|
<div style="width:120px;height:120px;border-radius:18px;background:rgba(255,255,255,.92);display:flex;align-items:center;justify-content:center;overflow:hidden;">'
|
|
. ($imageUrl !== ''
|
|
? '<img src="' . kapvoe_mail_html_escape($imageUrl) . '" alt="' . $productCode . '" style="width:100%;height:100%;object-fit:contain;display:block;">'
|
|
: '<div style="padding:8px;color:#0b1630;font-size:12px;font-weight:800;text-align:center;">Sense imatge</div>')
|
|
. '</div>
|
|
<div>
|
|
<div style="margin:0 0 8px;font-size:38px;line-height:1;font-weight:900;letter-spacing:-.04em;color:#ffffff;">' . $productCode . '</div>
|
|
<div style="font-size:15px;line-height:1.55;color:#b9c8df;">
|
|
Sessio Stripe: <strong style="color:#eef4ff;">' . $sessionIdSafe . '</strong><br>
|
|
Metode: <strong style="color:#eef4ff;">' . kapvoe_mail_html_escape($shippingLabel) . '</strong>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:18px;">
|
|
<div style="border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.04);border-radius:24px;padding:18px;">
|
|
<div style="margin:0 0 14px;color:#9fb0ca;font-size:12px;text-transform:uppercase;letter-spacing:.16em;font-weight:800;">Resum economic</div>
|
|
<div style="display:flex;justify-content:space-between;gap:14px;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.06);font-size:15px;color:#eef4ff;"><span>Subtotal producte</span><strong>' . kapvoe_mail_html_escape($subtotal) . '</strong></div>
|
|
<div style="display:flex;justify-content:space-between;gap:14px;padding:8px 0;border-bottom:1px solid rgba(255,255,255,.06);font-size:15px;color:#eef4ff;"><span>Enviament</span><strong>' . kapvoe_mail_html_escape($shippingCost) . '</strong></div>
|
|
<div style="display:flex;justify-content:space-between;gap:14px;padding:12px 0 0;font-size:18px;font-weight:800;color:#ffffff;"><span>Total</span><strong>' . kapvoe_mail_html_escape($totalAmount) . '</strong></div>
|
|
</div>
|
|
|
|
<div style="border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.04);border-radius:24px;padding:18px;">
|
|
<div style="margin:0 0 14px;color:#9fb0ca;font-size:12px;text-transform:uppercase;letter-spacing:.16em;font-weight:800;">Dades de contacte</div>
|
|
<div style="padding:10px 0;border-bottom:1px solid rgba(255,255,255,.06);"><div style="font-size:12px;color:#9fb0ca;text-transform:uppercase;letter-spacing:.12em;font-weight:700;">Nom</div><div style="margin-top:6px;font-size:15px;line-height:1.55;color:#eef4ff;">' . $customerName . '</div></div>
|
|
<div style="padding:10px 0;border-bottom:1px solid rgba(255,255,255,.06);"><div style="font-size:12px;color:#9fb0ca;text-transform:uppercase;letter-spacing:.12em;font-weight:700;">Telefon</div><div style="margin-top:6px;font-size:15px;line-height:1.55;color:#eef4ff;">' . $phone . '</div></div>
|
|
<div style="padding:10px 0;border-bottom:1px solid rgba(255,255,255,.06);"><div style="font-size:12px;color:#9fb0ca;text-transform:uppercase;letter-spacing:.12em;font-weight:700;">Correu electronic</div><div style="margin-top:6px;font-size:15px;line-height:1.55;color:#eef4ff;">' . $customerEmail . '</div></div>
|
|
' . $deliveryBlock . '
|
|
</div>
|
|
</div>
|
|
|
|
' . ($cta !== '' ? '<div style="padding-top:6px;">' . $cta . '</div>' : '') . '
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>';
|
|
}
|
|
|
|
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(['<br>', '<br/>', '<br />'], "\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);
|
|
}
|