false, 'message' => 'Erreur inconnue']; // ============================================================ // AUTHENTIFICATION : qui appelle cet endpoint ? // - Super Admin (usa_info) -> plafond par evenement (meme ventilation que promoteur) // - Promoteur (com_info) -> inscriptions_gestion.refund + plafond par evenement // - Personne -> refus // L'autorisation fine du promoteur (ownership) se fait plus bas, une fois la commande relue en DB. // ============================================================ $blnIsSuperAdmin = !empty($_SESSION['usa_info']); $intComId = intval($_SESSION['com_info']['com_id'] ?? 0); if ($blnIsSuperAdmin) { $strAjoutePar = trim(($_SESSION['usa_info']['com_prenom'] ?? '') . ' ' . ($_SESSION['usa_info']['com_nom'] ?? '')); } else { $strAjoutePar = trim(($_SESSION['com_info']['com_prenom'] ?? '') . ' ' . ($_SESSION['com_info']['com_nom'] ?? '')); } if (!$blnIsSuperAdmin && $intComId <= 0) { // Aucun acteur authentifie : on refuse avant tout traitement. echo json_encode( ['success' => false, 'message' => 'Session expirée ou accès refusé.'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); exit; } try { // --- 1) Anti double clic / re-soumission --- $postedToken = $_POST['refund_token'] ?? ''; if (!hash_equals($_SESSION['refund_token'] ?? '', $postedToken)) { throw new Exception("Requête invalide ou déjà traitée."); } // On consomme le jeton : pas de re-soumission possible unset($_SESSION['refund_token']); // (Le prochain jeton sera régénéré lors du prochain affichage de la page) // --- 2) Inputs --- $amountStr = $_POST['refund_amount'] ?? ''; $note = $_POST['refund_note'] ?? ''; $CAPTURE_ID = $_POST['capture_id'] ?? ''; $currencyUI = $_POST['currency'] ?? 'CAD'; $eve_id = $_POST['eve_id'] ?? ''; $no_commande = $_POST['no_commande'] ?? ''; if ($CAPTURE_ID === '' || !is_string($CAPTURE_ID)) { throw new Exception("CAPTURE_ID manquant."); } if ($amountStr === '' || !is_numeric($amountStr)) { throw new Exception("Montant invalide."); } $amount = (float)$amountStr; if ($amount <= 0) { throw new Exception("Le montant doit être > 0."); } // --- 2bis) Commande relue en BD + plafond par evenement (Super Admin et promoteur) --- $sqlOwner = "SELECT * FROM inscriptions_panier_acheteurs" . " WHERE no_commande = '" . $db->fxEscape($no_commande) . "' LIMIT 1"; $rowOwner = $db->fxGetRow($sqlOwner); if (!$rowOwner) { throw new Exception("Commande introuvable."); } if ((string)$rowOwner['TransactionID'] !== (string)$CAPTURE_ID) { throw new Exception("Incohérence commande / capture."); } $intEveContext = intval($_POST['refund_eve_context'] ?? 0); if ($intEveContext <= 0) { $intEveContext = intval($rowOwner['eve_id']); } if ($intEveContext <= 0 || !fxRefundPanierLinkedToEvent($rowOwner['no_panier'], $intEveContext, intval($rowOwner['eve_id']))) { throw new Exception("Cette commande n'est pas rattachée à l'événement sélectionné."); } if (!$blnIsSuperAdmin) { require_once $_SERVER["DOCUMENT_ROOT"] . "/php/inc_fx_eve_acces.php"; if (!fxEveAccesHasPermission($intComId, $intEveContext, 'inscriptions_gestion.refund')) { throw new Exception("Accès refusé à cet événement."); } } $arrRefundCtx = fxRefundComputeContext($rowOwner, $intEveContext); $fltMaxAllowed = (float) ($arrRefundCtx['maxblock'] ?? 0); if ($fltMaxAllowed <= 0 || $amount > ($fltMaxAllowed + 0.009)) { throw new Exception("Montant supérieur au plafond remboursable pour cet événement (" . number_format($fltMaxAllowed, 2, '.', '') . " $currencyUI)."); } $eve_id = $intEveContext; // --- 3) Appel PayPal --- require_once $_SERVER["DOCUMENT_ROOT"] .'/paypal_advanced/PaypalCheckout.class.php'; $paypal = new PayPalCheckout(); // adapte si nécessaire // Idempotence possible ici si ton SDK le supporte // $idemKey = 'refund_' . hash('sha256', $CAPTURE_ID.'|'.number_format($amount,2,'.','').'|'.$currencyUI); // $res = $paypal->refundCapture($CAPTURE_ID, number_format($amount,2,'.',''), $currencyUI, $note, $idemKey); $res = $paypal->refundCapture($CAPTURE_ID, number_format($amount,2,'.',''), $currencyUI, $note); $status = strtoupper((string)($res['status'] ?? '')); if (in_array($status, ['COMPLETED','PENDING','SUCCESS'], true)) { // --- 4) Post-traitement maison (tables locales, logs, etc.) --- // Implémente ici ta logique (mise à jour des tables, etc.) try { postRefundSuccess($CAPTURE_ID, $amount, $currencyUI, $note, $res, $eve_id, $no_commande, $strAjoutePar); } catch (Throwable $inner) { // On n’échoue pas le remboursement pour autant : on renvoie un warning dans le message si tu veux. // $response['post_warning'] = $inner->getMessage(); } $response = [ 'success' => true, 'message' => "Remboursement effectué: " . number_format($amount, 2, '.', '') . " $currencyUI (capture: " . htmlspecialchars($CAPTURE_ID, ENT_QUOTES, 'UTF-8') . ").", 'status' => $status, ]; } else { throw new Exception("Remboursement non confirmé (statut: " . ($res['status'] ?? 'inconnu') . ")."); } } catch (Throwable $e) { // Nettoyage du message pour afficher une erreur claire (ex: REFUND_AMOUNT_EXCEEDED) $msg = $e->getMessage(); if (preg_match('/\{.*\}/s', $msg, $m)) { $json = json_decode($m[0], true); if (!empty($json['details'][0]['description'])) { $msg = $json['details'][0]['description']; } elseif (!empty($json['message'])) { $msg = $json['message']; } } $response['message'] = "Erreur remboursement: " . $msg; } // Re-emission d'un jeton anti double-clic frais (l'ancien a ete consomme plus haut). // Permet un 2e remboursement partiel ou une nouvelle tentative apres erreur, sans recharger. // Le front (handler delegue) met a jour le champ cache refund_token avec cette valeur. $_SESSION['refund_token'] = bin2hex(random_bytes(16)); $response['new_token'] = $_SESSION['refund_token']; echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);