Files
ms1inscription-v5/paypal_advanced/PaypalCheckout.class.php
2026-05-13 09:43:32 -04:00

674 lines
29 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
global $vPaypal_devise,$vPaypaladv_SANDBOX,$db;
/**
*
* This PayPal Checkout API handler class is a custom PHP library to handle the PayPal REST API calls.
*
* @class PaypalCheckout
* @author CodexWorld
* @link https://www.codexworld.com
* @version 1.0
*/
// Include the configuration file
require_once($_SERVER['DOCUMENT_ROOT'].'/paypal_advanced/config.php');
class PaypalCheckout
{
// public $paypalAuthAPI = PAYPAL_SANDBOX ? 'https://api-m.sandbox.paypal.com/v1/oauth2/token' : 'https://api-m.paypal.com/v1/oauth2/token';
// public $paypalAPI = PAYPAL_SANDBOX ? 'https://api-m.sandbox.paypal.com/v2/checkout' : 'https://api-m.paypal.com/v2/checkout';
// public $paypalClientID = PAYPAL_SANDBOX ? PAYPAL_SANDBOX_CLIENT_ID : PAYPAL_PROD_CLIENT_ID;
// private $paypalSecret = PAYPAL_SANDBOX ? PAYPAL_SANDBOX_CLIENT_SECRET : PAYPAL_PROD_CLIENT_SECRET;
public $paypalAuthAPI = PAYPAL_BASE_URL.'/v1/oauth2/token';
public $paypalAPI = PAYPAL_BASE_URL.'/v2/checkout';
public $paypalClientID = PAYPAL_CLIENT_ID;
private $paypalSecret = PAYPAL_CLIENT_SECRET;
public function generateAccessToken()
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_VERBOSE, 1);
curl_setopt($ch, CURLOPT_URL, $this->paypalAuthAPI);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERPWD, $this->paypalClientID . ":" . $this->paypalSecret);
curl_setopt($ch, CURLOPT_POSTFIELDS, "grant_type=client_credentials");
$auth_response = json_decode(curl_exec($ch));
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code != 200 && !empty($auth_response->error)) {
throw new Exception('Failed to generate Access Token: ' . $auth_response->error . ' >>> ' . $auth_response->error_description);
}
if (!empty($auth_response)) {
return $auth_response->access_token;
} else {
return false;
}
}
public function createOrder($productInfo, $paymentSource)
{
$accessToken = $this->generateAccessToken();
if (empty($accessToken)) {
return false;
} else {
$postParams = $productInfo;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->paypalAPI . '/orders/');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json', 'Authorization: Bearer ' . $accessToken));
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postParams));
$api_resp = curl_exec($ch);
$api_data = json_decode($api_resp, true);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code != 200 && $http_code != 201 && $http_code != 422 ) {
throw new Exception('Failed to create Order sl (' . $http_code . '): ' . $api_resp . json_encode($postParams));
}
return !empty($api_data) && ($http_code == 200 || $http_code == 201 || $http_code == 422) ? $api_data : false;
}
}
public function captureOrder($orderId)
{
$accessToken = $this->generateAccessToken();
if (empty($accessToken)) {
return false;
} else {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->paypalAPI . '/orders/' . $orderId . '/capture');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json', 'Authorization: Bearer ' . $accessToken));
curl_setopt($ch, CURLOPT_POST, true);
$api_resp = curl_exec($ch);
$api_data = json_decode($api_resp, true);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlsl_code = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
// $result = urldecode ($ch );
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($http_code != 200 && $http_code != 201) {
throw new Exception('Failed to create Order sl4(' . $http_code . '): ' . '1- ' . $api_resp);
}
curl_close($ch);
return !empty($api_data) && ($http_code == 200 || $http_code == 201) ? $api_data : false;
}
}
public function setInvoiceId($orderId,$invoiceId)
{
global $db;
$accessToken = $this->generateAccessToken(); // même mécanique que createOrder/captureOrder
$url = $this->paypalAPI . '/orders/' . rawurlencode($orderId);
$my_custom_id = $invoiceId;
$ops = [
['op' => 'add', 'path' => "/purchase_units/@reference_id=='default'/custom_id", 'value' => $my_custom_id],
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $accessToken,
'Content-Type: application/json'
],
CURLOPT_POSTFIELDS => json_encode($ops),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 20,
]);
$body = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlerr = curl_error($ch);
curl_close($ch);
if ($curlerr) {
throw new \Exception('PayPal PATCH error (cURL): ' . $curlerr);
}
// PayPal renvoie 204 No Content quand le PATCH réussit
if ($httpcode === 204 || $httpcode === 200) {
return true;
}
// Si échec: on lève une exception avec le code HTTP et, si présent, le corps
throw new \Exception('PayPal PATCH /orders failed (HTTP ' . $httpcode . '): ' . ($body ?: 'no body'));
}
// *****************************************************remboursement
public function getCaptureDetails(string $captureId): array {
if ($captureId === '') throw new \InvalidArgumentException('captureId requis');
$accessToken = $this->generateAccessToken();
if (empty($accessToken)) throw new \Exception('Impossible de générer le token daccès PayPal');
// Base API absolue (voir section 2)
$base = PAYPAL_BASE_URL;
$url = $base . '/v2/payments/captures/' . rawurlencode($captureId);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . $accessToken,
],
]);
$resp = curl_exec($ch);
$curlerr = curl_error($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($curlerr) throw new \Exception('PayPal getCaptureDetails error (cURL): ' . $curlerr);
$data = json_decode($resp, true);
if ($httpcode >= 200 && $httpcode < 300) return $data ?: [];
throw new \Exception('getCaptureDetails failed (HTTP ' . $httpcode . '): ' . ($resp ?: 'no body'));
}
public function paypaldetails($capId)
{
error_reporting(E_ALL);
ini_set('display_errors', '1');
// --- Guards de classe ---
if (!isset($this)) {
throw new RuntimeException('PaypalCheckout ($this->paypal) nest pas initialisé dans la classe.');
}
// Helpers pour un retour propre si PayPal ne répond pas (inchangés)
$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 (utilise $this->paypal, pas de new/require)
try {
$delails = $this->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 = $this->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,
];
}
/**
* Retourne les remboursements liés à une capture (tableau, pas d'affichage).
* - Cherche via Transaction Search autour de la date de la capture.
* - Inclut les écritures NON encore comptabilisées (PENDING).
* - Reconnaît plusieurs codes d'événements de refund (pas seulement T1107).
*
* @param array $capture tableau /v2/payments/captures/{id}
* @param int $hoursBefore fenêtre avant create_time (défaut 24h)
* @param int $hoursAfter fenêtre après update_time (défaut 120h)
* @return array { ok, capture_id, invoice_id, order_id, refunds[], totals{}, errors[] }
* @throws Exception
*/
public function getRefundsForCapture(array $capture, int $hoursBefore = 24, int $hoursAfter = 120): array
{
$errors = [];
$capId = $capture['id'] ?? '';
$invId = $capture['invoice_id'] ?? '';
$ordId = $capture['supplementary_data']['related_ids']['order_id'] ?? '';
$capAmt = isset($capture['amount']['value']) ? (float)$capture['amount']['value'] : 0.0;
$capCur = $capture['amount']['currency_code'] ?? '';
$tCreate = $capture['create_time'] ?? '';
$tUpdate = $capture['update_time'] ?? $tCreate;
if ($capId === '' || $tCreate === '') {
throw new \InvalidArgumentException('Capture id ou create_time manquants');
}
// Fenêtre de recherche (UTC strict)
$tzUtc = new \DateTimeZone('UTC');
$t0 = (new \DateTime($tCreate))->setTimezone($tzUtc);
$tU = (new \DateTime($tUpdate))->setTimezone($tzUtc);
$startIso = $t0->sub(new \DateInterval('PT' . max(0, $hoursBefore) . 'H'))->format('Y-m-d\TH:i:s\Z');
$endIso = $tU->add(new \DateInterval('PT' . max(1, $hoursAfter) . 'H'))->format('Y-m-d\TH:i:s\Z');
// OAuth + base api-m
$token = $this->generateAccessToken();
if (!$token) throw new \Exception('Token PayPal indisponible');
$base = PAYPAL_BASE_URL;
// Vérifie à quel compte appartient le token
$urlUserInfo = $base . '/v1/identity/oauth2/userinfo?schema=paypalv1.1';
$ch = curl_init($urlUserInfo);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token, 'Accept: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
]);
$resUser = curl_exec($ch);
$httpUser = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpUser !== 200) {
throw new \Exception("UserInfo HTTP $httpUser: " . ($resUser ?: 'Échec vérification compte.'));
}
$me = json_decode($resUser, true);
if (!empty($this->merchant_payer_id) && (($me['payer_id'] ?? '') !== $this->merchant_payer_id)) {
throw new \Exception(
"NOT_AUTHORIZED: le token appartient à " . ($me['payer_id'] ?? '?') .
" et non au marchand attendu " . $this->merchant_payer_id .
". Vérifie le client_id/secret utilisés."
);
}
// Prépare éventuellement un header PayPal-Auth-Assertion si besoin dagir pour un autre marchand
$authAssertion = null;
if (!empty($this->act_as_payer_id)) {
$header = rtrim(strtr(base64_encode('{"alg":"none"}'), '+/', '-_'), '=');
$payload = rtrim(strtr(base64_encode(json_encode([
'iss' => $this->clientId,
'payer_id' => $this->act_as_payer_id
])), '+/', '-_'), '=');
$authAssertion = $header . '.' . $payload . '.';
}
// Fetch paginé /v1/reporting/transactions
$fetchTx = function (array $params) use ($base, $token, $authAssertion): array {
$page = 1;
$pageSize = $params['page_size'] ?? 200;
$all = [];
do {
$params['page'] = $page;
$url = $base . '/v1/reporting/transactions?' . http_build_query($params);
$headers = ['Authorization: Bearer ' . $token, 'Accept: application/json'];
if ($authAssertion) $headers[] = 'PayPal-Auth-Assertion: ' . $authAssertion;
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
]);
$res = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($http !== 200) throw new \Exception("Transaction Search HTTP $http: " . ($res ?: $err ?: ''));
$json = json_decode($res, true);
$rows = $json['transaction_details'] ?? [];
$all = array_merge($all, $rows);
$page++;
} while (count($rows) >= $pageSize);
return $all;
};
// On inclut aussi les écritures non "balance-affecting" pour capter les PENDING
$rows = $fetchTx([
'start_date' => $startIso,
'end_date' => $endIso,
'fields' => 'all',
'page_size' => 200,
'balance_affecting_records_only' => 'N',
]);
// Codes dévénements refund (variantes connues)
$refundCodes = [
'T1107', 'T1108', 'T1109', 'T1110', 'T0407',
'T0700', 'T0701', 'T0702', 'T0703'
];
$refundClassHints = ['REFUND', 'REVERSAL', 'CHARGEBACK'];
$refunds = [];
$seen = [];
foreach ($rows as $row) {
$ti = $row['transaction_info'] ?? [];
$eid = $ti['transaction_id'] ?? '';
$ev = $ti['transaction_event_code'] ?? '';
$subj = strtolower($ti['transaction_subject'] ?? '');
$amt = isset($ti['transaction_amount']['value']) ? (float)$ti['transaction_amount']['value'] : null;
$cur = $ti['transaction_amount']['currency_code'] ?? null;
$stat = $ti['transaction_status'] ?? '';
$ts = $ti['transaction_updated_date'] ?? ($ti['transaction_initiation_date'] ?? '');
$pref = $ti['paypal_reference_id'] ?? '';
$ridType = strtoupper($ti['paypal_reference_id_type'] ?? '');
$relId = $ti['related_transaction_id'] ?? '';
$inv = $ti['invoice_id'] ?? '';
$classes = array_map('strtoupper', (array)($ti['transaction_classification'] ?? []));
$parents = [];
foreach ((array)($ti['extended_related_transactions'] ?? []) as $rt) {
if (!empty($rt['parent_transaction_id'])) $parents[] = $rt['parent_transaction_id'];
if (!empty($rt['paypal_reference_id'])) $parents[] = $rt['paypal_reference_id'];
}
$looksLikeRefund =
in_array($ev, $refundCodes, true)
|| strpos($subj, 'refund') !== false
|| count(array_intersect($refundClassHints, $classes)) > 0;
if (!$looksLikeRefund) continue;
$strongMatch =
($pref === $capId)
|| ($relId === $capId)
|| ($ridType === 'CAPTURE' && $pref === $capId)
|| in_array($capId, $parents, true)
|| ($invId !== '' && $inv === $invId);
$fallbackMatch = false;
if (!$strongMatch && $amt !== null) {
$fallbackMatch =
($amt < 0) &&
(($cur ?: $capCur) === $capCur) &&
(($invId !== '' && $inv === $invId) || in_array($ordId, $parents, true));
}
if (!($strongMatch || $fallbackMatch)) continue;
$rid = $eid;
if ($rid && !isset($seen[$rid])) {
$seen[$rid] = true;
$refunds[] = [
'refund_id' => $rid,
'amount' => abs((float)($amt ?? 0)),
'currency' => $cur ?: $capCur,
'status' => $stat,
'time' => $ts,
'paypal_reference_id' => $pref,
'reference_type' => $ridType,
'related_transaction' => $relId,
'event_code' => $ev,
'classification' => $classes,
'invoice_id' => $inv,
'refund_api' => ($rid ? ($base . '/v2/payments/refunds/' . $rid) : null),
];
}
}
$refunded = 0.0;
foreach ($refunds as $r) $refunded += (float)$r['amount'];
$remaining = max(0, $capAmt - $refunded);
return [
'ok' => true,
'capture_id' => $capId,
'invoice_id' => $invId,
'order_id' => $ordId,
'refunds' => $refunds,
'totals' => [
'currency' => $capCur,
'captured_total' => (float)number_format($capAmt, 2, '.', ''),
'refunded_total' => (float)number_format($refunded, 2, '.', ''),
'remaining' => (float)number_format($remaining, 2, '.', ''),
],
'errors' => $errors,
];
}
/**
* Rembourse une capture PayPal (total ou partiel).
*
* @param string $captureId ID de la capture (ex: $captures[0]['id'])
* @param string|null $amount Montant du remboursement (null = remboursement total)
* @param string|null $currency Devise (ex: 'CAD'); si null on prend la devise de la classe/config
* @param string|null $note Note visible par le payer (optionnel)
* @param string|null $invoiceId ID de facture de référence côté PayPal (optionnel)
* @param string|null $idempotency Clé unique pour éviter les doubles remboursements (optionnel)
* @return array Réponse PayPal décodée
* @throws \Exception En cas derreur HTTP ou cURL
*/
public function refundCapture(
string $captureId,
?string $amount = null,
?string $currency = null,
?string $note = null,
?string $invoiceId = null,
?string $idempotency = null
): array {
if ($captureId === '') {
throw new \InvalidArgumentException('captureId requis');
}
$accessToken = $this->generateAccessToken();
if (empty($accessToken)) {
throw new \Exception('Impossible de générer le token daccès PayPal');
}
global $db;
// Devise par défaut
if ($currency === null || $currency === '') {
$currency = $this->currency ?? (function () {
global $vPaypal_devise;
return $vPaypal_devise ?? 'CAD';
})();
}
$payload = [];
if ($amount !== null && $amount !== '') {
$payload['amount'] = [
'value' => number_format((float)$amount, 2, '.', ''),
'currency_code' => $currency,
];
}
if ($note) $payload['note_to_payer'] = mb_strimwidth($note, 0, 255, '');
if ($invoiceId) $payload['invoice_id'] = $invoiceId;
// ✅ Base API ABSOLUE selon sandbox/live
$base = PAYPAL_BASE_URL;
$url = $base . '/v2/payments/captures/' . rawurlencode($captureId) . '/refund';
$headers = [
'Content-Type: application/json',
'Authorization: Bearer ' . $accessToken,
'Prefer: return=representation', // pour obtenir la représentation complète
];
if ($idempotency) $headers[] = 'PayPal-Request-Id: ' . $idempotency;
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => 1,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
CURLOPT_HEADER => true, // pour récupérer les entêtes (PayPal-Debug-Id)
]);
$respAll = curl_exec($ch);
$curlerr = curl_error($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$hdrSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
if ($curlerr) throw new \Exception('PayPal refund error (cURL): ' . $curlerr);
// Sépare entêtes et corps
$rawHeaders = substr($respAll, 0, $hdrSize);
$resp = substr($respAll, $hdrSize);
$data = json_decode($resp, true);
if ($httpcode >= 200 && $httpcode < 300) {
// update bd
if (isset($data['status']) && strtoupper($data['status']) === 'COMPLETED') {
// PayPal-Debug-Id si présent
$paypalDebugId = null;
if ($rawHeaders && preg_match('/^PayPal-Debug-Id:\s*([^\r\n]+)/mi', $rawHeaders, $m)) {
$paypalDebugId = trim($m[1]);
}
// Normalisation montants
$norm = static function($v) {
if ($v === null || $v === '' || !is_numeric($v)) return null;
return number_format((float)$v, 2, '.', '');
};
$row = [
'refund_id' => $data['id'] ?? null,
'capture_id' => $captureId,
'status' => $data['status'] ?? null,
'amount_value' => $norm($data['amount']['value'] ?? null),
'amount_currency' => $data['amount']['currency_code'] ?? null,
'invoice_id' => $data['invoice_id'] ?? null,
'custom_id' => $data['custom_id'] ?? null,
'acquirer_reference_number' => $data['acquirer_reference_number'] ?? null,
'seller_gross' => $norm($data['seller_payable_breakdown']['gross_amount']['value'] ?? null),
'seller_net' => $norm($data['seller_payable_breakdown']['net_amount']['value'] ?? null),
'paypal_fee' => $norm($data['seller_payable_breakdown']['paypal_fee']['value'] ?? null),
'note_to_payer' => isset($data['note_to_payer']) ? mb_strimwidth($data['note_to_payer'], 0, 255, '') : null,
'paypal_debug_id' => $paypalDebugId,
'idempotency_key' => $idempotency ?: null,
'create_time' => isset($data['create_time']) ? date('Y-m-d H:i:s', strtotime($data['create_time'])) : null,
'update_time' => isset($data['update_time']) ? date('Y-m-d H:i:s', strtotime($data['update_time'])) : null,
'raw_json' => $resp,
];
// INSERT via $GLOBALS['db']->fxQuery (remplace le bloc PDO par ceci)
$Q = function($v){ return $v === null ? "NULL" : "'" . addslashes($v) . "'"; };
$QN = function($v){ return $v === null ? "NULL" : (string)(float)$v; }; // nombres décimaux
$sql = "INSERT INTO inscriptions_panier_remboursement SET
refund_id = " . $Q($row['refund_id']) . ",
capture_id = " . $Q($row['capture_id']) . ",
status = " . $Q($row['status']) . ",
amount_value = " . $QN($row['amount_value']) . ",
amount_currency = " . $Q($row['amount_currency']) . ",
invoice_id = " . $Q($row['invoice_id']) . ",
custom_id = " . $Q($row['custom_id']) . ",
acquirer_reference_number = " . $Q($row['acquirer_reference_number']) . ",
seller_gross = " . $QN($row['seller_gross']) . ",
seller_net = " . $QN($row['seller_net']) . ",
paypal_fee = " . $QN($row['paypal_fee']) . ",
note_to_payer = " . $Q($row['note_to_payer']) . ",
paypal_debug_id = " . $Q($row['paypal_debug_id']) . ",
idempotency_key = " . $Q($row['idempotency_key']) . ",
create_time = " . $Q($row['create_time']) . ",
raw_json = " . $Q($row['raw_json']) . "
ON DUPLICATE KEY UPDATE
/* nécrase que si une valeur est fournie; sinon on garde lexistante */
capture_id = COALESCE(VALUES(capture_id), capture_id),
status = COALESCE(VALUES(status), status),
amount_value = COALESCE(VALUES(amount_value), amount_value),
amount_currency = COALESCE(VALUES(amount_currency), amount_currency),
invoice_id = COALESCE(VALUES(invoice_id), invoice_id),
custom_id = COALESCE(VALUES(custom_id), custom_id),
acquirer_reference_number = COALESCE(VALUES(acquirer_reference_number), acquirer_reference_number),
seller_gross = COALESCE(VALUES(seller_gross), seller_gross),
seller_net = COALESCE(VALUES(seller_net), seller_net),
paypal_fee = COALESCE(VALUES(paypal_fee), paypal_fee),
note_to_payer = COALESCE(VALUES(note_to_payer), note_to_payer),
paypal_debug_id = COALESCE(VALUES(paypal_debug_id), paypal_debug_id),
idempotency_key = COALESCE(VALUES(idempotency_key), idempotency_key),
raw_json = COALESCE(VALUES(raw_json), raw_json),
update_time = NOW()";
$res = $db->fxQuery($sql);
if (!$res) {
if (method_exists($GLOBALS['db'], 'fxError')) {
echo "<pre>ERREUR SQL : " . $GLOBALS['db']->fxError() . "</pre>";
} elseif (property_exists($GLOBALS['db'], 'error') && $GLOBALS['db']->error) {
echo "<pre>ERREUR SQL : " . $GLOBALS['db']->error . "</pre>";
} else {
echo "<pre>ERREUR SQL : requête échouée (aucun message disponible)</pre>";
}
}
// (Optionnel) si ta lib offre une méthode derreur, tu peux logger ici
// if(!$res) error_log('paypal_refunds insert failed: ' . $GLOBALS['db']->fxError());
}
return $data ?: ['status' => 'OK'];
}
// Affiche l'erreur JSON si dispo; sinon le body brut
$msg = $resp ?: 'no body';
throw new \Exception('PayPal refund failed (HTTP ' . $httpcode . '): ' . $msg);
}
}