|
1 | 1 | import { Statement, Transaction, StructuredDescription } from "./types"; |
2 | | - |
3 | | -function getTag(xml: string, tag: string): string | undefined { |
4 | | - const match = xml.match(new RegExp(`<(?:[^:>]+:)?${tag}[^>]*>([\\s\\S]*?)<\\/(?:[^:>]+:)?${tag}>`, "i")); |
5 | | - return match?.[1]?.trim(); |
6 | | -} |
7 | | - |
8 | | -function getAllTags(xml: string, tag: string): string[] { |
9 | | - const results: string[] = []; |
10 | | - const regex = new RegExp(`<(?:[^:>]+:)?${tag}[^>]*>([\\s\\S]*?)<\\/(?:[^:>]+:)?${tag}>`, "gi"); |
11 | | - let match: RegExpExecArray | null; |
12 | | - while ((match = regex.exec(xml)) !== null) { |
13 | | - results.push(match[1].trim()); |
| 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]; |
14 | 20 | } |
15 | | - return results; |
| 21 | + return cur; |
16 | 22 | } |
17 | 23 |
|
18 | | -function getAttr(xml: string, tag: string, attr: string): string | undefined { |
19 | | - const match = xml.match(new RegExp(`<(?:[^:>]+:)?${tag}[^>]*\\s${attr}="([^"]*)"`, "i")); |
20 | | - return match?.[1]; |
| 24 | +function getStr(obj: unknown, path: string): string | undefined { |
| 25 | + const v = getVal(obj, path); |
| 26 | + return v == null ? undefined : String(v); |
21 | 27 | } |
22 | 28 |
|
23 | 29 | function parseDate(dateStr: string | undefined): Date { |
24 | 30 | if (!dateStr) return new Date(0); |
25 | | - return new Date(dateStr.trim()); |
| 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]; |
26 | 50 | } |
27 | 51 |
|
28 | | -/** |
29 | | - * Parse CAMT.052 XML into Statement objects compatible with the mt940-js Statement interface. |
30 | | - */ |
31 | 52 | export function parseCamt052(xml: string): Statement[] { |
32 | | - const reports = getAllTags(xml, "Rpt"); |
| 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 | + |
33 | 61 | if (reports.length === 0) return []; |
34 | 62 |
|
35 | 63 | return reports.map((rpt) => { |
36 | | - const ibanTag = getTag(rpt, "IBAN"); |
37 | | - const accountIdentification = ibanTag || getTag(rpt, "Id") || ""; |
38 | | - |
39 | | - const openingBalanceXml = getAllTags(rpt, "Bal").find( |
40 | | - (b) => getTag(b, "Tp") && /OPBD|PRCD/.test(getTag(b, "Cd") || ""), |
41 | | - ); |
42 | | - const closingBalanceXml = getAllTags(rpt, "Bal").find( |
43 | | - (b) => getTag(b, "Tp") && /CLBD|CLAV/.test(getTag(b, "Cd") || ""), |
44 | | - ); |
45 | | - |
46 | | - const openingBalance = openingBalanceXml |
47 | | - ? { |
48 | | - date: parseDate(getTag(openingBalanceXml, "Dt")), |
49 | | - currency: getAttr(openingBalanceXml, "Amt", "Ccy") || "EUR", |
50 | | - value: parseFloat(getTag(openingBalanceXml, "Amt") || "0"), |
51 | | - } |
52 | | - : undefined; |
53 | | - |
54 | | - const closingBalance = closingBalanceXml |
55 | | - ? { |
56 | | - date: parseDate(getTag(closingBalanceXml, "Dt")), |
57 | | - currency: getAttr(closingBalanceXml, "Amt", "Ccy") || "EUR", |
58 | | - value: parseFloat(getTag(closingBalanceXml, "Amt") || "0"), |
59 | | - } |
60 | | - : undefined; |
61 | | - |
62 | | - const ntryElements = getAllTags(rpt, "Ntry"); |
63 | | - const transactions: Transaction[] = ntryElements.map((ntry) => { |
64 | | - const amtStr = getTag(ntry, "Amt"); |
65 | | - const currency = getAttr(ntry, "Amt", "Ccy") || "EUR"; |
66 | | - const amount = parseFloat(amtStr || "0"); |
67 | | - const cdtDbtInd = getTag(ntry, "CdtDbtInd") || ""; |
68 | | - const isCredit = cdtDbtInd.trim().toUpperCase() === "CRDT"; |
69 | | - const isReversal = (getTag(ntry, "RvslInd") || "").trim().toLowerCase() === "true"; |
70 | | - |
71 | | - const bookgDt = getTag(ntry, "BookgDt"); |
72 | | - const valDt = getTag(ntry, "ValDt"); |
73 | | - const date = parseDate(getTag(bookgDt || "", "Dt") || bookgDt); |
74 | | - const valueDate = parseDate(getTag(valDt || "", "Dt") || valDt); |
75 | | - |
76 | | - const acctSvcrRef = getTag(ntry, "AcctSvcrRef") || ""; |
77 | | - const txDtls = getTag(ntry, "TxDtls") || ntry; |
78 | | - |
79 | | - const debtorName = getTag(getTag(txDtls, "Dbtr") || "", "Nm"); |
80 | | - const creditorName = getTag(getTag(txDtls, "Cdtr") || "", "Nm"); |
| 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"); |
81 | 103 | const name = (isCredit ? debtorName : creditorName) || ""; |
82 | 104 |
|
83 | | - const dbtrIban = getTag(getTag(txDtls, "DbtrAcct") || "", "IBAN"); |
84 | | - const cdtrIban = getTag(getTag(txDtls, "CdtrAcct") || "", "IBAN"); |
| 105 | + const dbtrIban = |
| 106 | + getStr(txDtls, "RltdPties.DbtrAcct.Id.IBAN") || getStr(txDtls, "DbtrAcct.Id.IBAN"); |
| 107 | + const cdtrIban = |
| 108 | + getStr(txDtls, "RltdPties.CdtrAcct.Id.IBAN") || getStr(txDtls, "CdtrAcct.Id.IBAN"); |
85 | 109 | const counterpartyIban = (isCredit ? dbtrIban : cdtrIban) || ""; |
86 | 110 |
|
87 | | - const dbtrBic = getTag(getTag(txDtls, "DbtrAgt") || "", "BICFI") || getTag(getTag(txDtls, "DbtrAgt") || "", "BIC"); |
88 | | - const cdtrBic = getTag(getTag(txDtls, "CdtrAgt") || "", "BICFI") || getTag(getTag(txDtls, "CdtrAgt") || "", "BIC"); |
| 111 | + const dbtrBic = |
| 112 | + getStr(txDtls, "RltdAgts.DbtrAgt.FinInstnId.BICFI") || |
| 113 | + getStr(txDtls, "RltdAgts.DbtrAgt.FinInstnId.BIC") || |
| 114 | + getStr(txDtls, "DbtrAgt.FinInstnId.BICFI") || |
| 115 | + getStr(txDtls, "DbtrAgt.FinInstnId.BIC"); |
| 116 | + const cdtrBic = |
| 117 | + getStr(txDtls, "RltdAgts.CdtrAgt.FinInstnId.BICFI") || |
| 118 | + getStr(txDtls, "RltdAgts.CdtrAgt.FinInstnId.BIC") || |
| 119 | + getStr(txDtls, "CdtrAgt.FinInstnId.BICFI") || |
| 120 | + getStr(txDtls, "CdtrAgt.FinInstnId.BIC"); |
89 | 121 | const counterpartyBic = (isCredit ? dbtrBic : cdtrBic) || ""; |
90 | 122 |
|
91 | | - const ustrd = getAllTags(txDtls, "Ustrd").join(" "); |
92 | | - const addtlNtryInf = getTag(ntry, "AddtlNtryInf") || getTag(ntry, "AddtlTxInf") || ""; |
| 123 | + const rmtInf = getVal(txDtls, "RmtInf") as Record<string, unknown> | undefined; |
| 124 | + const ustrdArr: string[] = rmtInf ? ensureArray(rmtInf.Ustrd as string[]).map(String) : []; |
| 125 | + const ustrd = ustrdArr.join(" "); |
| 126 | + const addtlNtryInf = getStr(ntry, "AddtlNtryInf") || getStr(ntry, "AddtlTxInf") || ""; |
93 | 127 | const description = ustrd || addtlNtryInf; |
94 | 128 |
|
95 | | - const endToEndId = getTag(txDtls, "EndToEndId") || ""; |
96 | | - const mandateId = getTag(txDtls, "MndtId") || ""; |
97 | | - const creditorId = getTag(txDtls, "CdtrId") || ""; |
| 129 | + const endToEndId = getStr(txDtls, "Refs.EndToEndId") || ""; |
| 130 | + const mandateId = getStr(txDtls, "Refs.MndtId") || ""; |
| 131 | + const creditorId = |
| 132 | + getStr(txDtls, "RltdPties.Cdtr.Pty.Id.PrvtId.Othr.Id") || |
| 133 | + getStr(txDtls, "RltdPties.Cdtr.Id.PrvtId.Othr.Id") || |
| 134 | + ""; |
98 | 135 |
|
99 | 136 | const descriptionStructured: Partial<StructuredDescription> = { |
100 | 137 | name, |
@@ -129,8 +166,8 @@ export function parseCamt052(xml: string): Statement[] { |
129 | 166 |
|
130 | 167 | return { |
131 | 168 | accountIdentification, |
132 | | - openingBalance, |
133 | | - closingBalance, |
| 169 | + openingBalance: toBalance(openingBal), |
| 170 | + closingBalance: toBalance(closingBal), |
134 | 171 | transactions, |
135 | 172 | } as unknown as Statement; |
136 | 173 | }); |
|
0 commit comments