1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-08 20:22:42 +01:00

Added support for Stripe token billing

This commit is contained in:
Hillel Coren 2015-02-18 00:22:12 +02:00
parent fbf7c235e8
commit 7f27abace2
24 changed files with 332 additions and 17 deletions

View File

@ -184,6 +184,11 @@ class AccountController extends \BaseController
}
}
$tokenBillingOptions = [];
for ($i=1; $i<=4; $i++) {
$tokenBillingOptions[$i] = trans("texts.token_billing_{$i}");
}
$data = [
'account' => $account,
'accountGateway' => $accountGateway,
@ -195,7 +200,8 @@ class AccountController extends \BaseController
->orderBy('name')
->get(),
'recommendedGateways' => $recommendedGatewayArray,
'creditCardTypes' => $creditCards,
'creditCardTypes' => $creditCards,
'tokenBillingOptions' => $tokenBillingOptions,
];
return View::make('accounts.payments', $data);
@ -663,10 +669,22 @@ class AccountController extends \BaseController
}
}
if ($isMasked && count($account->account_gateways)) {
// check if a gateway is already configured
$currentGateway = false;
if (count($account->account_gateways)) {
$currentGateway = $account->account_gateways[0];
}
// if the values haven't changed don't update the config
if ($isMasked && $currentGateway) {
$currentGateway->accepted_credit_cards = $cardCount;
$currentGateway->save();
// if there's an existing config for this gateway update it
} elseif (!$isMasked && $currentGateway && $currentGateway->gateway_id == $gatewayId) {
$currentGateway->accepted_credit_cards = $cardCount;
$currentGateway->config = json_encode($config);
$currentGateway->save();
// otherwise, create a new gateway config
} else {
$accountGateway->config = json_encode($config);
$accountGateway->accepted_credit_cards = $cardCount;
@ -675,6 +693,11 @@ class AccountController extends \BaseController
$account->account_gateways()->save($accountGateway);
}
if (Input::get('token_billing_type_id')) {
$account->token_billing_type_id = Input::get('token_billing_type_id');
$account->save();
}
Session::flash('message', trans('texts.updated_settings'));
} else {
Session::flash('error', trans('validation.required', ['attribute' => 'gateway']));

View File

@ -106,6 +106,7 @@ class ClientController extends \BaseController
'credit' => $client->getTotalCredit(),
'title' => trans('texts.view_client'),
'hasRecurringInvoices' => Invoice::scope()->where('is_recurring', '=', true)->whereClientId($client->id)->count() > 0,
'gatewayLink' => $client->getGatewayLink(),
);
return View::make('clients.show', $data);

View File

@ -177,6 +177,7 @@ class InvoiceController extends \BaseController
'invitation' => $invitation,
'invoiceLabels' => $client->account->getInvoiceLabels(),
'contact' => $contact,
'hasToken' => $client->getGatewayToken()
);
return View::make('invoices.view', $data);

View File

@ -287,14 +287,16 @@ class PaymentController extends \BaseController
public function show_payment($invitationKey)
{
// Handle token billing
if (Input::get('use_token') == 'true') {
return self::do_payment($invitationKey, false, true);
}
// For PayPal Express we redirect straight to their site
$invitation = Invitation::with('invoice.client.account', 'invoice.client.account.account_gateways.gateway')->where('invitation_key', '=', $invitationKey)->firstOrFail();
$account = $invitation->invoice->client->account;
if ($account->isGatewayConfigured(GATEWAY_PAYPAL_EXPRESS)) {
if (Session::has('error')) {
Session::reflash();
return Redirect::to('view/'.$invitationKey);
} else {
return self::do_payment($invitationKey, false);
@ -321,6 +323,7 @@ class PaymentController extends \BaseController
'acceptedCreditCardTypes' => $acceptedCreditCardTypes,
'countries' => Country::remember(DEFAULT_QUERY_CACHE)->orderBy('name')->get(),
'currencyId' => $client->currency_id,
'account' => $client->account
];
return View::make('payments.payment', $data);
@ -486,7 +489,7 @@ class PaymentController extends \BaseController
}
}
public function do_payment($invitationKey, $onSite = true)
public function do_payment($invitationKey, $onSite = true, $useToken = false)
{
$rules = array(
'first_name' => 'required',
@ -512,11 +515,12 @@ class PaymentController extends \BaseController
$invitation = Invitation::with('invoice.invoice_items', 'invoice.client.currency', 'invoice.client.account.account_gateways.gateway')->where('invitation_key', '=', $invitationKey)->firstOrFail();
$invoice = $invitation->invoice;
$accountGateway = $invoice->client->account->account_gateways[0];
$client = $invoice->client;
$account = $client->account;
$accountGateway = $account->account_gateways[0];
$paymentLibrary = $accountGateway->gateway->paymentlibrary;
if ($onSite) {
$client = $invoice->client;
$client->address1 = trim(Input::get('address1'));
$client->address2 = trim(Input::get('address2'));
$client->city = trim(Input::get('city'));
@ -528,7 +532,32 @@ class PaymentController extends \BaseController
try {
if ($paymentLibrary->id == PAYMENT_LIBRARY_OMNIPAY) {
$gateway = self::createGateway($accountGateway);
$details = self::getPaymentDetails($invoice, Input::all());
$details = self::getPaymentDetails($invoice, $useToken ? false : Input::all());
if ($accountGateway->gateway_id == GATEWAY_STRIPE) {
if ($useToken) {
$details['cardReference'] = $client->getGatewayToken();
} elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing')) {
$tokenResponse = $gateway->createCard($details)->send();
$cardReference = $tokenResponse->getCardReference();
$details['cardReference'] = $cardReference;
$token = AccountGatewayToken::where('client_id', '=', $client->id)
->where('account_gateway_id', '=', $accountGateway->id)->first();
if (!$token) {
$token = new AccountGatewayToken();
$token->account_id = $account->id;
$token->contact_id = $invitation->contact_id;
$token->account_gateway_id = $accountGateway->id;
$token->client_id = $client->id;
}
$token->token = $cardReference;
$token->save();
}
}
$response = $gateway->purchase($details)->send();
$ref = $response->getTransactionReference();
@ -541,7 +570,6 @@ class PaymentController extends \BaseController
if ($response->isSuccessful()) {
$payment = self::createPayment($invitation, $ref);
Session::flash('message', trans('texts.applied_payment'));
return Redirect::to('view/'.$payment->invitation->invitation_key);
@ -693,12 +721,14 @@ class PaymentController extends \BaseController
->withInput();
} else {
$this->paymentRepo->save($publicId, Input::all());
if ($publicId) {
Session::flash('message', trans('texts.updated_payment'));
return Redirect::to('payments/');
} else {
Session::flash('message', trans('texts.created_payment'));
return Redirect::to('clients/'.Input::get('client'));
}
}

View File

@ -0,0 +1,56 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class SupportTokenBilling extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('accounts', function($table)
{
$table->smallInteger('token_billing_type_id')->default(TOKEN_BILLING_OPT_IN);
});
Schema::create('account_gateway_tokens', function($table)
{
$table->increments('id');
$table->unsignedInteger('account_id');
$table->unsignedInteger('contact_id');
$table->unsignedInteger('account_gateway_id');
$table->unsignedInteger('client_id');
$table->string('token');
$table->timestamps();
$table->softDeletes();
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
$table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade');
$table->foreign('account_gateway_id')->references('id')->on('account_gateways')->onDelete('cascade');
$table->foreign('client_id')->references('id')->on('clients')->onDelete('cascade');
});
DB::table('accounts')->update(['token_billing_type_id' => TOKEN_BILLING_OPT_IN]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('accounts', function($table)
{
$table->dropColumn('token_billing_type_id');
});
Schema::drop('account_gateway_tokens');
}
}

View File

@ -512,4 +512,17 @@ return array(
'reset_all' => 'Reset All',
'approve' => 'Approve',
'token_billing_type_id' => 'Token Billing',
'token_billing_help' => 'Enables you to store credit cards with your gateway, and charge them at a later date.',
'token_billing_1' => 'Disabled',
'token_billing_2' => 'Opt-in - checkbox is shown but not selected',
'token_billing_3' => 'Opt-out - checkbox is shown and selected',
'token_billing_4' => 'Always',
'token_billing_checkbox' => 'Store credit card details',
'view_in_stripe' => 'View in Stripe',
'use_card_on_file' => 'Use card on file',
'edit_payment_details' => 'Edit payment details',
'token_billing' => 'Save card details',
'token_billing_secure' => 'All data is stored securely by :stripe_link',
);

View File

@ -500,6 +500,19 @@ return array(
'payment_email' => 'Zahlungsmail',
'quote_email' => 'Angebotsmail',
'reset_all' => 'Alle zurücksetzen',
'approve' => 'Approve',
'token_billing_type_id' => 'Token Billing',
'token_billing_help' => 'Enables you to store credit cards with your gateway, and charge them at a later date.',
'token_billing_1' => 'Disabled',
'token_billing_2' => 'Opt-in - checkbox is shown but not selected',
'token_billing_3' => 'Opt-out - checkbox is shown and selected',
'token_billing_4' => 'Always',
'token_billing_checkbox' => 'Store credit card details',
'view_in_stripe' => 'View in Stripe',
'use_card_on_file' => 'Use card on file',
'edit_payment_details' => 'Edit payment details',
'token_billing' => 'Save card details',
'token_billing_secure' => 'All data is stored securely by :stripe_link',
);

View File

@ -510,4 +510,18 @@ return array(
'reset_all' => 'Reset All',
'approve' => 'Approve',
'token_billing_type_id' => 'Token Billing',
'token_billing_help' => 'Enables you to store credit cards with your gateway, and charge them at a later date.',
'token_billing_1' => 'Disabled',
'token_billing_2' => 'Opt-in - checkbox is shown but not selected',
'token_billing_3' => 'Opt-out - checkbox is shown and selected',
'token_billing_4' => 'Always',
'token_billing_checkbox' => 'Store credit card details',
'view_in_stripe' => 'View in Stripe',
'use_card_on_file' => 'Use card on file',
'edit_payment_details' => 'Edit payment details',
'token_billing' => 'Save card details',
'token_billing_secure' => 'All data is stored securely by :stripe_link',
);

View File

@ -482,4 +482,17 @@ return array(
'reset_all' => 'Reset All',
'approve' => 'Approve',
'token_billing_type_id' => 'Token Billing',
'token_billing_help' => 'Enables you to store credit cards with your gateway, and charge them at a later date.',
'token_billing_1' => 'Disabled',
'token_billing_2' => 'Opt-in - checkbox is shown but not selected',
'token_billing_3' => 'Opt-out - checkbox is shown and selected',
'token_billing_4' => 'Always',
'token_billing_checkbox' => 'Store credit card details',
'view_in_stripe' => 'View in Stripe',
'use_card_on_file' => 'Use card on file',
'edit_payment_details' => 'Edit payment details',
'token_billing' => 'Save card details',
'token_billing_secure' => 'All data is stored securely by :stripe_link',
);

View File

@ -502,5 +502,18 @@ return array(
'quote_email' => 'Quote Email',
'reset_all' => 'Reset All',
'approve' => 'Approve',
'token_billing_type_id' => 'Token Billing',
'token_billing_help' => 'Enables you to store credit cards with your gateway, and charge them at a later date.',
'token_billing_1' => 'Disabled',
'token_billing_2' => 'Opt-in - checkbox is shown but not selected',
'token_billing_3' => 'Opt-out - checkbox is shown and selected',
'token_billing_4' => 'Always',
'token_billing_checkbox' => 'Store credit card details',
'view_in_stripe' => 'View in Stripe',
'use_card_on_file' => 'Use card on file',
'edit_payment_details' => 'Edit payment details',
'token_billing' => 'Save card details',
'token_billing_secure' => 'All data is stored securely by :stripe_link',
);

View File

@ -504,5 +504,18 @@ return array(
'quote_email' => 'Quote Email',
'reset_all' => 'Reset All',
'approve' => 'Approve',
'token_billing_type_id' => 'Token Billing',
'token_billing_help' => 'Enables you to store credit cards with your gateway, and charge them at a later date.',
'token_billing_1' => 'Disabled',
'token_billing_2' => 'Opt-in - checkbox is shown but not selected',
'token_billing_3' => 'Opt-out - checkbox is shown and selected',
'token_billing_4' => 'Always',
'token_billing_checkbox' => 'Store credit card details',
'view_in_stripe' => 'View in Stripe',
'use_card_on_file' => 'Use card on file',
'edit_payment_details' => 'Edit payment details',
'token_billing' => 'Save card details',
'token_billing_secure' => 'All data is stored securely by :stripe_link',
);

View File

@ -512,6 +512,19 @@ return array(
'quote_email' => 'Quote Email',
'reset_all' => 'Reset All',
'approve' => 'Approve',
'token_billing_type_id' => 'Token Billing',
'token_billing_help' => 'Enables you to store credit cards with your gateway, and charge them at a later date.',
'token_billing_1' => 'Disabled',
'token_billing_2' => 'Opt-in - checkbox is shown but not selected',
'token_billing_3' => 'Opt-out - checkbox is shown and selected',
'token_billing_4' => 'Always',
'token_billing_checkbox' => 'Store credit card details',
'view_in_stripe' => 'View in Stripe',
'use_card_on_file' => 'Use card on file',
'edit_payment_details' => 'Edit payment details',
'token_billing' => 'Save card details',
'token_billing_secure' => 'All data is stored securely by :stripe_link',
);

View File

@ -511,5 +511,18 @@ return array(
'reset_all' => 'Reset All',
'approve' => 'Approve',
'token_billing_type_id' => 'Token Billing',
'token_billing_help' => 'Enables you to store credit cards with your gateway, and charge them at a later date.',
'token_billing_1' => 'Disabled',
'token_billing_2' => 'Opt-in - checkbox is shown but not selected',
'token_billing_3' => 'Opt-out - checkbox is shown and selected',
'token_billing_4' => 'Always',
'token_billing_checkbox' => 'Store credit card details',
'view_in_stripe' => 'View in Stripe',
'use_card_on_file' => 'Use card on file',
'edit_payment_details' => 'Edit payment details',
'token_billing' => 'Save card details',
'token_billing_secure' => 'All data is stored securely by :stripe_link',
);

View File

@ -505,6 +505,19 @@ return array(
'quote_email' => 'Quote Email',
'reset_all' => 'Reset All',
'approve' => 'Approve',
'token_billing_type_id' => 'Token Billing',
'token_billing_help' => 'Enables you to store credit cards with your gateway, and charge them at a later date.',
'token_billing_1' => 'Disabled',
'token_billing_2' => 'Opt-in - checkbox is shown but not selected',
'token_billing_3' => 'Opt-out - checkbox is shown and selected',
'token_billing_4' => 'Always',
'token_billing_checkbox' => 'Store credit card details',
'view_in_stripe' => 'View in Stripe',
'use_card_on_file' => 'Use card on file',
'edit_payment_details' => 'Edit payment details',
'token_billing' => 'Save card details',
'token_billing_secure' => 'All data is stored securely by :stripe_link',
);

View File

@ -492,6 +492,19 @@ return array(
'quote_email' => 'Quote Email',
'reset_all' => 'Reset All',
'approve' => 'Approve',
'token_billing_type_id' => 'Token Billing',
'token_billing_help' => 'Enables you to store credit cards with your gateway, and charge them at a later date.',
'token_billing_1' => 'Disabled',
'token_billing_2' => 'Opt-in - checkbox is shown but not selected',
'token_billing_3' => 'Opt-out - checkbox is shown and selected',
'token_billing_4' => 'Always',
'token_billing_checkbox' => 'Store credit card details',
'view_in_stripe' => 'View in Stripe',
'use_card_on_file' => 'Use card on file',
'edit_payment_details' => 'Edit payment details',
'token_billing' => 'Save card details',
'token_billing_secure' => 'All data is stored securely by :stripe_link',
);

View File

@ -334,4 +334,15 @@ class Account extends Eloquent
return "<p>" . trans('texts.email_signature') . "<br>\$account</p>";
}
}
public function showTokenCheckbox()
{
return $this->token_billing_type_id == TOKEN_BILLING_OPT_IN
|| $this->token_billing_type_id == TOKEN_BILLING_OPT_OUT;
}
public function selectTokenCheckbox()
{
return $this->token_billing_type_id == TOKEN_BILLING_OPT_OUT;
}
}

View File

@ -0,0 +1,7 @@
<?php
class AccountGatewayToken extends Eloquent
{
protected $softDelete = true;
public $timestamps = true;
}

View File

@ -219,6 +219,33 @@ class Client extends EntityModel
return $this->created_at->format('m/d/y h:i a');
}
}
public function getGatewayToken()
{
$this->account->load('account_gateways');
if (!count($this->account->account_gateways)) {
return false;
}
$accountGateway = $this->account->account_gateways[0];
if ($accountGateway->gateway_id != GATEWAY_STRIPE) {
return false;
}
$token = AccountGatewayToken::where('client_id', '=', $this->id)
->where('account_gateway_id', '=', $accountGateway->id)->first();
return $token ? $token->token : false;
}
public function getGatewayLink()
{
$token = $this->getGatewayToken();
return $token ? "https://dashboard.stripe.com/customers/{$token}" : false;
}
}
/*

View File

@ -318,6 +318,11 @@ define('USER_TYPE_SELF_HOST', 'SELF_HOST');
define('USER_TYPE_CLOUD_HOST', 'CLOUD_HOST');
define('NEW_VERSION_AVAILABLE', 'NEW_VERSION_AVAILABLE');
define('TOKEN_BILLING_DISABLED', 1);
define('TOKEN_BILLING_OPT_IN', 2);
define('TOKEN_BILLING_OPT_OUT', 3);
define('TOKEN_BILLING_ALWAYS', 4);
/*
define('GATEWAY_AMAZON', 30);
define('GATEWAY_BLUEPAY', 31);

View File

@ -79,14 +79,18 @@
@endforeach
@if($gateway->getHelp())
@if ($gateway->getHelp())
<div class="form-group">
<label class="control-label col-lg-4 col-sm-4"></label>
<div class="col-lg-8 col-sm-8">
<div class="col-lg-8 col-sm-8 help-block">
{{ $gateway->getHelp() }}
</div>
</div>
@endif
@if ($gateway->id == GATEWAY_STRIPE)
{{ Former::select('token_billing_type_id')->options($tokenBillingOptions)->help(trans('texts.token_billing_help')) }}
@endif
</div>
@endforeach

View File

@ -10,6 +10,10 @@
{{ Former::text('id')->value($client->public_id) }}
</div>
@if ($gatewayLink)
{{ Button::link($gatewayLink, trans('texts.view_in_stripe'), ['target' => '_blank']) }}
@endif
@if ($client->trashed())
{{ Button::primary(trans('texts.restore_client'), ['onclick' => 'onRestoreClick()']) }}
@else

View File

@ -81,7 +81,7 @@
var needsRefresh = false;
function refreshPDF() {
PDFJS.disableWorker = true;
PDFJS.workerSrc = '{{ asset('js/pdf_viewer.worker.js') }}';
if ({{ Auth::check() && Auth::user()->force_pdfjs ? 'false' : 'true' }} && (isFirefox || (isChrome && !isChromium))) {
var string = getPDFString();
if (!string) return;

View File

@ -30,7 +30,14 @@
@endif
@elseif ($invoice->client->account->isGatewayConfigured() && !$invoice->isPaid() && !$invoice->is_recurring)
{{ Button::normal(trans('texts.download_pdf'), array('onclick' => 'onDownloadClick()', 'class' => 'btn-lg')) }}&nbsp;&nbsp;
{{ Button::success_link(URL::to('payment/' . $invitation->invitation_key), trans('texts.pay_now'), array('class' => 'btn-lg')) }}
@if ($hasToken)
{{ DropdownButton::success_lg(trans('texts.pay_now'), [
['url' => URL::to("payment/{$invitation->invitation_key}?use_token=true"), 'label' => trans('texts.use_card_on_file')],
['url' => URL::to('payment/' . $invitation->invitation_key), 'label' => trans('texts.edit_payment_details')]
])->addClass('btn-lg') }}
@else
{{ Button::success_link(URL::to('payment/' . $invitation->invitation_key), trans('texts.pay_now'), array('class' => 'btn-lg')) }}
@endif
@else
{{ Button::success('Download PDF', array('onclick' => 'onDownloadClick()', 'class' => 'btn-lg')) }}
@endif

View File

@ -196,7 +196,15 @@
</div>
</div>
@if(isset($acceptedCreditCardTypes))
@if ($account->showTokenCheckbox())
<div class="form-group">
<input id="token_billing" type="checkbox" name="token_billing" {{ $account->selectTokenCheckbox() ? 'CHECKED' : '' }} value="1" style="margin-left:0px; vertical-align:text-top">
<label for="token_billing" class="checkbox" style="display: inline">{{ trans('texts.token_billing') }}</label>
<span class="help-block">{{ trans('texts.token_billing_secure', ['stripe_link' => link_to('https://stripe.com/', 'Stripe.com', ['target' => '_blank'])]) }}</span>
</div>
@endif
@if (isset($acceptedCreditCardTypes))
<div class="row">
<div class="form-group col-md-12">
@foreach ($acceptedCreditCardTypes as $card)