mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Merge pull request #9004 from paulwer/feature-nordigen-payment-provider
Feature: nordigen bank integration
This commit is contained in:
commit
2e20abfc41
@ -69,3 +69,6 @@ MICROSOFT_REDIRECT_URI=
|
||||
APPLE_CLIENT_ID=
|
||||
APPLE_CLIENT_SECRET=
|
||||
APPLE_REDIRECT_URI=
|
||||
|
||||
NORDIGEN_SECRET_ID=
|
||||
NORDIGEN_SECRET_KEY=
|
||||
|
@ -97,6 +97,9 @@ class Kernel extends ConsoleKernel
|
||||
/* Fires webhooks for overdue Invoice */
|
||||
$schedule->job(new InvoiceCheckLateWebhook)->dailyAt('07:00')->withoutOverlapping()->name('invoice-overdue-job')->onOneServer();
|
||||
|
||||
/* Pulls in bank transactions from third party services */
|
||||
$schedule->job(new BankTransactionSync)->everyFourHours()->withoutOverlapping()->name('bank-trans-sync-job')->onOneServer();
|
||||
|
||||
if (Ninja::isSelfHost()) {
|
||||
$schedule->call(function () {
|
||||
Account::query()->whereNotNull('id')->update(['is_scheduler_running' => true]);
|
||||
@ -107,9 +110,6 @@ class Kernel extends ConsoleKernel
|
||||
if (Ninja::isHosted()) {
|
||||
$schedule->job(new AdjustEmailQuota)->dailyAt('23:30')->withoutOverlapping();
|
||||
|
||||
/* Pulls in bank transactions from third party services */
|
||||
$schedule->job(new BankTransactionSync)->everyFourHours()->withoutOverlapping()->name('bank-trans-sync-job')->onOneServer();
|
||||
|
||||
/* Checks ACH verification status and updates state to authorize when verified */
|
||||
$schedule->job(new CheckACHStatus)->everySixHours()->withoutOverlapping()->name('ach-status-job')->onOneServer();
|
||||
|
||||
@ -120,7 +120,7 @@ class Kernel extends ConsoleKernel
|
||||
$schedule->command('ninja:s3-cleanup')->dailyAt('23:15')->withoutOverlapping()->name('s3-cleanup-job')->onOneServer();
|
||||
}
|
||||
|
||||
if (config('queue.default') == 'database' && Ninja::isSelfHost() && config('ninja.internal_queue_enabled') && ! config('ninja.is_docker')) {
|
||||
if (config('queue.default') == 'database' && Ninja::isSelfHost() && config('ninja.internal_queue_enabled') && !config('ninja.is_docker')) {
|
||||
$schedule->command('queue:work database --stop-when-empty --memory=256')->everyMinute()->withoutOverlapping();
|
||||
|
||||
$schedule->command('queue:restart')->everyFiveMinutes()->withoutOverlapping();
|
||||
@ -134,7 +134,7 @@ class Kernel extends ConsoleKernel
|
||||
*/
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
$this->load(__DIR__ . '/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
|
126
app/Helpers/Bank/Nordigen/Nordigen.php
Normal file
126
app/Helpers/Bank/Nordigen/Nordigen.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @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
|
||||
*
|
||||
* Institutions: Are Banks or Payment-Providers, which manages bankaccounts.
|
||||
*
|
||||
* Accounts: Accounts are existing bank_accounts at a specific institution.
|
||||
*
|
||||
* Requisitions: Are registered/active user-flows to authenticate one or many accounts. After completition, the accoundId could be used to fetch data for this account. After the access expires, the user could create a new requisition to connect accounts again.
|
||||
*/
|
||||
|
||||
namespace App\Helpers\Bank\Nordigen;
|
||||
|
||||
use App\Helpers\Bank\Nordigen\Transformer\AccountTransformer;
|
||||
use App\Helpers\Bank\Nordigen\Transformer\TransactionTransformer;
|
||||
|
||||
class Nordigen
|
||||
{
|
||||
public bool $test_mode; // https://developer.gocardless.com/bank-account-data/sandbox
|
||||
|
||||
public string $sandbox_institutionId = "SANDBOXFINANCE_SFIN0000";
|
||||
|
||||
protected \Nordigen\NordigenPHP\API\NordigenClient $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->test_mode = config('ninja.nordigen.test_mode');
|
||||
|
||||
if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')))
|
||||
throw new \Exception('missing nordigen credentials');
|
||||
|
||||
$this->client = new \Nordigen\NordigenPHP\API\NordigenClient(config('ninja.nordigen.secret_id'), config('ninja.nordigen.secret_key'));
|
||||
|
||||
$this->client->createAccessToken(); // access_token is valid 24h -> so we dont have to implement a refresh-cycle
|
||||
}
|
||||
|
||||
// metadata-section for frontend
|
||||
public function getInstitutions()
|
||||
{
|
||||
if ($this->test_mode)
|
||||
return [$this->client->institution->getInstitution($this->sandbox_institutionId)];
|
||||
|
||||
return $this->client->institution->getInstitutions();
|
||||
}
|
||||
|
||||
// requisition-section
|
||||
public function createRequisition(string $redirect, string $initutionId, string $reference)
|
||||
{
|
||||
if ($this->test_mode && $initutionId != $this->sandbox_institutionId)
|
||||
throw new \Exception('invalid institutionId while in test-mode');
|
||||
|
||||
return $this->client->requisition->createRequisition($redirect, $initutionId, null, $reference);
|
||||
}
|
||||
|
||||
public function getRequisition(string $requisitionId)
|
||||
{
|
||||
try {
|
||||
return $this->client->requisition->getRequisition($requisitionId);
|
||||
} catch (\Exception $e) {
|
||||
if (strpos($e->getMessage(), "Invalid Requisition ID") !== false)
|
||||
return false;
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: return null on not found
|
||||
public function getAccount(string $account_id)
|
||||
{
|
||||
try {
|
||||
$out = new \stdClass();
|
||||
|
||||
$out->data = $this->client->account($account_id)->getAccountDetails()["account"];
|
||||
$out->metadata = $this->client->account($account_id)->getAccountMetaData();
|
||||
$out->balances = $this->client->account($account_id)->getAccountBalances()["balances"];
|
||||
$out->institution = $this->client->institution->getInstitution($out->metadata["institution_id"]);
|
||||
|
||||
$it = new AccountTransformer();
|
||||
return $it->transform($out);
|
||||
} catch (\Exception $e) {
|
||||
if (strpos($e->getMessage(), "Invalid Account ID") !== false)
|
||||
return false;
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function isAccountActive(string $account_id)
|
||||
{
|
||||
try {
|
||||
$account = $this->client->account($account_id)->getAccountMetaData();
|
||||
|
||||
if ($account["status"] != "READY") {
|
||||
nlog('nordigen account was not in status ready. accountId: ' . $account_id . ' status: ' . $account["status"]);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
if (strpos($e->getMessage(), "Invalid Account ID") !== false)
|
||||
return false;
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* this method returns booked transactions from the bank_account, pending transactions are not part of the result
|
||||
* @todo @turbo124 should we include pending transactions within the integration-process and mark them with a specific category?!
|
||||
*/
|
||||
public function getTransactions(string $accountId, string $dateFrom = null)
|
||||
{
|
||||
$transactionResponse = $this->client->account($accountId)->getAccountTransactions($dateFrom);
|
||||
|
||||
$it = new TransactionTransformer();
|
||||
return $it->transform($transactionResponse);
|
||||
}
|
||||
}
|
121
app/Helpers/Bank/Nordigen/Transformer/AccountTransformer.php
Normal file
121
app/Helpers/Bank/Nordigen/Transformer/AccountTransformer.php
Normal file
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Helpers\Bank\Nordigen\Transformer;
|
||||
|
||||
use App\Helpers\Bank\AccountTransformerInterface;
|
||||
|
||||
/**
|
||||
[0] => stdClass Object
|
||||
(
|
||||
[data] => stdClass Object
|
||||
(
|
||||
[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] => [
|
||||
{
|
||||
[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] => []
|
||||
)
|
||||
|
||||
)
|
||||
*/
|
||||
|
||||
|
||||
class AccountTransformer implements AccountTransformerInterface
|
||||
{
|
||||
|
||||
public function transform($nordigen_account)
|
||||
{
|
||||
|
||||
if (!property_exists($nordigen_account, 'data') || !property_exists($nordigen_account, 'metadata') || !property_exists($nordigen_account, 'balances') || !property_exists($nordigen_account, 'institution'))
|
||||
throw new \Exception('invalid dataset');
|
||||
|
||||
$used_balance = $nordigen_account->balances[0];
|
||||
// prefer entry with closingBooked
|
||||
foreach ($nordigen_account->balances as $entry) {
|
||||
if ($entry["balanceType"] === 'closingBooked') { // available: closingBooked, interimAvailable
|
||||
$used_balance = $entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $nordigen_account->metadata["id"],
|
||||
'account_type' => "bank",
|
||||
'account_name' => $nordigen_account->data["iban"],
|
||||
'account_status' => $nordigen_account->metadata["status"],
|
||||
'account_number' => '**** ' . substr($nordigen_account->data["iban"], -7),
|
||||
'provider_account_id' => $nordigen_account->metadata["id"],
|
||||
'provider_id' => $nordigen_account->institution["id"],
|
||||
'provider_name' => $nordigen_account->institution["name"],
|
||||
'nickname' => $nordigen_account->data["ownerName"] ? $nordigen_account->data["ownerName"] : '',
|
||||
'current_balance' => (int) $used_balance ? $used_balance["balanceAmount"]["amount"] : 0,
|
||||
'account_currency' => $used_balance ? $used_balance["balanceAmount"]["currency"] : '',
|
||||
];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
149
app/Helpers/Bank/Nordigen/Transformer/TransactionTransformer.php
Normal file
149
app/Helpers/Bank/Nordigen/Transformer/TransactionTransformer.php
Normal file
@ -0,0 +1,149 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Helpers\Bank\Nordigen\Transformer;
|
||||
|
||||
use App\Helpers\Bank\BankRevenueInterface;
|
||||
use App\Models\BankIntegration;
|
||||
use App\Utils\Traits\AppSetup;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Log;
|
||||
|
||||
/**
|
||||
{
|
||||
"transactions": {
|
||||
"booked": [
|
||||
{
|
||||
"transactionId": "string",
|
||||
"debtorName": "string",
|
||||
"debtorAccount": {
|
||||
"iban": "string"
|
||||
},
|
||||
"transactionAmount": {
|
||||
"currency": "string",
|
||||
"amount": "328.18"
|
||||
},
|
||||
"bankTransactionCode": "string",
|
||||
"bookingDate": "date",
|
||||
"valueDate": "date",
|
||||
"remittanceInformationUnstructured": "string"
|
||||
},
|
||||
{
|
||||
"transactionId": "string",
|
||||
"transactionAmount": {
|
||||
"currency": "string",
|
||||
"amount": "947.26"
|
||||
},
|
||||
"bankTransactionCode": "string",
|
||||
"bookingDate": "date",
|
||||
"valueDate": "date",
|
||||
"remittanceInformationUnstructured": "string"
|
||||
}
|
||||
],
|
||||
"pending": [
|
||||
{
|
||||
"transactionAmount": {
|
||||
"currency": "string",
|
||||
"amount": "99.20"
|
||||
},
|
||||
"valueDate": "date",
|
||||
"remittanceInformationUnstructured": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
class TransactionTransformer implements BankRevenueInterface
|
||||
{
|
||||
use AppSetup;
|
||||
|
||||
public function transform($transactionResponse)
|
||||
{
|
||||
$data = [];
|
||||
|
||||
if (!array_key_exists('transactions', $transactionResponse) || !array_key_exists('booked', $transactionResponse["transactions"]))
|
||||
throw new \Exception('invalid dataset');
|
||||
|
||||
foreach ($transactionResponse["transactions"]["booked"] as $transaction) {
|
||||
$data[] = $this->transformTransaction($transaction);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function transformTransaction($transaction)
|
||||
{
|
||||
|
||||
if (!array_key_exists('transactionId', $transaction) || !array_key_exists('transactionAmount', $transaction))
|
||||
throw new \Exception('invalid dataset');
|
||||
|
||||
// description could be in varios places
|
||||
$description = '';
|
||||
if (array_key_exists('remittanceInformationStructured', $transaction))
|
||||
$description = $transaction["remittanceInformationStructured"];
|
||||
else if (array_key_exists('remittanceInformationStructuredArray', $transaction))
|
||||
$description = implode('\n', $transaction["remittanceInformationStructuredArray"]);
|
||||
else if (array_key_exists('remittanceInformationUnstructured', $transaction))
|
||||
$description = $transaction["remittanceInformationUnstructured"];
|
||||
else if (array_key_exists('remittanceInformationUnstructuredArray', $transaction))
|
||||
$description = implode('\n', $transaction["remittanceInformationUnstructuredArray"]);
|
||||
else
|
||||
Log::warning("Missing description for the following transaction: " . json_encode($transaction));
|
||||
|
||||
// participant
|
||||
$participant = array_key_exists('debtorAccount', $transaction) && array_key_exists('iban', $transaction["debtorAccount"]) ?
|
||||
$transaction['debtorAccount']['iban'] :
|
||||
(array_key_exists('creditorAccount', $transaction) && array_key_exists('iban', $transaction["creditorAccount"]) ?
|
||||
$transaction['creditorAccount']['iban'] : null);
|
||||
$participant_name = array_key_exists('debtorName', $transaction) ?
|
||||
$transaction['debtorName'] :
|
||||
(array_key_exists('creditorName', $transaction) ?
|
||||
$transaction['creditorName'] : null);
|
||||
|
||||
return [
|
||||
'transaction_id' => $transaction["transactionId"],
|
||||
'amount' => abs((int) $transaction["transactionAmount"]["amount"]),
|
||||
'currency_id' => $this->convertCurrency($transaction["transactionAmount"]["currency"]),
|
||||
'category_id' => null, // nordigen has no categories
|
||||
'category_type' => array_key_exists('additionalInformation', $transaction) ? $transaction["additionalInformation"] : null, // TODO: institution specific keys like: GUTSCHRIFT, ABSCHLUSS, MONATSABSCHLUSS etc
|
||||
'date' => $transaction["bookingDate"],
|
||||
'description' => $description,
|
||||
'participant' => $participant,
|
||||
'participant_name' => $participant_name,
|
||||
'base_type' => (int) $transaction["transactionAmount"]["amount"] <= 0 ? 'DEBIT' : 'CREDIT',
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
private function convertCurrency(string $code)
|
||||
{
|
||||
|
||||
$currencies = Cache::get('currencies');
|
||||
|
||||
if (!$currencies) {
|
||||
$this->buildCache(true);
|
||||
}
|
||||
|
||||
$currency = $currencies->filter(function ($item) use ($code) {
|
||||
return $item->code == $code;
|
||||
})->first();
|
||||
|
||||
if ($currency)
|
||||
return $currency->id;
|
||||
|
||||
return 1;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
304
app/Http/Controllers/Bank/NordigenController.php
Normal file
304
app/Http/Controllers/Bank/NordigenController.php
Normal file
@ -0,0 +1,304 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Bank;
|
||||
|
||||
use App\Helpers\Bank\Nordigen\Nordigen;
|
||||
use App\Http\Controllers\BaseController;
|
||||
use App\Http\Requests\Nordigen\ConfirmNordigenBankIntegrationRequest;
|
||||
use App\Http\Requests\Nordigen\ConnectNordigenBankIntegrationRequest;
|
||||
use App\Jobs\Bank\ProcessBankTransactionsNordigen;
|
||||
use App\Models\BankIntegration;
|
||||
use App\Utils\Ninja;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
use Nordigen\NordigenPHP\Exceptions\NordigenExceptions\NordigenException;
|
||||
|
||||
class NordigenController extends BaseController
|
||||
{
|
||||
/**
|
||||
* VIEW: Connect Nordigen Bank Integration
|
||||
* @param ConnectNordigenBankIntegrationRequest $request
|
||||
*/
|
||||
public function connect(ConnectNordigenBankIntegrationRequest $request)
|
||||
{
|
||||
$data = $request->all();
|
||||
$context = $request->getTokenContent();
|
||||
$lang = $data['lang'] ?? 'en';
|
||||
$context["lang"] = $lang;
|
||||
|
||||
if (!$context)
|
||||
return view('bank.nordigen.handler', [
|
||||
'lang' => $lang,
|
||||
'failed_reason' => "token-invalid",
|
||||
"redirectUrl" => config("ninja.app_url") . "?action=nordigen_connect&status=failed&reason=token-invalid",
|
||||
]);
|
||||
|
||||
$context["redirect"] = $data["redirect"];
|
||||
if ($context["context"] != "nordigen" || array_key_exists("requisitionId", $context))
|
||||
return view('bank.nordigen.handler', [
|
||||
'lang' => $lang,
|
||||
'failed_reason' => "token-invalid",
|
||||
"redirectUrl" => ($context["redirect"]) . "?action=nordigen_connect&status=failed&reason=token-invalid",
|
||||
]);
|
||||
|
||||
$company = $request->getCompany();
|
||||
$account = $company->account;
|
||||
|
||||
if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')))
|
||||
return view('bank.nordigen.handler', [
|
||||
'lang' => $lang,
|
||||
'company' => $company,
|
||||
'account' => $company->account,
|
||||
'failed_reason' => "account-config-invalid",
|
||||
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=account-config-invalid",
|
||||
]);
|
||||
|
||||
if (!(Ninja::isSelfHost() || (Ninja::isHosted() && $account->isPaid() && $account->plan == 'enterprise')))
|
||||
return view('bank.nordigen.handler', [
|
||||
'lang' => $lang,
|
||||
'company' => $company,
|
||||
'account' => $company->account,
|
||||
'failed_reason' => "not-available",
|
||||
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=not-available",
|
||||
]);
|
||||
|
||||
$nordigen = new Nordigen();
|
||||
|
||||
// show bank_selection_screen, when institution_id is not present
|
||||
if (!array_key_exists("institution_id", $data))
|
||||
return view('bank.nordigen.handler', [
|
||||
'lang' => $lang,
|
||||
'company' => $company,
|
||||
'account' => $company->account,
|
||||
'institutions' => $nordigen->getInstitutions(),
|
||||
'redirectUrl' => $context["redirect"] . "?action=nordigen_connect&status=user-aborted"
|
||||
]);
|
||||
|
||||
// redirect to requisition flow
|
||||
try {
|
||||
$requisition = $nordigen->createRequisition(config('ninja.app_url') . '/nordigen/confirm', $data['institution_id'], $request->token);
|
||||
} catch (NordigenException $e) { // TODO: property_exists returns null in these cases... => why => therefore we just get unknown error everytime $responseBody is typeof GuzzleHttp\Psr7\Stream
|
||||
$responseBody = (string) $e->getResponse()->getBody();
|
||||
|
||||
if (str_contains($responseBody, '"institution_id"')) // provided institution_id was wrong
|
||||
return view('bank.nordigen.handler', [
|
||||
'lang' => $lang,
|
||||
'company' => $company,
|
||||
'account' => $company->account,
|
||||
'failed_reason' => "institution-invalid",
|
||||
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=institution-invalid",
|
||||
]);
|
||||
else if (str_contains($responseBody, '"reference"')) // this error can occur, when a reference was used double or is invalid => therefor we suggest the frontend to use another token
|
||||
return view('bank.nordigen.handler', [
|
||||
'lang' => $lang,
|
||||
'company' => $company,
|
||||
'account' => $company->account,
|
||||
'failed_reason' => "token-invalid",
|
||||
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=token-invalid",
|
||||
]);
|
||||
else {
|
||||
nlog("Unknown Error from nordigen: " . $e);
|
||||
nlog($responseBody);
|
||||
|
||||
return view('bank.nordigen.handler', [
|
||||
'lang' => $lang,
|
||||
'company' => $company,
|
||||
'account' => $company->account,
|
||||
'failed_reason' => "unknown",
|
||||
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=unknown",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// save cache
|
||||
$context["requisitionId"] = $requisition["id"];
|
||||
Cache::put($request->token, $context, 3600);
|
||||
|
||||
return response()->redirectTo($requisition["link"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* VIEW: Confirm Nordigen Bank Integration (redirect after nordigen flow)
|
||||
* @param ConnectNordigenBankIntegrationRequest $request
|
||||
*/
|
||||
public function confirm(ConfirmNordigenBankIntegrationRequest $request)
|
||||
{
|
||||
$data = $request->all();
|
||||
$context = $request->getTokenContent();
|
||||
if (!array_key_exists('lang', $data) && $context['lang'] != 'en')
|
||||
return redirect()->route('nordigen.confirm', array_merge(["lang" => $context['lang']], $request->query())); // redirect is required in order for the bank-ui to display everything properly
|
||||
$lang = $data['lang'] ?? 'en';
|
||||
|
||||
if (!$context || $context["context"] != "nordigen" || !array_key_exists("requisitionId", $context))
|
||||
return view('bank.nordigen.handler', [
|
||||
'lang' => $lang,
|
||||
'failed_reason' => "ref-invalid",
|
||||
"redirectUrl" => ($context && array_key_exists("redirect", $context) ? $context["redirect"] : config('ninja.app_url')) . "?action=nordigen_connect&status=failed&reason=ref-invalid",
|
||||
]);
|
||||
|
||||
$company = $request->getCompany();
|
||||
$account = $company->account;
|
||||
|
||||
if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')))
|
||||
return view('bank.nordigen.handler', [
|
||||
'lang' => $lang,
|
||||
'company' => $company,
|
||||
'account' => $company->account,
|
||||
'failed_reason' => "account-config-invalid",
|
||||
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=account-config-invalid",
|
||||
]);
|
||||
|
||||
if (!(Ninja::isSelfHost() || (Ninja::isHosted() && $account->isPaid() && $account->plan == 'enterprise')))
|
||||
return view('bank.nordigen.handler', [
|
||||
'lang' => $lang,
|
||||
'company' => $company,
|
||||
'account' => $company->account,
|
||||
'failed_reason' => "not-available",
|
||||
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=not-available",
|
||||
]);
|
||||
|
||||
// fetch requisition
|
||||
$nordigen = new Nordigen();
|
||||
$requisition = $nordigen->getRequisition($context["requisitionId"]);
|
||||
|
||||
// check validity of requisition
|
||||
if (!$requisition)
|
||||
return view('bank.nordigen.handler', [
|
||||
'lang' => $lang,
|
||||
'company' => $company,
|
||||
'account' => $company->account,
|
||||
'failed_reason' => "requisition-not-found",
|
||||
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=requisition-not-found",
|
||||
]);
|
||||
if ($requisition["status"] != "LN")
|
||||
return view('bank.nordigen.handler', [
|
||||
'lang' => $lang,
|
||||
'company' => $company,
|
||||
'account' => $company->account,
|
||||
'failed_reason' => "requisition-invalid-status",
|
||||
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=requisition-invalid-status&status=" . $requisition["status"],
|
||||
]);
|
||||
if (sizeof($requisition["accounts"]) == 0)
|
||||
return view('bank.nordigen.handler', [
|
||||
'lang' => $lang,
|
||||
'company' => $company,
|
||||
'account' => $company->account,
|
||||
'failed_reason' => "requisition-no-accounts",
|
||||
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=requisition-no-accounts",
|
||||
]);
|
||||
|
||||
// connect new accounts
|
||||
$bank_integration_ids = [];
|
||||
foreach ($requisition["accounts"] as $nordigenAccountId) {
|
||||
|
||||
$nordigen_account = $nordigen->getAccount($nordigenAccountId);
|
||||
|
||||
$existing_bank_integration = BankIntegration::withTrashed()->where('nordigen_account_id', $nordigen_account['id'])->where('company_id', $company->id)->first();
|
||||
|
||||
if (!$existing_bank_integration) {
|
||||
|
||||
$bank_integration = new BankIntegration();
|
||||
$bank_integration->integration_type = BankIntegration::INTEGRATION_TYPE_NORDIGEN;
|
||||
$bank_integration->company_id = $company->id;
|
||||
$bank_integration->account_id = $company->account_id;
|
||||
$bank_integration->user_id = $company->owner()->id;
|
||||
$bank_integration->nordigen_account_id = $nordigen_account['id'];
|
||||
$bank_integration->bank_account_type = $nordigen_account['account_type'];
|
||||
$bank_integration->bank_account_name = $nordigen_account['account_name'];
|
||||
$bank_integration->bank_account_status = $nordigen_account['account_status'];
|
||||
$bank_integration->bank_account_number = $nordigen_account['account_number'];
|
||||
$bank_integration->nordigen_institution_id = $nordigen_account['provider_id'];
|
||||
$bank_integration->provider_name = $nordigen_account['provider_name'];
|
||||
$bank_integration->nickname = $nordigen_account['nickname'];
|
||||
$bank_integration->balance = $nordigen_account['current_balance'];
|
||||
$bank_integration->currency = $nordigen_account['account_currency'];
|
||||
$bank_integration->disabled_upstream = false;
|
||||
$bank_integration->auto_sync = true;
|
||||
$bank_integration->from_date = now()->subDays(90); // default max-fetch interval of nordigen is 90 days
|
||||
|
||||
$bank_integration->save();
|
||||
|
||||
array_push($bank_integration_ids, $bank_integration->id);
|
||||
|
||||
} else {
|
||||
|
||||
// resetting metadata for account status
|
||||
$existing_bank_integration->balance = $account['current_balance'];
|
||||
$existing_bank_integration->bank_account_status = $account['account_status'];
|
||||
$existing_bank_integration->disabled_upstream = false;
|
||||
$existing_bank_integration->auto_sync = true;
|
||||
$existing_bank_integration->from_date = now()->subDays(90); // default max-fetch interval of nordigen is 90 days
|
||||
$existing_bank_integration->deleted_at = null;
|
||||
|
||||
$existing_bank_integration->save();
|
||||
|
||||
array_push($bank_integration_ids, $existing_bank_integration->id);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// perform update in background
|
||||
$company->account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_NORDIGEN)->where('auto_sync', true)->each(function ($bank_integration) {
|
||||
ProcessBankTransactionsNordigen::dispatch($bank_integration);
|
||||
});
|
||||
|
||||
// prevent rerun of this method with same ref
|
||||
Cache::delete($data["ref"]);
|
||||
|
||||
// Successfull Response => Redirect
|
||||
return response()->redirectTo($context["redirect"] . "?action=nordigen_connect&status=success&bank_integrations=" . implode(',', $bank_integration_ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Nordigen Institutions GETTER.
|
||||
*
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/api/v1/nordigen/institutions",
|
||||
* operationId="nordigenRefreshWebhook",
|
||||
* tags={"nordigen"},
|
||||
* summary="Getting available institutions from nordigen",
|
||||
* description="Used to determine the available institutions for sending and creating a new connect-link",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||
* @OA\Parameter(ref="#/components/parameters/include"),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="",
|
||||
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||
* @OA\JsonContent(ref="#/components/schemas/Credit"),
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="Validation error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||
*
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="default",
|
||||
* description="Unexpected Error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
public function institutions(Request $request)
|
||||
{
|
||||
if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')))
|
||||
return response()->json(['message' => 'Not yet authenticated with Nordigen Bank Integration service'], 400);
|
||||
|
||||
$nordigen = new Nordigen();
|
||||
return response()->json($nordigen->getInstitutions());
|
||||
}
|
||||
|
||||
}
|
@ -16,7 +16,7 @@ use App\Helpers\Bank\Yodlee\Yodlee;
|
||||
use App\Http\Controllers\BaseController;
|
||||
use App\Http\Requests\Yodlee\YodleeAdminRequest;
|
||||
use App\Http\Requests\Yodlee\YodleeAuthRequest;
|
||||
use App\Jobs\Bank\ProcessBankTransactions;
|
||||
use App\Jobs\Bank\ProcessBankTransactionsYodlee;
|
||||
use App\Models\BankIntegration;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@ -90,6 +90,7 @@ class YodleeController extends BaseController
|
||||
$bank_integration->balance = $account['current_balance'];
|
||||
$bank_integration->currency = $account['account_currency'];
|
||||
$bank_integration->from_date = now()->subYear();
|
||||
|
||||
$bank_integration->auto_sync = true;
|
||||
|
||||
$bank_integration->save();
|
||||
@ -97,12 +98,11 @@ class YodleeController extends BaseController
|
||||
}
|
||||
|
||||
|
||||
$company->account->bank_integrations->each(function ($bank_integration) use ($company) {
|
||||
ProcessBankTransactions::dispatch($company->account->bank_integration_account_id, $bank_integration);
|
||||
$company->account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_YODLEE)->where('auto_sync', true)->each(function ($bank_integration) use ($company) { // TODO: filter to yodlee only
|
||||
ProcessBankTransactionsYodlee::dispatch($company->account->id, $bank_integration);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process Yodlee Refresh Webhook.
|
||||
*
|
||||
@ -137,7 +137,6 @@ class YodleeController extends BaseController
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
|
||||
/*
|
||||
{
|
||||
"event":{
|
||||
@ -295,7 +294,7 @@ class YodleeController extends BaseController
|
||||
->where('account_id', $account_number)
|
||||
->exists();
|
||||
|
||||
if(!$bank_integration) {
|
||||
if (!$bank_integration) {
|
||||
return response()->json(['message' => 'Account does not exist.'], 400);
|
||||
}
|
||||
|
||||
|
@ -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,10 +23,14 @@ 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\Models\User;
|
||||
use App\Repositories\BankIntegrationRepository;
|
||||
use App\Transformers\BankIntegrationTransformer;
|
||||
use App\Utils\Ninja;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Response;
|
||||
@ -189,27 +194,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
|
||||
|
||||
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth()->user();
|
||||
|
||||
$user_account = $user->account;
|
||||
|
||||
$bank_account_id = $user_account->bank_integration_account_id;
|
||||
$this->refreshAccountsYodlee($user);
|
||||
|
||||
if (!$bank_account_id) {
|
||||
return response()->json(['message' => 'Not yet authenticated with Bank Integration service'], 400);
|
||||
$this->refreshAccountsNordigen($user);
|
||||
|
||||
if (Cache::get("throttle_polling:{$user_account->key}"))
|
||||
return response()->json(BankIntegration::query()->company(), 200);
|
||||
|
||||
// Processing transactions for each bank account
|
||||
if (Ninja::isHosted() && $user->account->bank_integration_account_id)
|
||||
$user_account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_YODLEE)->each(function ($bank_integration) use ($user_account) {
|
||||
ProcessBankTransactionsYodlee::dispatch($user_account->id, $bank_integration);
|
||||
});
|
||||
|
||||
if (config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key') && (Ninja::isSelfHost() || (Ninja::isHosted() && $user_account->isPaid() && $user_account->plan == 'enterprise')))
|
||||
$user_account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_NORDIGEN)->each(function ($bank_integration) {
|
||||
ProcessBankTransactionsNordigen::dispatch($bank_integration);
|
||||
});
|
||||
|
||||
Cache::put("throttle_polling:{$user_account->key}", true, 300);
|
||||
|
||||
return response()->json(BankIntegration::query()->company(), 200);
|
||||
}
|
||||
|
||||
$yodlee = new Yodlee($bank_account_id);
|
||||
private function refreshAccountsYodlee(User $user)
|
||||
{
|
||||
if (!Ninja::isHosted() || !$user->account->bank_integration_account_id)
|
||||
return;
|
||||
|
||||
$yodlee = new Yodlee($user->account->bank_integration_account_id);
|
||||
|
||||
$accounts = $yodlee->getAccounts();
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
if ($bi = BankIntegration::withTrashed()->where('bank_account_id', $account['id'])->where('company_id', $user->company()->id)->first()) {
|
||||
if ($bi = BankIntegration::withTrashed()->where("integration_type", BankIntegration::INTEGRATION_TYPE_YODLEE)->where('bank_account_id', $account['id'])->where('company_id', $user->company()->id)->first()) {
|
||||
$bi->balance = $account['current_balance'];
|
||||
$bi->currency = $account['account_currency'];
|
||||
$bi->save();
|
||||
@ -233,18 +256,31 @@ class BankIntegrationController extends BaseController
|
||||
$bank_integration->save();
|
||||
}
|
||||
}
|
||||
|
||||
if (Cache::get("throttle_polling:{$user_account->key}")) {
|
||||
return response()->json(BankIntegration::query()->company(), 200);
|
||||
}
|
||||
|
||||
$user_account->bank_integrations->each(function ($bank_integration) use ($user_account) {
|
||||
ProcessBankTransactions::dispatch($user_account->bank_integration_account_id, $bank_integration);
|
||||
private function refreshAccountsNordigen(User $user)
|
||||
{
|
||||
if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')))
|
||||
return;
|
||||
|
||||
$nordigen = new Nordigen();
|
||||
|
||||
BankIntegration::where("integration_type", BankIntegration::INTEGRATION_TYPE_NORDIGEN)->whereNotNull('nordigen_account_id')->each(function (BankIntegration $bank_integration) use ($nordigen) {
|
||||
$account = $nordigen->getAccount($bank_integration->nordigen_account_id);
|
||||
if (!$account) {
|
||||
$bank_integration->disabled_upstream = true;
|
||||
|
||||
$bank_integration->save();
|
||||
return;
|
||||
}
|
||||
|
||||
$bank_integration->disabled_upstream = false;
|
||||
$bank_integration->bank_account_status = $account['account_status'];
|
||||
$bank_integration->balance = $account['current_balance'];
|
||||
$bank_integration->currency = $account['account_currency'];
|
||||
|
||||
$bank_integration->save();
|
||||
});
|
||||
|
||||
Cache::put("throttle_polling:{$user_account->key}", true, 300);
|
||||
|
||||
return response()->json(BankIntegration::query()->company(), 200);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -262,23 +298,27 @@ class BankIntegrationController extends BaseController
|
||||
|
||||
$account = $user->account;
|
||||
|
||||
$bank_account_id = $account->bank_integration_account_id;
|
||||
$bank_integration = BankIntegration::withTrashed()->where('bank_account_id', $acc_id)->orWhere('nordigen_account_id', $acc_id)->company()->firstOrFail(); // @turbo124 please check
|
||||
|
||||
if (!$bank_account_id) {
|
||||
if ($bank_integration->integration_type == BankIntegration::INTEGRATION_TYPE_YODLEE)
|
||||
$this->removeAccountYodlee($account, $bank_integration);
|
||||
// we dont remove Accounts from nordigen, because they could be used within other companies
|
||||
|
||||
$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_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($bank_account_id);
|
||||
$res = $yodlee->deleteAccount($acc_id);
|
||||
|
||||
$this->bank_integration_repo->delete($bi);
|
||||
|
||||
return $this->itemResponse($bi->fresh());
|
||||
$yodlee = new Yodlee($account->bank_integration_account_id);
|
||||
$yodlee->deleteAccount($bank_integration->bank_account_id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the remote list of accounts stored on the third party provider
|
||||
* and update our local cache.
|
||||
@ -288,12 +328,20 @@ class BankIntegrationController extends BaseController
|
||||
*/
|
||||
public function getTransactions(AdminBankIntegrationRequest $request)
|
||||
{
|
||||
/** @var \App\Models\User $user */
|
||||
$user = auth()->user();
|
||||
/** @var \App\Models\Account $account */
|
||||
$account = auth()->user()->account;
|
||||
|
||||
$user->account->bank_integrations->each(function ($bank_integration) use ($user) {
|
||||
(new ProcessBankTransactions($user->account->bank_integration_account_id, $bank_integration))->handle();
|
||||
if (Ninja::isHosted() && $account->isPaid() && $account->plan == 'enterprise') {
|
||||
$account->bank_integrations()->where('integration_type', BankIntegration::INTEGRATION_TYPE_YODLEE)->where('auto_sync', true)->cursor()->each(function ($bank_integration) use ($account) {
|
||||
(new ProcessBankTransactionsYodlee($account->id, $bank_integration))->handle();
|
||||
});
|
||||
}
|
||||
|
||||
if (config("ninja.nordigen.secret_id") && config("ninja.nordigen.secret_key") && (Ninja::isSelfHost() || (Ninja::isHosted() && $account->isPaid() && $account->plan == 'enterprise'))) {
|
||||
$account->bank_integrations()->where('integration_type', BankIntegration::INTEGRATION_TYPE_NORDIGEN)->where('auto_sync', true)->cursor()->each(function ($bank_integration) {
|
||||
(new ProcessBankTransactionsNordigen($bank_integration))->handle();
|
||||
});
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'Fetching transactions....'], 200);
|
||||
}
|
||||
|
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests\Nordigen;
|
||||
|
||||
use App\Http\Requests\Request;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\Company;
|
||||
use Cache;
|
||||
|
||||
class ConfirmNordigenBankIntegrationRequest extends Request
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'ref' => 'required|string', // nordigen redirects only with the ref-property
|
||||
'lang' => 'string',
|
||||
];
|
||||
}
|
||||
public function getTokenContent()
|
||||
{
|
||||
$input = $this->all();
|
||||
|
||||
$data = Cache::get($input['ref']);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getCompany()
|
||||
{
|
||||
MultiDB::findAndSetDbByCompanyKey($this->getTokenContent()['company_key']);
|
||||
|
||||
return Company::where('company_key', $this->getTokenContent()['company_key'])->firstOrFail();
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Http\Requests\Nordigen;
|
||||
|
||||
use App\Http\Requests\Request;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\Company;
|
||||
use Cache;
|
||||
|
||||
class ConnectNordigenBankIntegrationRequest extends Request
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'lang' => 'string',
|
||||
'institution_id' => 'string',
|
||||
'redirect' => 'string',
|
||||
];
|
||||
}
|
||||
|
||||
// @turbo124 @todo please check for validity, when request from frontend
|
||||
public function prepareForValidation()
|
||||
{
|
||||
$input = $this->all();
|
||||
|
||||
if (!array_key_exists('redirect', $input)) {
|
||||
$context = $this->getTokenContent();
|
||||
|
||||
$input["redirect"] = isset($context["is_react"]) && $context['is_react'] ? config('ninja.react_url') . "/#/settings/bank_accounts" : config('ninja.app_url');
|
||||
|
||||
$this->replace($input);
|
||||
}
|
||||
}
|
||||
public function getTokenContent()
|
||||
{
|
||||
if ($this->state) {
|
||||
$this->token = $this->state;
|
||||
}
|
||||
|
||||
$data = Cache::get($this->token);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getCompany()
|
||||
{
|
||||
MultiDB::findAndSetDbByCompanyKey($this->getTokenContent()['company_key']);
|
||||
|
||||
return Company::where('company_key', $this->getTokenContent()['company_key'])->firstOrFail();
|
||||
}
|
||||
}
|
@ -94,7 +94,6 @@ class MatchBankTransactions implements ShouldQueue
|
||||
}
|
||||
|
||||
$bank_categories = Cache::get('bank_categories');
|
||||
|
||||
if (!$bank_categories && $yodlee) {
|
||||
$_categories = $yodlee->getTransactionCategories();
|
||||
$this->categories = collect($_categories->transactionCategory);
|
||||
@ -135,7 +134,7 @@ class MatchBankTransactions implements ShouldQueue
|
||||
return $collection->toArray();
|
||||
}
|
||||
|
||||
private function checkPayable($invoices) :bool
|
||||
private function checkPayable($invoices): bool
|
||||
{
|
||||
foreach ($invoices as $invoice) {
|
||||
$invoice->service()->markSent();
|
||||
@ -158,7 +157,7 @@ class MatchBankTransactions implements ShouldQueue
|
||||
|
||||
$_expenses = explode(",", $input['expense_id']);
|
||||
|
||||
foreach($_expenses as $_expense) {
|
||||
foreach ($_expenses as $_expense) {
|
||||
|
||||
$expense = Expense::withTrashed()
|
||||
->where('id', $this->decodePrimaryKey($_expense))
|
||||
@ -218,7 +217,7 @@ class MatchBankTransactions implements ShouldQueue
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function matchInvoicePayment($input) :self
|
||||
private function matchInvoicePayment($input): self
|
||||
{
|
||||
$this->bt = BankTransaction::withTrashed()->find($input['id']);
|
||||
|
||||
@ -242,7 +241,7 @@ 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::query()->withTrashed()->find($input['id']);
|
||||
@ -283,7 +282,7 @@ class MatchBankTransactions implements ShouldQueue
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function createPayment($invoices, float $amount) :void
|
||||
private function createPayment($invoices, float $amount): void
|
||||
{
|
||||
$this->available_balance = $amount;
|
||||
|
||||
@ -371,7 +370,7 @@ class MatchBankTransactions implements ShouldQueue
|
||||
$this->invoice
|
||||
->client
|
||||
->service()
|
||||
->updateBalanceAndPaidToDate($this->applied_amount*-1, $amount)
|
||||
->updateBalanceAndPaidToDate($this->applied_amount * -1, $amount)
|
||||
->save();
|
||||
|
||||
$this->invoice = $this->invoice
|
||||
@ -389,13 +388,13 @@ 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);
|
||||
|
177
app/Jobs/Bank/ProcessBankTransactionsNordigen.php
Normal file
177
app/Jobs/Bank/ProcessBankTransactionsNordigen.php
Normal file
@ -0,0 +1,177 @@
|
||||
<?php
|
||||
/**
|
||||
* Credit Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2022. Credit Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Jobs\Bank;
|
||||
|
||||
use App\Helpers\Bank\Nordigen\Nordigen;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Models\BankIntegration;
|
||||
use App\Models\BankTransaction;
|
||||
use App\Models\Company;
|
||||
use App\Notifications\Ninja\GenericNinjaAdminNotification;
|
||||
use App\Services\Bank\BankMatchingService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ProcessBankTransactionsNordigen implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
private BankIntegration $bank_integration;
|
||||
|
||||
private ?string $from_date;
|
||||
|
||||
public Company $company;
|
||||
public Nordigen $nordigen;
|
||||
public $nordigen_account;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(BankIntegration $bank_integration)
|
||||
{
|
||||
$this->bank_integration = $bank_integration;
|
||||
$this->from_date = $bank_integration->from_date ?: now()->subDays(90);
|
||||
$this->company = $this->bank_integration->company;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if ($this->bank_integration->integration_type != BankIntegration::INTEGRATION_TYPE_NORDIGEN)
|
||||
throw new \Exception("Invalid BankIntegration Type");
|
||||
|
||||
if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')))
|
||||
throw new \Exception("Missing credentials for bank_integration service nordigen");
|
||||
|
||||
$this->nordigen = new Nordigen();
|
||||
|
||||
set_time_limit(0);
|
||||
|
||||
nlog("Nordigen: Processing transactions for account: {$this->bank_integration->account->key}");
|
||||
|
||||
// UPDATE ACCOUNT
|
||||
try {
|
||||
$this->updateAccount();
|
||||
} catch (\Exception $e) {
|
||||
nlog("Nordigen: {$this->bank_integration->nordigen_account_id} - exited abnormally => " . $e->getMessage());
|
||||
|
||||
$content = [
|
||||
"Processing transactions for account: {$this->bank_integration->nordigen_account_id} failed",
|
||||
"Exception Details => ",
|
||||
$e->getMessage(),
|
||||
];
|
||||
|
||||
$this->bank_integration->company->notification(new GenericNinjaAdminNotification($content))->ninja();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
if (!$this->nordigen_account)
|
||||
return;
|
||||
|
||||
// UPDATE TRANSACTIONS
|
||||
try {
|
||||
$this->processTransactions();
|
||||
} catch (\Exception $e) {
|
||||
nlog("Nordigen: {$this->bank_integration->nordigen_account_id} - exited abnormally => " . $e->getMessage());
|
||||
|
||||
$content = [
|
||||
"Processing transactions for account: {$this->bank_integration->nordigen_account_id} failed",
|
||||
"Exception Details => ",
|
||||
$e->getMessage(),
|
||||
];
|
||||
|
||||
$this->bank_integration->company->notification(new GenericNinjaAdminNotification($content))->ninja();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Perform Matching
|
||||
BankMatchingService::dispatch($this->company->id, $this->company->db);
|
||||
}
|
||||
|
||||
private function updateAccount()
|
||||
{
|
||||
if (!$this->nordigen->isAccountActive($this->bank_integration->nordigen_account_id)) {
|
||||
$this->bank_integration->disabled_upstream = true;
|
||||
$this->bank_integration->save();
|
||||
$this->stop_loop = false;
|
||||
nlog("Nordigen: account inactive: " . $this->bank_integration->nordigen_account_id);
|
||||
// @turbo124 @todo send email for expired account
|
||||
return;
|
||||
}
|
||||
|
||||
$this->nordigen_account = $this->nordigen->getAccount($this->bank_integration->nordigen_account_id);
|
||||
|
||||
$this->bank_integration->disabled_upstream = false;
|
||||
$this->bank_integration->bank_account_status = $this->nordigen_account['account_status'];
|
||||
$this->bank_integration->balance = $this->nordigen_account['current_balance'];
|
||||
|
||||
$this->bank_integration->save();
|
||||
}
|
||||
|
||||
private function processTransactions()
|
||||
{
|
||||
//Get transaction count object
|
||||
$transactions = $this->nordigen->getTransactions($this->bank_integration->nordigen_account_id, $this->from_date);
|
||||
|
||||
//if no transactions, update the from_date and move on
|
||||
if (count($transactions) == 0) {
|
||||
|
||||
$this->bank_integration->from_date = now()->subDays(5);
|
||||
$this->bank_integration->disabled_upstream = false;
|
||||
$this->bank_integration->save();
|
||||
|
||||
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)->where('bank_integration_id', $this->bank_integration->id)->withTrashed()->exists())
|
||||
continue;
|
||||
|
||||
//this should be much faster to insert than using ::create()
|
||||
\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->bank_integration->from_date = now()->subDays(5);
|
||||
$this->bank_integration->save();
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ namespace App\Jobs\Bank;
|
||||
use App\Helpers\Bank\Yodlee\Transformer\AccountTransformer;
|
||||
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;
|
||||
@ -26,7 +27,7 @@ use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class ProcessBankTransactions implements ShouldQueue
|
||||
class ProcessBankTransactionsYodlee implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
@ -61,21 +62,24 @@ class ProcessBankTransactions implements ShouldQueue
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if ($this->bank_integration->integration_type != BankIntegration::INTEGRATION_TYPE_YODLEE)
|
||||
throw new \Exception("Invalid BankIntegration Type");
|
||||
|
||||
set_time_limit(0);
|
||||
|
||||
//Loop through everything until we are up to date
|
||||
$this->from_date = $this->from_date ?: '2021-01-01';
|
||||
|
||||
nlog("Processing transactions for account: {$this->bank_integration->account->key}");
|
||||
nlog("Yodlee: Processing transactions for account: {$this->bank_integration->account->key}");
|
||||
|
||||
do {
|
||||
try {
|
||||
$this->processTransactions();
|
||||
} catch(\Exception $e) {
|
||||
nlog("{$this->bank_integration_account_id} - exited abnormally => ". $e->getMessage());
|
||||
} catch (\Exception $e) {
|
||||
nlog("Yodlee: {$this->bank_integration->bank_account_id} - exited abnormally => " . $e->getMessage());
|
||||
|
||||
$content = [
|
||||
"Processing transactions for account: {$this->bank_integration->account->key} failed",
|
||||
"Processing transactions for account: {$this->bank_integration->bank_account_id} failed",
|
||||
"Exception Details => ",
|
||||
$e->getMessage(),
|
||||
];
|
||||
@ -103,12 +107,12 @@ class ProcessBankTransactions implements ShouldQueue
|
||||
try {
|
||||
$account_summary = $yodlee->getAccountSummary($this->bank_integration->bank_account_id);
|
||||
|
||||
if($account_summary) {
|
||||
if ($account_summary) {
|
||||
|
||||
$at = new AccountTransformer();
|
||||
$account = $at->transform($account_summary);
|
||||
|
||||
if($account[0]['current_balance']) {
|
||||
if ($account[0]['current_balance']) {
|
||||
$this->bank_integration->balance = $account[0]['current_balance'];
|
||||
$this->bank_integration->currency = $account[0]['account_currency'];
|
||||
$this->bank_integration->bank_account_status = $account[0]['account_status'];
|
||||
@ -116,8 +120,8 @@ class ProcessBankTransactions implements ShouldQueue
|
||||
}
|
||||
|
||||
}
|
||||
} catch(\Exception $e) {
|
||||
nlog("YODLEE: unable to update account summary for {$this->bank_integration->bank_account_id} => ". $e->getMessage());
|
||||
} catch (\Exception $e) {
|
||||
nlog("YODLEE: unable to update account summary for {$this->bank_integration->bank_account_id} => " . $e->getMessage());
|
||||
}
|
||||
|
||||
$data = [
|
||||
@ -158,7 +162,7 @@ class ProcessBankTransactions implements ShouldQueue
|
||||
$now = now();
|
||||
|
||||
foreach ($transactions as $transaction) {
|
||||
if (BankTransaction::query()->where('transaction_id', $transaction['transaction_id'])->where('company_id', $this->company->id)->withTrashed()->exists()) {
|
||||
if (BankTransaction::query()->where('transaction_id', $transaction['transaction_id'])->where('company_id', $this->company->id)->where('bank_integration_id', $this->bank_integration->id)->withTrashed()->exists()) { // @turbo124 was not scoped to bank_integration_id => from my pov this should be present, because when an account was historized (is_deleted) a transaction can occur multiple (in the archived bank_integration and in the new one
|
||||
continue;
|
||||
}
|
||||
|
@ -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;
|
||||
@ -43,20 +45,52 @@ class BankTransactionSync implements ShouldQueue
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
//multiDB environment, need to
|
||||
if (config('ninja.db.multi_db_enabled')) {
|
||||
|
||||
foreach (MultiDB::$dbs as $db) {
|
||||
MultiDB::setDB($db);
|
||||
|
||||
nlog("syncing transactions");
|
||||
$this->processYodlee();
|
||||
$this->processNordigen();
|
||||
}
|
||||
|
||||
$a = Account::with('bank_integrations')->whereNotNull('bank_integration_account_id')->cursor()->each(function ($account) {
|
||||
// $queue = Ninja::isHosted() ? 'bank' : 'default';
|
||||
} else {
|
||||
$this->processYodlee();
|
||||
$this->processNordigen();
|
||||
}
|
||||
|
||||
nlog("syncing transactions - done");
|
||||
}
|
||||
|
||||
private function processYodlee()
|
||||
{
|
||||
if (Ninja::isHosted()) { // @turbo124 @todo I migrated the schedule for the job within the kernel to execute on all platforms and use the same expression here to determine if yodlee can run or not. Please chek/verify
|
||||
nlog("syncing transactions - yodlee");
|
||||
|
||||
Account::with('bank_integrations')->whereNotNull('bank_integration_account_id')->cursor()->each(function ($account) {
|
||||
|
||||
if ($account->isPaid() && $account->plan == 'enterprise') {
|
||||
$account->bank_integrations()->where('auto_sync', true)->cursor()->each(function ($bank_integration) use ($account) {
|
||||
(new ProcessBankTransactions($account->bank_integration_account_id, $bank_integration))->handle();
|
||||
$account->bank_integrations()->where('integration_type', BankIntegration::INTEGRATION_TYPE_YODLEE)->where('auto_sync', true)->cursor()->each(function ($bank_integration) use ($account) {
|
||||
(new ProcessBankTransactionsYodlee($account->id, $bank_integration))->handle();
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
private function processNordigen()
|
||||
{
|
||||
if (config("ninja.nordigen.secret_id") && config("ninja.nordigen.secret_key")) { // @turbo124 check condition, when to execute this should be placed here (isSelfHosted || isPro/isEnterprise)
|
||||
nlog("syncing transactions - nordigen");
|
||||
|
||||
Account::with('bank_integrations')->cursor()->each(function ($account) {
|
||||
|
||||
if ((Ninja::isSelfHost() || (Ninja::isHosted() && $account->isPaid() && $account->plan == 'enterprise'))) {
|
||||
$account->bank_integrations()->where('integration_type', BankIntegration::INTEGRATION_TYPE_NORDIGEN)->where('auto_sync', true)->cursor()->each(function ($bank_integration) {
|
||||
(new ProcessBankTransactionsNordigen($bank_integration))->handle();
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -233,7 +233,7 @@ class Account extends BaseModel
|
||||
public function hasFeature($feature)
|
||||
{
|
||||
$plan_details = $this->getPlanDetails();
|
||||
$self_host = ! Ninja::isNinja();
|
||||
$self_host = !Ninja::isNinja();
|
||||
|
||||
switch ($feature) {
|
||||
case self::FEATURE_TASKS:
|
||||
@ -254,35 +254,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
|
||||
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;
|
||||
@ -291,12 +291,12 @@ class Account extends BaseModel
|
||||
|
||||
public function isPaid(): bool
|
||||
{
|
||||
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(): bool
|
||||
{
|
||||
if (! Ninja::isNinja()) {
|
||||
if (!Ninja::isNinja()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -310,7 +310,7 @@ class Account extends BaseModel
|
||||
|
||||
public function isFreeHostedClient(): bool
|
||||
{
|
||||
if (! Ninja::isNinja()) {
|
||||
if (!Ninja::isNinja()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -323,7 +323,7 @@ class Account extends BaseModel
|
||||
|
||||
public function isEnterpriseClient(): bool
|
||||
{
|
||||
if (! Ninja::isNinja()) {
|
||||
if (!Ninja::isNinja()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -359,7 +359,7 @@ class Account extends BaseModel
|
||||
|
||||
public function isTrial(): bool
|
||||
{
|
||||
if (! Ninja::isNinja()) {
|
||||
if (!Ninja::isNinja()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -370,7 +370,7 @@ class Account extends BaseModel
|
||||
|
||||
public function startTrial($plan): void
|
||||
{
|
||||
if (! Ninja::isNinja()) {
|
||||
if (!Ninja::isNinja()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -389,7 +389,7 @@ 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;
|
||||
}
|
||||
|
||||
@ -398,7 +398,7 @@ class Account extends BaseModel
|
||||
$trial_started = false;
|
||||
|
||||
//14 day trial
|
||||
$duration = 60*60*24*14;
|
||||
$duration = 60 * 60 * 24 * 14;
|
||||
|
||||
if ($trial_plan && $include_trial) {
|
||||
$trial_started = $this->trial_started;
|
||||
@ -423,22 +423,22 @@ 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;
|
||||
@ -508,21 +508,21 @@ class Account extends BaseModel
|
||||
|
||||
public function emailsSent()
|
||||
{
|
||||
if (is_null(Cache::get("email_quota".$this->key))) {
|
||||
if (is_null(Cache::get("email_quota" . $this->key))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Cache::get("email_quota".$this->key);
|
||||
return Cache::get("email_quota" . $this->key);
|
||||
}
|
||||
|
||||
public function emailQuotaExceeded() :bool
|
||||
public function emailQuotaExceeded(): bool
|
||||
{
|
||||
if (is_null(Cache::get("email_quota".$this->key))) {
|
||||
if (is_null(Cache::get("email_quota" . $this->key))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (Cache::get("email_quota".$this->key) > $this->getDailyEmailLimit()) {
|
||||
if (Cache::get("email_quota" . $this->key) > $this->getDailyEmailLimit()) {
|
||||
if (is_null(Cache::get("throttle_notified:{$this->key}"))) {
|
||||
App::forgetInstance('translator');
|
||||
$t = app('translator');
|
||||
@ -544,14 +544,14 @@ class Account extends BaseModel
|
||||
|
||||
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");
|
||||
|
||||
@ -582,7 +582,7 @@ class Account extends BaseModel
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch(\Exception $e) {
|
||||
} catch (\Exception $e) {
|
||||
\Sentry\captureMessage("I encountered an error with sending with gmail for account {$this->key}");
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property int $account_id
|
||||
* @property int $company_id
|
||||
* @property int $user_id
|
||||
* @property string $integration_type
|
||||
* @property string $provider_name
|
||||
* @property int $provider_id
|
||||
* @property int $bank_account_id
|
||||
@ -30,6 +31,8 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property float $balance
|
||||
* @property int|null $currency
|
||||
* @property string $nickname
|
||||
* @property string $nordigen_account_id
|
||||
* @property string $nordigen_institution_id
|
||||
* @property string|null $from_date
|
||||
* @property bool $is_deleted
|
||||
* @property int|null $created_at
|
||||
@ -73,6 +76,10 @@ class BankIntegration extends BaseModel
|
||||
'auto_sync',
|
||||
];
|
||||
|
||||
const INTEGRATION_TYPE_YODLEE = 'YODLEE';
|
||||
|
||||
const INTEGRATION_TYPE_NORDIGEN = 'NORDIGEN';
|
||||
|
||||
public function getEntityType()
|
||||
{
|
||||
return self::class;
|
||||
|
@ -34,6 +34,8 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property string|null $date
|
||||
* @property int $bank_account_id
|
||||
* @property string|null $description
|
||||
* @property string|null $participant
|
||||
* @property string|null $participant_name
|
||||
* @property string $invoice_ids
|
||||
* @property int|null $expense_id
|
||||
* @property int|null $vendor_id
|
||||
@ -84,7 +86,9 @@ class BankTransaction extends BaseModel
|
||||
'base_type',
|
||||
'expense_id',
|
||||
'vendor_id',
|
||||
'amount'
|
||||
'amount',
|
||||
'participant',
|
||||
'participant_name'
|
||||
];
|
||||
|
||||
|
||||
@ -162,7 +166,7 @@ class BankTransaction extends BaseModel
|
||||
// return $this->belongsTo(Expense::class)->withTrashed();
|
||||
// }
|
||||
|
||||
public function service() :BankService
|
||||
public function service(): BankService
|
||||
{
|
||||
return new BankService($this);
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ class BaseRepository
|
||||
*/
|
||||
private function getEventClass($entity, $type)
|
||||
{
|
||||
return 'App\Events\\'.ucfirst(class_basename($entity)).'\\'.ucfirst(class_basename($entity)).'Was'.$type;
|
||||
return 'App\Events\\' . ucfirst(class_basename($entity)) . '\\' . ucfirst(class_basename($entity)) . 'Was' . $type;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -67,7 +67,7 @@ class BaseRepository
|
||||
*/
|
||||
public function restore($entity)
|
||||
{
|
||||
if (! $entity->trashed()) {
|
||||
if (!$entity->trashed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -104,7 +104,7 @@ class BaseRepository
|
||||
|
||||
$className = $this->getEventClass($entity, 'Deleted');
|
||||
|
||||
if (class_exists($className) && ! ($entity instanceof Company)) {
|
||||
if (class_exists($className) && !($entity instanceof Company)) {
|
||||
event(new $className($entity, $entity->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
|
||||
}
|
||||
}
|
||||
@ -112,7 +112,7 @@ class BaseRepository
|
||||
/* Returns an invoice if defined as a key in the $resource array*/
|
||||
public function getInvitation($invitation, $resource)
|
||||
{
|
||||
if (is_array($invitation) && ! array_key_exists('key', $invitation)) {
|
||||
if (is_array($invitation) && !array_key_exists('key', $invitation)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -163,7 +163,7 @@ class BaseRepository
|
||||
|
||||
$state['starting_amount'] = $model->balance;
|
||||
|
||||
if (! $model->id) {
|
||||
if (!$model->id) {
|
||||
$company_defaults = $client->setCompanyDefaults($data, lcfirst($resource));
|
||||
$data['exchange_rate'] = $company_defaults['exchange_rate'];
|
||||
$model->uses_inclusive_taxes = $client->getSetting('inclusive_taxes');
|
||||
@ -233,7 +233,7 @@ class BaseRepository
|
||||
|
||||
foreach ($data['invitations'] as $invitation) {
|
||||
//if no invitations are present - create one.
|
||||
if (! $this->getInvitation($invitation, $resource)) {
|
||||
if (!$this->getInvitation($invitation, $resource)) {
|
||||
if (isset($invitation['id'])) {
|
||||
unset($invitation['id']);
|
||||
}
|
||||
@ -279,7 +279,7 @@ class BaseRepository
|
||||
$model = $model->service()->applyNumber()->save();
|
||||
|
||||
/* Handle attempts where the deposit is greater than the amount/balance of the invoice */
|
||||
if ((int)$model->balance != 0 && $model->partial > $model->amount && $model->amount > 0) {
|
||||
if ((int) $model->balance != 0 && $model->partial > $model->amount && $model->amount > 0) {
|
||||
$model->partial = min($model->amount, $model->balance);
|
||||
}
|
||||
|
||||
@ -300,7 +300,7 @@ class BaseRepository
|
||||
// $model->ledger()->updateInvoiceBalance(($state['finished_amount'] - $state['starting_amount']), "Update adjustment for invoice {$model->number}");
|
||||
}
|
||||
|
||||
if (! $model->design_id) {
|
||||
if (!$model->design_id) {
|
||||
$model->design_id = intval($this->decodePrimaryKey($client->getSetting('invoice_design_id')));
|
||||
}
|
||||
|
||||
@ -316,7 +316,7 @@ class BaseRepository
|
||||
}
|
||||
|
||||
/** If the client does not have tax_data - then populate this now */
|
||||
if($client->country_id == 840 && !$client->tax_data && $model->company->calculate_taxes && !$model->company->account->isFreeHostedClient()) {
|
||||
if ($client->country_id == 840 && !$client->tax_data && $model->company->calculate_taxes && !$model->company->account->isFreeHostedClient()) {
|
||||
UpdateTaxData::dispatch($client, $client->company);
|
||||
}
|
||||
|
||||
@ -325,7 +325,7 @@ class BaseRepository
|
||||
if ($model instanceof Credit) {
|
||||
$model = $model->calc()->getCredit();
|
||||
|
||||
if (! $model->design_id) {
|
||||
if (!$model->design_id) {
|
||||
$model->design_id = $this->decodePrimaryKey($client->getSetting('credit_design_id'));
|
||||
}
|
||||
|
||||
@ -345,7 +345,7 @@ class BaseRepository
|
||||
}
|
||||
|
||||
if ($model instanceof Quote) {
|
||||
if (! $model->design_id) {
|
||||
if (!$model->design_id) {
|
||||
$model->design_id = intval($this->decodePrimaryKey($client->getSetting('quote_design_id')));
|
||||
}
|
||||
|
||||
@ -359,7 +359,7 @@ class BaseRepository
|
||||
}
|
||||
|
||||
if ($model instanceof RecurringInvoice) {
|
||||
if (! $model->design_id) {
|
||||
if (!$model->design_id) {
|
||||
$model->design_id = intval($this->decodePrimaryKey($client->getSetting('invoice_design_id')));
|
||||
}
|
||||
|
||||
|
@ -50,20 +50,21 @@ class BankIntegrationTransformer extends EntityTransformer
|
||||
{
|
||||
return [
|
||||
'id' => (string) $this->encodePrimaryKey($bank_integration->id),
|
||||
'provider_name' => (string)$bank_integration->provider_name ?: '',
|
||||
'provider_name' => (string) $bank_integration->provider_name ?: '',
|
||||
'provider_id' => (int) $bank_integration->provider_id ?: 0,
|
||||
'bank_account_id' => (int) $bank_integration->bank_account_id ?: 0,
|
||||
'bank_account_name' => (string) $bank_integration->bank_account_name ?: '',
|
||||
'bank_account_number' => (string) $bank_integration->bank_account_number ?: '',
|
||||
'bank_account_status' => (string)$bank_integration->bank_account_status ?: '',
|
||||
'bank_account_type' => (string)$bank_integration->bank_account_type ?: '',
|
||||
'balance' => (float)$bank_integration->balance ?: 0,
|
||||
'currency' => (string)$bank_integration->currency ?: '',
|
||||
'nickname' => (string)$bank_integration->nickname ?: '',
|
||||
'from_date' => (string)$bank_integration->from_date ?: '',
|
||||
'bank_account_status' => (string) $bank_integration->bank_account_status ?: '',
|
||||
'bank_account_type' => (string) $bank_integration->bank_account_type ?: '',
|
||||
'nordigen_institution_id' => (string) $bank_integration->nordigen_institution_id ?: '',
|
||||
'balance' => (float) $bank_integration->balance ?: 0,
|
||||
'currency' => (string) $bank_integration->currency ?: '',
|
||||
'nickname' => (string) $bank_integration->nickname ?: '',
|
||||
'from_date' => (string) $bank_integration->from_date ?: '',
|
||||
'is_deleted' => (bool) $bank_integration->is_deleted,
|
||||
'disabled_upstream' => (bool) $bank_integration->disabled_upstream,
|
||||
'auto_sync' => (bool)$bank_integration->auto_sync,
|
||||
'auto_sync' => (bool) $bank_integration->auto_sync,
|
||||
'created_at' => (int) $bank_integration->created_at,
|
||||
'updated_at' => (int) $bank_integration->updated_at,
|
||||
'archived_at' => (int) $bank_integration->deleted_at,
|
||||
|
@ -63,11 +63,13 @@ class BankTransactionTransformer extends EntityTransformer
|
||||
'bank_account_id' => (int) $bank_transaction->bank_account_id,
|
||||
'status_id' => (string) $bank_transaction->status_id,
|
||||
'description' => (string) $bank_transaction->description ?: '',
|
||||
'participant' => (string) $bank_transaction->participant ?: '',
|
||||
'participant_name' => (string) $bank_transaction->participant_name ?: '',
|
||||
'base_type' => (string) $bank_transaction->base_type ?: '',
|
||||
'invoice_ids' => (string) $bank_transaction->invoice_ids ?: '',
|
||||
'expense_id'=> (string) $bank_transaction->expense_id ?: '',
|
||||
'payment_id'=> (string) $this->encodePrimaryKey($bank_transaction->payment_id) ?: '',
|
||||
'vendor_id'=> (string) $this->encodePrimaryKey($bank_transaction->vendor_id) ?: '',
|
||||
'expense_id' => (string) $bank_transaction->expense_id ?: '',
|
||||
'payment_id' => (string) $this->encodePrimaryKey($bank_transaction->payment_id) ?: '',
|
||||
'vendor_id' => (string) $this->encodePrimaryKey($bank_transaction->vendor_id) ?: '',
|
||||
'bank_transaction_rule_id' => (string) $this->encodePrimaryKey($bank_transaction->bank_transaction_rule_id) ?: '',
|
||||
'is_deleted' => (bool) $bank_transaction->is_deleted,
|
||||
'created_at' => (int) $bank_transaction->created_at,
|
||||
|
@ -72,6 +72,7 @@
|
||||
"microsoft/microsoft-graph": "^1.69",
|
||||
"mollie/mollie-api-php": "^2.36",
|
||||
"nelexa/zip": "^4.0",
|
||||
"nordigen/nordigen-php": "^1.1",
|
||||
"nwidart/laravel-modules": "^10.0",
|
||||
"omnipay/paypal": "^3.0",
|
||||
"payfast/payfast-php-sdk": "^1.1",
|
||||
|
2638
composer.lock
generated
2638
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -196,7 +196,7 @@ return [
|
||||
'ninja_default_company_id' => env('NINJA_COMPANY_ID', null),
|
||||
'ninja_default_company_gateway_id' => env('NINJA_COMPANY_GATEWAY_ID', null),
|
||||
'ninja_hosted_secret' => env('NINJA_HOSTED_SECRET', ''),
|
||||
'ninja_hosted_header' =>env('NINJA_HEADER', ''),
|
||||
'ninja_hosted_header' => env('NINJA_HEADER', ''),
|
||||
'ninja_connect_secret' => env('NINJA_CONNECT_SECRET', ''),
|
||||
'internal_queue_enabled' => env('INTERNAL_QUEUE_ENABLED', true),
|
||||
'ninja_apple_api_key' => env('APPLE_API_KEY', false),
|
||||
@ -218,6 +218,11 @@ return [
|
||||
'dev_mode' => env("YODLEE_DEV_MODE", false),
|
||||
'config_name' => env("YODLEE_CONFIG_NAME", false),
|
||||
],
|
||||
'nordigen' => [
|
||||
'secret_id' => env('NORDIGEN_SECRET_ID', false),
|
||||
'secret_key' => env('NORDIGEN_SECRET_KEY', false),
|
||||
'test_mode' => env("NORDIGEN_TEST_MODE", false),
|
||||
],
|
||||
'licenses' => env('LICENSES', false),
|
||||
'google_application_credentials' => env("GOOGLE_APPLICATION_CREDENTIALS", false),
|
||||
'shopify' => [
|
||||
|
@ -23,6 +23,7 @@ class BankIntegrationFactory extends Factory
|
||||
public function definition()
|
||||
{
|
||||
return [
|
||||
'integration_type' => null,
|
||||
'provider_name' => $this->faker->company(),
|
||||
'provider_id' => 1,
|
||||
'bank_account_name' => $this->faker->catchPhrase(),
|
||||
|
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Account;
|
||||
use App\Models\BankIntegration;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('bank_integrations', function (Blueprint $table) {
|
||||
$table->string('integration_type')->nullable();
|
||||
$table->string('nordigen_account_id')->nullable();
|
||||
$table->string('nordigen_institution_id')->nullable();
|
||||
});
|
||||
|
||||
// migrate old account to be used with yodlee
|
||||
BankIntegration::query()->whereNull('integration_type')->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('bank_transactions', function (Blueprint $table) {
|
||||
$table->string('participant')->nullable(); // iban, credit-card info or else
|
||||
$table->string('participant_name')->nullable(); // name
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
@ -5227,6 +5227,27 @@ $lang = array(
|
||||
'primary_contact' => 'Primary Contact',
|
||||
'all_contacts' => 'All Contacts',
|
||||
'insert_below' => 'Insert Below',
|
||||
'nordigen_handler_subtitle' => 'will gain access for your selected bank account. After selecting your institution you are redirected to theire front-page to complete the request with your account credentials.',
|
||||
'nordigen_handler_error_heading_unknown' => 'An Error has occured',
|
||||
'nordigen_handler_error_contents_unknown' => 'An unknown Error has occured! Reason:',
|
||||
'nordigen_handler_error_heading_token_invalid' => 'Invalid Token',
|
||||
'nordigen_handler_error_contents_token_invalid' => 'The provided token was invalid. Please restart the flow, with a valid one_time_token. Contact support for help, if this issue persists.',
|
||||
'nordigen_handler_error_heading_account_config_invalid' => 'Missing Credentials',
|
||||
'nordigen_handler_error_contents_account_config_invalid' => 'The provided credentials for nordigen are eighter missing or invalid. Contact support for help, if this issue persists.',
|
||||
'nordigen_handler_error_heading_not_available' => 'Not Available',
|
||||
'nordigen_handler_error_contents_not_available' => 'This flow is not available for your account. Considder upgrading to enterprise version. Contact support for help, if this issue persists.',
|
||||
'nordigen_handler_error_heading_institution_invalid' => 'Invalid Institution',
|
||||
'nordigen_handler_error_contents_institution_invalid' => 'The provided institution-id is invalid or no longer valid. You can go to the bank selection page by clicking the button below or cancel the flow by clicking on the \'X\' above.',
|
||||
'nordigen_handler_error_heading_ref_invalid' => 'Invalid Reference',
|
||||
'nordigen_handler_error_contents_ref_invalid' => 'Nordigen did not provide a valid reference. Please run flow again and contact support, if this issue persists.',
|
||||
'nordigen_handler_error_heading_not_found' => 'Invalid Requisition',
|
||||
'nordigen_handler_error_contents_not_found' => 'Nordigen did not provide a valid reference. Please run flow again and contact support, if this issue persists.',
|
||||
'nordigen_handler_error_heading_requisition_invalid_status' => 'Not Ready',
|
||||
'nordigen_handler_error_contents_requisition_invalid_status' => 'You may called this site to early. Please finish authorization and refresh this page. Contact support for help, if this issue persists.',
|
||||
'nordigen_handler_error_heading_requisition_no_accounts' => 'No Accounts selected',
|
||||
'nordigen_handler_error_contents_requisition_no_accounts' => 'The service has not returned any valid accounts. Considder restarting the flow.',
|
||||
'nordigen_handler_restart' => 'Restart flow.',
|
||||
'nordigen_handler_return' => 'Return to application.',
|
||||
);
|
||||
|
||||
return $lang;
|
||||
|
135
resources/views/bank/nordigen/handler.blade.php
Normal file
135
resources/views/bank/nordigen/handler.blade.php
Normal file
@ -0,0 +1,135 @@
|
||||
@extends('layouts.ninja')
|
||||
@section('meta_title', ctrans('texts.new_bank_account'))
|
||||
|
||||
@push('head')
|
||||
|
||||
<link href="https://unpkg.com/nordigen-bank-ui@1.5.2/package/src/selector.min.css" rel="stylesheet" />
|
||||
|
||||
@endpush
|
||||
|
||||
@section('body')
|
||||
|
||||
<div id="institution-content-wrapper"></div>
|
||||
|
||||
@endsection
|
||||
|
||||
@push('footer')
|
||||
|
||||
<script type='text/javascript' src='https://unpkg.com/nordigen-bank-ui@1.5.2/package/src/selector.min.js'></script>
|
||||
|
||||
<script>
|
||||
// Pass your redirect link after user has been authorized in institution
|
||||
const config = {
|
||||
// Redirect URL that is being used when modal is being closed.
|
||||
redirectUrl: "{{ $redirectUrl }}" || new URL("", window.location.origin).href,
|
||||
// Text that will be displayed on the left side under the logo. Text is limited to 100 characters, and rest will be truncated. @turbo124 replace with a translated version like ctrans()
|
||||
text: "{{ ($account ?? false) && !$account->isPaid() ? 'Invoice Ninja' : (isset($company) && !is_null($company) ? $company->name : 'Invoice Ninja') }} {{ ctrans('texts.nordigen_handler_subtitle', [], $lang ?? 'en') }}",
|
||||
// Logo URL that will be shown below the modal form.
|
||||
logoUrl: "{{ ($account ?? false) && !$account->isPaid() ? asset('images/invoiceninja-black-logo-2.png') : (isset($company) && !is_null($company) ? $company->present()->logo() : asset('images/invoiceninja-black-logo-2.png')) }}",
|
||||
// Will display country list with corresponding institutions. When `countryFilter` is set to `false`, only list of institutions will be shown.
|
||||
countryFilter: false,
|
||||
// style configs
|
||||
styles: {
|
||||
// Primary
|
||||
// Link to google font
|
||||
fontFamily: new URL("assets/fonts/Roboto-Regular.ttf", window.location.origin).href,
|
||||
fontSize: '15',
|
||||
backgroundColor: '#F2F2F2',
|
||||
textColor: '#222',
|
||||
headingColor: '#222',
|
||||
linkColor: '#8d9090',
|
||||
// Modal
|
||||
modalTextColor: '#1B2021',
|
||||
modalBackgroundColor: '#fff',
|
||||
// Button
|
||||
buttonColor: '#3A53EE',
|
||||
buttonTextColor: '#fff'
|
||||
}
|
||||
};
|
||||
|
||||
const failedReason = "{{ $failed_reason ?? '' }}".trim();
|
||||
|
||||
new institutionSelector(@json($institutions ?? []), 'institution-modal-content', config);
|
||||
|
||||
if (!failedReason) {
|
||||
|
||||
const institutionList = Array.from(document.querySelectorAll('.ob-list-institution > a'));
|
||||
|
||||
institutionList.forEach((institution) => {
|
||||
institution.addEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
const institutionId = institution.getAttribute('data-institution');
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('institution_id', institutionId);
|
||||
window.location.href = url.href;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
document.getElementsByClassName("institution-search-container")[0].remove();
|
||||
document.getElementsByClassName("institution-container")[0].remove();
|
||||
|
||||
const heading = document.querySelectorAll('h2')[0];
|
||||
const wrapper = document.getElementById("institution-modal-content");
|
||||
const contents = document.createElement("div");
|
||||
contents.id = "failed-container";
|
||||
contents.className = "mt-2";
|
||||
contents.style["font-size"] = "80%";
|
||||
contents.style["opacity"] = "80%";
|
||||
|
||||
let restartFlow = false; // return, restart, refresh
|
||||
heading.innerHTML = "{{ ctrans('texts.nordigen_handler_error_heading_unknown', [], $lang ?? 'en') }}";
|
||||
contents.innerHTML = "{{ ctrans('texts.nordigen_handler_error_contents_unknown', [], $lang ?? 'en') }} " + failedReason;
|
||||
switch (failedReason) {
|
||||
// Connect Screen Errors
|
||||
case "token-invalid":
|
||||
heading.innerHTML = "{{ ctrans('texts.nordigen_handler_error_heading_token_invalid', [], $lang ?? 'en') }}";
|
||||
contents.innerHTML = "{{ ctrans('texts.nordigen_handler_error_contents_token_invalid', [], $lang ?? 'en') }}";
|
||||
break;
|
||||
case "account-config-invalid":
|
||||
heading.innerHTML = "{{ ctrans('texts.nordigen_handler_error_heading_account_config_invalid', [], $lang ?? 'en') }}";
|
||||
contents.innerHTML = "{{ ctrans('texts.nordigen_handler_error_contents_account_config_invalid', [], $lang ?? 'en') }}";
|
||||
break;
|
||||
case "not-available":
|
||||
heading.innerHTML = "{{ ctrans('texts.nordigen_handler_error_heading_not_available', [], $lang ?? 'en') }}";
|
||||
contents.innerHTML = "{{ ctrans('texts.nordigen_handler_error_contents_not_available', [], $lang ?? 'en') }}";
|
||||
break;
|
||||
case "institution-invalid":
|
||||
restartFlow = true;
|
||||
heading.innerHTML = "{{ ctrans('texts.nordigen_handler_error_heading_institution_invalid', [], $lang ?? 'en') }}";
|
||||
contents.innerHTML = "{{ ctrans('texts.nordigen_handler_error_contents_institution_invalid', [], $lang ?? 'en') }}";
|
||||
break;
|
||||
// Confirm Screen Errors
|
||||
case "ref-invalid":
|
||||
heading.innerHTML = "{{ ctrans('texts.nordigen_handler_error_heading_ref_invalid', [], $lang ?? 'en') }}";
|
||||
contents.innerHTML = "{{ ctrans('texts.nordigen_handler_error_contents_ref_invalid', [], $lang ?? 'en') }}";
|
||||
break;
|
||||
case "requisition-not-found":
|
||||
heading.innerHTML = "{{ ctrans('texts.nordigen_handler_error_heading_not_found', [], $lang ?? 'en') }}";
|
||||
contents.innerHTML = "{{ ctrans('texts.nordigen_handler_error_contents_not_found', [], $lang ?? 'en') }}";
|
||||
break;
|
||||
case "requisition-invalid-status":
|
||||
heading.innerHTML = "{{ ctrans('texts.nordigen_handler_error_heading_requisition_invalid_status', [], $lang ?? 'en') }}";
|
||||
contents.innerHTML = "{{ ctrans('texts.nordigen_handler_error_contents_requisition_invalid_status', [], $lang ?? 'en') }}";
|
||||
break;
|
||||
case "requisition-no-accounts":
|
||||
heading.innerHTML = "{{ ctrans('texts.nordigen_handler_error_heading_requisition_no_accounts', [], $lang ?? 'en') }}";
|
||||
contents.innerHTML = "{{ ctrans('texts.nordigen_handler_error_contents_requisition_no_accounts', [], $lang ?? 'en') }}";
|
||||
break;
|
||||
case "unknown":
|
||||
break;
|
||||
default:
|
||||
console.warn('Invalid or missing failed_reason code: ' + failedReason);
|
||||
break;
|
||||
}
|
||||
wrapper.appendChild(contents);
|
||||
|
||||
const restartUrl = new URL(window.location.pathname, window.location.origin); // no searchParams
|
||||
const returnButton = document.createElement('div');
|
||||
returnButton.className = "mt-4";
|
||||
returnButton.innerHTML = `<a class="button button-primary bg-blue-600 my-4" href="${restartFlow ? restartUrl.href : config.redirectUrl}">${restartFlow ? "{{ ctrans('texts.nordigen_handler_restart', [], $lang ?? 'en') }}" : "{{ ctrans('texts.nordigen_handler_return', [], $lang ?? 'en') }}"}</a>`
|
||||
wrapper.appendChild(returnButton);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@endpush
|
@ -15,6 +15,7 @@ use App\Http\Controllers\ActivityController;
|
||||
use App\Http\Controllers\Auth\ForgotPasswordController;
|
||||
use App\Http\Controllers\Auth\LoginController;
|
||||
use App\Http\Controllers\Auth\PasswordTimeoutController;
|
||||
use App\Http\Controllers\Bank\NordigenController;
|
||||
use App\Http\Controllers\Bank\YodleeController;
|
||||
use App\Http\Controllers\BankIntegrationController;
|
||||
use App\Http\Controllers\BankTransactionController;
|
||||
@ -121,7 +122,7 @@ Route::group(['middleware' => ['throttle:api', 'api_secret_check']], function ()
|
||||
Route::post('api/v1/oauth_login', [LoginController::class, 'oauthApiLogin']);
|
||||
});
|
||||
|
||||
Route::group(['middleware' => ['throttle:login','api_secret_check','email_db']], function () {
|
||||
Route::group(['middleware' => ['throttle:login', 'api_secret_check', 'email_db']], function () {
|
||||
Route::post('api/v1/login', [LoginController::class, 'apiLogin'])->name('login.submit');
|
||||
Route::post('api/v1/reset_password', [ForgotPasswordController::class, 'sendResetLinkEmail']);
|
||||
});
|
||||
@ -400,7 +401,9 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale']
|
||||
Route::get('statics', StaticController::class);
|
||||
// Route::post('apple_pay/upload_file','ApplyPayController::class, 'upload');
|
||||
|
||||
Route::post('api/v1/yodlee/status/{account_number}', [YodleeController::class, 'accountStatus']);
|
||||
Route::post('yodlee/status/{account_number}', [YodleeController::class, 'accountStatus']); // @todo @turbo124 check route-path?!
|
||||
|
||||
Route::get('nordigen/institutions', [NordigenController::class, 'institutions'])->name('nordigen.institutions');
|
||||
});
|
||||
|
||||
Route::post('api/v1/sms_reset', [TwilioController::class, 'generate2faResetCode'])->name('sms_reset.generate')->middleware('throttle:10,1');
|
||||
@ -421,6 +424,7 @@ Route::post('api/v1/get_migration_account', [HostedMigrationController::class, '
|
||||
Route::post('api/v1/confirm_forwarding', [HostedMigrationController::class, 'confirmForwarding'])->middleware('guest')->middleware('throttle:100,1');
|
||||
Route::post('api/v1/process_webhook', [AppleController::class, 'process_webhook'])->middleware('throttle:1000,1');
|
||||
Route::post('api/v1/confirm_purchase', [AppleController::class, 'confirm_purchase'])->middleware('throttle:1000,1');
|
||||
|
||||
Route::post('api/v1/yodlee/refresh', [YodleeController::class, 'refreshWebhook'])->middleware('throttle:100,1');
|
||||
Route::post('api/v1/yodlee/data_updates', [YodleeController::class, 'dataUpdatesWebhook'])->middleware('throttle:100,1');
|
||||
Route::post('api/v1/yodlee/refresh_updates', [YodleeController::class, 'refreshUpdatesWebhook'])->middleware('throttle:100,1');
|
||||
|
@ -3,6 +3,7 @@
|
||||
use App\Http\Controllers\Auth\ForgotPasswordController;
|
||||
use App\Http\Controllers\Auth\LoginController;
|
||||
use App\Http\Controllers\Auth\ResetPasswordController;
|
||||
use App\Http\Controllers\Bank\NordigenController;
|
||||
use App\Http\Controllers\Bank\YodleeController;
|
||||
use App\Http\Controllers\BaseController;
|
||||
use App\Http\Controllers\ClientPortal\ApplePayDomainController;
|
||||
@ -55,6 +56,9 @@ Route::get('stripe/completed', [StripeConnectController::class, 'completed'])->n
|
||||
|
||||
Route::get('yodlee/onboard/{token}', [YodleeController::class, 'auth'])->name('yodlee.auth');
|
||||
|
||||
Route::get('nordigen/connect/{token}', [NordigenController::class, 'connect'])->name('nordigen.connect');
|
||||
Route::any('nordigen/confirm', [NordigenController::class, 'confirm'])->name('nordigen.confirm');
|
||||
|
||||
Route::get('checkout/3ds_redirect/{company_key}/{company_gateway_id}/{hash}', [Checkout3dsController::class, 'index'])->middleware('domain_db')->name('checkout.3ds_redirect');
|
||||
Route::get('mollie/3ds_redirect/{company_key}/{company_gateway_id}/{hash}', [Mollie3dsController::class, 'index'])->middleware('domain_db')->name('mollie.3ds_redirect');
|
||||
Route::get('gocardless/ibp_redirect/{company_key}/{company_gateway_id}/{hash}', [GoCardlessController::class, 'ibpRedirect'])->middleware('domain_db')->name('gocardless.ibp_redirect');
|
||||
|
@ -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;
|
||||
@ -70,7 +70,7 @@ 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()
|
||||
@ -127,7 +127,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());
|
||||
|
||||
@ -213,10 +213,10 @@ class YodleeApiTest extends TestCase
|
||||
public function testDataMatching()
|
||||
{
|
||||
$transaction = collect([
|
||||
(object)[
|
||||
(object) [
|
||||
'description' => 'tinkertonkton'
|
||||
],
|
||||
(object)[
|
||||
(object) [
|
||||
'description' => 'spud'
|
||||
],
|
||||
]);
|
||||
@ -237,10 +237,10 @@ class YodleeApiTest extends TestCase
|
||||
|
||||
|
||||
$transaction = collect([
|
||||
(object)[
|
||||
(object) [
|
||||
'description' => 'tinker and spice'
|
||||
],
|
||||
(object)[
|
||||
(object) [
|
||||
'description' => 'spud with water'
|
||||
],
|
||||
]);
|
||||
|
Loading…
x
Reference in New Issue
Block a user