64119d30bc
Issue #1716: a user stuck in Handelsbankens fullmakt step got a raw provider token back (server_error, invalid_state) and support had nothing to look at afterwards: the failed pending row is deleted by design, the callback only logged to console (short retention), and event_log recorded successes only. Diagnosis of the reported case: the failures were on the bank's side (the corporate fullmakt requirement); both of the reporter's companies connected successfully on 2026-08-12 with no code change on our side in between, and the connections have been active and syncing since. Changes: - lib/errors/get-error-message.ts: getBankConnectionErrorMessage() maps PSD2 callback outcomes (access_denied, server_error, temporarily_unavailable, session expiry, plus the internal invalid_state, missing_parameters and invalid_code_format tokens) to Swedish user messages, appending the raw provider description so the underlying error is still surfaced. - callback route: every bank_error redirect and the stored error_message now carry the mapped Swedish text; bank_error_code, bank_name and psu_type still flow so the settings page keeps its targeted guidance (Handelsbanken fullmakt steps included). - New audit events bank_connection.consent_denied and bank_connection.finalize_failed are emitted on the two failure paths and persisted to event_log, so support can answer which attempt failed, with which provider error, on whose side, even after the row is gone. Claude-Session: https://claude.ai/code/session_01SyDuePXxUFowaPBKpAv8SF Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
67 lines
3.1 KiB
TypeScript
67 lines
3.1 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { getBankConnectionErrorMessage } from '../get-error-message'
|
|
|
|
// The PSD2 callback mapper (issue #1716): raw provider tokens used to reach
|
|
// the user verbatim ("server_error", "invalid_state"), which left them with
|
|
// nothing to act on and support with nothing to answer.
|
|
describe('getBankConnectionErrorMessage', () => {
|
|
it('maps a user cancel (access_denied) without echoing the provider text', () => {
|
|
const msg = getBankConnectionErrorMessage('access_denied', 'User cancelled')
|
|
expect(msg).toContain('Anslutningen avbröts hos banken')
|
|
expect(msg).not.toContain('User cancelled')
|
|
})
|
|
|
|
it('treats "Cancelled by user" descriptions as a cancel regardless of code', () => {
|
|
const msg = getBankConnectionErrorMessage('server_error', 'Cancelled by user')
|
|
expect(msg).toContain('Anslutningen avbröts hos banken')
|
|
})
|
|
|
|
it('maps a bare server_error to the bank-side failure explanation', () => {
|
|
const msg = getBankConnectionErrorMessage('server_error')
|
|
expect(msg).toContain('fel på bankens sida')
|
|
// The Handelsbanken corporate case: point at mandates without naming a bank
|
|
// (the settings page adds the bank-specific steps from bank_error_code).
|
|
expect(msg).toContain('fullmakt')
|
|
})
|
|
|
|
it('surfaces the provider description in parentheses on unknown codes', () => {
|
|
const msg = getBankConnectionErrorMessage('aspsp_error', 'PSU lacks corporate mandate')
|
|
expect(msg).toContain('Banken avvisade anslutningen')
|
|
expect(msg).toContain('(PSU lacks corporate mandate)')
|
|
})
|
|
|
|
it('does not duplicate the code when the description equals the code', () => {
|
|
const msg = getBankConnectionErrorMessage('server_error', 'server_error')
|
|
expect(msg).toContain('fel på bankens sida')
|
|
expect(msg).not.toContain('(server_error)')
|
|
})
|
|
|
|
it('maps session-expiry descriptions to the expired-session message', () => {
|
|
const msg = getBankConnectionErrorMessage('server_error', 'Session expired at ASPSP')
|
|
expect(msg).toContain('inloggningssession')
|
|
expect(msg).toContain('(Session expired at ASPSP)')
|
|
})
|
|
|
|
it('maps the internal invalid_state token to a retry explanation', () => {
|
|
const msg = getBankConnectionErrorMessage('invalid_state')
|
|
expect(msg).toContain('Starta bankkopplingen på nytt')
|
|
expect(msg).not.toContain('invalid_state')
|
|
})
|
|
|
|
it('maps missing_parameters and invalid_code_format to Swedish', () => {
|
|
expect(getBankConnectionErrorMessage('missing_parameters')).toContain('ofullständigt svar')
|
|
expect(getBankConnectionErrorMessage('invalid_code_format')).toContain('ogiltigt svar')
|
|
})
|
|
|
|
it('maps temporarily_unavailable to the try-later message', () => {
|
|
const msg = getBankConnectionErrorMessage('temporarily_unavailable')
|
|
expect(msg).toContain('tillfälligt otillgänglig')
|
|
})
|
|
|
|
it('falls back to a Swedish rejection message for unknown codes without description', () => {
|
|
const msg = getBankConnectionErrorMessage('weird_code')
|
|
expect(msg).toContain('Banken avvisade anslutningen')
|
|
expect(msg).not.toContain('weird_code')
|
|
})
|
|
})
|