@perrylink/dsh-bankrec 0.1.0-alpha.2 → 0.1.0-alpha.3

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.
@@ -1,6 +1,7 @@
1
1
  import type { TxnRecord } from '../plugin/storage.js';
2
2
  export declare function parseCamt(text: string, accountOverride?: string, evidenceSha?: string): TxnRecord[];
3
3
  export interface CamtBalance {
4
+ account: string;
4
5
  type: string;
5
6
  amount_cents: number;
6
7
  date: string;
package/lib/core/camt.js CHANGED
@@ -53,13 +53,14 @@ export function parseCamtBalances(text) {
53
53
  const statements = asArray(asObject(root?.Document)?.BkToCstmrStmt?.Stmt);
54
54
  const balances = [];
55
55
  for (const stmt of statements) {
56
+ const account = scalar(stmt?.Acct?.Id?.IBAN) || scalar(stmt?.Acct?.Id?.Othr?.Id);
56
57
  for (const bal of asArray(stmt?.Bal)) {
57
58
  const type = scalar(bal?.Tp?.CdOrPrtry?.Cd);
58
59
  const raw = scalar(bal?.Amt);
59
60
  if (!type || !raw)
60
61
  continue;
61
62
  const sign = scalar(bal?.CdtDbtInd).toUpperCase() === 'DBIT' ? -1 : 1;
62
- balances.push({ type, amount_cents: sign * cents(raw), date: scalar(bal?.Dt?.Dt) || scalar(bal?.Dt?.DtTm).slice(0, 10) });
63
+ balances.push({ account, type, amount_cents: sign * cents(raw), date: scalar(bal?.Dt?.Dt) || scalar(bal?.Dt?.DtTm).slice(0, 10) });
63
64
  }
64
65
  }
65
66
  return balances;
@@ -25,7 +25,8 @@ export function createTools(deps) {
25
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
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))] }; } });
27
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 }; } });
28
- 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 } }, 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 balances = parseCamtBalances(a.camt_xml); const rows = parseCamt(a.camt_xml); const open = balances.find(b => /^OPBD|^PRCD/.test(b.type)); const close = balances.find(b => /^CLBD|^CLAV/.test(b.type)); if (!open || !close)
28
+ 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
+ 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)
29
30
  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 }; } });
30
31
  return [ingest, camt, match, reconcile, exportTool];
31
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@perrylink/dsh-bankrec",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.3",
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",