Stripe payments

This commit is contained in:
David Bomba 2019-09-25 12:07:33 +10:00
parent 8027e72217
commit 46602a21c5
9 changed files with 266 additions and 54 deletions

View File

@ -122,7 +122,6 @@ class InvoiceController extends Controller
$total = $invoices->sum('balance'); $total = $invoices->sum('balance');
$invoices->filter(function ($invoice){ $invoices->filter(function ($invoice){
return $invoice->isPayable(); return $invoice->isPayable();
})->map(function ($invoice){ })->map(function ($invoice){
@ -131,8 +130,6 @@ class InvoiceController extends Controller
return $invoice; return $invoice;
}); });
$formatted_total = Number::formatMoney($total, auth()->user()->client); $formatted_total = Number::formatMoney($total, auth()->user()->client);
$payment_methods = auth()->user()->client->getPaymentMethods($total); $payment_methods = auth()->user()->client->getPaymentMethods($total);
@ -142,6 +139,7 @@ class InvoiceController extends Controller
'invoices' => $invoices, 'invoices' => $invoices,
'formatted_total' => $formatted_total, 'formatted_total' => $formatted_total,
'payment_methods' => $payment_methods, 'payment_methods' => $payment_methods,
'hashed_ids' => $ids,
'total' => $total, 'total' => $total,
]; ];

View File

@ -11,26 +11,30 @@
namespace App\Http\Controllers\ClientPortal; namespace App\Http\Controllers\ClientPortal;
use Cache;
use App\Filters\PaymentFilters; use App\Filters\PaymentFilters;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\CompanyGateway; use App\Models\CompanyGateway;
use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Utils\Number;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Cache;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Yajra\DataTables\Facades\DataTables; use Yajra\DataTables\Facades\DataTables;
use Yajra\DataTables\Html\Builder; use Yajra\DataTables\Html\Builder;
/** /**
* Class InvoiceController * Class PaymentController
* @package App\Http\Controllers\ClientPortal\InvoiceController * @package App\Http\Controllers\ClientPortal\PaymentController
*/ */
class PaymentController extends Controller class PaymentController extends Controller
{ {
use MakesHash; use MakesHash;
use MakesDates;
/** /**
* Show the list of Invoices * Show the list of Invoices
@ -84,41 +88,47 @@ class PaymentController extends Controller
* The request will also contain the amount * The request will also contain the amount
* and invoice ids for reference. * and invoice ids for reference.
* *
* @param int $company_gateway_id The CompanyGateway ID
* @param int $gateway_type_id The gateway_type_id ID
* @return void * @return void
*/ */
public function process($company_gateway_id) public function process()
{ {
$invoices = Invoice::whereIn('id', $this->transformKeys(request()->input('invoice_ids'))) $invoices = Invoice::whereIn('id', $this->transformKeys(explode(",",request()->input('hashed_ids'))))
->whereClientId(auth()->user()->client->id) ->whereClientId(auth()->user()->client->id)
->get(); ->get();
$amount = request()->input('amount'); $amount = $invoices->sum('balance');
//build a cache record to maintain state $invoices->filter(function ($invoice){
$cache_hash = str_random(config('ninja.key_length')); return $invoice->isPayable();
})->map(function ($invoice){
$invoice->balance = Number::formatMoney($invoice->balance, $invoice->client);
$invoice->due_date = $this->formatDate($invoice->due_date, $invoice->client->date_format());
return $invoice;
});
Cache::put($cache_hash, 'value', now()->addMinutes(10)); $payment_methods = auth()->user()->client->getPaymentMethods($amount);
//boot the payment gateway //boot the payment gateway
$gateway = CompanyGateway::find($company_gateway_id); $gateway = CompanyGateway::find(request()->input('company_gateway_id'));
$payment_method_id = request()->input('payment_method_id');
//if there is a gateway fee, now is the time to calculate it //if there is a gateway fee, now is the time to calculate it
//and add it to the invoice //and add it to the invoice
$data = [ $data = [
'cache_hash' => $cache_hash,
'invoices' => $invoices, 'invoices' => $invoices,
'amount' => $amount, 'amount' => $amount,
'fee' => $gateway->calcGatewayFee($amount), 'fee' => $gateway->calcGatewayFee($amount),
'amount_with_fee' => ($amount + $gateway->calcGatewayFee($amount)), 'amount_with_fee' => $amount + $gateway->calcGatewayFee($amount),
'gateway' => $gateway, 'token' => auth()->user()->client->gateway_token($gateway->id, $payment_method_id),
'token' => auth()->user()->client->gateway_token($gateway->id), 'payment_method_id' => $payment_method_id,
]; ];
return view('gateways.pay_now', $data);
return $gateway->driver(auth()->user()->client)->processPayment($data);
} }

View File

@ -110,15 +110,15 @@ class Client extends BaseModel
* Allows the storage of multiple tokens * Allows the storage of multiple tokens
* per client per gateway per payment_method * per client per gateway per payment_method
* *
* @param int $gateway_id The gateway ID * @param int $company_gateway_id The company gateway ID
* @param int $payment_method_id The payment method ID * @param int $payment_method_id The payment method ID
* @return ClientGatewayToken The client token record * @return ClientGatewayToken The client token record
*/ */
public function gateway_token($gateway_id, $payment_method_id) public function gateway_token($company_gateway_id, $payment_method_id)
{ {
return $this->gateway_tokens return $this->gateway_tokens()
->whereCompanyGatewayId($gateway_id) ->whereCompanyGatewayId($company_gateway_id)
->wherePaymentMethod_id($payment_method_id) ->whereGatewayTypeId($payment_method_id)
->first(); ->first();
} }
@ -264,6 +264,14 @@ class Client extends BaseModel
return null; return null;
} }
public function getCurrencyCode()
{
if ($this->currency) {
return $this->currency->code;
}
return 'USD';
}
/** /**
* Generates an array of payment urls per client * Generates an array of payment urls per client
* for a given amount. * for a given amount.
@ -326,9 +334,8 @@ class Client extends BaseModel
$payment_urls[] = [ $payment_urls[] = [
'label' => ctrans('texts.' . $gateway->getTypeAlias($gateway_type_id)) . $fee_label, 'label' => ctrans('texts.' . $gateway->getTypeAlias($gateway_type_id)) . $fee_label,
'url' => URL::signedRoute('client.payments.process', [
'company_gateway_id' => $gateway_id, 'company_gateway_id' => $gateway_id,
'gateway_type_id' => $gateway_type_id]) 'gateway_type_id' => $gateway_type_id
]; ];
} }

View File

@ -68,7 +68,7 @@ class StripePaymentDriver extends BasePaymentDriver
{ {
$types = [ $types = [
GatewayType::CREDIT_CARD, GatewayType::CREDIT_CARD,
GatewayType::TOKEN, //GatewayType::TOKEN,
]; ];
if($this->company_gateway->getSofortEnabled() && $this->invitation && $this->client() && isset($this->client()->country) && in_array($this->client()->country, ['AUT', 'BEL', 'DEU', 'ITA', 'NLD', 'ESP'])) if($this->company_gateway->getSofortEnabled() && $this->invitation && $this->client() && isset($this->client()->country) && in_array($this->client()->country, ['AUT', 'BEL', 'DEU', 'ITA', 'NLD', 'ESP']))
@ -130,6 +130,7 @@ class StripePaymentDriver extends BasePaymentDriver
public function authorizeCreditCardView($data) public function authorizeCreditCardView($data)
{ {
$intent['intent'] = $this->getSetupIntent(); $intent['intent'] = $this->getSetupIntent();
return view('portal.default.gateways.stripe.add_credit_card', array_merge($data, $intent)); return view('portal.default.gateways.stripe.add_credit_card', array_merge($data, $intent));
@ -185,13 +186,52 @@ class StripePaymentDriver extends BasePaymentDriver
return redirect()->route('client.payment_methods.index'); return redirect()->route('client.payment_methods.index');
} }
/**
* Processes the payment with this gateway
*
* @var invoices
* @var amount
* @var fee
* @var amount_with_fee
* @var token
* @var payment_method_id
* @param array $data variables required to build payment page
* @return view Gateway and payment method specific view
*/
public function processPayment(array $data)
{
$payment_intent_data = [
'amount' => $data['amount_with_fee']*100,
'currency' => $this->client->getCurrencyCode(),
'customer' => $this->findOrCreateCustomer(),
'description' => $data['invoices']->pluck('id'),
];
if($data['token'])
$payment_intent_data['payment_method'] = $data['token']->token;
else{
// $payment_intent_data['setup_future_usage'] = 'off_session';
// $payment_intent_data['save_payment_method'] = true;
// $payment_intent_data['confirm'] = true;
}
$data['intent'] = $this->createPaymentIntent($payment_intent_data);
$data['gateway'] = $this;
return view($this->viewForType($data['payment_method_id']), $data);
}
/** /**
* Creates a new String Payment Intent * Creates a new String Payment Intent
* *
* @param array $data The data array to be passed to Stripe * @param array $data The data array to be passed to Stripe
* @return PaymentIntent The Stripe payment intent object * @return PaymentIntent The Stripe payment intent object
*/ */
public function createIntent($data) :?\Stripe\PaymentIntent public function createPaymentIntent($data) :?\Stripe\PaymentIntent
{ {
$this->init(); $this->init();
@ -258,6 +298,9 @@ class StripePaymentDriver extends BasePaymentDriver
} }
if(!$customer)
throw Exception('Unable to create gateway customer');
return $customer; return $customer;
} }

View File

@ -49,7 +49,7 @@
</ul> </ul>
</div> </div>
@include($gateway->driver(auth()->user()->client)->viewForType($gateway_type_id)) @yield('pay_now')
</div> </div>
</div> </div>

View File

@ -40,8 +40,6 @@
{{ ctrans('texts.save') }} {{ ctrans('texts.save') }}
</button> </button>
</div> </div>
</div> </div>
@endsection @endsection

View File

@ -0,0 +1,143 @@
@extends('portal.default.gateways.pay_now')
@section('pay_now')
@if($token)
<div class="py-md-5 ninja stripe">
<div class="form-group">
<input class="form-control" id="cardholder-name" type="text" placeholder="{{ ctrans('texts.name') }}">
</div>
<div class="form-group">
<div id="card-element"></div>
</div>
<div class="form-group">
<button id="card-button" data-secret="{{ $intent->client_secret }}">
Submit Payment
</button>
</div>
</div>
@else
<div class="py-md-5 ninja stripe">
<div class="form-group">
<input class="form-control" id="cardholder-name" type="text" placeholder="{{ ctrans('texts.name') }}">
</div>
<!-- placeholder for Elements -->
<div class="form-group">
<div id="card-element" class="form-control"></div>
</div>
<div class="form-check form-check-inline mr-1">
<input class="form-check-input" id="proxy_is_default" type="checkbox">
<label class="form-check-label" for="proxy_is_default">{{ ctrans('texts.save_as_default') }}</label>
</div>
<div id="card-errors" role="alert"></div>
<div class="form-group">
<button id="card-button" class="btn btn-primary pull-right" data-secret="{{ $intent->client_secret }}">
{{ ctrans('texts.pay_now') }}
</button>
</div>
</div>
@endif
@endsection
@push('scripts')
<script src="https://js.stripe.com/v3/"></script>
<script type="text/javascript">
var stripe = Stripe('{{ $gateway->getPublishableKey() }}');
var elements = stripe.elements();
var cardElement = elements.create('card');
cardElement.mount('#card-element');
var cardholderName = document.getElementById('cardholder-name');
var cardButton = document.getElementById('card-button');
var clientSecret = cardButton.dataset.secret;
cardButton.addEventListener('click', function(ev) {
stripe.handleCardPayment(
clientSecret, cardElement, {
payment_method_data: {
billing_details: {name: cardholderName.value}
}
}
).then(function(result) {
if (result.error) {
// Display error.message in your UI.
// console.log(result.error);
// console.log(result.error.message);
$("#card-errors").empty();
$("#card-errors").append("<b>" + result.error.message + "</b>");
$("#card-button").removeAttr("disabled");
} else {
// The setup has succeeded. Display a success message.
console.log(result);
postResult(result);
}
});
});
$("#card-button").attr("disabled", true);
$('#cardholder-name').on('input',function(e){
if($("#cardholder-name").val().length >=1)
$("#card-button").removeAttr("disabled");
else
$("#card-button").attr("disabled", true);
});
function postResult(result)
{
$("#gateway_response").val(JSON.stringify(result.setupIntent));
$("#is_default").val($('#proxy_is_default').is(":checked"));
$("#card-button").attr("disabled", true);
$('#server_response').submit();
}
</script>
@endpush
@push('css')
<style type="text/css">
.StripeElement {
box-sizing: border-box;
height: 40px;
padding: 10px 12px;
border: 1px solid transparent;
border-radius: 4px;
background-color: white;
box-shadow: 0 1px 3px 0 #e6ebf1;
-webkit-transition: box-shadow 150ms ease;
transition: box-shadow 150ms ease;
}
.StripeElement--focus {
box-shadow: 0 1px 3px 0 #cfd7df;
}
.StripeElement--invalid {
border-color: #fa755a;
}
.StripeElement--webkit-autofill {
background-color: #fefde5 !important;
}
</style>
@endpush

View File

@ -5,6 +5,19 @@
@section('body') @section('body')
<main class="main"> <main class="main">
<div class="container-fluid"> <div class="container-fluid">
{!! Former::framework('TwitterBootstrap4'); !!}
{!! Former::horizontal_open()
->id('payment_form')
->route('client.payments.process')
->method('POST'); !!}
{!! Former::hidden('hashed_ids')->id('hashed_ids')->value($hashed_ids) !!}
{!! Former::hidden('company_gateway_id')->id('company_gateway_id') !!}
{!! Former::hidden('payment_method_id')->id('payment_method_id') !!}
{!! Former::close() !!}
<div class="row" style="padding-top: 30px;"> <div class="row" style="padding-top: 30px;">
<div class="col d-flex justify-content-center"> <div class="col d-flex justify-content-center">
<div class="card w-50 p-10"> <div class="card w-50 p-10">
@ -43,21 +56,12 @@
</div> </div>
<div class="btn-group pull-right" role="group"> <div class="btn-group pull-right" role="group">
<button class="btn btn-primary dropdown-toggle" id="pay_now" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ ctrans('texts.pay_now') }}</button> <button class="btn btn-primary dropdown-toggle" id="pay_now" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">{{ ctrans('texts.pay_now') }}</button>
<div class="dropdown-menu" aria-labelledby="pay_now"> <div class="dropdown-menu" aria-labelledby="pay_now">
<a class="dropdown-item" href="#">Dropdown link</a> @foreach($payment_methods as $payment_method)
<a class="dropdown-item" href="#">Dropdown link</a> <a class="dropdown-item" onClick="paymentMethod({{ $payment_method['company_gateway_id'] }}, {{ $payment_method['gateway_type_id'] }})">{{$payment_method['label']}}</a>
</div> @endforeach
</div>
<div class="btn-group pull-right">
<button type="button" class="btn btn-primary" id="pay_now">{{ ctrans('texts.pay_now') }}</button>
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" id="pay_now_drop" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu">
<a class="dropdown-item" id="download_invoices">{{ctrans('texts.download_pdf')}}</a>
</div> </div>
</div> </div>
@ -172,6 +176,15 @@ $("#modal_pay_now_button").on('click', function(e){
}); });
function paymentMethod(company_gateway_id, payment_method_id)
{
$('#company_gateway_id').val(company_gateway_id);
$('#payment_method_id').val(payment_method_id);
$('#payment_form').submit();
}
function getSignature() function getSignature()
{ {
//check in signature is required //check in signature is required

View File

@ -24,7 +24,7 @@ Route::group(['middleware' => ['auth:contact'], 'prefix' => 'client', 'as' => 'c
Route::get('recurring_invoices', 'ClientPortal\RecurringInvoiceController@index')->name('recurring_invoices.index'); Route::get('recurring_invoices', 'ClientPortal\RecurringInvoiceController@index')->name('recurring_invoices.index');
Route::get('payments', 'ClientPortal\PaymentController@index')->name('payments.index'); Route::get('payments', 'ClientPortal\PaymentController@index')->name('payments.index');
Route::get('payments/{company_gateway_id}/{payment_method_id}', 'PaymentController@process')->name('payments.process')->middleware('signed'); Route::post('payments/process', 'ClientPortal\PaymentController@process')->name('payments.process');
Route::get('profile/{client_contact}/edit', 'ClientPortal\ProfileController@edit')->name('profile.edit'); Route::get('profile/{client_contact}/edit', 'ClientPortal\ProfileController@edit')->name('profile.edit');
Route::put('profile/{client_contact}/edit', 'ClientPortal\ProfileController@update')->name('profile.update'); Route::put('profile/{client_contact}/edit', 'ClientPortal\ProfileController@update')->name('profile.update');