diff --git a/app/Services/Banking/EnableBankingProvider.php b/app/Services/Banking/EnableBankingProvider.php index 6ae6336b..46bd9115 100644 --- a/app/Services/Banking/EnableBankingProvider.php +++ b/app/Services/Banking/EnableBankingProvider.php @@ -127,6 +127,15 @@ class EnableBankingProvider implements BankingProviderInterface ); } + if ($this->isTransientServerError($e)) { + throw new TransientBankingProviderException( + 'EnableBanking returned a server error while fetching account transactions.', + provider: 'enablebanking', + statusCode: $e->response->status(), + previous: $e, + ); + } + if (! $this->isAspspError($e)) { throw $e; } @@ -178,6 +187,15 @@ class EnableBankingProvider implements BankingProviderInterface ); } + if ($this->isTransientServerError($e)) { + throw new TransientBankingProviderException( + 'EnableBanking returned a server error while fetching account balances.', + provider: 'enablebanking', + statusCode: $e->response->status(), + previous: $e, + ); + } + if (! $this->isAspspError($e)) { throw $e; } @@ -230,6 +248,14 @@ class EnableBankingProvider implements BankingProviderInterface && ($body['error'] ?? null) === 'ASPSP_ERROR'; } + private function isTransientServerError(RequestException $e): bool + { + // Any upstream 5xx (EnableBanking itself or the ASPSP behind it) is a + // transient server-side failure — same class as a ConnectionException, + // so retry/self-heal rather than report it as an app error. + return $e->response->status() >= 500; + } + private function isExpiredSession(RequestException $e): bool { $body = $this->errorBody($e); diff --git a/tests/Feature/OpenBanking/EnableBankingProviderTest.php b/tests/Feature/OpenBanking/EnableBankingProviderTest.php index 78b66a92..ac4d68c1 100644 --- a/tests/Feature/OpenBanking/EnableBankingProviderTest.php +++ b/tests/Feature/OpenBanking/EnableBankingProviderTest.php @@ -59,6 +59,54 @@ test('getTransactions wraps connection failures as non-reportable transient erro test()->fail('Expected transient banking provider exception.'); }); +test('getTransactions wraps an upstream 500 as a non-reportable transient error', function () { + Http::fake([ + 'api.enablebanking.com/accounts/ext-123/transactions*' => Http::response([ + 'code' => 500, + 'message' => 'Internal server error', + ], 500), + ]); + + $provider = enableBankingProviderForTest(); + + try { + $provider->getTransactions('ext-123', now()->toDateString(), now()->toDateString()); + } catch (TransientBankingProviderException $e) { + expect($e)->toBeInstanceOf(ShouldntReport::class) + ->and($e->provider)->toBe('enablebanking') + ->and($e->statusCode)->toBe(500) + ->and($e->getPrevious())->toBeInstanceOf(RequestException::class); + + return; + } + + test()->fail('Expected transient banking provider exception.'); +}); + +test('getBalances wraps an upstream 500 as a non-reportable transient error', function () { + Http::fake([ + 'api.enablebanking.com/accounts/ext-123/balances*' => Http::response([ + 'code' => 500, + 'message' => 'Internal server error', + ], 500), + ]); + + $provider = enableBankingProviderForTest(); + + try { + $provider->getBalances('ext-123'); + } catch (TransientBankingProviderException $e) { + expect($e)->toBeInstanceOf(ShouldntReport::class) + ->and($e->provider)->toBe('enablebanking') + ->and($e->statusCode)->toBe(500) + ->and($e->getPrevious())->toBeInstanceOf(RequestException::class); + + return; + } + + test()->fail('Expected transient banking provider exception.'); +}); + test('getTransactions wraps an expired session 401 as a non-reportable expired session error', function () { Http::fake([ 'api.enablebanking.com/accounts/ext-123/transactions*' => Http::response([