From f6a7c0ddda326f6395e408fb8ac7cd6e7bc70f14 Mon Sep 17 00:00:00 2001 From: Dave Shoreman Date: Mon, 23 Dec 2024 18:52:37 +0000 Subject: [PATCH] Attempt to reuse existing agreements Implements changes from #10410 but using `Arr::first()` rather than a foreach loop to fix bugs returning invalid or expired agreements. If an agreement exists with at least the requested `$txDays` then that is used, otherwise a new one is created with the given parameters. If it fails, we error out because `createRequisition()` would fail regardless. Skips accepting EUAs: seems it's done automatically during requisition. --- app/Helpers/Bank/Nordigen/Nordigen.php | 150 ++++++++++-------- .../Controllers/Bank/NordigenController.php | 15 +- 2 files changed, 95 insertions(+), 70 deletions(-) diff --git a/app/Helpers/Bank/Nordigen/Nordigen.php b/app/Helpers/Bank/Nordigen/Nordigen.php index e7484aa8fa..54abadf7c7 100644 --- a/app/Helpers/Bank/Nordigen/Nordigen.php +++ b/app/Helpers/Bank/Nordigen/Nordigen.php @@ -23,6 +23,7 @@ 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; @@ -73,21 +74,90 @@ class Nordigen * accepted: string * } Agreement details */ - public function getAgreement(string $euaId): array { - $eua = $this->client->endUserAgreement->getEndUserAgreement($euaId); + public function getAgreement(string $euaId): array + { + return $this->client->endUserAgreement->getEndUserAgreement($euaId); + } - return $eua; + /** + * 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 $txDays): ?array + { + $requiredScopes = ['balances', 'details', 'transactions']; + + try { + return Arr::first( + $this->client->endUserAgreement->getEndUserAgreements()['results'], + function (array $eua) use ($institutionId, $requiredScopes, $txDays): bool { + $expiresAt = $eua['accepted'] ? (new \DateTimeImmutable($eua['accepted']))->add( + new \DateInterval("P{$eua['access_valid_for_days']}D") + ) : false; + + return $eua['institution_id'] === $institutionId + && $eua['accepted'] === null + && $eua['max_historical_days'] >= $txDays + && !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} $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 $transactionDays): array + { + $txDays = $transactionDays < 30 ? 30 : $transactionDays; + $max = $institution['transaction_total_days']; + + return $this->client->endUserAgreement->createEndUserAgreement( + maxHistoricalDays: $txDays > $max ? $max : $txDays, + institutionId: $institution['id'], + ); } /** * Create a new Bank Requisition * - * @param array{id: string} $institution + * @param array{id: string} $institution, + * @param array{id: string, transaction_total_days: int} $agreement */ public function createRequisition( string $redirect, array $institution, - int $transactionDays, + array $agreement, string $reference, string $userLanguage, ): array { @@ -95,71 +165,13 @@ class Nordigen throw new \Exception('invalid institutionId while in test-mode'); } - $txDays = $transactionDays < 30 ? 30 : $transactionDays; - $max = $institution['transaction_total_days']; - - $eua = $this->client->endUserAgreement->createEndUserAgreement( - maxHistoricalDays: $txDays > $max ? $max : $txDays, - institutionId: $institution['id'], + return $this->client->requisition->createRequisition( + $redirect, + $institution['id'], + $agreement['id'] ?? null, + $reference, + $userLanguage ); - - return $this->client->requisition->createRequisition($redirect, $institution['id'], $eua['id'], $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) diff --git a/app/Http/Controllers/Bank/NordigenController.php b/app/Http/Controllers/Bank/NordigenController.php index 64081d11d8..cf690fe0b4 100644 --- a/app/Http/Controllers/Bank/NordigenController.php +++ b/app/Http/Controllers/Bank/NordigenController.php @@ -90,12 +90,25 @@ class NordigenController extends BaseController $data['tx_days'] = $nordigen->getAgreement($euaId)['max_historical_days']; } + try { + $txDays = $data['tx_days'] ?? 0; + + $agreement = $nordigen->firstValidAgreement($institution['id'], $txDays) + ?? $nordigen->createAgreement($institution, $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); + } + // redirect to requisition flow try { $requisition = $nordigen->createRequisition( config('ninja.app_url') . '/nordigen/confirm', $institution, - (int) ($data['tx_days'] ?? 0), + $agreement, $request->token, $lang, );