1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-09 12:42:36 +01:00

Allow admin to set min/max amount for each payment type

This commit is contained in:
Joshua Dwire 2016-09-05 18:16:56 -04:00
parent 03cc5035c5
commit 620e93084f
25 changed files with 174668 additions and 71 deletions

View File

@ -1,6 +1,8 @@
<?php namespace App\Http\Controllers;
use App\Models\AccountGateway;
use App\Models\AccountGatewaySettings;
use App\Models\GatewayType;
use App\Services\TemplateService;
use Auth;
use File;
@ -458,10 +460,12 @@ class AccountController extends BaseController
}
return View::make('accounts.payments', [
'showAdd' => $count < count(Gateway::$alternate) + 1,
'title' => trans('texts.online_payments'),
'showAdd' => $count < count(Gateway::$alternate) + 1,
'title' => trans('texts.online_payments'),
'tokenBillingOptions' => $tokenBillingOptions,
'account' => $account,
'currency' => Utils::getFromCache(Session::get(SESSION_CURRENCY, DEFAULT_CURRENCY),
'currencies'),
'account' => $account,
]);
}
}
@ -1224,6 +1228,35 @@ class AccountController extends BaseController
return Redirect::to('settings/'.ACCOUNT_PAYMENTS);
}
/**
* @return \Illuminate\Http\RedirectResponse
*/
public function savePaymentGatewayLimits()
{
$gateway_type_id = intval(Input::get('gateway_type_id'));
$gateway_settings = AccountGatewaySettings::scope()->where('gateway_type_id', '=', $gateway_type_id)->first();
if ( ! $gateway_settings) {
$gateway_settings = AccountGatewaySettings::createNew();
$gateway_settings->gateway_type_id = $gateway_type_id;
}
$gateway_settings->min_limit = Input::get('limit_min_enable') ? intval(Input::get('limit_min')) : 0;
$gateway_settings->max_limit = Input::get('limit_max_enable') ? intval(Input::get('limit_max')) : 0;
if ($gateway_settings->max_limit && $gateway_settings->min_limit > $gateway_settings->max_limit) {
$gateway_settings->max_limit = $gateway_settings->min_limit;
}
$gateway_settings->save();
event(new UserSettingsChanged());
Session::flash('message', trans('texts.updated_settings'));
return Redirect::to('settings/' . ACCOUNT_PAYMENTS);
}
/**
* @return \Illuminate\Http\RedirectResponse
*/

View File

@ -132,6 +132,7 @@ Route::group(['middleware' => 'auth:user'], function() {
Route::get('settings/user_details', 'AccountController@showUserDetails');
Route::post('settings/user_details', 'AccountController@saveUserDetails');
Route::post('settings/payment_gateway_limits', 'AccountController@savePaymentGatewayLimits');
Route::post('users/change_password', 'UserController@changePassword');
Route::resource('clients', 'ClientController');
@ -703,11 +704,11 @@ if (!defined('CONTACT_EMAIL')) {
define('PAYMENT_METHOD_STATUS_VERIFICATION_FAILED', 'verification_failed');
define('PAYMENT_METHOD_STATUS_VERIFIED', 'verified');
define('GATEWAY_TYPE_CREDIT_CARD', 'credit_card');
define('GATEWAY_TYPE_BANK_TRANSFER', 'bank_transfer');
define('GATEWAY_TYPE_PAYPAL', 'paypal');
define('GATEWAY_TYPE_BITCOIN', 'bitcoin');
define('GATEWAY_TYPE_DWOLLA', 'dwolla');
define('GATEWAY_TYPE_CREDIT_CARD', 1);
define('GATEWAY_TYPE_BANK_TRANSFER', 2);
define('GATEWAY_TYPE_PAYPAL', 3);
define('GATEWAY_TYPE_BITCOIN', 4);
define('GATEWAY_TYPE_DWOLLA', 5);
define('GATEWAY_TYPE_TOKEN', 'token');
define('REMINDER1', 'reminder1');
@ -851,6 +852,7 @@ if (!defined('CONTACT_EMAIL')) {
'invoiceStatus' => 'App\Models\InvoiceStatus',
'frequencies' => 'App\Models\Frequency',
'gateways' => 'App\Models\Gateway',
'gatewayTypes' => 'App\Models\GatewayType',
'fonts' => 'App\Models\Font',
'banks' => 'App\Models\Bank',
];

View File

@ -0,0 +1,62 @@
<?php namespace App\Models;
use Auth;
/**
* Class AccountGatewaySettings
*/
class AccountGatewaySettings extends EntityModel
{
/**
* @var array
*/
protected $dates = ['updated_at'];
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function gatewayType()
{
return $this->belongsTo('App\Models\GatewayType');
}
/**
* @param null $context
* @return mixed
*/
public static function createNew($context = null)
{
$className = get_called_class();
$entity = new $className();
if ($context) {
$user = $context instanceof User ? $context : $context->user;
$account = $context->account;
} elseif (Auth::check()) {
$user = Auth::user();
$account = Auth::user()->account;
} else {
Utils::fatalError();
}
$entity->user_id = $user->id;
$entity->account_id = $account->id;
// store references to the original user/account to prevent needing to reload them
$entity->setRelation('user', $user);
$entity->setRelation('account', $account);
if (method_exists($className, 'trashed')){
$lastEntity = $className::whereAccountId($entity->account_id)->withTrashed();
} else {
$lastEntity = $className::whereAccountId($entity->account_id);
}
return $entity;
}
public function setCreatedAtAttribute($value)
{
// to Disable created_at
}
}

View File

@ -0,0 +1,22 @@
<?php namespace App\Models;
use Eloquent;
/**
* Class GatewayType
*/
class GatewayType extends Eloquent
{
/**
* @var bool
*/
public $timestamps = false;
/**
* @return mixed
*/
public function getName()
{
return $this->name;
}
}

View File

@ -1,6 +1,11 @@
<?php namespace App\Ninja\Datatables;
use App\Models\AccountGatewaySettings;
use App\Models\GatewayType;
use URL;
use Cache;
use Utils;
use Session;
use App\Models\AccountGateway;
class AccountGatewayDatatable extends EntityDatatable
@ -45,12 +50,55 @@ class AccountGatewayDatatable extends EntityDatatable
}
}
],
[
'limit',
function ($model) {
$accountGateway = AccountGateway::find($model->id);
$paymentDriver = $accountGateway->paymentDriver();
$gatewayTypes = $paymentDriver->gatewayTypes();
$gatewayTypes = array_diff($gatewayTypes, array(GATEWAY_TYPE_TOKEN));
$html = '';
foreach ($gatewayTypes as $gatewayTypeId) {
$accountGatewaySettings = AccountGatewaySettings::scope()->where('account_gateway_settings.gateway_type_id',
'=', $gatewayTypeId)->first();
$gatewayType = GatewayType::find($gatewayTypeId);
if (count($gatewayTypes) > 1) {
if ($html) {
$html .= '<br>';
}
$html .= $gatewayType->name . ' &mdash; ';
}
// Decide how many nines to add to the end of the max.
$currency = Utils::getFromCache(Session::get(SESSION_CURRENCY, DEFAULT_CURRENCY), 'currencies');
$limit_max_adjustment = $currency->precision ? floatval('.' . str_repeat('9',
$currency->precision)) : 0;
if ($accountGatewaySettings && $accountGatewaySettings->min_limit && $accountGatewaySettings->max_limit) {
$html .= Utils::formatMoney($accountGatewaySettings->min_limit) . ' - ' . Utils::formatMoney($accountGatewaySettings->max_limit + $limit_max_adjustment);
} elseif ($accountGatewaySettings && $accountGatewaySettings->min_limit) {
$html .= trans('texts.min_limit',
array('min' => Utils::formatMoney($accountGatewaySettings->min_limit)));
} elseif ($accountGatewaySettings && $accountGatewaySettings->max_limit) {
$html .= trans('texts.max_limit',
array('max' => Utils::formatMoney($accountGatewaySettings->max_limit + $limit_max_adjustment)));
} else {
$html .= trans('texts.no_limit');
}
}
return $html;
}
],
];
}
public function actions()
{
return [
$actions = [
[
uctrans('texts.resend_confirmation_email'),
function ($model) {
@ -98,6 +146,30 @@ class AccountGatewayDatatable extends EntityDatatable
}
]
];
foreach (Cache::get('gatewayTypes') as $gatewayType) {
$actions[] = [
trans('texts.set_limits', ['gateway_type' => $gatewayType->name]),
function () use ($gatewayType) {
$accountGatewaySettings = AccountGatewaySettings::scope()->where('account_gateway_settings.gateway_type_id',
'=', $gatewayType->id)->first();
$min = $accountGatewaySettings ? $accountGatewaySettings->min_limit : 0;
$max = $accountGatewaySettings ? $accountGatewaySettings->max_limit : 0;
return "javascript:showLimitsModal('{$gatewayType->name}',{$gatewayType->id},$min,$max)";
},
function ($model) use ($gatewayType) {
// Only show this action if the given gateway supports this gateway type
$accountGateway = AccountGateway::find($model->id);
$paymentDriver = $accountGateway->paymentDriver();
$gatewayTypes = $paymentDriver->gatewayTypes();
return in_array($gatewayType->id, $gatewayTypes);
}
];
}
return $actions;
}
}

View File

@ -29,7 +29,8 @@
"stacktrace-js": "~1.0.1",
"fuse.js": "~2.0.2",
"dropzone": "~4.3.0",
"sweetalert": "~1.1.3"
"sweetalert": "~1.1.3",
"nouislider": "~8.5.1"
},
"resolutions": {
"jquery": "~1.11"

View File

@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateGatewayTypes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::dropIfExists('gateway_types');
Schema::create('gateway_types', function($t)
{
$t->increments('id');
$t->string('name');
});
Schema::dropIfExists('account_gateway_settings');
Schema::create('account_gateway_settings', function($t)
{
$t->increments('id');
$t->unsignedInteger('account_id');
$t->unsignedInteger('user_id');
$t->unsignedInteger('gateway_type_id')->nullable();
$t->timestamp('updated_at')->nullable();
$t->unsignedInteger('min_limit');
$t->unsignedInteger('max_limit');
$t->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
$t->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$t->foreign('gateway_type_id')->references('id')->on('gateway_types')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('account_gateway_settings');
Schema::dropIfExists('gateway_types');
}
}

View File

@ -25,6 +25,7 @@ class DatabaseSeeder extends Seeder
$this->call('InvoiceDesignsSeeder');
$this->call('PaymentTermsSeeder');
$this->call('PaymentTypesSeeder');
$this->call('GatewayTypesSeeder');
$this->call('LanguageSeeder');
$this->call('IndustrySeeder');
}

View File

@ -0,0 +1,28 @@
<?php
use App\Models\GatewayType;
class GatewayTypesSeeder extends Seeder
{
public function run()
{
Eloquent::unguard();
$gateway_types = [
['name' => 'Credit Card'],
['name' => 'Bank Transfer'],
['name' => 'PayPal'],
['name' => 'Bitcoin'],
['name' => 'Dwolla'],
];
foreach ($gateway_types as $gateway_type) {
$record = GatewayType::where('name', '=', $gateway_type['name'])->first();
if (!$record) {
GatewayType::create($gateway_type);
}
}
}
}

View File

@ -21,6 +21,7 @@ class UpdateSeeder extends Seeder
$this->call('InvoiceDesignsSeeder');
$this->call('PaymentTermsSeeder');
$this->call('PaymentTypesSeeder');
$this->call('GatewayTypesSeeder');
$this->call('LanguageSeeder');
$this->call('IndustrySeeder');
}

View File

@ -101,6 +101,7 @@ elixir(function(mix) {
//bowerDir + '/stacktrace-js/dist/stacktrace-with-polyfills.min.js',
bowerDir + '/fuse.js/src/fuse.js',
bowerDir + '/sweetalert/dist/sweetalert-dev.js',
bowerDir + '/nouislider/distribute/nouislider.js',
'bootstrap-combobox.js',
'script.js',
'pdf.pdfmake.js',

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

13481
public/css/built.css vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1428
public/js/Chart.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

9559
public/js/d3.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1091,3 +1091,144 @@ div.panel-body div.panel-body {
width: 100%;
height: 100%;
}
/* Limits */
.noUi-target,
.noUi-target * {
-webkit-touch-callout: none;
-webkit-user-select: none;
-ms-touch-action: none;
touch-action: none;
-ms-user-select: none;
-moz-user-select: none;
user-select: none;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.noUi-target {
position: relative;
direction: ltr;
margin:0 17px;
}
.noUi-base {
width: 100%;
height: 100%;
position: relative;
z-index: 1; /* Fix 401 */
}
.noUi-origin {
position: absolute;
right: 0;
top: 0;
left: 0;
bottom: 0;
}
.noUi-handle {
position: relative;
z-index: 1;
}
.noUi-stacking .noUi-handle {
/* This class is applied to the lower origin when
its values is > 50%. */
z-index: 10;
}
.noUi-state-tap .noUi-origin {
-webkit-transition: left 0.3s, top 0.3s;
transition: left 0.3s, top 0.3s;
}
.noUi-state-drag * {
cursor: inherit !important;
}
.noUi-base,
.noUi-handle {
-webkit-transform: translate3d(0,0,0);
transform: translate3d(0,0,0);
}
.noUi-horizontal {
height: 15px;
}
.noUi-horizontal .noUi-handle {
width: 34px;
height: 25px;
left: -17px;
top: -6px;
}
.noUi-background {
background: #f9f9f9;
}
.noUi-connect {
background: #286090;
}
.noUi-origin {
border-radius: 2px;
}
.noUi-target {
border-radius: 4px;
border: 1px solid #ddd;
}
.noUi-draggable {
cursor: w-resize;
}
.noUi-vertical .noUi-draggable {
cursor: n-resize;
}
.noUi-handle {
border: 1px solid #777;
border-radius: 3px;
background: #FFF;
cursor: pointer;
}
.noUi-handle:before,
.noUi-handle:after {
content: "";
display: block;
position: absolute;
height: 13px;
width: 1px;
background: #777;
left: 14px;
top: 5px;
}
.noUi-handle:after {
left: 17px;
}
.noUi-vertical .noUi-handle:before,
.noUi-vertical .noUi-handle:after {
width: 14px;
height: 1px;
left: 6px;
top: 14px;
}
.noUi-vertical .noUi-handle:after {
top: 17px;
}
#payment-limits-slider{
margin-bottom:10px;
}
#payment-limit-min-container .input-group,
#payment-limit-max-container .input-group{
max-width:200px;
clear:both;
}
#payment-limit-max-container{
text-align:right;
}
#payment-limit-max-container .input-group,
#payment-limit-max-container label{
float:right;
}
#payment-limit-min[disabled],
#payment-limit-max[disabled]{
background-color: #eee!important;
color: #999!important;
}

View File

@ -2095,6 +2095,17 @@ $LANG = array(
'city_state_postal' => 'City/State/Postal',
'custom_field' => 'Custom Field',
// Limits
'limit' => 'Limit',
'min_limit' => 'Min: :min',
'max_limit' => 'Max: :max',
'no_limit' => 'No Limits',
'set_limits' => 'Set :gateway_type Limits',
'enable_min' => 'Enable min',
'enable_max' => 'Enable max',
'min' => 'Min',
'max' => 'Max',
);
return $LANG;

View File

@ -50,15 +50,79 @@
{!! Datatable::table()
->addColumn(
trans('texts.name'),
trans('texts.limit'),
trans('texts.action'))
->setUrl(url('api/gateways/'))
->setOptions('sPaginationType', 'bootstrap')
->setOptions('bFilter', false)
->setOptions('bAutoWidth', false)
->setOptions('aoColumns', [[ "sWidth"=> "80%" ], ["sWidth"=> "20%"]])
->setOptions('aoColumns', [[ "sWidth"=> "50%" ], ["sWidth"=> "30%"], ["sWidth"=> "20%"]])
->setOptions('aoColumnDefs', [['bSortable'=>false, 'aTargets'=>[1]]])
->render('datatable') !!}
{!! Former::open( 'settings/payment_gateway_limits') !!}
<div class="modal fade" id="paymentLimitsModal" tabindex="-1" role="dialog"
aria-labelledby="paymentLimitsModalLabel"
aria-hidden="true">
<div class="modal-dialog" style="min-width:150px">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="paymentLimitsModalLabel"></h4>
</div>
<div class="modal-body">
<div class="row" style="text-align:center">
<div class="col-xs-12">
<div id="payment-limits-slider"></div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div id="payment-limit-min-container">
<label for="payment-limit-min">{{ trans('texts.min') }}</label><br>
<div class="input-group">
<span class="input-group-addon">{{ $currency->symbol }}</span>
<input type="number" class="form-control" min="0" id="payment-limit-min"
name="limit_min">
@if ($currency->precision)
<span class="input-group-addon">{{ $currency->decimal_separator }}{{ str_repeat( '0', $currency->precision) }}</span>
@endif
</div>
<label><input type="checkbox" id="payment-limit-min-enable"
name="limit_min_enable"> {{ trans('texts.enable_min') }}</label>
</div>
</div>
<div class="col-md-6">
<div id="payment-limit-max-container">
<label for="payment-limit-max">{{ trans('texts.max') }}</label><br>
<div class="input-group">
<span class="input-group-addon">{{ $currency->symbol }}</span>
<input type="number" class="form-control" min="0" id="payment-limit-max"
name="limit_max">
@if ($currency->precision)
<span class="input-group-addon">{{ $currency->decimal_separator }}{{ str_repeat( '9', $currency->precision) }}</span>
@endif
</div>
<label><input type="checkbox" id="payment-limit-max-enable"
name="limit_max_enable"> {{ trans('texts.enable_max') }}</label>
</div>
</div>
</div>
<input type="hidden" name="gateway_type_id" id="payment-limit-gateway-type">
</div>
<div class="modal-footer" style="margin-top: 0px">
<button type="button" class="btn btn-default"
data-dismiss="modal">{{ trans('texts.cancel') }}</button>
<button type="submit" class="btn btn-primary">{{ trans('texts.save') }}</button>
</div>
</div>
</div>
</div>
{!! Former::close() !!}
<script>
window.onDatatableReady = actionListHandler;
function setTrashVisible() {
@ -69,6 +133,89 @@
refreshDatatable();
})
}
function showLimitsModal(gateway_type, gateway_type_id, min_limit, max_limit) {
var modalLabel = {!! json_encode(trans('texts.set_limits')) !!};
$('#paymentLimitsModalLabel').text(modalLabel.replace(':gateway_type', gateway_type));
limitsSlider.noUiSlider.set([min_limit, max_limit ? max_limit : 100000]);
if (min_limit) {
$('#payment-limit-min').removeAttr('disabled');
$('#payment-limit-min-enable').prop('checked', true);
} else {
$('#payment-limit-min').attr('disabled', 'disabled');
$('#payment-limit-min-enable').prop('checked', false);
}
if (max_limit) {
$('#payment-limit-max').removeAttr('disabled');
$('#payment-limit-max-enable').prop('checked', true);
} else {
$('#payment-limit-max').attr('disabled', 'disabled');
$('#payment-limit-max-enable').prop('checked', false);
}
$('#payment-limit-gateway-type').val(gateway_type_id);
$('#paymentLimitsModal').modal('show');
}
var limitsSlider = document.getElementById('payment-limits-slider');
noUiSlider.create(limitsSlider, {
start: [0, 100000],
connect: true,
range: {
'min': [0, 1],
'30%': [500, 1],
'70%': [5000, 1],
'max': [100000, 1]
}
});
limitsSlider.noUiSlider.on('update', function (values, handle) {
var value = values[handle];
if (handle == 1) {
$('#payment-limit-max').val(Math.round(value)).removeAttr('disabled');
$('#payment-limit-max-enable').prop('checked', true);
} else {
$('#payment-limit-min').val(Math.round(value)).removeAttr('disabled');
$('#payment-limit-min-enable').prop('checked', true);
}
});
$('#payment-limit-min').on('change keyup', function () {
setTimeout(function () {
limitsSlider.noUiSlider.set([$('#payment-limit-min').val(), null]);
}, 100);
$('#payment-limit-min-enable').attr('checked', 'checked');
});
$('#payment-limit-max').on('change keyup', function () {
setTimeout(function () {
limitsSlider.noUiSlider.set([null, $('#payment-limit-max').val()]);
}, 100);
$('#payment-limit-max-enable').attr('checked', 'checked');
});
$('#payment-limit-min-enable').change(function () {
if ($(this).is(':checked')) {
$('#payment-limit-min').removeAttr('disabled');
} else {
$('#payment-limit-min').attr('disabled', 'disabled');
}
});
$('#payment-limit-max-enable').change(function () {
if ($(this).is(':checked')) {
$('#payment-limit-max').removeAttr('disabled');
} else {
$('#payment-limit-max').attr('disabled', 'disabled');
}
});
</script>
@stop