mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-05-24 02:14:21 -04:00
Stripe payments
This commit is contained in:
parent
8027e72217
commit
46602a21c5
@ -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,
|
||||
];
|
||||
|
||||
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -49,8 +49,8 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@include($gateway->driver(auth()->user()->client)->viewForType($gateway_type_id))
|
||||
|
||||
@yield('pay_now')
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -40,8 +40,6 @@
|
||||
{{ ctrans('texts.save') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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');
|
||||
|
Loading…
x
Reference in New Issue
Block a user