diff --git a/app/(dashboard)/import/page.tsx b/app/(dashboard)/import/page.tsx index 74e74e15..f812bf4c 100644 --- a/app/(dashboard)/import/page.tsx +++ b/app/(dashboard)/import/page.tsx @@ -814,10 +814,12 @@ const OB_STEP_LABELS: Record = { function OpeningBalanceFlow() { const { toast } = useToast() const { dialogProps, confirm } = useDestructiveConfirm() + const router = useRouter() const [obStep, setObStep] = useState('upload') const [obIsLoading, setObIsLoading] = useState(false) const [obError, setObError] = useState(null) + const [obBankFormatHint, setObBankFormatHint] = useState(null) const [obFile, setObFile] = useState(null) const [parseResult, setParseResult] = useState(null) const [editedRows, setEditedRows] = useState<{ @@ -836,6 +838,7 @@ function OpeningBalanceFlow() { const handleFileSelect = useCallback(async (file: File) => { setObError(null) + setObBankFormatHint(null) setObIsLoading(true) setObFile(file) @@ -859,7 +862,13 @@ function OpeningBalanceFlow() { setParseResult(result) if (result.rows.length === 0) { - setObError('Inga konton med belopp hittades i filen. Kontrollera att filen innehåller kontonummer och belopp.') + if (result.detected_bank_format) { + // The file is a bank statement uploaded to the wrong importer (#918) + setObBankFormatHint(result.detected_bank_format) + setObError(`Filen ser ut som ett kontoutdrag från ${result.detected_bank_format}, inte ingående balanser. Kontoutdrag importeras under "Banktransaktioner".`) + } else { + setObError('Inga konton med belopp hittades i filen. Kontrollera att filen innehåller kontonummer och belopp.') + } return } @@ -981,6 +990,7 @@ function OpeningBalanceFlow() { setEditedRows([]) setExecuteResult(null) setObError(null) + setObBankFormatHint(null) } return ( @@ -1016,6 +1026,11 @@ function OpeningBalanceFlow() { onFileSelect={handleFileSelect} isLoading={obIsLoading} error={obError} + errorAction={ + obBankFormatHint + ? { label: 'Importera banktransaktioner', onClick: () => router.push('/import?mode=bank') } + : undefined + } /> )} diff --git a/components/import/OpeningBalanceUploadStep.tsx b/components/import/OpeningBalanceUploadStep.tsx index b061713c..9e887a9c 100644 --- a/components/import/OpeningBalanceUploadStep.tsx +++ b/components/import/OpeningBalanceUploadStep.tsx @@ -10,12 +10,15 @@ interface OpeningBalanceUploadStepProps { onFileSelect: (file: File) => void isLoading: boolean error: string | null + /** Optional action rendered under the error text (e.g. route to the bank importer) */ + errorAction?: { label: string; onClick: () => void } } export default function OpeningBalanceUploadStep({ onFileSelect, isLoading, error, + errorAction, }: OpeningBalanceUploadStepProps) { const [isDragging, setIsDragging] = useState(false) @@ -108,7 +111,14 @@ export default function OpeningBalanceUploadStep({ {error && (
-

{error}

+
+

{error}

+ {errorAction && ( + + )} +
)} diff --git a/lib/import/opening-balance/__tests__/parser.test.ts b/lib/import/opening-balance/__tests__/parser.test.ts index 5ff14b67..cb92a265 100644 --- a/lib/import/opening-balance/__tests__/parser.test.ts +++ b/lib/import/opening-balance/__tests__/parser.test.ts @@ -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() + }) +}) diff --git a/lib/import/opening-balance/parser.ts b/lib/import/opening-balance/parser.ts index fce23685..4b1985a9 100644 --- a/lib/import/opening-balance/parser.ts +++ b/lib/import/opening-balance/parser.ts @@ -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, } } diff --git a/lib/import/opening-balance/types.ts b/lib/import/opening-balance/types.ts index 2c85ea63..28a9d4eb 100644 --- a/lib/import/opening-balance/types.ts +++ b/lib/import/opening-balance/types.ts @@ -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 */