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();
|
||||
|
||||
|
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":{
|
||||
|
@ -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);
|
||||
|
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());
|
||||
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(),
|
||||
];
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
];
|
||||
|
||||
|
||||
|
@ -57,6 +57,7 @@ class BankIntegrationTransformer extends EntityTransformer
|
||||
'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 ?: '',
|
||||
'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 ?: '',
|
||||
|
@ -63,6 +63,8 @@ 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 ?: '',
|
||||
|
@ -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
@ -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;
|
||||
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user