From e486c22bfcb0098f4495b17313c9a10d39b47252 Mon Sep 17 00:00:00 2001 From: stephan Date: Sat, 27 Jun 2026 11:14:41 -0400 Subject: [PATCH] Refactor refund process by removing the `inc_tableau_remboursement.php` file and updating the refund handling logic in `ajax_refund.php` and `inc_fx_paypal.php`. The changes include enhanced authorization checks for promoters, improved session management for refund tokens, and better integration of user information in refund records. This update streamlines the refund workflow and enhances security and maintainability. --- compte.php | 8 - inc_footer_scripts.php | 45 +++++ inc_tableau_remboursement.php | 294 --------------------------------- superadm/ajax_refund.php | 60 ++++++- superadm/php/inc_fx_paypal.php | 18 +- 5 files changed, 119 insertions(+), 306 deletions(-) delete mode 100644 inc_tableau_remboursement.php diff --git a/compte.php b/compte.php index 30a30e4..1923305 100644 --- a/compte.php +++ b/compte.php @@ -716,14 +716,6 @@ require_once("inc_header.php"); $footer_script = true; include("inc_tableau_rabais.php"); break; -// MSIN-3679 - case 'compte_inc_tableau_remboursement': - $blnBoutonRetour = false; - $footer_script = true; - - include("inc_tableau_remboursement.php"); - break; - case 'compte_inc_tableau_inscriptions': $blnBoutonRetour = false; $footer_script = true; diff --git a/inc_footer_scripts.php b/inc_footer_scripts.php index d4eb34c..9a90ef3 100644 --- a/inc_footer_scripts.php +++ b/inc_footer_scripts.php @@ -1255,6 +1255,50 @@ if ($strLangue == 'fr') { // ===== Remboursement PayPal (fiche de gestion) ===== // Bouton bascule : ouvre/ferme le panneau repliable et charge le formulaire en AJAX a la 1ere ouverture. (function () { + // Texte explicatif affiche apres un remboursement reussi (oriente vers le bouton Annuler). + var refundCancelHint = ; + + // Comportement post-succes : recalcul du restant, re-emission du jeton, mise en evidence du bouton Annuler. + function onRefundSuccess(form, json) { + var $form = $(form); + + // 1) Re-emission du jeton anti double-clic (le serveur l'a consomme) -> autorise un 2e remboursement partiel. + if (json && json.new_token) { + $form.find('input[name="refund_token"]').val(json.new_token); + } + + // 2) Recalcul du montant restant (remboursement partiel) cote client : ancien max - montant rembourse. + var amount = parseFloat($form.find('input[name="refund_amount"]').val()) || 0; + var $max = $('#pp_ref_max'); + var oldMax = parseFloat(String($max.text() || '0').replace(',', '.')) || 0; + var newMax = Math.max(0, oldMax - amount); + $max.text(newMax.toFixed(2)); + + var $amountInput = $('#pp_ref_amount'); + $amountInput.attr('max', newMax.toFixed(2)); + if (newMax <= 0) { + // Plus rien a rembourser : on verrouille le formulaire (le solde affiche reste a 0). + $amountInput.val('0.00').prop('disabled', true); + $form.find('button[type="submit"]').prop('disabled', true); + } else { + $amountInput.val(newMax.toFixed(2)); + } + + // 3) Mise en evidence du bouton Annuler + texte explicatif (uniquement si le bouton est visible). + var $cancelBtn = $('#annuler_fiche'); + if ($cancelBtn.length && $cancelBtn.is(':visible')) { + $cancelBtn.removeClass('btn-secondary').addClass('btn-danger') + .css('box-shadow', '0 0 0 3px rgba(220,53,69,.35)'); + if (!$('#inscr-mobile-refund-cancel-hint').length) { + $('
') + .text(refundCancelHint) + .insertAfter($('#inscr-mobile-refund-panel')); + } + } + } + $(document).on('click', '.btn_refund_toggle', function () { var $btn = $(this); var $panel = $($btn.attr('data-refund-target') || '#inscr-mobile-refund-panel'); @@ -1316,6 +1360,7 @@ if ($strLangue == 'fr') { .then(function (json) { if (json && json.success) { showMsg('\u2705 ' + (json.message || 'Remboursement effectue.'), true); + onRefundSuccess(form, json); } else { showMsg('\u26a0\ufe0f ' + ((json && json.message) || 'Erreur remboursement.'), false); } diff --git a/inc_tableau_remboursement.php b/inc_tableau_remboursement.php deleted file mode 100644 index 43d9b7c..0000000 --- a/inc_tableau_remboursement.php +++ /dev/null @@ -1,294 +0,0 @@ - - 0, (SELECT c.com_login FROM inscriptions_comptes c WHERE a.com_id = c.com_id), '') AS usager FROM inscriptions_panier_acheteurs a, inscriptions_panier_statuts s WHERE s.sta_id = a.sta_id AND a.no_panier = '" . $_GET['no_panier']."'"; -$recCommandes = $objDatabase->fxGetRow($sqlCommandes); - - - -?> - - - - -
- -

Détails de la commande

- -

 

- -
- - -

PAYPAL

- - - // ⚠️ CAPTURE_ID non fourni côté serveur. - // '; - return; -} - -// Charger ta classe PayPal -require_once $_SERVER["DOCUMENT_ROOT"] . "/paypal_advanced/PaypalCheckout.class.php"; -$paypal = new PayPalCheckout(); // adapte si nécessaire - -// Préparation -$errors = []; -$successMsg = null; - -// Devise par défaut si jamais les détails PayPal n’en donnent pas -$currencyUI = 'CAD'; - -// ====== Calculs initiaux côté serveur ====== - -// Solde dispo promoteur (ton helper existant) -$solde_dispo_ms1 = statut_payement($recCommandes["eve_id"]); - -// Récupérer les détails PayPal de la capture -$paypaldetails = $paypal->paypaldetails($CAPTURE_ID); - -// Harmoniser avec tes remboursements locaux si besoin -$paypaldetails = reconcileRefundsWithLocal($paypaldetails); - -// Déterminer le max remboursable PayPal -$max_remboursement_paypal = 0.0; - -// Cas habituel : clé remaining -if (!empty($paypaldetails['refunds']['totals']['remaining'])) { - $max_remboursement_paypal = (float)$paypaldetails['refunds']['totals']['remaining']; -} -// Fallback : montant total capturé (certains retours utilisent "details", d’autres ont eu une coquille "delails") - elseif (!empty($paypaldetails['details']['amount']['value'])) { - $max_remboursement_paypal = (float)$paypaldetails['details']['amount']['value']; - } elseif (!empty($paypaldetails['delails']['amount']['value'])) { // compat. si ton code historique renvoie "delails" - $max_remboursement_paypal = (float)$paypaldetails['delails']['amount']['value']; - } - -// Déterminer la devise d’affichage si fournie par PayPal -if (!empty($paypaldetails['refunds']['totals']['currency'])) { - $currencyUI = (string)$paypaldetails['refunds']['totals']['currency']; -} elseif (!empty($paypaldetails['details']['amount']['currency_code'])) { - $currencyUI = (string)$paypaldetails['details']['amount']['currency_code']; -} elseif (!empty($paypaldetails['delails']['amount']['currency_code'])) { - $currencyUI = (string)$paypaldetails['delails']['amount']['currency_code']; -} - -// Frais MS1 (selon ta structure) -$frais_ms1 = (float)($recCommandes['fra_total'] ?? 0); - -// Max remboursable à l’écran (ta logique actuelle) -$maxUI = $max_remboursement_paypal - $frais_ms1; - -// maxblock = limite stricte d’input côté front, selon ton choix actuel -if ($max_remboursement_paypal < $solde_dispo_ms1) { - $maxblock = $max_remboursement_paypal; -} else { - $maxblock = 0; - $maxUI = 0; -} -// ******************** variable info -//print_rsl($recCommandes); - echosl( "no_commande : ".$recCommandes["no_commande"]); - // echosl( "rem_ajoute_par : ".$_SESSION["usa_info"]["com_prenom"]." ".$_SESSION["usa_info"]["com_nom"]); - echosl( "eve_id : ".$recCommandes["eve_id"]) ; - echosl( "Montant remboursable : " . number_format($max_remboursement_paypal, 2) . " " . ($result['refunds']['totals']['currency'] ?? $result['delails']['amount']['currency_code'] ?? '')); - - - - echo("solde dispo ms1 :"); - echosl($solde_dispo_ms1); - echo("frais ms1 :"); - $frais_ms1=$recCommandes['fra_total']; - echosl($frais_ms1); - echo("max Remboursement :"); - - // $maxUI le max Remboursement - $maxUI=$max_remboursement_paypal-$frais_ms1; - echosl($maxUI); - ?> - - - - - Remboursement PayPal - - - - -
-
Remboursement PayPal (Checkout)
- - - - - 0): ?> -
-
Capture:
-
Max remboursable (affiché):
-
- -
- - - - "> - "> - -
- - -
- -
- - -
- -
- -
-
- -
- Aucun montant remboursable pour cette capture. -
- -
- - - - - - - - - -

Détails PAYPAL

- - - - - - - false, 'message' => 'Erreur inconnue']; +// ============================================================ +// AUTHENTIFICATION : qui appelle cet endpoint ? +// - Super Admin (usa_info) -> bypass total (comportement historique, aucun droit granulaire) +// - Promoteur (com_info) -> soumis a inscriptions_gestion.refund + ownership de l'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'] ?? ''; @@ -50,6 +75,33 @@ try { throw new Exception("Le montant doit être > 0."); } + // --- 2bis) AUTORISATION du promoteur (le Super Admin est en bypass) --- + if (!$blnIsSuperAdmin) { + // L'evenement de reference est RELU en DB depuis la commande (jamais fourni par le client). + require_once $_SERVER["DOCUMENT_ROOT"] . "/php/inc_fx_eve_acces.php"; + + $sqlOwner = "SELECT eve_id, TransactionID FROM inscriptions_panier_acheteurs" + . " WHERE no_commande = '" . $db->fxEscape($no_commande) . "' LIMIT 1"; + $rowOwner = $db->fxGetRow($sqlOwner); + if (!$rowOwner) { + throw new Exception("Commande introuvable."); + } + + // Defense : la capture postee doit correspondre a la commande reclamee. + if ((string)$rowOwner['TransactionID'] !== (string)$CAPTURE_ID) { + throw new Exception("Incohérence commande / capture."); + } + + $intEveIdReel = intval($rowOwner['eve_id']); + // inscriptions_gestion.refund implique aussi l'acces du promoteur a cet evenement. + if ($intEveIdReel <= 0 || !fxEveAccesHasPermission($intComId, $intEveIdReel, 'inscriptions_gestion.refund')) { + throw new Exception("Accès refusé à cet événement."); + } + + // On force l'eve_id reel pour la suite (journalisation du remboursement). + $eve_id = $intEveIdReel; + } + // --- 3) Appel PayPal --- require_once $_SERVER["DOCUMENT_ROOT"] .'/paypal_advanced/PaypalCheckout.class.php'; $paypal = new PayPalCheckout(); // adapte si nécessaire @@ -66,7 +118,7 @@ try { // --- 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); + 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(); @@ -97,6 +149,12 @@ try { $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); diff --git a/superadm/php/inc_fx_paypal.php b/superadm/php/inc_fx_paypal.php index 524b773..bd484f2 100644 --- a/superadm/php/inc_fx_paypal.php +++ b/superadm/php/inc_fx_paypal.php @@ -778,11 +778,23 @@ $out['refunds'] = $sectionOut; return $out; } -function postRefundSuccess(string $capture_id, float $amount, string $currency, string $note, array $paypalResponse = [],int $eve_id,string $no_commande): void +function postRefundSuccess(string $capture_id, float $amount, string $currency, string $note, array $paypalResponse = [],int $eve_id = 0,string $no_commande = '', string $strAjoutePar = ''): void { -$sqlInsert = "INSERT INTO remboursements SET no_commande='".$no_commande."', paypal_capture='".$capture_id."',rem_date=now(),rem_date_pour=now(),rem_details = '" . $note . "',eve_id=".$eve_id.",mr_id=6,tr_id=2,rem_montant=".$amount.",rem_ajoute_par='".$_SESSION["usa_info"]["com_prenom"]." ".$_SESSION["usa_info"]["com_nom"]."'" ; -$qryInsert = $GLOBALS['db']->fxQuery($sqlInsert); // $stmt->execute([$capture_id, json_encode($paypalResponse, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES)]); +// Auteur du remboursement : fourni par l'appelant (Super Admin OU promoteur). +// Fallback legacy sur la session Super Admin si l'appelant ne fournit rien. +if ($strAjoutePar === '') { + $strAjoutePar = trim(($_SESSION["usa_info"]["com_prenom"] ?? '') . ' ' . ($_SESSION["usa_info"]["com_nom"] ?? '')); +} +$db = $GLOBALS['db']; +// Echappement defensif (les valeurs peuvent venir d'un POST promoteur). +$strNoCmdSql = $db->fxEscape($no_commande); +$strCaptureSql = $db->fxEscape($capture_id); +$strNoteSql = $db->fxEscape($note); +$strAuteurSql = $db->fxEscape($strAjoutePar); + +$sqlInsert = "INSERT INTO remboursements SET no_commande='".$strNoCmdSql."', paypal_capture='".$strCaptureSql."',rem_date=now(),rem_date_pour=now(),rem_details = '".$strNoteSql."',eve_id=".intval($eve_id).",mr_id=6,tr_id=2,rem_montant=".(float)$amount.",rem_ajoute_par='".$strAuteurSql."'" ; +$db->fxQuery($sqlInsert); } function h($s) { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }