Init Git Prod Mars 2026
This commit is contained in:
commit
d81efe93b1
362 files changed
+86627
No files matched your search
@@ -0,0 +1,21 @@
|
||||
# /plata/.htaccess
|
||||
Options -Indexes
|
||||
|
||||
# Tout bloquer par défaut
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Deny from all
|
||||
</IfModule>
|
||||
|
||||
# Débloquer UNIQUEMENT create_checkout.php s'il est dans /plata/
|
||||
<Files "create_checkout.php">
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all granted
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Allow from all
|
||||
Satisfy any
|
||||
</IfModule>
|
||||
</Files>
|
||||
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
// /plata/actions/create_checkout.php
|
||||
|
||||
require_once realpath(__DIR__ . '/../../public/includes/init_praticien.php');
|
||||
require_once __DIR__ . '/../lib/stripe_client.php';
|
||||
// $pdo
|
||||
//require_once __DIR__ . '/../../public/includes/securite.php';
|
||||
|
||||
require_login_or_redirect(['praticien']);
|
||||
|
||||
$configAll = require __DIR__ . '/../config/stripe.php';
|
||||
$cfg = $configAll[$configAll['mode']] ?? null;
|
||||
|
||||
if (!$cfg || empty($cfg['secret_key'])) {
|
||||
http_response_code(500);
|
||||
exit('Config Stripe invalide.');
|
||||
}
|
||||
|
||||
$logFile = '/tmp/stripe_create_checkout.log';
|
||||
@touch($logFile);
|
||||
|
||||
function log_line(string $msg): void {
|
||||
global $logFile;
|
||||
@file_put_contents($logFile, '['.date('c').'] '.$msg."\n", FILE_APPEND);
|
||||
}
|
||||
|
||||
// ---- Données session ----
|
||||
$praticien_id = $_SESSION['reference_id'] ?? null;
|
||||
$abonnement_id = $_SESSION['paiement_abonnement_id'] ?? null;
|
||||
$prix_raw = $_SESSION['paiement_prix'] ?? null;
|
||||
|
||||
// Nouveau (manual|auto)
|
||||
$billing_mode = (string)($_SESSION['paiement_billing_mode'] ?? 'manual');
|
||||
$billing_mode = in_array($billing_mode, ['manual','auto'], true) ? $billing_mode : 'manual';
|
||||
|
||||
// Nouveau : Stripe price_id récurrent (si auto)
|
||||
$provider_plan_id = (string)($_SESSION['paiement_provider_plan_id'] ?? '');
|
||||
|
||||
if (!$praticien_id || !$abonnement_id || $prix_raw === null) {
|
||||
http_response_code(400);
|
||||
exit('Données paiement manquantes.');
|
||||
}
|
||||
// ---- Tentative de paiement (anti réutilisation de session Stripe) ----
|
||||
$attemptId = (string)($_SESSION['checkout_attempt_id'] ?? '');
|
||||
if ($attemptId === '') {
|
||||
$attemptId = bin2hex(random_bytes(16));
|
||||
$_SESSION['checkout_attempt_id'] = $attemptId;
|
||||
}
|
||||
|
||||
|
||||
// ---- Récupérer la clé de l’abonnement ----
|
||||
$abonnement_key = '';
|
||||
try {
|
||||
if (!isset($pdo) || !($pdo instanceof PDO)) {
|
||||
throw new RuntimeException('PDO indisponible');
|
||||
}
|
||||
|
||||
$st = $pdo->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');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
// /plata/config/stripe.php
|
||||
if (isset($_SERVER['SCRIPT_FILENAME']) && realpath(__FILE__) === realpath($_SERVER['SCRIPT_FILENAME'])) {
|
||||
http_response_code(403);exit('Forbidden');
|
||||
}
|
||||
|
||||
/**
|
||||
* Mets tes 2 paires de clés (test et live).
|
||||
* N'utilise JAMAIS la clé secrète côté client.
|
||||
*/
|
||||
return [
|
||||
'mode' => 'live', // 'live' en prod
|
||||
|
||||
'test' => [
|
||||
'secret_key' => getenv('TEST_STRIPE_SECRET_KEY') ?: '',
|
||||
'publishable_key' => getenv('TEST_STRIPE_PUBLISHABLE_KEY') ?: '',
|
||||
'webhook_secret' => getenv('TEST_STRIPE_WEBHOOK_SECRET') ?: '',
|
||||
],
|
||||
'live' => [
|
||||
'secret_key' => getenv('LIVE_STRIPE_SECRET_KEY') ?: '',
|
||||
'publishable_key' => getenv('LIVE_STRIPE_PUBLISHABLE_KEY') ?: '',
|
||||
'webhook_secret' => getenv('LIVE_STRIPE_WEBHOOK') ?: '',
|
||||
],
|
||||
|
||||
'currency' => 'eur',
|
||||
];
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
// /plata/lib/stripe_client.php
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
$configAll = require __DIR__ . '/../config/stripe.php';
|
||||
$cfg = $configAll[$configAll['mode']];
|
||||
|
||||
$stripe = new \Stripe\StripeClient([
|
||||
'api_key' => $cfg['secret_key'],
|
||||
]);
|
||||
@@ -0,0 +1,9 @@
|
||||
# /plata/views/.htaccess
|
||||
Options -Indexes
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all granted
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Allow from all
|
||||
Satisfy any
|
||||
</IfModule>
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
// /plata/views/cancel.php
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
require_once __DIR__.'/../../public/includes/securite.php';
|
||||
require_login_or_redirect(['praticien']);
|
||||
require_once __DIR__.'/../../public/includes/lang.php';
|
||||
|
||||
$t_page = $translations['paiement_annule_title'] ?? 'Paiement annulé';
|
||||
$t_msg = $translations['paiement_annule_message'] ?? "Le paiement n'a pas été finalisé. Vous pouvez réessayer ou choisir une autre méthode.";
|
||||
$t_btn = $translations['retour_abonnements'] ?? 'Retour aux abonnements';
|
||||
//$t_redirect= $translations['redirection_abonnements'] ?? 'Redirection vers vos abonnements…';
|
||||
unset($_SESSION['checkout_attempt_id']);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title><?= htmlspecialchars($t_page) ?> | USLUGI</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root{
|
||||
--brand-blue:#38378B; --brand-red:#E03C3F; --page-blue:#8CA9FF;
|
||||
--text-dark:#fff; --border:#e5e7eb; --radius:18px; --shadow:0 10px 28px rgba(0,0,0,.10);
|
||||
}
|
||||
html,body{height:100%}
|
||||
body{
|
||||
background-color: #8CA9FF;
|
||||
font-family:'Montserrat',system-ui,-apple-system, Segoe UI, Roboto, 'Helvetica Neue', Arial;
|
||||
color:var(--text-dark);
|
||||
display:flex; align-items:center; justify-content:center; padding:24px;
|
||||
}
|
||||
.card{
|
||||
border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); background:#fff;
|
||||
max-width:560px; width:100%;
|
||||
}
|
||||
.btn-brand{
|
||||
background:var(--brand-red); border: none; border-radius:999px; font-weight:600;
|
||||
padding:.8rem 1.2rem; box-shadow:0 8px 22px rgba(0,0,0,.18); color:#fff;
|
||||
}
|
||||
.btn-brand:hover,.btn-brand:focus{ background:var(--brand-red); border: none; color: #fff;}
|
||||
.cross-wrap{
|
||||
width:82px; height:82px; border-radius:50%; display:grid; place-items:center;
|
||||
background:rgba(224,60,63,.08); margin:0 auto 18px auto; position:relative;
|
||||
border:3px solid rgba(224,60,63,.25);
|
||||
animation: pop .45s ease-out both;
|
||||
}
|
||||
@keyframes pop{ from{ transform:scale(.85); opacity:.5 } to{ transform:scale(1); opacity:1 } }
|
||||
.cross{
|
||||
position:relative; width:38px; height:38px;
|
||||
}
|
||||
.cross:before, .cross:after{
|
||||
content:""; position:absolute; top:50%; left:0; width:100%; height:6px;
|
||||
background:#E03C3F; border-radius:4px;
|
||||
}
|
||||
.cross:before{ transform:translateY(-50%) rotate(45deg); }
|
||||
.cross:after{ transform:translateY(-50%) rotate(-45deg); }
|
||||
.small-muted{ color:#64748b; font-size:.95rem }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="card p-4 p-md-5">
|
||||
<div class="text-center">
|
||||
<div class="cross-wrap"><div class="cross"></div></div>
|
||||
<h1 class="h3 fw-bold mb-2"><?= htmlspecialchars($t_page) ?></h1>
|
||||
<p class="small-muted mb-4"><?= htmlspecialchars($t_msg) ?></p>
|
||||
|
||||
<div class="d-grid d-sm-inline-block">
|
||||
<a href="/public/praticien/abonnement.php" class="btn btn-brand px-4"><?= htmlspecialchars($t_btn) ?></a>
|
||||
</div>
|
||||
|
||||
<p class="small-muted mt-3" id="redirMsg" aria-live="polite"><?= htmlspecialchars($t_redirect) ?></p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Redirection automatique après 5 secondes (désactive si inutile)
|
||||
setTimeout(function(){ window.location.href = "/public/choisir_abonnement.php"; }, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
// /plata/views/success.php
|
||||
if (session_status() === PHP_SESSION_NONE) session_start();
|
||||
require_once __DIR__.'/../../public/includes/securite.php';
|
||||
require_login_or_redirect(['praticien']);
|
||||
require_once __DIR__.'/../../public/includes/lang.php';
|
||||
|
||||
// Texte avec fallback si pas de $translations
|
||||
$t_page = $translations['paiement_succes_title'] ?? 'Paiement réussi';
|
||||
//$t_msg = $translations['paiement_succes_message'] ?? "Votre paiement a été reçu. La facture sera disponible dans votre espace.";
|
||||
$t_btn = $translations['voir_mes_factures'] ?? 'Voir mes factures';
|
||||
//$t_redirect= $translations['redirection_factures'] ?? 'Redirection vers vos factures…';
|
||||
unset($_SESSION['checkout_attempt_id']);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title><?= htmlspecialchars($t_page) ?> | USLUGI</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root{
|
||||
--brand-blue:#38378B; --brand-red:#E03C3F; --page-blue:#8CA9FF;
|
||||
--text-dark:#fff; --border:#e5e7eb; --radius:18px; --shadow:0 10px 28px rgba(0,0,0,.10);
|
||||
}
|
||||
html,body{height:100%}
|
||||
body{
|
||||
background-color: #8CA9FF;
|
||||
font-family:'Montserrat',system-ui,-apple-system, Segoe UI, Roboto, 'Helvetica Neue', Arial;
|
||||
color:var(--text-dark);
|
||||
display:flex; align-items:center; justify-content:center; padding:24px;
|
||||
}
|
||||
.card{
|
||||
border:1px solid var(--border); border-radius:var(--radius); box-shadow:var(--shadow); background:#fff;
|
||||
max-width:560px; width:100%;
|
||||
}
|
||||
.badge-pill{
|
||||
display:inline-flex; align-items:center; gap:10px;
|
||||
background:#eaf8f0; color:#1e7f4f; border-radius:999px; padding:10px 16px; font-weight:600;
|
||||
}
|
||||
.btn-brand{
|
||||
background:var(--brand-red); border: none; border-radius:999px; font-weight:600;
|
||||
padding:.8rem 1.2rem; box-shadow:0 8px 22px rgba(0,0,0,.18); color:#fff;
|
||||
}
|
||||
.btn-brand:hover,.btn-brand:focus{ background:var(--brand-red); border: none; color:#fff; }
|
||||
.check-wrap{
|
||||
width:82px; height:82px; border-radius:50%; display:grid; place-items:center;
|
||||
background:rgba(30, 128, 80, .1); margin:0 auto 18px auto; position:relative;
|
||||
border:3px solid rgba(30,128,80,.25);
|
||||
animation: pop .45s ease-out both;
|
||||
}
|
||||
@keyframes pop{ from{ transform:scale(.85); opacity:.5 } to{ transform:scale(1); opacity:1 } }
|
||||
.check{
|
||||
width:38px; height:26px; border:5px solid #1e8050; border-top:0;border-left:0; transform: rotate(45deg);
|
||||
animation: draw .5s .2s ease-out both;
|
||||
}
|
||||
@keyframes draw{ from{ opacity:0; clip-path: inset(0 100% 0 0) } to{ opacity:1; clip-path: inset(0 0 0 0) } }
|
||||
.small-muted{ color:#64748b; font-size:.95rem }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="card p-4 p-md-5">
|
||||
<div class="text-center">
|
||||
<div class="check-wrap"><div class="check"></div></div>
|
||||
<h1 class="h3 fw-bold mb-2"><?= htmlspecialchars($t_page) ?></h1>
|
||||
<p class="small-muted mb-4"><?= htmlspecialchars($t_msg) ?></p>
|
||||
|
||||
<div class="d-grid d-sm-inline-block">
|
||||
<a href="/public/praticien/factures.php" class="btn btn-brand px-4"><?= htmlspecialchars($t_btn) ?></a>
|
||||
</div>
|
||||
|
||||
<p class="small-muted mt-3" id="redirMsg" aria-live="polite"><?= htmlspecialchars($t_redirect) ?></p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
// Redirection automatique après 5 secondes (désactive en commentant)
|
||||
setTimeout(function(){ window.location.href = "/public/praticien/factures.php"; }, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,9 @@
|
||||
# /plata/webhooks/.htaccess
|
||||
Options -Indexes
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all granted
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Allow from all
|
||||
Satisfy any
|
||||
</IfModule>
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user