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,93 @@
|
|
|
1
|
+
/** Half a paisa — comfortably inside any rounding a bank applies. */
|
|
2
|
+
const EPSILON = 0.005;
|
|
3
|
+
/**
|
|
4
|
+
* Verify parsed amounts against the statement's running balance.
|
|
5
|
+
*
|
|
6
|
+
* Almost every Indian statement carries a closing-balance column, which makes
|
|
7
|
+
* the parse self-checkable: each transaction must equal the change in balance
|
|
8
|
+
* it caused. This is the difference between "the numbers look plausible" and
|
|
9
|
+
* "the numbers are provably right", and it catches precisely the failure modes
|
|
10
|
+
* of geometric PDF extraction — inverted debit/credit signs, dropped rows, and
|
|
11
|
+
* columns read one position across.
|
|
12
|
+
*
|
|
13
|
+
* Statements come in both date orders, so both interpretations are scored and
|
|
14
|
+
* the better one wins:
|
|
15
|
+
* ascending (oldest first): amount[i] === balance[i] - balance[i-1]
|
|
16
|
+
* descending (newest first): amount[i] === balance[i] - balance[i+1]
|
|
17
|
+
*/
|
|
18
|
+
export function validateBalances(transactions) {
|
|
19
|
+
const withBalance = transactions.filter(transaction => transaction.balance !== undefined);
|
|
20
|
+
if (withBalance.length < 2) {
|
|
21
|
+
return {
|
|
22
|
+
status: 'skipped',
|
|
23
|
+
checked: 0,
|
|
24
|
+
matched: 0,
|
|
25
|
+
issues: [
|
|
26
|
+
withBalance.length === 0
|
|
27
|
+
? 'No balance column found — amounts could not be cross-checked.'
|
|
28
|
+
: 'Only one row carried a balance — not enough to cross-check.',
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const ascending = [];
|
|
33
|
+
const descending = [];
|
|
34
|
+
let ascendingMatches = 0;
|
|
35
|
+
let descendingMatches = 0;
|
|
36
|
+
for (let index = 1; index < withBalance.length; index += 1) {
|
|
37
|
+
const current = withBalance[index];
|
|
38
|
+
const previous = withBalance[index - 1];
|
|
39
|
+
if (!current || !previous) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// Ascending: this row's balance moved by this row's amount.
|
|
43
|
+
const ascDelta = (current.balance ?? 0) - (previous.balance ?? 0);
|
|
44
|
+
if (Math.abs(ascDelta - current.amount) < EPSILON) {
|
|
45
|
+
ascendingMatches += 1;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
ascending.push(describe(current, ascDelta));
|
|
49
|
+
}
|
|
50
|
+
// Descending: the *previous* row is the later transaction, so its amount
|
|
51
|
+
// explains the gap between the two balances.
|
|
52
|
+
const descDelta = (previous.balance ?? 0) - (current.balance ?? 0);
|
|
53
|
+
if (Math.abs(descDelta - previous.amount) < EPSILON) {
|
|
54
|
+
descendingMatches += 1;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
descending.push(describe(previous, descDelta));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const checked = withBalance.length - 1;
|
|
61
|
+
const isDescending = descendingMatches > ascendingMatches;
|
|
62
|
+
const matched = isDescending ? descendingMatches : ascendingMatches;
|
|
63
|
+
const failures = isDescending ? descending : ascending;
|
|
64
|
+
if (matched === checked) {
|
|
65
|
+
return {
|
|
66
|
+
status: 'passed',
|
|
67
|
+
order: isDescending ? 'descending' : 'ascending',
|
|
68
|
+
checked,
|
|
69
|
+
matched,
|
|
70
|
+
issues: [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
status: 'failed',
|
|
75
|
+
order: isDescending ? 'descending' : 'ascending',
|
|
76
|
+
checked,
|
|
77
|
+
matched,
|
|
78
|
+
issues: [
|
|
79
|
+
`${checked - matched} of ${checked} rows do not agree with the balance column.`,
|
|
80
|
+
// A handful of examples is enough to diagnose; the full list is noise.
|
|
81
|
+
...failures.slice(0, 5),
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function describe(transaction, observedDelta) {
|
|
86
|
+
const expected = observedDelta.toFixed(2);
|
|
87
|
+
const actual = transaction.amount.toFixed(2);
|
|
88
|
+
return `${transaction.date} "${truncate(transaction.raw)}": balance moved by ${expected} but the parsed amount is ${actual}`;
|
|
89
|
+
}
|
|
90
|
+
function truncate(value, limit = 60) {
|
|
91
|
+
return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/interpret/validate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Date component order, for the genuinely ambiguous cases like `01/02/2024`. */
|
|
2
|
+
export type DateOrder = 'dmy' | 'mdy' | 'ymd';
|
|
3
|
+
/**
|
|
4
|
+
* Parse a statement date into `YYYY-MM-DD`.
|
|
5
|
+
*
|
|
6
|
+
* Handles `01/04/2024`, `01-04-24`, `01.04.2024`, `01-Apr-2024`, `1 Apr 24`,
|
|
7
|
+
* `2024-04-01`, and any of those with a trailing time component.
|
|
8
|
+
*
|
|
9
|
+
* `order` only applies to all-numeric dates, where `01/02/2024` is genuinely
|
|
10
|
+
* ambiguous. It defaults to day-first, which is the Indian convention.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseStatementDate(raw: string, order?: DateOrder): string | null;
|
|
13
|
+
/**
|
|
14
|
+
* Parse an Indian statement amount into rupees.
|
|
15
|
+
*
|
|
16
|
+
* Handles lakh/crore grouping (`1,23,456.78`), currency prefixes (`₹`, `Rs.`,
|
|
17
|
+
* `INR`), parenthesised negatives, Unicode minus, and `Dr`/`Cr` suffixes.
|
|
18
|
+
*
|
|
19
|
+
* Returns null for blanks and for cells with no digits (`-`, `NIL`), which is
|
|
20
|
+
* how an empty side of a Withdrawal/Deposit pair is signalled.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseAmount(raw: string): number | null;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const MONTHS = {
|
|
2
|
+
jan: 1,
|
|
3
|
+
feb: 2,
|
|
4
|
+
mar: 3,
|
|
5
|
+
apr: 4,
|
|
6
|
+
may: 5,
|
|
7
|
+
jun: 6,
|
|
8
|
+
jul: 7,
|
|
9
|
+
aug: 8,
|
|
10
|
+
sep: 9,
|
|
11
|
+
oct: 10,
|
|
12
|
+
nov: 11,
|
|
13
|
+
dec: 12,
|
|
14
|
+
};
|
|
15
|
+
function expandYear(value) {
|
|
16
|
+
return value < 100 ? 2000 + value : value;
|
|
17
|
+
}
|
|
18
|
+
function build(year, month, day) {
|
|
19
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
// Round-trip through Date to reject impossible days like 31 February.
|
|
23
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
24
|
+
if (date.getUTCFullYear() !== year ||
|
|
25
|
+
date.getUTCMonth() !== month - 1 ||
|
|
26
|
+
date.getUTCDate() !== day) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const mm = String(month).padStart(2, '0');
|
|
30
|
+
const dd = String(day).padStart(2, '0');
|
|
31
|
+
return `${year}-${mm}-${dd}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse a statement date into `YYYY-MM-DD`.
|
|
35
|
+
*
|
|
36
|
+
* Handles `01/04/2024`, `01-04-24`, `01.04.2024`, `01-Apr-2024`, `1 Apr 24`,
|
|
37
|
+
* `2024-04-01`, and any of those with a trailing time component.
|
|
38
|
+
*
|
|
39
|
+
* `order` only applies to all-numeric dates, where `01/02/2024` is genuinely
|
|
40
|
+
* ambiguous. It defaults to day-first, which is the Indian convention.
|
|
41
|
+
*/
|
|
42
|
+
export function parseStatementDate(raw, order = 'dmy') {
|
|
43
|
+
const cleaned = raw.trim();
|
|
44
|
+
if (!cleaned) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
// Unambiguous ISO form, regardless of `order`.
|
|
48
|
+
const iso = cleaned.match(/^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})/);
|
|
49
|
+
if (iso) {
|
|
50
|
+
return build(Number(iso[1]), Number(iso[2]), Number(iso[3]));
|
|
51
|
+
}
|
|
52
|
+
// Month-name form: `01-Apr-2024`, `1 April 24`.
|
|
53
|
+
const named = cleaned.match(/^(\d{1,2})[\s\-/.]+([a-z]{3,})[\s\-/.]+(\d{2,4})/i);
|
|
54
|
+
if (named) {
|
|
55
|
+
const month = MONTHS[(named[2] ?? '').slice(0, 3).toLowerCase()];
|
|
56
|
+
if (month) {
|
|
57
|
+
return build(expandYear(Number(named[3])), month, Number(named[1]));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// All-numeric. Extra parts (a time component) are ignored.
|
|
61
|
+
const parts = cleaned
|
|
62
|
+
.split(/[^\d]+/)
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.map(Number);
|
|
65
|
+
if (parts.length < 3) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const [first = 0, second = 0, third = 0] = parts;
|
|
69
|
+
switch (order) {
|
|
70
|
+
case 'ymd':
|
|
71
|
+
return build(expandYear(first), second, third);
|
|
72
|
+
case 'mdy':
|
|
73
|
+
return build(expandYear(third), first, second);
|
|
74
|
+
case 'dmy':
|
|
75
|
+
default:
|
|
76
|
+
return build(expandYear(third), second, first);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Parse an Indian statement amount into rupees.
|
|
81
|
+
*
|
|
82
|
+
* Handles lakh/crore grouping (`1,23,456.78`), currency prefixes (`₹`, `Rs.`,
|
|
83
|
+
* `INR`), parenthesised negatives, Unicode minus, and `Dr`/`Cr` suffixes.
|
|
84
|
+
*
|
|
85
|
+
* Returns null for blanks and for cells with no digits (`-`, `NIL`), which is
|
|
86
|
+
* how an empty side of a Withdrawal/Deposit pair is signalled.
|
|
87
|
+
*/
|
|
88
|
+
export function parseAmount(raw) {
|
|
89
|
+
let text = raw.trim();
|
|
90
|
+
if (!text) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
let negative = false;
|
|
94
|
+
// `Dr`/`Cr` marker, which can sit either side of the number.
|
|
95
|
+
const marker = text.match(/(^|\s)(dr|cr)\b\.?/i);
|
|
96
|
+
if (marker) {
|
|
97
|
+
negative = (marker[2] ?? '').toLowerCase() === 'dr';
|
|
98
|
+
text = text.replace(marker[0], ' ');
|
|
99
|
+
}
|
|
100
|
+
text = text.replace(/−/g, '-').trim();
|
|
101
|
+
if (text.startsWith('(') && text.endsWith(')')) {
|
|
102
|
+
negative = true;
|
|
103
|
+
text = text.slice(1, -1);
|
|
104
|
+
}
|
|
105
|
+
if (/^\s*-/.test(text)) {
|
|
106
|
+
negative = true;
|
|
107
|
+
}
|
|
108
|
+
// Strip currency markers before touching the number. This has to happen
|
|
109
|
+
// first: the full stop in `Rs.` would otherwise survive the digit filter
|
|
110
|
+
// below and be read as the decimal point, turning `Rs. 1,234.50` into 0.12.
|
|
111
|
+
text = text.replace(/\b(inr|rs)\b\.?/gi, ' ').replace(/[₹$]/g, ' ');
|
|
112
|
+
// Commas are always thousands/lakh separators in Indian statements; the
|
|
113
|
+
// decimal marker is always `.`.
|
|
114
|
+
let digits = text.replace(/[^0-9.]/g, '');
|
|
115
|
+
if (!/\d/.test(digits)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
// If any separators remain, the last one is the decimal point and the rest
|
|
119
|
+
// are grouping. Belt and braces for decorations we did not anticipate.
|
|
120
|
+
const separators = digits.split('.');
|
|
121
|
+
if (separators.length > 2) {
|
|
122
|
+
const decimals = separators.pop() ?? '';
|
|
123
|
+
digits = `${separators.join('')}.${decimals}`;
|
|
124
|
+
}
|
|
125
|
+
const value = Number.parseFloat(digits);
|
|
126
|
+
if (!Number.isFinite(value)) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return negative ? -value : value;
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=values.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"values.js","sourceRoot":"","sources":["../../src/interpret/values.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MerchantRule } from './narration/merchants.js';
|
|
2
|
+
/**
|
|
3
|
+
* Load user merchant rules from JSON:
|
|
4
|
+
*
|
|
5
|
+
* [
|
|
6
|
+
* { "pattern": "^mylocalkirana", "name": "Kirana Store" },
|
|
7
|
+
* { "pattern": "^acmecorp", "name": "Acme Payroll" }
|
|
8
|
+
* ]
|
|
9
|
+
*
|
|
10
|
+
* Patterns are matched against a lowercased, separator-stripped form of the
|
|
11
|
+
* VPA local-part or merchant name, so write them without spaces or
|
|
12
|
+
* punctuation. Loaded rules take precedence over the built-in map.
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadMerchantRules(path: string): Promise<MerchantRule[]>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
/**
|
|
3
|
+
* Load user merchant rules from JSON:
|
|
4
|
+
*
|
|
5
|
+
* [
|
|
6
|
+
* { "pattern": "^mylocalkirana", "name": "Kirana Store" },
|
|
7
|
+
* { "pattern": "^acmecorp", "name": "Acme Payroll" }
|
|
8
|
+
* ]
|
|
9
|
+
*
|
|
10
|
+
* Patterns are matched against a lowercased, separator-stripped form of the
|
|
11
|
+
* VPA local-part or merchant name, so write them without spaces or
|
|
12
|
+
* punctuation. Loaded rules take precedence over the built-in map.
|
|
13
|
+
*/
|
|
14
|
+
export async function loadMerchantRules(path) {
|
|
15
|
+
const contents = await readFile(path, 'utf8');
|
|
16
|
+
let parsed;
|
|
17
|
+
try {
|
|
18
|
+
parsed = JSON.parse(contents);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
throw new Error(`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
22
|
+
}
|
|
23
|
+
if (!Array.isArray(parsed)) {
|
|
24
|
+
throw new Error(`${path} must contain an array of { pattern, name } rules`);
|
|
25
|
+
}
|
|
26
|
+
return parsed.map((entry, index) => {
|
|
27
|
+
const { pattern, name } = entry ?? {};
|
|
28
|
+
if (typeof pattern !== 'string' || typeof name !== 'string' || !name) {
|
|
29
|
+
throw new Error(`${path}: rule ${index + 1} needs a string "pattern" and a non-empty string "name"`);
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
// Case-insensitive to match how the built-in rules are applied.
|
|
33
|
+
return { pattern: new RegExp(pattern, 'i'), name };
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new Error(`${path}: rule ${index + 1} has an invalid regular expression (${pattern}): ${error instanceof Error ? error.message : String(error)}`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=merchants-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merchants-file.js","sourceRoot":"","sources":["../src/merchants-file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAM5C;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,IAAY;IAClD,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAE9C,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAChC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,uBAAuB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CACvF,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,GAAG,IAAI,mDAAmD,CAAC,CAAC;IAC9E,CAAC;IAED,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAc,EAAE,KAAK,EAAE,EAAE;QAC1C,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,KAAK,IAAI,EAAE,CAAC;QAEtC,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;YACrE,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,UAAU,KAAK,GAAG,CAAC,yDAAyD,CACpF,CAAC;QACJ,CAAC;QAED,IAAI,CAAC;YACH,gEAAgE;YAChE,OAAO,EAAE,OAAO,EAAE,IAAI,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC;QACrD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,UAAU,KAAK,GAAG,CAAC,uCAAuC,OAAO,MACtE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CACvD,EAAE,CACH,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merchant identity map.
|
|
3
|
+
*
|
|
4
|
+
* Indian merchants appear under many spellings across banks and payment
|
|
5
|
+
* routes — `swiggy@ybl`, `swiggystores@ybl`, `SWIGGY BANGALORE`, `Swiggy Ltd`.
|
|
6
|
+
* Matching a pattern to one canonical name is what collapses them into a
|
|
7
|
+
* single payee in Actual.
|
|
8
|
+
*
|
|
9
|
+
* Patterns are matched against a lowercased, separator-stripped form of either
|
|
10
|
+
* the VPA local-part or the merchant name token, so `bharat-pe` and
|
|
11
|
+
* `bharatpe123` both hit /^bharatpe/.
|
|
12
|
+
*
|
|
13
|
+
* This list is the asset that accrues value over time. Users can extend it
|
|
14
|
+
* without editing source via a local override file (see `loadLocalMerchants`).
|
|
15
|
+
*/
|
|
16
|
+
export type MerchantRule = {
|
|
17
|
+
pattern: RegExp;
|
|
18
|
+
name: string;
|
|
19
|
+
};
|
|
20
|
+
export declare const MERCHANT_RULES: MerchantRule[];
|
|
21
|
+
/**
|
|
22
|
+
* Bank postings that are not payments to anyone: interest, tax, card autopay.
|
|
23
|
+
*
|
|
24
|
+
* These are matched against the *whole* narration rather than an extracted
|
|
25
|
+
* name token, because there is no name in them to extract — ICICI writes
|
|
26
|
+
* interest as `000123456789:Int.Pd:30-09-2025 to 30-12-2025`. Without this the
|
|
27
|
+
* payee would be the entire narration, which differs every quarter and so
|
|
28
|
+
* creates a new payee each time.
|
|
29
|
+
*
|
|
30
|
+
* Kept separate from `MERCHANT_RULES` so that merchant patterns, which are
|
|
31
|
+
* anchored to the start of a name, are never accidentally matched against the
|
|
32
|
+
* middle of a narration.
|
|
33
|
+
*/
|
|
34
|
+
export declare const POSTING_RULES: MerchantRule[];
|
|
35
|
+
/** Resolve a bank posting type from a full narration, or null. */
|
|
36
|
+
export declare function lookupPosting(raw: string): string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Collapse a VPA local-part or name token into the form the patterns expect:
|
|
39
|
+
* lowercase, separators and spaces removed. `Bharat-Pe 123` -> `bharatpe123`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function normalizeForLookup(value: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a canonical merchant name, or null when nothing matches.
|
|
44
|
+
* `extraRules` come from the user's local override file and win over built-ins.
|
|
45
|
+
*/
|
|
46
|
+
export declare function lookupMerchant(value: string, extraRules?: MerchantRule[]): string | null;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merchant identity map.
|
|
3
|
+
*
|
|
4
|
+
* Indian merchants appear under many spellings across banks and payment
|
|
5
|
+
* routes — `swiggy@ybl`, `swiggystores@ybl`, `SWIGGY BANGALORE`, `Swiggy Ltd`.
|
|
6
|
+
* Matching a pattern to one canonical name is what collapses them into a
|
|
7
|
+
* single payee in Actual.
|
|
8
|
+
*
|
|
9
|
+
* Patterns are matched against a lowercased, separator-stripped form of either
|
|
10
|
+
* the VPA local-part or the merchant name token, so `bharat-pe` and
|
|
11
|
+
* `bharatpe123` both hit /^bharatpe/.
|
|
12
|
+
*
|
|
13
|
+
* This list is the asset that accrues value over time. Users can extend it
|
|
14
|
+
* without editing source via a local override file (see `loadLocalMerchants`).
|
|
15
|
+
*/
|
|
16
|
+
export const MERCHANT_RULES = [
|
|
17
|
+
// Food delivery & dining
|
|
18
|
+
{ pattern: /^swiggy/, name: 'Swiggy' },
|
|
19
|
+
{ pattern: /^zomato/, name: 'Zomato' },
|
|
20
|
+
{ pattern: /^(dominos|jubilant.*pizza)/, name: "Domino's Pizza" },
|
|
21
|
+
{ pattern: /^(kfc|mcdonalds|mcd)/, name: 'Fast Food' },
|
|
22
|
+
{ pattern: /^(starbucks|tatastarbucks)/, name: 'Starbucks' },
|
|
23
|
+
{ pattern: /^(blinkit|grofers)/, name: 'Blinkit' },
|
|
24
|
+
{ pattern: /^(zepto|geddit)/, name: 'Zepto' },
|
|
25
|
+
{ pattern: /^(bigbasket|innovativeretail)/, name: 'BigBasket' },
|
|
26
|
+
{ pattern: /^dunzo/, name: 'Dunzo' },
|
|
27
|
+
// Commerce
|
|
28
|
+
{ pattern: /^amazon/, name: 'Amazon' },
|
|
29
|
+
{ pattern: /^(flipkart|fkrt)/, name: 'Flipkart' },
|
|
30
|
+
{ pattern: /^myntra/, name: 'Myntra' },
|
|
31
|
+
{ pattern: /^(ajio|relianceretail)/, name: 'AJIO' },
|
|
32
|
+
{ pattern: /^(nykaa|fsnecommerce)/, name: 'Nykaa' },
|
|
33
|
+
{ pattern: /^meesho/, name: 'Meesho' },
|
|
34
|
+
{ pattern: /^(dmart|avenuesupermarts)/, name: 'DMart' },
|
|
35
|
+
{ pattern: /^(reliancesmart|reliancefresh)/, name: 'Reliance Smart' },
|
|
36
|
+
{ pattern: /^decathlon/, name: 'Decathlon' },
|
|
37
|
+
{ pattern: /^ikea/, name: 'IKEA' },
|
|
38
|
+
// Transport
|
|
39
|
+
{ pattern: /^(uber|uberindia)/, name: 'Uber' },
|
|
40
|
+
{ pattern: /^(ola|olacabs|anitechnologies)/, name: 'Ola' },
|
|
41
|
+
{ pattern: /^rapido/, name: 'Rapido' },
|
|
42
|
+
{ pattern: /^(irctc|indianrail)/, name: 'IRCTC' },
|
|
43
|
+
{ pattern: /^(redbus|pilani)/, name: 'RedBus' },
|
|
44
|
+
{ pattern: /^(indigo|interglobe)/, name: 'IndiGo' },
|
|
45
|
+
{ pattern: /^(airindia|vistara)/, name: 'Air India' },
|
|
46
|
+
{ pattern: /^(fastag|nhai|netc)/, name: 'FASTag' },
|
|
47
|
+
{
|
|
48
|
+
pattern: /^(iocl|indianoil|hpcl|bpcl|hindustanpetro|bharatpetro|shell|nayara)/,
|
|
49
|
+
name: 'Fuel',
|
|
50
|
+
},
|
|
51
|
+
// Utilities & telecom
|
|
52
|
+
{ pattern: /^(airtel|bhartiairtel)/, name: 'Airtel' },
|
|
53
|
+
{ pattern: /^(jio|reliancejio)/, name: 'Jio' },
|
|
54
|
+
{ pattern: /^(vi|vodafone|idea)$/, name: 'Vi' },
|
|
55
|
+
{ pattern: /^bsnl/, name: 'BSNL' },
|
|
56
|
+
{ pattern: /^(actfibernet|act)$/, name: 'ACT Fibernet' },
|
|
57
|
+
{
|
|
58
|
+
pattern: /^(tatapower|adanielectricity|bescom|kseb|mseb|tneb|bses)/,
|
|
59
|
+
name: 'Electricity',
|
|
60
|
+
},
|
|
61
|
+
{ pattern: /^(indane|hpgas|bharatgas)/, name: 'LPG Gas' },
|
|
62
|
+
// Entertainment & subscriptions
|
|
63
|
+
{ pattern: /^netflix/, name: 'Netflix' },
|
|
64
|
+
{ pattern: /^(hotstar|disney)/, name: 'Disney+ Hotstar' },
|
|
65
|
+
{ pattern: /^(primevideo|amazonprime)/, name: 'Amazon Prime' },
|
|
66
|
+
{ pattern: /^spotify/, name: 'Spotify' },
|
|
67
|
+
{ pattern: /^(youtube|googleplay|google)/, name: 'Google' },
|
|
68
|
+
{ pattern: /^apple/, name: 'Apple' },
|
|
69
|
+
{ pattern: /^(bookmyshow|bigtree)/, name: 'BookMyShow' },
|
|
70
|
+
{ pattern: /^(pvr|inox|cinepolis)/, name: 'Cinema' },
|
|
71
|
+
// Health & pharmacy
|
|
72
|
+
{ pattern: /^(pharmeasy|axelia)/, name: 'PharmEasy' },
|
|
73
|
+
{ pattern: /^(1mg|tata1mg)/, name: 'Tata 1mg' },
|
|
74
|
+
{ pattern: /^(apollo|apollopharmacy)/, name: 'Apollo Pharmacy' },
|
|
75
|
+
{ pattern: /^(practo|cult|curefit)/, name: 'Cult.fit' },
|
|
76
|
+
// Payment intermediaries.
|
|
77
|
+
// These are aggregators, not the real merchant — but a consistent payee is
|
|
78
|
+
// still far better than one per transaction, and a rule in Actual can split
|
|
79
|
+
// them further if wanted.
|
|
80
|
+
{ pattern: /^(paytm|one97)/, name: 'Paytm' },
|
|
81
|
+
{ pattern: /^(phonepe|phonepay)/, name: 'PhonePe' },
|
|
82
|
+
{ pattern: /^(gpay|googlepay|tez)/, name: 'Google Pay' },
|
|
83
|
+
{ pattern: /^bharatpe/, name: 'BharatPe' },
|
|
84
|
+
{ pattern: /^(razorpay|rzp)/, name: 'Razorpay' },
|
|
85
|
+
{ pattern: /^(billdesk|ccavenue|payu|cashfree)/, name: 'Payment Gateway' },
|
|
86
|
+
// State enterprises and institutions.
|
|
87
|
+
// Reached via a truncated handle rather than a spelled-out name
|
|
88
|
+
{ pattern: /^thekeralastate(f|financial)/, name: 'KSFE' },
|
|
89
|
+
// Listed companies, reached through NACH dividend payouts rather than through
|
|
90
|
+
// a payment. Anchored to the whole token: a bare /^tcs/ would also claim
|
|
91
|
+
// unrelated names starting with those letters.
|
|
92
|
+
{ pattern: /^(tcs|tataconsultancy(services)?)$/, name: 'TCS' },
|
|
93
|
+
// Investing
|
|
94
|
+
{ pattern: /^(zerodha|kite)/, name: 'Zerodha' },
|
|
95
|
+
{ pattern: /^(groww|nextbillion)/, name: 'Groww' },
|
|
96
|
+
{ pattern: /^(upstox|rksv)/, name: 'Upstox' },
|
|
97
|
+
{ pattern: /^(indmoney|finzoom)/, name: 'INDmoney' },
|
|
98
|
+
{ pattern: /^(kuvera|coin)$/, name: 'Kuvera' },
|
|
99
|
+
];
|
|
100
|
+
/**
|
|
101
|
+
* Bank postings that are not payments to anyone: interest, tax, card autopay.
|
|
102
|
+
*
|
|
103
|
+
* These are matched against the *whole* narration rather than an extracted
|
|
104
|
+
* name token, because there is no name in them to extract — ICICI writes
|
|
105
|
+
* interest as `000123456789:Int.Pd:30-09-2025 to 30-12-2025`. Without this the
|
|
106
|
+
* payee would be the entire narration, which differs every quarter and so
|
|
107
|
+
* creates a new payee each time.
|
|
108
|
+
*
|
|
109
|
+
* Kept separate from `MERCHANT_RULES` so that merchant patterns, which are
|
|
110
|
+
* anchored to the start of a name, are never accidentally matched against the
|
|
111
|
+
* middle of a narration.
|
|
112
|
+
*/
|
|
113
|
+
export const POSTING_RULES = [
|
|
114
|
+
{ pattern: /wtaxpd/, name: 'Withholding Tax' },
|
|
115
|
+
{ pattern: /intpd/, name: 'Interest Paid' },
|
|
116
|
+
// Savings-bank interest, written `SBINT:29-06-2026`.
|
|
117
|
+
{ pattern: /sbint/, name: 'Interest Paid' },
|
|
118
|
+
// Minimum average balance charge, billed monthly as `MABChgs-Mar2026`.
|
|
119
|
+
{ pattern: /mabchgs/, name: 'Minimum Balance Charge' },
|
|
120
|
+
{ pattern: /autodebitcc/, name: 'Credit Card Autopay' },
|
|
121
|
+
// Debit card annual fee, e.g. `DCARDFEE0000AUG26-JUL27+GST`.
|
|
122
|
+
{ pattern: /dcardfee/, name: 'Debit Card Fee' },
|
|
123
|
+
{ pattern: /(atmwdl|cashwdl|nwdcash)/, name: 'ATM Withdrawal' },
|
|
124
|
+
];
|
|
125
|
+
/** Resolve a bank posting type from a full narration, or null. */
|
|
126
|
+
export function lookupPosting(raw) {
|
|
127
|
+
const normalized = normalizeForLookup(raw);
|
|
128
|
+
if (!normalized) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
for (const rule of POSTING_RULES) {
|
|
132
|
+
if (rule.pattern.test(normalized)) {
|
|
133
|
+
return rule.name;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Collapse a VPA local-part or name token into the form the patterns expect:
|
|
140
|
+
* lowercase, separators and spaces removed. `Bharat-Pe 123` -> `bharatpe123`.
|
|
141
|
+
*/
|
|
142
|
+
export function normalizeForLookup(value) {
|
|
143
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Resolve a canonical merchant name, or null when nothing matches.
|
|
147
|
+
* `extraRules` come from the user's local override file and win over built-ins.
|
|
148
|
+
*/
|
|
149
|
+
export function lookupMerchant(value, extraRules = []) {
|
|
150
|
+
const normalized = normalizeForLookup(value);
|
|
151
|
+
if (!normalized) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
for (const rule of [...extraRules, ...MERCHANT_RULES]) {
|
|
155
|
+
if (rule.pattern.test(normalized)) {
|
|
156
|
+
return rule.name;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=merchants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merchants.js","sourceRoot":"","sources":["../../src/narration/merchants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAOH,MAAM,CAAC,MAAM,cAAc,GAAmB;IAC5C,yBAAyB;IACzB,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE;IACtC,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE;IACtC,EAAE,OAAO,EAAE,4BAA4B,EAAE,IAAI,EAAE,gBAAgB,EAAE;IACjE,EAAE,OAAO,EAAE,sBAAsB,EAAE,IAAI,EAAE,WAAW,EAAE;IACtD,EAAE,OAAO,EAAE,4BAA4B,EAAE,IAAI,EAAE,WAAW,EAAE;IAC5D,EAAE,OAAO,EAAE,oBAAoB,EAAE,IAAI,EAAE,SAAS,EAAE;IAClD,EAAE,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,OAAO,EAAE;IAC7C,EAAE,OAAO,EAAE,+BAA+B,EAAE,IAAI,EAAE,WAAW,EAAE;IAC/D,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE;IAEpC,WAAW;IACX,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE;IACtC,EAAE,OAAO,EAAE,kBAAkB,EAAE,IAAI,EAAE,UAAU,EAAE;IACjD,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE;IACtC,EAAE,OAAO,EAAE,wBAAwB,EAAE,IAAI,EAAE,MAAM,EAAE;IACnD,EAAE,OAAO,EAAE,uBAAuB,EAAE,IAAI,EAAE,OAAO,EAAE;IACnD,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE;IACtC,EAAE,OAAO,EAAE,2BAA2B,EAAE,IAAI,EAAE,OAAO,EAAE;IACvD,EAAE,OAAO,EAAE,gCAAgC,EAAE,IAAI,EAAE,gBAAgB,EAAE;IACrE,EAAE,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,WAAW,EAAE;IAC5C,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE;IAElC,YAAY;IACZ,EAAE,OAAO,EAAE,mBAAmB,EAAE,IAAI,EAAE,MAAM,EAAE;IAC9C,EAAE,OAAO,EAAE,gCAAgC,EAAE,IAAI,EAAE,KAAK,EAAE;IAC1D,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE;IACtC,EAAE,OAAO,EAAE,qBAAqB,EAAE,IAAI,EAAE,OAAO,EAAE;IACjD,EAAE,OAAO,EAAE,kBAAkB,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC/C,EAAE,OAAO,EAAE,sBAAsB,EAAE,IAAI,EAAE,QAAQ,EAAE;IACnD,EAAE,OAAO,EAAE,qBAAqB,EAAE,IAAI,EAAE,WAAW,EAAE;IACrD,EAAE,OAAO,EAAE,qBAAqB,EAAE,IAAI,EAAE,QAAQ,EAAE;IAClD;QACE,OAAO,EACL,qEAAqE;QACvE,IAAI,EAAE,MAAM;KACb;IAED,sBAAsB;IACtB,EAAE,OAAO,EAAE,wBAAwB,EAAE,IAAI,EAAE,QAAQ,EAAE;IACrD,EAAE,OAAO,EAAE,oBAAoB,EAAE,IAAI,EAAE,KAAK,EAAE;IAC9C,EAAE,OAAO,EAAE,sBAAsB,EAAE,IAAI,EAAE,IAAI,EAAE;IAC/C,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE;IAClC,EAAE,OAAO,EAAE,qBAAqB,EAAE,IAAI,EAAE,cAAc,EAAE;IACxD;QACE,OAAO,EAAE,0DAA0D;QACnE,IAAI,EAAE,aAAa;KACpB;IACD,EAAE,OAAO,EAAE,2BAA2B,EAAE,IAAI,EAAE,SAAS,EAAE;IAEzD,gCAAgC;IAChC,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE;IACxC,EAAE,OAAO,EAAE,mBAAmB,EAAE,IAAI,EAAE,iBAAiB,EAAE;IACzD,EAAE,OAAO,EAAE,2BAA2B,EAAE,IAAI,EAAE,cAAc,EAAE;IAC9D,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE;IACxC,EAAE,OAAO,EAAE,8BAA8B,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC3D,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE;IACpC,EAAE,OAAO,EAAE,uBAAuB,EAAE,IAAI,EAAE,YAAY,EAAE;IACxD,EAAE,OAAO,EAAE,uBAAuB,EAAE,IAAI,EAAE,QAAQ,EAAE;IAEpD,oBAAoB;IACpB,EAAE,OAAO,EAAE,qBAAqB,EAAE,IAAI,EAAE,WAAW,EAAE;IACrD,EAAE,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,UAAU,EAAE;IAC/C,EAAE,OAAO,EAAE,0BAA0B,EAAE,IAAI,EAAE,iBAAiB,EAAE;IAChE,EAAE,OAAO,EAAE,wBAAwB,EAAE,IAAI,EAAE,UAAU,EAAE;IAEvD,0BAA0B;IAC1B,2EAA2E;IAC3E,4EAA4E;IAC5E,0BAA0B;IAC1B,EAAE,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,OAAO,EAAE;IAC5C,EAAE,OAAO,EAAE,qBAAqB,EAAE,IAAI,EAAE,SAAS,EAAE;IACnD,EAAE,OAAO,EAAE,uBAAuB,EAAE,IAAI,EAAE,YAAY,EAAE;IACxD,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,UAAU,EAAE;IAC1C,EAAE,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,UAAU,EAAE;IAChD,EAAE,OAAO,EAAE,oCAAoC,EAAE,IAAI,EAAE,iBAAiB,EAAE;IAE1E,sCAAsC;IACtC,gEAAgE;IAChE,EAAE,OAAO,EAAE,8BAA8B,EAAE,IAAI,EAAE,MAAM,EAAE;IAEzD,8EAA8E;IAC9E,yEAAyE;IACzE,+CAA+C;IAC/C,EAAE,OAAO,EAAE,oCAAoC,EAAE,IAAI,EAAE,KAAK,EAAE;IAE9D,YAAY;IACZ,EAAE,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,SAAS,EAAE;IAC/C,EAAE,OAAO,EAAE,sBAAsB,EAAE,IAAI,EAAE,OAAO,EAAE;IAClD,EAAE,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,QAAQ,EAAE;IAC7C,EAAE,OAAO,EAAE,qBAAqB,EAAE,IAAI,EAAE,UAAU,EAAE;IACpD,EAAE,OAAO,EAAE,iBAAiB,EAAE,IAAI,EAAE,QAAQ,EAAE;CAC/C,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,aAAa,GAAmB;IAC3C,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,iBAAiB,EAAE;IAC9C,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE;IAC3C,qDAAqD;IACrD,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE;IAC3C,uEAAuE;IACvE,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,wBAAwB,EAAE;IACtD,EAAE,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,qBAAqB,EAAE;IACvD,6DAA6D;IAC7D,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,gBAAgB,EAAE;IAC/C,EAAE,OAAO,EAAE,0BAA0B,EAAE,IAAI,EAAE,gBAAgB,EAAE;CAChE,CAAC;AAEF,kEAAkE;AAClE,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,UAAU,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;IAC3C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;QACjC,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAClC,OAAO,IAAI,CAAC,IAAI,CAAC;QACnB,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC9C,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;AACvD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,KAAa,EACb,aAA6B,EAAE;IAE/B,MAAM,UAAU,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IAC7C,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,UAAU,EAAE,GAAG,cAAc,CAAC,EAAE,CAAC;QACtD,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAClC,OAAO,IAAI,CAAC,IAAI,CAAC;QACnB,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MerchantRule } from './merchants.js';
|
|
2
|
+
import type { ParsedNarration } from './types.js';
|
|
3
|
+
export type ParseNarrationOptions = {
|
|
4
|
+
/** User-supplied merchant rules; these take precedence over built-ins. */
|
|
5
|
+
merchantRules?: MerchantRule[];
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Parse an Indian bank narration into a usable payee plus the structured
|
|
9
|
+
* fields hiding inside it.
|
|
10
|
+
*
|
|
11
|
+
* `merchant` is never empty — the fallback chain ends at the raw narration, so
|
|
12
|
+
* the worst outcome equals what Actual shows today.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseNarration(raw: string, options?: ParseNarrationOptions): ParsedNarration;
|