@f-o-t/ofx 1.1.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/LICENSE.md +10 -0
- package/README.md +276 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +357 -0
- package/package.json +67 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
|
|
2
|
+
MIT License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2025 FOT
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
7
|
+
|
|
8
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# @fot/ofx
|
|
2
|
+
|
|
3
|
+
Type-safe OFX (Open Financial Exchange) parser with Zod schema validation.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @fot/ofx
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { parse, getTransactions, getBalance } from "@fot/ofx";
|
|
15
|
+
|
|
16
|
+
const ofxContent = fs.readFileSync("statement.ofx", "utf-8");
|
|
17
|
+
const result = parse(ofxContent);
|
|
18
|
+
|
|
19
|
+
if (result.success) {
|
|
20
|
+
const transactions = getTransactions(result.data);
|
|
21
|
+
const balances = getBalance(result.data);
|
|
22
|
+
|
|
23
|
+
for (const txn of transactions) {
|
|
24
|
+
console.log(`${txn.DTPOSTED.toDate()} - ${txn.NAME}: ${txn.TRNAMT}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## API Reference
|
|
30
|
+
|
|
31
|
+
### Parsing Functions
|
|
32
|
+
|
|
33
|
+
#### `parse(content: string): ParseResult<OFXDocument>`
|
|
34
|
+
|
|
35
|
+
Parses an OFX file content and returns a result object.
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
const result = parse(ofxContent);
|
|
39
|
+
|
|
40
|
+
if (result.success) {
|
|
41
|
+
console.log(result.data);
|
|
42
|
+
} else {
|
|
43
|
+
console.error(result.error);
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
#### `parseOrThrow(content: string): OFXDocument`
|
|
48
|
+
|
|
49
|
+
Parses an OFX file content and throws on validation errors.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
try {
|
|
53
|
+
const doc = parseOrThrow(ofxContent);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// ZodError
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Extraction Functions
|
|
60
|
+
|
|
61
|
+
#### `getTransactions(document: OFXDocument): OFXTransaction[]`
|
|
62
|
+
|
|
63
|
+
Extracts all transactions from bank and credit card statements.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const transactions = getTransactions(doc);
|
|
67
|
+
|
|
68
|
+
for (const txn of transactions) {
|
|
69
|
+
console.log({
|
|
70
|
+
type: txn.TRNTYPE,
|
|
71
|
+
amount: txn.TRNAMT,
|
|
72
|
+
date: txn.DTPOSTED.toDate(),
|
|
73
|
+
name: txn.NAME,
|
|
74
|
+
memo: txn.MEMO,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
#### `getAccountInfo(document: OFXDocument): (OFXBankAccount | OFXCreditCardAccount)[]`
|
|
80
|
+
|
|
81
|
+
Extracts account information from the document.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
const accounts = getAccountInfo(doc);
|
|
85
|
+
|
|
86
|
+
for (const account of accounts) {
|
|
87
|
+
console.log({
|
|
88
|
+
accountId: account.ACCTID,
|
|
89
|
+
bankId: "BANKID" in account ? account.BANKID : undefined,
|
|
90
|
+
type: "ACCTTYPE" in account ? account.ACCTTYPE : "CREDIT_CARD",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### `getBalance(document: OFXDocument): BalanceInfo[]`
|
|
96
|
+
|
|
97
|
+
Extracts balance information (ledger and available).
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
const balances = getBalance(doc);
|
|
101
|
+
|
|
102
|
+
for (const balance of balances) {
|
|
103
|
+
console.log({
|
|
104
|
+
ledger: balance.ledger?.BALAMT,
|
|
105
|
+
available: balance.available?.BALAMT,
|
|
106
|
+
asOf: balance.ledger?.DTASOF.toDate(),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### `getSignOnInfo(document: OFXDocument): OFXSignOnResponse`
|
|
112
|
+
|
|
113
|
+
Extracts sign-on response information.
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const signOn = getSignOnInfo(doc);
|
|
117
|
+
|
|
118
|
+
console.log({
|
|
119
|
+
status: signOn.STATUS.CODE,
|
|
120
|
+
serverDate: signOn.DTSERVER.toDate(),
|
|
121
|
+
language: signOn.LANGUAGE,
|
|
122
|
+
institution: signOn.FI?.ORG,
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Types
|
|
127
|
+
|
|
128
|
+
### OFXTransaction
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
interface OFXTransaction {
|
|
132
|
+
TRNTYPE: OFXTransactionType;
|
|
133
|
+
DTPOSTED: OFXDate;
|
|
134
|
+
TRNAMT: number;
|
|
135
|
+
FITID: string;
|
|
136
|
+
NAME?: string;
|
|
137
|
+
MEMO?: string;
|
|
138
|
+
CHECKNUM?: string;
|
|
139
|
+
REFNUM?: string;
|
|
140
|
+
DTUSER?: OFXDate;
|
|
141
|
+
DTAVAIL?: OFXDate;
|
|
142
|
+
CORRECTFITID?: string;
|
|
143
|
+
CORRECTACTION?: "DELETE" | "REPLACE";
|
|
144
|
+
SRVRTID?: string;
|
|
145
|
+
SIC?: string;
|
|
146
|
+
PAYEEID?: string;
|
|
147
|
+
CURRENCY?: string;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### OFXTransactionType
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
type OFXTransactionType =
|
|
155
|
+
| "CREDIT"
|
|
156
|
+
| "DEBIT"
|
|
157
|
+
| "INT"
|
|
158
|
+
| "DIV"
|
|
159
|
+
| "FEE"
|
|
160
|
+
| "SRVCHG"
|
|
161
|
+
| "DEP"
|
|
162
|
+
| "ATM"
|
|
163
|
+
| "POS"
|
|
164
|
+
| "XFER"
|
|
165
|
+
| "CHECK"
|
|
166
|
+
| "PAYMENT"
|
|
167
|
+
| "CASH"
|
|
168
|
+
| "DIRECTDEP"
|
|
169
|
+
| "DIRECTDEBIT"
|
|
170
|
+
| "REPEATPMT"
|
|
171
|
+
| "HOLD"
|
|
172
|
+
| "OTHER";
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### OFXDate
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
interface OFXDate {
|
|
179
|
+
raw: string;
|
|
180
|
+
year: number;
|
|
181
|
+
month: number;
|
|
182
|
+
day: number;
|
|
183
|
+
hour: number;
|
|
184
|
+
minute: number;
|
|
185
|
+
second: number;
|
|
186
|
+
timezone: { offset: number; name: string };
|
|
187
|
+
toDate(): Date;
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### OFXBankAccount
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
interface OFXBankAccount {
|
|
195
|
+
ACCTID: string;
|
|
196
|
+
BANKID: string;
|
|
197
|
+
ACCTTYPE: "CHECKING" | "SAVINGS" | "MONEYMRKT" | "CREDITLINE" | "CD";
|
|
198
|
+
BRANCHID?: string;
|
|
199
|
+
ACCTKEY?: string;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### OFXCreditCardAccount
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
interface OFXCreditCardAccount {
|
|
207
|
+
ACCTID: string;
|
|
208
|
+
ACCTKEY?: string;
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### OFXBalance
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
interface OFXBalance {
|
|
216
|
+
BALAMT: number;
|
|
217
|
+
DTASOF: OFXDate;
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### BalanceInfo
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
interface BalanceInfo {
|
|
225
|
+
ledger?: OFXBalance;
|
|
226
|
+
available?: OFXBalance;
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Schemas
|
|
231
|
+
|
|
232
|
+
All Zod schemas are exported for custom validation:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { schemas } from "@fot/ofx";
|
|
236
|
+
|
|
237
|
+
const customTransactionSchema = schemas.transaction.extend({
|
|
238
|
+
customField: z.string(),
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Available schemas:
|
|
243
|
+
|
|
244
|
+
- `schemas.transaction`
|
|
245
|
+
- `schemas.transactionType`
|
|
246
|
+
- `schemas.transactionList`
|
|
247
|
+
- `schemas.bankAccount`
|
|
248
|
+
- `schemas.creditCardAccount`
|
|
249
|
+
- `schemas.accountType`
|
|
250
|
+
- `schemas.balance`
|
|
251
|
+
- `schemas.status`
|
|
252
|
+
- `schemas.financialInstitution`
|
|
253
|
+
- `schemas.signOnResponse`
|
|
254
|
+
- `schemas.bankStatementResponse`
|
|
255
|
+
- `schemas.creditCardStatementResponse`
|
|
256
|
+
- `schemas.ofxDocument`
|
|
257
|
+
- `schemas.ofxHeader`
|
|
258
|
+
- `schemas.ofxResponse`
|
|
259
|
+
- `schemas.ofxDate`
|
|
260
|
+
|
|
261
|
+
## Performance
|
|
262
|
+
|
|
263
|
+
Tested on realistic business statement sizes:
|
|
264
|
+
|
|
265
|
+
| Transactions | File Size | Parse Time |
|
|
266
|
+
| ------------ | --------- | ---------- |
|
|
267
|
+
| ~10,000 | 2.5 MB | ~800ms |
|
|
268
|
+
| ~25,000 | 5.4 MB | ~1.3s |
|
|
269
|
+
| ~50,000 | 10.4 MB | ~4.3s |
|
|
270
|
+
| ~100,000 | 20.5 MB | ~27s |
|
|
271
|
+
|
|
272
|
+
Extraction operations (`getTransactions`, `getBalance`, etc.) are sub-millisecond even on large datasets.
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const ofxDateSchema: unknown;
|
|
3
|
+
type OFXDate = z.infer<typeof ofxDateSchema>;
|
|
4
|
+
declare const statusSchema: unknown;
|
|
5
|
+
type OFXStatus = z.infer<typeof statusSchema>;
|
|
6
|
+
declare const financialInstitutionSchema: unknown;
|
|
7
|
+
type OFXFinancialInstitution = z.infer<typeof financialInstitutionSchema>;
|
|
8
|
+
declare const transactionTypeSchema: unknown;
|
|
9
|
+
type OFXTransactionType = z.infer<typeof transactionTypeSchema>;
|
|
10
|
+
declare const transactionSchema: unknown;
|
|
11
|
+
type OFXTransaction = z.infer<typeof transactionSchema>;
|
|
12
|
+
declare const accountTypeSchema: unknown;
|
|
13
|
+
type OFXAccountType = z.infer<typeof accountTypeSchema>;
|
|
14
|
+
declare const bankAccountSchema: unknown;
|
|
15
|
+
type OFXBankAccount = z.infer<typeof bankAccountSchema>;
|
|
16
|
+
declare const creditCardAccountSchema: unknown;
|
|
17
|
+
type OFXCreditCardAccount = z.infer<typeof creditCardAccountSchema>;
|
|
18
|
+
declare const balanceSchema: unknown;
|
|
19
|
+
type OFXBalance = z.infer<typeof balanceSchema>;
|
|
20
|
+
declare const transactionListSchema: unknown;
|
|
21
|
+
type OFXTransactionList = z.infer<typeof transactionListSchema>;
|
|
22
|
+
declare const bankStatementResponseSchema: unknown;
|
|
23
|
+
type OFXBankStatementResponse = z.infer<typeof bankStatementResponseSchema>;
|
|
24
|
+
declare const creditCardStatementResponseSchema: unknown;
|
|
25
|
+
type OFXCreditCardStatementResponse = z.infer<typeof creditCardStatementResponseSchema>;
|
|
26
|
+
declare const signOnResponseSchema: unknown;
|
|
27
|
+
type OFXSignOnResponse = z.infer<typeof signOnResponseSchema>;
|
|
28
|
+
declare const bankStatementTransactionResponseSchema: unknown;
|
|
29
|
+
type OFXBankStatementTransactionResponse = z.infer<typeof bankStatementTransactionResponseSchema>;
|
|
30
|
+
declare const creditCardStatementTransactionResponseSchema: unknown;
|
|
31
|
+
type OFXCreditCardStatementTransactionResponse = z.infer<typeof creditCardStatementTransactionResponseSchema>;
|
|
32
|
+
declare const bankMessageSetResponseSchema: unknown;
|
|
33
|
+
type OFXBankMessageSetResponse = z.infer<typeof bankMessageSetResponseSchema>;
|
|
34
|
+
declare const creditCardMessageSetResponseSchema: unknown;
|
|
35
|
+
type OFXCreditCardMessageSetResponse = z.infer<typeof creditCardMessageSetResponseSchema>;
|
|
36
|
+
declare const signOnMessageSetResponseSchema: unknown;
|
|
37
|
+
type OFXSignOnMessageSetResponse = z.infer<typeof signOnMessageSetResponseSchema>;
|
|
38
|
+
declare const ofxResponseSchema: unknown;
|
|
39
|
+
type OFXResponse = z.infer<typeof ofxResponseSchema>;
|
|
40
|
+
declare const ofxHeaderSchema: unknown;
|
|
41
|
+
type OFXHeader = z.infer<typeof ofxHeaderSchema>;
|
|
42
|
+
declare const ofxDocumentSchema: unknown;
|
|
43
|
+
type OFXDocument = z.infer<typeof ofxDocumentSchema>;
|
|
44
|
+
declare const schemas: {};
|
|
45
|
+
type ParseResult<T> = {
|
|
46
|
+
success: true;
|
|
47
|
+
data: T;
|
|
48
|
+
} | {
|
|
49
|
+
success: false;
|
|
50
|
+
error: z.ZodError;
|
|
51
|
+
};
|
|
52
|
+
declare function parse(content: string): ParseResult<OFXDocument>;
|
|
53
|
+
declare function parseOrThrow(content: string): OFXDocument;
|
|
54
|
+
declare function getTransactions(document: OFXDocument): OFXTransaction[];
|
|
55
|
+
declare function getAccountInfo(document: OFXDocument): (OFXBankAccount | OFXCreditCardAccount)[];
|
|
56
|
+
interface BalanceInfo {
|
|
57
|
+
ledger?: OFXBalance;
|
|
58
|
+
available?: OFXBalance;
|
|
59
|
+
}
|
|
60
|
+
declare function getBalance(document: OFXDocument): BalanceInfo[];
|
|
61
|
+
declare function getSignOnInfo(document: OFXDocument): OFXSignOnResponse;
|
|
62
|
+
export { schemas, parseOrThrow, parse, getTransactions, getSignOnInfo, getBalance, getAccountInfo, ParseResult, OFXTransactionType, OFXTransactionList, OFXTransaction, OFXStatus, OFXSignOnResponse, OFXSignOnMessageSetResponse, OFXResponse, OFXHeader, OFXFinancialInstitution, OFXDocument, OFXDate, OFXCreditCardStatementTransactionResponse, OFXCreditCardStatementResponse, OFXCreditCardMessageSetResponse, OFXCreditCardAccount, OFXBankStatementTransactionResponse, OFXBankStatementResponse, OFXBankMessageSetResponse, OFXBankAccount, OFXBalance, OFXAccountType, BalanceInfo };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var toInt = (val) => Number.parseInt(val, 10);
|
|
4
|
+
var toFloat = (val) => Number.parseFloat(val);
|
|
5
|
+
var toArray = (value) => Array.isArray(value) ? value : [value];
|
|
6
|
+
function parseDateComponents(val) {
|
|
7
|
+
return {
|
|
8
|
+
day: toInt(val.substring(6, 8)),
|
|
9
|
+
hour: toInt(val.substring(8, 10) || "0"),
|
|
10
|
+
minute: toInt(val.substring(10, 12) || "0"),
|
|
11
|
+
month: toInt(val.substring(4, 6)),
|
|
12
|
+
second: toInt(val.substring(12, 14) || "0"),
|
|
13
|
+
year: toInt(val.substring(0, 4))
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function parseTimezone(val) {
|
|
17
|
+
const match = val.match(/\[([+-]?\d+):(\w+)\]/);
|
|
18
|
+
return {
|
|
19
|
+
name: match?.[2] ?? "UTC",
|
|
20
|
+
offset: match ? toInt(match[1] ?? "0") : 0
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
var ofxDateSchema = z.string().transform((val) => {
|
|
24
|
+
const components = parseDateComponents(val);
|
|
25
|
+
const timezone = parseTimezone(val);
|
|
26
|
+
return {
|
|
27
|
+
...components,
|
|
28
|
+
raw: val,
|
|
29
|
+
timezone,
|
|
30
|
+
toDate() {
|
|
31
|
+
const offsetMs = timezone.offset * 60 * 60 * 1000;
|
|
32
|
+
return new Date(Date.UTC(components.year, components.month - 1, components.day, components.hour, components.minute, components.second) - offsetMs);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
var statusSchema = z.object({
|
|
37
|
+
CODE: z.string(),
|
|
38
|
+
MESSAGE: z.string().optional(),
|
|
39
|
+
SEVERITY: z.enum(["INFO", "WARN", "ERROR"])
|
|
40
|
+
});
|
|
41
|
+
var financialInstitutionSchema = z.object({
|
|
42
|
+
FID: z.string().optional(),
|
|
43
|
+
ORG: z.string().optional()
|
|
44
|
+
});
|
|
45
|
+
var transactionTypeSchema = z.enum([
|
|
46
|
+
"CREDIT",
|
|
47
|
+
"DEBIT",
|
|
48
|
+
"INT",
|
|
49
|
+
"DIV",
|
|
50
|
+
"FEE",
|
|
51
|
+
"SRVCHG",
|
|
52
|
+
"DEP",
|
|
53
|
+
"ATM",
|
|
54
|
+
"POS",
|
|
55
|
+
"XFER",
|
|
56
|
+
"CHECK",
|
|
57
|
+
"PAYMENT",
|
|
58
|
+
"CASH",
|
|
59
|
+
"DIRECTDEP",
|
|
60
|
+
"DIRECTDEBIT",
|
|
61
|
+
"REPEATPMT",
|
|
62
|
+
"HOLD",
|
|
63
|
+
"OTHER"
|
|
64
|
+
]);
|
|
65
|
+
var transactionSchema = z.object({
|
|
66
|
+
CHECKNUM: z.string().optional(),
|
|
67
|
+
CORRECTACTION: z.enum(["DELETE", "REPLACE"]).optional(),
|
|
68
|
+
CORRECTFITID: z.string().optional(),
|
|
69
|
+
CURRENCY: z.string().optional(),
|
|
70
|
+
DTAVAIL: ofxDateSchema.optional(),
|
|
71
|
+
DTPOSTED: ofxDateSchema,
|
|
72
|
+
DTUSER: ofxDateSchema.optional(),
|
|
73
|
+
FITID: z.string(),
|
|
74
|
+
MEMO: z.string().optional(),
|
|
75
|
+
NAME: z.string().optional(),
|
|
76
|
+
PAYEEID: z.string().optional(),
|
|
77
|
+
REFNUM: z.string().optional(),
|
|
78
|
+
SIC: z.string().optional(),
|
|
79
|
+
SRVRTID: z.string().optional(),
|
|
80
|
+
TRNAMT: z.string().transform(toFloat),
|
|
81
|
+
TRNTYPE: transactionTypeSchema
|
|
82
|
+
});
|
|
83
|
+
var accountTypeSchema = z.enum([
|
|
84
|
+
"CHECKING",
|
|
85
|
+
"SAVINGS",
|
|
86
|
+
"MONEYMRKT",
|
|
87
|
+
"CREDITLINE",
|
|
88
|
+
"CD"
|
|
89
|
+
]);
|
|
90
|
+
var bankAccountSchema = z.object({
|
|
91
|
+
ACCTID: z.string(),
|
|
92
|
+
ACCTKEY: z.string().optional(),
|
|
93
|
+
ACCTTYPE: accountTypeSchema,
|
|
94
|
+
BANKID: z.string(),
|
|
95
|
+
BRANCHID: z.string().optional()
|
|
96
|
+
});
|
|
97
|
+
var creditCardAccountSchema = z.object({
|
|
98
|
+
ACCTID: z.string(),
|
|
99
|
+
ACCTKEY: z.string().optional()
|
|
100
|
+
});
|
|
101
|
+
var balanceSchema = z.object({
|
|
102
|
+
BALAMT: z.string().transform(toFloat),
|
|
103
|
+
DTASOF: ofxDateSchema
|
|
104
|
+
});
|
|
105
|
+
var transactionListSchema = z.object({
|
|
106
|
+
DTEND: ofxDateSchema,
|
|
107
|
+
DTSTART: ofxDateSchema,
|
|
108
|
+
STMTTRN: z.array(transactionSchema).default([])
|
|
109
|
+
});
|
|
110
|
+
var bankStatementResponseSchema = z.object({
|
|
111
|
+
AVAILBAL: balanceSchema.optional(),
|
|
112
|
+
BANKACCTFROM: bankAccountSchema,
|
|
113
|
+
BANKTRANLIST: transactionListSchema.optional(),
|
|
114
|
+
CURDEF: z.string().default("USD"),
|
|
115
|
+
LEDGERBAL: balanceSchema.optional(),
|
|
116
|
+
MKTGINFO: z.string().optional()
|
|
117
|
+
});
|
|
118
|
+
var creditCardStatementResponseSchema = z.object({
|
|
119
|
+
AVAILBAL: balanceSchema.optional(),
|
|
120
|
+
BANKTRANLIST: transactionListSchema.optional(),
|
|
121
|
+
CCACCTFROM: creditCardAccountSchema,
|
|
122
|
+
CURDEF: z.string().default("USD"),
|
|
123
|
+
LEDGERBAL: balanceSchema.optional(),
|
|
124
|
+
MKTGINFO: z.string().optional()
|
|
125
|
+
});
|
|
126
|
+
var signOnResponseSchema = z.object({
|
|
127
|
+
ACCESSKEY: z.string().optional(),
|
|
128
|
+
DTSERVER: ofxDateSchema,
|
|
129
|
+
FI: financialInstitutionSchema.optional(),
|
|
130
|
+
LANGUAGE: z.string().default("ENG"),
|
|
131
|
+
SESSCOOKIE: z.string().optional(),
|
|
132
|
+
STATUS: statusSchema
|
|
133
|
+
});
|
|
134
|
+
var bankStatementTransactionResponseSchema = z.object({
|
|
135
|
+
STATUS: statusSchema,
|
|
136
|
+
STMTRS: bankStatementResponseSchema.optional(),
|
|
137
|
+
TRNUID: z.string()
|
|
138
|
+
});
|
|
139
|
+
var creditCardStatementTransactionResponseSchema = z.object({
|
|
140
|
+
CCSTMTRS: creditCardStatementResponseSchema.optional(),
|
|
141
|
+
STATUS: statusSchema,
|
|
142
|
+
TRNUID: z.string()
|
|
143
|
+
});
|
|
144
|
+
var singleOrArray = (schema) => z.union([schema, z.array(schema)]).optional();
|
|
145
|
+
var bankMessageSetResponseSchema = z.object({
|
|
146
|
+
STMTTRNRS: singleOrArray(bankStatementTransactionResponseSchema)
|
|
147
|
+
});
|
|
148
|
+
var creditCardMessageSetResponseSchema = z.object({
|
|
149
|
+
CCSTMTTRNRS: singleOrArray(creditCardStatementTransactionResponseSchema)
|
|
150
|
+
});
|
|
151
|
+
var signOnMessageSetResponseSchema = z.object({
|
|
152
|
+
SONRS: signOnResponseSchema
|
|
153
|
+
});
|
|
154
|
+
var ofxResponseSchema = z.object({
|
|
155
|
+
BANKMSGSRSV1: bankMessageSetResponseSchema.optional(),
|
|
156
|
+
CREDITCARDMSGSRSV1: creditCardMessageSetResponseSchema.optional(),
|
|
157
|
+
SIGNONMSGSRSV1: signOnMessageSetResponseSchema
|
|
158
|
+
});
|
|
159
|
+
var ofxHeaderSchema = z.object({
|
|
160
|
+
CHARSET: z.string().optional(),
|
|
161
|
+
COMPRESSION: z.string().optional(),
|
|
162
|
+
DATA: z.string().optional(),
|
|
163
|
+
ENCODING: z.string().optional(),
|
|
164
|
+
NEWFILEUID: z.string().optional(),
|
|
165
|
+
OFXHEADER: z.string().optional(),
|
|
166
|
+
OLDFILEUID: z.string().optional(),
|
|
167
|
+
SECURITY: z.string().optional(),
|
|
168
|
+
VERSION: z.string().optional()
|
|
169
|
+
});
|
|
170
|
+
var ofxDocumentSchema = z.object({
|
|
171
|
+
header: ofxHeaderSchema,
|
|
172
|
+
OFX: ofxResponseSchema
|
|
173
|
+
});
|
|
174
|
+
var schemas = {
|
|
175
|
+
accountType: accountTypeSchema,
|
|
176
|
+
balance: balanceSchema,
|
|
177
|
+
bankAccount: bankAccountSchema,
|
|
178
|
+
bankMessageSetResponse: bankMessageSetResponseSchema,
|
|
179
|
+
bankStatementResponse: bankStatementResponseSchema,
|
|
180
|
+
bankStatementTransactionResponse: bankStatementTransactionResponseSchema,
|
|
181
|
+
creditCardAccount: creditCardAccountSchema,
|
|
182
|
+
creditCardMessageSetResponse: creditCardMessageSetResponseSchema,
|
|
183
|
+
creditCardStatementResponse: creditCardStatementResponseSchema,
|
|
184
|
+
creditCardStatementTransactionResponse: creditCardStatementTransactionResponseSchema,
|
|
185
|
+
financialInstitution: financialInstitutionSchema,
|
|
186
|
+
ofxDate: ofxDateSchema,
|
|
187
|
+
ofxDocument: ofxDocumentSchema,
|
|
188
|
+
ofxHeader: ofxHeaderSchema,
|
|
189
|
+
ofxResponse: ofxResponseSchema,
|
|
190
|
+
signOnMessageSetResponse: signOnMessageSetResponseSchema,
|
|
191
|
+
signOnResponse: signOnResponseSchema,
|
|
192
|
+
status: statusSchema,
|
|
193
|
+
transaction: transactionSchema,
|
|
194
|
+
transactionList: transactionListSchema,
|
|
195
|
+
transactionType: transactionTypeSchema
|
|
196
|
+
};
|
|
197
|
+
function parseHeader(content) {
|
|
198
|
+
const lines = content.split(/\r?\n/);
|
|
199
|
+
const header = {};
|
|
200
|
+
let bodyStartIndex = 0;
|
|
201
|
+
for (let i = 0;i < lines.length; i++) {
|
|
202
|
+
const line = lines[i]?.trim() ?? "";
|
|
203
|
+
if (line.startsWith("<?xml") || line.startsWith("<OFX>")) {
|
|
204
|
+
bodyStartIndex = i;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
const match = line.match(/^(\w+):(.*)$/);
|
|
208
|
+
if (match?.[1] && match[2] !== undefined) {
|
|
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) };
|
|
219
|
+
}
|
|
220
|
+
function addToContent(content, key, value) {
|
|
221
|
+
const existing = content[key];
|
|
222
|
+
if (existing !== undefined) {
|
|
223
|
+
content[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
224
|
+
} else {
|
|
225
|
+
content[key] = value;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function sgmlToObject(sgml) {
|
|
229
|
+
const result = {};
|
|
230
|
+
const tagStack = [{ content: result, name: "root" }];
|
|
231
|
+
const cleanSgml = sgml.replace(/<\?.*?\?>/g, "").replace(/<!--.*?-->/gs, "").trim();
|
|
232
|
+
const tagRegex = /<(\/?)([\w.]+)>([^<]*)/g;
|
|
233
|
+
let match = tagRegex.exec(cleanSgml);
|
|
234
|
+
while (match !== null) {
|
|
235
|
+
const isClosing = match[1];
|
|
236
|
+
const tagName = match[2];
|
|
237
|
+
const textContent = match[3]?.trim() ?? "";
|
|
238
|
+
if (!tagName) {
|
|
239
|
+
match = tagRegex.exec(cleanSgml);
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const current = tagStack[tagStack.length - 1];
|
|
243
|
+
if (!current) {
|
|
244
|
+
match = tagRegex.exec(cleanSgml);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (isClosing) {
|
|
248
|
+
if (tagStack.length > 1) {
|
|
249
|
+
tagStack.pop();
|
|
250
|
+
}
|
|
251
|
+
} else if (textContent) {
|
|
252
|
+
addToContent(current.content, tagName, textContent);
|
|
253
|
+
} else {
|
|
254
|
+
const newObj = {};
|
|
255
|
+
addToContent(current.content, tagName, newObj);
|
|
256
|
+
tagStack.push({ content: newObj, name: tagName });
|
|
257
|
+
}
|
|
258
|
+
match = tagRegex.exec(cleanSgml);
|
|
259
|
+
}
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
function processObject(obj) {
|
|
263
|
+
const processed = {};
|
|
264
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
265
|
+
if (key === "STMTTRN") {
|
|
266
|
+
processed[key] = toArray(value).map((v) => typeof v === "object" && v !== null ? processObject(v) : v);
|
|
267
|
+
} else if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
268
|
+
processed[key] = processObject(value);
|
|
269
|
+
} else {
|
|
270
|
+
processed[key] = value;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return processed;
|
|
274
|
+
}
|
|
275
|
+
function normalizeTransactions(data) {
|
|
276
|
+
return processObject(data);
|
|
277
|
+
}
|
|
278
|
+
function parse(content) {
|
|
279
|
+
try {
|
|
280
|
+
const { header, body } = parseHeader(content);
|
|
281
|
+
const rawData = sgmlToObject(body);
|
|
282
|
+
const normalizedData = normalizeTransactions(rawData);
|
|
283
|
+
const parseResult = ofxResponseSchema.safeParse(normalizedData.OFX);
|
|
284
|
+
if (!parseResult.success) {
|
|
285
|
+
return { error: parseResult.error, success: false };
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
data: { header, OFX: parseResult.data },
|
|
289
|
+
success: true
|
|
290
|
+
};
|
|
291
|
+
} catch (err) {
|
|
292
|
+
if (err instanceof z.ZodError) {
|
|
293
|
+
return { error: err, success: false };
|
|
294
|
+
}
|
|
295
|
+
throw err;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
function parseOrThrow(content) {
|
|
299
|
+
const result = parse(content);
|
|
300
|
+
if (!result.success) {
|
|
301
|
+
throw result.error;
|
|
302
|
+
}
|
|
303
|
+
return result.data;
|
|
304
|
+
}
|
|
305
|
+
function extractFromBankResponses(document, extractor) {
|
|
306
|
+
const bankResponse = document.OFX.BANKMSGSRSV1?.STMTTRNRS;
|
|
307
|
+
if (!bankResponse)
|
|
308
|
+
return [];
|
|
309
|
+
const results = [];
|
|
310
|
+
for (const response of toArray(bankResponse)) {
|
|
311
|
+
const result = extractor(response);
|
|
312
|
+
if (result !== undefined) {
|
|
313
|
+
results.push(result);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return results;
|
|
317
|
+
}
|
|
318
|
+
function extractFromCreditCardResponses(document, extractor) {
|
|
319
|
+
const ccResponse = document.OFX.CREDITCARDMSGSRSV1?.CCSTMTTRNRS;
|
|
320
|
+
if (!ccResponse)
|
|
321
|
+
return [];
|
|
322
|
+
const results = [];
|
|
323
|
+
for (const response of toArray(ccResponse)) {
|
|
324
|
+
const result = extractor(response);
|
|
325
|
+
if (result !== undefined) {
|
|
326
|
+
results.push(result);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return results;
|
|
330
|
+
}
|
|
331
|
+
function getTransactions(document) {
|
|
332
|
+
const bankTransactions = extractFromBankResponses(document, (r) => r.STMTRS?.BANKTRANLIST?.STMTTRN);
|
|
333
|
+
const ccTransactions = extractFromCreditCardResponses(document, (r) => r.CCSTMTRS?.BANKTRANLIST?.STMTTRN);
|
|
334
|
+
return [...bankTransactions, ...ccTransactions].flat();
|
|
335
|
+
}
|
|
336
|
+
function getAccountInfo(document) {
|
|
337
|
+
const bankAccounts = extractFromBankResponses(document, (r) => r.STMTRS?.BANKACCTFROM);
|
|
338
|
+
const ccAccounts = extractFromCreditCardResponses(document, (r) => r.CCSTMTRS?.CCACCTFROM);
|
|
339
|
+
return [...bankAccounts, ...ccAccounts];
|
|
340
|
+
}
|
|
341
|
+
function getBalance(document) {
|
|
342
|
+
const bankBalances = extractFromBankResponses(document, (r) => r.STMTRS ? { available: r.STMTRS.AVAILBAL, ledger: r.STMTRS.LEDGERBAL } : undefined);
|
|
343
|
+
const ccBalances = extractFromCreditCardResponses(document, (r) => r.CCSTMTRS ? { available: r.CCSTMTRS.AVAILBAL, ledger: r.CCSTMTRS.LEDGERBAL } : undefined);
|
|
344
|
+
return [...bankBalances, ...ccBalances];
|
|
345
|
+
}
|
|
346
|
+
function getSignOnInfo(document) {
|
|
347
|
+
return document.OFX.SIGNONMSGSRSV1.SONRS;
|
|
348
|
+
}
|
|
349
|
+
export {
|
|
350
|
+
schemas,
|
|
351
|
+
parseOrThrow,
|
|
352
|
+
parse,
|
|
353
|
+
getTransactions,
|
|
354
|
+
getSignOnInfo,
|
|
355
|
+
getBalance,
|
|
356
|
+
getAccountInfo
|
|
357
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bugs": {
|
|
3
|
+
"url": "https://github.com/F-O-T/montte-nx/issues"
|
|
4
|
+
},
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"zod": "4.1.13"
|
|
7
|
+
},
|
|
8
|
+
"description": "Typesafe ofx handling",
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@biomejs/biome": "^2.3.7",
|
|
11
|
+
"@types/bun": "1.3.3",
|
|
12
|
+
"bumpp": "10.3.2",
|
|
13
|
+
"bunup": "0.16.10",
|
|
14
|
+
"simple-git-hooks": "2.13.1",
|
|
15
|
+
"typescript": "5.9.3"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"import": {
|
|
20
|
+
"default": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"homepage": "https://github.com/F-O-T/montte-nx#readme",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"module": "./dist/index.js",
|
|
32
|
+
"name": "@f-o-t/ofx",
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"typescript": ">=4.5.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"typescript": {
|
|
38
|
+
"optional": true
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"private": false,
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/F-O-T/montte-nx.git"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "bunup",
|
|
51
|
+
"dev": "bunup --watch",
|
|
52
|
+
"lint": "biome check .",
|
|
53
|
+
"lint:fix": "biome check --write .",
|
|
54
|
+
"postinstall": "bun simple-git-hooks",
|
|
55
|
+
"release": "bumpp --commit --push --tag",
|
|
56
|
+
"test": "bun test",
|
|
57
|
+
"test:coverage": "bun test --coverage",
|
|
58
|
+
"test:watch": "bun test --watch",
|
|
59
|
+
"type-check": "tsc --noEmit"
|
|
60
|
+
},
|
|
61
|
+
"simple-git-hooks": {
|
|
62
|
+
"pre-commit": "bun run lint && bun run type-check"
|
|
63
|
+
},
|
|
64
|
+
"type": "module",
|
|
65
|
+
"types": "./dist/index.d.ts",
|
|
66
|
+
"version": "1.1.0"
|
|
67
|
+
}
|