@f-o-t/ofx 2.4.2 → 2.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/extractors.d.ts +10 -0
- package/dist/extractors.d.ts.map +1 -0
- package/dist/generator.d.ts +147 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1260 -0
- package/dist/index.js.map +15 -0
- package/dist/parser.d.ts +26 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/schemas.d.ts +1626 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/stream.d.ts +106 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/utils.d.ts +18 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +34 -33
package/dist/index.js
ADDED
|
@@ -0,0 +1,1260 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/utils.ts
|
|
3
|
+
var toArray = (value) => Array.isArray(value) ? value : [value];
|
|
4
|
+
var ENTITY_MAP = {
|
|
5
|
+
"&": "&",
|
|
6
|
+
"'": "'",
|
|
7
|
+
">": ">",
|
|
8
|
+
"<": "<",
|
|
9
|
+
""": '"'
|
|
10
|
+
};
|
|
11
|
+
var ENTITY_REGEX = /&(?:amp|lt|gt|quot|apos);/g;
|
|
12
|
+
function decodeEntities(text) {
|
|
13
|
+
if (!text.includes("&"))
|
|
14
|
+
return text;
|
|
15
|
+
return text.replace(ENTITY_REGEX, (match) => ENTITY_MAP[match] ?? match);
|
|
16
|
+
}
|
|
17
|
+
var pad = (n, width = 2) => n.toString().padStart(width, "0");
|
|
18
|
+
function escapeOfxText(text) {
|
|
19
|
+
if (!text.includes("&") && !text.includes("<") && !text.includes(">")) {
|
|
20
|
+
return text;
|
|
21
|
+
}
|
|
22
|
+
return text.replace(/[&<>]/g, (c) => c === "&" ? "&" : c === "<" ? "<" : ">");
|
|
23
|
+
}
|
|
24
|
+
function formatAmount(amount) {
|
|
25
|
+
const rounded = Math.round(amount * 100) / 100;
|
|
26
|
+
const [intPart, decPart = ""] = rounded.toString().split(".");
|
|
27
|
+
return `${intPart}.${decPart.padEnd(2, "0").slice(0, 2)}`;
|
|
28
|
+
}
|
|
29
|
+
function formatOfxDate(date, timezone) {
|
|
30
|
+
const tz = timezone ?? { name: "GMT", offset: 0 };
|
|
31
|
+
const offsetMs = tz.offset * 60 * 60 * 1000;
|
|
32
|
+
const adjustedDate = new Date(date.getTime() + offsetMs);
|
|
33
|
+
const year = adjustedDate.getUTCFullYear();
|
|
34
|
+
const month = pad(adjustedDate.getUTCMonth() + 1);
|
|
35
|
+
const day = pad(adjustedDate.getUTCDate());
|
|
36
|
+
const hour = pad(adjustedDate.getUTCHours());
|
|
37
|
+
const minute = pad(adjustedDate.getUTCMinutes());
|
|
38
|
+
const second = pad(adjustedDate.getUTCSeconds());
|
|
39
|
+
const sign = tz.offset >= 0 ? "+" : "";
|
|
40
|
+
return `${year}${month}${day}${hour}${minute}${second}[${sign}${tz.offset}:${tz.name}]`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/extractors.ts
|
|
44
|
+
function getTransactions(document) {
|
|
45
|
+
const results = [];
|
|
46
|
+
const bankResponse = document.OFX.BANKMSGSRSV1?.STMTTRNRS;
|
|
47
|
+
if (bankResponse) {
|
|
48
|
+
for (const r of toArray(bankResponse)) {
|
|
49
|
+
const txns = r.STMTRS?.BANKTRANLIST?.STMTTRN;
|
|
50
|
+
if (txns)
|
|
51
|
+
results.push(...txns);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const ccResponse = document.OFX.CREDITCARDMSGSRSV1?.CCSTMTTRNRS;
|
|
55
|
+
if (ccResponse) {
|
|
56
|
+
for (const r of toArray(ccResponse)) {
|
|
57
|
+
const txns = r.CCSTMTRS?.BANKTRANLIST?.STMTTRN;
|
|
58
|
+
if (txns)
|
|
59
|
+
results.push(...txns);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return results;
|
|
63
|
+
}
|
|
64
|
+
function getAccountInfo(document) {
|
|
65
|
+
const results = [];
|
|
66
|
+
const bankResponse = document.OFX.BANKMSGSRSV1?.STMTTRNRS;
|
|
67
|
+
if (bankResponse) {
|
|
68
|
+
for (const r of toArray(bankResponse)) {
|
|
69
|
+
const account = r.STMTRS?.BANKACCTFROM;
|
|
70
|
+
if (account)
|
|
71
|
+
results.push(account);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const ccResponse = document.OFX.CREDITCARDMSGSRSV1?.CCSTMTTRNRS;
|
|
75
|
+
if (ccResponse) {
|
|
76
|
+
for (const r of toArray(ccResponse)) {
|
|
77
|
+
const account = r.CCSTMTRS?.CCACCTFROM;
|
|
78
|
+
if (account)
|
|
79
|
+
results.push(account);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
84
|
+
function getBalance(document) {
|
|
85
|
+
const results = [];
|
|
86
|
+
const bankResponse = document.OFX.BANKMSGSRSV1?.STMTTRNRS;
|
|
87
|
+
if (bankResponse) {
|
|
88
|
+
for (const r of toArray(bankResponse)) {
|
|
89
|
+
if (r.STMTRS) {
|
|
90
|
+
results.push({
|
|
91
|
+
available: r.STMTRS.AVAILBAL,
|
|
92
|
+
ledger: r.STMTRS.LEDGERBAL
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const ccResponse = document.OFX.CREDITCARDMSGSRSV1?.CCSTMTTRNRS;
|
|
98
|
+
if (ccResponse) {
|
|
99
|
+
for (const r of toArray(ccResponse)) {
|
|
100
|
+
if (r.CCSTMTRS) {
|
|
101
|
+
results.push({
|
|
102
|
+
available: r.CCSTMTRS.AVAILBAL,
|
|
103
|
+
ledger: r.CCSTMTRS.LEDGERBAL
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return results;
|
|
109
|
+
}
|
|
110
|
+
function getSignOnInfo(document) {
|
|
111
|
+
return document.OFX.SIGNONMSGSRSV1.SONRS;
|
|
112
|
+
}
|
|
113
|
+
// src/generator.ts
|
|
114
|
+
import { z as z2 } from "zod";
|
|
115
|
+
|
|
116
|
+
// src/schemas.ts
|
|
117
|
+
import { z } from "zod";
|
|
118
|
+
var toFloat = (val) => Number.parseFloat(val);
|
|
119
|
+
var dateComponentsSchema = z.object({
|
|
120
|
+
year: z.number(),
|
|
121
|
+
month: z.number(),
|
|
122
|
+
day: z.number(),
|
|
123
|
+
hour: z.number(),
|
|
124
|
+
minute: z.number(),
|
|
125
|
+
second: z.number()
|
|
126
|
+
});
|
|
127
|
+
var DATE_REGEX = /^(\d{4})(\d{2})(\d{2})(\d{2})?(\d{2})?(\d{2})?/;
|
|
128
|
+
var TIMEZONE_REGEX = /\[([+-]?\d+):(\w+)\]/;
|
|
129
|
+
function parseDateComponents(val) {
|
|
130
|
+
const m = DATE_REGEX.exec(val);
|
|
131
|
+
if (!m)
|
|
132
|
+
return { day: 0, hour: 0, minute: 0, month: 0, second: 0, year: 0 };
|
|
133
|
+
return {
|
|
134
|
+
day: +m[3],
|
|
135
|
+
hour: +(m[4] || 0),
|
|
136
|
+
minute: +(m[5] || 0),
|
|
137
|
+
month: +m[2],
|
|
138
|
+
second: +(m[6] || 0),
|
|
139
|
+
year: +m[1]
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function parseTimezone(val) {
|
|
143
|
+
const match = TIMEZONE_REGEX.exec(val);
|
|
144
|
+
return {
|
|
145
|
+
name: match?.[2] ?? "UTC",
|
|
146
|
+
offset: match ? +match[1] : 0
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
var ofxDateSchema = z.string().transform((val) => {
|
|
150
|
+
const components = parseDateComponents(val);
|
|
151
|
+
const timezone = parseTimezone(val);
|
|
152
|
+
return {
|
|
153
|
+
...components,
|
|
154
|
+
raw: val,
|
|
155
|
+
timezone,
|
|
156
|
+
toDate() {
|
|
157
|
+
const offsetMs = timezone.offset * 60 * 60 * 1000;
|
|
158
|
+
return new Date(Date.UTC(components.year, components.month - 1, components.day, components.hour, components.minute, components.second) - offsetMs);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
var statusSchema = z.object({
|
|
163
|
+
CODE: z.string(),
|
|
164
|
+
MESSAGE: z.string().optional(),
|
|
165
|
+
SEVERITY: z.enum(["INFO", "WARN", "ERROR"])
|
|
166
|
+
});
|
|
167
|
+
var financialInstitutionSchema = z.object({
|
|
168
|
+
FID: z.string().optional(),
|
|
169
|
+
ORG: z.string().optional()
|
|
170
|
+
});
|
|
171
|
+
var transactionTypeSchema = z.enum([
|
|
172
|
+
"CREDIT",
|
|
173
|
+
"DEBIT",
|
|
174
|
+
"INT",
|
|
175
|
+
"DIV",
|
|
176
|
+
"FEE",
|
|
177
|
+
"SRVCHG",
|
|
178
|
+
"DEP",
|
|
179
|
+
"ATM",
|
|
180
|
+
"POS",
|
|
181
|
+
"XFER",
|
|
182
|
+
"CHECK",
|
|
183
|
+
"PAYMENT",
|
|
184
|
+
"CASH",
|
|
185
|
+
"DIRECTDEP",
|
|
186
|
+
"DIRECTDEBIT",
|
|
187
|
+
"REPEATPMT",
|
|
188
|
+
"HOLD",
|
|
189
|
+
"OTHER"
|
|
190
|
+
]);
|
|
191
|
+
var transactionSchema = z.object({
|
|
192
|
+
CHECKNUM: z.string().optional(),
|
|
193
|
+
CORRECTACTION: z.enum(["DELETE", "REPLACE"]).optional(),
|
|
194
|
+
CORRECTFITID: z.string().optional(),
|
|
195
|
+
CURRENCY: z.string().optional(),
|
|
196
|
+
DTAVAIL: ofxDateSchema.optional(),
|
|
197
|
+
DTPOSTED: ofxDateSchema,
|
|
198
|
+
DTUSER: ofxDateSchema.optional(),
|
|
199
|
+
FITID: z.string().optional(),
|
|
200
|
+
MEMO: z.string().optional(),
|
|
201
|
+
NAME: z.string().optional(),
|
|
202
|
+
PAYEEID: z.string().optional(),
|
|
203
|
+
REFNUM: z.string().optional(),
|
|
204
|
+
SIC: z.string().optional(),
|
|
205
|
+
SRVRTID: z.string().optional(),
|
|
206
|
+
TRNAMT: z.string().transform(toFloat),
|
|
207
|
+
TRNTYPE: transactionTypeSchema
|
|
208
|
+
});
|
|
209
|
+
var accountTypeSchema = z.enum([
|
|
210
|
+
"CHECKING",
|
|
211
|
+
"SAVINGS",
|
|
212
|
+
"MONEYMRKT",
|
|
213
|
+
"CREDITLINE",
|
|
214
|
+
"CD"
|
|
215
|
+
]);
|
|
216
|
+
var extendedAccountTypeSchema = z.enum([
|
|
217
|
+
"CHECKING",
|
|
218
|
+
"SAVINGS",
|
|
219
|
+
"MONEYMRKT",
|
|
220
|
+
"CREDITLINE",
|
|
221
|
+
"CD",
|
|
222
|
+
"CREDITCARD"
|
|
223
|
+
]);
|
|
224
|
+
var bankAccountSchema = z.object({
|
|
225
|
+
ACCTID: z.string(),
|
|
226
|
+
ACCTKEY: z.string().optional(),
|
|
227
|
+
ACCTTYPE: accountTypeSchema,
|
|
228
|
+
BANKID: z.string(),
|
|
229
|
+
BRANCHID: z.string().optional()
|
|
230
|
+
});
|
|
231
|
+
var flexibleBankAccountSchema = z.object({
|
|
232
|
+
ACCTID: z.string(),
|
|
233
|
+
ACCTKEY: z.string().optional(),
|
|
234
|
+
ACCTTYPE: extendedAccountTypeSchema.optional(),
|
|
235
|
+
BANKID: z.string().optional(),
|
|
236
|
+
BRANCHID: z.string().optional()
|
|
237
|
+
});
|
|
238
|
+
var creditCardAccountSchema = z.object({
|
|
239
|
+
ACCTID: z.string(),
|
|
240
|
+
ACCTKEY: z.string().optional()
|
|
241
|
+
});
|
|
242
|
+
var balanceSchema = z.object({
|
|
243
|
+
BALAMT: z.string().transform(toFloat),
|
|
244
|
+
DTASOF: ofxDateSchema
|
|
245
|
+
});
|
|
246
|
+
var transactionListSchema = z.object({
|
|
247
|
+
DTEND: ofxDateSchema,
|
|
248
|
+
DTSTART: ofxDateSchema,
|
|
249
|
+
STMTTRN: z.array(transactionSchema).default([])
|
|
250
|
+
});
|
|
251
|
+
var bankStatementResponseSchema = z.object({
|
|
252
|
+
AVAILBAL: balanceSchema.optional(),
|
|
253
|
+
BANKACCTFROM: bankAccountSchema,
|
|
254
|
+
BANKTRANLIST: transactionListSchema.optional(),
|
|
255
|
+
CURDEF: z.string().default("USD"),
|
|
256
|
+
LEDGERBAL: balanceSchema.optional(),
|
|
257
|
+
MKTGINFO: z.string().optional()
|
|
258
|
+
});
|
|
259
|
+
var creditCardStatementResponseSchema = z.object({
|
|
260
|
+
AVAILBAL: balanceSchema.optional(),
|
|
261
|
+
BANKACCTFROM: flexibleBankAccountSchema.optional(),
|
|
262
|
+
BANKTRANLIST: transactionListSchema.optional(),
|
|
263
|
+
CCACCTFROM: creditCardAccountSchema.optional(),
|
|
264
|
+
CURDEF: z.string().default("USD"),
|
|
265
|
+
LEDGERBAL: balanceSchema.optional(),
|
|
266
|
+
MKTGINFO: z.string().optional()
|
|
267
|
+
}).refine((data) => data.CCACCTFROM || data.BANKACCTFROM, {
|
|
268
|
+
message: "Either CCACCTFROM or BANKACCTFROM is required"
|
|
269
|
+
});
|
|
270
|
+
var signOnResponseSchema = z.object({
|
|
271
|
+
ACCESSKEY: z.string().optional(),
|
|
272
|
+
DTSERVER: ofxDateSchema,
|
|
273
|
+
FI: financialInstitutionSchema.optional(),
|
|
274
|
+
LANGUAGE: z.string().default("ENG"),
|
|
275
|
+
SESSCOOKIE: z.string().optional(),
|
|
276
|
+
STATUS: statusSchema
|
|
277
|
+
});
|
|
278
|
+
var bankStatementTransactionResponseSchema = z.object({
|
|
279
|
+
STATUS: statusSchema.optional(),
|
|
280
|
+
STMTRS: bankStatementResponseSchema.optional(),
|
|
281
|
+
TRNUID: z.string().optional()
|
|
282
|
+
});
|
|
283
|
+
var creditCardStatementTransactionResponseSchema = z.object({
|
|
284
|
+
CCSTMTRS: creditCardStatementResponseSchema.optional(),
|
|
285
|
+
STATUS: statusSchema.optional(),
|
|
286
|
+
TRNUID: z.string().optional()
|
|
287
|
+
});
|
|
288
|
+
var singleOrArray = (schema) => z.union([schema, z.array(schema)]).optional();
|
|
289
|
+
var bankMessageSetResponseSchema = z.object({
|
|
290
|
+
STMTTRNRS: singleOrArray(bankStatementTransactionResponseSchema)
|
|
291
|
+
});
|
|
292
|
+
var creditCardMessageSetResponseSchema = z.object({
|
|
293
|
+
CCSTMTTRNRS: singleOrArray(creditCardStatementTransactionResponseSchema)
|
|
294
|
+
});
|
|
295
|
+
var signOnMessageSetResponseSchema = z.object({
|
|
296
|
+
SONRS: signOnResponseSchema
|
|
297
|
+
});
|
|
298
|
+
var ofxResponseSchema = z.object({
|
|
299
|
+
BANKMSGSRSV1: bankMessageSetResponseSchema.optional(),
|
|
300
|
+
CREDITCARDMSGSRSV1: creditCardMessageSetResponseSchema.optional(),
|
|
301
|
+
SIGNONMSGSRSV1: signOnMessageSetResponseSchema
|
|
302
|
+
});
|
|
303
|
+
var ofxHeaderSchema = z.object({
|
|
304
|
+
CHARSET: z.string().optional(),
|
|
305
|
+
COMPRESSION: z.string().optional(),
|
|
306
|
+
DATA: z.string().optional(),
|
|
307
|
+
ENCODING: z.string().optional(),
|
|
308
|
+
NEWFILEUID: z.string().optional(),
|
|
309
|
+
OFXHEADER: z.string().optional(),
|
|
310
|
+
OLDFILEUID: z.string().optional(),
|
|
311
|
+
SECURITY: z.string().optional(),
|
|
312
|
+
VERSION: z.string().optional()
|
|
313
|
+
});
|
|
314
|
+
var ofxDocumentSchema = z.object({
|
|
315
|
+
header: ofxHeaderSchema,
|
|
316
|
+
OFX: ofxResponseSchema
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// src/generator.ts
|
|
320
|
+
var generateHeaderOptionsSchema = z2.object({
|
|
321
|
+
version: z2.string().optional(),
|
|
322
|
+
encoding: z2.string().optional(),
|
|
323
|
+
charset: z2.string().optional()
|
|
324
|
+
}).optional();
|
|
325
|
+
function generateHeader(options) {
|
|
326
|
+
const version = options?.version ?? "100";
|
|
327
|
+
const encoding = options?.encoding ?? "USASCII";
|
|
328
|
+
const charset = options?.charset ?? "1252";
|
|
329
|
+
return [
|
|
330
|
+
"OFXHEADER:100",
|
|
331
|
+
"DATA:OFXSGML",
|
|
332
|
+
`VERSION:${version}`,
|
|
333
|
+
"SECURITY:NONE",
|
|
334
|
+
`ENCODING:${encoding}`,
|
|
335
|
+
`CHARSET:${charset}`,
|
|
336
|
+
"COMPRESSION:NONE",
|
|
337
|
+
"OLDFILEUID:NONE",
|
|
338
|
+
"NEWFILEUID:NONE",
|
|
339
|
+
""
|
|
340
|
+
].join(`
|
|
341
|
+
`);
|
|
342
|
+
}
|
|
343
|
+
var generateTransactionInputSchema = z2.object({
|
|
344
|
+
type: transactionTypeSchema,
|
|
345
|
+
datePosted: z2.date(),
|
|
346
|
+
amount: z2.number(),
|
|
347
|
+
fitId: z2.string().min(1),
|
|
348
|
+
name: z2.string().optional(),
|
|
349
|
+
memo: z2.string().optional(),
|
|
350
|
+
checkNum: z2.string().optional(),
|
|
351
|
+
refNum: z2.string().optional()
|
|
352
|
+
});
|
|
353
|
+
function generateTransaction(trn) {
|
|
354
|
+
const lines = [
|
|
355
|
+
"<STMTTRN>",
|
|
356
|
+
`<TRNTYPE>${trn.type}`,
|
|
357
|
+
`<DTPOSTED>${formatOfxDate(trn.datePosted)}`,
|
|
358
|
+
`<TRNAMT>${formatAmount(trn.amount)}`,
|
|
359
|
+
`<FITID>${escapeOfxText(trn.fitId)}`
|
|
360
|
+
];
|
|
361
|
+
if (trn.name) {
|
|
362
|
+
lines.push(`<NAME>${escapeOfxText(trn.name)}`);
|
|
363
|
+
}
|
|
364
|
+
if (trn.memo) {
|
|
365
|
+
lines.push(`<MEMO>${escapeOfxText(trn.memo)}`);
|
|
366
|
+
}
|
|
367
|
+
if (trn.checkNum) {
|
|
368
|
+
lines.push(`<CHECKNUM>${escapeOfxText(trn.checkNum)}`);
|
|
369
|
+
}
|
|
370
|
+
if (trn.refNum) {
|
|
371
|
+
lines.push(`<REFNUM>${escapeOfxText(trn.refNum)}`);
|
|
372
|
+
}
|
|
373
|
+
lines.push("</STMTTRN>");
|
|
374
|
+
return lines.join(`
|
|
375
|
+
`);
|
|
376
|
+
}
|
|
377
|
+
var balanceSchema2 = z2.object({
|
|
378
|
+
amount: z2.number(),
|
|
379
|
+
asOfDate: z2.date()
|
|
380
|
+
});
|
|
381
|
+
var financialInstitutionSchema2 = z2.object({
|
|
382
|
+
org: z2.string().optional(),
|
|
383
|
+
fid: z2.string().optional()
|
|
384
|
+
});
|
|
385
|
+
var generateBankStatementOptionsSchema = z2.object({
|
|
386
|
+
bankId: z2.string().min(1),
|
|
387
|
+
accountId: z2.string().min(1),
|
|
388
|
+
accountType: accountTypeSchema,
|
|
389
|
+
currency: z2.string().min(1),
|
|
390
|
+
startDate: z2.date(),
|
|
391
|
+
endDate: z2.date(),
|
|
392
|
+
transactions: z2.array(generateTransactionInputSchema),
|
|
393
|
+
ledgerBalance: balanceSchema2.optional(),
|
|
394
|
+
availableBalance: balanceSchema2.optional(),
|
|
395
|
+
financialInstitution: financialInstitutionSchema2.optional(),
|
|
396
|
+
language: z2.string().optional()
|
|
397
|
+
});
|
|
398
|
+
function generateBankStatement(options) {
|
|
399
|
+
const parts = [generateHeader()];
|
|
400
|
+
const serverDate = formatOfxDate(new Date);
|
|
401
|
+
const language = options.language ?? "POR";
|
|
402
|
+
parts.push(`<OFX>
|
|
403
|
+
<SIGNONMSGSRSV1>
|
|
404
|
+
<SONRS>
|
|
405
|
+
<STATUS>
|
|
406
|
+
<CODE>0
|
|
407
|
+
<SEVERITY>INFO
|
|
408
|
+
</STATUS>
|
|
409
|
+
<DTSERVER>${serverDate}
|
|
410
|
+
<LANGUAGE>${language}`);
|
|
411
|
+
if (options.financialInstitution) {
|
|
412
|
+
parts.push("<FI>");
|
|
413
|
+
if (options.financialInstitution.org) {
|
|
414
|
+
parts.push(`<ORG>${escapeOfxText(options.financialInstitution.org)}`);
|
|
415
|
+
}
|
|
416
|
+
if (options.financialInstitution.fid) {
|
|
417
|
+
parts.push(`<FID>${escapeOfxText(options.financialInstitution.fid)}`);
|
|
418
|
+
}
|
|
419
|
+
parts.push("</FI>");
|
|
420
|
+
}
|
|
421
|
+
parts.push(`</SONRS>
|
|
422
|
+
</SIGNONMSGSRSV1>
|
|
423
|
+
<BANKMSGSRSV1>
|
|
424
|
+
<STMTTRNRS>
|
|
425
|
+
<TRNUID>0
|
|
426
|
+
<STATUS>
|
|
427
|
+
<CODE>0
|
|
428
|
+
<SEVERITY>INFO
|
|
429
|
+
</STATUS>
|
|
430
|
+
<STMTRS>
|
|
431
|
+
<CURDEF>${options.currency}
|
|
432
|
+
<BANKACCTFROM>
|
|
433
|
+
<BANKID>${escapeOfxText(options.bankId)}
|
|
434
|
+
<ACCTID>${escapeOfxText(options.accountId)}
|
|
435
|
+
<ACCTTYPE>${options.accountType}
|
|
436
|
+
</BANKACCTFROM>
|
|
437
|
+
<BANKTRANLIST>
|
|
438
|
+
<DTSTART>${formatOfxDate(options.startDate)}
|
|
439
|
+
<DTEND>${formatOfxDate(options.endDate)}`);
|
|
440
|
+
for (const trn of options.transactions) {
|
|
441
|
+
parts.push(generateTransaction(trn));
|
|
442
|
+
}
|
|
443
|
+
parts.push("</BANKTRANLIST>");
|
|
444
|
+
if (options.ledgerBalance) {
|
|
445
|
+
parts.push(`<LEDGERBAL>
|
|
446
|
+
<BALAMT>${formatAmount(options.ledgerBalance.amount)}
|
|
447
|
+
<DTASOF>${formatOfxDate(options.ledgerBalance.asOfDate)}
|
|
448
|
+
</LEDGERBAL>`);
|
|
449
|
+
}
|
|
450
|
+
if (options.availableBalance) {
|
|
451
|
+
parts.push(`<AVAILBAL>
|
|
452
|
+
<BALAMT>${formatAmount(options.availableBalance.amount)}
|
|
453
|
+
<DTASOF>${formatOfxDate(options.availableBalance.asOfDate)}
|
|
454
|
+
</AVAILBAL>`);
|
|
455
|
+
}
|
|
456
|
+
parts.push(`</STMTRS>
|
|
457
|
+
</STMTTRNRS>
|
|
458
|
+
</BANKMSGSRSV1>
|
|
459
|
+
</OFX>`);
|
|
460
|
+
return parts.join(`
|
|
461
|
+
`);
|
|
462
|
+
}
|
|
463
|
+
var generateCreditCardStatementOptionsSchema = z2.object({
|
|
464
|
+
accountId: z2.string().min(1),
|
|
465
|
+
currency: z2.string().min(1),
|
|
466
|
+
startDate: z2.date(),
|
|
467
|
+
endDate: z2.date(),
|
|
468
|
+
transactions: z2.array(generateTransactionInputSchema),
|
|
469
|
+
ledgerBalance: balanceSchema2.optional(),
|
|
470
|
+
availableBalance: balanceSchema2.optional(),
|
|
471
|
+
financialInstitution: financialInstitutionSchema2.optional(),
|
|
472
|
+
language: z2.string().optional()
|
|
473
|
+
});
|
|
474
|
+
function generateCreditCardStatement(options) {
|
|
475
|
+
const parts = [generateHeader()];
|
|
476
|
+
const serverDate = formatOfxDate(new Date);
|
|
477
|
+
const language = options.language ?? "POR";
|
|
478
|
+
parts.push(`<OFX>
|
|
479
|
+
<SIGNONMSGSRSV1>
|
|
480
|
+
<SONRS>
|
|
481
|
+
<STATUS>
|
|
482
|
+
<CODE>0
|
|
483
|
+
<SEVERITY>INFO
|
|
484
|
+
</STATUS>
|
|
485
|
+
<DTSERVER>${serverDate}
|
|
486
|
+
<LANGUAGE>${language}`);
|
|
487
|
+
if (options.financialInstitution) {
|
|
488
|
+
parts.push("<FI>");
|
|
489
|
+
if (options.financialInstitution.org) {
|
|
490
|
+
parts.push(`<ORG>${escapeOfxText(options.financialInstitution.org)}`);
|
|
491
|
+
}
|
|
492
|
+
if (options.financialInstitution.fid) {
|
|
493
|
+
parts.push(`<FID>${escapeOfxText(options.financialInstitution.fid)}`);
|
|
494
|
+
}
|
|
495
|
+
parts.push("</FI>");
|
|
496
|
+
}
|
|
497
|
+
parts.push(`</SONRS>
|
|
498
|
+
</SIGNONMSGSRSV1>
|
|
499
|
+
<CREDITCARDMSGSRSV1>
|
|
500
|
+
<CCSTMTTRNRS>
|
|
501
|
+
<TRNUID>0
|
|
502
|
+
<STATUS>
|
|
503
|
+
<CODE>0
|
|
504
|
+
<SEVERITY>INFO
|
|
505
|
+
</STATUS>
|
|
506
|
+
<CCSTMTRS>
|
|
507
|
+
<CURDEF>${options.currency}
|
|
508
|
+
<CCACCTFROM>
|
|
509
|
+
<ACCTID>${escapeOfxText(options.accountId)}
|
|
510
|
+
</CCACCTFROM>
|
|
511
|
+
<BANKTRANLIST>
|
|
512
|
+
<DTSTART>${formatOfxDate(options.startDate)}
|
|
513
|
+
<DTEND>${formatOfxDate(options.endDate)}`);
|
|
514
|
+
for (const trn of options.transactions) {
|
|
515
|
+
parts.push(generateTransaction(trn));
|
|
516
|
+
}
|
|
517
|
+
parts.push("</BANKTRANLIST>");
|
|
518
|
+
if (options.ledgerBalance) {
|
|
519
|
+
parts.push(`<LEDGERBAL>
|
|
520
|
+
<BALAMT>${formatAmount(options.ledgerBalance.amount)}
|
|
521
|
+
<DTASOF>${formatOfxDate(options.ledgerBalance.asOfDate)}
|
|
522
|
+
</LEDGERBAL>`);
|
|
523
|
+
}
|
|
524
|
+
if (options.availableBalance) {
|
|
525
|
+
parts.push(`<AVAILBAL>
|
|
526
|
+
<BALAMT>${formatAmount(options.availableBalance.amount)}
|
|
527
|
+
<DTASOF>${formatOfxDate(options.availableBalance.asOfDate)}
|
|
528
|
+
</AVAILBAL>`);
|
|
529
|
+
}
|
|
530
|
+
parts.push(`</CCSTMTRS>
|
|
531
|
+
</CCSTMTTRNRS>
|
|
532
|
+
</CREDITCARDMSGSRSV1>
|
|
533
|
+
</OFX>`);
|
|
534
|
+
return parts.join(`
|
|
535
|
+
`);
|
|
536
|
+
}
|
|
537
|
+
// src/parser.ts
|
|
538
|
+
import { z as z3 } from "zod";
|
|
539
|
+
var CHARSET_MAP = {
|
|
540
|
+
"1252": "windows-1252",
|
|
541
|
+
"WINDOWS-1252": "windows-1252",
|
|
542
|
+
CP1252: "windows-1252",
|
|
543
|
+
"8859-1": "windows-1252",
|
|
544
|
+
"ISO-8859-1": "windows-1252",
|
|
545
|
+
LATIN1: "windows-1252",
|
|
546
|
+
"LATIN-1": "windows-1252",
|
|
547
|
+
"UTF-8": "utf-8",
|
|
548
|
+
UTF8: "utf-8",
|
|
549
|
+
NONE: "utf-8",
|
|
550
|
+
"": "utf-8"
|
|
551
|
+
};
|
|
552
|
+
function getEncodingFromCharset(charset) {
|
|
553
|
+
if (!charset)
|
|
554
|
+
return "utf-8";
|
|
555
|
+
const normalized = charset.toUpperCase().trim();
|
|
556
|
+
return CHARSET_MAP[normalized] ?? "windows-1252";
|
|
557
|
+
}
|
|
558
|
+
function addToContent(content, key, value) {
|
|
559
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const existing = content[key];
|
|
563
|
+
if (existing !== undefined) {
|
|
564
|
+
if (Array.isArray(existing)) {
|
|
565
|
+
existing.push(value);
|
|
566
|
+
} else {
|
|
567
|
+
content[key] = [existing, value];
|
|
568
|
+
}
|
|
569
|
+
} else {
|
|
570
|
+
content[key] = value;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function sgmlToObject(sgml) {
|
|
574
|
+
const result = {};
|
|
575
|
+
const tagStack = [{ content: result, name: "root" }];
|
|
576
|
+
const stackMap = new Map([["root", 0]]);
|
|
577
|
+
const hasSpecialContent = sgml.includes("<?") || sgml.includes("<!--");
|
|
578
|
+
const cleanSgml = hasSpecialContent ? sgml.replace(/<\?.*?\?>|<!--.*?-->/gs, "").trim() : sgml.trim();
|
|
579
|
+
const tagRegex = /<(\/?)([\w.]+)>([^<]*)/g;
|
|
580
|
+
let match = tagRegex.exec(cleanSgml);
|
|
581
|
+
while (match !== null) {
|
|
582
|
+
const isClosing = match[1];
|
|
583
|
+
const tagName = match[2];
|
|
584
|
+
const textContent = match[3]?.trim() ?? "";
|
|
585
|
+
if (!tagName) {
|
|
586
|
+
match = tagRegex.exec(cleanSgml);
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const current = tagStack[tagStack.length - 1];
|
|
590
|
+
if (!current) {
|
|
591
|
+
match = tagRegex.exec(cleanSgml);
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (isClosing) {
|
|
595
|
+
const stackIndex = stackMap.get(tagName);
|
|
596
|
+
if (stackIndex !== undefined && stackIndex > 0) {
|
|
597
|
+
for (let i = tagStack.length - 1;i >= stackIndex; i--) {
|
|
598
|
+
const item = tagStack[i];
|
|
599
|
+
if (item)
|
|
600
|
+
stackMap.delete(item.name);
|
|
601
|
+
}
|
|
602
|
+
tagStack.length = stackIndex;
|
|
603
|
+
}
|
|
604
|
+
} else if (textContent) {
|
|
605
|
+
const decoded = textContent.includes("&") ? decodeEntities(textContent) : textContent;
|
|
606
|
+
addToContent(current.content, tagName, decoded);
|
|
607
|
+
} else {
|
|
608
|
+
const newObj = {};
|
|
609
|
+
addToContent(current.content, tagName, newObj);
|
|
610
|
+
stackMap.set(tagName, tagStack.length);
|
|
611
|
+
tagStack.push({ content: newObj, name: tagName });
|
|
612
|
+
}
|
|
613
|
+
match = tagRegex.exec(cleanSgml);
|
|
614
|
+
}
|
|
615
|
+
return result;
|
|
616
|
+
}
|
|
617
|
+
function generateFitId(txn, index) {
|
|
618
|
+
const date = String(txn.DTPOSTED ?? "");
|
|
619
|
+
const amount = String(txn.TRNAMT ?? "0");
|
|
620
|
+
const name = String(txn.NAME ?? txn.MEMO ?? "");
|
|
621
|
+
const input = `${date}:${amount}:${name}:${index}`;
|
|
622
|
+
let hash = 0;
|
|
623
|
+
for (let i = 0;i < input.length; i++) {
|
|
624
|
+
hash = (hash << 5) - hash + input.charCodeAt(i);
|
|
625
|
+
hash = hash | 0;
|
|
626
|
+
}
|
|
627
|
+
return `AUTO${Math.abs(hash).toString(16).toUpperCase().padStart(8, "0")}`;
|
|
628
|
+
}
|
|
629
|
+
function normalizeResponseArray(msgs, responseKey, statementKey) {
|
|
630
|
+
const responses = msgs[responseKey];
|
|
631
|
+
if (!responses)
|
|
632
|
+
return;
|
|
633
|
+
for (const response of toArray(responses)) {
|
|
634
|
+
const stmt = response?.[statementKey];
|
|
635
|
+
const tranList = stmt?.BANKTRANLIST;
|
|
636
|
+
if (tranList?.STMTTRN !== undefined) {
|
|
637
|
+
tranList.STMTTRN = toArray(tranList.STMTTRN);
|
|
638
|
+
const transactions = tranList.STMTTRN;
|
|
639
|
+
transactions.forEach((txn, idx) => {
|
|
640
|
+
if (!txn.FITID) {
|
|
641
|
+
txn.FITID = generateFitId(txn, idx);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
function normalizeSignOn(data) {
|
|
648
|
+
const ofx = data.OFX;
|
|
649
|
+
if (!ofx)
|
|
650
|
+
return;
|
|
651
|
+
const signonMsgs = ofx.SIGNONMSGSRSV1;
|
|
652
|
+
const sonrs = signonMsgs?.SONRS;
|
|
653
|
+
if (!sonrs)
|
|
654
|
+
return;
|
|
655
|
+
const status = sonrs.STATUS;
|
|
656
|
+
if (!status)
|
|
657
|
+
return;
|
|
658
|
+
if (!sonrs.DTSERVER && status.DTSERVER) {
|
|
659
|
+
sonrs.DTSERVER = status.DTSERVER;
|
|
660
|
+
delete status.DTSERVER;
|
|
661
|
+
}
|
|
662
|
+
if (!sonrs.LANGUAGE && status.LANGUAGE) {
|
|
663
|
+
sonrs.LANGUAGE = status.LANGUAGE;
|
|
664
|
+
delete status.LANGUAGE;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function normalizeTransactions(data) {
|
|
668
|
+
const ofx = data.OFX;
|
|
669
|
+
if (!ofx)
|
|
670
|
+
return data;
|
|
671
|
+
normalizeSignOn(data);
|
|
672
|
+
const bankMsgs = ofx.BANKMSGSRSV1;
|
|
673
|
+
if (bankMsgs) {
|
|
674
|
+
normalizeResponseArray(bankMsgs, "STMTTRNRS", "STMTRS");
|
|
675
|
+
}
|
|
676
|
+
const ccMsgs = ofx.CREDITCARDMSGSRSV1;
|
|
677
|
+
if (ccMsgs) {
|
|
678
|
+
normalizeResponseArray(ccMsgs, "CCSTMTTRNRS", "CCSTMTRS");
|
|
679
|
+
}
|
|
680
|
+
return data;
|
|
681
|
+
}
|
|
682
|
+
function parseHeader(content) {
|
|
683
|
+
const lines = content.split(/\r?\n/);
|
|
684
|
+
const header = {};
|
|
685
|
+
let bodyStartIndex = 0;
|
|
686
|
+
for (let i = 0;i < lines.length; i++) {
|
|
687
|
+
const line = lines[i]?.trim() ?? "";
|
|
688
|
+
if (line.startsWith("<?xml") || line.startsWith("<OFX>")) {
|
|
689
|
+
bodyStartIndex = i;
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
const match = line.match(/^(\w+):(.*)$/);
|
|
693
|
+
if (match?.[1] && match[2] !== undefined) {
|
|
694
|
+
header[match[1]] = match[2];
|
|
695
|
+
}
|
|
696
|
+
if (line === "" && Object.keys(header).length > 0) {
|
|
697
|
+
bodyStartIndex = i + 1;
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const body = lines.slice(bodyStartIndex).join(`
|
|
702
|
+
`);
|
|
703
|
+
return { body, header: ofxHeaderSchema.parse(header) };
|
|
704
|
+
}
|
|
705
|
+
function parse(content) {
|
|
706
|
+
try {
|
|
707
|
+
if (typeof content !== "string") {
|
|
708
|
+
return {
|
|
709
|
+
error: new z3.ZodError([
|
|
710
|
+
{
|
|
711
|
+
code: "invalid_type",
|
|
712
|
+
expected: "string",
|
|
713
|
+
message: `Expected string, received ${typeof content}`,
|
|
714
|
+
path: []
|
|
715
|
+
}
|
|
716
|
+
]),
|
|
717
|
+
success: false
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
if (content.trim() === "") {
|
|
721
|
+
return {
|
|
722
|
+
error: new z3.ZodError([
|
|
723
|
+
{
|
|
724
|
+
code: "custom",
|
|
725
|
+
message: "Content cannot be empty",
|
|
726
|
+
path: []
|
|
727
|
+
}
|
|
728
|
+
]),
|
|
729
|
+
success: false
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
const { header, body } = parseHeader(content);
|
|
733
|
+
const rawData = sgmlToObject(body);
|
|
734
|
+
const normalizedData = normalizeTransactions(rawData);
|
|
735
|
+
const parseResult = ofxResponseSchema.safeParse(normalizedData.OFX);
|
|
736
|
+
if (!parseResult.success) {
|
|
737
|
+
return { error: parseResult.error, success: false };
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
data: { header, OFX: parseResult.data },
|
|
741
|
+
success: true
|
|
742
|
+
};
|
|
743
|
+
} catch (err) {
|
|
744
|
+
if (err instanceof z3.ZodError) {
|
|
745
|
+
return { error: err, success: false };
|
|
746
|
+
}
|
|
747
|
+
throw err;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
function parseOrThrow(content) {
|
|
751
|
+
const result = parse(content);
|
|
752
|
+
if (!result.success) {
|
|
753
|
+
throw result.error;
|
|
754
|
+
}
|
|
755
|
+
return result.data;
|
|
756
|
+
}
|
|
757
|
+
function isValidUtf8(buffer) {
|
|
758
|
+
let i = 0;
|
|
759
|
+
while (i < buffer.length) {
|
|
760
|
+
const byte = buffer[i];
|
|
761
|
+
if (byte === undefined)
|
|
762
|
+
break;
|
|
763
|
+
if (byte <= 127) {
|
|
764
|
+
i++;
|
|
765
|
+
} else if ((byte & 224) === 192) {
|
|
766
|
+
const b1 = buffer[i + 1];
|
|
767
|
+
if (i + 1 >= buffer.length || b1 === undefined || (b1 & 192) !== 128)
|
|
768
|
+
return false;
|
|
769
|
+
i += 2;
|
|
770
|
+
} else if ((byte & 240) === 224) {
|
|
771
|
+
const b1 = buffer[i + 1];
|
|
772
|
+
const b2 = buffer[i + 2];
|
|
773
|
+
if (i + 2 >= buffer.length || b1 === undefined || b2 === undefined || (b1 & 192) !== 128 || (b2 & 192) !== 128)
|
|
774
|
+
return false;
|
|
775
|
+
i += 3;
|
|
776
|
+
} else if ((byte & 248) === 240) {
|
|
777
|
+
const b1 = buffer[i + 1];
|
|
778
|
+
const b2 = buffer[i + 2];
|
|
779
|
+
const b3 = buffer[i + 3];
|
|
780
|
+
if (i + 3 >= buffer.length || b1 === undefined || b2 === undefined || b3 === undefined || (b1 & 192) !== 128 || (b2 & 192) !== 128 || (b3 & 192) !== 128)
|
|
781
|
+
return false;
|
|
782
|
+
i += 4;
|
|
783
|
+
} else {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return true;
|
|
788
|
+
}
|
|
789
|
+
function hasUtf8MultiByte(buffer) {
|
|
790
|
+
for (let i = 0;i < buffer.length; i++) {
|
|
791
|
+
const byte = buffer[i];
|
|
792
|
+
if (byte !== undefined && byte > 127) {
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
function parseHeaderFromBuffer(buffer) {
|
|
799
|
+
const maxHeaderSize = Math.min(buffer.length, 1000);
|
|
800
|
+
const headerSection = new TextDecoder("windows-1252").decode(buffer.slice(0, maxHeaderSize));
|
|
801
|
+
const header = {};
|
|
802
|
+
const singleLineMatch = headerSection.match(/^(OFXHEADER:\d+.*?)(?=<OFX|<\?xml)/is);
|
|
803
|
+
if (singleLineMatch?.[1]) {
|
|
804
|
+
const headerPart = singleLineMatch[1];
|
|
805
|
+
const fieldRegex = /(\w+):([^\s<]+)/g;
|
|
806
|
+
let fieldMatch = fieldRegex.exec(headerPart);
|
|
807
|
+
while (fieldMatch !== null) {
|
|
808
|
+
const key = fieldMatch[1];
|
|
809
|
+
const value = fieldMatch[2];
|
|
810
|
+
if (key && value !== undefined) {
|
|
811
|
+
header[key] = value;
|
|
812
|
+
}
|
|
813
|
+
fieldMatch = fieldRegex.exec(headerPart);
|
|
814
|
+
}
|
|
815
|
+
} else {
|
|
816
|
+
const lines = headerSection.split(/\r?\n/);
|
|
817
|
+
for (const line of lines) {
|
|
818
|
+
const trimmed = line.trim();
|
|
819
|
+
if (trimmed.startsWith("<?xml") || trimmed.startsWith("<OFX")) {
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
const match = trimmed.match(/^(\w+):(.*)$/);
|
|
823
|
+
if (match?.[1] && match[2] !== undefined) {
|
|
824
|
+
header[match[1]] = match[2];
|
|
825
|
+
}
|
|
826
|
+
if (trimmed === "" && Object.keys(header).length > 0) {
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const parsedHeader = ofxHeaderSchema.parse(header);
|
|
832
|
+
let encoding = getEncodingFromCharset(parsedHeader.CHARSET);
|
|
833
|
+
if (encoding !== "utf-8" && hasUtf8MultiByte(buffer) && isValidUtf8(buffer)) {
|
|
834
|
+
encoding = "utf-8";
|
|
835
|
+
}
|
|
836
|
+
return { encoding, header: parsedHeader };
|
|
837
|
+
}
|
|
838
|
+
function decodeOfxBuffer(buffer) {
|
|
839
|
+
const { encoding } = parseHeaderFromBuffer(buffer);
|
|
840
|
+
const decoder = new TextDecoder(encoding);
|
|
841
|
+
return decoder.decode(buffer);
|
|
842
|
+
}
|
|
843
|
+
function parseBuffer(buffer) {
|
|
844
|
+
try {
|
|
845
|
+
if (!(buffer instanceof Uint8Array)) {
|
|
846
|
+
return {
|
|
847
|
+
error: new z3.ZodError([
|
|
848
|
+
{
|
|
849
|
+
code: "invalid_type",
|
|
850
|
+
expected: "object",
|
|
851
|
+
message: "Expected Uint8Array",
|
|
852
|
+
path: []
|
|
853
|
+
}
|
|
854
|
+
]),
|
|
855
|
+
success: false
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
if (buffer.length === 0) {
|
|
859
|
+
return {
|
|
860
|
+
error: new z3.ZodError([
|
|
861
|
+
{
|
|
862
|
+
code: "custom",
|
|
863
|
+
message: "Buffer cannot be empty",
|
|
864
|
+
path: []
|
|
865
|
+
}
|
|
866
|
+
]),
|
|
867
|
+
success: false
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
const content = decodeOfxBuffer(buffer);
|
|
871
|
+
return parse(content);
|
|
872
|
+
} catch (err) {
|
|
873
|
+
if (err instanceof z3.ZodError) {
|
|
874
|
+
return { error: err, success: false };
|
|
875
|
+
}
|
|
876
|
+
throw err;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
function parseBufferOrThrow(buffer) {
|
|
880
|
+
const result = parseBuffer(buffer);
|
|
881
|
+
if (!result.success) {
|
|
882
|
+
throw result.error;
|
|
883
|
+
}
|
|
884
|
+
return result.data;
|
|
885
|
+
}
|
|
886
|
+
// src/stream.ts
|
|
887
|
+
function parseHeaderFromBuffer2(buffer) {
|
|
888
|
+
const lines = buffer.split(/\r?\n/);
|
|
889
|
+
const header = {};
|
|
890
|
+
let bodyStartIndex = 0;
|
|
891
|
+
for (let i = 0;i < lines.length; i++) {
|
|
892
|
+
const line = lines[i]?.trim() ?? "";
|
|
893
|
+
if (line.startsWith("<?xml") || line.startsWith("<OFX>")) {
|
|
894
|
+
bodyStartIndex = i;
|
|
895
|
+
break;
|
|
896
|
+
}
|
|
897
|
+
const match = line.match(/^(\w+):(.*)$/);
|
|
898
|
+
if (match?.[1] && match[2] !== undefined) {
|
|
899
|
+
header[match[1]] = match[2];
|
|
900
|
+
}
|
|
901
|
+
if (line === "" && Object.keys(header).length > 0) {
|
|
902
|
+
bodyStartIndex = i + 1;
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (Object.keys(header).length === 0)
|
|
907
|
+
return null;
|
|
908
|
+
const headerResult = ofxHeaderSchema.safeParse(header);
|
|
909
|
+
if (!headerResult.success)
|
|
910
|
+
return null;
|
|
911
|
+
const bodyStartChar = lines.slice(0, bodyStartIndex).join(`
|
|
912
|
+
`).length + 1;
|
|
913
|
+
return { bodyStart: bodyStartChar, header: headerResult.data };
|
|
914
|
+
}
|
|
915
|
+
function tryParseTransaction(obj) {
|
|
916
|
+
const result = transactionSchema.safeParse(obj);
|
|
917
|
+
return result.success ? result.data : null;
|
|
918
|
+
}
|
|
919
|
+
function tryParseBankAccount(obj) {
|
|
920
|
+
const result = bankAccountSchema.safeParse(obj);
|
|
921
|
+
return result.success ? result.data : null;
|
|
922
|
+
}
|
|
923
|
+
function tryParseCreditCardAccount(obj) {
|
|
924
|
+
const result = creditCardAccountSchema.safeParse(obj);
|
|
925
|
+
return result.success ? result.data : null;
|
|
926
|
+
}
|
|
927
|
+
function tryParseBalance(obj) {
|
|
928
|
+
const result = balanceSchema.safeParse(obj);
|
|
929
|
+
return result.success ? result.data : null;
|
|
930
|
+
}
|
|
931
|
+
async function* parseStream(input, options) {
|
|
932
|
+
const state = {
|
|
933
|
+
buffer: "",
|
|
934
|
+
currentObject: {},
|
|
935
|
+
currentPath: [],
|
|
936
|
+
headerParsed: false,
|
|
937
|
+
inHeader: true,
|
|
938
|
+
objectStack: [{}],
|
|
939
|
+
transactionCount: 0
|
|
940
|
+
};
|
|
941
|
+
let detectedEncoding = options?.encoding;
|
|
942
|
+
let decoder = new TextDecoder(detectedEncoding ?? "utf-8");
|
|
943
|
+
const tagRegex = /<(\/?)([\w.]+)>([^<]*)/g;
|
|
944
|
+
let pendingLedgerBalance;
|
|
945
|
+
let pendingAvailableBalance;
|
|
946
|
+
let emittedBalanceForCurrentStatement = false;
|
|
947
|
+
async function* processChunk(chunk, isLast = false) {
|
|
948
|
+
state.buffer += chunk;
|
|
949
|
+
if (!state.headerParsed) {
|
|
950
|
+
const headerResult = parseHeaderFromBuffer2(state.buffer);
|
|
951
|
+
if (headerResult) {
|
|
952
|
+
state.headerParsed = true;
|
|
953
|
+
state.inHeader = false;
|
|
954
|
+
if (!detectedEncoding && headerResult.header.CHARSET) {
|
|
955
|
+
detectedEncoding = getEncodingFromCharset(headerResult.header.CHARSET);
|
|
956
|
+
decoder = new TextDecoder(detectedEncoding);
|
|
957
|
+
}
|
|
958
|
+
yield { data: headerResult.header, type: "header" };
|
|
959
|
+
state.buffer = state.buffer.slice(headerResult.bodyStart);
|
|
960
|
+
} else {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const lastLt = state.buffer.lastIndexOf("<");
|
|
965
|
+
const safeEnd = isLast ? state.buffer.length : lastLt;
|
|
966
|
+
if (safeEnd <= 0)
|
|
967
|
+
return;
|
|
968
|
+
const safeBuffer = state.buffer.slice(0, safeEnd);
|
|
969
|
+
let processedUpTo = 0;
|
|
970
|
+
tagRegex.lastIndex = 0;
|
|
971
|
+
for (let match = tagRegex.exec(safeBuffer);match !== null; match = tagRegex.exec(safeBuffer)) {
|
|
972
|
+
const isClosing = match[1] === "/";
|
|
973
|
+
const tagName = match[2];
|
|
974
|
+
const textContent = match[3]?.trim() ?? "";
|
|
975
|
+
if (!tagName)
|
|
976
|
+
continue;
|
|
977
|
+
const currentObj = state.objectStack[state.objectStack.length - 1];
|
|
978
|
+
if (!currentObj)
|
|
979
|
+
continue;
|
|
980
|
+
if (isClosing) {
|
|
981
|
+
if (tagName === "STMTTRN") {
|
|
982
|
+
const txn = tryParseTransaction(currentObj);
|
|
983
|
+
if (txn) {
|
|
984
|
+
state.transactionCount++;
|
|
985
|
+
yield { data: txn, type: "transaction" };
|
|
986
|
+
}
|
|
987
|
+
} else if (tagName === "BANKACCTFROM") {
|
|
988
|
+
const account = tryParseBankAccount(currentObj);
|
|
989
|
+
if (account) {
|
|
990
|
+
yield { data: account, type: "account" };
|
|
991
|
+
}
|
|
992
|
+
} else if (tagName === "CCACCTFROM") {
|
|
993
|
+
const account = tryParseCreditCardAccount(currentObj);
|
|
994
|
+
if (account) {
|
|
995
|
+
yield { data: account, type: "account" };
|
|
996
|
+
}
|
|
997
|
+
} else if (tagName === "LEDGERBAL") {
|
|
998
|
+
pendingLedgerBalance = tryParseBalance(currentObj) ?? undefined;
|
|
999
|
+
} else if (tagName === "AVAILBAL") {
|
|
1000
|
+
pendingAvailableBalance = tryParseBalance(currentObj) ?? undefined;
|
|
1001
|
+
} else if ((tagName === "STMTRS" || tagName === "CCSTMTRS") && !emittedBalanceForCurrentStatement) {
|
|
1002
|
+
if (pendingLedgerBalance || pendingAvailableBalance) {
|
|
1003
|
+
yield {
|
|
1004
|
+
data: {
|
|
1005
|
+
available: pendingAvailableBalance,
|
|
1006
|
+
ledger: pendingLedgerBalance
|
|
1007
|
+
},
|
|
1008
|
+
type: "balance"
|
|
1009
|
+
};
|
|
1010
|
+
emittedBalanceForCurrentStatement = true;
|
|
1011
|
+
}
|
|
1012
|
+
} else if (tagName === "STMTTRNRS" || tagName === "CCSTMTTRNRS") {
|
|
1013
|
+
pendingLedgerBalance = undefined;
|
|
1014
|
+
pendingAvailableBalance = undefined;
|
|
1015
|
+
emittedBalanceForCurrentStatement = false;
|
|
1016
|
+
}
|
|
1017
|
+
const pathIndex = state.currentPath.lastIndexOf(tagName);
|
|
1018
|
+
if (pathIndex !== -1) {
|
|
1019
|
+
state.currentPath.length = pathIndex;
|
|
1020
|
+
state.objectStack.length = Math.max(pathIndex + 1, 1);
|
|
1021
|
+
}
|
|
1022
|
+
} else if (textContent) {
|
|
1023
|
+
const decoded = decodeEntities(textContent);
|
|
1024
|
+
const existing = currentObj[tagName];
|
|
1025
|
+
if (existing !== undefined) {
|
|
1026
|
+
if (Array.isArray(existing)) {
|
|
1027
|
+
existing.push(decoded);
|
|
1028
|
+
} else {
|
|
1029
|
+
currentObj[tagName] = [existing, decoded];
|
|
1030
|
+
}
|
|
1031
|
+
} else {
|
|
1032
|
+
currentObj[tagName] = decoded;
|
|
1033
|
+
}
|
|
1034
|
+
} else {
|
|
1035
|
+
const newObj = {};
|
|
1036
|
+
const existing = currentObj[tagName];
|
|
1037
|
+
if (existing !== undefined) {
|
|
1038
|
+
if (Array.isArray(existing)) {
|
|
1039
|
+
existing.push(newObj);
|
|
1040
|
+
} else {
|
|
1041
|
+
currentObj[tagName] = [existing, newObj];
|
|
1042
|
+
}
|
|
1043
|
+
} else {
|
|
1044
|
+
currentObj[tagName] = newObj;
|
|
1045
|
+
}
|
|
1046
|
+
state.currentPath.push(tagName);
|
|
1047
|
+
state.objectStack.push(newObj);
|
|
1048
|
+
}
|
|
1049
|
+
processedUpTo = tagRegex.lastIndex;
|
|
1050
|
+
}
|
|
1051
|
+
if (processedUpTo > 0) {
|
|
1052
|
+
state.buffer = state.buffer.slice(processedUpTo);
|
|
1053
|
+
}
|
|
1054
|
+
tagRegex.lastIndex = 0;
|
|
1055
|
+
}
|
|
1056
|
+
if (input instanceof ReadableStream) {
|
|
1057
|
+
const reader = input.getReader();
|
|
1058
|
+
const initialChunks = [];
|
|
1059
|
+
let headerFound = false;
|
|
1060
|
+
try {
|
|
1061
|
+
while (!headerFound) {
|
|
1062
|
+
const { done, value } = await reader.read();
|
|
1063
|
+
if (done)
|
|
1064
|
+
break;
|
|
1065
|
+
initialChunks.push(value);
|
|
1066
|
+
const combined = new Uint8Array(initialChunks.reduce((sum, chunk) => sum + chunk.length, 0));
|
|
1067
|
+
let offset = 0;
|
|
1068
|
+
for (const chunk of initialChunks) {
|
|
1069
|
+
combined.set(chunk, offset);
|
|
1070
|
+
offset += chunk.length;
|
|
1071
|
+
}
|
|
1072
|
+
const headerSection = new TextDecoder("windows-1252").decode(combined.slice(0, Math.min(combined.length, 1000)));
|
|
1073
|
+
if (headerSection.includes("<OFX") || headerSection.includes("<?xml")) {
|
|
1074
|
+
const charsetMatch = headerSection.match(/CHARSET:(\S+)/i);
|
|
1075
|
+
if (charsetMatch && !detectedEncoding) {
|
|
1076
|
+
detectedEncoding = getEncodingFromCharset(charsetMatch[1]);
|
|
1077
|
+
decoder = new TextDecoder(detectedEncoding);
|
|
1078
|
+
}
|
|
1079
|
+
headerFound = true;
|
|
1080
|
+
const content = decoder.decode(combined);
|
|
1081
|
+
yield* processChunk(content);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
while (true) {
|
|
1085
|
+
const { done, value } = await reader.read();
|
|
1086
|
+
if (done)
|
|
1087
|
+
break;
|
|
1088
|
+
yield* processChunk(decoder.decode(value, { stream: true }));
|
|
1089
|
+
}
|
|
1090
|
+
yield* processChunk(decoder.decode(), true);
|
|
1091
|
+
} finally {
|
|
1092
|
+
reader.releaseLock();
|
|
1093
|
+
}
|
|
1094
|
+
} else {
|
|
1095
|
+
const chunks = [];
|
|
1096
|
+
for await (const chunk of input) {
|
|
1097
|
+
chunks.push(chunk);
|
|
1098
|
+
}
|
|
1099
|
+
for (let i = 0;i < chunks.length; i++) {
|
|
1100
|
+
yield* processChunk(chunks[i] ?? "", i === chunks.length - 1);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
yield { transactionCount: state.transactionCount, type: "complete" };
|
|
1104
|
+
}
|
|
1105
|
+
async function parseStreamToArray(input, options) {
|
|
1106
|
+
const result = {
|
|
1107
|
+
accounts: [],
|
|
1108
|
+
balances: [],
|
|
1109
|
+
transactions: []
|
|
1110
|
+
};
|
|
1111
|
+
for await (const event of parseStream(input, options)) {
|
|
1112
|
+
switch (event.type) {
|
|
1113
|
+
case "header":
|
|
1114
|
+
result.header = event.data;
|
|
1115
|
+
break;
|
|
1116
|
+
case "transaction":
|
|
1117
|
+
result.transactions.push(event.data);
|
|
1118
|
+
break;
|
|
1119
|
+
case "account":
|
|
1120
|
+
result.accounts.push(event.data);
|
|
1121
|
+
break;
|
|
1122
|
+
case "balance":
|
|
1123
|
+
result.balances.push(event.data);
|
|
1124
|
+
break;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return result;
|
|
1128
|
+
}
|
|
1129
|
+
async function* createChunkIterable(content, chunkSize = 65536) {
|
|
1130
|
+
for (let i = 0;i < content.length; i += chunkSize) {
|
|
1131
|
+
yield content.slice(i, i + chunkSize);
|
|
1132
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
async function* parseBatchStream(files, options) {
|
|
1136
|
+
let totalTransactions = 0;
|
|
1137
|
+
let errorCount = 0;
|
|
1138
|
+
for (let i = 0;i < files.length; i++) {
|
|
1139
|
+
const file = files[i];
|
|
1140
|
+
if (!file)
|
|
1141
|
+
continue;
|
|
1142
|
+
yield { type: "file_start", fileIndex: i, filename: file.filename };
|
|
1143
|
+
try {
|
|
1144
|
+
let fileTransactionCount = 0;
|
|
1145
|
+
const content = decodeOfxBuffer(file.buffer);
|
|
1146
|
+
const chunkIterable = createChunkIterable(content);
|
|
1147
|
+
for await (const event of parseStream(chunkIterable, options)) {
|
|
1148
|
+
switch (event.type) {
|
|
1149
|
+
case "header":
|
|
1150
|
+
yield { type: "header", fileIndex: i, data: event.data };
|
|
1151
|
+
break;
|
|
1152
|
+
case "transaction":
|
|
1153
|
+
yield { type: "transaction", fileIndex: i, data: event.data };
|
|
1154
|
+
fileTransactionCount++;
|
|
1155
|
+
break;
|
|
1156
|
+
case "account":
|
|
1157
|
+
yield { type: "account", fileIndex: i, data: event.data };
|
|
1158
|
+
break;
|
|
1159
|
+
case "balance":
|
|
1160
|
+
yield { type: "balance", fileIndex: i, data: event.data };
|
|
1161
|
+
break;
|
|
1162
|
+
case "complete":
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
totalTransactions += fileTransactionCount;
|
|
1167
|
+
yield {
|
|
1168
|
+
type: "file_complete",
|
|
1169
|
+
fileIndex: i,
|
|
1170
|
+
filename: file.filename,
|
|
1171
|
+
transactionCount: fileTransactionCount
|
|
1172
|
+
};
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
errorCount++;
|
|
1175
|
+
yield {
|
|
1176
|
+
type: "file_error",
|
|
1177
|
+
fileIndex: i,
|
|
1178
|
+
filename: file.filename,
|
|
1179
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1183
|
+
}
|
|
1184
|
+
yield {
|
|
1185
|
+
type: "batch_complete",
|
|
1186
|
+
totalFiles: files.length,
|
|
1187
|
+
totalTransactions,
|
|
1188
|
+
errorCount
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
async function parseBatchStreamToArray(files, options) {
|
|
1192
|
+
const results = files.map((file, index) => ({
|
|
1193
|
+
fileIndex: index,
|
|
1194
|
+
filename: file.filename,
|
|
1195
|
+
transactions: [],
|
|
1196
|
+
accounts: [],
|
|
1197
|
+
balances: []
|
|
1198
|
+
}));
|
|
1199
|
+
for await (const event of parseBatchStream(files, options)) {
|
|
1200
|
+
switch (event.type) {
|
|
1201
|
+
case "header": {
|
|
1202
|
+
const result = results[event.fileIndex];
|
|
1203
|
+
if (result)
|
|
1204
|
+
result.header = event.data;
|
|
1205
|
+
break;
|
|
1206
|
+
}
|
|
1207
|
+
case "transaction": {
|
|
1208
|
+
const result = results[event.fileIndex];
|
|
1209
|
+
if (result)
|
|
1210
|
+
result.transactions.push(event.data);
|
|
1211
|
+
break;
|
|
1212
|
+
}
|
|
1213
|
+
case "account": {
|
|
1214
|
+
const result = results[event.fileIndex];
|
|
1215
|
+
if (result)
|
|
1216
|
+
result.accounts.push(event.data);
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
1219
|
+
case "balance": {
|
|
1220
|
+
const result = results[event.fileIndex];
|
|
1221
|
+
if (result)
|
|
1222
|
+
result.balances.push(event.data);
|
|
1223
|
+
break;
|
|
1224
|
+
}
|
|
1225
|
+
case "file_error": {
|
|
1226
|
+
const result = results[event.fileIndex];
|
|
1227
|
+
if (result)
|
|
1228
|
+
result.error = event.error;
|
|
1229
|
+
break;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
return results;
|
|
1234
|
+
}
|
|
1235
|
+
export {
|
|
1236
|
+
parseStreamToArray,
|
|
1237
|
+
parseStream,
|
|
1238
|
+
parseOrThrow,
|
|
1239
|
+
parseBufferOrThrow,
|
|
1240
|
+
parseBuffer,
|
|
1241
|
+
parseBatchStreamToArray,
|
|
1242
|
+
parseBatchStream,
|
|
1243
|
+
parse,
|
|
1244
|
+
getTransactions,
|
|
1245
|
+
getSignOnInfo,
|
|
1246
|
+
getEncodingFromCharset,
|
|
1247
|
+
getBalance,
|
|
1248
|
+
getAccountInfo,
|
|
1249
|
+
generateTransactionInputSchema,
|
|
1250
|
+
generateHeaderOptionsSchema,
|
|
1251
|
+
generateHeader,
|
|
1252
|
+
generateCreditCardStatementOptionsSchema,
|
|
1253
|
+
generateCreditCardStatement,
|
|
1254
|
+
generateBankStatementOptionsSchema,
|
|
1255
|
+
generateBankStatement,
|
|
1256
|
+
formatOfxDate,
|
|
1257
|
+
decodeOfxBuffer
|
|
1258
|
+
};
|
|
1259
|
+
|
|
1260
|
+
//# debugId=7C69B4A1D0DBA73E64756E2164756E21
|