From b54b626332a7ab50275db5082db0b8aff1a7ada9 Mon Sep 17 00:00:00 2001 From: paulwer Date: Fri, 1 Dec 2023 14:30:33 +0100 Subject: [PATCH] wip: changes --- app/Helpers/Bank/Nordigen/Nordigen.php | 144 ++-- .../Transformer/AccountTransformer.php | 145 ++-- .../Transformer/ExpenseTransformer.php | 4 +- .../Transformer/IncomeTransformer.php | 19 +- .../Controllers/Bank/NordigenController.php | 15 +- .../Controllers/Bank/YodleeController.php | 239 ++++--- .../Controllers/BankIntegrationController.php | 140 ++-- app/Jobs/Bank/MatchBankTransactions.php | 186 +++--- .../Bank/ProcessBankTransactionsNordigen.php | 166 +++++ ....php => ProcessBankTransactionsYodlee.php} | 54 +- app/Jobs/Ninja/BankTransactionSync.php | 38 +- app/Models/Account.php | 114 ++-- app/Models/BankIntegration.php | 1 - database/factories/BankIntegrationFactory.php | 4 +- ...3_11_26_082959_add_bank_integration_id.php | 18 +- public/mix-manifest.json | 94 +-- tests/Feature/Bank/YodleeApiTest.php | 624 +++++++++--------- 17 files changed, 1172 insertions(+), 833 deletions(-) create mode 100644 app/Jobs/Bank/ProcessBankTransactionsNordigen.php rename app/Jobs/Bank/{ProcessBankTransactions.php => ProcessBankTransactionsYodlee.php} (71%) diff --git a/app/Helpers/Bank/Nordigen/Nordigen.php b/app/Helpers/Bank/Nordigen/Nordigen.php index 35f18aad5e72..18518847f5c7 100644 --- a/app/Helpers/Bank/Nordigen/Nordigen.php +++ b/app/Helpers/Bank/Nordigen/Nordigen.php @@ -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(); - } - } diff --git a/app/Helpers/Bank/Nordigen/Transformer/AccountTransformer.php b/app/Helpers/Bank/Nordigen/Transformer/AccountTransformer.php index e2b90656fb6f..48c43559d2a2 100644 --- a/app/Helpers/Bank/Nordigen/Transformer/AccountTransformer.php +++ b/app/Helpers/Bank/Nordigen/Transformer/AccountTransformer.php @@ -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 : '', ]; } } diff --git a/app/Helpers/Bank/Nordigen/Transformer/ExpenseTransformer.php b/app/Helpers/Bank/Nordigen/Transformer/ExpenseTransformer.php index 6274bb2ed911..ade514d8fdea 100644 --- a/app/Helpers/Bank/Nordigen/Transformer/ExpenseTransformer.php +++ b/app/Helpers/Bank/Nordigen/Transformer/ExpenseTransformer.php @@ -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", diff --git a/app/Helpers/Bank/Nordigen/Transformer/IncomeTransformer.php b/app/Helpers/Bank/Nordigen/Transformer/IncomeTransformer.php index fc272f0eb1bd..3de6c3e7680f 100644 --- a/app/Helpers/Bank/Nordigen/Transformer/IncomeTransformer.php +++ b/app/Helpers/Bank/Nordigen/Transformer/IncomeTransformer.php @@ -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; diff --git a/app/Http/Controllers/Bank/NordigenController.php b/app/Http/Controllers/Bank/NordigenController.php index fe5e0d2a456c..5e077ec63a87 100644 --- a/app/Http/Controllers/Bank/NordigenController.php +++ b/app/Http/Controllers/Bank/NordigenController.php @@ -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); }); diff --git a/app/Http/Controllers/Bank/YodleeController.php b/app/Http/Controllers/Bank/YodleeController.php index ad651f4704ed..13cb76c75bfe 100644 --- a/app/Http/Controllers/Bank/YodleeController.php +++ b/app/Http/Controllers/Bank/YodleeController.php @@ -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); diff --git a/app/Http/Controllers/BankIntegrationController.php b/app/Http/Controllers/BankIntegrationController.php index 552aa02fe6d7..e9b890893c3e 100644 --- a/app/Http/Controllers/BankIntegrationController.php +++ b/app/Http/Controllers/BankIntegrationController.php @@ -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); } -} \ No newline at end of file +} diff --git a/app/Jobs/Bank/MatchBankTransactions.php b/app/Jobs/Bank/MatchBankTransactions.php index 708206c15a3f..f84022cf4fcf 100644 --- a/app/Jobs/Bank/MatchBankTransactions.php +++ b/app/Jobs/Bank/MatchBankTransactions.php @@ -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 -} \ No newline at end of file +} diff --git a/app/Jobs/Bank/ProcessBankTransactionsNordigen.php b/app/Jobs/Bank/ProcessBankTransactionsNordigen.php new file mode 100644 index 000000000000..a71d64943858 --- /dev/null +++ b/app/Jobs/Bank/ProcessBankTransactionsNordigen.php @@ -0,0 +1,166 @@ +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(); + + } + + } +} diff --git a/app/Jobs/Bank/ProcessBankTransactions.php b/app/Jobs/Bank/ProcessBankTransactionsYodlee.php similarity index 71% rename from app/Jobs/Bank/ProcessBankTransactions.php rename to app/Jobs/Bank/ProcessBankTransactionsYodlee.php index 74d996f0fd14..e2d3acda54df 100644 --- a/app/Jobs/Bank/ProcessBankTransactions.php +++ b/app/Jobs/Bank/ProcessBankTransactionsYodlee.php @@ -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 } -} \ No newline at end of file +} diff --git a/app/Jobs/Ninja/BankTransactionSync.php b/app/Jobs/Ninja/BankTransactionSync.php index 370fef9e7b86..8840661ea706 100644 --- a/app/Jobs/Ninja/BankTransactionSync.php +++ b/app/Jobs/Ninja/BankTransactionSync.php @@ -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"); } } diff --git a/app/Models/Account.php b/app/Models/Account.php index 2dbd6373dfa2..bc36fad08282 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -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; } diff --git a/app/Models/BankIntegration.php b/app/Models/BankIntegration.php index 517382960e57..2d85f3c1e3a3 100644 --- a/app/Models/BankIntegration.php +++ b/app/Models/BankIntegration.php @@ -22,7 +22,6 @@ class BankIntegration extends BaseModel use Excludable; protected $fillable = [ - 'integration_type', 'bank_account_name', 'provider_name', 'bank_account_number', diff --git a/database/factories/BankIntegrationFactory.php b/database/factories/BankIntegrationFactory.php index ad588f872a84..9d04cdfc56f8 100644 --- a/database/factories/BankIntegrationFactory.php +++ b/database/factories/BankIntegrationFactory.php @@ -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, ]; } -} \ No newline at end of file +} diff --git a/database/migrations/2023_11_26_082959_add_bank_integration_id.php b/database/migrations/2023_11_26_082959_add_bank_integration_id.php index 4d7ba978ec08..8895b7729926 100644 --- a/database/migrations/2023_11_26_082959_add_bank_integration_id.php +++ b/database/migrations/2023_11_26_082959_add_bank_integration_id.php @@ -1,5 +1,6 @@ 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'); - }); + // } }; diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 490236f79ed8..460e977d7617 100644 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -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" } diff --git a/tests/Feature/Bank/YodleeApiTest.php b/tests/Feature/Bank/YodleeApiTest.php index e586c71519e3..6133e51230b4 100644 --- a/tests/Feature/Bank/YodleeApiTest.php +++ b/tests/Feature/Bank/YodleeApiTest.php @@ -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);