ofx-data-extractor 1.4.7 → 1.5.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 +82 -60
- package/dist/@types/common.d.ts +48 -0
- package/dist/@types/ofx/banking.d.ts +2 -2
- package/dist/common/analysis.d.ts +4 -0
- package/dist/common/date.d.ts +13 -0
- package/dist/common/reader.d.ts +1 -0
- package/dist/esm/index.min.js +8 -9
- package/dist/implementations/extractor.d.ts +12 -1
- package/dist/implementations/reader.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/interfaces/extractor.interface.d.ts +4 -1
- package/dist/older-implementation/main.d.ts +5 -1
- package/dist/umd/bundle.min.js +8 -9
- package/package.json +23 -13
package/README.md
CHANGED
|
@@ -1,98 +1,120 @@
|
|
|
1
1
|
# [ofx-data-extractor](https://www.npmjs.com/package/ofx-data-extractor)
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/js/ofx-data-extractor)
|
|
3
|
+
[](https://badge.fury.io/js/ofx-data-extractor)
|
|
4
|
+
[![MIT License][license-image]][license-url]
|
|
4
5
|
[](https://codecov.io/gh/Fabiopf02/ofx-data-extractor)
|
|
5
6
|
[](https://github.com/Fabiopf02/ofx-data-extractor/actions/workflows/release.yml)
|
|
6
7
|
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
27
|
+
|
|
18
28
|
```bash
|
|
19
29
|
yarn add ofx-data-extractor
|
|
20
30
|
```
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
The `Ofx` class provides the following methods:
|
|
24
|
-
|
|
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:
|
|
32
|
+
## Quick Start
|
|
38
33
|
|
|
39
|
-
```
|
|
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)
|
|
38
|
+
const ofx = new Ofx(data)
|
|
44
39
|
|
|
45
|
-
const
|
|
46
|
-
|
|
40
|
+
const raw = ofx.toJson()
|
|
41
|
+
const summary = ofx.getTransactionsSummary()
|
|
42
|
+
const normalized = ofx.toNormalized()
|
|
43
|
+
const validation = ofx.validate()
|
|
44
|
+
```
|
|
47
45
|
|
|
48
|
-
|
|
49
|
-
console.log(transactionsSummary)
|
|
46
|
+
## Node.js
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
```ts
|
|
49
|
+
import fs from 'fs'
|
|
50
|
+
import { Ofx } from 'ofx-data-extractor'
|
|
53
51
|
|
|
54
|
-
const
|
|
55
|
-
|
|
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
|
-
|
|
61
|
-
`config`: Optional configuration options for formatting the OFX data.
|
|
57
|
+
## Browser (`fromBlob` is async)
|
|
62
58
|
|
|
63
|
-
|
|
64
|
-
```typescript
|
|
59
|
+
```ts
|
|
65
60
|
import { Ofx } from 'ofx-data-extractor'
|
|
66
|
-
import fs from 'fs'
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
const
|
|
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
|
|
72
|
-
console.log(
|
|
67
|
+
const ofx = await Ofx.fromBlob(file)
|
|
68
|
+
console.log(ofx.toJson())
|
|
69
|
+
}
|
|
73
70
|
```
|
|
74
71
|
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
import { Ofx } from 'ofx-data-extractor'
|
|
72
|
+
## Main Public API (`Ofx`)
|
|
78
73
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
## Configuration (Extract)
|
|
90
|
+
|
|
91
|
+
- `nativeTypes?: boolean`
|
|
92
|
+
- `fitId?: 'normal' | 'separated'`
|
|
93
|
+
- `formatDate?: string`
|
|
94
|
+
- `parserMode?: 'strict' | 'lenient'`
|
|
95
|
+
|
|
96
|
+
Parser mode:
|
|
97
|
+
|
|
98
|
+
- `strict` (default): parsing errors throw.
|
|
99
|
+
- `lenient`: parsing errors fallback and are collected via `getWarnings()`.
|
|
100
|
+
|
|
101
|
+
## Developer Documentation
|
|
102
|
+
|
|
103
|
+
- [API Reference](docs/api.md)
|
|
104
|
+
- [Normalization Guide](docs/normalization.md)
|
|
105
|
+
- [Validation Guide](docs/validation.md)
|
|
106
|
+
- [Recipes](docs/recipes.md)
|
|
107
|
+
- [Migration Guide](docs/migration.md)
|
|
108
|
+
|
|
109
|
+
## Examples
|
|
88
110
|
|
|
111
|
+
- [Node basic](examples/node-basic.ts)
|
|
112
|
+
- [Browser basic](examples/browser-basic.ts)
|
|
113
|
+
- [Normalize + validate](examples/normalize-and-validate.ts)
|
|
89
114
|
|
|
90
|
-
|
|
91
|
-
The `Ofx` (`constructor` and `config` method) class can be configured with the following options:
|
|
115
|
+
## License
|
|
92
116
|
|
|
93
|
-
|
|
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.
|
|
117
|
+
MIT
|
|
96
118
|
|
|
97
119
|
[license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat
|
|
98
120
|
[license-url]: LICENSE
|
package/dist/@types/common.d.ts
CHANGED
|
@@ -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
|
+
};
|
|
@@ -26,7 +26,7 @@ export type BankAccount = {
|
|
|
26
26
|
export type BankTransactionList = {
|
|
27
27
|
DTSTART: string;
|
|
28
28
|
DTEND: string;
|
|
29
|
-
STMTTRN
|
|
30
|
-
STRTTRN
|
|
29
|
+
STMTTRN?: StatementTransaction[];
|
|
30
|
+
STRTTRN?: StatementTransaction[];
|
|
31
31
|
[key: string]: any;
|
|
32
32
|
};
|
|
@@ -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;
|
package/dist/common/date.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/common/reader.d.ts
CHANGED
package/dist/esm/index.min.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
${e}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
`).
|
|
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};
|
|
1
|
+
const L=/<\/[\w.]+>/g,B=/<[\w.]+>/g,g="<BANKMSGSRSV1>",m="</BANKMSGSRSV1>",S="<CREDITCARDMSGSRSV1>",C="</CREDITCARDMSGSRSV1>",D="<STMTTRN>",h="</STMTTRN>";const j=new RegExp("_#_","g"),Q=[g,D,S],Z=[m,C,h];function q(n){const t=n.slice(0,4),e=n.slice(4,6),r=n.slice(6,8),a=n.slice(8,10),s=n.slice(10,12),i=n.slice(12,14);return{yyyy:t,yy:t.slice(2),y:t,MM:e,M:e,dd:r,d:r,hh:a,h:a,mm:s,m:s,ss:i,s:i}}function _(n,t){const e=["yyyy","yy","y","MM","M","dd","d","hh","h","mm","m","ss","s"];let r=n;for(const a of e)r=r.replace(a,t[a]);return r}function tt(n,t){const e=q(n);return _(t,e)}function nt(n,t){const e=String(n.getUTCFullYear()),r=String(n.getUTCMonth()+1).padStart(2,"0"),a=String(n.getUTCDate()).padStart(2,"0"),s=String(n.getUTCHours()).padStart(2,"0"),i=String(n.getUTCMinutes()).padStart(2,"0"),c=String(n.getUTCSeconds()).padStart(2,"0");return _(t,{yyyy:e,yy:e.slice(2),y:e,MM:r,M:r,dd:a,d:a,hh:s,h:s,mm:i,m:i,ss:c,s:c})}function et(n){return n%400===0?!0:n%100===0?!1:n%4===0}function rt(n,t){return n===2?et(t)?29:28:[4,6,9,11].includes(n)?30:31}function at(n){if(!/^\d{4}-\d{2}-\d{2}$/.test(n))return null;const[t,e,r]=n.split("-").map(Number);return{year:t,month:e,day:r,hour:0,minutes:0,seconds:0}}function st(n){if(!/^\d{4}-\d{2}-\d{2}T/.test(n))return null;const t=n.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 e=Number(t[1]),r=Number(t[2]),a=Number(t[3]),s=Number(t[4]),i=Number(t[5]),c=Number(t[6]||"0");if(!v({year:e,month:r,day:a,hour:s,minutes:i,seconds:c}))return null;const l=Number(t[7]||"0"),f=Number(t[8]||"0"),T=l<0?l*60-f:l*60+f,u=new Date(Date.UTC(e,r-1,a,s,i,c)-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 it(n){const e=n.split("[")[0].split(".")[0];return/^\d{8}(\d{6})?$/.test(e)?{year:Number(e.slice(0,4)),month:Number(e.slice(4,6)),day:Number(e.slice(6,8)),hour:Number(e.slice(8,10)||"00"),minutes:Number(e.slice(10,12)||"00"),seconds:Number(e.slice(12,14)||"00")}:null}function ot(n){const t=n.match(/^(\d{1,4})[/-](\d{1,2})[/-](\d{1,4})$/);if(!t)return null;const e=Number(t[1]),r=Number(t[2]),a=Number(t[3]),s=String(t[1]).length===4,i=String(t[3]).length===4;return!s&&!i?null:{year:s?e:a,month:r,day:s?a:e,hour:0,minutes:0,seconds:0}}function ct(n){return at(n)||st(n)||ot(n)||it(n)}function v(n){return!(!Number.isFinite(n.year)||!Number.isFinite(n.month)||!Number.isFinite(n.day)||!Number.isFinite(n.hour)||!Number.isFinite(n.minutes)||!Number.isFinite(n.seconds)||n.month<1||n.month>12||n.day<1||n.day>rt(n.month,n.year)||n.hour<0||n.hour>23||n.minutes<0||n.minutes>59||n.seconds<0||n.seconds>59)}function N(n){const t=ct(n);return!t||!v(t)?null:new Date(Date.UTC(t.year,t.month-1,t.day,t.hour,t.minutes,t.seconds))}var p;(function(n){n.BANK="BANK",n.CREDIT_CARD="CREDIT_CARD"})(p||(p={}));const ut=["debit","fee","srvchg","atm","pos","check","payment","directdebit","cash","repeatpmt"],lt=n=>n.includes(g)?p.BANK:p.CREDIT_CARD;function k(n){if(String(n.TRNAMT).startsWith("-"))return!0;const t=String(n.TRNTYPE).toLocaleLowerCase();return t==="1"||ut.includes(t)}function I(n){const t=n.replace(/(\\)/g,"\\\\").replace(L,e=>$(e,!0)).replace(B,e=>P(e,!0)).replace(/(},})/g,"}}").replace(/(}")/g,'},"').replace(/(]")/g,'],"').replace(/(},])/g,"}]").replace(/(,})/g,"}").replace(/,\s*}/g,"}").replace(/(,",)/,'",').replace(j,'\\"').slice(0,-1);return t.endsWith(",")?t.slice(0,-1):t}const dt=n=>JSON.stringify({date:n.slice(0,12),transactionCode:n.slice(12,19),protocol:n.slice(19)}),Tt=n=>n.startsWith("DT"),U=n=>n.trim();function P(n,t=!1){return Q.includes(n)&&!t?n:n.replace(/[<]/g,`
|
|
2
|
+
"`).replace(/[>]/g,'":{')}function $(n,t=!1){return Z.includes(n)&&!t?`
|
|
3
|
+
${n}`:`},
|
|
4
|
+
`}function ft({fitId:n,fitValue:t}){return n==="separated"?dt(t):`"${t}",`}function gt(n){return n.search(",")>n.search(".")?n.replace(/[.]/g,"").replace(/[,]/g,"."):n.replace(/[,]/g,"")}function E({dateString:n,formatDate:t="y-M-d"}){return!/^\d{8}(\d{6})?(\.\d+)?(\[[^\]]+\])?$/.test(n)||!N(n)?n:tt(n,t)}function ht(n){let t=n.value.replace(/[{]/g,"").replace(/(},)/g,"").replace(/["]/g,"_#_");const e=n.field.replace(/['"]/g,"");return e.endsWith("AMT")&&(t=gt(t)),Tt(e)&&(t=E({dateString:t,formatDate:n.formatDate})),e==="FITID"?ft({fitId:n.fitId,fitValue:t}):n.nativeTypes&&G(e,t)?`${Number(t)},`:`"${t}",`}function mt(n,t){let e=n;return n.match(/{(\w|\W)+/)&&(e=e.replace(/({(\w|\W)+)$/,r=>ht({field:e.slice(0,e.indexOf(":")),value:r,...t}))),e}function G(n,t){return n.endsWith("ID")||n.endsWith("NUM")?!1:!isNaN(Number(t))}function K(n){const t=n.indexOf(D),e=n.lastIndexOf(h)+h.length;if(t<0||e<0||t>=e)return{oldListText:n,newListText:n};const r=n.substring(t,e),a=new RegExp(D,"g"),s=new RegExp(h,"g"),i=`"STRTTRN":[${r.replace(a,"{").replace(s,"},")}]`;return{oldListText:n,newListText:n.replace(r,i)}}function O(n){if(!(n.indexOf(g)>0))return{newBankStatementTransactions:null,oldBankStatementTransactions:null};const e=K(n.substring(n.indexOf(g),n.indexOf(m)+m.length));return{newBankStatementTransactions:e.newListText,oldBankStatementTransactions:e.oldListText}}function w(n){if(!(n.indexOf(S)>0))return{newCreditCardStatementTransactions:null,oldCreditCardStatementTransactions:null};const e=n.substring(n.indexOf(S),n.indexOf(C)+C.length),r=K(e);return{newCreditCardStatementTransactions:r.newListText,oldCreditCardStatementTransactions:r.oldListText}}function St(n){const t=[];for(const e of n)t.push(...e.replace("<?xml"," ").replace("<?OFX"," ").replace("?>"," ").split(" ").filter(Boolean).map(U).map(r=>r.replace("=",":").replace(/["]/g,"")));return t}function Ct(n,t){const e={};n.join("").search("<?")>-1&&(n=St(n));for(const r of n){const[a,s]=r.split(":"),i=a.replace(`
|
|
5
|
+
`,"").toUpperCase();e[i]=t&&G(a,s)?Number(s):String(s).replace(/\?>/,"")}return e}function Nt(n){return n.reduce((t,e)=>{const r=Math.abs(+e.TRNAMT);return k(e)?(t.amountOfDebits++,t.debit+=r,t):(t.amountOfCredits++,t.credit+=r,t)},{credit:0,debit:0,amountOfCredits:0,amountOfDebits:0})}class pt{config;internConfig={};constructor(t){this.config=t,this.internConfig=t}getConfig(){return this.internConfig}sanitizeRow(t){return mt(t,this.internConfig)}getPartialJsonData(t){const[e,r]=t.split("<OFX>"),a="<OFX>"+r,{sanitizeRow:s}=this;return a.replace(/<(?=[^\/])/g,`
|
|
6
|
+
<`).replace(L,i=>$(i)).replace(B,i=>P(i)).split(`
|
|
7
|
+
`).map(U).filter(Boolean).map(s,this).join("")}}function x(n){return n.toString()}async function F(n){return await new Promise((e,r)=>{if(typeof window<"u"&&window.FileReader){const a=new window.FileReader;a.onload=s=>e(s.target.result),a.onerror=s=>r(s.target.error),a.readAsText(n)}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(x(t))}async fromBlob(t){return new d(await F(t))}static fromString(t){return new d(t)}static fromBuffer(t){return new d(x(t))}static async fromBlob(t){return new d(await F(t))}}class Rt{configInstance={}}class X extends Rt{setConfig(t){this.configInstance=t}getBankTransferList(t){const{newBankStatementTransactions:e}=O(this.configInstance.getPartialJsonData(t));return e?JSON.parse(`{${I(e)}}`)?.BANKMSGSRSV1?.STMTTRNRS?.STMTRS?.BANKTRANLIST?.STRTTRN:[]}getCreditCardTransferList(t){const{newCreditCardStatementTransactions:e}=w(this.configInstance.getPartialJsonData(t));return e?JSON.parse(`{${I(e)}}`)?.CREDITCARDMSGSRSV1?.CCSTMTTRNRS?.CCSTMTRS?.BANKTRANLIST?.STRTTRN:[]}getTransactionsSummary(t){let e=[],r=[];const a=this.configInstance?.getConfig?.().parserMode||"strict";let s=null,i=null;try{e=this.getBankTransferList(t)||[]}catch(R){s=R,e=[]}try{r=this.getCreditCardTransferList(t)||[]}catch(R){i=R,r=[]}const c=t.includes("<STMTTRN>");if(a==="strict"&&s&&i&&c)throw s;const o=e.length?e:r,l=e.length?g:S,f=e.length?m:C,T=t.indexOf(l),u=t.indexOf(f),b=T>=0&&u>T?t.slice(T,u+f.length):"",A=b.match(/<DTSTART>([^\n<]+)/),M=b.match(/<DTEND>([^\n<]+)/),V=Nt(o||[]),H=A?.[1]?E({dateString:A[1],formatDate:this.configInstance.getConfig().formatDate}):"",Y=M?.[1]?E({dateString:M[1],formatDate:this.configInstance.getConfig().formatDate}):"";return{dateStart:H,dateEnd:Y,...V}}getContent(t){const e=this.configInstance.getPartialJsonData(t),{newBankStatementTransactions:r,oldBankStatementTransactions:a}=O(e),{newCreditCardStatementTransactions:s,oldCreditCardStatementTransactions:i}=w(e);let c=e;return r&&(c=c.replace(a,r)),s&&(c=c.replace(i,s)),JSON.parse(`{${I(c)}}`)}}function z(n){if(n==null)return"";if(typeof n=="string"||typeof n=="number")return String(n);if(typeof n=="object"){const t=n;if(t.date!==void 0&&t.transactionCode!==void 0&&t.protocol!==void 0)return`${t.date}${t.transactionCode}${t.protocol}`;try{return JSON.stringify(n)}catch{return String(n)}}return String(n)}function W(n){if(n==null)return null;const t=Number(String(n).replace(/,/g,"."));return Number.isNaN(t)?null:t}function It(n){return n.normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/\s+/g," ").trim().toLowerCase()}function Dt(n,t){const e=t.dateMode||"formatted";if(!n)return{value:null,warning:{code:"MISSING_DATE",message:"Transaction does not include DTPOSTED.",severity:"warning"}};if(e==="raw"||e==="formatted"){const a=N(n);return a?e==="formatted"&&t.formatDate?{value:nt(a,t.formatDate)}:{value:n}:{value:n,warning:{code:"INVALID_DATE",message:`Unable to parse date '${n}'.`,severity:"warning"}}}const r=N(n);return r?e==="date"?{value:r}:e==="timestamp"?{value:r.getTime()}:{value:r.toISOString()}:{value:null,warning:{code:"INVALID_DATE",message:`Unable to parse date '${n}'.`,severity:"warning"}}}function Et(n,t){const e=W(n);return e===null?{amount:null,amountAbs:null,warning:{code:"INVALID_AMOUNT",message:`Unable to parse amount '${String(n)}'.`,severity:"warning"}}:t==="string"?{amount:String(n),amountAbs:String(Math.abs(e))}:t==="cents"?{amount:Math.round(e*100),amountAbs:Math.round(Math.abs(e)*100)}:{amount:e,amountAbs:Math.abs(e)}}function J(n){const t=[],e=n?.OFX?.SIGNONMSGSRSV1?.SONRS?.FI||null,r=n?.OFX?.BANKMSGSRSV1?.STMTTRNRS?.STMTRS;(r?.BANKTRANLIST?.STRTTRN||r?.BANKTRANLIST?.STMTTRN||[]).forEach((c,o)=>{t.push({source:"bank",transaction:c,currency:r?.CURDEF||null,account:r?.BANKACCTFROM||null,institution:e,path:`OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.${o}`})});const s=n?.OFX?.CREDITCARDMSGSRSV1?.CCSTMTTRNRS?.CCSTMTRS;return(s?.BANKTRANLIST?.STRTTRN||s?.BANKTRANLIST?.STMTTRN||[]).forEach((c,o)=>{t.push({source:"credit_card",transaction:c,currency:s?.CURDEF||null,account:s?.CCACCTFROM||null,institution:e,path:`OFX.CREDITCARDMSGSRSV1.CCSTMTTRNRS.CCSTMTRS.BANKTRANLIST.${o}`})}),t}function yt(n,t={}){const e=t.amountMode||"number",r=[],a=[];return J(n).forEach(i=>{const c=[],o=String(i.transaction.MEMO||i.transaction.NAME||i.transaction.TRNTYPE||"").trim(),{amount:l,amountAbs:f,warning:T}=Et(i.transaction.TRNAMT,e);T&&(T.path=i.path,c.push(T),r.push(T));const u=Dt(i.transaction.DTPOSTED,t);u.warning&&(u.warning.path=i.path,c.push(u.warning),r.push(u.warning)),a.push({source:i.source,direction:k(i.transaction)?"debit":"credit",amount:l,amountAbs:f,postedAt:u.value,description:o,descriptionNormalized:It(o),fitId:z(i.transaction.FITID),currency:i.currency,account:i.account,institution:i.institution,raw:i.transaction,warnings:c})}),{transactions:a,warnings:r}}function bt(n){const t=[],e=[],r=J(n);n?.OFX||e.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 a=new Map;r.forEach(o=>{const l=z(o.transaction.FITID);l?a.set(l,(a.get(l)||0)+1):t.push({code:"MISSING_FITID",message:"Transaction does not include FITID.",severity:"warning",path:o.path}),W(o.transaction.TRNAMT)===null&&e.push({code:"INVALID_AMOUNT",message:`Invalid amount '${String(o.transaction.TRNAMT)}'.`,severity:"error",path:o.path}),N(String(o.transaction.DTPOSTED||""))||t.push({code:"INVALID_DATE",message:`Invalid DTPOSTED '${String(o.transaction.DTPOSTED||"")}'.`,severity:"warning",path:o.path})});const s=Array.from(a.values()).filter(o=>o>1).length;s>0&&t.push({code:"DUPLICATED_FITID",message:`Found ${s} duplicated FITID values.`,severity:"warning",path:"OFX"});const i=r.filter(o=>o.source==="bank").length,c=r.filter(o=>o.source==="credit_card").length;return{isValid:e.length===0,warnings:t,errors:e,stats:{totalTransactions:r.length,bankTransactions:i,creditCardTransactions:c,duplicatedFitIds:s}}}class At{customExtractor;customExtractorInstance;dataReaderInstance=new d("");diagnostics=[];constructor(t){this.customExtractor=t,this.customExtractorInstance=t||new X,this.config({})}data(t){return this.dataReaderInstance=t,this}config(t){return this.customExtractorInstance.setConfig(new pt(t)),this}getType(){return lt(this.dataReaderInstance.getData())}beginOperation(){this.diagnostics=[]}getParserMode(){return this.customExtractorInstance.configInstance.getConfig().parserMode}pushWarning(t){this.diagnostics.push(t)}safelyExecute(t,e,r){try{return r()}catch(a){if(this.getParserMode()!=="lenient")throw a;return this.pushWarning({code:"PARSE_ERROR",message:`Failed to parse OFX while running '${t}'.`,severity:"warning",context:a instanceof Error?a.message:String(a),path:t}),e}}getHeadersInternal(){return this.safelyExecute("getHeaders",{},()=>{const[t]=this.dataReaderInstance.getData().split("<OFX>");return Ct(String(t).split(`
|
|
8
|
+
`).filter(e=>!!e.trim().length),!!this.customExtractorInstance.configInstance.getConfig().nativeTypes)})}getContentInternal(){return this.safelyExecute("getContent",{OFX:{}},()=>this.customExtractorInstance.getContent(this.dataReaderInstance.getData()))}getResponseInternal(){const t=this.getHeadersInternal(),e=this.getContentInternal();return{...t,...e}}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()}toNormalized(t={}){this.beginOperation();const e=this.getResponseInternal();return yt(e,{...t,formatDate:t.formatDate||this.customExtractorInstance.configInstance.getConfig().formatDate})}validate(){this.beginOperation();const t=this.getResponseInternal();return bt(t)}getWarnings(){return[...this.diagnostics]}}class y{extractor;constructor(t,e){this.extractor=new At(new X),this.extractor.data(new d(t)),this.extractor.config(e||{})}getType(){return this.extractor.getType()}static fromBuffer(t){return new y(d.fromBuffer(t).getData())}static async fromBlob(t){const e=await d.fromBlob(t);return new y(e.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{At as Extractor,y as Ofx,X as OfxExtractor,d as Reader,F as blobToString,x as bufferToString,I as fixJsonProblems,tt 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,25 @@ 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
|
+
toNormalized(options?: NormalizeOptions): NormalizedOfxData;
|
|
30
|
+
validate(): ValidationReport;
|
|
31
|
+
getWarnings(): OfxDiagnostic[];
|
|
21
32
|
}
|
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
|
-
|
|
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
|
}
|
package/dist/umd/bundle.min.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
${e}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
`).
|
|
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});
|
|
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,x=/<[\w.]+>/g,m="<BANKMSGSRSV1>",S="</BANKMSGSRSV1>",C="<CREDITCARDMSGSRSV1>",N="</CREDITCARDMSGSRSV1>",y="<STMTTRN>",p="</STMTTRN>",j=new RegExp("_#_","g"),Q=[m,y,C],Z=[S,N,p];function q(n){const t=n.slice(0,4),e=n.slice(4,6),r=n.slice(6,8),a=n.slice(8,10),s=n.slice(10,12),i=n.slice(12,14);return{yyyy:t,yy:t.slice(2),y:t,MM:e,M:e,dd:r,d:r,hh:a,h:a,mm:s,m:s,ss:i,s:i}}function F(n,t){const e=["yyyy","yy","y","MM","M","dd","d","hh","h","mm","m","ss","s"];let r=n;for(const a of e)r=r.replace(a,t[a]);return r}function L(n,t){const e=q(n);return F(t,e)}function tt(n,t){const e=String(n.getUTCFullYear()),r=String(n.getUTCMonth()+1).padStart(2,"0"),a=String(n.getUTCDate()).padStart(2,"0"),s=String(n.getUTCHours()).padStart(2,"0"),i=String(n.getUTCMinutes()).padStart(2,"0"),c=String(n.getUTCSeconds()).padStart(2,"0");return F(t,{yyyy:e,yy:e.slice(2),y:e,MM:r,M:r,dd:a,d:a,hh:s,h:s,mm:i,m:i,ss:c,s:c})}function nt(n){return n%400===0?!0:n%100===0?!1:n%4===0}function et(n,t){return n===2?nt(t)?29:28:[4,6,9,11].includes(n)?30:31}function rt(n){if(!/^\d{4}-\d{2}-\d{2}$/.test(n))return null;const[t,e,r]=n.split("-").map(Number);return{year:t,month:e,day:r,hour:0,minutes:0,seconds:0}}function at(n){if(!/^\d{4}-\d{2}-\d{2}T/.test(n))return null;const t=n.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 e=Number(t[1]),r=Number(t[2]),a=Number(t[3]),s=Number(t[4]),i=Number(t[5]),c=Number(t[6]||"0");if(!B({year:e,month:r,day:a,hour:s,minutes:i,seconds:c}))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(e,r-1,a,s,i,c)-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 st(n){const e=n.split("[")[0].split(".")[0];return/^\d{8}(\d{6})?$/.test(e)?{year:Number(e.slice(0,4)),month:Number(e.slice(4,6)),day:Number(e.slice(6,8)),hour:Number(e.slice(8,10)||"00"),minutes:Number(e.slice(10,12)||"00"),seconds:Number(e.slice(12,14)||"00")}:null}function it(n){const t=n.match(/^(\d{1,4})[/-](\d{1,2})[/-](\d{1,4})$/);if(!t)return null;const e=Number(t[1]),r=Number(t[2]),a=Number(t[3]),s=String(t[1]).length===4,i=String(t[3]).length===4;return!s&&!i?null:{year:s?e:a,month:r,day:s?a:e,hour:0,minutes:0,seconds:0}}function ot(n){return rt(n)||at(n)||it(n)||st(n)}function B(n){return!(!Number.isFinite(n.year)||!Number.isFinite(n.month)||!Number.isFinite(n.day)||!Number.isFinite(n.hour)||!Number.isFinite(n.minutes)||!Number.isFinite(n.seconds)||n.month<1||n.month>12||n.day<1||n.day>et(n.month,n.year)||n.hour<0||n.hour>23||n.minutes<0||n.minutes>59||n.seconds<0||n.seconds>59)}function R(n){const t=ot(n);return!t||!B(t)?null:new Date(Date.UTC(t.year,t.month-1,t.day,t.hour,t.minutes,t.seconds))}var I;(function(n){n.BANK="BANK",n.CREDIT_CARD="CREDIT_CARD"})(I||(I={}));const ct=["debit","fee","srvchg","atm","pos","check","payment","directdebit","cash","repeatpmt"],ut=n=>n.includes(m)?I.BANK:I.CREDIT_CARD;function _(n){if(String(n.TRNAMT).startsWith("-"))return!0;const t=String(n.TRNTYPE).toLocaleLowerCase();return t==="1"||ct.includes(t)}function D(n){const t=n.replace(/(\\)/g,"\\\\").replace(h,e=>U(e,!0)).replace(x,e=>k(e,!0)).replace(/(},})/g,"}}").replace(/(}")/g,'},"').replace(/(]")/g,'],"').replace(/(},])/g,"}]").replace(/(,})/g,"}").replace(/,\s*}/g,"}").replace(/(,",)/,'",').replace(j,'\\"').slice(0,-1);return t.endsWith(",")?t.slice(0,-1):t}const lt=n=>JSON.stringify({date:n.slice(0,12),transactionCode:n.slice(12,19),protocol:n.slice(19)}),dt=n=>n.startsWith("DT"),v=n=>n.trim();function k(n,t=!1){return Q.includes(n)&&!t?n:n.replace(/[<]/g,`
|
|
2
|
+
"`).replace(/[>]/g,'":{')}function U(n,t=!1){return Z.includes(n)&&!t?`
|
|
3
|
+
${n}`:`},
|
|
4
|
+
`}function ft({fitId:n,fitValue:t}){return n==="separated"?lt(t):`"${t}",`}function Tt(n){return n.search(",")>n.search(".")?n.replace(/[.]/g,"").replace(/[,]/g,"."):n.replace(/[,]/g,"")}function b({dateString:n,formatDate:t="y-M-d"}){return!/^\d{8}(\d{6})?(\.\d+)?(\[[^\]]+\])?$/.test(n)||!R(n)?n:L(n,t)}function gt(n){let t=n.value.replace(/[{]/g,"").replace(/(},)/g,"").replace(/["]/g,"_#_");const e=n.field.replace(/['"]/g,"");return e.endsWith("AMT")&&(t=Tt(t)),dt(e)&&(t=b({dateString:t,formatDate:n.formatDate})),e==="FITID"?ft({fitId:n.fitId,fitValue:t}):n.nativeTypes&&P(e,t)?`${Number(t)},`:`"${t}",`}function ht(n,t){let e=n;return n.match(/{(\w|\W)+/)&&(e=e.replace(/({(\w|\W)+)$/,r=>gt({field:e.slice(0,e.indexOf(":")),value:r,...t}))),e}function P(n,t){return n.endsWith("ID")||n.endsWith("NUM")?!1:!isNaN(Number(t))}function $(n){const t=n.indexOf(y),e=n.lastIndexOf(p)+p.length;if(t<0||e<0||t>=e)return{oldListText:n,newListText:n};const r=n.substring(t,e),a=new RegExp(y,"g"),s=new RegExp(p,"g"),i=`"STRTTRN":[${r.replace(a,"{").replace(s,"},")}]`;return{oldListText:n,newListText:n.replace(r,i)}}function G(n){if(!(n.indexOf(m)>0))return{newBankStatementTransactions:null,oldBankStatementTransactions:null};const e=$(n.substring(n.indexOf(m),n.indexOf(S)+S.length));return{newBankStatementTransactions:e.newListText,oldBankStatementTransactions:e.oldListText}}function K(n){if(!(n.indexOf(C)>0))return{newCreditCardStatementTransactions:null,oldCreditCardStatementTransactions:null};const e=n.substring(n.indexOf(C),n.indexOf(N)+N.length),r=$(e);return{newCreditCardStatementTransactions:r.newListText,oldCreditCardStatementTransactions:r.oldListText}}function mt(n){const t=[];for(const e of n)t.push(...e.replace("<?xml"," ").replace("<?OFX"," ").replace("?>"," ").split(" ").filter(Boolean).map(v).map(r=>r.replace("=",":").replace(/["]/g,"")));return t}function St(n,t){const e={};n.join("").search("<?")>-1&&(n=mt(n));for(const r of n){const[a,s]=r.split(":"),i=a.replace(`
|
|
5
|
+
`,"").toUpperCase();e[i]=t&&P(a,s)?Number(s):String(s).replace(/\?>/,"")}return e}function Ct(n){return n.reduce((t,e)=>{const r=Math.abs(+e.TRNAMT);return _(e)?(t.amountOfDebits++,t.debit+=r,t):(t.amountOfCredits++,t.credit+=r,t)},{credit:0,debit:0,amountOfCredits:0,amountOfDebits:0})}class Nt{config;internConfig={};constructor(t){this.config=t,this.internConfig=t}getConfig(){return this.internConfig}sanitizeRow(t){return ht(t,this.internConfig)}getPartialJsonData(t){const[e,r]=t.split("<OFX>"),a="<OFX>"+r,{sanitizeRow:s}=this;return a.replace(/<(?=[^\/])/g,`
|
|
6
|
+
<`).replace(h,i=>U(i)).replace(x,i=>k(i)).split(`
|
|
7
|
+
`).map(v).filter(Boolean).map(s,this).join("")}}function A(n){return n.toString()}async function O(n){return await new Promise((e,r)=>{if(typeof window<"u"&&window.FileReader){const a=new window.FileReader;a.onload=s=>e(s.target.result),a.onerror=s=>r(s.target.error),a.readAsText(n)}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(A(t))}async fromBlob(t){return new d(await O(t))}static fromString(t){return new d(t)}static fromBuffer(t){return new d(A(t))}static async fromBlob(t){return new d(await O(t))}}class pt{configInstance={}}class M extends pt{setConfig(t){this.configInstance=t}getBankTransferList(t){const{newBankStatementTransactions:e}=G(this.configInstance.getPartialJsonData(t));return e?JSON.parse(`{${D(e)}}`)?.BANKMSGSRSV1?.STMTTRNRS?.STMTRS?.BANKTRANLIST?.STRTTRN:[]}getCreditCardTransferList(t){const{newCreditCardStatementTransactions:e}=K(this.configInstance.getPartialJsonData(t));return e?JSON.parse(`{${D(e)}}`)?.CREDITCARDMSGSRSV1?.CCSTMTTRNRS?.CCSTMTRS?.BANKTRANLIST?.STRTTRN:[]}getTransactionsSummary(t){let e=[],r=[];const a=this.configInstance?.getConfig?.().parserMode||"strict";let s=null,i=null;try{e=this.getBankTransferList(t)||[]}catch(w){s=w,e=[]}try{r=this.getCreditCardTransferList(t)||[]}catch(w){i=w,r=[]}const c=t.includes("<STMTTRN>");if(a==="strict"&&s&&i&&c)throw s;const o=e.length?e:r,f=e.length?m:C,g=e.length?S:N,T=t.indexOf(f),u=t.indexOf(g),V=T>=0&&u>T?t.slice(T,u+g.length):"",H=V.match(/<DTSTART>([^\n<]+)/),Y=V.match(/<DTEND>([^\n<]+)/),bt=Ct(o||[]),At=H?.[1]?b({dateString:H[1],formatDate:this.configInstance.getConfig().formatDate}):"",Ot=Y?.[1]?b({dateString:Y[1],formatDate:this.configInstance.getConfig().formatDate}):"";return{dateStart:At,dateEnd:Ot,...bt}}getContent(t){const e=this.configInstance.getPartialJsonData(t),{newBankStatementTransactions:r,oldBankStatementTransactions:a}=G(e),{newCreditCardStatementTransactions:s,oldCreditCardStatementTransactions:i}=K(e);let c=e;return r&&(c=c.replace(a,r)),s&&(c=c.replace(i,s)),JSON.parse(`{${D(c)}}`)}}function X(n){if(n==null)return"";if(typeof n=="string"||typeof n=="number")return String(n);if(typeof n=="object"){const t=n;if(t.date!==void 0&&t.transactionCode!==void 0&&t.protocol!==void 0)return`${t.date}${t.transactionCode}${t.protocol}`;try{return JSON.stringify(n)}catch{return String(n)}}return String(n)}function z(n){if(n==null)return null;const t=Number(String(n).replace(/,/g,"."));return Number.isNaN(t)?null:t}function Rt(n){return n.normalize("NFD").replace(/[\u0300-\u036f]/g,"").replace(/\s+/g," ").trim().toLowerCase()}function It(n,t){const e=t.dateMode||"formatted";if(!n)return{value:null,warning:{code:"MISSING_DATE",message:"Transaction does not include DTPOSTED.",severity:"warning"}};if(e==="raw"||e==="formatted"){const a=R(n);return a?e==="formatted"&&t.formatDate?{value:tt(a,t.formatDate)}:{value:n}:{value:n,warning:{code:"INVALID_DATE",message:`Unable to parse date '${n}'.`,severity:"warning"}}}const r=R(n);return r?e==="date"?{value:r}:e==="timestamp"?{value:r.getTime()}:{value:r.toISOString()}:{value:null,warning:{code:"INVALID_DATE",message:`Unable to parse date '${n}'.`,severity:"warning"}}}function Dt(n,t){const e=z(n);return e===null?{amount:null,amountAbs:null,warning:{code:"INVALID_AMOUNT",message:`Unable to parse amount '${String(n)}'.`,severity:"warning"}}:t==="string"?{amount:String(n),amountAbs:String(Math.abs(e))}:t==="cents"?{amount:Math.round(e*100),amountAbs:Math.round(Math.abs(e)*100)}:{amount:e,amountAbs:Math.abs(e)}}function W(n){const t=[],e=n?.OFX?.SIGNONMSGSRSV1?.SONRS?.FI||null,r=n?.OFX?.BANKMSGSRSV1?.STMTTRNRS?.STMTRS;(r?.BANKTRANLIST?.STRTTRN||r?.BANKTRANLIST?.STMTTRN||[]).forEach((c,o)=>{t.push({source:"bank",transaction:c,currency:r?.CURDEF||null,account:r?.BANKACCTFROM||null,institution:e,path:`OFX.BANKMSGSRSV1.STMTTRNRS.STMTRS.BANKTRANLIST.${o}`})});const s=n?.OFX?.CREDITCARDMSGSRSV1?.CCSTMTTRNRS?.CCSTMTRS;return(s?.BANKTRANLIST?.STRTTRN||s?.BANKTRANLIST?.STMTTRN||[]).forEach((c,o)=>{t.push({source:"credit_card",transaction:c,currency:s?.CURDEF||null,account:s?.CCACCTFROM||null,institution:e,path:`OFX.CREDITCARDMSGSRSV1.CCSTMTTRNRS.CCSTMTRS.BANKTRANLIST.${o}`})}),t}function Et(n,t={}){const e=t.amountMode||"number",r=[],a=[];return W(n).forEach(i=>{const c=[],o=String(i.transaction.MEMO||i.transaction.NAME||i.transaction.TRNTYPE||"").trim(),{amount:f,amountAbs:g,warning:T}=Dt(i.transaction.TRNAMT,e);T&&(T.path=i.path,c.push(T),r.push(T));const u=It(i.transaction.DTPOSTED,t);u.warning&&(u.warning.path=i.path,c.push(u.warning),r.push(u.warning)),a.push({source:i.source,direction:_(i.transaction)?"debit":"credit",amount:f,amountAbs:g,postedAt:u.value,description:o,descriptionNormalized:Rt(o),fitId:X(i.transaction.FITID),currency:i.currency,account:i.account,institution:i.institution,raw:i.transaction,warnings:c})}),{transactions:a,warnings:r}}function yt(n){const t=[],e=[],r=W(n);n?.OFX||e.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 a=new Map;r.forEach(o=>{const f=X(o.transaction.FITID);f?a.set(f,(a.get(f)||0)+1):t.push({code:"MISSING_FITID",message:"Transaction does not include FITID.",severity:"warning",path:o.path}),z(o.transaction.TRNAMT)===null&&e.push({code:"INVALID_AMOUNT",message:`Invalid amount '${String(o.transaction.TRNAMT)}'.`,severity:"error",path:o.path}),R(String(o.transaction.DTPOSTED||""))||t.push({code:"INVALID_DATE",message:`Invalid DTPOSTED '${String(o.transaction.DTPOSTED||"")}'.`,severity:"warning",path:o.path})});const s=Array.from(a.values()).filter(o=>o>1).length;s>0&&t.push({code:"DUPLICATED_FITID",message:`Found ${s} duplicated FITID values.`,severity:"warning",path:"OFX"});const i=r.filter(o=>o.source==="bank").length,c=r.filter(o=>o.source==="credit_card").length;return{isValid:e.length===0,warnings:t,errors:e,stats:{totalTransactions:r.length,bankTransactions:i,creditCardTransactions:c,duplicatedFitIds:s}}}class J{customExtractor;customExtractorInstance;dataReaderInstance=new d("");diagnostics=[];constructor(t){this.customExtractor=t,this.customExtractorInstance=t||new M,this.config({})}data(t){return this.dataReaderInstance=t,this}config(t){return this.customExtractorInstance.setConfig(new Nt(t)),this}getType(){return ut(this.dataReaderInstance.getData())}beginOperation(){this.diagnostics=[]}getParserMode(){return this.customExtractorInstance.configInstance.getConfig().parserMode}pushWarning(t){this.diagnostics.push(t)}safelyExecute(t,e,r){try{return r()}catch(a){if(this.getParserMode()!=="lenient")throw a;return this.pushWarning({code:"PARSE_ERROR",message:`Failed to parse OFX while running '${t}'.`,severity:"warning",context:a instanceof Error?a.message:String(a),path:t}),e}}getHeadersInternal(){return this.safelyExecute("getHeaders",{},()=>{const[t]=this.dataReaderInstance.getData().split("<OFX>");return St(String(t).split(`
|
|
8
|
+
`).filter(e=>!!e.trim().length),!!this.customExtractorInstance.configInstance.getConfig().nativeTypes)})}getContentInternal(){return this.safelyExecute("getContent",{OFX:{}},()=>this.customExtractorInstance.getContent(this.dataReaderInstance.getData()))}getResponseInternal(){const t=this.getHeadersInternal(),e=this.getContentInternal();return{...t,...e}}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()}toNormalized(t={}){this.beginOperation();const e=this.getResponseInternal();return Et(e,{...t,formatDate:t.formatDate||this.customExtractorInstance.configInstance.getConfig().formatDate})}validate(){this.beginOperation();const t=this.getResponseInternal();return yt(t)}getWarnings(){return[...this.diagnostics]}}class E{extractor;constructor(t,e){this.extractor=new J(new M),this.extractor.data(new d(t)),this.extractor.config(e||{})}getType(){return this.extractor.getType()}static fromBuffer(t){return new E(d.fromBuffer(t).getData())}static async fromBlob(t){const e=await d.fromBlob(t);return new E(e.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=J,l.Ofx=E,l.OfxExtractor=M,l.Reader=d,l.blobToString=O,l.bufferToString=A,l.fixJsonProblems=D,l.formatDate=L}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofx-data-extractor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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-
|
|
31
|
+
"url": "https://github.com/Fabiopf02/ofx-data-extractor.git"
|
|
32
32
|
},
|
|
33
33
|
"bugs": {
|
|
34
|
-
"url": "https://github.com/Fabiopf02/ofx-
|
|
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.
|
|
55
|
-
"@rollup/plugin-node-resolve": "^15.1
|
|
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.
|
|
58
|
-
"@types/node": "^18.
|
|
59
|
-
"esbuild": "^0.25.
|
|
60
|
-
"jest": "^29.
|
|
61
|
-
"prettier": "^2.8.
|
|
62
|
-
"rollup": "^3.
|
|
67
|
+
"@types/jest": "^29.5.14",
|
|
68
|
+
"@types/node": "^18.19.130",
|
|
69
|
+
"esbuild": "^0.25.12",
|
|
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.
|
|
74
|
+
"ts-jest": "^29.4.6",
|
|
65
75
|
"tslint": "^6.1.3",
|
|
66
76
|
"tslint-config-prettier": "^1.18.0",
|
|
67
|
-
"typescript": "^4.9.
|
|
77
|
+
"typescript": "^4.9.5"
|
|
68
78
|
}
|
|
69
79
|
}
|