whisper-money/app/Jobs
Víctor Falcón c36df98d32
fix(banking): handle EnableBanking expired sessions as reconnect, not error (#557)
## Problem

Sentry issue
[PHP-LARAVEL-3J](https://whisper-money.sentry.io/issues/PHP-LARAVEL-3J)
— `RequestException: HTTP 401 {"error":"EXPIRED_SESSION"}` in
`EnableBankingProvider::getTransactions` — was escalating (26 events, 2
users).

When an EnableBanking **session** expires *before* its 90-day consent
window (`valid_until`), the sync hit a `401 EXPIRED_SESSION` that was
not classified as transient or ASPSP, so it was re-thrown as a raw
`RequestException`. Consequences:

1. **Sentry noise** — reported as an error on every scheduled retry
until the failure cap.
2. **Silent breakage for the user** — EnableBanking's syncer returns
`notifiesOnAuthFailure() === false`, so the 401 went through the generic
permanent-error path: connection set to `Error` (not `Expired`) with
**no reconnect email**.

Confirmed in production: **10 of 17** EnableBanking connections in
`error` status are actually expired sessions (`valid_until` in the
future + "Authentication failed" message), all with no notification
sent.

## Fix

Treat a `401 EXPIRED_SESSION` as the expected lifecycle event it is:

- New domain exception `ExpiredBankingSessionException` (implements
`ShouldntReport`, like `TransientBankingProviderException`).
- `EnableBankingProvider::getTransactions()` / `getBalances()` wrap the
`401 EXPIRED_SESSION` in it instead of re-throwing the raw
`RequestException`.
- `SyncBankingConnectionJob` catches it and routes through the existing
expiry handling (extracted to `markExpired()`): marks the connection
`Expired`, sends `BankingConnectionExpiredEmail`, logs a skipped
attempt, returns **without throwing**.
- The provider's HTTP error logger downgrades the expected expiry from
`error` → `warning`.

`Expired` connections are not re-dispatched by
`SyncAllBankingConnectionsJob`, so retries stop and the user is prompted
to reconnect.

## Tests

- `EnableBankingProviderTest`: `getTransactions` / `getBalances` wrap a
`401 EXPIRED_SESSION` as a non-reportable
`ExpiredBankingSessionException`.
- `SyncRetryAndLoggingTest`: an expired session marks the connection
`Expired`, queues the reconnect email, and does not throw.

All three failed before the fix. Full `tests/Feature/OpenBanking` suite
passes (264 tests).

## Follow-up (not in this PR)

The 10 connections already stuck in `Error` from this bug are past the
retry cap and won't self-heal. A one-off command to reclassify them to
`Expired` and send the reconnect email would recover those users — worth
a separate, reviewed change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-06-18 13:48:13 +00:00
..
Drip Support soft-deleted users with reusable emails (#316) 2026-04-22 11:41:41 +01:00
ApplySingleAutomationRuleJob.php fix: batch automation rule application (#435) 2026-05-26 10:45:37 +02:00
AssignHistoricalTransactionsToBudget.php feat: Load transactions history on budget created (#72) 2026-01-22 11:10:15 +01:00
CategorizeOnboardingTransactionsJob.php feat(ai): defer per-transaction categorization until onboarding completes (#536) 2026-06-15 17:33:26 +02:00
GenerateHistoricalLoanBalancesJob.php feat(loans): backfill historical balances on loan creation (#322) 2026-04-24 13:09:34 +01:00
GenerateHistoricalRealEstateBalancesJob.php feat(real-estate): auto-calculate revaluation % and generate historical balances (#253) 2026-04-15 12:18:33 +00:00
GenerateRuleSuggestionsJob.php feat(ai): suggest automation rules during onboarding (#523) 2026-06-13 22:51:15 +02:00
ReEvaluateTransactionRulesJob.php feat(transactions): re-add select all matching filters to bulk actions bar (#169) 2026-03-01 10:27:43 +00:00
SendDailyBankTransactionsSyncedEmailJob.php feat(settings): let users disable bank transactions email (#472) 2026-06-02 12:24:39 +02:00
SendUpdateEmailJob.php Support soft-deleted users with reusable emails (#316) 2026-04-22 11:41:41 +01:00
SyncAllBankingConnectionsJob.php refactor: centralize duplicated provider & locale keys into enums (#543) 2026-06-16 13:43:14 +00:00
SyncBankingConnectionJob.php fix(banking): handle EnableBanking expired sessions as reconnect, not error (#557) 2026-06-18 13:48:13 +00:00
SyncBinanceHistoricalBalancesJob.php feat: Add Binance integration (#131) 2026-02-18 15:23:46 +01:00