ofx-data-extractor 1.4.8 → 1.6.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 CHANGED
@@ -1,98 +1,126 @@
1
1
  # [ofx-data-extractor](https://www.npmjs.com/package/ofx-data-extractor)
2
2
 
3
- [![npm version](https://badge.fury.io/js/ofx-data-extractor.svg)](https://badge.fury.io/js/ofx-data-extractor) [![MIT License][license-image]][license-url]
3
+ [![npm version](https://badge.fury.io/js/ofx-data-extractor.svg)](https://badge.fury.io/js/ofx-data-extractor)
4
+ [![MIT License][license-image]][license-url]
4
5
  [![codecov](https://codecov.io/gh/Fabiopf02/ofx-data-extractor/branch/main/graph/badge.svg?token=L4A7E4H8IN)](https://codecov.io/gh/Fabiopf02/ofx-data-extractor)
5
6
  [![Release Package](https://github.com/Fabiopf02/ofx-data-extractor/actions/workflows/release.yml/badge.svg)](https://github.com/Fabiopf02/ofx-data-extractor/actions/workflows/release.yml)
6
7
 
7
- ## Ofx Data Extractor and Formatter
8
- This is a `Node.js` and `Browser` module written in `TypeScript` that provides a utility for extracting data from an `OFX` file. The module can also format some of the data and retrieve specific parts of the file if requested.
8
+ `ofx-data-extractor` is a TypeScript library for parsing, normalizing and validating OFX files in Node.js and browser environments.
9
9
 
10
- ### Installation
11
- You can install this module using the Node Package Manager (`NPM`) with the following command:
10
+ ## Table of Contents
11
+
12
+ - [Installation](#installation)
13
+ - [Quick Start](#quick-start)
14
+ - [Node.js](#nodejs)
15
+ - [Browser (`fromBlob` is async)](#browser-fromblob-is-async)
16
+ - [Main Public API (`Ofx`)](#main-public-api-ofx)
17
+ - [Configuration (Extract)](#configuration-extract)
18
+ - [Developer Documentation](#developer-documentation)
19
+ - [Examples](#examples)
20
+ - [License](#license)
21
+
22
+ ## Installation
12
23
 
13
- **npm**
14
24
  ```bash
15
25
  npm install ofx-data-extractor
16
26
  ```
17
- **yarn**
27
+
18
28
  ```bash
19
29
  yarn add ofx-data-extractor
20
30
  ```
21
31
 
22
- ### Methods
23
- The `Ofx` class provides the following methods:
32
+ ## Quick Start
24
33
 
25
- - `getType(): Types`: Used to get type of transactions from ofx (BANK or CREDIT_CARD)
26
- - `fromBuffer(data: Buffer)`: Used to read files on the node. Returns the methods
27
- - `fromBlob(data: Blob)`: Used to read files in the browser. Returns the methods below.
28
- - `config(options: OfxConfig)`: Used for formatting the generated json.`
29
- - `getHeaders(): OFXMetaData`: Returns the metadata section of the OFX file as an object.
30
- - `getBankTransferList(): Pick<BankTransferList, 'STRTTRN'>`: Returns a list of bank transfer transactions as an object.
31
- - `getCreditCardTransferList(): Pick<BankTransferList, 'STRTTRN'>`: Returns a list of credit card transactions as an object.
32
- - `getTransactionsSummary()`: Object: Returns a summary of transactions for a bank statement as an object.
33
- - `getContent(): OfxStructure`: Returns the OFX file content as an object.
34
- - `toJson(): OfxResponse`: Returns the entire OFX file content as a JSON object.
35
-
36
- ### Usage
37
- The module provides a class called Ofx that can be used to extract and format data from an `OFX` file. Here's an example of how to use it:
38
-
39
- ```typescript
34
+ ```ts
40
35
  import { Ofx } from 'ofx-data-extractor'
41
36
 
42
37
  const data = 'OFXHEADER:100\nDATA:OFXSGML\nVERSION:102\n...'
43
- const ofx = new Ofx(data) // works in node.js and browser
38
+ const ofx = new Ofx(data)
44
39
 
45
- const headers = ofx.getHeaders()
46
- console.log(headers)
40
+ const raw = ofx.toJson()
41
+ const summary = ofx.getTransactionsSummary()
42
+ const normalized = ofx.toNormalized()
43
+ const validation = ofx.validate()
44
+ ```
47
45
 
48
- const transactionsSummary = ofx.getTransactionsSummary()
49
- console.log(transactionsSummary)
46
+ ## Node.js
50
47
 
51
- const bankTransferList = ofx.getBankTransferList()
52
- console.log(bankTransferList)
48
+ ```ts
49
+ import fs from 'fs'
50
+ import { Ofx } from 'ofx-data-extractor'
53
51
 
54
- const ofxResponse = ofx.toJson()
55
- console.log(ofxResponse)
52
+ const file = fs.readFileSync('/path/to/file.ofx')
53
+ const ofx = Ofx.fromBuffer(file)
54
+ console.log(ofx.getTransactionsSummary())
56
55
  ```
57
- ### Constructor
58
- The `Ofx` class constructor takes in two arguments:
59
56
 
60
- `data`: The `OFX` file content as a string or a Buffer.
61
- `config`: Optional configuration options for formatting the OFX data.
57
+ ## Browser (`fromBlob` is async)
62
58
 
63
- ### Read data - static methods (`Node.js`)
64
- ```typescript
59
+ ```ts
65
60
  import { Ofx } from 'ofx-data-extractor'
66
- import fs from 'fs'
67
61
 
68
- const file = await fs.readFile('/path/to/file')
69
- const ofx = await Ofx.fromBuffer(file)
62
+ async function handleFile(event: Event) {
63
+ const input = event.target as HTMLInputElement
64
+ const file = input.files?.[0]
65
+ if (!file) return
70
66
 
71
- const ofxResponse = ofx.toJson()
72
- console.log(ofxResponse)
67
+ const ofx = await Ofx.fromBlob(file)
68
+ console.log(ofx.toJson())
69
+ }
73
70
  ```
74
71
 
75
- ### Read data - static methods (`Browser`)
76
- ```typescript
77
- import { Ofx } from 'ofx-data-extractor'
72
+ ## Main Public API (`Ofx`)
78
73
 
79
- function handleFile(event) {
80
- const ofx = Ofx.fromBlob(event.target.files[0])
81
- const ofxResponse = ofx.toJson()
82
- console.log(ofxResponse)
83
- }
74
+ - `new Ofx(data: string, config?: ExtractorConfig)`
75
+ - `Ofx.fromBuffer(buffer: Buffer)`
76
+ - `Ofx.fromBlob(blob: Blob)`
77
+ - `config(options)`
78
+ - `getType()`
79
+ - `getHeaders()`
80
+ - `getBankTransferList()`
81
+ - `getCreditCardTransferList()`
82
+ - `getTransactionsSummary()`
83
+ - `getContent()`
84
+ - `toJson()`
85
+ - `toNormalized(options?)`
86
+ - `validate()`
87
+ - `getWarnings()`
84
88
 
85
- // tsx/jsx/html
86
- <input type="file" onChange={handleFile} />
87
- ```
89
+ ## Configuration (Extract)
90
+
91
+ - `nativeTypes?: boolean`
92
+ - `fitId?: 'normal' | 'separated'`
93
+ - `formatDate?: string`
94
+ - `parserMode?: 'strict' | 'lenient'`
95
+
96
+ Parsing guarantees:
97
+
98
+ - OFX 1.x (SGML) and 2.x (XML) are supported, including bodies without line breaks.
99
+ - Character entities (`&amp;`, `&lt;`, `&gt;`, `&quot;`, `&apos;`, numeric references) are decoded in text values.
100
+ - `toNormalized({ dateMode })` keeps the full `DT*` value (including time) for `raw`, `iso`, `date` and `timestamp`.
101
+
102
+ Parser mode:
103
+
104
+ - `strict` (default): parsing errors throw.
105
+ - `lenient`: parsing errors fallback and are collected via `getWarnings()`.
106
+
107
+ ## Developer Documentation
108
+
109
+ - [API Reference](docs/api.md)
110
+ - [Normalization Guide](docs/normalization.md)
111
+ - [Validation Guide](docs/validation.md)
112
+ - [Recipes](docs/recipes.md)
113
+ - [Migration Guide](docs/migration.md)
114
+
115
+ ## Examples
88
116
 
117
+ - [Node basic](examples/node-basic.ts)
118
+ - [Browser basic](examples/browser-basic.ts)
119
+ - [Normalize + validate](examples/normalize-and-validate.ts)
89
120
 
90
- ### Configuration
91
- The `Ofx` (`constructor` and `config` method) class can be configured with the following options:
121
+ ## License
92
122
 
93
- - `formatDate`: A function that takes in a date string and returns a formatted date string.
94
- - `fitId`: A string that determines how the financial institution transaction ID is handled. Possible values are "separated" (the default) or "included".
95
- - `nativeTypes`: A boolean value that determines whether numeric fields should be represented as numbers or strings in the resulting JSON object.
123
+ MIT
96
124
 
97
125
  [license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat
98
126
  [license-url]: LICENSE
@@ -35,9 +35,13 @@ export type ConfigDate = {
35
35
  formatDate?: string;
36
36
  };
37
37
  export type ConfigFitId = 'normal' | 'separated';
38
+ export type ParserMode = 'strict' | 'lenient';
39
+ export type AmountMode = 'string' | 'number' | 'cents';
40
+ export type DateMode = 'raw' | 'formatted' | 'iso' | 'date' | 'timestamp';
38
41
  export type ExtractorConfig = ConfigDate & {
39
42
  fitId?: ConfigFitId;
40
43
  nativeTypes?: boolean;
44
+ parserMode?: ParserMode;
41
45
  };
42
46
  export type TransactionsSummary = {
43
47
  credit: number;
@@ -51,3 +55,47 @@ export declare enum Types {
51
55
  'BANK' = "BANK",
52
56
  'CREDIT_CARD' = "CREDIT_CARD"
53
57
  }
58
+ export type OfxSeverity = 'warning' | 'error';
59
+ export type OfxDiagnostic = {
60
+ code: string;
61
+ message: string;
62
+ severity: OfxSeverity;
63
+ path?: string;
64
+ context?: string;
65
+ };
66
+ export type ValidationStats = {
67
+ totalTransactions: number;
68
+ bankTransactions: number;
69
+ creditCardTransactions: number;
70
+ duplicatedFitIds: number;
71
+ };
72
+ export type ValidationReport = {
73
+ isValid: boolean;
74
+ warnings: OfxDiagnostic[];
75
+ errors: OfxDiagnostic[];
76
+ stats: ValidationStats;
77
+ };
78
+ export type NormalizeOptions = ConfigDate & {
79
+ amountMode?: AmountMode;
80
+ dateMode?: DateMode;
81
+ };
82
+ export type NormalizedDirection = 'credit' | 'debit';
83
+ export type NormalizedTransaction = {
84
+ source: 'bank' | 'credit_card';
85
+ direction: NormalizedDirection;
86
+ amount: number | string | null;
87
+ amountAbs: number | string | null;
88
+ postedAt: string | number | Date | null;
89
+ description: string;
90
+ descriptionNormalized: string;
91
+ fitId: string;
92
+ currency: string | null;
93
+ account: Record<string, any> | null;
94
+ institution: Record<string, any> | null;
95
+ raw: Record<string, any>;
96
+ warnings: OfxDiagnostic[];
97
+ };
98
+ export type NormalizedOfxData = {
99
+ transactions: NormalizedTransaction[];
100
+ warnings: OfxDiagnostic[];
101
+ };
@@ -0,0 +1,4 @@
1
+ import { NormalizedOfxData, NormalizeOptions, ValidationReport } from '../@types/common';
2
+ import { OfxResponse } from '../@types/ofx/index';
3
+ export declare function normalizeOfxData(data: OfxResponse, options?: NormalizeOptions): NormalizedOfxData;
4
+ export declare function validateOfxData(data: OfxResponse): ValidationReport;
@@ -1,9 +1,16 @@
1
1
  import { ExtractorConfig } from '../@types/common';
2
2
  export declare class Config {
3
3
  private readonly config;
4
+ private readonly rawDates;
4
5
  private internConfig;
5
- constructor(config: ExtractorConfig);
6
+ private readonly sanitizeConfig;
7
+ /**
8
+ * @param rawDates internal: keep `DT*` values exactly as they appear in the
9
+ * OFX instead of applying `formatDate` (used by `toNormalized`).
10
+ */
11
+ constructor(config: ExtractorConfig, rawDates?: boolean);
6
12
  getConfig(): ExtractorConfig;
13
+ withRawDates(): Config;
7
14
  sanitizeRow(row: string): string;
8
15
  getPartialJsonData(data: string): string;
9
16
  }
@@ -1,5 +1,10 @@
1
1
  export declare const ELEMENT_CLOSURE_REGEX: RegExp;
2
2
  export declare const ELEMENT_OPENING_REGEX: RegExp;
3
+ /**
4
+ * `<LEAF>value</OTHER>`: an unclosed SGML leaf immediately followed by the
5
+ * closing tag of its parent container (OFX 1.x allows this without line breaks).
6
+ */
7
+ export declare const SGML_LEAF_FOLLOWED_BY_CLOSURE_REGEX: RegExp;
3
8
  export declare const BANK_SERVICE_START = "<BANKMSGSRSV1>";
4
9
  export declare const BANK_SERVICE_END = "</BANKMSGSRSV1>";
5
10
  export declare const CREDIT_CARD_SERVICE_START = "<CREDITCARDMSGSRSV1>";
@@ -7,6 +12,12 @@ export declare const CREDIT_CARD_SERVICE_END = "</CREDITCARDMSGSRSV1>";
7
12
  export declare const START_STATEMENT_TRANSACION = "<STMTTRN>";
8
13
  export declare const FINISH_STATEMENT_TRANSACTION = "</STMTTRN>";
9
14
  export declare const QUOTE_PATTERN = "_#_";
15
+ /**
16
+ * Text values may contain characters the parser uses as structure (`{ } [ ]`).
17
+ * They are swapped for these tokens while the text is rewritten as JSON and
18
+ * restored once it has been parsed (same idea as `QUOTE_PATTERN`).
19
+ */
20
+ export declare const STRUCTURAL_CHAR_TOKENS: Readonly<Record<string, string>>;
10
21
  export declare const QUOTE_PATTERN_REGEX: RegExp;
11
22
  export declare const OPENING_TAGS_INITIALLY_IGNORED: string[];
12
23
  export declare const CLOSING_TAGS_INITIALLY_IGNORED: string[];
@@ -1 +1,14 @@
1
1
  export declare function formatDate(date: string, format: string): string;
2
+ export declare function formatDateFromUtcDate(date: Date, format: string): string;
3
+ type ParsedDateParts = {
4
+ year: number;
5
+ month: number;
6
+ day: number;
7
+ hour: number;
8
+ minutes: number;
9
+ seconds: number;
10
+ };
11
+ export declare function parseDateParts(date: string): ParsedDateParts | null;
12
+ export declare function isValidParsedDate(parts: ParsedDateParts): boolean;
13
+ export declare function parseDateToUtc(date: string): Date | null;
14
+ export {};
@@ -1,6 +1,20 @@
1
1
  import { ExtractorConfig } from '../@types/common';
2
2
  import type { StatementTransaction } from '../@types/ofx/common';
3
3
  export declare function fixJsonProblems(content: string): string;
4
+ export declare function protectStructuralChars(value: string): string;
5
+ export declare function restoreStructuralChars(value: string): string;
6
+ /** Protects `{ } [ ]` inside text nodes (what follows `>` up to the next `<`). */
7
+ export declare function protectTextNodes(content: string): string;
8
+ /**
9
+ * Decodes the XML/SGML character entities OFX allows in text fields.
10
+ * Single pass (`&amp;lt;` -> `&lt;`); unknown or invalid entities are kept as-is.
11
+ */
12
+ export declare function decodeEntities(value: string): string;
13
+ /**
14
+ * Parses the JSON built from the OFX text, decoding entities only in values
15
+ * (after parsing, so decoded text can never be mistaken for tags or JSON).
16
+ */
17
+ export declare function parseOfxJson(text: string): any;
4
18
  export declare const extractFinancialInstitutionTransactionId: (fitid: string) => string;
5
19
  export declare const isDateField: (field: string) => boolean;
6
20
  export declare const trim: (str: string) => string;
@@ -14,7 +28,10 @@ type GetDateParams = Pick<ExtractorConfig, 'formatDate'> & {
14
28
  dateString: string;
15
29
  };
16
30
  export declare function getConfiguredDate({ dateString, formatDate: format, }: GetDateParams): string;
17
- export declare function sanitize(row: string, config: ExtractorConfig): string;
31
+ export declare function applyConfiguredDates(transaction: unknown, format?: string): void;
32
+ export declare function sanitize(row: string, config: ExtractorConfig & {
33
+ rawDates?: boolean;
34
+ }): string;
18
35
  export declare function isValidNumberToConvert(field: string, value: string): boolean;
19
36
  export declare function parseTransactions(content: string): {
20
37
  oldListText: string;
@@ -1,3 +1,4 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  export declare function bufferToString(data: Buffer): string;
3
4
  export declare function blobToString(blob: Blob): Promise<string>;
@@ -1,9 +1,9 @@
1
- // Ofx-data-extractor v1.4.8 Copyright (c) 2025 Fábio Pereira França
2
- const E=/<\/[\w.]+>/g,N=/<[\w.]+>/g,u="<BANKMSGSRSV1>",d="</BANKMSGSRSV1>",g="<CREDITCARDMSGSRSV1>",S="</CREDITCARDMSGSRSV1>",R="<STMTTRN>",T="</STMTTRN>",x="_#_",M=new RegExp(x,"g"),L=[u,R,g],b=[d,S,T];function _(e){const t=e.slice(0,4),n=e.slice(4,6),r=e.slice(6,8),s=e.slice(8,10),a=e.slice(10,12),i=e.slice(12,14);return{yyyy:t,yy:t.slice(2),y:t,MM:n,M:n,dd:r,d:r,hh:s,h:s,mm:a,m:a,ss:i,s:i}}function k(e,t){const n=_(e);let r=t;for(const[s,a]of Object.entries(n))r=r.replace(s,a);return r}var l;(function(e){e.BANK="BANK",e.CREDIT_CARD="CREDIT_CARD"})(l||(l={}));const G=["debit","fee","srvchg","atm","pos","check","payment","directdebit","cash","repeatpmt"],F=e=>e.includes(u)?l.BANK:l.CREDIT_CARD;function K(e){if(String(e.TRNAMT).startsWith("-"))return!0;const t=String(e.TRNTYPE).toLocaleLowerCase();return t==="1"||G.includes(t)}function f(e){const t=e.replace(/(\\)/g,"\\\\").replace(E,n=>A(n,!0)).replace(N,n=>w(n,!0)).replace(/(},})/g,"}}").replace(/(}")/g,'},"').replace(/(]")/g,'],"').replace(/(},])/g,"}]").replace(/(,})/g,"}").replace(/,\s*}/g,"}").replace(/(,",)/,'",').replace(M,'\\"').slice(0,-1);return t.endsWith(",")?t.slice(0,-1):t}const J=e=>JSON.stringify({date:e.slice(0,12),transactionCode:e.slice(12,19),protocol:e.slice(19)}),P=e=>e.startsWith("DT"),D=e=>e.trim();function w(e,t=!1){return L.includes(e)&&!t?e:e.replace(/[<]/g,`
3
- "`).replace(/[>]/g,'":{')}function A(e,t=!1){return b.includes(e)&&!t?`
1
+ const U=/<\/[\w.]+>/g,v=/<[\w.]+>/g,nt=/<([\w.]+)>([^<]*)<\/([\w.]+)>/g,T="<BANKMSGSRSV1>",m="</BANKMSGSRSV1>",S="<CREDITCARDMSGSRSV1>",p="</CREDITCARDMSGSRSV1>",b="<STMTTRN>",h="</STMTTRN>";const A={"{":"_#LB#_","}":"_#RB#_","[":"_#LS#_","]":"_#RS#_"},rt=new RegExp("_#_","g"),st=[T,b,S],at=[m,p,h];function it(e){const t=e.slice(0,4),n=e.slice(4,6),r=e.slice(6,8),s=e.slice(8,10),a=e.slice(10,12),i=e.slice(12,14);return{yyyy:t,yy:t.slice(2),y:t,MM:n,M:n,dd:r,d:r,hh:s,h:s,mm:a,m:a,ss:i,s:i}}function P(e,t){const n=["yyyy","yy","y","MM","M","dd","d","hh","h","mm","m","ss","s"];let r=e;for(const s of n)r=r.replace(s,t[s]);return r}function ot(e,t){const n=it(e);return P(t,n)}function ct(e,t){const n=String(e.getUTCFullYear()),r=String(e.getUTCMonth()+1).padStart(2,"0"),s=String(e.getUTCDate()).padStart(2,"0"),a=String(e.getUTCHours()).padStart(2,"0"),i=String(e.getUTCMinutes()).padStart(2,"0"),o=String(e.getUTCSeconds()).padStart(2,"0");return P(t,{yyyy:n,yy:n.slice(2),y:n,MM:r,M:r,dd:s,d:s,hh:a,h:a,mm:i,m:i,ss:o,s:o})}function ut(e){return e%400===0?!0:e%100===0?!1:e%4===0}function ft(e,t){return e===2?ut(t)?29:28:[4,6,9,11].includes(e)?30:31}function lt(e){if(!/^\d{4}-\d{2}-\d{2}$/.test(e))return null;const[t,n,r]=e.split("-").map(Number);return{year:t,month:n,day:r,hour:0,minutes:0,seconds:0}}function dt(e){if(!/^\d{4}-\d{2}-\d{2}T/.test(e))return null;const t=e.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):?(\d{2})(?::?(\d{2}))?(?:\.\d+)?(?:([+-]\d{2}):?(\d{2})|Z)?$/);if(!t)return null;const n=Number(t[1]),r=Number(t[2]),s=Number(t[3]),a=Number(t[4]),i=Number(t[5]),o=Number(t[6]||"0");if(!$({year:n,month:r,day:s,hour:a,minutes:i,seconds:o}))return null;const f=Number(t[7]||"0"),g=Number(t[8]||"0"),d=f<0?f*60-g:f*60+g,u=new Date(Date.UTC(n,r-1,s,a,i,o)-d*6e4);return Number.isNaN(u.getTime())?null:{year:u.getUTCFullYear(),month:u.getUTCMonth()+1,day:u.getUTCDate(),hour:u.getUTCHours(),minutes:u.getUTCMinutes(),seconds:u.getUTCSeconds()}}function gt(e){const n=e.split("[")[0].split(".")[0];return/^\d{8}(\d{6})?$/.test(n)?{year:Number(n.slice(0,4)),month:Number(n.slice(4,6)),day:Number(n.slice(6,8)),hour:Number(n.slice(8,10)||"00"),minutes:Number(n.slice(10,12)||"00"),seconds:Number(n.slice(12,14)||"00")}:null}function Tt(e){const t=e.match(/^(\d{1,4})[/-](\d{1,2})[/-](\d{1,4})$/);if(!t)return null;const n=Number(t[1]),r=Number(t[2]),s=Number(t[3]),a=String(t[1]).length===4,i=String(t[3]).length===4;return!a&&!i?null:{year:a?n:s,month:r,day:a?s:n,hour:0,minutes:0,seconds:0}}function ht(e){return lt(e)||dt(e)||Tt(e)||gt(e)}function $(e){return!(!Number.isFinite(e.year)||!Number.isFinite(e.month)||!Number.isFinite(e.day)||!Number.isFinite(e.hour)||!Number.isFinite(e.minutes)||!Number.isFinite(e.seconds)||e.month<1||e.month>12||e.day<1||e.day>ft(e.month,e.year)||e.hour<0||e.hour>23||e.minutes<0||e.minutes>59||e.seconds<0||e.seconds>59)}function R(e){const t=ht(e);return!t||!$(t)?null:new Date(Date.UTC(t.year,t.month-1,t.day,t.hour,t.minutes,t.seconds))}var C;(function(e){e.BANK="BANK",e.CREDIT_CARD="CREDIT_CARD"})(C||(C={}));const mt=["debit","fee","srvchg","atm","pos","check","payment","directdebit","cash","repeatpmt"],St=e=>e.includes(T)?C.BANK:C.CREDIT_CARD;function G(e){if(String(e.TRNAMT).startsWith("-"))return!0;const t=String(e.TRNTYPE).toLocaleLowerCase();return t==="1"||mt.includes(t)}function E(e){const t=e.replace(/(\\)/g,"\\\\").replace(U,n=>J(n,!0)).replace(v,n=>W(n,!0)).replace(/(},})/g,"}}").replace(/(}")/g,'},"').replace(/(]")/g,'],"').replace(/(},])/g,"}]").replace(/(,})/g,"}").replace(/,\s*}/g,"}").replace(/(,",)/,'",').replace(rt,'\\"').slice(0,-1);return t.endsWith(",")?t.slice(0,-1):t}const pt=Object.keys(A).reduce((e,t)=>({...e,[A[t]]:t}),{});function N(e){return e.replace(/[{}\[\]]/g,t=>A[t])}function K(e){return e.replace(/_#(?:LB|RB|LS|RS)#_/g,t=>pt[t])}function Rt(e){return e.replace(/>([^<]+)/g,(t,n)=>`>${N(n)}`)}const Ct={amp:"&",lt:"<",gt:">",quot:'"',apos:"'"};function Nt(e){return e.indexOf("&")<0?e:e.replace(/&(?:(amp|lt|gt|quot|apos)|#(\d{1,7})|#[xX]([0-9a-fA-F]{1,6}));/g,(t,n,r,s)=>{if(n)return Ct[n];const a=r?Number(r):parseInt(s,16);return a<1||a>1114111||a>=55296&&a<=57343?t:String.fromCodePoint(a)})}function y(e){return JSON.parse(e,(t,n)=>typeof n=="string"?Nt(K(n)):n)}const Dt=e=>JSON.stringify({date:e.slice(0,12),transactionCode:e.slice(12,19),protocol:e.slice(19)}),X=e=>e.startsWith("DT"),z=e=>e.trim();function W(e,t=!1){return st.includes(e)&&!t?e:e.replace(/[<]/g,`
2
+ "`).replace(/[>]/g,'":{')}function J(e,t=!1){return at.includes(e)&&!t?`
4
3
  ${e}`:`},
5
- `}function X({fitId:e,fitValue:t}){return e==="separated"?J(t):`"${t}",`}function $(e){return e.search(",")>e.search(".")?e.replace(/[.]/g,"").replace(/[,]/g,"."):e.replace(/[,]/g,"")}function V({dateString:e,formatDate:t="y-M-d"}){return k(e,t)}function W(e){let t=e.value.replace(/[{]/g,"").replace(/(},)/g,"").replace(/["]/g,x);const n=e.field.replace(/['"]/g,"");return n.endsWith("AMT")&&(t=$(t)),P(n)&&(t=V({dateString:t,formatDate:e.formatDate})),n==="FITID"?X({fitId:e.fitId,fitValue:t}):e.nativeTypes&&B(n,t)?`${Number(t)},`:`"${t}",`}function z(e,t){let n=e;return e.match(/{(\w|\W)+/)&&(n=n.replace(/({(\w|\W)+)$/,r=>W({field:n.slice(0,n.indexOf(":")),value:r,...t}))),n}function B(e,t){return e.endsWith("ID")||e.endsWith("NUM")?!1:!isNaN(Number(t))}function y(e){const t=e.indexOf(R),n=e.lastIndexOf(T)+T.length;if(t<0||n<0||t>=n)return{oldListText:e,newListText:e};const r=e.substring(t,n),s=new RegExp(R,"g"),a=new RegExp(T,"g"),i=`"STRTTRN":[${r.replace(s,"{").replace(a,"},")}]`;return{oldListText:e,newListText:e.replace(r,i)}}function m(e){if(!(e.indexOf(u)>0))return{newBankStatementTransactions:null,oldBankStatementTransactions:null};const n=y(e.substring(e.indexOf(u),e.indexOf(d)+d.length));return{newBankStatementTransactions:n.newListText,oldBankStatementTransactions:n.oldListText}}function h(e){if(!(e.indexOf(g)>0))return{newCreditCardStatementTransactions:null,oldCreditCardStatementTransactions:null};const n=e.substring(e.indexOf(g),e.indexOf(S)+S.length),r=y(n);return{newCreditCardStatementTransactions:r.newListText,oldCreditCardStatementTransactions:r.oldListText}}function j(e){const t=[];for(const n of e)t.push(...n.replace("<?xml"," ").replace("<?OFX"," ").replace("?>"," ").split(" ").filter(Boolean).map(D).map(r=>r.replace("=",":").replace(/["]/g,"")));return t}function H(e,t){const n={};e.join("").search("<?")>-1&&(e=j(e));for(const r of e){const[s,a]=r.split(":"),i=s.replace(`
6
- `,"").toUpperCase();n[i]=t&&B(s,a)?Number(a):String(a).replace(/\?>/,"")}return n}function U(e){return e.reduce((t,n)=>{const r=Math.abs(+n.TRNAMT);return K(n)?(t.amountOfDebits++,t.debit+=r,t):(t.amountOfCredits++,t.credit+=r,t)},{credit:0,debit:0,amountOfCredits:0,amountOfDebits:0})}class v{config;internConfig={};constructor(t){this.config=t,this.internConfig=t}getConfig(){return this.internConfig}sanitizeRow(t){return z(t,this.internConfig)}getPartialJsonData(t){const[n,r]=t.split("<OFX>"),s="<OFX>"+r,{sanitizeRow:a}=this;return s.replace(/<(?=[^\/])/g,`
7
- <`).replace(E,i=>A(i)).replace(N,i=>w(i)).split(`
8
- `).map(D).filter(Boolean).map(a,this).join("")}}function I(e){return e.toString()}async function p(e){return await new Promise((n,r)=>{if(typeof window<"u"&&window.FileReader){const s=new window.FileReader;s.onload=a=>n(a.target.result),s.onerror=a=>r(a.target.error),s.readAsText(e)}else r(new Error("FileReader is not available in this environment."))})}class c{data;dataRead="";constructor(t){this.data=t,t&&(this.dataRead=t)}getData(){return this.dataRead}fromString(t){return new c(t)}fromBuffer(t){return new c(I(t))}async fromBlob(t){return new c(await p(t))}static fromString(t){return new c(t)}static fromBuffer(t){return new c(I(t))}static async fromBlob(t){return new c(await p(t))}}class Y{configInstance={}}class O extends Y{setConfig(t){this.configInstance=t}getBankTransferList(t){const{newBankStatementTransactions:n}=m(this.configInstance.getPartialJsonData(t));return n?JSON.parse(`{${f(n)}}`)?.BANKMSGSRSV1?.STMTTRNRS?.STMTRS?.BANKTRANLIST?.STRTTRN:[]}getCreditCardTransferList(t){const{newCreditCardStatementTransactions:n}=h(this.configInstance.getPartialJsonData(t));return n?JSON.parse(`{${f(n)}}`)?.CREDITCARDMSGSRSV1?.CCSTMTTRNRS?.CCSTMTRS?.BANKTRANLIST?.STRTTRN:[]}getTransactionsSummary(t){const n=this.getContent(t),{DTEND:r,DTSTART:s,...a}=n.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST,i=U(a.STMTTRN||a.STRTTRN||[]);return{dateStart:s,dateEnd:r,...i}}getContent(t){const n=this.configInstance.getPartialJsonData(t),{newBankStatementTransactions:r,oldBankStatementTransactions:s}=m(n),{newCreditCardStatementTransactions:a,oldCreditCardStatementTransactions:i}=h(n);let o=n;return r&&(o=o.replace(s,r)),a&&(o=o.replace(i,a)),JSON.parse(`{${f(o)}}`)}}class Q{customExtractor;customExtractorInstance;dataReaderInstance=new c("");constructor(t){this.customExtractor=t,this.customExtractorInstance=t||new O,this.config({})}data(t){return this.dataReaderInstance=t,this}config(t){return this.customExtractorInstance.setConfig(new v(t)),this}getType(){return F(this.dataReaderInstance.getData())}getHeaders(){const[t]=this.dataReaderInstance.getData().split("<OFX>");return H(t.split(`
9
- `).filter(n=>!!n.trim().length),!!this.customExtractorInstance.configInstance.getConfig().nativeTypes)}getBankTransferList(){return this.customExtractorInstance.getBankTransferList(this.dataReaderInstance.getData())}getCreditCardTransferList(){return this.customExtractorInstance.getCreditCardTransferList(this.dataReaderInstance.getData())}getTransactionsSummary(){return this.customExtractorInstance.getTransactionsSummary(this.dataReaderInstance.getData())}getContent(){return this.customExtractorInstance.getContent(this.dataReaderInstance.getData())}toJson(){const t=this.getHeaders(),n=this.customExtractorInstance.getContent(this.dataReaderInstance.getData());return{...t,...n}}}class C{extractor;constructor(t,n){this.extractor=new Q(new O),this.extractor.data(new c(t)),this.extractor.config(n||{})}getType(){return this.extractor.getType()}static fromBuffer(t){return new C(c.fromBuffer(t).getData())}static async fromBlob(t){const n=await c.fromBlob(t);return new C(n.getData())}config(t){return this.extractor.config(t),this}getHeaders(){return this.extractor.getHeaders()}getBankTransferList(){return this.extractor.getBankTransferList()}getCreditCardTransferList(){return this.extractor.getCreditCardTransferList()}getTransactionsSummary(){return this.extractor.getTransactionsSummary()}getContent(){return this.extractor.getContent()}toJson(){return this.extractor.toJson()}}export{Q as Extractor,C as Ofx,O as OfxExtractor,c as Reader,p as blobToString,I as bufferToString,f as fixJsonProblems,k as formatDate};
4
+ `}function It({fitId:e,fitValue:t}){if(e==="separated"){const n=JSON.parse(Dt(t));return JSON.stringify(Object.keys(n).reduce((r,s)=>({...r,[s]:N(n[s])}),{}))}return`"${N(t)}",`}function Et(e){return e.search(",")>e.search(".")?e.replace(/[.]/g,"").replace(/[,]/g,"."):e.replace(/[,]/g,"")}function D({dateString:e,formatDate:t="y-M-d"}){return!/^\d{8}(\d{6})?(\.\d+)?(\[[^\]]+\])?$/.test(e)||!R(e)?e:ot(e,t)}function yt(e,t){if(!e||typeof e!="object")return;const n=e;for(const r of Object.keys(n)){const s=n[r];X(r)&&typeof s=="string"&&(n[r]=D({dateString:s,formatDate:t}))}}function bt(e){let t=K(e.value.replace(/[{]/g,"").replace(/(},)/g,"")).replace(/["]/g,"_#_");const n=e.field.replace(/['"]/g,"");return n.endsWith("AMT")&&(t=Et(t)),X(n)&&!e.rawDates&&(t=D({dateString:t,formatDate:e.formatDate})),n==="FITID"?It({fitId:e.fitId,fitValue:t}):e.nativeTypes&&H(n,t)?`${Number(t)},`:`"${N(t)}",`}function At(e,t){let n=e;return e.match(/{(\w|\W)+/)&&(n=n.replace(/({(\w|\W)+)$/,r=>bt({field:n.slice(0,n.indexOf(":")),value:r,...t}))),n}function H(e,t){return e.endsWith("ID")||e.endsWith("NUM")?!1:!isNaN(Number(t))}function V(e){const t=e.indexOf(b),n=e.lastIndexOf(h)+h.length;if(t<0||n<0||t>=n)return{oldListText:e,newListText:e};const r=e.substring(t,n),s=new RegExp(b,"g"),a=new RegExp(h,"g"),i=`"STRTTRN":[${r.replace(s,"{").replace(a,"},")}]`;return{oldListText:e,newListText:e.replace(r,i)}}function L(e){if(!(e.indexOf(T)>0))return{newBankStatementTransactions:null,oldBankStatementTransactions:null};const n=V(e.substring(e.indexOf(T),e.indexOf(m)+m.length));return{newBankStatementTransactions:n.newListText,oldBankStatementTransactions:n.oldListText}}function F(e){if(!(e.indexOf(S)>0))return{newCreditCardStatementTransactions:null,oldCreditCardStatementTransactions:null};const n=e.substring(e.indexOf(S),e.indexOf(p)+p.length),r=V(n);return{newCreditCardStatementTransactions:r.newListText,oldCreditCardStatementTransactions:r.oldListText}}function Ot(e){const t=[];for(const n of e)t.push(...n.replace("<?xml"," ").replace("<?OFX"," ").replace("?>"," ").split(" ").filter(Boolean).map(z).map(r=>r.replace("=",":").replace(/["]/g,"")));return t}function wt(e,t){const n={};e.join("").search("<?")>-1&&(e=Ot(e));for(const r of e){const[s,a]=r.split(":"),i=s.replace(`
5
+ `,"").toUpperCase();n[i]=t&&H(s,a)?Number(a):String(a).replace(/\?>/,"")}return n}function Mt(e){return e.reduce((t,n)=>{const r=Math.abs(+n.TRNAMT);return G(n)?(t.amountOfDebits++,t.debit+=r,t):(t.amountOfCredits++,t.credit+=r,t)},{credit:0,debit:0,amountOfCredits:0,amountOfDebits:0})}class w{config;rawDates;internConfig={};sanitizeConfig;constructor(t,n=!1){this.config=t,this.rawDates=n,this.internConfig=t,this.sanitizeConfig={...t,rawDates:n}}getConfig(){return this.internConfig}withRawDates(){return new w(this.config,!0)}sanitizeRow(t){return At(t,this.sanitizeConfig)}getPartialJsonData(t){const[n,r]=t.split("<OFX>"),s=Rt("<OFX>"+r),{sanitizeRow:a}=this;return s.replace(nt,(i,o,c,f)=>o===f?i:`<${o}>${c}
6
+ </${f}>`).replace(/<(?=[^\/])/g,`
7
+ <`).replace(U,i=>J(i)).replace(v,i=>W(i)).split(`
8
+ `).map(z).filter(Boolean).map(a,this).join("")}}function B(e){return e.toString()}async function k(e){return await new Promise((n,r)=>{if(typeof window<"u"&&window.FileReader){const s=new window.FileReader;s.onload=a=>n(a.target.result),s.onerror=a=>r(a.target.error),s.readAsText(e)}else r(new Error("FileReader is not available in this environment."))})}class l{data;dataRead="";constructor(t){this.data=t,t&&(this.dataRead=t)}getData(){return this.dataRead}fromString(t){return new l(t)}fromBuffer(t){return new l(B(t))}async fromBlob(t){return new l(await k(t))}static fromString(t){return new l(t)}static fromBuffer(t){return new l(B(t))}static async fromBlob(t){return new l(await k(t))}}class xt{configInstance={}}class Y extends xt{setConfig(t){this.configInstance=t}getBankTransferList(t){const{newBankStatementTransactions:n}=L(this.configInstance.getPartialJsonData(t));return n?y(`{${E(n)}}`)?.BANKMSGSRSV1?.STMTTRNRS?.STMTRS?.BANKTRANLIST?.STRTTRN:[]}getCreditCardTransferList(t){const{newCreditCardStatementTransactions:n}=F(this.configInstance.getPartialJsonData(t));return n?y(`{${E(n)}}`)?.CREDITCARDMSGSRSV1?.CCSTMTTRNRS?.CCSTMTRS?.BANKTRANLIST?.STRTTRN:[]}getTransactionsSummary(t){let n=[],r=[];const s=this.configInstance?.getConfig?.().parserMode||"strict";let a=null,i=null;try{n=this.getBankTransferList(t)||[]}catch(I){a=I,n=[]}try{r=this.getCreditCardTransferList(t)||[]}catch(I){i=I,r=[]}const o=t.includes("<STMTTRN>");if(s==="strict"&&a&&i&&o)throw a;const c=n.length?n:r,f=n.length?T:S,g=n.length?m:p,d=t.indexOf(f),u=t.indexOf(g),M=d>=0&&u>d?t.slice(d,u+g.length):"",x=M.match(/<DTSTART>([^\n<]+)/),_=M.match(/<DTEND>([^\n<]+)/),Z=Mt(c||[]),tt=x?.[1]?D({dateString:x[1],formatDate:this.configInstance.getConfig().formatDate}):"",et=_?.[1]?D({dateString:_[1],formatDate:this.configInstance.getConfig().formatDate}):"";return{dateStart:tt,dateEnd:et,...Z}}getContent(t){const n=this.configInstance.getPartialJsonData(t),{newBankStatementTransactions:r,oldBankStatementTransactions:s}=L(n),{newCreditCardStatementTransactions:a,oldCreditCardStatementTransactions:i}=F(n);let o=n;return r&&(o=o.replace(s,r)),a&&(o=o.replace(i,a)),y(`{${E(o)}}`)}}const _t="y-M-d";function j(e){if(e==null)return"";if(typeof e=="string"||typeof e=="number")return String(e);if(typeof e=="object"){const t=e;if(t.date!==void 0&&t.transactionCode!==void 0&&t.protocol!==void 0)return`${t.date}${t.transactionCode}${t.protocol}`;try{return JSON.stringify(e)}catch{return String(e)}}return String(e)}function Q(e){if(e==null)return null;const t=Number(String(e).replace(/,/g,"."));return Number.isNaN(t)?null:t}function Lt(e){return e.normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/\s+/g," ").trim().toLowerCase()}function Ft(e,t){const n=t.dateMode||"formatted";if(!e)return{value:null,warning:{code:"MISSING_DATE",message:"Transaction does not include DTPOSTED.",severity:"warning"}};if(n==="raw"||n==="formatted"){const s=R(e);return s?n==="formatted"?{value:ct(s,t.formatDate||_t)}:{value:e}:{value:e,warning:{code:"INVALID_DATE",message:`Unable to parse date '${e}'.`,severity:"warning"}}}const r=R(e);return r?n==="date"?{value:r}:n==="timestamp"?{value:r.getTime()}:{value:r.toISOString()}:{value:null,warning:{code:"INVALID_DATE",message:`Unable to parse date '${e}'.`,severity:"warning"}}}function Bt(e,t){const n=Q(e);return n===null?{amount:null,amountAbs:null,warning:{code:"INVALID_AMOUNT",message:`Unable to parse amount '${String(e)}'.`,severity:"warning"}}:t==="string"?{amount:String(e),amountAbs:String(Math.abs(n))}:t==="cents"?{amount:Math.round(n*100),amountAbs:Math.round(Math.abs(n)*100)}:{amount:n,amountAbs:Math.abs(n)}}function q(e){const t=[],n=e?.OFX?.SIGNONMSGSRSV1?.SONRS?.FI||null,r=e?.OFX?.BANKMSGSRSV1?.STMTTRNRS?.STMTRS;(r?.BANKTRANLIST?.STRTTRN||r?.BANKTRANLIST?.STMTTRN||[]).forEach((o,c)=>{t.push({source:"bank",transaction:o,currency:r?.CURDEF||null,account:r?.BANKACCTFROM||null,institution:n,path:`OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.${c}`})});const a=e?.OFX?.CREDITCARDMSGSRSV1?.CCSTMTTRNRS?.CCSTMTRS;return(a?.BANKTRANLIST?.STRTTRN||a?.BANKTRANLIST?.STMTTRN||[]).forEach((o,c)=>{t.push({source:"credit_card",transaction:o,currency:a?.CURDEF||null,account:a?.CCACCTFROM||null,institution:n,path:`OFX.CREDITCARDMSGSRSV1.CCSTMTTRNRS.CCSTMTRS.BANKTRANLIST.${c}`})}),t}function kt(e,t={}){const n=t.amountMode||"number",r=[],s=[];return q(e).forEach(i=>{const o=[],c=String(i.transaction.MEMO||i.transaction.NAME||i.transaction.TRNTYPE||"").trim(),{amount:f,amountAbs:g,warning:d}=Bt(i.transaction.TRNAMT,n);d&&(d.path=i.path,o.push(d),r.push(d));const u=Ft(i.transaction.DTPOSTED,t);u.warning&&(u.warning.path=i.path,o.push(u.warning),r.push(u.warning)),s.push({source:i.source,direction:G(i.transaction)?"debit":"credit",amount:f,amountAbs:g,postedAt:u.value,description:c,descriptionNormalized:Lt(c),fitId:j(i.transaction.FITID),currency:i.currency,account:i.account,institution:i.institution,raw:i.transaction,warnings:o})}),{transactions:s,warnings:r}}function Ut(e){const t=[],n=[],r=q(e);e?.OFX||n.push({code:"MISSING_OFX_BLOCK",message:"Missing OFX root block.",severity:"error",path:"OFX"}),r.length||t.push({code:"NO_TRANSACTIONS_FOUND",message:"No transactions were found in bank or credit card statements.",severity:"warning",path:"OFX"});const s=new Map;r.forEach(c=>{const f=j(c.transaction.FITID);f?s.set(f,(s.get(f)||0)+1):t.push({code:"MISSING_FITID",message:"Transaction does not include FITID.",severity:"warning",path:c.path}),Q(c.transaction.TRNAMT)===null&&n.push({code:"INVALID_AMOUNT",message:`Invalid amount '${String(c.transaction.TRNAMT)}'.`,severity:"error",path:c.path}),R(String(c.transaction.DTPOSTED||""))||t.push({code:"INVALID_DATE",message:`Invalid DTPOSTED '${String(c.transaction.DTPOSTED||"")}'.`,severity:"warning",path:c.path})});const a=Array.from(s.values()).filter(c=>c>1).length;a>0&&t.push({code:"DUPLICATED_FITID",message:`Found ${a} duplicated FITID values.`,severity:"warning",path:"OFX"});const i=r.filter(c=>c.source==="bank").length,o=r.filter(c=>c.source==="credit_card").length;return{isValid:n.length===0,warnings:t,errors:n,stats:{totalTransactions:r.length,bankTransactions:i,creditCardTransactions:o,duplicatedFitIds:a}}}class vt{customExtractor;customExtractorInstance;dataReaderInstance=new l("");diagnostics=[];constructor(t){this.customExtractor=t,this.customExtractorInstance=t||new Y,this.config({})}data(t){return this.dataReaderInstance=t,this}config(t){return this.customExtractorInstance.setConfig(new w(t)),this}getType(){return St(this.dataReaderInstance.getData())}beginOperation(){this.diagnostics=[]}getParserMode(){return this.customExtractorInstance.configInstance.getConfig().parserMode}pushWarning(t){this.diagnostics.push(t)}safelyExecute(t,n,r){try{return r()}catch(s){if(this.getParserMode()!=="lenient")throw s;return this.pushWarning({code:"PARSE_ERROR",message:`Failed to parse OFX while running '${t}'.`,severity:"warning",context:s instanceof Error?s.message:String(s),path:t}),n}}getHeadersInternal(){return this.safelyExecute("getHeaders",{},()=>{const[t]=this.dataReaderInstance.getData().split("<OFX>");return wt(String(t).split(`
9
+ `).filter(n=>!!n.trim().length),!!this.customExtractorInstance.configInstance.getConfig().nativeTypes)})}getContentInternal(){return this.safelyExecute("getContent",{OFX:{}},()=>this.customExtractorInstance.getContent(this.dataReaderInstance.getData()))}getResponseInternal(){const t=this.getHeadersInternal(),n=this.getContentInternal();return{...t,...n}}getHeaders(){return this.beginOperation(),this.getHeadersInternal()}getBankTransferList(){return this.beginOperation(),this.safelyExecute("getBankTransferList",[],()=>this.customExtractorInstance.getBankTransferList(this.dataReaderInstance.getData()))}getCreditCardTransferList(){return this.beginOperation(),this.safelyExecute("getCreditCardTransferList",[],()=>this.customExtractorInstance.getCreditCardTransferList(this.dataReaderInstance.getData()))}getTransactionsSummary(){return this.beginOperation(),this.safelyExecute("getTransactionsSummary",{credit:0,debit:0,amountOfCredits:0,amountOfDebits:0,dateStart:"",dateEnd:""},()=>this.customExtractorInstance.getTransactionsSummary(this.dataReaderInstance.getData()))}getContent(){return this.beginOperation(),this.getContentInternal()}toJson(){return this.beginOperation(),this.getResponseInternal()}getResponseWithRawDates(){const t=this.customExtractorInstance.configInstance;this.customExtractorInstance.setConfig(t.withRawDates());try{return this.getResponseInternal()}finally{this.customExtractorInstance.setConfig(t)}}toNormalized(t={}){this.beginOperation();const n=this.customExtractorInstance.configInstance.getConfig().formatDate,r=kt(this.getResponseWithRawDates(),{...t,formatDate:t.formatDate||n});return r.transactions.forEach(s=>yt(s.raw,n)),r}validate(){return this.beginOperation(),Ut(this.getResponseWithRawDates())}getWarnings(){return[...this.diagnostics]}}class O{extractor;constructor(t,n){this.extractor=new vt(new Y),this.extractor.data(new l(t)),this.extractor.config(n||{})}getType(){return this.extractor.getType()}static fromBuffer(t){return new O(l.fromBuffer(t).getData())}static async fromBlob(t){const n=await l.fromBlob(t);return new O(n.getData())}config(t){return this.extractor.config(t),this}getHeaders(){return this.extractor.getHeaders()}getBankTransferList(){return this.extractor.getBankTransferList()}getCreditCardTransferList(){return this.extractor.getCreditCardTransferList()}getTransactionsSummary(){return this.extractor.getTransactionsSummary()}getContent(){return this.extractor.getContent()}toJson(){return this.extractor.toJson()}toNormalized(t){return this.extractor.toNormalized(t)}validate(){return this.extractor.validate()}getWarnings(){return this.extractor.getWarnings()}}export{vt as Extractor,O as Ofx,Y as OfxExtractor,l as Reader,k as blobToString,B as bufferToString,E as fixJsonProblems,ot as formatDate};
@@ -1,4 +1,4 @@
1
- import type { ExtractorConfig, MetaData, TransactionsSummary } from '../@types/common';
1
+ import type { ExtractorConfig, MetaData, NormalizeOptions, NormalizedOfxData, OfxDiagnostic, TransactionsSummary, ValidationReport } from '../@types/common';
2
2
  import { IExtractor } from '../interfaces/extractor.interface';
3
3
  import { CustomExtractor } from '../interfaces/custom-extractor.interface';
4
4
  import { Reader } from './reader';
@@ -8,14 +8,30 @@ export declare class Extractor<T = any> implements IExtractor<T> {
8
8
  private readonly customExtractor?;
9
9
  private customExtractorInstance;
10
10
  private dataReaderInstance;
11
+ private diagnostics;
11
12
  constructor(customExtractor?: CustomExtractor | undefined);
12
13
  data(readData: Reader): this;
13
14
  config(config: ExtractorConfig): this;
14
15
  getType(): import("../@types/common").Types;
16
+ private beginOperation;
17
+ private getParserMode;
18
+ private pushWarning;
19
+ private safelyExecute;
20
+ private getHeadersInternal;
21
+ private getContentInternal;
22
+ private getResponseInternal;
15
23
  getHeaders(): MetaData;
16
24
  getBankTransferList(): StatementTransaction[];
17
25
  getCreditCardTransferList(): StatementTransaction[];
18
26
  getTransactionsSummary(): TransactionsSummary;
19
27
  getContent(): OfxStructure;
20
28
  toJson(): OfxResponse;
29
+ /**
30
+ * Runs the extraction with `DT*` fields untouched (no `formatDate`), so
31
+ * date-aware consumers see the full OFX value (including hh:mm:ss).
32
+ */
33
+ private getResponseWithRawDates;
34
+ toNormalized(options?: NormalizeOptions): NormalizedOfxData;
35
+ validate(): ValidationReport;
36
+ getWarnings(): OfxDiagnostic[];
21
37
  }
@@ -1,4 +1,5 @@
1
1
  /// <reference types="node" />
2
+ /// <reference types="node" />
2
3
  export declare class Reader {
3
4
  private readonly data?;
4
5
  private dataRead;
package/dist/index.d.ts CHANGED
@@ -9,4 +9,4 @@ export type { BankTransactionList } from './@types/ofx/banking';
9
9
  export { Reader } from './implementations/reader';
10
10
  export { OfxExtractor } from './implementations/ofx-extractor';
11
11
  export { Extractor } from './implementations/extractor';
12
- export type { MetaData, ExtractorConfig, DateResponse } from './@types/common';
12
+ export type { MetaData, ExtractorConfig, DateResponse, ValidationReport, ValidationStats, OfxDiagnostic, NormalizeOptions, NormalizedTransaction, NormalizedOfxData, ParserMode, AmountMode, DateMode, } from './@types/common';
@@ -1,5 +1,5 @@
1
1
  import { OfxResponse, OfxStructure } from '../@types/ofx/index';
2
- import { ExtractorConfig, MetaData, TransactionsSummary, Types } from '../@types/common';
2
+ import { ExtractorConfig, MetaData, NormalizeOptions, NormalizedOfxData, TransactionsSummary, Types, ValidationReport, OfxDiagnostic } from '../@types/common';
3
3
  import type { StatementTransaction } from '../@types/ofx/common';
4
4
  export interface IExtractor<T> {
5
5
  getType(): Types;
@@ -10,4 +10,7 @@ export interface IExtractor<T> {
10
10
  getTransactionsSummary(): TransactionsSummary;
11
11
  getContent(): OfxStructure;
12
12
  toJson(): OfxResponse;
13
+ toNormalized(options?: NormalizeOptions): NormalizedOfxData;
14
+ validate(): ValidationReport;
15
+ getWarnings(): OfxDiagnostic[];
13
16
  }
@@ -1,5 +1,6 @@
1
1
  /// <reference types="node" />
2
- import { ExtractorConfig, MetaData } from '../@types/common';
2
+ /// <reference types="node" />
3
+ import { ExtractorConfig, MetaData, NormalizeOptions, NormalizedOfxData, OfxDiagnostic, ValidationReport } from '../@types/common';
3
4
  import { OfxResponse, OfxStructure } from '../@types/ofx/index';
4
5
  import { StatementTransaction } from '../@types/ofx/common';
5
6
  export declare class Ofx {
@@ -15,4 +16,7 @@ export declare class Ofx {
15
16
  getTransactionsSummary(): import("../@types/common").TransactionsSummary;
16
17
  getContent(): OfxStructure;
17
18
  toJson(): OfxResponse;
19
+ toNormalized(options?: NormalizeOptions): NormalizedOfxData;
20
+ validate(): ValidationReport;
21
+ getWarnings(): OfxDiagnostic[];
18
22
  }
@@ -1,9 +1,9 @@
1
- // Ofx-data-extractor v1.4.8 Copyright (c) 2025 Fábio Pereira França
2
- (function(c,u){typeof exports=="object"&&typeof module<"u"?u(exports):typeof define=="function"&&define.amd?define(["exports"],u):(c=typeof globalThis<"u"?globalThis:c||self,u(c.ofx={}))})(this,function(c){"use strict";const u=/<\/[\w.]+>/g,x=/<[\w.]+>/g,f="<BANKMSGSRSV1>",R="</BANKMSGSRSV1>",m="<CREDITCARDMSGSRSV1>",C="</CREDITCARDMSGSRSV1>",h="<STMTTRN>",d="</STMTTRN>",N="_#_",_=new RegExp(N,"g"),k=[f,h,m],G=[R,C,d];function F(e){const t=e.slice(0,4),n=e.slice(4,6),r=e.slice(6,8),s=e.slice(8,10),a=e.slice(10,12),i=e.slice(12,14);return{yyyy:t,yy:t.slice(2),y:t,MM:n,M:n,dd:r,d:r,hh:s,h:s,mm:a,m:a,ss:i,s:i}}function D(e,t){const n=F(e);let r=t;for(const[s,a]of Object.entries(n))r=r.replace(s,a);return r}var l;(function(e){e.BANK="BANK",e.CREDIT_CARD="CREDIT_CARD"})(l||(l={}));const J=["debit","fee","srvchg","atm","pos","check","payment","directdebit","cash","repeatpmt"],K=e=>e.includes(f)?l.BANK:l.CREDIT_CARD;function P(e){if(String(e.TRNAMT).startsWith("-"))return!0;const t=String(e.TRNTYPE).toLocaleLowerCase();return t==="1"||J.includes(t)}function g(e){const t=e.replace(/(\\)/g,"\\\\").replace(u,n=>y(n,!0)).replace(x,n=>A(n,!0)).replace(/(},})/g,"}}").replace(/(}")/g,'},"').replace(/(]")/g,'],"').replace(/(},])/g,"}]").replace(/(,})/g,"}").replace(/,\s*}/g,"}").replace(/(,",)/,'",').replace(_,'\\"').slice(0,-1);return t.endsWith(",")?t.slice(0,-1):t}const X=e=>JSON.stringify({date:e.slice(0,12),transactionCode:e.slice(12,19),protocol:e.slice(19)}),$=e=>e.startsWith("DT"),w=e=>e.trim();function A(e,t=!1){return k.includes(e)&&!t?e:e.replace(/[<]/g,`
3
- "`).replace(/[>]/g,'":{')}function y(e,t=!1){return G.includes(e)&&!t?`
1
+ (function(l,h){typeof exports=="object"&&typeof module<"u"?h(exports):typeof define=="function"&&define.amd?define(["exports"],h):(l=typeof globalThis<"u"?globalThis:l||self,h(l.ofx={}))})(this,(function(l){"use strict";const h=/<\/[\w.]+>/g,B=/<[\w.]+>/g,nt=/<([\w.]+)>([^<]*)<\/([\w.]+)>/g,m="<BANKMSGSRSV1>",S="</BANKMSGSRSV1>",R="<CREDITCARDMSGSRSV1>",p="</CREDITCARDMSGSRSV1>",O="<STMTTRN>",C="</STMTTRN>",A={"{":"_#LB#_","}":"_#RB#_","[":"_#LS#_","]":"_#RS#_"},rt=new RegExp("_#_","g"),st=[m,O,R],at=[S,p,C];function it(e){const t=e.slice(0,4),n=e.slice(4,6),r=e.slice(6,8),s=e.slice(8,10),a=e.slice(10,12),i=e.slice(12,14);return{yyyy:t,yy:t.slice(2),y:t,MM:n,M:n,dd:r,d:r,hh:s,h:s,mm:a,m:a,ss:i,s:i}}function k(e,t){const n=["yyyy","yy","y","MM","M","dd","d","hh","h","mm","m","ss","s"];let r=e;for(const s of n)r=r.replace(s,t[s]);return r}function U(e,t){const n=it(e);return k(t,n)}function ot(e,t){const n=String(e.getUTCFullYear()),r=String(e.getUTCMonth()+1).padStart(2,"0"),s=String(e.getUTCDate()).padStart(2,"0"),a=String(e.getUTCHours()).padStart(2,"0"),i=String(e.getUTCMinutes()).padStart(2,"0"),o=String(e.getUTCSeconds()).padStart(2,"0");return k(t,{yyyy:n,yy:n.slice(2),y:n,MM:r,M:r,dd:s,d:s,hh:a,h:a,mm:i,m:i,ss:o,s:o})}function ct(e){return e%400===0?!0:e%100===0?!1:e%4===0}function ut(e,t){return e===2?ct(t)?29:28:[4,6,9,11].includes(e)?30:31}function ft(e){if(!/^\d{4}-\d{2}-\d{2}$/.test(e))return null;const[t,n,r]=e.split("-").map(Number);return{year:t,month:n,day:r,hour:0,minutes:0,seconds:0}}function lt(e){if(!/^\d{4}-\d{2}-\d{2}T/.test(e))return null;const t=e.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):?(\d{2})(?::?(\d{2}))?(?:\.\d+)?(?:([+-]\d{2}):?(\d{2})|Z)?$/);if(!t)return null;const n=Number(t[1]),r=Number(t[2]),s=Number(t[3]),a=Number(t[4]),i=Number(t[5]),o=Number(t[6]||"0");if(!v({year:n,month:r,day:s,hour:a,minutes:i,seconds:o}))return null;const f=Number(t[7]||"0"),g=Number(t[8]||"0"),T=f<0?f*60-g:f*60+g,u=new Date(Date.UTC(n,r-1,s,a,i,o)-T*6e4);return Number.isNaN(u.getTime())?null:{year:u.getUTCFullYear(),month:u.getUTCMonth()+1,day:u.getUTCDate(),hour:u.getUTCHours(),minutes:u.getUTCMinutes(),seconds:u.getUTCSeconds()}}function dt(e){const n=e.split("[")[0].split(".")[0];return/^\d{8}(\d{6})?$/.test(n)?{year:Number(n.slice(0,4)),month:Number(n.slice(4,6)),day:Number(n.slice(6,8)),hour:Number(n.slice(8,10)||"00"),minutes:Number(n.slice(10,12)||"00"),seconds:Number(n.slice(12,14)||"00")}:null}function Tt(e){const t=e.match(/^(\d{1,4})[/-](\d{1,2})[/-](\d{1,4})$/);if(!t)return null;const n=Number(t[1]),r=Number(t[2]),s=Number(t[3]),a=String(t[1]).length===4,i=String(t[3]).length===4;return!a&&!i?null:{year:a?n:s,month:r,day:a?s:n,hour:0,minutes:0,seconds:0}}function gt(e){return ft(e)||lt(e)||Tt(e)||dt(e)}function v(e){return!(!Number.isFinite(e.year)||!Number.isFinite(e.month)||!Number.isFinite(e.day)||!Number.isFinite(e.hour)||!Number.isFinite(e.minutes)||!Number.isFinite(e.seconds)||e.month<1||e.month>12||e.day<1||e.day>ut(e.month,e.year)||e.hour<0||e.hour>23||e.minutes<0||e.minutes>59||e.seconds<0||e.seconds>59)}function N(e){const t=gt(e);return!t||!v(t)?null:new Date(Date.UTC(t.year,t.month-1,t.day,t.hour,t.minutes,t.seconds))}var D;(function(e){e.BANK="BANK",e.CREDIT_CARD="CREDIT_CARD"})(D||(D={}));const ht=["debit","fee","srvchg","atm","pos","check","payment","directdebit","cash","repeatpmt"],mt=e=>e.includes(m)?D.BANK:D.CREDIT_CARD;function P(e){if(String(e.TRNAMT).startsWith("-"))return!0;const t=String(e.TRNTYPE).toLocaleLowerCase();return t==="1"||ht.includes(t)}function I(e){const t=e.replace(/(\\)/g,"\\\\").replace(h,n=>z(n,!0)).replace(B,n=>X(n,!0)).replace(/(},})/g,"}}").replace(/(}")/g,'},"').replace(/(]")/g,'],"').replace(/(},])/g,"}]").replace(/(,})/g,"}").replace(/,\s*}/g,"}").replace(/(,",)/,'",').replace(rt,'\\"').slice(0,-1);return t.endsWith(",")?t.slice(0,-1):t}const St=Object.keys(A).reduce((e,t)=>({...e,[A[t]]:t}),{});function E(e){return e.replace(/[{}\[\]]/g,t=>A[t])}function $(e){return e.replace(/_#(?:LB|RB|LS|RS)#_/g,t=>St[t])}function Rt(e){return e.replace(/>([^<]+)/g,(t,n)=>`>${E(n)}`)}const pt={amp:"&",lt:"<",gt:">",quot:'"',apos:"'"};function Ct(e){return e.indexOf("&")<0?e:e.replace(/&(?:(amp|lt|gt|quot|apos)|#(\d{1,7})|#[xX]([0-9a-fA-F]{1,6}));/g,(t,n,r,s)=>{if(n)return pt[n];const a=r?Number(r):parseInt(s,16);return a<1||a>1114111||a>=55296&&a<=57343?t:String.fromCodePoint(a)})}function w(e){return JSON.parse(e,(t,n)=>typeof n=="string"?Ct($(n)):n)}const Nt=e=>JSON.stringify({date:e.slice(0,12),transactionCode:e.slice(12,19),protocol:e.slice(19)}),G=e=>e.startsWith("DT"),K=e=>e.trim();function X(e,t=!1){return st.includes(e)&&!t?e:e.replace(/[<]/g,`
2
+ "`).replace(/[>]/g,'":{')}function z(e,t=!1){return at.includes(e)&&!t?`
4
3
  ${e}`:`},
5
- `}function V({fitId:e,fitValue:t}){return e==="separated"?X(t):`"${t}",`}function W(e){return e.search(",")>e.search(".")?e.replace(/[.]/g,"").replace(/[,]/g,"."):e.replace(/[,]/g,"")}function j({dateString:e,formatDate:t="y-M-d"}){return D(e,t)}function z(e){let t=e.value.replace(/[{]/g,"").replace(/(},)/g,"").replace(/["]/g,N);const n=e.field.replace(/['"]/g,"");return n.endsWith("AMT")&&(t=W(t)),$(n)&&(t=j({dateString:t,formatDate:e.formatDate})),n==="FITID"?V({fitId:e.fitId,fitValue:t}):e.nativeTypes&&B(n,t)?`${Number(t)},`:`"${t}",`}function H(e,t){let n=e;return e.match(/{(\w|\W)+/)&&(n=n.replace(/({(\w|\W)+)$/,r=>z({field:n.slice(0,n.indexOf(":")),value:r,...t}))),n}function B(e,t){return e.endsWith("ID")||e.endsWith("NUM")?!1:!isNaN(Number(t))}function O(e){const t=e.indexOf(h),n=e.lastIndexOf(d)+d.length;if(t<0||n<0||t>=n)return{oldListText:e,newListText:e};const r=e.substring(t,n),s=new RegExp(h,"g"),a=new RegExp(d,"g"),i=`"STRTTRN":[${r.replace(s,"{").replace(a,"},")}]`;return{oldListText:e,newListText:e.replace(r,i)}}function b(e){if(!(e.indexOf(f)>0))return{newBankStatementTransactions:null,oldBankStatementTransactions:null};const n=O(e.substring(e.indexOf(f),e.indexOf(R)+R.length));return{newBankStatementTransactions:n.newListText,oldBankStatementTransactions:n.oldListText}}function M(e){if(!(e.indexOf(m)>0))return{newCreditCardStatementTransactions:null,oldCreditCardStatementTransactions:null};const n=e.substring(e.indexOf(m),e.indexOf(C)+C.length),r=O(n);return{newCreditCardStatementTransactions:r.newListText,oldCreditCardStatementTransactions:r.oldListText}}function U(e){const t=[];for(const n of e)t.push(...n.replace("<?xml"," ").replace("<?OFX"," ").replace("?>"," ").split(" ").filter(Boolean).map(w).map(r=>r.replace("=",":").replace(/["]/g,"")));return t}function v(e,t){const n={};e.join("").search("<?")>-1&&(e=U(e));for(const r of e){const[s,a]=r.split(":"),i=s.replace(`
6
- `,"").toUpperCase();n[i]=t&&B(s,a)?Number(a):String(a).replace(/\?>/,"")}return n}function Y(e){return e.reduce((t,n)=>{const r=Math.abs(+n.TRNAMT);return P(n)?(t.amountOfDebits++,t.debit+=r,t):(t.amountOfCredits++,t.credit+=r,t)},{credit:0,debit:0,amountOfCredits:0,amountOfDebits:0})}class Q{config;internConfig={};constructor(t){this.config=t,this.internConfig=t}getConfig(){return this.internConfig}sanitizeRow(t){return H(t,this.internConfig)}getPartialJsonData(t){const[n,r]=t.split("<OFX>"),s="<OFX>"+r,{sanitizeRow:a}=this;return s.replace(/<(?=[^\/])/g,`
7
- <`).replace(u,i=>y(i)).replace(x,i=>A(i)).split(`
8
- `).map(w).filter(Boolean).map(a,this).join("")}}function E(e){return e.toString()}async function I(e){return await new Promise((n,r)=>{if(typeof window<"u"&&window.FileReader){const s=new window.FileReader;s.onload=a=>n(a.target.result),s.onerror=a=>r(a.target.error),s.readAsText(e)}else r(new Error("FileReader is not available in this environment."))})}class o{data;dataRead="";constructor(t){this.data=t,t&&(this.dataRead=t)}getData(){return this.dataRead}fromString(t){return new o(t)}fromBuffer(t){return new o(E(t))}async fromBlob(t){return new o(await I(t))}static fromString(t){return new o(t)}static fromBuffer(t){return new o(E(t))}static async fromBlob(t){return new o(await I(t))}}class q{configInstance={}}class p extends q{setConfig(t){this.configInstance=t}getBankTransferList(t){const{newBankStatementTransactions:n}=b(this.configInstance.getPartialJsonData(t));return n?JSON.parse(`{${g(n)}}`)?.BANKMSGSRSV1?.STMTTRNRS?.STMTRS?.BANKTRANLIST?.STRTTRN:[]}getCreditCardTransferList(t){const{newCreditCardStatementTransactions:n}=M(this.configInstance.getPartialJsonData(t));return n?JSON.parse(`{${g(n)}}`)?.CREDITCARDMSGSRSV1?.CCSTMTTRNRS?.CCSTMTRS?.BANKTRANLIST?.STRTTRN:[]}getTransactionsSummary(t){const n=this.getContent(t),{DTEND:r,DTSTART:s,...a}=n.OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST,i=Y(a.STMTTRN||a.STRTTRN||[]);return{dateStart:s,dateEnd:r,...i}}getContent(t){const n=this.configInstance.getPartialJsonData(t),{newBankStatementTransactions:r,oldBankStatementTransactions:s}=b(n),{newCreditCardStatementTransactions:a,oldCreditCardStatementTransactions:i}=M(n);let T=n;return r&&(T=T.replace(s,r)),a&&(T=T.replace(i,a)),JSON.parse(`{${g(T)}}`)}}class L{customExtractor;customExtractorInstance;dataReaderInstance=new o("");constructor(t){this.customExtractor=t,this.customExtractorInstance=t||new p,this.config({})}data(t){return this.dataReaderInstance=t,this}config(t){return this.customExtractorInstance.setConfig(new Q(t)),this}getType(){return K(this.dataReaderInstance.getData())}getHeaders(){const[t]=this.dataReaderInstance.getData().split("<OFX>");return v(t.split(`
9
- `).filter(n=>!!n.trim().length),!!this.customExtractorInstance.configInstance.getConfig().nativeTypes)}getBankTransferList(){return this.customExtractorInstance.getBankTransferList(this.dataReaderInstance.getData())}getCreditCardTransferList(){return this.customExtractorInstance.getCreditCardTransferList(this.dataReaderInstance.getData())}getTransactionsSummary(){return this.customExtractorInstance.getTransactionsSummary(this.dataReaderInstance.getData())}getContent(){return this.customExtractorInstance.getContent(this.dataReaderInstance.getData())}toJson(){const t=this.getHeaders(),n=this.customExtractorInstance.getContent(this.dataReaderInstance.getData());return{...t,...n}}}class S{extractor;constructor(t,n){this.extractor=new L(new p),this.extractor.data(new o(t)),this.extractor.config(n||{})}getType(){return this.extractor.getType()}static fromBuffer(t){return new S(o.fromBuffer(t).getData())}static async fromBlob(t){const n=await o.fromBlob(t);return new S(n.getData())}config(t){return this.extractor.config(t),this}getHeaders(){return this.extractor.getHeaders()}getBankTransferList(){return this.extractor.getBankTransferList()}getCreditCardTransferList(){return this.extractor.getCreditCardTransferList()}getTransactionsSummary(){return this.extractor.getTransactionsSummary()}getContent(){return this.extractor.getContent()}toJson(){return this.extractor.toJson()}}c.Extractor=L,c.Ofx=S,c.OfxExtractor=p,c.Reader=o,c.blobToString=I,c.bufferToString=E,c.fixJsonProblems=g,c.formatDate=D});
4
+ `}function Dt({fitId:e,fitValue:t}){if(e==="separated"){const n=JSON.parse(Nt(t));return JSON.stringify(Object.keys(n).reduce((r,s)=>({...r,[s]:E(n[s])}),{}))}return`"${E(t)}",`}function It(e){return e.search(",")>e.search(".")?e.replace(/[.]/g,"").replace(/[,]/g,"."):e.replace(/[,]/g,"")}function y({dateString:e,formatDate:t="y-M-d"}){return!/^\d{8}(\d{6})?(\.\d+)?(\[[^\]]+\])?$/.test(e)||!N(e)?e:U(e,t)}function Et(e,t){if(!e||typeof e!="object")return;const n=e;for(const r of Object.keys(n)){const s=n[r];G(r)&&typeof s=="string"&&(n[r]=y({dateString:s,formatDate:t}))}}function yt(e){let t=$(e.value.replace(/[{]/g,"").replace(/(},)/g,"")).replace(/["]/g,"_#_");const n=e.field.replace(/['"]/g,"");return n.endsWith("AMT")&&(t=It(t)),G(n)&&!e.rawDates&&(t=y({dateString:t,formatDate:e.formatDate})),n==="FITID"?Dt({fitId:e.fitId,fitValue:t}):e.nativeTypes&&W(n,t)?`${Number(t)},`:`"${E(t)}",`}function bt(e,t){let n=e;return e.match(/{(\w|\W)+/)&&(n=n.replace(/({(\w|\W)+)$/,r=>yt({field:n.slice(0,n.indexOf(":")),value:r,...t}))),n}function W(e,t){return e.endsWith("ID")||e.endsWith("NUM")?!1:!isNaN(Number(t))}function J(e){const t=e.indexOf(O),n=e.lastIndexOf(C)+C.length;if(t<0||n<0||t>=n)return{oldListText:e,newListText:e};const r=e.substring(t,n),s=new RegExp(O,"g"),a=new RegExp(C,"g"),i=`"STRTTRN":[${r.replace(s,"{").replace(a,"},")}]`;return{oldListText:e,newListText:e.replace(r,i)}}function H(e){if(!(e.indexOf(m)>0))return{newBankStatementTransactions:null,oldBankStatementTransactions:null};const n=J(e.substring(e.indexOf(m),e.indexOf(S)+S.length));return{newBankStatementTransactions:n.newListText,oldBankStatementTransactions:n.oldListText}}function V(e){if(!(e.indexOf(R)>0))return{newCreditCardStatementTransactions:null,oldCreditCardStatementTransactions:null};const n=e.substring(e.indexOf(R),e.indexOf(p)+p.length),r=J(n);return{newCreditCardStatementTransactions:r.newListText,oldCreditCardStatementTransactions:r.oldListText}}function Ot(e){const t=[];for(const n of e)t.push(...n.replace("<?xml"," ").replace("<?OFX"," ").replace("?>"," ").split(" ").filter(Boolean).map(K).map(r=>r.replace("=",":").replace(/["]/g,"")));return t}function At(e,t){const n={};e.join("").search("<?")>-1&&(e=Ot(e));for(const r of e){const[s,a]=r.split(":"),i=s.replace(`
5
+ `,"").toUpperCase();n[i]=t&&W(s,a)?Number(a):String(a).replace(/\?>/,"")}return n}function wt(e){return e.reduce((t,n)=>{const r=Math.abs(+n.TRNAMT);return P(n)?(t.amountOfDebits++,t.debit+=r,t):(t.amountOfCredits++,t.credit+=r,t)},{credit:0,debit:0,amountOfCredits:0,amountOfDebits:0})}class x{config;rawDates;internConfig={};sanitizeConfig;constructor(t,n=!1){this.config=t,this.rawDates=n,this.internConfig=t,this.sanitizeConfig={...t,rawDates:n}}getConfig(){return this.internConfig}withRawDates(){return new x(this.config,!0)}sanitizeRow(t){return bt(t,this.sanitizeConfig)}getPartialJsonData(t){const[n,r]=t.split("<OFX>"),s=Rt("<OFX>"+r),{sanitizeRow:a}=this;return s.replace(nt,(i,o,c,f)=>o===f?i:`<${o}>${c}
6
+ </${f}>`).replace(/<(?=[^\/])/g,`
7
+ <`).replace(h,i=>z(i)).replace(B,i=>X(i)).split(`
8
+ `).map(K).filter(Boolean).map(a,this).join("")}}function M(e){return e.toString()}async function _(e){return await new Promise((n,r)=>{if(typeof window<"u"&&window.FileReader){const s=new window.FileReader;s.onload=a=>n(a.target.result),s.onerror=a=>r(a.target.error),s.readAsText(e)}else r(new Error("FileReader is not available in this environment."))})}class d{data;dataRead="";constructor(t){this.data=t,t&&(this.dataRead=t)}getData(){return this.dataRead}fromString(t){return new d(t)}fromBuffer(t){return new d(M(t))}async fromBlob(t){return new d(await _(t))}static fromString(t){return new d(t)}static fromBuffer(t){return new d(M(t))}static async fromBlob(t){return new d(await _(t))}}class xt{configInstance={}}class L extends xt{setConfig(t){this.configInstance=t}getBankTransferList(t){const{newBankStatementTransactions:n}=H(this.configInstance.getPartialJsonData(t));return n?w(`{${I(n)}}`)?.BANKMSGSRSV1?.STMTTRNRS?.STMTRS?.BANKTRANLIST?.STRTTRN:[]}getCreditCardTransferList(t){const{newCreditCardStatementTransactions:n}=V(this.configInstance.getPartialJsonData(t));return n?w(`{${I(n)}}`)?.CREDITCARDMSGSRSV1?.CCSTMTTRNRS?.CCSTMTRS?.BANKTRANLIST?.STRTTRN:[]}getTransactionsSummary(t){let n=[],r=[];const s=this.configInstance?.getConfig?.().parserMode||"strict";let a=null,i=null;try{n=this.getBankTransferList(t)||[]}catch(F){a=F,n=[]}try{r=this.getCreditCardTransferList(t)||[]}catch(F){i=F,r=[]}const o=t.includes("<STMTTRN>");if(s==="strict"&&a&&i&&o)throw a;const c=n.length?n:r,f=n.length?m:R,g=n.length?S:p,T=t.indexOf(f),u=t.indexOf(g),Z=T>=0&&u>T?t.slice(T,u+g.length):"",tt=Z.match(/<DTSTART>([^\n<]+)/),et=Z.match(/<DTEND>([^\n<]+)/),Ut=wt(c||[]),vt=tt?.[1]?y({dateString:tt[1],formatDate:this.configInstance.getConfig().formatDate}):"",Pt=et?.[1]?y({dateString:et[1],formatDate:this.configInstance.getConfig().formatDate}):"";return{dateStart:vt,dateEnd:Pt,...Ut}}getContent(t){const n=this.configInstance.getPartialJsonData(t),{newBankStatementTransactions:r,oldBankStatementTransactions:s}=H(n),{newCreditCardStatementTransactions:a,oldCreditCardStatementTransactions:i}=V(n);let o=n;return r&&(o=o.replace(s,r)),a&&(o=o.replace(i,a)),w(`{${I(o)}}`)}}const Mt="y-M-d";function j(e){if(e==null)return"";if(typeof e=="string"||typeof e=="number")return String(e);if(typeof e=="object"){const t=e;if(t.date!==void 0&&t.transactionCode!==void 0&&t.protocol!==void 0)return`${t.date}${t.transactionCode}${t.protocol}`;try{return JSON.stringify(e)}catch{return String(e)}}return String(e)}function Y(e){if(e==null)return null;const t=Number(String(e).replace(/,/g,"."));return Number.isNaN(t)?null:t}function _t(e){return e.normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/\s+/g," ").trim().toLowerCase()}function Lt(e,t){const n=t.dateMode||"formatted";if(!e)return{value:null,warning:{code:"MISSING_DATE",message:"Transaction does not include DTPOSTED.",severity:"warning"}};if(n==="raw"||n==="formatted"){const s=N(e);return s?n==="formatted"?{value:ot(s,t.formatDate||Mt)}:{value:e}:{value:e,warning:{code:"INVALID_DATE",message:`Unable to parse date '${e}'.`,severity:"warning"}}}const r=N(e);return r?n==="date"?{value:r}:n==="timestamp"?{value:r.getTime()}:{value:r.toISOString()}:{value:null,warning:{code:"INVALID_DATE",message:`Unable to parse date '${e}'.`,severity:"warning"}}}function Ft(e,t){const n=Y(e);return n===null?{amount:null,amountAbs:null,warning:{code:"INVALID_AMOUNT",message:`Unable to parse amount '${String(e)}'.`,severity:"warning"}}:t==="string"?{amount:String(e),amountAbs:String(Math.abs(n))}:t==="cents"?{amount:Math.round(n*100),amountAbs:Math.round(Math.abs(n)*100)}:{amount:n,amountAbs:Math.abs(n)}}function Q(e){const t=[],n=e?.OFX?.SIGNONMSGSRSV1?.SONRS?.FI||null,r=e?.OFX?.BANKMSGSRSV1?.STMTTRNRS?.STMTRS;(r?.BANKTRANLIST?.STRTTRN||r?.BANKTRANLIST?.STMTTRN||[]).forEach((o,c)=>{t.push({source:"bank",transaction:o,currency:r?.CURDEF||null,account:r?.BANKACCTFROM||null,institution:n,path:`OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.${c}`})});const a=e?.OFX?.CREDITCARDMSGSRSV1?.CCSTMTTRNRS?.CCSTMTRS;return(a?.BANKTRANLIST?.STRTTRN||a?.BANKTRANLIST?.STMTTRN||[]).forEach((o,c)=>{t.push({source:"credit_card",transaction:o,currency:a?.CURDEF||null,account:a?.CCACCTFROM||null,institution:n,path:`OFX.CREDITCARDMSGSRSV1.CCSTMTTRNRS.CCSTMTRS.BANKTRANLIST.${c}`})}),t}function Bt(e,t={}){const n=t.amountMode||"number",r=[],s=[];return Q(e).forEach(i=>{const o=[],c=String(i.transaction.MEMO||i.transaction.NAME||i.transaction.TRNTYPE||"").trim(),{amount:f,amountAbs:g,warning:T}=Ft(i.transaction.TRNAMT,n);T&&(T.path=i.path,o.push(T),r.push(T));const u=Lt(i.transaction.DTPOSTED,t);u.warning&&(u.warning.path=i.path,o.push(u.warning),r.push(u.warning)),s.push({source:i.source,direction:P(i.transaction)?"debit":"credit",amount:f,amountAbs:g,postedAt:u.value,description:c,descriptionNormalized:_t(c),fitId:j(i.transaction.FITID),currency:i.currency,account:i.account,institution:i.institution,raw:i.transaction,warnings:o})}),{transactions:s,warnings:r}}function kt(e){const t=[],n=[],r=Q(e);e?.OFX||n.push({code:"MISSING_OFX_BLOCK",message:"Missing OFX root block.",severity:"error",path:"OFX"}),r.length||t.push({code:"NO_TRANSACTIONS_FOUND",message:"No transactions were found in bank or credit card statements.",severity:"warning",path:"OFX"});const s=new Map;r.forEach(c=>{const f=j(c.transaction.FITID);f?s.set(f,(s.get(f)||0)+1):t.push({code:"MISSING_FITID",message:"Transaction does not include FITID.",severity:"warning",path:c.path}),Y(c.transaction.TRNAMT)===null&&n.push({code:"INVALID_AMOUNT",message:`Invalid amount '${String(c.transaction.TRNAMT)}'.`,severity:"error",path:c.path}),N(String(c.transaction.DTPOSTED||""))||t.push({code:"INVALID_DATE",message:`Invalid DTPOSTED '${String(c.transaction.DTPOSTED||"")}'.`,severity:"warning",path:c.path})});const a=Array.from(s.values()).filter(c=>c>1).length;a>0&&t.push({code:"DUPLICATED_FITID",message:`Found ${a} duplicated FITID values.`,severity:"warning",path:"OFX"});const i=r.filter(c=>c.source==="bank").length,o=r.filter(c=>c.source==="credit_card").length;return{isValid:n.length===0,warnings:t,errors:n,stats:{totalTransactions:r.length,bankTransactions:i,creditCardTransactions:o,duplicatedFitIds:a}}}class q{customExtractor;customExtractorInstance;dataReaderInstance=new d("");diagnostics=[];constructor(t){this.customExtractor=t,this.customExtractorInstance=t||new L,this.config({})}data(t){return this.dataReaderInstance=t,this}config(t){return this.customExtractorInstance.setConfig(new x(t)),this}getType(){return mt(this.dataReaderInstance.getData())}beginOperation(){this.diagnostics=[]}getParserMode(){return this.customExtractorInstance.configInstance.getConfig().parserMode}pushWarning(t){this.diagnostics.push(t)}safelyExecute(t,n,r){try{return r()}catch(s){if(this.getParserMode()!=="lenient")throw s;return this.pushWarning({code:"PARSE_ERROR",message:`Failed to parse OFX while running '${t}'.`,severity:"warning",context:s instanceof Error?s.message:String(s),path:t}),n}}getHeadersInternal(){return this.safelyExecute("getHeaders",{},()=>{const[t]=this.dataReaderInstance.getData().split("<OFX>");return At(String(t).split(`
9
+ `).filter(n=>!!n.trim().length),!!this.customExtractorInstance.configInstance.getConfig().nativeTypes)})}getContentInternal(){return this.safelyExecute("getContent",{OFX:{}},()=>this.customExtractorInstance.getContent(this.dataReaderInstance.getData()))}getResponseInternal(){const t=this.getHeadersInternal(),n=this.getContentInternal();return{...t,...n}}getHeaders(){return this.beginOperation(),this.getHeadersInternal()}getBankTransferList(){return this.beginOperation(),this.safelyExecute("getBankTransferList",[],()=>this.customExtractorInstance.getBankTransferList(this.dataReaderInstance.getData()))}getCreditCardTransferList(){return this.beginOperation(),this.safelyExecute("getCreditCardTransferList",[],()=>this.customExtractorInstance.getCreditCardTransferList(this.dataReaderInstance.getData()))}getTransactionsSummary(){return this.beginOperation(),this.safelyExecute("getTransactionsSummary",{credit:0,debit:0,amountOfCredits:0,amountOfDebits:0,dateStart:"",dateEnd:""},()=>this.customExtractorInstance.getTransactionsSummary(this.dataReaderInstance.getData()))}getContent(){return this.beginOperation(),this.getContentInternal()}toJson(){return this.beginOperation(),this.getResponseInternal()}getResponseWithRawDates(){const t=this.customExtractorInstance.configInstance;this.customExtractorInstance.setConfig(t.withRawDates());try{return this.getResponseInternal()}finally{this.customExtractorInstance.setConfig(t)}}toNormalized(t={}){this.beginOperation();const n=this.customExtractorInstance.configInstance.getConfig().formatDate,r=Bt(this.getResponseWithRawDates(),{...t,formatDate:t.formatDate||n});return r.transactions.forEach(s=>Et(s.raw,n)),r}validate(){return this.beginOperation(),kt(this.getResponseWithRawDates())}getWarnings(){return[...this.diagnostics]}}class b{extractor;constructor(t,n){this.extractor=new q(new L),this.extractor.data(new d(t)),this.extractor.config(n||{})}getType(){return this.extractor.getType()}static fromBuffer(t){return new b(d.fromBuffer(t).getData())}static async fromBlob(t){const n=await d.fromBlob(t);return new b(n.getData())}config(t){return this.extractor.config(t),this}getHeaders(){return this.extractor.getHeaders()}getBankTransferList(){return this.extractor.getBankTransferList()}getCreditCardTransferList(){return this.extractor.getCreditCardTransferList()}getTransactionsSummary(){return this.extractor.getTransactionsSummary()}getContent(){return this.extractor.getContent()}toJson(){return this.extractor.toJson()}toNormalized(t){return this.extractor.toNormalized(t)}validate(){return this.extractor.validate()}getWarnings(){return this.extractor.getWarnings()}}l.Extractor=q,l.Ofx=b,l.OfxExtractor=L,l.Reader=d,l.blobToString=_,l.bufferToString=M,l.fixJsonProblems=I,l.formatDate=U}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofx-data-extractor",
3
- "version": "1.4.8",
3
+ "version": "1.6.0",
4
4
  "keywords": [
5
5
  "ofx",
6
6
  "financial",
@@ -28,16 +28,25 @@
28
28
  "author": "Fábio Pereira França",
29
29
  "repository": {
30
30
  "type": "git",
31
- "url": "https://github.com/Fabiopf02/ofx-ts.git"
31
+ "url": "https://github.com/Fabiopf02/ofx-data-extractor.git"
32
32
  },
33
33
  "bugs": {
34
- "url": "https://github.com/Fabiopf02/ofx-ts/issues"
34
+ "url": "https://github.com/Fabiopf02/ofx-data-extractor/issues"
35
35
  },
36
36
  "license": "MIT",
37
+ "sideEffects": false,
37
38
  "description": "A module written in TypeScript that provides a utility to extract data from an OFX file in Node.js and Browser",
38
39
  "main": "dist/umd/bundle.min.js",
39
40
  "module": "dist/esm/index.min.js",
40
41
  "typings": "dist/index.d.ts",
42
+ "exports": {
43
+ ".": {
44
+ "types": "./dist/index.d.ts",
45
+ "import": "./dist/esm/index.min.js",
46
+ "require": "./dist/umd/bundle.min.js",
47
+ "default": "./dist/esm/index.min.js"
48
+ }
49
+ },
41
50
  "files": [
42
51
  "dist/**/*"
43
52
  ],
@@ -45,25 +54,26 @@
45
54
  "test": "jest tests/*.spec.ts --config jest.config.js",
46
55
  "test:coverage": "jest tests/*.spec.ts --coverage --config jest.config.js",
47
56
  "build": "rollup --config",
57
+ "build:analyze": "npm run build && node -e \"const fs=require('fs');const z=require('zlib');const files=['dist/esm/index.min.js','dist/umd/bundle.min.js'];for(const f of files){const b=fs.readFileSync(f);const gz=z.gzipSync(b);console.log(f, 'raw='+b.length+'B', 'gzip='+gz.length+'B');}\"",
48
58
  "start": "node dist/index.js",
49
59
  "format": "prettier --write \"src/**/*.ts\"",
50
60
  "prepublish": "npm run build",
51
61
  "lint": "tslint -p tsconfig.json"
52
62
  },
53
63
  "devDependencies": {
54
- "@rollup/plugin-commonjs": "^25.0.3",
55
- "@rollup/plugin-node-resolve": "^15.1.0",
64
+ "@rollup/plugin-commonjs": "^25.0.8",
65
+ "@rollup/plugin-node-resolve": "^15.3.1",
56
66
  "@rollup/plugin-typescript": "^10.0.1",
57
- "@types/jest": "^29.2.3",
58
- "@types/node": "^18.11.10",
59
- "esbuild": "^0.25.0",
60
- "jest": "^29.3.1",
61
- "prettier": "^2.8.0",
62
- "rollup": "^3.5.1",
67
+ "@types/jest": "^29.5.14",
68
+ "@types/node": "^18.19.130",
69
+ "esbuild": "^0.28.1",
70
+ "jest": "^29.7.0",
71
+ "prettier": "^2.8.8",
72
+ "rollup": "^3.30.0",
63
73
  "rollup-plugin-esbuild": "^5.0.0",
64
- "ts-jest": "^29.0.3",
74
+ "ts-jest": "^29.4.6",
65
75
  "tslint": "^6.1.3",
66
76
  "tslint-config-prettier": "^1.18.0",
67
- "typescript": "^4.9.3"
77
+ "typescript": "^4.9.5"
68
78
  }
69
79
  }