jaz-clio 3.3.0 → 3.4.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 +1 -1
- package/assets/skills/conversion/SKILL.md +1 -1
- package/assets/skills/jobs/SKILL.md +4 -2
- package/assets/skills/jobs/references/bank-recon.md +1 -1
- package/assets/skills/jobs/references/document-collection.md +95 -78
- package/assets/skills/transaction-recipes/SKILL.md +1 -5
- package/dist/commands/jobs.js +12 -30
- package/dist/index.js +2 -1
- package/dist/jobs/document-collection/blueprint.js +8 -9
- package/dist/jobs/document-collection/tools/ingest/format.js +2 -38
- package/dist/jobs/document-collection/tools/ingest/ingest.js +20 -119
- package/dist/jobs/document-collection/tools/ingest/scanner.js +7 -3
- package/package.json +1 -1
- package/dist/jobs/document-collection/tools/ingest/magic-upload.js +0 -142
- /package/assets/skills/{transaction-recipes → jobs}/references/bank-match.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: api
|
|
3
|
-
version: 3.
|
|
3
|
+
version: 3.4.0
|
|
4
4
|
description: Complete reference for the Jaz/Juan 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/Juan API key (x-jk-api-key header). Works with Claude Code, Claude Cowork, Claude.ai, and any agent that reads markdown.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: conversion
|
|
3
|
-
version: 3.
|
|
3
|
+
version: 3.4.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: jobs
|
|
3
|
-
version: 3.
|
|
3
|
+
version: 3.4.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 transaction-recipes skill.
|
|
@@ -41,7 +41,7 @@ Period-close jobs build on each other. Quarter = month + extras. Year = quarter
|
|
|
41
41
|
| Job | CLI Command | Description |
|
|
42
42
|
|-----|-------------|-------------|
|
|
43
43
|
| **Bank Recon** | `clio jobs bank-recon` | Clear unreconciled items: match, categorize, resolve. Paired tool: `clio jobs bank-recon match`. |
|
|
44
|
-
| **Document Collection** | `clio jobs document-collection` | Scan
|
|
44
|
+
| **Document Collection** | `clio jobs document-collection` | Scan and classify client documents from local directories and cloud links (Dropbox, Drive, OneDrive). Outputs file paths for agent upload. Paired tool: `clio jobs document-collection ingest`. |
|
|
45
45
|
| **GST/VAT Filing** | `clio jobs gst-vat --period YYYY-QN` | Tax ledger review, discrepancy check, filing summary. |
|
|
46
46
|
| **Payment Run** | `clio jobs payment-run` | Select outstanding bills by due date, process payments. |
|
|
47
47
|
| **Credit Control** | `clio jobs credit-control` | AR aging review, overdue chase list, bad debt assessment. |
|
|
@@ -99,6 +99,8 @@ clio jobs fa-review [--json]
|
|
|
99
99
|
- **[references/quarter-end-close.md](./references/quarter-end-close.md)** — Quarter-end close: monthly + quarterly extras
|
|
100
100
|
- **[references/year-end-close.md](./references/year-end-close.md)** — Year-end close: quarterly + annual extras
|
|
101
101
|
- **[references/bank-recon.md](./references/bank-recon.md)** — Bank reconciliation catch-up
|
|
102
|
+
- **[references/bank-match.md](./references/bank-match.md)** — Bank reconciliation matcher: 5-phase cascade algorithm (1:1, N:1, 1:N, N:M matches)
|
|
103
|
+
- **[references/document-collection.md](./references/document-collection.md)** — Document collection: scan, classify, upload — local + cloud (Dropbox, Drive, OneDrive)
|
|
102
104
|
- **[references/gst-vat-filing.md](./references/gst-vat-filing.md)** — GST/VAT filing preparation
|
|
103
105
|
- **[references/payment-run.md](./references/payment-run.md)** — Payment run (bulk bill payments)
|
|
104
106
|
- **[references/credit-control.md](./references/credit-control.md)** — Credit control / AR chase
|
|
@@ -79,7 +79,7 @@ Work through each unreconciled item. There are four resolution paths.
|
|
|
79
79
|
|
|
80
80
|
The bank record matches an invoice payment, bill payment, or journal already in the books. This is the ideal case — the transaction exists, it just hasn't been linked to the bank record.
|
|
81
81
|
|
|
82
|
-
**For large volumes:** Use `clio jobs bank-recon match --input data.json --json` to auto-match bank records to transactions before manual review. The calculator finds 1:1, N:1, 1:N, and N:M matches with confidence scores. See the [bank-match reference](
|
|
82
|
+
**For large volumes:** Use `clio jobs bank-recon match --input data.json --json` to auto-match bank records to transactions before manual review. The calculator finds 1:1, N:1, 1:N, and N:M matches with confidence scores. See the [bank-match reference](./bank-match.md) for input format and algorithm details.
|
|
83
83
|
|
|
84
84
|
**How to find the match manually:** Search cashflow transactions for the same amount and approximate date:
|
|
85
85
|
|
|
@@ -1,42 +1,45 @@
|
|
|
1
1
|
# Document Collection
|
|
2
2
|
|
|
3
|
-
Scan
|
|
3
|
+
Scan and classify client documents (invoices, bills, bank statements) from local directories or cloud share links (Dropbox, Google Drive, OneDrive). Outputs classified file paths with metadata — the AI agent handles uploads via the api skill.
|
|
4
4
|
|
|
5
5
|
## When to Use
|
|
6
6
|
|
|
7
|
-
- Client sends a folder of PDFs (invoices, bills, receipts) for bulk
|
|
7
|
+
- Client sends a folder of PDFs (invoices, bills, receipts) for bulk processing
|
|
8
8
|
- Processing bank statement CSVs/OFX files for import
|
|
9
9
|
- Migrating documents from file dumps (Dropbox, shared folders, email attachments)
|
|
10
|
-
-
|
|
10
|
+
- Processing documents from a shared Dropbox, Google Drive, or OneDrive link
|
|
11
|
+
- Batch-processing scanned documents during onboarding
|
|
11
12
|
|
|
12
13
|
## How It Works
|
|
13
14
|
|
|
14
15
|
```
|
|
15
|
-
|
|
16
|
-
┌───────────────┐
|
|
17
|
-
│ invoices/
|
|
18
|
-
│ inv-001.pdf │
|
|
19
|
-
│ inv-002.jpg │
|
|
20
|
-
│
|
|
21
|
-
│
|
|
22
|
-
│
|
|
23
|
-
│
|
|
24
|
-
|
|
25
|
-
│
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
Source (local or cloud) CLI Output (IngestPlan)
|
|
17
|
+
┌───────────────┐ ┌──────────────────────────────────────────┐
|
|
18
|
+
│ invoices/ │── scan + classify ─► absolutePath, documentType: INVOICE │
|
|
19
|
+
│ inv-001.pdf │ │ sizeBytes: 45230 │
|
|
20
|
+
│ inv-002.jpg │ │ │
|
|
21
|
+
│ bills/ │── scan + classify ─► absolutePath, documentType: BILL │
|
|
22
|
+
│ acme-jan.pdf│ │ │
|
|
23
|
+
│ bank/ │── scan + classify ─► absolutePath, documentType: BANK_STATEMENT│
|
|
24
|
+
│ dbs-jan.csv │ │ │
|
|
25
|
+
└───────────────┘ └──────────────────────────────────────────┘
|
|
26
|
+
│
|
|
27
|
+
AI Agent reads plan,
|
|
28
|
+
uploads via api skill (curl)
|
|
28
29
|
```
|
|
29
30
|
|
|
31
|
+
Cloud links are downloaded to a temp directory first, then scanned through the same pipeline.
|
|
32
|
+
|
|
30
33
|
## Folder Classification
|
|
31
34
|
|
|
32
35
|
The tool auto-classifies documents by **folder name** (case-insensitive prefix match):
|
|
33
36
|
|
|
34
|
-
| Folder name pattern | Classification |
|
|
35
|
-
|
|
36
|
-
| `invoice*`, `sales*`, `ar*`, `receivable*`, `revenue*` | INVOICE |
|
|
37
|
-
| `bill*`, `purchase*`, `expense*`, `ap*`, `payable*`, `supplier*`, `vendor*`, `cost*` | BILL |
|
|
38
|
-
| `bank*`, `statement*`, `recon*` | BANK_STATEMENT |
|
|
39
|
-
| (unknown) | UNKNOWN — skipped unless `--type` forced |
|
|
37
|
+
| Folder name pattern | Classification |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `invoice*`, `sales*`, `ar*`, `receivable*`, `revenue*` | INVOICE |
|
|
40
|
+
| `bill*`, `purchase*`, `expense*`, `ap*`, `payable*`, `supplier*`, `vendor*`, `cost*` | BILL |
|
|
41
|
+
| `bank*`, `statement*`, `recon*` | BANK_STATEMENT |
|
|
42
|
+
| (unknown) | UNKNOWN — skipped unless `--type` forced |
|
|
40
43
|
|
|
41
44
|
### File Extension Filters
|
|
42
45
|
|
|
@@ -51,87 +54,101 @@ The tool auto-classifies documents by **folder name** (case-insensitive prefix m
|
|
|
51
54
|
3. **Max depth** — 10 levels (prevents runaway recursion)
|
|
52
55
|
4. **Hidden files/dirs** — skipped (anything starting with `.`)
|
|
53
56
|
|
|
57
|
+
## Cloud Provider Support
|
|
58
|
+
|
|
59
|
+
The `--source` flag accepts public share links from Dropbox, Google Drive, and OneDrive. Files are downloaded to a temp directory, then processed through the same scan/classify pipeline as local directories.
|
|
60
|
+
|
|
61
|
+
| Provider | File Links | Folder Links | Auth Required |
|
|
62
|
+
|----------|-----------|--------------|---------------|
|
|
63
|
+
| **Dropbox** | Direct download (dl=1 trick) | ZIP download + extract | No |
|
|
64
|
+
| **Google Drive** | Direct download (large file confirmation) | Not supported (requires API key) | No |
|
|
65
|
+
| **OneDrive/SharePoint** | MS Graph sharing API | MS Graph sharing API (first page only) | No (best-effort) |
|
|
66
|
+
|
|
67
|
+
### Cloud Limitations
|
|
68
|
+
|
|
69
|
+
- **Google Drive folders** require authentication — download manually and use a local path
|
|
70
|
+
- **OneDrive** is best-effort: Microsoft has restricted public link access since Feb 2025
|
|
71
|
+
- **Dropbox folders** download as ZIP — extracted automatically, macOS metadata stripped
|
|
72
|
+
- **Max file size**: 100MB per file, 500MB total for folder downloads
|
|
73
|
+
- **Timeout**: Default 30s (files) / 120s (folders). Override with `--timeout <ms>`
|
|
74
|
+
|
|
54
75
|
## CLI Usage
|
|
55
76
|
|
|
56
77
|
```bash
|
|
57
|
-
#
|
|
78
|
+
# Scan + classify local directory
|
|
58
79
|
clio jobs document-collection ingest --source ./client-docs/ [--json]
|
|
59
80
|
|
|
60
|
-
#
|
|
61
|
-
clio jobs document-collection ingest --source
|
|
62
|
-
|
|
81
|
+
# Cloud sources — Dropbox, Google Drive, OneDrive
|
|
82
|
+
clio jobs document-collection ingest --source "https://www.dropbox.com/scl/fo/.../folder?rlkey=..." [--json]
|
|
83
|
+
clio jobs document-collection ingest --source "https://drive.google.com/file/d/FILE_ID/view" [--json]
|
|
84
|
+
clio jobs document-collection ingest --source "https://1drv.ms/f/s!..." [--json]
|
|
63
85
|
|
|
64
|
-
#
|
|
65
|
-
clio jobs document-collection ingest --source
|
|
66
|
-
--api-key <key> --api-url https://api.jaz.ai
|
|
86
|
+
# With timeout for large cloud downloads
|
|
87
|
+
clio jobs document-collection ingest --source "https://www.dropbox.com/..." --timeout 120000 [--json]
|
|
67
88
|
|
|
68
|
-
#
|
|
69
|
-
clio jobs document-collection ingest --source ./
|
|
70
|
-
--bank-account <resourceId> --api-key <key> --api-url https://api.jaz.ai
|
|
89
|
+
# Force classification (skip auto-detect)
|
|
90
|
+
clio jobs document-collection ingest --source ./scans/ --type invoice [--json]
|
|
71
91
|
```
|
|
72
92
|
|
|
73
93
|
### Options
|
|
74
94
|
|
|
75
95
|
| Flag | Description |
|
|
76
96
|
|------|-------------|
|
|
77
|
-
| `--source <path>` | Local directory path (required) |
|
|
97
|
+
| `--source <path\|url>` | Local directory path or public cloud share link — Dropbox, Google Drive, OneDrive (required) |
|
|
78
98
|
| `--type <type>` | Force all files to: `invoice`, `bill`, or `bank-statement` |
|
|
79
|
-
| `--
|
|
80
|
-
| `--
|
|
81
|
-
| `--
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
| `--timeout <ms>` | Download timeout in milliseconds (default: 30000 for files, 120000 for folders) |
|
|
100
|
+
| `--currency <code>` | Functional/reporting currency label |
|
|
101
|
+
| `--json` | Structured JSON output with absolute file paths |
|
|
102
|
+
|
|
103
|
+
### JSON Output
|
|
104
|
+
|
|
105
|
+
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.
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"source": "./client-docs/",
|
|
110
|
+
"sourceType": "local",
|
|
111
|
+
"localPath": "/tmp/client-docs",
|
|
112
|
+
"folders": [{
|
|
113
|
+
"folder": "invoices",
|
|
114
|
+
"documentType": "INVOICE",
|
|
115
|
+
"files": [{
|
|
116
|
+
"path": "invoices/inv-001.pdf",
|
|
117
|
+
"filename": "inv-001.pdf",
|
|
118
|
+
"extension": ".pdf",
|
|
119
|
+
"documentType": "INVOICE",
|
|
120
|
+
"absolutePath": "/tmp/client-docs/invoices/inv-001.pdf",
|
|
121
|
+
"sizeBytes": 45230,
|
|
122
|
+
"confidence": "auto",
|
|
123
|
+
"reason": "Folder \"invoices\" → INVOICE"
|
|
124
|
+
}],
|
|
125
|
+
"count": 1
|
|
126
|
+
}],
|
|
127
|
+
"summary": {
|
|
128
|
+
"total": 1,
|
|
129
|
+
"uploadable": 1,
|
|
130
|
+
"needClassification": 0,
|
|
131
|
+
"skipped": 0,
|
|
132
|
+
"byType": { "INVOICE": 1 }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
98
135
|
```
|
|
99
136
|
|
|
100
|
-
|
|
101
|
-
- Extraction is **asynchronous** — response confirms upload, extraction runs server-side
|
|
102
|
-
- `subscriptionFBPath` in response tracks extraction progress via Firebase
|
|
103
|
-
- Response maps: `INVOICE` → `SALE`, `BILL` → `PURCHASE`
|
|
104
|
-
- Jaz Magic extracts: line items, contact, CoA mapping, tax, dates, currency
|
|
105
|
-
|
|
106
|
-
### Bank Statements
|
|
107
|
-
|
|
108
|
-
```
|
|
109
|
-
POST /api/v1/magic/importBankStatementFromAttachment
|
|
110
|
-
Content-Type: multipart/form-data
|
|
111
|
-
Header: x-magic-api-key: <key>
|
|
112
|
-
|
|
113
|
-
Fields:
|
|
114
|
-
- sourceFile: CSV/OFX file (NOT "file")
|
|
115
|
-
- accountResourceId: bank account CoA resourceId
|
|
116
|
-
- businessTransactionType: "BANK_STATEMENT"
|
|
117
|
-
- sourceType: "FILE"
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
**CSV format:** `Date,Description,Debit,Credit`
|
|
137
|
+
For cloud sources, `localPath` points to the temp directory where files were downloaded.
|
|
121
138
|
|
|
122
139
|
## Phases (Blueprint)
|
|
123
140
|
|
|
124
|
-
When run without `ingest` subcommand, produces a
|
|
141
|
+
When run without `ingest` subcommand, produces a 4-phase blueprint:
|
|
125
142
|
|
|
126
143
|
1. **Intake** — Identify source, validate access
|
|
127
144
|
2. **Scan** — Traverse directory tree, list all files
|
|
128
145
|
3. **Classify** — Auto-classify by folder name
|
|
129
|
-
4. **Review** — Present plan for user
|
|
130
|
-
|
|
131
|
-
|
|
146
|
+
4. **Review** — Present plan for user/agent action
|
|
147
|
+
|
|
148
|
+
The AI agent then uses the classified file paths to upload via the Jaz Magic API (see api skill for endpoint details).
|
|
132
149
|
|
|
133
150
|
## Relationship to Other Skills
|
|
134
151
|
|
|
135
|
-
- **api skill** — Field names, auth headers, error codes for Magic endpoints
|
|
152
|
+
- **api skill** — Field names, auth headers, error codes for Magic endpoints. Agent uses this to upload classified files.
|
|
136
153
|
- **bank-recon job** — After bank statement import, use bank-recon to match and reconcile
|
|
137
154
|
- **transaction-recipes** — After Magic creates draft transactions, use recipes for complex accounting patterns
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: transaction-recipes
|
|
3
|
-
version: 3.
|
|
3
|
+
version: 3.4.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.
|
|
@@ -117,10 +117,6 @@ Each recipe includes: scenario description, accounts involved, journal entries,
|
|
|
117
117
|
|
|
118
118
|
16. **[Capital WIP to Fixed Asset](references/capital-wip.md)** — Cost accumulation in CIP account during construction/development, transfer to FA on completion, auto-depreciation via Jaz FA module.
|
|
119
119
|
|
|
120
|
-
### Utility — Reconciliation
|
|
121
|
-
|
|
122
|
-
17. **[Bank Reconciliation Matcher](references/bank-match.md)** — 5-phase cascade algorithm matching bank records to cashflow transactions. Finds 1:1, N:1, 1:N, and N:M matches with confidence scores. *Paired job tool: `clio jobs bank-recon match`*
|
|
123
|
-
|
|
124
120
|
## How to Use These Recipes
|
|
125
121
|
|
|
126
122
|
1. **Read the recipe** for your scenario — understand the accounts, journal entries, and capsule structure.
|
package/dist/commands/jobs.js
CHANGED
|
@@ -12,8 +12,8 @@ import { generateAuditPrepBlueprint } from '../jobs/audit-prep/blueprint.js';
|
|
|
12
12
|
import { generateFaReviewBlueprint } from '../jobs/fa-review/blueprint.js';
|
|
13
13
|
import { generateDocumentCollectionBlueprint } from '../jobs/document-collection/blueprint.js';
|
|
14
14
|
import { generateStatutoryFilingBlueprint } from '../jobs/statutory-filing/blueprint.js';
|
|
15
|
-
import {
|
|
16
|
-
import { printIngestPlan, printIngestPlanJson
|
|
15
|
+
import { ingest } from '../jobs/document-collection/tools/ingest/ingest.js';
|
|
16
|
+
import { printIngestPlan, printIngestPlanJson } from '../jobs/document-collection/tools/ingest/format.js';
|
|
17
17
|
import { matchBankRecords } from '../jobs/bank-recon/tools/match/match.js';
|
|
18
18
|
import { computeFormCs } from '../jobs/statutory-filing/tools/sg-tax/form-cs.js';
|
|
19
19
|
import { computeCapitalAllowances } from '../jobs/statutory-filing/tools/sg-tax/capital-allowances.js';
|
|
@@ -41,6 +41,7 @@ function jobAction(fn) {
|
|
|
41
41
|
export function registerJobsCommand(program) {
|
|
42
42
|
const jobs = program
|
|
43
43
|
.command('jobs')
|
|
44
|
+
.enablePositionalOptions()
|
|
44
45
|
.description('Accounting job blueprints — month-end, quarter-end, year-end, bank-recon, gst-vat, payment-run, credit-control, supplier-recon, audit-prep, fa-review, document-collection, statutory-filing');
|
|
45
46
|
// ── clio jobs month-end ──────────────────────────────────────────
|
|
46
47
|
jobs
|
|
@@ -92,6 +93,8 @@ export function registerJobsCommand(program) {
|
|
|
92
93
|
const bankRecon = jobs
|
|
93
94
|
.command('bank-recon')
|
|
94
95
|
.description('Bank reconciliation — blueprint + match tool')
|
|
96
|
+
.enablePositionalOptions()
|
|
97
|
+
.passThroughOptions()
|
|
95
98
|
.option('--account <name>', 'Specific bank account name')
|
|
96
99
|
.option('--period <YYYY-MM>', 'Month period to reconcile')
|
|
97
100
|
.option('--currency <code>', 'Currency code (e.g. SGD, USD)')
|
|
@@ -249,12 +252,12 @@ export function registerJobsCommand(program) {
|
|
|
249
252
|
const docCollection = jobs
|
|
250
253
|
.command('document-collection')
|
|
251
254
|
.description('Document collection — scan, classify, and upload client documents')
|
|
252
|
-
.
|
|
255
|
+
.enablePositionalOptions()
|
|
256
|
+
.passThroughOptions()
|
|
253
257
|
.option('--currency <code>', 'Currency code (e.g. SGD, USD)')
|
|
254
258
|
.option('--json', 'Output as JSON')
|
|
255
259
|
.action(jobAction((opts) => {
|
|
256
260
|
const bp = generateDocumentCollectionBlueprint({
|
|
257
|
-
source: opts.source,
|
|
258
261
|
currency: opts.currency,
|
|
259
262
|
});
|
|
260
263
|
opts.json ? printBlueprintJson(bp) : printBlueprint(bp);
|
|
@@ -262,13 +265,9 @@ export function registerJobsCommand(program) {
|
|
|
262
265
|
// ── clio jobs document-collection ingest ────────────────────────
|
|
263
266
|
docCollection
|
|
264
267
|
.command('ingest')
|
|
265
|
-
.description('Scan
|
|
268
|
+
.description('Scan and classify client documents — outputs file paths for agent upload')
|
|
266
269
|
.requiredOption('--source <path>', 'Local directory path or public share URL (Dropbox, Google Drive, OneDrive)')
|
|
267
270
|
.option('--type <type>', 'Force document type: invoice, bill, or bank-statement')
|
|
268
|
-
.option('--execute', 'Upload files to Jaz (without this flag, shows a preview)')
|
|
269
|
-
.option('--api-key <key>', 'Jaz Magic API key (required for --execute)')
|
|
270
|
-
.option('--api-url <url>', 'Jaz API base URL (required for --execute)')
|
|
271
|
-
.option('--bank-account <id>', 'Bank account CoA resourceId (required for bank statement uploads)')
|
|
272
271
|
.option('--timeout <ms>', 'Download timeout in milliseconds for cloud sources (default: 30000)', parseInt)
|
|
273
272
|
.option('--currency <code>', 'Functional/reporting currency (e.g. SGD)')
|
|
274
273
|
.option('--json', 'Output as JSON')
|
|
@@ -284,31 +283,12 @@ export function registerJobsCommand(program) {
|
|
|
284
283
|
if (opts.type && !forceType) {
|
|
285
284
|
throw new JobValidationError(`Invalid --type "${opts.type}". Use: invoice, bill, or bank-statement`);
|
|
286
285
|
}
|
|
287
|
-
|
|
286
|
+
ingest({
|
|
288
287
|
source: opts.source,
|
|
289
288
|
type: forceType,
|
|
290
|
-
execute: opts.execute,
|
|
291
|
-
apiKey: opts.apiKey,
|
|
292
|
-
apiUrl: opts.apiUrl,
|
|
293
|
-
bankAccount: opts.bankAccount,
|
|
294
289
|
currency: opts.currency,
|
|
295
290
|
timeout: opts.timeout,
|
|
296
|
-
}
|
|
297
|
-
if (ingestOpts.execute) {
|
|
298
|
-
validateExecuteOptions(ingestOpts);
|
|
299
|
-
// Execute mode — scan + classify + upload to Jaz Magic API
|
|
300
|
-
ingestExecute(ingestOpts).then((result) => {
|
|
301
|
-
opts.json ? printIngestResultJson(result) : printIngestResult(result);
|
|
302
|
-
if (result.summary.failed > 0)
|
|
303
|
-
process.exit(1);
|
|
304
|
-
}).catch((err) => {
|
|
305
|
-
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
306
|
-
process.exit(1);
|
|
307
|
-
});
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
|
-
// Dry-run mode — scan + classify (async for cloud sources)
|
|
311
|
-
ingestDryRun(ingestOpts).then((plan) => {
|
|
291
|
+
}).then((plan) => {
|
|
312
292
|
opts.json ? printIngestPlanJson(plan) : printIngestPlan(plan);
|
|
313
293
|
}).catch((err) => {
|
|
314
294
|
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
@@ -319,6 +299,8 @@ export function registerJobsCommand(program) {
|
|
|
319
299
|
const statFiling = jobs
|
|
320
300
|
.command('statutory-filing')
|
|
321
301
|
.description('Statutory filing — corporate income tax computation and filing')
|
|
302
|
+
.enablePositionalOptions()
|
|
303
|
+
.passThroughOptions()
|
|
322
304
|
.option('--ya <year>', 'Year of Assessment', parseInt)
|
|
323
305
|
.option('--jurisdiction <code>', 'Jurisdiction: sg (default)', 'sg')
|
|
324
306
|
.option('--currency <code>', 'Currency code (e.g. SGD)')
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,8 @@ const program = new Command();
|
|
|
16
16
|
program
|
|
17
17
|
.name('clio')
|
|
18
18
|
.description('Clio — Command Line Interface Orchestrator for Jaz AI')
|
|
19
|
-
.version(pkg.version)
|
|
19
|
+
.version(pkg.version)
|
|
20
|
+
.enablePositionalOptions();
|
|
20
21
|
program
|
|
21
22
|
.command('init')
|
|
22
23
|
.description('Install Jaz AI skills into the current project')
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import { buildSummary } from '../types.js';
|
|
7
7
|
export function generateDocumentCollectionBlueprint(opts = {}) {
|
|
8
8
|
const currency = opts.currency ?? 'SGD';
|
|
9
|
-
const source = opts.source ?? '(local folder or shared link)';
|
|
10
9
|
// Phase 1: Intake
|
|
11
10
|
const intake = {
|
|
12
11
|
name: 'Intake',
|
|
@@ -16,7 +15,7 @@ export function generateDocumentCollectionBlueprint(opts = {}) {
|
|
|
16
15
|
order: 1,
|
|
17
16
|
description: 'Identify document source',
|
|
18
17
|
category: 'verify',
|
|
19
|
-
notes:
|
|
18
|
+
notes: 'Supported sources: local folders, Dropbox/GDrive/OneDrive shared links.',
|
|
20
19
|
},
|
|
21
20
|
{
|
|
22
21
|
order: 2,
|
|
@@ -81,14 +80,14 @@ export function generateDocumentCollectionBlueprint(opts = {}) {
|
|
|
81
80
|
order: 8,
|
|
82
81
|
description: 'Confirm plan with user',
|
|
83
82
|
category: 'review',
|
|
84
|
-
notes: 'User reviews classification, approves or adjusts.
|
|
83
|
+
notes: 'User reviews classification, approves or adjusts. Agent then uploads each file using the api skill.',
|
|
85
84
|
},
|
|
86
85
|
],
|
|
87
86
|
};
|
|
88
|
-
// Phase 5: Upload
|
|
87
|
+
// Phase 5: Upload (agent-executed — uses file paths from ingest plan)
|
|
89
88
|
const upload = {
|
|
90
89
|
name: 'Upload',
|
|
91
|
-
description: '
|
|
90
|
+
description: 'Agent uploads each classified file to the appropriate Jaz Magic API endpoint using absolute paths from the ingest plan.',
|
|
92
91
|
steps: [
|
|
93
92
|
{
|
|
94
93
|
order: 9,
|
|
@@ -99,7 +98,7 @@ export function generateDocumentCollectionBlueprint(opts = {}) {
|
|
|
99
98
|
sourceType: 'FILE',
|
|
100
99
|
businessTransactionType: '(INVOICE or BILL per classification)',
|
|
101
100
|
},
|
|
102
|
-
notes: '
|
|
101
|
+
notes: 'Agent uses curl -F "sourceFile=@<absolutePath>" for each file. Extraction is async — returns subscriptionFBPath for tracking.',
|
|
103
102
|
},
|
|
104
103
|
{
|
|
105
104
|
order: 10,
|
|
@@ -110,14 +109,14 @@ export function generateDocumentCollectionBlueprint(opts = {}) {
|
|
|
110
109
|
sourceType: 'FILE',
|
|
111
110
|
businessTransactionType: 'BANK_STATEMENT',
|
|
112
111
|
},
|
|
113
|
-
notes: 'Requires accountResourceId for the target bank account. Supports CSV and OFX.',
|
|
112
|
+
notes: 'Agent uses curl -F "sourceFile=@<absolutePath>". Requires accountResourceId for the target bank account. Supports CSV and OFX.',
|
|
114
113
|
},
|
|
115
114
|
],
|
|
116
115
|
};
|
|
117
|
-
// Phase 6: Verify
|
|
116
|
+
// Phase 6: Verify (agent-executed)
|
|
118
117
|
const verify = {
|
|
119
118
|
name: 'Verify',
|
|
120
|
-
description: '
|
|
119
|
+
description: 'Agent checks extraction results and flags failures.',
|
|
121
120
|
steps: [
|
|
122
121
|
{
|
|
123
122
|
order: 11,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Pretty-print and JSON formatters for document-collection ingest output.
|
|
3
3
|
*/
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
/** Print
|
|
5
|
+
/** Print an ingestion plan in human-readable format. */
|
|
6
6
|
export function printIngestPlan(plan) {
|
|
7
7
|
console.log();
|
|
8
8
|
console.log(chalk.bold('Document Collection — Ingestion Plan'));
|
|
@@ -10,6 +10,7 @@ export function printIngestPlan(plan) {
|
|
|
10
10
|
if (plan.sourceType === 'url' && plan.cloudProvider) {
|
|
11
11
|
const providerName = { dropbox: 'Dropbox', gdrive: 'Google Drive', onedrive: 'OneDrive' };
|
|
12
12
|
console.log(chalk.gray(`Provider: ${providerName[plan.cloudProvider] ?? plan.cloudProvider}`));
|
|
13
|
+
console.log(chalk.gray(`Local path: ${plan.localPath}`));
|
|
13
14
|
}
|
|
14
15
|
console.log();
|
|
15
16
|
for (const folder of plan.folders) {
|
|
@@ -42,45 +43,8 @@ export function printIngestPlan(plan) {
|
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
45
|
console.log();
|
|
45
|
-
console.log(chalk.gray('Add --execute --api-key <key> --api-url <url> to upload.'));
|
|
46
|
-
console.log();
|
|
47
46
|
}
|
|
48
47
|
/** Print ingestion plan as JSON. */
|
|
49
48
|
export function printIngestPlanJson(plan) {
|
|
50
49
|
console.log(JSON.stringify(plan, null, 2));
|
|
51
50
|
}
|
|
52
|
-
/** Print an execution result in human-readable format. */
|
|
53
|
-
export function printIngestResult(result) {
|
|
54
|
-
console.log();
|
|
55
|
-
console.log(chalk.bold('Document Collection — Ingestion Result'));
|
|
56
|
-
console.log(chalk.gray(`Source: ${result.source}`));
|
|
57
|
-
console.log();
|
|
58
|
-
for (const r of result.results) {
|
|
59
|
-
const status = r.status === 'success'
|
|
60
|
-
? chalk.green('✓')
|
|
61
|
-
: r.status === 'skipped'
|
|
62
|
-
? chalk.gray('–')
|
|
63
|
-
: chalk.red('✗');
|
|
64
|
-
const extra = r.recordsCreated ? ` (${r.recordsCreated} records)` : '';
|
|
65
|
-
const errMsg = r.error ? chalk.red(` — ${r.error}`) : '';
|
|
66
|
-
console.log(` ${status} ${r.file} [${r.classification}]${extra}${errMsg}`);
|
|
67
|
-
}
|
|
68
|
-
console.log();
|
|
69
|
-
console.log(chalk.bold('Summary'));
|
|
70
|
-
console.log(` Total: ${result.summary.total}`);
|
|
71
|
-
console.log(` Uploaded: ${chalk.green(String(result.summary.uploaded))}`);
|
|
72
|
-
if (result.summary.skipped > 0) {
|
|
73
|
-
console.log(` Skipped: ${chalk.gray(String(result.summary.skipped))}`);
|
|
74
|
-
}
|
|
75
|
-
if (result.summary.failed > 0) {
|
|
76
|
-
console.log(` Failed: ${chalk.red(String(result.summary.failed))}`);
|
|
77
|
-
for (const err of result.summary.errors) {
|
|
78
|
-
console.log(chalk.red(` ${err.file}: ${err.error}`));
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
console.log();
|
|
82
|
-
}
|
|
83
|
-
/** Print execution result as JSON. */
|
|
84
|
-
export function printIngestResultJson(result) {
|
|
85
|
-
console.log(JSON.stringify(result, null, 2));
|
|
86
|
-
}
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Document Collection ingest tool.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 2. Execute (--execute): Scan + classify + upload to Jaz Magic API → IngestResult
|
|
4
|
+
* Scans a local directory or cloud share link, classifies documents by folder name,
|
|
5
|
+
* and produces an IngestPlan with absolute file paths for the AI agent to upload.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* The CLI does NOT make API calls — the agent uses the api skill for that.
|
|
9
8
|
*/
|
|
10
9
|
import { existsSync, statSync } from 'node:fs';
|
|
11
|
-
import { resolve
|
|
10
|
+
import { resolve } from 'node:path';
|
|
12
11
|
import { JobValidationError } from '../../../validate.js';
|
|
13
12
|
import { scanLocalDirectory } from './scanner.js';
|
|
14
|
-
import { uploadFile } from './magic-upload.js';
|
|
15
13
|
import { downloadCloudSource } from './cloud/index.js';
|
|
16
14
|
/**
|
|
17
15
|
* Detect if a source string is a URL (public share link) or local path.
|
|
@@ -40,7 +38,8 @@ function validateLocalSource(source) {
|
|
|
40
38
|
}
|
|
41
39
|
/**
|
|
42
40
|
* Resolve a source (local path or URL) to a local directory path.
|
|
43
|
-
* For URLs, downloads to a temp dir.
|
|
41
|
+
* For URLs, downloads to a temp dir. The temp dir is NOT cleaned up —
|
|
42
|
+
* the agent needs the files to upload via the API.
|
|
44
43
|
*/
|
|
45
44
|
async function resolveSource(opts) {
|
|
46
45
|
if (isUrl(opts.source)) {
|
|
@@ -52,118 +51,20 @@ async function resolveSource(opts) {
|
|
|
52
51
|
return { localPath: validateLocalSource(opts.source), originalSource: opts.source, cloud: null };
|
|
53
52
|
}
|
|
54
53
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const { localPath, originalSource, cloud } = await resolveSource(opts);
|
|
60
|
-
try {
|
|
61
|
-
const plan = scanLocalDirectory(localPath, { forceType: opts.type });
|
|
62
|
-
// Override source display for URL sources
|
|
63
|
-
if (cloud) {
|
|
64
|
-
plan.source = originalSource;
|
|
65
|
-
plan.sourceType = 'url';
|
|
66
|
-
plan.cloudProvider = cloud.provider;
|
|
67
|
-
}
|
|
68
|
-
return plan;
|
|
69
|
-
}
|
|
70
|
-
finally {
|
|
71
|
-
cloud?.cleanup();
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Run the ingest tool in execute mode.
|
|
76
|
-
* Scans, classifies, then uploads each file to the Jaz Magic API.
|
|
54
|
+
* Scan, classify, and return an IngestPlan with absolute file paths.
|
|
55
|
+
*
|
|
56
|
+
* For cloud sources, files are downloaded to a temp directory first.
|
|
57
|
+
* The temp dir is preserved so the agent can use the file paths for uploads.
|
|
77
58
|
*/
|
|
78
|
-
export async function
|
|
59
|
+
export async function ingest(opts) {
|
|
79
60
|
const { localPath, originalSource, cloud } = await resolveSource(opts);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
for (const file of allFiles) {
|
|
90
|
-
// Skip UNKNOWN and SKIPPED files
|
|
91
|
-
if (file.documentType === 'UNKNOWN' || file.documentType === 'SKIPPED') {
|
|
92
|
-
skipped++;
|
|
93
|
-
results.push({
|
|
94
|
-
file: file.path,
|
|
95
|
-
classification: 'SKIPPED',
|
|
96
|
-
status: 'skipped',
|
|
97
|
-
reason: file.reason,
|
|
98
|
-
});
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
const absolutePath = join(localPath, file.path);
|
|
102
|
-
const result = await uploadFile({
|
|
103
|
-
filePath: absolutePath,
|
|
104
|
-
relativePath: file.path,
|
|
105
|
-
documentType: file.documentType,
|
|
106
|
-
apiKey: opts.apiKey,
|
|
107
|
-
apiUrl: opts.apiUrl,
|
|
108
|
-
bankAccountId: opts.bankAccount,
|
|
109
|
-
});
|
|
110
|
-
results.push(result);
|
|
111
|
-
if (result.status === 'success') {
|
|
112
|
-
uploaded++;
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
failed++;
|
|
116
|
-
if (result.error) {
|
|
117
|
-
errors.push({ file: result.file, error: result.error });
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return {
|
|
122
|
-
type: 'document-collection-ingest',
|
|
123
|
-
source: originalSource,
|
|
124
|
-
results,
|
|
125
|
-
summary: {
|
|
126
|
-
total: allFiles.length,
|
|
127
|
-
uploaded,
|
|
128
|
-
skipped,
|
|
129
|
-
failed,
|
|
130
|
-
errors,
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
finally {
|
|
135
|
-
cloud?.cleanup();
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
/** Allowed API hosts for execute mode. */
|
|
139
|
-
const ALLOWED_API_HOSTS = new Set(['api.jaz.ai', 'api.juan.ac', 'staging-api.jaz.ai', 'localhost']);
|
|
140
|
-
/**
|
|
141
|
-
* Validate options for execute mode.
|
|
142
|
-
* Throws JobValidationError if required options are missing or unsafe.
|
|
143
|
-
*/
|
|
144
|
-
export function validateExecuteOptions(opts) {
|
|
145
|
-
if (!opts.apiKey) {
|
|
146
|
-
throw new JobValidationError('--api-key is required for --execute mode');
|
|
147
|
-
}
|
|
148
|
-
if (!opts.apiUrl) {
|
|
149
|
-
throw new JobValidationError('--api-url is required for --execute mode');
|
|
150
|
-
}
|
|
151
|
-
// Validate URL to prevent credential exfiltration (SSRF)
|
|
152
|
-
let parsed;
|
|
153
|
-
try {
|
|
154
|
-
parsed = new URL(opts.apiUrl);
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
throw new JobValidationError('--api-url must be a valid URL');
|
|
158
|
-
}
|
|
159
|
-
if (parsed.protocol !== 'https:' && parsed.hostname !== 'localhost') {
|
|
160
|
-
throw new JobValidationError('--api-url must use HTTPS');
|
|
161
|
-
}
|
|
162
|
-
if (parsed.username || parsed.password) {
|
|
163
|
-
throw new JobValidationError('--api-url must not contain credentials');
|
|
164
|
-
}
|
|
165
|
-
if (!ALLOWED_API_HOSTS.has(parsed.hostname)) {
|
|
166
|
-
throw new JobValidationError(`--api-url host "${parsed.hostname}" is not allowed. ` +
|
|
167
|
-
`Allowed: ${[...ALLOWED_API_HOSTS].join(', ')}`);
|
|
168
|
-
}
|
|
61
|
+
const plan = scanLocalDirectory(localPath, { forceType: opts.type });
|
|
62
|
+
// Override source display and localPath for URL sources
|
|
63
|
+
if (cloud) {
|
|
64
|
+
plan.source = originalSource;
|
|
65
|
+
plan.sourceType = 'url';
|
|
66
|
+
plan.cloudProvider = cloud.provider;
|
|
67
|
+
plan.localPath = localPath;
|
|
68
|
+
}
|
|
69
|
+
return plan;
|
|
169
70
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* and produces an IngestPlan (dry-run output).
|
|
6
6
|
*/
|
|
7
7
|
import { readdirSync, statSync } from 'node:fs';
|
|
8
|
-
import { join, basename, extname, relative } from 'node:path';
|
|
8
|
+
import { join, basename, extname, relative, resolve } from 'node:path';
|
|
9
9
|
import { classifyFolder, checkExtension } from './classify.js';
|
|
10
10
|
/** Max recursion depth to prevent runaway traversal. */
|
|
11
11
|
const MAX_DEPTH = 10;
|
|
@@ -19,9 +19,10 @@ const MAX_DEPTH = 10;
|
|
|
19
19
|
* 4. Nested subfolders (depth > 1) inherit from nearest classified ancestor.
|
|
20
20
|
*/
|
|
21
21
|
export function scanLocalDirectory(sourcePath, opts = {}) {
|
|
22
|
+
const base = resolve(sourcePath);
|
|
22
23
|
const maxDepth = opts.maxDepth ?? MAX_DEPTH;
|
|
23
24
|
const files = [];
|
|
24
|
-
scanDir(
|
|
25
|
+
scanDir(base, base, null, opts.forceType ?? null, 0, maxDepth, files);
|
|
25
26
|
// Group files by folder
|
|
26
27
|
const folderMap = new Map();
|
|
27
28
|
for (const f of files) {
|
|
@@ -75,8 +76,9 @@ export function scanLocalDirectory(sourcePath, opts = {}) {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
return {
|
|
78
|
-
source:
|
|
79
|
+
source: base,
|
|
79
80
|
sourceType: 'local',
|
|
81
|
+
localPath: base,
|
|
80
82
|
folders,
|
|
81
83
|
summary: { total, uploadable, needClassification, skipped, byType },
|
|
82
84
|
};
|
|
@@ -151,6 +153,8 @@ function scanDir(rootPath, dirPath, inheritedType, forceType, depth, maxDepth, o
|
|
|
151
153
|
folder: relDir,
|
|
152
154
|
confidence,
|
|
153
155
|
reason,
|
|
156
|
+
absolutePath: fullPath,
|
|
157
|
+
sizeBytes: stat.size,
|
|
154
158
|
});
|
|
155
159
|
}
|
|
156
160
|
}
|
package/package.json
CHANGED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Jaz Magic API upload logic for document collection ingest.
|
|
3
|
-
*
|
|
4
|
-
* Two endpoints:
|
|
5
|
-
* 1. POST /magic/createBusinessTransactionFromAttachment — invoices + bills (PDF/JPG/PNG)
|
|
6
|
-
* 2. POST /magic/importBankStatementFromAttachment — bank statements (CSV/OFX)
|
|
7
|
-
*
|
|
8
|
-
* Both use multipart/form-data with `sourceFile` field (NOT "file").
|
|
9
|
-
* Magic endpoints use `x-magic-api-key` header (NOT `x-jk-api-key`).
|
|
10
|
-
* Extraction is asynchronous — response confirms upload, not extraction.
|
|
11
|
-
*/
|
|
12
|
-
import { readFileSync } from 'node:fs';
|
|
13
|
-
import { basename } from 'node:path';
|
|
14
|
-
/**
|
|
15
|
-
* Upload a single file to the appropriate Jaz Magic API endpoint.
|
|
16
|
-
*
|
|
17
|
-
* Returns an IngestFileResult (never throws — errors are captured in the result).
|
|
18
|
-
*/
|
|
19
|
-
export async function uploadFile(opts) {
|
|
20
|
-
const { filePath, relativePath, documentType, apiKey, apiUrl, bankAccountId } = opts;
|
|
21
|
-
try {
|
|
22
|
-
const fileBuffer = readFileSync(filePath);
|
|
23
|
-
const fileName = basename(filePath);
|
|
24
|
-
const blob = new Blob([fileBuffer]);
|
|
25
|
-
if (documentType === 'BANK_STATEMENT') {
|
|
26
|
-
return await uploadBankStatement(blob, fileName, relativePath, apiKey, apiUrl, bankAccountId);
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
const txType = documentType === 'INVOICE' ? 'INVOICE' : 'BILL';
|
|
30
|
-
return await uploadTransaction(blob, fileName, relativePath, txType, apiKey, apiUrl);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
catch (err) {
|
|
34
|
-
return {
|
|
35
|
-
file: relativePath,
|
|
36
|
-
classification: documentType,
|
|
37
|
-
status: 'failed',
|
|
38
|
-
error: err instanceof Error ? err.message : String(err),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Upload invoice/bill via POST /magic/createBusinessTransactionFromAttachment.
|
|
44
|
-
*/
|
|
45
|
-
async function uploadTransaction(blob, fileName, relativePath, txType, apiKey, apiUrl) {
|
|
46
|
-
const formData = new FormData();
|
|
47
|
-
formData.append('sourceFile', blob, fileName);
|
|
48
|
-
formData.append('businessTransactionType', txType);
|
|
49
|
-
formData.append('sourceType', 'FILE');
|
|
50
|
-
const url = `${apiUrl.replace(/\/+$/, '')}/api/v1/magic/createBusinessTransactionFromAttachment`;
|
|
51
|
-
const response = await fetch(url, {
|
|
52
|
-
method: 'POST',
|
|
53
|
-
headers: { 'x-magic-api-key': apiKey },
|
|
54
|
-
body: formData,
|
|
55
|
-
});
|
|
56
|
-
if (!response.ok) {
|
|
57
|
-
const errorBody = await safeReadBody(response);
|
|
58
|
-
return {
|
|
59
|
-
file: relativePath,
|
|
60
|
-
classification: txType === 'INVOICE' ? 'INVOICE' : 'BILL',
|
|
61
|
-
status: 'failed',
|
|
62
|
-
error: `${response.status}: ${errorBody}`,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
const json = (await response.json());
|
|
66
|
-
const valid = json.data?.validFiles?.[0];
|
|
67
|
-
const invalid = json.data?.invalidFiles?.[0];
|
|
68
|
-
if (invalid) {
|
|
69
|
-
return {
|
|
70
|
-
file: relativePath,
|
|
71
|
-
classification: txType === 'INVOICE' ? 'INVOICE' : 'BILL',
|
|
72
|
-
status: 'failed',
|
|
73
|
-
error: `${invalid.errorCode}: ${invalid.errorMessage}`,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
return {
|
|
77
|
-
file: relativePath,
|
|
78
|
-
classification: txType === 'INVOICE' ? 'INVOICE' : 'BILL',
|
|
79
|
-
status: 'success',
|
|
80
|
-
subscriptionFBPath: valid?.subscriptionFBPath,
|
|
81
|
-
fileId: valid?.fileDetails?.fileId,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Upload bank statement via POST /magic/importBankStatementFromAttachment.
|
|
86
|
-
*/
|
|
87
|
-
async function uploadBankStatement(blob, fileName, relativePath, apiKey, apiUrl, bankAccountId) {
|
|
88
|
-
if (!bankAccountId) {
|
|
89
|
-
return {
|
|
90
|
-
file: relativePath,
|
|
91
|
-
classification: 'BANK_STATEMENT',
|
|
92
|
-
status: 'failed',
|
|
93
|
-
error: 'Missing --bank-account (accountResourceId required for bank statement uploads)',
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
const formData = new FormData();
|
|
97
|
-
formData.append('sourceFile', blob, fileName);
|
|
98
|
-
formData.append('accountResourceId', bankAccountId);
|
|
99
|
-
formData.append('businessTransactionType', 'BANK_STATEMENT');
|
|
100
|
-
formData.append('sourceType', 'FILE');
|
|
101
|
-
const url = `${apiUrl.replace(/\/+$/, '')}/api/v1/magic/importBankStatementFromAttachment`;
|
|
102
|
-
const response = await fetch(url, {
|
|
103
|
-
method: 'POST',
|
|
104
|
-
headers: { 'x-magic-api-key': apiKey },
|
|
105
|
-
body: formData,
|
|
106
|
-
});
|
|
107
|
-
if (!response.ok) {
|
|
108
|
-
const errorBody = await safeReadBody(response);
|
|
109
|
-
return {
|
|
110
|
-
file: relativePath,
|
|
111
|
-
classification: 'BANK_STATEMENT',
|
|
112
|
-
status: 'failed',
|
|
113
|
-
error: `${response.status}: ${errorBody}`,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
// Bank statement response shape varies — extract record count if available
|
|
117
|
-
const json = (await response.json());
|
|
118
|
-
const data = json.data;
|
|
119
|
-
return {
|
|
120
|
-
file: relativePath,
|
|
121
|
-
classification: 'BANK_STATEMENT',
|
|
122
|
-
status: 'success',
|
|
123
|
-
recordsCreated: typeof data?.recordsCreated === 'number' ? data.recordsCreated : undefined,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
/** Safely read response body as text, truncated to 200 chars. */
|
|
127
|
-
async function safeReadBody(response) {
|
|
128
|
-
try {
|
|
129
|
-
const text = await response.text();
|
|
130
|
-
// Try to extract message from JSON error body
|
|
131
|
-
try {
|
|
132
|
-
const parsed = JSON.parse(text);
|
|
133
|
-
return parsed.message ?? parsed.error ?? text.slice(0, 200);
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return text.slice(0, 200);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
return response.statusText;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
File without changes
|