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:
co-authored by
Claude Fable 5
parent
b21aa84268
commit
982fe77f72
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user