fix(enable-banking): recover error-state connections, respect PSD2 balance quota, clean error surface (#968)
* fix(enable-banking): recover error-state connections, respect PSD2 balance quota, clean error surface Three defects from the 2026-07-09 production log triage, all in how the enable-banking extension handles upstream (Enable Banking / ASPSP) failures: 1. Retry dead-end: a non-session sync failure parked the connection in status='error', but POST /sync rejected anything not 'active' with 400, so the UI's "Försök igen" button could never succeed and the connection stayed stranded until a full re-auth. /sync now accepts 'error' (while still rejecting 'expired': a dead consent needs re-authorization), and a successful sync restores status='active' and clears error_message. 2. Balance quota burn: every sync (manual or cron) called the BALANCES endpoint although PSD2 unattended consents allow only 4 calls/day (observed 429 "Consent daily limit 4 is exceeded"), and the retry wrapper retried those 429s twice against a daily quota. The sync now skips the balance call while the stored balance_updated_at is fresher than 12 hours, and authenticatedFetchWithRetry fails fast on a 429 whose body signals a daily limit. 3. Raw JSON in UI: sync failures persisted the raw English Enable Banking error body into bank_connections.error_message, which the settings panel renders verbatim. Failures are now mapped to short Swedish user messages (shared constants in api-client.ts); the raw body stays in server logs only. Also ratchets the eslint baseline down by 1: the no-explicit-any disable in the cron route was on the wrong line and never suppressed anything. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(enable-banking): treat future balance timestamps as stale (CodeRabbit) A future balance_updated_at yielded a negative age that always passed the freshness check, suppressing balance refreshes indefinitely; only 0 <= age < BALANCE_MAX_AGE_MS now counts as fresh. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6a9adcf00a
commit
b06d73c23e
@@ -27,6 +27,15 @@ export interface SyncOptions {
|
||||
strategy?: TransactionsFetchStrategy
|
||||
}
|
||||
|
||||
/**
|
||||
* How long a stored account balance stays fresh before a sync refreshes it.
|
||||
* PSD2 unattended consents allow only 4 balance calls per account per day
|
||||
* (observed: 429 "Consent daily limit 4 is exceeded"), while transaction
|
||||
* fetches are budgeted separately. 12h keeps at most 2 balance calls per day
|
||||
* regardless of how many manual "Synka nu" clicks or cron runs happen.
|
||||
*/
|
||||
const BALANCE_MAX_AGE_MS = 12 * 60 * 60 * 1000
|
||||
|
||||
export interface SyncResult {
|
||||
imported: number
|
||||
duplicates: number
|
||||
@@ -209,13 +218,32 @@ export async function syncAccountTransactions(
|
||||
}
|
||||
}
|
||||
|
||||
// Update account balance
|
||||
try {
|
||||
const balance = await getAccountBalance(account.uid)
|
||||
account.balance = balance.amount
|
||||
account.balance_updated_at = new Date().toISOString()
|
||||
} catch {
|
||||
// Keep previous balance, don't update timestamp
|
||||
// Update account balance, but only when the stored one has gone stale:
|
||||
// every skipped call preserves the account's scarce daily BALANCES quota
|
||||
// (see BALANCE_MAX_AGE_MS). balance_updated_at is written ONLY on a
|
||||
// successful refresh below, so a stale/missing/invalid timestamp always
|
||||
// falls through to a refresh attempt (NaN and Infinity both fail the
|
||||
// freshness comparison). A FUTURE timestamp (clock skew, bad data) yields a
|
||||
// negative age; treat it as stale too, or refreshes would be suppressed
|
||||
// until the wall clock catches up.
|
||||
const balanceAgeMs = account.balance_updated_at
|
||||
? Date.now() - new Date(account.balance_updated_at).getTime()
|
||||
: Number.POSITIVE_INFINITY
|
||||
const balanceIsFresh = balanceAgeMs >= 0 && balanceAgeMs < BALANCE_MAX_AGE_MS
|
||||
if (balanceIsFresh) {
|
||||
console.log('[enable-banking] Skipping balance refresh (stored balance is fresh)', {
|
||||
connectionId,
|
||||
accountUid: account.uid,
|
||||
balanceUpdatedAt: account.balance_updated_at,
|
||||
})
|
||||
} else {
|
||||
try {
|
||||
const balance = await getAccountBalance(account.uid)
|
||||
account.balance = balance.amount
|
||||
account.balance_updated_at = new Date().toISOString()
|
||||
} catch {
|
||||
// Keep previous balance, don't update timestamp
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user