diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index ff36acb2ee4e..a918ef9605e0 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -193,7 +193,7 @@ class PaymentController extends Controller $payment_hash = new PaymentHash; $payment_hash->hash = Str::random(128); - $payment_hash->data = $payable_invoices->toArray(); + $payment_hash->data = ['invoices' => $payable_invoices->toArray()]; $payment_hash->fee_total = $fee_totals; $payment_hash->fee_invoice_id = $first_invoice->id; $payment_hash->save(); diff --git a/app/Models/PaymentHash.php b/app/Models/PaymentHash.php index 2e16a8fc2631..f8eddf35f915 100644 --- a/app/Models/PaymentHash.php +++ b/app/Models/PaymentHash.php @@ -21,9 +21,10 @@ class PaymentHash extends Model 'data' => 'object', ]; + public function invoices() { - return $this->data; + return $this->data->invoices; } public function payment() diff --git a/app/PaymentDrivers/CheckoutCom/CreditCard.php b/app/PaymentDrivers/CheckoutCom/CreditCard.php new file mode 100644 index 000000000000..13c6f1236363 --- /dev/null +++ b/app/PaymentDrivers/CheckoutCom/CreditCard.php @@ -0,0 +1,154 @@ +checkout = $checkout; + } + + /** + * An authorization view for credit card. + * + * @param mixed $data + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function authorizeView($data) + { + return render('gateways.checkout.credit_card.authorize'); + } + + /** + * Checkout.com supports doesn't support direct authorization of the credit card. + * Token can be saved after the first (successful) purchase. + * + * @param mixed $data + * @return void + */ + public function authorizeResponse($data) + { + return; + } + + public function paymentView($data) + { + $data['gateway'] = $this->checkout; + $data['company_gateway'] = $this->checkout->company_gateway; + $data['client'] = $this->checkout->client; + $data['currency'] = $this->checkout->client->getCurrencyCode(); + $data['value'] = $this->checkout->convertToCheckoutAmount($data['amount_with_fee'], $this->checkout->client->getCurrencyCode()); + $data['raw_value'] = $data['amount_with_fee']; + $data['customer_email'] = $this->checkout->client->present()->email; + + return render('gateways.checkout.credit_card.pay', $data); + } + + public function paymentResponse($request) + { + $this->checkout->init(); + + $state = [ + 'server_response' => json_decode($request->gateway_response), + 'value' => $request->value, + 'raw_value' => $request->raw_value, + 'currency' => $request->currency, + 'payment_hash' => $request->payment_hash, + 'reference' => $request->payment_hash, + ]; + + $state = array_merge($state, $request->all()); + $state['store_card'] = boolval($state['store_card']); + + $payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->payment_hash])->first(); + + $payment_hash->data = array_merge((array) $payment_hash->data, $state); + $payment_hash->save(); + + $this->payment_hash = $payment_hash; + + if ($request->has('token') && !is_null($request->token)) { + return $this->attemptPaymentUsingToken(); + } + + return $this->attemptPaymentUsingCreditCard(); + } + + private function attemptPaymentUsingToken() + { + $method = new IdSource($this->payment_hash->data->token); + + return $this->completePayment($method); + } + + private function attemptPaymentUsingCreditCard() + { + $checkout_response = $this->payment_hash->data->server_response; + + $method = new TokenSource( + $checkout_response->cardToken + ); + + return $this->completePayment($method); + } + + private function completePayment($method, $enable_3ds = false) + { + // TODO: confirmGatewayFee & unwind + + $payment = new Payment($method, $this->payment_hash->data->currency); + $payment->amount = $this->payment_hash->data->value; + $payment->reference = $this->payment_hash->data->reference; + + if ($this->checkout->client->currency()->code === 'EUR' && $enable_3ds) { + $payment->{'3ds'} = ['enabled' => true]; + } + + try { + $response = $this->checkout->gateway->payments()->request($payment); + + if ($response->status == 'Authorized') { + return $this->processSuccessfulPayment($response); + } + + if ($response->status == 'Pending') { + return $this->processPendingPayment($response); + } + + if ($response->status == 'Declined') { + return $this->processUnsuccessfulPayment($response); + } + } catch (\Checkout\Library\Exceptions\CheckoutHttpException $e) { + return $this->processInternallyFailedPayment($e); + } + } +} diff --git a/app/PaymentDrivers/CheckoutCom/Utilities.php b/app/PaymentDrivers/CheckoutCom/Utilities.php index 20804a258de0..22e4a3678d4d 100644 --- a/app/PaymentDrivers/CheckoutCom/Utilities.php +++ b/app/PaymentDrivers/CheckoutCom/Utilities.php @@ -12,6 +12,13 @@ namespace App\PaymentDrivers\CheckoutCom; +use App\Events\Payment\PaymentWasCreated; +use App\Jobs\Mail\PaymentFailureMailer; +use App\Jobs\Util\SystemLogger; +use App\Models\PaymentType; +use App\Models\SystemLog; +use App\Utils\Ninja; + trait Utilities { public function getPublishableKey() @@ -39,4 +46,131 @@ trait Utilities // https://docs.checkout.com/resources/calculating-the-value#Calculatingthevalue-Option3:Thevaluedividedby100valuediv100 return round($amount * 100); } + + private function processSuccessfulPayment(\Checkout\Models\Payments\Payment $_payment) + { + if ($this->payment_hash->data->store_card) { + // $this->saveCreditCard(); + } + + $data = [ + 'payment_method' => $_payment->source['id'], + 'payment_type' => PaymentType::parseCardType(strtolower($_payment->source['scheme'])), + 'amount' => $this->payment_hash->data->value, + ]; + + $payment = $this->checkout->createPayment($data, \App\Models\Payment::STATUS_COMPLETED); + + $this->payment_hash->payment_id = $payment->id; + $this->payment_hash->save(); + + $this->checkout->attachInvoices($payment, $this->payment_hash); + + $payment->service()->updateInvoicePayment($this->payment_hash); + + event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); + + SystemLogger::dispatch( + ['response' => $_payment, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_CHECKOUT, + $this->checkout->client + ); + + return redirect()->route('client.payments.show', ['payment' => $this->checkout->encodePrimaryKey($payment->id)]); + } + + public function processUnsuccessfulPayment(\Checkout\Models\Payments\Payment $_payment) + { + PaymentFailureMailer::dispatch( + $this->checkout->client, + $_payment, + $this->checkout->client->company, + $this->payment_hash->data->value + ); + + $message = [ + 'server_response' => $_payment, + 'data' => $this->payment_hash->data, + ]; + + SystemLogger::dispatch( + $message, + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_CHECKOUT, + $this->checkout->client + ); + + return render('gateways.unsuccessful', [ + 'code' => $_payment->http_code, + 'message' => $_payment->status, + ]); + } + + private function processPendingPayment(\Checkout\Models\Payments\Payment $_payment) + { + $data = [ + 'payment_method' => $_payment->source['id'], + 'payment_type' => PaymentType::parseCardType(strtolower($_payment->source['scheme'])), + 'amount' => $this->payment_hash->data->value, + ]; + + $payment = $this->checkout->createPayment($data, \App\Models\Payment::STATUS_PENDING); + + $this->payment_hash->payment_id = $payment->id; + $this->payment_hash->save(); + + $this->checkout->attachInvoices($payment, $this->payment_hash); + + $payment->service()->updateInvoicePayment($this->payment_hash); + + event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); + + SystemLogger::dispatch( + ['response' => $_payment, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_CHECKOUT, + $this->checkout->client + ); + + try { + return redirect($_payment->_links['redirect']['href']); + } catch (\Exception $e) { + return $this->processInternallyFailedPayment($e); + } + } + + private function processInternallyFailedPayment($e) + { + if ($e instanceof \Checkout\Library\Exceptions\CheckoutHttpException) { + $error = $e->getBody(); + } + + if ($e instanceof \Exception) { + $error = $e->getMessage(); + } + + PaymentFailureMailer::dispatch( + $this->checkout->client, + $error, + $this->checkout->client->company, + $this->payment_hash->data->value + ); + + SystemLogger::dispatch( + $this->payment_hash, + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_ERROR, + SystemLog::TYPE_CHECKOUT, + $this->checkout->client, + ); + + return render('gateways.unsuccessful', [ + 'error' => $e->getCode(), + 'message' => $error, + ]); + } } diff --git a/app/PaymentDrivers/CheckoutComPaymentDriver.php b/app/PaymentDrivers/CheckoutComPaymentDriver.php index 4d854817df27..d4afee61f9bb 100644 --- a/app/PaymentDrivers/CheckoutComPaymentDriver.php +++ b/app/PaymentDrivers/CheckoutComPaymentDriver.php @@ -12,24 +12,15 @@ namespace App\PaymentDrivers; -use App\Events\Payment\PaymentWasCreated; -use App\Jobs\Mail\PaymentFailureMailer; -use App\Jobs\Util\SystemLogger; use App\Models\ClientGatewayToken; use App\Models\GatewayType; use App\Models\Payment; use App\Models\PaymentHash; -use App\Models\PaymentType; -use App\Models\SystemLog; use App\PaymentDrivers\BaseDriver; use App\PaymentDrivers\CheckoutCom\Utilities; -use App\Utils\Ninja; use App\Utils\Traits\SystemLogTrait; use Checkout\CheckoutApi; use Checkout\Library\Exceptions\CheckoutHttpException; -use Checkout\Models\Payments\IdSource; -use Checkout\Models\Payments\Payment as CheckoutPayment; -use Checkout\Models\Payments\TokenSource; class CheckoutComPaymentDriver extends BaseDriver { @@ -50,13 +41,15 @@ class CheckoutComPaymentDriver extends BaseDriver /* Authorise payment methods */ public $can_authorise_credit_card = true; - /** Instance of \Checkout\CheckoutApi */ + /** + * @var \Checkout\CheckoutApi; + */ public $gateway; public $payment_method; //the gateway type id public static $methods = [ - GatewayType::CREDIT_CARD => '', + GatewayType::CREDIT_CARD => \App\PaymentDrivers\CheckoutCom\CreditCard::class, ]; /** @@ -75,6 +68,13 @@ class CheckoutComPaymentDriver extends BaseDriver */ public function setPaymentMethod($payment_method = null) { + // At the moment Checkout.com payment + // driver only supports payments using credit card. + + $class = self::$methods[GatewayType::CREDIT_CARD]; + + $this->payment_method = new $class($this); + return $this; } @@ -82,7 +82,7 @@ class CheckoutComPaymentDriver extends BaseDriver * Initialize the checkout payment driver * @return $this */ - public function init() + public function init() { $config = [ 'secret' => $this->company_gateway->getConfigField('secretApiKey'), @@ -102,22 +102,20 @@ class CheckoutComPaymentDriver extends BaseDriver */ public function viewForType($gateway_type_id) { - //currently only ever token or creditcard so no need for switches - $this->payment_method = $gateway_type_id; + // At the moment Checkout.com payment + // driver only supports payments using credit card. - return 'gateways.checkout.credit_card'; - + return 'gateways.checkout.credit_card.pay'; } - /** - * Authorization view - * - * @param array $data Payment data array - * @return view Authorization View - */ public function authorizeView($data) { - return render('gateways.checkout.authorize'); + return $this->payment_method->authorizeView($data); + } + + public function authorizeResponse($data) + { + return $this->payment_method->authorizeResponse($data); } /** @@ -128,15 +126,7 @@ class CheckoutComPaymentDriver extends BaseDriver */ public function processPaymentView(array $data) { - $data['gateway'] = $this; - $data['company_gateway'] = $this->company_gateway; - $data['client'] = $this->client; - $data['currency'] = $this->client->getCurrencyCode(); - $data['value'] = $this->convertToCheckoutAmount($data['amount_with_fee'], $this->client->getCurrencyCode()); - $data['raw_value'] = $data['amount_with_fee']; - $data['customer_email'] = $this->client->present()->email; - - return render($this->viewForType($data['payment_method_id']), $data); + return $this->payment_method->paymentView($data); } /** @@ -147,194 +137,7 @@ class CheckoutComPaymentDriver extends BaseDriver */ public function processPaymentResponse($request) { - $this->init(); - - $state = [ - 'server_response' => json_decode($request->gateway_response), - 'value' => $request->value, - 'raw_value' => $request->raw_value, - 'currency' => $request->currency, - 'payment_hash' =>$request->payment_hash, - 'reference' => $request->payment_hash, - ]; - - $payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->payment_hash])->first(); - - $state = array_merge($state, $request->all()); - $state['store_card'] = boolval($state['store_card']); - - if ($request->has('token') && ! is_null($request->token)) { - $method = new IdSource($state['token']); - $payment = new CheckoutPayment($method, $state['currency']); - $payment->amount = $state['value']; - $payment->reference = $state['reference']; - } else { - $method = new TokenSource($state['server_response']->cardToken); - $payment = new CheckoutPayment($method, $state['currency']); - $payment->amount = $state['value']; - $payment->reference = $state['reference']; - - if ($this->client->currency()->code === 'EUR') { - $payment->{'3ds'} = ['enabled' => true]; - } - } - - try { - $response = $this->gateway->payments()->request($payment); - $state['payment_response'] = $response; - - if ($response->status === 'Authorized') { - - $this->confirmGatewayFee($request); - - return $this->processSuccessfulPayment($state); - } - - if ($response->status === 'Pending') { - - $this->confirmGatewayFee($request); - - return $this->processPendingPayment($state); - } - - if ($response->status === 'Declined') { - $this->unWindGatewayFees($payment_hash); - - return $this->processUnsuccessfulPayment($state); - } - } catch (CheckoutHttpException $e) { - - $this->unWindGatewayFees($payment_hash); - - return $this->processInternallyFailedPayment($e, $state); - } - } - - /** - * Process a successful payment response - * - * @param array $state The state array - * @return view The response - */ - public function processSuccessfulPayment($state) - { - $state['charge_id'] = $state['payment_response']->id; - - if (isset($state['store_card']) && $state['store_card']) { - $this->saveCard($state); - } - - $data = [ - 'payment_method' => $state['charge_id'], - 'payment_type' => PaymentType::parseCardType($state['payment_response']->source['scheme']), - 'amount' => $state['raw_value'], - ]; - - $payment = $this->createPayment($data, Payment::STATUS_COMPLETED); - $payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$state['payment_hash']])->firstOrFail(); - $payment_hash->payment_id = $payment->id; - $payment_hash->save(); - - $this->attachInvoices($payment, $payment_hash); - $payment->service()->updateInvoicePayment($payment_hash); - - event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); - - $logger_message = [ - 'server_response' => $state['payment_response'], - 'data' => $data, - ]; - - SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_CHECKOUT, $this->client); - - return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]); - } - - public function processPendingPayment($state) - { - $state['charge_id'] = $state['payment_response']->id; - - if (isset($state['store_card']) && $state['store_card']) { - $this->saveCard($state); - } - - $data = [ - 'payment_method' => $state['charge_id'], - 'payment_type' => PaymentType::parseCardType($state['payment_response']->source['scheme']), - 'amount' => $state['raw_value'], - ]; - - $payment = $this->createPayment($data, Payment::STATUS_PENDING); - - $this->attachInvoices($payment, $state['payment_hash']); - - $payment->service()->updateInvoicePayment($state['payment_hash']); - - event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); - - $logger_message = [ - 'server_response' => $state['payment_response'], - 'data' => $data, - ]; - - SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_CHECKOUT, $this->client); - - try { - return redirect($state['payment_response']->_links['redirect']['href']); - } catch (\Exception $e) { - - SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_CHECKOUT, $this->client); - - $this->unWindGatewayFees($state['payment_hash']); - - return render('gateways.unsuccessful', [ - 'code' => $state['payment_response']->response_code, - 'message' => ctrans('texts.payment_error'), - ]); - - } - } - - public function processUnsuccessfulPayment($state) - { - PaymentFailureMailer::dispatch($this->client, $state['payment_response']->response_summary, $this->client->company, $state['payment_response']->amount); - - $message = [ - 'server_response' => $state['server_response'], - 'data' => $state, - ]; - - SystemLogger::dispatch($message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_CHECKOUT, $this->client); - - // throw new \Exception('Failed to process the payment: ' . $state['payment_response']->response_summary, 1); - - return render('gateways.unsuccessful', [ - 'code' => $state['payment_response']->response_code, - 'message' => ctrans('texts.payment_error'), - ]); - } - - public function processInternallyFailedPayment($e, $state) - { - $error_message = json_decode($e->getBody()); - - PaymentFailureMailer::dispatch($this->client, $error_message->message, $this->client->company, $state['value']); - - $message = [ - 'server_response' => $state['server_response'], - 'data' => $e->getBody(), - ]; - - SystemLogger::dispatch($message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_CHECKOUT, $this->client); - - //todo push to a error page with the exception message. - - //throw new \Exception('Failed to process the payment.', 1); - - return render('gateways.unsuccessful', [ - 'code' => '500', - 'message' => ctrans('texts.payment_error'), - ]); + return $this->payment_method->paymentResponse($request); } public function createPayment($data, $status = Payment::STATUS_COMPLETED): Payment @@ -356,27 +159,27 @@ class CheckoutComPaymentDriver extends BaseDriver public function saveCard($state) { //some cards just can't be tokenized.... - if(!$state['payment_response']->source['id']) + if (!$state['payment_response']->source['id']) return; - - // [id] => src_hck5nsv3fljehbam2cvdm7fioa - // [type] => card - // [expiry_month] => 10 - // [expiry_year] => 2022 - // [scheme] => Visa - // [last4] => 4242 - // [fingerprint] => 688192847DB9AE8A26C53776D036D5B8AD2CEAF1D5A8F5475F542B021041EFA1 - // [bin] => 424242 - // [card_type] => Credit - // [card_category] => Consumer - // [issuer] => JPMORGAN CHASE BANK NA - // [issuer_country] => US - // [product_id] => A - // [product_type] => Visa Traditional - // [avs_check] => S - // [cvv_check] => Y - // [payouts] => 1 - // [fast_funds] => d + + // [id] => src_hck5nsv3fljehbam2cvdm7fioa + // [type] => card + // [expiry_month] => 10 + // [expiry_year] => 2022 + // [scheme] => Visa + // [last4] => 4242 + // [fingerprint] => 688192847DB9AE8A26C53776D036D5B8AD2CEAF1D5A8F5475F542B021041EFA1 + // [bin] => 424242 + // [card_type] => Credit + // [card_category] => Consumer + // [issuer] => JPMORGAN CHASE BANK NA + // [issuer_country] => US + // [product_id] => A + // [product_type] => Visa Traditional + // [avs_check] => S + // [cvv_check] => Y + // [payouts] => 1 + // [fast_funds] => d $payment_meta = new \stdClass; $payment_meta->exp_month = (string)$state['payment_response']->source['expiry_month']; diff --git a/resources/views/portal/ninja2020/gateways/checkout/authorize.blade.php b/resources/views/portal/ninja2020/gateways/checkout/credit_card/authorize.blade.php similarity index 100% rename from resources/views/portal/ninja2020/gateways/checkout/authorize.blade.php rename to resources/views/portal/ninja2020/gateways/checkout/credit_card/authorize.blade.php diff --git a/resources/views/portal/ninja2020/gateways/checkout/credit_card.blade.php b/resources/views/portal/ninja2020/gateways/checkout/credit_card/pay.blade.php similarity index 100% rename from resources/views/portal/ninja2020/gateways/checkout/credit_card.blade.php rename to resources/views/portal/ninja2020/gateways/checkout/credit_card/pay.blade.php