whisper-money/app/Exceptions/Banking
Víctor Falcón 46568700b2
fix(banking): skip inaccessible EnableBanking accounts instead of failing the connection (#559)
## Problem

Sentry issue
[PHP-LARAVEL-3J](https://whisper-money.sentry.io/issues/PHP-LARAVEL-3J)
started escalating again — but with a **different** error than the 401
expired-session fixed in #557. Sentry groups both under the same
`getTransactions` culprit:

```
RequestException: HTTP request returned status code 400:
{"code":400,"message":"Account not found","detail":{"message":"Account not found","error_name":"AccountNotAccessibleException"}}
```

When EnableBanking returns `400 AccountNotAccessibleException` for
**one** account (closed, or no longer covered by the consent), the raw
`RequestException` was re-thrown. This:

1. **Aborted the whole connection's account loop** in
`EnableBankingSyncer` — accounts after the dead one never synced.
2. Marked the **entire connection** `Error` (`friendlyErrorMessage`
default), even though its other accounts were fine.
3. **Reported to Sentry** on every scheduled retry (escalating noise).

Confirmed in production (`agent:db --prod`): connection `019ea143-…` is
`active` with `valid_until` in the future (2026-09-05) and **6
accounts**; one (`08bbcdf9-…`) returns the 400, and the connection is
now stuck in `error` with the generic "try again later" message. 2 users
affected.

## Fix

A single account the bank no longer exposes must not break the rest of
the connection.

- New domain exception `InaccessibleBankAccountException` (implements
`ShouldntReport`, like `TransientBankingProviderException` /
`ExpiredBankingSessionException`).
- `EnableBankingProvider::getTransactions()` / `getBalances()` wrap a
`400 AccountNotAccessibleException` in it (keyed on `detail.error_name`)
instead of re-throwing the raw `RequestException`.
- `EnableBankingSyncer` catches it **per account**, logs a warning, and
`continue`s — the remaining accounts sync and the connection stays
`Active`.

Connections currently stuck in `error` from this self-heal on the next
scheduled sync (they're `Error` + under the retry cap + `valid_until`
future, so the scheduler re-dispatches them; the dead account is now
skipped).

Composes with #558: a user can permanently *stop syncing* such an
account from the manage-accounts screen. This PR only stops one dead
account from breaking automated syncs in the meantime.

## Tests

- `EnableBankingProviderTest`: `getTransactions` / `getBalances` wrap a
`400 AccountNotAccessibleException` as a non-reportable
`InaccessibleBankAccountException`.
- `SyncRetryAndLoggingTest`: with one inaccessible and one good account,
the good one still syncs, the connection stays `Active`, and nothing is
thrown.

All three failed before the fix. Full `tests/Feature/OpenBanking` suite
passes (276), pint clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-19 08:59:05 +02:00
..
ExpiredBankingSessionException.php fix(banking): handle EnableBanking expired sessions as reconnect, not error (#557) 2026-06-18 13:48:13 +00:00
InaccessibleBankAccountException.php fix(banking): skip inaccessible EnableBanking accounts instead of failing the connection (#559) 2026-06-19 08:59:05 +02:00
TransientBankingProviderException.php Handle transient EnableBanking sync failures (#358) 2026-05-06 09:24:05 +02:00