paisa-mcp 0.0.12 → 0.0.14
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 +479 -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,368 @@ 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 links = [];
|
|
45680
|
+
let created = 0;
|
|
45681
|
+
let reused = 0;
|
|
45682
|
+
for (const [norm, g2] of groups) {
|
|
45683
|
+
const upiHandles = [...g2.upiHandles];
|
|
45684
|
+
let personId = personIdByNorm.get(norm);
|
|
45685
|
+
const action = personId ? "reuse" : "create";
|
|
45686
|
+
plan.push({
|
|
45687
|
+
name: g2.displayName,
|
|
45688
|
+
normalized: norm,
|
|
45689
|
+
txnCount: g2.txnIds.length,
|
|
45690
|
+
upiHandles,
|
|
45691
|
+
action
|
|
45692
|
+
});
|
|
45693
|
+
if (!opts.dryRun) {
|
|
45694
|
+
if (!personId) {
|
|
45695
|
+
const fields = {
|
|
45696
|
+
name: g2.displayName,
|
|
45697
|
+
upiHandles,
|
|
45698
|
+
phoneNumbers: [],
|
|
45699
|
+
relationship: null,
|
|
45700
|
+
notes: null
|
|
45701
|
+
};
|
|
45702
|
+
const body = crypto3 ? await encryptPersonCipher(fields, crypto3.key) : { name: fields.name, upiHandles, phoneNumbers: [], relationship: null, notes: null };
|
|
45703
|
+
const res = await client.post("/api/persons", body);
|
|
45704
|
+
personId = res.data.id;
|
|
45705
|
+
personIdByNorm.set(norm, personId);
|
|
45706
|
+
created++;
|
|
45707
|
+
} else {
|
|
45708
|
+
reused++;
|
|
45709
|
+
}
|
|
45710
|
+
for (const txnId of g2.txnIds)
|
|
45711
|
+
links.push({ transactionId: txnId, personId });
|
|
45712
|
+
} else if (personId) {
|
|
45713
|
+
reused++;
|
|
45714
|
+
} else {
|
|
45715
|
+
created++;
|
|
45716
|
+
}
|
|
45717
|
+
}
|
|
45718
|
+
if (!opts.dryRun && links.length > 0) {
|
|
45719
|
+
const CHUNK = 500;
|
|
45720
|
+
for (let i2 = 0;i2 < links.length; i2 += CHUNK) {
|
|
45721
|
+
await client.post("/api/transactions/bulk-link-entity", { links: links.slice(i2, i2 + CHUNK) });
|
|
45722
|
+
}
|
|
45723
|
+
}
|
|
45724
|
+
return { candidatesScanned: candidates.length, created, reused, linked: links.length, plan };
|
|
45725
|
+
}
|
|
45726
|
+
|
|
45350
45727
|
// src/tools/transactions.ts
|
|
45351
45728
|
async function importTransactions(client, crypto3, opts) {
|
|
45352
45729
|
const rows = opts.txns.map((t2) => {
|
|
@@ -45387,18 +45764,7 @@ async function runRescan(client, crypto3) {
|
|
|
45387
45764
|
const globalMerchants = allMerchants.filter((m2) => !m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
|
|
45388
45765
|
const localMerchants = allMerchants.filter((m2) => m2.householdId && m2.cleanName).map((m2) => ({ id: m2.id, name: m2.cleanName }));
|
|
45389
45766
|
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
|
-
}
|
|
45767
|
+
const persons = await loadDecryptedPersons(client, crypto3);
|
|
45402
45768
|
const allRows = [];
|
|
45403
45769
|
const limit = 200;
|
|
45404
45770
|
let cursor;
|
|
@@ -45444,9 +45810,11 @@ async function runRescan(client, crypto3) {
|
|
|
45444
45810
|
}
|
|
45445
45811
|
function registerTransactionTools(server, client, crypto3) {
|
|
45446
45812
|
server.registerTool("get_transactions", {
|
|
45447
|
-
description: "Search and filter transactions. Returns paginated results with merchant and category details.",
|
|
45813
|
+
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
45814
|
inputSchema: {
|
|
45449
|
-
search: exports_external.string().optional().describe("Search merchant name
|
|
45815
|
+
search: exports_external.string().optional().describe("Search merchant name (NOT encrypted descriptions)"),
|
|
45816
|
+
personId: exports_external.string().optional().describe("Filter to one person (exact, indexed)"),
|
|
45817
|
+
personName: exports_external.string().optional().describe("Resolve a counterparty name to a person and filter by it"),
|
|
45450
45818
|
startDate: exports_external.string().optional().describe("Filter from date (YYYY-MM-DD)"),
|
|
45451
45819
|
endDate: exports_external.string().optional().describe("Filter to date (YYYY-MM-DD)"),
|
|
45452
45820
|
owner: exports_external.string().optional(),
|
|
@@ -45460,11 +45828,51 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45460
45828
|
offset: exports_external.number().optional()
|
|
45461
45829
|
}
|
|
45462
45830
|
}, async (params) => {
|
|
45463
|
-
const
|
|
45831
|
+
const { personName, ...rest } = params;
|
|
45832
|
+
const apiParams = { ...rest };
|
|
45833
|
+
if (personName && !apiParams.personId) {
|
|
45834
|
+
const persons = await loadDecryptedPersons(client, crypto3);
|
|
45835
|
+
const match = resolvePersonByName(persons, personName);
|
|
45836
|
+
if (!match) {
|
|
45837
|
+
return {
|
|
45838
|
+
content: [
|
|
45839
|
+
{
|
|
45840
|
+
type: "text",
|
|
45841
|
+
text: JSON.stringify({
|
|
45842
|
+
error: `No person matches "${personName}". Use list_persons to see known counterparties, or run backfill_persons if persons haven't been extracted yet.`
|
|
45843
|
+
})
|
|
45844
|
+
}
|
|
45845
|
+
]
|
|
45846
|
+
};
|
|
45847
|
+
}
|
|
45848
|
+
if ("candidates" in match) {
|
|
45849
|
+
return {
|
|
45850
|
+
content: [
|
|
45851
|
+
{
|
|
45852
|
+
type: "text",
|
|
45853
|
+
text: JSON.stringify({
|
|
45854
|
+
error: `Ambiguous person "${personName}". Candidates: ${match.candidates.join(", ")}. Re-run with an exact name or personId.`
|
|
45855
|
+
})
|
|
45856
|
+
}
|
|
45857
|
+
]
|
|
45858
|
+
};
|
|
45859
|
+
}
|
|
45860
|
+
apiParams.personId = match.personId;
|
|
45861
|
+
}
|
|
45862
|
+
const data = await client.get("/api/transactions", Object.fromEntries(Object.entries(apiParams).filter(([, v2]) => v2 !== undefined).map(([k2, v2]) => [k2, String(v2)])));
|
|
45464
45863
|
if (crypto3 && data.data)
|
|
45465
45864
|
await decryptTransactionFields(data.data, crypto3.key);
|
|
45466
45865
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
45467
45866
|
});
|
|
45867
|
+
server.registerTool("backfill_persons", {
|
|
45868
|
+
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.",
|
|
45869
|
+
inputSchema: {
|
|
45870
|
+
dryRun: exports_external.boolean().optional().describe("Preview the persons that would be created/reused without writing")
|
|
45871
|
+
}
|
|
45872
|
+
}, async ({ dryRun }) => {
|
|
45873
|
+
const result = await backfillPersons(client, crypto3, { dryRun });
|
|
45874
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
45875
|
+
});
|
|
45468
45876
|
server.registerTool("update_transaction", {
|
|
45469
45877
|
description: "Update a transaction's category, merchant, owner, notes, or flags.",
|
|
45470
45878
|
inputSchema: {
|
|
@@ -45482,6 +45890,16 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45482
45890
|
const body = { ...updates };
|
|
45483
45891
|
if (crypto3 && body.notes)
|
|
45484
45892
|
body.notes = await encryptField(body.notes, crypto3.key);
|
|
45893
|
+
if (body.categorySlug) {
|
|
45894
|
+
const cats = await client.get("/api/categories");
|
|
45895
|
+
const allCats = cats.data.flatMap((c2) => [c2, ...c2.children ?? []]);
|
|
45896
|
+
const cat = allCats.find((c2) => c2.slug === body.categorySlug);
|
|
45897
|
+
if (cat) {
|
|
45898
|
+
body.categoryId = cat.id;
|
|
45899
|
+
body.categoryType = cat.type;
|
|
45900
|
+
}
|
|
45901
|
+
delete body.categorySlug;
|
|
45902
|
+
}
|
|
45485
45903
|
const data = await client.patch(`/api/transactions/${id}`, body);
|
|
45486
45904
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
45487
45905
|
});
|
|
@@ -45607,6 +46025,49 @@ function registerTransactionTools(server, client, crypto3) {
|
|
|
45607
46025
|
]
|
|
45608
46026
|
};
|
|
45609
46027
|
});
|
|
46028
|
+
server.registerTool("bulk_categorize_transactions", {
|
|
46029
|
+
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.",
|
|
46030
|
+
inputSchema: {
|
|
46031
|
+
links: exports_external.array(exports_external.object({
|
|
46032
|
+
transactionId: exports_external.string().describe("Transaction UUID"),
|
|
46033
|
+
categoryId: exports_external.string().describe("Category UUID")
|
|
46034
|
+
})).min(1).max(500)
|
|
46035
|
+
}
|
|
46036
|
+
}, async ({ links }) => {
|
|
46037
|
+
const catsRes = await client.get("/api/categories");
|
|
46038
|
+
const allCats = catsRes.data.flatMap((c2) => [c2, ...c2.children ?? []]);
|
|
46039
|
+
const typeById = new Map(allCats.map((c2) => [c2.id, c2.type]));
|
|
46040
|
+
const groups = new Map;
|
|
46041
|
+
for (const { transactionId, categoryId } of links) {
|
|
46042
|
+
const arr = groups.get(categoryId) ?? [];
|
|
46043
|
+
arr.push(transactionId);
|
|
46044
|
+
groups.set(categoryId, arr);
|
|
46045
|
+
}
|
|
46046
|
+
const CHUNK = 200;
|
|
46047
|
+
let succeeded = 0;
|
|
46048
|
+
const failed = [];
|
|
46049
|
+
await Promise.allSettled([...groups.entries()].flatMap(([categoryId, ids]) => {
|
|
46050
|
+
const categoryType = typeById.get(categoryId);
|
|
46051
|
+
const chunks = [];
|
|
46052
|
+
for (let i2 = 0;i2 < ids.length; i2 += CHUNK)
|
|
46053
|
+
chunks.push(ids.slice(i2, i2 + CHUNK));
|
|
46054
|
+
return chunks.map((chunk) => client.patch("/api/transactions/bulk", {
|
|
46055
|
+
ids: chunk,
|
|
46056
|
+
categoryId,
|
|
46057
|
+
...categoryType ? { categoryType } : {}
|
|
46058
|
+
}).then((r2) => {
|
|
46059
|
+
succeeded += r2.updated ?? chunk.length;
|
|
46060
|
+
}).catch((e2) => failed.push({ categoryId, error: String(e2) })));
|
|
46061
|
+
}));
|
|
46062
|
+
return {
|
|
46063
|
+
content: [
|
|
46064
|
+
{
|
|
46065
|
+
type: "text",
|
|
46066
|
+
text: JSON.stringify({ succeeded, failedCount: failed.length, failed })
|
|
46067
|
+
}
|
|
46068
|
+
]
|
|
46069
|
+
};
|
|
46070
|
+
});
|
|
45610
46071
|
server.registerTool("delete_transactions", {
|
|
45611
46072
|
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
46073
|
inputSchema: {
|
|
@@ -45696,7 +46157,7 @@ async function main() {
|
|
|
45696
46157
|
console.error("Error: PAISA_USER_ID is required when PAISA_PIN is set.");
|
|
45697
46158
|
process.exit(1);
|
|
45698
46159
|
}
|
|
45699
|
-
const cryptoMeta = await client.get("/auth-crypto").catch(() => null);
|
|
46160
|
+
const cryptoMeta = await client.get("/api/auth-crypto").catch(() => null);
|
|
45700
46161
|
if (cryptoMeta && (cryptoMeta.kdfVersion ?? 1) >= 2 && cryptoMeta.kdfSalt && cryptoMeta.wrappedDek) {
|
|
45701
46162
|
const kek = await deriveKEK(pin, cryptoMeta.kdfSalt);
|
|
45702
46163
|
const dek = await unwrapDEK(cryptoMeta.wrappedDek, kek);
|
|
@@ -45746,5 +46207,5 @@ main().catch((err) => {
|
|
|
45746
46207
|
process.exit(1);
|
|
45747
46208
|
});
|
|
45748
46209
|
|
|
45749
|
-
//# debugId=
|
|
46210
|
+
//# debugId=52C61062ED34DC2364756E2164756E21
|
|
45750
46211
|
//# sourceMappingURL=index.js.map
|