fix: BankID fallback, bank import fixes, account deletion (#216)
* fix: BankID graceful fallback, bank import fixes, and account deletion - BankID: surface service_unavailable state in login/register with password fallback messaging; structured error codes in tic extension; poll failure counter in BankIdAuth avoids infinite retry when TIC API is down. - Transactions: allow deleting unbooked bank-synced and imported transactions (only posted entries remain protected); detect reconnect duplicates by also checking unbooked bank-synced rows in content-based dedup. - Enable Banking: key external_id by account iban/uid instead of connection id so reconnects don't create duplicates. - Bank import: detect SEB privatbanken CSV variant (Bokföringsdatum / Valutadatum headers) via regex. - Banking settings: replace full-screen sync loader with toast notifications. - delete_user_account: raise statement_timeout, pre-clear NO ACTION FK references, and disable audit/immutability triggers during CASCADE. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address Greptile review — ingest dedup scoping and migration EXCEPTION handler - ingest: split buildExistingTransactionMap into two maps. Booked rows (any source) remain consumed by any incoming raw transaction, but unbooked enable_banking slots are only consumed when the incoming raw transaction is also enable_banking. This preserves reconnect dedup while preventing false positives where a pending bank-synced row silently blocks a legitimately separate CSV row with the same date/amount. - delete_user_account: add EXCEPTION WHEN OTHERS handler that re-enables every legally required enforcement trigger before re-raising. Postgres transactional DDL already rolls back on abort, but the explicit guard makes the intent unambiguous and covers sub-transaction edge cases so enforcement triggers are never left disabled. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
aa405b9a74
commit
bca00cc5bd
@@ -43,6 +43,14 @@ const SEB_CSV = [
|
||||
'2024-01-13;2024-01-13;12347;LÖNEUTBETALNING;25000,00;12877,17',
|
||||
].join('\n')
|
||||
|
||||
// SEB privatbanken web export: "Bokföringsdatum" (with ö AND -datum suffix)
|
||||
const SEB_PRIVAT_CSV = [
|
||||
'Bokföringsdatum;Valutadatum;Verifikationsnummer;Text;Belopp;Saldo',
|
||||
'2025-12-30;2025-12-31;0;RÄNTA;7,84;1241,16',
|
||||
'2025-09-15;2025-09-14;5490990004;53290171515;-1000,00;1233,32',
|
||||
'2024-10-31;2024-10-31;5841990687;H31520956893;433,16;5147,56',
|
||||
].join('\n')
|
||||
|
||||
const SWEDBANK_CSV = [
|
||||
'Kontouppgifter',
|
||||
'Clearingnummer,Kontonummer,Datum,Text,Belopp,Saldo',
|
||||
@@ -711,6 +719,33 @@ describe('parseBankFile — SEB format', () => {
|
||||
expect(result.transactions).toHaveLength(1)
|
||||
expect(result.transactions[0].amount).toBe(-50)
|
||||
})
|
||||
|
||||
it('auto-detects SEB privatbanken variant with Bokföringsdatum + Valutadatum headers', () => {
|
||||
const format = detectFileFormat(SEB_PRIVAT_CSV, 'kontoutdrag.csv')
|
||||
expect(format).not.toBeNull()
|
||||
expect(format!.id).toBe('seb')
|
||||
})
|
||||
|
||||
it('parses SEB privatbanken CSV (Bokföringsdatum / Valutadatum / RÄNTA / negative amounts)', () => {
|
||||
const result = parseBankFile(SEB_PRIVAT_CSV, 'kontoutdrag.csv')
|
||||
|
||||
expect(result.format).toBe('seb')
|
||||
expect(result.transactions).toHaveLength(3)
|
||||
expect(result.issues).toHaveLength(0)
|
||||
|
||||
const ranta = result.transactions[0]
|
||||
expect(ranta.date).toBe('2025-12-30')
|
||||
expect(ranta.description).toBe('RÄNTA')
|
||||
expect(ranta.amount).toBe(7.84)
|
||||
expect(ranta.balance).toBe(1241.16)
|
||||
|
||||
const withdrawal = result.transactions[1]
|
||||
expect(withdrawal.date).toBe('2025-09-15')
|
||||
expect(withdrawal.amount).toBe(-1000)
|
||||
|
||||
const deposit = result.transactions[2]
|
||||
expect(deposit.amount).toBe(433.16)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseBankFile — Swedbank format', () => {
|
||||
|
||||
@@ -25,14 +25,13 @@ export const sebFormat: BankFileFormat = {
|
||||
detect(content: string, _filename: string): boolean {
|
||||
const prepared = prepareContent(content)
|
||||
const firstLine = prepared.split('\n')[0]?.toLowerCase() || ''
|
||||
// SEB headers contain "bokföringsdag"/"bokforingsdatum" AND "valutadag"/"verifikationsnummer"
|
||||
// The secondary check distinguishes SEB from Länsförsäkringar (which also has "bokföringsdag")
|
||||
return (
|
||||
firstLine.includes(';') &&
|
||||
(firstLine.includes('bokföringsdag') ||
|
||||
firstLine.includes('bokforingsdatum')) &&
|
||||
(firstLine.includes('valutadag') || firstLine.includes('verifikationsnummer'))
|
||||
)
|
||||
// SEB uses semicolon delimiter. Header always has a bokföringsdag/bokföringsdatum
|
||||
// column plus either valutadag/valutadatum or verifikationsnummer. The secondary
|
||||
// check distinguishes SEB from Länsförsäkringar (which also has bokföringsdag).
|
||||
const hasBookingDate = /bokf(ö|o)ringsda(g|tum)/.test(firstLine)
|
||||
const hasSebSecondary =
|
||||
/valuta(dag|datum)/.test(firstLine) || firstLine.includes('verifikationsnummer')
|
||||
return firstLine.includes(';') && hasBookingDate && hasSebSecondary
|
||||
},
|
||||
|
||||
parse(content: string): BankFileParseResult {
|
||||
@@ -48,9 +47,7 @@ export const sebFormat: BankFileFormat = {
|
||||
const headers = headerLine.split(';').map((h) => h.trim().toLowerCase().replace(/"/g, ''))
|
||||
|
||||
// Find column indices dynamically
|
||||
const dateIdx = headers.findIndex(
|
||||
(h) => h.includes('bokföringsdag') || h.includes('bokforingsdatum')
|
||||
)
|
||||
const dateIdx = headers.findIndex((h) => /bokf(ö|o)ringsda(g|tum)/.test(h))
|
||||
const descIdx = headers.findIndex(
|
||||
(h) => h.includes('text') || h.includes('mottagare') || h.includes('beskrivning')
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user