From 5c60a3efed8b0979dec0cd913e30ba41e75420a3 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 11 Jan 2025 18:25:54 +1100 Subject: [PATCH] Revert latest nordigen commit --- app/Helpers/Bank/Nordigen/Nordigen.php | 186 ++++------- .../Transformer/AccountTransformer.php | 2 +- .../Controllers/Bank/NordigenController.php | 301 +++++++++--------- .../ConfirmNordigenBankIntegrationRequest.php | 32 +- .../ConnectNordigenBankIntegrationRequest.php | 37 +-- .../views/bank/nordigen/handler.blade.php | 106 +----- 6 files changed, 268 insertions(+), 396 deletions(-) diff --git a/app/Helpers/Bank/Nordigen/Nordigen.php b/app/Helpers/Bank/Nordigen/Nordigen.php index 6481f87d30..4174accacf 100644 --- a/app/Helpers/Bank/Nordigen/Nordigen.php +++ b/app/Helpers/Bank/Nordigen/Nordigen.php @@ -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 } -} +} \ No newline at end of file diff --git a/app/Helpers/Bank/Nordigen/Transformer/AccountTransformer.php b/app/Helpers/Bank/Nordigen/Transformer/AccountTransformer.php index 82b0261176..3306c5d27b 100644 --- a/app/Helpers/Bank/Nordigen/Transformer/AccountTransformer.php +++ b/app/Helpers/Bank/Nordigen/Transformer/AccountTransformer.php @@ -1,4 +1,5 @@ $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"] : '', diff --git a/app/Http/Controllers/Bank/NordigenController.php b/app/Http/Controllers/Bank/NordigenController.php index aa919dac33..56da6023a6 100644 --- a/app/Http/Controllers/Bank/NordigenController.php +++ b/app/Http/Controllers/Bank/NordigenController.php @@ -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()); } -} + +} \ No newline at end of file diff --git a/app/Http/Requests/Nordigen/ConfirmNordigenBankIntegrationRequest.php b/app/Http/Requests/Nordigen/ConfirmNordigenBankIntegrationRequest.php index 494be24c4d..7842289eb8 100644 --- a/app/Http/Requests/Nordigen/ConfirmNordigenBankIntegrationRequest.php +++ b/app/Http/Requests/Nordigen/ConfirmNordigenBankIntegrationRequest.php @@ -1,4 +1,5 @@ '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(); } } diff --git a/app/Http/Requests/Nordigen/ConnectNordigenBankIntegrationRequest.php b/app/Http/Requests/Nordigen/ConnectNordigenBankIntegrationRequest.php index 69c90008d4..d4b1e4f4f9 100644 --- a/app/Http/Requests/Nordigen/ConnectNordigenBankIntegrationRequest.php +++ b/app/Http/Requests/Nordigen/ConnectNordigenBankIntegrationRequest.php @@ -1,4 +1,5 @@ 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(); } } diff --git a/resources/views/bank/nordigen/handler.blade.php b/resources/views/bank/nordigen/handler.blade.php index 3ebdbfe18e..3044fb62dd 100644 --- a/resources/views/bank/nordigen/handler.blade.php +++ b/resources/views/bank/nordigen/handler.blade.php @@ -4,9 +4,6 @@ @push('head') - @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, - }); -@endpush +@endpush \ No newline at end of file