mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-08 19:14:41 -04:00
Fixing payment hooks
This commit is contained in:
parent
4281a1c3b0
commit
6ad49cf281
@ -146,189 +146,14 @@ class OnlinePaymentController extends BaseController
|
|||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch($gatewayId) {
|
$paymentDriver = $accountGateway->paymentDriver();
|
||||||
case GATEWAY_STRIPE:
|
|
||||||
return $this->handleStripeWebhook($accountGateway);
|
try {
|
||||||
case GATEWAY_WEPAY:
|
$result = $paymentDriver->handleWebHook(Input::all());
|
||||||
return $this->handleWePayWebhook($accountGateway);
|
return response()->json(['message' => $result]);
|
||||||
default:
|
} catch (Exception $exception) {
|
||||||
return response()->json([
|
return response()->json(['message' => ]$exception->getMessage()], 500);
|
||||||
'message' => 'Unsupported gateway',
|
|
||||||
], 404);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function handleWePayWebhook($accountGateway) {
|
|
||||||
$data = Input::all();
|
|
||||||
$accountId = $accountGateway->account_id;
|
|
||||||
|
|
||||||
foreach (array_keys($data) as $key) {
|
|
||||||
if ('_id' == substr($key, -3)) {
|
|
||||||
$objectType = substr($key, 0, -3);
|
|
||||||
$objectId = $data[$key];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($objectType)) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => 'Could not find object id parameter',
|
|
||||||
], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($objectType == 'credit_card') {
|
|
||||||
$paymentMethod = PaymentMethod::scope(false, $accountId)->where('source_reference', '=', $objectId)->first();
|
|
||||||
|
|
||||||
if (!$paymentMethod) {
|
|
||||||
return array('message' => 'Unknown payment method');
|
|
||||||
}
|
|
||||||
|
|
||||||
$wepay = \Utils::setupWePay($accountGateway);
|
|
||||||
$source = $wepay->request('credit_card', array(
|
|
||||||
'client_id' => WEPAY_CLIENT_ID,
|
|
||||||
'client_secret' => WEPAY_CLIENT_SECRET,
|
|
||||||
'credit_card_id' => intval($objectId),
|
|
||||||
));
|
|
||||||
|
|
||||||
if ($source->state == 'deleted') {
|
|
||||||
$paymentMethod->delete();
|
|
||||||
} else {
|
|
||||||
$this->paymentService->convertPaymentMethodFromWePay($source, null, $paymentMethod)->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
return array('message' => 'Processed successfully');
|
|
||||||
} elseif ($objectType == 'account') {
|
|
||||||
$config = $accountGateway->getConfig();
|
|
||||||
if ($config->accountId != $objectId) {
|
|
||||||
return array('message' => 'Unknown account');
|
|
||||||
}
|
|
||||||
|
|
||||||
$wepay = \Utils::setupWePay($accountGateway);
|
|
||||||
$wepayAccount = $wepay->request('account', array(
|
|
||||||
'account_id' => intval($objectId),
|
|
||||||
));
|
|
||||||
|
|
||||||
if ($wepayAccount->state == 'deleted') {
|
|
||||||
$accountGateway->delete();
|
|
||||||
} else {
|
|
||||||
$config->state = $wepayAccount->state;
|
|
||||||
$accountGateway->setConfig($config);
|
|
||||||
$accountGateway->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
return array('message' => 'Processed successfully');
|
|
||||||
} elseif ($objectType == 'checkout') {
|
|
||||||
$payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $objectId)->first();
|
|
||||||
|
|
||||||
if (!$payment) {
|
|
||||||
return array('message' => 'Unknown payment');
|
|
||||||
}
|
|
||||||
|
|
||||||
$wepay = \Utils::setupWePay($accountGateway);
|
|
||||||
$checkout = $wepay->request('checkout', array(
|
|
||||||
'checkout_id' => intval($objectId),
|
|
||||||
));
|
|
||||||
|
|
||||||
if ($checkout->state == 'refunded') {
|
|
||||||
$payment->recordRefund();
|
|
||||||
} elseif (!empty($checkout->refund) && !empty($checkout->refund->amount_refunded) && ($checkout->refund->amount_refunded - $payment->refunded) > 0) {
|
|
||||||
$payment->recordRefund($checkout->refund->amount_refunded - $payment->refunded);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($checkout->state == 'captured') {
|
|
||||||
$payment->markComplete();
|
|
||||||
} elseif ($checkout->state == 'cancelled') {
|
|
||||||
$payment->markCancelled();
|
|
||||||
} elseif ($checkout->state == 'failed') {
|
|
||||||
$payment->markFailed();
|
|
||||||
}
|
|
||||||
|
|
||||||
return array('message' => 'Processed successfully');
|
|
||||||
} else {
|
|
||||||
return array('message' => 'Ignoring event');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function handleStripeWebhook($accountGateway) {
|
|
||||||
$eventId = Input::get('id');
|
|
||||||
$eventType= Input::get('type');
|
|
||||||
$accountId = $accountGateway->account_id;
|
|
||||||
|
|
||||||
if (!$eventId) {
|
|
||||||
return response()->json(['message' => 'Missing event id'], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$eventType) {
|
|
||||||
return response()->json(['message' => 'Missing event type'], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$supportedEvents = array(
|
|
||||||
'charge.failed',
|
|
||||||
'charge.succeeded',
|
|
||||||
'customer.source.updated',
|
|
||||||
'customer.source.deleted',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!in_array($eventType, $supportedEvents)) {
|
|
||||||
return array('message' => 'Ignoring event');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the event directly from Stripe for security
|
|
||||||
$eventDetails = $this->paymentService->makeStripeCall($accountGateway, 'GET', 'events/'.$eventId);
|
|
||||||
|
|
||||||
if (is_string($eventDetails) || !$eventDetails) {
|
|
||||||
return response()->json([
|
|
||||||
'message' => $eventDetails ? $eventDetails : 'Could not get event details.',
|
|
||||||
], 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($eventType != $eventDetails['type']) {
|
|
||||||
return response()->json(['message' => 'Event type mismatch'], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$eventDetails['pending_webhooks']) {
|
|
||||||
return response()->json(['message' => 'This is not a pending event'], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if ($eventType == 'charge.failed' || $eventType == 'charge.succeeded') {
|
|
||||||
$charge = $eventDetails['data']['object'];
|
|
||||||
$transactionRef = $charge['id'];
|
|
||||||
|
|
||||||
$payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $transactionRef)->first();
|
|
||||||
|
|
||||||
if (!$payment) {
|
|
||||||
return array('message' => 'Unknown payment');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($eventType == 'charge.failed') {
|
|
||||||
if (!$payment->isFailed()) {
|
|
||||||
$payment->markFailed($charge['failure_message']);
|
|
||||||
$this->userMailer->sendNotification($payment->user, $payment->invoice, 'payment_failed', $payment);
|
|
||||||
}
|
|
||||||
} elseif ($eventType == 'charge.succeeded') {
|
|
||||||
$payment->markComplete();
|
|
||||||
} elseif ($eventType == 'charge.refunded') {
|
|
||||||
$payment->recordRefund($charge['amount_refunded'] / 100 - $payment->refunded);
|
|
||||||
}
|
|
||||||
} elseif($eventType == 'customer.source.updated' || $eventType == 'customer.source.deleted') {
|
|
||||||
$source = $eventDetails['data']['object'];
|
|
||||||
$sourceRef = $source['id'];
|
|
||||||
|
|
||||||
$paymentMethod = PaymentMethod::scope(false, $accountId)->where('source_reference', '=', $sourceRef)->first();
|
|
||||||
|
|
||||||
if (!$paymentMethod) {
|
|
||||||
return array('message' => 'Unknown payment method');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($eventType == 'customer.source.deleted') {
|
|
||||||
$paymentMethod->delete();
|
|
||||||
} elseif ($eventType == 'customer.source.updated') {
|
|
||||||
$this->paymentService->convertPaymentMethodFromStripe($source, null, $paymentMethod)->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return array('message' => 'Processed successfully');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ class VerifyCsrfToken extends BaseVerifier {
|
|||||||
'hook/email_opened',
|
'hook/email_opened',
|
||||||
'hook/email_bounced',
|
'hook/email_bounced',
|
||||||
'reseller_stats',
|
'reseller_stats',
|
||||||
'paymenthook/*',
|
'payment_hook/*',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -72,7 +72,6 @@ Route::group(['middleware' => 'auth:client'], function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
Route::post('paymenthook/{accountKey}/{gatewayId}', 'PaymentController@handlePaymentWebhook');
|
|
||||||
Route::get('license', 'NinjaController@show_license_payment');
|
Route::get('license', 'NinjaController@show_license_payment');
|
||||||
Route::post('license', 'NinjaController@do_license_payment');
|
Route::post('license', 'NinjaController@do_license_payment');
|
||||||
Route::get('claim_license', 'NinjaController@claim_license');
|
Route::get('claim_license', 'NinjaController@claim_license');
|
||||||
@ -85,6 +84,7 @@ Route::get('/auth_unlink', 'Auth\AuthController@authUnlink');
|
|||||||
|
|
||||||
Route::post('/hook/email_bounced', 'AppController@emailBounced');
|
Route::post('/hook/email_bounced', 'AppController@emailBounced');
|
||||||
Route::post('/hook/email_opened', 'AppController@emailOpened');
|
Route::post('/hook/email_opened', 'AppController@emailOpened');
|
||||||
|
Route::post('/payment_hook/{accountKey}/{gatewayId}', 'PaymentController@handlePaymentWebhook');
|
||||||
|
|
||||||
// Laravel auth routes
|
// Laravel auth routes
|
||||||
Route::get('/signup', array('as' => 'signup', 'uses' => 'Auth\AuthController@getRegister'));
|
Route::get('/signup', array('as' => 'signup', 'uses' => 'Auth\AuthController@getRegister'));
|
||||||
|
@ -143,6 +143,6 @@ class AccountGateway extends EntityModel
|
|||||||
{
|
{
|
||||||
$account = $this->account ? $this->account : Account::find($this->account_id);
|
$account = $this->account ? $this->account : Account::find($this->account_id);
|
||||||
|
|
||||||
return \URL::to(env('WEBHOOK_PREFIX','').'paymenthook/'.$account->account_key.'/'.$this->gateway_id.env('WEBHOOK_SUFFIX',''));
|
return \URL::to(env('WEBHOOK_PREFIX','').'payment_hook/'.$account->account_key.'/'.$this->gateway_id.env('WEBHOOK_SUFFIX',''));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -794,4 +794,9 @@ class BasePaymentDriver
|
|||||||
return PAYMENT_TYPE_CREDIT_CARD_OTHER;
|
return PAYMENT_TYPE_CREDIT_CARD_OTHER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function handleWebHook($input)
|
||||||
|
{
|
||||||
|
throw new Exception('Unsupported gateway');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Cache;
|
use Cache;
|
||||||
|
use App\Models\Payment;
|
||||||
use App\Models\PaymentMethod;
|
use App\Models\PaymentMethod;
|
||||||
|
|
||||||
class StripePaymentDriver extends BasePaymentDriver
|
class StripePaymentDriver extends BasePaymentDriver
|
||||||
@ -301,4 +302,92 @@ class StripePaymentDriver extends BasePaymentDriver
|
|||||||
return $e->getMessage();
|
return $e->getMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function handleWebHook($input)
|
||||||
|
{
|
||||||
|
$eventId = array_get($input, 'id');
|
||||||
|
$eventType= array_get($input, 'type');
|
||||||
|
|
||||||
|
$accountGateway = $this->accountGateway;
|
||||||
|
$accountId = $accountGateway->account_id;
|
||||||
|
|
||||||
|
if (!$eventId) {
|
||||||
|
throw new Exception('Missing event id');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$eventType) {
|
||||||
|
throw new Exception('Missing event type');
|
||||||
|
}
|
||||||
|
|
||||||
|
$supportedEvents = array(
|
||||||
|
'charge.failed',
|
||||||
|
'charge.succeeded',
|
||||||
|
'customer.source.updated',
|
||||||
|
'customer.source.deleted',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!in_array($eventType, $supportedEvents)) {
|
||||||
|
return array('message' => 'Ignoring event');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the event directly from Stripe for security
|
||||||
|
$eventDetails = $this->makeStripeCall('GET', 'events/'.$eventId);
|
||||||
|
|
||||||
|
if (is_string($eventDetails) || !$eventDetails) {
|
||||||
|
throw new Exception('Could not get event details');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($eventType != $eventDetails['type']) {
|
||||||
|
throw new Exception('Event type mismatch');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$eventDetails['pending_webhooks']) {
|
||||||
|
throw new Exception('This is not a pending event');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($eventType == 'charge.failed' || $eventType == 'charge.succeeded') {
|
||||||
|
$charge = $eventDetails['data']['object'];
|
||||||
|
$transactionRef = $charge['id'];
|
||||||
|
|
||||||
|
$payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $transactionRef)->first();
|
||||||
|
|
||||||
|
if (!$payment) {
|
||||||
|
throw new Exception('Unknown payment');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($eventType == 'charge.failed') {
|
||||||
|
if (!$payment->isFailed()) {
|
||||||
|
$payment->markFailed($charge['failure_message']);
|
||||||
|
|
||||||
|
$userMailer = app('App\Ninja\Mailers\UserMailer');
|
||||||
|
$userMailer->sendNotification($payment->user, $payment->invoice, 'payment_failed', $payment);
|
||||||
|
}
|
||||||
|
} elseif ($eventType == 'charge.succeeded') {
|
||||||
|
$payment->markComplete();
|
||||||
|
} elseif ($eventType == 'charge.refunded') {
|
||||||
|
$payment->recordRefund($charge['amount_refunded'] / 100 - $payment->refunded);
|
||||||
|
}
|
||||||
|
} elseif($eventType == 'customer.source.updated' || $eventType == 'customer.source.deleted') {
|
||||||
|
$source = $eventDetails['data']['object'];
|
||||||
|
$sourceRef = $source['id'];
|
||||||
|
|
||||||
|
$paymentMethod = PaymentMethod::scope(false, $accountId)->where('source_reference', '=', $sourceRef)->first();
|
||||||
|
|
||||||
|
if (!$paymentMethod) {
|
||||||
|
throw new Exception('Unknown payment method');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($eventType == 'customer.source.deleted') {
|
||||||
|
$paymentMethod->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
} elseif ($eventType == 'customer.source.updated') {
|
||||||
|
$this->paymentService->convertPaymentMethodFromStripe($source, null, $paymentMethod)->save();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Processed successfully';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use Session;
|
use Session;
|
||||||
use Utils;
|
use Utils;
|
||||||
|
use App\Models\Payment;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class WePayPaymentDriver extends BasePaymentDriver
|
class WePayPaymentDriver extends BasePaymentDriver
|
||||||
@ -210,4 +211,93 @@ class WePayPaymentDriver extends BasePaymentDriver
|
|||||||
return floor(min($fee, $amount * 0.2));// Maximum fee is 20% of the amount.
|
return floor(min($fee, $amount * 0.2));// Maximum fee is 20% of the amount.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function handleWebHook($input)
|
||||||
|
{
|
||||||
|
$accountId = $accountGateway->account_id;
|
||||||
|
|
||||||
|
foreach (array_keys($input) as $key) {
|
||||||
|
if ('_id' == substr($key, -3)) {
|
||||||
|
$objectType = substr($key, 0, -3);
|
||||||
|
$objectId = $input[$key];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($objectType)) {
|
||||||
|
throw new Exception('Could not find object id parameter');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($objectType == 'credit_card') {
|
||||||
|
$paymentMethod = PaymentMethod::scope(false, $accountId)->where('source_reference', '=', $objectId)->first();
|
||||||
|
|
||||||
|
if (!$paymentMethod) {
|
||||||
|
throw new Exception('Unknown payment method');
|
||||||
|
}
|
||||||
|
|
||||||
|
$wepay = Utils::setupWePay($accountGateway);
|
||||||
|
$source = $wepay->request('credit_card', array(
|
||||||
|
'client_id' => WEPAY_CLIENT_ID,
|
||||||
|
'client_secret' => WEPAY_CLIENT_SECRET,
|
||||||
|
'credit_card_id' => intval($objectId),
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($source->state == 'deleted') {
|
||||||
|
$paymentMethod->delete();
|
||||||
|
} else {
|
||||||
|
$this->paymentService->convertPaymentMethodFromWePay($source, null, $paymentMethod)->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Processed successfully';
|
||||||
|
} elseif ($objectType == 'account') {
|
||||||
|
$config = $accountGateway->getConfig();
|
||||||
|
if ($config->accountId != $objectId) {
|
||||||
|
throw new Exception('Unknown account');
|
||||||
|
}
|
||||||
|
|
||||||
|
$wepay = Utils::setupWePay($accountGateway);
|
||||||
|
$wepayAccount = $wepay->request('account', array(
|
||||||
|
'account_id' => intval($objectId),
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($wepayAccount->state == 'deleted') {
|
||||||
|
$accountGateway->delete();
|
||||||
|
} else {
|
||||||
|
$config->state = $wepayAccount->state;
|
||||||
|
$accountGateway->setConfig($config);
|
||||||
|
$accountGateway->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return array('message' => 'Processed successfully');
|
||||||
|
} elseif ($objectType == 'checkout') {
|
||||||
|
$payment = Payment::scope(false, $accountId)->where('transaction_reference', '=', $objectId)->first();
|
||||||
|
|
||||||
|
if (!$payment) {
|
||||||
|
throw new Exception('Unknown payment');
|
||||||
|
}
|
||||||
|
|
||||||
|
$wepay = Utils::setupWePay($accountGateway);
|
||||||
|
$checkout = $wepay->request('checkout', array(
|
||||||
|
'checkout_id' => intval($objectId),
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($checkout->state == 'refunded') {
|
||||||
|
$payment->recordRefund();
|
||||||
|
} elseif (!empty($checkout->refund) && !empty($checkout->refund->amount_refunded) && ($checkout->refund->amount_refunded - $payment->refunded) > 0) {
|
||||||
|
$payment->recordRefund($checkout->refund->amount_refunded - $payment->refunded);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($checkout->state == 'captured') {
|
||||||
|
$payment->markComplete();
|
||||||
|
} elseif ($checkout->state == 'cancelled') {
|
||||||
|
$payment->markCancelled();
|
||||||
|
} elseif ($checkout->state == 'failed') {
|
||||||
|
$payment->markFailed();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Processed successfully';
|
||||||
|
} else {
|
||||||
|
return 'Ignoring event';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,6 @@ use App\Ninja\Datatables\PaymentDatatable;
|
|||||||
|
|
||||||
class PaymentService extends BaseService
|
class PaymentService extends BaseService
|
||||||
{
|
{
|
||||||
public $lastError;
|
|
||||||
protected $datatableService;
|
protected $datatableService;
|
||||||
|
|
||||||
public function __construct(PaymentRepository $paymentRepo, AccountRepository $accountRepo, DatatableService $datatableService)
|
public function __construct(PaymentRepository $paymentRepo, AccountRepository $accountRepo, DatatableService $datatableService)
|
||||||
|
@ -91,7 +91,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="control-label col-lg-4 col-sm-4">{{ trans('texts.webhook_url') }}</label>
|
<label class="control-label col-lg-4 col-sm-4">{{ trans('texts.webhook_url') }}</label>
|
||||||
<div class="col-lg-8 col-sm-8 help-block">
|
<div class="col-lg-8 col-sm-8 help-block">
|
||||||
<input type="text" class="form-control" onfocus="$(this).select()" readonly value="{{ URL::to(env('WEBHOOK_PREFIX','').'paymenthook/'.$account->account_key.'/'.GATEWAY_STRIPE) }}">
|
<input type="text" class="form-control" onfocus="$(this).select()" readonly value="{{ URL::to(env('WEBHOOK_PREFIX','').'payment_hook/'.$account->account_key.'/'.GATEWAY_STRIPE) }}">
|
||||||
<div class="help-block"><strong>{!! trans('texts.stripe_webhook_help', [
|
<div class="help-block"><strong>{!! trans('texts.stripe_webhook_help', [
|
||||||
'link'=>'<a href="https://dashboard.stripe.com/account/webhooks" target="_blank">'.trans('texts.stripe_webhook_help_link_text').'</a>'
|
'link'=>'<a href="https://dashboard.stripe.com/account/webhooks" target="_blank">'.trans('texts.stripe_webhook_help_link_text').'</a>'
|
||||||
]) !!}</strong></div>
|
]) !!}</strong></div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user