Primer commit

This commit is contained in:
2026-04-07 23:30:33 +02:00
commit 618efcd05f
40 changed files with 1547 additions and 0 deletions
+329
View File
@@ -0,0 +1,329 @@
<?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_append_order(array $config, array $order): void {
$path = kapvoe_orders_csv_path($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, array_keys($order), ';');
}
fputcsv($fh, array_values($order), ';');
fclose($fh);
}
function kapvoe_update_order_status(array $config, string $sessionId, string $newStatus, string $paymentIntent = ''): void {
$path = kapvoe_orders_csv_path($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);
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);
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_create_checkout_session(array $config, array $payload): array {
$secretKey = (string)$config['stripe_secret_key'];
$fields = [
'mode' => 'payment',
'success_url' => (string)$config['success_url'] . '?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => (string)$config['cancel_url'],
'customer_email' => (string)$payload['email'],
'client_reference_id' => (string)$payload['order_id'],
'locale' => 'es',
'payment_method_types[0]' => 'card',
'line_items[0][price_data][currency]' => (string)$config['currency'],
'line_items[0][price_data][product_data][name]' => (string)$payload['product_name'],
'line_items[0][price_data][unit_amount]' => (string)$payload['unit_amount_cents'],
'line_items[0][quantity]' => (string)$payload['quantity'],
'metadata[order_id]' => (string)$payload['order_id'],
'metadata[product_code]' => (string)$payload['product_code'],
'metadata[customer_name]' => (string)$payload['customer_name'],
'metadata[phone]' => (string)$payload['phone'],
'metadata[postal_code]' => (string)$payload['postal_code'],
'metadata[city]' => (string)$payload['city'],
'metadata[province]' => (string)$payload['province'],
];
$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($fields),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $secretKey
],
]);
$raw = curl_exec($ch);
if ($raw === false) {
throw new RuntimeException('Error cURL Stripe: ' . curl_error($ch));
}
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$decoded = json_decode($raw, true);
if ($httpCode >= 400 || !is_array($decoded)) {
throw new RuntimeException('Stripe ha retornat un error: ' . $raw);
}
return $decoded;
}
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);
}