india2actual 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +23 -0
- package/LICENSE +21 -0
- package/README.md +280 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +295 -0
- package/dist/cli.js.map +1 -0
- package/dist/env-file.d.ts +38 -0
- package/dist/env-file.js +40 -0
- package/dist/env-file.js.map +1 -0
- package/dist/extract/csv.d.ts +13 -0
- package/dist/extract/csv.js +60 -0
- package/dist/extract/csv.js.map +1 -0
- package/dist/extract/html-table.d.ts +3 -0
- package/dist/extract/html-table.js +77 -0
- package/dist/extract/html-table.js.map +1 -0
- package/dist/extract/index.d.ts +21 -0
- package/dist/extract/index.js +44 -0
- package/dist/extract/index.js.map +1 -0
- package/dist/extract/pdf.d.ts +47 -0
- package/dist/extract/pdf.js +439 -0
- package/dist/extract/pdf.js.map +1 -0
- package/dist/extract/sniff.d.ts +12 -0
- package/dist/extract/sniff.js +57 -0
- package/dist/extract/sniff.js.map +1 -0
- package/dist/extract/spreadsheetml.d.ts +3 -0
- package/dist/extract/spreadsheetml.js +93 -0
- package/dist/extract/spreadsheetml.js.map +1 -0
- package/dist/extract/types.d.ts +20 -0
- package/dist/extract/types.js +2 -0
- package/dist/extract/types.js.map +1 -0
- package/dist/extract/xlsx.d.ts +2 -0
- package/dist/extract/xlsx.js +64 -0
- package/dist/extract/xlsx.js.map +1 -0
- package/dist/interpret/header.d.ts +24 -0
- package/dist/interpret/header.js +57 -0
- package/dist/interpret/header.js.map +1 -0
- package/dist/interpret/roundtrip.d.ts +29 -0
- package/dist/interpret/roundtrip.js +89 -0
- package/dist/interpret/roundtrip.js.map +1 -0
- package/dist/interpret/rows.d.ts +54 -0
- package/dist/interpret/rows.js +151 -0
- package/dist/interpret/rows.js.map +1 -0
- package/dist/interpret/synonyms.d.ts +27 -0
- package/dist/interpret/synonyms.js +55 -0
- package/dist/interpret/synonyms.js.map +1 -0
- package/dist/interpret/validate.d.ts +27 -0
- package/dist/interpret/validate.js +93 -0
- package/dist/interpret/validate.js.map +1 -0
- package/dist/interpret/values.d.ts +22 -0
- package/dist/interpret/values.js +131 -0
- package/dist/interpret/values.js.map +1 -0
- package/dist/merchants-file.d.ts +14 -0
- package/dist/merchants-file.js +40 -0
- package/dist/merchants-file.js.map +1 -0
- package/dist/narration/merchants.d.ts +46 -0
- package/dist/narration/merchants.js +161 -0
- package/dist/narration/merchants.js.map +1 -0
- package/dist/narration/parse.d.ts +14 -0
- package/dist/narration/parse.js +659 -0
- package/dist/narration/parse.js.map +1 -0
- package/dist/narration/types.d.ts +25 -0
- package/dist/narration/types.js +2 -0
- package/dist/narration/types.js.map +1 -0
- package/dist/out/csv.d.ts +21 -0
- package/dist/out/csv.js +40 -0
- package/dist/out/csv.js.map +1 -0
- package/dist/out/push.d.ts +74 -0
- package/dist/out/push.js +103 -0
- package/dist/out/push.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/extract/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import ExcelJS from 'exceljs';
|
|
2
|
+
/**
|
|
3
|
+
* Render a cell as the text a human would see.
|
|
4
|
+
*
|
|
5
|
+
* Dates are the reason this exists: a date-formatted cell comes back as a
|
|
6
|
+
* `Date`, and stringifying it naively yields a locale-dependent form that the
|
|
7
|
+
* day-first date parser would then misread. Emitting ISO makes it unambiguous.
|
|
8
|
+
*/
|
|
9
|
+
function cellText(value) {
|
|
10
|
+
if (value === null || value === undefined) {
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
if (value instanceof Date) {
|
|
14
|
+
return value.toISOString().slice(0, 10);
|
|
15
|
+
}
|
|
16
|
+
if (typeof value === 'object') {
|
|
17
|
+
// Formula cells carry their computed result; hyperlinks and rich text
|
|
18
|
+
// carry display text.
|
|
19
|
+
if ('result' in value && value.result !== undefined) {
|
|
20
|
+
return cellText(value.result);
|
|
21
|
+
}
|
|
22
|
+
if ('text' in value && value.text !== undefined) {
|
|
23
|
+
return String(value.text);
|
|
24
|
+
}
|
|
25
|
+
if ('richText' in value && Array.isArray(value.richText)) {
|
|
26
|
+
return value.richText.map(part => part.text).join('');
|
|
27
|
+
}
|
|
28
|
+
if ('hyperlink' in value) {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
return String(value);
|
|
34
|
+
}
|
|
35
|
+
export async function extractXlsx(path) {
|
|
36
|
+
const workbook = new ExcelJS.Workbook();
|
|
37
|
+
await workbook.xlsx.readFile(path);
|
|
38
|
+
// Statement workbooks occasionally carry a cover or summary sheet, so the
|
|
39
|
+
// densest sheet is a better bet than the first one.
|
|
40
|
+
let best = null;
|
|
41
|
+
for (const worksheet of workbook.worksheets) {
|
|
42
|
+
const rows = [];
|
|
43
|
+
worksheet.eachRow({ includeEmpty: true }, row => {
|
|
44
|
+
const cells = [];
|
|
45
|
+
// `row.cellCount` counts to the last populated cell, which is what keeps
|
|
46
|
+
// column indexes aligned with the header.
|
|
47
|
+
for (let column = 1; column <= row.cellCount; column += 1) {
|
|
48
|
+
cells.push(cellText(row.getCell(column).value).trim());
|
|
49
|
+
}
|
|
50
|
+
rows.push(cells);
|
|
51
|
+
});
|
|
52
|
+
if (!best || rows.length > best.rows.length) {
|
|
53
|
+
best = { rows, name: worksheet.name };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!best) {
|
|
57
|
+
return { rows: [], source: { path, format: 'xlsx' } };
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
rows: best.rows,
|
|
61
|
+
source: { path, format: 'xlsx', part: best.name },
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=xlsx.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xlsx.js","sourceRoot":"","sources":["../../src/extract/xlsx.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ColumnRole } from './synonyms.js';
|
|
2
|
+
/** Column index for each role we could identify. */
|
|
3
|
+
export type ColumnMap = Partial<Record<ColumnRole, number>>;
|
|
4
|
+
export type HeaderMatch = {
|
|
5
|
+
/** Row index of the header within the table. */
|
|
6
|
+
index: number;
|
|
7
|
+
map: ColumnMap;
|
|
8
|
+
/** Number of roles identified — used to choose between candidate rows. */
|
|
9
|
+
score: number;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* A table is only usable if we can find a date, something to use as a payee,
|
|
13
|
+
* and at least one amount column.
|
|
14
|
+
*/
|
|
15
|
+
export declare function isUsable(map: ColumnMap): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Locate the transaction table's header row.
|
|
18
|
+
*
|
|
19
|
+
* Indian statements put a variable number of preamble rows above the table
|
|
20
|
+
* (branch address, account holder, statement period), so the header cannot be
|
|
21
|
+
* assumed to be row 0. Instead every row in range is scored by how many known
|
|
22
|
+
* columns it contains, and the best usable row wins.
|
|
23
|
+
*/
|
|
24
|
+
export declare function findHeader(rows: string[][]): HeaderMatch | null;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { roleForHeader } from './synonyms.js';
|
|
2
|
+
/** How far into the file to look before giving up. */
|
|
3
|
+
const MAX_HEADER_SCAN = 40;
|
|
4
|
+
/**
|
|
5
|
+
* A table is only usable if we can find a date, something to use as a payee,
|
|
6
|
+
* and at least one amount column.
|
|
7
|
+
*/
|
|
8
|
+
export function isUsable(map) {
|
|
9
|
+
const hasDate = map.date !== undefined || map.valueDate !== undefined;
|
|
10
|
+
const hasDescription = map.description !== undefined;
|
|
11
|
+
const hasAmount = map.debit !== undefined ||
|
|
12
|
+
map.credit !== undefined ||
|
|
13
|
+
map.amount !== undefined;
|
|
14
|
+
return hasDate && hasDescription && hasAmount;
|
|
15
|
+
}
|
|
16
|
+
function mapRow(row) {
|
|
17
|
+
const map = {};
|
|
18
|
+
for (const [index, cell] of row.entries()) {
|
|
19
|
+
const role = roleForHeader(cell);
|
|
20
|
+
// First occurrence wins: ICICI repeats `Value Date` and `Transaction
|
|
21
|
+
// Date`, and the leftmost match is the one the bank leads with.
|
|
22
|
+
if (role && map[role] === undefined) {
|
|
23
|
+
map[role] = index;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return map;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Locate the transaction table's header row.
|
|
30
|
+
*
|
|
31
|
+
* Indian statements put a variable number of preamble rows above the table
|
|
32
|
+
* (branch address, account holder, statement period), so the header cannot be
|
|
33
|
+
* assumed to be row 0. Instead every row in range is scored by how many known
|
|
34
|
+
* columns it contains, and the best usable row wins.
|
|
35
|
+
*/
|
|
36
|
+
export function findHeader(rows) {
|
|
37
|
+
let best = null;
|
|
38
|
+
const limit = Math.min(rows.length, MAX_HEADER_SCAN);
|
|
39
|
+
for (let index = 0; index < limit; index += 1) {
|
|
40
|
+
const row = rows[index];
|
|
41
|
+
if (!row) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const map = mapRow(row);
|
|
45
|
+
if (!isUsable(map)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const score = Object.keys(map).length;
|
|
49
|
+
// Strictly greater, so the earliest of equally-good rows wins. Some banks
|
|
50
|
+
// repeat the header on every page; the first one is the real table start.
|
|
51
|
+
if (!best || score > best.score) {
|
|
52
|
+
best = { index, map, score };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return best;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=header.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"header.js","sourceRoot":"","sources":["../../src/interpret/header.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAc9C,sDAAsD;AACtD,MAAM,eAAe,GAAG,EAAE,CAAC;AAE3B;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,GAAc;IACrC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,KAAK,SAAS,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC;IACtE,MAAM,cAAc,GAAG,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC;IACrD,MAAM,SAAS,GACb,GAAG,CAAC,KAAK,KAAK,SAAS;QACvB,GAAG,CAAC,MAAM,KAAK,SAAS;QACxB,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC;IAE3B,OAAO,OAAO,IAAI,cAAc,IAAI,SAAS,CAAC;AAChD,CAAC;AAED,SAAS,MAAM,CAAC,GAAa;IAC3B,MAAM,GAAG,GAAc,EAAE,CAAC;IAE1B,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,GAAG,CAAC,OAAO,EAAE,EAAE,CAAC;QAC1C,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;QACjC,qEAAqE;QACrE,gEAAgE;QAChE,IAAI,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;YACpC,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;QACpB,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CAAC,IAAgB;IACzC,IAAI,IAAI,GAAuB,IAAI,CAAC;IAEpC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACrD,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QAC9C,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;QACxB,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,SAAS;QACX,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QACxB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACnB,SAAS;QACX,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;QACtC,0EAA0E;QAC1E,0EAA0E;QAC1E,IAAI,CAAC,IAAI,IAAI,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YAChC,IAAI,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;QAC/B,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read this tool's own CSV output back in, so a reviewed and hand-corrected CSV
|
|
3
|
+
* can be pushed.
|
|
4
|
+
*
|
|
5
|
+
* Generic header detection requires a description column, and neither `Payee`
|
|
6
|
+
* nor `Notes` matches any bank's narration vocabulary, so without this path the
|
|
7
|
+
* tool cannot read its own output.
|
|
8
|
+
*
|
|
9
|
+
* Adding `payee`/`notes` to that vocabulary instead would feed the Notes column
|
|
10
|
+
* back through narration parsing and overwrite the Payee column, discarding the
|
|
11
|
+
* manual corrections. Hence an exact-header match with payees kept verbatim.
|
|
12
|
+
*/
|
|
13
|
+
import type { Table } from '../extract/types.js';
|
|
14
|
+
import type { Interpreted } from './rows.js';
|
|
15
|
+
/**
|
|
16
|
+
* Does this table look like output this tool produced?
|
|
17
|
+
*
|
|
18
|
+
* Strict by design: an exact header match, in order. A bank CSV that happens to
|
|
19
|
+
* have a `Payee` column must still take the generic path so its narration gets
|
|
20
|
+
* parsed.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isConvertedOutput(table: Table): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Interpret converted output, taking every field at face value.
|
|
25
|
+
*
|
|
26
|
+
* The amount is already signed, the date already ISO, and the payee already
|
|
27
|
+
* resolved, possibly by hand. Nothing here re-derives any of it.
|
|
28
|
+
*/
|
|
29
|
+
export declare function interpretConvertedOutput(table: Table): Interpreted;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read this tool's own CSV output back in, so a reviewed and hand-corrected CSV
|
|
3
|
+
* can be pushed.
|
|
4
|
+
*
|
|
5
|
+
* Generic header detection requires a description column, and neither `Payee`
|
|
6
|
+
* nor `Notes` matches any bank's narration vocabulary, so without this path the
|
|
7
|
+
* tool cannot read its own output.
|
|
8
|
+
*
|
|
9
|
+
* Adding `payee`/`notes` to that vocabulary instead would feed the Notes column
|
|
10
|
+
* back through narration parsing and overwrite the Payee column, discarding the
|
|
11
|
+
* manual corrections. Hence an exact-header match with payees kept verbatim.
|
|
12
|
+
*/
|
|
13
|
+
import { parseAmount, parseStatementDate } from './values.js';
|
|
14
|
+
/** The header `toCsv` writes. Must stay in step with `COLUMNS` there. */
|
|
15
|
+
const CONVERTED_HEADER = ['date', 'payee', 'notes', 'amount', 'reference'];
|
|
16
|
+
/** For reporting only. `description: 2` is Notes, though it is not re-parsed. */
|
|
17
|
+
const CONVERTED_MAP = {
|
|
18
|
+
date: 0,
|
|
19
|
+
description: 2,
|
|
20
|
+
amount: 3,
|
|
21
|
+
ref: 4,
|
|
22
|
+
};
|
|
23
|
+
function normalize(cell) {
|
|
24
|
+
return cell.trim().toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Does this table look like output this tool produced?
|
|
28
|
+
*
|
|
29
|
+
* Strict by design: an exact header match, in order. A bank CSV that happens to
|
|
30
|
+
* have a `Payee` column must still take the generic path so its narration gets
|
|
31
|
+
* parsed.
|
|
32
|
+
*/
|
|
33
|
+
export function isConvertedOutput(table) {
|
|
34
|
+
const header = table.rows[0];
|
|
35
|
+
if (!header) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const cells = header.map(normalize);
|
|
39
|
+
return (cells.length >= CONVERTED_HEADER.length &&
|
|
40
|
+
CONVERTED_HEADER.every((name, index) => cells[index] === name));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Interpret converted output, taking every field at face value.
|
|
44
|
+
*
|
|
45
|
+
* The amount is already signed, the date already ISO, and the payee already
|
|
46
|
+
* resolved, possibly by hand. Nothing here re-derives any of it.
|
|
47
|
+
*/
|
|
48
|
+
export function interpretConvertedOutput(table) {
|
|
49
|
+
const transactions = [];
|
|
50
|
+
const skipped = [];
|
|
51
|
+
for (let index = 1; index < table.rows.length; index += 1) {
|
|
52
|
+
const row = table.rows[index];
|
|
53
|
+
if (!row || row.every(value => !value.trim())) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const date = parseStatementDate(row[0] ?? '', 'ymd');
|
|
57
|
+
if (!date) {
|
|
58
|
+
skipped.push({ index, reason: 'no parseable date', row });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const amount = parseAmount(row[3] ?? '');
|
|
62
|
+
if (amount === null) {
|
|
63
|
+
skipped.push({ index, reason: 'no parseable amount', row });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const notes = (row[2] ?? '').trim();
|
|
67
|
+
const payee = (row[1] ?? '').trim();
|
|
68
|
+
const ref = (row[4] ?? '').trim();
|
|
69
|
+
transactions.push({
|
|
70
|
+
date,
|
|
71
|
+
amount,
|
|
72
|
+
// A blanked-out Payee still needs a value; same fallback as the parser.
|
|
73
|
+
payee: payee || notes || 'Unknown',
|
|
74
|
+
raw: notes,
|
|
75
|
+
// Already-converted rows carry no narration to classify, and the kind is
|
|
76
|
+
// only used for reporting.
|
|
77
|
+
kind: 'other',
|
|
78
|
+
...(ref ? { ref } : {}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
transactions,
|
|
83
|
+
skipped,
|
|
84
|
+
header: { index: 0, map: CONVERTED_MAP },
|
|
85
|
+
// References were already checked for uniqueness when the CSV was written.
|
|
86
|
+
droppedRefs: 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=roundtrip.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roundtrip.js","sourceRoot":"","sources":["../../src/interpret/roundtrip.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Table } from '../extract/types.js';
|
|
2
|
+
import type { MerchantRule } from '../narration/merchants.js';
|
|
3
|
+
import type { NarrationKind } from '../narration/types.js';
|
|
4
|
+
import type { ColumnMap } from './header.js';
|
|
5
|
+
import type { DateOrder } from './values.js';
|
|
6
|
+
export type StatementTransaction = {
|
|
7
|
+
/** `YYYY-MM-DD`. */
|
|
8
|
+
date: string;
|
|
9
|
+
/** Signed rupees: negative for money out. */
|
|
10
|
+
amount: number;
|
|
11
|
+
/** Cleaned merchant, for Actual's `payee_name`. */
|
|
12
|
+
payee: string;
|
|
13
|
+
/** Original narration, for Actual's `imported_payee`. */
|
|
14
|
+
raw: string;
|
|
15
|
+
kind: NarrationKind;
|
|
16
|
+
/** Only set when safe to use as `imported_id` — see `dropRepeatedRefs`. */
|
|
17
|
+
ref?: string;
|
|
18
|
+
/** Running balance, when the statement has one. Used only for validation. */
|
|
19
|
+
balance?: number;
|
|
20
|
+
};
|
|
21
|
+
export type SkippedRow = {
|
|
22
|
+
/** Row index in the original table. */
|
|
23
|
+
index: number;
|
|
24
|
+
reason: string;
|
|
25
|
+
row: string[];
|
|
26
|
+
};
|
|
27
|
+
export type InterpretResult = {
|
|
28
|
+
transactions: StatementTransaction[];
|
|
29
|
+
skipped: SkippedRow[];
|
|
30
|
+
header: {
|
|
31
|
+
index: number;
|
|
32
|
+
map: ColumnMap;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
export type InterpretOptions = {
|
|
36
|
+
dateOrder?: DateOrder;
|
|
37
|
+
merchantRules?: MerchantRule[];
|
|
38
|
+
/** Force a column map instead of detecting one (per-bank override). */
|
|
39
|
+
columnMap?: ColumnMap;
|
|
40
|
+
/** Force the header row index. */
|
|
41
|
+
headerIndex?: number;
|
|
42
|
+
};
|
|
43
|
+
export type Interpreted = InterpretResult & {
|
|
44
|
+
/** How many references were discarded as non-unique. */
|
|
45
|
+
droppedRefs: number;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Turn an extracted table into transactions.
|
|
49
|
+
*
|
|
50
|
+
* Rows that lack a parseable date or amount are skipped rather than guessed
|
|
51
|
+
* at — that is what removes statement preambles, page headers repeated
|
|
52
|
+
* mid-file, and footer totals without needing to know how many there are.
|
|
53
|
+
*/
|
|
54
|
+
export declare function interpretTable(table: Table, options?: InterpretOptions): Interpreted | null;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { parseNarration } from '../narration/parse.js';
|
|
2
|
+
import { findHeader } from './header.js';
|
|
3
|
+
import { parseAmount, parseStatementDate } from './values.js';
|
|
4
|
+
function cell(row, index) {
|
|
5
|
+
if (index === undefined) {
|
|
6
|
+
return '';
|
|
7
|
+
}
|
|
8
|
+
return row[index] ?? '';
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A zero in a Withdrawal/Deposit pair means "not this side", not a zero-rupee
|
|
12
|
+
* transaction, so it is treated as absent.
|
|
13
|
+
*/
|
|
14
|
+
function presentAmount(value) {
|
|
15
|
+
if (value === null || value === 0) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function resolveAmount(row, map) {
|
|
21
|
+
const debit = presentAmount(parseAmount(cell(row, map.debit)));
|
|
22
|
+
const credit = presentAmount(parseAmount(cell(row, map.credit)));
|
|
23
|
+
if (map.debit !== undefined || map.credit !== undefined) {
|
|
24
|
+
if (credit !== null) {
|
|
25
|
+
return Math.abs(credit);
|
|
26
|
+
}
|
|
27
|
+
if (debit !== null) {
|
|
28
|
+
return -Math.abs(debit);
|
|
29
|
+
}
|
|
30
|
+
// Fall through: some banks carry both a Dr/Cr pair and a single amount
|
|
31
|
+
// column, and only populate one of them.
|
|
32
|
+
}
|
|
33
|
+
const amount = presentAmount(parseAmount(cell(row, map.amount)));
|
|
34
|
+
if (amount === null) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
// An explicit indicator column overrides whatever sign the amount carried.
|
|
38
|
+
const indicator = cell(row, map.drcr).trim().toLowerCase();
|
|
39
|
+
if (indicator) {
|
|
40
|
+
if (/^d/.test(indicator)) {
|
|
41
|
+
return -Math.abs(amount);
|
|
42
|
+
}
|
|
43
|
+
if (/^c/.test(indicator)) {
|
|
44
|
+
return Math.abs(amount);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return amount;
|
|
48
|
+
}
|
|
49
|
+
/** Reference-column values that carry no information. */
|
|
50
|
+
function usableColumnRef(value) {
|
|
51
|
+
const trimmed = value.trim();
|
|
52
|
+
if (trimmed.length < 6) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
// `0`, `000000`, `-`, `NA`.
|
|
56
|
+
if (/^[0\s\-.]*$/.test(trimmed) || /^na$/i.test(trimmed)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return trimmed;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Strip references that occur more than once in the file.
|
|
63
|
+
*
|
|
64
|
+
* A genuine bank reference is unique per transaction, so a repeat means we
|
|
65
|
+
* picked up something else — an account number, a padded placeholder, a
|
|
66
|
+
* recurring mandate id. Leaving it in place would make Actual treat distinct
|
|
67
|
+
* transactions as the same one and silently drop them, which is strictly worse
|
|
68
|
+
* than having no reference at all (where Actual's date+amount fuzzy matching
|
|
69
|
+
* takes over).
|
|
70
|
+
*/
|
|
71
|
+
function dropRepeatedRefs(transactions) {
|
|
72
|
+
const counts = new Map();
|
|
73
|
+
for (const transaction of transactions) {
|
|
74
|
+
if (transaction.ref) {
|
|
75
|
+
counts.set(transaction.ref, (counts.get(transaction.ref) ?? 0) + 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
let dropped = 0;
|
|
79
|
+
for (const transaction of transactions) {
|
|
80
|
+
if (transaction.ref && (counts.get(transaction.ref) ?? 0) > 1) {
|
|
81
|
+
delete transaction.ref;
|
|
82
|
+
dropped += 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return dropped;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Turn an extracted table into transactions.
|
|
89
|
+
*
|
|
90
|
+
* Rows that lack a parseable date or amount are skipped rather than guessed
|
|
91
|
+
* at — that is what removes statement preambles, page headers repeated
|
|
92
|
+
* mid-file, and footer totals without needing to know how many there are.
|
|
93
|
+
*/
|
|
94
|
+
export function interpretTable(table, options = {}) {
|
|
95
|
+
let headerIndex = options.headerIndex;
|
|
96
|
+
let map = options.columnMap;
|
|
97
|
+
if (headerIndex === undefined || !map) {
|
|
98
|
+
const detected = findHeader(table.rows);
|
|
99
|
+
if (!detected) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
headerIndex = headerIndex ?? detected.index;
|
|
103
|
+
map = map ?? detected.map;
|
|
104
|
+
}
|
|
105
|
+
const transactions = [];
|
|
106
|
+
const skipped = [];
|
|
107
|
+
for (let index = headerIndex + 1; index < table.rows.length; index += 1) {
|
|
108
|
+
const row = table.rows[index];
|
|
109
|
+
if (!row || row.every(value => !value.trim())) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const dateCell = cell(row, map.date) || cell(row, map.valueDate);
|
|
113
|
+
const date = parseStatementDate(dateCell, options.dateOrder);
|
|
114
|
+
if (!date) {
|
|
115
|
+
skipped.push({ index, reason: 'no parseable date', row });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const amount = resolveAmount(row, map);
|
|
119
|
+
if (amount === null) {
|
|
120
|
+
skipped.push({ index, reason: 'no parseable amount', row });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const narration = cell(row, map.description);
|
|
124
|
+
const parsed = parseNarration(narration, {
|
|
125
|
+
...(options.merchantRules
|
|
126
|
+
? { merchantRules: options.merchantRules }
|
|
127
|
+
: {}),
|
|
128
|
+
});
|
|
129
|
+
// The narration's UTR is the most trustworthy reference; the reference
|
|
130
|
+
// column is a fallback and is subject to the uniqueness guard below.
|
|
131
|
+
const ref = parsed.ref ?? usableColumnRef(cell(row, map.ref));
|
|
132
|
+
const balance = parseAmount(cell(row, map.balance));
|
|
133
|
+
transactions.push({
|
|
134
|
+
date,
|
|
135
|
+
amount,
|
|
136
|
+
payee: parsed.merchant,
|
|
137
|
+
raw: parsed.raw,
|
|
138
|
+
kind: parsed.kind,
|
|
139
|
+
...(ref ? { ref } : {}),
|
|
140
|
+
...(balance !== null ? { balance } : {}),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const droppedRefs = dropRepeatedRefs(transactions);
|
|
144
|
+
return {
|
|
145
|
+
transactions,
|
|
146
|
+
skipped,
|
|
147
|
+
header: { index: headerIndex, map },
|
|
148
|
+
droppedRefs,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=rows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rows.js","sourceRoot":"","sources":["../../src/interpret/rows.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column vocabulary for Indian bank statements.
|
|
3
|
+
*
|
|
4
|
+
* Every Indian bank ships variations of the same seven columns, which is what
|
|
5
|
+
* makes a generic interpreter viable instead of an adapter per bank:
|
|
6
|
+
*
|
|
7
|
+
* HDFC Date | Narration | Chq./Ref.No. | Value Dt | Withdrawal Amt. | Deposit Amt. | Closing Balance
|
|
8
|
+
* ICICI S No. | Value Date | Transaction Date | Cheque Number | Transaction Remarks | Withdrawal Amount (INR) | Deposit Amount (INR) | Balance (INR)
|
|
9
|
+
* SBI Txn Date | Value Date | Description | Ref No./Cheque No. | Debit | Credit | Balance
|
|
10
|
+
* Axis Tran Date | CHQNO | PARTICULARS | DR | CR | BAL
|
|
11
|
+
*
|
|
12
|
+
* Matching is done on a normalised header cell (lowercased, non-alphanumerics
|
|
13
|
+
* stripped), so `Withdrawal Amt.` becomes `withdrawalamt` and
|
|
14
|
+
* `Deposit Amount (INR)` becomes `depositamountinr`. Prefix/substring patterns
|
|
15
|
+
* then cover the variants without enumerating every spelling.
|
|
16
|
+
*/
|
|
17
|
+
export type ColumnRole = 'date' | 'valueDate' | 'description' | 'debit' | 'credit' | 'amount' | 'drcr' | 'balance' | 'ref';
|
|
18
|
+
/**
|
|
19
|
+
* Order is significant — the first pattern to match a header cell wins.
|
|
20
|
+
*
|
|
21
|
+
* `valueDate` precedes `date` because `Value Date` also ends in "date", and
|
|
22
|
+
* `drcr` precedes `debit`/`credit` because a `DR/CR` indicator column would
|
|
23
|
+
* otherwise be mistaken for an amount column.
|
|
24
|
+
*/
|
|
25
|
+
export declare const COLUMN_PATTERNS: Array<[ColumnRole, RegExp]>;
|
|
26
|
+
export declare function normalizeHeader(cell: string): string;
|
|
27
|
+
export declare function roleForHeader(cell: string): ColumnRole | null;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column vocabulary for Indian bank statements.
|
|
3
|
+
*
|
|
4
|
+
* Every Indian bank ships variations of the same seven columns, which is what
|
|
5
|
+
* makes a generic interpreter viable instead of an adapter per bank:
|
|
6
|
+
*
|
|
7
|
+
* HDFC Date | Narration | Chq./Ref.No. | Value Dt | Withdrawal Amt. | Deposit Amt. | Closing Balance
|
|
8
|
+
* ICICI S No. | Value Date | Transaction Date | Cheque Number | Transaction Remarks | Withdrawal Amount (INR) | Deposit Amount (INR) | Balance (INR)
|
|
9
|
+
* SBI Txn Date | Value Date | Description | Ref No./Cheque No. | Debit | Credit | Balance
|
|
10
|
+
* Axis Tran Date | CHQNO | PARTICULARS | DR | CR | BAL
|
|
11
|
+
*
|
|
12
|
+
* Matching is done on a normalised header cell (lowercased, non-alphanumerics
|
|
13
|
+
* stripped), so `Withdrawal Amt.` becomes `withdrawalamt` and
|
|
14
|
+
* `Deposit Amount (INR)` becomes `depositamountinr`. Prefix/substring patterns
|
|
15
|
+
* then cover the variants without enumerating every spelling.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Order is significant — the first pattern to match a header cell wins.
|
|
19
|
+
*
|
|
20
|
+
* `valueDate` precedes `date` because `Value Date` also ends in "date", and
|
|
21
|
+
* `drcr` precedes `debit`/`credit` because a `DR/CR` indicator column would
|
|
22
|
+
* otherwise be mistaken for an amount column.
|
|
23
|
+
*/
|
|
24
|
+
export const COLUMN_PATTERNS = [
|
|
25
|
+
['valueDate', /^value(date|dt)$/],
|
|
26
|
+
['drcr', /^(drcr|crdr|debitcredit|drcrindicator)$/],
|
|
27
|
+
['drcr', /^(transaction)?type$/],
|
|
28
|
+
[
|
|
29
|
+
'date',
|
|
30
|
+
/^(txn|tran|transaction|posting|post|entry|booking|trade)?d(ate|t)$/,
|
|
31
|
+
],
|
|
32
|
+
['date', /^dateoftransaction$/],
|
|
33
|
+
['description', /(narration|particular|description|remark|detail|narrative)/],
|
|
34
|
+
['debit', /^(withdrawal|debit|dr)/],
|
|
35
|
+
['credit', /^(deposit|credit|cr)/],
|
|
36
|
+
['balance', /(balance|^bal$)/],
|
|
37
|
+
['amount', /^(amount|amt|txnamount|transactionamount)/],
|
|
38
|
+
['ref', /(chq|cheque|refno|reference|utr|rrn|transactionid)/],
|
|
39
|
+
];
|
|
40
|
+
export function normalizeHeader(cell) {
|
|
41
|
+
return cell.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
42
|
+
}
|
|
43
|
+
export function roleForHeader(cell) {
|
|
44
|
+
const normalized = normalizeHeader(cell);
|
|
45
|
+
if (!normalized) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
for (const [role, pattern] of COLUMN_PATTERNS) {
|
|
49
|
+
if (pattern.test(normalized)) {
|
|
50
|
+
return role;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=synonyms.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"synonyms.js","sourceRoot":"","sources":["../../src/interpret/synonyms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAaH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAAgC;IAC1D,CAAC,WAAW,EAAE,kBAAkB,CAAC;IACjC,CAAC,MAAM,EAAE,yCAAyC,CAAC;IACnD,CAAC,MAAM,EAAE,sBAAsB,CAAC;IAChC;QACE,MAAM;QACN,oEAAoE;KACrE;IACD,CAAC,MAAM,EAAE,qBAAqB,CAAC;IAC/B,CAAC,aAAa,EAAE,4DAA4D,CAAC;IAC7E,CAAC,OAAO,EAAE,wBAAwB,CAAC;IACnC,CAAC,QAAQ,EAAE,sBAAsB,CAAC;IAClC,CAAC,SAAS,EAAE,iBAAiB,CAAC;IAC9B,CAAC,QAAQ,EAAE,2CAA2C,CAAC;IACvD,CAAC,KAAK,EAAE,oDAAoD,CAAC;CAC9D,CAAC;AAEF,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,UAAU,GAAG,eAAe,CAAC,IAAI,CAAC,CAAC;IACzC,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,eAAe,EAAE,CAAC;QAC9C,IAAI,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAC7B,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { StatementTransaction } from './rows.js';
|
|
2
|
+
export type ValidationStatus = 'passed' | 'failed' | 'skipped';
|
|
3
|
+
export type Validation = {
|
|
4
|
+
status: ValidationStatus;
|
|
5
|
+
/** Row order the balance column implies, when it could be determined. */
|
|
6
|
+
order?: 'ascending' | 'descending';
|
|
7
|
+
/** Number of consecutive pairs checked. */
|
|
8
|
+
checked: number;
|
|
9
|
+
matched: number;
|
|
10
|
+
issues: string[];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Verify parsed amounts against the statement's running balance.
|
|
14
|
+
*
|
|
15
|
+
* Almost every Indian statement carries a closing-balance column, which makes
|
|
16
|
+
* the parse self-checkable: each transaction must equal the change in balance
|
|
17
|
+
* it caused. This is the difference between "the numbers look plausible" and
|
|
18
|
+
* "the numbers are provably right", and it catches precisely the failure modes
|
|
19
|
+
* of geometric PDF extraction — inverted debit/credit signs, dropped rows, and
|
|
20
|
+
* columns read one position across.
|
|
21
|
+
*
|
|
22
|
+
* Statements come in both date orders, so both interpretations are scored and
|
|
23
|
+
* the better one wins:
|
|
24
|
+
* ascending (oldest first): amount[i] === balance[i] - balance[i-1]
|
|
25
|
+
* descending (newest first): amount[i] === balance[i] - balance[i+1]
|
|
26
|
+
*/
|
|
27
|
+
export declare function validateBalances(transactions: StatementTransaction[]): Validation;
|