paisa-mcp 0.0.12 → 0.0.15
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/dist/index.js +486 -18
- package/dist/index.js.map +11 -7
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -38162,6 +38162,7 @@ class PaisaApiClient {
|
|
|
38162
38162
|
constructor(baseUrl, token) {
|
|
38163
38163
|
this.http = axios_default.create({
|
|
38164
38164
|
baseURL: baseUrl.replace(/\/$/, ""),
|
|
38165
|
+
timeout: 30000,
|
|
38165
38166
|
headers: {
|
|
38166
38167
|
Authorization: `Bearer ${token}`,
|
|
38167
38168
|
"Content-Type": "application/json"
|
|
@@ -43081,7 +43082,21 @@ function registerNetWorthTools(server, client) {
|
|
|
43081
43082
|
breakdown: exports_external.record(exports_external.string(), exports_external.string()).optional()
|
|
43082
43083
|
}
|
|
43083
43084
|
}, async ({ mode, ...body }) => {
|
|
43084
|
-
|
|
43085
|
+
let data;
|
|
43086
|
+
if (mode === "auto") {
|
|
43087
|
+
data = await client.post("/api/net-worth/auto", { month: body.month, year: body.year });
|
|
43088
|
+
} else {
|
|
43089
|
+
const liquidCash = parseFloat(body.liquidCash ?? "0");
|
|
43090
|
+
const investments = parseFloat(body.investments ?? "0");
|
|
43091
|
+
const emergencyFund = parseFloat(body.emergencyFund ?? "0");
|
|
43092
|
+
const liabilities = parseFloat(body.liabilities ?? "0");
|
|
43093
|
+
const totalNetWorth = (liquidCash + investments + emergencyFund - liabilities).toFixed(2);
|
|
43094
|
+
data = await client.post("/api/net-worth", {
|
|
43095
|
+
...body,
|
|
43096
|
+
emergencyFund: body.emergencyFund ?? "0",
|
|
43097
|
+
totalNetWorth
|
|
43098
|
+
});
|
|
43099
|
+
}
|
|
43085
43100
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
43086
43101
|
});
|
|
43087
43102
|
}
|
|
@@ -45347,6 +45362,375 @@ function settlementEnrichedData(description, type, source) {
|
|
|
45347
45362
|
out.settlementCardLast4 = last4;
|
|
45348
45363
|
return out;
|
|
45349
45364
|
}
|
|
45365
|
+
// ../parsers/src/classify.ts
|
|
45366
|
+
var CORPORATE_TOKENS = [
|
|
45367
|
+
"PVT",
|
|
45368
|
+
"PRIVATE",
|
|
45369
|
+
"LIMITED",
|
|
45370
|
+
"LTD",
|
|
45371
|
+
"LLP",
|
|
45372
|
+
"INC",
|
|
45373
|
+
"CORP",
|
|
45374
|
+
"ENTERPRISES",
|
|
45375
|
+
"TECHNOLOGIES",
|
|
45376
|
+
"SOLUTIONS",
|
|
45377
|
+
"SERVICES"
|
|
45378
|
+
];
|
|
45379
|
+
var CORPORATE_REGEX = new RegExp(`\\b(${CORPORATE_TOKENS.join("|")})\\b`);
|
|
45380
|
+
function classifyEntity({ paymentMethod, extractedName }) {
|
|
45381
|
+
if (paymentMethod === "atm")
|
|
45382
|
+
return "merchant";
|
|
45383
|
+
if (paymentMethod === "pos")
|
|
45384
|
+
return "merchant";
|
|
45385
|
+
const upper = (extractedName ?? "").toUpperCase();
|
|
45386
|
+
if (upper && CORPORATE_REGEX.test(upper))
|
|
45387
|
+
return "merchant";
|
|
45388
|
+
if (paymentMethod === "imps")
|
|
45389
|
+
return "person";
|
|
45390
|
+
if (paymentMethod === "upi")
|
|
45391
|
+
return "person";
|
|
45392
|
+
if (paymentMethod === "neft" || paymentMethod === "rtgs")
|
|
45393
|
+
return "merchant";
|
|
45394
|
+
if (paymentMethod === "ach")
|
|
45395
|
+
return "merchant";
|
|
45396
|
+
return "unknown";
|
|
45397
|
+
}
|
|
45398
|
+
// ../parsers/src/hdfc/narration.ts
|
|
45399
|
+
function parseHdfcNarration(narration) {
|
|
45400
|
+
const trimmed = narration.trim();
|
|
45401
|
+
if (trimmed.startsWith("UPI-")) {
|
|
45402
|
+
return parseUpi(trimmed);
|
|
45403
|
+
}
|
|
45404
|
+
if (trimmed.startsWith("NEFT CR-") || trimmed.startsWith("NEFT DR-")) {
|
|
45405
|
+
return parseNeft(trimmed);
|
|
45406
|
+
}
|
|
45407
|
+
if (trimmed.startsWith("ACH D-") || trimmed.startsWith("ACH C-")) {
|
|
45408
|
+
return parseAch(trimmed);
|
|
45409
|
+
}
|
|
45410
|
+
if (trimmed.startsWith("FT-") || trimmed.startsWith("FT -")) {
|
|
45411
|
+
return parseFt(trimmed);
|
|
45412
|
+
}
|
|
45413
|
+
if (trimmed.startsWith("IMPS-") || trimmed.startsWith("IMPS/")) {
|
|
45414
|
+
return parseImps(trimmed);
|
|
45415
|
+
}
|
|
45416
|
+
if (trimmed.startsWith("ATW-") || trimmed.startsWith("ATM-") || trimmed.startsWith("ATM/")) {
|
|
45417
|
+
return {
|
|
45418
|
+
paymentMethod: "atm",
|
|
45419
|
+
name: "ATM Withdrawal",
|
|
45420
|
+
upiId: null,
|
|
45421
|
+
referenceNumber: null,
|
|
45422
|
+
description: trimmed
|
|
45423
|
+
};
|
|
45424
|
+
}
|
|
45425
|
+
if (trimmed.startsWith("POS ")) {
|
|
45426
|
+
return parsePos(trimmed);
|
|
45427
|
+
}
|
|
45428
|
+
return parseFallback(trimmed);
|
|
45429
|
+
}
|
|
45430
|
+
function parseUpi(narration) {
|
|
45431
|
+
const rest = narration.slice(4);
|
|
45432
|
+
const parts = rest.split("-");
|
|
45433
|
+
if (parts.length < 4) {
|
|
45434
|
+
return {
|
|
45435
|
+
paymentMethod: "upi",
|
|
45436
|
+
name: rest,
|
|
45437
|
+
upiId: null,
|
|
45438
|
+
referenceNumber: null,
|
|
45439
|
+
description: null
|
|
45440
|
+
};
|
|
45441
|
+
}
|
|
45442
|
+
const name = parts[0].trim();
|
|
45443
|
+
const upiId = parts[1]?.trim() || null;
|
|
45444
|
+
const referenceNumber = parts[3]?.trim() || null;
|
|
45445
|
+
const description = parts.length > 4 ? parts.slice(4).join("-").trim() : null;
|
|
45446
|
+
return {
|
|
45447
|
+
paymentMethod: "upi",
|
|
45448
|
+
name: cleanName(name),
|
|
45449
|
+
upiId,
|
|
45450
|
+
referenceNumber,
|
|
45451
|
+
description: description || null
|
|
45452
|
+
};
|
|
45453
|
+
}
|
|
45454
|
+
function parseNeft(narration) {
|
|
45455
|
+
const isCredit = narration.startsWith("NEFT CR-");
|
|
45456
|
+
const rest = narration.slice(isCredit ? 8 : 8);
|
|
45457
|
+
const parts = rest.split("-");
|
|
45458
|
+
if (parts.length < 2) {
|
|
45459
|
+
return {
|
|
45460
|
+
paymentMethod: "neft",
|
|
45461
|
+
name: rest,
|
|
45462
|
+
upiId: null,
|
|
45463
|
+
referenceNumber: null,
|
|
45464
|
+
description: null
|
|
45465
|
+
};
|
|
45466
|
+
}
|
|
45467
|
+
const companyName = parts[1]?.trim() || parts[0]?.trim() || rest;
|
|
45468
|
+
const referenceNumber = parts[parts.length - 1]?.trim() || null;
|
|
45469
|
+
return {
|
|
45470
|
+
paymentMethod: "neft",
|
|
45471
|
+
name: cleanName(companyName),
|
|
45472
|
+
upiId: null,
|
|
45473
|
+
referenceNumber,
|
|
45474
|
+
description: null
|
|
45475
|
+
};
|
|
45476
|
+
}
|
|
45477
|
+
function parseAch(narration) {
|
|
45478
|
+
const rest = narration.slice(7).trim();
|
|
45479
|
+
const parts = rest.split("-");
|
|
45480
|
+
const name = parts[0]?.trim() || rest;
|
|
45481
|
+
const referenceNumber = parts.length > 1 ? parts[parts.length - 1]?.trim() : null;
|
|
45482
|
+
return {
|
|
45483
|
+
paymentMethod: "ach",
|
|
45484
|
+
name: cleanName(name),
|
|
45485
|
+
upiId: null,
|
|
45486
|
+
referenceNumber,
|
|
45487
|
+
description: null
|
|
45488
|
+
};
|
|
45489
|
+
}
|
|
45490
|
+
function parseFt(narration) {
|
|
45491
|
+
const rest = narration.replace(/^FT[\s-]+/, "").trim();
|
|
45492
|
+
const spaceDashParts = rest.split(" - ");
|
|
45493
|
+
if (spaceDashParts.length >= 2) {
|
|
45494
|
+
const namePart = spaceDashParts.find((p2, i2) => i2 > 0 && p2.trim().length > 0);
|
|
45495
|
+
return {
|
|
45496
|
+
paymentMethod: "fund_transfer",
|
|
45497
|
+
name: cleanName(namePart?.trim() || rest),
|
|
45498
|
+
upiId: null,
|
|
45499
|
+
referenceNumber: spaceDashParts[0]?.split("-")[0]?.trim() || null,
|
|
45500
|
+
description: null
|
|
45501
|
+
};
|
|
45502
|
+
}
|
|
45503
|
+
return {
|
|
45504
|
+
paymentMethod: "fund_transfer",
|
|
45505
|
+
name: cleanName(rest),
|
|
45506
|
+
upiId: null,
|
|
45507
|
+
referenceNumber: null,
|
|
45508
|
+
description: null
|
|
45509
|
+
};
|
|
45510
|
+
}
|
|
45511
|
+
function parseImps(narration) {
|
|
45512
|
+
const rest = narration.replace(/^IMPS[-/]/, "").trim();
|
|
45513
|
+
const parts = rest.split("-");
|
|
45514
|
+
const referenceNumber = parts[0]?.trim() || null;
|
|
45515
|
+
const name = parts.length > 1 ? parts[1]?.trim() : rest;
|
|
45516
|
+
return {
|
|
45517
|
+
paymentMethod: "imps",
|
|
45518
|
+
name: cleanName(name || rest),
|
|
45519
|
+
upiId: null,
|
|
45520
|
+
referenceNumber,
|
|
45521
|
+
description: null
|
|
45522
|
+
};
|
|
45523
|
+
}
|
|
45524
|
+
function parsePos(narration) {
|
|
45525
|
+
const rest = narration.slice(4).trim();
|
|
45526
|
+
const cleaned = rest.replace(/^[0-9X]{4,}\s*/, "").trim();
|
|
45527
|
+
return {
|
|
45528
|
+
paymentMethod: "pos",
|
|
45529
|
+
name: cleanName(cleaned || rest),
|
|
45530
|
+
upiId: null,
|
|
45531
|
+
referenceNumber: null,
|
|
45532
|
+
description: null
|
|
45533
|
+
};
|
|
45534
|
+
}
|
|
45535
|
+
function parseFallback(narration) {
|
|
45536
|
+
return {
|
|
45537
|
+
paymentMethod: "other",
|
|
45538
|
+
name: cleanName(narration),
|
|
45539
|
+
upiId: null,
|
|
45540
|
+
referenceNumber: null,
|
|
45541
|
+
description: null
|
|
45542
|
+
};
|
|
45543
|
+
}
|
|
45544
|
+
function cleanName(raw) {
|
|
45545
|
+
return raw.replace(/\s+(PRIVATE|PVT\.?)\s*\w*/gi, "").replace(/\s+(LIMITED|LTD\.?)(\s|$)/gi, " ").replace(/[\s.-]+$/, "").replace(/\s{2,}/g, " ").trim();
|
|
45546
|
+
}
|
|
45547
|
+
|
|
45548
|
+
// ../parsers/src/sbi/savings.ts
|
|
45549
|
+
function parseSbiNarration(narration) {
|
|
45550
|
+
const upper = narration.toUpperCase();
|
|
45551
|
+
const upiMatch = narration.match(/UPI\/(?:DR|CR)\/(\d+)\/([^/]+)\/([A-Z]{2,6})\/([^/\s]+)/i);
|
|
45552
|
+
if (upiMatch) {
|
|
45553
|
+
const name = upiMatch[2].trim();
|
|
45554
|
+
const upiId = upiMatch[4].trim();
|
|
45555
|
+
return {
|
|
45556
|
+
paymentMethod: "upi",
|
|
45557
|
+
name: name || null,
|
|
45558
|
+
upiId: upiId || null
|
|
45559
|
+
};
|
|
45560
|
+
}
|
|
45561
|
+
if (upper.includes("NEFT")) {
|
|
45562
|
+
const neftMatch = narration.match(/NEFT[-/][\w]+[-/]([\w\s]+?)(?:[-/]|$)/i);
|
|
45563
|
+
return {
|
|
45564
|
+
paymentMethod: "neft",
|
|
45565
|
+
name: neftMatch ? neftMatch[1].trim() : null,
|
|
45566
|
+
upiId: null
|
|
45567
|
+
};
|
|
45568
|
+
}
|
|
45569
|
+
if (upper.includes("RTGS")) {
|
|
45570
|
+
return { paymentMethod: "rtgs", name: null, upiId: null };
|
|
45571
|
+
}
|
|
45572
|
+
if (upper.includes("IMPS")) {
|
|
45573
|
+
return { paymentMethod: "imps", name: null, upiId: null };
|
|
45574
|
+
}
|
|
45575
|
+
if (upper.includes("ATM") || upper.includes("WDL ATM")) {
|
|
45576
|
+
return { paymentMethod: "atm", name: null, upiId: null };
|
|
45577
|
+
}
|
|
45578
|
+
if (upper.includes("POS") || upper.includes("PURCHASE")) {
|
|
45579
|
+
return { paymentMethod: "pos", name: null, upiId: null };
|
|
45580
|
+
}
|
|
45581
|
+
if (upper.includes("INT.COLL") || upper.includes("INTEREST")) {
|
|
45582
|
+
return { paymentMethod: "other", name: "Interest", upiId: null };
|
|
45583
|
+
}
|
|
45584
|
+
return { paymentMethod: null, name: null, upiId: null };
|
|
45585
|
+
}
|
|
45586
|
+
// src/lib/person-backfill.ts
|
|
45587
|
+
function normalizePersonName(name) {
|
|
45588
|
+
return name.toLowerCase().trim().replace(/\s+/g, " ");
|
|
45589
|
+
}
|
|
45590
|
+
async function loadDecryptedPersons(client, crypto3) {
|
|
45591
|
+
const res = await client.get("/api/persons");
|
|
45592
|
+
const persons = [];
|
|
45593
|
+
for (const p2 of res.data ?? []) {
|
|
45594
|
+
let name = p2.name ?? "";
|
|
45595
|
+
if (crypto3 && p2.cipher) {
|
|
45596
|
+
try {
|
|
45597
|
+
name = (await decryptPersonCipher(p2.cipher, crypto3.key)).name;
|
|
45598
|
+
} catch {}
|
|
45599
|
+
}
|
|
45600
|
+
if (name)
|
|
45601
|
+
persons.push({ id: p2.id, name });
|
|
45602
|
+
}
|
|
45603
|
+
return persons;
|
|
45604
|
+
}
|
|
45605
|
+
function resolvePersonByName(persons, query) {
|
|
45606
|
+
const q2 = normalizePersonName(query);
|
|
45607
|
+
if (!q2)
|
|
45608
|
+
return null;
|
|
45609
|
+
const exact = persons.filter((p2) => normalizePersonName(p2.name) === q2);
|
|
45610
|
+
if (exact.length === 1)
|
|
45611
|
+
return { personId: exact[0].id };
|
|
45612
|
+
if (exact.length > 1)
|
|
45613
|
+
return { candidates: exact.map((p2) => p2.name) };
|
|
45614
|
+
const partial2 = persons.filter((p2) => normalizePersonName(p2.name).includes(q2));
|
|
45615
|
+
if (partial2.length === 1)
|
|
45616
|
+
return { personId: partial2[0].id };
|
|
45617
|
+
if (partial2.length > 1)
|
|
45618
|
+
return { candidates: partial2.map((p2) => p2.name) };
|
|
45619
|
+
return null;
|
|
45620
|
+
}
|
|
45621
|
+
function parseNarration(description) {
|
|
45622
|
+
const hdfc = parseHdfcNarration(description);
|
|
45623
|
+
if (hdfc.name && hdfc.paymentMethod)
|
|
45624
|
+
return { name: hdfc.name, upiId: hdfc.upiId, paymentMethod: hdfc.paymentMethod };
|
|
45625
|
+
const sbi = parseSbiNarration(description);
|
|
45626
|
+
if (sbi.name && sbi.paymentMethod)
|
|
45627
|
+
return { name: sbi.name, upiId: sbi.upiId, paymentMethod: sbi.paymentMethod };
|
|
45628
|
+
return {
|
|
45629
|
+
name: hdfc.name || sbi.name,
|
|
45630
|
+
upiId: hdfc.upiId ?? sbi.upiId,
|
|
45631
|
+
paymentMethod: hdfc.paymentMethod ?? sbi.paymentMethod
|
|
45632
|
+
};
|
|
45633
|
+
}
|
|
45634
|
+
async function backfillPersons(client, crypto3, opts = {}) {
|
|
45635
|
+
const existing = await loadDecryptedPersons(client, crypto3);
|
|
45636
|
+
const personIdByNorm = new Map;
|
|
45637
|
+
for (const p2 of existing)
|
|
45638
|
+
personIdByNorm.set(normalizePersonName(p2.name), p2.id);
|
|
45639
|
+
const candidates = [];
|
|
45640
|
+
let cursor;
|
|
45641
|
+
const MAX_PAGES = 100;
|
|
45642
|
+
for (let page = 0;page < MAX_PAGES; page++) {
|
|
45643
|
+
const params = { limit: 200 };
|
|
45644
|
+
if (cursor)
|
|
45645
|
+
params.cursor = cursor;
|
|
45646
|
+
const res = await client.get("/api/transactions", params);
|
|
45647
|
+
const rows = res.data ?? [];
|
|
45648
|
+
if (crypto3)
|
|
45649
|
+
await decryptTransactionFields(rows, crypto3.key);
|
|
45650
|
+
for (const r2 of rows) {
|
|
45651
|
+
if (!r2.merchantId && !r2.personId && !r2.isTransfer && !r2.isIgnored)
|
|
45652
|
+
candidates.push(r2);
|
|
45653
|
+
}
|
|
45654
|
+
if (!res.hasMore || !res.nextCursor || rows.length === 0)
|
|
45655
|
+
break;
|
|
45656
|
+
cursor = res.nextCursor;
|
|
45657
|
+
}
|
|
45658
|
+
const groups = new Map;
|
|
45659
|
+
for (const t2 of candidates) {
|
|
45660
|
+
const { name, upiId, paymentMethod } = parseNarration(t2.description);
|
|
45661
|
+
if (!name)
|
|
45662
|
+
continue;
|
|
45663
|
+
if (classifyEntity({ paymentMethod, extractedName: name }) !== "person")
|
|
45664
|
+
continue;
|
|
45665
|
+
const norm = normalizePersonName(name);
|
|
45666
|
+
if (!norm)
|
|
45667
|
+
continue;
|
|
45668
|
+
const g2 = groups.get(norm) ?? {
|
|
45669
|
+
displayName: name.trim(),
|
|
45670
|
+
upiHandles: new Set,
|
|
45671
|
+
txnIds: []
|
|
45672
|
+
};
|
|
45673
|
+
if (upiId)
|
|
45674
|
+
g2.upiHandles.add(upiId);
|
|
45675
|
+
g2.txnIds.push(t2.id);
|
|
45676
|
+
groups.set(norm, g2);
|
|
45677
|
+
}
|
|
45678
|
+
const plan = [];
|
|
45679
|
+
const newGroups = [];
|
|
45680
|
+
let reused = 0;
|
|
45681
|
+
for (const [norm, g2] of groups) {
|
|
45682
|
+
const upiHandles = [...g2.upiHandles];
|
|
45683
|
+
const isReuse = personIdByNorm.has(norm);
|
|
45684
|
+
plan.push({
|
|
45685
|
+
name: g2.displayName,
|
|
45686
|
+
normalized: norm,
|
|
45687
|
+
txnCount: g2.txnIds.length,
|
|
45688
|
+
upiHandles,
|
|
45689
|
+
action: isReuse ? "reuse" : "create"
|
|
45690
|
+
});
|
|
45691
|
+
if (isReuse)
|
|
45692
|
+
reused++;
|
|
45693
|
+
else
|
|
45694
|
+
newGroups.push({ norm, displayName: g2.displayName, upiHandles });
|
|
45695
|
+
}
|
|
45696
|
+
if (!opts.dryRun && newGroups.length > 0) {
|
|
45697
|
+
const bodies = await Promise.all(newGroups.map(async ({ displayName, upiHandles }) => {
|
|
45698
|
+
const fields = {
|
|
45699
|
+
name: displayName,
|
|
45700
|
+
upiHandles,
|
|
45701
|
+
phoneNumbers: [],
|
|
45702
|
+
relationship: null,
|
|
45703
|
+
notes: null
|
|
45704
|
+
};
|
|
45705
|
+
return crypto3 ? await encryptPersonCipher(fields, crypto3.key) : { name: displayName, upiHandles, phoneNumbers: [], relationship: null, notes: null };
|
|
45706
|
+
}));
|
|
45707
|
+
const res = await client.post("/api/persons/bulk", {
|
|
45708
|
+
persons: bodies
|
|
45709
|
+
});
|
|
45710
|
+
res.data.forEach((p2, i2) => {
|
|
45711
|
+
personIdByNorm.set(newGroups[i2].norm, p2.id);
|
|
45712
|
+
});
|
|
45713
|
+
}
|
|
45714
|
+
const created = newGroups.length;
|
|
45715
|
+
const links = [];
|
|
45716
|
+
if (!opts.dryRun) {
|
|
45717
|
+
for (const [norm, g2] of groups) {
|
|
45718
|
+
const personId = personIdByNorm.get(norm);
|
|
45719
|
+
if (!personId)
|
|
45720
|
+
continue;
|
|
45721
|
+
for (const txnId of g2.txnIds)
|
|
45722
|
+
links.push({ transactionId: txnId, personId });
|
|
45723
|
+
}
|
|
45724
|
+
}
|
|
45725
|
+
if (!opts.dryRun && links.length > 0) {
|
|
45726
|
+
const CHUNK = 500;
|
|
45727
|
+
for (let i2 = 0;i2 < links.length; i2 += CHUNK) {
|
|
45728
|
+
await client.post("/api/transactions/bulk-link-entity", { links: links.slice(i2, i2 + CHUNK) });
|
|
45729
|
+
}
|
|
45730
|
+
}
|
|
45731
|
+
return { candidatesScanned: candidates.length, created, reused, linked: links.length, plan };
|
|
45732
|
+
}
|
|
45733
|
+
|
|
45350
45734
|
// src/tools/transactions.ts
|
|
45351
45735
|
async function importTransactions(client, crypto3, opts) {
|
|
45352
45736
|
const rows = opts.txns.map((t2) => {
|
|
@@ -45387,18 +45771,7 @@ async function runRescan(client, crypto3) {
|
|
|
45387
45771
|
const globalMerchants = allMerchants.filter((m2) => !m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
|
|
45388
45772
|
const localMerchants = allMerchants.filter((m2) => m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
|
|
45389
45773
|
const merchantCategoryById = new Map(allMerchants.filter((m2) => m2.categoryId).map((m2) => [m2.id, m2.categoryId]));
|
|
45390
|
-
const
|
|
45391
|
-
const persons = [];
|
|
45392
|
-
for (const p2 of personsRes.data ?? []) {
|
|
45393
|
-
let name = p2.name ?? "";
|
|
45394
|
-
if (crypto3 && p2.cipher) {
|
|
45395
|
-
try {
|
|
45396
|
-
name = (await decryptPersonCipher(p2.cipher, crypto3.key)).name;
|
|
45397
|
-
} catch {}
|
|
45398
|
-
}
|
|
45399
|
-
if (name)
|
|
45400
|
-
persons.push({ id: p2.id, name });
|
|
45401
|
-
}
|
|
45774
|
+
const persons = await loadDecryptedPersons(client, crypto3);
|
|
45402
45775
|
const allRows = [];
|
|
45403
45776
|
const limit = 200;
|
|
45404
45777
|
let cursor;
|
|
@@ -45444,9 +45817,11 @@ async function runRescan(client, crypto3) {
|
|
|
45444
45817
|
}
|
|
45445
45818
|
function registerTransactionTools(server, client, crypto3) {
|
|
45446
45819
|
server.registerTool("get_transactions", {
|
|
45447
|
-
description: "Search and filter transactions. Returns paginated results with merchant and category details.",
|
|
45820
|
+
description: "Search and filter transactions. Returns paginated results with merchant and category details. To find everything paid to a specific counterparty, pass `personName` (resolved to a person via the household's persons list) or `personId` directly — this is an indexed lookup, not a description scan. Note: free-text `search` only matches plaintext merchant names, not encrypted descriptions.",
|
|
45448
45821
|
inputSchema: {
|
|
45449
|
-
search: exports_external.string().optional().describe("Search merchant name
|
|
45822
|
+
search: exports_external.string().optional().describe("Search merchant name (NOT encrypted descriptions)"),
|
|
45823
|
+
personId: exports_external.string().optional().describe("Filter to one person (exact, indexed)"),
|
|
45824
|
+
personName: exports_external.string().optional().describe("Resolve a counterparty name to a person and filter by it"),
|
|
45450
45825
|
startDate: exports_external.string().optional().describe("Filter from date (YYYY-MM-DD)"),
|
|
45451
45826
|
endDate: exports_external.string().optional().describe("Filter to date (YYYY-MM-DD)"),
|
|
45452
45827
|
owner: exports_external.string().optional(),
|
|
@@ -45460,11 +45835,51 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45460
45835
|
offset: exports_external.number().optional()
|
|
45461
45836
|
}
|
|
45462
45837
|
}, async (params) => {
|
|
45463
|
-
const
|
|
45838
|
+
const { personName, ...rest } = params;
|
|
45839
|
+
const apiParams = { ...rest };
|
|
45840
|
+
if (personName && !apiParams.personId) {
|
|
45841
|
+
const persons = await loadDecryptedPersons(client, crypto3);
|
|
45842
|
+
const match = resolvePersonByName(persons, personName);
|
|
45843
|
+
if (!match) {
|
|
45844
|
+
return {
|
|
45845
|
+
content: [
|
|
45846
|
+
{
|
|
45847
|
+
type: "text",
|
|
45848
|
+
text: JSON.stringify({
|
|
45849
|
+
error: `No person matches "${personName}". Use list_persons to see known counterparties, or run backfill_persons if persons haven't been extracted yet.`
|
|
45850
|
+
})
|
|
45851
|
+
}
|
|
45852
|
+
]
|
|
45853
|
+
};
|
|
45854
|
+
}
|
|
45855
|
+
if ("candidates" in match) {
|
|
45856
|
+
return {
|
|
45857
|
+
content: [
|
|
45858
|
+
{
|
|
45859
|
+
type: "text",
|
|
45860
|
+
text: JSON.stringify({
|
|
45861
|
+
error: `Ambiguous person "${personName}". Candidates: ${match.candidates.join(", ")}. Re-run with an exact name or personId.`
|
|
45862
|
+
})
|
|
45863
|
+
}
|
|
45864
|
+
]
|
|
45865
|
+
};
|
|
45866
|
+
}
|
|
45867
|
+
apiParams.personId = match.personId;
|
|
45868
|
+
}
|
|
45869
|
+
const data = await client.get("/api/transactions", Object.fromEntries(Object.entries(apiParams).filter(([, v2]) => v2 !== undefined).map(([k2, v2]) => [k2, String(v2)])));
|
|
45464
45870
|
if (crypto3 && data.data)
|
|
45465
45871
|
await decryptTransactionFields(data.data, crypto3.key);
|
|
45466
45872
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
45467
45873
|
});
|
|
45874
|
+
server.registerTool("backfill_persons", {
|
|
45875
|
+
description: "Extract UPI/IMPS counterparties from unlinked transactions, create person records for them (deduped by name), and link transactions via personId — so you can later filter all payments to a person. Idempotent: re-running reuses existing persons and skips already-linked rows. Pass dryRun to preview without writing.",
|
|
45876
|
+
inputSchema: {
|
|
45877
|
+
dryRun: exports_external.boolean().optional().describe("Preview the persons that would be created/reused without writing")
|
|
45878
|
+
}
|
|
45879
|
+
}, async ({ dryRun }) => {
|
|
45880
|
+
const result = await backfillPersons(client, crypto3, { dryRun });
|
|
45881
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
45882
|
+
});
|
|
45468
45883
|
server.registerTool("update_transaction", {
|
|
45469
45884
|
description: "Update a transaction's category, merchant, owner, notes, or flags.",
|
|
45470
45885
|
inputSchema: {
|
|
@@ -45482,6 +45897,16 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45482
45897
|
const body = { ...updates };
|
|
45483
45898
|
if (crypto3 && body.notes)
|
|
45484
45899
|
body.notes = await encryptField(body.notes, crypto3.key);
|
|
45900
|
+
if (body.categorySlug) {
|
|
45901
|
+
const cats = await client.get("/api/categories");
|
|
45902
|
+
const allCats = cats.data.flatMap((c2) => [c2, ...c2.children ?? []]);
|
|
45903
|
+
const cat = allCats.find((c2) => c2.slug === body.categorySlug);
|
|
45904
|
+
if (cat) {
|
|
45905
|
+
body.categoryId = cat.id;
|
|
45906
|
+
body.categoryType = cat.type;
|
|
45907
|
+
}
|
|
45908
|
+
delete body.categorySlug;
|
|
45909
|
+
}
|
|
45485
45910
|
const data = await client.patch(`/api/transactions/${id}`, body);
|
|
45486
45911
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
45487
45912
|
});
|
|
@@ -45607,6 +46032,49 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45607
46032
|
]
|
|
45608
46033
|
};
|
|
45609
46034
|
});
|
|
46035
|
+
server.registerTool("bulk_categorize_transactions", {
|
|
46036
|
+
description: "Set categories on multiple transactions at once without needing to link them to a merchant or person. " + "Pass an array of {transactionId, categoryId} pairs. Runs PATCH /api/transactions/{id} for each link in parallel batches. " + "Use this when you have category UUIDs and want to bulk-categorize uncategorized transactions directly.",
|
|
46037
|
+
inputSchema: {
|
|
46038
|
+
links: exports_external.array(exports_external.object({
|
|
46039
|
+
transactionId: exports_external.string().describe("Transaction UUID"),
|
|
46040
|
+
categoryId: exports_external.string().describe("Category UUID")
|
|
46041
|
+
})).min(1).max(500)
|
|
46042
|
+
}
|
|
46043
|
+
}, async ({ links }) => {
|
|
46044
|
+
const catsRes = await client.get("/api/categories");
|
|
46045
|
+
const allCats = catsRes.data.flatMap((c2) => [c2, ...c2.children ?? []]);
|
|
46046
|
+
const typeById = new Map(allCats.map((c2) => [c2.id, c2.type]));
|
|
46047
|
+
const groups = new Map;
|
|
46048
|
+
for (const { transactionId, categoryId } of links) {
|
|
46049
|
+
const arr = groups.get(categoryId) ?? [];
|
|
46050
|
+
arr.push(transactionId);
|
|
46051
|
+
groups.set(categoryId, arr);
|
|
46052
|
+
}
|
|
46053
|
+
const CHUNK = 200;
|
|
46054
|
+
let succeeded = 0;
|
|
46055
|
+
const failed = [];
|
|
46056
|
+
await Promise.allSettled([...groups.entries()].flatMap(([categoryId, ids]) => {
|
|
46057
|
+
const categoryType = typeById.get(categoryId);
|
|
46058
|
+
const chunks = [];
|
|
46059
|
+
for (let i2 = 0;i2 < ids.length; i2 += CHUNK)
|
|
46060
|
+
chunks.push(ids.slice(i2, i2 + CHUNK));
|
|
46061
|
+
return chunks.map((chunk) => client.patch("/api/transactions/bulk", {
|
|
46062
|
+
ids: chunk,
|
|
46063
|
+
categoryId,
|
|
46064
|
+
...categoryType ? { categoryType } : {}
|
|
46065
|
+
}).then((r2) => {
|
|
46066
|
+
succeeded += r2.updated ?? chunk.length;
|
|
46067
|
+
}).catch((e2) => failed.push({ categoryId, error: String(e2) })));
|
|
46068
|
+
}));
|
|
46069
|
+
return {
|
|
46070
|
+
content: [
|
|
46071
|
+
{
|
|
46072
|
+
type: "text",
|
|
46073
|
+
text: JSON.stringify({ succeeded, failedCount: failed.length, failed })
|
|
46074
|
+
}
|
|
46075
|
+
]
|
|
46076
|
+
};
|
|
46077
|
+
});
|
|
45610
46078
|
server.registerTool("delete_transactions", {
|
|
45611
46079
|
description: "Hard-delete transactions. This is PERMANENT and cannot be undone. " + "Provide exactly one of: `id` (single transaction), `ids` (a list of transactions), " + "or `filters` (deletes all matching transactions). Returns the number of deleted transactions " + "for the bulk paths.",
|
|
45612
46080
|
inputSchema: {
|
|
@@ -45696,7 +46164,7 @@ async function main() {
|
|
|
45696
46164
|
console.error("Error: PAISA_USER_ID is required when PAISA_PIN is set.");
|
|
45697
46165
|
process.exit(1);
|
|
45698
46166
|
}
|
|
45699
|
-
const cryptoMeta = await client.get("/auth-crypto").catch(() => null);
|
|
46167
|
+
const cryptoMeta = await client.get("/api/auth-crypto").catch(() => null);
|
|
45700
46168
|
if (cryptoMeta && (cryptoMeta.kdfVersion ?? 1) >= 2 && cryptoMeta.kdfSalt && cryptoMeta.wrappedDek) {
|
|
45701
46169
|
const kek = await deriveKEK(pin, cryptoMeta.kdfSalt);
|
|
45702
46170
|
const dek = await unwrapDEK(cryptoMeta.wrappedDek, kek);
|
|
@@ -45746,5 +46214,5 @@ main().catch((err) => {
|
|
|
45746
46214
|
process.exit(1);
|
|
45747
46215
|
});
|
|
45748
46216
|
|
|
45749
|
-
//# debugId=
|
|
46217
|
+
//# debugId=D3F9CA60DACD6F8864756E2164756E21
|
|
45750
46218
|
//# sourceMappingURL=index.js.map
|