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,659 @@
1
+ import { lookupMerchant, lookupPosting } from './merchants.js';
2
+ /**
3
+ * A VPA as it appears as a standalone token, e.g. `swiggy@ybl`,
4
+ * `paytm-9876@paytm`, `9876543210@ybl`.
5
+ *
6
+ * Deliberately anchored and applied per token rather than searched across the
7
+ * whole narration: `-` is legal inside a VPA local-part, so an unanchored
8
+ * search over `UPI-SWIGGY-swiggy@ybl-...` would happily match
9
+ * `SWIGGY-swiggy@ybl` and pollute the merchant name.
10
+ */
11
+ const VPA_TOKEN = /^[a-z0-9][a-z0-9._-]*@[a-z][a-z0-9]+$/i;
12
+ /** UPI UTR / RRN. Exactly 12 digits — see `extractRef` for why not 9-16. */
13
+ const UTR_TOKEN = /^\d{12}$/;
14
+ /** IFSC, e.g. `HDFC0001234`. Routing detail, never a payee. */
15
+ const IFSC_TOKEN = /^[A-Z]{4}0[A-Z0-9]{6}$/i;
16
+ /**
17
+ * An account number written in VPA shape, e.g. `0000000000000000@BANK000`.
18
+ *
19
+ * Federal uses this for a UPI transfer addressed to a bare account rather than
20
+ * to a handle. It matches `VPA_TOKEN`, but it is not a VPA and must not be
21
+ * treated as one: there is no readable local-part, so the VPA path ends up
22
+ * emitting the whole string as the payee.
23
+ *
24
+ * The discriminator is digits in the part after `@`. A PSP handle (`ybl`,
25
+ * `oksbi`, `yesbankltd`) never carries them, so phone-number VPAs such as
26
+ * `9876543210@ybl`, whose local-part is also all digits, are unaffected.
27
+ */
28
+ const ACCOUNT_AT_BANK = /^(\d{6,})@([a-z]{2,})\d+$/i;
29
+ /**
30
+ * A NACH/ACH mandate reference: a four-letter bank code followed by a long
31
+ * digit run, e.g. `ICIC0000000000000001`.
32
+ *
33
+ * Stable for the life of the mandate, unlike the per-collection sequence
34
+ * number printed beside it, which makes it the only usable payee identity for
35
+ * recurring debits such as loan EMIs and mutual-fund SIPs.
36
+ */
37
+ const MANDATE_TOKEN = /^[a-z]{4}\d{8,}$/i;
38
+ /** Masked card/account numbers, e.g. `1234XXXX5678`, `XXXXXXXX1234`. */
39
+ const MASKED_TOKEN = /^[\dX*]+$/i;
40
+ /**
41
+ * Route markers, directions and filler. These carry no payee information but
42
+ * survive tokenisation, so they must be excluded from merchant candidates.
43
+ */
44
+ const NOISE_TOKENS = new Set([
45
+ 'dr',
46
+ 'cr',
47
+ 'db',
48
+ 'p2a',
49
+ 'p2m',
50
+ 'p2p',
51
+ 'payment',
52
+ 'pymt',
53
+ 'pay',
54
+ 'na',
55
+ 'nil',
56
+ 'null',
57
+ 'upi',
58
+ 'neft',
59
+ 'imps',
60
+ 'rtgs',
61
+ 'pos',
62
+ 'ach',
63
+ 'atw',
64
+ 'nwd',
65
+ 'atm',
66
+ // ICICI's code for a card autopay debit, e.g. `ATD/Auto Debit CC0xx0000`.
67
+ 'atd',
68
+ // Federal's channel codes: `UPIOUT/...`, `MB FTO/...`, `FT IMPS/IFI/...`.
69
+ 'upiout',
70
+ 'upiin',
71
+ 'fto',
72
+ 'fti',
73
+ 'ft',
74
+ 'ifi',
75
+ 'impstxn',
76
+ 'nre',
77
+ 'nro',
78
+ 'mmt',
79
+ 'ecs',
80
+ 'nach',
81
+ 'si',
82
+ 'ecom',
83
+ 'vps',
84
+ 'inft',
85
+ 'txn',
86
+ 'trf',
87
+ 'transfer',
88
+ 'ref',
89
+ 'refno',
90
+ 'sent',
91
+ 'received',
92
+ 'paid',
93
+ 'collect',
94
+ 'inb',
95
+ 'ib',
96
+ 'mob',
97
+ 'mb',
98
+ 'self',
99
+ 'other',
100
+ 'others',
101
+ 'misc',
102
+ 'chq',
103
+ 'cheque',
104
+ 'cash',
105
+ 'wdl',
106
+ 'to',
107
+ 'from',
108
+ 'by',
109
+ 'via',
110
+ ]);
111
+ /**
112
+ * Bank short codes that appear as their own token in UPI narrations to
113
+ * identify the counterparty's bank. Not payees.
114
+ */
115
+ const BANK_CODES = new Set([
116
+ 'hdfc',
117
+ 'icic',
118
+ 'ici',
119
+ 'sbin',
120
+ 'sbi',
121
+ 'utib',
122
+ 'axis',
123
+ 'fdrl',
124
+ 'yesb',
125
+ 'yes',
126
+ 'ybl',
127
+ 'kkbk',
128
+ 'kotak',
129
+ 'punb',
130
+ 'pnb',
131
+ 'barb',
132
+ 'bob',
133
+ 'cnrb',
134
+ 'canara',
135
+ 'idib',
136
+ 'ioba',
137
+ 'ubin',
138
+ 'mahb',
139
+ 'indb',
140
+ 'idfb',
141
+ 'ratn',
142
+ 'aubl',
143
+ 'bkid',
144
+ 'cbin',
145
+ 'dbss',
146
+ 'hsbc',
147
+ 'scbl',
148
+ 'citi',
149
+ 'okhdfcbank',
150
+ 'okicici',
151
+ 'oksbi',
152
+ 'okaxis',
153
+ 'apl',
154
+ 'ptyes',
155
+ 'ptsbi',
156
+ 'ptaxis',
157
+ 'pytm',
158
+ 'ibl',
159
+ 'axl',
160
+ 'jupiteraxis',
161
+ 'waaxis',
162
+ 'airp',
163
+ 'jio',
164
+ ]);
165
+ /**
166
+ * Noise that a fixed word list cannot cover, because banks truncate fields to
167
+ * a fixed width and spell bank names out in full.
168
+ *
169
+ * ICICI cuts every narration field to about ten characters, so `Payment from`
170
+ * arrives as `Payment fr` and `Pay request` as `Pay reques` — prefixes, not
171
+ * whole words. It also names the counterparty's bank in words (`State Bank`,
172
+ * `FEDERAL BA`, `IDFC FIRST`, `HDFC BANK LTD`), and those would otherwise
173
+ * compete with the actual payee for the longest-name-wins rule.
174
+ */
175
+ const NOISE_PATTERNS = [
176
+ // Truncated descriptors.
177
+ /^payment/,
178
+ /^payreq/,
179
+ /^paymentfr/,
180
+ /^debittrxn$/,
181
+ /^credittrxn$/,
182
+ /^autodebit/,
183
+ /^(debit|credit|trxn|txnno)$/,
184
+ /^collectfr/,
185
+ /^fundtransf/,
186
+ // Bank names, including ICICI's truncated forms. Anchored deliberately: a
187
+ // bare /^hdfc/ would also reject `HDFC MUTUAL FUND SIP`, which is a real
188
+ // payee. Only a token that is *entirely* a bank name counts as noise.
189
+ /^[a-z]{3,}bank(ltd|limited)?$/,
190
+ /^federalba(nk)?$/,
191
+ /^idfcfirst$/,
192
+ /^axisbank$/,
193
+ /^yesbank(ltd)?$/,
194
+ /^unionofindia$/,
195
+ /^punjabnational$/,
196
+ ];
197
+ /**
198
+ * Words to leave in upper case when building a display name. Bank codes are
199
+ * included because they legitimately appear in payee names for mutual funds
200
+ * and standing instructions (`HDFC MUTUAL FUND SIP`).
201
+ */
202
+ const KNOWN_ACRONYMS = new Set([
203
+ ...BANK_CODES,
204
+ 'ikea',
205
+ 'bsnl',
206
+ 'irctc',
207
+ 'nhai',
208
+ 'sip',
209
+ 'emi',
210
+ 'gst',
211
+ 'tds',
212
+ 'nach',
213
+ 'fastag',
214
+ ]);
215
+ const KIND_MARKERS = [
216
+ // `UPIOUT`/`UPIIN` are Federal's directional forms of the UPI marker.
217
+ [/^upi(out|in)?$/i, 'upi'],
218
+ [/^neft$/i, 'neft'],
219
+ [/^(imps|mmt|inft)$/i, 'imps'],
220
+ [/^rtgs$/i, 'rtgs'],
221
+ [/^(pos|ecom|vps|vin)$/i, 'pos'],
222
+ [/^(ach|nach|ecs|si)$/i, 'ach'],
223
+ [/^(atw|nwd|atm|awb|cwdr)$/i, 'atm'],
224
+ ];
225
+ /**
226
+ * Banks delimit narrations with either `/` (HDFC, SBI) or `-` (ICICI), and
227
+ * some use neither. Pick whichever split yields more fields.
228
+ */
229
+ function tokenize(raw) {
230
+ const split = (by) => raw
231
+ .split(by)
232
+ .map(token => token.trim())
233
+ .filter(Boolean);
234
+ const bySlash = split('/');
235
+ const byDash = split('-');
236
+ const delimited = bySlash.length >= byDash.length ? bySlash : byDash;
237
+ if (delimited.length > 1) {
238
+ return { tokens: delimited, phraseMode: false };
239
+ }
240
+ return {
241
+ tokens: raw.split(/\s+/).filter(Boolean),
242
+ phraseMode: true,
243
+ };
244
+ }
245
+ /**
246
+ * Remove VPA words, leaving the rest of each field intact. A VPA is resolved on
247
+ * its own path, so it must not also compete as name text: a field holding both
248
+ * would otherwise be picked whole and title-cased with the VPA still in it.
249
+ */
250
+ function withoutVpas(tokens) {
251
+ return tokens
252
+ .map(token => subTokens([token])
253
+ .filter(word => !VPA_TOKEN.test(word))
254
+ .join(' '))
255
+ .filter(Boolean);
256
+ }
257
+ /** Tokens split down to whitespace, for shape matching. */
258
+ function subTokens(tokens) {
259
+ return tokens.flatMap(token => token.split(/\s+/)).filter(Boolean);
260
+ }
261
+ function detectKind(tokens) {
262
+ // Scan the words of the first two tokens: banks prefix the route with a
263
+ // channel marker, sometimes in the same field — `MMT/IMPS/...` but also
264
+ // Federal's `FT IMPS/IFI/...`, where the marker is the second word.
265
+ for (const token of subTokens(tokens.slice(0, 2))) {
266
+ for (const [marker, kind] of KIND_MARKERS) {
267
+ if (marker.test(token)) {
268
+ return kind;
269
+ }
270
+ }
271
+ }
272
+ return 'other';
273
+ }
274
+ function extractVpa(tokens) {
275
+ return subTokens(tokens).find(token => VPA_TOKEN.test(token) && !ACCOUNT_AT_BANK.test(token));
276
+ }
277
+ /**
278
+ * `0000000000000000@BANK000` -> `BANK 0000000000000000`.
279
+ *
280
+ * Nothing names the counterparty here, so the account is the identity. It is
281
+ * stable per counterparty, unlike the raw narration, which carries the UTR.
282
+ */
283
+ function nameFromAccountReference(tokens) {
284
+ for (const token of subTokens(tokens)) {
285
+ const [, account, bank] = ACCOUNT_AT_BANK.exec(token) ?? [];
286
+ if (account && bank) {
287
+ return `${bank.toUpperCase()} ${account}`;
288
+ }
289
+ }
290
+ return null;
291
+ }
292
+ /**
293
+ * Extract the bank reference, but only when it is safe to use as Actual's
294
+ * `imported_id`.
295
+ *
296
+ * Restricted to exactly 12 digits (the UPI UTR/RRN format) rather than a
297
+ * looser 9-16 range, because account numbers and card numbers also appear in
298
+ * narrations at those lengths and are *not* unique per transaction. A repeated
299
+ * `imported_id` would make Actual treat distinct transactions as the same one
300
+ * and silently drop them — far worse than having no id, where Actual's
301
+ * date+amount fuzzy matching takes over.
302
+ */
303
+ function extractRef(tokens) {
304
+ return subTokens(tokens).find(token => UTR_TOKEN.test(token));
305
+ }
306
+ function letterCount(value) {
307
+ return (value.match(/[a-z]/gi) ?? []).length;
308
+ }
309
+ /** Could this token name a payee? */
310
+ /**
311
+ * Is this an opaque machine reference rather than a name?
312
+ *
313
+ * ICICI appends a long provider reference after the UTR, e.g.
314
+ * `SBI1aa2bb3cc4dd5ee6ff7aa8bb9cc0dd1ee` or `ICIC0000000000000001`. These carry
315
+ * ~20 letters, so a "longest run of letters wins" rule would happily pick one
316
+ * as the payee. Mixing letters and digits over this length is not something a
317
+ * merchant name does.
318
+ */
319
+ function isOpaqueReference(bare) {
320
+ // Eight characters rather than twelve: PDF extraction can break a long
321
+ // reference across lines, leaving a short tail such as `b9cc0dd2ee` that
322
+ // would otherwise qualify as a name.
323
+ if (bare.length < 8 || !/\d/.test(bare) || !/[a-z]/.test(bare)) {
324
+ return false;
325
+ }
326
+ // "Letters and digits" alone rejected merchant handles, which are a name with
327
+ // a numeric id appended. What separates the two is arrangement: a reference
328
+ // interleaves them (`axl1aa2bb3cc`), so its longest letter run is short.
329
+ if (longestLetterRun(bare) < 6) {
330
+ return true;
331
+ }
332
+ // Some provider references have long letter runs too, but scatter digits
333
+ // through them. A name carrying an id has one digit group, usually trailing.
334
+ return digitGroupCount(bare) > 1;
335
+ }
336
+ function digitGroupCount(value) {
337
+ return (value.match(/\d+/g) ?? []).length;
338
+ }
339
+ function longestLetterRun(value) {
340
+ return (value.match(/[a-z]+/gi) ?? []).reduce((longest, run) => Math.max(longest, run.length), 0);
341
+ }
342
+ /**
343
+ * The most words a payee name can plausibly have. Statement name fields are
344
+ * short, so a very wordy field is a leaked page footer or branch address, which
345
+ * being mostly letters would otherwise beat the real name.
346
+ */
347
+ const MAX_NAME_WORDS = 6;
348
+ function wordCount(value) {
349
+ return value.trim().split(/\s+/).filter(Boolean).length;
350
+ }
351
+ function isMerchantCandidate(token) {
352
+ const bare = token.toLowerCase().replace(/[^a-z0-9]/g, '');
353
+ if (!bare || NOISE_TOKENS.has(bare) || BANK_CODES.has(bare)) {
354
+ return false;
355
+ }
356
+ if (NOISE_PATTERNS.some(pattern => pattern.test(bare))) {
357
+ return false;
358
+ }
359
+ if (VPA_TOKEN.test(token) || IFSC_TOKEN.test(token)) {
360
+ return false;
361
+ }
362
+ if (MASKED_TOKEN.test(token) || isOpaqueReference(bare)) {
363
+ return false;
364
+ }
365
+ if (wordCount(token) > MAX_NAME_WORDS) {
366
+ return false;
367
+ }
368
+ // Needs a real word. Three letters rather than two, because date ranges in
369
+ // interest postings leave fragments like `2025to 30`, whose only letters are
370
+ // the `to`, and that would otherwise be chosen as the payee.
371
+ return letterCount(token) >= 3;
372
+ }
373
+ /**
374
+ * Pick the most payee-like fragment of the narration.
375
+ *
376
+ * In phrase mode consecutive surviving tokens are joined, so
377
+ * `POS 1234XXXX5678 SWIGGY BANGALORE` yields `SWIGGY BANGALORE` rather than
378
+ * the single longest word (which would be `BANGALORE`).
379
+ */
380
+ function pickCandidate(tokens, phraseMode) {
381
+ if (!phraseMode) {
382
+ let best = null;
383
+ for (const token of tokens) {
384
+ if (isMerchantCandidate(token) &&
385
+ letterCount(token) > letterCount(best ?? '')) {
386
+ best = token;
387
+ }
388
+ }
389
+ return best;
390
+ }
391
+ const phrases = [];
392
+ let current = [];
393
+ for (const token of tokens) {
394
+ if (isMerchantCandidate(token)) {
395
+ current.push(token);
396
+ }
397
+ else if (current.length) {
398
+ phrases.push(current.join(' '));
399
+ current = [];
400
+ }
401
+ }
402
+ if (current.length) {
403
+ phrases.push(current.join(' '));
404
+ }
405
+ let best = null;
406
+ for (const phrase of phrases) {
407
+ if (letterCount(phrase) > letterCount(best ?? '')) {
408
+ best = phrase;
409
+ }
410
+ }
411
+ return best;
412
+ }
413
+ function titleCase(value) {
414
+ return value
415
+ .split(/\s+/)
416
+ .filter(Boolean)
417
+ .map(word => {
418
+ // Keep known acronyms upper-case and title-case everything else. Word
419
+ // length is not a usable signal: `HDFC`, `SIP`, `JOHN` and `DOE` are
420
+ // all short and all-caps, and only some are acronyms. Brand acronyms
421
+ // outside this set (`KFC`) are resolved by the merchant map instead.
422
+ if (KNOWN_ACRONYMS.has(word.toLowerCase())) {
423
+ return word.toUpperCase();
424
+ }
425
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
426
+ })
427
+ .join(' ');
428
+ }
429
+ /**
430
+ * Payment-service suffixes that appear inside a VPA local-part rather than
431
+ * after the `@`: `something.rz` / `.rzp` is Razorpay, `.ibz` Ibibo. They
432
+ * identify the gateway, not the merchant.
433
+ */
434
+ const HANDLE_SUFFIX = /\.(rz|rzp|ibz|payu|cf|eko)$/i;
435
+ /**
436
+ * Clean a merchant handle into a name: `merchantname123456.rz` -> `Merchantname`.
437
+ *
438
+ * Federal writes UPI counterparties as a bare handle with no `@psp` part at
439
+ * all. The trailing digits identify the merchant's payment account, so keeping
440
+ * them gives a payee per account rather than per merchant.
441
+ */
442
+ function cleanHandle(token) {
443
+ const withoutSuffix = token.replace(HANDLE_SUFFIX, '');
444
+ return withoutSuffix
445
+ .replace(/[._-]+/g, ' ')
446
+ .split(/\s+/)
447
+ .filter(word => !/^\d+$/.test(word))
448
+ .map(word => word.replace(/\d+$/, ''))
449
+ .filter(Boolean)
450
+ .join(' ');
451
+ }
452
+ /** Does this token look like a bare merchant handle rather than plain words? */
453
+ function isHandle(token) {
454
+ return /\d/.test(token) || HANDLE_SUFFIX.test(token);
455
+ }
456
+ /**
457
+ * Turn a VPA into a readable name: `bharatpe90771@yesbankltd` -> `Bharatpe`,
458
+ * `john.doe@oksbi` -> `John Doe`. Returns null for phone-number VPAs like
459
+ * `9876543210@ybl`, where nothing readable remains.
460
+ */
461
+ function nameFromVpa(vpa) {
462
+ const local = vpa.split('@')[0] ?? '';
463
+ const words = local
464
+ .replace(/[._-]+/g, ' ')
465
+ .split(/\s+/)
466
+ // Drop pure-digit words, then trailing digits on the rest.
467
+ .filter(word => !/^\d+$/.test(word))
468
+ .map(word => word.replace(/\d+$/, ''))
469
+ .filter(Boolean);
470
+ if (!words.length) {
471
+ return null;
472
+ }
473
+ return titleCase(words.join(' '));
474
+ }
475
+ /**
476
+ * Name text sharing a field with the VPA, e.g. `firstname. last9@oksbi`, where
477
+ * the VPA's local-part holds only part of the name.
478
+ *
479
+ * Restricted to the field the VPA is in. Names in other fields are already
480
+ * handled as candidates, and pulling those in would merge payer with payee.
481
+ */
482
+ function nameBesideVpa(tokens, vpa) {
483
+ const field = tokens.find(token => subTokens([token]).includes(vpa));
484
+ if (!field) {
485
+ return null;
486
+ }
487
+ const words = subTokens([field]).filter(word => word !== vpa && isMerchantCandidate(word));
488
+ return words.length ? words.join(' ') : null;
489
+ }
490
+ /**
491
+ * The named entity inside a NACH description field, e.g. the `TCS` of
492
+ * `ACME3rdINTDiv01012026`.
493
+ *
494
+ * A NACH collection or payout writes the company, the purpose, a date and a
495
+ * sequence number into one unspaced field, so only the leading letter run is
496
+ * stable. Everything after it changes with each payout and would otherwise
497
+ * mint a new payee each time.
498
+ */
499
+ function nachEntity(tokens) {
500
+ for (const token of subTokens(tokens)) {
501
+ const words = token
502
+ .split(SQUASHED_WORD_BOUNDARY)
503
+ .filter(word => /^[a-z]{3,}$/i.test(word));
504
+ const index = words.findIndex((word) => {
505
+ const bare = word.toLowerCase();
506
+ return (!NOISE_TOKENS.has(bare) &&
507
+ !BANK_CODES.has(bare) &&
508
+ !NOISE_PATTERNS.some(pattern => pattern.test(bare)));
509
+ });
510
+ const name = index >= 0 ? words[index] : undefined;
511
+ if (name) {
512
+ return { name, rest: words.slice(index + 1) };
513
+ }
514
+ }
515
+ return null;
516
+ }
517
+ /**
518
+ * Acronyms stay upper-case, longer names are title-cased. Length is the only
519
+ * available signal here: the field is unspaced and often wholly upper-case, so
520
+ * casing cannot distinguish `TCS` from `DIVYAPRAKASH`.
521
+ */
522
+ const MAX_ACRONYM_LENGTH = 5;
523
+ function nachEntityName(name) {
524
+ if (name === name.toUpperCase() && name.length <= MAX_ACRONYM_LENGTH) {
525
+ return name;
526
+ }
527
+ return titleCase(name);
528
+ }
529
+ /**
530
+ * Word boundaries inside an unspaced description field: digit runs,
531
+ * separators, and both camel-case transitions. The second lookahead pair is
532
+ * what separates `INT` from `Div` in `INTDiv`, where the first (lower to
533
+ * upper) does not apply.
534
+ */
535
+ const SQUASHED_WORD_BOUNDARY = /\d+|[ ._-]|(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])/;
536
+ /**
537
+ * Is this NACH credit a dividend payout? `ACME3rdINTDiv01012026` -> yes.
538
+ *
539
+ * Matched on a whole word of the description rather than a substring, because
540
+ * `div` is a common fragment of Indian names (`DIVYA`) and of `INDIVIDUAL`.
541
+ */
542
+ function isDividend(rest) {
543
+ return rest.some(word => /^div(idend)?$/i.test(word));
544
+ }
545
+ /**
546
+ * Parse an Indian bank narration into a usable payee plus the structured
547
+ * fields hiding inside it.
548
+ *
549
+ * `merchant` is never empty — the fallback chain ends at the raw narration, so
550
+ * the worst outcome equals what Actual shows today.
551
+ */
552
+ export function parseNarration(raw, options = {}) {
553
+ const rules = options.merchantRules ?? [];
554
+ const trimmed = raw.trim();
555
+ const { tokens, phraseMode } = tokenize(trimmed);
556
+ const vpa = extractVpa(tokens);
557
+ const ref = extractRef(tokens);
558
+ const kind = detectKind(tokens);
559
+ const candidate = pickCandidate(withoutVpas(tokens), phraseMode);
560
+ // Parenthesised deliberately: `a ?? b || c` is a syntax error in JS.
561
+ const merchant = resolveMerchant({ vpa, candidate, rules, kind, raw: trimmed, tokens }) ??
562
+ (trimmed || 'Unknown');
563
+ return {
564
+ kind,
565
+ merchant,
566
+ raw: trimmed,
567
+ ...(vpa ? { vpa } : {}),
568
+ ...(ref ? { ref } : {}),
569
+ };
570
+ }
571
+ function resolveMerchant({ vpa, candidate, rules, kind, raw, tokens, }) {
572
+ // A mapped merchant is the strongest signal, and the VPA is the most stable
573
+ // thing to map on — it survives spelling changes in the name field.
574
+ if (vpa) {
575
+ const mapped = lookupMerchant(vpa.split('@')[0] ?? '', rules);
576
+ if (mapped) {
577
+ return mapped;
578
+ }
579
+ }
580
+ if (candidate) {
581
+ const mapped = lookupMerchant(candidate, rules);
582
+ if (mapped) {
583
+ return mapped;
584
+ }
585
+ }
586
+ // Bank postings that are not payments to anyone — interest, tax, charges.
587
+ // Checked ahead of the weaker name heuristics below, because a charge like
588
+ // `MABChgs-Mar2026` does contain a word-ish token (`MABChgs`) that would
589
+ // otherwise be title-cased into a payee.
590
+ const posting = lookupPosting(raw);
591
+ if (posting) {
592
+ return posting;
593
+ }
594
+ // A cash withdrawal has no payee. Without this the leftover token is
595
+ // usually the ATM's location, which makes every withdrawal a new payee —
596
+ // exactly the problem this tool exists to fix.
597
+ if (kind === 'atm') {
598
+ return 'ATM Withdrawal';
599
+ }
600
+ // Unmapped: prefer a multi-word name token, which is nearly always a real
601
+ // person or place (`JOHN DOE`, `SWIGGY BANGALORE`), over a squashed VPA
602
+ // local-part (`johndoe`). Single-word tokens lose to the VPA, which is
603
+ // better normalised.
604
+ const candidateIsMultiWord = !!candidate && /\s/.test(candidate.trim());
605
+ if (candidateIsMultiWord) {
606
+ return titleCase(cleanHandle(candidate));
607
+ }
608
+ if (vpa) {
609
+ const fromVpa = nameFromVpa(vpa);
610
+ // Words sharing the field with the VPA belong to the same name, and the
611
+ // VPA's local-part is often only part of it.
612
+ const beside = nameBesideVpa(tokens, vpa);
613
+ if (beside) {
614
+ return titleCase(cleanHandle(`${beside} ${fromVpa ?? ''}`.trim()));
615
+ }
616
+ if (fromVpa) {
617
+ return fromVpa;
618
+ }
619
+ // Nothing readable in it — a phone number or an account number as the
620
+ // local part. The VPA itself is still the right answer: it is stable per
621
+ // counterparty, whereas the raw narration carries a per-transaction
622
+ // reference and so would mint a new payee every time.
623
+ return vpa.toLowerCase();
624
+ }
625
+ if (candidate) {
626
+ // Handles carry a per-account numeric id; stripping it keeps one payee per
627
+ // merchant instead of one per merchant account.
628
+ return titleCase(isHandle(candidate) ? cleanHandle(candidate) : candidate);
629
+ }
630
+ // A transfer addressed to an account number rather than to a handle. Ranked
631
+ // below every name, since the account is an identity and not a name.
632
+ const account = nameFromAccountReference(tokens);
633
+ if (account) {
634
+ return account;
635
+ }
636
+ // A recurring mandate has no name in its narration, only the collecting
637
+ // bank and the mandate reference. Naming it after the mandate keeps every
638
+ // collection under one payee — otherwise the sequence number printed beside
639
+ // it makes each month a brand new payee, which is the exact problem this
640
+ // tool exists to solve. Give it a meaningful name with a merchant rule.
641
+ if (kind === 'ach') {
642
+ const mandate = subTokens(tokens).find(token => MANDATE_TOKEN.test(token));
643
+ if (mandate) {
644
+ return `NACH ${mandate.toUpperCase()}`;
645
+ }
646
+ // No mandate reference either, but the description field still leads with
647
+ // the company: `NACH/ACME3rdINTDiv01012026/1000001`. The run is rejected as
648
+ // a candidate above because the trailing date makes it look like a machine
649
+ // reference, so it is recovered here rather than falling through to the
650
+ // raw narration.
651
+ const entity = nachEntity(tokens);
652
+ if (entity) {
653
+ const name = lookupMerchant(entity.name, rules) ?? nachEntityName(entity.name);
654
+ return isDividend(entity.rest) ? `${name} Dividend` : name;
655
+ }
656
+ }
657
+ return null;
658
+ }
659
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/narration/parse.ts"],"names":[],"mappings":"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"}