@perrylink/dsh-bankrec 0.1.0-alpha.0 → 0.1.0-alpha.1
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/README.md +1 -1
- package/lib/core/camt.d.ts +2 -0
- package/lib/core/camt.js +44 -0
- package/lib/plugin/tools.js +4 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
# @perrylink/dsh-bankrec
|
|
2
2
|
Bank CSV/GL ingest, 1:1 matching and 3-sheet reconciliation workpaper — SKU-06 S1 public/synthetic demo. **Partial, not GA**: 0 real firms; user-approved public/synthetic (2026-10-31).
|
|
3
|
-
Tools: `bankrec.ingest`, `bankrec.match`, `bankrec.export` (Bank/GL/Matches xlsx). Env `DSH_BANKREC_HOME`. Apache-2.0.
|
|
3
|
+
Tools: `bankrec.ingest`, `bankrec.ingest_camt` (CAMT.053), `bankrec.match`, `bankrec.export` (Bank/GL/Matches xlsx). Env `DSH_BANKREC_HOME`. Apache-2.0.
|
package/lib/core/camt.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
3
|
+
import { BankrecError, cents } from './bankrec.js';
|
|
4
|
+
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_', removeNSPrefix: true, parseTagValue: false, trimValues: true, processEntities: false });
|
|
5
|
+
const asArray = (value) => Array.isArray(value) ? value : value === undefined ? [] : [value];
|
|
6
|
+
const asObject = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
7
|
+
function scalar(value) { if (value === null || value === undefined)
|
|
8
|
+
return ''; if (typeof value !== 'object')
|
|
9
|
+
return String(value); const o = value; return String(o['#text'] ?? ''); }
|
|
10
|
+
function dateOf(entry) { const d = scalar(entry.BookgDt?.Dt) || scalar(entry.BookgDt?.DtTm) || scalar(entry.ValDt?.Dt) || scalar(entry.ValDt?.DtTm); return d.slice(0, 10); }
|
|
11
|
+
export function parseCamt(text, accountOverride, evidenceSha) {
|
|
12
|
+
let root;
|
|
13
|
+
try {
|
|
14
|
+
root = parser.parse(text);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new BankrecError('E_BR_CSV_INVALID', 'CAMT XML is not parseable');
|
|
18
|
+
}
|
|
19
|
+
const statements = asArray(asObject(root?.Document)?.BkToCstmrStmt?.Stmt);
|
|
20
|
+
if (statements.length === 0)
|
|
21
|
+
throw new BankrecError('E_BR_CSV_INVALID', 'CAMT has no BkToCstmrStmt/Stmt');
|
|
22
|
+
const evidence = evidenceSha ?? createHash('sha256').update(text).digest('hex');
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const stmt of statements) {
|
|
25
|
+
const iban = scalar(stmt?.Acct?.Id?.IBAN) || scalar(stmt?.Acct?.Id?.Othr?.Id);
|
|
26
|
+
const account = accountOverride?.trim() || iban;
|
|
27
|
+
if (!account)
|
|
28
|
+
throw new BankrecError('E_BR_CSV_INVALID', 'CAMT statement has no account/IBAN');
|
|
29
|
+
for (const [index, entry] of asArray(stmt?.Ntry).entries()) {
|
|
30
|
+
const amount = scalar(entry?.Amt);
|
|
31
|
+
const sign = scalar(entry?.CdtDbtInd).toUpperCase() === 'DBIT' ? -1 : 1;
|
|
32
|
+
const value = sign * cents(amount);
|
|
33
|
+
const date = dateOf(entry);
|
|
34
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date))
|
|
35
|
+
throw new BankrecError('E_BR_CSV_INVALID', `CAMT entry has no booking date (${index})`);
|
|
36
|
+
const details = entry?.NtryDtls?.TxDtls;
|
|
37
|
+
const reference = scalar(entry?.AcctSvcrRef) || scalar(details?.Refs?.TxId) || scalar(details?.Refs?.EndToEndId) || scalar(details?.RmtInf?.Ustrd) || scalar(details?.RmtInf?.Strd?.CdtrRefInf?.Ref) || 'camt';
|
|
38
|
+
out.push({ txn_id: `camt-${account}-${date}-${value}-${index}`, account_id: account, period: date.slice(0, 7), side: 'bank', date, amount_cents: value, reference, evidence_sha256: evidence, created_at: new Date().toISOString() });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (out.length === 0)
|
|
42
|
+
throw new BankrecError('E_BR_CSV_INVALID', 'CAMT has no entries');
|
|
43
|
+
return out;
|
|
44
|
+
}
|
package/lib/plugin/tools.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import * as XLSX from 'xlsx';
|
|
5
5
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
6
6
|
import { BankrecError, parseCsv } from '../core/bankrec.js';
|
|
7
|
+
import { parseCamt } from '../core/camt.js';
|
|
7
8
|
const text = (t) => [{ type: 'text', text: t }];
|
|
8
9
|
const sha = (v) => createHash('sha256').update(v).digest('hex');
|
|
9
10
|
export function createTools(deps) {
|
|
@@ -21,6 +22,8 @@ export function createTools(deps) {
|
|
|
21
22
|
}
|
|
22
23
|
} return { matched, unmatched_bank: bank.filter(b => !usedBank.has(b.txn_id)).map(b => b.txn_id), unmatched_gl: gl.filter(g => !used.has(g.txn_id)).map(g => g.txn_id) }; } });
|
|
23
24
|
// fix unmatched_bank below via replacement later
|
|
25
|
+
const camt = defineTool({ name: 'bankrec.ingest_camt', description: 'Ingest CAMT.053 XML bank statement entries; supports DBIT/CRDT and stores bank rows. Local R1 write.', parameters: { camt_xml: { type: 'string', required: true }, account_id: { type: 'string' } }, output: { schema: { type: 'object', additionalProperties: false, properties: { rows: { type: 'number', required: true }, accounts: { type: 'array', items: { type: 'string' }, required: true }, periods: { type: 'array', items: { type: 'string' }, required: true } } }, render: (_a, v) => text(`CAMT rows=${v.rows}`) }, async execute(a) { const rows = parseCamt(a.camt_xml, a.account_id); for (const t of rows)
|
|
26
|
+
await deps.transactions.put(t.txn_id, t); return { rows: rows.length, accounts: [...new Set(rows.map(r => r.account_id))], periods: [...new Set(rows.map(r => r.period))] }; } });
|
|
24
27
|
const exportTool = defineTool({ name: 'bankrec.export', description: 'Export 3-sheet reconciliation xlsx (Bank, GL, Matches). Local R1 write.', parameters: { account_id: { type: 'string', required: true }, period: { type: 'string', required: true } }, output: { schema: { type: 'object', additionalProperties: false, properties: { path: { type: 'string', required: true }, sha256: { type: 'string', required: true }, sheets: { type: 'number', required: true } } }, render: (_a, v) => text(`export ${v.sheets} sheets`) }, async execute(args) { const all = [...deps.transactions.entries()].map(([, v]) => v).filter(t => t.account_id === args.account_id && t.period === args.period); const ms = [...deps.matches.entries()].map(([, v]) => v); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(all.filter(t => t.side === 'bank').map(t => ({ date: t.date, amount_cents: t.amount_cents, reference: t.reference, txn_id: t.txn_id }))), 'Bank'); XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(all.filter(t => t.side === 'gl').map(t => ({ date: t.date, amount_cents: t.amount_cents, reference: t.reference, txn_id: t.txn_id }))), 'GL'); XLSX.utils.book_append_sheet(wb, XLSX.utils.json_to_sheet(ms), 'Matches'); const dir = path.join(deps.baseDir, 'exports'); await mkdir(dir, { recursive: true }); const p = path.join(dir, `${args.account_id}-${args.period}.xlsx`); const b = Buffer.from(XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' })); await writeFile(p, b); return { path: p, sha256: sha(b), sheets: wb.SheetNames.length }; } });
|
|
25
|
-
return [ingest, match, exportTool];
|
|
28
|
+
return [ingest, camt, match, exportTool];
|
|
26
29
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@perrylink/dsh-bankrec",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
4
|
"description": "Bank statement ingest, 1:1 matching and reconciliation workpaper (SKU-06 S1 public/synthetic demo).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/plugin/index.js",
|
|
@@ -61,9 +61,10 @@
|
|
|
61
61
|
"@deepseek-ai/dsh-tools": ">=0.2.1-rc.3 <0.3.0"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
+
"fast-xml-parser": "5.11.2",
|
|
64
65
|
"fflate": "0.8.3",
|
|
65
|
-
"
|
|
66
|
-
"
|
|
66
|
+
"xlsx": "0.18.5",
|
|
67
|
+
"zod": "4.4.3"
|
|
67
68
|
},
|
|
68
69
|
"devDependencies": {
|
|
69
70
|
"@deepseek-ai/cordis": "4.0.5",
|