Add all files
This commit is contained in:
315
extensions/Gateways/PayPal/PayPal.php
Normal file
315
extensions/Gateways/PayPal/PayPal.php
Normal file
@@ -0,0 +1,315 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Gateways\PayPal;
|
||||
|
||||
use App\Classes\Extension\Gateway;
|
||||
use App\Events\Service\Updated;
|
||||
use App\Events\ServiceCancellation\Created;
|
||||
use App\Helpers\ExtensionHelper;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\InvoiceItem;
|
||||
use App\Models\Service;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\View;
|
||||
|
||||
class PayPal extends Gateway
|
||||
{
|
||||
public function boot()
|
||||
{
|
||||
require __DIR__ . '/routes.php';
|
||||
// Register webhook route
|
||||
View::addNamespace('gateways.paypal', __DIR__ . '/resources/views');
|
||||
|
||||
Event::listen(Updated::class, function ($event) {
|
||||
if ($event->service->properties->where('key', 'has_paypal_subscription')->first()?->value !== '1') {
|
||||
return;
|
||||
}
|
||||
if ($event->service->isDirty('price')) {
|
||||
try {
|
||||
$this->updateSubscription($event->service);
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
if ($event->service->isDirty('status') && $event->service->status === Service::STATUS_CANCELLED) {
|
||||
try {
|
||||
$this->cancelSubscription($event->service);
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Event::listen(Created::class, function (Created $event) {
|
||||
$service = $event->cancellation->service;
|
||||
if ($service->properties->where('key', 'has_paypal_subscription')->first()?->value !== '1') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
$this->cancelSubscription($service);
|
||||
} catch (Exception $e) {
|
||||
// Log the error or handle it as needed
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function getConfig($values = [])
|
||||
{
|
||||
return [
|
||||
[
|
||||
'name' => 'client_id',
|
||||
'label' => 'Client ID',
|
||||
'type' => 'text',
|
||||
'description' => 'Find your API keys at https://developer.paypal.com/developer/applications',
|
||||
'required' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'client_secret',
|
||||
'label' => 'Client Secret',
|
||||
'type' => 'text',
|
||||
'description' => 'Find your API keys at https://developer.paypal.com/developer/applications',
|
||||
'required' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'webhook_id',
|
||||
'label' => 'Webhook ID',
|
||||
'type' => 'text',
|
||||
'description' => 'Find your webhook ID at https://developer.paypal.com/developer/webhooks',
|
||||
'required' => true,
|
||||
],
|
||||
[
|
||||
'name' => 'test_mode',
|
||||
'label' => 'Test Mode',
|
||||
'type' => 'checkbox',
|
||||
'description' => 'Enable test mode',
|
||||
'required' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'paypal_use_subscriptions',
|
||||
'label' => 'Use subscriptions',
|
||||
'type' => 'checkbox',
|
||||
'description' => 'Enable this option if you want to use subscriptions with PayPal (if available)',
|
||||
'required' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function generateAccessToken()
|
||||
{
|
||||
return once(function () {
|
||||
$url = $this->config('test_mode') ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
|
||||
|
||||
$result = Http::withHeaders([
|
||||
'Accept' => 'application/json',
|
||||
'Authorization' => 'Basic ' . base64_encode($this->config('client_id') . ':' . $this->config('client_secret')),
|
||||
])->asForm()->post($url . '/v1/oauth2/token', [
|
||||
'grant_type' => 'client_credentials',
|
||||
]);
|
||||
|
||||
if ($result->failed()) {
|
||||
throw new Exception('Failed to generate access token: ' . $result->body());
|
||||
}
|
||||
|
||||
return $result->json()['access_token'];
|
||||
});
|
||||
}
|
||||
|
||||
public function request($method, $url, $data = [])
|
||||
{
|
||||
return Http::withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $this->generateAccessToken(),
|
||||
])->$method($url, $data)->object();
|
||||
}
|
||||
|
||||
public function pay($invoice, $total)
|
||||
{
|
||||
$url = $this->config('test_mode') ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
|
||||
$eligableforSubscription = collect($invoice->items)->filter(function ($item) {
|
||||
return $item->reference_type === Service::class;
|
||||
})->count() == $invoice->items->count();
|
||||
|
||||
if ($this->config('paypal_use_subscriptions') && $eligableforSubscription && $invoice->items->map(fn ($item) => $item->reference->plan->billing_period . $item->reference->plan->billing_unit)->unique()->count() === 1) {
|
||||
$paypalProduct = $this->request('post', $url . '/v1/catalogs/products', [
|
||||
'name' => $invoice->items->first()->reference->product->name,
|
||||
'type' => 'SERVICE',
|
||||
]);
|
||||
|
||||
// For each item in the invoice, create a billing cycle
|
||||
$billingCycles[] = [
|
||||
'frequency' => [
|
||||
'interval_unit' => strtoupper($invoice->items->first()->reference->plan->billing_unit),
|
||||
'interval_count' => $invoice->items->first()->reference->plan->billing_period,
|
||||
],
|
||||
'tenure_type' => 'TRIAL',
|
||||
'sequence' => 1,
|
||||
'total_cycles' => 1,
|
||||
'pricing_scheme' => [
|
||||
'fixed_price' => [
|
||||
'value' => $total,
|
||||
'currency_code' => $invoice->currency_code,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$nextSum = $invoice->items->sum(fn ($item) => $item->reference->price * $item->reference->quantity);
|
||||
|
||||
$billingCycles[] = [
|
||||
'frequency' => [
|
||||
'interval_unit' => strtoupper($invoice->items->first()->reference->plan->billing_unit),
|
||||
'interval_count' => $invoice->items->first()->reference->plan->billing_period,
|
||||
],
|
||||
'tenure_type' => 'REGULAR',
|
||||
'sequence' => 2,
|
||||
'total_cycles' => 0,
|
||||
'pricing_scheme' => [
|
||||
'fixed_price' => [
|
||||
'value' => $nextSum,
|
||||
'currency_code' => $invoice->currency_code,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$plan = $this->request('post', $url . '/v1/billing/plans', [
|
||||
'product_id' => $paypalProduct->id,
|
||||
'name' => $invoice->items->first()->reference->plan->name,
|
||||
'description' => $invoice->items->first()->reference->plan->description,
|
||||
'billing_cycles' => $billingCycles,
|
||||
'payment_preferences' => [
|
||||
'auto_bill_outstanding' => true,
|
||||
'setup_fee_failure_action' => 'CONTINUE',
|
||||
'payment_failure_threshold' => 3,
|
||||
],
|
||||
]);
|
||||
} else {
|
||||
$order = $this->request('post', $url . '/v2/checkout/orders', [
|
||||
'intent' => 'CAPTURE',
|
||||
'purchase_units' => [
|
||||
[
|
||||
'reference_id' => $invoice->id,
|
||||
'amount' => [
|
||||
'currency_code' => $invoice->currency_code,
|
||||
'value' => $total,
|
||||
],
|
||||
],
|
||||
],
|
||||
'application_context' => [
|
||||
'return_url' => route('invoices.show', $invoice),
|
||||
'cancel_url' => route('invoices.show', $invoice),
|
||||
// Disable shipping information
|
||||
'shipping_preference' => 'NO_SHIPPING',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
return view('gateways.paypal::pay', ['invoice' => $invoice, 'total' => $total, 'order' => $order ?? null, 'plan' => $plan ?? null, 'clientId' => $this->config('client_id')]);
|
||||
}
|
||||
|
||||
public function capture(Request $request)
|
||||
{
|
||||
if (!$request->has('orderID')) {
|
||||
abort(400);
|
||||
}
|
||||
$orderID = $request->input('orderID');
|
||||
$url = $this->config('test_mode') ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
|
||||
// First check if the order is already captured
|
||||
$order = $this->request('get', $url . '/v2/checkout/orders/' . $orderID);
|
||||
if ($order->status === 'COMPLETED') {
|
||||
return $order;
|
||||
}
|
||||
|
||||
$response = $this->request('post', $url . '/v2/checkout/orders/' . $orderID . '/capture', [
|
||||
'intent' => 'CAPTURE',
|
||||
]);
|
||||
|
||||
ExtensionHelper::addPayment($response->purchase_units[0]->reference_id, 'PayPal', $response->purchase_units[0]->payments->captures[0]->amount->value, $response->purchase_units[0]->payments->captures[0]->seller_receivable_breakdown->paypal_fee->value, $response->id);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function webhook(Request $request)
|
||||
{
|
||||
$body = $request->getContent();
|
||||
$sigString = $request->header('PAYPAL-TRANSMISSION-ID') . '|' . $request->header('PAYPAL-TRANSMISSION-TIME') . '|' . $this->config('webhook_id') . '|' . crc32($body);
|
||||
$pubKey = openssl_pkey_get_public(file_get_contents($request->header('PAYPAL-CERT-URL')));
|
||||
$details = openssl_pkey_get_details($pubKey);
|
||||
$verifyResult = openssl_verify(
|
||||
$sigString,
|
||||
base64_decode(
|
||||
$request->header('PAYPAL-TRANSMISSION-SIG')
|
||||
),
|
||||
$details['key'],
|
||||
'sha256WithRSAEncryption'
|
||||
);
|
||||
if ($verifyResult !== 1) {
|
||||
return response()->json(['status' => 'error']);
|
||||
}
|
||||
|
||||
$body = $request->json()->all();
|
||||
|
||||
// Handle the subscription event
|
||||
if ($body['event_type'] === 'BILLING.SUBSCRIPTION.CREATED' && isset($body['resource']['custom_id'])) {
|
||||
// Its activated so we can now add the subscription to the user (custom is the order id)
|
||||
Invoice::findOrFail($body['resource']['custom_id'])->items()->where('reference_type', Service::class)->each(function (InvoiceItem $item) use ($body) {
|
||||
$service = $item->reference;
|
||||
$service->subscription_id = $body['resource']['id'];
|
||||
$service->save();
|
||||
$service->properties()->updateOrCreate([
|
||||
'key' => 'has_paypal_subscription',
|
||||
'name' => 'Has PayPal Subscription',
|
||||
], [
|
||||
'value' => true,
|
||||
]);
|
||||
});
|
||||
} elseif ($body['event_type'] === 'PAYMENT.SALE.COMPLETED' && isset($body['resource']['billing_agreement_id'])) {
|
||||
Service::where('subscription_id', $body['resource']['billing_agreement_id'])->each(function (Service $service) use ($body) {
|
||||
// Add payment to the latest invoice
|
||||
$latestInvoice = $service->invoices()->latest()->first();
|
||||
ExtensionHelper::addPayment($latestInvoice->id, 'PayPal', $body['resource']['amount']['total'], $body['resource']['transaction_fee']['value'], $body['resource']['id']);
|
||||
});
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
public function updateSubscription(Service $service)
|
||||
{
|
||||
if ($service->properties->where('key', 'has_paypal_subscription')->first()?->value !== '1') {
|
||||
return false;
|
||||
}
|
||||
$paypal = new PayPal;
|
||||
// Update subscription price
|
||||
$newPrice = Service::where('subscription_id', $service->subscription_id)->sum('price');
|
||||
// Grab currenct subscription ID
|
||||
$subscriptionId = $service->subscription_id;
|
||||
$url = $paypal->config('test_mode') ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
|
||||
|
||||
// Update subscription price
|
||||
$paypal->request('PATCH', $url . '/v1/billing/subscriptions/' . $subscriptionId, [
|
||||
[
|
||||
'op' => 'replace',
|
||||
'path' => '/plan/billing_cycles/@sequence==2/pricing_scheme/fixed_price',
|
||||
'value' => [
|
||||
'value' => $newPrice,
|
||||
'currency_code' => $service->currency_code,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function cancelSubscription(Service $service)
|
||||
{
|
||||
// Cancel subscription
|
||||
$url = $this->config('test_mode') ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com';
|
||||
$this->request('post', $url . '/v1/billing/subscriptions/' . $service->subscription_id . '/cancel', [
|
||||
'reason' => 'User canceled',
|
||||
]);
|
||||
|
||||
$service->properties()->where('key', 'has_paypal_subscription')->delete();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user