@f-o-t/ofx 1.2.0 → 1.3.0
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/README.md +125 -4
- package/dist/index.d.ts +104 -10
- package/dist/index.js +595 -84
- package/package.json +8 -11
package/README.md
CHANGED
|
@@ -123,6 +123,112 @@ console.log({
|
|
|
123
123
|
});
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
+
### Generation Functions
|
|
127
|
+
|
|
128
|
+
#### `generateBankStatement(options: GenerateBankStatementOptions): string`
|
|
129
|
+
|
|
130
|
+
Generates a complete OFX bank statement file.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { generateBankStatement } from "@fot/ofx";
|
|
134
|
+
|
|
135
|
+
const statement = generateBankStatement({
|
|
136
|
+
bankId: "123456",
|
|
137
|
+
accountId: "987654321",
|
|
138
|
+
accountType: "CHECKING",
|
|
139
|
+
currency: "USD",
|
|
140
|
+
startDate: new Date("2025-01-01"),
|
|
141
|
+
endDate: new Date("2025-01-31"),
|
|
142
|
+
transactions: [
|
|
143
|
+
{
|
|
144
|
+
type: "CREDIT",
|
|
145
|
+
datePosted: new Date(),
|
|
146
|
+
amount: 1000,
|
|
147
|
+
fitId: "1",
|
|
148
|
+
name: "Deposit",
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
console.log(statement);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### `generateCreditCardStatement(options: GenerateCreditCardStatementOptions): string`
|
|
157
|
+
|
|
158
|
+
Generates a complete OFX credit card statement file.
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { generateCreditCardStatement } from "@fot/ofx";
|
|
162
|
+
|
|
163
|
+
const statement = generateCreditCardStatement({
|
|
164
|
+
accountId: "123456789",
|
|
165
|
+
currency: "USD",
|
|
166
|
+
startDate: new Date("2025-01-01"),
|
|
167
|
+
endDate: new Date("2025-01-31"),
|
|
168
|
+
transactions: [
|
|
169
|
+
{
|
|
170
|
+
type: "DEBIT",
|
|
171
|
+
datePosted: new Date(),
|
|
172
|
+
amount: -75.5,
|
|
173
|
+
fitId: "2",
|
|
174
|
+
name: "Purchase at a store",
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
console.log(statement);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Streaming Functions
|
|
183
|
+
|
|
184
|
+
For processing large OFX files with low memory footprint.
|
|
185
|
+
|
|
186
|
+
#### `parseStream(input): AsyncGenerator<StreamEvent>`
|
|
187
|
+
|
|
188
|
+
Parses an OFX file as a stream, yielding events as they are parsed.
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { parseStream } from "@fot/ofx";
|
|
192
|
+
|
|
193
|
+
// From a ReadableStream (e.g., fetch response)
|
|
194
|
+
const response = await fetch("https://example.com/statement.ofx");
|
|
195
|
+
for await (const event of parseStream(response.body)) {
|
|
196
|
+
switch (event.type) {
|
|
197
|
+
case "header":
|
|
198
|
+
console.log("OFX Version:", event.data.VERSION);
|
|
199
|
+
break;
|
|
200
|
+
case "account":
|
|
201
|
+
console.log("Account:", event.data.ACCTID);
|
|
202
|
+
break;
|
|
203
|
+
case "transaction":
|
|
204
|
+
console.log("Transaction:", event.data.NAME, event.data.TRNAMT);
|
|
205
|
+
break;
|
|
206
|
+
case "balance":
|
|
207
|
+
console.log("Ledger Balance:", event.data.ledger?.BALAMT);
|
|
208
|
+
break;
|
|
209
|
+
case "complete":
|
|
210
|
+
console.log("Total transactions:", event.transactionCount);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### `parseStreamToArray(input): Promise<StreamResult>`
|
|
217
|
+
|
|
218
|
+
Collects all stream events into arrays for easier processing.
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { parseStreamToArray } from "@fot/ofx";
|
|
222
|
+
|
|
223
|
+
const response = await fetch("https://example.com/statement.ofx");
|
|
224
|
+
const result = await parseStreamToArray(response.body);
|
|
225
|
+
|
|
226
|
+
console.log("Header:", result.header);
|
|
227
|
+
console.log("Transactions:", result.transactions.length);
|
|
228
|
+
console.log("Accounts:", result.accounts);
|
|
229
|
+
console.log("Balances:", result.balances);
|
|
230
|
+
```
|
|
231
|
+
|
|
126
232
|
## Types
|
|
127
233
|
|
|
128
234
|
### OFXTransaction
|
|
@@ -227,6 +333,17 @@ interface BalanceInfo {
|
|
|
227
333
|
}
|
|
228
334
|
```
|
|
229
335
|
|
|
336
|
+
### StreamEvent
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
type StreamEvent =
|
|
340
|
+
| { type: "header"; data: OFXHeader }
|
|
341
|
+
| { type: "transaction"; data: OFXTransaction }
|
|
342
|
+
| { type: "account"; data: OFXBankAccount | OFXCreditCardAccount }
|
|
343
|
+
| { type: "balance"; data: { ledger?: OFXBalance; available?: OFXBalance } }
|
|
344
|
+
| { type: "complete"; transactionCount: number };
|
|
345
|
+
```
|
|
346
|
+
|
|
230
347
|
## Schemas
|
|
231
348
|
|
|
232
349
|
All Zod schemas are exported for custom validation:
|
|
@@ -264,13 +381,17 @@ Tested on realistic business statement sizes:
|
|
|
264
381
|
|
|
265
382
|
| Transactions | File Size | Parse Time |
|
|
266
383
|
| ------------ | --------- | ---------- |
|
|
267
|
-
| ~
|
|
268
|
-
| ~
|
|
269
|
-
| ~
|
|
270
|
-
| ~
|
|
384
|
+
| ~5,000 | 1.2 MB | ~37ms |
|
|
385
|
+
| ~10,000 | 2.5 MB | ~108ms |
|
|
386
|
+
| ~25,000 | 5.4 MB | ~230ms |
|
|
387
|
+
| ~50,000 | 10.4 MB | ~450ms |
|
|
271
388
|
|
|
272
389
|
Extraction operations (`getTransactions`, `getBalance`, etc.) are sub-millisecond even on large datasets.
|
|
273
390
|
|
|
391
|
+
### Streaming Performance
|
|
392
|
+
|
|
393
|
+
The streaming API achieves ~55,000-66,000 transactions/sec throughput with minimal memory overhead, making it ideal for processing very large files or network streams.
|
|
394
|
+
|
|
274
395
|
## License
|
|
275
396
|
|
|
276
397
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -42,21 +42,115 @@ type OFXHeader = z.infer<typeof ofxHeaderSchema>;
|
|
|
42
42
|
declare const ofxDocumentSchema: unknown;
|
|
43
43
|
type OFXDocument = z.infer<typeof ofxDocumentSchema>;
|
|
44
44
|
declare const schemas: {};
|
|
45
|
+
interface BalanceInfo {
|
|
46
|
+
ledger?: OFXBalance;
|
|
47
|
+
available?: OFXBalance;
|
|
48
|
+
}
|
|
49
|
+
declare function getTransactions(document: OFXDocument): OFXTransaction[];
|
|
50
|
+
declare function getAccountInfo(document: OFXDocument): (OFXBankAccount | OFXCreditCardAccount)[];
|
|
51
|
+
declare function getBalance(document: OFXDocument): BalanceInfo[];
|
|
52
|
+
declare function getSignOnInfo(document: OFXDocument): OFXSignOnResponse;
|
|
53
|
+
interface GenerateHeaderOptions {
|
|
54
|
+
version?: string;
|
|
55
|
+
encoding?: string;
|
|
56
|
+
charset?: string;
|
|
57
|
+
}
|
|
58
|
+
declare function generateHeader(options?: GenerateHeaderOptions): string;
|
|
59
|
+
interface GenerateTransactionInput {
|
|
60
|
+
type: OFXTransactionType;
|
|
61
|
+
datePosted: Date;
|
|
62
|
+
amount: number;
|
|
63
|
+
fitId: string;
|
|
64
|
+
name?: string;
|
|
65
|
+
memo?: string;
|
|
66
|
+
checkNum?: string;
|
|
67
|
+
refNum?: string;
|
|
68
|
+
}
|
|
69
|
+
interface GenerateBankStatementOptions {
|
|
70
|
+
bankId: string;
|
|
71
|
+
accountId: string;
|
|
72
|
+
accountType: OFXAccountType;
|
|
73
|
+
currency: string;
|
|
74
|
+
startDate: Date;
|
|
75
|
+
endDate: Date;
|
|
76
|
+
transactions: GenerateTransactionInput[];
|
|
77
|
+
ledgerBalance?: {
|
|
78
|
+
amount: number;
|
|
79
|
+
asOfDate: Date;
|
|
80
|
+
};
|
|
81
|
+
availableBalance?: {
|
|
82
|
+
amount: number;
|
|
83
|
+
asOfDate: Date;
|
|
84
|
+
};
|
|
85
|
+
financialInstitution?: {
|
|
86
|
+
org?: string;
|
|
87
|
+
fid?: string;
|
|
88
|
+
};
|
|
89
|
+
language?: string;
|
|
90
|
+
}
|
|
91
|
+
declare function generateBankStatement(options: GenerateBankStatementOptions): string;
|
|
92
|
+
interface GenerateCreditCardStatementOptions {
|
|
93
|
+
accountId: string;
|
|
94
|
+
currency: string;
|
|
95
|
+
startDate: Date;
|
|
96
|
+
endDate: Date;
|
|
97
|
+
transactions: GenerateTransactionInput[];
|
|
98
|
+
ledgerBalance?: {
|
|
99
|
+
amount: number;
|
|
100
|
+
asOfDate: Date;
|
|
101
|
+
};
|
|
102
|
+
availableBalance?: {
|
|
103
|
+
amount: number;
|
|
104
|
+
asOfDate: Date;
|
|
105
|
+
};
|
|
106
|
+
financialInstitution?: {
|
|
107
|
+
org?: string;
|
|
108
|
+
fid?: string;
|
|
109
|
+
};
|
|
110
|
+
language?: string;
|
|
111
|
+
}
|
|
112
|
+
declare function generateCreditCardStatement(options: GenerateCreditCardStatementOptions): string;
|
|
113
|
+
import { z as z2 } from "zod";
|
|
45
114
|
type ParseResult<T> = {
|
|
46
115
|
success: true;
|
|
47
116
|
data: T;
|
|
48
117
|
} | {
|
|
49
118
|
success: false;
|
|
50
|
-
error:
|
|
119
|
+
error: z2.ZodError;
|
|
51
120
|
};
|
|
52
121
|
declare function parse(content: string): ParseResult<OFXDocument>;
|
|
53
122
|
declare function parseOrThrow(content: string): OFXDocument;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
123
|
+
type StreamEvent = {
|
|
124
|
+
type: "header";
|
|
125
|
+
data: OFXHeader;
|
|
126
|
+
} | {
|
|
127
|
+
type: "transaction";
|
|
128
|
+
data: OFXTransaction;
|
|
129
|
+
} | {
|
|
130
|
+
type: "account";
|
|
131
|
+
data: OFXBankAccount | OFXCreditCardAccount;
|
|
132
|
+
} | {
|
|
133
|
+
type: "balance";
|
|
134
|
+
data: {
|
|
135
|
+
ledger?: OFXBalance;
|
|
136
|
+
available?: OFXBalance;
|
|
137
|
+
};
|
|
138
|
+
} | {
|
|
139
|
+
type: "complete";
|
|
140
|
+
transactionCount: number;
|
|
141
|
+
};
|
|
142
|
+
declare function parseStream(input: ReadableStream<Uint8Array> | AsyncIterable<string>): AsyncGenerator<StreamEvent>;
|
|
143
|
+
declare function parseStreamToArray(input: ReadableStream<Uint8Array> | AsyncIterable<string>): Promise<{
|
|
144
|
+
header?: OFXHeader;
|
|
145
|
+
transactions: OFXTransaction[];
|
|
146
|
+
accounts: (OFXBankAccount | OFXCreditCardAccount)[];
|
|
147
|
+
balances: {
|
|
148
|
+
ledger?: OFXBalance;
|
|
149
|
+
available?: OFXBalance;
|
|
150
|
+
}[];
|
|
151
|
+
}>;
|
|
152
|
+
declare function formatOfxDate(date: Date, timezone?: {
|
|
153
|
+
offset: number;
|
|
154
|
+
name: string;
|
|
155
|
+
}): string;
|
|
156
|
+
export { schemas, parseStreamToArray, parseStream, parseOrThrow, parse, getTransactions, getSignOnInfo, getBalance, getAccountInfo, generateHeader, generateCreditCardStatement, generateBankStatement, formatOfxDate, StreamEvent, ParseResult, OFXTransactionType, OFXTransactionList, OFXTransaction, OFXStatus, OFXSignOnResponse, OFXSignOnMessageSetResponse, OFXResponse, OFXHeader, OFXFinancialInstitution, OFXDocument, OFXDate, OFXCreditCardStatementTransactionResponse, OFXCreditCardStatementResponse, OFXCreditCardMessageSetResponse, OFXCreditCardAccount, OFXBankStatementTransactionResponse, OFXBankStatementResponse, OFXBankMessageSetResponse, OFXBankAccount, OFXBalance, OFXAccountType, GenerateTransactionInput, GenerateHeaderOptions, GenerateCreditCardStatementOptions, GenerateBankStatementOptions, BalanceInfo };
|
package/dist/index.js
CHANGED
|
@@ -1,23 +1,296 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/utils.ts
|
|
2
|
+
var toArray = (value) => Array.isArray(value) ? value : [value];
|
|
3
|
+
var pad = (n, width = 2) => n.toString().padStart(width, "0");
|
|
4
|
+
function escapeOfxText(text) {
|
|
5
|
+
if (!text.includes("&") && !text.includes("<") && !text.includes(">")) {
|
|
6
|
+
return text;
|
|
7
|
+
}
|
|
8
|
+
return text.replace(/[&<>]/g, (c) => c === "&" ? "&" : c === "<" ? "<" : ">");
|
|
9
|
+
}
|
|
10
|
+
function formatAmount(amount) {
|
|
11
|
+
return amount.toFixed(2);
|
|
12
|
+
}
|
|
13
|
+
function formatOfxDate(date, timezone) {
|
|
14
|
+
const tz = timezone ?? { name: "GMT", offset: 0 };
|
|
15
|
+
const offsetMs = tz.offset * 60 * 60 * 1000;
|
|
16
|
+
const adjustedDate = new Date(date.getTime() + offsetMs);
|
|
17
|
+
const year = adjustedDate.getUTCFullYear();
|
|
18
|
+
const month = pad(adjustedDate.getUTCMonth() + 1);
|
|
19
|
+
const day = pad(adjustedDate.getUTCDate());
|
|
20
|
+
const hour = pad(adjustedDate.getUTCHours());
|
|
21
|
+
const minute = pad(adjustedDate.getUTCMinutes());
|
|
22
|
+
const second = pad(adjustedDate.getUTCSeconds());
|
|
23
|
+
const sign = tz.offset >= 0 ? "+" : "";
|
|
24
|
+
return `${year}${month}${day}${hour}${minute}${second}[${sign}${tz.offset}:${tz.name}]`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/extractors.ts
|
|
28
|
+
function getTransactions(document) {
|
|
29
|
+
const results = [];
|
|
30
|
+
const bankResponse = document.OFX.BANKMSGSRSV1?.STMTTRNRS;
|
|
31
|
+
if (bankResponse) {
|
|
32
|
+
for (const r of toArray(bankResponse)) {
|
|
33
|
+
const txns = r.STMTRS?.BANKTRANLIST?.STMTTRN;
|
|
34
|
+
if (txns)
|
|
35
|
+
results.push(...txns);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const ccResponse = document.OFX.CREDITCARDMSGSRSV1?.CCSTMTTRNRS;
|
|
39
|
+
if (ccResponse) {
|
|
40
|
+
for (const r of toArray(ccResponse)) {
|
|
41
|
+
const txns = r.CCSTMTRS?.BANKTRANLIST?.STMTTRN;
|
|
42
|
+
if (txns)
|
|
43
|
+
results.push(...txns);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
48
|
+
function getAccountInfo(document) {
|
|
49
|
+
const results = [];
|
|
50
|
+
const bankResponse = document.OFX.BANKMSGSRSV1?.STMTTRNRS;
|
|
51
|
+
if (bankResponse) {
|
|
52
|
+
for (const r of toArray(bankResponse)) {
|
|
53
|
+
const account = r.STMTRS?.BANKACCTFROM;
|
|
54
|
+
if (account)
|
|
55
|
+
results.push(account);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const ccResponse = document.OFX.CREDITCARDMSGSRSV1?.CCSTMTTRNRS;
|
|
59
|
+
if (ccResponse) {
|
|
60
|
+
for (const r of toArray(ccResponse)) {
|
|
61
|
+
const account = r.CCSTMTRS?.CCACCTFROM;
|
|
62
|
+
if (account)
|
|
63
|
+
results.push(account);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return results;
|
|
67
|
+
}
|
|
68
|
+
function getBalance(document) {
|
|
69
|
+
const results = [];
|
|
70
|
+
const bankResponse = document.OFX.BANKMSGSRSV1?.STMTTRNRS;
|
|
71
|
+
if (bankResponse) {
|
|
72
|
+
for (const r of toArray(bankResponse)) {
|
|
73
|
+
if (r.STMTRS) {
|
|
74
|
+
results.push({
|
|
75
|
+
available: r.STMTRS.AVAILBAL,
|
|
76
|
+
ledger: r.STMTRS.LEDGERBAL
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const ccResponse = document.OFX.CREDITCARDMSGSRSV1?.CCSTMTTRNRS;
|
|
82
|
+
if (ccResponse) {
|
|
83
|
+
for (const r of toArray(ccResponse)) {
|
|
84
|
+
if (r.CCSTMTRS) {
|
|
85
|
+
results.push({
|
|
86
|
+
available: r.CCSTMTRS.AVAILBAL,
|
|
87
|
+
ledger: r.CCSTMTRS.LEDGERBAL
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return results;
|
|
93
|
+
}
|
|
94
|
+
function getSignOnInfo(document) {
|
|
95
|
+
return document.OFX.SIGNONMSGSRSV1.SONRS;
|
|
96
|
+
}
|
|
97
|
+
// src/generator.ts
|
|
98
|
+
function generateHeader(options) {
|
|
99
|
+
const version = options?.version ?? "100";
|
|
100
|
+
const encoding = options?.encoding ?? "USASCII";
|
|
101
|
+
const charset = options?.charset ?? "1252";
|
|
102
|
+
return [
|
|
103
|
+
"OFXHEADER:100",
|
|
104
|
+
"DATA:OFXSGML",
|
|
105
|
+
`VERSION:${version}`,
|
|
106
|
+
"SECURITY:NONE",
|
|
107
|
+
`ENCODING:${encoding}`,
|
|
108
|
+
`CHARSET:${charset}`,
|
|
109
|
+
"COMPRESSION:NONE",
|
|
110
|
+
"OLDFILEUID:NONE",
|
|
111
|
+
"NEWFILEUID:NONE",
|
|
112
|
+
""
|
|
113
|
+
].join(`
|
|
114
|
+
`);
|
|
115
|
+
}
|
|
116
|
+
function generateTransaction(trn) {
|
|
117
|
+
const lines = [
|
|
118
|
+
"<STMTTRN>",
|
|
119
|
+
`<TRNTYPE>${trn.type}`,
|
|
120
|
+
`<DTPOSTED>${formatOfxDate(trn.datePosted)}`,
|
|
121
|
+
`<TRNAMT>${formatAmount(trn.amount)}`,
|
|
122
|
+
`<FITID>${escapeOfxText(trn.fitId)}`
|
|
123
|
+
];
|
|
124
|
+
if (trn.name) {
|
|
125
|
+
lines.push(`<NAME>${escapeOfxText(trn.name)}`);
|
|
126
|
+
}
|
|
127
|
+
if (trn.memo) {
|
|
128
|
+
lines.push(`<MEMO>${escapeOfxText(trn.memo)}`);
|
|
129
|
+
}
|
|
130
|
+
if (trn.checkNum) {
|
|
131
|
+
lines.push(`<CHECKNUM>${escapeOfxText(trn.checkNum)}`);
|
|
132
|
+
}
|
|
133
|
+
if (trn.refNum) {
|
|
134
|
+
lines.push(`<REFNUM>${escapeOfxText(trn.refNum)}`);
|
|
135
|
+
}
|
|
136
|
+
lines.push("</STMTTRN>");
|
|
137
|
+
return lines.join(`
|
|
138
|
+
`);
|
|
139
|
+
}
|
|
140
|
+
function generateBankStatement(options) {
|
|
141
|
+
const parts = [generateHeader()];
|
|
142
|
+
const serverDate = formatOfxDate(new Date);
|
|
143
|
+
const language = options.language ?? "POR";
|
|
144
|
+
parts.push(`<OFX>
|
|
145
|
+
<SIGNONMSGSRSV1>
|
|
146
|
+
<SONRS>
|
|
147
|
+
<STATUS>
|
|
148
|
+
<CODE>0
|
|
149
|
+
<SEVERITY>INFO
|
|
150
|
+
</STATUS>
|
|
151
|
+
<DTSERVER>${serverDate}
|
|
152
|
+
<LANGUAGE>${language}`);
|
|
153
|
+
if (options.financialInstitution) {
|
|
154
|
+
parts.push("<FI>");
|
|
155
|
+
if (options.financialInstitution.org) {
|
|
156
|
+
parts.push(`<ORG>${escapeOfxText(options.financialInstitution.org)}`);
|
|
157
|
+
}
|
|
158
|
+
if (options.financialInstitution.fid) {
|
|
159
|
+
parts.push(`<FID>${escapeOfxText(options.financialInstitution.fid)}`);
|
|
160
|
+
}
|
|
161
|
+
parts.push("</FI>");
|
|
162
|
+
}
|
|
163
|
+
parts.push(`</SONRS>
|
|
164
|
+
</SIGNONMSGSRSV1>
|
|
165
|
+
<BANKMSGSRSV1>
|
|
166
|
+
<STMTTRNRS>
|
|
167
|
+
<TRNUID>0
|
|
168
|
+
<STATUS>
|
|
169
|
+
<CODE>0
|
|
170
|
+
<SEVERITY>INFO
|
|
171
|
+
</STATUS>
|
|
172
|
+
<STMTRS>
|
|
173
|
+
<CURDEF>${options.currency}
|
|
174
|
+
<BANKACCTFROM>
|
|
175
|
+
<BANKID>${escapeOfxText(options.bankId)}
|
|
176
|
+
<ACCTID>${escapeOfxText(options.accountId)}
|
|
177
|
+
<ACCTTYPE>${options.accountType}
|
|
178
|
+
</BANKACCTFROM>
|
|
179
|
+
<BANKTRANLIST>
|
|
180
|
+
<DTSTART>${formatOfxDate(options.startDate)}
|
|
181
|
+
<DTEND>${formatOfxDate(options.endDate)}`);
|
|
182
|
+
for (const trn of options.transactions) {
|
|
183
|
+
parts.push(generateTransaction(trn));
|
|
184
|
+
}
|
|
185
|
+
parts.push("</BANKTRANLIST>");
|
|
186
|
+
if (options.ledgerBalance) {
|
|
187
|
+
parts.push(`<LEDGERBAL>
|
|
188
|
+
<BALAMT>${formatAmount(options.ledgerBalance.amount)}
|
|
189
|
+
<DTASOF>${formatOfxDate(options.ledgerBalance.asOfDate)}
|
|
190
|
+
</LEDGERBAL>`);
|
|
191
|
+
}
|
|
192
|
+
if (options.availableBalance) {
|
|
193
|
+
parts.push(`<AVAILBAL>
|
|
194
|
+
<BALAMT>${formatAmount(options.availableBalance.amount)}
|
|
195
|
+
<DTASOF>${formatOfxDate(options.availableBalance.asOfDate)}
|
|
196
|
+
</AVAILBAL>`);
|
|
197
|
+
}
|
|
198
|
+
parts.push(`</STMTRS>
|
|
199
|
+
</STMTTRNRS>
|
|
200
|
+
</BANKMSGSRSV1>
|
|
201
|
+
</OFX>`);
|
|
202
|
+
return parts.join(`
|
|
203
|
+
`);
|
|
204
|
+
}
|
|
205
|
+
function generateCreditCardStatement(options) {
|
|
206
|
+
const parts = [generateHeader()];
|
|
207
|
+
const serverDate = formatOfxDate(new Date);
|
|
208
|
+
const language = options.language ?? "POR";
|
|
209
|
+
parts.push(`<OFX>
|
|
210
|
+
<SIGNONMSGSRSV1>
|
|
211
|
+
<SONRS>
|
|
212
|
+
<STATUS>
|
|
213
|
+
<CODE>0
|
|
214
|
+
<SEVERITY>INFO
|
|
215
|
+
</STATUS>
|
|
216
|
+
<DTSERVER>${serverDate}
|
|
217
|
+
<LANGUAGE>${language}`);
|
|
218
|
+
if (options.financialInstitution) {
|
|
219
|
+
parts.push("<FI>");
|
|
220
|
+
if (options.financialInstitution.org) {
|
|
221
|
+
parts.push(`<ORG>${escapeOfxText(options.financialInstitution.org)}`);
|
|
222
|
+
}
|
|
223
|
+
if (options.financialInstitution.fid) {
|
|
224
|
+
parts.push(`<FID>${escapeOfxText(options.financialInstitution.fid)}`);
|
|
225
|
+
}
|
|
226
|
+
parts.push("</FI>");
|
|
227
|
+
}
|
|
228
|
+
parts.push(`</SONRS>
|
|
229
|
+
</SIGNONMSGSRSV1>
|
|
230
|
+
<CREDITCARDMSGSRSV1>
|
|
231
|
+
<CCSTMTTRNRS>
|
|
232
|
+
<TRNUID>0
|
|
233
|
+
<STATUS>
|
|
234
|
+
<CODE>0
|
|
235
|
+
<SEVERITY>INFO
|
|
236
|
+
</STATUS>
|
|
237
|
+
<CCSTMTRS>
|
|
238
|
+
<CURDEF>${options.currency}
|
|
239
|
+
<CCACCTFROM>
|
|
240
|
+
<ACCTID>${escapeOfxText(options.accountId)}
|
|
241
|
+
</CCACCTFROM>
|
|
242
|
+
<BANKTRANLIST>
|
|
243
|
+
<DTSTART>${formatOfxDate(options.startDate)}
|
|
244
|
+
<DTEND>${formatOfxDate(options.endDate)}`);
|
|
245
|
+
for (const trn of options.transactions) {
|
|
246
|
+
parts.push(generateTransaction(trn));
|
|
247
|
+
}
|
|
248
|
+
parts.push("</BANKTRANLIST>");
|
|
249
|
+
if (options.ledgerBalance) {
|
|
250
|
+
parts.push(`<LEDGERBAL>
|
|
251
|
+
<BALAMT>${formatAmount(options.ledgerBalance.amount)}
|
|
252
|
+
<DTASOF>${formatOfxDate(options.ledgerBalance.asOfDate)}
|
|
253
|
+
</LEDGERBAL>`);
|
|
254
|
+
}
|
|
255
|
+
if (options.availableBalance) {
|
|
256
|
+
parts.push(`<AVAILBAL>
|
|
257
|
+
<BALAMT>${formatAmount(options.availableBalance.amount)}
|
|
258
|
+
<DTASOF>${formatOfxDate(options.availableBalance.asOfDate)}
|
|
259
|
+
</AVAILBAL>`);
|
|
260
|
+
}
|
|
261
|
+
parts.push(`</CCSTMTRS>
|
|
262
|
+
</CCSTMTTRNRS>
|
|
263
|
+
</CREDITCARDMSGSRSV1>
|
|
264
|
+
</OFX>`);
|
|
265
|
+
return parts.join(`
|
|
266
|
+
`);
|
|
267
|
+
}
|
|
268
|
+
// src/parser.ts
|
|
269
|
+
import { z as z2 } from "zod";
|
|
270
|
+
|
|
271
|
+
// src/schemas.ts
|
|
2
272
|
import { z } from "zod";
|
|
3
|
-
var toInt = (val) => Number.parseInt(val, 10);
|
|
4
273
|
var toFloat = (val) => Number.parseFloat(val);
|
|
5
|
-
var
|
|
274
|
+
var DATE_REGEX = /^(\d{4})(\d{2})(\d{2})(\d{2})?(\d{2})?(\d{2})?/;
|
|
275
|
+
var TIMEZONE_REGEX = /\[([+-]?\d+):(\w+)\]/;
|
|
6
276
|
function parseDateComponents(val) {
|
|
277
|
+
const m = DATE_REGEX.exec(val);
|
|
278
|
+
if (!m)
|
|
279
|
+
return { day: 0, hour: 0, minute: 0, month: 0, second: 0, year: 0 };
|
|
7
280
|
return {
|
|
8
|
-
day:
|
|
9
|
-
hour:
|
|
10
|
-
minute:
|
|
11
|
-
month:
|
|
12
|
-
second:
|
|
13
|
-
year:
|
|
281
|
+
day: +m[3],
|
|
282
|
+
hour: +(m[4] || 0),
|
|
283
|
+
minute: +(m[5] || 0),
|
|
284
|
+
month: +m[2],
|
|
285
|
+
second: +(m[6] || 0),
|
|
286
|
+
year: +m[1]
|
|
14
287
|
};
|
|
15
288
|
}
|
|
16
289
|
function parseTimezone(val) {
|
|
17
|
-
const match =
|
|
290
|
+
const match = TIMEZONE_REGEX.exec(val);
|
|
18
291
|
return {
|
|
19
292
|
name: match?.[2] ?? "UTC",
|
|
20
|
-
offset: match ?
|
|
293
|
+
offset: match ? +match[1] : 0
|
|
21
294
|
};
|
|
22
295
|
}
|
|
23
296
|
var ofxDateSchema = z.string().transform((val) => {
|
|
@@ -194,28 +467,18 @@ var schemas = {
|
|
|
194
467
|
transactionList: transactionListSchema,
|
|
195
468
|
transactionType: transactionTypeSchema
|
|
196
469
|
};
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
header[match[1]] = match[2];
|
|
210
|
-
}
|
|
211
|
-
if (line === "" && Object.keys(header).length > 0) {
|
|
212
|
-
bodyStartIndex = i + 1;
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
const body = lines.slice(bodyStartIndex).join(`
|
|
217
|
-
`);
|
|
218
|
-
return { body, header: ofxHeaderSchema.parse(header) };
|
|
470
|
+
|
|
471
|
+
// src/parser.ts
|
|
472
|
+
var ENTITY_MAP = {
|
|
473
|
+
"&": "&",
|
|
474
|
+
"'": "'",
|
|
475
|
+
">": ">",
|
|
476
|
+
"<": "<",
|
|
477
|
+
""": '"'
|
|
478
|
+
};
|
|
479
|
+
var ENTITY_REGEX = /&(?:amp|lt|gt|quot|apos);/g;
|
|
480
|
+
function decodeEntities(text) {
|
|
481
|
+
return text.replace(ENTITY_REGEX, (match) => ENTITY_MAP[match] ?? match);
|
|
219
482
|
}
|
|
220
483
|
function addToContent(content, key, value) {
|
|
221
484
|
const existing = content[key];
|
|
@@ -233,7 +496,8 @@ function sgmlToObject(sgml) {
|
|
|
233
496
|
const result = {};
|
|
234
497
|
const tagStack = [{ content: result, name: "root" }];
|
|
235
498
|
const stackMap = new Map([["root", 0]]);
|
|
236
|
-
const
|
|
499
|
+
const hasSpecialContent = sgml.includes("<?") || sgml.includes("<!--");
|
|
500
|
+
const cleanSgml = hasSpecialContent ? sgml.replace(/<\?.*?\?>|<!--.*?-->/gs, "").trim() : sgml.trim();
|
|
237
501
|
const tagRegex = /<(\/?)([\w.]+)>([^<]*)/g;
|
|
238
502
|
let match = tagRegex.exec(cleanSgml);
|
|
239
503
|
while (match !== null) {
|
|
@@ -260,7 +524,8 @@ function sgmlToObject(sgml) {
|
|
|
260
524
|
tagStack.length = stackIndex;
|
|
261
525
|
}
|
|
262
526
|
} else if (textContent) {
|
|
263
|
-
|
|
527
|
+
const decoded = textContent.includes("&") ? decodeEntities(textContent) : textContent;
|
|
528
|
+
addToContent(current.content, tagName, decoded);
|
|
264
529
|
} else {
|
|
265
530
|
const newObj = {};
|
|
266
531
|
addToContent(current.content, tagName, newObj);
|
|
@@ -271,24 +536,82 @@ function sgmlToObject(sgml) {
|
|
|
271
536
|
}
|
|
272
537
|
return result;
|
|
273
538
|
}
|
|
274
|
-
function
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
539
|
+
function normalizeResponseArray(msgs, responseKey, statementKey) {
|
|
540
|
+
const responses = msgs[responseKey];
|
|
541
|
+
if (!responses)
|
|
542
|
+
return;
|
|
543
|
+
for (const response of toArray(responses)) {
|
|
544
|
+
const stmt = response?.[statementKey];
|
|
545
|
+
const tranList = stmt?.BANKTRANLIST;
|
|
546
|
+
if (tranList?.STMTTRN !== undefined) {
|
|
547
|
+
tranList.STMTTRN = toArray(tranList.STMTTRN);
|
|
283
548
|
}
|
|
284
549
|
}
|
|
285
|
-
return processed;
|
|
286
550
|
}
|
|
287
551
|
function normalizeTransactions(data) {
|
|
288
|
-
|
|
552
|
+
const ofx = data.OFX;
|
|
553
|
+
if (!ofx)
|
|
554
|
+
return data;
|
|
555
|
+
const bankMsgs = ofx.BANKMSGSRSV1;
|
|
556
|
+
if (bankMsgs) {
|
|
557
|
+
normalizeResponseArray(bankMsgs, "STMTTRNRS", "STMTRS");
|
|
558
|
+
}
|
|
559
|
+
const ccMsgs = ofx.CREDITCARDMSGSRSV1;
|
|
560
|
+
if (ccMsgs) {
|
|
561
|
+
normalizeResponseArray(ccMsgs, "CCSTMTTRNRS", "CCSTMTRS");
|
|
562
|
+
}
|
|
563
|
+
return data;
|
|
564
|
+
}
|
|
565
|
+
function parseHeader(content) {
|
|
566
|
+
const lines = content.split(/\r?\n/);
|
|
567
|
+
const header = {};
|
|
568
|
+
let bodyStartIndex = 0;
|
|
569
|
+
for (let i = 0;i < lines.length; i++) {
|
|
570
|
+
const line = lines[i]?.trim() ?? "";
|
|
571
|
+
if (line.startsWith("<?xml") || line.startsWith("<OFX>")) {
|
|
572
|
+
bodyStartIndex = i;
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
const match = line.match(/^(\w+):(.*)$/);
|
|
576
|
+
if (match?.[1] && match[2] !== undefined) {
|
|
577
|
+
header[match[1]] = match[2];
|
|
578
|
+
}
|
|
579
|
+
if (line === "" && Object.keys(header).length > 0) {
|
|
580
|
+
bodyStartIndex = i + 1;
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const body = lines.slice(bodyStartIndex).join(`
|
|
585
|
+
`);
|
|
586
|
+
return { body, header: ofxHeaderSchema.parse(header) };
|
|
289
587
|
}
|
|
290
588
|
function parse(content) {
|
|
291
589
|
try {
|
|
590
|
+
if (typeof content !== "string") {
|
|
591
|
+
return {
|
|
592
|
+
error: new z2.ZodError([
|
|
593
|
+
{
|
|
594
|
+
code: "invalid_type",
|
|
595
|
+
expected: "string",
|
|
596
|
+
message: "Expected string, received " + typeof content,
|
|
597
|
+
path: []
|
|
598
|
+
}
|
|
599
|
+
]),
|
|
600
|
+
success: false
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
if (content.trim() === "") {
|
|
604
|
+
return {
|
|
605
|
+
error: new z2.ZodError([
|
|
606
|
+
{
|
|
607
|
+
code: "custom",
|
|
608
|
+
message: "Content cannot be empty",
|
|
609
|
+
path: []
|
|
610
|
+
}
|
|
611
|
+
]),
|
|
612
|
+
success: false
|
|
613
|
+
};
|
|
614
|
+
}
|
|
292
615
|
const { header, body } = parseHeader(content);
|
|
293
616
|
const rawData = sgmlToObject(body);
|
|
294
617
|
const normalizedData = normalizeTransactions(rawData);
|
|
@@ -301,7 +624,7 @@ function parse(content) {
|
|
|
301
624
|
success: true
|
|
302
625
|
};
|
|
303
626
|
} catch (err) {
|
|
304
|
-
if (err instanceof
|
|
627
|
+
if (err instanceof z2.ZodError) {
|
|
305
628
|
return { error: err, success: false };
|
|
306
629
|
}
|
|
307
630
|
throw err;
|
|
@@ -314,56 +637,244 @@ function parseOrThrow(content) {
|
|
|
314
637
|
}
|
|
315
638
|
return result.data;
|
|
316
639
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
640
|
+
// src/stream.ts
|
|
641
|
+
var ENTITY_MAP2 = {
|
|
642
|
+
"&": "&",
|
|
643
|
+
"'": "'",
|
|
644
|
+
">": ">",
|
|
645
|
+
"<": "<",
|
|
646
|
+
""": '"'
|
|
647
|
+
};
|
|
648
|
+
var ENTITY_REGEX2 = /&(?:amp|lt|gt|quot|apos);/g;
|
|
649
|
+
function decodeEntities2(text) {
|
|
650
|
+
if (!text.includes("&"))
|
|
651
|
+
return text;
|
|
652
|
+
return text.replace(ENTITY_REGEX2, (match) => ENTITY_MAP2[match] ?? match);
|
|
329
653
|
}
|
|
330
|
-
function
|
|
331
|
-
const
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
654
|
+
function parseHeaderFromBuffer(buffer) {
|
|
655
|
+
const lines = buffer.split(/\r?\n/);
|
|
656
|
+
const header = {};
|
|
657
|
+
let bodyStartIndex = 0;
|
|
658
|
+
for (let i = 0;i < lines.length; i++) {
|
|
659
|
+
const line = lines[i]?.trim() ?? "";
|
|
660
|
+
if (line.startsWith("<?xml") || line.startsWith("<OFX>")) {
|
|
661
|
+
bodyStartIndex = i;
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
const match = line.match(/^(\w+):(.*)$/);
|
|
665
|
+
if (match?.[1] && match[2] !== undefined) {
|
|
666
|
+
header[match[1]] = match[2];
|
|
667
|
+
}
|
|
668
|
+
if (line === "" && Object.keys(header).length > 0) {
|
|
669
|
+
bodyStartIndex = i + 1;
|
|
670
|
+
break;
|
|
339
671
|
}
|
|
340
672
|
}
|
|
341
|
-
|
|
673
|
+
if (Object.keys(header).length === 0)
|
|
674
|
+
return null;
|
|
675
|
+
const headerResult = ofxHeaderSchema.safeParse(header);
|
|
676
|
+
if (!headerResult.success)
|
|
677
|
+
return null;
|
|
678
|
+
const bodyStartChar = lines.slice(0, bodyStartIndex).join(`
|
|
679
|
+
`).length + 1;
|
|
680
|
+
return { bodyStart: bodyStartChar, header: headerResult.data };
|
|
342
681
|
}
|
|
343
|
-
function
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
return [...bankTransactions, ...ccTransactions].flat();
|
|
682
|
+
function tryParseTransaction(obj) {
|
|
683
|
+
const result = transactionSchema.safeParse(obj);
|
|
684
|
+
return result.success ? result.data : null;
|
|
347
685
|
}
|
|
348
|
-
function
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
return [...bankAccounts, ...ccAccounts];
|
|
686
|
+
function tryParseBankAccount(obj) {
|
|
687
|
+
const result = bankAccountSchema.safeParse(obj);
|
|
688
|
+
return result.success ? result.data : null;
|
|
352
689
|
}
|
|
353
|
-
function
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
return [...bankBalances, ...ccBalances];
|
|
690
|
+
function tryParseCreditCardAccount(obj) {
|
|
691
|
+
const result = creditCardAccountSchema.safeParse(obj);
|
|
692
|
+
return result.success ? result.data : null;
|
|
357
693
|
}
|
|
358
|
-
function
|
|
359
|
-
|
|
694
|
+
function tryParseBalance(obj) {
|
|
695
|
+
const result = balanceSchema.safeParse(obj);
|
|
696
|
+
return result.success ? result.data : null;
|
|
697
|
+
}
|
|
698
|
+
async function* parseStream(input) {
|
|
699
|
+
const state = {
|
|
700
|
+
buffer: "",
|
|
701
|
+
currentObject: {},
|
|
702
|
+
currentPath: [],
|
|
703
|
+
headerParsed: false,
|
|
704
|
+
inHeader: true,
|
|
705
|
+
objectStack: [{}],
|
|
706
|
+
transactionCount: 0
|
|
707
|
+
};
|
|
708
|
+
const decoder = new TextDecoder;
|
|
709
|
+
const tagRegex = /<(\/?)([\w.]+)>([^<]*)/g;
|
|
710
|
+
let pendingLedgerBalance;
|
|
711
|
+
let pendingAvailableBalance;
|
|
712
|
+
let emittedBalanceForCurrentStatement = false;
|
|
713
|
+
async function* processChunk(chunk, isLast = false) {
|
|
714
|
+
state.buffer += chunk;
|
|
715
|
+
if (!state.headerParsed) {
|
|
716
|
+
const headerResult = parseHeaderFromBuffer(state.buffer);
|
|
717
|
+
if (headerResult) {
|
|
718
|
+
state.headerParsed = true;
|
|
719
|
+
state.inHeader = false;
|
|
720
|
+
yield { data: headerResult.header, type: "header" };
|
|
721
|
+
state.buffer = state.buffer.slice(headerResult.bodyStart);
|
|
722
|
+
} else {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const lastLt = state.buffer.lastIndexOf("<");
|
|
727
|
+
const safeEnd = isLast ? state.buffer.length : lastLt;
|
|
728
|
+
if (safeEnd <= 0)
|
|
729
|
+
return;
|
|
730
|
+
const safeBuffer = state.buffer.slice(0, safeEnd);
|
|
731
|
+
let processedUpTo = 0;
|
|
732
|
+
tagRegex.lastIndex = 0;
|
|
733
|
+
for (let match = tagRegex.exec(safeBuffer);match !== null; match = tagRegex.exec(safeBuffer)) {
|
|
734
|
+
const isClosing = match[1] === "/";
|
|
735
|
+
const tagName = match[2];
|
|
736
|
+
const textContent = match[3]?.trim() ?? "";
|
|
737
|
+
if (!tagName)
|
|
738
|
+
continue;
|
|
739
|
+
const currentObj = state.objectStack[state.objectStack.length - 1];
|
|
740
|
+
if (!currentObj)
|
|
741
|
+
continue;
|
|
742
|
+
if (isClosing) {
|
|
743
|
+
if (tagName === "STMTTRN") {
|
|
744
|
+
const txn = tryParseTransaction(currentObj);
|
|
745
|
+
if (txn) {
|
|
746
|
+
state.transactionCount++;
|
|
747
|
+
yield { data: txn, type: "transaction" };
|
|
748
|
+
}
|
|
749
|
+
} else if (tagName === "BANKACCTFROM") {
|
|
750
|
+
const account = tryParseBankAccount(currentObj);
|
|
751
|
+
if (account) {
|
|
752
|
+
yield { data: account, type: "account" };
|
|
753
|
+
}
|
|
754
|
+
} else if (tagName === "CCACCTFROM") {
|
|
755
|
+
const account = tryParseCreditCardAccount(currentObj);
|
|
756
|
+
if (account) {
|
|
757
|
+
yield { data: account, type: "account" };
|
|
758
|
+
}
|
|
759
|
+
} else if (tagName === "LEDGERBAL") {
|
|
760
|
+
pendingLedgerBalance = tryParseBalance(currentObj) ?? undefined;
|
|
761
|
+
} else if (tagName === "AVAILBAL") {
|
|
762
|
+
pendingAvailableBalance = tryParseBalance(currentObj) ?? undefined;
|
|
763
|
+
} else if ((tagName === "STMTRS" || tagName === "CCSTMTRS") && !emittedBalanceForCurrentStatement) {
|
|
764
|
+
if (pendingLedgerBalance || pendingAvailableBalance) {
|
|
765
|
+
yield {
|
|
766
|
+
data: {
|
|
767
|
+
available: pendingAvailableBalance,
|
|
768
|
+
ledger: pendingLedgerBalance
|
|
769
|
+
},
|
|
770
|
+
type: "balance"
|
|
771
|
+
};
|
|
772
|
+
emittedBalanceForCurrentStatement = true;
|
|
773
|
+
}
|
|
774
|
+
} else if (tagName === "STMTTRNRS" || tagName === "CCSTMTTRNRS") {
|
|
775
|
+
pendingLedgerBalance = undefined;
|
|
776
|
+
pendingAvailableBalance = undefined;
|
|
777
|
+
emittedBalanceForCurrentStatement = false;
|
|
778
|
+
}
|
|
779
|
+
const pathIndex = state.currentPath.lastIndexOf(tagName);
|
|
780
|
+
if (pathIndex !== -1) {
|
|
781
|
+
state.currentPath.length = pathIndex;
|
|
782
|
+
state.objectStack.length = Math.max(pathIndex + 1, 1);
|
|
783
|
+
}
|
|
784
|
+
} else if (textContent) {
|
|
785
|
+
const decoded = decodeEntities2(textContent);
|
|
786
|
+
const existing = currentObj[tagName];
|
|
787
|
+
if (existing !== undefined) {
|
|
788
|
+
if (Array.isArray(existing)) {
|
|
789
|
+
existing.push(decoded);
|
|
790
|
+
} else {
|
|
791
|
+
currentObj[tagName] = [existing, decoded];
|
|
792
|
+
}
|
|
793
|
+
} else {
|
|
794
|
+
currentObj[tagName] = decoded;
|
|
795
|
+
}
|
|
796
|
+
} else {
|
|
797
|
+
const newObj = {};
|
|
798
|
+
const existing = currentObj[tagName];
|
|
799
|
+
if (existing !== undefined) {
|
|
800
|
+
if (Array.isArray(existing)) {
|
|
801
|
+
existing.push(newObj);
|
|
802
|
+
} else {
|
|
803
|
+
currentObj[tagName] = [existing, newObj];
|
|
804
|
+
}
|
|
805
|
+
} else {
|
|
806
|
+
currentObj[tagName] = newObj;
|
|
807
|
+
}
|
|
808
|
+
state.currentPath.push(tagName);
|
|
809
|
+
state.objectStack.push(newObj);
|
|
810
|
+
}
|
|
811
|
+
processedUpTo = tagRegex.lastIndex;
|
|
812
|
+
}
|
|
813
|
+
if (processedUpTo > 0) {
|
|
814
|
+
state.buffer = state.buffer.slice(processedUpTo);
|
|
815
|
+
}
|
|
816
|
+
tagRegex.lastIndex = 0;
|
|
817
|
+
}
|
|
818
|
+
if (input instanceof ReadableStream) {
|
|
819
|
+
const reader = input.getReader();
|
|
820
|
+
try {
|
|
821
|
+
while (true) {
|
|
822
|
+
const { done, value } = await reader.read();
|
|
823
|
+
if (done)
|
|
824
|
+
break;
|
|
825
|
+
yield* processChunk(decoder.decode(value, { stream: true }));
|
|
826
|
+
}
|
|
827
|
+
yield* processChunk(decoder.decode(), true);
|
|
828
|
+
} finally {
|
|
829
|
+
reader.releaseLock();
|
|
830
|
+
}
|
|
831
|
+
} else {
|
|
832
|
+
const chunks = [];
|
|
833
|
+
for await (const chunk of input) {
|
|
834
|
+
chunks.push(chunk);
|
|
835
|
+
}
|
|
836
|
+
for (let i = 0;i < chunks.length; i++) {
|
|
837
|
+
yield* processChunk(chunks[i] ?? "", i === chunks.length - 1);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
yield { transactionCount: state.transactionCount, type: "complete" };
|
|
841
|
+
}
|
|
842
|
+
async function parseStreamToArray(input) {
|
|
843
|
+
const result = {
|
|
844
|
+
accounts: [],
|
|
845
|
+
balances: [],
|
|
846
|
+
transactions: []
|
|
847
|
+
};
|
|
848
|
+
for await (const event of parseStream(input)) {
|
|
849
|
+
switch (event.type) {
|
|
850
|
+
case "header":
|
|
851
|
+
result.header = event.data;
|
|
852
|
+
break;
|
|
853
|
+
case "transaction":
|
|
854
|
+
result.transactions.push(event.data);
|
|
855
|
+
break;
|
|
856
|
+
case "account":
|
|
857
|
+
result.accounts.push(event.data);
|
|
858
|
+
break;
|
|
859
|
+
case "balance":
|
|
860
|
+
result.balances.push(event.data);
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return result;
|
|
360
865
|
}
|
|
361
866
|
export {
|
|
362
867
|
schemas,
|
|
868
|
+
parseStreamToArray,
|
|
869
|
+
parseStream,
|
|
363
870
|
parseOrThrow,
|
|
364
871
|
parse,
|
|
365
872
|
getTransactions,
|
|
366
873
|
getSignOnInfo,
|
|
367
874
|
getBalance,
|
|
368
|
-
getAccountInfo
|
|
875
|
+
getAccountInfo,
|
|
876
|
+
generateHeader,
|
|
877
|
+
generateCreditCardStatement,
|
|
878
|
+
generateBankStatement,
|
|
879
|
+
formatOfxDate
|
|
369
880
|
};
|
package/package.json
CHANGED
|
@@ -7,26 +7,27 @@
|
|
|
7
7
|
},
|
|
8
8
|
"description": "Typesafe ofx handling",
|
|
9
9
|
"devDependencies": {
|
|
10
|
-
"@biomejs/biome": "
|
|
10
|
+
"@biomejs/biome": "2.3.8",
|
|
11
11
|
"@types/bun": "1.3.3",
|
|
12
12
|
"bumpp": "10.3.2",
|
|
13
13
|
"bunup": "0.16.10",
|
|
14
|
-
"simple-git-hooks": "2.13.1",
|
|
15
14
|
"typescript": "5.9.3"
|
|
16
15
|
},
|
|
17
16
|
"exports": {
|
|
18
17
|
".": {
|
|
18
|
+
"bun": "./src/index.ts",
|
|
19
19
|
"import": {
|
|
20
20
|
"default": "./dist/index.js",
|
|
21
21
|
"types": "./dist/index.d.ts"
|
|
22
|
-
}
|
|
22
|
+
},
|
|
23
|
+
"types": "./src/index.ts"
|
|
23
24
|
},
|
|
24
25
|
"./package.json": "./package.json"
|
|
25
26
|
},
|
|
26
27
|
"files": [
|
|
27
28
|
"dist"
|
|
28
29
|
],
|
|
29
|
-
"homepage": "https://github.com/F-O-T/montte-nx
|
|
30
|
+
"homepage": "https://github.com/F-O-T/montte-nx/blob/master/libraries/ofx",
|
|
30
31
|
"license": "MIT",
|
|
31
32
|
"module": "./dist/index.js",
|
|
32
33
|
"name": "@f-o-t/ofx",
|
|
@@ -48,20 +49,16 @@
|
|
|
48
49
|
},
|
|
49
50
|
"scripts": {
|
|
50
51
|
"build": "bunup",
|
|
52
|
+
"check": "biome check --write .",
|
|
51
53
|
"dev": "bunup --watch",
|
|
52
|
-
"
|
|
53
|
-
"lint:fix": "biome check --write .",
|
|
54
|
-
"postinstall": "bun simple-git-hooks",
|
|
54
|
+
"publish": "bunx npm publish",
|
|
55
55
|
"release": "bumpp --commit --push --tag",
|
|
56
56
|
"test": "bun test",
|
|
57
57
|
"test:coverage": "bun test --coverage",
|
|
58
58
|
"test:watch": "bun test --watch",
|
|
59
59
|
"type-check": "tsc --noEmit"
|
|
60
60
|
},
|
|
61
|
-
"simple-git-hooks": {
|
|
62
|
-
"pre-commit": "bun run lint && bun run type-check"
|
|
63
|
-
},
|
|
64
61
|
"type": "module",
|
|
65
62
|
"types": "./dist/index.d.ts",
|
|
66
|
-
"version": "1.
|
|
63
|
+
"version": "1.3.0"
|
|
67
64
|
}
|