268 lines
8.6 KiB
PHP
268 lines
8.6 KiB
PHP
<?php
|
|
/**
|
|
* PayPal Checkout API Helper
|
|
*
|
|
* Handles communication with PayPal REST API v2
|
|
* Uses client ID and secret for OAuth authentication
|
|
*/
|
|
|
|
require_once __DIR__ . '/paypal_checkout_config.php';
|
|
|
|
class PayPalCheckoutAPI {
|
|
|
|
private $clientId;
|
|
private $clientSecret;
|
|
private $baseUrl;
|
|
private $accessToken;
|
|
|
|
public function __construct() {
|
|
$this->clientId = getPayPalClientId();
|
|
$this->clientSecret = getPayPalClientSecret();
|
|
$this->baseUrl = getPayPalBaseUrl();
|
|
}
|
|
|
|
private function httpRequest($method, $url, $headers = [], $body = null, $basicAuth = null) {
|
|
$method = strtoupper($method);
|
|
|
|
$verifySsl = defined('PAYPAL_VERIFY_SSL') ? (bool)PAYPAL_VERIFY_SSL : true;
|
|
|
|
// Prefer cURL when available
|
|
if (function_exists('curl_init')) {
|
|
$ch = curl_init();
|
|
|
|
$opts = [
|
|
CURLOPT_URL => $url,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_CUSTOMREQUEST => $method,
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
CURLOPT_SSL_VERIFYPEER => $verifySsl,
|
|
CURLOPT_SSL_VERIFYHOST => $verifySsl ? 2 : 0,
|
|
CURLOPT_TIMEOUT => 30,
|
|
];
|
|
|
|
if ($basicAuth) {
|
|
$opts[CURLOPT_USERPWD] = $basicAuth;
|
|
}
|
|
if ($body !== null) {
|
|
$opts[CURLOPT_POSTFIELDS] = $body;
|
|
}
|
|
|
|
curl_setopt_array($ch, $opts);
|
|
|
|
$responseBody = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$error = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($error) {
|
|
throw new Exception("cURL Error: $error");
|
|
}
|
|
|
|
return [$httpCode, $responseBody];
|
|
}
|
|
|
|
// Fallback to file_get_contents if cURL isn't enabled
|
|
$headerLines = $headers;
|
|
if ($basicAuth) {
|
|
$headerLines[] = 'Authorization: Basic ' . base64_encode($basicAuth);
|
|
}
|
|
|
|
$context = stream_context_create([
|
|
'http' => [
|
|
'method' => $method,
|
|
'header' => implode("\r\n", $headerLines),
|
|
'content' => $body ?? '',
|
|
'ignore_errors' => true,
|
|
'timeout' => 30,
|
|
],
|
|
'ssl' => [
|
|
'verify_peer' => $verifySsl,
|
|
'verify_peer_name' => $verifySsl,
|
|
],
|
|
]);
|
|
|
|
$responseBody = @file_get_contents($url, false, $context);
|
|
$httpCode = 0;
|
|
|
|
if (isset($http_response_header) && is_array($http_response_header)) {
|
|
foreach ($http_response_header as $h) {
|
|
if (preg_match('#^HTTP/\S+\s+(\d{3})#', $h, $m)) {
|
|
$httpCode = intval($m[1]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($responseBody === false) {
|
|
$lastError = error_get_last();
|
|
$msg = $lastError['message'] ?? 'HTTP request failed';
|
|
throw new Exception($msg);
|
|
}
|
|
|
|
return [$httpCode, $responseBody];
|
|
}
|
|
|
|
/**
|
|
* Get OAuth 2.0 access token from PayPal
|
|
*/
|
|
public function getAccessToken() {
|
|
if ($this->accessToken) {
|
|
return $this->accessToken;
|
|
}
|
|
|
|
[$httpCode, $response] = $this->httpRequest(
|
|
'POST',
|
|
$this->baseUrl . '/v1/oauth2/token',
|
|
[
|
|
'Accept: application/json',
|
|
'Accept-Language: en_US',
|
|
'Content-Type: application/x-www-form-urlencoded',
|
|
],
|
|
'grant_type=client_credentials',
|
|
$this->clientId . ':' . $this->clientSecret
|
|
);
|
|
|
|
$data = json_decode($response, true);
|
|
|
|
if ($httpCode !== 200 || !isset($data['access_token'])) {
|
|
$errorMsg = isset($data['error_description']) ? $data['error_description'] : 'Failed to get access token';
|
|
throw new Exception("PayPal Auth Error: $errorMsg");
|
|
}
|
|
|
|
$this->accessToken = $data['access_token'];
|
|
return $this->accessToken;
|
|
}
|
|
|
|
/**
|
|
* Create a PayPal order
|
|
*
|
|
* @param float $amount The order amount
|
|
* @param string $currency Currency code (e.g., 'USD', 'CAD')
|
|
* @param string $description Order description
|
|
* @param string $customId Your internal reference ID (e.g., TRXnum)
|
|
* @return array Order data including order ID
|
|
*/
|
|
public function createOrder($amount, $currency, $description = '', $customId = '') {
|
|
$token = $this->getAccessToken();
|
|
|
|
$orderData = [
|
|
'intent' => 'CAPTURE',
|
|
'purchase_units' => [
|
|
[
|
|
'amount' => [
|
|
'currency_code' => $currency,
|
|
'value' => number_format($amount, 2, '.', '')
|
|
],
|
|
'description' => $description,
|
|
'custom_id' => $customId // This is like the 'custom' field in old PayPal
|
|
]
|
|
],
|
|
'application_context' => [
|
|
'brand_name' => 'Hot Lunches Online',
|
|
'landing_page' => 'NO_PREFERENCE',
|
|
'user_action' => 'PAY_NOW',
|
|
'return_url' => $this->getCurrentUrl() . $this->appendParam('paypal=success'),
|
|
'cancel_url' => $this->getCurrentUrl() . $this->appendParam('paypal=cancel')
|
|
]
|
|
];
|
|
|
|
[$httpCode, $response] = $this->httpRequest(
|
|
'POST',
|
|
$this->baseUrl . '/v2/checkout/orders',
|
|
[
|
|
'Content-Type: application/json',
|
|
'Authorization: Bearer ' . $token,
|
|
'PayPal-Request-Id: ' . uniqid('order_', true),
|
|
],
|
|
json_encode($orderData)
|
|
);
|
|
|
|
$data = json_decode($response, true);
|
|
|
|
if ($httpCode !== 201 || !isset($data['id'])) {
|
|
$errorMsg = isset($data['message']) ? $data['message'] : 'Failed to create order';
|
|
if (isset($data['details'])) {
|
|
$errorMsg .= ': ' . json_encode($data['details']);
|
|
}
|
|
throw new Exception("PayPal Create Order Error: $errorMsg");
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Capture a PayPal order (complete the payment)
|
|
*
|
|
* @param string $orderId PayPal order ID
|
|
* @return array Capture result including transaction details
|
|
*/
|
|
public function captureOrder($orderId) {
|
|
$token = $this->getAccessToken();
|
|
|
|
[$httpCode, $response] = $this->httpRequest(
|
|
'POST',
|
|
$this->baseUrl . '/v2/checkout/orders/' . $orderId . '/capture',
|
|
[
|
|
'Content-Type: application/json',
|
|
'Authorization: Bearer ' . $token,
|
|
'PayPal-Request-Id: ' . uniqid('capture_', true),
|
|
],
|
|
'{}'
|
|
);
|
|
|
|
$data = json_decode($response, true);
|
|
|
|
if ($httpCode !== 201 || !isset($data['status'])) {
|
|
$errorMsg = isset($data['message']) ? $data['message'] : 'Failed to capture order';
|
|
if (isset($data['details'])) {
|
|
$errorMsg .= ': ' . json_encode($data['details']);
|
|
}
|
|
throw new Exception("PayPal Capture Error: $errorMsg");
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Get order details
|
|
*
|
|
* @param string $orderId PayPal order ID
|
|
* @return array Order details
|
|
*/
|
|
public function getOrder($orderId) {
|
|
$token = $this->getAccessToken();
|
|
|
|
[$httpCode, $response] = $this->httpRequest(
|
|
'GET',
|
|
$this->baseUrl . '/v2/checkout/orders/' . $orderId,
|
|
[
|
|
'Content-Type: application/json',
|
|
'Authorization: Bearer ' . $token,
|
|
]
|
|
);
|
|
|
|
return json_decode($response, true);
|
|
}
|
|
|
|
/**
|
|
* Get current page URL (for return/cancel URLs) — includes the path and
|
|
* query string, not just the host, so PayPal redirects the buyer back
|
|
* to the same page/view they paid from (the one that can capture the
|
|
* order, e.g. ?action=account-deposit), not the site root.
|
|
*/
|
|
private function getCurrentUrl() {
|
|
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
|
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
|
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
|
return $protocol . '://' . $host . $uri;
|
|
}
|
|
|
|
/**
|
|
* Append a query param to a URL that may already have a query string.
|
|
*/
|
|
private function appendParam($param) {
|
|
$uri = $_SERVER['REQUEST_URI'] ?? '/';
|
|
return (strpos($uri, '?') !== false ? '&' : '?') . $param;
|
|
}
|
|
}
|