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

wip: changes

This commit is contained in:
paulwer 2023-12-01 14:30:33 +01:00
parent ef48bd150c
commit b54b626332
17 changed files with 1172 additions and 833 deletions

View File

@ -7,9 +7,11 @@
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*
* Documentation of Api-Usage: https://developer.gocardless.com/bank-account-data/overview
*/
namespace App\Helpers\Bank\Yodlee;
namespace App\Helpers\Bank\Nordigen;
use App\Exceptions\NordigenApiException;
use App\Helpers\Bank\Nordigen\Transformer\AccountTransformer;
@ -48,79 +50,143 @@ $requisitionId = $session["requisition_id"];
class Nordigen
{
public bool $test_mode = false;
public bool $test_mode = false; // https://developer.gocardless.com/bank-account-data/sandbox
public string $sandbox_institutionId = "SANDBOXFINANCE_SFIN0000";
protected \Nordigen\NordigenPHP\API\NordigenClient $client;
protected string $secret_id;
protected string $secret_key;
public function __construct()
public function __construct(string $client_id, string $client_secret)
{
$this->secret_id = config('ninja.nordigen.secret_id');
$this->secret_key = config('ninja.nordigen.secret_key');
$this->client = new \Nordigen\NordigenPHP\API\NordigenClient($client_id, $client_secret);
$this->client = new \Nordigen\NordigenPHP\API\NordigenClient($this->secret_id, $this->secret_key);
}
// metadata-section for frontend
public function getInstitutions()
{
if ($this->test_mode)
return (array) $this->client->institution->getInstitution($this->sandbox_institutionId);
return $this->client->institution->getInstitutions();
}
public function getValidAccounts()
// requisition-section
public function createRequisition(string $redirect, string $initutionId)
{
if ($this->test_mode && $initutionId != $this->sandbox_institutionId)
throw new \Exception('invalid institutionId while in test-mode');
return $this->client->requisition->createRequisition($redirect, $initutionId);
}
public function getRequisition(string $requisitionId)
{
return $this->client->requisition->getRequisition($requisitionId);
}
public function cleanupRequisitions()
{
$requisitions = $this->client->requisition->getRequisitions();
foreach ($requisitions as $requisition) {
// filter to expired OR older than 7 days created and no accounts
if ($requisition->status == "EXPIRED" || (sizeOf($requisition->accounts) != 0 && strtotime($requisition->created) > (new \DateTime())->modify('-7 days')))
continue;
$this->client->requisition->deleteRequisition($requisition->id);
}
}
// account-section: these methods should be used to get data of connected accounts
public function getAccounts()
{
// get all valid requisitions
$requisitions = $this->client->requisition->getRequisitions();
// fetch all valid accounts for activated requisitions
$accounts = [];
$nordigen_accountIds = [];
foreach ($requisitions as $requisition) {
foreach ($requisition->accounts as $account) {
$account = $account = $this->client->account($account);
array_push($accounts, $account);
foreach ($requisition->accounts as $accountId) {
array_push($nordigen_accountIds, $accountId);
}
}
return $accounts;
$nordigen_accountIds = array_unique($nordigen_accountIds);
$nordigen_accounts = [];
foreach ($nordigen_accountIds as $accountId) {
$nordigen_account = $this->getAccount($accountId);
array_push($nordigen_accounts, $nordigen_account);
}
return $nordigen_accounts;
}
public function cleanup()
public function getAccount(string $account_id)
{
$out = new \stdClass();
$out->data = $this->client->account($account_id)->getAccountDetails();
$out->metadata = $this->client->account($account_id)->getAccountMetaData();
$out->balances = $this->client->account($account_id)->getAccountBalances();
$out->institution = $this->client->institution->getInstitution($out->metadata["institution_id"]);
$it = new AccountTransformer();
return $it->transform($out);
}
public function isAccountActive(string $account_id)
{
try {
$account = $this->client->account($account_id)->getAccountMetaData();
if ($account["status"] != "READY")
return false;
return true;
} catch (\Exception $e) {
// TODO: check for not-found exception
return false;
}
}
/**
* this method will remove all according requisitions => this can result in removing multiple accounts, if a user reuses a requisition
*/
public function deleteAccount(string $account_id)
{
// get all valid requisitions
$requisitions = $this->client->requisition->getRequisitions();
// TODO: filter to older than 2 days created AND (no accounts or invalid)
// fetch all valid accounts for activated requisitions
foreach ($requisitions as $requisition) {
$this->client->requisition->deleteRequisition($requisition->id);
foreach ($requisition->accounts as $accountId) {
if ($accountId) {
$this->client->requisition->deleteRequisition($accountId);
}
}
}
}
// account-section: these methods should be used to get data of connected accounts
public function getAccountMetaData(string $account_id)
public function getTransactions(string $accountId, string $dateFrom = null)
{
return $this->client->account($account_id)->getAccountMetaData();
}
public function getAccountDetails(string $account_id)
{
return $this->client->account($account_id)->getAccountDetails();
}
return $this->client->account($accountId)->getAccountTransactions($dateFrom);
public function getAccountBalances(string $account_id)
{
return $this->client->account($account_id)->getAccountBalances();
}
public function getAccountTransactions(string $account_id)
{
return $this->client->account($account_id)->getAccountTransactions();
}
}

View File

@ -9,55 +9,79 @@
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Helpers\Bank\Yodlee\Transformer;
namespace App\Helpers\Bank\Nordigen\Transformer;
use App\Helpers\Bank\AccountTransformerInterface;
/**
[0] => stdClass Object
(
[CONTAINER] => bank
[providerAccountId] => 11308693
[accountName] => My CD - 8878
[accountStatus] => ACTIVE
[accountNumber] => xxxx8878
[aggregationSource] => USER
[isAsset] => 1
[balance] => stdClass Object
[data] => stdClass Object
(
[currency] => USD
[amount] => 49778.07
[resourceId] => XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
[iban] => DE0286055592XXXXXXXXXX
[currency] => EUR
[ownerName] => Max Mustermann
[product] => GiroKomfort
[bic] => WELADE8LXXX
[usage] => PRIV
)
[metadata] => stdClass Object
(
[id] => XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
[created] => 2022-12-05T18:41:53.986028Z
[last_accessed] => 2023-10-29T08:35:34.003611Z
[iban] => DE0286055592XXXXXXXXXX
[institution_id] => STADT_KREISSPARKASSE_LEIPZIG_WELADE8LXXX
[status] => READY
[owner_name] => Max Mustermann
)
[balances] => stdClass Object
(
[balances]: [
{
[balanceAmount]: {
[amount] => 9825.64
[currency] => EUR
},
[balanceType] => closingBooked
[referenceDate] => 2023-12-01
},
{
[balanceAmount[: {
[amount] => 10325.64
[currency] => EUR
},
[balanceType] => interimAvailable
[creditLimitIncluded]: true,
[referenceDate] => 2023-12-01
}
]
)
[institution] => stdClass Object
(
[id] => STADT_KREISSPARKASSE_LEIPZIG_WELADE8LXXX
[name] => Stadt- und Kreissparkasse Leipzig
[bic] => WELADE8LXXX
[transaction_total_days] => 360
[countries] => [
"DE"
],
[logo] => https://storage.googleapis.com/gc-prd-institution_icons-production/DE/PNG/sparkasse.png
[supported_payments] => {
[single-payment] => [
"SCT",
"ISCT"
]
},
[supported_features] => [
"card_accounts",
"payments",
"pending_transactions"
],
[identification_codes] => []
)
[id] => 12331861
[includeInNetWorth] => 1
[providerId] => 18769
[providerName] => Dag Site Captcha
[isManual] =>
[currentBalance] => stdClass Object
(
[currency] => USD
[amount] => 49778.07
)
[accountType] => CD
[displayedName] => LORETTA
[createdDate] => 2022-07-28T06:55:33Z
[lastUpdated] => 2022-07-28T06:56:09Z
[dataset] => Array
(
[0] => stdClass Object
(
[name] => BASIC_AGG_DATA
[additionalStatus] => AVAILABLE_DATA_RETRIEVED
[updateEligibility] => ALLOW_UPDATE
[lastUpdated] => 2022-07-28T06:55:50Z
[lastUpdateAttempt] => 2022-07-28T06:55:50Z
)
)
)
)
*/
@ -65,16 +89,15 @@ use App\Helpers\Bank\AccountTransformerInterface;
class AccountTransformer implements AccountTransformerInterface
{
public function transform($yodlee_account)
public function transform($nordigen_account)
{
$data = [];
if(!property_exists($yodlee_account, 'account'))
if (!property_exists($nordigen_account, 'data') || !property_exists($nordigen_account, 'metadata') || !property_exists($nordigen_account, 'balances') || !property_exists($nordigen_account, 'institution'))
return $data;
foreach($yodlee_account->account as $account)
{
foreach ($nordigen_account->account as $account) {
$data[] = $this->transformAccount($account);
}
@ -83,20 +106,28 @@ class AccountTransformer implements AccountTransformerInterface
public function transformAccount($account)
{
$used_balance = $account->balances[0];
// prefer entry with closingBooked
foreach ($account->balances as $entry) {
if ($entry->balanceType === 'closingBooked') { // available: closingBooked, interimAvailable
$used_balance = $entry;
break;
}
}
return [
'id' => $account->id,
'id' => $account->data->id,
'account_type' => $account->CONTAINER,
// 'account_name' => $account->accountName,
'account_name' => property_exists($account, 'accountName') ? $account->accountName : $account->nickname,
'account_status' => $account->accountStatus,
'account_number' => property_exists($account, 'accountNumber') ? '**** ' . substr($account?->accountNumber, -7) : '',
'provider_account_id' => $account->providerAccountId,
'provider_id' => $account->providerId,
'provider_name' => $account->providerName,
'nickname' => property_exists($account, 'nickname') ? $account->nickname : '',
'current_balance' => property_exists($account, 'currentBalance') ? $account->currentBalance->amount : 0,
'account_currency' => property_exists($account, 'currency') ? $account->currentBalance->currency : '',
'account_name' => $account->data->iban,
'account_status' => $account->metadata->status,
'account_number' => '**** ' . substr($account->data->iban, -7),
'provider_account_id' => $account->data->iban,
'provider_id' => $account->institution_id,
'provider_name' => $account->institution->name,
'nickname' => property_exists($account->data, 'owner_name') ? $account->data->owner_name : '',
'current_balance' => (int) property_exists($used_balance, 'balanceAmount') ? $used_balance->balanceAmount->amount : 0,
'account_currency' => property_exists($used_balance, 'balanceAmount') ? $used_balance->balanceAmount->currency : '',
];
}
}

View File

@ -9,8 +9,8 @@
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Helpers\Bank\Yodlee\Transformer;
namespace App\Helpers\Bank\Nordigen\Transformer;
/**
"date": "string",
"sourceId": "string",

View File

@ -9,7 +9,7 @@
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Helpers\Bank\Yodlee\Transformer;
namespace App\Helpers\Bank\Nordigen\Transformer;
use App\Helpers\Bank\BankRevenueInterface;
use App\Utils\Traits\AppSetup;
@ -74,7 +74,7 @@ use Illuminate\Support\Facades\Cache;
"holdingDescription": "string",
"isin": "string",
"status": "POSTED"
(
[CONTAINER] => bank
[id] => 103953585
@ -96,7 +96,7 @@ use Illuminate\Support\Facades\Cache;
[original] => CHEROKEE NATION TAX TA TAHLEQUAH OK
)
[isManual] =>
[isManual] =>
[sourceType] => AGGREGATED
[date] => 2022-08-03
[transactionDate] => 2022-08-03
@ -122,11 +122,10 @@ class IncomeTransformer implements BankRevenueInterface
$data = [];
if(!property_exists($transaction, 'transaction'))
if (!property_exists($transaction, 'transaction'))
return $data;
foreach($transaction->transaction as $transaction)
{
foreach ($transaction->transaction as $transaction) {
$data[] = $this->transformTransaction($transaction);
}
@ -154,7 +153,7 @@ class IncomeTransformer implements BankRevenueInterface
{
//CREDIT / DEBIT
if(property_exists($transaction, 'highLevelCategoryId') && $transaction->highLevelCategoryId == 10000012)
if (property_exists($transaction, 'highLevelCategoryId') && $transaction->highLevelCategoryId == 10000012)
return 'CREDIT';
return 'DEBIT';
@ -166,15 +165,15 @@ class IncomeTransformer implements BankRevenueInterface
$currencies = Cache::get('currencies');
if (! $currencies) {
if (!$currencies) {
$this->buildCache(true);
}
$currency = $currencies->filter(function ($item) use($code){
$currency = $currencies->filter(function ($item) use ($code) {
return $item->code == $code;
})->first();
if($currency)
if ($currency)
return $currency->id;
return 1;

View File

@ -14,13 +14,14 @@ namespace App\Http\Controllers\Bank;
use App\Helpers\Bank\Nordigen\Nordigen;
use App\Http\Controllers\BaseController;
use App\Http\Requests\Yodlee\YodleeAuthRequest;
use App\Jobs\Bank\ProcessBankTransactions;
use App\Jobs\Bank\ProcessBankTransactionsNordigen;
use App\Models\BankIntegration;
use Illuminate\Http\Request;
class YodleeController extends BaseController
class NordigenController extends BaseController
{
// TODO!!!!!
public function auth(YodleeAuthRequest $request)
{
@ -35,21 +36,21 @@ class YodleeController extends BaseController
//ensure user is enterprise!!
if ($company->account->bank_integration_account_id) {
if ($company->account->bank_integration_nordigen_client_id && $company->account->bank_integration_nordigen_client_id) {
$flow = 'edit';
$token = $company->account->bank_integration_account_id;
$token = $company->account->bank_integration_nordigen_client_id;
} else {
$flow = 'add';
$response = $yodlee->createUser($company);
$response = $nordigen->createUser($company);
$token = $response->user->loginName;
$company->account->bank_integration_account_id = $token;
$company->account->bank_integration_nordigen_client_id = $token;
$company->push();
@ -107,7 +108,7 @@ class YodleeController extends BaseController
$company->account->bank_integrations->each(function ($bank_integration) use ($company) {
ProcessBankTransactions::dispatch($company->account->bank_integration_account_id, $bank_integration);
ProcessBankTransactionsNordigen::dispatch($company->account, $bank_integration);
});

View File

@ -14,7 +14,7 @@ namespace App\Http\Controllers\Bank;
use App\Helpers\Bank\Yodlee\Yodlee;
use App\Http\Controllers\BaseController;
use App\Http\Requests\Yodlee\YodleeAuthRequest;
use App\Jobs\Bank\ProcessBankTransactions;
use App\Jobs\Bank\ProcessBankTransactionsYodlee;
use App\Models\BankIntegration;
use Illuminate\Http\Request;
@ -24,7 +24,7 @@ class YodleeController extends BaseController
public function auth(YodleeAuthRequest $request)
{
// create a user at this point
// create a user at this point
// use the one time token here to pull in the actual user
// store the user_account_id on the accounts table
@ -35,14 +35,13 @@ class YodleeController extends BaseController
//ensure user is enterprise!!
if($company->account->bank_integration_account_id){
if ($company->account->bank_integration_yodlee_account_id) {
$flow = 'edit';
$token = $company->account->bank_integration_account_id;
$token = $company->account->bank_integration_yodlee_account_id;
}
else{
} else {
$flow = 'add';
@ -50,15 +49,15 @@ class YodleeController extends BaseController
$token = $response->user->loginName;
$company->account->bank_integration_account_id = $token;
$company->account->bank_integration_yodlee_account_id = $token;
$company->push();
}
$yodlee = new Yodlee($token);
if($request->has('window_closed') && $request->input("window_closed") == "true")
if ($request->has('window_closed') && $request->input("window_closed") == "true")
$this->getAccounts($company, $token);
$data = [
@ -79,13 +78,11 @@ class YodleeController extends BaseController
{
$yodlee = new Yodlee($token);
$accounts = $yodlee->getAccounts();
$accounts = $yodlee->getAccounts();
foreach($accounts as $account)
{
foreach ($accounts as $account) {
if(!BankIntegration::where('bank_account_id', $account['id'])->where('company_id', $company->id)->exists())
{
if (!BankIntegration::where('bank_account_id', $account['id'])->where('company_id', $company->id)->exists()) {
$bank_integration = new BankIntegration();
$bank_integration->company_id = $company->id;
$bank_integration->account_id = $company->account_id;
@ -101,23 +98,23 @@ class YodleeController extends BaseController
$bank_integration->balance = $account['current_balance'];
$bank_integration->currency = $account['account_currency'];
$bank_integration->from_date = now()->subYear();
$bank_integration->save();
}
}
$company->account->bank_integrations->each(function ($bank_integration) use ($company){
ProcessBankTransactions::dispatch($company->account->bank_integration_account_id, $bank_integration);
$company->account->bank_integrations->each(function ($bank_integration) use ($company) {
ProcessBankTransactionsYodlee::dispatch($company->account, $bank_integration);
});
}
/**
/**
* Process Yodlee Refresh Webhook.
*
*
@ -152,70 +149,70 @@ class YodleeController extends BaseController
* )
*/
/*
{
"event":{
"info":"REFRESH.PROCESS_COMPLETED",
"loginName":"fri21",
"data":{
"providerAccount":[
{
"id":10995860,
"providerId":16441,
"isManual":false,
"createdDate":"2017-12-22T05:47:35Z",
"aggregationSource":"USER",
"status":"SUCCESS",
"requestId":"NSyMGo+R4dktywIu3hBIkc3PgWA=",
"dataset":[
{
"name":"BASIC_AGG_DATA",
"additionalStatus":"AVAILABLE_DATA_RETRIEVED",
"updateEligibility":"ALLOW_UPDATE",
"lastUpdated":"2017-12-22T05:48:16Z",
"lastUpdateAttempt":"2017-12-22T05:48:16Z"
}
]
}
]
/*
{
"event":{
"info":"REFRESH.PROCESS_COMPLETED",
"loginName":"fri21",
"data":{
"providerAccount":[
{
"id":10995860,
"providerId":16441,
"isManual":false,
"createdDate":"2017-12-22T05:47:35Z",
"aggregationSource":"USER",
"status":"SUCCESS",
"requestId":"NSyMGo+R4dktywIu3hBIkc3PgWA=",
"dataset":[
{
"name":"BASIC_AGG_DATA",
"additionalStatus":"AVAILABLE_DATA_RETRIEVED",
"updateEligibility":"ALLOW_UPDATE",
"lastUpdated":"2017-12-22T05:48:16Z",
"lastUpdateAttempt":"2017-12-22T05:48:16Z"
}
]
}
]
}
}
}
}*/
}*/
public function refreshWebhook(Request $request)
{
//we should ignore this one
//we should ignore this one
nlog("yodlee refresh");
nlog($request->all());
return response()->json(['message' => 'Success'], 200);
//
// return response()->json(['message' => 'Unauthorized'], 403);
}
/*
{
"event":{
"notificationId":"63c73475-4db5-49ef-8553-8303337ca7c3",
"info":"LATEST_BALANCE_UPDATES",
"loginName":"user1",
"data":{
"providerAccountId":658552,
"latestBalanceEvent":[
{
"accountId":12345,
"status":"SUCCESS"
},
{
"accountId":12346,
"status":"FAILED"
}
]
}
}
}
*/
/*
{
"event":{
"notificationId":"63c73475-4db5-49ef-8553-8303337ca7c3",
"info":"LATEST_BALANCE_UPDATES",
"loginName":"user1",
"data":{
"providerAccountId":658552,
"latestBalanceEvent":[
{
"accountId":12345,
"status":"SUCCESS"
},
{
"accountId":12346,
"status":"FAILED"
}
]
}
}
}
*/
public function balanceWebhook(Request $request)
{
@ -223,79 +220,79 @@ class YodleeController extends BaseController
nlog($request->all());
return response()->json(['message' => 'Success'], 200);
//
// return response()->json(['message' => 'Unauthorized'], 403);
}
/*
{
"event":{
"data":[
{
"autoRefresh":{
"additionalStatus":"SCHEDULED",
"status":"ENABLED"
},
"accountIds":[
1112645899,
1112645898
],
"loginName":"YSL1555332811628",
"providerAccountId":11381459
}
],
"notificationTime":"2019-06-14T04:49:39Z",
"notificationId":"4e672150-156048777",
"info":"AUTO_REFRESH_UPDATES"
}
}
*/
/*
{
"event":{
"data":[
{
"autoRefresh":{
"additionalStatus":"SCHEDULED",
"status":"ENABLED"
},
"accountIds":[
1112645899,
1112645898
],
"loginName":"YSL1555332811628",
"providerAccountId":11381459
}
],
"notificationTime":"2019-06-14T04:49:39Z",
"notificationId":"4e672150-156048777",
"info":"AUTO_REFRESH_UPDATES"
}
}
*/
public function refreshUpdatesWebhook(Request $request)
{
//notifies a user if there are problems with yodlee accessing the data
//notifies a user if there are problems with yodlee accessing the data
nlog("update refresh");
nlog($request->all());
return response()->json(['message' => 'Success'], 200);
//
// return response()->json(['message' => 'Unauthorized'], 403);
}
/*
"event": {
"notificationId": "64b7ed1a-1530523285",
"info": "DATA_UPDATES.USER_DATA",
"data": {
"userCount": 1,
"fromDate": "2017-11-10T10:18:44Z",
"toDate": "2017-11-10T11:18:43Z",
"userData": [{
"user": {
"loginName": "YSL1484052178554"
},
"links": [{
"methodType": "GET",
"rel": "getUserData",
"href": "dataExtracts/userData?fromDate=2017-11-10T10:18:44Z&toDate=2017-11-10T11:18:43Z&loginName=YSL1484052178554"
/*
"event": {
"notificationId": "64b7ed1a-1530523285",
"info": "DATA_UPDATES.USER_DATA",
"data": {
"userCount": 1,
"fromDate": "2017-11-10T10:18:44Z",
"toDate": "2017-11-10T11:18:43Z",
"userData": [{
"user": {
"loginName": "YSL1484052178554"
},
"links": [{
"methodType": "GET",
"rel": "getUserData",
"href": "dataExtracts/userData?fromDate=2017-11-10T10:18:44Z&toDate=2017-11-10T11:18:43Z&loginName=YSL1484052178554"
}]
}]
}]
}
}
}
*/
*/
public function dataUpdatesWebhook(Request $request)
{
//this is the main hook we use for notifications
//this is the main hook we use for notifications
nlog("data refresh");
nlog($request->all());
return response()->json(['message' => 'Success'], 200);
//
// return response()->json(['message' => 'Unauthorized'], 403);

View File

@ -14,6 +14,7 @@ namespace App\Http\Controllers;
use App\Factory\BankIntegrationFactory;
use App\Filters\BankIntegrationFilters;
use App\Helpers\Bank\Yodlee\Yodlee;
use App\Helpers\Bank\Nordigen\Nordigen;
use App\Http\Requests\BankIntegration\AdminBankIntegrationRequest;
use App\Http\Requests\BankIntegration\BulkBankIntegrationRequest;
use App\Http\Requests\BankIntegration\CreateBankIntegrationRequest;
@ -22,7 +23,9 @@ use App\Http\Requests\BankIntegration\EditBankIntegrationRequest;
use App\Http\Requests\BankIntegration\ShowBankIntegrationRequest;
use App\Http\Requests\BankIntegration\StoreBankIntegrationRequest;
use App\Http\Requests\BankIntegration\UpdateBankIntegrationRequest;
use App\Jobs\Bank\ProcessBankTransactions;
use App\Jobs\Bank\ProcessBankTransactionsYodlee;
use App\Jobs\Bank\ProcessBankTransactionsNordigen;
use App\Models\Account;
use App\Models\BankIntegration;
use App\Repositories\BankIntegrationRepository;
use App\Services\Bank\BankMatchingService;
@ -471,7 +474,7 @@ class BankIntegrationController extends BaseController
$action = request()->input('action');
$ids = request()->input('ids');
$bank_integrations = BankIntegration::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get();
$bank_integrations->each(function ($bank_integration, $key) use ($action) {
@ -521,23 +524,45 @@ class BankIntegrationController extends BaseController
*/
public function refreshAccounts(AdminBankIntegrationRequest $request)
{
// As yodlee is the first integration we don't need to perform switches yet, however
// if we add additional providers we can reuse this class
$account = auth()->user()->account;
$bank_account_id = auth()->user()->account->bank_integration_account_id;
$this->refreshAccountsYodlee($account);
if(!$bank_account_id)
$this->refreshAccountsNordigen($account);
if (Cache::get("throttle_polling:{$account->key}"))
return response()->json(BankIntegration::query()->company(), 200);
// Processing transactions for each bank account
$account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_YODLEE)->each(function ($bank_integration) use ($account) {
ProcessBankTransactionsYodlee::dispatch($account, $bank_integration);
});
$account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_NORDIGEN)->each(function ($bank_integration) use ($account) {
ProcessBankTransactionsNordigen::dispatch($account, $bank_integration);
});
Cache::put("throttle_polling:{$account->key}", true, 300);
return response()->json(BankIntegration::query()->company(), 200);
}
private function refreshAccountsYodlee(Account $account)
{
if (!$account->bank_integration_yodlee_account_id)
return response()->json(['message' => 'Not yet authenticated with Bank Integration service'], 400);
$yodlee = new Yodlee($bank_account_id);
$yodlee = new Yodlee($account->bank_integration_yodlee_account_id);
$accounts = $yodlee->getAccounts();
$accounts = $yodlee->getAccounts();
foreach($accounts as $account)
{
foreach ($accounts as $account) {
if(!BankIntegration::where('bank_account_id', $account['id'])->where('company_id', auth()->user()->company()->id)->exists())
{
if (!BankIntegration::where("integration_type", BankIntegration::INTEGRATION_TYPE_YODLEE)->where('bank_account_id', $account['id'])->where('company_id', auth()->user()->company()->id)->exists()) {
$bank_integration = new BankIntegration();
$bank_integration->company_id = auth()->user()->company()->id;
$bank_integration->account_id = auth()->user()->account_id;
@ -552,26 +577,44 @@ class BankIntegrationController extends BaseController
$bank_integration->nickname = $account['nickname'];
$bank_integration->balance = $account['current_balance'];
$bank_integration->currency = $account['account_currency'];
$bank_integration->save();
}
}
}
private function refreshAccountsNordigen(Account $account)
{
$account = auth()->user()->account;
if(Cache::get("throttle_polling:{$account->key}"))
return response()->json(BankIntegration::query()->company(), 200);
if (!$account->bank_integration_nordigen_client_id || !$account->bank_integration_nordigen_client_secret)
return response()->json(['message' => 'Not yet authenticated with Bank Integration service'], 400);
$account->bank_integrations->each(function ($bank_integration) use ($account){
ProcessBankTransactions::dispatch($account->bank_integration_account_id, $bank_integration);
$nordigen = new Nordigen($account->bank_integration_nordigen_client_id, $account->bank_integration_nordigen_client_secret);
});
$accounts = $nordigen->getAccounts();
Cache::put("throttle_polling:{$account->key}", true, 300);
foreach ($accounts as $account) {
return response()->json(BankIntegration::query()->company(), 200);
if (!BankIntegration::where("integration_type", BankIntegration::INTEGRATION_TYPE_NORDIGEN)->where('bank_account_id', $account['id'])->where('company_id', auth()->user()->company()->id)->exists()) {
$bank_integration = new BankIntegration();
$bank_integration->company_id = auth()->user()->company()->id;
$bank_integration->account_id = auth()->user()->account_id;
$bank_integration->user_id = auth()->user()->id;
$bank_integration->bank_account_id = $account['id'];
$bank_integration->bank_account_type = $account['account_type'];
$bank_integration->bank_account_name = $account['account_name'];
$bank_integration->bank_account_status = $account['account_status'];
$bank_integration->bank_account_number = $account['account_number'];
$bank_integration->provider_id = $account['provider_id'];
$bank_integration->provider_name = $account['provider_name'];
$bank_integration->nickname = $account['nickname'];
$bank_integration->balance = $account['current_balance'];
$bank_integration->currency = $account['account_currency'];
$bank_integration->save();
}
}
}
/**
@ -614,20 +657,36 @@ class BankIntegrationController extends BaseController
public function removeAccount(AdminBankIntegrationRequest $request, $acc_id)
{
$bank_account_id = auth()->user()->account->bank_integration_account_id;
$account = auth()->user()->account;
if(!$bank_account_id)
$bank_integration = BankIntegration::withTrashed()->where('bank_account_id', $acc_id)->company()->firstOrFail();
if ($bank_integration->integration_type == BankIntegration::INTEGRATION_TYPE_YODLEE)
$this->removeAccountYodlee($account, $bank_integration);
else if ($bank_integration->integration_type == BankIntegration::INTEGRATION_TYPE_NORDIGEN)
$this->removeAccountNordigen($account, $bank_integration);
$this->bank_integration_repo->delete($bank_integration);
return $this->itemResponse($bank_integration->fresh());
}
private function removeAccountYodlee(Account $account, BankIntegration $bank_integration)
{
if (!$account->bank_integration_yodlee_account_id)
return response()->json(['message' => 'Not yet authenticated with Bank Integration service'], 400);
$bi = BankIntegration::withTrashed()->where('bank_account_id', $acc_id)->company()->firstOrFail();
$yodlee = new Yodlee($account->bank_integration_yodlee_account_id);
$yodlee->deleteAccount($bank_integration->bank_account_id);
}
$yodlee = new Yodlee($bank_account_id);
$res = $yodlee->deleteAccount($acc_id);
$this->bank_integration_repo->delete($bi);
return $this->itemResponse($bi->fresh());
private function removeAccountNordigen(Account $account, BankIntegration $bank_integration)
{
if (!$account->bank_integration_nordigen_client_id || !$account->bank_integration_nordigen_client_secret)
return response()->json(['message' => 'Not yet authenticated with Bank Integration service'], 400);
$nordigen = new Nordigen($account->bank_integration_nordigen_client_id, $account->bank_integration_nordigen_client_secret);
$nordigen->deleteAccount($bank_integration->bank_account_id);
}
@ -669,14 +728,21 @@ class BankIntegrationController extends BaseController
*/
public function getTransactions(AdminBankIntegrationRequest $request)
{
// Yodlee
auth()->user()->account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_YODLEE)->each(function ($bank_integration) {
auth()->user()->account->bank_integrations->each(function ($bank_integration) {
(new ProcessBankTransactions(auth()->user()->account->bank_integration_account_id, $bank_integration))->handle();
(new ProcessBankTransactionsYodlee(auth()->user()->account, $bank_integration))->handle();
});
// Nordigen
auth()->user()->account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_NORDIGEN)->each(function ($bank_integration) {
(new ProcessBankTransactionsYodlee(auth()->user()->account, $bank_integration))->handle();
});
return response()->json(['message' => 'Fetching transactions....'], 200);
}
}
}

View File

@ -59,7 +59,7 @@ class MatchBankTransactions implements ShouldQueue
private $categories;
private float $available_balance = 0;
private float $applied_amount = 0;
private array $attachable_invoices = [];
@ -86,6 +86,7 @@ class MatchBankTransactions implements ShouldQueue
*
* @return void
*/
// TODO: what are these categories, and for what do we need them
public function handle()
{
@ -93,33 +94,31 @@ class MatchBankTransactions implements ShouldQueue
$this->company = Company::find($this->company_id);
if($this->company->account->bank_integration_account_id)
$yodlee = new Yodlee($this->company->account->bank_integration_account_id);
if ($this->company->account->bank_integration_yodlee_account_id)
$yodlee = new Yodlee($this->company->account->bank_integration_yodlee_account_id);
else
$yodlee = false;
$bank_categories = Cache::get('bank_categories');
if(!$bank_categories && $yodlee){
if (!$bank_categories && $yodlee) {
$_categories = $yodlee->getTransactionCategories();
$this->categories = collect($_categories->transactionCategory);
Cache::forever('bank_categories', $this->categories);
}
else {
} else {
$this->categories = collect($bank_categories);
}
foreach($this->input as $input)
{
foreach ($this->input as $input) {
nlog($input);
if(array_key_exists('invoice_ids', $input) && strlen($input['invoice_ids']) >= 1)
if (array_key_exists('invoice_ids', $input) && strlen($input['invoice_ids']) >= 1)
$this->matchInvoicePayment($input);
elseif(array_key_exists('payment_id', $input) && strlen($input['payment_id']) >= 1)
elseif (array_key_exists('payment_id', $input) && strlen($input['payment_id']) >= 1)
$this->linkPayment($input);
elseif(array_key_exists('expense_id', $input) && strlen($input['expense_id']) >= 1)
elseif (array_key_exists('expense_id', $input) && strlen($input['expense_id']) >= 1)
$this->linkExpense($input);
elseif((array_key_exists('vendor_id', $input) && strlen($input['vendor_id']) >= 1) || (array_key_exists('ninja_category_id', $input) && strlen($input['ninja_category_id']) >= 1))
elseif ((array_key_exists('vendor_id', $input) && strlen($input['vendor_id']) >= 1) || (array_key_exists('ninja_category_id', $input) && strlen($input['ninja_category_id']) >= 1))
$this->matchExpense($input);
}
@ -133,28 +132,27 @@ class MatchBankTransactions implements ShouldQueue
$invoices = explode(",", $invoice_hashed_ids);
if(count($invoices) >= 1)
{
if (count($invoices) >= 1) {
foreach($invoices as $invoice){
foreach ($invoices as $invoice) {
if(is_string($invoice) && strlen($invoice) > 1)
if (is_string($invoice) && strlen($invoice) > 1)
$collection->push($this->decodePrimaryKey($invoice));
}
}
return $collection;
}
private function checkPayable($invoices) :bool
private function checkPayable($invoices): bool
{
foreach($invoices as $invoice){
foreach ($invoices as $invoice) {
$invoice->service()->markSent();
if(!$invoice->isPayable())
if (!$invoice->isPayable())
return false;
}
@ -168,12 +166,12 @@ class MatchBankTransactions implements ShouldQueue
$this->bt = BankTransaction::find($input['id']);
if(!$this->bt || $this->bt->status_id == BankTransaction::STATUS_CONVERTED)
if (!$this->bt || $this->bt->status_id == BankTransaction::STATUS_CONVERTED)
return $this;
$expense = Expense::withTrashed()->find($input['expense_id']);
if($expense && !$expense->transaction_id) {
if ($expense && !$expense->transaction_id) {
$expense->transaction_id = $this->bt->id;
$expense->save();
@ -187,7 +185,7 @@ class MatchBankTransactions implements ShouldQueue
$this->bts->push($this->bt->id);
}
return $this;
}
@ -197,12 +195,12 @@ class MatchBankTransactions implements ShouldQueue
$this->bt = BankTransaction::find($input['id']);
if(!$this->bt || $this->bt->status_id == BankTransaction::STATUS_CONVERTED)
if (!$this->bt || $this->bt->status_id == BankTransaction::STATUS_CONVERTED)
return $this;
$payment = Payment::withTrashed()->find($input['payment_id']);
if($payment && !$payment->transaction_id) {
if ($payment && !$payment->transaction_id) {
$payment->transaction_id = $this->bt->id;
$payment->save();
@ -218,18 +216,18 @@ class MatchBankTransactions implements ShouldQueue
return $this;
}
private function matchInvoicePayment($input) :self
{
private function matchInvoicePayment($input): self
{
$this->bt = BankTransaction::find($input['id']);
if(!$this->bt || $this->bt->status_id == BankTransaction::STATUS_CONVERTED)
return $this;
if (!$this->bt || $this->bt->status_id == BankTransaction::STATUS_CONVERTED)
return $this;
$_invoices = Invoice::withTrashed()->find($this->getInvoices($input['invoice_ids']));
$amount = $this->bt->amount;
if($_invoices && $this->checkPayable($_invoices)){
if ($_invoices && $this->checkPayable($_invoices)) {
$this->createPayment($_invoices, $amount);
@ -240,13 +238,13 @@ class MatchBankTransactions implements ShouldQueue
return $this;
}
private function matchExpense($input) :self
{
private function matchExpense($input): self
{
//if there is a category id, pull it from Yodlee and insert - or just reuse!!
$this->bt = BankTransaction::find($input['id']);
if(!$this->bt || $this->bt->status_id == BankTransaction::STATUS_CONVERTED)
return $this;
if (!$this->bt || $this->bt->status_id == BankTransaction::STATUS_CONVERTED)
return $this;
$expense = ExpenseFactory::create($this->bt->company_id, $this->bt->user_id);
$expense->category_id = $this->resolveCategory($input);
@ -258,7 +256,7 @@ class MatchBankTransactions implements ShouldQueue
$expense->transaction_reference = $this->bt->description;
$expense->transaction_id = $this->bt->id;
if(array_key_exists('vendor_id', $input))
if (array_key_exists('vendor_id', $input))
$expense->vendor_id = $input['vendor_id'];
$expense->invoice_documents = $this->company->invoice_expense_documents;
@ -267,9 +265,9 @@ class MatchBankTransactions implements ShouldQueue
$this->bt->expense_id = $expense->id;
if(array_key_exists('vendor_id', $input))
if (array_key_exists('vendor_id', $input))
$this->bt->vendor_id = $input['vendor_id'];
$this->bt->status_id = BankTransaction::STATUS_CONVERTED;
$this->bt->save();
@ -278,52 +276,48 @@ class MatchBankTransactions implements ShouldQueue
return $this;
}
private function createPayment($invoices, float $amount) :void
private function createPayment($invoices, float $amount): void
{
$this->available_balance = $amount;
\DB::connection(config('database.default'))->transaction(function () use($invoices) {
\DB::connection(config('database.default'))->transaction(function () use ($invoices) {
$invoices->each(function ($invoice) use ($invoices) {
$invoices->each(function ($invoice) use ($invoices){
$this->invoice = Invoice::withTrashed()->where('id', $invoice->id)->lockForUpdate()->first();
$_amount = false;
$_amount = false;
if(floatval($this->invoice->balance) < floatval($this->available_balance) && $this->available_balance > 0)
{
$_amount = $this->invoice->balance;
$this->applied_amount += $this->invoice->balance;
$this->available_balance = $this->available_balance - $this->invoice->balance;
}
elseif(floatval($this->invoice->balance) >= floatval($this->available_balance) && $this->available_balance > 0)
{
$_amount = $this->available_balance;
$this->applied_amount += $this->available_balance;
$this->available_balance = 0;
}
if (floatval($this->invoice->balance) < floatval($this->available_balance) && $this->available_balance > 0) {
$_amount = $this->invoice->balance;
$this->applied_amount += $this->invoice->balance;
$this->available_balance = $this->available_balance - $this->invoice->balance;
} elseif (floatval($this->invoice->balance) >= floatval($this->available_balance) && $this->available_balance > 0) {
$_amount = $this->available_balance;
$this->applied_amount += $this->available_balance;
$this->available_balance = 0;
}
if($_amount)
{
if ($_amount) {
$this->attachable_invoices[] = ['id' => $this->invoice->id, 'amount' => $_amount];
$this->attachable_invoices[] = ['id' => $this->invoice->id, 'amount' => $_amount];
$this->invoice
->service()
->setExchangeRate()
->updateBalance($_amount * -1)
->updatePaidToDate($_amount)
->setCalculatedStatus()
->save();
}
$this->invoice
->service()
->setExchangeRate()
->updateBalance($_amount * -1)
->updatePaidToDate($_amount)
->setCalculatedStatus()
->save();
}
});
});
}, 2);
if(!$this->invoice)
if (!$this->invoice)
return;
/* Create Payment */
$payment = PaymentFactory::create($this->invoice->company_id, $this->invoice->user_id);
@ -336,7 +330,7 @@ class MatchBankTransactions implements ShouldQueue
$payment->currency_id = $this->bt->currency_id;
$payment->is_manual = false;
$payment->date = $this->bt->date ? Carbon::parse($this->bt->date) : now();
/* Bank Transfer! */
$payment_type_id = 1;
@ -344,15 +338,14 @@ class MatchBankTransactions implements ShouldQueue
$payment->saveQuietly();
$payment->service()->applyNumber()->save();
if($payment->client->getSetting('send_email_on_mark_paid'))
if ($payment->client->getSetting('send_email_on_mark_paid'))
$payment->service()->sendEmail();
$this->setExchangeRate($payment);
/* Create a payment relationship to the invoice entity */
foreach($this->attachable_invoices as $attachable_invoice)
{
foreach ($this->attachable_invoices as $attachable_invoice) {
$payment->invoices()->attach($attachable_invoice['id'], [
'amount' => $attachable_invoice['amount'],
@ -365,24 +358,24 @@ class MatchBankTransactions implements ShouldQueue
$this->invoice->next_send_date = null;
$this->invoice
->service()
->applyNumber()
->touchPdf()
->save();
->service()
->applyNumber()
->touchPdf()
->save();
$payment->ledger()
->updatePaymentBalance($amount * -1);
->updatePaymentBalance($amount * -1);
$this->invoice
->client
->service()
->updateBalanceAndPaidToDate($this->applied_amount*-1, $amount)
->save();
->client
->service()
->updateBalanceAndPaidToDate($this->applied_amount * -1, $amount)
->save();
$this->invoice = $this->invoice
->service()
->workFlow()
->save();
->service()
->workFlow()
->save();
/* Update Invoice balance */
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
@ -394,24 +387,23 @@ class MatchBankTransactions implements ShouldQueue
$this->bt->save();
}
private function resolveCategory($input) :?int
private function resolveCategory($input): ?int
{
if(array_key_exists('ninja_category_id', $input) && (int)$input['ninja_category_id'] > 1){
if (array_key_exists('ninja_category_id', $input) && (int) $input['ninja_category_id'] > 1) {
$this->bt->ninja_category_id = $input['ninja_category_id'];
$this->bt->save();
return (int)$input['ninja_category_id'];
return (int) $input['ninja_category_id'];
}
$category = $this->categories->firstWhere('highLevelCategoryId', $this->bt->category_id);
$ec = ExpenseCategory::where('company_id', $this->bt->company_id)->where('bank_category_id', $this->bt->category_id)->first();
if($ec)
if ($ec)
return $ec->id;
if($category)
{
if ($category) {
$ec = ExpenseCategoryFactory::create($this->bt->company_id, $this->bt->user_id);
$ec->bank_category_id = $this->bt->category_id;
$ec->name = $category->highLevelCategoryName;
@ -419,7 +411,7 @@ class MatchBankTransactions implements ShouldQueue
return $ec->id;
}
return null;
}
@ -452,4 +444,4 @@ class MatchBankTransactions implements ShouldQueue
}
}

View File

@ -0,0 +1,166 @@
<?php
/**
* Credit Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Credit Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Jobs\Bank;
use App\Helpers\Bank\Nordigen\Nordigen;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\Company;
use App\Services\Bank\BankMatchingService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
class ProcessBankTransactionsNordigen implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private Account $account;
private BankIntegration $bank_integration;
private ?string $from_date;
private bool $stop_loop = true;
private int $skip = 0;
public Company $company;
/**
* Create a new job instance.
*/
public function __construct(Account $account, BankIntegration $bank_integration)
{
$this->account = $account;
$this->bank_integration = $bank_integration;
$this->from_date = $bank_integration->from_date;
$this->company = $this->bank_integration->company;
if ($this->bank_integration->integration_type != BankIntegration::INTEGRATION_TYPE_NORDIGEN)
throw new \Exception("Invalid BankIntegration Type");
}
/**
* Execute the job.
*
*
* @return void
*/
public function handle()
{
set_time_limit(0);
//Loop through everything until we are up to date
$this->from_date = $this->from_date ?: '2021-01-01';
do {
try {
$this->processTransactions();
} catch (\Exception $e) {
nlog("{$this->account->bank_integration_nordigen_client_id} - exited abnormally => " . $e->getMessage());
return;
}
}
while ($this->stop_loop);
BankMatchingService::dispatch($this->company->id, $this->company->db);
}
private function processTransactions()
{
$nordigen = new Nordigen($this->account->bank_integration_nordigen_client_id, $this->account->bank_integration_nordigen_client_secret); // TODO: maybe implement credentials
if (!$nordigen->isAccountActive($this->bank_integration->bank_account_id)) {
$this->bank_integration->disabled_upstream = true;
$this->bank_integration->save();
$this->stop_loop = false;
return;
}
$data = [
'top' => 500,
'fromDate' => $this->from_date,
'accountId' => $this->bank_integration->bank_account_id,
'skip' => $this->skip,
];
//Get transaction count object
$transactions = $nordigen->getTransactions($this->bank_integration->bank_account_id, $this->from_date);
//Get int count
$count = sizeof($transactions->transactions->booked);
//if no transactions, update the from_date and move on
if (count($transactions) == 0) {
$this->bank_integration->from_date = now()->subDays(2);
$this->bank_integration->disabled_upstream = false;
$this->bank_integration->save();
$this->stop_loop = false;
return;
}
//Harvest the company
MultiDB::setDb($this->company->db);
/*Get the user */
$user_id = $this->company->owner()->id;
/* Unguard the model to perform batch inserts */
BankTransaction::unguard();
$now = now();
foreach ($transactions as $transaction) {
if (BankTransaction::where('transaction_id', $transaction['transaction_id'])->where('company_id', $this->company->id)->withTrashed()->exists())
continue;
//this should be much faster to insert than using ::create()
$bt = \DB::table('bank_transactions')->insert(
array_merge($transaction, [
'company_id' => $this->company->id,
'user_id' => $user_id,
'bank_integration_id' => $this->bank_integration->id,
'created_at' => $now,
'updated_at' => $now,
])
);
}
$this->skip = $this->skip + 500;
if ($count < 500) {
$this->stop_loop = false;
$this->bank_integration->from_date = now()->subDays(2);
$this->bank_integration->save();
}
}
}

View File

@ -11,8 +11,10 @@
namespace App\Jobs\Bank;
use App\Helpers\Bank\Yodlee\Nordigen;
use App\Helpers\Bank\Yodlee\Yodlee;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\Company;
@ -24,11 +26,11 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
class ProcessBankTransactions implements ShouldQueue
class ProcessBankTransactionsYodlee implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private string $bank_integration_account_id;
private Account $account;
private BankIntegration $bank_integration;
@ -43,13 +45,16 @@ class ProcessBankTransactions implements ShouldQueue
/**
* Create a new job instance.
*/
public function __construct(string $bank_integration_account_id, BankIntegration $bank_integration)
public function __construct(Account $account, BankIntegration $bank_integration)
{
$this->bank_integration_account_id = $bank_integration_account_id;
$this->account = $account;
$this->bank_integration = $bank_integration;
$this->from_date = $bank_integration->from_date;
$this->company = $this->bank_integration->company;
if ($this->bank_integration->integration_type != BankIntegration::INTEGRATION_TYPE_YODLEE)
throw new \Exception("Invalid BankIntegration Type");
}
/**
@ -66,18 +71,17 @@ class ProcessBankTransactions implements ShouldQueue
//Loop through everything until we are up to date
$this->from_date = $this->from_date ?: '2021-01-01';
do{
do {
try {
$this->processTransactions();
}
catch(\Exception $e) {
nlog("{$this->bank_integration_account_id} - exited abnormally => ". $e->getMessage());
} catch (\Exception $e) {
nlog("{$this->account->bank_integration_yodlee_account_id} - exited abnormally => " . $e->getMessage());
return;
}
}
while($this->stop_loop);
while ($this->stop_loop);
BankMatchingService::dispatch($this->company->id, $this->company->db);
@ -87,14 +91,13 @@ class ProcessBankTransactions implements ShouldQueue
private function processTransactions()
{
$yodlee = new Yodlee($this->bank_integration_account_id);
$yodlee = new Yodlee($this->account->bank_integration_yodlee_account_id);
if(!$yodlee->getAccount($this->bank_integration->bank_account_id))
{
$this->bank_integration->disabled_upstream = true;
$this->bank_integration->save();
$this->stop_loop = false;
return;
if (!$yodlee->getAccount($this->bank_integration->bank_account_id)) {
$this->bank_integration->disabled_upstream = true;
$this->bank_integration->save();
$this->stop_loop = false;
return;
}
$data = [
@ -111,10 +114,10 @@ class ProcessBankTransactions implements ShouldQueue
$count = $transaction_count->transaction->TOTAL->count;
//get transactions array
$transactions = $yodlee->getTransactions($data);
$transactions = $yodlee->getTransactions($data);
//if no transactions, update the from_date and move on
if(count($transactions) == 0){
if (count($transactions) == 0) {
$this->bank_integration->from_date = now()->subDays(2);
$this->bank_integration->disabled_upstream = false;
@ -129,21 +132,20 @@ class ProcessBankTransactions implements ShouldQueue
/*Get the user */
$user_id = $this->company->owner()->id;
/* Unguard the model to perform batch inserts */
BankTransaction::unguard();
$now = now();
foreach($transactions as $transaction)
{
if(BankTransaction::where('transaction_id', $transaction['transaction_id'])->where('company_id', $this->company->id)->withTrashed()->exists())
foreach ($transactions as $transaction) {
if (BankTransaction::where('transaction_id', $transaction['transaction_id'])->where('company_id', $this->company->id)->withTrashed()->exists())
continue;
//this should be much faster to insert than using ::create()
$bt = \DB::table('bank_transactions')->insert(
array_merge($transaction,[
array_merge($transaction, [
'company_id' => $this->company->id,
'user_id' => $user_id,
'bank_integration_id' => $this->bank_integration->id,
@ -157,7 +159,7 @@ class ProcessBankTransactions implements ShouldQueue
$this->skip = $this->skip + 500;
if($count < 500){
if ($count < 500) {
$this->stop_loop = false;
$this->bank_integration->from_date = now()->subDays(2);
$this->bank_integration->save();
@ -166,4 +168,4 @@ class ProcessBankTransactions implements ShouldQueue
}
}
}

View File

@ -11,9 +11,11 @@
namespace App\Jobs\Ninja;
use App\Jobs\Bank\ProcessBankTransactions;
use App\Jobs\Bank\ProcessBankTransactionsYodlee;
use App\Jobs\Bank\ProcessBankTransactionsNordigen;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\BankIntegration;
use App\Utils\Ninja;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -46,28 +48,38 @@ class BankTransactionSync implements ShouldQueue
{
//multiDB environment, need to
foreach (MultiDB::$dbs as $db)
{
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
nlog("syncing transactions");
nlog("syncing transactions - yodlee");
$a = Account::with('bank_integrations')->whereNotNull('bank_integration_account_id')->cursor()->each(function ($account){
Account::with('bank_integrations')->whereNotNull('bank_integration_yodlee_account_id')->cursor()->each(function ($account) {
// $queue = Ninja::isHosted() ? 'bank' : 'default';
if ($account->isPaid() && $account->plan == 'enterprise') {
if($account->isPaid() && $account->plan == 'enterprise')
{
$account->bank_integrations()->where('integration_type', BankIntegration::INTEGRATION_TYPE_YODLEE)->andWhere('auto_sync', true)->cursor()->each(function ($bank_integration) use ($account) {
$account->bank_integrations()->where('auto_sync', true)->cursor()->each(function ($bank_integration) use ($account){
(new ProcessBankTransactions($account->bank_integration_account_id, $bank_integration))->handle();
(new ProcessBankTransactionsYodlee($account, $bank_integration))->handle();
});
});
}
}
});
nlog("syncing transactions - nordigen");
Account::with('bank_integrations')->whereNotNull('bank_integration_nordigen_client_id')->andWhereNotNull('bank_integration_nordigen_client_secret')->cursor()->each(function ($account) {
$account->bank_integrations()->where('integration_type', BankIntegration::INTEGRATION_TYPE_NORDIGEN)->andWhere('auto_sync', true)->cursor()->each(function ($bank_integration) use ($account) {
(new ProcessBankTransactionsNordigen($account, $bank_integration))->handle();
});
});
nlog("syncing transactions - done");
}
}

View File

@ -60,6 +60,8 @@ class Account extends BaseModel
'set_react_as_default_ap',
'inapp_transaction_id',
'num_users',
'bank_integration_nordigen_client_id',
'bank_integration_nordigen_client_secret',
];
/**
@ -156,7 +158,7 @@ class Account extends BaseModel
public function getPlan()
{
if(Carbon::parse($this->plan_expires)->lt(now()))
if (Carbon::parse($this->plan_expires)->lt(now()))
return '';
return $this->plan ?: '';
@ -165,7 +167,7 @@ class Account extends BaseModel
public function hasFeature($feature)
{
$plan_details = $this->getPlanDetails();
$self_host = ! Ninja::isNinja();
$self_host = !Ninja::isNinja();
switch ($feature) {
@ -187,35 +189,35 @@ class Account extends BaseModel
case self::FEATURE_API:
case self::FEATURE_CLIENT_PORTAL_PASSWORD:
case self::FEATURE_CUSTOM_URL:
return $self_host || ! empty($plan_details);
return $self_host || !empty($plan_details);
// Pro; No trial allowed, unless they're trialing enterprise with an active pro plan
case self::FEATURE_MORE_CLIENTS:
return $self_host || ! empty($plan_details) && (! $plan_details['trial'] || ! empty($this->getPlanDetails(false, false)));
return $self_host || !empty($plan_details) && (!$plan_details['trial'] || !empty($this->getPlanDetails(false, false)));
// White Label
case self::FEATURE_WHITE_LABEL:
if (! $self_host && $plan_details && ! $plan_details['expires']) {
if (!$self_host && $plan_details && !$plan_details['expires']) {
return false;
}
// Fallthrough
// no break
// Fallthrough
// no break
case self::FEATURE_REMOVE_CREATED_BY:
return ! empty($plan_details); // A plan is required even for self-hosted users
return !empty($plan_details); // A plan is required even for self-hosted users
// Enterprise; No Trial allowed; grandfathered for old pro users
case self::FEATURE_USERS:// Grandfathered for old Pro users
case self::FEATURE_USERS: // Grandfathered for old Pro users
if ($plan_details && $plan_details['trial']) {
// Do they have a non-trial plan?
$plan_details = $this->getPlanDetails(false, false);
}
return $self_host || ! empty($plan_details) && ($plan_details['plan'] == self::PLAN_ENTERPRISE);
return $self_host || !empty($plan_details) && ($plan_details['plan'] == self::PLAN_ENTERPRISE);
// Enterprise; No Trial allowed
case self::FEATURE_DOCUMENTS:
case self::FEATURE_USER_PERMISSIONS:
return $self_host || ! empty($plan_details) && $plan_details['plan'] == self::PLAN_ENTERPRISE && ! $plan_details['trial'];
return $self_host || !empty($plan_details) && $plan_details['plan'] == self::PLAN_ENTERPRISE && !$plan_details['trial'];
default:
return false;
@ -224,16 +226,16 @@ class Account extends BaseModel
public function isPaid()
{
return Ninja::isNinja() ? ($this->isPaidHostedClient() && ! $this->isTrial()) : $this->hasFeature(self::FEATURE_WHITE_LABEL);
return Ninja::isNinja() ? ($this->isPaidHostedClient() && !$this->isTrial()) : $this->hasFeature(self::FEATURE_WHITE_LABEL);
}
public function isPaidHostedClient()
{
if (! Ninja::isNinja()) {
if (!Ninja::isNinja()) {
return false;
}
if($this->plan_expires && Carbon::parse($this->plan_expires)->lt(now()))
if ($this->plan_expires && Carbon::parse($this->plan_expires)->lt(now()))
return false;
return $this->plan == 'pro' || $this->plan == 'enterprise';
@ -241,11 +243,11 @@ class Account extends BaseModel
public function isFreeHostedClient()
{
if (! Ninja::isNinja()) {
if (!Ninja::isNinja()) {
return false;
}
if($this->plan_expires && Carbon::parse($this->plan_expires)->lt(now()))
if ($this->plan_expires && Carbon::parse($this->plan_expires)->lt(now()))
return true;
return $this->plan == 'free' || is_null($this->plan) || empty($this->plan);
@ -253,7 +255,7 @@ class Account extends BaseModel
public function isEnterpriseClient()
{
if (! Ninja::isNinja()) {
if (!Ninja::isNinja()) {
return false;
}
@ -262,7 +264,7 @@ class Account extends BaseModel
public function isTrial()
{
if (! Ninja::isNinja()) {
if (!Ninja::isNinja()) {
return false;
}
@ -273,7 +275,7 @@ class Account extends BaseModel
public function startTrial($plan)
{
if (! Ninja::isNinja()) {
if (!Ninja::isNinja()) {
return;
}
@ -292,22 +294,22 @@ class Account extends BaseModel
$price = $this->plan_price;
$trial_plan = $this->trial_plan;
if ((! $plan || $plan == self::PLAN_FREE) && (! $trial_plan || ! $include_trial)) {
if ((!$plan || $plan == self::PLAN_FREE) && (!$trial_plan || !$include_trial)) {
return null;
}
$trial_active = false;
//14 day trial
$duration = 60*60*24*14;
$duration = 60 * 60 * 24 * 14;
if ($trial_plan && $include_trial) {
$trial_started = $this->trial_started;
$trial_expires = Carbon::parse($this->trial_started)->addSeconds($duration);
if($trial_expires->greaterThan(now())){
if ($trial_expires->greaterThan(now())) {
$trial_active = true;
}
}
}
@ -324,23 +326,23 @@ class Account extends BaseModel
}
}
if (! $include_inactive && ! $plan_active && ! $trial_active) {
if (!$include_inactive && !$plan_active && !$trial_active) {
return null;
}
// Should we show plan details or trial details?
if (($plan && ! $trial_plan) || ! $include_trial) {
if (($plan && !$trial_plan) || !$include_trial) {
$use_plan = true;
} elseif (! $plan && $trial_plan) {
} elseif (!$plan && $trial_plan) {
$use_plan = false;
} else {
// There is both a plan and a trial
if (! empty($plan_active) && empty($trial_active)) {
if (!empty($plan_active) && empty($trial_active)) {
$use_plan = true;
} elseif (empty($plan_active) && ! empty($trial_active)) {
} elseif (empty($plan_active) && !empty($trial_active)) {
$use_plan = false;
} elseif (! empty($plan_active) && ! empty($trial_active)) {
} elseif (!empty($plan_active) && !empty($trial_active)) {
// Both are active; use whichever is a better plan
if ($plan == self::PLAN_ENTERPRISE) {
$use_plan = true;
@ -385,20 +387,19 @@ class Account extends BaseModel
public function getDailyEmailLimit()
{
if($this->is_flagged)
if ($this->is_flagged)
return 0;
if(Carbon::createFromTimestamp($this->created_at)->diffInWeeks() == 0)
if (Carbon::createFromTimestamp($this->created_at)->diffInWeeks() == 0)
return 20;
if(Carbon::createFromTimestamp($this->created_at)->diffInWeeks() <= 2 && !$this->payment_id)
if (Carbon::createFromTimestamp($this->created_at)->diffInWeeks() <= 2 && !$this->payment_id)
return 20;
if($this->isPaid()){
if ($this->isPaid()) {
$limit = $this->paid_plan_email_quota;
$limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 50;
}
else{
} else {
$limit = $this->free_plan_email_quota;
$limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 10;
}
@ -408,21 +409,21 @@ class Account extends BaseModel
public function emailsSent()
{
if(is_null(Cache::get($this->key)))
if (is_null(Cache::get($this->key)))
return 0;
return Cache::get($this->key);
}
}
public function emailQuotaExceeded() :bool
public function emailQuotaExceeded(): bool
{
if(is_null(Cache::get($this->key)))
if (is_null(Cache::get($this->key)))
return false;
try {
if(Cache::get($this->key) > $this->getDailyEmailLimit()) {
if (Cache::get($this->key) > $this->getDailyEmailLimit()) {
if(is_null(Cache::get("throttle_notified:{$this->key}"))) {
if (is_null(Cache::get("throttle_notified:{$this->key}"))) {
App::forgetInstance('translator');
$t = app('translator');
@ -437,32 +438,31 @@ class Account extends BaseModel
Cache::put("throttle_notified:{$this->key}", true, 60 * 24);
if(config('ninja.notification.slack'))
if (config('ninja.notification.slack'))
$this->companies()->first()->notification(new EmailQuotaNotification($this))->ninja();
}
return true;
}
}
catch(\Exception $e){
} catch (\Exception $e) {
\Sentry\captureMessage("I encountered an error with email quotas for account {$this->key} - defaulting to SEND");
}
return false;
}
public function gmailCredentialNotification() :bool
public function gmailCredentialNotification(): bool
{
nlog("checking if gmail credential notification has already been sent");
if(is_null(Cache::get($this->key)))
if (is_null(Cache::get($this->key)))
return false;
nlog("Sending notification");
try {
if(is_null(Cache::get("gmail_credentials_notified:{$this->key}"))) {
if (is_null(Cache::get("gmail_credentials_notified:{$this->key}"))) {
App::forgetInstance('translator');
$t = app('translator');
@ -477,14 +477,13 @@ class Account extends BaseModel
Cache::put("gmail_credentials_notified:{$this->key}", true, 60 * 24);
if(config('ninja.notification.slack'))
if (config('ninja.notification.slack'))
$this->companies()->first()->notification(new GmailCredentialNotification($this))->ninja();
}
return true;
}
catch(\Exception $e){
} catch (\Exception $e) {
\Sentry\captureMessage("I encountered an error with sending with gmail for account {$this->key}");
}
@ -506,17 +505,18 @@ class Account extends BaseModel
public function getTrialDays()
{
if($this->payment_id)
if ($this->payment_id)
return 0;
$plan_expires = Carbon::parse($this->plan_expires);
if(!$this->payment_id && $plan_expires->gt(now())){
if (!$this->payment_id && $plan_expires->gt(now())) {
$diff = $plan_expires->diffInDays();
if($diff > 14);
return 0;
if ($diff > 14)
;
return 0;
return $diff;
}

View File

@ -22,7 +22,6 @@ class BankIntegration extends BaseModel
use Excludable;
protected $fillable = [
'integration_type',
'bank_account_name',
'provider_name',
'bank_account_number',

View File

@ -12,6 +12,7 @@
namespace Database\Factories;
use App\Models\Account;
use App\Models\BankIntegration;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
@ -25,6 +26,7 @@ class BankIntegrationFactory extends Factory
public function definition()
{
return [
'integration_type' => BankIntegration::INTEGRATION_TYPE_NONE,
'provider_name' => $this->faker->company(),
'provider_id' => 1,
'bank_account_name' => $this->faker->catchPhrase(),
@ -38,4 +40,4 @@ class BankIntegrationFactory extends Factory
'is_deleted' => false,
];
}
}
}

View File

@ -1,5 +1,6 @@
<?php
use App\Models\Account;
use App\Models\BankIntegration;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
@ -13,15 +14,22 @@ return new class extends Migration {
*/
public function up()
{
Schema::table('bank_integration', function (Blueprint $table) {
$table->string('integration_type')->nullable();
Schema::table('bank_integrations', function (Blueprint $table) {
$table->string('integration_type')->default(BankIntegration::INTEGRATION_TYPE_NONE);
});
// migrate old account to be used with yodlee
BankIntegration::query()->whereNull('integration_type')->cursor()->each(function ($bank_integration) {
BankIntegration::query()->where('integration_type', BankIntegration::INTEGRATION_TYPE_NONE)->whereNotNull('account_id')->cursor()->each(function ($bank_integration) {
$bank_integration->integration_type = BankIntegration::INTEGRATION_TYPE_YODLEE;
$bank_integration->save();
});
// MAYBE migration of account->bank_account_id etc
Schema::table('accounts', function (Blueprint $table) {
$table->renameColumn('bank_integration_account_id', 'bank_integration_yodlee_account_id');
$table->string('bank_integration_nordigen_secret_id')->nullable();
$table->string('bank_integration_nordigen_secret_key')->nullable();
});
}
/**
@ -31,8 +39,6 @@ return new class extends Migration {
*/
public function down()
{
Schema::table('bank_integration', function (Blueprint $table) {
$table->dropColumn('integration_id');
});
//
}
};

View File

@ -1,49 +1,49 @@
{
"/js/app.js": "/js/app.js",
"/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js",
"/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js",
"/js/clients/payments/forte-credit-card-payment.js": "/js/clients/payments/forte-credit-card-payment.js",
"/js/clients/payments/forte-ach-payment.js": "/js/clients/payments/forte-ach-payment.js",
"/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js",
"/js/clients/payments/stripe-klarna.js": "/js/clients/payments/stripe-klarna.js",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js",
"/js/clients/purchase_orders/action-selectors.js": "/js/clients/purchase_orders/action-selectors.js",
"/js/clients/purchase_orders/accept.js": "/js/clients/purchase_orders/accept.js",
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js",
"/js/clients/payments/stripe-sofort.js": "/js/clients/payments/stripe-sofort.js",
"/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js",
"/js/clients/payments/checkout-credit-card.js": "/js/clients/payments/checkout-credit-card.js",
"/js/clients/quotes/action-selectors.js": "/js/clients/quotes/action-selectors.js",
"/js/clients/quotes/approve.js": "/js/clients/quotes/approve.js",
"/js/clients/payments/stripe-credit-card.js": "/js/clients/payments/stripe-credit-card.js",
"/js/setup/setup.js": "/js/setup/setup.js",
"/js/clients/payments/card-js.min.js": "/js/clients/payments/card-js.min.js",
"/js/clients/shared/pdf.js": "/js/clients/shared/pdf.js",
"/js/clients/shared/multiple-downloads.js": "/js/clients/shared/multiple-downloads.js",
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js",
"/js/clients/payments/braintree-credit-card.js": "/js/clients/payments/braintree-credit-card.js",
"/js/clients/payments/braintree-paypal.js": "/js/clients/payments/braintree-paypal.js",
"/js/clients/payments/wepay-credit-card.js": "/js/clients/payments/wepay-credit-card.js",
"/js/clients/payment_methods/wepay-bank-account.js": "/js/clients/payment_methods/wepay-bank-account.js",
"/js/clients/payments/paytrace-credit-card.js": "/js/clients/payments/paytrace-credit-card.js",
"/js/clients/payments/mollie-credit-card.js": "/js/clients/payments/mollie-credit-card.js",
"/js/clients/payments/eway-credit-card.js": "/js/clients/payments/eway-credit-card.js",
"/js/clients/payment_methods/braintree-ach.js": "/js/clients/payment_methods/braintree-ach.js",
"/js/clients/payments/square-credit-card.js": "/js/clients/payments/square-credit-card.js",
"/js/clients/statements/view.js": "/js/clients/statements/view.js",
"/js/clients/payments/razorpay-aio.js": "/js/clients/payments/razorpay-aio.js",
"/js/clients/payments/stripe-sepa.js": "/js/clients/payments/stripe-sepa.js",
"/js/clients/payment_methods/authorize-checkout-card.js": "/js/clients/payment_methods/authorize-checkout-card.js",
"/js/clients/payments/stripe-giropay.js": "/js/clients/payments/stripe-giropay.js",
"/js/clients/payments/stripe-acss.js": "/js/clients/payments/stripe-acss.js",
"/js/clients/payments/stripe-bancontact.js": "/js/clients/payments/stripe-bancontact.js",
"/js/clients/payments/stripe-becs.js": "/js/clients/payments/stripe-becs.js",
"/js/clients/payments/stripe-eps.js": "/js/clients/payments/stripe-eps.js",
"/js/clients/payments/stripe-ideal.js": "/js/clients/payments/stripe-ideal.js",
"/js/clients/payments/stripe-przelewy24.js": "/js/clients/payments/stripe-przelewy24.js",
"/js/clients/payments/stripe-browserpay.js": "/js/clients/payments/stripe-browserpay.js",
"/js/clients/payments/stripe-fpx.js": "/js/clients/payments/stripe-fpx.js",
"/css/app.css": "/css/app.css",
"/css/card-js.min.css": "/css/card-js.min.css",
"/vendor/clipboard.min.js": "/vendor/clipboard.min.js"
"/js/app.js": "/js/app.js?id=7b6124b74168ccb1cc7da22f7a2bc9ed",
"/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js?id=b6723e0b8ea33f1f50617fa5f289a9d3",
"/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js?id=faf4828cc6b3b73b69c53d3046661884",
"/js/clients/payments/forte-credit-card-payment.js": "/js/clients/payments/forte-credit-card-payment.js?id=1ecc2e5ed666e5c6fae7830b5ab5c77a",
"/js/clients/payments/forte-ach-payment.js": "/js/clients/payments/forte-ach-payment.js?id=04cadfa45e77d49e8253b9ffbc000767",
"/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=0c520b9a787b6b9031300330e060a7f5",
"/js/clients/payments/stripe-klarna.js": "/js/clients/payments/stripe-klarna.js?id=2529dac592a6c34028addedf1198bcf2",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=f2b6ebf3c1da387c6268d6e0a28b8c65",
"/js/clients/purchase_orders/action-selectors.js": "/js/clients/purchase_orders/action-selectors.js?id=f8e554acde01ad91784e1046ef4ecdb4",
"/js/clients/purchase_orders/accept.js": "/js/clients/purchase_orders/accept.js?id=9bb483a89a887f753e49c0b635d6276a",
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=752e2bb6390f1a422e31868cf2a2bf67",
"/js/clients/payments/stripe-sofort.js": "/js/clients/payments/stripe-sofort.js?id=6b3381f59d2ef53cdd85a2435f54c2c3",
"/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js?id=018ecad3a1bcc1ecc47f76754a573ff2",
"/js/clients/payments/checkout-credit-card.js": "/js/clients/payments/checkout-credit-card.js?id=eea8dc5452e299f2e4148f5a0e168613",
"/js/clients/quotes/action-selectors.js": "/js/clients/quotes/action-selectors.js?id=07a94a1d7649b1bb2f6fdfe35b0cf4a1",
"/js/clients/quotes/approve.js": "/js/clients/quotes/approve.js?id=1e58e219878ce3f3ee4d313346ad5f68",
"/js/clients/payments/stripe-credit-card.js": "/js/clients/payments/stripe-credit-card.js?id=6e7c8ab039a239727317ae8622de10db",
"/js/setup/setup.js": "/js/setup/setup.js?id=cba079b7c249f2aa73731e1fa952d646",
"/js/clients/payments/card-js.min.js": "/js/clients/payments/card-js.min.js?id=cf50b5ba1fcd1d184bf0c10d710672c8",
"/js/clients/shared/pdf.js": "/js/clients/shared/pdf.js?id=682de6347049b32c9488f39c78a68ace",
"/js/clients/shared/multiple-downloads.js": "/js/clients/shared/multiple-downloads.js?id=ecfc8b8db2b8aec42ca295b5e6c75974",
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=e1c0599d6f7dc163b549a6df0b3490b4",
"/js/clients/payments/braintree-credit-card.js": "/js/clients/payments/braintree-credit-card.js?id=e051c84bfaf6b63a4971181e3ece6ecb",
"/js/clients/payments/braintree-paypal.js": "/js/clients/payments/braintree-paypal.js?id=6ff0f8ea53b30fe242706586399e61e8",
"/js/clients/payments/wepay-credit-card.js": "/js/clients/payments/wepay-credit-card.js?id=a7c2aef52dfdb7e6bef25abbf5373917",
"/js/clients/payment_methods/wepay-bank-account.js": "/js/clients/payment_methods/wepay-bank-account.js?id=be64a69a5fdf374ba3af7030db1d5155",
"/js/clients/payments/paytrace-credit-card.js": "/js/clients/payments/paytrace-credit-card.js?id=3869bc6d80acc83f81d9afe8efaae728",
"/js/clients/payments/mollie-credit-card.js": "/js/clients/payments/mollie-credit-card.js?id=dcebf12d3742e39c47676e2439426e6e",
"/js/clients/payments/eway-credit-card.js": "/js/clients/payments/eway-credit-card.js?id=27274d334aed0824ce4654fa22132f7f",
"/js/clients/payment_methods/braintree-ach.js": "/js/clients/payment_methods/braintree-ach.js?id=93f6f8c0a45cd46cd4d4c123f05ae9e7",
"/js/clients/payments/square-credit-card.js": "/js/clients/payments/square-credit-card.js?id=238e7001420a22b001856193689a1e70",
"/js/clients/statements/view.js": "/js/clients/statements/view.js?id=632aa120ab205dcc5807606a45844b4a",
"/js/clients/payments/razorpay-aio.js": "/js/clients/payments/razorpay-aio.js?id=df93901708dc49a732cbe0a11c8e6404",
"/js/clients/payments/stripe-sepa.js": "/js/clients/payments/stripe-sepa.js?id=77d4e397d193196e482af80737bff64a",
"/js/clients/payment_methods/authorize-checkout-card.js": "/js/clients/payment_methods/authorize-checkout-card.js?id=e58bdaeadf150e9fe8fa75c8540ae6c2",
"/js/clients/payments/stripe-giropay.js": "/js/clients/payments/stripe-giropay.js?id=9839796e7c08d6f4f372c03a8a5543f6",
"/js/clients/payments/stripe-acss.js": "/js/clients/payments/stripe-acss.js?id=4c3c5ee61948e8f49b174e1c1fae084c",
"/js/clients/payments/stripe-bancontact.js": "/js/clients/payments/stripe-bancontact.js?id=dfcd1f2f7080177c4dcbc58432bf4167",
"/js/clients/payments/stripe-becs.js": "/js/clients/payments/stripe-becs.js?id=c7ad959f7b79be68618d2937943aef95",
"/js/clients/payments/stripe-eps.js": "/js/clients/payments/stripe-eps.js?id=749cba1332a29baa444b37cee2ade2d7",
"/js/clients/payments/stripe-ideal.js": "/js/clients/payments/stripe-ideal.js?id=f0e2e00fa779a20967a2ea9489bf4fcb",
"/js/clients/payments/stripe-przelewy24.js": "/js/clients/payments/stripe-przelewy24.js?id=448b197a1d94b4408e130b5b8b1c2e53",
"/js/clients/payments/stripe-browserpay.js": "/js/clients/payments/stripe-browserpay.js?id=d0658f7d90db9869fe79a84851f91234",
"/js/clients/payments/stripe-fpx.js": "/js/clients/payments/stripe-fpx.js?id=62317369167d31654d18ecdb75ca5a45",
"/css/app.css": "/css/app.css?id=0cb847167b91d8db2ca50d30e0d691ae",
"/css/card-js.min.css": "/css/card-js.min.css?id=62afeb675235451543ada60afcedcb7c",
"/vendor/clipboard.min.js": "/vendor/clipboard.min.js?id=15f52a1ee547f2bdd46e56747332ca2d"
}

View File

@ -17,7 +17,7 @@ use App\Factory\BankTransactionFactory;
use App\Helpers\Bank\Yodlee\Yodlee;
use App\Helpers\Invoice\InvoiceSum;
use App\Jobs\Bank\MatchBankTransactions;
use App\Jobs\Bank\ProcessBankTransactions;
use App\Jobs\Bank\ProcessBankTransactionsYodlee;
use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\Expense;
@ -38,10 +38,10 @@ class YodleeApiTest extends TestCase
parent::setUp();
// if(!config('ninja.yodlee.client_id'))
$this->markTestSkipped('Skip test no Yodlee API credentials found');
$this->markTestSkipped('Skip test no Yodlee API credentials found');
$this->makeTestData();
}
public function testExpenseGenerationFromBankFeed()
@ -72,13 +72,13 @@ class YodleeApiTest extends TestCase
$expense = Expense::where('transaction_reference', 'Fuel')->first();
$this->assertNotNull($expense);
$this->assertEquals(10, (int)$expense->amount);
$this->assertEquals(10, (int) $expense->amount);
}
public function testIncomeMatchingAndPaymentGeneration()
{
$this->account->bank_integration_account_id = 'sbMem62e1e69547bfb2';
$this->account->bank_integration_yodlee_account_id = 'sbMem62e1e69547bfb2';
$this->account->save();
$invoice = Invoice::factory()->create(['user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $this->client->id]);
@ -117,7 +117,7 @@ class YodleeApiTest extends TestCase
$bt->date = now()->format('Y-m-d');
$bt->transaction_id = 123456;
$bt->save();
$data['transactions'][] = [
'id' => $bt->id,
'invoice_ids' => $invoice->hashed_id
@ -130,7 +130,7 @@ class YodleeApiTest extends TestCase
$this->assertNotNull($payment);
$this->assertEquals(10, (int)$payment->amount);
$this->assertEquals(10, (int) $payment->amount);
$this->assertEquals(4, $payment->status_id);
$this->assertEquals(1, $payment->invoices()->count());
@ -148,7 +148,7 @@ class YodleeApiTest extends TestCase
$transactions = $yodlee->getTransactionCategories();
$this->assertTrue(property_exists($transactions,'transactionCategory'));
$this->assertTrue(property_exists($transactions, 'transactionCategory'));
$t = collect($transactions->transactionCategory);
@ -160,17 +160,17 @@ class YodleeApiTest extends TestCase
}
// public function testFunctionalMatching()
// public function testFunctionalMatching()
// {
// $yodlee = new Yodlee('sbMem62e1e69547bfb1');
// $yodlee = new Yodlee('sbMem62e1e69547bfb1');
// $accounts = $yodlee->getAccounts();
// $accounts = $yodlee->getAccounts();
// foreach($accounts as $account)
// foreach($accounts as $account)
// {
// if(!BankIntegration::where('bank_account_id', $account['id'])->where('company_id', $this->company->id)->exists())
// if(!BankIntegration::where('bank_account_id', $account['id'])->where('company_id', $this->company->id)->exists())
// {
// $bank_integration = new BankIntegration();
// $bank_integration->company_id = $this->company->id;
@ -186,42 +186,42 @@ class YodleeApiTest extends TestCase
// $bank_integration->nickname = $account['nickname'];
// $bank_integration->balance = $account['current_balance'];
// $bank_integration->currency = $account['account_currency'];
// $bank_integration->save();
// ProcessBankTransactions::dispatchSync('sbMem62e1e69547bfb1', $bank_integration);
// $bank_integration->save();
// }
// ProcessBankTransactionsYodlee::dispatchSync('sbMem62e1e69547bfb1', $bank_integration);
// }
// }
// $this->assertGreaterThan(0, BankIntegration::count());
// $this->assertGreaterThan(0, BankIntegration::count());
// $this->assertGreaterThan(0, BankTransaction::count());
// $this->invoice->company_id = $this->company->id;
// $this->invoice->company_id = $this->company->id;
// $this->invoice->number = "XXXXXX8501";
// $this->invoice->save();
// BankService::dispatchSync($this->company->id, $this->company->db);
// $bt = BankTransaction::where('invoice_ids', $this->invoice->hashed_id)->first();
// BankService::dispatchSync($this->company->id, $this->company->db);
// nlog(BankTransaction::where('company_id', $this->company->id)->pluck('invoice_ids'));
// $bt = BankTransaction::where('invoice_ids', $this->invoice->hashed_id)->first();
// $this->assertNotNull($bt);
// nlog(BankTransaction::where('company_id', $this->company->id)->pluck('invoice_ids'));
// $this->assertEquals(BankTransaction::STATUS_MATCHED, $bt->status_id);
// $this->assertNotNull($bt);
// }
// $this->assertEquals(BankTransaction::STATUS_MATCHED, $bt->status_id);
// }
public function testDataMatching()
{
$transaction = collect([
(object)[
(object) [
'description' => 'tinkertonkton'
],
(object)[
(object) [
'description' => 'spud'
],
]);
@ -242,10 +242,10 @@ class YodleeApiTest extends TestCase
$transaction = collect([
(object)[
(object) [
'description' => 'tinker and spice'
],
(object)[
(object) [
'description' => 'spud with water'
],
]);
@ -258,7 +258,7 @@ class YodleeApiTest extends TestCase
$invoice = $transaction->first(function ($value, $key) {
return str_contains($value->description, 'tinker');
});
$this->assertNotNull($invoice);
@ -286,179 +286,179 @@ class YodleeApiTest extends TestCase
$this->assertNotNull($access_token);
}
/**
/**
[transactionCategory] => Array
(
[0] => stdClass Object
(
[id] => 1
[source] => SYSTEM
[classification] => PERSONAL
[category] => Uncategorized
[type] => UNCATEGORIZE
[highLevelCategoryId] => 10000017
[highLevelCategoryName] => Uncategorized
[defaultCategoryName] => Uncategorized
[defaultHighLevelCategoryName] => Uncategorized
)
[transactionCategory] => Array
(
[0] => stdClass Object
(
[id] => 1
[source] => SYSTEM
[classification] => PERSONAL
[category] => Uncategorized
[type] => UNCATEGORIZE
[highLevelCategoryId] => 10000017
[highLevelCategoryName] => Uncategorized
[defaultCategoryName] => Uncategorized
[defaultHighLevelCategoryName] => Uncategorized
)
[1] => stdClass Object
(
[id] => 2
[source] => SYSTEM
[classification] => PERSONAL
[category] => Automotive/Fuel
[type] => EXPENSE
[detailCategory] => Array
(
[0] => stdClass Object
(
[id] => 1041
[name] => Registration/Licensing
)
[1] => stdClass Object
(
[id] => 2
[source] => SYSTEM
[classification] => PERSONAL
[category] => Automotive/Fuel
[type] => EXPENSE
[detailCategory] => Array
(
[0] => stdClass Object
(
[id] => 1041
[name] => Registration/Licensing
)
[1] => stdClass Object
(
[id] => 1145
[name] => Automotive
)
[1] => stdClass Object
(
[id] => 1145
[name] => Automotive
)
[2] => stdClass Object
(
[id] => 1218
[name] => Auto Fees/Penalties
)
[2] => stdClass Object
(
[id] => 1218
[name] => Auto Fees/Penalties
)
[3] => stdClass Object
(
[id] => 1260
[name] => Car Appraisers
)
[3] => stdClass Object
(
[id] => 1260
[name] => Car Appraisers
)
[4] => stdClass Object
(
[id] => 1261
[name] => Car Dealers
)
[4] => stdClass Object
(
[id] => 1261
[name] => Car Dealers
)
[5] => stdClass Object
(
[id] => 1262
[name] => Car Dealers and Leasing
)
[5] => stdClass Object
(
[id] => 1262
[name] => Car Dealers and Leasing
)
[6] => stdClass Object
(
[id] => 1263
[name] => Car Parts and Accessories
)
[6] => stdClass Object
(
[id] => 1263
[name] => Car Parts and Accessories
)
[7] => stdClass Object
(
[id] => 1264
[name] => Car Wash and Detail
)
[7] => stdClass Object
(
[id] => 1264
[name] => Car Wash and Detail
)
[8] => stdClass Object
(
[id] => 1265
[name] => Classic and Antique Car
)
[8] => stdClass Object
(
[id] => 1265
[name] => Classic and Antique Car
)
[9] => stdClass Object
(
[id] => 1267
[name] => Maintenance and Repair
)
[9] => stdClass Object
(
[id] => 1267
[name] => Maintenance and Repair
)
[10] => stdClass Object
(
[id] => 1268
[name] => Motorcycles/Mopeds/Scooters
)
[10] => stdClass Object
(
[id] => 1268
[name] => Motorcycles/Mopeds/Scooters
)
[11] => stdClass Object
(
[id] => 1269
[name] => Oil and Lube
)
[11] => stdClass Object
(
[id] => 1269
[name] => Oil and Lube
)
[12] => stdClass Object
(
[id] => 1270
[name] => Motorcycle Repair
)
[12] => stdClass Object
(
[id] => 1270
[name] => Motorcycle Repair
)
[13] => stdClass Object
(
[id] => 1271
[name] => RVs and Motor Homes
)
[13] => stdClass Object
(
[id] => 1271
[name] => RVs and Motor Homes
)
[14] => stdClass Object
(
[id] => 1272
[name] => Motorcycle Sales
)
[14] => stdClass Object
(
[id] => 1272
[name] => Motorcycle Sales
)
[15] => stdClass Object
(
[id] => 1273
[name] => Salvage Yards
)
[15] => stdClass Object
(
[id] => 1273
[name] => Salvage Yards
)
[16] => stdClass Object
(
[id] => 1274
[name] => Smog Check
)
[16] => stdClass Object
(
[id] => 1274
[name] => Smog Check
)
[17] => stdClass Object
(
[id] => 1275
[name] => Tires
)
[17] => stdClass Object
(
[id] => 1275
[name] => Tires
)
[18] => stdClass Object
(
[id] => 1276
[name] => Towing
)
[18] => stdClass Object
(
[id] => 1276
[name] => Towing
)
[19] => stdClass Object
(
[id] => 1277
[name] => Transmissions
)
[19] => stdClass Object
(
[id] => 1277
[name] => Transmissions
)
[20] => stdClass Object
(
[id] => 1278
[name] => Used Cars
)
[20] => stdClass Object
(
[id] => 1278
[name] => Used Cars
)
[21] => stdClass Object
(
[id] => 1240
[name] => e-Charging
)
[21] => stdClass Object
(
[id] => 1240
[name] => e-Charging
)
[22] => stdClass Object
(
[id] => 1266
[name] => Gas Stations
)
[22] => stdClass Object
(
[id] => 1266
[name] => Gas Stations
)
)
)
[highLevelCategoryId] => 10000003
[highLevelCategoryName] => Automotive Expenses
[defaultCategoryName] => Automotive Expenses
[defaultHighLevelCategoryName] => Automotive Expenses
)
[highLevelCategoryId] => 10000003
[highLevelCategoryName] => Automotive Expenses
[defaultCategoryName] => Automotive Expenses
[defaultHighLevelCategoryName] => Automotive Expenses
)
*/
*/
public function testGetCategories()
@ -467,113 +467,113 @@ class YodleeApiTest extends TestCase
$yodlee = new Yodlee('sbMem62e1e69547bfb2');
$transactions = $yodlee->getTransactionCategories();
$this->assertIsArray($transactions->transactionCategory);
}
/**
[2022-08-05 01:29:45] local.INFO: stdClass Object
(
[account] => Array
(
[0] => stdClass Object
(
[CONTAINER] => bank
[providerAccountId] => 11308693
[accountName] => My CD - 8878
[accountStatus] => ACTIVE
[accountNumber] => xxxx8878
[aggregationSource] => USER
[isAsset] => 1
[balance] => stdClass Object
(
[currency] => USD
[amount] => 49778.07
)
/**
[2022-08-05 01:29:45] local.INFO: stdClass Object
(
[account] => Array
(
[0] => stdClass Object
(
[CONTAINER] => bank
[providerAccountId] => 11308693
[accountName] => My CD - 8878
[accountStatus] => ACTIVE
[accountNumber] => xxxx8878
[aggregationSource] => USER
[isAsset] => 1
[balance] => stdClass Object
(
[currency] => USD
[amount] => 49778.07
)
[id] => 12331861
[includeInNetWorth] => 1
[providerId] => 18769
[providerName] => Dag Site Captcha
[isManual] =>
[currentBalance] => stdClass Object
(
[currency] => USD
[amount] => 49778.07
)
[id] => 12331861
[includeInNetWorth] => 1
[providerId] => 18769
[providerName] => Dag Site Captcha
[isManual] =>
[currentBalance] => stdClass Object
(
[currency] => USD
[amount] => 49778.07
)
[accountType] => CD
[displayedName] => LORETTA
[createdDate] => 2022-07-28T06:55:33Z
[lastUpdated] => 2022-07-28T06:56:09Z
[dataset] => Array
(
[0] => stdClass Object
(
[name] => BASIC_AGG_DATA
[additionalStatus] => AVAILABLE_DATA_RETRIEVED
[updateEligibility] => ALLOW_UPDATE
[lastUpdated] => 2022-07-28T06:55:50Z
[lastUpdateAttempt] => 2022-07-28T06:55:50Z
)
[accountType] => CD
[displayedName] => LORETTA
[createdDate] => 2022-07-28T06:55:33Z
[lastUpdated] => 2022-07-28T06:56:09Z
[dataset] => Array
(
[0] => stdClass Object
(
[name] => BASIC_AGG_DATA
[additionalStatus] => AVAILABLE_DATA_RETRIEVED
[updateEligibility] => ALLOW_UPDATE
[lastUpdated] => 2022-07-28T06:55:50Z
[lastUpdateAttempt] => 2022-07-28T06:55:50Z
)
)
)
)
[1] => stdClass Object
(
[CONTAINER] => bank
[providerAccountId] => 11308693
[accountName] => Joint Savings - 7159
[accountStatus] => ACTIVE
[accountNumber] => xxxx7159
[aggregationSource] => USER
[isAsset] => 1
[balance] => stdClass Object
(
[currency] => USD
[amount] => 186277.45
)
)
[1] => stdClass Object
(
[CONTAINER] => bank
[providerAccountId] => 11308693
[accountName] => Joint Savings - 7159
[accountStatus] => ACTIVE
[accountNumber] => xxxx7159
[aggregationSource] => USER
[isAsset] => 1
[balance] => stdClass Object
(
[currency] => USD
[amount] => 186277.45
)
[id] => 12331860
[includeInNetWorth] => 1
[providerId] => 18769
[providerName] => Dag Site Captcha
[isManual] =>
[availableBalance] => stdClass Object
(
[currency] => USD
[amount] => 186277.45
)
[id] => 12331860
[includeInNetWorth] => 1
[providerId] => 18769
[providerName] => Dag Site Captcha
[isManual] =>
[availableBalance] => stdClass Object
(
[currency] => USD
[amount] => 186277.45
)
[currentBalance] => stdClass Object
(
[currency] => USD
[amount] => 186277.45
)
[currentBalance] => stdClass Object
(
[currency] => USD
[amount] => 186277.45
)
[accountType] => SAVINGS
[displayedName] => LYDIA
[createdDate] => 2022-07-28T06:55:33Z
[classification] => PERSONAL
[lastUpdated] => 2022-07-28T06:56:09Z
[dataset] => Array
(
[0] => stdClass Object
(
[name] => BASIC_AGG_DATA
[additionalStatus] => AVAILABLE_DATA_RETRIEVED
[updateEligibility] => ALLOW_UPDATE
[lastUpdated] => 2022-07-28T06:55:50Z
[lastUpdateAttempt] => 2022-07-28T06:55:50Z
)
[accountType] => SAVINGS
[displayedName] => LYDIA
[createdDate] => 2022-07-28T06:55:33Z
[classification] => PERSONAL
[lastUpdated] => 2022-07-28T06:56:09Z
[dataset] => Array
(
[0] => stdClass Object
(
[name] => BASIC_AGG_DATA
[additionalStatus] => AVAILABLE_DATA_RETRIEVED
[updateEligibility] => ALLOW_UPDATE
[lastUpdated] => 2022-07-28T06:55:50Z
[lastUpdateAttempt] => 2022-07-28T06:55:50Z
)
)
)
)
*/
)
*/
public function testGetAccounts()
{
@ -585,51 +585,51 @@ class YodleeApiTest extends TestCase
}
/**
[2022-08-05 01:36:34] local.INFO: stdClass Object
(
[transaction] => Array
(
[0] => stdClass Object
(
[CONTAINER] => bank
[id] => 103953585
[amount] => stdClass Object
(
[amount] => 480.66
[currency] => USD
)
/**
[2022-08-05 01:36:34] local.INFO: stdClass Object
(
[transaction] => Array
(
[0] => stdClass Object
(
[CONTAINER] => bank
[id] => 103953585
[amount] => stdClass Object
(
[amount] => 480.66
[currency] => USD
)
[categoryType] => UNCATEGORIZE
[categoryId] => 1
[category] => Uncategorized
[categorySource] => SYSTEM
[highLevelCategoryId] => 10000017
[createdDate] => 2022-08-04T21:50:17Z
[lastUpdated] => 2022-08-04T21:50:17Z
[description] => stdClass Object
(
[original] => CHEROKEE NATION TAX TA TAHLEQUAH OK
)
[categoryType] => UNCATEGORIZE
[categoryId] => 1
[category] => Uncategorized
[categorySource] => SYSTEM
[highLevelCategoryId] => 10000017
[createdDate] => 2022-08-04T21:50:17Z
[lastUpdated] => 2022-08-04T21:50:17Z
[description] => stdClass Object
(
[original] => CHEROKEE NATION TAX TA TAHLEQUAH OK
)
[isManual] =>
[sourceType] => AGGREGATED
[date] => 2022-08-03
[transactionDate] => 2022-08-03
[postDate] => 2022-08-03
[status] => POSTED
[accountId] => 12331794
[runningBalance] => stdClass Object
(
[amount] => 480.66
[currency] => USD
)
[isManual] =>
[sourceType] => AGGREGATED
[date] => 2022-08-03
[transactionDate] => 2022-08-03
[postDate] => 2022-08-03
[status] => POSTED
[accountId] => 12331794
[runningBalance] => stdClass Object
(
[amount] => 480.66
[currency] => USD
)
[checkNumber] => 998
)
[checkNumber] => 998
)
*/
*/
public function testGetTransactions()
{
@ -654,7 +654,7 @@ class YodleeApiTest extends TestCase
'fromDate' => '2000-10-10', /// YYYY-MM-DD
];
$accounts = $yodlee->getTransactions($data);
$accounts = $yodlee->getTransactions($data);
$this->assertIsArray($accounts);