@ashray.mehta/statement-converter 1.4.2 → 1.4.4-alpha
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/.github/dependabot.yml +7 -0
- package/.github/workflows/node.js.yml +28 -0
- package/.github/workflows/npm-publish.yml +35 -0
- package/.mocharc.json +5 -0
- package/README.md +1 -0
- package/package.json +8 -10
- package/src/StatementConverter.ts +32 -0
- package/src/adapters/ABNAdapter.ts +45 -0
- package/src/adapters/AxisAdapter.ts +42 -0
- package/src/adapters/ICICIAdapter.ts +50 -0
- package/src/adapters/ICICICreditCardAdapter.ts +50 -0
- package/src/adapters/MT940Adapter.ts +49 -0
- package/src/adapters/N26Adapter.ts +45 -0
- package/src/adapters/StandardCharteredAdapter.ts +46 -0
- package/src/adapters/TransactionAdapter.ts +8 -0
- package/src/adapters/TransactionsQifConverter.ts +28 -0
- package/src/helpers/NumberUtil.ts +27 -0
- package/src/helpers/XLSXUtil.ts +38 -0
- package/{index.d.ts → src/index.ts} +1 -1
- package/src/models/Bank.ts +9 -0
- package/{models/Transaction.d.ts → src/models/Transaction.ts} +1 -1
- package/test/AxisBankStatement.xls +0 -0
- package/test/ICICIBankStatement.xls +0 -0
- package/test/N26Statement.csv +29 -0
- package/test/NumberUtil.spec.js +32 -0
- package/test/StatementConverter.spec.js +34 -0
- package/tsconfig.json +17 -0
- package/StatementConverter.d.ts +0 -7
- package/StatementConverter.js +0 -45
- package/adapters/ABNAdapter.d.ts +0 -7
- package/adapters/ABNAdapter.js +0 -55
- package/adapters/AxisAdapter.d.ts +0 -7
- package/adapters/AxisAdapter.js +0 -53
- package/adapters/ICICIAdapter.d.ts +0 -8
- package/adapters/ICICIAdapter.js +0 -56
- package/adapters/ICICICreditCardAdapter.d.ts +0 -8
- package/adapters/ICICICreditCardAdapter.js +0 -56
- package/adapters/MT940Adapter.d.ts +0 -7
- package/adapters/MT940Adapter.js +0 -56
- package/adapters/N26Adapter.d.ts +0 -7
- package/adapters/N26Adapter.js +0 -56
- package/adapters/StandardCharteredAdapter.d.ts +0 -7
- package/adapters/StandardCharteredAdapter.js +0 -52
- package/adapters/TransactionAdapter.d.ts +0 -6
- package/adapters/TransactionAdapter.js +0 -6
- package/adapters/TransactionsQifConverter.d.ts +0 -5
- package/adapters/TransactionsQifConverter.js +0 -31
- package/helpers/NumberUtil.d.ts +0 -3
- package/helpers/NumberUtil.js +0 -17
- package/helpers/XLSXUtil.d.ts +0 -6
- package/helpers/XLSXUtil.js +0 -41
- package/index.js +0 -18
- package/models/Bank.d.ts +0 -9
- package/models/Bank.js +0 -13
- package/models/Transaction.js +0 -2
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
|
2
|
+
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
|
3
|
+
|
|
4
|
+
name: Build
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches: [ master ]
|
|
9
|
+
pull_request:
|
|
10
|
+
branches: [ master ]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
strategy:
|
|
18
|
+
matrix:
|
|
19
|
+
node-version: [10.x, 12.x, 14.x]
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v2
|
|
23
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
24
|
+
uses: actions/setup-node@v1
|
|
25
|
+
with:
|
|
26
|
+
node-version: ${{ matrix.node-version }}
|
|
27
|
+
- run: npm ci
|
|
28
|
+
- run: npm run build --if-present
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
|
|
2
|
+
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
|
|
3
|
+
|
|
4
|
+
name: Node.js Package
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [created]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v2
|
|
15
|
+
- uses: actions/setup-node@v1
|
|
16
|
+
with:
|
|
17
|
+
node-version: 12
|
|
18
|
+
- run: npm ci
|
|
19
|
+
- run: npm test
|
|
20
|
+
|
|
21
|
+
publish-npm:
|
|
22
|
+
needs: build
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v2
|
|
26
|
+
- uses: actions/setup-node@v1
|
|
27
|
+
with:
|
|
28
|
+
node-version: 12
|
|
29
|
+
registry-url: https://registry.npmjs.org/
|
|
30
|
+
- run: npm ci
|
|
31
|
+
- run: npm run build
|
|
32
|
+
- run: npm publish
|
|
33
|
+
working-directory: 'build/out'
|
|
34
|
+
env:
|
|
35
|
+
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
|
package/.mocharc.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+

|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ashray.mehta/statement-converter",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4-alpha",
|
|
4
4
|
"description": "Library to convert statements from frequently used banks to QIF.",
|
|
5
|
-
"main": "index.js",
|
|
5
|
+
"main": "dist/out/index.js",
|
|
6
|
+
"types": "dist/out/index.d.ts",
|
|
6
7
|
"scripts": {
|
|
7
|
-
"
|
|
8
|
+
"prebuild": "npm run clean",
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"clean": "rm -rf build"
|
|
8
11
|
},
|
|
9
12
|
"author": "Ashray Mehta",
|
|
10
13
|
"repository": {
|
|
@@ -19,7 +22,7 @@
|
|
|
19
22
|
"multi-number-parse": "^1.1.0",
|
|
20
23
|
"qif": "0.0.2",
|
|
21
24
|
"stitch-swiftmessageparser": "^2.0.1",
|
|
22
|
-
"xlsx": "0.
|
|
25
|
+
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz"
|
|
23
26
|
},
|
|
24
27
|
"devDependencies": {
|
|
25
28
|
"@types/chai": "^4.2.14",
|
|
@@ -29,12 +32,7 @@
|
|
|
29
32
|
"@types/node": "^18.11.7",
|
|
30
33
|
"@types/yargs": "^12.0.1",
|
|
31
34
|
"chai": "^4.2.0",
|
|
32
|
-
"
|
|
33
|
-
"gulp": "^4.0.0",
|
|
34
|
-
"gulp-run": "^1.7.1",
|
|
35
|
-
"gulp-typescript": "^5.0.0",
|
|
36
|
-
"gulp-zip": "^4.2.0",
|
|
37
|
-
"mocha": "^8.2.1",
|
|
35
|
+
"mocha": "^10.4.0",
|
|
38
36
|
"ts-node": "^10.9.1",
|
|
39
37
|
"typescript": "^4.8.3"
|
|
40
38
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Bank } from "./models/Bank";
|
|
2
|
+
import { N26Adapter } from "./adapters/N26Adapter";
|
|
3
|
+
import { ABNAdapter } from "./adapters/ABNAdapter";
|
|
4
|
+
import { AxisAdapter } from "./adapters/AxisAdapter";
|
|
5
|
+
import { ICICIAdapter } from "./adapters/ICICIAdapter";
|
|
6
|
+
import { ICICICreditCardAdapter } from "./adapters/ICICICreditCardAdapter";
|
|
7
|
+
import { StandardCharteredAdapter } from "./adapters/StandardCharteredAdapter";
|
|
8
|
+
import { TransactionsQifConverter } from "./adapters/TransactionsQifConverter";
|
|
9
|
+
import { MT940Adapter } from "./adapters/MT940Adapter";
|
|
10
|
+
|
|
11
|
+
export class StatementConverter {
|
|
12
|
+
private readonly transactionsToQif = new TransactionsQifConverter();
|
|
13
|
+
private readonly allAdapters = [
|
|
14
|
+
new AxisAdapter(),
|
|
15
|
+
new ICICIAdapter(),
|
|
16
|
+
new ICICICreditCardAdapter(),
|
|
17
|
+
new StandardCharteredAdapter(),
|
|
18
|
+
new ABNAdapter(),
|
|
19
|
+
new N26Adapter(),
|
|
20
|
+
new MT940Adapter(),
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
public async convert(bank: Bank, fileData: ArrayBuffer): Promise<Buffer> {
|
|
24
|
+
const adapter = this.allAdapters.find((adapter) => adapter.supports(bank));
|
|
25
|
+
if (!adapter) {
|
|
26
|
+
throw new Error(`No adapter found for bank [${bank}]`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const transactions = await adapter.convert(fileData);
|
|
30
|
+
return this.transactionsToQif.convert(transactions);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Bank } from '..';
|
|
2
|
+
import { XLSXUtil } from '../helpers/XLSXUtil';
|
|
3
|
+
import { Transaction } from '../models/Transaction';
|
|
4
|
+
import { TransactionAdapter } from './TransactionAdapter';
|
|
5
|
+
import XLSX = require('xlsx');
|
|
6
|
+
import moment = require('moment');
|
|
7
|
+
|
|
8
|
+
export class ABNAdapter extends TransactionAdapter {
|
|
9
|
+
public async convert(xlsxData: ArrayBuffer): Promise<Transaction[]> {
|
|
10
|
+
const workBook = XLSX.read(xlsxData, { raw: true, type: 'array' });
|
|
11
|
+
const sheet = workBook.Sheets[workBook.SheetNames[0]];
|
|
12
|
+
const addressForTransactionDate = XLSXUtil.findText(sheet, "transactiondate");
|
|
13
|
+
const addressForDescription = XLSXUtil.findText(sheet, "description");
|
|
14
|
+
const addressForAmount = XLSXUtil.findText(sheet, "amount");
|
|
15
|
+
const startingRow = addressForTransactionDate.r + 1;
|
|
16
|
+
const range = XLSX.utils.decode_range(sheet['!ref']);
|
|
17
|
+
const rangeEnd = range.e;
|
|
18
|
+
const lastRow = rangeEnd.r;
|
|
19
|
+
const rows = [...Array(1 + lastRow - startingRow).keys()].map(v => startingRow + v);
|
|
20
|
+
return rows.map(row => {
|
|
21
|
+
const parsedDate = moment(XLSXUtil.getCellValue(sheet, row, addressForTransactionDate.c) as string, 'YYYYMMDD');
|
|
22
|
+
if (!parsedDate.isValid()) return undefined;
|
|
23
|
+
const date = parsedDate.toDate();
|
|
24
|
+
|
|
25
|
+
const amount = XLSXUtil.getNumberInCell(sheet, row, addressForAmount.c);
|
|
26
|
+
const outflow = amount < 0 ? Math.abs(amount) : 0;
|
|
27
|
+
const inflow = amount >= 0 ? amount : 0;
|
|
28
|
+
|
|
29
|
+
const memo = XLSXUtil.getCellValue(sheet, row, addressForDescription.c);
|
|
30
|
+
|
|
31
|
+
return <Transaction>{
|
|
32
|
+
Payee: memo,
|
|
33
|
+
Outflow: outflow,
|
|
34
|
+
Inflow: inflow,
|
|
35
|
+
Date: date,
|
|
36
|
+
Memo: memo,
|
|
37
|
+
Category: null
|
|
38
|
+
};
|
|
39
|
+
}).filter(row => !!row);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public supports(bank: Bank): boolean {
|
|
43
|
+
return bank === Bank.ABN;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Bank } from '..';
|
|
2
|
+
import { XLSXUtil } from '../helpers/XLSXUtil';
|
|
3
|
+
import { Transaction } from '../models/Transaction';
|
|
4
|
+
import { TransactionAdapter } from './TransactionAdapter';
|
|
5
|
+
import XLSX = require("xlsx");
|
|
6
|
+
import moment = require('moment');
|
|
7
|
+
|
|
8
|
+
export class AxisAdapter extends TransactionAdapter {
|
|
9
|
+
public async convert(xlsxData: ArrayBuffer): Promise<Transaction[]> {
|
|
10
|
+
const workBook = XLSX.read(xlsxData, { raw: true, type: 'array' });
|
|
11
|
+
const sheet = workBook.Sheets[workBook.SheetNames[0]];
|
|
12
|
+
const addressForTransactionDate = XLSXUtil.findText(sheet, "Tran Date");
|
|
13
|
+
const addressForDetails = XLSXUtil.findText(sheet, "PARTICULARS");
|
|
14
|
+
const addressForDepositAmount = XLSXUtil.findText(sheet, "CR");
|
|
15
|
+
const addressForWithdrawalAmount = XLSXUtil.findText(sheet, "DR");
|
|
16
|
+
const startingRow = addressForTransactionDate.r + 1;
|
|
17
|
+
const range = XLSX.utils.decode_range(sheet['!ref']);
|
|
18
|
+
const rangeEnd = range.e;
|
|
19
|
+
const lastRow = rangeEnd.r;
|
|
20
|
+
const rows = [...Array(1 + lastRow - startingRow).keys()].map(v => startingRow + v);
|
|
21
|
+
return rows.map(row => {
|
|
22
|
+
const parsedDate = moment(XLSXUtil.getCellValue(sheet, row, addressForTransactionDate.c) as string, 'DD-MM-YYYY');
|
|
23
|
+
if (!parsedDate.isValid()) return undefined;
|
|
24
|
+
|
|
25
|
+
const date = parsedDate.toDate();
|
|
26
|
+
const payee = XLSXUtil.getCellValue(sheet, row, addressForDetails.c);
|
|
27
|
+
|
|
28
|
+
return <Transaction>{
|
|
29
|
+
Payee: payee,
|
|
30
|
+
Outflow: XLSXUtil.getNumberInCell(sheet, row, addressForWithdrawalAmount.c),
|
|
31
|
+
Inflow: XLSXUtil.getNumberInCell(sheet, row, addressForDepositAmount.c),
|
|
32
|
+
Date: date,
|
|
33
|
+
Memo: payee,
|
|
34
|
+
Category: null
|
|
35
|
+
};
|
|
36
|
+
}).filter(row => !!row);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public supports(bank: Bank) {
|
|
40
|
+
return bank === Bank.Axis;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import XLSX = require('xlsx');
|
|
2
|
+
import { Bank } from '..';
|
|
3
|
+
import { WorkSheet } from 'xlsx';
|
|
4
|
+
import { XLSXUtil } from '../helpers/XLSXUtil';
|
|
5
|
+
import { Transaction } from '../models/Transaction';
|
|
6
|
+
import { TransactionAdapter } from './TransactionAdapter';
|
|
7
|
+
import moment = require('moment');
|
|
8
|
+
|
|
9
|
+
export class ICICIAdapter extends TransactionAdapter {
|
|
10
|
+
public async convert(xlsxData: ArrayBuffer): Promise<Transaction[]> {
|
|
11
|
+
const workBook = XLSX.read(xlsxData, { raw: true, type: 'array' });
|
|
12
|
+
const sheet = workBook.Sheets[workBook.SheetNames[0]];
|
|
13
|
+
|
|
14
|
+
const addressForTransactionDate = XLSXUtil.findText(sheet, "Transaction Date");
|
|
15
|
+
const addressForDetails = XLSXUtil.findText(sheet, "Transaction Remark");
|
|
16
|
+
const addressForAmount = XLSXUtil.findText(sheet, "Amount (INR)");
|
|
17
|
+
const addressForCreditOrDebit = XLSXUtil.findText(sheet, "CR/DR");
|
|
18
|
+
|
|
19
|
+
const addressForLegend = XLSXUtil.findText(sheet, "Legends Used in Account Statement");
|
|
20
|
+
const startingRow = addressForTransactionDate.r + 1;
|
|
21
|
+
const lastRow = addressForLegend.r - 1;
|
|
22
|
+
|
|
23
|
+
const rows = [...Array(1 + lastRow - startingRow).keys()].map(v => startingRow + v);
|
|
24
|
+
|
|
25
|
+
return rows.map(row => {
|
|
26
|
+
const isOutflow = this.isOutflow(sheet, row, addressForCreditOrDebit.c);
|
|
27
|
+
const amount = XLSXUtil.getNumberInCell(sheet, row, addressForAmount.c);
|
|
28
|
+
const date = moment(XLSXUtil.getCellValue(sheet, row, addressForTransactionDate.c) as string, 'DD/MM/YYYY').toDate();
|
|
29
|
+
const payee = XLSXUtil.getCellValue(sheet, row, addressForDetails.c);
|
|
30
|
+
|
|
31
|
+
return <Transaction>{
|
|
32
|
+
Payee: payee,
|
|
33
|
+
Outflow: isOutflow ? amount : 0,
|
|
34
|
+
Inflow: !isOutflow ? amount : 0,
|
|
35
|
+
Date: date,
|
|
36
|
+
Memo: payee,
|
|
37
|
+
Category: null
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private isOutflow(sheet: WorkSheet, row: number, column: number): boolean {
|
|
43
|
+
const cellValue = XLSXUtil.getCellValue(sheet, row, column) as string;
|
|
44
|
+
return cellValue.includes("Dr.");
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
public supports(bank: Bank): boolean {
|
|
48
|
+
return bank === Bank.ICICI;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import XLSX = require('xlsx');
|
|
2
|
+
import { Bank } from '..';
|
|
3
|
+
import { WorkSheet } from 'xlsx';
|
|
4
|
+
import { XLSXUtil } from '../helpers/XLSXUtil';
|
|
5
|
+
import { Transaction } from '../models/Transaction';
|
|
6
|
+
import { TransactionAdapter } from './TransactionAdapter';
|
|
7
|
+
import moment = require('moment');
|
|
8
|
+
|
|
9
|
+
export class ICICICreditCardAdapter extends TransactionAdapter {
|
|
10
|
+
public async convert(xlsxData: ArrayBuffer): Promise<Transaction[]> {
|
|
11
|
+
const workBook = XLSX.read(xlsxData, { raw: true, type: 'array' });
|
|
12
|
+
const sheet = workBook.Sheets[workBook.SheetNames[0]];
|
|
13
|
+
|
|
14
|
+
const addressForTransactionDate = XLSXUtil.findText(sheet, "Transaction Date");
|
|
15
|
+
const addressForDetails = XLSXUtil.findText(sheet, "Details");
|
|
16
|
+
const addressForAmount = XLSXUtil.findText(sheet, "Amount (INR)");
|
|
17
|
+
const addressForReferenceNumber = XLSXUtil.findText(sheet, "Reference Number");
|
|
18
|
+
|
|
19
|
+
const startingRow = addressForTransactionDate.r + 1;
|
|
20
|
+
const range = XLSX.utils.decode_range(sheet['!ref']);
|
|
21
|
+
|
|
22
|
+
const rangeEnd = range.e;
|
|
23
|
+
const lastRow = rangeEnd.r;
|
|
24
|
+
|
|
25
|
+
const rows = [...Array(1 + lastRow - startingRow).keys()].map(v => startingRow + v);
|
|
26
|
+
|
|
27
|
+
return rows.map(row => {
|
|
28
|
+
const { isOutflow, amount } = this.determineDebitOrCredit(sheet, row, addressForAmount.c);
|
|
29
|
+
const date = moment(XLSXUtil.getCellValue(sheet, row, addressForTransactionDate.c) as string, 'DD/MM/YYYY').toDate();
|
|
30
|
+
return <Transaction>{
|
|
31
|
+
Payee: XLSXUtil.getCellValue(sheet, row, addressForDetails.c),
|
|
32
|
+
Outflow: isOutflow ? amount : 0,
|
|
33
|
+
Inflow: !isOutflow ? amount : 0,
|
|
34
|
+
Date: date,
|
|
35
|
+
Memo: XLSXUtil.getCellValue(sheet, row, addressForReferenceNumber.c),
|
|
36
|
+
Category: null
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private determineDebitOrCredit(sheet: WorkSheet, row: number, column: number): { isOutflow: boolean, amount: number } {
|
|
42
|
+
const cellValue = XLSXUtil.getCellValue(sheet, row, column) as string;
|
|
43
|
+
// TODO - Replace parsing with Globalize
|
|
44
|
+
return { isOutflow: cellValue.includes("Dr."), amount: parseFloat(cellValue.replace(/,/g, '')) };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
public supports(bank: Bank): boolean {
|
|
48
|
+
return bank === Bank.ICICICreditCard;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Bank } from '..';
|
|
2
|
+
import { Transaction } from '../models/Transaction';
|
|
3
|
+
import { TransactionAdapter } from './TransactionAdapter';
|
|
4
|
+
import parser from 'stitch-swiftmessageparser';
|
|
5
|
+
import {flatMap, filter, isEmpty, negate} from 'lodash';
|
|
6
|
+
import { Transaction as SwiftTransaction } from 'stitch-swiftmessageparser/dist/lib/transaction';
|
|
7
|
+
import moment = require('moment');
|
|
8
|
+
|
|
9
|
+
export class MT940Adapter extends TransactionAdapter {
|
|
10
|
+
public async convert(data: ArrayBuffer): Promise<Transaction[]> {
|
|
11
|
+
const rows = parser.parse({
|
|
12
|
+
data: new TextDecoder().decode(data),
|
|
13
|
+
type: "mt940",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return flatMap(rows, row => row.transactions)
|
|
17
|
+
.map((row: SwiftTransaction) => {
|
|
18
|
+
const parsedDate = moment(row.date);
|
|
19
|
+
const date = parsedDate.toDate();
|
|
20
|
+
|
|
21
|
+
const amount = row.amount;
|
|
22
|
+
const outflow = amount.isNegative() ? amount.abs().toNumber() : 0;
|
|
23
|
+
const inflow = amount.isPositive() ? amount.toNumber() : 0;
|
|
24
|
+
|
|
25
|
+
const memo = filter([
|
|
26
|
+
row.bankReference,
|
|
27
|
+
row.details,
|
|
28
|
+
row.extraDetails,
|
|
29
|
+
row.reference,
|
|
30
|
+
filter(row.detailSegments, negate(isEmpty)).join(" - ")
|
|
31
|
+
], negate(isEmpty))
|
|
32
|
+
.join(" - ")
|
|
33
|
+
.trim();
|
|
34
|
+
|
|
35
|
+
return <Transaction>{
|
|
36
|
+
Payee: memo,
|
|
37
|
+
Outflow: outflow,
|
|
38
|
+
Inflow: inflow,
|
|
39
|
+
Date: date,
|
|
40
|
+
Memo: memo,
|
|
41
|
+
Category: null
|
|
42
|
+
};
|
|
43
|
+
}).filter(row => !!row);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public supports(bank: Bank): boolean {
|
|
47
|
+
return bank === Bank.GenericMT940;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Bank } from '..';
|
|
2
|
+
import { XLSXUtil } from '../helpers/XLSXUtil';
|
|
3
|
+
import { Transaction } from '../models/Transaction';
|
|
4
|
+
import { TransactionAdapter } from './TransactionAdapter';
|
|
5
|
+
import XLSX = require('xlsx');
|
|
6
|
+
import moment = require('moment');
|
|
7
|
+
|
|
8
|
+
export class N26Adapter extends TransactionAdapter {
|
|
9
|
+
public async convert(xlsxData: ArrayBuffer): Promise<Transaction[]> {
|
|
10
|
+
const workBook = XLSX.read(xlsxData, { raw: true, type: 'array' });
|
|
11
|
+
const sheet = workBook.Sheets[workBook.SheetNames[0]];
|
|
12
|
+
const addressForTransactionDate = XLSXUtil.findText(sheet, "Date");
|
|
13
|
+
const addressForDescription = XLSXUtil.findText(sheet, "Payment reference");
|
|
14
|
+
const addressForAmount = XLSXUtil.findText(sheet, "Amount (EUR)");
|
|
15
|
+
const addressForPayee = XLSXUtil.findText(sheet, "Payee");
|
|
16
|
+
const addressForCategory = XLSXUtil.findText(sheet, "Transaction type");
|
|
17
|
+
const startingRow = addressForTransactionDate.r + 1;
|
|
18
|
+
const range = XLSX.utils.decode_range(sheet['!ref']);
|
|
19
|
+
const rangeEnd = range.e;
|
|
20
|
+
const lastRow = rangeEnd.r;
|
|
21
|
+
const rows = [...Array(1 + lastRow - startingRow).keys()].map(v => startingRow + v);
|
|
22
|
+
return rows.map(row => {
|
|
23
|
+
const parsedDate = moment(XLSXUtil.getCellValue(sheet, row, addressForTransactionDate.c) as string, 'YYYYMMDD');
|
|
24
|
+
if (!parsedDate.isValid()) return undefined;
|
|
25
|
+
const date = parsedDate.toDate();
|
|
26
|
+
|
|
27
|
+
const amount = XLSXUtil.getNumberInCell(sheet, row, addressForAmount.c);
|
|
28
|
+
const outflow = amount < 0 ? Math.abs(amount) : 0;
|
|
29
|
+
const inflow = amount >= 0 ? amount : 0;
|
|
30
|
+
|
|
31
|
+
return <Transaction>{
|
|
32
|
+
Payee: XLSXUtil.getCellValue(sheet, row, addressForPayee.c),
|
|
33
|
+
Outflow: outflow,
|
|
34
|
+
Inflow: inflow,
|
|
35
|
+
Date: date,
|
|
36
|
+
Memo: XLSXUtil.getCellValue(sheet, row, addressForDescription.c),
|
|
37
|
+
Category: XLSXUtil.getCellValue(sheet, row, addressForCategory.c)
|
|
38
|
+
};
|
|
39
|
+
}).filter(row => !!row);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public supports(bank: Bank): boolean {
|
|
43
|
+
return bank === Bank.N26;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Bank } from '..';
|
|
2
|
+
import { XLSXUtil } from '../helpers/XLSXUtil';
|
|
3
|
+
import { Transaction } from '../models/Transaction';
|
|
4
|
+
import { TransactionAdapter } from './TransactionAdapter';
|
|
5
|
+
import XLSX = require("xlsx");
|
|
6
|
+
import moment = require('moment');
|
|
7
|
+
|
|
8
|
+
export class StandardCharteredAdapter extends TransactionAdapter {
|
|
9
|
+
public async convert(xlsxData: ArrayBuffer): Promise<Transaction[]> {
|
|
10
|
+
const workBook = XLSX.read(xlsxData, { raw: true, type: 'array' });
|
|
11
|
+
const sheet = workBook.Sheets[workBook.SheetNames[0]];
|
|
12
|
+
|
|
13
|
+
const addressForTransactionDate = XLSXUtil.findText(sheet, "Date");
|
|
14
|
+
const addressForDetails = XLSXUtil.findText(sheet, "Transaction");
|
|
15
|
+
const addressForDepositAmount = XLSXUtil.findText(sheet, "Deposit");
|
|
16
|
+
const addressForWithdrawalAmount = XLSXUtil.findText(sheet, "Withdrawal");
|
|
17
|
+
|
|
18
|
+
const startingRow = addressForTransactionDate.r + 1;
|
|
19
|
+
const range = XLSX.utils.decode_range(sheet['!ref']);
|
|
20
|
+
|
|
21
|
+
const rangeEnd = range.e;
|
|
22
|
+
const lastRow = rangeEnd.r;
|
|
23
|
+
|
|
24
|
+
const rows = [...Array(1 + lastRow - startingRow).keys()].map(v => startingRow + v);
|
|
25
|
+
|
|
26
|
+
return rows.map(row => {
|
|
27
|
+
const date = moment(XLSXUtil.getCellValue(sheet, row, addressForTransactionDate.c) as string, 'DD/MM/YYYY').toDate();
|
|
28
|
+
const outflow = XLSXUtil.getNumberInCell(sheet, row, addressForWithdrawalAmount.c);
|
|
29
|
+
const inflow = XLSXUtil.getNumberInCell(sheet, row, addressForDepositAmount.c);
|
|
30
|
+
const payee = XLSXUtil.getCellValue(sheet, row, addressForDetails.c);
|
|
31
|
+
|
|
32
|
+
return <Transaction>{
|
|
33
|
+
Payee: payee,
|
|
34
|
+
Outflow: outflow,
|
|
35
|
+
Inflow: inflow,
|
|
36
|
+
Date: date,
|
|
37
|
+
Memo: payee,
|
|
38
|
+
Category: null
|
|
39
|
+
};
|
|
40
|
+
}).filter(datum => moment(datum.Date).isValid());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public supports(bank: Bank): boolean {
|
|
44
|
+
return bank === Bank.StandardChartered;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import qif = require('qif');
|
|
2
|
+
import moment = require('moment');
|
|
3
|
+
import {Transaction} from '../models/Transaction';
|
|
4
|
+
|
|
5
|
+
export class TransactionsQifConverter {
|
|
6
|
+
public async convert(transactions: Transaction[]): Promise<Buffer> {
|
|
7
|
+
const qifTransactions = transactions.map(t => {
|
|
8
|
+
return <QifTransaction>{
|
|
9
|
+
date: moment(t.Date).format('D/M/YYYY'),
|
|
10
|
+
amount: t.Inflow ? t.Inflow : -t.Outflow,
|
|
11
|
+
payee: t.Payee,
|
|
12
|
+
memo: t.Memo,
|
|
13
|
+
category: t.Category
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
return qif.write({cash: qifTransactions});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface QifTransaction {
|
|
22
|
+
date: string;
|
|
23
|
+
amount: number;
|
|
24
|
+
payee: string;
|
|
25
|
+
memo: string;
|
|
26
|
+
category: string;
|
|
27
|
+
checknumber?: number;
|
|
28
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import parse from "multi-number-parse";
|
|
2
|
+
|
|
3
|
+
export class NumberUtil {
|
|
4
|
+
private readonly commonSeparators = [",", "."];
|
|
5
|
+
|
|
6
|
+
public parseNumber(text: string): Number {
|
|
7
|
+
if (!text) {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const trimmedText = text.trim();
|
|
12
|
+
if (!trimmedText) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Remove additional separators as the library seems to fail if they're present. Eg: 2,00,000.00
|
|
17
|
+
var parsableText = trimmedText;
|
|
18
|
+
for(const separator of this.commonSeparators) {
|
|
19
|
+
if (trimmedText.split(separator).length > 2) {
|
|
20
|
+
parsableText = trimmedText.replace(separator, '');
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return parse(parsableText);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { trim, isNumber } from 'lodash';
|
|
2
|
+
import { NumberUtil } from './NumberUtil';
|
|
3
|
+
import { CellAddress, utils, WorkSheet } from 'xlsx';
|
|
4
|
+
|
|
5
|
+
export class XLSXUtil {
|
|
6
|
+
public static findText(sheet: WorkSheet, text: string): CellAddress {
|
|
7
|
+
const range = utils.decode_range(sheet['!ref']);
|
|
8
|
+
const rangeEnd = range.e;
|
|
9
|
+
const rangeStart = range.s;
|
|
10
|
+
for (let R = rangeStart.r; R <= rangeEnd.r; ++R) {
|
|
11
|
+
for (let C = rangeStart.c; C <= rangeEnd.c; ++C) {
|
|
12
|
+
const cellAddress = utils.encode_cell({ c: C, r: R });
|
|
13
|
+
if (!sheet[cellAddress]) continue;
|
|
14
|
+
const cell = sheet[cellAddress];
|
|
15
|
+
|
|
16
|
+
if (!(cell.t == 's' || cell.t == 'str')) continue;
|
|
17
|
+
if (trim(cell.v) === trim(text)) return utils.decode_cell(cellAddress);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public static getCellValue(sheet: WorkSheet, row: number, column: number): string | number | boolean {
|
|
23
|
+
const cellAddress = utils.encode_cell({ c: column, r: row });
|
|
24
|
+
return !sheet[cellAddress] ? null : sheet[cellAddress].v;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public static getNumberInCell(sheet: WorkSheet, row: number, column: number): number {
|
|
28
|
+
const cellAddress = utils.encode_cell({ c: column, r: row });
|
|
29
|
+
if (!sheet[cellAddress]) return null;
|
|
30
|
+
const value = sheet[cellAddress].v;
|
|
31
|
+
if (isNumber(value)) {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const number = new NumberUtil().parseNumber(value)
|
|
36
|
+
return number?.valueOf();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export * from './StatementConverter';
|
|
2
|
-
export * from './models/Bank';
|
|
2
|
+
export * from './models/Bank';
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"Date","Payee","Account number","Transaction type","Payment reference","Amount (EUR)","Amount (Foreign Currency)","Type Foreign Currency","Exchange Rate"
|
|
2
|
+
"2021-10-01","DIGITALOCEAN.COM","","MasterCard Payment","","-5.39","-6.23","USD","0.865169"
|
|
3
|
+
"2021-10-02","SNCB-NMBS INTERNET NA","","MasterCard Payment","","-31.2","-31.2","EUR","1.0"
|
|
4
|
+
"2021-10-11","AMZN MKTP NL*S23TM24Q5","","MasterCard Payment","","-12.44","-12.44","EUR","1.0"
|
|
5
|
+
"2021-10-12","TODOIST","","MasterCard Payment","","-3.46","-4.0","USD","0.865"
|
|
6
|
+
"2021-10-19","PAYPAL *GOOGLE GOOGLE","","MasterCard Payment","","-928.0","-928.0","EUR","1.0"
|
|
7
|
+
"2021-10-26","PAYPAL *GOOGLE GOOGLE","","MasterCard Payment","","928.0","928.0","EUR","1.0"
|
|
8
|
+
"2021-11-01","DIGITALOCEAN.COM","","MasterCard Payment","","-6.43","-7.42","USD","0.866577"
|
|
9
|
+
"2021-11-01","A MEHTA","NL54ABNA0123456789","Income","N26 Account","500.0","","",""
|
|
10
|
+
"2021-11-01","PAYPAL *GOOGLE GOOGLE","","MasterCard Payment","","-928.0","-928.0","EUR","1.0"
|
|
11
|
+
"2021-11-02","SP * ANKER DE","","MasterCard Payment","","-69.98","-69.98","EUR","1.0"
|
|
12
|
+
"2021-11-04","A MEHTA","NL54ABNA0123456789","Income","N26 Account","500.0","","",""
|
|
13
|
+
"2021-11-04","Media Markt","","MasterCard Payment","","-899.0","-899.0","EUR","1.0"
|
|
14
|
+
"2021-11-04","A MEHTA","NL54ABNA0123456789","Income","N26 Account","1000.0","","",""
|
|
15
|
+
"2021-11-04","Media Markt","","MasterCard Payment","","-899.0","-899.0","EUR","1.0"
|
|
16
|
+
"2021-11-04","A MEHTA","NL54ABNA0123456789","Income","N26 Account","1000.0","","",""
|
|
17
|
+
"2021-11-04","PAYPAL *MEDIAMARKT","","MasterCard Payment","","-1.0","-1.0","EUR","1.0"
|
|
18
|
+
"2021-11-04","PAYPAL *MEDIAMARKT","","MasterCard Payment","","1.0","1.0","EUR","1.0"
|
|
19
|
+
"2021-11-04","PAYPAL *MEDIAMARKT","","MasterCard Payment","","-1.0","-1.0","EUR","1.0"
|
|
20
|
+
"2021-11-04","PAYPAL *MEDIAMARKT","","MasterCard Payment","","-899.0","-899.0","EUR","1.0"
|
|
21
|
+
"2021-11-04","PAYPAL *MEDIAMARKT","","MasterCard Payment","","1.0","1.0","EUR","1.0"
|
|
22
|
+
"2021-11-05","Media Markt","","MasterCard Payment","","899.0","899.0","EUR","1.0"
|
|
23
|
+
"2021-11-05","PAYPAL *MEDIAMARKT","","MasterCard Payment","","-1.0","-1.0","EUR","1.0"
|
|
24
|
+
"2021-11-05","PAYPAL *MEDIAMARKT","","MasterCard Payment","","-899.0","-899.0","EUR","1.0"
|
|
25
|
+
"2021-11-05","PAYPAL *MEDIAMARKT","","MasterCard Payment","","1.0","1.0","EUR","1.0"
|
|
26
|
+
"2021-11-05","A MEHTA","NL54ABNA0123456789","Income","N26 Account","1000.0","","",""
|
|
27
|
+
"2021-11-05","Saturn Online","","MasterCard Payment","","-899.0","-899.0","EUR","1.0"
|
|
28
|
+
"2021-11-06","Saturn Online","","MasterCard Payment","","899.0","899.0","EUR","1.0"
|
|
29
|
+
"2021-11-06","Media Markt","","MasterCard Payment","","899.0","899.0","EUR","1.0"
|