connect_errno) { printf("sl Connect failed: %s\n", $dbpaypal->connect_error); exit(); } require_once '../paypal_advanced/PaypalCheckout.class.php'; $paypal = new PaypalCheckout; // Helpers pour un retour propre si PayPal ne répond pas $makeUnavailableCapture = function(string $capId, string $rawMsg='') { // essaie d’extraire debug_id du message d’exception PayPal $debugId = ''; if ($rawMsg && preg_match('/"debug_id"\s*:\s*"([^"]+)"/', $rawMsg, $m)) { $debugId = $m[1]; } return [ 'id' => $capId, 'status' => 'NON_DISPONIBLE', '_error' => 'Pas disponible chez PayPal', '_paypal_debug_id' => $debugId, '_raw_error' => $rawMsg, ]; }; $makeEmptyRefunds = function(string $capId, string $msg='Pas disponible chez PayPal') { return [ 'ok' => false, 'capture_id' => $capId, 'invoice_id' => '', 'order_id' => '', 'refunds' => [], 'totals' => [ 'currency' => '', 'captured_total' => 0.00, 'refunded_total' => 0.00, 'remaining' => 0.00, ], 'errors' => [$msg], ]; }; // 1) Détails de la capture try { $delails = $paypal->getCaptureDetails($capId); } catch (Throwable $e) { // Retour propre si la ressource n’existe pas (404/INVALID_RESOURCE_ID) $delails = $makeUnavailableCapture($capId, $e->getMessage()); // On renvoie directement le bundle, avec refunds vides return [ 'delails' => $delails, 'refunds' => $makeEmptyRefunds($capId), ]; } // 2) Refunds (si on a des détails valides) try { $Refunds = $paypal->getRefundsForCapture($delails); } catch (Throwable $e) { // Si la recherche refunds échoue, on n’explose pas; on renvoie un bloc vide + erreur $Refunds = $makeEmptyRefunds($capId, 'Remboursements non disponibles ('.$e->getMessage().')'); } return [ 'delails' => $delails, 'refunds' => $Refunds, ]; } /** * Rendu HTML simple et lisible pour une capture PayPal v2 + ses remboursements. * - $cap : tableau de la capture (/v2/payments/captures/{id}) * - $refunds : tableaux de refunds (réponses de refundCapture() ou getRefundDetails()) * - $tz : fuseau pour afficher les dates */ function renderPayPalCaptureHtml($capOrBundle, array $refundsParam = [], string $tz = 'America/Toronto'): string { // --- Helpers --- $h = static function ($v): string { return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); }; $get = static function ($a, array $path, $default = '') { if (!is_array($a)) return $default; foreach ($path as $k) { if (!is_array($a) || !array_key_exists($k, $a)) return $default; $a = $a[$k]; } return $a; }; $moneyStr = static function ($amt): string { if (is_array($amt)) { $v = isset($amt['value']) ? (string)$amt['value'] : ''; $c = isset($amt['currency_code']) ? (string)$amt['currency_code'] : ''; if ($v !== '' && $c !== '') return number_format((float)$v, 2, '.', '') . ' ' . $c; if ($v !== '') return number_format((float)$v, 2, '.', ''); if ($c !== '') return $c; } elseif (is_numeric($amt)) { return number_format((float)$amt, 2, '.', ''); } return ''; }; $moneyVal = static function ($amt): float { if (is_array($amt) && isset($amt['value'])) return (float)$amt['value']; if (is_numeric($amt)) return (float)$amt; return 0.0; }; $yn = static function ($b): string { return ($b === true || $b === 1 || $b === '1') ? 'Oui' : (($b === false || $b === 0 || $b === '0') ? 'Non' : ''); }; $fmtDate = static function ($iso, string $tz): string { if (!$iso) return ''; try { $dt = new DateTime($iso); $dt->setTimezone(new DateTimeZone($tz)); return $dt->format('Y-m-d H:i:s T'); } catch (Throwable $e) { return (string)$iso; } }; // --- Détecte le "nouveau bundle" vs ancien appel --- $isBundle = is_array($capOrBundle) && (array_key_exists('delails', $capOrBundle) || array_key_exists('details', $capOrBundle)); if ($isBundle) { $cap = $capOrBundle['delails'] ?? $capOrBundle['details']; // tolère "details" si tu corriges plus tard $refBloc = $capOrBundle['refunds'] ?? []; // Items de remboursements $refundItems = $refBloc['refunds'] ?? []; // Totaux fournis par l’API de recherche (sinon on recalcule) $totals = $refBloc['totals'] ?? []; } else { $cap = $capOrBundle; $refundItems = $refundsParam; $totals = []; } // ------------------ TABLE CAPTURE ------------------ $rows = []; $rows['ID de capture'] = $get($cap, ['id']); $rows['Statut'] = $get($cap, ['status']); $rows['Montant capturé'] = $moneyStr($get($cap, ['amount'], [])); $rows['Frais PayPal'] = $moneyStr($get($cap, ['seller_receivable_breakdown','paypal_fee'], [])); $rows['Montant net'] = $moneyStr($get($cap, ['seller_receivable_breakdown','net_amount'], [])); $rows['Montant brut'] = $moneyStr($get($cap, ['seller_receivable_breakdown','gross_amount'], [])); $rows['Capture finale'] = $yn($get($cap, ['final_capture'], '')); $rows['Protection vendeur'] = $get($cap, ['seller_protection','status']); $rows['Invoice ID (invoice_id)'] = $get($cap, ['invoice_id']); $rows['ID personnalisé (custom_id)'] = $get($cap, ['custom_id']); $rows['ID commande (order_id)'] = $get($cap, ['supplementary_data','related_ids','order_id']); $rows['Payee (email)'] = $get($cap, ['payee','email_address']); $rows['Compte marchand'] = $get($cap, ['payee','merchant_id']); $rows['Réseau (carte)'] = $get($cap, ['network_transaction_reference','network']); $rows['Référence réseau'] = $get($cap, ['network_transaction_reference','id']); $rows['Créé le'] = $fmtDate($get($cap, ['create_time']), $tz); $rows['Mis à jour le'] = $fmtDate($get($cap, ['refundCapture(update_time']), $tz); // Lien refund API (utile debug) $refundLink = ''; foreach ((array)$get($cap, ['links'], []) as $lk) { if (($lk['rel'] ?? '') === 'refund' && ($lk['method'] ?? '') === 'POST' && !empty($lk['href'])) { $refundLink = $lk['href']; break; } } if ($refundLink !== '') $rows['Lien remboursement (API)'] = $refundLink; $html = ''; foreach ($rows as $label => $val) { if ($val === '' || $val === null) continue; $html .= ''; $html .= ''; if (is_string($val) && strpos($val, 'http') === 0) { $html .= ''; } else { $html .= ''; } $html .= ''; } $html .= '
'.$h($label).''.$h($val).''.$h($val).'
'; // ------------------ SECTION REMBOURSEMENTS ------------------ // Normalise les items (acceptons 'id' ou 'refund_id', 'create_time' ou 'time') // Normalise les items (acceptons 'id' ou 'refund_id', 'create_time' ou 'time') // Normalise les items (acceptons 'id' ou 'refund_id', 'create_time' ou 'time') $normRefunds = []; foreach ((array)$refundItems as $rf) { $rid = $get($rf, ['refund_id']) ?: $get($rf, ['id']); $amtA = ['value' => $get($rf, ['amount'], 0), 'currency_code' => $get($rf, ['currency'],'')]; $stat = $get($rf, ['status']); $time = $get($rf, ['create_time']) ?: $get($rf, ['time']); // Notes $notePending = $get($rf, ['note']); // ex.: "En attente de confirmation PayPal" $noteToPayerHtml = $rf['note_to_payer_ui'] ?? ''; // ex.: "Note TO PAYER : ..." $normRefunds[] = [ 'id' => $rid, 'amountArr' => $amtA, 'amountVal' => $moneyVal($amtA), 'status' => $stat, 'time' => $time, 'note' => $notePending, 'note_to_payer_ui' => $noteToPayerHtml, ]; } // Totaux (prend ceux fournis, sinon calcule) $capCurrency = $get($cap, ['amount','currency_code'], ''); $capTotal = $moneyVal($get($cap, ['amount'], [])); $refTotal = isset($totals['refunded_total']) ? (float)$totals['refunded_total'] : 0.0; if ($refTotal <= 0 && !empty($normRefunds)) { foreach ($normRefunds as $nrf) $refTotal += (float)$nrf['amountVal']; } $currency = $totals['currency'] ?? $capCurrency; $remaining = isset($totals['remaining']) ? (float)$totals['remaining'] : max(0, $capTotal - $refTotal); // S'il n'y a aucun remboursement, on n'affiche RIEN (pas de titre, pas de totaux a 0, // pas de "Aucun remboursement trouve") : ce serait contre-productif. if (!empty($normRefunds)) { // Affichage section $html .= '

Remboursements

'; // Résumé des totaux $html .= ''; $html .= '' . ''; $html .= '' . ''; $html .= '' . ''; $html .= '
Total capturé'.$h(number_format($capTotal,2,'.','').($currency?' '.$currency:'' )).'
Total remboursé'.$h(number_format($refTotal,2,'.','').($currency?' '.$currency:'' )).'
Solde après remboursement'.$h(number_format($remaining,2,'.','').($currency?' '.$currency:'' )).'
'; // Détail des remboursements $html .= ''; $html .= '' . '' . '' . '' . '' . '' // ⬅️ NOUVEAU . '' // ⬅️ NOUVEAU . ''; $statusMap = [ 'S' => 'Effectué (COMPLETED)', 'P' => 'En attente (PENDING)', 'D' => 'Refusé/Échoué (DENIED/FAILED)', 'V' => 'Annulé/Void (VOIDED)', 'R' => 'Reversé (REVERSED)', ]; foreach ($normRefunds as $nrf) { $statRaw = $nrf['status'] ?? ''; $statNice = $statusMap[$statRaw] ?? $statRaw; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; // Colonne "Note" (texte simple) $html .= ''; // Colonne "Note TO PAYER" (déjà formaté avec , ne pas ré-échaper) $html .= ''; $html .= ''; } $html .= '
Refund IDStatutMontantCréé leNoteNote TO PAYER
'.$h($nrf['id']).''.$h($statNice).''.$h($moneyStr($nrf['amountArr'])).''.$h($fmtDate($nrf['time'], $tz)).'' . ($nrf['note'] !== '' ? $h($nrf['note']) : '') . '' . (!empty($nrf['note_to_payer_ui']) ? $nrf['note_to_payer_ui'] : '') . '
'; } return $html; } /** * Sommaire compact d'une capture PayPal + remboursements (vue promoteur/gestion). * Contrairement a renderPayPalCaptureHtml(), n'affiche que l'essentiel : * statut, montant capture, total rembourse, solde restant et une ligne par remboursement. * - $capOrBundle : meme entree que renderPayPalCaptureHtml() (bundle 'delails'/'details' + 'refunds') * - $tz : fuseau pour afficher les dates */ function renderPayPalRefundSummaryHtml($capOrBundle, string $tz = 'America/Toronto'): string { $h = static function ($v): string { return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); }; $get = static function ($a, array $path, $default = '') { if (!is_array($a)) return $default; foreach ($path as $k) { if (!is_array($a) || !array_key_exists($k, $a)) return $default; $a = $a[$k]; } return $a; }; $moneyVal = static function ($amt): float { if (is_array($amt) && isset($amt['value'])) return (float)$amt['value']; if (is_numeric($amt)) return (float)$amt; return 0.0; }; $fmtDate = static function ($iso, string $tz): string { if (!$iso) return ''; try { $dt = new DateTime($iso); $dt->setTimezone(new DateTimeZone($tz)); return $dt->format('Y-m-d H:i'); } catch (Throwable $e) { return (string)$iso; } }; // --- Detecte le bundle vs capture brute --- $isBundle = is_array($capOrBundle) && (array_key_exists('delails', $capOrBundle) || array_key_exists('details', $capOrBundle)); if ($isBundle) { $cap = $capOrBundle['delails'] ?? $capOrBundle['details']; $refBloc = $capOrBundle['refunds'] ?? []; $refundItems = $refBloc['refunds'] ?? []; $totals = $refBloc['totals'] ?? []; } else { $cap = $capOrBundle; $refundItems = []; $totals = []; } // Aucun remboursement => on n'affiche RIEN (pas de bloc, pas de totaux a 0). if (empty($refundItems)) { return ''; } // --- Statut traduit --- $statusMapCap = [ 'COMPLETED' => 'Complété', 'PARTIALLY_REFUNDED' => 'Partiellement remboursé', 'REFUNDED' => 'Remboursé', 'PENDING' => 'En attente', 'DECLINED' => 'Refusé', 'NON_DISPONIBLE' => 'Non disponible', ]; $statRaw = (string)$get($cap, ['status'], ''); $statNice = $statusMapCap[$statRaw] ?? $statRaw; // --- Montants --- // (empty() et pas ?? : la section refunds peut contenir une devise vide '' quand // l'API reporting a echoue ; on retombe alors sur la devise de la capture.) $currency = !empty($totals['currency']) ? (string)$totals['currency'] : (string)$get($cap, ['amount','currency_code'], ''); $capTotal = $moneyVal($get($cap, ['amount'], [])); $refTotal = isset($totals['refunded_total']) ? (float)$totals['refunded_total'] : 0.0; if ($refTotal <= 0 && !empty($refundItems)) { foreach ($refundItems as $rf) { $refTotal += $moneyVal(['value' => $get($rf, ['amount'], 0)]); } } $remaining = isset($totals['remaining']) ? (float)$totals['remaining'] : max(0.0, $capTotal - $refTotal); $fmtMoney = static function (float $v) use ($currency): string { return number_format($v, 2, '.', '') . ($currency ? ' ' . $currency : ''); }; // --- Sommaire (cartes inline, pas de grande table) --- $html = '
'; $html .= '
Remboursements
'; $html .= '
'; $cell = static function (string $label, string $value, string $color) use ($h): string { return '
' . '
' . $h($label) . '
' . '
' . $h($value) . '
' . '
'; }; $html .= $cell('Statut', $statNice ?: '—', '#2d6cdf'); $html .= $cell('Capturé', $fmtMoney($capTotal), '#333'); $html .= $cell('Remboursé', $fmtMoney($refTotal), '#c0392b'); $html .= $cell('Solde', $fmtMoney($remaining), '#1e7e34'); $html .= '
'; // --- Lignes de remboursements (compactes) --- $statusMapRef = [ 'S' => 'Effectué', 'COMPLETED' => 'Effectué', 'P' => 'En attente', 'PENDING' => 'En attente', 'D' => 'Refusé', 'DENIED' => 'Refusé', 'FAILED' => 'Échoué', 'V' => 'Annulé', 'VOIDED' => 'Annulé', 'R' => 'Reversé', 'REVERSED' => 'Reversé', ]; if (!empty($refundItems)) { foreach ($refundItems as $rf) { $rid = $get($rf, ['refund_id']) ?: $get($rf, ['id']); $amt = $moneyVal(['value' => $get($rf, ['amount'], 0)]); $cur = $get($rf, ['currency'], $currency); $srfRaw = (string)$get($rf, ['status'], ''); $srf = $statusMapRef[$srfRaw] ?? $srfRaw; $time = $get($rf, ['create_time']) ?: $get($rf, ['time']); $html .= '
' . '' . $h($fmtDate($time, $tz)) . '' . '' . $h(number_format($amt, 2, '.', '') . ($cur ? ' ' . $cur : '')) . '' . '' . $h($srf) . '' . '
'; } } $html .= '
'; return $html; } function money2($v) { return number_format((float)$v, 2, '.', ''); } function statut_payement($eve_id){ global $objDatabase; if($eve_id!=649){ $sqlPromoteur1 = " SELECT (SUM(a.ach_total) - SUM(a.fra_total) - COALESCE(SUM(rfm.ach_total), 0)) AS promoteur1 FROM inscriptions_panier_acheteurs a LEFT JOIN rapport_finances_membership rfm ON rfm.no_panier = a.no_panier WHERE a.sta_id = 3 AND a.pai_id <> 6 AND a.ach_total > 0 and a.eve_id=$eve_id GROUP BY a.eve_id ORDER BY a.eve_id "; $tabPromoteur1 = $objDatabase->fxGetVar($sqlPromoteur1); }else{ $sqlPromoteur1 = " SELECT SUM(montant) AS promoteur1 FROM ( SELECT (SUM(a.ach_total) - SUM(a.fra_total) - COALESCE(SUM(rfm.ach_total), 0)) AS montant FROM inscriptions_panier_acheteurs a LEFT JOIN rapport_finances_membership rfm ON rfm.no_panier = a.no_panier WHERE a.sta_id = 3 AND a.pai_id <> 6 AND a.ach_total > 0 AND a.eve_id = 649 UNION ALL SELECT SUM(rfm.ach_total) AS montant FROM rapport_finances_membership rfm JOIN inscriptions_panier_acheteurs a2 ON rfm.no_panier = a2.no_panier WHERE a2.sta_id = 3 AND a2.pai_id <> 6 AND rfm.ach_total > 0 AND rfm.eve_id_membership = 649 UNION ALL SELECT SUM(r.rem_montant * -1) AS montant FROM remboursements r WHERE r.rem_deleted = 0 AND r.eve_id = 649 AND 1 = 2 ) x; "; $tabPromoteur1 = $objDatabase->fxGetVar($sqlPromoteur1); } $sqlPromoteur2 = " SELECT (SUM(rfm.ach_total)) AS promoteur2 FROM rapport_finances_membership rfm JOIN inscriptions_panier_acheteurs a ON rfm.no_panier = a.no_panier WHERE a.sta_id = 3 AND a.pai_id <> 6 AND rfm.ach_total > 0 and a.eve_id=$eve_id GROUP BY rfm.eve_id_membership ORDER BY rfm.eve_id_membership "; $tabPromoteur2 = $objDatabase->fxGetVar($sqlPromoteur2); $sqlPromoteur3 = " SELECT SUM(r.rem_montant) AS remboursement FROM remboursements r WHERE r.rem_deleted = 0 AND r.tr_id = 2 and r.eve_id=$eve_id GROUP BY r.eve_id ORDER BY r.eve_id "; $tabPromoteur3 = $objDatabase->fxGetVar($sqlPromoteur3); $sqlPromoteur4 = " SELECT SUM(r.rem_montant) AS paiement FROM remboursements r JOIN inscriptions_evenements e ON e.eve_id = r.eve_id WHERE r.rem_deleted = 0 AND r.tr_id = 1 and r.eve_id=$eve_id GROUP BY e.eve_id ORDER BY e.eve_id "; $tabPromoteur4 = $objDatabase->fxGetVar($sqlPromoteur4); $fltRemboursement = floatval($tabPromoteur3); //echosl("Remboursement ".$fltRemboursement); $fltPaiement = floatval($tabPromoteur4); //echosl("Paiement ".$fltPaiement); // corriger member // $fltMontantPromoteur = floatval($tabPromoteur1[$eve_id]) + floatval($tabPromoteur2[$eve_id]); $fltMontantPromoteur = floatval($tabPromoteur1) ; //echosl("MontantPromoteur ".$fltMontantPromoteur); $fltSolde = floatval($fltMontantPromoteur - ($fltPaiement + $fltRemboursement)); return $fltSolde ; } /** * Renvoie le montant restant remboursable pour une capture PayPal. * * RÈGLES: * - REFUNDED => 0.00 * - PARTIALLY_REFUNDED + 0.00 en BD => 0.00 (on attend que les refunds apparaissent) * - Sinon: captured - somme(refunds visibles en BD), min=0.00 * * @param object $paypal Ton SDK/service PayPal (doit exposer getCaptureDetails($captureId)) * @param string $captureId ID de capture PayPal (ex: "3G598638AN207873G") * @param string &$currencyOut Mis à jour avec la devise (ex: "CAD") * @return float Montant restant remboursable * @throws RuntimeException si les détails de capture sont incomplets */ function getRefundableRemainingSimple($paypal, $captureId, &$currencyOut) { // 1) Détails de la capture $cap = $paypal->getCaptureDetails($captureId); if (!is_array($cap) || empty($cap['amount']['value']) || empty($cap['amount']['currency_code'])) { throw new RuntimeException("Détails de capture indisponibles pour $captureId."); } $captured = (float) $cap['amount']['value']; $currencyOut = (string) $cap['amount']['currency_code']; $status = strtoupper($cap['status'] ?? ''); // 2) Si entièrement remboursée, on coupe court if ($status === 'REFUNDED') { return 0.0; } // 3) Total des remboursements visibles en BD locale $totalRefunds = (float) sumRefundsFromLocalDB($captureId, $currencyOut); // 4) Règle de latence: statut partiel mais rien en BD => 0.00 if ($status === 'PARTIALLY_REFUNDED' && $totalRefunds <= 0.0) { return 0.0; } // 5) Calcul du restant $remaining = $captured - $totalRefunds; if ($remaining < 0) { $remaining = 0.0; } // On arrondit à 2 décimales façon prix return round($remaining, 2); } /** * Total des remboursements déjà enregistrés pour une capture. * Adapter l'implémentation à ton stack (CI3/CI4/PDO). * Filtrage par devise et statuts "réussis". * * @param string $captureId * @param string $currency (ex: "CAD") * @return float */ function sumRefundsFromLocalDB($captureId, $currency) { // ==== EXEMPLE CodeIgniter 3 ==== // $CI =& get_instance(); // $CI->db->select('SUM(amount_value) AS total', false); // $CI->db->from('paypal_refunds'); // $CI->db->where('capture_id', $captureId); // $CI->db->where('amount_currency', $currency); // $CI->db->where_in('status', ['COMPLETED','PARTIALLY_REFUNDED','REFUNDED']); // // (Exclure explicitement les échecs/annulations si tu les stockes) // // $CI->db->where_not_in('status', ['FAILED','CANCELLED']); // $row = $CI->db->get()->row_array(); // return (float) ($row['total'] ?? 0); // ==== EXEMPLE CodeIgniter 4 ==== // $db = \Config\Database::connect(); // $builder = $db->table('paypal_refunds'); // $builder->select('SUM(amount_value) AS total', false) // ->where('capture_id', $captureId) // ->where('amount_currency', $currency) // ->whereIn('status', ['COMPLETED','PARTIALLY_REFUNDED','REFUNDED']); // $row = $builder->get()->getRowArray(); // return (float) ($row['total'] ?? 0); // ==== EXEMPLE PDO pur ==== // try { // $pdo = getPdo(); // ta fonction pour récupérer le PDO // $sql = "SELECT SUM(amount_value) AS total // FROM paypal_refunds // WHERE capture_id = :cap // AND amount_currency = :cur // AND status IN ('COMPLETED','PARTIALLY_REFUNDED','REFUNDED')"; // $stmt = $pdo->prepare($sql); // $stmt->execute([':cap' => $captureId, ':cur' => $currency]); // $total = $stmt->fetchColumn(); // return (float) ($total ?: 0); // } catch (Throwable $e) { // // En cas d’erreur BD, on retourne 0 (ou on relance selon ta politique) // return 0.0; // } // Par défaut si tu colles ce fichier sans implémentation: return 0.0; } /** * Fusionne les remboursements PayPal avec la BD locale (clé = refund_id). * * @param array $paypal Tableau tel que retourné par ta fonction PayPal (avec clés: ok, capture_id, refunds[], totals, ...) * @param PDO $pdo Connexion PDO vers la BD contenant la table des refunds (schéma fourni). * @param string $table Nom de la table locale (par défaut 'paypal_refunds' si c’est ton nom; adapte-le si besoin). * @return array Même structure que $paypal, avec refunds[] enrichi + totaux recalculés. */ /** * Fusionne les remboursements PayPal avec la BD locale (clé = refund_id). * * @param array $paypal Tableau tel que retourné par ta fonction PayPal (ok, capture_id, refunds[], totals, ...) * @param object $objDatabase Ton objet DB déjà initialisé (doit exposer fxFetchAll($sql)) * @param string $table Nom de la table locale (par défaut 'paypal_refunds') * @return array Même structure que $paypal, avec refunds[] enrichi + totaux recalculés. */ /** * Enrichit le payload complet PayPal (qui contient 'delails' et 'refunds') avec les remboursements locaux. * - Entrée: $payload tel que retourné par ton code (avec ['delails'] et ['refunds']). * - Sortie: même payload, mêmes clés, mêmes valeurs, sauf: * - $payload['refunds']['refunds'] est fusionné avec la BD locale (clé = refund_id) * - $payload['refunds']['totals'] est recalculé d'après la liste fusionnée * * @param array $payload Payload complet PayPal (incluant 'delails' + 'refunds') * @param object $objDatabase Ton objet DB (fxFetchAll($sql) dispo) * @param string $table Nom de la table locale des refunds (par défaut 'paypal_refunds') * @return array Payload complet, inchangé hors de la section 'refunds' */ /** * Enrichit le payload complet PayPal (qui contient 'delails' et 'refunds') avec les remboursements locaux. * - Entrée : $payload tel que retourné par ton code (avec ['delails'] et ['refunds']). * - Sortie : même payload, mêmes clés/valeurs, SAUF : * - $payload['refunds']['refunds'] fusionnée avec la BD locale (clé = refund_id) * - $payload['refunds']['totals'] recalculée * - DB : utilise $objDatabase->fxGetResults($sql) pour récupérer des listes. * - Ajoute systématiquement la note TO PAYER depuis la BD : * - 'note_to_payer' (brut) * - 'note_to_payer_ui' : "Note TO PAYER : {valeur}" */ function reconcileRefundsWithLocal(array $payload ): array { // Copie du payload d'origine ; on ne modifiera QUE la section 'refunds' $out = $payload; global $objDatabase; // -- Détails (clé 'delails' telle quelle ; fallback 'details' si jamais) $details = []; if (isset($payload['delails']) && is_array($payload['delails'])) { $details = $payload['delails']; } elseif (isset($payload['details']) && is_array($payload['details'])) { $details = $payload['details']; } $table = 'inscriptions_panier_remboursement'; // -- Section refunds actuelle (préservée) $sectionRefunds = isset($payload['refunds']) && is_array($payload['refunds']) ? $payload['refunds'] : []; // -- Ancrages // NB: on n'utilise pas ?? seul car la section refunds peut exister mais etre VIDE // (makeEmptyRefunds() quand l'API reporting echoue : captured_total=0, currency=''), // et ?? ne retombe que sur null/unset. On retombe donc sur la capture (details) des // que la valeur de la section est vide/zero, sinon le solde restant serait fausse a 0 // et bloquerait a tort tout remboursement. $strFirst = static function (...$vals): string { foreach ($vals as $v) { if (is_string($v) ? $v !== '' : !empty($v)) return (string)$v; } return ''; }; $floatFirst = static function (...$vals): float { foreach ($vals as $v) { if ((float)$v > 0) return (float)$v; } return 0.0; }; $captureId = $strFirst( $sectionRefunds['capture_id'] ?? '', $details['id'] ?? '' ); $invoiceId = $strFirst( $sectionRefunds['invoice_id'] ?? '', $details['invoice_id'] ?? '' ); $orderId = $strFirst( $sectionRefunds['order_id'] ?? '', $details['supplementary_data']['related_ids']['order_id'] ?? '' ); $capCur = $strFirst( $sectionRefunds['totals']['currency'] ?? '', $details['amount']['currency_code'] ?? '' ); // La capture (details) fait foi pour le montant capture ; la section refunds ne sert // que de repli (et vaut 0 quand l'API reporting a echoue). $capTotal = $floatFirst( $details['amount']['value'] ?? 0, $sectionRefunds['totals']['captured_total'] ?? 0 ); // Déterminer l'URL base pour 'refund_api' des items locaux $anyHref = $details['links'][0]['href'] ?? ''; $apiBase = (strpos($anyHref, 'sandbox') !== false) ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com'; // -- Liste PayPal de départ $ppRefunds = (isset($sectionRefunds['refunds']) && is_array($sectionRefunds['refunds'])) ? $sectionRefunds['refunds'] : []; // Index par refund_id $ppById = []; $ppIds = []; foreach ($ppRefunds as $r) { $rid = (string)($r['refund_id'] ?? ''); if ($rid !== '') { $ppById[$rid] = $r; $ppIds[] = $rid; } } // -- 1) Locaux pour refund_id connus par PayPal $localById = []; if (!empty($ppIds)) { $idsIn = "'" . implode("','", array_map('addslashes', $ppIds)) . "'"; $sql = "SELECT * FROM `{$table}` WHERE `refund_id` IN ($idsIn)"; $rowsLocal = (array)$objDatabase->fxGetResults($sql); foreach ($rowsLocal as $row) { $localById[(string)$row['refund_id']] = $row; } } // -- 2) Locaux supplémentaires (absents côté PayPal) if (!empty($ppIds)) { $idsNotIn = "'" . implode("','", array_map('addslashes', $ppIds)) . "'"; $sqlExtra = "SELECT * FROM `{$table}` WHERE `capture_id` = '" . addslashes($captureId) . "' AND `refund_id` NOT IN ($idsNotIn)"; } else { $sqlExtra = "SELECT * FROM `{$table}` WHERE `capture_id` = '" . addslashes($captureId) . "'"; } $localExtra = (array)$objDatabase->fxGetResults($sqlExtra); // -- 3) Fusion : partir de PayPal et compléter avec la BD $merged = []; foreach ($ppById as $rid => $r) { $loc = $localById[$rid] ?? null; $m = $r; // base = PayPal // Compléter les champs vides depuis la BD locale (sans écraser ce que PayPal fournit déjà) if ((!isset($m['amount']) || $m['amount'] === '' || $m['amount'] === null) && $loc && $loc['amount_value'] !== null) { $m['amount'] = (float)$loc['amount_value']; } if ((!isset($m['currency']) || $m['currency'] === '' || $m['currency'] === null) && $loc && !empty($loc['amount_currency'])) { $m['currency'] = (string)$loc['amount_currency']; } if ((!isset($m['status']) || $m['status'] === '' || $m['status'] === null) && $loc) { $m['status'] = (string)($loc['status'] ?? $loc['status_interne'] ?? ($m['status'] ?? '')); } if ((!isset($m['time']) || $m['time'] === '' || $m['time'] === null) && $loc) { $t = $loc['update_time'] ?? $loc['create_time'] ?? null; if ($t) { $dt = new DateTime($t, new DateTimeZone('UTC')); $m['time'] = $dt->format('Y-m-d\TH:i:s\Z'); } } if ((!isset($m['invoice_id']) || $m['invoice_id'] === '' || $m['invoice_id'] === null) && $loc && !empty($loc['invoice_id'])) { $m['invoice_id'] = (string)$loc['invoice_id']; } // 🔹 Ajouter systématiquement la note TO PAYER (si dispo en BD) if ($loc && isset($loc['note_to_payer']) && $loc['note_to_payer'] !== '') { $m['note_to_payer'] = (string)$loc['note_to_payer']; $m['note_to_payer_ui'] = htmlspecialchars((string)$loc['note_to_payer'], ENT_QUOTES, 'UTF-8'); } // Bloc ms1 (trace locale non destructive) if ($loc) { $m['ms1'] = [ 'status' => $loc['status'] ?? null, 'status_interne' => $loc['status_interne'] ?? null, 'amount_value' => $loc['amount_value'] ?? null, 'amount_currency' => $loc['amount_currency'] ?? null, 'invoice_id' => $loc['invoice_id'] ?? null, 'custom_id' => $loc['custom_id'] ?? null, 'acquirer_reference_number' => $loc['acquirer_reference_number'] ?? null, 'seller_gross' => $loc['seller_gross'] ?? null, 'seller_net' => $loc['seller_net'] ?? null, 'paypal_fee' => $loc['paypal_fee'] ?? null, 'note_to_payer' => $loc['note_to_payer'] ?? null, 'paypal_debug_id' => $loc['paypal_debug_id'] ?? null, 'idempotency_key' => $loc['idempotency_key'] ?? null, 'create_time' => $loc['create_time'] ?? null, 'update_time' => $loc['update_time'] ?? null, // 'raw_json' => $loc['raw_json'] ?? null, ]; } $merged[] = $m; } // -- 4) Ajouter les remboursements présents uniquement en local foreach ($localExtra as $loc) { $rid = (string)($loc['refund_id'] ?? ''); if ($rid === '') continue; $amt = isset($loc['amount_value']) ? (float)$loc['amount_value'] : 0.0; $cur = !empty($loc['amount_currency']) ? (string)$loc['amount_currency'] : $capCur; $dt = null; if (!empty($loc['update_time']) || !empty($loc['create_time'])) { $raw = $loc['update_time'] ?: $loc['create_time']; $dt = (new DateTime($raw, new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z'); } $item = [ 'refund_id' => $rid, 'amount' => abs($amt), 'currency' => $cur, 'status' => 'PENDING', // en attente de PayPal 'note' => 'En attente de confirmation PayPal', 'time' => $dt, 'paypal_reference_id' => $captureId, 'reference_type' => 'CAPTURE', 'related_transaction' => null, 'event_code' => null, 'classification' => [], 'invoice_id' => (string)($loc['invoice_id'] ?? ''), 'refund_api' => ($rid ? ($apiBase . '/v2/payments/refunds/' . $rid) : null), 'ms1' => [ 'status' => $loc['status'] ?? null, 'status_interne' => $loc['status_interne'] ?? null, 'amount_value' => $loc['amount_value'] ?? null, 'amount_currency' => $loc['amount_currency'] ?? null, 'invoice_id' => $loc['invoice_id'] ?? null, 'custom_id' => $loc['custom_id'] ?? null, 'acquirer_reference_number' => $loc['acquirer_reference_number'] ?? null, 'seller_gross' => $loc['seller_gross'] ?? null, 'seller_net' => $loc['seller_net'] ?? null, 'paypal_fee' => $loc['paypal_fee'] ?? null, 'note_to_payer' => $loc['note_to_payer'] ?? null, 'paypal_debug_id' => $loc['paypal_debug_id'] ?? null, 'idempotency_key' => $loc['idempotency_key'] ?? null, 'create_time' => $loc['create_time'] ?? null, 'update_time' => $loc['update_time'] ?? null, ], ]; // 🔹 Ajouter systématiquement la note TO PAYER si dispo if (isset($loc['note_to_payer']) && $loc['note_to_payer'] !== '') { $item['note_to_payer'] = (string)$loc['note_to_payer']; $item['note_to_payer_ui'] = htmlspecialchars((string)$loc['note_to_payer'], ENT_QUOTES, 'UTF-8'); } $merged[] = $item; } // -- 5) Recalcul des totaux $refunded = 0.0; foreach ($merged as $m) { $refunded += (float)($m['amount'] ?? 0); } $remaining = max(0, (float)$capTotal - $refunded); // -- 6) Sortie : reconstruire la section refunds en conservant les autres clés d'origine $sectionOut = $sectionRefunds; // copie intégrale if (!isset($sectionOut['ok'])) $sectionOut['ok'] = 1; if (!isset($sectionOut['capture_id'])) $sectionOut['capture_id'] = $captureId; if (!isset($sectionOut['invoice_id'])) $sectionOut['invoice_id'] = $invoiceId; if (!isset($sectionOut['order_id'])) $sectionOut['order_id'] = $orderId; if (!isset($sectionOut['errors'])) $sectionOut['errors'] = []; $sectionOut['refunds'] = $merged; $sectionOut['totals'] = [ 'currency' => $capCur, 'captured_total' => (float)number_format((float)$capTotal, 2, '.', ''), 'refunded_total' => (float)number_format((float)$refunded, 2, '.', ''), 'remaining' => (float)number_format((float)$remaining, 2, '.', ''), ]; $out['refunds'] = $sectionOut; return $out; } function postRefundSuccess(string $capture_id, float $amount, string $currency, string $note, array $paypalResponse = [],int $eve_id = 0,string $no_commande = '', string $strAjoutePar = ''): void { // 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'); } /* ================================================================== * Remboursement PayPal - briques mutualisees (Super Admin + fiche gestion) * * IMPORTANT : ces fonctions ne dependent QUE de ce fichier * (statut_payement, reconcileRefundsWithLocal, money2, h) + de la classe * PaypalCheckout. Elles n'incluent JAMAIS superadm/php/inc_functions.php * (le « U »), afin d'eviter tout conflit "Cannot redeclare" avec * php/inc_fonctions.php (le « O ») charge dans le contexte /compte. * ================================================================== */ /** * Garantit la presence d'un jeton anti double-clic en session. * @return string le jeton */ function fxRefundEnsureToken(): string { if (session_status() === PHP_SESSION_NONE) { session_start(); } if (empty($_SESSION['refund_token'])) { $_SESSION['refund_token'] = bin2hex(random_bytes(16)); } 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). * @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, * split_active:bool, event_share:float, sister_share:float, eve_id_context:int * } */ function fxRefundComputeContext(array $recCommandes, int $intEveContext = 0): array { $out = [ 'ok' => false, 'capture_id' => '', 'currency' => 'CAD', 'paypaldetails' => [], 'max_paypal' => 0.0, 'solde_dispo' => 0.0, 'frais_ms1' => 0.0, '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; if (!is_string($captureId) || $captureId === '') { return $out; // pas de capture PayPal exploitable } $out['capture_id'] = $captureId; require_once $_SERVER['DOCUMENT_ROOT'] . '/paypal_advanced/PaypalCheckout.class.php'; $paypal = new PayPalCheckout(); $intSoldeEveId = ($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); $paypaldetails = reconcileRefundsWithLocal($paypaldetails); $out['paypaldetails'] = $paypaldetails; // Max remboursable PayPal : remaining si dispo, sinon montant capture (tolere "delails") $maxPaypal = 0.0; if (isset($paypaldetails['refunds']['totals']['remaining'])) { $maxPaypal = (float) $paypaldetails['refunds']['totals']['remaining']; } elseif (!empty($paypaldetails['details']['amount']['value'])) { $maxPaypal = (float) $paypaldetails['details']['amount']['value']; } elseif (!empty($paypaldetails['delails']['amount']['value'])) { $maxPaypal = (float) $paypaldetails['delails']['amount']['value']; } $out['max_paypal'] = $maxPaypal; // Devise d'affichage if (!empty($paypaldetails['refunds']['totals']['currency'])) { $out['currency'] = (string) $paypaldetails['refunds']['totals']['currency']; } elseif (!empty($paypaldetails['details']['amount']['currency_code'])) { $out['currency'] = (string) $paypaldetails['details']['amount']['currency_code']; } elseif (!empty($paypaldetails['delails']['amount']['currency_code'])) { $out['currency'] = (string) $paypaldetails['delails']['amount']['currency_code']; } $out['frais_ms1'] = (float) ($recCommandes['fra_total'] ?? 0); 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; } /** * Rend le formulaire de remboursement PayPal (carte + script AJAX). * Fragment HTML autonome (pas de /), reutilisable partout. * * @param array $recCommandes Ligne commande * @param string $refundToken Jeton anti double-clic * @param array $ctx Resultat de fxRefundComputeContext() * @param string $vDomaine Domaine de base (pour l'URL AJAX par defaut) * @param string $auteur Nom de la personne qui effectue le remboursement * (resolu par l'appelant : Super Admin -> usa_info, * promoteur -> com_info). Affiche dans l'encart "Ajoute par". * @param string|null $ajaxUrl URL de l'endpoint (def: $vDomaine.'/superadm/ajax_refund.php') * @param bool $blnInlineScript true = inclut le