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.
Files changed (70) hide show
  1. package/.env.example +23 -0
  2. package/LICENSE +21 -0
  3. package/README.md +280 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +295 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/env-file.d.ts +38 -0
  8. package/dist/env-file.js +40 -0
  9. package/dist/env-file.js.map +1 -0
  10. package/dist/extract/csv.d.ts +13 -0
  11. package/dist/extract/csv.js +60 -0
  12. package/dist/extract/csv.js.map +1 -0
  13. package/dist/extract/html-table.d.ts +3 -0
  14. package/dist/extract/html-table.js +77 -0
  15. package/dist/extract/html-table.js.map +1 -0
  16. package/dist/extract/index.d.ts +21 -0
  17. package/dist/extract/index.js +44 -0
  18. package/dist/extract/index.js.map +1 -0
  19. package/dist/extract/pdf.d.ts +47 -0
  20. package/dist/extract/pdf.js +439 -0
  21. package/dist/extract/pdf.js.map +1 -0
  22. package/dist/extract/sniff.d.ts +12 -0
  23. package/dist/extract/sniff.js +57 -0
  24. package/dist/extract/sniff.js.map +1 -0
  25. package/dist/extract/spreadsheetml.d.ts +3 -0
  26. package/dist/extract/spreadsheetml.js +93 -0
  27. package/dist/extract/spreadsheetml.js.map +1 -0
  28. package/dist/extract/types.d.ts +20 -0
  29. package/dist/extract/types.js +2 -0
  30. package/dist/extract/types.js.map +1 -0
  31. package/dist/extract/xlsx.d.ts +2 -0
  32. package/dist/extract/xlsx.js +64 -0
  33. package/dist/extract/xlsx.js.map +1 -0
  34. package/dist/interpret/header.d.ts +24 -0
  35. package/dist/interpret/header.js +57 -0
  36. package/dist/interpret/header.js.map +1 -0
  37. package/dist/interpret/roundtrip.d.ts +29 -0
  38. package/dist/interpret/roundtrip.js +89 -0
  39. package/dist/interpret/roundtrip.js.map +1 -0
  40. package/dist/interpret/rows.d.ts +54 -0
  41. package/dist/interpret/rows.js +151 -0
  42. package/dist/interpret/rows.js.map +1 -0
  43. package/dist/interpret/synonyms.d.ts +27 -0
  44. package/dist/interpret/synonyms.js +55 -0
  45. package/dist/interpret/synonyms.js.map +1 -0
  46. package/dist/interpret/validate.d.ts +27 -0
  47. package/dist/interpret/validate.js +93 -0
  48. package/dist/interpret/validate.js.map +1 -0
  49. package/dist/interpret/values.d.ts +22 -0
  50. package/dist/interpret/values.js +131 -0
  51. package/dist/interpret/values.js.map +1 -0
  52. package/dist/merchants-file.d.ts +14 -0
  53. package/dist/merchants-file.js +40 -0
  54. package/dist/merchants-file.js.map +1 -0
  55. package/dist/narration/merchants.d.ts +46 -0
  56. package/dist/narration/merchants.js +161 -0
  57. package/dist/narration/merchants.js.map +1 -0
  58. package/dist/narration/parse.d.ts +14 -0
  59. package/dist/narration/parse.js +659 -0
  60. package/dist/narration/parse.js.map +1 -0
  61. package/dist/narration/types.d.ts +25 -0
  62. package/dist/narration/types.js +2 -0
  63. package/dist/narration/types.js.map +1 -0
  64. package/dist/out/csv.d.ts +21 -0
  65. package/dist/out/csv.js +40 -0
  66. package/dist/out/csv.js.map +1 -0
  67. package/dist/out/push.d.ts +74 -0
  68. package/dist/out/push.js +103 -0
  69. package/dist/out/push.js.map +1 -0
  70. 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,2 @@
1
+ import type { Table } from './types.js';
2
+ export declare function extractXlsx(path: string): Promise<Table>;
@@ -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;