jaz-clio 4.3.0 → 4.5.0
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/assets/skills/api/SKILL.md +6 -2
- package/assets/skills/api/references/endpoints.md +41 -0
- package/assets/skills/conversion/SKILL.md +1 -1
- package/assets/skills/jobs/SKILL.md +1 -1
- package/assets/skills/jobs/references/document-collection.md +16 -5
- package/assets/skills/transaction-recipes/SKILL.md +1 -1
- package/dist/commands/api-action.js +1 -1
- package/dist/commands/bills.js +35 -5
- package/dist/commands/customer-credit-notes.js +211 -2
- package/dist/commands/draft-helpers.js +187 -25
- package/dist/commands/invoices.js +212 -2
- package/dist/commands/jobs.js +47 -39
- package/dist/commands/journals.js +195 -1
- package/dist/commands/magic.js +314 -10
- package/dist/commands/resolve.js +12 -7
- package/dist/commands/supplier-credit-notes.js +211 -2
- package/dist/core/api/bills.js +1 -0
- package/dist/core/api/customer-cn.js +10 -0
- package/dist/core/api/invoices.js +10 -0
- package/dist/core/api/journals.js +10 -0
- package/dist/core/api/supplier-cn.js +10 -0
- package/dist/core/jobs/document-collection/tools/ingest/decrypt.js +19 -0
- package/dist/core/jobs/document-collection/tools/ingest/format.js +15 -2
- package/dist/core/jobs/document-collection/tools/ingest/scanner.js +5 -2
- package/dist/core/jobs/document-collection/tools/ingest/upload.js +4 -4
- package/dist/core/pdf/detect.js +344 -0
- package/dist/core/pdf/index.js +8 -0
- package/dist/core/pdf/split.js +81 -0
- package/dist/core/pdf/types.js +4 -0
- package/package.json +2 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: jaz-api
|
|
3
|
-
version: 4.
|
|
3
|
+
version: 4.5.0
|
|
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.
|
|
@@ -211,8 +211,12 @@ Step 3: For each draft where ready = true:
|
|
|
211
211
|
|
|
212
212
|
81. **`--account` bulk patches line items** — when used with `clio bills draft finalize`, `--account` resolves the name to a UUID then sets `accountResourceId` on EVERY line item where it's currently null. Existing accounts are NOT overwritten. Same for `--tax-profile`. `--lines` takes priority (full replacement).
|
|
213
213
|
82. **`--dry-run` validates without modifying** — returns the same validation structure as `draft list` (per-field status/hint), so agents can preview what would happen before committing. No API write occurs.
|
|
214
|
-
83. **Finalization is a single PUT** — `updateBill()` with `saveAsDraft: false` transitions DRAFT → UNPAID (per Rule 67) and updates all fields in one call. No delete-and-recreate.
|
|
214
|
+
83. **Finalization is a single PUT** — `updateBill()` with `saveAsDraft: false` transitions DRAFT → UNPAID (per Rule 67) and updates all fields in one call. No delete-and-recreate. The CLI handles all field normalization automatically (date format, line item sanitization, account field name mapping).
|
|
215
215
|
84. **Draft list attachment count** — `draft list` includes `attachmentCount` per draft (from `GET /bills/:id/attachments`). Use `draft attachments <id>` for full details including `fileUrl` download links.
|
|
216
|
+
85. **PUT body requires `resourceId`** — The UpdateBill PUT endpoint requires `resourceId` in the body (in addition to the URL path). Dates must be `YYYY-MM-DD` (not ISO with time). `taxInclusion` is boolean (`true`/`false`), not string. Line items must use `accountResourceId` (not `organizationAccountResourceId` from GET).
|
|
217
|
+
86. **GET→PUT field asymmetry** — GET returns `organizationAccountResourceId` on line items; PUT requires `accountResourceId`. GET returns dates as `2026-02-27T00:00:00Z`; PUT requires `2026-02-27`. GET returns `taxProfile: { resourceId }` object; PUT requires `taxProfileResourceId` string. The CLI `draft finalize` command normalizes all of these automatically.
|
|
218
|
+
87. **Magic workflow status may be null immediately after creation** — The `POST /magic/workflows/search` endpoint may return a workflow with `status: null` right after `POST /magic/create-from-attachment`. Allow 2-3 seconds before polling, or default to `SUBMITTED`. The CLI `magic status` command defaults null status to `SUBMITTED`.
|
|
219
|
+
88. **Finalized invoices/bills need `accountResourceId` on all line items** — When `saveAsDraft: false` (or using `--finalize`), every `lineItems[i].accountResourceId` must be set. Omitting it causes 422: "lineItems[0].accountResourceId is required if [saveAsDraft] is false". The CLI validates this pre-flight.
|
|
216
220
|
|
|
217
221
|
#### DRY Extension Pattern
|
|
218
222
|
|
|
@@ -985,6 +985,12 @@ Content-Type: application/json
|
|
|
985
985
|
- Tax amounts and profiles
|
|
986
986
|
- Document reference numbers, dates, currency
|
|
987
987
|
|
|
988
|
+
**Encrypted PDFs:** Magic cannot process password-protected PDFs. The CLI auto-detects and decrypts before upload:
|
|
989
|
+
- Embed password in filename: `receipt__pw__s3cRetP@ss.pdf` → decrypts with password `s3cRetP@ss`, uploads as `receipt.pdf`
|
|
990
|
+
- `__pw__` delimiter is case-insensitive; password is case-sensitive
|
|
991
|
+
- Requires `qpdf` installed (`brew install qpdf`)
|
|
992
|
+
- If no password in filename, CLI prompts interactively (or errors in `--json` mode with actionable rename instructions)
|
|
993
|
+
|
|
988
994
|
**Key gotchas:**
|
|
989
995
|
- `sourceFile` is the field name (NOT `file`) — same pattern as bank statement endpoint
|
|
990
996
|
- `EXPENSE` returns 422 — use one of the 4 valid types above
|
|
@@ -1056,6 +1062,41 @@ Content-Type: application/json
|
|
|
1056
1062
|
3. When `COMPLETED` → read `businessTransactionDetails.businessTransactionResourceId`
|
|
1057
1063
|
4. Use the BT resource ID with `GET /invoices/:id`, `GET /bills/:id`, `GET /customer-credit-notes/:id`, or `GET /supplier-credit-notes/:id`
|
|
1058
1064
|
|
|
1065
|
+
### CLI: clio magic split — Merged PDF Splitting
|
|
1066
|
+
|
|
1067
|
+
Splits a merged PDF containing multiple documents (invoices, bills, credit notes) into individual files and uploads each to Magic. Uses structural PDF signals (bookmarks, page labels) + text heuristics (keywords, "Page 1 of N" patterns) for boundary detection. **No AI tokens used.**
|
|
1068
|
+
|
|
1069
|
+
```bash
|
|
1070
|
+
# Auto-detect boundaries + upload
|
|
1071
|
+
clio magic split --file merged.pdf --type bill
|
|
1072
|
+
|
|
1073
|
+
# Manual page ranges (for scanned PDFs or override)
|
|
1074
|
+
clio magic split --file merged.pdf --type bill --pages "1-3,4-6,7-9"
|
|
1075
|
+
|
|
1076
|
+
# Dry-run: detect boundaries only (no qpdf needed)
|
|
1077
|
+
clio magic split --file merged.pdf --type bill --dry-run
|
|
1078
|
+
|
|
1079
|
+
# JSON output (for agents)
|
|
1080
|
+
clio magic split --file merged.pdf --type bill --dry-run --json
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
**Detection signals (score-based, threshold >= 50):**
|
|
1084
|
+
- outline-bookmark (+80): PDF bookmark points to this page
|
|
1085
|
+
- page-label-reset (+70): PDF page label restarts at "1"
|
|
1086
|
+
- keyword in header (+40): Document keyword (INVOICE, BILL, etc.) in upper 40%
|
|
1087
|
+
- page-one-of (+35): "Page 1 of N" pattern
|
|
1088
|
+
- keyword-large (+25): Large font (>18pt) keyword bonus
|
|
1089
|
+
- doc-ref (+20): Document reference (INV-001, SO-2024-100, etc.)
|
|
1090
|
+
- continuation (-60): "Page N>1 of M" anti-signal
|
|
1091
|
+
- continuation-text (-40): "Continued" anti-signal
|
|
1092
|
+
|
|
1093
|
+
**Edge cases:**
|
|
1094
|
+
- Scanned PDFs (no extractable text): warns and requires `--pages` manual override
|
|
1095
|
+
- Mixed scanned+digital: low confidence on scanned portions triggers confirmation prompt
|
|
1096
|
+
- Single document detected: suggests `clio magic create` instead
|
|
1097
|
+
- Encrypted PDFs: same `__pw__` pattern as `magic create`
|
|
1098
|
+
- Requires `qpdf` for splitting (not needed for `--dry-run` auto-detect mode)
|
|
1099
|
+
|
|
1059
1100
|
---
|
|
1060
1101
|
|
|
1061
1102
|
## 15. Bank Records
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: jaz-conversion
|
|
3
|
-
version: 4.
|
|
3
|
+
version: 4.5.0
|
|
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.5.0
|
|
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.
|
|
@@ -97,8 +97,10 @@ clio jobs document-collection ingest --source "https://www.dropbox.com/..." --ti
|
|
|
97
97
|
clio jobs document-collection ingest --source ./scans/ --type invoice [--json]
|
|
98
98
|
clio jobs document-collection ingest --source ./scans/ --type credit-note-customer [--json]
|
|
99
99
|
|
|
100
|
-
# Scan + upload (
|
|
101
|
-
clio jobs document-collection ingest --source ./bank-docs/ --upload --bank-account "DBS Checking" --
|
|
100
|
+
# Scan + upload (encrypted PDFs with password embedded in filename)
|
|
101
|
+
clio jobs document-collection ingest --source ./bank-docs/ --upload --bank-account "DBS Checking" --json
|
|
102
|
+
# To decrypt: rename encrypted PDF to: receipt__pw__actualPassword.pdf
|
|
103
|
+
# The __pw__ delimiter is case-insensitive; the password itself is case-sensitive.
|
|
102
104
|
```
|
|
103
105
|
|
|
104
106
|
### Options
|
|
@@ -109,12 +111,21 @@ clio jobs document-collection ingest --source ./bank-docs/ --upload --bank-accou
|
|
|
109
111
|
| `--type <type>` | Force all files to: `invoice`, `bill`, `credit-note-customer`, `credit-note-supplier`, or `bank-statement` |
|
|
110
112
|
| `--upload` | Upload classified files to Jaz after scanning (requires auth) |
|
|
111
113
|
| `--bank-account <name-or-id>` | Bank account name or resourceId (required for bank statements) |
|
|
112
|
-
| `--pdf-password <password>` | Password for encrypted PDFs — same password applied to all. Requires `qpdf` installed (`brew install qpdf`) |
|
|
113
114
|
| `--api-key <key>` | API key for upload (or use `JAZ_API_KEY` env var) |
|
|
114
115
|
| `--timeout <ms>` | Download timeout in milliseconds (default: 30000 for files, 120000 for folders) |
|
|
115
116
|
| `--currency <code>` | Functional/reporting currency label |
|
|
116
117
|
| `--json` | Structured JSON output with absolute file paths |
|
|
117
118
|
|
|
119
|
+
### Encrypted PDF Passwords
|
|
120
|
+
|
|
121
|
+
Embed the password in the filename using the `__pw__` pattern:
|
|
122
|
+
```
|
|
123
|
+
receipt__pw__s3cRetP@ss.pdf → password: "s3cRetP@ss", display name: "receipt.pdf"
|
|
124
|
+
```
|
|
125
|
+
- `__pw__` is case-insensitive (`__PW__`, `__Pw__`, etc.)
|
|
126
|
+
- The password after `__pw__` is case-sensitive
|
|
127
|
+
- Requires `qpdf` installed (`brew install qpdf`)
|
|
128
|
+
|
|
118
129
|
### JSON Output
|
|
119
130
|
|
|
120
131
|
The `--json` output includes absolute file paths, classification, and size for each file. The AI agent uses these paths to upload via the api skill.
|
|
@@ -171,7 +182,7 @@ sudo apt install qpdf # Ubuntu/Debian
|
|
|
171
182
|
choco install qpdf # Windows
|
|
172
183
|
```
|
|
173
184
|
|
|
174
|
-
|
|
185
|
+
Passwords are embedded in the filename using the `__pw__` pattern: `receipt__pw__myPass.pdf`. During upload, encrypted PDFs with a filename password are decrypted to a temp file, uploaded, then cleaned up.
|
|
175
186
|
|
|
176
187
|
### Error Handling (JSON mode)
|
|
177
188
|
|
|
@@ -180,7 +191,7 @@ If encrypted PDFs are found during `--upload` without the required dependencies,
|
|
|
180
191
|
| Error Code | Condition | Action |
|
|
181
192
|
|------------|-----------|--------|
|
|
182
193
|
| `ENCRYPTED_PDF_NO_QPDF` | qpdf not installed | Install qpdf, then retry |
|
|
183
|
-
| `ENCRYPTED_PDF_NO_PASSWORD` |
|
|
194
|
+
| `ENCRYPTED_PDF_NO_PASSWORD` | Encrypted PDF without `__pw__` in filename | Rename file to embed password: `name__pw__password.pdf` |
|
|
184
195
|
|
|
185
196
|
## Phases (Blueprint)
|
|
186
197
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: jaz-recipes
|
|
3
|
-
version: 4.
|
|
3
|
+
version: 4.5.0
|
|
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 10 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.
|
|
@@ -51,7 +51,7 @@ export function apiAction(fn) {
|
|
|
51
51
|
process.exit(3);
|
|
52
52
|
}
|
|
53
53
|
if (err instanceof JazApiError) {
|
|
54
|
-
console.error(chalk.red(`API Error (${err.status}): ${err.
|
|
54
|
+
console.error(chalk.red(`API Error (${err.status}): ${err.message}`));
|
|
55
55
|
process.exit(2);
|
|
56
56
|
}
|
|
57
57
|
console.error(chalk.red(`Error: ${err.message}`));
|
package/dist/commands/bills.js
CHANGED
|
@@ -5,7 +5,7 @@ import { apiAction } from './api-action.js';
|
|
|
5
5
|
import { resolveContactFlag, resolveAccountFlag, resolveTaxProfileFlag } from './resolve.js';
|
|
6
6
|
import { parsePositiveInt, parseNonNegativeInt, parseMoney, parseRate, parseLineItems, readBodyInput, requireFields } from './parsers.js';
|
|
7
7
|
import { paginatedFetch, paginatedJson, displaySlice } from './pagination.js';
|
|
8
|
-
import { BILL_REQUIRED_FIELDS, buildDraftReport, formatDraftTable, addDraftFinalizeOptions, mergeDraftFlags, validateDraft, buildValidation, } from './draft-helpers.js';
|
|
8
|
+
import { BILL_REQUIRED_FIELDS, buildDraftReport, formatDraftTable, addDraftFinalizeOptions, mergeDraftFlags, validateDraft, buildValidation, normalizeDate, sanitizeLineItem, } from './draft-helpers.js';
|
|
9
9
|
export function registerBillsCommand(program) {
|
|
10
10
|
const bills = program
|
|
11
11
|
.command('bills')
|
|
@@ -448,12 +448,40 @@ export function registerBillsCommand(program) {
|
|
|
448
448
|
else {
|
|
449
449
|
updateData = mergeDraftFlags(bill, resolvedOpts);
|
|
450
450
|
}
|
|
451
|
+
// Track which fields the user explicitly changed (before we build the full PUT body)
|
|
452
|
+
const userChangedFields = Object.keys(updateData);
|
|
451
453
|
// 4. Validate after merge — apply updates to a virtual copy
|
|
452
454
|
const merged = { ...bill, ...updateData };
|
|
453
455
|
if (updateData.lineItems) {
|
|
454
456
|
merged.lineItems = updateData.lineItems;
|
|
455
457
|
}
|
|
456
458
|
const { missingFields, missingCount, ready } = validateDraft(merged, BILL_REQUIRED_FIELDS);
|
|
459
|
+
// 4b. Build a full PUT-compatible body from merged state.
|
|
460
|
+
// The API PUT requires: reference, contactResourceId, dates (YYYY-MM-DD),
|
|
461
|
+
// and line items in create-request format (not GET response shape).
|
|
462
|
+
if (!body) {
|
|
463
|
+
const fullBody = {
|
|
464
|
+
reference: merged.reference || undefined,
|
|
465
|
+
contactResourceId: merged.contactResourceId,
|
|
466
|
+
valueDate: normalizeDate(updateData.valueDate) || normalizeDate(bill.valueDate),
|
|
467
|
+
dueDate: normalizeDate(updateData.dueDate) || normalizeDate(bill.dueDate),
|
|
468
|
+
};
|
|
469
|
+
// Sanitize line items: strip response-only fields, normalize account field name
|
|
470
|
+
const rawItems = (updateData.lineItems ?? bill.lineItems ?? []); // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
471
|
+
fullBody.lineItems = rawItems.map((li) => sanitizeLineItem(li)); // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
472
|
+
// Carry over tax settings
|
|
473
|
+
if (updateData.isTaxVatApplicable != null || bill.isTaxVATApplicable != null) {
|
|
474
|
+
fullBody.isTaxVATApplicable = updateData.isTaxVatApplicable ?? bill.isTaxVATApplicable;
|
|
475
|
+
}
|
|
476
|
+
if (updateData.taxInclusion != null || bill.taxInclusion != null) {
|
|
477
|
+
fullBody.taxInclusion = updateData.taxInclusion ?? bill.taxInclusion;
|
|
478
|
+
}
|
|
479
|
+
if (updateData.notes)
|
|
480
|
+
fullBody.notes = updateData.notes;
|
|
481
|
+
if (updateData.tag)
|
|
482
|
+
fullBody.tag = updateData.tag;
|
|
483
|
+
updateData = fullBody;
|
|
484
|
+
}
|
|
457
485
|
// 5. Dry run: just report validation
|
|
458
486
|
if (opts.dryRun) {
|
|
459
487
|
const validation = buildValidation(merged, BILL_REQUIRED_FIELDS);
|
|
@@ -508,10 +536,12 @@ export function registerBillsCommand(program) {
|
|
|
508
536
|
process.exit(1);
|
|
509
537
|
}
|
|
510
538
|
// 7. Finalize: PUT with saveAsDraft=false
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const
|
|
539
|
+
await finalizeBill(client, resourceId, updateData);
|
|
540
|
+
// 8. GET the finalized bill to confirm status (PUT response may not include status)
|
|
541
|
+
const verifyRes = await getBill(client, resourceId);
|
|
542
|
+
const updated = verifyRes.data;
|
|
543
|
+
// Report which fields the user explicitly changed (not the full PUT body)
|
|
544
|
+
const fieldsUpdated = userChangedFields.filter((k) => k !== 'saveAsDraft' && k !== 'resourceId');
|
|
515
545
|
if (opts.json) {
|
|
516
546
|
console.log(JSON.stringify({
|
|
517
547
|
finalized: true,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { listCustomerCreditNotes, getCustomerCreditNote, searchCustomerCreditNotes, createCustomerCreditNote, updateCustomerCreditNote, deleteCustomerCreditNote, createCustomerCreditNoteRefund, listCustomerCreditNoteRefunds, } from '../core/api/customer-cn.js';
|
|
2
|
+
import { listCustomerCreditNotes, getCustomerCreditNote, searchCustomerCreditNotes, createCustomerCreditNote, updateCustomerCreditNote, deleteCustomerCreditNote, createCustomerCreditNoteRefund, listCustomerCreditNoteRefunds, finalizeCustomerCreditNote, } from '../core/api/customer-cn.js';
|
|
3
|
+
import { listAttachments } from '../core/api/attachments.js';
|
|
3
4
|
import { apiAction } from './api-action.js';
|
|
4
|
-
import { resolveContactFlag, resolveAccountFlag } from './resolve.js';
|
|
5
|
+
import { resolveContactFlag, resolveAccountFlag, resolveTaxProfileFlag } from './resolve.js';
|
|
5
6
|
import { parsePositiveInt, parseNonNegativeInt, parseMoney, parseRate, parseLineItems, readBodyInput, requireFields } from './parsers.js';
|
|
6
7
|
import { paginatedFetch, paginatedJson, displaySlice } from './pagination.js';
|
|
8
|
+
import { CREDIT_NOTE_REQUIRED_FIELDS, buildDraftReport, formatDraftTable, addDraftFinalizeOptions, mergeDraftFlags, validateDraft, buildValidation, normalizeDate, sanitizeLineItem, } from './draft-helpers.js';
|
|
7
9
|
export function registerCustomerCreditNotesCommand(program) {
|
|
8
10
|
const ccn = program
|
|
9
11
|
.command('customer-credit-notes')
|
|
@@ -301,4 +303,211 @@ export function registerCustomerCreditNotesCommand(program) {
|
|
|
301
303
|
}
|
|
302
304
|
}
|
|
303
305
|
})(opts));
|
|
306
|
+
// ── clio customer-credit-notes draft ──────────────────────────
|
|
307
|
+
const draft = ccn
|
|
308
|
+
.command('draft')
|
|
309
|
+
.description('Manage draft customer credit notes (inspect, finalize, attachments)');
|
|
310
|
+
// ── clio customer-credit-notes draft list ─────────────────────
|
|
311
|
+
draft
|
|
312
|
+
.command('list')
|
|
313
|
+
.description('List all draft customer credit notes with per-field validation status')
|
|
314
|
+
.option('--ids <ids>', 'Comma-separated IDs to inspect (instead of all drafts)')
|
|
315
|
+
.option('--max-rows <n>', 'Max drafts to fetch (default 10000)', parsePositiveInt)
|
|
316
|
+
.option('--api-key <key>', 'API key (overrides stored/env)')
|
|
317
|
+
.option('--json', 'Output as JSON')
|
|
318
|
+
.action(apiAction(async (client, opts) => {
|
|
319
|
+
let items; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
320
|
+
if (opts.ids) {
|
|
321
|
+
const ids = opts.ids.split(',').map((s) => s.trim()).filter(Boolean);
|
|
322
|
+
items = await Promise.all(ids.map(async (id) => {
|
|
323
|
+
const res = await getCustomerCreditNote(client, id);
|
|
324
|
+
return res.data;
|
|
325
|
+
}));
|
|
326
|
+
items = items.filter((cn) => cn.status === 'DRAFT');
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
const result = await paginatedFetch({ all: true, json: true, maxRows: opts.maxRows }, ({ limit, offset }) => searchCustomerCreditNotes(client, {
|
|
330
|
+
filter: { status: { eq: 'DRAFT' } },
|
|
331
|
+
limit,
|
|
332
|
+
offset,
|
|
333
|
+
sort: { sortBy: ['valueDate'], order: 'DESC' },
|
|
334
|
+
}), { label: 'Fetching draft customer credit notes' });
|
|
335
|
+
items = result.data;
|
|
336
|
+
}
|
|
337
|
+
const BATCH_SIZE = 5;
|
|
338
|
+
const reports = [];
|
|
339
|
+
for (let i = 0; i < items.length; i += BATCH_SIZE) {
|
|
340
|
+
const batch = items.slice(i, i + BATCH_SIZE);
|
|
341
|
+
const batchReports = await Promise.all(batch.map(async (cn) => {
|
|
342
|
+
let attachmentCount = 0;
|
|
343
|
+
try {
|
|
344
|
+
const attRes = await listAttachments(client, 'customer-credit-notes', cn.resourceId);
|
|
345
|
+
attachmentCount = Array.isArray(attRes.data) ? attRes.data.length : 0;
|
|
346
|
+
}
|
|
347
|
+
catch { /* Attachment listing may fail — don't block the report */ }
|
|
348
|
+
return buildDraftReport(cn, CREDIT_NOTE_REQUIRED_FIELDS, attachmentCount);
|
|
349
|
+
}));
|
|
350
|
+
reports.push(...batchReports);
|
|
351
|
+
}
|
|
352
|
+
if (opts.json) {
|
|
353
|
+
const readyCount = reports.filter((r) => r.ready).length;
|
|
354
|
+
console.log(JSON.stringify({
|
|
355
|
+
totalDrafts: reports.length,
|
|
356
|
+
readyCount,
|
|
357
|
+
needsAttentionCount: reports.length - readyCount,
|
|
358
|
+
drafts: reports,
|
|
359
|
+
}, null, 2));
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
formatDraftTable('Customer Credit Notes', reports);
|
|
363
|
+
}
|
|
364
|
+
}));
|
|
365
|
+
// ── clio customer-credit-notes draft finalize ─────────────────
|
|
366
|
+
addDraftFinalizeOptions(draft
|
|
367
|
+
.command('finalize <resourceId>')
|
|
368
|
+
.description('Fill missing fields and convert a draft customer credit note to APPROVED')).action((resourceId, opts) => apiAction(async (client) => {
|
|
369
|
+
const body = readBodyInput(opts);
|
|
370
|
+
// 1. GET CURRENT DRAFT
|
|
371
|
+
const res = await getCustomerCreditNote(client, resourceId);
|
|
372
|
+
const cn = res.data;
|
|
373
|
+
if (cn.status !== 'DRAFT') {
|
|
374
|
+
const msg = `Customer credit note ${resourceId} is ${cn.status}, not DRAFT.`;
|
|
375
|
+
if (opts.json) {
|
|
376
|
+
console.log(JSON.stringify({ finalized: false, error: msg }));
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
console.error(chalk.red(msg));
|
|
380
|
+
}
|
|
381
|
+
process.exit(1);
|
|
382
|
+
}
|
|
383
|
+
// 2. RESOLVE NAMES → UUIDs
|
|
384
|
+
const resolvedOpts = { ...opts }; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
385
|
+
if (opts.contact) {
|
|
386
|
+
const resolved = await resolveContactFlag(client, opts.contact, { silent: opts.json });
|
|
387
|
+
resolvedOpts.contact = resolved.resourceId;
|
|
388
|
+
}
|
|
389
|
+
if (opts.account) {
|
|
390
|
+
const resolved = await resolveAccountFlag(client, opts.account, { filter: 'line-item', silent: opts.json });
|
|
391
|
+
resolvedOpts.account = resolved.resourceId;
|
|
392
|
+
}
|
|
393
|
+
if (opts.taxProfile) {
|
|
394
|
+
const resolved = await resolveTaxProfileFlag(client, opts.taxProfile, { silent: opts.json });
|
|
395
|
+
resolvedOpts.taxProfile = resolved.resourceId;
|
|
396
|
+
}
|
|
397
|
+
// 3. MERGE FLAGS WITH EXISTING VALUES
|
|
398
|
+
let updateData;
|
|
399
|
+
if (body) {
|
|
400
|
+
updateData = body;
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
updateData = mergeDraftFlags(cn, resolvedOpts);
|
|
404
|
+
}
|
|
405
|
+
const userChangedFields = Object.keys(updateData);
|
|
406
|
+
// 4. VALIDATE AFTER MERGE
|
|
407
|
+
const merged = { ...cn, ...updateData };
|
|
408
|
+
if (updateData.lineItems) {
|
|
409
|
+
merged.lineItems = updateData.lineItems;
|
|
410
|
+
}
|
|
411
|
+
const { missingFields, missingCount, ready } = validateDraft(merged, CREDIT_NOTE_REQUIRED_FIELDS);
|
|
412
|
+
// 4b. BUILD FULL PUT-COMPATIBLE BODY (no dueDate for credit notes)
|
|
413
|
+
if (!body) {
|
|
414
|
+
const fullBody = {
|
|
415
|
+
reference: merged.reference || undefined,
|
|
416
|
+
contactResourceId: merged.contactResourceId,
|
|
417
|
+
valueDate: normalizeDate(updateData.valueDate) || normalizeDate(cn.valueDate),
|
|
418
|
+
};
|
|
419
|
+
const rawItems = (updateData.lineItems ?? cn.lineItems ?? []); // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
420
|
+
fullBody.lineItems = rawItems.map((li) => sanitizeLineItem(li)); // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
421
|
+
if (updateData.isTaxVatApplicable != null || cn.isTaxVATApplicable != null) {
|
|
422
|
+
fullBody.isTaxVATApplicable = updateData.isTaxVatApplicable ?? cn.isTaxVATApplicable;
|
|
423
|
+
}
|
|
424
|
+
if (updateData.taxInclusion != null || cn.taxInclusion != null) {
|
|
425
|
+
fullBody.taxInclusion = updateData.taxInclusion ?? cn.taxInclusion;
|
|
426
|
+
}
|
|
427
|
+
if (updateData.notes)
|
|
428
|
+
fullBody.notes = updateData.notes;
|
|
429
|
+
if (updateData.tag)
|
|
430
|
+
fullBody.tag = updateData.tag;
|
|
431
|
+
updateData = fullBody;
|
|
432
|
+
}
|
|
433
|
+
// 5. DRY RUN
|
|
434
|
+
if (opts.dryRun) {
|
|
435
|
+
const validation = buildValidation(merged, CREDIT_NOTE_REQUIRED_FIELDS);
|
|
436
|
+
if (opts.json) {
|
|
437
|
+
console.log(JSON.stringify({ finalized: false, dryRun: true, resourceId, reference: merged.reference || null, ready, missingCount, missingFields, validation }, null, 2));
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
if (ready) {
|
|
441
|
+
console.log(chalk.green(`✓ ${merged.reference || resourceId} is ready to finalize.`));
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
console.log(chalk.yellow(`✗ ${merged.reference || resourceId} — ${missingCount} issue${missingCount > 1 ? 's' : ''} remaining:`));
|
|
445
|
+
for (const f of missingFields) {
|
|
446
|
+
const spec = CREDIT_NOTE_REQUIRED_FIELDS.find((s) => f === s.field || f.endsWith(`.${s.field}`));
|
|
447
|
+
console.log(` ${f}: ${chalk.red('MISSING')} — ${spec?.hint ?? ''}`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
// 6. VALIDATION FAILURE
|
|
454
|
+
if (!ready) {
|
|
455
|
+
const validation = buildValidation(merged, CREDIT_NOTE_REQUIRED_FIELDS);
|
|
456
|
+
if (opts.json) {
|
|
457
|
+
console.log(JSON.stringify({ finalized: false, resourceId, reference: merged.reference || null, ready: false, missingCount, missingFields, validation }, null, 2));
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
console.error(chalk.red(`\n✗ Cannot finalize ${merged.reference || resourceId} — ${missingCount} issue${missingCount > 1 ? 's' : ''} remaining:\n`));
|
|
461
|
+
for (const f of missingFields) {
|
|
462
|
+
const spec = CREDIT_NOTE_REQUIRED_FIELDS.find((s) => f === s.field || f.endsWith(`.${s.field}`));
|
|
463
|
+
console.error(` ${f}: ${chalk.red('MISSING')} — ${spec?.hint ?? ''}`);
|
|
464
|
+
}
|
|
465
|
+
console.error(chalk.dim(`\n Fix the issues and retry:\n clio customer-credit-notes draft finalize ${resourceId} ...\n`));
|
|
466
|
+
}
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
// 7. FINALIZE
|
|
470
|
+
await finalizeCustomerCreditNote(client, resourceId, updateData);
|
|
471
|
+
// 8. VERIFY
|
|
472
|
+
const verifyRes = await getCustomerCreditNote(client, resourceId);
|
|
473
|
+
const updated = verifyRes.data;
|
|
474
|
+
const fieldsUpdated = userChangedFields.filter((k) => k !== 'saveAsDraft' && k !== 'resourceId');
|
|
475
|
+
if (opts.json) {
|
|
476
|
+
console.log(JSON.stringify({ finalized: true, resourceId: updated.resourceId, reference: updated.reference || null, status: updated.status, fieldsUpdated }, null, 2));
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
console.log(chalk.green(`\n✓ Customer credit note finalized: ${updated.reference || updated.resourceId}`));
|
|
480
|
+
console.log(chalk.bold(' Status:'), updated.status);
|
|
481
|
+
if (fieldsUpdated.length > 0) {
|
|
482
|
+
console.log(chalk.bold(' Updated:'), fieldsUpdated.join(', '));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
})(opts));
|
|
486
|
+
// ── clio customer-credit-notes draft attachments ──────────────
|
|
487
|
+
draft
|
|
488
|
+
.command('attachments <resourceId>')
|
|
489
|
+
.description('List attachments for a customer credit note (URLs for agent inspection)')
|
|
490
|
+
.option('--api-key <key>', 'API key (overrides stored/env)')
|
|
491
|
+
.option('--json', 'Output as JSON')
|
|
492
|
+
.action((resourceId, opts) => apiAction(async (client) => {
|
|
493
|
+
const cnRes = await getCustomerCreditNote(client, resourceId);
|
|
494
|
+
const cn = cnRes.data;
|
|
495
|
+
const attRes = await listAttachments(client, 'customer-credit-notes', resourceId);
|
|
496
|
+
const attachments = Array.isArray(attRes.data) ? attRes.data : [];
|
|
497
|
+
if (opts.json) {
|
|
498
|
+
console.log(JSON.stringify({ creditNoteResourceId: resourceId, creditNoteReference: cn.reference || null, attachments }, null, 2));
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
if (attachments.length === 0) {
|
|
502
|
+
console.log(chalk.yellow(`No attachments for customer credit note ${cn.reference || resourceId}.`));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
console.log(chalk.bold(`Attachments for ${cn.reference || resourceId}:\n`));
|
|
506
|
+
for (const att of attachments) {
|
|
507
|
+
console.log(` ${chalk.cyan(att.resourceId)} ${att.fileName || '(unnamed)'}`);
|
|
508
|
+
if (att.fileUrl)
|
|
509
|
+
console.log(` ${chalk.dim(att.fileUrl)}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
})(opts));
|
|
304
513
|
}
|