diff --git a/app/api/extensions/enable-banking/sync/cron/route.ts b/app/api/extensions/enable-banking/sync/cron/route.ts index f881db48..9438b484 100644 --- a/app/api/extensions/enable-banking/sync/cron/route.ts +++ b/app/api/extensions/enable-banking/sync/cron/route.ts @@ -132,7 +132,13 @@ export async function GET(request: Request) { } const toDate = new Date().toISOString().split('T')[0] - const fromDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) + // First sync: 90-day lookback (PSD2 max). Subsequent: 7-day window. + const isFirstSync = !connection.last_synced_at + const lookbackDays = isFirstSync ? 90 : 7 + if (isFirstSync) { + console.log(`[bank-sync-cron] First sync for connection ${connection.id}, using ${lookbackDays}-day lookback`) + } + const fromDate = new Date(Date.now() - lookbackDays * 24 * 60 * 60 * 1000) .toISOString() .split('T')[0] diff --git a/components/import/BankFileColumnMappingStep.tsx b/components/import/BankFileColumnMappingStep.tsx index 6f36f2e3..66c80d84 100644 --- a/components/import/BankFileColumnMappingStep.tsx +++ b/components/import/BankFileColumnMappingStep.tsx @@ -44,6 +44,7 @@ export default function BankFileColumnMappingStep({ const [balanceCol, setBalanceCol] = useState(-1) const [delimiter, setDelimiter] = useState(',') const [decimalSep, setDecimalSep] = useState<',' | '.'>(',') + const [dateFormat, setDateFormat] = useState('YYYY-MM-DD') const isValid = dateCol >= 0 && descCol >= 0 && amountCol >= 0 @@ -58,7 +59,7 @@ export default function BankFileColumnMappingStep({ delimiter, decimal_separator: decimalSep, skip_rows: 1, // Skip header - date_format: 'YYYY-MM-DD', + date_format: dateFormat, } onConfirm(mapping) } @@ -78,8 +79,8 @@ export default function BankFileColumnMappingStep({ - {/* Delimiter and decimal settings */} -
+ {/* Delimiter, decimal, and date format settings */} +
+
+ + +
{/* Required column mappings */} diff --git a/components/suppliers/SupplierInvoiceReviewContent.tsx b/components/suppliers/SupplierInvoiceReviewContent.tsx index ded594de..6bc47257 100644 --- a/components/suppliers/SupplierInvoiceReviewContent.tsx +++ b/components/suppliers/SupplierInvoiceReviewContent.tsx @@ -40,12 +40,19 @@ interface JournalPreviewLine { credit: number } +function getOutputVatAccount(rate: number): string { + if (rate === 0.12) return '2624' + if (rate === 0.06) return '2634' + return '2614' +} + function buildJournalPreview( items: ReviewLineItem[], subtotal: number, totalVat: number, total: number, reverseCharge: boolean, + supplierType?: string, ): JournalPreviewLine[] { const lines: JournalPreviewLine[] = [] @@ -67,21 +74,35 @@ function buildJournalPreview( } if (reverseCharge) { - // EU reverse charge: fiktiv moms - const vatRate = 0.25 - const fiktivVat = Math.round(subtotal * vatRate * 100) / 100 - lines.push({ - account_number: '2645', - description: 'Beräknad ingående moms', - debit: fiktivVat, - credit: 0, - }) - lines.push({ - account_number: '2614', - description: 'Utgående moms omvänd', - debit: 0, - credit: fiktivVat, - }) + // Reverse charge: fiktiv moms per VAT rate (matches engine groupVatByRate logic) + const isDomesticRC = supplierType === 'swedish_business' + const inputAccount = isDomesticRC ? '2647' : '2645' + + const vatByRate = new Map() + for (const item of items) { + if (item.vat_rate > 0) { + const current = vatByRate.get(item.vat_rate) || 0 + vatByRate.set(item.vat_rate, current + Math.round(item.amount * 100) / 100) + } + } + + for (const [rate, netAmount] of vatByRate) { + const fiktivVat = Math.round(netAmount * rate * 100) / 100 + const outputAccount = getOutputVatAccount(rate) + lines.push({ + account_number: inputAccount, + description: inputAccount, + debit: fiktivVat, + credit: 0, + }) + lines.push({ + account_number: outputAccount, + description: outputAccount, + debit: 0, + credit: fiktivVat, + }) + } + // Credit: 2440 at subtotal (no real VAT for reverse charge) lines.push({ account_number: '2440', @@ -113,8 +134,11 @@ function buildJournalPreview( const ACCOUNT_LABELS: Record = { '2440': 'Leverantörsskulder', '2641': 'Ingående moms', - '2645': 'Beräknad ingående moms', - '2614': 'Utg. moms omvänd skattskyldighet', + '2645': 'Beräknad ing. moms förvärv utlandet', + '2647': 'Beräknad ing. moms omvänd i Sverige', + '2614': 'Utg. moms omvänd 25%', + '2624': 'Utg. moms omvänd 12%', + '2634': 'Utg. moms omvänd 6%', } export function SupplierInvoiceReviewContent({ @@ -132,7 +156,7 @@ export function SupplierInvoiceReviewContent({ totalVat, total, }: SupplierInvoiceReviewContentProps) { - const journalLines = buildJournalPreview(items, subtotal, totalVat, total, reverseCharge) + const journalLines = buildJournalPreview(items, subtotal, totalVat, total, reverseCharge, supplier.supplier_type) const totalDebit = journalLines.reduce((sum, l) => sum + l.debit, 0) const totalCredit = journalLines.reduce((sum, l) => sum + l.credit, 0) diff --git a/lib/bookkeeping/client-account-names.ts b/lib/bookkeeping/client-account-names.ts index ede4cb22..c86e9fe5 100644 --- a/lib/bookkeeping/client-account-names.ts +++ b/lib/bookkeeping/client-account-names.ts @@ -22,9 +22,12 @@ const ACCOUNT_NAMES: Record = { '2611': 'Utg. moms 25%', '2621': 'Utg. moms 12%', '2631': 'Utg. moms 6%', - '2614': 'Utg. moms omvänd', + '2614': 'Utg. moms omvänd 25%', + '2624': 'Utg. moms omvänd 12%', + '2634': 'Utg. moms omvänd 6%', '2641': 'Ing. moms', - '2645': 'Beräknad ing. moms', + '2645': 'Beräknad ing. moms förvärv utlandet', + '2647': 'Beräknad ing. moms omvänd i Sverige', '2731': 'Arbetsgivaravgifter', '2893': 'Skuld till ägare', diff --git a/lib/import/bank-file/__tests__/date-utils.test.ts b/lib/import/bank-file/__tests__/date-utils.test.ts new file mode 100644 index 00000000..1e09a7e6 --- /dev/null +++ b/lib/import/bank-file/__tests__/date-utils.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect } from 'vitest' +import { normalizeDate } from '../date-utils' + +describe('normalizeDate', () => { + // Pass-through for canonical format + it('passes through YYYY-MM-DD', () => { + expect(normalizeDate('2024-01-15')).toBe('2024-01-15') + expect(normalizeDate('2026-12-31')).toBe('2026-12-31') + }) + + // European dot format + it('normalizes DD.MM.YYYY', () => { + expect(normalizeDate('15.01.2024')).toBe('2024-01-15') + expect(normalizeDate('31.12.2026')).toBe('2026-12-31') + }) + + it('normalizes D.M.YYYY (single-digit day/month)', () => { + expect(normalizeDate('5.1.2024')).toBe('2024-01-05') + expect(normalizeDate('9.3.2025')).toBe('2025-03-09') + }) + + // European slash format + it('normalizes DD/MM/YYYY', () => { + expect(normalizeDate('15/01/2024')).toBe('2024-01-15') + expect(normalizeDate('31/12/2026')).toBe('2026-12-31') + }) + + it('normalizes D/M/YYYY (single-digit)', () => { + expect(normalizeDate('5/1/2024')).toBe('2024-01-05') + }) + + // US format with hint + it('normalizes MM/DD/YYYY when hint is provided', () => { + expect(normalizeDate('01/15/2024', 'MM/DD/YYYY')).toBe('2024-01-15') + expect(normalizeDate('12/31/2026', 'MM/DD/YYYY')).toBe('2026-12-31') + }) + + // Compact format + it('normalizes YYYYMMDD', () => { + expect(normalizeDate('20240115')).toBe('2024-01-15') + expect(normalizeDate('20261231')).toBe('2026-12-31') + }) + + // Slash year-first format + it('normalizes YYYY/MM/DD', () => { + expect(normalizeDate('2024/01/15')).toBe('2024-01-15') + expect(normalizeDate('2026/12/31')).toBe('2026-12-31') + }) + + // Whitespace handling + it('trims whitespace', () => { + expect(normalizeDate(' 2024-01-15 ')).toBe('2024-01-15') + expect(normalizeDate(' 15.01.2024 ')).toBe('2024-01-15') + }) + + // Invalid dates + it('returns null for empty/null/undefined', () => { + expect(normalizeDate('')).toBeNull() + expect(normalizeDate(null)).toBeNull() + expect(normalizeDate(undefined)).toBeNull() + expect(normalizeDate(' ')).toBeNull() + }) + + it('returns null for unrecognized formats', () => { + expect(normalizeDate('Jan 15, 2024')).toBeNull() + expect(normalizeDate('15-Jan-2024')).toBeNull() + expect(normalizeDate('abc')).toBeNull() + expect(normalizeDate('2024')).toBeNull() + }) + + it('returns null for invalid month', () => { + expect(normalizeDate('2024-13-01')).toBeNull() + expect(normalizeDate('2024-00-01')).toBeNull() + }) + + it('returns null for invalid day', () => { + expect(normalizeDate('2024-02-30')).toBeNull() // Feb 30 doesn't exist + expect(normalizeDate('2024-04-31')).toBeNull() // Apr has 30 days + expect(normalizeDate('2024-01-32')).toBeNull() + expect(normalizeDate('2024-01-00')).toBeNull() + }) + + it('handles leap year correctly', () => { + expect(normalizeDate('2024-02-29')).toBe('2024-02-29') // 2024 is leap year + expect(normalizeDate('2023-02-29')).toBeNull() // 2023 is not + }) + + it('returns null for years out of range', () => { + expect(normalizeDate('1899-01-01')).toBeNull() + expect(normalizeDate('2101-01-01')).toBeNull() + }) +}) diff --git a/lib/import/bank-file/__tests__/parser.test.ts b/lib/import/bank-file/__tests__/parser.test.ts index 2348113e..8438e430 100644 --- a/lib/import/bank-file/__tests__/parser.test.ts +++ b/lib/import/bank-file/__tests__/parser.test.ts @@ -1151,7 +1151,7 @@ describe('parseBankFile — explicit format override', () => { expect(result.format_name).toBe('Unknown') expect(result.transactions).toHaveLength(0) expect(result.issues).toHaveLength(1) - expect(result.issues[0].message).toContain('Could not auto-detect') + expect(result.issues[0].message).toContain('Kunde inte identifiera bankformat') }) it('can force generic_csv format by explicit ID', () => { diff --git a/lib/import/bank-file/date-utils.ts b/lib/import/bank-file/date-utils.ts new file mode 100644 index 00000000..828cb809 --- /dev/null +++ b/lib/import/bank-file/date-utils.ts @@ -0,0 +1,79 @@ +/** + * Date normalization for bank file imports. + * + * Converts common Swedish/European date formats to canonical YYYY-MM-DD. + * Used by all bank-specific parsers and the generic CSV fallback. + */ + +/** + * Normalize a date string to YYYY-MM-DD. + * + * Supported formats: + * - YYYY-MM-DD (pass-through) + * - YYYY/MM/DD + * - YYYYMMDD + * - DD.MM.YYYY / D.M.YYYY + * - DD/MM/YYYY / D/M/YYYY + * + * The `hint` parameter disambiguates DD/MM vs MM/DD when using slash separators. + * Default assumption is DD/MM (European convention) since this is a Swedish app. + * + * Returns the canonical YYYY-MM-DD string, or null if unparseable. + */ +export function normalizeDate(raw: string | undefined | null, hint?: string): string | null { + if (!raw) return null + const s = raw.trim() + if (!s) return null + + let year: number + let month: number + let day: number + + // YYYY-MM-DD + if (/^\d{4}-\d{2}-\d{2}$/.test(s)) { + const [y, m, d] = s.split('-').map(Number) + year = y; month = m; day = d + + // YYYY/MM/DD + } else if (/^\d{4}\/\d{2}\/\d{2}$/.test(s)) { + const [y, m, d] = s.split('/').map(Number) + year = y; month = m; day = d + + // YYYYMMDD + } else if (/^\d{8}$/.test(s)) { + year = parseInt(s.substring(0, 4), 10) + month = parseInt(s.substring(4, 6), 10) + day = parseInt(s.substring(6, 8), 10) + + // DD.MM.YYYY or D.M.YYYY + } else if (/^\d{1,2}\.\d{1,2}\.\d{4}$/.test(s)) { + const parts = s.split('.').map(Number) + day = parts[0]; month = parts[1]; year = parts[2] + + // DD/MM/YYYY or D/M/YYYY (or MM/DD/YYYY based on hint) + } else if (/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(s)) { + const parts = s.split('/').map(Number) + if (hint === 'MM/DD/YYYY') { + month = parts[0]; day = parts[1]; year = parts[2] + } else { + // Default: DD/MM/YYYY (European) + day = parts[0]; month = parts[1]; year = parts[2] + } + + } else { + return null + } + + // Validate ranges + if (year < 1900 || year > 2100) return null + if (month < 1 || month > 12) return null + if (day < 1 || day > 31) return null + + // Check day is valid for the given month + const maxDay = new Date(year, month, 0).getDate() + if (day > maxDay) return null + + const mm = String(month).padStart(2, '0') + const dd = String(day).padStart(2, '0') + return `${year}-${mm}-${dd}` +} diff --git a/lib/import/bank-file/formats/generic-csv.ts b/lib/import/bank-file/formats/generic-csv.ts index 48034777..b1d8e5a7 100644 --- a/lib/import/bank-file/formats/generic-csv.ts +++ b/lib/import/bank-file/formats/generic-csv.ts @@ -8,6 +8,7 @@ import type { BankFileFormat, BankFileParseResult, ParsedBankTransaction, BankFileParseIssue, GenericCSVColumnMapping } from '../types' import { prepareContent } from '../encoding' import { parseCSVLine } from './nordea' +import { normalizeDate } from '../date-utils' /** * Parse a generic CSV with user-provided column mapping @@ -26,6 +27,32 @@ export function parseGenericCSV( // Skip configured number of header/metadata rows const startRow = mapping.skip_rows + // Detect decimal separator mismatch by sampling amount column + const sampleSize = Math.min(lines.length - startRow, 20) + let commaPattern = 0 + let periodPattern = 0 + for (let s = startRow; s < startRow + sampleSize && s < lines.length; s++) { + const sampleLine = lines[s]?.trim() + if (!sampleLine) continue + const sampleFields = parseCSVLine(sampleLine, mapping.delimiter).map(f => f.trim().replace(/^"|"$/g, '')) + const amtStr = sampleFields[mapping.amount] || '' + if (/\d,\d{1,2}$/.test(amtStr)) commaPattern++ + if (/\d\.\d{1,2}$/.test(amtStr)) periodPattern++ + } + if (mapping.decimal_separator === ',' && periodPattern > commaPattern && periodPattern >= 3) { + issues.push({ + row: 0, + message: 'Decimalavgränsare verkar vara punkt (.) men komma (,) är valt. Kontrollera inställningen.', + severity: 'warning', + }) + } else if (mapping.decimal_separator === '.' && commaPattern > periodPattern && commaPattern >= 3) { + issues.push({ + row: 0, + message: 'Decimalavgränsare verkar vara komma (,) men punkt (.) är valt. Kontrollera inställningen.', + severity: 'warning', + }) + } + for (let i = startRow; i < lines.length; i++) { const line = lines[i].trim() if (!line) continue @@ -54,6 +81,10 @@ export function parseGenericCSV( const balanceStr = mapping.balance !== undefined ? fields[mapping.balance] : undefined if (!dateStr || !amountStr) { + const missing = [] + if (!dateStr) missing.push('datum') + if (!amountStr) missing.push('belopp') + issues.push({ row: i + 1, message: `Saknar ${missing.join(' och ')}`, severity: 'warning' }) skippedRows++ continue } @@ -72,10 +103,10 @@ export function parseGenericCSV( continue } - // Parse date - expect YYYY-MM-DD - const date = dateStr.trim() - if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { - issues.push({ row: i + 1, message: `Invalid date format: ${date} (expected YYYY-MM-DD)`, severity: 'warning' }) + // Normalize date from multiple formats to YYYY-MM-DD + const date = normalizeDate(dateStr, mapping.date_format) + if (!date) { + issues.push({ row: i + 1, message: `Ogiltigt datumformat: ${dateStr.trim()}`, severity: 'warning' }) skippedRows++ continue } diff --git a/lib/import/bank-file/formats/handelsbanken.ts b/lib/import/bank-file/formats/handelsbanken.ts index df618cf2..7568bb2b 100644 --- a/lib/import/bank-file/formats/handelsbanken.ts +++ b/lib/import/bank-file/formats/handelsbanken.ts @@ -12,6 +12,7 @@ import type { BankFileFormat, BankFileParseResult, ParsedBankTransaction, BankFileParseIssue } from '../types' import { prepareContent } from '../encoding' +import { normalizeDate } from '../date-utils' function parseCommaDecimal(value: string): number { const cleaned = value.replace(/\s/g, '').replace(',', '.') @@ -91,6 +92,10 @@ export const handelsbankenFormat: BankFileFormat = { } if (!date || !amountStr) { + const missing = [] + if (!date) missing.push('datum') + if (!amountStr) missing.push('belopp') + issues.push({ row: i + 1, message: `Saknar ${missing.join(' och ')}`, severity: 'warning' }) skippedRows++ continue } @@ -102,7 +107,8 @@ export const handelsbankenFormat: BankFileFormat = { continue } - if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { + const normalizedDate = normalizeDate(date) + if (!normalizedDate) { issues.push({ row: i + 1, message: `Invalid date: ${date}`, severity: 'warning' }) skippedRows++ continue @@ -111,7 +117,7 @@ export const handelsbankenFormat: BankFileFormat = { const balance = balanceStr ? parseCommaDecimal(balanceStr) : null transactions.push({ - date, + date: normalizedDate, description: (description || 'Unknown').trim(), amount, currency: 'SEK', diff --git a/lib/import/bank-file/formats/ica-banken.ts b/lib/import/bank-file/formats/ica-banken.ts index 6bd4329d..02f9f076 100644 --- a/lib/import/bank-file/formats/ica-banken.ts +++ b/lib/import/bank-file/formats/ica-banken.ts @@ -14,6 +14,7 @@ import type { BankFileFormat, BankFileParseResult, ParsedBankTransaction, BankFileParseIssue } from '../types' import { prepareContent } from '../encoding' +import { normalizeDate } from '../date-utils' function parseCommaDecimal(value: string): number { const cleaned = value.replace(/\s/g, '').replace(',', '.') @@ -135,6 +136,10 @@ export const icaBankenFormat: BankFileFormat = { const balanceStr = balanceIdx >= 0 ? fields[balanceIdx] : undefined if (!date || !amountStr) { + const missing = [] + if (!date) missing.push('datum') + if (!amountStr) missing.push('belopp') + issues.push({ row: i + 1, message: `Saknar ${missing.join(' och ')}`, severity: 'warning' }) skippedRows++ continue } @@ -146,7 +151,8 @@ export const icaBankenFormat: BankFileFormat = { continue } - if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { + const normalizedDate = normalizeDate(date) + if (!normalizedDate) { issues.push({ row: i + 1, message: `Invalid date: ${date}`, severity: 'warning' }) skippedRows++ continue @@ -155,7 +161,7 @@ export const icaBankenFormat: BankFileFormat = { const balance = balanceStr ? parseCommaDecimal(balanceStr) : null transactions.push({ - date, + date: normalizedDate, description: (description || 'Unknown').trim(), amount, currency: 'SEK', diff --git a/lib/import/bank-file/formats/lansforsakringar.ts b/lib/import/bank-file/formats/lansforsakringar.ts index ca8d3da9..9c9a24ea 100644 --- a/lib/import/bank-file/formats/lansforsakringar.ts +++ b/lib/import/bank-file/formats/lansforsakringar.ts @@ -14,6 +14,7 @@ import type { BankFileFormat, BankFileParseResult, ParsedBankTransaction, BankFileParseIssue } from '../types' import { prepareContent } from '../encoding' +import { normalizeDate } from '../date-utils' import { parseCSVLine } from './nordea' function parseCommaDecimal(value: string): number { @@ -123,6 +124,10 @@ export const lansforsakringarFormat: BankFileFormat = { const balanceStr = balanceIdx >= 0 && balanceIdx < fields.length ? fields[balanceIdx] : undefined if (!date || !amountStr) { + const missing = [] + if (!date) missing.push('datum') + if (!amountStr) missing.push('belopp') + issues.push({ row: i + 1, message: `Saknar ${missing.join(' och ')}`, severity: 'warning' }) skippedRows++ continue } @@ -134,7 +139,8 @@ export const lansforsakringarFormat: BankFileFormat = { continue } - if (!DATE_RE.test(date)) { + const normalizedDate = normalizeDate(date) + if (!normalizedDate) { issues.push({ row: i + 1, message: `Invalid date: ${date}`, severity: 'warning' }) skippedRows++ continue @@ -143,7 +149,7 @@ export const lansforsakringarFormat: BankFileFormat = { const balance = balanceStr ? parseCommaDecimal(balanceStr) : null transactions.push({ - date, + date: normalizedDate, description: description.trim(), amount, currency: 'SEK', diff --git a/lib/import/bank-file/formats/lunar.ts b/lib/import/bank-file/formats/lunar.ts index a88efd32..48868360 100644 --- a/lib/import/bank-file/formats/lunar.ts +++ b/lib/import/bank-file/formats/lunar.ts @@ -15,6 +15,7 @@ import type { BankFileFormat, BankFileParseResult, ParsedBankTransaction, BankFileParseIssue } from '../types' import { prepareContent } from '../encoding' +import { normalizeDate } from '../date-utils' import { parseCSVLine } from './nordea' function parseLunarAmount(value: string): number { @@ -90,6 +91,10 @@ export const lunarFormat: BankFileFormat = { const balanceStr = balanceIdx >= 0 ? fields[balanceIdx] : undefined if (!date || !amountStr) { + const missing = [] + if (!date) missing.push('datum') + if (!amountStr) missing.push('belopp') + issues.push({ row: i + 1, message: `Saknar ${missing.join(' och ')}`, severity: 'warning' }) skippedRows++ continue } @@ -101,7 +106,8 @@ export const lunarFormat: BankFileFormat = { continue } - if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { + const normalizedDate = normalizeDate(date) + if (!normalizedDate) { issues.push({ row: i + 1, message: `Invalid date: ${date}`, severity: 'warning' }) skippedRows++ continue @@ -110,7 +116,7 @@ export const lunarFormat: BankFileFormat = { const balance = balanceStr ? parseLunarAmount(balanceStr) : null transactions.push({ - date, + date: normalizedDate, description: (description || 'Unknown').trim(), amount, currency: 'SEK', diff --git a/lib/import/bank-file/formats/nordea-business.ts b/lib/import/bank-file/formats/nordea-business.ts index cbf313d4..f43cd78c 100644 --- a/lib/import/bank-file/formats/nordea-business.ts +++ b/lib/import/bank-file/formats/nordea-business.ts @@ -22,6 +22,7 @@ import type { BankFileFormat, BankFileParseResult, ParsedBankTransaction, BankFileParseIssue } from '../types' import { prepareContent } from '../encoding' +import { normalizeDate } from '../date-utils' function parseCommaDecimal(value: string): number { const cleaned = value.replace(/\s/g, '').replace(',', '.') @@ -158,11 +159,8 @@ export const nordeaBusinessFormat: BankFileFormat = { continue } - // Normalize YYYY/MM/DD → YYYY-MM-DD (Format D variant) - const normalizedDate = date.includes('/') ? date.replace(/\//g, '-') : date - - // Validate date format (YYYY-MM-DD) - if (!/^\d{4}-\d{2}-\d{2}$/.test(normalizedDate)) { + const normalizedDate = normalizeDate(date) + if (!normalizedDate) { issues.push({ row: i + 1, message: `Invalid date: ${date}`, severity: 'warning' }) skippedRows++ continue diff --git a/lib/import/bank-file/formats/nordea.ts b/lib/import/bank-file/formats/nordea.ts index d662e5df..5672d6c8 100644 --- a/lib/import/bank-file/formats/nordea.ts +++ b/lib/import/bank-file/formats/nordea.ts @@ -13,6 +13,7 @@ import type { BankFileFormat, BankFileParseResult, ParsedBankTransaction, BankFileParseIssue } from '../types' import { prepareContent } from '../encoding' +import { normalizeDate } from '../date-utils' function parseCommaDecimal(value: string): number { // Swedish format: "1 234,56" or "-1 234,56" @@ -77,8 +78,8 @@ export const nordeaFormat: BankFileFormat = { continue } - // Validate date format - if (!/^\d{4}-\d{2}-\d{2}$/.test(date?.trim())) { + const normalizedDate = normalizeDate(date) + if (!normalizedDate) { issues.push({ row: i + 1, message: `Invalid date: ${date}`, severity: 'warning' }) skippedRows++ continue @@ -87,7 +88,7 @@ export const nordeaFormat: BankFileFormat = { const balance = balanceStr ? parseCommaDecimal(balanceStr) : null transactions.push({ - date: date.trim(), + date: normalizedDate, description: description?.trim() || 'Unknown', amount, currency: 'SEK', diff --git a/lib/import/bank-file/formats/seb.ts b/lib/import/bank-file/formats/seb.ts index 91892f03..2027b20c 100644 --- a/lib/import/bank-file/formats/seb.ts +++ b/lib/import/bank-file/formats/seb.ts @@ -10,6 +10,7 @@ import type { BankFileFormat, BankFileParseResult, ParsedBankTransaction, BankFileParseIssue } from '../types' import { prepareContent } from '../encoding' +import { normalizeDate } from '../date-utils' function parseCommaDecimal(value: string): number { const cleaned = value.replace(/\s/g, '').replace(',', '.') @@ -95,7 +96,8 @@ export const sebFormat: BankFileFormat = { continue } - if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { + const normalizedDate = normalizeDate(date) + if (!normalizedDate) { issues.push({ row: i + 1, message: `Invalid date: ${date}`, severity: 'warning' }) skippedRows++ continue @@ -104,7 +106,7 @@ export const sebFormat: BankFileFormat = { const balance = balanceStr ? parseCommaDecimal(balanceStr) : null transactions.push({ - date, + date: normalizedDate, description: description.trim(), amount, currency: 'SEK', diff --git a/lib/import/bank-file/formats/skandia.ts b/lib/import/bank-file/formats/skandia.ts index 0c810d4b..6eabc614 100644 --- a/lib/import/bank-file/formats/skandia.ts +++ b/lib/import/bank-file/formats/skandia.ts @@ -13,6 +13,7 @@ import type { BankFileFormat, BankFileParseResult, ParsedBankTransaction, BankFileParseIssue } from '../types' import { prepareContent } from '../encoding' +import { normalizeDate } from '../date-utils' function parseCommaDecimal(value: string): number { const cleaned = value.replace(/\s/g, '').replace(',', '.') @@ -96,6 +97,10 @@ export const skandiaFormat: BankFileFormat = { const balanceStr = balanceIdx >= 0 ? fields[balanceIdx] : undefined if (!date || !amountStr) { + const missing = [] + if (!date) missing.push('datum') + if (!amountStr) missing.push('belopp') + issues.push({ row: i + 1, message: `Saknar ${missing.join(' och ')}`, severity: 'warning' }) skippedRows++ continue } @@ -107,7 +112,8 @@ export const skandiaFormat: BankFileFormat = { continue } - if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { + const normalizedDate = normalizeDate(date) + if (!normalizedDate) { issues.push({ row: i + 1, message: `Invalid date: ${date}`, severity: 'warning' }) skippedRows++ continue @@ -116,7 +122,7 @@ export const skandiaFormat: BankFileFormat = { const balance = balanceStr ? parseCommaDecimal(balanceStr) : null transactions.push({ - date, + date: normalizedDate, description: (description || 'Unknown').trim(), amount, currency: 'SEK', diff --git a/lib/import/bank-file/formats/swedbank.ts b/lib/import/bank-file/formats/swedbank.ts index 1eb8dbd3..6755a616 100644 --- a/lib/import/bank-file/formats/swedbank.ts +++ b/lib/import/bank-file/formats/swedbank.ts @@ -18,6 +18,7 @@ import type { BankFileFormat, BankFileParseResult, ParsedBankTransaction, BankFileParseIssue } from '../types' import { prepareContent } from '../encoding' +import { normalizeDate } from '../date-utils' import { parseCSVLine } from './nordea' /** @@ -137,6 +138,10 @@ export const swedbankFormat: BankFileFormat = { const balanceStr = balanceIdx >= 0 ? fields[balanceIdx] : undefined if (!date || !amountStr) { + const missing = [] + if (!date) missing.push('datum') + if (!amountStr) missing.push('belopp') + issues.push({ row: i + 1, message: `Saknar ${missing.join(' och ')}`, severity: 'warning' }) skippedRows++ continue } @@ -149,7 +154,8 @@ export const swedbankFormat: BankFileFormat = { continue } - if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { + const normalizedDate = normalizeDate(date) + if (!normalizedDate) { issues.push({ row: i + 1, message: `Invalid date: ${date}`, severity: 'warning' }) skippedRows++ continue @@ -163,7 +169,7 @@ export const swedbankFormat: BankFileFormat = { : reference || textDesc || 'Unknown' transactions.push({ - date, + date: normalizedDate, description, amount, currency: 'SEK', diff --git a/lib/import/bank-file/parser.ts b/lib/import/bank-file/parser.ts index 129b5c9e..fe04673d 100644 --- a/lib/import/bank-file/parser.ts +++ b/lib/import/bank-file/parser.ts @@ -98,6 +98,11 @@ export function parseBankFile( } else { format = detectFileFormat(content, filename) || undefined if (!format) { + // Build diagnostic message listing which formats were tried + const tried = FORMATS + .filter(f => f.id !== 'generic_csv') + .map(f => f.name) + const firstLine = content.split('\n')[0]?.substring(0, 80) || '' return { format: 'generic_csv', format_name: 'Unknown', @@ -106,7 +111,7 @@ export function parseBankFile( date_to: null, issues: [{ row: 0, - message: 'Could not auto-detect file format. Please select your bank manually.', + message: `Kunde inte identifiera bankformat. Testade: ${tried.join(', ')}. Första raden: "${firstLine}". Välj bank manuellt eller använd "Annan CSV".`, severity: 'error', }], stats: { total_rows: 0, parsed_rows: 0, skipped_rows: 0, total_income: 0, total_expenses: 0 },