Revert latest nordigen commit

This commit is contained in:
David Bomba 2025-01-11 18:25:54 +11:00
parent 95b8454c15
commit 5c60a3efed
6 changed files with 268 additions and 396 deletions

View File

@ -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
}
}
}

View File

@ -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"] : '',

View File

@ -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());
}
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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