Initial work on OFX support

This commit is contained in:
Hillel Coren 2016-01-20 01:07:31 +02:00
parent 3d790d29a1
commit bf778aa616
20 changed files with 995 additions and 4 deletions

View File

@ -0,0 +1,30 @@
<?php namespace app\Console\Commands;
use Illuminate\Console\Command;
use App\Services\BankAccountService;
class TestOFX extends Command
{
protected $name = 'ninja:test-ofx';
protected $description = 'Test OFX';
public function __construct(BankAccountService $bankAccountService)
{
parent::__construct();
$this->bankAccountService = $bankAccountService;
}
public function fire()
{
$this->info(date('Y-m-d').' Running TestOFX...');
$bankId = env('TEST_BANK_ID');
$username = env('TEST_BANK_USERNAME');
$password = env('TEST_BANK_PASSWORD');
$data = $this->bankAccountService->loadBankAccounts($bankId, $username, $password, false);
print "<pre>".print_r($data, 1)."</pre>";
}
}

View File

@ -17,6 +17,7 @@ class Kernel extends ConsoleKernel
'App\Console\Commands\CheckData',
'App\Console\Commands\SendRenewalInvoices',
'App\Console\Commands\SendReminders',
'App\Console\Commands\TestOFX',
];
/**

View File

@ -144,6 +144,8 @@ class AccountController extends BaseController
return self::showLocalization();
} elseif ($section == ACCOUNT_PAYMENTS) {
return self::showOnlinePayments();
} elseif ($section == ACCOUNT_BANKS) {
return self::showBankAccounts();
} elseif ($section == ACCOUNT_INVOICE_SETTINGS) {
return self::showInvoiceSettings();
} elseif ($section == ACCOUNT_IMPORT_EXPORT) {
@ -263,6 +265,21 @@ class AccountController extends BaseController
return View::make('accounts.localization', $data);
}
private function showBankAccounts()
{
$account = Auth::user()->account;
$account->load('bank_accounts');
$count = count($account->bank_accounts);
if ($count == 0) {
return Redirect::to('bank_accounts/create');
} else {
return View::make('accounts.banks', [
'title' => trans('texts.bank_accounts')
]);
}
}
private function showOnlinePayments()
{
$account = Auth::user()->account;

View File

@ -0,0 +1,152 @@
<?php namespace App\Http\Controllers;
use Crypt;
use Cache;
use Auth;
use Datatable;
use DB;
use Input;
use Redirect;
use Session;
use View;
use Validator;
use stdClass;
use URL;
use Utils;
use App\Models\Gateway;
use App\Models\Account;
use App\Models\BankAccount;
use App\Ninja\Repositories\AccountRepository;
use App\Services\BankAccountService;
class BankAccountController extends BaseController
{
protected $bankAccountService;
public function __construct(BankAccountService $bankAccountService)
{
parent::__construct();
$this->bankAccountService = $bankAccountService;
}
public function index()
{
return Redirect::to('settings/' . ACCOUNT_BANKS);
}
public function getDatatable()
{
return $this->bankAccountService->getDatatable(Auth::user()->account_id);
}
public function edit($publicId)
{
$bankAccount = BankAccount::scope($publicId)->firstOrFail();
$bankAccount->username = str_repeat('*', 16);
$data = [
'url' => 'bank_accounts/' . $publicId,
'method' => 'PUT',
'title' => trans('texts.edit_bank_account'),
'banks' => Cache::get('banks'),
'bankAccount' => $bankAccount,
];
return View::make('accounts.bank_account', $data);
}
public function update($publicId)
{
return $this->save($publicId);
}
public function store()
{
return $this->save();
}
/**
* Displays the form for account creation
*
*/
public function create()
{
$data = [
'url' => 'bank_accounts',
'method' => 'POST',
'title' => trans('texts.add_bank_account'),
'banks' => Cache::get('banks'),
'bankAccount' => null,
];
return View::make('accounts.bank_account', $data);
}
public function bulk()
{
$action = Input::get('bulk_action');
$ids = Input::get('bulk_public_id');
$count = $this->bankAccountService->bulk($ids, $action);
Session::flash('message', trans('texts.archived_bank_account'));
return Redirect::to('settings/' . ACCOUNT_BANKS);
}
/**
* Stores new account
*
*/
public function save($bankAccountPublicId = false)
{
$account = Auth::user()->account;
$bankId = Input::get('bank_id');
$username = Input::get('bank_username');
$rules = [
'bank_id' => $bankAccountPublicId ? '' : 'required',
'bank_username' => 'required',
];
$validator = Validator::make(Input::all(), $rules);
if ($validator->fails()) {
return Redirect::to('bank_accounts/create')
->withErrors($validator)
->withInput();
} else {
if ($bankAccountPublicId) {
$bankAccount = BankAccount::scope($bankAccountPublicId)->firstOrFail();
} else {
$bankAccount = BankAccount::createNew();
$bankAccount->bank_id = $bankId;
}
if ($username != str_repeat('*', strlen($username))) {
$bankAccount->username = Crypt::encrypt(trim($username));
}
if ($bankAccountPublicId) {
$bankAccount->save();
$message = trans('texts.updated_bank_account');
} else {
$account->bank_accounts()->save($bankAccount);
$message = trans('texts.created_bank_account');
}
Session::flash('message', $message);
return Redirect::to("bank_accounts/{$bankAccount->public_id}/edit");
}
}
public function test()
{
$bankId = Input::get('bank_id');
$username = Input::get('bank_username');
$password = Input::get('bank_password');
return json_encode($this->bankAccountService->loadBankAccounts($bankId, $username, $password, false));
}
}

View File

@ -163,7 +163,7 @@ class StartupCheck
$orderBy = 'num_days';
} elseif ($name == 'fonts') {
$orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries'])) {
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
$orderBy = 'name';
} else {
$orderBy = 'id';

View File

@ -132,6 +132,11 @@ Route::group(['middleware' => 'auth'], function() {
Route::get('api/gateways', array('as'=>'api.gateways', 'uses'=>'AccountGatewayController@getDatatable'));
Route::post('account_gateways/bulk', 'AccountGatewayController@bulk');
Route::resource('bank_accounts', 'BankAccountController');
Route::get('api/bank_accounts', array('as'=>'api.bank_accounts', 'uses'=>'BankAccountController@getDatatable'));
Route::post('bank_accounts/bulk', 'BankAccountController@bulk');
Route::post('bank_accounts/test', 'BankAccountController@test');
Route::resource('clients', 'ClientController');
Route::get('api/clients', array('as'=>'api.clients', 'uses'=>'ClientController@getDatatable'));
Route::get('api/activities/{client_id?}', array('as'=>'api.activities', 'uses'=>'ActivityController@getDatatable'));
@ -253,6 +258,7 @@ if (!defined('CONTACT_EMAIL')) {
define('ENTITY_QUOTE', 'quote');
define('ENTITY_TASK', 'task');
define('ENTITY_ACCOUNT_GATEWAY', 'account_gateway');
define('ENTITY_BANK_ACCOUNT', 'bank_account');
define('ENTITY_USER', 'user');
define('ENTITY_TOKEN', 'token');
define('ENTITY_TAX_RATE', 'tax_rate');
@ -271,6 +277,7 @@ if (!defined('CONTACT_EMAIL')) {
define('ACCOUNT_NOTIFICATIONS', 'notifications');
define('ACCOUNT_IMPORT_EXPORT', 'import_export');
define('ACCOUNT_PAYMENTS', 'online_payments');
define('ACCOUNT_BANKS', 'bank_accounts');
define('ACCOUNT_MAP', 'import_map');
define('ACCOUNT_EXPORT', 'export');
define('ACCOUNT_TAX_RATES', 'tax_rates');
@ -518,6 +525,8 @@ if (!defined('CONTACT_EMAIL')) {
define('EMAIL_DESIGN_LIGHT', 2);
define('EMAIL_DESIGN_DARK', 3);
define('BANK_LIBRARY_OFX', 1);
$creditCards = [
1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'],
2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'],
@ -543,6 +552,7 @@ if (!defined('CONTACT_EMAIL')) {
'frequencies' => 'App\Models\Frequency',
'gateways' => 'App\Models\Gateway',
'fonts' => 'App\Models\Font',
'banks' => 'App\Models\Bank',
];
define('CACHED_TABLES', serialize($cachedTables));
@ -596,3 +606,4 @@ if (Auth::check() && Auth::user()->id === 1)
Auth::loginUsingId(1);
}
*/

225
app/Libraries/OFX.php Normal file
View File

@ -0,0 +1,225 @@
<?php namespace App\Libraries;
// https://github.com/denvertimothy/OFX
use SimpleXMLElement;
class OFX
{
public $bank;
public $request;
public $response;
public $responseHeader;
public $responseBody;
public function __construct($bank, $request)
{
$this->bank = $bank;
$this->request = $request;
}
public function go()
{
$c = curl_init();
curl_setopt($c, CURLOPT_URL, $this->bank->url);
curl_setopt($c, CURLOPT_POST, 1);
curl_setopt($c, CURLOPT_HTTPHEADER, array('Content-Type: application/x-ofx'));
curl_setopt($c, CURLOPT_POSTFIELDS, $this->request);
curl_setopt($c, CURLOPT_RETURNTRANSFER, 1);
//curl_setopt($c, CURLOPT_SSL_VERIFYPEER, false);
$this->response = curl_exec($c);
curl_close($c);
$tmp = explode('<OFX>', $this->response);
$this->responseHeader = $tmp[0];
$this->responseBody = '<OFX>'.$tmp[1];
}
public function xml()
{
$xml = $this->responseBody;
self::closeTags($xml);
$x = new SimpleXMLElement($xml);
return $x;
}
public static function closeTags(&$x)
{
$x = preg_replace('/(<([^<\/]+)>)(?!.*?<\/\2>)([^<]+)/', '\1\3</\2>', $x);
}
}
class Finance
{
public $banks;
}
class Bank
{
public $logins; // array of class User
public $finance; // the Finance object that hold this Bank object
public $fid;
public $org;
public $url;
public function __construct($finance, $fid, $url, $org)
{
$this->finance = $finance;
$this->fid = $fid;
$this->url = $url;
$this->org = $org;
}
}
class Login
{
public $accounts;
public $bank;
public $id;
public $pass;
public function __construct($bank, $id, $pass)
{
$this->bank = $bank;
$this->id = $id;
$this->pass = $pass;
}
public function setup()
{
$ofxRequest =
"OFXHEADER:100\n".
"DATA:OFXSGML\n".
"VERSION:102\n".
"SECURITY:NONE\n".
"ENCODING:USASCII\n".
"CHARSET:1252\n".
"COMPRESSION:NONE\n".
"OLDFILEUID:NONE\n".
"NEWFILEUID:NONE\n".
"\n".
"<OFX>\n".
"<SIGNONMSGSRQV1>\n".
"<SONRQ>\n".
"<DTCLIENT>20110412162900.000[-7:MST]\n".
"<USERID>".$this->id."\n".
"<USERPASS>".$this->pass."\n".
"<GENUSERKEY>N\n".
"<LANGUAGE>ENG\n".
"<FI>\n".
"<ORG>".$this->bank->org."\n".
"<FID>".$this->bank->fid."\n".
"</FI>\n".
"<APPID>QMOFX\n".
"<APPVER>1900\n".
"</SONRQ>\n".
"</SIGNONMSGSRQV1>\n".
"<SIGNUPMSGSRQV1>\n".
"<ACCTINFOTRNRQ>\n".
"<TRNUID>".md5(time().$this->bank->url.$this->id)."\n".
"<ACCTINFORQ>\n".
"<DTACCTUP>19900101\n".
"</ACCTINFORQ>\n".
"</ACCTINFOTRNRQ> \n".
"</SIGNUPMSGSRQV1>\n".
"</OFX>\n";
$o = new OFX($this->bank, $ofxRequest);
$o->go();
$x = $o->xml();
foreach ($x->xpath('/OFX/SIGNUPMSGSRSV1/ACCTINFOTRNRS/ACCTINFORS/ACCTINFO/BANKACCTINFO/BANKACCTFROM') as $a) {
$this->accounts[] = new Account($this, (string) $a->ACCTID, 'BANK', (string) $a->ACCTTYPE, (string) $a->BANKID);
}
foreach ($x->xpath('/OFX/SIGNUPMSGSRSV1/ACCTINFOTRNRS/ACCTINFORS/ACCTINFO/CCACCTINFO/CCACCTFROM') as $a) {
$this->accounts[] = new Account($this, (string) $a->ACCTID, 'CC');
}
}
}
class Account
{
public $login;
public $id;
public $type;
public $subType;
public $bankId;
public $ledgerBalance;
public $availableBalance;
public $response;
public function __construct($login, $id, $type, $subType = null, $bankId = null)
{
$this->login = $login;
$this->id = $id;
$this->type = $type;
$this->subType = $subType;
$this->bankId = $bankId;
}
public function setup($includeTransactions = true)
{
$ofxRequest =
"OFXHEADER:100\n".
"DATA:OFXSGML\n".
"VERSION:102\n".
"SECURITY:NONE\n".
"ENCODING:USASCII\n".
"CHARSET:1252\n".
"COMPRESSION:NONE\n".
"OLDFILEUID:NONE\n".
"NEWFILEUID:NONE\n".
"\n".
"<OFX>\n".
"<SIGNONMSGSRQV1>\n".
"<SONRQ>\n".
"<DTCLIENT>20110412162900.000[-7:MST]\n".
"<USERID>".$this->login->id."\n".
"<USERPASS>".$this->login->pass."\n".
"<LANGUAGE>ENG\n".
"<FI>\n".
"<ORG>".$this->login->bank->org."\n".
"<FID>".$this->login->bank->fid."\n".
"</FI>\n".
"<APPID>QMOFX\n".
"<APPVER>1900\n".
"</SONRQ>\n".
"</SIGNONMSGSRQV1>\n";
if ($this->type == 'BANK') {
$ofxRequest .=
" <BANKMSGSRQV1>\n".
" <STMTTRNRQ>\n".
" <TRNUID>".md5(time().$this->login->bank->url.$this->id)."\n".
" <STMTRQ>\n".
" <BANKACCTFROM>\n".
" <BANKID>".$this->bankId."\n".
" <ACCTID>".$this->id."\n".
" <ACCTTYPE>".$this->subType."\n".
" </BANKACCTFROM>\n".
" <INCTRAN>\n".
" <DTSTART>20110301\n".
" <INCLUDE>".($includeTransactions ? 'Y' : 'N')."\n".
" </INCTRAN>\n".
" </STMTRQ>\n".
" </STMTTRNRQ>\n".
" </BANKMSGSRQV1>\n";
} elseif ($this->type == 'CC') {
$ofxRequest .=
" <CREDITCARDMSGSRQV1>\n".
" <CCSTMTTRNRQ>\n".
" <TRNUID>".md5(time().$this->login->bank->url.$this->id)."\n".
" <CCSTMTRQ>\n".
" <CCACCTFROM>\n".
" <ACCTID>".$this->id."\n".
" </CCACCTFROM>\n".
" <INCTRAN>\n".
" <DTSTART>20110320\n".
" <INCLUDE>".($includeTransactions ? 'Y' : 'N')."\n".
" </INCTRAN>\n".
" </CCSTMTRQ>\n".
" </CCSTMTTRNRQ>\n".
" </CREDITCARDMSGSRQV1>\n";
}
$ofxRequest .=
"</OFX>";
$o = new OFX($this->login->bank, $ofxRequest);
$o->go();
$this->response = $o->response;
$x = $o->xml();
$a = $x->xpath('/OFX/*/*/*/LEDGERBAL/BALAMT');
$this->ledgerBalance = (double) $a[0];
$a = $x->xpath('/OFX/*/*/*/AVAILBAL/BALAMT');
if (isset($a[0])) {
$this->availableBalance = (double) $a[0];
}
}
}

View File

@ -311,6 +311,39 @@ class Utils
return $string;
}
public static function maskAccountNumber($value)
{
$length = strlen($value);
if ($length < 4) {
str_repeat('*', 16);
}
$lastDigits = substr($value, -4);
return str_repeat('*', $length - 4) . $lastDigits;
}
// http://wephp.co/detect-credit-card-type-php/
public static function getCardType($number)
{
$number = preg_replace('/[^\d]/', '', $number);
if (preg_match('/^3[47][0-9]{13}$/', $number)) {
return 'American Express';
} elseif (preg_match('/^3(?:0[0-5]|[68][0-9])[0-9]{11}$/', $number)) {
return 'Diners Club';
} elseif (preg_match('/^6(?:011|5[0-9][0-9])[0-9]{12}$/', $number)) {
return 'Discover';
} elseif (preg_match('/^(?:2131|1800|35\d{3})\d{11}$/', $number)) {
return 'JCB';
} elseif (preg_match('/^5[1-5][0-9]{14}$/', $number)) {
return 'MasterCard';
} elseif (preg_match('/^4[0-9]{12}(?:[0-9]{3})?$/', $number)) {
return 'Visa';
} else {
return 'Unknown';
}
}
public static function toArray($data)
{
return json_decode(json_encode((array) $data), true);

View File

@ -26,6 +26,7 @@ class Account extends Eloquent
ACCOUNT_USER_DETAILS,
ACCOUNT_LOCALIZATION,
ACCOUNT_PAYMENTS,
ACCOUNT_BANKS,
ACCOUNT_TAX_RATES,
ACCOUNT_PRODUCTS,
ACCOUNT_NOTIFICATIONS,
@ -79,6 +80,11 @@ class Account extends Eloquent
return $this->hasMany('App\Models\AccountGateway');
}
public function bank_accounts()
{
return $this->hasMany('App\Models\BankAccount');
}
public function tax_rates()
{
return $this->hasMany('App\Models\TaxRate');

15
app/Models/Bank.php Normal file
View File

@ -0,0 +1,15 @@
<?php namespace App\Models;
use Eloquent;
class Bank extends Eloquent
{
public $timestamps = false;
public function getOFXBank($finance)
{
$config = json_decode($this->config);
return new \App\Libraries\Bank($finance, $config->fid, $config->url, $config->org);
}
}

View File

@ -0,0 +1,23 @@
<?php namespace App\Models;
use Crypt;
use App\Models\Bank;
use Illuminate\Database\Eloquent\SoftDeletes;
class BankAccount extends EntityModel
{
use SoftDeletes;
protected $dates = ['deleted_at'];
public function getEntityType()
{
return ENTITY_BANK_ACCOUNT;
}
public function bank()
{
return $this->belongsTo('App\Models\Bank');
}
}

View File

@ -0,0 +1,24 @@
<?php namespace App\Ninja\Repositories;
use DB;
use Utils;
use Session;
use App\Models\BankAccount;
use App\Ninja\Repositories\BaseRepository;
class BankAccountRepository extends BaseRepository
{
public function getClassName()
{
return 'App\Models\BankAccount';
}
public function find($accountId)
{
return DB::table('bank_accounts')
->join('banks', 'banks.id', '=', 'bank_accounts.bank_id')
->where('bank_accounts.deleted_at', '=', null)
->where('bank_accounts.account_id', '=', $accountId)
->select('bank_accounts.public_id', 'banks.name as bank_name', 'bank_accounts.deleted_at', 'banks.bank_library_id');
}
}

View File

@ -0,0 +1,107 @@
<?php namespace App\Services;
use stdClass;
use Utils;
use URL;
use App\Models\Gateway;
use App\Services\BaseService;
use App\Ninja\Repositories\BankAccountRepository;
use App\Libraries\Finance;
use App\Libraries\Login;
class BankAccountService extends BaseService
{
protected $bankAccountRepo;
protected $datatableService;
public function __construct(BankAccountRepository $bankAccountRepo, DatatableService $datatableService)
{
$this->bankAccountRepo = $bankAccountRepo;
$this->datatableService = $datatableService;
}
protected function getRepo()
{
return $this->bankAccountRepo;
}
/*
public function save()
{
return null;
}
*/
public function loadBankAccounts($bankId, $username, $password, $includeTransactions = true)
{
if ( ! $bankId || ! $username || ! $password) {
return false;
}
$bank = Utils::getFromCache($bankId, 'banks');
$data = [];
try {
$finance = new Finance();
$finance->banks[$bankId] = $bank->getOFXBank($finance);
$finance->banks[$bankId]->logins[] = new Login($finance->banks[$bankId], $username, $password);
foreach ($finance->banks as $bank) {
foreach ($bank->logins as $login) {
$login->setup();
foreach ($login->accounts as $account) {
$account->setup($includeTransactions);
$obj = new stdClass;
$obj->account_number = Utils::maskAccountNumber($account->id);
$obj->type = $account->type;
$obj->balance = Utils::formatMoney($account->ledgerBalance, CURRENCY_DOLLAR);
$data[] = $obj;
}
}
}
return $data;
} catch (\Exception $e) {
return false;
}
}
public function getDatatable($accountId)
{
$query = $this->bankAccountRepo->find($accountId);
return $this->createDatatable(ENTITY_BANK_ACCOUNT, $query, false);
}
protected function getDatatableColumns($entityType, $hideClient)
{
return [
[
'bank_name',
function ($model) {
return link_to("bank_accounts/{$model->public_id}/edit", $model->bank_name);
}
],
[
'bank_library_id',
function ($model) {
return 'OFX';
}
],
];
}
protected function getDatatableActions($entityType)
{
return [
[
uctrans('texts.edit_bank_account'),
function ($model) {
return URL::to("bank_accounts/{$model->public_id}/edit");
}
]
];
}
}

View File

@ -89,7 +89,8 @@
"App\\": "app/"
},
"files": [
"app/Libraries/lib_autolink.php"
"app/Libraries/lib_autolink.php",
"app/Libraries/OFX.php"
]
},
"autoload-dev": {

View File

@ -0,0 +1,56 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddBankAccounts extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('banks', function($table)
{
$table->increments('id');
$table->string('name');
$table->string('remote_id');
$table->integer('bank_library_id')->default(BANK_LIBRARY_OFX);
$table->text('config');
});
Schema::create('bank_accounts', function($table)
{
$table->increments('id');
$table->unsignedInteger('account_id');
$table->unsignedInteger('bank_id');
$table->unsignedInteger('user_id');
$table->string('username');
$table->timestamps();
$table->softDeletes();
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('bank_id')->references('id')->on('banks');
$table->unsignedInteger('public_id')->index();
$table->unique(['account_id', 'public_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('bank_accounts');
Schema::drop('banks');
}
}

View File

@ -0,0 +1,44 @@
<?php
use App\Models\Bank;
class BanksSeeder extends Seeder
{
public function run()
{
Eloquent::unguard();
$this->createBanks();
}
// Source: http://www.ofxhome.com/
private function createBanks()
{
$banks = [
[
'remote_id' => 425,
'name' => 'American Express Card',
'config' => json_encode([
'fid' => 3101,
'org' => 'AMEX',
'url' => 'https://online.americanexpress.com/myca/ofxdl/desktop/desktopDownload.do?request_type=nl_ofxdownload',
])
],
[
'remote_id' => 497,
'name' => 'AIM Investments',
'config' => json_encode([
'fid' => '',
'org' => '',
'url' => 'https://ofx3.financialtrans.com/tf/OFXServer?tx=OFXController&amp;cz=702110804131918&amp;cl=3000812',
])
],
];
foreach ($banks as $bank) {
if (!DB::table('banks')->where('remote_id', '=', $bank['remote_id'])->get()) {
Bank::create($bank);
}
}
}
}

View File

@ -16,7 +16,8 @@ class DatabaseSeeder extends Seeder {
$this->call('ConstantsSeeder');
$this->call('CountriesSeeder');
$this->call('PaymentLibrariesSeeder');
$this->call('FontsSeeder');
$this->call('FontsSeeder');
$this->call('BanksSeeder');
}
}

View File

@ -1038,6 +1038,21 @@ return array(
'quote_message_button' => 'To view your quote for :amount, click the button below.',
'payment_message_button' => 'Thank you for your payment of :amount.',
'payment_type_direct_debit' => 'Direct Debit',
'bank_accounts' => 'Bank Accounts',
'add_bank_account' => 'Add Bank Account',
'bank_id' => 'bank',
'integration_type' => 'Integration Type',
'updated_bank_account' => 'Successfully updated bank account',
'edit_bank_account' => 'Edit Bank Account',
'archive_bank_account' => 'Archive Bank Account',
'archived_bank_account' => 'Successfully archived bank account',
'created_bank_account' => 'Successfully created bank account',
'test' => 'Test',
'test_bank_account' => 'Test Bank Account',
'bank_password_help' => 'Note: your password is transmitted securely and never stored on our servers.',
'bank_password_warning' => 'Warning: your password may be transmitted in plain text, consider enabling HTTPS.',
'username' => 'Username',
'account_number' => 'Account Number',
'bank_account_error' => 'Failed to retreive account details, please check your credentials.',
);

View File

@ -0,0 +1,199 @@
@extends('header')
@section('head')
@parent
<style type="text/css">
table.accounts-table > thead > tr > th.header {
background-color: #e37329 !important;
color:#fff !important;
padding-top:8px;
}
</style>
@stop
@section('content')
@parent
@include('accounts.nav', ['selected' => ACCOUNT_BANKS])
{!! Former::open($url)
->method($method)
->rule()
->addClass('main-form warn-on-exit') !!}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{!! trans($title) !!}</h3>
</div>
<div class="panel-body form-padding-right">
@if ($bankAccount)
{!! Former::populateField('bank_id', $bankAccount->bank_id) !!}
@endif
{!! Former::select('bank_id')
->data_bind('dropdown: bank_id')
->addOption('', '')
->fromQuery($banks, 'name', 'id') !!}
{!! Former::password('bank_username')
->data_bind("value: bank_username, valueUpdate: 'afterkeydown'")
->label(trans('texts.username'))
->blockHelp(trans(Request::secure() ? 'texts.bank_password_help' : 'texts.bank_password_warning')) !!}
</div>
</div>
<div class="modal fade" id="testModal" tabindex="-1" role="dialog" aria-labelledby="testModalLabel" 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="testModalLabel">{!! trans('texts.test_bank_account') !!}</h4>
</div>
<div class="panel-body row">
<div class="form-group" style="padding-bottom:30px">
<label for="username" class="control-label col-lg-4 col-sm-4">{{ trans('texts.password') }}</label>
<div class="col-lg-6 col-sm-6">
<input class="form-control" id="bank_password" name="bank_password" type="password" data-bind="value: bank_password, valueUpdate: 'afterkeydown'"><br/>
</div>
</div>
<div class="col-lg-12 col-sm-12" data-bind="visible: state() == 'loading'">
<div class="progress">
<div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 100%">
</div>
</div>
</div>
<div class="col-lg-12 col-sm-12" data-bind="visible: state() == 'error'">
<div class="alert alert-danger" role="alert">
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
{{ trans('texts.bank_account_error') }}
</div>
</div>
<div class="col-lg-12 col-sm-12" data-bind="visible: bank_accounts().length">
<table class="table table-striped accounts-table">
<thead>
<tr>
<th class="header">{{ trans('texts.account_number') }}</th>
<th class="header">{{ trans('texts.type') }}</th>
<th class="header">{{ trans('texts.balance') }}</th>
</tr>
</thead>
<tbody data-bind="foreach: bank_accounts">
<tr>
<td data-bind="text: account_number"></td>
<td data-bind="text: type"></td>
<td data-bind="text: balance"></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer" style="margin-top: 0px; padding-top:30px;">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ trans('texts.close') }}</button>
<button type="button" class="btn btn-success" onclick="doTest()" data-bind="css: { disabled: disableDoTest }">{{ trans('texts.test') }}</button>
</div>
</div>
</div>
</div>
<p/>&nbsp;<p/>
{!! Former::actions(
count(Cache::get('banks')) > 0 ? Button::normal(trans('texts.cancel'))->large()->asLinkTo(URL::to('/settings/bank_accounts'))->appendIcon(Icon::create('remove-circle')) : false,
(!$bankAccount ?
Button::primary(trans('texts.test'))
->withAttributes([
'data-bind' => 'css: {disabled: disableMainButton}',
'onclick' => 'showTest()'
])
->large()
->appendIcon(Icon::create('download-alt'))
: false),
Button::success(trans('texts.save'))
->submit()->large()
->withAttributes([
'data-bind' => 'css: {disabled: disableMainButton}',
])
->appendIcon(Icon::create('floppy-disk'))) !!}
{!! Former::close() !!}
<script type="text/javascript">
function showTest() {
$('#testModal').modal('show');
}
function doTest() {
model.state('loading');
$.post('{{ URL::to('/bank_accounts/test') }}', $('.main-form').serialize())
.done(function(data) {
model.state('');
data = JSON.parse(data);
if (!data || !data.length) {
model.state('error');
} else {
for (var i=0; i<data.length; i++) {
model.bank_accounts.push(data[i]);
}
}
}).fail(function() {
model.state('error');
});
}
$(function() {
@if ($bankAccount)
$('#bank_id').prop('disabled', true);
@else
$('#bank_id').combobox().on('change', function(e) {
model.bank_id($('#bank_id').val());
});
@endif
$('#testModal').on('shown.bs.modal', function() {
$('#bank_password').focus();
});
});
// Here's my data model
var ViewModel = function() {
var self = this;
self.bank_id = ko.observable({{ $bankAccount ? $bankAccount->bank_id : 0 }});
self.bank_username = ko.observable('{{ $bankAccount ? $bankAccount->username : false }}');
self.bank_password = ko.observable();
self.bank_accounts = ko.observableArray();
self.state = ko.observable(false);
self.disableMainButton = ko.computed(function() {
return !self.bank_id() || !self.bank_username();
}, self);
self.disableDoTest = ko.computed(function() {
return !self.bank_id() || !self.bank_username() || !self.bank_password();
}, self);
$('#bank_id').focus();
};
window.model = new ViewModel();
ko.applyBindings(model);
</script>
@stop

View File

@ -0,0 +1,31 @@
@extends('header')
@section('content')
@parent
@include('accounts.nav', ['selected' => ACCOUNT_BANKS])
{!! Button::primary(trans('texts.add_bank_account'))
->asLinkTo(URL::to('/bank_accounts/create'))
->withAttributes(['class' => 'pull-right'])
->appendIcon(Icon::create('plus-sign')) !!}
@include('partials.bulk_form', ['entityType' => ENTITY_BANK_ACCOUNT])
{!! Datatable::table()
->addColumn(
trans('texts.name'),
trans('texts.integration_type'),
trans('texts.action'))
->setUrl(url('api/bank_accounts/'))
->setOptions('sPaginationType', 'bootstrap')
->setOptions('bFilter', false)
->setOptions('bAutoWidth', false)
->setOptions('aoColumns', [[ "sWidth"=> "50%" ], [ "sWidth"=> "30%" ], ["sWidth"=> "20%"]])
->setOptions('aoColumnDefs', [['bSortable'=>false, 'aTargets'=>[2]]])
->render('datatable') !!}
<script>
window.onDatatableReady = actionListHandler;
</script>
@stop