mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 02:44:29 -04:00
Initial work on OFX support
This commit is contained in:
parent
3d790d29a1
commit
bf778aa616
30
app/Console/Commands/TestOFX.php
Normal file
30
app/Console/Commands/TestOFX.php
Normal 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>";
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@ class Kernel extends ConsoleKernel
|
||||
'App\Console\Commands\CheckData',
|
||||
'App\Console\Commands\SendRenewalInvoices',
|
||||
'App\Console\Commands\SendReminders',
|
||||
'App\Console\Commands\TestOFX',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
152
app/Http/Controllers/BankAccountController.php
Normal file
152
app/Http/Controllers/BankAccountController.php
Normal 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));
|
||||
}
|
||||
|
||||
}
|
@ -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';
|
||||
|
@ -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
225
app/Libraries/OFX.php
Normal 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];
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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
15
app/Models/Bank.php
Normal 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);
|
||||
}
|
||||
}
|
23
app/Models/BankAccount.php
Normal file
23
app/Models/BankAccount.php
Normal 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');
|
||||
}
|
||||
|
||||
}
|
||||
|
24
app/Ninja/Repositories/BankAccountRepository.php
Normal file
24
app/Ninja/Repositories/BankAccountRepository.php
Normal 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');
|
||||
}
|
||||
}
|
107
app/Services/BankAccountService.php
Normal file
107
app/Services/BankAccountService.php
Normal 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");
|
||||
}
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
}
|
@ -89,7 +89,8 @@
|
||||
"App\\": "app/"
|
||||
},
|
||||
"files": [
|
||||
"app/Libraries/lib_autolink.php"
|
||||
"app/Libraries/lib_autolink.php",
|
||||
"app/Libraries/OFX.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
|
56
database/migrations/2016_01_18_195351_add_bank_accounts.php
Normal file
56
database/migrations/2016_01_18_195351_add_bank_accounts.php
Normal 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');
|
||||
}
|
||||
|
||||
}
|
44
database/seeds/BanksSeeder.php
Normal file
44
database/seeds/BanksSeeder.php
Normal 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&cz=702110804131918&cl=3000812',
|
||||
])
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($banks as $bank) {
|
||||
if (!DB::table('banks')->where('remote_id', '=', $bank['remote_id'])->get()) {
|
||||
Bank::create($bank);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
@ -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.',
|
||||
|
||||
);
|
199
resources/views/accounts/bank_account.blade.php
Normal file
199
resources/views/accounts/bank_account.blade.php
Normal 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">×</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/> <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
|
31
resources/views/accounts/banks.blade.php
Normal file
31
resources/views/accounts/banks.blade.php
Normal 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
|
Loading…
x
Reference in New Issue
Block a user