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 .= '| '.$h($label).' | ';
if (is_string($val) && strpos($val, 'http') === 0) {
$html .= ''.$h($val).' | ';
} else {
$html .= ''.$h($val).' | ';
}
$html .= '
';
}
$html .= '
';
// ------------------ 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 .= '| Total capturé | '
. ''.$h(number_format($capTotal,2,'.','').($currency?' '.$currency:'' )).' |
';
$html .= '| Total remboursé | '
. ''.$h(number_format($refTotal,2,'.','').($currency?' '.$currency:'' )).' |
';
$html .= '| Solde après remboursement | '
. ''.$h(number_format($remaining,2,'.','').($currency?' '.$currency:'' )).' |
';
$html .= '
';
// Détail des remboursements
$html .= '';
$html .= ''
. '| Refund ID | '
. 'Statut | '
. 'Montant | '
. 'Créé le | '
. 'Note | ' // ⬅️ NOUVEAU
. 'Note TO PAYER | ' // ⬅️ 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 .= '| '.$h($nrf['id']).' | ';
$html .= ''.$h($statNice).' | ';
$html .= ''.$h($moneyStr($nrf['amountArr'])).' | ';
$html .= ''.$h($fmtDate($nrf['time'], $tz)).' | ';
// Colonne "Note" (texte simple)
$html .= ''
. ($nrf['note'] !== '' ? $h($nrf['note']) : '')
. ' | ';
// Colonne "Note TO PAYER" (déjà formaté avec …, ne pas ré-échaper)
$html .= ''
. (!empty($nrf['note_to_payer_ui']) ? $nrf['note_to_payer_ui'] : '')
. ' | ';
$html .= '
';
}
$html .= '
';
}
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