@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 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
@@ -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
+ }