674 lines
29 KiB
PHP
674 lines
29 KiB
PHP
<?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 d’accè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) n’est 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 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 (utilise $this->paypal, pas de new/require)
|
||
try {
|
||
$delails = $this->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 = $this->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,
|
||
];
|
||
}
|
||
/**
|
||
* 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 d’agir 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 d’erreur 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 d’accè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 l’existante */
|
||
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 d’erreur, 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);
|
||
}
|
||
|
||
|
||
|
||
|
||
}
|