From 797e66fec300e7b58873df4b56268fbc4b41b69f Mon Sep 17 00:00:00 2001 From: stephan Date: Tue, 30 Jun 2026 15:13:06 -0400 Subject: [PATCH] MSIN-4417 Refactor refund handling in registration management to support event context This commit enhances the refund functionality by introducing event context handling for refunds. It updates the logic to ensure that refunds are linked to the correct event, improving authorization checks and refund calculations. Additionally, the refund context is now passed through various functions to ensure accurate processing. The version code is incremented to 4.72.718 to reflect these changes. --- ajax_inscr_gestion.php | 20 ++- inc_footer_scripts.php | 1 + php/inc_fx_inscriptions_gestion.php | 1 + php/inc_settings.php | 2 +- superadm/ajax_refund.php | 28 ++- superadm/commandes.php | 2 +- superadm/php/inc_fx_paypal.php | 259 ++++++++++++++++++++++++++-- 7 files changed, 286 insertions(+), 27 deletions(-) diff --git a/ajax_inscr_gestion.php b/ajax_inscr_gestion.php index a203322..92ef6f2 100644 --- a/ajax_inscr_gestion.php +++ b/ajax_inscr_gestion.php @@ -81,18 +81,24 @@ switch ($strAction) { break; } - // L'eve_id fait foi cote serveur (jamais le client) pour le controle d'acces a l'evenement. - $intEveId = intval($recCommandes['eve_id']); - if ($intEveId <= 0 || !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.refund')) { + // L'eve_id de contexte = evenement de la fiche (participant), pas seulement celui du checkout. + require_once $_SERVER['DOCUMENT_ROOT'] . '/superadm/php/inc_fx_paypal.php'; + + $intEveContext = intval($_REQUEST['eve_id'] ?? 0); + if ($intEveContext <= 0) { + $intEveContext = intval($recCommandes['eve_id']); + } + if ($intEveContext <= 0 || !fxInscrGestionCanDo($intComId, $intEveContext, 'inscriptions_gestion.refund')) { $tabRetour = array('state' => 'error', 'message' => 'unauthorized'); break; } - - // Module mutualise (ne tire jamais inc_functions.php "U") : voir superadm/php/inc_fx_paypal.php - require_once $_SERVER['DOCUMENT_ROOT'] . '/superadm/php/inc_fx_paypal.php'; + if (!fxRefundPanierLinkedToEvent($recCommandes['no_panier'], $intEveContext, intval($recCommandes['eve_id']))) { + $tabRetour = array('state' => 'error', 'message' => 'panier_evenement_incompatible'); + break; + } $strRefundToken = fxRefundEnsureToken(); - $arrRefundCtx = fxRefundComputeContext($recCommandes); + $arrRefundCtx = fxRefundComputeContext($recCommandes, $intEveContext, false); // Auteur = promoteur connecte (alimente l'encart "Ajoute par" du formulaire). $strAuteur = trim((string)($_SESSION['com_info']['com_prenom'] ?? '') . ' ' diff --git a/inc_footer_scripts.php b/inc_footer_scripts.php index 6356f28..a4fe754 100644 --- a/inc_footer_scripts.php +++ b/inc_footer_scripts.php @@ -1401,6 +1401,7 @@ if ($strLangue == 'fr') { $.post('/ajax_inscr_gestion.php', { a: 'refund_form', no_panier: $btn.data('no_panier'), + eve_id: $btn.data('eve_id'), lang: '' }, function (result) { if (result && result.state === 'ok' && result.html) { diff --git a/php/inc_fx_inscriptions_gestion.php b/php/inc_fx_inscriptions_gestion.php index ef1de5f..e62f9b9 100644 --- a/php/inc_fx_inscriptions_gestion.php +++ b/php/inc_fx_inscriptions_gestion.php @@ -1420,6 +1420,7 @@ function fxInscrGestionRenderFiche($intEveId, $strLangue, $strBaseUrl, $arrReq, echo ''; } diff --git a/php/inc_settings.php b/php/inc_settings.php index e6cde5c..3c25153 100644 --- a/php/inc_settings.php +++ b/php/inc_settings.php @@ -7,7 +7,7 @@ * Constantes * * **************/ -define('_VERSION_CODE', '4.72.717'); +define('_VERSION_CODE', '4.72.718'); define('_DATE_CODE', '2026-06-30'); //MSIN-4290 define('QR_SECRET_KEY', 'ms1_qr_2026_cle_secrete_longue_et_fixe'); diff --git a/superadm/ajax_refund.php b/superadm/ajax_refund.php index 61abc3a..e646c96 100644 --- a/superadm/ajax_refund.php +++ b/superadm/ajax_refund.php @@ -76,30 +76,46 @@ try { } // --- 2bis) AUTORISATION du promoteur (le Super Admin est en bypass) --- + $recCommandesFull = null; 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" + $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."); } + $recCommandesFull = $rowOwner; // 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')) { + $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 à votre événement."); + } + + // inscriptions_gestion.refund sur l'evenement de contexte (fiche), pas seulement le checkout. + if (!fxEveAccesHasPermission($intComId, $intEveContext, '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; + $arrRefundCtx = fxRefundComputeContext($rowOwner, $intEveContext, false); + $fltMaxAllowed = (float) ($arrRefundCtx['maxblock'] ?? 0); + if ($fltMaxAllowed <= 0 || $amount > ($fltMaxAllowed + 0.009)) { + throw new Exception("Montant supérieur au plafond remboursable pour votre événement (" . number_format($fltMaxAllowed, 2, '.', '') . " $currencyUI)."); + } + + // Journalisation sur l'evenement de contexte (ventilation inter-evenements). + $eve_id = $intEveContext; } // --- 3) Appel PayPal --- diff --git a/superadm/commandes.php b/superadm/commandes.php index c523611..6644dd8 100644 --- a/superadm/commandes.php +++ b/superadm/commandes.php @@ -412,7 +412,7 @@ if (!isset($CAPTURE_ID) || !is_string($CAPTURE_ID) || $CAPTURE_ID === '') { } // Contexte de remboursement mutualisé (calculs + détails PayPal) -$refundCtx = fxRefundComputeContext($recCommandes); +$refundCtx = fxRefundComputeContext($recCommandes, 0, true); $paypaldetails = $refundCtx['paypaldetails']; // Auteur du remboursement (Super Admin connecté) : alimente l'encart "Ajouté par". diff --git a/superadm/php/inc_fx_paypal.php b/superadm/php/inc_fx_paypal.php index 160866f..0b1a190 100644 --- a/superadm/php/inc_fx_paypal.php +++ b/superadm/php/inc_fx_paypal.php @@ -969,17 +969,215 @@ function fxRefundEnsureToken(): string return $_SESSION['refund_token']; } +/** Acces BD depuis inc_fx_paypal (contexte promoteur ou Super Admin). */ +function fxRefundDb() { + if (isset($GLOBALS['objDatabase'])) { + return $GLOBALS['objDatabase']; + } + if (isset($GLOBALS['db'])) { + return $GLOBALS['db']; + } + return null; +} + +/** Somme des remboursements deja imputes a un evenement pour une commande. */ +function fxRefundSumEventRefunds(string $strNoCommande, int $intEveId): float +{ + $db = fxRefundDb(); + if (!$db || $strNoCommande === '' || $intEveId <= 0) { + return 0.0; + } + + $sql = "SELECT COALESCE(SUM(rem_montant), 0) AS total" + . " FROM remboursements" + . " WHERE rem_deleted = 0 AND tr_id = 2" + . " AND eve_id = " . intval($intEveId) + . " AND no_commande = '" . $db->fxEscape($strNoCommande) . "'"; + + return (float) $db->fxGetVar($sql); +} + +/** Ligne rapport_finances_membership pour un panier (transaction inter-evenements). */ +function fxRefundGetMembershipRow(string $strNoPanier): array +{ + $db = fxRefundDb(); + if (!$db || $strNoPanier === '') { + return array(); + } + + $sql = "SELECT * FROM rapport_finances_membership" + . " WHERE no_panier = '" . $db->fxEscape($strNoPanier) . "' LIMIT 1"; + $row = $db->fxGetRow($sql); + + return is_array($row) ? $row : array(); +} + +/** + * Part membership si la table jonction est absente (repli fxTotalPanierMembership). + */ +function fxRefundResolveMembershipTotal(string $strNoPanier, int $intOrderEveId): float +{ + $row = fxRefundGetMembershipRow($strNoPanier); + if (!empty($row['ach_total'])) { + return (float) $row['ach_total']; + } + + $db = fxRefundDb(); + if (!$db || $strNoPanier === '' || $intOrderEveId <= 0) { + return 0.0; + } + + $intMembershipEveId = (int) $db->fxGetVar( + "SELECT eve_id_membership FROM inscriptions_evenements" + . " WHERE eve_id = " . intval($intOrderEveId) . " LIMIT 1" + ); + if ($intMembershipEveId <= 0) { + return 0.0; + } + + $intHasSisterItems = (int) $db->fxGetVar( + "SELECT COUNT(*) FROM (" + . " SELECT no_panier FROM inscriptions_panier_epreuves_commandees" + . " WHERE no_panier = '" . $db->fxEscape($strNoPanier) . "' AND eve_id = " . intval($intMembershipEveId) + . " UNION" + . " SELECT no_panier FROM inscriptions_panier_produits_new" + . " WHERE no_panier = '" . $db->fxEscape($strNoPanier) . "' AND eve_id = " . intval($intMembershipEveId) + . ") AS sister_items" + ); + if ($intHasSisterItems <= 0) { + return 0.0; + } + + if (!function_exists('fxTotalPanierMembership')) { + require_once $_SERVER['DOCUMENT_ROOT'] . '/php/inc_fx_panier_js.php'; + } + if (!function_exists('fxTotalPanierMembership')) { + return 0.0; + } + + $arrTotals = fxTotalPanierMembership($intOrderEveId, $intMembershipEveId, $strNoPanier); + return (float) ($arrTotals['total'] ?? 0); +} + +/** Verifie qu'un panier a bien une part rattachable a l'evenement de contexte. */ +function fxRefundPanierLinkedToEvent(string $strNoPanier, int $intEveContext, int $intOrderEveId): bool +{ + if ($strNoPanier === '' || $intEveContext <= 0) { + return false; + } + if ($intEveContext === $intOrderEveId) { + return true; + } + + $row = fxRefundGetMembershipRow($strNoPanier); + if (!empty($row['eve_id_membership']) && intval($row['eve_id_membership']) === $intEveContext) { + return true; + } + + $db = fxRefundDb(); + if (!$db) { + return false; + } + + $intCount = (int) $db->fxGetVar( + "SELECT COUNT(*) FROM (" + . " SELECT no_panier FROM inscriptions_panier_epreuves_commandees" + . " WHERE no_panier = '" . $db->fxEscape($strNoPanier) . "' AND eve_id = " . intval($intEveContext) + . " UNION" + . " SELECT no_panier FROM inscriptions_panier_produits_new" + . " WHERE no_panier = '" . $db->fxEscape($strNoPanier) . "' AND eve_id = " . intval($intEveContext) + . ") AS ctx_items" + ); + + return $intCount > 0; +} + +/** + * Part remboursable pour un evenement (aligne rapport finances MSIN-4228). + * + * @return array{ + * role:string, event_share:float, sister_share:float, already_refunded:float, + * remaining_event:float, membership_eve_id:int, subtract_frais:bool + * } + */ +function fxRefundComputeEventShare(array $recCommandes, int $intEveContext): array +{ + $intOrderEveId = intval($recCommandes['eve_id'] ?? 0); + $strNoPanier = (string) ($recCommandes['no_panier'] ?? ''); + $strNoCommande = (string) ($recCommandes['no_commande'] ?? ''); + $fltAchTotal = (float) ($recCommandes['ach_total'] ?? 0); + $fltFraTotal = (float) ($recCommandes['fra_total'] ?? 0); + $intPaiId = intval($recCommandes['pai_id'] ?? 0); + + if ($intEveContext <= 0) { + $intEveContext = $intOrderEveId; + } + + $fltMemberTotal = fxRefundResolveMembershipTotal($strNoPanier, $intOrderEveId); + $rowMember = fxRefundGetMembershipRow($strNoPanier); + $intMembershipEveId = intval($rowMember['eve_id_membership'] ?? 0); + if ($intMembershipEveId <= 0 && $fltMemberTotal > 0) { + $db = fxRefundDb(); + if ($db && $intOrderEveId > 0) { + $intMembershipEveId = (int) $db->fxGetVar( + "SELECT eve_id_membership FROM inscriptions_evenements" + . " WHERE eve_id = " . intval($intOrderEveId) . " LIMIT 1" + ); + } + } + + $strRole = 'simple'; + $fltEventShare = 0.0; + $fltSisterShare = 0.0; + $blnSubtractFrais = false; + + if ($fltMemberTotal > 0.00001) { + $fltSisterShare = $fltMemberTotal; + if ($intEveContext === $intOrderEveId) { + $strRole = 'host'; + $fltEventShare = max(0.0, $fltAchTotal - $fltMemberTotal); + $blnSubtractFrais = ($intPaiId !== 6); + } elseif ($intMembershipEveId > 0 && $intEveContext === $intMembershipEveId) { + $strRole = 'sister'; + $fltEventShare = max(0.0, $fltMemberTotal); + $fltSisterShare = 0.0; + $blnSubtractFrais = false; + } + } elseif ($intEveContext === $intOrderEveId) { + $strRole = 'simple'; + $fltEventShare = max(0.0, $fltAchTotal); + $blnSubtractFrais = ($intPaiId !== 6); + } + + $fltAlreadyRefunded = fxRefundSumEventRefunds($strNoCommande, $intEveContext); + $fltRemainingEvent = max(0.0, $fltEventShare - $fltAlreadyRefunded); + + return array( + 'role' => $strRole, + 'event_share' => $fltEventShare, + 'sister_share' => $fltSisterShare, + 'already_refunded' => $fltAlreadyRefunded, + 'remaining_event' => $fltRemainingEvent, + 'membership_eve_id' => $intMembershipEveId, + 'subtract_frais' => $blnSubtractFrais, + 'fra_total' => $fltFraTotal, + ); +} + /** * Calcule le contexte de remboursement d'une commande (capture PayPal). * * @param array $recCommandes Ligne commande (TransactionID, eve_id, no_commande, fra_total...) + * @param int $intEveContext Evenement depuis lequel on rembourse (0 = eve_id commande). Ignore si Super Admin. + * @param bool $blnSuperAdmin true = capture PayPal complete (option A Super Admin). * @return array { * ok:bool, capture_id:string, currency:string, paypaldetails:array, * max_paypal:float, solde_dispo:float, frais_ms1:float, - * maxUI:float, maxblock:float, maxproposer:float + * maxUI:float, maxblock:float, maxproposer:float, + * split_active:bool, event_share:float, sister_share:float, eve_id_context:int * } */ -function fxRefundComputeContext(array $recCommandes): array +function fxRefundComputeContext(array $recCommandes, int $intEveContext = 0, bool $blnSuperAdmin = false): array { $out = [ 'ok' => false, @@ -992,6 +1190,11 @@ function fxRefundComputeContext(array $recCommandes): array 'maxUI' => 0.0, 'maxblock' => 0.0, 'maxproposer' => 0.0, + 'split_active' => false, + 'event_share' => 0.0, + 'sister_share' => 0.0, + 'eve_id_context'=> 0, + 'share_role' => 'simple', ]; $captureId = $recCommandes['TransactionID'] ?? null; @@ -1003,8 +1206,8 @@ function fxRefundComputeContext(array $recCommandes): array require_once $_SERVER['DOCUMENT_ROOT'] . '/paypal_advanced/PaypalCheckout.class.php'; $paypal = new PayPalCheckout(); - // Solde disponible promoteur (helper de ce fichier) - $out['solde_dispo'] = (float) statut_payement($recCommandes['eve_id'] ?? 0); + $intSoldeEveId = $blnSuperAdmin ? intval($recCommandes['eve_id'] ?? 0) : ($intEveContext > 0 ? $intEveContext : intval($recCommandes['eve_id'] ?? 0)); + $out['solde_dispo'] = (float) statut_payement($intSoldeEveId); // Details PayPal de la capture + reconciliation locale $paypaldetails = $paypal->paypaldetails($captureId); @@ -1031,16 +1234,35 @@ function fxRefundComputeContext(array $recCommandes): array $out['currency'] = (string) $paypaldetails['delails']['amount']['currency_code']; } - // Frais ms1 $out['frais_ms1'] = (float) ($recCommandes['fra_total'] ?? 0); - // Plafonds (fidele a l'ecran Super Admin actuel) : - // - maxUI affiche = max remboursable PayPal - // - maxblock (input max) = max_paypal si max_paypal < solde dispo, sinon 0 - // - valeur proposee = max(0, maxUI - frais ms1) - $out['maxUI'] = $maxPaypal; - $out['maxblock'] = ($maxPaypal < $out['solde_dispo']) ? $maxPaypal : 0.0; - $out['maxproposer'] = max(0.0, $out['maxUI'] - $out['frais_ms1']); + if ($blnSuperAdmin) { + // Option A — Super Admin : capture PayPal complete (comportement historique). + $out['maxUI'] = $maxPaypal; + $out['maxblock'] = ($maxPaypal < $out['solde_dispo']) ? $maxPaypal : 0.0; + $out['maxproposer'] = max(0.0, $out['maxUI'] - $out['frais_ms1']); + $out['ok'] = true; + return $out; + } + + if ($intEveContext <= 0) { + $intEveContext = intval($recCommandes['eve_id'] ?? 0); + } + $out['eve_id_context'] = $intEveContext; + + $arrShare = fxRefundComputeEventShare($recCommandes, $intEveContext); + $out['share_role'] = $arrShare['role']; + $out['event_share'] = (float) $arrShare['event_share']; + $out['sister_share'] = (float) $arrShare['sister_share']; + $out['split_active'] = ($arrShare['sister_share'] > 0.00001 || $arrShare['role'] === 'sister'); + + $fltCapEvent = (float) $arrShare['remaining_event']; + $fltMaxUi = min($maxPaypal, $fltCapEvent); + $fltFraisForProposal = !empty($arrShare['subtract_frais']) ? (float) $arrShare['fra_total'] : 0.0; + + $out['maxUI'] = $fltMaxUi; + $out['maxblock'] = $fltMaxUi; + $out['maxproposer'] = max(0.0, $fltMaxUi - $fltFraisForProposal); $out['ok'] = true; return $out; @@ -1081,6 +1303,10 @@ function fxRenderRefundForm(array $recCommandes, string $refundToken, array $ctx $soldeDispo = (float) ($ctx['solde_dispo'] ?? 0); $fraisMs1 = (float) ($ctx['frais_ms1'] ?? 0); $noCommande = (string) ($recCommandes['no_commande'] ?? ''); + $intEveContext = intval($ctx['eve_id_context'] ?? 0); + $blnSplitActive = !empty($ctx['split_active']); + $fltEventShare = (float) ($ctx['event_share'] ?? 0); + $fltSisterShare = (float) ($ctx['sister_share'] ?? 0); $blnHasDocAside = trim($strDocAsideHtml) !== ''; @@ -1105,6 +1331,12 @@ function fxRenderRefundForm(array $recCommandes, string $refundToken, array $ctx
Capture PayPal :
Solde disponible MS1 :
Frais MS1 :
+ +
Part de votre événement :
+ 0): ?> +
Part autre événement (non remboursable ici) :
+ +
Max remboursable :
@@ -1116,6 +1348,9 @@ function fxRenderRefundForm(array $recCommandes, string $refundToken, array $ctx + 0): ?> + +