fix(import): actionable hint when a bank CSV lands in the opening-balance importer (#953)

* fix(import): hint when a bank statement is uploaded as opening balances

Uploading a bank statement CSV to the opening-balance importer produced
the generic 'Inga konton med belopp hittades' error with no clue that
the file belongs in the bank-transactions importer (#918, users got
stuck together with #915).

When the opening-balance parse yields zero account rows, the parser now
runs the registered bank-file format detectors over the CSV content
(the generic CSV fallback never auto-detects, so any match is a real
bank format) and reports the matched format name as
detected_bank_format on the parse result. The upload step then shows an
actionable Swedish error naming the bank plus a button that routes to
the bank-transactions importer (/import?mode=bank).

Closes #918

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(import): use the standard bank-import CTA wording (CodeRabbit)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-09 21:10:09 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent b21aa84268
commit 982fe77f72
5 changed files with 122 additions and 2 deletions
@@ -255,3 +255,68 @@ describe('parseOpeningBalanceFile', () => {
expect(result.total_credit).toBe(40000)
})
})
describe('bank statement detection (issue #918)', () => {
const toBuffer = (csv: string): ArrayBuffer =>
new TextEncoder().encode(csv).buffer as ArrayBuffer
it('flags a Swedbank bank statement CSV that yields no account rows', async () => {
const { parseOpeningBalanceFile } = await import('../parser')
const csv = [
'Radnr,Clnr,Kontonr,Produkt,Valuta,Bokfdag,Transdag,Valutadag,Referens,Text,Belopp,Saldo',
'1,8385-9,9350000000,Företagskonto,SEK,2026-05-02,2026-05-02,2026-05-02,Hyra maj,Bg-bet. via internet,-12000.00,54321.00',
'2,8385-9,9350000000,Företagskonto,SEK,2026-05-03,2026-05-03,2026-05-03,Kundbetalning,Insättning,25000.00,79321.00',
].join('\n')
const result = parseOpeningBalanceFile(toBuffer(csv), 'kontoutdrag.csv')
expect(result.rows.length).toBe(0)
expect(result.detected_bank_format).toBe('Swedbank')
})
it('flags an SEB bank statement CSV that yields no account rows', async () => {
const { parseOpeningBalanceFile } = await import('../parser')
const csv = [
'Bokföringsdatum;Valutadatum;Verifikationsnummer;Text;Belopp;Saldo',
'2026-05-02;2026-05-02;5501234567;Hyra maj;-12000,00;54321,00',
'2026-05-03;2026-05-03;5501234568;Kundbetalning;25000,00;79321,00',
].join('\n')
const result = parseOpeningBalanceFile(toBuffer(csv), 'export.csv')
expect(result.rows.length).toBe(0)
expect(result.detected_bank_format).toBe('SEB')
})
it('does not flag a CSV that yields no rows but matches no bank format', async () => {
const { parseOpeningBalanceFile } = await import('../parser')
const csv = [
'Namn,Stad,Antal',
'Alfa,Göteborg,3',
'Beta,Malmö,7',
].join('\n')
const result = parseOpeningBalanceFile(toBuffer(csv), 'annat.csv')
expect(result.rows.length).toBe(0)
expect(result.detected_bank_format).toBeNull()
})
it('does not flag a valid opening balance file', async () => {
const { parseOpeningBalanceFile } = await import('../parser')
const csv = [
'Kontonr,Kontonamn,Debet,Kredit',
'1930,Företagskonto,50000,0',
'2099,Årets resultat,0,50000',
].join('\n')
const result = parseOpeningBalanceFile(toBuffer(csv), 'ib.csv')
expect(result.rows.length).toBe(2)
expect(result.detected_bank_format).toBeNull()
})
})
+24
View File
@@ -1,6 +1,8 @@
import * as XLSX from 'xlsx'
import { detectColumns } from './column-detector'
import { getBASReference } from '@/lib/bookkeeping/bas-reference'
import { detectFileFormat } from '../bank-file/parser'
import { decodeFileContent } from '../shared/encoding'
import { readWorkbookFromBuffer } from '../shared/workbook-reader'
import type {
DetectedColumns,
@@ -31,6 +33,25 @@ export function parseAmount(value: unknown): number {
return Math.round(num * 100) / 100
}
/**
* When an opening-balance parse yields no rows, check whether the uploaded
* file is actually a bank statement (issue #918: users upload bank CSV
* exports here and only get a generic "no accounts found" error). Only CSV
* files can match: the bank-file detectors operate on decoded text, and the
* generic CSV fallback never auto-detects, so any match is a real bank format.
*/
function detectBankStatementFormat(buffer: ArrayBuffer, filename: string): string | null {
const ext = filename.toLowerCase().split('.').pop() ?? ''
if (ext !== 'csv') return null
try {
const content = decodeFileContent(buffer)
return detectFileFormat(content, filename)?.name ?? null
} catch {
// Detection is a best-effort hint: never let it break the parse result
return null
}
}
/**
* Parse an opening balance file (Excel or CSV) and return structured rows
* with validation and BAS account matching.
@@ -87,6 +108,7 @@ export function parseOpeningBalanceFile(
total_credit: 0,
is_balanced: true,
warnings: ['Filen innehåller för få rader.'],
detected_bank_format: detectBankStatementFormat(buffer, filename),
}
}
@@ -242,5 +264,7 @@ export function parseOpeningBalanceFile(
total_credit: totalCredit,
is_balanced: isBalanced,
warnings,
detected_bank_format:
mergedRows.length === 0 ? detectBankStatementFormat(buffer, filename) : null,
}
}
+6
View File
@@ -44,6 +44,12 @@ export interface OpeningBalanceParseResult {
total_credit: number
is_balanced: boolean
warnings: string[]
/**
* Bank-file format name (e.g. "Swedbank") when the file produced no account
* rows but matches a known bank statement format: the user most likely
* uploaded a bank statement to the wrong importer. Null otherwise.
*/
detected_bank_format: string | null
}
/** Input for executing the opening balance import */