import type { OFXAccount, OFXTransaction } from "./types"; export function parseOFX(content: string): OFXAccount | null { try { // Remove SGML header and clean up const xmlStart = content.indexOf(""); if (xmlStart === -1) return null; let xml = content.substring(xmlStart); // Convert SGML to XML-like format xml = xml.replace(/<(\w+)>([^<]+)(?=<)/g, "<$1>$2"); // Extract account info const bankId = extractValue(xml, "BANKID") || extractValue(xml, "ORG") || "UNKNOWN"; const accountId = extractValue(xml, "ACCTID") || "UNKNOWN"; const accountType = extractValue(xml, "ACCTTYPE") || "CHECKING"; const balanceStr = extractValue(xml, "BALAMT") || "0"; const balance = Number.parseFloat(balanceStr); const balanceDate = extractValue(xml, "DTASOF") || new Date().toISOString(); const currency = extractValue(xml, "CURDEF") || "EUR"; // Extract transactions const transactions: OFXTransaction[] = []; const stmtTrnRegex = /([\s\S]*?)<\/STMTTRN>/gi; let match; while ((match = stmtTrnRegex.exec(xml)) !== null) { const trnXml = match[1]; const fitId = extractValue(trnXml, "FITID") || `${Date.now()}-${Math.random()}`; const dateStr = extractValue(trnXml, "DTPOSTED") || ""; const amountStr = extractValue(trnXml, "TRNAMT") || "0"; const name = extractValue(trnXml, "NAME") || extractValue(trnXml, "MEMO") || "Unknown"; const memo = extractValue(trnXml, "MEMO"); const checkNum = extractValue(trnXml, "CHECKNUM"); const type = extractValue(trnXml, "TRNTYPE") || "OTHER"; transactions.push({ fitId, date: parseOFXDate(dateStr), amount: Number.parseFloat(amountStr), name: cleanString(name), memo: memo ? cleanString(memo) : undefined, checkNum: checkNum ?? undefined, type, }); } return { bankId, accountId, accountType: mapAccountType(accountType), balance, balanceDate: parseOFXDate(balanceDate), currency, transactions, }; } catch (error) { console.error("Error parsing OFX:", error); return null; } } function extractValue(xml: string, tag: string): string | null { const regex = new RegExp(`<${tag}>([^<]+)`, "i"); const match = xml.match(regex); return match ? match[1].trim() : null; } function parseOFXDate(dateStr: string): string { if (!dateStr || dateStr.length < 8) return new Date().toISOString(); const year = dateStr.substring(0, 4); const month = dateStr.substring(4, 6); const day = dateStr.substring(6, 8); return `${year}-${month}-${day}`; } function mapAccountType( type: string, ): "CHECKING" | "SAVINGS" | "CREDIT_CARD" | "OTHER" { const upper = type.toUpperCase(); if (upper.includes("CHECK") || upper.includes("CURRENT")) return "CHECKING"; if (upper.includes("SAV")) return "SAVINGS"; if (upper.includes("CREDIT")) return "CREDIT_CARD"; return "OTHER"; } function cleanString(str: string): string { return str.replace(/\s+/g, " ").trim(); }