diff --git a/app/Http/Controllers/OnlinePaymentController.php b/app/Http/Controllers/OnlinePaymentController.php index b6b558624e2d..549485d50e2d 100644 --- a/app/Http/Controllers/OnlinePaymentController.php +++ b/app/Http/Controllers/OnlinePaymentController.php @@ -146,189 +146,14 @@ class OnlinePaymentController extends BaseController ], 404); } - switch($gatewayId) { - case GATEWAY_STRIPE: - return $this->handleStripeWebhook($accountGateway); - case GATEWAY_WEPAY: - return $this->handleWePayWebhook($accountGateway); - default: - return response()->json([ - 'message' => 'Unsupported gateway', - ], 404); + $paymentDriver = $accountGateway->paymentDriver(); + + try { + $result = $paymentDriver->handleWebHook(Input::all()); + return response()->json(['message' => $result]); + } catch (Exception $exception) { + return response()->json(['message' => ]$exception->getMessage()], 500); } } - 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'); - } - } diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php index fa485068fc05..b3f5257a3fa6 100644 --- a/app/Http/Middleware/VerifyCsrfToken.php +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -22,7 +22,7 @@ class VerifyCsrfToken extends BaseVerifier { 'hook/email_opened', 'hook/email_bounced', 'reseller_stats', - 'paymenthook/*', + 'payment_hook/*', ]; /** diff --git a/app/Http/routes.php b/app/Http/routes.php index 8e91cf764e02..97c5d3f1a0db 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -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::post('license', 'NinjaController@do_license_payment'); 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_opened', 'AppController@emailOpened'); +Route::post('/payment_hook/{accountKey}/{gatewayId}', 'PaymentController@handlePaymentWebhook'); // Laravel auth routes Route::get('/signup', array('as' => 'signup', 'uses' => 'Auth\AuthController@getRegister')); diff --git a/app/Models/AccountGateway.php b/app/Models/AccountGateway.php index 54cbdfcbd833..15842486a98e 100644 --- a/app/Models/AccountGateway.php +++ b/app/Models/AccountGateway.php @@ -143,6 +143,6 @@ class AccountGateway extends EntityModel { $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','')); } } diff --git a/app/Ninja/PaymentDrivers/BasePaymentDriver.php b/app/Ninja/PaymentDrivers/BasePaymentDriver.php index 21522702b89b..61e2a0d61c3d 100644 --- a/app/Ninja/PaymentDrivers/BasePaymentDriver.php +++ b/app/Ninja/PaymentDrivers/BasePaymentDriver.php @@ -794,4 +794,9 @@ class BasePaymentDriver return PAYMENT_TYPE_CREDIT_CARD_OTHER; } } + + public function handleWebHook($input) + { + throw new Exception('Unsupported gateway'); + } } diff --git a/app/Ninja/PaymentDrivers/StripePaymentDriver.php b/app/Ninja/PaymentDrivers/StripePaymentDriver.php index c733796a6462..11e3c5646789 100644 --- a/app/Ninja/PaymentDrivers/StripePaymentDriver.php +++ b/app/Ninja/PaymentDrivers/StripePaymentDriver.php @@ -2,6 +2,7 @@ use Exception; use Cache; +use App\Models\Payment; use App\Models\PaymentMethod; class StripePaymentDriver extends BasePaymentDriver @@ -301,4 +302,92 @@ class StripePaymentDriver extends BasePaymentDriver 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'; + } } diff --git a/app/Ninja/PaymentDrivers/WePayPaymentDriver.php b/app/Ninja/PaymentDrivers/WePayPaymentDriver.php index 6a8b226ea2da..e95426df1604 100644 --- a/app/Ninja/PaymentDrivers/WePayPaymentDriver.php +++ b/app/Ninja/PaymentDrivers/WePayPaymentDriver.php @@ -2,6 +2,7 @@ use Session; use Utils; +use App\Models\Payment; use Exception; 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. } + 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'; + } + } + } diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 675b944f9554..0ada659acd27 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -28,7 +28,6 @@ use App\Ninja\Datatables\PaymentDatatable; class PaymentService extends BaseService { - public $lastError; protected $datatableService; public function __construct(PaymentRepository $paymentRepo, AccountRepository $accountRepo, DatatableService $datatableService) diff --git a/resources/views/accounts/account_gateway.blade.php b/resources/views/accounts/account_gateway.blade.php index 093fdef59ddb..5823a37fceaf 100644 --- a/resources/views/accounts/account_gateway.blade.php +++ b/resources/views/accounts/account_gateway.blade.php @@ -91,7 +91,7 @@