@perrylink/dsh-bankrec 0.1.0-alpha.3 → 0.1.0-alpha.4
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/mt940.d.ts +7 -0
- package/lib/core/mt940.js +55 -0
- package/lib/plugin/tools.js +4 -1
- package/package.json +1 -1
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.ingest_camt` (CAMT.053), `bankrec.reconcile_camt`, `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.ingest_mt940`, `bankrec.reconcile_camt`, `bankrec.match`, `bankrec.export` (Bank/GL/Matches xlsx). Env `DSH_BANKREC_HOME`. Apache-2.0.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TxnRecord } from '../plugin/storage.js';
|
|
2
|
+
export declare function parseMt940(text: string, accountOverride?: string, evidenceSha?: string): TxnRecord[];
|
|
3
|
+
export interface Mt940Balance {
|
|
4
|
+
opening_cents: number;
|
|
5
|
+
closing_cents: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function parseMt940Balances(text: string): Mt940Balance;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { BankrecError, cents } from './bankrec.js';
|
|
3
|
+
function dateOf(yymmdd) {
|
|
4
|
+
if (!/^\d{6}$/.test(yymmdd))
|
|
5
|
+
throw new BankrecError('E_BR_CSV_INVALID', `MT940 bad date: ${yymmdd}`);
|
|
6
|
+
return `20${yymmdd.slice(0, 2)}-${yymmdd.slice(2, 4)}-${yymmdd.slice(4, 6)}`;
|
|
7
|
+
}
|
|
8
|
+
export function parseMt940(text, accountOverride, evidenceSha) {
|
|
9
|
+
const lines = text.replace(/\r/g, '').split('\n');
|
|
10
|
+
const account = accountOverride?.trim() || lines.find((line) => line.startsWith(':25:'))?.slice(4).trim() || '';
|
|
11
|
+
if (!account)
|
|
12
|
+
throw new BankrecError('E_BR_CSV_INVALID', 'MT940 has no :25: account');
|
|
13
|
+
const evidence = evidenceSha ?? createHash('sha256').update(text).digest('hex');
|
|
14
|
+
const out = [];
|
|
15
|
+
let pending;
|
|
16
|
+
for (const line of lines) {
|
|
17
|
+
if (line.startsWith(':61:')) {
|
|
18
|
+
const body = line.slice(4);
|
|
19
|
+
const match = body.match(/^(\d{6})(?:\d{4})?([CD]R?)([\d.,]+)([A-Z].*)$/);
|
|
20
|
+
if (!match)
|
|
21
|
+
throw new BankrecError('E_BR_CSV_INVALID', `MT940 bad :61: ${line}`);
|
|
22
|
+
const sign = match[2].startsWith('D') ? -1 : 1;
|
|
23
|
+
const amount = sign * cents(match[3].replace(/\./g, '').replace(',', '.'));
|
|
24
|
+
const tail = match[4];
|
|
25
|
+
const refMatch = tail.match(/\/\/(.+)$/);
|
|
26
|
+
const reference = refMatch?.[1] ?? tail.replace(/^[A-Z]{4}/, '').slice(0, 80) ?? 'mt940';
|
|
27
|
+
pending = { date: dateOf(match[1]), amount, reference };
|
|
28
|
+
out.push({ txn_id: `mt940-${account}-${match[1]}-${out.length}`, account_id: account, period: pending.date.slice(0, 7), side: 'bank', date: pending.date, amount_cents: amount, reference, evidence_sha256: evidence, created_at: new Date().toISOString() });
|
|
29
|
+
}
|
|
30
|
+
else if (line.startsWith(':86:') && pending) {
|
|
31
|
+
const description = line.slice(4).trim();
|
|
32
|
+
if (description) {
|
|
33
|
+
const last = out[out.length - 1];
|
|
34
|
+
if (last)
|
|
35
|
+
last.reference = `${last.reference} ${description}`.trim().slice(0, 200);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (out.length === 0)
|
|
40
|
+
throw new BankrecError('E_BR_CSV_INVALID', 'MT940 has no :61: entries');
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
export function parseMt940Balances(text) {
|
|
44
|
+
const line60 = text.split(/\r?\n/).find((line) => line.startsWith(':60F:'));
|
|
45
|
+
const line62 = text.split(/\r?\n/).find((line) => line.startsWith(':62F:'));
|
|
46
|
+
const parse = (line) => {
|
|
47
|
+
if (!line)
|
|
48
|
+
throw new BankrecError('E_BR_CSV_INVALID', 'MT940 lacks :60F:/:62F:');
|
|
49
|
+
const m = line.match(/^:[0-9]{2}F:([CD])(\d{6})[A-Z]{3}([\d.,]+)$/);
|
|
50
|
+
if (!m)
|
|
51
|
+
throw new BankrecError('E_BR_CSV_INVALID', `bad MT940 balance: ${line}`);
|
|
52
|
+
return (m[1] === 'D' ? -1 : 1) * cents(m[3].replace(/\./g, '').replace(',', '.'));
|
|
53
|
+
};
|
|
54
|
+
return { opening_cents: parse(line60), closing_cents: parse(line62) };
|
|
55
|
+
}
|
package/lib/plugin/tools.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as XLSX from 'xlsx';
|
|
|
5
5
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
6
6
|
import { BankrecError, parseCsv } from '../core/bankrec.js';
|
|
7
7
|
import { parseCamt, parseCamtBalances } from '../core/camt.js';
|
|
8
|
+
import { parseMt940 } from '../core/mt940.js';
|
|
8
9
|
const text = (t) => [{ type: 'text', text: t }];
|
|
9
10
|
const sha = (v) => createHash('sha256').update(v).digest('hex');
|
|
10
11
|
export function createTools(deps) {
|
|
@@ -25,8 +26,10 @@ export function createTools(deps) {
|
|
|
25
26
|
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
27
|
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))] }; } });
|
|
27
28
|
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 }; } });
|
|
29
|
+
const mt940 = defineTool({ name: 'bankrec.ingest_mt940', description: 'Ingest MT940 statement entries; local R1 write.', parameters: { mt940_text: { type: 'string', required: true }, account_id: { type: 'string' } }, output: { schema: { type: 'object', additionalProperties: false, properties: { rows: { type: 'number', required: true }, account: { type: 'string', required: true } } }, render: (_a, v) => text(`MT940 rows=${v.rows}`) }, async execute(a) { const rows = parseMt940(a.mt940_text, a.account_id); for (const t of rows)
|
|
30
|
+
await deps.transactions.put(t.txn_id, t); return { rows: rows.length, account: rows[0].account_id }; } });
|
|
28
31
|
const reconcile = defineTool({ name: 'bankrec.reconcile_camt', description: 'Check CAMT.053 opening/closing balance continuity against the sum of entries; read-only.', parameters: { camt_xml: { type: 'string', required: true }, account_id: { type: 'string' } }, output: { schema: { type: 'object', additionalProperties: false, properties: { opening_cents: { type: 'number', required: true }, closing_cents: { type: 'number', required: true }, net_cents: { type: 'number', required: true }, difference_cents: { type: 'number', required: true }, balanced: { type: 'boolean', required: true } } }, render: (_a, v) => text(`CAMT balance ${v.balanced ? 'ok' : 'DIFF'} (${v.difference_cents})`) }, async execute(a) { const allBalances = parseCamtBalances(a.camt_xml); const accounts = [...new Set(allBalances.map(b => b.account))]; const account = a.account_id ?? (accounts.length === 1 ? accounts[0] : undefined); if (!account)
|
|
29
32
|
throw new BankrecError('E_BR_CSV_INVALID', 'multiple accounts; account_id required'); const balances = allBalances.filter(b => b.account === account); const rows = parseCamt(a.camt_xml).filter(r => r.account_id === account); const open = balances.find(b => /^OPBD|^PRCD/.test(b.type)); const close = balances.find(b => /^CLBD|^CLAV/.test(b.type)); if (!open || !close)
|
|
30
33
|
throw new BankrecError('E_BR_CSV_INVALID', 'CAMT lacks opening/closing balance'); const net = rows.reduce((n, r) => n + r.amount_cents, 0); const diff = open.amount_cents + net - close.amount_cents; return { opening_cents: open.amount_cents, closing_cents: close.amount_cents, net_cents: net, difference_cents: diff, balanced: diff === 0 }; } });
|
|
31
|
-
return [ingest, camt, match, reconcile, exportTool];
|
|
34
|
+
return [ingest, camt, mt940, match, reconcile, exportTool];
|
|
32
35
|
}
|
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.4",
|
|
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",
|