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
@@ -5,7 +5,13 @@ import {
|
||||
runReconciliation,
|
||||
DEFAULT_UNATTENDED_CONFIDENCE_THRESHOLD,
|
||||
} from '@/lib/reconciliation/bank-reconciliation'
|
||||
import { isConsentExpiringSoon, getDaysUntilExpiry, SessionExpiredError } from '@/extensions/general/enable-banking/lib/api-client'
|
||||
import {
|
||||
isConsentExpiringSoon,
|
||||
getDaysUntilExpiry,
|
||||
SessionExpiredError,
|
||||
REAUTH_REQUIRED_MESSAGE,
|
||||
SYNC_FAILED_MESSAGE,
|
||||
} from '@/extensions/general/enable-banking/lib/api-client'
|
||||
import { getEmailService } from '@/lib/email/service'
|
||||
import {
|
||||
generateConsentExpiryEmailHtml,
|
||||
@@ -278,7 +284,6 @@ export const GET = withCronContext('cron.bank_sync', async (_request, ctx) => {
|
||||
daysUntilExpiry: daysLeft,
|
||||
})
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
ctx.log.error('sync failed for connection', error as Error, {
|
||||
connectionId: connection.id,
|
||||
userId: connection.user_id,
|
||||
@@ -291,11 +296,13 @@ export const GET = withCronContext('cron.bank_sync', async (_request, ctx) => {
|
||||
// condition, not a transient failure: flip it to 'expired' (same state
|
||||
// the consent-elapsed branch uses) so the UI offers a reconnect instead
|
||||
// of a retry. Other errors stay 'error'.
|
||||
//
|
||||
// error_message is rendered verbatim on the settings panel, so it gets
|
||||
// the short Swedish user message in both cases: the raw Enable Banking
|
||||
// error body (an English JSON envelope) stays in the server log above.
|
||||
const isSessionDead = error instanceof SessionExpiredError
|
||||
const failureStatus = isSessionDead ? 'expired' : 'error'
|
||||
const failureMessage = isSessionDead
|
||||
? 'Bankanslutningen har löpt ut. Förnya anslutningen för att fortsätta synka.'
|
||||
: message
|
||||
const failureMessage = isSessionDead ? REAUTH_REQUIRED_MESSAGE : SYNC_FAILED_MESSAGE
|
||||
|
||||
await supabase
|
||||
.from('bank_connections')
|
||||
@@ -341,8 +348,8 @@ export const GET = withCronContext('cron.bank_sync', async (_request, ctx) => {
|
||||
* Send consent expiry notification email.
|
||||
* Guards with last_expiry_notification_at to avoid spamming (2-day cooldown).
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async function sendConsentExpiryNotification(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
supabase: SupabaseClient<any>,
|
||||
connection: Record<string, unknown>,
|
||||
daysLeft: number,
|
||||
|
||||
Reference in New Issue
Block a user