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

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

View File

@ -11,26 +11,30 @@
namespace App\Http\Controllers\ClientPortal;
use Cache;
use App\Filters\PaymentFilters;
use App\Http\Controllers\Controller;
use App\Models\CompanyGateway;
use App\Models\Invoice;
use App\Models\Payment;
use App\Utils\Number;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use Cache;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Yajra\DataTables\Facades\DataTables;
use Yajra\DataTables\Html\Builder;
/**
* Class InvoiceController
* @package App\Http\Controllers\ClientPortal\InvoiceController
* Class PaymentController
* @package App\Http\Controllers\ClientPortal\PaymentController
*/
class PaymentController extends Controller
{
use MakesHash;
use MakesDates;
/**
* Show the list of Invoices
@ -84,41 +88,47 @@ class PaymentController extends Controller
* The request will also contain the amount
* 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
*/
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)
->get();
$amount = request()->input('amount');
$amount = $invoices->sum('balance');
//build a cache record to maintain state
$cache_hash = str_random(config('ninja.key_length'));
$invoices->filter(function ($invoice){
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
$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
//and add it to the invoice
$data = [
'cache_hash' => $cache_hash,
'invoices' => $invoices,
'amount' => $amount,
'fee' => $gateway->calcGatewayFee($amount),
'amount_with_fee' => ($amount + $gateway->calcGatewayFee($amount)),
'gateway' => $gateway,
'token' => auth()->user()->client->gateway_token($gateway->id),
'amount_with_fee' => $amount + $gateway->calcGatewayFee($amount),
'token' => auth()->user()->client->gateway_token($gateway->id, $payment_method_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
* 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
* @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
->whereCompanyGatewayId($gateway_id)
->wherePaymentMethod_id($payment_method_id)
return $this->gateway_tokens()
->whereCompanyGatewayId($company_gateway_id)
->whereGatewayTypeId($payment_method_id)
->first();
}
@ -264,6 +264,14 @@ class Client extends BaseModel
return null;
}
public function getCurrencyCode()
{
if ($this->currency) {
return $this->currency->code;
}
return 'USD';
}
/**
* Generates an array of payment urls per client
* for a given amount.
@ -324,11 +332,10 @@ class Client extends BaseModel
$fee_label = $gateway->calcGatewayFeeLabel($amount, $this);
$payment_urls[] = [
'label' => ctrans('texts.' . $gateway->getTypeAlias($gateway_type_id)) . $fee_label,
'url' => URL::signedRoute('client.payments.process', [
'company_gateway_id' => $gateway_id,
'gateway_type_id' => $gateway_type_id])
$payment_urls[] = [
'label' => ctrans('texts.' . $gateway->getTypeAlias($gateway_type_id)) . $fee_label,
'company_gateway_id' => $gateway_id,
'gateway_type_id' => $gateway_type_id
];
}

View File

@ -68,7 +68,7 @@ class StripePaymentDriver extends BasePaymentDriver
{
$types = [
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']))
@ -130,6 +130,7 @@ class StripePaymentDriver extends BasePaymentDriver
public function authorizeCreditCardView($data)
{
$intent['intent'] = $this->getSetupIntent();
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');
}
/**
* 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
*
* @param array $data The data array to be passed to Stripe
* @return PaymentIntent The Stripe payment intent object
*/
public function createIntent($data) :?\Stripe\PaymentIntent
public function createPaymentIntent($data) :?\Stripe\PaymentIntent
{
$this->init();
@ -258,6 +298,9 @@ class StripePaymentDriver extends BasePaymentDriver
}
if(!$customer)
throw Exception('Unable to create gateway customer');
return $customer;
}

View File

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

View File

@ -40,8 +40,6 @@
{{ ctrans('texts.save') }}
</button>
</div>
</div>
@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')
<main class="main">
<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="col d-flex justify-content-center">
<div class="card w-50 p-10">
@ -43,22 +56,13 @@
</div>
<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>
<div class="dropdown-menu" aria-labelledby="pay_now">
<a class="dropdown-item" href="#">Dropdown link</a>
<a class="dropdown-item" href="#">Dropdown link</a>
</div>
</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 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>
<div class="dropdown-menu" aria-labelledby="pay_now">
@foreach($payment_methods as $payment_method)
<a class="dropdown-item" onClick="paymentMethod({{ $payment_method['company_gateway_id'] }}, {{ $payment_method['gateway_type_id'] }})">{{$payment_method['label']}}</a>
@endforeach
</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()
{
//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('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::put('profile/{client_contact}/edit', 'ClientPortal\ProfileController@update')->name('profile.update');