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.

This commit is contained in:
2026-06-27 11:14:41 -04:00
parent bc407a7c20
commit e486c22bfc
5 changed files with 119 additions and 306 deletions

View File

@ -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;

View File

@ -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 = <?php echo json_encode($strLangue === 'en'
? 'Your transaction has been refunded. If you also want to cancel it in the registration system, click "Cancel".'
: "Votre transaction a été remboursée. Si vous voulez aussi l'annuler dans le système d'inscription, cliquez sur « Annuler ».", JSON_UNESCAPED_UNICODE); ?>;
// 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) {
$('<div id="inscr-mobile-refund-cancel-hint" style="margin-top:8px;padding:10px;border:1px solid #ffe1a6;background:#fff8e6;border-radius:8px;font-size:13px;"></div>')
.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);
}

View File

@ -1,294 +0,0 @@
<?php
// MSIN-3679 nouveau tableau
ini_set('display_errors', true);
error_reporting(E_ALL);
require_once $_SERVER["DOCUMENT_ROOT"] . "/superadm/php/inc_fx_paypal.php";
global $objDatabase, $vDomaine, $strLangue;
$intEveId = 0;
$blnDon = false;
if ($strLangue == 'fr') {
$strPage = '/compte';
} else {
$strPage = '/account';
}
if($strLangue == 'fr')
$strCode = 'compte/';
else
$strCode = 'account/';
$strBibCode = $strCode;
if (!empty($_GET['code'])) {
$code = $_GET['code'];
} else {
$code = 'bib_assignment';
}
if (isset($_GET['promoteur_eve_id'])) {
$intEveId = base64_decode(urldecode($_GET['promoteur_eve_id']));
$arrEvenement = fxGetEvenementsId($intEveId);
//$tabEvenement['general'] = $arrEvenement;
}
// a metre dimamique
$sqlCommandes = "SELECT *, IF(a.com_id <> 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);
?>
<a class="btn btn-primary rounded-pill" id="link_back" href="<?php echo $vDomaine . $strPage . "/inc_tableau_inscriptions?promoteur_eve_id=" . urlencode(base64_encode($intEveId)); ?>">
<i class="fa fa-chevron-left mr-2" aria-hidden="true"></i>
<?php afficheTexte('promoteur_back'); ?>
</a>
<br>
<h3><img src="images_blue/ico_commandes_big.png" alt="">Détails de la commande</h3>
<?php echo fxShowPanier($recCommandes['no_panier'], 0, 'web', 1, 'fr'); ?>
<p>&nbsp;</p>
<br>
<h3><img src="images_blue/ico_commandes_big.png" alt="">PAYPAL</h3>
<?php
// ************************************************ rembourcement
/****************************************************
* Page: remboursement.php
* Rôle : affiche le formulaire de remboursement + prépare
* les variables serveur nécessaires au front (AJAX).
****************************************************/
// Démarrer la session
//session_start();
// Jeton anti double clic / re-soumission
if (empty($_SESSION['refund_token'])) {
$_SESSION['refund_token'] = bin2hex(random_bytes(16));
}
$refundToken = $_SESSION['refund_token'];
// Récupération des infos commande (présentes dans ton contexte)
$CAPTURE_ID = $recCommandes["TransactionID"] ?? null;
if (!isset($CAPTURE_ID) || !is_string($CAPTURE_ID) || $CAPTURE_ID === '') {
//echo '<div style="background:#feecec;border:1px solid #f2b8b5;padding:10px;border-radius:8px;">
// ⚠️ CAPTURE_ID non fourni côté serveur.
// </div>';
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 nen 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", dautres 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 daffichage 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 dinput 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);
?>
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Remboursement PayPal</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body style="font-family:system-ui,Arial,Helvetica;">
<div style="max-width:560px;border:1px solid #ddd;border-radius:10px;padding:14px;">
<div style="font-weight:700;font-size:16px;margin-bottom:8px;">Remboursement PayPal (Checkout)</div>
<!-- Zone messages AJAX -->
<div id="refund-msg" style="margin-bottom:10px;display:none;padding:10px;border-radius:8px;"></div>
<?php if ($maxUI > 0): ?>
<div style="background:#f9fbff;border:1px solid #dbe7ff;padding:10px;border-radius:8px;margin-bottom:10px;">
<div><strong>Capture:</strong> <?= h($CAPTURE_ID) ?></div>
<div><strong>Max remboursable (affiché):</strong> <span id="pp_ref_max"><?= h(money2($maxUI)) ?></span> <?= h($currencyUI) ?></div>
</div>
<form id="refund-form" method="post" style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<input type="hidden" name="refund_token" value="<?= h($refundToken) ?>">
<input type="hidden" name="capture_id" value="<?= h($CAPTURE_ID) ?>">
<input type="hidden" name="currency" value="<?= h($currencyUI) ?>">
<input type="hidden" name="eve_id" value="<?= h($recCommandes["eve_id"]) ?>">
<input type="hidden" name="no_commande" value="<?= h($recCommandes["no_commande"]) ?>">
<div style="grid-column:1/2;">
<label style="display:block;font-size:12px;color:#333;margin-bottom:4px;">
Montant à rembourser (<?= h($currencyUI) ?>)
</label>
<input id="pp_ref_amount" type="number" name="refund_amount" step="0.01" min="0.01"
max="<?= h(money2($maxblock)) ?>"
value="<?= h(money2($maxUI)) ?>"
style="width:100%;padding:8px;border:1px solid #ccc;border-radius:8px;">
</div>
<div style="grid-column:2/3;">
<label style="display:block;font-size:12px;color:#333;margin-bottom:4px;">Note (optionnelle)</label>
<input type="text" name="refund_note" placeholder="Raison du remboursement"
style="width:100%;padding:8px;border:1px solid #ccc;border-radius:8px;">
</div>
<div style="grid-column:1/3;text-align:right;margin-top:6px;">
<button type="submit"
style="padding:10px 14px;border:1px solid #2d6cdf;border-radius:8px;background:#2d6cdf;color:#fff;cursor:pointer;">
Rembourser
</button>
</div>
</form>
<?php else: ?>
<div style="background:#fff8e6;border:1px solid #ffe1a6;padding:10px;border-radius:8px;">
Aucun montant remboursable pour cette capture.
</div>
<?php endif; ?>
</div>
<script>
// Petit helper visuel
function showMsg(text, ok) {
const box = document.getElementById('refund-msg');
box.style.display = 'block';
box.style.background = ok ? '#e9f8ef' : '#feecec';
box.style.border = ok ? '1px solid #b8e3c5' : '1px solid #f2b8b5';
box.innerText = text;
}
document.getElementById('refund-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
showMsg('⏳ Traitement en cours...', false);
const data = new FormData(e.target);
const res = await fetch( '<?php echo $vDomaine; ?>/superadm/ajax_refund.php', { method: 'POST', body: data });
// Défensif
if (!res.ok) {
showMsg('⚠️ Erreur réseau pendant le remboursement.', false);
return;
}
let json;
try { json = await res.json(); } catch (_) {
showMsg('⚠️ Réponse invalide du serveur.', false);
return;
}
if (json.success) {
showMsg('✅ ' + (json.message || 'Remboursement effectué.'), true);
// 👉 Ici, tu peux déclencher des actions UI :
// - recharger une grille des remboursements
// - rafraîchir un solde
// - etc.
// refreshTableRemboursements();
} else {
showMsg('⚠️ ' + (json.message || 'Erreur remboursement.'), false);
}
});
</script>
</body>
</html>
<h3><img src="images_blue/ico_commandes_big.png" alt="">Détails PAYPAL</h3>
<?php
// $sortie=paypaldetails($recCommandes["TransactionID"]);
echo(renderPayPalCaptureHtml($paypaldetails));
?>
<a class="btn btn-primary rounded-pill" id="link_back" href="<?php echo $vDomaine . $strPage . "/inc_tableau_promoteur?promoteur_eve_id=" . urlencode(base64_encode($intEveId)); ?>">
<i class="fa fa-chevron-left mr-2" aria-hidden="true"></i>
<?php afficheTexte('promoteur_back'); ?>
</a>
<?php

View File

@ -21,6 +21,31 @@ if (session_status() !== PHP_SESSION_ACTIVE) {
$response = ['success' => 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);

View File

@ -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'); }