chore: init from v0

This commit is contained in:
Julien Froidefond
2025-11-27 09:51:18 +01:00
commit e9e44916fd
109 changed files with 15966 additions and 0 deletions

91
lib/ofx-parser.tsx Normal file
View File

@@ -0,0 +1,91 @@
import type { OFXAccount, OFXTransaction } from "./types"
export function parseOFX(content: string): OFXAccount | null {
try {
// Remove SGML header and clean up
const xmlStart = content.indexOf("<OFX>")
if (xmlStart === -1) return null
let xml = content.substring(xmlStart)
// Convert SGML to XML-like format
xml = xml.replace(/<(\w+)>([^<]+)(?=<)/g, "<$1>$2</$1>")
// 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 = /<STMTTRN>([\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,
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()
}