885f362a29
Replace PSD2 bank integration as the default with file-based bank import (CSV/XML), which better suits Swedish sole traders and small companies. Enable Banking is now an opt-in extension. - Phase 1: Extract generic transaction ingestion service (ingest.ts) with dedup, auto-categorization, and OCR-based invoice matching - Phase 2: Bank file parser library supporting Nordea, SEB, Swedbank, Handelsbanken CSV formats and ISO 20022 camt.053 XML - Phase 3: Database migration adding import_source, reference columns and bank_file_imports tracking table - Phase 4: Import wizard UI (5-step flow) and API routes for parse/execute - Phase 5: Move Enable Banking to extensions/enable-banking/ with commented-out loader entry for opt-in activation - Phase 6: 104 new tests (ingestion + all parser formats), fixing Nordea detection overlap and camt.053 XML tag collision bugs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
88 lines
2.7 KiB
TypeScript
88 lines
2.7 KiB
TypeScript
/**
|
|
* Encoding detection and conversion for Swedish bank files.
|
|
*
|
|
* Swedish bank exports use either UTF-8 or Windows-1252 (ISO-8859-1).
|
|
* We detect encoding by checking for valid Swedish characters.
|
|
*/
|
|
|
|
/**
|
|
* Decode file content, handling both UTF-8 and Windows-1252 encodings.
|
|
*
|
|
* Strategy: Try UTF-8 first. If the result contains replacement characters
|
|
* (U+FFFD) or garbled Swedish chars, fall back to Windows-1252.
|
|
*/
|
|
export function decodeFileContent(buffer: ArrayBuffer): string {
|
|
// Try UTF-8 first
|
|
const utf8Decoder = new TextDecoder('utf-8', { fatal: false })
|
|
const utf8Result = utf8Decoder.decode(buffer)
|
|
|
|
// Check if UTF-8 decode produced valid Swedish text
|
|
if (!hasEncodingIssues(utf8Result)) {
|
|
return utf8Result
|
|
}
|
|
|
|
// Fall back to Windows-1252 (superset of ISO-8859-1)
|
|
const latin1Decoder = new TextDecoder('windows-1252', { fatal: false })
|
|
return latin1Decoder.decode(buffer)
|
|
}
|
|
|
|
/**
|
|
* Decode a string that may have been incorrectly decoded as UTF-8
|
|
* when the source was actually Windows-1252.
|
|
*/
|
|
export function decodeStringContent(content: string): string {
|
|
// If the string already contains valid Swedish chars, return as-is
|
|
if (!hasEncodingIssues(content)) {
|
|
return content
|
|
}
|
|
|
|
// Try re-encoding as Latin-1 and decoding as Windows-1252
|
|
try {
|
|
const bytes = new Uint8Array(content.length)
|
|
for (let i = 0; i < content.length; i++) {
|
|
bytes[i] = content.charCodeAt(i) & 0xff
|
|
}
|
|
const decoder = new TextDecoder('windows-1252', { fatal: false })
|
|
return decoder.decode(bytes)
|
|
} catch {
|
|
return content
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a string has encoding issues (garbled Swedish characters).
|
|
*/
|
|
function hasEncodingIssues(text: string): boolean {
|
|
// U+FFFD = replacement character (means invalid UTF-8 byte sequences)
|
|
if (text.includes('\uFFFD')) return true
|
|
|
|
// Common garbled patterns when Windows-1252 is read as UTF-8:
|
|
// Ã¥ = å, ä = ä, ö = ö, Ã… = Å, Ä = Ä, Ö = Ö
|
|
const garbledPatterns = ['Ã¥', 'ä', 'ö', 'Ã\u0085', 'Ã\u0084', 'Ã\u0096']
|
|
return garbledPatterns.some((pattern) => text.includes(pattern))
|
|
}
|
|
|
|
/**
|
|
* Normalize line endings to \n
|
|
*/
|
|
export function normalizeLineEndings(content: string): string {
|
|
return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
|
|
}
|
|
|
|
/**
|
|
* Strip BOM (Byte Order Mark) from start of content
|
|
*/
|
|
export function stripBOM(content: string): string {
|
|
if (content.charCodeAt(0) === 0xfeff) {
|
|
return content.slice(1)
|
|
}
|
|
return content
|
|
}
|
|
|
|
/**
|
|
* Prepare file content for parsing: strip BOM, normalize line endings, handle encoding
|
|
*/
|
|
export function prepareContent(content: string): string {
|
|
return normalizeLineEndings(stripBOM(decodeStringContent(content)))
|
|
}
|