jaz-clio 4.20.0 → 4.21.2
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 +10 -13
- package/assets/skills/api/SKILL.md +8 -7
- package/assets/skills/api/references/errors.md +8 -9
- package/assets/skills/api/references/field-map.md +13 -5
- package/assets/skills/conversion/SKILL.md +1 -1
- package/assets/skills/jobs/SKILL.md +1 -1
- package/assets/skills/transaction-recipes/SKILL.md +1 -1
- package/dist/commands/attachments.js +47 -6
- package/dist/commands/bank.js +63 -19
- package/dist/commands/exports.js +1 -1
- package/dist/commands/schedulers.js +4 -0
- package/dist/core/api/attachments.js +2 -2
- package/dist/core/api/bank.js +6 -0
- package/dist/core/jobs/bank-recon/tools/match/match.js +86 -63
- package/dist/core/registry/pagination.js +33 -0
- package/dist/core/registry/tools.js +68 -11
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -128,7 +128,7 @@ Every command supports `--json` for structured output — ideal for piping to ot
|
|
|
128
128
|
|
|
129
129
|
## MCP Server
|
|
130
130
|
|
|
131
|
-
Expose all
|
|
131
|
+
Expose all 146 CLI tools to AI coding assistants via the [Model Context Protocol](https://modelcontextprotocol.io). The server runs locally on your machine — no cloud, no ports. API calls go directly from your machine to the Jaz API.
|
|
132
132
|
|
|
133
133
|
**Claude Code:**
|
|
134
134
|
|
|
@@ -167,20 +167,17 @@ clio init --platform claude # Force a specific platform
|
|
|
167
167
|
|
|
168
168
|
See the [full README](https://github.com/teamtinvio/jaz-ai) for skill details, reference file catalogs, and plugin installation.
|
|
169
169
|
|
|
170
|
-
##
|
|
170
|
+
## Privacy & Security
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
Jaz AI runs entirely on your machine. API calls go directly from your machine to the Jaz API over HTTPS. This tool does not include telemetry or collect data. Your API key is stored locally in `~/.config/jaz-clio/credentials.json`.
|
|
173
173
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
| Payments | `[{ ... }]` | `{ payments: [{ ... }] }` (wrapped) |
|
|
182
|
-
| CN Refunds | `{ payments: [{ paymentAmount }] }` | `{ refunds: [{ refundAmount }] }` |
|
|
183
|
-
| Apply credits | `{ amount: 100 }` | `{ credits: [{ creditNoteResourceId, amountApplied }] }` |
|
|
174
|
+
See our full privacy policy: [jaz.ai/legal](https://jaz.ai/legal)
|
|
175
|
+
|
|
176
|
+
## Support
|
|
177
|
+
|
|
178
|
+
- Help center: [help.jaz.ai](https://help.jaz.ai)
|
|
179
|
+
- GitHub Issues: [github.com/teamtinvio/jaz-ai/issues](https://github.com/teamtinvio/jaz-ai/issues)
|
|
180
|
+
- Email: api-support@jaz.ai
|
|
184
181
|
|
|
185
182
|
## License
|
|
186
183
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: jaz-api
|
|
3
|
-
version: 4.
|
|
3
|
+
version: 4.21.2
|
|
4
4
|
description: Complete reference for the Jaz REST API — the accounting platform backend. Use this skill whenever building, modifying, debugging, or extending any code that calls the API — including API clients, integrations, data seeding, test data, or new endpoint work. Contains every field name, response shape, error, gotcha, and edge case discovered through live production testing.
|
|
5
5
|
license: MIT
|
|
6
6
|
compatibility: Requires Jaz API key (x-jk-api-key header). Works with Claude Code, Google Antigravity, OpenAI Codex, GitHub Copilot, Cursor, and any agent that reads markdown.
|
|
@@ -77,8 +77,8 @@ You are working with the **Jaz REST API** — the accounting platform backend. A
|
|
|
77
77
|
31. **Cash transfers use `cashOut`/`cashIn` sub-objects** — NOT flat `fromAccountResourceId`/`toAccountResourceId`. Each: `{ accountResourceId, amount }`.
|
|
78
78
|
|
|
79
79
|
### Schedulers
|
|
80
|
-
32. **Scheduled invoices/bills wrap in `{ invoice: {...} }` or `{ bill: {...} }`** — not flat. Recurrence field is `repeat` (NOT `frequency`/`interval`). `saveAsDraft: false` required.
|
|
81
|
-
33. **Scheduled journals use FLAT structure** with `schedulerEntries` — not nested in `journal` wrapper.
|
|
80
|
+
32. **Scheduled invoices/bills wrap in `{ invoice: {...} }` or `{ bill: {...} }`** — not flat. Recurrence field is `repeat` (NOT `frequency`/`interval`). `saveAsDraft: false` required. **`reference` is required** inside the `invoice`/`bill` wrapper — omitting it causes 422.
|
|
81
|
+
33. **Scheduled journals use FLAT structure** with `schedulerEntries` — not nested in `journal` wrapper. **`valueDate` is required** at the top level (alongside `startDate`, `repeat`, etc.).
|
|
82
82
|
|
|
83
83
|
### Bookmarks
|
|
84
84
|
34. **Bookmarks use `items` array wrapper** with `name`, `value`, `categoryCode`, `datatypeCode`.
|
|
@@ -113,11 +113,11 @@ You are working with the **Jaz REST API** — the accounting platform backend. A
|
|
|
113
113
|
41. **Invoice GET uses `organizationAccountResourceId`** for line item accounts — POST uses `accountResourceId`. Request-side aliases resolve `issueDate` → `valueDate`, `bankAccountResourceId` → `accountResourceId`, etc.
|
|
114
114
|
42. **Scheduler GET returns `interval`** — POST uses `repeat`. (Response-side asymmetry remains.)
|
|
115
115
|
43. **Search sort is an object** — `{ sort: { sortBy: ["valueDate"], order: "DESC" } }`. Required when `offset` is present (even `offset: 0`).
|
|
116
|
-
44. **Bank records
|
|
116
|
+
44. **Bank records** — **Create**: Multipart CSV/OFX via `POST /magic/importBankStatementFromAttachment` or JSON via `POST /bank-records/:accountResourceId` with `{ records: [{amount, transactionDate, description?, payerOrPayee?, reference?}] }` (positive = cash-in, negative = cash-out, response: `{data: {errors: []}}`). **Search**: `POST /bank-records/:accountResourceId/search` — filter fields: `valueDate` (DateExpression), `status` (StringExpression: UNRECONCILED, RECONCILED, ARCHIVED, POSSIBLE_DUPLICATE), `description`, `extContactName` (payer/payee), `extReference`, `netAmount` (BigDecimalExpression), `extAccountNumber`. Sort by `valueDate` DESC default.
|
|
117
117
|
45. **Withholding tax** on bills/supplier CNs only. Retry pattern: if `WITHHOLDING_CODE_NOT_FOUND`, strip field and retry.
|
|
118
118
|
46. **Known API bugs (500s)**: Contact groups PUT, custom fields PUT, capsules POST, catalogs POST, inventory balances GET — all return 500.
|
|
119
119
|
47. **Non-existent endpoints**: `POST /deposits`, `POST /inventory/adjustments`, `GET /payments` (list), and `POST /payments/search` return 404 — these endpoints are not implemented. To list/search payments, use `POST /cashflow-transactions/search` (the unified transaction ledger — see Rule 63).
|
|
120
|
-
48. **Attachments require
|
|
120
|
+
48. **Attachments require multipart `file` field**: `POST /:type/:id/attachments` expects a binary file in the `file` form-data field — NOT a `sourceUrl` text field. Rejects `text/plain`. Use `application/pdf` or `image/png`. CLI: `clio attachments add --file <path>` (local file) or `--url <url>` (downloads first, then uploads as `file`).
|
|
121
121
|
49. **Currency rate direction: `rate` = functionalToSource (1 base = X foreign)** — POST `rate: 0.74` for a SGD org means 1 SGD = 0.74 USD. **If your data stores rates as "1 USD = 1.35 SGD" (sourceToFunctional), you MUST invert: `rate = 1 / 1.35 = 0.74`.** GET confirms both: `rateFunctionalToSource` (what you POSTed) and `rateSourceToFunctional` (the inverse).
|
|
122
122
|
|
|
123
123
|
### Search & Filter
|
|
@@ -269,6 +269,7 @@ The backend DX overhaul is live. Key improvements now available:
|
|
|
269
269
|
- **Field names**: All request bodies use camelCase
|
|
270
270
|
- **Date serialization**: Python `date` type → `YYYY-MM-DD` strings
|
|
271
271
|
- **Bill payments**: Embed in bill creation body (safest). Standalone `POST /bills/{id}/payments` also works.
|
|
272
|
-
- **Bank records**:
|
|
273
|
-
- **Scheduled bills**: Wrap as `{ status, startDate, endDate, repeat, bill: {
|
|
272
|
+
- **Bank records**: Create via JSON `POST /bank-records/:id` or multipart `POST /magic/importBankStatementFromAttachment`. Search via `POST /bank-records/:id/search` with filters (valueDate, status, description, extContactName, netAmount, extReference).
|
|
273
|
+
- **Scheduled invoices/bills**: Wrap as `{ status, startDate, endDate, repeat, invoice/bill: { reference, valueDate, dueDate, contactResourceId, lineItems, saveAsDraft: false } }`. `reference` is required.
|
|
274
|
+
- **Scheduled journals**: Flat: `{ status, startDate, endDate, repeat, valueDate, schedulerEntries, reference }`. `valueDate` is required.
|
|
274
275
|
- **FX currency (invoices, bills, credit notes, AND journals)**: `currency: { sourceCurrency: "USD" }` (auto-fetches platform rate) or `currency: { sourceCurrency: "USD", exchangeRate: 1.35 }` (custom rate). Same object form on all transaction types. **Never use `currencyCode` string** — silently ignored.
|
|
@@ -322,10 +322,14 @@ if (acct.code) ctx.coaIds[acct.code] = acct.resourceId;
|
|
|
322
322
|
|
|
323
323
|
## Bank Record Errors
|
|
324
324
|
|
|
325
|
-
###
|
|
326
|
-
There is no JSON POST endpoint for creating bank records. Use multipart import:
|
|
325
|
+
### Bank record creation
|
|
327
326
|
|
|
328
|
-
|
|
327
|
+
Two methods for creating bank records:
|
|
328
|
+
|
|
329
|
+
**Method 1: JSON POST** (programmatic)
|
|
330
|
+
`POST /api/v1/bank-records/:accountResourceId` with `{ records: [{amount, transactionDate, description?, payerOrPayee?, reference?}] }`. Returns `{ data: { errors: [] } }` on success. 1-100 records per call.
|
|
331
|
+
|
|
332
|
+
**Method 2: Multipart import** (file upload)
|
|
329
333
|
```
|
|
330
334
|
POST /api/v1/magic/importBankStatementFromAttachment
|
|
331
335
|
Content-Type: multipart/form-data
|
|
@@ -337,12 +341,7 @@ Fields:
|
|
|
337
341
|
- sourceType: "FILE" (valid values: URL, FILE)
|
|
338
342
|
```
|
|
339
343
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
**Legacy guidance** (still valid for general bank record handling):
|
|
343
|
-
1. Always use `Math.abs()` on amounts — amounts must be positive
|
|
344
|
-
2. Use `type: "CREDIT"` or `type: "DEBIT"` to indicate direction
|
|
345
|
-
3. Check that `bankAccountResourceId` is a valid CoA entry with `accountType: "Bank Accounts"`
|
|
344
|
+
**Amount rules**: For JSON POST, positive = cash-in, negative = cash-out. Dates in YYYY-MM-DD format. `accountResourceId` must be a bank-type CoA account.
|
|
346
345
|
|
|
347
346
|
---
|
|
348
347
|
|
|
@@ -326,12 +326,20 @@ DELETE → expects "A" (parentEntityResourceId, via /cashflow-journals/:id)
|
|
|
326
326
|
|
|
327
327
|
| What You'd Guess | Actual API Field | Notes |
|
|
328
328
|
|------------------|-------------------|-------|
|
|
329
|
-
| `date` | `transactionDate` | ISO date string |
|
|
330
|
-
| `
|
|
329
|
+
| `date` (create) | `transactionDate` | ISO date string (YYYY-MM-DD) |
|
|
330
|
+
| `date` (search response) | `valueDate` | Returned as ISO 8601 UTC |
|
|
331
|
+
| `amount` (create) | `amount` | Positive = cash-in, negative = cash-out |
|
|
332
|
+
| `amount` (search response) | `netAmount` | Same sign convention |
|
|
333
|
+
| `payer` / `payee` (create) | `payerOrPayee` | Free text |
|
|
334
|
+
| `payer` / `payee` (search) | `extContactName` | Search filter field |
|
|
335
|
+
| `reference` (search) | `extReference` | Search filter field |
|
|
331
336
|
| `memo` | `description` | Free text |
|
|
332
|
-
|
|
|
337
|
+
| `source` | `bankStatementEntrySource` | `FILE_IMPORT` or `API` (read-only) |
|
|
338
|
+
| `status` | `status` | `UNRECONCILED`, `RECONCILED`, `ARCHIVED`, `POSSIBLE_DUPLICATE` |
|
|
333
339
|
|
|
334
|
-
**Creating bank records**:
|
|
340
|
+
**Creating bank records**: Two methods. JSON POST via `POST /bank-records/:accountResourceId` with `{ records: [{amount, transactionDate, ...}] }` (1-100 records). Multipart via `POST /magic/importBankStatementFromAttachment` for CSV/OFX file uploads.
|
|
341
|
+
|
|
342
|
+
**Searching bank records**: `POST /bank-records/:accountResourceId/search` with filter fields: `valueDate` (DateExpression), `status` (StringExpression), `description` (StringExpression), `extContactName` (StringExpression), `extReference` (StringExpression), `netAmount` (BigDecimalExpression), `extAccountNumber` (StringExpression). Sort by `valueDate` DESC default.
|
|
335
343
|
|
|
336
344
|
---
|
|
337
345
|
|
|
@@ -510,7 +518,7 @@ Battle-tested patterns from production Jaz API clients:
|
|
|
510
518
|
| Alias generator | `alias_generator=to_camel` (snake_case to camelCase) |
|
|
511
519
|
| Date type | Python `date` type serializes to `YYYY-MM-DD` strings |
|
|
512
520
|
| Bill payments | Always embedded in bill creation body, never standalone |
|
|
513
|
-
| Bank records |
|
|
521
|
+
| Bank records | JSON POST `/bank-records/:id` or multipart `/magic/importBankStatementFromAttachment`. Search via `/bank-records/:id/search`. |
|
|
514
522
|
| Scheduled bills | Wrapped as `{ repeat, startDate, endDate, bill: {...} }`. Field is `repeat` (NOT `frequency`/`interval`) |
|
|
515
523
|
| FX currency | MUST use `currency` OBJECT on ALL transaction types (invoices, bills, credit notes, journals): `{ sourceCurrency: "USD" }` (auto platform rate) or `{ sourceCurrency: "USD", exchangeRate: 1.35 }` (custom). String `currencyCode` silently ignored. |
|
|
516
524
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: jaz-conversion
|
|
3
|
-
version: 4.
|
|
3
|
+
version: 4.21.2
|
|
4
4
|
description: Accounting data conversion skill — migrates customer data from Xero, QuickBooks, Sage, MYOB, and Excel exports to Jaz. Covers config, quick, and full conversion workflows, Excel parsing, CoA/contact/tax/items mapping, clearing accounts, TTB, and TB verification.
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: jaz-jobs
|
|
3
|
-
version: 4.
|
|
3
|
+
version: 4.21.2
|
|
4
4
|
description: 12 accounting jobs for SMB bookkeepers and accountants — month-end, quarter-end, and year-end close playbooks plus 9 ad-hoc operational jobs (bank recon, document collection, GST/VAT filing, payment runs, credit control, supplier recon, audit prep, fixed asset review, statutory filing). Jobs can have paired tools as nested subcommands (e.g., `clio jobs bank-recon match`, `clio jobs document-collection ingest`, `clio jobs statutory-filing sg-cs`). Paired with an interactive CLI blueprint generator (clio jobs).
|
|
5
5
|
license: MIT
|
|
6
6
|
compatibility: Works with Claude Code, Claude Cowork, Claude.ai, and any agent that reads markdown. For API payloads, load the jaz-api skill. For individual transaction patterns, load the jaz-recipes skill.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: jaz-recipes
|
|
3
|
-
version: 4.
|
|
3
|
+
version: 4.21.2
|
|
4
4
|
description: 16 IFRS-compliant recipes for complex multi-step accounting in Jaz — prepaid amortization, deferred revenue, loan schedules, IFRS 16 leases, hire purchase, fixed deposits, asset disposal, FX revaluation, ECL provisioning, IAS 37 provisions, dividends, intercompany, and capital WIP. Each recipe includes journal entries, capsule structure, and verification steps. Paired with 13 financial calculators that produce execution-ready blueprints with workings.
|
|
5
5
|
license: MIT
|
|
6
6
|
compatibility: Works with Claude Code, Claude Cowork, Claude.ai, and any agent that reads markdown. For API payloads, load the jaz-api skill alongside this one.
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { basename, extname, resolve } from 'node:path';
|
|
2
4
|
import { listAttachments, addAttachment, fetchAttachmentTable } from '../core/api/attachments.js';
|
|
3
5
|
import { apiAction } from './api-action.js';
|
|
4
6
|
const BT_TYPES = ['invoices', 'bills', 'journals', 'scheduled_journals', 'customer-credit-notes', 'supplier-credit-notes'];
|
|
7
|
+
const ATTACHMENT_MIME_MAP = {
|
|
8
|
+
'.pdf': 'application/pdf',
|
|
9
|
+
'.png': 'image/png',
|
|
10
|
+
'.jpg': 'image/jpeg',
|
|
11
|
+
'.jpeg': 'image/jpeg',
|
|
12
|
+
'.gif': 'image/gif',
|
|
13
|
+
'.webp': 'image/webp',
|
|
14
|
+
'.svg': 'image/svg+xml',
|
|
15
|
+
'.csv': 'text/csv',
|
|
16
|
+
'.xls': 'application/vnd.ms-excel',
|
|
17
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
18
|
+
};
|
|
5
19
|
export function registerAttachmentsCommand(program) {
|
|
6
20
|
const attachments = program
|
|
7
21
|
.command('attachments')
|
|
@@ -35,11 +49,12 @@ export function registerAttachmentsCommand(program) {
|
|
|
35
49
|
// ── clio attachments add ───────────────────────────────────────
|
|
36
50
|
attachments
|
|
37
51
|
.command('add')
|
|
38
|
-
.description('Add an attachment to a transaction')
|
|
52
|
+
.description('Add an attachment to a transaction (file upload or URL download)')
|
|
39
53
|
.requiredOption('--type <type>', `Transaction type (${BT_TYPES.join(', ')})`)
|
|
40
54
|
.requiredOption('--id <resourceId>', 'Transaction resourceId')
|
|
41
|
-
.option('--
|
|
42
|
-
.option('--url <url>', '
|
|
55
|
+
.option('--file <path>', 'Local file path (PDF, PNG, JPG, etc.)')
|
|
56
|
+
.option('--url <url>', 'Download file from URL and attach')
|
|
57
|
+
.option('--attachment-id <id>', 'Link existing attachment by ID')
|
|
43
58
|
.option('--api-key <key>', 'API key')
|
|
44
59
|
.option('--json', 'Output as JSON')
|
|
45
60
|
.action(apiAction(async (client, opts) => {
|
|
@@ -48,15 +63,41 @@ export function registerAttachmentsCommand(program) {
|
|
|
48
63
|
console.error(chalk.red(`Invalid type. Use one of: ${BT_TYPES.join(', ')}`));
|
|
49
64
|
process.exit(1);
|
|
50
65
|
}
|
|
51
|
-
if (!opts.attachmentId && !opts.url) {
|
|
52
|
-
console.error(chalk.red('Provide --
|
|
66
|
+
if (!opts.attachmentId && !opts.url && !opts.file) {
|
|
67
|
+
console.error(chalk.red('Provide --file, --url, or --attachment-id'));
|
|
53
68
|
process.exit(1);
|
|
54
69
|
}
|
|
70
|
+
let file;
|
|
71
|
+
let fileName;
|
|
72
|
+
if (opts.file) {
|
|
73
|
+
// Local file upload
|
|
74
|
+
const filePath = resolve(opts.file);
|
|
75
|
+
const ext = extname(filePath).toLowerCase();
|
|
76
|
+
const mime = ATTACHMENT_MIME_MAP[ext] ?? 'application/octet-stream';
|
|
77
|
+
const buffer = readFileSync(filePath);
|
|
78
|
+
file = new Blob([buffer], { type: mime });
|
|
79
|
+
fileName = basename(filePath);
|
|
80
|
+
}
|
|
81
|
+
else if (opts.url) {
|
|
82
|
+
// Download from URL, then upload as file
|
|
83
|
+
const res = await fetch(opts.url);
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
console.error(chalk.red(`Failed to download: ${res.status} ${res.statusText}`));
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
const buffer = await res.arrayBuffer();
|
|
89
|
+
const contentType = res.headers.get('content-type') ?? 'application/octet-stream';
|
|
90
|
+
file = new Blob([buffer], { type: contentType });
|
|
91
|
+
// Extract filename from URL path
|
|
92
|
+
const urlPath = new URL(opts.url).pathname;
|
|
93
|
+
fileName = basename(urlPath) || 'attachment';
|
|
94
|
+
}
|
|
55
95
|
const result = await addAttachment(client, {
|
|
56
96
|
businessTransactionType: btType,
|
|
57
97
|
businessTransactionResourceId: opts.id,
|
|
98
|
+
file,
|
|
99
|
+
fileName,
|
|
58
100
|
attachmentId: opts.attachmentId,
|
|
59
|
-
sourceUrl: opts.url,
|
|
60
101
|
});
|
|
61
102
|
if (opts.json) {
|
|
62
103
|
console.log(JSON.stringify(result, null, 2));
|
package/dist/commands/bank.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { basename, extname, resolve } from 'node:path';
|
|
4
|
-
import { listBankAccounts, getBankAccount, searchBankRecords, importBankStatement, } from '../core/api/bank.js';
|
|
4
|
+
import { listBankAccounts, getBankAccount, searchBankRecords, addBankRecords, importBankStatement, } from '../core/api/bank.js';
|
|
5
|
+
import { buildBankRecordFilter } from '../core/registry/pagination.js';
|
|
5
6
|
import { apiAction } from './api-action.js';
|
|
6
7
|
import { parsePositiveInt } from './parsers.js';
|
|
7
8
|
const BANK_MIME_MAP = {
|
|
@@ -61,27 +62,31 @@ export function registerBankCommand(program) {
|
|
|
61
62
|
.description('Search bank records for a bank account')
|
|
62
63
|
.option('--from <YYYY-MM-DD>', 'Filter from date (inclusive)')
|
|
63
64
|
.option('--to <YYYY-MM-DD>', 'Filter to date (inclusive)')
|
|
64
|
-
.option('--status <status>', 'Filter by status (UNRECONCILED, RECONCILED)')
|
|
65
|
+
.option('--status <status>', 'Filter by status (UNRECONCILED, RECONCILED, ARCHIVED, POSSIBLE_DUPLICATE)')
|
|
66
|
+
.option('--description <text>', 'Filter by description (contains)')
|
|
67
|
+
.option('--payer <name>', 'Filter by payer/payee name (contains)')
|
|
68
|
+
.option('--reference <ref>', 'Filter by reference (contains)')
|
|
69
|
+
.option('--amount-min <n>', 'Filter by minimum amount', parseFloat)
|
|
70
|
+
.option('--amount-max <n>', 'Filter by maximum amount', parseFloat)
|
|
65
71
|
.option('--limit <n>', 'Max results (default 50)', parsePositiveInt)
|
|
66
72
|
.option('--api-key <key>', 'API key (overrides stored/env)')
|
|
67
73
|
.option('--json', 'Output as JSON')
|
|
68
74
|
.action((accountResourceId, opts) => apiAction(async (client) => {
|
|
69
|
-
const filter = {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
75
|
+
const filter = buildBankRecordFilter({
|
|
76
|
+
status: opts.status,
|
|
77
|
+
from: opts.from,
|
|
78
|
+
to: opts.to,
|
|
79
|
+
description: opts.description,
|
|
80
|
+
payer: opts.payer,
|
|
81
|
+
reference: opts.reference,
|
|
82
|
+
amountMin: opts.amountMin,
|
|
83
|
+
amountMax: opts.amountMax,
|
|
84
|
+
});
|
|
80
85
|
const res = await searchBankRecords(client, accountResourceId, {
|
|
81
|
-
filter
|
|
86
|
+
filter,
|
|
82
87
|
limit: opts.limit ?? 50,
|
|
83
88
|
offset: 0,
|
|
84
|
-
sort: { sortBy: ['
|
|
89
|
+
sort: { sortBy: ['valueDate'], order: 'DESC' },
|
|
85
90
|
});
|
|
86
91
|
if (opts.json) {
|
|
87
92
|
console.log(JSON.stringify(res, null, 2));
|
|
@@ -93,10 +98,49 @@ export function registerBankCommand(program) {
|
|
|
93
98
|
}
|
|
94
99
|
console.log(chalk.bold(`Bank Records (${res.data.length} of ${res.totalElements}):\n`));
|
|
95
100
|
for (const r of res.data) {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
const net = typeof r.netAmount === 'number' && Number.isFinite(r.netAmount) ? r.netAmount : 0;
|
|
102
|
+
const amount = net >= 0
|
|
103
|
+
? chalk.green(`+${net.toFixed(2)}`)
|
|
104
|
+
: chalk.red(net.toFixed(2));
|
|
105
|
+
const payer = r.extContactName ? ` ${chalk.yellow(r.extContactName)}` : '';
|
|
106
|
+
console.log(` ${chalk.cyan(r.resourceId)} ${r.valueDate} ${amount} ${r.description ?? ''}${payer} ${chalk.dim(r.status)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})(opts));
|
|
110
|
+
// ── clio bank add-records ──────────────────────────────────────
|
|
111
|
+
bank
|
|
112
|
+
.command('add-records <accountResourceId>')
|
|
113
|
+
.description('Add bank records via JSON (1-100 records per call)')
|
|
114
|
+
.requiredOption('--records <json>', 'JSON array: [{amount, transactionDate, description?, payerOrPayee?, reference?}]')
|
|
115
|
+
.option('--api-key <key>', 'API key (overrides stored/env)')
|
|
116
|
+
.option('--json', 'Output as JSON')
|
|
117
|
+
.action((accountResourceId, opts) => apiAction(async (client) => {
|
|
118
|
+
const parsed = JSON.parse(opts.records);
|
|
119
|
+
if (!Array.isArray(parsed))
|
|
120
|
+
throw new Error('--records must be a JSON array');
|
|
121
|
+
if (parsed.length < 1 || parsed.length > 100)
|
|
122
|
+
throw new Error('--records must contain 1-100 entries');
|
|
123
|
+
for (const [i, r] of parsed.entries()) {
|
|
124
|
+
if (typeof r !== 'object' || r === null)
|
|
125
|
+
throw new Error(`records[${i}] must be an object`);
|
|
126
|
+
if (typeof r.amount !== 'number' || !Number.isFinite(r.amount))
|
|
127
|
+
throw new Error(`records[${i}].amount must be a finite number`);
|
|
128
|
+
if (typeof r.transactionDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(r.transactionDate))
|
|
129
|
+
throw new Error(`records[${i}].transactionDate must be YYYY-MM-DD`);
|
|
130
|
+
}
|
|
131
|
+
const res = await addBankRecords(client, accountResourceId, parsed);
|
|
132
|
+
const errors = Array.isArray(res?.data?.errors) ? res.data.errors : [];
|
|
133
|
+
if (opts.json) {
|
|
134
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
if (errors.length > 0) {
|
|
138
|
+
console.log(chalk.red(`${errors.length} error(s):`));
|
|
139
|
+
for (const e of errors)
|
|
140
|
+
console.log(` ${chalk.red(String(e))}`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
console.log(chalk.green(`Created ${parsed.length} bank record(s).`));
|
|
100
144
|
}
|
|
101
145
|
}
|
|
102
146
|
})(opts));
|
package/dist/commands/exports.js
CHANGED
|
@@ -28,7 +28,7 @@ export function registerExportsCommand(program) {
|
|
|
28
28
|
params.startDate = opts.startDate;
|
|
29
29
|
if (opts.endDate) {
|
|
30
30
|
// Point-in-time exports (balance-sheet, trial-balance) use snapshotDate, not endDate
|
|
31
|
-
const pointInTime = ['balance-sheet'
|
|
31
|
+
const pointInTime = ['balance-sheet'];
|
|
32
32
|
if (pointInTime.includes(exportType)) {
|
|
33
33
|
params.snapshotDate = opts.endDate;
|
|
34
34
|
}
|
|
@@ -113,6 +113,7 @@ export function registerSchedulersCommand(program) {
|
|
|
113
113
|
{ flag: '--start-date', key: 'startDate' },
|
|
114
114
|
{ flag: '--repeat', key: 'repeat' },
|
|
115
115
|
{ flag: '--contact', key: 'contact' },
|
|
116
|
+
{ flag: '--reference', key: 'reference' },
|
|
116
117
|
{ flag: '--value-date', key: 'valueDate' },
|
|
117
118
|
{ flag: '--due-date', key: 'dueDate' },
|
|
118
119
|
{ flag: '--line-items', key: 'lineItems' },
|
|
@@ -170,6 +171,7 @@ export function registerSchedulersCommand(program) {
|
|
|
170
171
|
{ flag: '--start-date', key: 'startDate' },
|
|
171
172
|
{ flag: '--repeat', key: 'repeat' },
|
|
172
173
|
{ flag: '--contact', key: 'contact' },
|
|
174
|
+
{ flag: '--reference', key: 'reference' },
|
|
173
175
|
{ flag: '--value-date', key: 'valueDate' },
|
|
174
176
|
{ flag: '--due-date', key: 'dueDate' },
|
|
175
177
|
{ flag: '--line-items', key: 'lineItems' },
|
|
@@ -224,6 +226,7 @@ export function registerSchedulersCommand(program) {
|
|
|
224
226
|
requireFields(opts, [
|
|
225
227
|
{ flag: '--start-date', key: 'startDate' },
|
|
226
228
|
{ flag: '--repeat', key: 'repeat' },
|
|
229
|
+
{ flag: '--value-date', key: 'valueDate' },
|
|
227
230
|
{ flag: '--entries', key: 'entries' },
|
|
228
231
|
]);
|
|
229
232
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- user-provided JSON, API validates
|
|
@@ -233,6 +236,7 @@ export function registerSchedulersCommand(program) {
|
|
|
233
236
|
startDate: opts.startDate,
|
|
234
237
|
endDate: opts.endDate,
|
|
235
238
|
repeat: opts.repeat,
|
|
239
|
+
valueDate: opts.valueDate,
|
|
236
240
|
schedulerEntries,
|
|
237
241
|
reference: opts.reference,
|
|
238
242
|
notes: opts.notes,
|
|
@@ -8,10 +8,10 @@ export async function listAttachments(client, btType, btResourceId) {
|
|
|
8
8
|
export async function addAttachment(client, data) {
|
|
9
9
|
const { businessTransactionType: btType, businessTransactionResourceId: btId, ...rest } = data;
|
|
10
10
|
const formData = new FormData();
|
|
11
|
+
if (rest.file)
|
|
12
|
+
formData.append('file', rest.file, rest.fileName ?? 'file');
|
|
11
13
|
if (rest.attachmentId)
|
|
12
14
|
formData.append('attachmentId', rest.attachmentId);
|
|
13
|
-
if (rest.sourceUrl)
|
|
14
|
-
formData.append('sourceUrl', rest.sourceUrl);
|
|
15
15
|
return client.postMultipart(`/api/v1/${btType}/${btId}/attachments`, formData);
|
|
16
16
|
}
|
|
17
17
|
/**
|
package/dist/core/api/bank.js
CHANGED
|
@@ -16,6 +16,12 @@ export async function getBankAccount(client, resourceId) {
|
|
|
16
16
|
export async function searchBankRecords(client, accountResourceId, params) {
|
|
17
17
|
return client.post(`/api/v1/bank-records/${accountResourceId}/search`, params);
|
|
18
18
|
}
|
|
19
|
+
export async function addBankRecords(client, accountResourceId, records) {
|
|
20
|
+
return client.post(`/api/v1/bank-records/${accountResourceId}`, {
|
|
21
|
+
accountResourceId,
|
|
22
|
+
records,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
19
25
|
export async function importBankStatement(client, data) {
|
|
20
26
|
const formData = new FormData();
|
|
21
27
|
formData.append('businessTransactionType', data.businessTransactionType);
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* 0. Normalize & index (integer cents, day offsets, text normalization)
|
|
9
9
|
* 1. Exact 1:1 hash join (amount + contact + date)
|
|
10
10
|
* 2. Fuzzy 1:1 greedy assignment (tolerance + scoring)
|
|
11
|
-
* 3. N:1 subset-sum (multiple txns → one bank record)
|
|
12
|
-
* 4. 1:N reverse subset-sum (multiple bank records → one txn)
|
|
11
|
+
* 3. N:1 subset-sum (multiple txns → one bank record, same-contact constraint)
|
|
12
|
+
* 4. 1:N reverse subset-sum (multiple bank records → one txn, no contact constraint)
|
|
13
13
|
* 5. N:M two-set matching (within contact groups)
|
|
14
14
|
*
|
|
15
15
|
* Performance: <50ms for 500 records × 2000 transactions.
|
|
@@ -231,7 +231,7 @@ export function matchBankRecords(input) {
|
|
|
231
231
|
let phase3Count = 0;
|
|
232
232
|
for (const ri of [...recordPool]) {
|
|
233
233
|
const r = records[ri];
|
|
234
|
-
// Gather same-sign candidates within date window
|
|
234
|
+
// Gather same-sign candidates within date window (enriched with contactNorm)
|
|
235
235
|
const candidates = [];
|
|
236
236
|
for (const ti of txnPool) {
|
|
237
237
|
const t = txns[ti];
|
|
@@ -239,47 +239,54 @@ export function matchBankRecords(input) {
|
|
|
239
239
|
continue;
|
|
240
240
|
if (Math.abs(r.day - t.day) > dateWindowDays)
|
|
241
241
|
continue;
|
|
242
|
-
candidates.push({ ti, cents: t.cents });
|
|
242
|
+
candidates.push({ ti, cents: t.cents, contactNorm: t.contactNorm });
|
|
243
243
|
}
|
|
244
244
|
if (candidates.length < 2)
|
|
245
245
|
continue;
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
score =
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
246
|
+
// Group by contact — all txns in a subset must share the same contact
|
|
247
|
+
const contactCandidateGroups = groupByContact(candidates);
|
|
248
|
+
let bestSubset = null;
|
|
249
|
+
let bestGroup = [];
|
|
250
|
+
let bestIsFx = false;
|
|
251
|
+
let bestRank = -Infinity;
|
|
252
|
+
let bestSignals = { text: 0, date: 0, type: 0, score: 0 };
|
|
253
|
+
for (const groupCandidates of contactCandidateGroups.values()) {
|
|
254
|
+
if (groupCandidates.length < 2)
|
|
255
|
+
continue;
|
|
256
|
+
// Sort descending by |cents| for DFS pruning
|
|
257
|
+
groupCandidates.sort((a, b) => Math.abs(b.cents) - Math.abs(a.cents));
|
|
258
|
+
const subCandidates = groupCandidates.map((c, i) => ({
|
|
259
|
+
index: i,
|
|
260
|
+
cents: Math.abs(c.cents),
|
|
261
|
+
}));
|
|
262
|
+
const isFxGroup = r.isFx || groupCandidates.some(c => txns[c.ti].isFx);
|
|
263
|
+
const effectiveTolCents = isFxGroup
|
|
264
|
+
? Math.max(toleranceCents, Math.round(Math.abs(r.cents) * fxTolerancePct))
|
|
265
|
+
: toleranceCents;
|
|
266
|
+
const subsets = subsetSumDFS(subCandidates, Math.abs(r.cents), effectiveTolCents, maxK, findAll, dfsNodes);
|
|
267
|
+
for (const ss of subsets) {
|
|
268
|
+
const matchedTxns = ss.indices.map(i => txns[groupCandidates[i].ti]);
|
|
269
|
+
const textSig = groupTextScore(r.contact, r.reference, r.description, matchedTxns.map(t => ({ contact: t.contact, reference: t.reference })));
|
|
270
|
+
const minDaysDiff = Math.min(...ss.indices.map(i => Math.abs(r.day - txns[groupCandidates[i].ti].day)));
|
|
271
|
+
const dateSig = dateScore(minDaysDiff);
|
|
272
|
+
const typeSig = typeScore('N:1', ss.indices.length);
|
|
273
|
+
let score = compositeScore(textSig, dateSig, typeSig);
|
|
274
|
+
if (isFxGroup)
|
|
275
|
+
score = Math.max(0, score - FX_PENALTY);
|
|
276
|
+
// Rank: prefer fewer items, then higher score, then deterministic tiebreak
|
|
277
|
+
const rank = -ss.indices.length * 1000 + score * 100;
|
|
278
|
+
if (rank > bestRank || (rank === bestRank && score > bestSignals.score)) {
|
|
279
|
+
bestSubset = ss;
|
|
280
|
+
bestGroup = groupCandidates;
|
|
281
|
+
bestIsFx = isFxGroup;
|
|
282
|
+
bestRank = rank;
|
|
283
|
+
bestSignals = { text: textSig, date: dateSig, type: typeSig, score };
|
|
284
|
+
}
|
|
279
285
|
}
|
|
280
286
|
}
|
|
281
|
-
|
|
282
|
-
|
|
287
|
+
if (!bestSubset)
|
|
288
|
+
continue;
|
|
289
|
+
const matchedTxnIndices = bestSubset.indices.map(i => bestGroup[i].ti);
|
|
283
290
|
// Verify none already consumed
|
|
284
291
|
if (matchedTxnIndices.some(ti => !txnPool.has(ti)))
|
|
285
292
|
continue;
|
|
@@ -287,28 +294,21 @@ export function matchBankRecords(input) {
|
|
|
287
294
|
for (const ti of matchedTxnIndices)
|
|
288
295
|
txnPool.delete(ti);
|
|
289
296
|
const matchedTxns = matchedTxnIndices.map(ti => txns[ti]);
|
|
290
|
-
const
|
|
291
|
-
const minDaysDiff = Math.min(...matchedTxns.map(t => Math.abs(r.day - t.day)));
|
|
292
|
-
const dateSig = dateScore(minDaysDiff);
|
|
293
|
-
const typeSig = typeScore('N:1', matchedTxns.length);
|
|
294
|
-
let score = compositeScore(textSig, dateSig, typeSig);
|
|
295
|
-
if (isFxRecord)
|
|
296
|
-
score = Math.max(0, score - FX_PENALTY);
|
|
297
|
-
const confidence = scoreToConfidence(score);
|
|
297
|
+
const confidence = scoreToConfidence(bestSignals.score);
|
|
298
298
|
const txnTotal = round2(matchedTxns.reduce((s, t) => s + t.signedAmount, 0));
|
|
299
299
|
const proposal = {
|
|
300
300
|
matchType: 'N:1',
|
|
301
301
|
confidence,
|
|
302
|
-
score: round2Sig(score),
|
|
302
|
+
score: round2Sig(bestSignals.score),
|
|
303
303
|
bankRecords: [{ id: r.id, amount: r.originalAmount, date: r.date }],
|
|
304
304
|
transactions: matchedTxns.map(t => ({ id: t.id, amount: t.originalAmount, date: t.date })),
|
|
305
305
|
bankTotal: r.originalAmount,
|
|
306
306
|
transactionTotal: txnTotal,
|
|
307
307
|
variance: round2(r.originalAmount - txnTotal),
|
|
308
|
-
signals: { text: round2Sig(
|
|
308
|
+
signals: { text: round2Sig(bestSignals.text), date: round2Sig(bestSignals.date), type: round2Sig(bestSignals.type) },
|
|
309
309
|
reason: `${matchedTxns.length} transactions sum to bank record amount (${confidence} confidence)`,
|
|
310
310
|
};
|
|
311
|
-
if (
|
|
311
|
+
if (bestIsFx) {
|
|
312
312
|
proposal.fxVariance = round2(fromCents(Math.abs(r.cents - bestSubset.sum)));
|
|
313
313
|
}
|
|
314
314
|
matches.push(proposal);
|
|
@@ -321,6 +321,9 @@ export function matchBankRecords(input) {
|
|
|
321
321
|
let phase4Count = 0;
|
|
322
322
|
for (const ti of [...txnPool]) {
|
|
323
323
|
const t = txns[ti];
|
|
324
|
+
// Gather same-sign candidates within date window
|
|
325
|
+
// Note: NO contact grouping here — bank record payer/payee is metadata,
|
|
326
|
+
// so different bank records can legitimately have different contacts.
|
|
324
327
|
const candidates = [];
|
|
325
328
|
for (const ri of recordPool) {
|
|
326
329
|
const r = records[ri];
|
|
@@ -333,7 +336,6 @@ export function matchBankRecords(input) {
|
|
|
333
336
|
if (candidates.length < 2)
|
|
334
337
|
continue;
|
|
335
338
|
candidates.sort((a, b) => Math.abs(b.cents) - Math.abs(a.cents));
|
|
336
|
-
// DFS operates on absolute values (same-sign filter guarantees all same direction)
|
|
337
339
|
const subCandidates = candidates.map((c, i) => ({
|
|
338
340
|
index: i,
|
|
339
341
|
cents: Math.abs(c.cents),
|
|
@@ -345,11 +347,25 @@ export function matchBankRecords(input) {
|
|
|
345
347
|
const subsets = subsetSumDFS(subCandidates, Math.abs(t.cents), effectiveTolCents, maxK, findAll, dfsNodes);
|
|
346
348
|
if (subsets.length === 0)
|
|
347
349
|
continue;
|
|
348
|
-
// Pick best:
|
|
350
|
+
// Pick best: fewer items + higher composite score (matches Phase 3 ranking)
|
|
349
351
|
let bestSubset = subsets[0];
|
|
352
|
+
let bestRank = -Infinity;
|
|
353
|
+
let bestSignals = { text: 0, date: 0, type: 0, score: 0 };
|
|
350
354
|
for (const ss of subsets) {
|
|
351
|
-
|
|
355
|
+
const matchedRecords = ss.indices.map(i => records[candidates[i].ri]);
|
|
356
|
+
const textSig = Math.max(...matchedRecords.map(r => crossFieldTextScore(r.contact, r.reference, r.description, t.contact, t.reference)));
|
|
357
|
+
const minDaysDiff = Math.min(...matchedRecords.map(r => Math.abs(r.day - t.day)));
|
|
358
|
+
const dateSig = dateScore(minDaysDiff);
|
|
359
|
+
const typeSig = typeScore('1:N', ss.indices.length);
|
|
360
|
+
let score = compositeScore(textSig, dateSig, typeSig);
|
|
361
|
+
if (isFxTxn)
|
|
362
|
+
score = Math.max(0, score - FX_PENALTY);
|
|
363
|
+
const rank = -ss.indices.length * 1000 + score * 100;
|
|
364
|
+
if (rank > bestRank || (rank === bestRank && score > bestSignals.score)) {
|
|
352
365
|
bestSubset = ss;
|
|
366
|
+
bestRank = rank;
|
|
367
|
+
bestSignals = { text: textSig, date: dateSig, type: typeSig, score };
|
|
368
|
+
}
|
|
353
369
|
}
|
|
354
370
|
const matchedRecordIndices = bestSubset.indices.map(i => candidates[i].ri);
|
|
355
371
|
if (matchedRecordIndices.some(ri => !recordPool.has(ri)))
|
|
@@ -358,25 +374,18 @@ export function matchBankRecords(input) {
|
|
|
358
374
|
for (const ri of matchedRecordIndices)
|
|
359
375
|
recordPool.delete(ri);
|
|
360
376
|
const matchedRecords = matchedRecordIndices.map(ri => records[ri]);
|
|
361
|
-
const
|
|
362
|
-
const minDaysDiff = Math.min(...matchedRecords.map(r => Math.abs(r.day - t.day)));
|
|
363
|
-
const dateSig = dateScore(minDaysDiff);
|
|
364
|
-
const typeSig = typeScore('1:N', matchedRecords.length);
|
|
365
|
-
let score = compositeScore(textSig, dateSig, typeSig);
|
|
366
|
-
if (isFxTxn)
|
|
367
|
-
score = Math.max(0, score - FX_PENALTY);
|
|
368
|
-
const confidence = scoreToConfidence(score);
|
|
377
|
+
const confidence = scoreToConfidence(bestSignals.score);
|
|
369
378
|
const bankTotal = round2(matchedRecords.reduce((s, r) => s + r.originalAmount, 0));
|
|
370
379
|
const proposal = {
|
|
371
380
|
matchType: '1:N',
|
|
372
381
|
confidence,
|
|
373
|
-
score: round2Sig(score),
|
|
382
|
+
score: round2Sig(bestSignals.score),
|
|
374
383
|
bankRecords: matchedRecords.map(r => ({ id: r.id, amount: r.originalAmount, date: r.date })),
|
|
375
384
|
transactions: [{ id: t.id, amount: t.originalAmount, date: t.date }],
|
|
376
385
|
bankTotal,
|
|
377
386
|
transactionTotal: t.signedAmount,
|
|
378
387
|
variance: round2(bankTotal - t.signedAmount),
|
|
379
|
-
signals: { text: round2Sig(
|
|
388
|
+
signals: { text: round2Sig(bestSignals.text), date: round2Sig(bestSignals.date), type: round2Sig(bestSignals.type) },
|
|
380
389
|
reason: `${matchedRecords.length} bank records sum to transaction amount (${confidence} confidence)`,
|
|
381
390
|
};
|
|
382
391
|
if (isFxTxn) {
|
|
@@ -569,6 +578,20 @@ export function matchBankRecords(input) {
|
|
|
569
578
|
function round2Sig(n) {
|
|
570
579
|
return Math.round(n * 100) / 100;
|
|
571
580
|
}
|
|
581
|
+
/** Group candidates by contactNorm for same-contact subset constraint (Phase 3 + 4). */
|
|
582
|
+
function groupByContact(candidates) {
|
|
583
|
+
const groups = new Map();
|
|
584
|
+
for (const c of candidates) {
|
|
585
|
+
const key = c.contactNorm || '';
|
|
586
|
+
let g = groups.get(key);
|
|
587
|
+
if (!g) {
|
|
588
|
+
g = [];
|
|
589
|
+
groups.set(key, g);
|
|
590
|
+
}
|
|
591
|
+
g.push(c);
|
|
592
|
+
}
|
|
593
|
+
return groups;
|
|
594
|
+
}
|
|
572
595
|
/** Build human-readable reason for a fuzzy 1:1 match. */
|
|
573
596
|
function buildReason(matchType, confidence, p) {
|
|
574
597
|
const parts = [];
|
|
@@ -52,3 +52,36 @@ export function buildCnFilter(input) {
|
|
|
52
52
|
f.contactResourceId = { eq: input.contactResourceId };
|
|
53
53
|
return Object.keys(f).length > 0 ? f : undefined;
|
|
54
54
|
}
|
|
55
|
+
/** Build a bank record search filter (shared by CLI + MCP tool). */
|
|
56
|
+
export function buildBankRecordFilter(input) {
|
|
57
|
+
const f = {};
|
|
58
|
+
if (input.status)
|
|
59
|
+
f.status = { eq: input.status };
|
|
60
|
+
if (input.description)
|
|
61
|
+
f.description = { contains: input.description };
|
|
62
|
+
if (input.payer)
|
|
63
|
+
f.extContactName = { contains: input.payer };
|
|
64
|
+
if (input.reference)
|
|
65
|
+
f.extReference = { contains: input.reference };
|
|
66
|
+
// Date range → valueDate expression
|
|
67
|
+
if (input.from || input.to) {
|
|
68
|
+
const d = {};
|
|
69
|
+
if (input.from)
|
|
70
|
+
d.gte = input.from;
|
|
71
|
+
if (input.to)
|
|
72
|
+
d.lte = input.to;
|
|
73
|
+
f.valueDate = d;
|
|
74
|
+
}
|
|
75
|
+
// Amount range → netAmount expression (guard against NaN/Infinity)
|
|
76
|
+
const min = typeof input.amountMin === 'number' && Number.isFinite(input.amountMin) ? input.amountMin : undefined;
|
|
77
|
+
const max = typeof input.amountMax === 'number' && Number.isFinite(input.amountMax) ? input.amountMax : undefined;
|
|
78
|
+
if (min != null || max != null) {
|
|
79
|
+
const a = {};
|
|
80
|
+
if (min != null)
|
|
81
|
+
a.gte = min;
|
|
82
|
+
if (max != null)
|
|
83
|
+
a.lte = max;
|
|
84
|
+
f.netAmount = a;
|
|
85
|
+
}
|
|
86
|
+
return Object.keys(f).length > 0 ? f : undefined;
|
|
87
|
+
}
|
|
@@ -5,7 +5,7 @@ import { listInvoices, searchInvoices, getInvoice, createInvoice, updateInvoice,
|
|
|
5
5
|
import { listBills, searchBills, getBill, createBill, updateBill, deleteBill, createBillPayment, createScheduledBill, finalizeBill, applyCreditsToBill, } from '../api/bills.js';
|
|
6
6
|
import { listJournals, searchJournals, getJournal, createJournal, deleteJournal, updateJournal, createScheduledJournal, } from '../api/journals.js';
|
|
7
7
|
import { generateTrialBalance, generateBalanceSheet, generateProfitAndLoss, generateCashflow, generateArSummary, generateApSummary, generateCashBalance, generateGeneralLedger, } from '../api/reports.js';
|
|
8
|
-
import { listBankAccounts, importBankStatement } from '../api/bank.js';
|
|
8
|
+
import { listBankAccounts, addBankRecords, importBankStatement } from '../api/bank.js';
|
|
9
9
|
import { listItems, searchItems, getItem, createItem, updateItem, deleteItem, } from '../api/items.js';
|
|
10
10
|
import { listTags, searchTags, createTag, deleteTag, } from '../api/tags.js';
|
|
11
11
|
import { listCapsuleTypes, listCapsules, searchCapsules, getCapsule, createCapsule, updateCapsule, deleteCapsule, } from '../api/capsules.js';
|
|
@@ -29,7 +29,7 @@ import { downloadExport } from '../api/data-exports.js';
|
|
|
29
29
|
import { listAttachments, addAttachment, fetchAttachmentTable } from '../api/attachments.js';
|
|
30
30
|
import { createFromAttachment, searchMagicWorkflows } from '../api/magic.js';
|
|
31
31
|
import { messageToPdf } from '../api/message-pdf.js';
|
|
32
|
-
import { handlePaginationMode, buildCnFilter } from './pagination.js';
|
|
32
|
+
import { handlePaginationMode, buildCnFilter, buildBankRecordFilter } from './pagination.js';
|
|
33
33
|
// Job blueprints (offline — no API calls)
|
|
34
34
|
import { generateMonthEndBlueprint } from '../jobs/month-end/blueprint.js';
|
|
35
35
|
import { generateQuarterEndBlueprint } from '../jobs/quarter-end/blueprint.js';
|
|
@@ -1625,9 +1625,17 @@ export const TOOL_DEFINITIONS = [
|
|
|
1625
1625
|
},
|
|
1626
1626
|
{
|
|
1627
1627
|
name: 'search_bank_records',
|
|
1628
|
-
description: 'Search bank records (imported bank transactions) for a specific account.',
|
|
1628
|
+
description: 'Search bank records (imported bank transactions) for a specific account. Supports filtering by status, date range, description, payer/payee, reference, and amount range.',
|
|
1629
1629
|
params: {
|
|
1630
1630
|
accountResourceId: { type: 'string', description: 'Bank account resourceId' },
|
|
1631
|
+
status: { type: 'string', enum: ['UNRECONCILED', 'RECONCILED', 'ARCHIVED', 'POSSIBLE_DUPLICATE'], description: 'Filter by reconciliation status' },
|
|
1632
|
+
from: { type: 'string', description: 'Filter from date inclusive (YYYY-MM-DD)' },
|
|
1633
|
+
to: { type: 'string', description: 'Filter to date inclusive (YYYY-MM-DD)' },
|
|
1634
|
+
description: { type: 'string', description: 'Filter by description (contains)' },
|
|
1635
|
+
payer: { type: 'string', description: 'Filter by payer/payee name (contains)' },
|
|
1636
|
+
reference: { type: 'string', description: 'Filter by reference (contains)' },
|
|
1637
|
+
amountMin: { type: 'number', description: 'Filter by minimum amount (inclusive)' },
|
|
1638
|
+
amountMax: { type: 'number', description: 'Filter by maximum amount (inclusive)' },
|
|
1631
1639
|
...SEARCH_PARAMS,
|
|
1632
1640
|
},
|
|
1633
1641
|
required: ['accountResourceId'],
|
|
@@ -1636,8 +1644,9 @@ export const TOOL_DEFINITIONS = [
|
|
|
1636
1644
|
execute: async (ctx, input) => {
|
|
1637
1645
|
const { mode, limit, offset, sortBy, sortOrder } = extractPaginationInput(input);
|
|
1638
1646
|
return handlePaginationMode(mode, (off, lim) => searchBankRecords(ctx.client, input.accountResourceId, {
|
|
1647
|
+
filter: buildBankRecordFilter(input),
|
|
1639
1648
|
limit: lim, offset: off,
|
|
1640
|
-
sort: { sortBy: [sortBy ?? '
|
|
1649
|
+
sort: { sortBy: [sortBy ?? 'valueDate'], order: (sortOrder ?? 'DESC') },
|
|
1641
1650
|
}), limit, offset, 100);
|
|
1642
1651
|
},
|
|
1643
1652
|
},
|
|
@@ -2038,7 +2047,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
|
|
|
2038
2047
|
},
|
|
2039
2048
|
notes: { type: 'string', description: 'Journal notes' },
|
|
2040
2049
|
},
|
|
2041
|
-
required: ['startDate', 'repeat', 'schedulerEntries'],
|
|
2050
|
+
required: ['startDate', 'repeat', 'valueDate', 'schedulerEntries'],
|
|
2042
2051
|
group: 'schedulers',
|
|
2043
2052
|
readOnly: false,
|
|
2044
2053
|
execute: async (ctx, input) => createScheduledJournal(ctx.client, {
|
|
@@ -2046,6 +2055,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
|
|
|
2046
2055
|
startDate: input.startDate,
|
|
2047
2056
|
endDate: input.endDate,
|
|
2048
2057
|
repeat: input.repeat,
|
|
2058
|
+
valueDate: input.valueDate,
|
|
2049
2059
|
schedulerEntries: input.schedulerEntries,
|
|
2050
2060
|
reference: input.reference,
|
|
2051
2061
|
notes: input.notes,
|
|
@@ -2081,7 +2091,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
|
|
|
2081
2091
|
},
|
|
2082
2092
|
tag: { type: 'string', description: 'Tag name' },
|
|
2083
2093
|
},
|
|
2084
|
-
required: ['startDate', 'repeat', 'contactResourceId', 'valueDate', 'dueDate', 'lineItems'],
|
|
2094
|
+
required: ['startDate', 'repeat', 'contactResourceId', 'reference', 'valueDate', 'dueDate', 'lineItems'],
|
|
2085
2095
|
group: 'schedulers',
|
|
2086
2096
|
readOnly: false,
|
|
2087
2097
|
execute: async (ctx, input) => createScheduledInvoice(ctx.client, {
|
|
@@ -2130,7 +2140,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
|
|
|
2130
2140
|
},
|
|
2131
2141
|
tag: { type: 'string', description: 'Tag name' },
|
|
2132
2142
|
},
|
|
2133
|
-
required: ['startDate', 'repeat', 'contactResourceId', 'valueDate', 'dueDate', 'lineItems'],
|
|
2143
|
+
required: ['startDate', 'repeat', 'contactResourceId', 'reference', 'valueDate', 'dueDate', 'lineItems'],
|
|
2134
2144
|
group: 'schedulers',
|
|
2135
2145
|
readOnly: false,
|
|
2136
2146
|
execute: async (ctx, input) => createScheduledBill(ctx.client, {
|
|
@@ -2209,6 +2219,36 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
|
|
|
2209
2219
|
});
|
|
2210
2220
|
},
|
|
2211
2221
|
},
|
|
2222
|
+
// ── Bank Records: JSON POST ──────────────────────────────────
|
|
2223
|
+
{
|
|
2224
|
+
name: 'add_bank_records',
|
|
2225
|
+
description: 'Create bank statement entries via JSON POST (1-100 records per call). For CSV/OFX file imports, use import_bank_statement instead.\n\nFields per record:\n- amount (required): positive = cash-in, negative = cash-out\n- transactionDate (required): YYYY-MM-DD\n- description, payerOrPayee, reference: optional strings\n\nReturns {data: {errors: []}} on success. accountResourceId must be a bank-type CoA account (find via list_bank_accounts).',
|
|
2226
|
+
params: {
|
|
2227
|
+
accountResourceId: {
|
|
2228
|
+
type: 'string',
|
|
2229
|
+
description: 'Bank account resourceId (from list_bank_accounts)',
|
|
2230
|
+
},
|
|
2231
|
+
records: {
|
|
2232
|
+
type: 'array',
|
|
2233
|
+
items: {
|
|
2234
|
+
type: 'object',
|
|
2235
|
+
properties: {
|
|
2236
|
+
amount: { type: 'number', description: 'Positive = cash-in, negative = cash-out' },
|
|
2237
|
+
transactionDate: { type: 'string', description: 'Date (YYYY-MM-DD)' },
|
|
2238
|
+
description: { type: 'string', description: 'Description' },
|
|
2239
|
+
payerOrPayee: { type: 'string', description: 'Payer or payee name' },
|
|
2240
|
+
reference: { type: 'string', description: 'Reference' },
|
|
2241
|
+
},
|
|
2242
|
+
required: ['amount', 'transactionDate'],
|
|
2243
|
+
},
|
|
2244
|
+
description: 'Array of 1-100 bank records',
|
|
2245
|
+
},
|
|
2246
|
+
},
|
|
2247
|
+
required: ['accountResourceId', 'records'],
|
|
2248
|
+
group: 'bank',
|
|
2249
|
+
readOnly: false,
|
|
2250
|
+
execute: async (ctx, input) => addBankRecords(ctx.client, input.accountResourceId, input.records),
|
|
2251
|
+
},
|
|
2212
2252
|
// ── Magic: Create BT from Attachment ──────────────────────────
|
|
2213
2253
|
{
|
|
2214
2254
|
name: 'create_bt_from_attachment',
|
|
@@ -2299,7 +2339,7 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
|
|
|
2299
2339
|
},
|
|
2300
2340
|
{
|
|
2301
2341
|
name: 'add_attachment',
|
|
2302
|
-
description: 'Add an attachment to a business transaction.',
|
|
2342
|
+
description: 'Add an attachment to a business transaction. Provide sourceUrl (downloads and uploads as file) or attachmentId (links existing).',
|
|
2303
2343
|
params: {
|
|
2304
2344
|
transactionType: {
|
|
2305
2345
|
type: 'string',
|
|
@@ -2307,8 +2347,8 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
|
|
|
2307
2347
|
description: 'Transaction type',
|
|
2308
2348
|
},
|
|
2309
2349
|
transactionId: { type: 'string', description: 'Transaction resourceId' },
|
|
2310
|
-
attachmentId: { type: 'string', description: 'Attachment ID to link' },
|
|
2311
|
-
sourceUrl: { type: 'string', description: 'Source file URL
|
|
2350
|
+
attachmentId: { type: 'string', description: 'Attachment ID to link (alternative to sourceUrl)' },
|
|
2351
|
+
sourceUrl: { type: 'string', description: 'Source file URL — downloaded and uploaded as multipart file' },
|
|
2312
2352
|
},
|
|
2313
2353
|
required: ['transactionType', 'transactionId'],
|
|
2314
2354
|
group: 'attachments',
|
|
@@ -2317,11 +2357,28 @@ Auto-resolves accounts from chart of accounts. Provide bankAccountName for recip
|
|
|
2317
2357
|
if (!input.attachmentId && !input.sourceUrl) {
|
|
2318
2358
|
throw new Error('Provide attachmentId or sourceUrl — one is required.');
|
|
2319
2359
|
}
|
|
2360
|
+
let file;
|
|
2361
|
+
let fileName;
|
|
2362
|
+
if (input.sourceUrl) {
|
|
2363
|
+
const res = await fetch(input.sourceUrl);
|
|
2364
|
+
if (!res.ok)
|
|
2365
|
+
throw new Error(`Failed to download ${input.sourceUrl}: ${res.status}`);
|
|
2366
|
+
const buffer = await res.arrayBuffer();
|
|
2367
|
+
const contentType = res.headers.get('content-type') ?? 'application/octet-stream';
|
|
2368
|
+
file = new Blob([buffer], { type: contentType });
|
|
2369
|
+
try {
|
|
2370
|
+
fileName = new URL(input.sourceUrl).pathname.split('/').pop() || 'attachment';
|
|
2371
|
+
}
|
|
2372
|
+
catch {
|
|
2373
|
+
fileName = 'attachment';
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2320
2376
|
return addAttachment(ctx.client, {
|
|
2321
2377
|
businessTransactionType: input.transactionType,
|
|
2322
2378
|
businessTransactionResourceId: input.transactionId,
|
|
2379
|
+
file,
|
|
2380
|
+
fileName,
|
|
2323
2381
|
attachmentId: input.attachmentId,
|
|
2324
|
-
sourceUrl: input.sourceUrl,
|
|
2325
2382
|
});
|
|
2326
2383
|
},
|
|
2327
2384
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jaz-clio",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.21.2",
|
|
4
4
|
"description": "Clio — Command Line Interface Orchestrator for Jaz AI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -56,9 +56,11 @@
|
|
|
56
56
|
"update-notifier": "^7.3.1"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
+
"@anthropic-ai/mcpb": "^2.1.2",
|
|
59
60
|
"@types/adm-zip": "^0.5.6",
|
|
60
61
|
"@types/node": "^22.10.1",
|
|
61
62
|
"@types/prompts": "^2.4.9",
|
|
63
|
+
"esbuild": "^0.27.3",
|
|
62
64
|
"pino-pretty": "^13.1.3",
|
|
63
65
|
"typescript": "^5.7.2",
|
|
64
66
|
"vitest": "^4.0.18"
|