252 lines
8.6 KiB
PHP
252 lines
8.6 KiB
PHP
<?php
|
||
|
||
namespace App\Services\Payment;
|
||
|
||
use Exception;
|
||
use GuzzleHttp\Client;
|
||
use Illuminate\Support\Facades\Http;
|
||
use Illuminate\Support\Facades\Log;
|
||
use Throwable;
|
||
|
||
class PayPalPayment implements PaymentInterface
|
||
{
|
||
private string $clientId;
|
||
private string $secretKey;
|
||
private string $currencyCode;
|
||
private string $paymentmode;
|
||
private Client $http;
|
||
private string $baseUrl;
|
||
|
||
|
||
public function __construct($clientId, $secretKey, $currencyCode, $paymentmode)
|
||
{
|
||
$this->clientId = $clientId;
|
||
$this->secretKey = $secretKey;
|
||
$this->currencyCode = $currencyCode;
|
||
$this->paymentmode = $paymentmode;
|
||
|
||
$this->http = new Client([
|
||
'base_uri' => ($paymentmode == "UAT") ? 'https://api.sandbox.paypal.com' : 'https://api-m.paypal.com',
|
||
'timeout' => 30,
|
||
]);
|
||
$this->baseUrl = ($paymentmode == "UAT")
|
||
? 'https://api.sandbox.paypal.com'
|
||
: 'https://api.paypal.com';
|
||
|
||
}
|
||
|
||
private function generateAccessToken(): string
|
||
{
|
||
$response = $this->http->post('/v1/oauth2/token', [
|
||
'auth' => [$this->clientId, $this->secretKey],
|
||
'form_params' => ['grant_type' => 'client_credentials'],
|
||
'headers' => [
|
||
'Accept' => 'application/json',
|
||
'Accept-Language' => 'en_US'
|
||
]
|
||
]);
|
||
|
||
$data = json_decode((string)$response->getBody(), true);
|
||
return $data['access_token'] ?? throw new Exception("Unable to generate PayPal token");
|
||
}
|
||
|
||
/**
|
||
* Create a PayPal Order (Payment Intent equivalent)
|
||
*/
|
||
public function createPaymentIntent($amount, $customMetaData)
|
||
{
|
||
$amount = $this->minimumAmountValidation($this->currencyCode, $amount);
|
||
$accessToken = $this->generateAccessToken();
|
||
if($customMetaData['platform_type'] == 'app') {
|
||
$callbackUrl = route('paypal.success') ;
|
||
}else{
|
||
$callbackUrl = route('paypal.success.web');
|
||
}
|
||
$metaData = 't' . '-' . $customMetaData['payment_transaction_id'] . '-' . 'p' . '-' . $customMetaData['package_id'];
|
||
$response = $this->http->post('/v2/checkout/orders', [
|
||
'headers' => [
|
||
'Authorization' => "Bearer {$accessToken}",
|
||
'Content-Type' => 'application/json'
|
||
],
|
||
'json' => [
|
||
'intent' => 'CAPTURE',
|
||
'purchase_units' => [[
|
||
'amount' => [
|
||
'currency_code' => $this->currencyCode,
|
||
'value' => number_format($amount, 2, '.', '')
|
||
],
|
||
'custom_id' => $metaData,
|
||
'description' => $customMetaData['description'] ?? null,
|
||
]],
|
||
'application_context' => [
|
||
'return_url' => $callbackUrl,
|
||
'cancel_url' => $callbackUrl,
|
||
]
|
||
]
|
||
|
||
]);
|
||
|
||
$data = json_decode((string)$response->getBody(), true);
|
||
return $data;
|
||
}
|
||
|
||
|
||
/**
|
||
* ✅ Matches PaymentInterface (like Stripe)
|
||
*/
|
||
public function createAndFormatPaymentIntent($amount, $customMetaData): array
|
||
{
|
||
$order = $this->createPaymentIntent($amount, $customMetaData);
|
||
|
||
// Extract approve link from PayPal response
|
||
$approveLink = null;
|
||
if (isset($order['links']) && is_array($order['links'])) {
|
||
foreach ($order['links'] as $link) {
|
||
if ($link['rel'] === 'approve') {
|
||
$approveLink = $link['href'];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
$formatted = $this->formatPaymentIntent(
|
||
$order['id'],
|
||
$amount,
|
||
$this->currencyCode,
|
||
$order['status'] ?? 'CREATED',
|
||
$customMetaData,
|
||
$order
|
||
);
|
||
|
||
// Add approval link for frontend
|
||
$formatted['approval_url'] = $approveLink;
|
||
|
||
return $formatted;
|
||
}
|
||
|
||
|
||
/**
|
||
* ✅ Retrieve order details (similar to Stripe’s retrievePaymentIntent)
|
||
*/
|
||
public function retrievePaymentIntent($paymentId): array
|
||
{
|
||
$accessToken = $this->generateAccessToken();
|
||
|
||
$response = $this->http->get("/v2/checkout/orders/{$paymentId}", [
|
||
'headers' => [
|
||
'Authorization' => "Bearer {$accessToken}",
|
||
'Content-Type' => 'application/json'
|
||
]
|
||
]);
|
||
|
||
$data = json_decode((string)$response->getBody(), true);
|
||
|
||
return $this->formatPaymentIntent(
|
||
$data['id'],
|
||
$data['purchase_units'][0]['amount']['value'] ?? 0,
|
||
$data['purchase_units'][0]['amount']['currency_code'] ?? $this->currencyCode,
|
||
$data['status'],
|
||
[],
|
||
$data
|
||
);
|
||
}
|
||
|
||
public function capturePayment($orderId): array
|
||
{
|
||
$accessToken = $this->generateAccessToken();
|
||
|
||
$response = $this->http->post("/v2/checkout/orders/{$orderId}/capture", [
|
||
'headers' => [
|
||
'Authorization' => "Bearer {$accessToken}",
|
||
'Content-Type' => 'application/json'
|
||
]
|
||
]);
|
||
|
||
$data = json_decode((string)$response->getBody(), true);
|
||
|
||
return $this->formatPaymentIntent(
|
||
$data['id'] ?? $orderId,
|
||
$data['purchase_units'][0]['payments']['captures'][0]['amount']['value'] ?? 0,
|
||
$data['purchase_units'][0]['payments']['captures'][0]['amount']['currency_code'] ?? $this->currencyCode,
|
||
$data['status'] ?? 'FAILED',
|
||
[],
|
||
$data
|
||
);
|
||
}
|
||
|
||
public function refund($captureId, $amount, $currency = null): array
|
||
{
|
||
$accessToken = $this->generateAccessToken();
|
||
$currency = $currency ?? $this->currencyCode;
|
||
|
||
$response = $this->http->post("/v2/payments/captures/{$captureId}/refund", [
|
||
'headers' => [
|
||
'Authorization' => "Bearer {$accessToken}",
|
||
'Content-Type' => 'application/json'
|
||
],
|
||
'json' => [
|
||
'amount' => [
|
||
'value' => number_format($amount, 2, '.', ''),
|
||
'currency_code' => $currency
|
||
]
|
||
]
|
||
]);
|
||
|
||
return json_decode((string)$response->getBody(), true);
|
||
}
|
||
|
||
public function formatPaymentIntent($id, $amount, $currency, $status, $metadata, $paymentIntent): array
|
||
{
|
||
return [
|
||
'id' => $id,
|
||
'amount' => $amount,
|
||
'currency' => $currency,
|
||
'metadata' => $metadata,
|
||
'status' => match (strtolower($status)) {
|
||
"completed" => "succeed",
|
||
"approved" => "pending",
|
||
"created" => "pending",
|
||
default => "failed",
|
||
},
|
||
'payment_gateway_response' => $paymentIntent
|
||
];
|
||
}
|
||
|
||
public function minimumAmountValidation($currency, $amount)
|
||
{
|
||
$minimumAmount = 0.50;
|
||
return max($amount, $minimumAmount);
|
||
}
|
||
|
||
public function verifyWebhookSignature(array $headers, string $payload, string $webhookId): bool
|
||
{
|
||
try {
|
||
$accessToken = $this->generateAccessToken();
|
||
if($this->baseUrl == 'https://api.sandbox.paypal.com'){
|
||
return true;
|
||
}
|
||
$verification = Http::withToken($accessToken)
|
||
->post($this->baseUrl . '/v1/notifications/verify-webhook-signature', [
|
||
'auth_algo' => $headers['paypal-auth-algo'][0] ?? '',
|
||
'cert_url' => $headers['paypal-cert-url'][0] ?? '',
|
||
'transmission_id' => $headers['paypal-transmission-id'][0] ?? '',
|
||
'transmission_sig' => $headers['paypal-transmission-sig'][0] ?? '',
|
||
'transmission_time' => $headers['paypal-transmission-time'][0] ?? '',
|
||
'webhook_id' => $webhookId,
|
||
'webhook_event' => json_decode($payload, true),
|
||
]);
|
||
|
||
if (!$verification->successful()) {
|
||
Log::error('PayPal verifyWebhookSignature failed: ' . $verification->body());
|
||
return false;
|
||
}
|
||
|
||
return ($verification->json()['verification_status'] ?? '') === 'SUCCESS';
|
||
} catch (Throwable $e) {
|
||
Log::error('PayPal verifyWebhookSignature exception: ' . $e->getMessage());
|
||
return false;
|
||
}
|
||
}
|
||
|
||
}
|