prepare($sql); $st->execute([$table, $col]); return (bool) $st->fetchColumn(); } function enumOptions(PDO $pdo, string $table, string $col): array { $sql = "SELECT COLUMN_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ? LIMIT 1"; $st = $pdo->prepare($sql); $st->execute([$table, $col]); $type = (string) $st->fetchColumn(); if (stripos($type, 'enum(') !== 0) { return []; } preg_match_all("/'([^']*)'/", $type, $m); return $m[1] ?? []; } function logErr(Throwable $e): void { $info = ($e instanceof PDOException && isset($e->errorInfo)) ? json_encode($e->errorInfo, JSON_UNESCAPED_UNICODE) : ''; @file_put_contents('/tmp/souscrire_abonnement.log', '[' . date('c') . "] " . $e->getMessage() . " " . $info . "\n", FILE_APPEND ); } function flash(string $type, string $msg): void { $_SESSION['flash_' . $type] = $msg; } // --- Sécurité PDO --- if (isset($pdo) && $pdo instanceof PDO) { $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); } $praticien_id = (int) ($_SESSION['reference_id'] ?? 0); if (! $praticien_id) { header('Location: /public/login.php');exit; } // --- CSRF depuis choisir_abonnement.php --- $method = $_SERVER['REQUEST_METHOD'] ?? ''; $sessionCSRF = (string) ($_SESSION['csrf_abon'] ?? ''); $postedCSRF = (string) ($_POST['csrf'] ?? ($_POST['csrf_abon'] ?? '')); if ($method !== 'POST' || $sessionCSRF === '' || $postedCSRF === '' || ! hash_equals($sessionCSRF, $postedCSRF)) { flash('error', $translations['erreur_csrf'] ?? 'Session expirée. Merci de réessayer.'); header('Location: /public/choisir_abonnement.php');exit; } // --- Billing mode (manual|auto) --- $billing_mode = (string) ($_POST['billing_mode'] ?? 'manual'); $billing_mode = in_array($billing_mode, ['manual', 'auto'], true) ? $billing_mode : 'manual'; // --- Mapping Stripe (TEST / staging) pour les abonnements auto --- // NOTE: on gardera ça ici pour l’instant (staging). Plus tard on déplacera vers /plata/config/stripe.php // Mensuel (id=3) -> price_... (25€/mois) // Annuel (id=4) -> price_... (275€/an) // Pro (id=5) -> price_... (575€/an) /*$AUTO_PRICE_MAP = [ 3 => 'price_1SoksWFEDARPqDCE0Ynql83K', 4 => 'price_1Sokt8FEDARPqDCEecbxJCZU', 5 => 'price_1Sol4VFEDARPqDCEdDpjrfUE', ]; Live;*/ $AUTO_PRICE_MAP = [ 3 => 'price_1SpFEMFEDARPqDCEfGsL0SIN', 4 => 'price_1SpFFLFEDARPqDCE1fLBFWbq', 5 => 'price_1SpFGMFEDARPqDCEYW7FGU0P', ]; // --- Lecture de l’abonnement choisi --- $abonnement_id = (int) ($_POST['abonnement_id'] ?? 0); if ($abonnement_id <= 0) { flash('error', $translations['abonnement_invalide'] ?? 'Abonnement invalide.'); header('Location: /public/choisir_abonnement.php');exit; } $st = $pdo->prepare("SELECT id, nom, prix, duree_jours, actif FROM abonnement WHERE id = ? LIMIT 1"); $st->execute([$abonnement_id]); $ab = $st->fetch(PDO::FETCH_ASSOC); if (! $ab || ((int) $ab['actif'] !== 1 && (int) $ab['id'] !== 2)) { flash('error', $translations['abonnement_introuvable'] ?? 'Abonnement introuvable ou inactif.'); header('Location: /public/choisir_abonnement.php');exit; } $prix = (float) $ab['prix']; // 0.00 si gratuit $duree_jours = (int) $ab['duree_jours']; $nomKey = trim((string) ($ab['nom'] ?? '')); $nomAb = $translations[$nomKey] ?? $nomKey; // --- Payant → flux Stripe (ponctuel ou auto) --- if ($prix >= 0.5) { // Auto autorisé uniquement sur les abonnements configurés (3,4,5) if ($billing_mode === 'auto') { if (! isset($AUTO_PRICE_MAP[$abonnement_id])) { flash('error', $translations['auto_indispo_text'] ?? "Le renouvellement automatique n'est disponible que pour les offres mensuelle/annuelle."); header('Location: /public/choisir_abonnement.php');exit; } // Sécurité : si on arrive ici avec l'ID promo (2), on force manuel (pas de price récurrent mappé) // (Normalement ton choisir_abonnement bloque déjà, mais on double-sécurise côté serveur.) if ($abonnement_id === 2) { $billing_mode = 'manual'; } } // On garde tes sessions actuelles (compatibles avec paiement_simule.php et create_checkout.php) $_SESSION['paiement_abonnement_id'] = $abonnement_id; $_SESSION['paiement_prix'] = $prix; $_SESSION['paiement_abonnement_nom'] = $nomAb; $_SESSION['paiement_abonnement_key'] = $nomKey; // On ajoute les sessions pour le nouveau mode $_SESSION['paiement_billing_mode'] = $billing_mode; // manual|auto $_SESSION['paiement_provider'] = 'stripe'; if ($billing_mode === 'auto') { $_SESSION['paiement_provider_plan_id'] = $AUTO_PRICE_MAP[$abonnement_id]; // Stripe price_id (recurring) } else { unset($_SESSION['paiement_provider_plan_id']); } header('Location: /public/paiement_simule.php');exit; } // --- Gratuit → activation locale (une seule fois) --- $FREE_PLAN_ID = 1; // adapte si besoin // Anti double-usage (cohérent avec ton affichage) $st = $pdo->prepare(" SELECT 1 FROM paiement WHERE praticien_id = ? AND abonnement_id = ? AND (statut IS NULL OR statut <> 'échoué') LIMIT 1 "); $st->execute([$praticien_id, $FREE_PLAN_ID]); $hasUsedFree = (bool) $st->fetchColumn(); if ($abonnement_id === $FREE_PLAN_ID && $hasUsedFree) { flash('error', $translations['essai_deja_utilise'] ?? "L'offre gratuite a déjà été utilisée."); header('Location: /public/choisir_abonnement.php');exit; } try { // Choisir un statut compatible avec l’ENUM de la table paiement $statut = null; if (colExists($pdo, 'paiement', 'statut')) { $opts = enumOptions($pdo, 'paiement', 'statut'); // ex: ['payé','en_attente','annulé'] if ($opts) { $prefs = ['payé', 'paye', 'paid', 'validé', 'valide', 'completed', 'ok']; foreach ($prefs as $p) { if (in_array($p, $opts, true)) {$statut = $p; break;} } if (! $statut) { $statut = $opts[0]; } } } // Calculer l’expiration $date_expiration = (new DateTime('now'))->modify("+$duree_jours days")->format('Y-m-d H:i:s'); $pdo->beginTransaction(); // INSERT dans paiement (montant 0, méthode 'gratuit'), uniquement les colonnes existantes $fields = []; if (colExists($pdo, 'paiement', 'praticien_id')) { $fields['praticien_id'] = $praticien_id; } if (colExists($pdo, 'paiement', 'abonnement_id')) { $fields['abonnement_id'] = $abonnement_id; } if (colExists($pdo, 'paiement', 'montant')) { $fields['montant'] = 0.0; } if (colExists($pdo, 'paiement', 'methode')) { $fields['methode'] = 'gratuit'; } if (colExists($pdo, 'paiement', 'statut') && $statut) { $fields['statut'] = $statut; } if (colExists($pdo, 'paiement', 'currency')) { $fields['currency'] = 'eur'; } if (colExists($pdo, 'paiement', 'created_at')) { $fields['created_at'] = ['expr' => 'NOW()']; } if (colExists($pdo, 'paiement', 'date_paiement')) { $fields['date_paiement'] = ['expr' => 'NOW()']; } if (colExists($pdo, 'paiement', 'abonnement_nom') && $nomAb !== '') { $fields['abonnement_nom'] = $nomAb; } if (! empty($fields)) { $cols = []; $holders = []; $vals = []; foreach ($fields as $col => $val) { $cols[] = $col; if (is_array($val) && isset($val['expr'])) { $holders[] = $val['expr']; } else { $holders[] = '?'; $vals[] = $val; } } $sql = "INSERT INTO paiement (" . implode(',', $cols) . ") VALUES (" . implode(',', $holders) . ")"; $pdo->prepare($sql)->execute($vals); } // UPDATE du praticien (abonnement + expiration) if (colExists($pdo, 'praticien', 'date_expiration')) { $pdo->prepare("UPDATE praticien SET abonnement_id = ?, date_expiration = ? WHERE id = ?") ->execute([$abonnement_id, $date_expiration, $praticien_id]); } elseif (colExists($pdo, 'praticien', 'abonnement_expire_le')) { $pdo->prepare("UPDATE praticien SET abonnement_id = ?, abonnement_expire_le = ? WHERE id = ?") ->execute([$abonnement_id, $date_expiration, $praticien_id]); } // sinon, on ne bloque pas (pas de colonne), mais rien à faire. $pdo->commit(); // Nettoyage session paiement (au cas où) unset($_SESSION['paiement_abonnement_id'], $_SESSION['paiement_prix'], $_SESSION['paiement_billing_mode'], $_SESSION['paiement_provider'], $_SESSION['paiement_provider_plan_id']); // Redirection finale flash('success', $translations['essai_active'] ?? "L'essai gratuit a été activé."); header('Location: /public/praticien/factures.php'); // ou ton tableau de bord exit; } catch (Throwable $e) { if ($pdo->inTransaction()) { $pdo->rollBack(); } logErr($e); http_response_code(500); exit(($translations['erreur_traitement'] ?? 'Erreur serveur') . ' (FREE)'); }