1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-09 20:52:56 +01:00

Store/display credit card type, expiration and last 4 digits

This commit is contained in:
Joshua Dwire 2016-04-23 22:10:51 -04:00
parent b8170f0324
commit 42c1f537bc
31 changed files with 245 additions and 31 deletions

View File

@ -55,6 +55,7 @@ class PaymentController extends BaseController
'client',
'transaction_reference',
'method',
'source',
'payment_amount',
'payment_date',
'status',
@ -439,10 +440,12 @@ class PaymentController extends BaseController
if ($useToken) {
$details['customerReference'] = $client->getGatewayToken();
unset($details['token']);
} elseif ($account->token_billing_type_id == TOKEN_BILLING_ALWAYS || Input::get('token_billing')) {
$token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id);
$token = $this->paymentService->createToken($gateway, $details, $accountGateway, $client, $invitation->contact_id, $customerReference);
if ($token) {
$details['customerReference'] = $token;
$details['token'] = $token;
$details['customerReference'] = $customerReference;
} else {
$this->error('Token-No-Ref', $this->paymentService->lastError, $accountGateway);
return Redirect::to('payment/'.$invitationKey)->withInput(Request::except('cvv'));
@ -479,7 +482,49 @@ class PaymentController extends BaseController
}
if ($response->isSuccessful()) {
$payment = $this->paymentService->createPayment($invitation, $accountGateway, $ref);
$last4 = null;
$expiration = null;
$card_type_id = null;
if (!empty($details['card'])) {
$card = $details['card'];
$last4 = substr($card->number, -4);
$year = $card->expiryYear;
if (strlen($year) == 2) {
$year = '20' . $year;
}
$expiration = $year . '-' . $card->expiryMonth . '-00';
$card_type_id = $this->detectCardType($card->number);
}
if ($accountGateway->gateway_id == GATEWAY_STRIPE) {
$card = $response->getSource();
if (!$card) {
$card = $response->getCard();
}
if ($card) {
$last4 = $card['last4'];
$expiration = $card['exp_year'] . '-' . $card['exp_month'] . '-00';
$stripe_card_types = array(
'Visa' => CARD_VISA,
'American Express' => CARD_AMERICAN_EXPRESS,
'MasterCard' => CARD_MASTERCARD,
'Discover' => CARD_DISCOVER,
'JCB' => CARD_JCB,
'Diners Club' => CARD_DINERS_CLUB
);
if (!empty($stripe_card_types[$card['brand']])) {
$card_type_id = $stripe_card_types[$card['brand']];
} else {
$card_type_id = CARD_UNKNOWN;
}
}
}
$payment = $this->paymentService->createPayment($invitation, $accountGateway, $ref, null, $last4, $expiration, $card_type_id);
Session::flash('message', trans('texts.applied_payment'));
if ($account->account_key == NINJA_ACCOUNT_KEY) {
@ -513,6 +558,24 @@ class PaymentController extends BaseController
}
}
private function detectCardType($number)
{
if (preg_match('/^3[47][0-9]{13}$/',$number)) {
return CARD_AMERICAN_EXPRESS;
} elseif (preg_match('/^3(?:0[0-5]|[68][0-9])[0-9]{11}$/',$number)) {
return CARD_DINERS_CLUB;
} elseif (preg_match('/^6(?:011|5[0-9][0-9])[0-9]{12}$/',$number)) {
return CARD_DISCOVER;
} elseif (preg_match('/^(?:2131|1800|35\d{3})\d{11}$/',$number)) {
return CARD_JCB;
} elseif (preg_match('/^5[1-5][0-9]{14}$/',$number)) {
return CARD_MASTERCARD;
} elseif (preg_match('/^4[0-9]{12}(?:[0-9]{3})?$/',$number)) {
return CARD_VISA;
}
return CARD_UNKNOWN;
}
public function offsite_payment()
{
$payerId = Request::query('PayerID');

View File

@ -324,7 +324,7 @@ class PublicClientController extends BaseController
'clientFontUrl' => $account->getFontsUrl(),
'entityType' => ENTITY_PAYMENT,
'title' => trans('texts.payments'),
'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date', 'status'])
'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'source', 'payment_amount', 'payment_date', 'status'])
];
return response()->view('public_list', $data);
@ -341,9 +341,16 @@ class PublicClientController extends BaseController
->addColumn('invoice_number', function ($model) { return $model->invitation_key ? link_to('/view/'.$model->invitation_key, $model->invoice_number)->toHtml() : $model->invoice_number; })
->addColumn('transaction_reference', function ($model) { return $model->transaction_reference ? $model->transaction_reference : '<i>Manual entry</i>'; })
->addColumn('payment_type', function ($model) { return $model->payment_type ? $model->payment_type : ($model->account_gateway_id ? '<i>Online payment</i>' : ''); })
->addColumn('payment_source', function ($model) {
if (!$model->card_type_code) return '';
$card_type = trans("texts.card_" . $model->card_type_code);
$expiration = trans('texts.card_expiration', array('expires'=>Utils::fromSqlDate($model->expiration, false)->format('m/d')));
return '<img height="22" src="'.URL::to('/images/credit_cards/'.$model->card_type_code.'.png').'" alt="'.htmlentities($card_type).'">&nbsp; &bull;&bull;&bull;'.$model->last4.' '.$expiration;
})
->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); })
->addColumn('payment_date', function ($model) { return Utils::dateToString($model->payment_date); })
->addColumn('status', function ($model) { return $this->getPaymentStatusLabel($model); })
->orderColumns( 'invoice_number', 'transaction_reference', 'payment_type', 'amount', 'payment_date')
->make();
}

View File

@ -482,6 +482,20 @@ if (!defined('CONTACT_EMAIL')) {
define('PAYMENT_STATUS_PARTIALLY_REFUNDED', 4);
define('PAYMENT_STATUS_REFUNDED', 5);
define('CARD_UNKNOWN', 0);
define('CARD_AMERICAN_EXPRESS', 1);
define('CARD_CARTE_BLANCHE', 2);
define('CARD_UNIONPAY', 3);
define('CARD_DINERS_CLUB', 4);
define('CARD_DISCOVER', 5);
define('CARD_JCB', 6);
define('CARD_LASER', 7);
define('CARD_MAISTRO', 8);
define('CARD_MASTERCARD', 9);
define('CARD_SOLO', 10);
define('CARD_SWITCH', 11);
define('CARD_VISA', 12);
define('PAYMENT_TYPE_CREDIT', 1);
define('CUSTOM_DESIGN', 11);

8
app/Models/CardType.php Normal file
View File

@ -0,0 +1,8 @@
<?php namespace App\Models;
use Eloquent;
class CardType extends Eloquent
{
public $timestamps = false;
}

View File

@ -59,6 +59,11 @@ class Payment extends EntityModel
return $this->belongsTo('App\Models\PaymentStatus');
}
public function card_type()
{
return $this->belongsTo('App\Models\CardTypes');
}
public function getRoute()
{
return "/payments/{$this->public_id}/edit";

View File

@ -23,6 +23,7 @@ class PaymentRepository extends BaseRepository
->join('invoices', 'invoices.id', '=', 'payments.invoice_id')
->join('contacts', 'contacts.client_id', '=', 'clients.id')
->join('payment_statuses', 'payment_statuses.id', '=', 'payments.payment_status_id')
->join('card_types', 'card_types.id', '=', 'payments.card_type_id')
->leftJoin('payment_types', 'payment_types.id', '=', 'payments.payment_type_id')
->leftJoin('account_gateways', 'account_gateways.id', '=', 'payments.account_gateway_id')
->leftJoin('gateways', 'gateways.id', '=', 'account_gateways.gateway_id')
@ -54,10 +55,13 @@ class PaymentRepository extends BaseRepository
'payments.is_deleted',
'payments.user_id',
'payments.refunded',
'payments.expiration',
'payments.last4',
'invoices.is_deleted as invoice_is_deleted',
'gateways.name as gateway_name',
'gateways.id as gateway_id',
'payment_statuses.name as payment_status_name'
'payment_statuses.name as payment_status_name',
'card_types.code as card_type_code'
);
if (!\Session::get('show_trash:payment')) {
@ -85,6 +89,7 @@ class PaymentRepository extends BaseRepository
->join('invoices', 'invoices.id', '=', 'payments.invoice_id')
->join('contacts', 'contacts.client_id', '=', 'clients.id')
->join('payment_statuses', 'payment_statuses.id', '=', 'payments.payment_status_id')
->join('card_types', 'card_types.id', '=', 'payments.card_type_id')
->leftJoin('invitations', function ($join) {
$join->on('invitations.invoice_id', '=', 'invoices.id')
->on('invitations.contact_id', '=', 'contacts.id');
@ -113,8 +118,11 @@ class PaymentRepository extends BaseRepository
'payment_types.name as payment_type',
'payments.account_gateway_id',
'payments.refunded',
'payments.expiration',
'payments.last4',
'payments.payment_status_id',
'payment_statuses.name as payment_status_name'
'payment_statuses.name as payment_status_name',
'card_types.code as card_type_code'
);
if ($filter) {

View File

@ -29,12 +29,12 @@ class BaseService
return count($entities);
}
public function createDatatable($entityType, $query, $showCheckbox = true, $hideClient = false)
public function createDatatable($entityType, $query, $showCheckbox = true, $hideClient = false, $orderColumns = [])
{
$columns = $this->getDatatableColumns($entityType, !$showCheckbox);
$actions = $this->getDatatableActions($entityType);
return $this->datatableService->createDatatable($entityType, $query, $columns, $actions, $showCheckbox);
return $this->datatableService->createDatatable($entityType, $query, $columns, $actions, $showCheckbox, $orderColumns);
}
protected function getDatatableColumns($entityType, $hideClient)

View File

@ -7,10 +7,10 @@ use Auth;
class DatatableService
{
public function createDatatable($entityType, $query, $columns, $actions = null, $showCheckbox = true)
public function createDatatable($entityType, $query, $columns, $actions = null, $showCheckbox = true, $orderColumns = [])
{
$table = Datatable::query($query);
$orderColumns = [];
$calculateOrderColumns = empty($orderColumns);
if ($actions && $showCheckbox) {
$table->addColumn('checkbox', function ($model) {
@ -31,9 +31,11 @@ class DatatableService
if ($visible) {
$table->addColumn($field, $value);
if ($calculateOrderColumns) {
$orderColumns[] = $field;
}
}
}
if ($actions) {
$this->createDropdown($entityType, $table, $actions);

View File

@ -75,7 +75,7 @@ class PaymentService extends BaseService
$data = $this->createDataForClient($invitation);
}
$card = new CreditCard($data);
$card = !empty($data['number']) ? new CreditCard($data) : null;
$data = [
'amount' => $invoice->getRequestedAmount(),
'card' => $card,
@ -155,12 +155,21 @@ class PaymentService extends BaseService
];
}
public function createToken($gateway, $details, $accountGateway, $client, $contactId)
public function createToken($gateway, $details, $accountGateway, $client, $contactId, &$customerReference = null)
{
$tokenResponse = $gateway->createCard($details)->send();
$cardReference = $tokenResponse->getCustomerReference();
$cardReference = $tokenResponse->getCardReference();
$customerReference = $tokenResponse->getCustomerReference();
if ($cardReference) {
if ($customerReference == $cardReference) {
// This customer was just created; find the card
$data = $tokenResponse->getData();
if(!empty($data['default_source'])) {
$cardReference = $data['default_source'];
}
}
if ($customerReference) {
$token = AccountGatewayToken::where('client_id', '=', $client->id)
->where('account_gateway_id', '=', $accountGateway->id)->first();
@ -172,7 +181,7 @@ class PaymentService extends BaseService
$token->client_id = $client->id;
}
$token->token = $cardReference;
$token->token = $customerReference;
$token->save();
} else {
$this->lastError = $tokenResponse->getMessage();
@ -205,7 +214,7 @@ class PaymentService extends BaseService
return $token;
}
public function createPayment($invitation, $accountGateway, $ref, $payerId = null)
public function createPayment($invitation, $accountGateway, $ref, $payerId = null, $last4 = null, $expiration = null, $card_type_id = null, $routing_number = null)
{
$invoice = $invitation->invoice;
@ -218,6 +227,10 @@ class PaymentService extends BaseService
$payment->contact_id = $invitation->contact_id;
$payment->transaction_reference = $ref;
$payment->payment_date = date_create()->format('Y-m-d');
$payment->last4 = $last4;
$payment->expiration = $expiration;
$payment->card_type_id = $card_type_id;
$payment->routing_number = $routing_number;
if ($payerId) {
$payment->payer_id = $payerId;
@ -329,7 +342,8 @@ class PaymentService extends BaseService
$query->where('payments.user_id', '=', Auth::user()->id);
}
return $this->createDatatable(ENTITY_PAYMENT, $query, !$clientPublicId);
return $this->createDatatable(ENTITY_PAYMENT, $query, !$clientPublicId, false,
['invoice_number', 'transaction_reference', 'payment_type', 'amount', 'payment_date']);
}
protected function getDatatableColumns($entityType, $hideClient)
@ -368,6 +382,15 @@ class PaymentService extends BaseService
return $model->payment_type ? $model->payment_type : ($model->account_gateway_id ? $model->gateway_name : '');
}
],
[
'source',
function ($model) {
if (!$model->card_type_code) return '';
$card_type = trans("texts.card_" . $model->card_type_code);
$expiration = trans('texts.card_expiration', array('expires'=>Utils::fromSqlDate($model->expiration, false)->format('m/d')));
return '<img height="22" src="'.URL::to('/images/credit_cards/'.$model->card_type_code.'.png').'" alt="'.htmlentities($card_type).'">&nbsp; &bull;&bull;&bull;'.$model->last4.' '.$expiration;
}
],
[
'amount',
function ($model) {

View File

@ -17,7 +17,7 @@
"omnipay/mollie": "dev-master#22956c1a62a9662afa5f5d119723b413770ac525",
"omnipay/2checkout": "dev-master#e9c079c2dde0d7ba461903b3b7bd5caf6dee1248",
"omnipay/gocardless": "dev-master",
"omnipay/stripe": "2.3.0",
"omnipay/stripe": "2.3.2",
"laravel/framework": "5.2.*",
"laravelcollective/html": "5.2.*",
"laravelcollective/bus": "5.2.*",

18
composer.lock generated
View File

@ -4,8 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "cf82d2ddb25cb1a7d6b4867bcc8692b8",
"content-hash": "481a95753b873249aebceb99e7426421",
"hash": "607b25b09aee82ac769bd942fac91ba6",
"content-hash": "122f3f59beb2286159145d219dbcbc90",
"packages": [
{
"name": "agmscode/omnipay-agms",
@ -127,7 +127,7 @@
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/formers/former/zipball/78ae8c65b1f8134e2db1c9491c251c03638823ca",
"url": "https://api.github.com/repos/formers/former/zipball/37f6876a5d211427b5c445cd64f0eb637f42f685",
"reference": "d97f907741323b390f43954a90a227921ecc6b96",
"shasum": ""
},
@ -699,7 +699,7 @@
"laravel"
],
"abandoned": "OpenSkill/Datatable",
"time": "2015-04-29 07:00:36"
"time": "2015-11-23 21:33:41"
},
{
"name": "classpreloader/classpreloader",
@ -5661,16 +5661,16 @@
},
{
"name": "omnipay/stripe",
"version": "v2.3.0",
"version": "v2.3.2",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/omnipay-stripe.git",
"reference": "54b816a5e95e34c988d71fb805b0232cfd7c1ce5"
"reference": "fe05ddd73d9ae38ca026dbc270ca98aaf3f99da4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/omnipay-stripe/zipball/54b816a5e95e34c988d71fb805b0232cfd7c1ce5",
"reference": "54b816a5e95e34c988d71fb805b0232cfd7c1ce5",
"url": "https://api.github.com/repos/thephpleague/omnipay-stripe/zipball/fe05ddd73d9ae38ca026dbc270ca98aaf3f99da4",
"reference": "fe05ddd73d9ae38ca026dbc270ca98aaf3f99da4",
"shasum": ""
},
"require": {
@ -5714,7 +5714,7 @@
"payment",
"stripe"
],
"time": "2015-11-10 16:17:35"
"time": "2016-03-19 08:35:06"
},
{
"name": "omnipay/targetpay",

View File

@ -13,6 +13,7 @@ class PaymentsChanges extends Migration
public function up()
{
Schema::dropIfExists('payment_statuses');
Schema::dropIfExists('card_types');
Schema::create('payment_statuses', function($table)
{
@ -20,13 +21,27 @@ class PaymentsChanges extends Migration
$table->string('name');
});
Schema::create('card_types', function($table)
{
$table->increments('id');
$table->string('name');
$table->string('code');
});
(new \PaymentStatusSeeder())->run();
(new \CardTypesSeeder())->run();
Schema::table('payments', function($table)
{
$table->decimal('refunded', 13, 2);
$table->unsignedInteger('payment_status_id')->default(3);
$table->foreign('payment_status_id')->references('id')->on('payment_statuses');
$table->unsignedInteger('routing_number')->nullable();
$table->smallInteger('last4')->unsigned()->nullable();
$table->date('expiration')->nullable();
$table->unsignedInteger('card_type_id')->nullable();
$table->foreign('card_type_id')->references('id')->on('card_types');
});
}
@ -42,8 +57,15 @@ class PaymentsChanges extends Migration
$table->dropColumn('refunded');
$table->dropForeign('payments_payment_status_id_foreign');
$table->dropColumn('payment_status_id');
$table->dropColumn('routing_number');
$table->dropColumn('last4');
$table->dropColumn('expiration');
$table->dropForeign('card_type_id_foreign');
$table->dropColumn('card_type_id');
});
Schema::dropIfExists('payment_statuses');
Schema::dropIfExists('card_types');
}
}

View File

@ -0,0 +1,44 @@
<?php
use App\Models\CardType;
class CardTypesSeeder extends Seeder
{
public function run()
{
Eloquent::unguard();
$this->createPaymentSourceTypes();
Eloquent::reguard();
}
private function createPaymentSourceTypes()
{
$statuses = [
['id' => '0', 'name' => 'Unknown', 'code'=>'unknown']
['id' => '1', 'name' => 'American Express', 'code'=>'amex'],
['id' => '2', 'name' => 'Carte Blanche', 'code'=>'carteblanche'],
['id' => '3', 'name' => 'China UnionPay', 'code'=>'unionpay'],
['id' => '4', 'name' => 'Diners Club', 'code'=>'diners'],
['id' => '5', 'name' => 'Discover', 'code'=>'discover'],
['id' => '6', 'name' => 'JCB', 'code'=>'jcb'],
['id' => '7', 'name' => 'Laser', 'code'=>'laser'],
['id' => '8', 'name' => 'Maestro', 'code'=>'maestro'],
['id' => '9', 'name' => 'MasterCard', 'code'=>'mastercard'],
['id' => '10', 'name' => 'Solo', 'code'=>'solo'],
['id' => '11', 'name' => 'Switch', 'code'=>'switch'],
['id' => '12', 'name' => 'Visa', 'code'=>'visa'],
];
foreach ($statuses as $status) {
$record = CardType::find($status['id']);
if ($record) {
$record->name = $status['name'];
$record->save();
} else {
CardType::create($status);
}
}
}
}

View File

@ -20,6 +20,7 @@ class DatabaseSeeder extends Seeder
$this->call('BanksSeeder');
$this->call('InvoiceStatusSeeder');
$this->call('PaymentStatusSeeder');
$this->call('CardTypesSeeder');
$this->call('CurrenciesSeeder');
$this->call('DateFormatsSeeder');
$this->call('InvoiceDesignsSeeder');

View File

@ -16,6 +16,7 @@ class UpdateSeeder extends Seeder
$this->call('BanksSeeder');
$this->call('InvoiceStatusSeeder');
$this->call('PaymentStatusSeeder');
$this->call('CardTypesSeeder');
$this->call('CurrenciesSeeder');
$this->call('DateFormatsSeeder');
$this->call('InvoiceDesignsSeeder');

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -1190,7 +1190,21 @@ $LANG = array(
'status_refunded' => 'Refunded',
'refunded_payment' => 'Refunded Payment',
'activity_39' => ':user refunded :adjustment of a :payment_amount payment (:payment)',
'card_expiration' => 'Exp: :expires',
'card_unknown' => 'Unknown',
'card_amex' => 'American Express',
'card_carteblanche' => 'Carte Blanche',
'card_unionpay' => 'UnionPay',
'card_diners' => 'Diners Club',
'card_discover' => 'Discover',
'card_jcb' => 'JCB',
'card_laser' => 'Laser',
'card_maestro' => 'Maestro',
'card_mastercard' => 'MasterCard',
'card_solo' => 'Solo',
'card_switch' => 'Switch',
'card_visa' => 'Visa',
);
return $LANG;

View File

@ -290,8 +290,10 @@
trans('texts.invoice'),
trans('texts.transaction_reference'),
trans('texts.method'),
trans('texts.source'),
trans('texts.payment_amount'),
trans('texts.payment_date'))
trans('texts.payment_date'),
trans('texts.status'))
->setUrl(url('api/payments/' . $client->public_id))
->setCustomValues('entityType', 'payments')
->setOptions('sPaginationType', 'bootstrap')