330 lines
9.0 KiB
PHP
330 lines
9.0 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_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);
|
|
}
|