Revert latest nordigen commit
This commit is contained in:
parent
95b8454c15
commit
5c60a3efed
|
|
@ -23,7 +23,6 @@ use App\Models\Company;
|
|||
use App\Services\Email\Email;
|
||||
use App\Models\BankIntegration;
|
||||
use App\Services\Email\EmailObject;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Mail\Mailables\Address;
|
||||
|
|
@ -34,7 +33,7 @@ class Nordigen
|
|||
{
|
||||
public bool $test_mode; // https://developer.gocardless.com/bank-account-data/sandbox
|
||||
|
||||
public string $sandbox_institutionId = 'SANDBOXFINANCE_SFIN0000';
|
||||
public string $sandbox_institutionId = "SANDBOXFINANCE_SFIN0000";
|
||||
|
||||
protected \Nordigen\NordigenPHP\API\NordigenClient $client;
|
||||
|
||||
|
|
@ -61,116 +60,70 @@ class Nordigen
|
|||
return $this->client->institution->getInstitutions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get end user agreement details by ID.
|
||||
*
|
||||
* @return array{
|
||||
* id: string,
|
||||
* created: string,
|
||||
* institution_id: string,
|
||||
* max_historical_days: int,
|
||||
* access_valid_for_days: int,
|
||||
* access_scope: string[],
|
||||
* accepted: string
|
||||
* } Agreement details
|
||||
*/
|
||||
public function getAgreement(string $euaId): array
|
||||
// requisition-section
|
||||
public function createRequisition(string $redirect, string $institutionId, string $reference, string $userLanguage)
|
||||
{
|
||||
return $this->client->endUserAgreement->getEndUserAgreement($euaId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of end user agreements
|
||||
*
|
||||
* @return array{
|
||||
* id: string,
|
||||
* created: string,
|
||||
* institution_id: string,
|
||||
* max_historical_days: int,
|
||||
* access_valid_for_days: int,
|
||||
* access_scope: string[],
|
||||
* accepted: ?string,
|
||||
* }[] EndUserAgreement list
|
||||
*/
|
||||
public function firstValidAgreement(string $institutionId, int $accessDays, int $txDays): ?array
|
||||
{
|
||||
$requiredScopes = ['balances', 'details', 'transactions'];
|
||||
|
||||
try {
|
||||
return Arr::first(
|
||||
$this->client->endUserAgreement->getEndUserAgreements()['results'],
|
||||
function (array $eua) use ($institutionId, $requiredScopes, $accessDays, $txDays): bool {
|
||||
return $eua['institution_id'] === $institutionId
|
||||
&& $eua['accepted'] === null
|
||||
&& $eua['max_historical_days'] >= $txDays
|
||||
&& $eua['access_valid_for_days'] >= $accessDays
|
||||
&& !array_diff($requiredScopes, $eua['access_scope'] ?? []);
|
||||
},
|
||||
null
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
$debug = "{$e->getMessage()} ({$e->getCode()})";
|
||||
|
||||
nlog("Nordigen: Unable to fetch End User Agreements for institution '{$institutionId}': {$debug}");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new End User Agreement with the given parameters
|
||||
*
|
||||
* @param array{id: string, transaction_total_days: int, max_access_valid_for_days: int} $institution
|
||||
*
|
||||
* @throws \Nordigen\NordigenPHP\Exceptions\NordigenExceptions\NordigenException
|
||||
*
|
||||
* @return array{
|
||||
* id: string,
|
||||
* created: string,
|
||||
* institution_id: string,
|
||||
* max_historical_days: int,
|
||||
* access_valid_for_days: int,
|
||||
* access_scope: string[],
|
||||
* accepted: string
|
||||
* }|null Agreement details
|
||||
*/
|
||||
public function createAgreement(array $institution, int $accessDays, int $transactionDays): ?array
|
||||
{
|
||||
$txDays = $transactionDays < 30 ? 30 : $transactionDays;
|
||||
$maxAccess = $institution['max_access_valid_for_days'];
|
||||
$maxTx = $institution['transaction_total_days'];
|
||||
|
||||
return $this->client->endUserAgreement->createEndUserAgreement(
|
||||
accessValidForDays: $accessDays > $maxAccess ? $maxAccess : $accessDays,
|
||||
maxHistoricalDays: $txDays > $maxTx ? $maxTx : $txDays,
|
||||
institutionId: $institution['id'],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Bank Requisition
|
||||
*
|
||||
* @param array{id: string} $institution,
|
||||
* @param array{id: ?string, transaction_total_days: int} $agreement
|
||||
*/
|
||||
public function createRequisition(
|
||||
string $redirect,
|
||||
array $institution,
|
||||
array $agreement,
|
||||
string $reference,
|
||||
string $userLanguage,
|
||||
): array {
|
||||
if ($this->test_mode && $institution['id'] != $this->sandbox_institutionId) {
|
||||
if ($this->test_mode && $institutionId != $this->sandbox_institutionId) {
|
||||
throw new \Exception('invalid institutionId while in test-mode');
|
||||
}
|
||||
|
||||
return $this->client->requisition->createRequisition(
|
||||
$redirect,
|
||||
$institution['id'],
|
||||
$agreement['id'] ?? null,
|
||||
$reference,
|
||||
$userLanguage
|
||||
);
|
||||
return $this->client->requisition->createRequisition($redirect, $institutionId, $this->getExtendedEndUserAggreementId($institutionId), $reference, $userLanguage);
|
||||
}
|
||||
|
||||
private function getExtendedEndUserAggreementId(string $institutionId): string|null
|
||||
{
|
||||
|
||||
$endUserAggreements = null;
|
||||
$endUserAgreement = null;
|
||||
|
||||
// try to fetch endUserAgreements
|
||||
try {
|
||||
$endUserAggreements = $this->client->endUserAgreement->getEndUserAgreements();
|
||||
} catch (\Exception $e) { // not able to accept it
|
||||
nlog("Nordigen: Was not able to fetch endUserAgreements. We continue with defaults to setup bank_integration. {$institutionId} {$e->getMessage()} {$e->getCode()}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// try to find an existing valid endUserAgreement
|
||||
foreach ($endUserAggreements["results"] as $row) {
|
||||
$endUserAgreement = $row;
|
||||
|
||||
// Validate Institution
|
||||
if ($endUserAgreement["institution_id"] != $institutionId)
|
||||
continue;
|
||||
|
||||
// Validate Access Scopes
|
||||
$requiredScopes = ["balances", "details", "transactions"];
|
||||
if (isset($endUserAgreement['access_scope']) && array_diff($requiredScopes, $endUserAgreement['access_scope']))
|
||||
continue;
|
||||
|
||||
// try to accept the endUserAgreement when not already accepted
|
||||
if (empty($endUserAgreement["accepted"]))
|
||||
try {
|
||||
$this->client->endUserAgreement->acceptEndUserAgreement($endUserAgreement["id"], request()->userAgent(), request()->ip());
|
||||
} catch (\Exception $e) { // not able to accept it
|
||||
nlog("Nordigen: Was not able to confirm an existing outstanding endUserAgreement for this institution. We now try to find another or will create and confirm a new one. {$institutionId} {$endUserAgreement["id"]} {$e->getMessage()} {$e->getCode()}");
|
||||
$endUserAgreement = null;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// try to create and accept an endUserAgreement
|
||||
if (!$endUserAgreement)
|
||||
try {
|
||||
$endUserAgreement = $this->client->endUserAgreement->createEndUserAgreement($institutionId, ['details', 'balances', 'transactions'], 90, 180);
|
||||
$this->client->endUserAgreement->acceptEndUserAgreement($endUserAgreement["id"], request()->userAgent(), request()->ip());
|
||||
} catch (\Exception $e) { // not able to create this for this institution
|
||||
nlog("Nordigen: Was not able to create and confirm a new endUserAgreement for this institution. We continue with defaults to setup bank_integration. {$institutionId} {$e->getMessage()} {$e->getCode()}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $endUserAgreement["id"];
|
||||
}
|
||||
|
||||
public function getRequisition(string $requisitionId)
|
||||
|
|
@ -178,7 +131,7 @@ class Nordigen
|
|||
try {
|
||||
return $this->client->requisition->getRequisition($requisitionId);
|
||||
} catch (\Exception $e) {
|
||||
if (strpos($e->getMessage(), 'Invalid Requisition ID') !== false) {
|
||||
if (strpos($e->getMessage(), "Invalid Requisition ID") !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -192,10 +145,10 @@ class Nordigen
|
|||
try {
|
||||
$out = new \stdClass();
|
||||
|
||||
$out->data = $this->client->account($account_id)->getAccountDetails()['account'];
|
||||
$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']);
|
||||
$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);
|
||||
|
|
@ -227,9 +180,8 @@ class Nordigen
|
|||
try {
|
||||
$account = $this->client->account($account_id)->getAccountMetaData();
|
||||
|
||||
if ($account['status'] != 'READY') {
|
||||
nlog("Nordigen account '{$account_id}' is not ready (status={$account['status']})");
|
||||
|
||||
if ($account["status"] != "READY") {
|
||||
nlog('nordigen account was not in status ready. accountId: ' . $account_id . ' status: ' . $account["status"]);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -238,7 +190,7 @@ class Nordigen
|
|||
|
||||
nlog("Nordigen:: AccountActiveStatus:: {$e->getMessage()} {$e->getCode()}");
|
||||
|
||||
if (strpos($e->getMessage(), 'Invalid Account ID') !== false) {
|
||||
if (strpos($e->getMessage(), "Invalid Account ID") !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -288,4 +240,4 @@ class Nordigen
|
|||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
|
|
@ -110,7 +111,6 @@ class AccountTransformer implements AccountTransformerInterface
|
|||
'provider_account_id' => $nordigen_account->metadata["id"],
|
||||
'provider_id' => $nordigen_account->institution["id"],
|
||||
'provider_name' => $nordigen_account->institution["name"],
|
||||
'provider_history' => $nordigen_account->institution["transaction_total_days"],
|
||||
'nickname' => isset($nordigen_account->data["ownerName"]) ? $nordigen_account->data["ownerName"] : '',
|
||||
'current_balance' => (float) $used_balance ? $used_balance["balanceAmount"]["amount"] : 0,
|
||||
'account_currency' => $used_balance ? $used_balance["balanceAmount"]["currency"] : '',
|
||||
|
|
|
|||
|
|
@ -17,170 +17,216 @@ use App\Http\Requests\Nordigen\ConfirmNordigenBankIntegrationRequest;
|
|||
use App\Http\Requests\Nordigen\ConnectNordigenBankIntegrationRequest;
|
||||
use App\Jobs\Bank\ProcessBankTransactionsNordigen;
|
||||
use App\Models\BankIntegration;
|
||||
use App\Models\Company;
|
||||
use App\Utils\Ninja;
|
||||
use Cache;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Nordigen\NordigenPHP\Exceptions\NordigenExceptions\NordigenException;
|
||||
|
||||
class NordigenController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Handles the initial bank connection flow
|
||||
* VIEW: Connect Nordigen Bank Integration
|
||||
* @param ConnectNordigenBankIntegrationRequest $request
|
||||
*/
|
||||
public function connect(ConnectNordigenBankIntegrationRequest $request): View|RedirectResponse
|
||||
public function connect(ConnectNordigenBankIntegrationRequest $request)
|
||||
{
|
||||
$data = $request->all();
|
||||
|
||||
/** @var array $context */
|
||||
$context = $request->getTokenContent();
|
||||
$company = $request->getCompany();
|
||||
$lang = substr($company->locale(), 0, 2);
|
||||
$context["lang"] = $lang;
|
||||
|
||||
if (!$context) {
|
||||
return $this->failed('token-invalid', ['lang' => 'en']);
|
||||
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();
|
||||
$context['redirect'] = $data['redirect'];
|
||||
$context['lang'] = $lang = substr($company->locale(), 0, 2);
|
||||
|
||||
if ($context['context'] != 'nordigen' || array_key_exists('requisitionId', $context)) {
|
||||
return $this->failed('token-invalid', $context);
|
||||
}
|
||||
$account = $company->account;
|
||||
|
||||
if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key'))) {
|
||||
return $this->failed('account-config-invalid', $context, $company);
|
||||
}
|
||||
|
||||
if (!(Ninja::isSelfHost() || (Ninja::isHosted() && $company->account->isEnterprisePaidClient()))) {
|
||||
return $this->failed('not-available', $context, $company);
|
||||
}
|
||||
|
||||
$nordigen = new Nordigen();
|
||||
$institutions = $nordigen->getInstitutions();
|
||||
|
||||
// show bank_selection_screen, when institution_id is not present
|
||||
if (!isset($data['institution_id'], $data['tx_days'])) {
|
||||
return view('bank.nordigen.handler', [
|
||||
'lang' => $lang,
|
||||
'company' => $company,
|
||||
'account' => $company->account,
|
||||
'institutions' => $institutions,
|
||||
'institutionId' => $data['institution_id'] ?? null,
|
||||
'redirectUrl' => $context['redirect'] . '?action=nordigen_connect&status=user-aborted'
|
||||
'failed_reason' => "account-config-invalid",
|
||||
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=account-config-invalid",
|
||||
]);
|
||||
}
|
||||
|
||||
$institution = array_values(array_filter($institutions, function ($institution) use ($data) {
|
||||
return $institution['id'] == $data['institution_id'];
|
||||
}))[0];
|
||||
if (!(Ninja::isSelfHost() || (Ninja::isHosted() && $account->isEnterprisePaidClient()))) {
|
||||
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",
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$txDays = $data['tx_days'] ?? 0;
|
||||
$nordigen = new Nordigen();
|
||||
|
||||
$agreement = $nordigen->firstValidAgreement($institution['id'], $data['access_days'] ?? 0, $txDays)
|
||||
?? $nordigen->createAgreement($institution, $data['access_days'] ?? 9999, $txDays);
|
||||
} catch (\Exception $e) {
|
||||
$debug = "{$e->getMessage()} ({$e->getCode()})";
|
||||
|
||||
nlog("Nordigen: Could not create an agreement with ${institution['name']}: {$debug}");
|
||||
|
||||
return $this->failed('eua-failure', $context, $company);
|
||||
// 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',
|
||||
$institution,
|
||||
$agreement,
|
||||
$request->token,
|
||||
$lang,
|
||||
);
|
||||
$requisition = $nordigen->createRequisition(config('ninja.app_url') . '/nordigen/confirm', $data['institution_id'], $request->token, $lang);
|
||||
} 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"')) {
|
||||
return $this->failed('institution-invalid', $context, $company);
|
||||
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",
|
||||
]);
|
||||
} elseif (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",
|
||||
]);
|
||||
}
|
||||
|
||||
// Reference invalid or already used, try a new token
|
||||
if (str_contains($responseBody, '"reference"')) {
|
||||
return $this->failed('token-invalid', $context, $company);
|
||||
}
|
||||
|
||||
nlog("Unknown Error from nordigen: {$e}");
|
||||
nlog($responseBody);
|
||||
|
||||
return $this->failed('unknown', $context, $company);
|
||||
}
|
||||
|
||||
// save cache
|
||||
$context['requisitionId'] = $requisition['id'];
|
||||
$context["requisitionId"] = $requisition["id"];
|
||||
Cache::put($request->token, $context, 3600);
|
||||
|
||||
return response()->redirectTo($requisition['link']);
|
||||
return response()->redirectTo($requisition["link"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the OAuth redirect and account setup after bank authentication
|
||||
* VIEW: Confirm Nordigen Bank Integration (redirect after nordigen flow)
|
||||
* @param ConfirmNordigenBankIntegrationRequest $request
|
||||
*/
|
||||
public function confirm(ConfirmNordigenBankIntegrationRequest $request): View|RedirectResponse
|
||||
public function confirm(ConfirmNordigenBankIntegrationRequest $request)
|
||||
{
|
||||
$data = $request->all();
|
||||
$company = $request->getCompany();
|
||||
$account = $company->account;
|
||||
$lang = substr($company->locale(), 0, 2);
|
||||
|
||||
/** @var array $context */
|
||||
$context = $request->getTokenContent();
|
||||
if (!array_key_exists('lang', $data) && $context['lang'] != 'en') {
|
||||
return redirect()->route('nordigen.confirm', array_merge(['lang' => $context['lang']], $request->query()));
|
||||
return redirect()->route('nordigen.confirm', array_merge(["lang" => $context['lang']], $request->query()));
|
||||
}
|
||||
|
||||
if (!$context || $context['context'] != 'nordigen' || !array_key_exists('requisitionId', $context)) {
|
||||
return $this->failed('ref-invalid', $context);
|
||||
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",
|
||||
]);
|
||||
}
|
||||
|
||||
if (!config('ninja.nordigen.secret_id') || !config('ninja.nordigen.secret_key')) {
|
||||
return $this->failed('account-config-invalid', $context, $company);
|
||||
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() && $company->account->isEnterprisePaidClient()))) {
|
||||
return $this->failed('not-available', $context, $company);
|
||||
if (!(Ninja::isSelfHost() || (Ninja::isHosted() && $account->isEnterprisePaidClient()))) {
|
||||
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']);
|
||||
$requisition = $nordigen->getRequisition($context["requisitionId"]);
|
||||
|
||||
// check validity of requisition
|
||||
if (!$requisition) {
|
||||
return $this->failed('requisition-not-found', $context, $company);
|
||||
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 $this->failed('requisition-invalid-status&status=' . $requisition['status'], $context, $company);
|
||||
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 $this->failed('requisition-no-accounts', $context, $company);
|
||||
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) {
|
||||
foreach ($requisition["accounts"] as $nordigenAccountId) {
|
||||
|
||||
$nordigen_account = $nordigen->getAccount($nordigenAccountId);
|
||||
|
||||
if (isset($nordigen_account['error'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$bank_integration = $this->findIntegrationBy('account', $nordigen_account, $company);
|
||||
$existing_bank_integration = BankIntegration::withTrashed()->where('nordigen_account_id', $nordigen_account['id'])->where('company_id', $company->id)->where('is_deleted', 0)->first();
|
||||
|
||||
if (!$existing_bank_integration) {
|
||||
|
||||
$bank_integration->deleted_at = null;
|
||||
} catch (ModelNotFoundException $e) {
|
||||
$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;
|
||||
|
|
@ -188,82 +234,53 @@ class NordigenController extends BaseController
|
|||
$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->currency = $nordigen_account['account_currency'];
|
||||
} finally {
|
||||
$bank_integration->auto_sync = true;
|
||||
$bank_integration->disabled_upstream = false;
|
||||
$bank_integration->balance = $nordigen_account['current_balance'];
|
||||
$bank_integration->bank_account_status = $nordigen_account['account_status'];
|
||||
$bank_integration->from_date = now()->subDays($nordigen_account['provider_history']);
|
||||
$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 = $nordigen_account['current_balance'];
|
||||
$existing_bank_integration->bank_account_status = $nordigen_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);
|
||||
});
|
||||
$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']);
|
||||
Cache::delete($data["ref"]);
|
||||
|
||||
// Successfull Response => Redirect
|
||||
return response()->redirectTo($context['redirect'] . '?action=nordigen_connect&status=success&bank_integrations=' . implode(',', $bank_integration_ids));
|
||||
return response()->redirectTo($context["redirect"] . "?action=nordigen_connect&status=success&bank_integrations=" . implode(',', $bank_integration_ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles failure scenarios for Nordigen bank integrations
|
||||
* Process Nordigen Institutions GETTER.
|
||||
*
|
||||
* @param array{lang: string, redirect?: string}|null $context
|
||||
* @param array{account: array}|null $company
|
||||
*/
|
||||
private function failed(string $reason, array $context, $company = null): View
|
||||
{
|
||||
$companyData = $company ? [
|
||||
'company' => $company,
|
||||
'account' => $company->account,
|
||||
] : [];
|
||||
|
||||
$url = $context['redirect'] ?? config('ninja.app_url');
|
||||
|
||||
return view('bank.nordigen.handler', [
|
||||
...$companyData,
|
||||
'lang' => $context['lang'],
|
||||
'failed_reason' => explode('&', $reason)[0],
|
||||
'redirectUrl' => $url . '?action=nordigen_connect&status=failed&reason=' . $reason,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first available Bank Integration from its Nordigen account or institution.
|
||||
*
|
||||
* @param 'account'|'institution' $key
|
||||
* @param array{id: string} $accountOrInstitution
|
||||
*/
|
||||
private function findIntegrationBy(
|
||||
string $key,
|
||||
array $accountOrInstitution,
|
||||
Company $company,
|
||||
): BankIntegration {
|
||||
return BankIntegration::withTrashed()
|
||||
->where($key == 'id' ? 'id' : "nordigen_{$key}_id", $accountOrInstitution['id'])
|
||||
->where('company_id', $company->id)
|
||||
->where('is_deleted', 0)
|
||||
->firstOrFail();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of available banking institutions from Nordigen
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/api/v1/nordigen/institutions",
|
||||
|
|
@ -295,14 +312,14 @@ class NordigenController extends BaseController
|
|||
* ),
|
||||
* )
|
||||
*/
|
||||
public function institutions(Request $request): JsonResponse
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
|
|
@ -20,36 +21,27 @@ class ConfirmNordigenBankIntegrationRequest extends Request
|
|||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(): bool
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules(): array
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'ref' => 'required|string', // nordigen redirects only with the ref-property
|
||||
'lang' => 'string',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* user_id: int,
|
||||
* company_key: string,
|
||||
* context: string,
|
||||
* is_react: bool,
|
||||
* institution_id: string,
|
||||
* lang: string,
|
||||
* redirect: string,
|
||||
* requisitionId: string
|
||||
* }
|
||||
*/
|
||||
public function getTokenContent(): array
|
||||
public function getTokenContent()
|
||||
{
|
||||
$input = $this->all();
|
||||
|
||||
|
|
@ -58,12 +50,10 @@ class ConfirmNordigenBankIntegrationRequest extends Request
|
|||
return $data;
|
||||
}
|
||||
|
||||
public function getCompany(): Company
|
||||
public function getCompany()
|
||||
{
|
||||
$key = $this->getTokenContent()['company_key'];
|
||||
MultiDB::findAndSetDbByCompanyKey($this->getTokenContent()['company_key']);
|
||||
|
||||
MultiDB::findAndSetDbByCompanyKey($key);
|
||||
|
||||
return Company::where('company_key', $key)->firstOrFail();
|
||||
return Company::where('company_key', $this->getTokenContent()['company_key'])->firstOrFail();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
|
|
@ -20,22 +21,26 @@ class ConnectNordigenBankIntegrationRequest extends Request
|
|||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize(): bool
|
||||
public function authorize()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules(): array
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
];
|
||||
}
|
||||
|
||||
public function prepareForValidation(): void
|
||||
public function prepareForValidation()
|
||||
{
|
||||
$input = $this->all();
|
||||
|
||||
|
|
@ -45,24 +50,12 @@ class ConnectNordigenBankIntegrationRequest extends Request
|
|||
$input['institution_id'] = $context['institution_id'];
|
||||
}
|
||||
|
||||
$input['redirect'] = ($context['is_react'] ?? false)
|
||||
? config('ninja.react_url') . '/#/settings/bank_accounts'
|
||||
: config('ninja.app_url');
|
||||
$input["redirect"] = isset($context["is_react"]) && $context['is_react'] ? config('ninja.react_url') . "/#/settings/bank_accounts" : config('ninja.app_url');
|
||||
|
||||
$this->replace($input);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* user_id: int,
|
||||
* company_key: string,
|
||||
* context: string,
|
||||
* is_react: bool,
|
||||
* institution_id: string,
|
||||
* requisitionId?: string
|
||||
* }
|
||||
*/
|
||||
public function getTokenContent(): ?array
|
||||
}
|
||||
public function getTokenContent()
|
||||
{
|
||||
if ($this->state) {
|
||||
$this->token = $this->state;
|
||||
|
|
@ -73,12 +66,10 @@ class ConnectNordigenBankIntegrationRequest extends Request
|
|||
return $data;
|
||||
}
|
||||
|
||||
public function getCompany(): Company
|
||||
public function getCompany()
|
||||
{
|
||||
$key = $this->getTokenContent()['company_key'];
|
||||
MultiDB::findAndSetDbByCompanyKey($this->getTokenContent()['company_key']);
|
||||
|
||||
MultiDB::findAndSetDbByCompanyKey($key);
|
||||
|
||||
return Company::where('company_key', $key)->firstOrFail();
|
||||
return Company::where('company_key', $this->getTokenContent()['company_key'])->firstOrFail();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@
|
|||
@push('head')
|
||||
|
||||
<link href="https://unpkg.com/nordigen-bank-ui@1.5.2/package/src/selector.min.css" rel="stylesheet" />
|
||||
<style type="text/css">
|
||||
.institution-modal-close { left: calc(100% - 12px); }
|
||||
</style>
|
||||
|
||||
@endpush
|
||||
|
||||
|
|
@ -30,7 +27,7 @@
|
|||
// 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: true,
|
||||
countryFilter: false,
|
||||
// style configs
|
||||
styles: {
|
||||
// Primary
|
||||
|
|
@ -48,83 +45,25 @@
|
|||
buttonColor: '#3A53EE',
|
||||
buttonTextColor: '#fff'
|
||||
}
|
||||
}, createSelectionUI = (donor, institution, skippedSelect = false) => {
|
||||
const clone = donor.cloneNode(true),
|
||||
container = document.querySelector('.institution-container'),
|
||||
max_history = parseInt(institution.transaction_total_days),
|
||||
url = new URL(window.location.href);
|
||||
|
||||
container.innerHTML = '';
|
||||
_changeHeading('Select your transaction history');
|
||||
|
||||
clone.classList.replace('ob-list-institution', 'ob-history-option');
|
||||
clone.querySelector('.ob-span-text').innerText = `${max_history} days`;
|
||||
url.searchParams.set('institution_id', institutionId);
|
||||
|
||||
// When we come from the renew button we need to replace the country flag
|
||||
if (skippedSelect) {
|
||||
const logo = document.createElement('img');
|
||||
|
||||
logo.setAttribute('src', institution.logo)
|
||||
logo.setAttribute('class', 'ob-institution-logo');
|
||||
|
||||
clone.querySelector('span.fi').replaceWith(logo)
|
||||
}
|
||||
|
||||
for (let i = 30, next = 30; i <= max_history; i += next) {
|
||||
// If we're close to max, just use the real value
|
||||
if (max_history - i < 15) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const option = clone.cloneNode(true);
|
||||
|
||||
url.searchParams.set('tx_days', i == 360 ? 365 : i);
|
||||
|
||||
option.querySelector('.ob-span-text').innerText = `${i == 360 ? 365 : i} days`;
|
||||
option.querySelector('a').href = url.href;
|
||||
container.append(option);
|
||||
|
||||
// 1, 2, 3, 4, 6, 9, 12, 14, 18, 24 months--as of 24/12/24, no bank exceeds 730 days of history
|
||||
next = i >= 500 ? 180 : i >= 400 ? 120 : i >= 360 ? 60 : i >= 180 ? 90 : i >= 120 ? 60 : 30;
|
||||
}
|
||||
|
||||
url.searchParams.set('tx_days', max_history);
|
||||
clone.querySelector('a').href = url.href;
|
||||
container.append(clone);
|
||||
};
|
||||
|
||||
const failedReason = "{{ $failed_reason ?? '' }}".trim(),
|
||||
institutions = @json($institutions ?? []);
|
||||
const failedReason = "{{ $failed_reason ?? '' }}".trim();
|
||||
|
||||
let institutionId = "{{ $institutionId ?? '' }}";
|
||||
new institutionSelector(@json($institutions ?? []), 'institution-modal-content', config);
|
||||
|
||||
new institutionSelector(institutions, 'institution-modal-content', config);
|
||||
if (!failedReason) {
|
||||
|
||||
if (null !== institutionId && !failedReason) {
|
||||
createSelectionUI(
|
||||
document.querySelector('.ob-institution'),
|
||||
institutions.find(i => i.id == institutionId),
|
||||
true
|
||||
);
|
||||
} else if (!failedReason) {
|
||||
const observer = new MutationObserver((event) => {
|
||||
const institutionButtons = document.querySelectorAll('.ob-list-institution > a');
|
||||
const institutionList = Array.from(document.querySelectorAll('.ob-list-institution > a'));
|
||||
|
||||
Array.from(institutionButtons).forEach((button) => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
createSelectionUI(button.parentElement, institutions.find(
|
||||
i => i.id == button.getAttribute('data-institution')
|
||||
));
|
||||
});
|
||||
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;
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.querySelector('.institution-container'), {
|
||||
childList: true,
|
||||
});
|
||||
} else {
|
||||
document.getElementsByClassName("institution-search-container")[0].remove();
|
||||
document.getElementsByClassName("institution-container")[0].remove();
|
||||
|
|
@ -140,6 +79,7 @@
|
|||
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":
|
||||
|
|
@ -159,10 +99,6 @@
|
|||
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;
|
||||
case "eua-failure":
|
||||
heading.innerHTML = "{{ ctrans('texts.nordigen_handler_error_heading_eua_failure', [], $lang ?? 'en') }}";
|
||||
contents.innerHTML = "{{ ctrans('texts.nordigen_handler_error_contents_eua_failure', [], $lang ?? 'en') }} " + failedReason;
|
||||
break;
|
||||
// Confirm Screen Errors
|
||||
case "ref-invalid":
|
||||
heading.innerHTML = "{{ ctrans('texts.nordigen_handler_error_heading_ref_invalid', [], $lang ?? 'en') }}";
|
||||
|
|
@ -195,20 +131,6 @@
|
|||
wrapper.appendChild(returnButton);
|
||||
}
|
||||
|
||||
const backButton = document.querySelector('.institution-arrow-block');
|
||||
const backButtonObserver = new MutationObserver((records) => {
|
||||
const title = document.querySelector('#institution-modal-header h2').innerText;
|
||||
|
||||
backButton.style.visibility = title == 'Select your country' ? 'hidden' : 'visible';
|
||||
backButton.style.display = 'flex';
|
||||
});
|
||||
|
||||
backButton.style.display = 'flex';
|
||||
backButton.style.visibility = 'hidden';
|
||||
|
||||
backButtonObserver.observe(document.querySelector('header h2'), {
|
||||
childList: true,
|
||||
});
|
||||
</script>
|
||||
|
||||
@endpush
|
||||
@endpush
|
||||
Loading…
Reference in New Issue