Files
portfoli-ulleres/checkout/common.php
T

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);
}