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

541 lines
22 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;
/**
*
* 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
include_once '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 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 showOrder($orderId)
{
$accessToken = $this->generateAccessToken();
if (empty($accessToken)) {
return false;
} else {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->paypalAPI . '/payments/authorizations/' . $orderId );
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 showOrder(' . $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)
{
$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'));
}
/**
* 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');
}
// 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 = (strpos($this->paypalAPI, 'sandbox') !== false)
? 'https://api-m.sandbox.paypal.com'
: 'https://api-m.paypal.com';
$url = $base . '/v2/payments/captures/' . rawurlencode($captureId) . '/refund';
$headers = [
'Content-Type: application/json',
'Authorization: Bearer ' . $accessToken,
];
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),
// (Optionnel) pour loguer le PayPal-Debug-Id:
// CURLOPT_HEADER => true,
]);
$resp = curl_exec($ch);
$curlerr = curl_error($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($curlerr) throw new \Exception('PayPal refund error (cURL): ' . $curlerr);
$data = json_decode($resp, true);
if ($httpcode >= 200 && $httpcode < 300) {
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);
}
public function getRefundDetails(string $refundId): array {
if ($refundId === '') throw new \InvalidArgumentException('refundId requis');
$accessToken = $this->generateAccessToken();
if (empty($accessToken)) throw new \Exception('Impossible de générer le token daccès PayPal');
$url = rtrim($this->paypalAPI, '/') . '/payments/refunds/' . rawurlencode($refundId);
$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 getRefundDetails error (cURL): ' . $curlerr);
$data = json_decode($resp, true);
if ($httpcode >= 200 && $httpcode < 300) return $data ?: [];
throw new \Exception('PayPal getRefundDetails failed (HTTP ' . $httpcode . '): ' . ($resp ?: 'no body'));
}
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 = (strpos($this->paypalAPI, 'sandbox') !== false)
? 'https://api-m.sandbox.paypal.com'
: 'https://api-m.paypal.com';
$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'));
}
/**
* 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
$t0 = new \DateTime($tCreate);
$tU = new \DateTime($tUpdate);
$startIso = $t0->sub(new \DateInterval('PT'.max(0,$hoursBefore).'H'))->format(\DateTime::ATOM);
$endIso = $tU->add(new \DateInterval('PT'.max(1,$hoursAfter).'H'))->format(\DateTime::ATOM);
// OAuth + base api-m
$token = $this->generateAccessToken();
if (!$token) throw new \Exception('Token PayPal indisponible');
$base = (strpos((string)$this->paypalAPI, 'sandbox') !== false)
? 'https://api-m.sandbox.paypal.com'
: 'https://api-m.paypal.com';
// Fetch paginé /v1/reporting/transactions
$fetchTx = function(array $params) use ($base, $token): array {
$page = 1; $pageSize = $params['page_size'] ?? 200; $all = [];
do {
$params['page'] = $page;
$url = $base . '/v1/reporting/transactions?' . http_build_query($params);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['Authorization: Bearer '.$token, 'Accept: application/json'],
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 (selon tenants)
$refundCodes = ['T1107','T1108','T1109','T1110','T0407'];
$refunds = [];
$seen = []; // dédoublonnage par refund_id
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'] ?? ''; // souvent = capture_id
$inv = $ti['invoice_id'] ?? '';
// Relations additionnelles
$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'];
}
// 1) Doit ressembler à un refund
$looksLikeRefund = in_array($ev, $refundCodes, true) || strpos($subj, 'refund') !== false;
if (!$looksLikeRefund) continue;
// 2) ANCRAGE OBLIGATOIRE : capture_id OU (si dispo) invoice_id
$strongMatch =
($pref === $capId) ||
in_array($capId, $parents, true) ||
($invId !== '' && $inv === $invId);
if (!$strongMatch) continue; // ← plus de match “montant négatif seul”
// 3) Normalisation
$rid = $eid;
if ($rid && !isset($seen[$rid])) {
$seen[$rid] = true;
$refunds[] = [
'refund_id' => $rid,
'amount' => abs((float)($amt ?? 0)),
'currency' => $cur ?: $capCur,
'status' => $stat, // S/P/…
'time' => $ts,
'paypal_reference_id' => $pref,
'event_code' => $ev,
];
}
}
// Totaux
$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,
];
}
public function debugReportAroundCapture(array $capture, int $hoursBefore = 24, int $hoursAfter = 120): array
{
$capId = $capture['id'] ?? '';
$invId = $capture['invoice_id'] ?? '';
$tCreate = $capture['create_time'] ?? '';
$tUpdate = $capture['update_time'] ?? $tCreate;
$t0 = new \DateTime($tCreate);
$tU = new \DateTime($tUpdate);
$startIso = $t0->sub(new \DateInterval('PT'.max(0,$hoursBefore).'H'))->format(\DateTime::ATOM);
$endIso = $tU->add(new \DateInterval('PT'.max(1,$hoursAfter).'H'))->format(\DateTime::ATOM);
$token = $this->generateAccessToken();
$base = (strpos((string)$this->paypalAPI, 'sandbox') !== false)
? 'https://api-m.sandbox.paypal.com'
: 'https://api-m.paypal.com';
$url = $base.'/v1/reporting/transactions?'.http_build_query([
'start_date' => $startIso,
'end_date' => $endIso,
'fields' => 'all',
'page_size' => 200,
'balance_affecting_records_only' => 'N',
]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ['Authorization: Bearer '.$token, 'Accept: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
]);
$res = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http !== 200) throw new \Exception("Transaction Search HTTP $http: ".($res ?: ''));
$json = json_decode($res, true);
$rows = $json['transaction_details'] ?? [];
$out = [];
foreach ($rows as $row) {
$ti = $row['transaction_info'] ?? [];
$out[] = [
'id' => $ti['transaction_id'] ?? '',
'event' => $ti['transaction_event_code'] ?? '',
'subject' => $ti['transaction_subject'] ?? '',
'amount' => $ti['transaction_amount']['value'] ?? '',
'currency' => $ti['transaction_amount']['currency_code'] ?? '',
'D/C' => $ti['debit_or_credit'] ?? '',
'ref' => $ti['paypal_reference_id'] ?? '',
'invoice' => $ti['invoice_id'] ?? '',
'status' => $ti['transaction_status'] ?? '',
'time' => $ti['transaction_updated_date'] ?? ($ti['transaction_initiation_date'] ?? ''),
];
}
return ['capture_id'=>$capId,'invoice_id'=>$invId,'window'=>[$startIso,$endIso],'rows'=>$out];
}
}