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.
This commit is contained in:
Dave Shoreman 2024-12-23 18:52:37 +00:00
parent 3204163e70
commit f6a7c0ddda
No known key found for this signature in database
GPG Key ID: C920D1D63709F443
2 changed files with 95 additions and 70 deletions

View File

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

View File

@ -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,
);