|
| 1 | +import { Statement, Transaction, StructuredDescription } from "./types"; |
| 2 | +import { XMLParser } from "fast-xml-parser"; |
| 3 | + |
| 4 | +const parserOptions = { |
| 5 | + ignoreAttributes: false, |
| 6 | + attributeNamePrefix: "@_", |
| 7 | + parseAttributeValue: false, |
| 8 | + parseTagValue: false, |
| 9 | + trimValues: true, |
| 10 | + removeNSPrefix: true, |
| 11 | + isArray: (name: string) => ["Rpt", "Ntry", "Bal", "Ustrd", "TxDtls"].includes(name), |
| 12 | +}; |
| 13 | + |
| 14 | +function getVal(obj: unknown, path: string): unknown { |
| 15 | + const parts = path.split("."); |
| 16 | + let cur: unknown = obj; |
| 17 | + for (const p of parts) { |
| 18 | + if (cur == null || typeof cur !== "object") return undefined; |
| 19 | + cur = (cur as Record<string, unknown>)[p]; |
| 20 | + } |
| 21 | + return cur; |
| 22 | +} |
| 23 | + |
| 24 | +function getStr(obj: unknown, path: string): string | undefined { |
| 25 | + const v = getVal(obj, path); |
| 26 | + return v == null ? undefined : String(v); |
| 27 | +} |
| 28 | + |
| 29 | +function parseDate(dateStr: string | undefined): Date { |
| 30 | + if (!dateStr) return new Date(0); |
| 31 | + const d = new Date(dateStr.trim()); |
| 32 | + return isNaN(d.getTime()) ? new Date(0) : d; |
| 33 | +} |
| 34 | + |
| 35 | +function parseAmount(amtNode: unknown): { value: number; currency: string } { |
| 36 | + if (amtNode == null) return { value: 0, currency: "EUR" }; |
| 37 | + if (typeof amtNode === "object") { |
| 38 | + const obj = amtNode as Record<string, unknown>; |
| 39 | + return { |
| 40 | + value: parseFloat(String(obj["#text"] ?? 0)) || 0, |
| 41 | + currency: String(obj["@_Ccy"] ?? "EUR"), |
| 42 | + }; |
| 43 | + } |
| 44 | + return { value: parseFloat(String(amtNode)) || 0, currency: "EUR" }; |
| 45 | +} |
| 46 | + |
| 47 | +function ensureArray<T>(value: T | T[] | undefined | null): T[] { |
| 48 | + if (value == null) return []; |
| 49 | + return Array.isArray(value) ? value : [value]; |
| 50 | +} |
| 51 | + |
| 52 | +export function parseCamt052(xml: string): Statement[] { |
| 53 | + if (!xml?.trim()) return []; |
| 54 | + |
| 55 | + const parser = new XMLParser(parserOptions); |
| 56 | + const parsed = parser.parse(xml); |
| 57 | + const doc = parsed.Document || parsed; |
| 58 | + const report = doc.BkToCstmrAcctRpt || doc; |
| 59 | + const reports: Record<string, unknown>[] = ensureArray(report.Rpt); |
| 60 | + |
| 61 | + if (reports.length === 0) return []; |
| 62 | + |
| 63 | + return reports.map((rpt) => { |
| 64 | + const accountIdentification = getStr(rpt, "Acct.Id.IBAN") || getStr(rpt, "Acct.Id.Id") || ""; |
| 65 | + |
| 66 | + const balances: Record<string, unknown>[] = ensureArray(rpt.Bal as Record<string, unknown>[]); |
| 67 | + const openingBal = balances.find((b) => /OPBD|PRCD/.test(getStr(b, "Tp.CdOrPrtry.Cd") || "")); |
| 68 | + const closingBal = balances.find((b) => /CLBD|CLAV/.test(getStr(b, "Tp.CdOrPrtry.Cd") || "")); |
| 69 | + |
| 70 | + const toBalance = (bal: Record<string, unknown> | undefined) => { |
| 71 | + if (!bal) return undefined; |
| 72 | + const { value, currency } = parseAmount(getVal(bal, "Amt")); |
| 73 | + const dateStr = getStr(bal, "Dt.Dt") || getStr(bal, "Dt.DtTm"); |
| 74 | + return { date: parseDate(dateStr), currency, value }; |
| 75 | + }; |
| 76 | + |
| 77 | + const entries: Record<string, unknown>[] = ensureArray(rpt.Ntry as Record<string, unknown>[]); |
| 78 | + |
| 79 | + const transactions: Transaction[] = entries.map((ntry) => { |
| 80 | + const { value: amount, currency } = parseAmount(getVal(ntry, "Amt")); |
| 81 | + const cdtDbtInd = (getStr(ntry, "CdtDbtInd") || "").trim().toUpperCase(); |
| 82 | + const isCredit = cdtDbtInd === "CRDT"; |
| 83 | + const isReversal = (getStr(ntry, "RvslInd") || "").trim().toLowerCase() === "true"; |
| 84 | + |
| 85 | + const date = parseDate(getStr(ntry, "BookgDt.Dt") || getStr(ntry, "BookgDt.DtTm")); |
| 86 | + const valueDate = parseDate(getStr(ntry, "ValDt.Dt") || getStr(ntry, "ValDt.DtTm")); |
| 87 | + const acctSvcrRef = getStr(ntry, "AcctSvcrRef") || ""; |
| 88 | + |
| 89 | + const ntryDtls = getVal(ntry, "NtryDtls") as Record<string, unknown> | undefined; |
| 90 | + const txDtlsList: Record<string, unknown>[] = ntryDtls |
| 91 | + ? ensureArray(ntryDtls.TxDtls as Record<string, unknown>[]) |
| 92 | + : []; |
| 93 | + const txDtls = txDtlsList[0] as Record<string, unknown> | undefined; |
| 94 | + |
| 95 | + const debtorName = |
| 96 | + getStr(txDtls, "RltdPties.Dbtr.Pty.Nm") || |
| 97 | + getStr(txDtls, "RltdPties.Dbtr.Nm") || |
| 98 | + getStr(txDtls, "Dbtr.Nm"); |
| 99 | + const creditorName = |
| 100 | + getStr(txDtls, "RltdPties.Cdtr.Pty.Nm") || |
| 101 | + getStr(txDtls, "RltdPties.Cdtr.Nm") || |
| 102 | + getStr(txDtls, "Cdtr.Nm"); |
| 103 | + const name = (isCredit ? debtorName : creditorName) || ""; |
| 104 | + |
| 105 | + const dbtrIban = getStr(txDtls, "RltdPties.DbtrAcct.Id.IBAN") || getStr(txDtls, "DbtrAcct.Id.IBAN"); |
| 106 | + const cdtrIban = getStr(txDtls, "RltdPties.CdtrAcct.Id.IBAN") || getStr(txDtls, "CdtrAcct.Id.IBAN"); |
| 107 | + const counterpartyIban = (isCredit ? dbtrIban : cdtrIban) || ""; |
| 108 | + |
| 109 | + const dbtrBic = |
| 110 | + getStr(txDtls, "RltdAgts.DbtrAgt.FinInstnId.BICFI") || |
| 111 | + getStr(txDtls, "RltdAgts.DbtrAgt.FinInstnId.BIC") || |
| 112 | + getStr(txDtls, "DbtrAgt.FinInstnId.BICFI") || |
| 113 | + getStr(txDtls, "DbtrAgt.FinInstnId.BIC"); |
| 114 | + const cdtrBic = |
| 115 | + getStr(txDtls, "RltdAgts.CdtrAgt.FinInstnId.BICFI") || |
| 116 | + getStr(txDtls, "RltdAgts.CdtrAgt.FinInstnId.BIC") || |
| 117 | + getStr(txDtls, "CdtrAgt.FinInstnId.BICFI") || |
| 118 | + getStr(txDtls, "CdtrAgt.FinInstnId.BIC"); |
| 119 | + const counterpartyBic = (isCredit ? dbtrBic : cdtrBic) || ""; |
| 120 | + |
| 121 | + const rmtInf = getVal(txDtls, "RmtInf") as Record<string, unknown> | undefined; |
| 122 | + const ustrdArr: string[] = rmtInf ? ensureArray(rmtInf.Ustrd as string[]).map(String) : []; |
| 123 | + const ustrd = ustrdArr.join(" "); |
| 124 | + const addtlNtryInf = getStr(ntry, "AddtlNtryInf") || getStr(ntry, "AddtlTxInf") || ""; |
| 125 | + const description = ustrd || addtlNtryInf; |
| 126 | + |
| 127 | + const endToEndId = getStr(txDtls, "Refs.EndToEndId") || ""; |
| 128 | + const mandateId = getStr(txDtls, "Refs.MndtId") || ""; |
| 129 | + const creditorId = |
| 130 | + getStr(txDtls, "RltdPties.Cdtr.Pty.Id.PrvtId.Othr.Id") || |
| 131 | + getStr(txDtls, "RltdPties.Cdtr.Id.PrvtId.Othr.Id") || |
| 132 | + ""; |
| 133 | + |
| 134 | + const descriptionStructured: Partial<StructuredDescription> = { |
| 135 | + name, |
| 136 | + text: description, |
| 137 | + iban: counterpartyIban, |
| 138 | + bic: counterpartyBic, |
| 139 | + primaNota: "", |
| 140 | + reference: { |
| 141 | + raw: description, |
| 142 | + endToEndRef: endToEndId || undefined, |
| 143 | + mandateRef: mandateId || undefined, |
| 144 | + creditorId: creditorId || undefined, |
| 145 | + }, |
| 146 | + }; |
| 147 | + |
| 148 | + return { |
| 149 | + id: acctSvcrRef, |
| 150 | + date, |
| 151 | + valueDate, |
| 152 | + amount, |
| 153 | + isCredit, |
| 154 | + isExpense: !isCredit, |
| 155 | + isReversal, |
| 156 | + currency, |
| 157 | + name, |
| 158 | + description, |
| 159 | + bankReference: description, |
| 160 | + descriptionStructured, |
| 161 | + details: [name, description, endToEndId, mandateId].filter(Boolean).join("\n"), |
| 162 | + } as unknown as Transaction; |
| 163 | + }); |
| 164 | + |
| 165 | + return { |
| 166 | + accountIdentification, |
| 167 | + openingBalance: toBalance(openingBal), |
| 168 | + closingBalance: toBalance(closingBal), |
| 169 | + transactions, |
| 170 | + } as unknown as Statement; |
| 171 | + }); |
| 172 | +} |
0 commit comments