prepare("SELECT nom FROM abonnement WHERE id = ? LIMIT 1"); $st->execute([(int)$abonnement_id]); $row = $st->fetch(PDO::FETCH_ASSOC); // nom = clé de traduction $abonnement_key = trim((string)($row['nom'] ?? '')); } catch (Throwable $e) { log_line('WARN plan lookup failed: '.$e->getMessage()); $abonnement_key = ''; } // Nom affiché : Abonnement #ID — traduction depuis fr.php / ru.php $display_name = 'Abonnement #'.(int)$abonnement_id; $abonnement_nom = ''; // nom affichable (traduit) if ($abonnement_key !== '') { $label = $translations[$abonnement_key] ?? ''; // traduction UNIQUEMENT if ($label !== '') { $abonnement_nom = $label; if (function_exists('mb_strimwidth')) { $display_name .= ' — '.mb_strimwidth($label, 0, 120, '…', 'UTF-8'); } else { $display_name .= ' — '.$label; } } } // ---- Normalisation prix ---- $prix_norm = str_replace(["\u{00A0}", ' '], '', (string)$prix_raw); $prix_norm = str_replace(',', '.', $prix_norm); if (!is_numeric($prix_norm)) { http_response_code(400); exit('Montant invalide.'); } $prix = (float)$prix_norm; if ($prix < 0.5) { http_response_code(400); exit('Montant trop faible (Stripe min 0,50 €).'); } $amount = (int) round($prix * 100); // centimes // ---- Email du praticien ---- $userEmail = $_SESSION['user_email'] ?? null; if (!$userEmail || !filter_var($userEmail, FILTER_VALIDATE_EMAIL)) { try { if (isset($pdo) && $pdo instanceof PDO) { $st = $pdo->prepare("SELECT email FROM praticien WHERE id = ? LIMIT 1"); $st->execute([(int)$praticien_id]); $row = $st->fetch(PDO::FETCH_ASSOC); $candidate = $row['email'] ?? null; if ($candidate && filter_var($candidate, FILTER_VALIDATE_EMAIL)) { $userEmail = $candidate; } else { $userEmail = null; } } } catch (Throwable $e) { log_line('WARN email lookup failed: '.$e->getMessage()); $userEmail = null; } } // ---- Construire la session Checkout ---- $successUrl = 'https://' . $_SERVER['HTTP_HOST'] . '/plata/views/success.php?session_id={CHECKOUT_SESSION_ID}'; $cancelUrl = 'https://' . $_SERVER['HTTP_HOST'] . '/plata/views/cancel.php'; $params = [ 'success_url' => $successUrl, 'cancel_url' => $cancelUrl, 'payment_method_types' => ['card'], 'metadata' => [ 'praticien_id' => (string)(int)$praticien_id, 'abonnement_id' => (string)(int)$abonnement_id, 'billing_mode' => $billing_mode, 'montant_cent' => (string)$amount, 'abonnement_key' => $abonnement_key !== '' ? $abonnement_key : 'N/A', 'abonnement_nom' => $abonnement_nom !== '' ? $abonnement_nom : 'N/A', 'provider_plan_id' => $provider_plan_id !== '' ? $provider_plan_id : 'N/A', ], ]; $params['client_reference_id'] = (string)(int)$praticien_id; // Recommandé : éviter customer=null dans les events if ($userEmail && filter_var($userEmail, FILTER_VALIDATE_EMAIL)) { $params['customer_email'] = $userEmail; } // Email si dispo if ($userEmail && filter_var($userEmail, FILTER_VALIDATE_EMAIL)) { $params['customer_email'] = $userEmail; } // ---- Mode ponctuel vs abonnement auto ---- if ($billing_mode === 'auto') { // Sécurité : il faut absolument un price_id récurrent if ($provider_plan_id === '' || stripos($provider_plan_id, 'price_') !== 0) { http_response_code(400); exit('Plan récurrent manquant.'); } $params['mode'] = 'subscription'; $params['line_items'] = [[ 'price' => $provider_plan_id, 'quantity' => 1, ]]; // Important: metadata aussi sur la subscription (utile pour les webhooks invoice/subscription) $params['subscription_data'] = [ 'metadata' => $params['metadata'], ]; } else { // Ponctuel (comportement actuel) $params['mode'] = 'payment'; $params['line_items'] = [[ 'price_data' => [ 'currency' => $configAll['currency'] ?? 'eur', 'product_data' => ['name' => $display_name], 'unit_amount' => $amount, ], 'quantity' => 1, ]]; } // ---- Génération d'une idempotency_key robuste ---- $idem_material = [ 'praticien_id' => (int)$praticien_id, 'abonnement_id' => (int)$abonnement_id, 'billing_mode' => (string)$billing_mode, 'provider_plan' => (string)$provider_plan_id, 'amount' => (int)$amount, 'currency' => (string)($configAll['currency'] ?? 'eur'), 'name' => (string)$display_name, 'success_url' => (string)$successUrl, 'cancel_url' => (string)$cancelUrl, 'attempt' => (string)$attemptId, ]; $idem_hash = hash('sha256', json_encode($idem_material, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)); $idempotency_key = 'chk_'.$idem_hash; try { /** @var \Stripe\StripeClient $stripe */ $session = $stripe->checkout->sessions->create($params, [ 'idempotency_key' => $idempotency_key, ]); header('Location: ' . $session->url, true, 303); exit; } catch (\Stripe\Exception\ApiErrorException $e) { $httpStatus = method_exists($e, 'getHttpStatus') ? $e->getHttpStatus() : null; $requestId = method_exists($e, 'getRequestId') ? $e->getRequestId() : null; $stripeCode = method_exists($e, 'getStripeCode') ? $e->getStripeCode() : null; $httpBody = method_exists($e, 'getHttpBody') ? $e->getHttpBody() : null; $jsonBody = method_exists($e, 'getJsonBody') ? json_encode($e->getJsonBody()) : null; log_line("STRIPE_API_ERROR status=$httpStatus req=$requestId code=$stripeCode msg=".$e->getMessage()." body=".$httpBody." json=".$jsonBody); http_response_code(500); exit('Erreur Stripe'); } catch (Throwable $e) { log_line('PHP_ERROR '.$e->getMessage().' at '.$e->getFile().':'.$e->getLine()); http_response_code(500); exit('Erreur Stripe'); }