Init Git Prod Mars 2026

This commit is contained in:
Oleg Barykin committed 2026-03-05 10:25:39 +01:00
commit d81efe93b1
362 files changed
+86627

No files matched your search

+21
View File
@@ -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>
+222
View File
@@ -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');
}
+26
View File
@@ -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',
];
+10
View File
@@ -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'],
]);
+9
View File
@@ -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>
+85
View File
@@ -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>
+85
View File
@@ -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>
+9
View File
@@ -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