From 37cc0cf4421f2eae00b7b72d486f8f08ab924145 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 13 Dec 2023 17:10:29 +1100 Subject: [PATCH] Working on upgrade for paypal --- app/Libraries/MultiDB.php | 36 +- .../Ninja/PayPalUnlinkedTransaction.php | 78 ++++ app/PaymentDrivers/PayPal/PayPalWebhook.php | 382 ++++++++++++++++++ .../PayPalPPCPPaymentDriver.php | 114 ++++-- .../PayPalRestPaymentDriver.php | 109 ----- config/ninja.php | 1 + routes/api.php | 2 + 7 files changed, 583 insertions(+), 139 deletions(-) create mode 100644 app/Notifications/Ninja/PayPalUnlinkedTransaction.php create mode 100644 app/PaymentDrivers/PayPal/PayPalWebhook.php diff --git a/app/Libraries/MultiDB.php b/app/Libraries/MultiDB.php index 57e734253bcf..73fc82d65848 100644 --- a/app/Libraries/MultiDB.php +++ b/app/Libraries/MultiDB.php @@ -11,16 +11,17 @@ namespace App\Libraries; -use App\Models\Account; -use App\Models\Client; -use App\Models\ClientContact; -use App\Models\Company; -use App\Models\CompanyToken; -use App\Models\Document; use App\Models\User; +use App\Models\Client; +use App\Models\Account; +use App\Models\Company; +use App\Models\Document; +use App\Models\PaymentHash; +use Illuminate\Support\Str; +use App\Models\CompanyToken; +use App\Models\ClientContact; use App\Models\VendorContact; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Str; /** * Class MultiDB. @@ -485,6 +486,27 @@ class MultiDB return false; } + public static function findAndSetByPaymentHash(string $hash) + { + if (! config('ninja.db.multi_db_enabled')) { + return PaymentHash::with('fee_invoice')->where('hash', $hash)->first(); + } + + $current_db = config('database.default'); + + foreach (self::$dbs as $db) { + if ($payment_hash = PaymentHash::on($db)->where('hash', $hash)->first()) { + self::setDb($db); + + return $payment_hash; + } + } + + self::setDB($current_db); + + return false; + } + public static function findAndSetDbByInvitation($entity, $invitation_key) { $class = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation'; diff --git a/app/Notifications/Ninja/PayPalUnlinkedTransaction.php b/app/Notifications/Ninja/PayPalUnlinkedTransaction.php new file mode 100644 index 000000000000..e0b209005e23 --- /dev/null +++ b/app/Notifications/Ninja/PayPalUnlinkedTransaction.php @@ -0,0 +1,78 @@ +order_id}\n"; + $content .= "Transaction ref: {$this->transaction_reference}\n"; + + + return (new SlackMessage) + ->success() + ->from(ctrans('texts.notification_bot')) + ->image('https://app.invoiceninja.com/favicon.png') + ->content($content); + } +} diff --git a/app/PaymentDrivers/PayPal/PayPalWebhook.php b/app/PaymentDrivers/PayPal/PayPalWebhook.php new file mode 100644 index 000000000000..d89d828d9f93 --- /dev/null +++ b/app/PaymentDrivers/PayPal/PayPalWebhook.php @@ -0,0 +1,382 @@ +verifyWebhook()) { + nlog('verified'); + + match($this->webhook_request['event_type']) { + 'CHECKOUT.ORDER.COMPLETED' => $this->checkoutOrderCompleted(), + }; + + + } + + } + /* + 'id' => 'WH-COC11055RA711503B-4YM959094A144403T', + 'create_time' => '2018-04-16T21:21:49.000Z', + 'event_type' => 'CHECKOUT.ORDER.COMPLETED', + 'resource_type' => 'checkout-order', + 'resource_version' => '2.0', + 'summary' => 'Checkout Order Completed', + 'resource' => + array ( + 'id' => '5O190127TN364715T', + 'status' => 'COMPLETED', + 'intent' => 'CAPTURE', + 'gross_amount' => + array ( + 'currency_code' => 'USD', + 'value' => '100.00', + ), + 'payer' => + array ( + 'name' => + array ( + 'given_name' => 'John', + 'surname' => 'Doe', + ), + 'email_address' => 'buyer@example.com', + 'payer_id' => 'QYR5Z8XDVJNXQ', + ), + 'purchase_units' => + array ( + 0 => + array ( + 'reference_id' => 'd9f80740-38f0-11e8-b467-0ed5f89f718b', + 'amount' => + array ( + 'currency_code' => 'USD', + 'value' => '100.00', + ), + 'payee' => + array ( + 'email_address' => 'seller@example.com', + ), + 'shipping' => + array ( + 'method' => 'United States Postal Service', + 'address' => + array ( + 'address_line_1' => '2211 N First Street', + 'address_line_2' => 'Building 17', + 'admin_area_2' => 'San Jose', + 'admin_area_1' => 'CA', + 'postal_code' => '95131', + 'country_code' => 'US', + ), + ), + 'payments' => + array ( + 'captures' => + array ( + 0 => + array ( + 'id' => '3C679366HH908993F', + 'status' => 'COMPLETED', + 'amount' => + array ( + 'currency_code' => 'USD', + 'value' => '100.00', + ), + 'seller_protection' => + array ( + 'status' => 'ELIGIBLE', + 'dispute_categories' => + array ( + 0 => 'ITEM_NOT_RECEIVED', + 1 => 'UNAUTHORIZED_TRANSACTION', + ), + ), + 'final_capture' => true, + 'seller_receivable_breakdown' => + array ( + 'gross_amount' => + array ( + 'currency_code' => 'USD', + 'value' => '100.00', + ), + 'paypal_fee' => + array ( + 'currency_code' => 'USD', + 'value' => '3.00', + ), + 'net_amount' => + array ( + 'currency_code' => 'USD', + 'value' => '97.00', + ), + ), + 'create_time' => '2018-04-01T21:20:49Z', + 'update_time' => '2018-04-01T21:20:49Z', + 'links' => + array ( + 0 => + array ( + 'href' => 'https://api.paypal.com/v2/payments/captures/3C679366HH908993F', + 'rel' => 'self', + 'method' => 'GET', + ), + 1 => + array ( + 'href' => 'https://api.paypal.com/v2/payments/captures/3C679366HH908993F/refund', + 'rel' => 'refund', + 'method' => 'POST', + ), + ), + ), + ), + ), + ), + ), + 'create_time' => '2018-04-01T21:18:49Z', + 'update_time' => '2018-04-01T21:20:49Z', + 'links' => + */ + private function checkoutOrderCompleted() + { + $order = $this->webhook_request['resource']; + $transaction_reference = $order['purchase_units'][0]['payments']['captures'][0]['id']; + $amount = $order['purchase_units'][0]['payments']['captures'][0]['amount']['value']; + $payment_hash = MultiDB::findAndSetByPaymentHash($order['purchase_units'][0]['custom_id']); + $merchant_id = $order['purchase_units'][0]['payee']['merchant_id']; + if(!$payment_hash) { + + $ninja_company = Company::on('db-ninja-01')->find(config('ninja.ninja_default_company_id')); + $ninja_company->notification(new PayPalUnlinkedTransaction($order['id'], $transaction_reference))->ninja(); + return; + } + + nlog("payment completed check"); + if($payment_hash->payment && $payment_hash->payment->status_id == Payment::STATUS_COMPLETED) // Payment made, all good! + return; + + nlog("invoice paid check"); + if($payment_hash->fee_invoice && $payment_hash->fee_invoice->status_id == Invoice::STATUS_PAID){ // Payment made, all good! + + nlog("payment status check"); + if($payment_hash->payment && $payment_hash->payment->status_id != Payment::STATUS_COMPLETED) { // Make sure the payment is marked as completed + $payment_hash->payment->status_id = Payment::STATUS_COMPLETED; + $payment_hash->push(); + } + return; + } + + nlog("create payment check"); + if($payment_hash->fee_invoice && in_array($payment_hash->fee_invoice->status_id, [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])) { + + $payment = Payment::where('transaction_reference', $transaction_reference)->first(); + + if(!$payment) { nlog("make payment here!"); + $payment = $this->createPayment($payment_hash, [ + 'amount' => $amount, + 'transaction_reference' => $transaction_reference, + 'merchant_id' => $merchant_id, + ]); + } + } + + } + + private function getPaymentType($source): int + { + $method = 'paypal'; + + match($source) { + "card" => $method = PaymentType::CREDIT_CARD_OTHER, + "paypal" => $method = PaymentType::PAYPAL, + "venmo" => $method = PaymentType::VENMO, + "paylater" => $method = PaymentType::PAY_LATER, + default => $method = PaymentType::PAYPAL, + }; + + return $method; + } + + private function createPayment(PaymentHash $payment_hash, array $data) + { + + $client = $payment_hash->fee_invoice->client; + + $company_gateway = $this->harvestGateway($client->company, $data['merchant_id']); + $driver = $company_gateway->driver($client)->init(); + $driver->setPaymentHash($payment_hash); + + $order = $driver->getOrder($this->webhook_request['resource']['id']); + $source = 'paypal'; + + if(isset($order['payment_source'])) { + $source = array_key_first($order['payment_source']); + } + + $data = [ + 'payment_type' => $this->getPaymentType($source), + 'amount' => $data['amount'], + 'transaction_reference' => $data['transaction_reference'], + 'gateway_type_id' => GatewayType::PAYPAL, + ]; + + $payment = $driver->createPayment($data, \App\Models\Payment::STATUS_COMPLETED); + + SystemLogger::dispatch( + ['response' => $this->webhook_request, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_PAYPAL, + $client, + $client->company, + ); + + } + + private function harvestGateway(Company $company, string $merchant_id): ?CompanyGateway + { + $gateway = CompanyGateway::query() + ->where('company_id', $company->id) + ->where('gateway_key', $this->gateway_key) + ->cursor() + ->first(function ($cg) use ($merchant_id){ + $config = $cg->getConfig(); + + if($config->merchantId == $merchant_id) + return $cg; + + }); + + return $gateway ?? false; + } + + //--------------------------------------------------------------------------------------// + private function verifyWebhook(): bool + { + $request = [ + 'auth_algo' => $this->headers['paypal-auth-algo'], + 'cert_url' => $this->headers['paypal-cert-url'], + 'transmission_id' => $this->headers['paypal-transmission-id'], + 'transmission_sig' => $this->headers['paypal-transmission-sig'], + 'transmission_time' => $this->headers['paypal-transmission-time'], + 'webhook_id' => config('ninja.paypal.webhook_id'), + 'webhook_event' => $this->webhook_request, + ]; + + $headers = [ + 'Accept' => 'application/json', + 'Content-type' => 'application/json', + 'Accept-Language' => 'en_US', + 'PayPal-Partner-Attribution-Id' => 'invoiceninja_SP_PPCP', + ]; + + $r = Http::withToken($this->access_token) + ->withHeaders($headers) + ->post("https://api-m.paypal.com/v1/notifications/verify-webhook-signature", $request); + + nlog($r); + nlog($r->json()); + + if($r->successful() && $r->json()['verification_status'] == 'SUCCESS') { + return true; + } + + return false; + + } +} +/* +{ +"auth_algo": "SHA256withRSA", +"cert_url": "cert_url", +"transmission_id": "69cd13f0-d67a-11e5-baa3-778b53f4ae55", +"transmission_sig": "lmI95Jx3Y9nhR5SJWlHVIWpg4AgFk7n9bCHSRxbrd8A9zrhdu2rMyFrmz+Zjh3s3boXB07VXCXUZy/UFzUlnGJn0wDugt7FlSvdKeIJenLRemUxYCPVoEZzg9VFNqOa48gMkvF+XTpxBeUx/kWy6B5cp7GkT2+pOowfRK7OaynuxUoKW3JcMWw272VKjLTtTAShncla7tGF+55rxyt2KNZIIqxNMJ48RDZheGU5w1npu9dZHnPgTXB9iomeVRoD8O/jhRpnKsGrDschyNdkeh81BJJMH4Ctc6lnCCquoP/GzCzz33MMsNdid7vL/NIWaCsekQpW26FpWPi/tfj8nLA==", +"transmission_time": "2016-02-18T20:01:35Z", +"webhook_id": "1JE4291016473214C", +"webhook_event": { +"id": "8PT597110X687430LKGECATA", +"create_time": "2013-06-25T21:41:28Z", +"resource_type": "authorization", +"event_type": "PAYMENT.AUTHORIZATION.CREATED", +"summary": "A payment authorization was created", +"resource": { +"id": "2DC87612EK520411B", +"create_time": "2013-06-25T21:39:15Z", +"update_time": "2013-06-25T21:39:17Z", +"state": "authorized", +"amount": { +"total": "7.47", +"currency": "USD", +"details": { +"subtotal": "7.47" +} +}, +"parent_payment": "PAY-36246664YD343335CKHFA4AY", +"valid_until": "2013-07-24T21:39:15Z", +"links": [ +{ +"href": "https://api-m.paypal.com/v1/payments/authorization/2DC87612EK520411B", +"rel": "self", +"method": "GET" +}, +{ +"href": "https://api-m.paypal.com/v1/payments/authorization/2DC87612EK520411B/capture", +"rel": "capture", +"method": "POST" +}, +{ +"href": "https://api-m.paypal.com/v1/payments/authorization/2DC87612EK520411B/void", +"rel": "void", +"method": "POST" +}, +{ +"href": "https://api-m.paypal.com/v1/payments/payment/PAY-36246664YD343335CKHFA4AY", +"rel": "parent_payment", +"method": "GET" +} +] +} +} +} +*/ diff --git a/app/PaymentDrivers/PayPalPPCPPaymentDriver.php b/app/PaymentDrivers/PayPalPPCPPaymentDriver.php index 58b7570182b4..72b74e27eb7b 100644 --- a/app/PaymentDrivers/PayPalPPCPPaymentDriver.php +++ b/app/PaymentDrivers/PayPalPPCPPaymentDriver.php @@ -12,16 +12,18 @@ namespace App\PaymentDrivers; -use App\Exceptions\PaymentFailed; -use App\Jobs\Util\SystemLogger; -use App\Models\GatewayType; -use App\Models\Invoice; -use App\Models\PaymentType; -use App\Models\SystemLog; -use App\Utils\Traits\MakesHash; -use Carbon\Carbon; -use Illuminate\Support\Facades\Http; use Str; +use Carbon\Carbon; +use App\Models\Invoice; +use App\Models\SystemLog; +use App\Models\GatewayType; +use App\Models\PaymentType; +use Illuminate\Http\Request; +use App\Jobs\Util\SystemLogger; +use App\Utils\Traits\MakesHash; +use App\Exceptions\PaymentFailed; +use Illuminate\Support\Facades\Http; +use App\PaymentDrivers\PayPal\PayPalWebhook; class PayPalPPCPPaymentDriver extends BaseDriver { @@ -141,8 +143,8 @@ class PayPalPPCPPaymentDriver extends BaseDriver public function init(): self { - $this->api_endpoint_url = 'https://api-m.paypal.com'; - // $this->api_endpoint_url = 'https://api-m.sandbox.paypal.com'; + // $this->api_endpoint_url = 'https://api-m.paypal.com'; + $this->api_endpoint_url = 'https://api-m.sandbox.paypal.com'; $secret = config('ninja.paypal.secret'); $client_id = config('ninja.paypal.client_id'); @@ -165,8 +167,14 @@ class PayPalPPCPPaymentDriver extends BaseDriver return $this; } - - public function setPaymentMethod($payment_method_id) + + /** + * Payment method setter + * + * @param mixed $payment_method_id + * @return self + */ + public function setPaymentMethod($payment_method_id): self { if(!$payment_method_id) { return $this; @@ -192,7 +200,12 @@ class PayPalPPCPPaymentDriver extends BaseDriver return $this; } - + + /** + * Checks whether payments are enabled on the account + * + * @return self + */ private function checkPaymentsReceivable(): self { @@ -217,7 +230,13 @@ class PayPalPPCPPaymentDriver extends BaseDriver return $this; } - + + /** + * Presents the Payment View to the client + * + * @param mixed $data + * @return void + */ public function processPaymentView($data) { $this->init()->checkPaymentsReceivable(); @@ -238,7 +257,13 @@ class PayPalPPCPPaymentDriver extends BaseDriver return render('gateways.paypal.ppcp.pay', $data); } - + + /** + * Processes the payment response + * + * @param mixed $request + * @return void + */ public function processPaymentResponse($request) { @@ -293,9 +318,21 @@ class PayPalPPCPPaymentDriver extends BaseDriver throw new PaymentFailed($message, 400); } } + + public function getOrder(string $order_id) + { + $this->init(); + $r = $this->gatewayRequest("/v2/checkout/orders/{$order_id}", 'get', ['body' => '']); + return $r->json(); + } + /** + * Generates a client token for the payment form. + * + * @return string + */ private function getClientToken(): string { @@ -308,7 +345,12 @@ class PayPalPPCPPaymentDriver extends BaseDriver throw new PaymentFailed('Unable to gain client token from Paypal. Check your configuration', 401); } - + + /** + * Builds the payment request. + * + * @return array + */ private function paymentSource(): array { /** we only need to support paypal as payment source until as we are only using hosted payment buttons */ @@ -335,7 +377,13 @@ class PayPalPPCPPaymentDriver extends BaseDriver ]; } - + + /** + * Creates the PayPal Order object + * + * @param array $data + * @return string + */ private function createOrder(array $data): string { @@ -353,7 +401,8 @@ class PayPalPPCPPaymentDriver extends BaseDriver "payment_source" => $this->paymentSource(), "purchase_units" => [ [ - "description" =>ctrans('texts.invoice_number').'# '.$invoice->number, + "custom_id" => $this->payment_hash->hash, + "description" => ctrans('texts.invoice_number').'# '.$invoice->number, "invoice_id" => $invoice->number, "payee" => [ "merchant_id" => $this->company_gateway->getConfigField('merchantId'), @@ -432,7 +481,16 @@ class PayPalPPCPPaymentDriver extends BaseDriver : null; } - + + /** + * Generates the gateway request + * + * @param string $uri + * @param string $verb + * @param array $data + * @param ?array $headers + * @return \Illuminate\Http\Client\Response + */ public function gatewayRequest(string $uri, string $verb, array $data, ?array $headers = []) { $this->init(); @@ -448,7 +506,6 @@ class PayPalPPCPPaymentDriver extends BaseDriver return $r; } - SystemLogger::dispatch( ['response' => $r->body()], SystemLog::CATEGORY_GATEWAY_RESPONSE, @@ -461,7 +518,13 @@ class PayPalPPCPPaymentDriver extends BaseDriver throw new PaymentFailed("Gateway failure - {$r->body()}", 401); } - + + /** + * Generates the request headers + * + * @param array $headers + * @return array + */ private function getHeaders(array $headers = []): array { return array_merge([ @@ -473,8 +536,13 @@ class PayPalPPCPPaymentDriver extends BaseDriver ], $headers); } - private function feeCalc($invoice, $invoice_total) + public function processWebhookRequest(Request $request) { + // nlog($request->all()); + // nlog($request->headers->all()); + $this->init(); + PayPalWebhook::dispatch($request->all(), $request->headers->all(), $this->access_token); } + } diff --git a/app/PaymentDrivers/PayPalRestPaymentDriver.php b/app/PaymentDrivers/PayPalRestPaymentDriver.php index efcdba1bfa9d..6a5cd9cd001a 100644 --- a/app/PaymentDrivers/PayPalRestPaymentDriver.php +++ b/app/PaymentDrivers/PayPalRestPaymentDriver.php @@ -300,115 +300,6 @@ class PayPalRestPaymentDriver extends BaseDriver ], $headers); } - /* - public function processPaymentResponse($request) - { - $this->initializeOmnipayGateway(); - - $response = $this->omnipay_gateway - ->completePurchase(['amount' => $this->payment_hash->data->amount, 'currency' => $this->client->getCurrencyCode()]) - ->send(); - - if ($response->isCancelled() && $this->client->getSetting('enable_client_portal')) { - return redirect()->route('client.invoices.index')->with('warning', ctrans('texts.status_cancelled')); - } elseif ($response->isCancelled() && !$this->client->getSetting('enable_client_portal')) { - redirect()->route('client.invoices.show', ['invoice' => $this->payment_hash->fee_invoice])->with('warning', ctrans('texts.status_cancelled')); - } - - if ($response->isSuccessful()) { - $data = [ - 'payment_method' => $response->getData()['TOKEN'], - 'payment_type' => PaymentType::PAYPAL, - 'amount' => $this->payment_hash->data->amount, - 'transaction_reference' => $response->getTransactionReference(), - 'gateway_type_id' => GatewayType::PAYPAL, - ]; - - $payment = $this->createPayment($data, \App\Models\Payment::STATUS_COMPLETED); - - SystemLogger::dispatch( - ['response' => (array) $response->getData(), 'data' => $data], - SystemLog::CATEGORY_GATEWAY_RESPONSE, - SystemLog::EVENT_GATEWAY_SUCCESS, - SystemLog::TYPE_PAYPAL, - $this->client, - $this->client->company, - ); - - return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]); - } - - if (! $response->isSuccessful()) { - $data = $response->getData(); - - $this->sendFailureMail($response->getMessage() ?: ''); - - $message = [ - 'server_response' => $data['L_LONGMESSAGE0'], - 'data' => $this->payment_hash->data, - ]; - - SystemLogger::dispatch( - $message, - SystemLog::CATEGORY_GATEWAY_RESPONSE, - SystemLog::EVENT_GATEWAY_FAILURE, - SystemLog::TYPE_PAYPAL, - $this->client, - $this->client->company, - ); - - throw new PaymentFailed($response->getMessage(), $response->getCode()); - } - } - - public function generatePaymentDetails(array $data) - { - $_invoice = collect($this->payment_hash->data->invoices)->first(); - $invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id)); - - // $this->fee = $this->feeCalc($invoice, $data['total']['amount_with_fee']); - - return [ - 'currency' => $this->client->getCurrencyCode(), - 'transactionType' => 'Purchase', - 'clientIp' => request()->getClientIp(), - // 'amount' => round(($data['total']['amount_with_fee'] + $this->fee),2), - 'amount' => round($data['total']['amount_with_fee'], 2), - 'returnUrl' => route('client.payments.response', [ - 'company_gateway_id' => $this->company_gateway->id, - 'payment_hash' => $this->payment_hash->hash, - 'payment_method_id' => GatewayType::PAYPAL, - ]), - 'cancelUrl' => $this->client->company->domain()."/client/invoices/{$invoice->hashed_id}", - 'description' => implode(',', collect($this->payment_hash->data->invoices) - ->map(function ($invoice) { - return sprintf('%s: %s', ctrans('texts.invoice_number'), $invoice->invoice_number); - })->toArray()), - 'transactionId' => $this->payment_hash->hash.'-'.time(), - 'ButtonSource' => 'InvoiceNinja_SP', - 'solutionType' => 'Sole', - ]; - } - - public function generatePaymentItems(array $data) - { - $_invoice = collect($this->payment_hash->data->invoices)->first(); - $invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id)); - - $items = []; - - $items[] = new Item([ - 'name' => ' ', - 'description' => ctrans('texts.invoice_number').'# '.$invoice->number, - 'price' => $data['total']['amount_with_fee'], - 'quantity' => 1, - ]); - - return $items; - } - - */ - private function feeCalc($invoice, $invoice_total) { $invoice->service()->removeUnpaidGatewayFees(); diff --git a/config/ninja.php b/config/ninja.php index 6221456e165f..db2b42e4ae47 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -227,5 +227,6 @@ return [ 'paypal' => [ 'secret' => env('PAYPAL_SECRET', null), 'client_id' => env('PAYPAL_CLIENT_ID', null), + 'webhook_id' => env('PAYPAL_WEBHOOK_ID', null), ] ]; diff --git a/routes/api.php b/routes/api.php index 4d648054e298..a559d65e8e7c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -113,6 +113,7 @@ use App\Http\Controllers\UserController; use App\Http\Controllers\VendorController; use App\Http\Controllers\WebCronController; use App\Http\Controllers\WebhookController; +use App\PaymentDrivers\PayPalPPCPPaymentDriver; use Illuminate\Support\Facades\Route; Route::group(['middleware' => ['throttle:api', 'api_secret_check']], function () { @@ -426,5 +427,6 @@ Route::post('api/v1/yodlee/refresh_updates', [YodleeController::class, 'refreshU Route::post('api/v1/yodlee/balance', [YodleeController::class, 'balanceWebhook'])->middleware('throttle:100,1'); Route::get('api/v1/protected_download/{hash}', [ProtectedDownloadController::class, 'index'])->name('protected_download')->middleware('throttle:300,1'); +Route::post('api/v1/ppcp/webhook', [PayPalPPCPPaymentDriver::class, 'processWebhookRequest'])->middleware('throttle:1000,1'); Route::fallback([BaseController::class, 'notFound'])->middleware('throttle:404');