Files
ms1inscription-v5/superadm/php/inc_fx_paypal.php
stephan 8933753fe0 MSIN-4417 Update refund context handling in registration management
This commit refines the refund context handling by removing the unnecessary boolean parameter for Super Admin in the `fxRefundComputeContext` function, simplifying the logic. It ensures that refunds are consistently linked to the correct event context across various functions, enhancing authorization checks and calculations. The version code is incremented to 4.72.719 to reflect these changes.
2026-06-30 15:33:40 -04:00

1427 lines
57 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
function paypaldetails($capId){
error_reporting(E_ALL);
ini_set('display_errors', '1');
require_once '../paypal_advanced/config.php';
include_once '../paypal_advanced/dbConnect.php';
$dbpaypal = new mysqli(DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME);
if ($dbpaypal->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 dextraire debug_id du message dexception 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 nexiste 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 nexplose 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 lAPI 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 = '<table class="pp-capture" style="border-collapse:collapse;width:100%;max-width:900px;font-family:sans-serif;font-size:14px;margin-bottom:16px;">';
foreach ($rows as $label => $val) {
if ($val === '' || $val === null) continue;
$html .= '<tr>';
$html .= '<th style="text-align:left;vertical-align:top;border:1px solid #ddd;padding:8px;background:#f7f7f7;width:35%;">'.$h($label).'</th>';
if (is_string($val) && strpos($val, 'http') === 0) {
$html .= '<td style="border:1px solid #ddd;padding:8px;"><a href="'.$h($val).'" target="_blank" rel="noopener">'.$h($val).'</a></td>';
} else {
$html .= '<td style="border:1px solid #ddd;padding:8px;">'.$h($val).'</td>';
}
$html .= '</tr>';
}
$html .= '</table>';
// ------------------ 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 <u>TO</u> <u>PAYER</u> : ..."
$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 .= '<h3 style="font-family:sans-serif;margin:12px 0 6px;">Remboursements</h3>';
// Résumé des totaux
$html .= '<table class="pp-refunds-summary" style="border-collapse:collapse;width:100%;max-width:900px;font-family:sans-serif;font-size:14px;margin-bottom:10px;">';
$html .= '<tr><th style="text-align:left;border:1px solid #ddd;padding:8px;background:#f7f7f7;width:35%;">Total capturé</th>'
. '<td style="border:1px solid #ddd;padding:8px;text-align:right;font-weight:bold;">'.$h(number_format($capTotal,2,'.','').($currency?' '.$currency:'' )).'</td></tr>';
$html .= '<tr><th style="text-align:left;border:1px solid #ddd;padding:8px;background:#f7f7f7;">Total remboursé</th>'
. '<td style="border:1px solid #ddd;padding:8px;text-align:right;font-weight:bold;">'.$h(number_format($refTotal,2,'.','').($currency?' '.$currency:'' )).'</td></tr>';
$html .= '<tr><th style="text-align:left;border:1px solid #ddd;padding:8px;background:#f7f7f7;">Solde après remboursement</th>'
. '<td style="border:1px solid #ddd;padding:8px;text-align:right;font-weight:bold;">'.$h(number_format($remaining,2,'.','').($currency?' '.$currency:'' )).'</td></tr>';
$html .= '</table>';
// Détail des remboursements
$html .= '<table class="pp-refunds" style="border-collapse:collapse;width:100%;max-width:900px;font-family:sans-serif;font-size:14px;">';
$html .= '<tr style="background:#f7f7f7">'
. '<th style="text-align:left;border:1px solid #ddd;padding:8px;">Refund ID</th>'
. '<th style="text-align:left;border:1px solid #ddd;padding:8px;">Statut</th>'
. '<th style="text-align:right;border:1px solid #ddd;padding:8px;">Montant</th>'
. '<th style="text-align:left;border:1px solid #ddd;padding:8px;">Créé le</th>'
. '<th style="text-align:left;border:1px solid #ddd;padding:8px;">Note</th>' // ⬅️ NOUVEAU
. '<th style="text-align:left;border:1px solid #ddd;padding:8px;">Note TO PAYER</th>' // ⬅️ NOUVEAU
. '</tr>';
$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 .= '<tr>';
$html .= '<td style="border:1px solid #ddd;padding:8px;">'.$h($nrf['id']).'</td>';
$html .= '<td style="border:1px solid #ddd;padding:8px;">'.$h($statNice).'</td>';
$html .= '<td style="border:1px solid #ddd;padding:8px;text-align:right;">'.$h($moneyStr($nrf['amountArr'])).'</td>';
$html .= '<td style="border:1px solid #ddd;padding:8px;">'.$h($fmtDate($nrf['time'], $tz)).'</td>';
// Colonne "Note" (texte simple)
$html .= '<td style="border:1px solid #ddd;padding:8px;">'
. ($nrf['note'] !== '' ? $h($nrf['note']) : '')
. '</td>';
// Colonne "Note TO PAYER" (déjà formaté avec <u>…</u>, ne pas ré-échaper)
$html .= '<td style="border:1px solid #ddd;padding:8px;">'
. (!empty($nrf['note_to_payer_ui']) ? $nrf['note_to_payer_ui'] : '')
. '</td>';
$html .= '</tr>';
}
$html .= '</table>';
}
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 = '<div style="font-family:sans-serif;font-size:13px;">';
$html .= '<div style="font-weight:700;margin-bottom:6px;">Remboursements</div>';
$html .= '<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:6px;">';
$cell = static function (string $label, string $value, string $color) use ($h): string {
return '<div style="flex:1 1 120px;min-width:110px;background:#fff;border:1px solid #dbe7ff;border-radius:8px;padding:6px 10px;">'
. '<div style="font-size:11px;color:#666;">' . $h($label) . '</div>'
. '<div style="font-weight:700;color:' . $color . ';">' . $h($value) . '</div>'
. '</div>';
};
$html .= $cell('Statut', $statNice ?: '—', '#2d6cdf');
$html .= $cell('Capturé', $fmtMoney($capTotal), '#333');
$html .= $cell('Remboursé', $fmtMoney($refTotal), '#c0392b');
$html .= $cell('Solde', $fmtMoney($remaining), '#1e7e34');
$html .= '</div>';
// --- 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 .= '<div style="display:flex;justify-content:space-between;gap:8px;border-top:1px solid #eee;padding:4px 0;color:#444;">'
. '<span>' . $h($fmtDate($time, $tz)) . '</span>'
. '<span style="font-weight:600;">' . $h(number_format($amt, 2, '.', '') . ($cur ? ' ' . $cur : '')) . '</span>'
. '<span style="color:#666;">' . $h($srf) . '</span>'
. '</div>';
}
}
$html .= '</div>';
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 derreur 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 cest 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 <u>TO</u> <u>PAYER</u> : {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 <html>/<head>), 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 <script> inline (page complete, ex. Super Admin).
* false = markup seul, sans <script> : a utiliser quand le formulaire
* est injecte via AJAX (innerHTML n'execute pas les <script>).
* Dans ce cas, le comportement est gere par un handler delegue
* (voir inc_footer_scripts.php) qui lit l'attribut data-refund-url.
* @param string $strDocAsideHtml Panneau doc promoteur (colonne droite). Vide = une seule colonne.
* @return string HTML
*/
function fxRenderRefundForm(array $recCommandes, string $refundToken, array $ctx, string $vDomaine, string $auteur = '', ?string $ajaxUrl = null, bool $blnInlineScript = true, string $strDocAsideHtml = ''): string
{
if ($ajaxUrl === null) {
$ajaxUrl = $vDomaine . '/superadm/ajax_refund.php';
}
// --- Donnees d'affichage extraites du contexte ---
$captureId = (string) ($ctx['capture_id'] ?? '');
$currencyUI = (string) ($ctx['currency'] ?? 'CAD');
$maxUI = (float) ($ctx['maxUI'] ?? 0);
$maxblock = (float) ($ctx['maxblock'] ?? 0);
$maxproposer = (float) ($ctx['maxproposer'] ?? 0);
$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) !== '';
ob_start();
if ($blnHasDocAside) {
echo '<div class="ms1-refund-wrap"><div class="ms1-refund-layout"><div class="ms1-refund-main">';
}
?>
<div class="ms1-refund<?= $blnHasDocAside ? '' : ' ms1-refund--solo' ?>">
<div style="font-weight:700;font-size:16px;margin-bottom:8px;">Remboursement PayPal (Checkout)</div>
<!-- Zone messages AJAX -->
<div id="refund-msg" style="margin-bottom:10px;display:none;padding:10px;border-radius:8px;"></div>
<!-- Encart d'informations : toujours visible, reutilise en Super Admin ET dans la fiche promoteur.
Remplace l'ancien bloc de debug echosl() qui etait code en mode rapide. -->
<div style="background:#f9fbff;border:1px solid #dbe7ff;padding:10px;border-radius:8px;margin-bottom:10px;font-size:13px;line-height:1.5;">
<div><strong>N° de commande :</strong> <?= h($noCommande) ?></div>
<?php if ($auteur !== ''): ?>
<div><strong>Ajouté par :</strong> <?= h($auteur) ?></div>
<?php endif; ?>
<div><strong>Capture PayPal :</strong> <?= h($captureId) ?></div>
<div><strong>Solde disponible MS1 :</strong> <?= h(money2($soldeDispo)) ?> <?= h($currencyUI) ?></div>
<div><strong>Frais MS1 :</strong> <?= h(money2($fraisMs1)) ?> <?= h($currencyUI) ?></div>
<?php if ($blnSplitActive): ?>
<div><strong>Part de votre événement :</strong> <?= h(money2($fltEventShare)) ?> <?= h($currencyUI) ?></div>
<?php if ($fltSisterShare > 0): ?>
<div><strong>Part autre événement (non remboursable ici) :</strong> <?= h(money2($fltSisterShare)) ?> <?= h($currencyUI) ?></div>
<?php endif; ?>
<?php endif; ?>
<!-- #pp_ref_max : valeur mise a jour dynamiquement par le JS apres un remboursement (etape 5) -->
<div><strong>Max remboursable :</strong> <span id="pp_ref_max"><?= h(money2($maxUI)) ?></span> <?= h($currencyUI) ?></div>
</div>
<?php if ($maxUI > 0): ?>
<!-- data-refund-url : lu par le handler delegue quand le <script> inline est absent (mode AJAX) -->
<form id="refund-form" method="post" data-refund-url="<?= h($ajaxUrl) ?>" style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<input type="hidden" name="refund_token" value="<?= h($refundToken) ?>">
<input type="hidden" name="capture_id" value="<?= h($captureId) ?>">
<input type="hidden" name="currency" value="<?= h($currencyUI) ?>">
<input type="hidden" name="eve_id" value="<?= h($recCommandes['eve_id'] ?? '') ?>">
<?php if ($intEveContext > 0): ?>
<input type="hidden" name="refund_eve_context" value="<?= h($intEveContext) ?>">
<?php endif; ?>
<input type="hidden" name="no_commande" value="<?= h($recCommandes['no_commande'] ?? '') ?>">
<div style="grid-column:1/2;">
<label style="display:block;font-size:12px;color:#333;margin-bottom:4px;">
Montant à rembourser (<?= h($currencyUI) ?>)
</label>
<input id="pp_ref_amount" type="number" name="refund_amount" step="0.01" min="0.01"
max="<?= h(money2($maxblock)) ?>"
value="<?= h(money2($maxproposer)) ?>"
style="width:100%;padding:8px;border:1px solid #ccc;border-radius:8px;">
</div>
<div style="grid-column:2/3;">
<label style="display:block;font-size:12px;color:#333;margin-bottom:4px;">Note (optionnelle)</label>
<input type="text" name="refund_note" placeholder="Raison du remboursement"
style="width:100%;padding:8px;border:1px solid #ccc;border-radius:8px;">
</div>
<div style="grid-column:1/3;text-align:right;margin-top:6px;">
<button type="submit"
style="padding:10px 14px;border:1px solid #2d6cdf;border-radius:8px;background:#2d6cdf;color:#fff;cursor:pointer;">
Rembourser
</button>
</div>
</form>
<?php else: ?>
<div style="background:#fff8e6;border:1px solid #ffe1a6;padding:10px;border-radius:8px;">
Aucun montant remboursable pour cette capture.
</div>
<?php endif; ?>
</div>
<?php
if ($blnHasDocAside) {
echo '</div>' . $strDocAsideHtml . '</div></div>';
}
?>
<?php if ($blnInlineScript): // script inline uniquement en page complete (ex. Super Admin) ?>
<script>
(function () {
function showMsg(text, ok) {
var box = document.getElementById('refund-msg');
if (!box) return;
box.style.display = 'block';
box.style.background = ok ? '#e9f8ef' : '#feecec';
box.style.border = ok ? '1px solid #b8e3c5' : '1px solid #f2b8b5';
box.innerText = text;
}
var form = document.getElementById('refund-form');
if (!form) return;
form.addEventListener('submit', async function (e) {
e.preventDefault();
showMsg('⏳ Traitement en cours...', false);
var data = new FormData(e.target);
var res;
try {
res = await fetch(<?= json_encode($ajaxUrl) ?>, { method: 'POST', body: data });
} catch (_) {
showMsg('⚠️ Erreur réseau pendant le remboursement.', false);
return;
}
if (!res.ok) {
showMsg('⚠️ Erreur réseau pendant le remboursement.', false);
return;
}
var json;
try { json = await res.json(); } catch (_) {
showMsg('⚠️ Réponse invalide du serveur.', false);
return;
}
if (json.success) {
showMsg('✅ ' + (json.message || 'Remboursement effectué.'), true);
} else {
showMsg('⚠️ ' + (json.message || 'Erreur remboursement.'), false);
}
});
})();
</script>
<?php endif; ?>
<?php
return ob_get_clean();
}