1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 05:02:36 +01:00

Working on upgrade for paypal

This commit is contained in:
David Bomba 2023-12-13 17:10:29 +11:00
parent 219e07c1c3
commit 37cc0cf442
7 changed files with 583 additions and 139 deletions

View File

@ -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';

View File

@ -0,0 +1,78 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Notifications\Ninja;
use App\Models\Account;
use App\Models\Client;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
class PayPalUnlinkedTransaction extends Notification
{
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(private string $order_id, private string $transaction_reference)
{
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['slack'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
*
*/
public function toMail($notifiable)
{
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
public function toSlack($notifiable)
{
$content = "PayPal Order Not Found\n";
$content .= "{$this->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);
}
}

View File

@ -0,0 +1,382 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\PayPal;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\SystemLog;
use App\Libraries\MultiDB;
use App\Models\GatewayType;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use Illuminate\Bus\Queueable;
use App\Models\CompanyGateway;
use App\Jobs\Util\SystemLogger;
use Illuminate\Support\Facades\Http;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Notifications\Ninja\PayPalUnlinkedTransaction;
class PayPalWebhook implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1; //number of retries
public $deleteWhenMissingModels = true;
private $gateway_key = '80af24a6a691230bbec33e930ab40666';
public function __construct(protected array $webhook_request, protected array $headers, protected string $access_token)
{
}
public function handle()
{
if($this->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"
}
]
}
}
}
*/

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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),
]
];

View File

@ -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');