@fuzzle/opencode-accountant 0.0.12 → 0.0.13-next.1
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 +23 -25
- package/agent/accountant.md +105 -5
- package/dist/index.js +100 -7
- package/docs/tools/classify-statements.md +404 -0
- package/docs/tools/import-statements.md +305 -0
- package/docs/tools/update-prices.md +581 -0
- package/package.json +3 -2
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# classify-statements Tool
|
|
2
|
+
|
|
3
|
+
The `classify-statements` tool organizes bank statement CSV files by automatically detecting their provider and currency, then moves them to the appropriate directories for import processing.
|
|
4
|
+
|
|
5
|
+
This tool is **restricted to the accountant agent only**.
|
|
6
|
+
|
|
7
|
+
## Arguments
|
|
8
|
+
|
|
9
|
+
| Argument | Type | Default | Description |
|
|
10
|
+
| -------- | ---- | ------- | ---------------------------- |
|
|
11
|
+
| (none) | - | - | This tool takes no arguments |
|
|
12
|
+
|
|
13
|
+
## Output Format
|
|
14
|
+
|
|
15
|
+
**Note on paths:** All file paths use `{paths.*}` variables configured in `config/import/providers.yaml`. Default values:
|
|
16
|
+
|
|
17
|
+
- `{paths.import}` = `import/incoming`
|
|
18
|
+
- `{paths.pending}` = `import/pending`
|
|
19
|
+
- `{paths.unrecognized}` = `import/unrecognized`
|
|
20
|
+
|
|
21
|
+
### Success - All Files Classified
|
|
22
|
+
|
|
23
|
+
When all CSV files are successfully classified:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"success": true,
|
|
28
|
+
"classified": [
|
|
29
|
+
{
|
|
30
|
+
"filename": "transactions-ubs-2026-02.csv",
|
|
31
|
+
"provider": "ubs",
|
|
32
|
+
"currency": "chf",
|
|
33
|
+
"targetPath": "{paths.pending}/ubs/chf/transactions-ubs-2026-02.csv"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"filename": "account-statement_2026-02.csv",
|
|
37
|
+
"provider": "revolut",
|
|
38
|
+
"currency": "eur",
|
|
39
|
+
"targetPath": "{paths.pending}/revolut/eur/account-statement_2026-02.csv"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"unrecognized": [],
|
|
43
|
+
"summary": {
|
|
44
|
+
"total": 2,
|
|
45
|
+
"classified": 2,
|
|
46
|
+
"unrecognized": 0
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Success - With Filename Renaming
|
|
52
|
+
|
|
53
|
+
When provider config includes `renamePattern` with metadata extraction:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"success": true,
|
|
58
|
+
"classified": [
|
|
59
|
+
{
|
|
60
|
+
"filename": "transactions-ubs-0235-90250546.csv",
|
|
61
|
+
"originalFilename": "export.csv",
|
|
62
|
+
"provider": "ubs",
|
|
63
|
+
"currency": "chf",
|
|
64
|
+
"targetPath": "{paths.pending}/ubs/chf/transactions-ubs-0235-90250546.csv"
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"unrecognized": [],
|
|
68
|
+
"summary": {
|
|
69
|
+
"total": 1,
|
|
70
|
+
"classified": 1,
|
|
71
|
+
"unrecognized": 0
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The `originalFilename` field appears when the file was renamed using metadata extraction.
|
|
77
|
+
|
|
78
|
+
### Success - Some Files Unrecognized
|
|
79
|
+
|
|
80
|
+
When some files cannot be classified:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"success": true,
|
|
85
|
+
"classified": [
|
|
86
|
+
{
|
|
87
|
+
"filename": "transactions-ubs-2026-02.csv",
|
|
88
|
+
"provider": "ubs",
|
|
89
|
+
"currency": "chf",
|
|
90
|
+
"targetPath": "{paths.pending}/ubs/chf/transactions-ubs-2026-02.csv"
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
"unrecognized": [
|
|
94
|
+
{
|
|
95
|
+
"filename": "mystery-bank.csv",
|
|
96
|
+
"targetPath": "{paths.unrecognized}/mystery-bank.csv"
|
|
97
|
+
}
|
|
98
|
+
],
|
|
99
|
+
"summary": {
|
|
100
|
+
"total": 2,
|
|
101
|
+
"classified": 1,
|
|
102
|
+
"unrecognized": 1
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Unrecognized files are moved to `{paths.unrecognized}` for manual review.
|
|
108
|
+
|
|
109
|
+
### Failure - File Collisions
|
|
110
|
+
|
|
111
|
+
When target files already exist (prevents overwriting):
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"success": false,
|
|
116
|
+
"error": "Cannot classify: 1 file(s) would overwrite existing pending files.",
|
|
117
|
+
"collisions": [
|
|
118
|
+
{
|
|
119
|
+
"filename": "transactions.csv",
|
|
120
|
+
"existingPath": "{paths.pending}/ubs/chf/transactions.csv"
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
"classified": [],
|
|
124
|
+
"unrecognized": []
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Important:** The tool uses a two-pass approach (detect → check collisions → move) to prevent partial classification. If ANY collision is detected, NO files are moved.
|
|
129
|
+
|
|
130
|
+
### Configuration Error
|
|
131
|
+
|
|
132
|
+
When `config/import/providers.yaml` is missing or invalid:
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"success": false,
|
|
137
|
+
"error": "Failed to load configuration: config/import/providers.yaml not found",
|
|
138
|
+
"classified": [],
|
|
139
|
+
"unrecognized": []
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Agent Restriction Error
|
|
144
|
+
|
|
145
|
+
When called by the wrong agent:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"success": false,
|
|
150
|
+
"error": "This tool is restricted to the accountant agent only.",
|
|
151
|
+
"hint": "Use: Task(subagent_type='accountant', prompt='classify statements')",
|
|
152
|
+
"caller": "main assistant",
|
|
153
|
+
"classified": [],
|
|
154
|
+
"unrecognized": []
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Provider Detection
|
|
159
|
+
|
|
160
|
+
The tool detects providers using rules defined in `config/import/providers.yaml`:
|
|
161
|
+
|
|
162
|
+
### Detection Methods
|
|
163
|
+
|
|
164
|
+
1. **Filename Pattern** (optional): Regex match against filename
|
|
165
|
+
2. **CSV Header** (required): Exact match of CSV header row
|
|
166
|
+
3. **Currency Field** (required): Which column contains the currency
|
|
167
|
+
|
|
168
|
+
### Detection Example
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
providers:
|
|
172
|
+
revolut:
|
|
173
|
+
detect:
|
|
174
|
+
- filenamePattern: '^account-statement_'
|
|
175
|
+
header: 'Type,Product,Started Date,Completed Date,Description,Amount,Fee,Currency,State,Balance'
|
|
176
|
+
currencyField: Currency
|
|
177
|
+
currencies:
|
|
178
|
+
CHF: chf
|
|
179
|
+
EUR: eur
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Detection process:
|
|
183
|
+
|
|
184
|
+
1. Check if filename matches `filenamePattern` (if specified)
|
|
185
|
+
2. Read CSV and check if header matches exactly
|
|
186
|
+
3. Determine currency from `currencyField` column
|
|
187
|
+
4. Map raw currency value (e.g., "EUR") to normalized folder name (e.g., "eur")
|
|
188
|
+
|
|
189
|
+
### Filename Renaming
|
|
190
|
+
|
|
191
|
+
Providers can specify `renamePattern` with metadata extraction:
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
ubs:
|
|
195
|
+
detect:
|
|
196
|
+
- header: 'Trade date,Trade time,...'
|
|
197
|
+
currencyField: Currency
|
|
198
|
+
skipRows: 9
|
|
199
|
+
delimiter: ';'
|
|
200
|
+
renamePattern: 'transactions-ubs-{account-number}.csv'
|
|
201
|
+
metadata:
|
|
202
|
+
- field: account-number
|
|
203
|
+
row: 0
|
|
204
|
+
column: 1
|
|
205
|
+
normalize: spaces-to-dashes
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
This extracts metadata from the CSV (e.g., account number from row 0, column 1) and uses it in the output filename.
|
|
209
|
+
|
|
210
|
+
## Directory Structure
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
your-project/
|
|
214
|
+
├── config/
|
|
215
|
+
│ └── import/
|
|
216
|
+
│ └── providers.yaml # Defines all paths and detection rules
|
|
217
|
+
├── {paths.import}/ # Drop CSV files here (default: import/incoming)
|
|
218
|
+
│ ├── bank1.csv
|
|
219
|
+
│ └── bank2.csv
|
|
220
|
+
│
|
|
221
|
+
├── {paths.pending}/ # Classified files (default: import/pending)
|
|
222
|
+
│ ├── <provider>/ # e.g., revolut, ubs
|
|
223
|
+
│ │ └── <currency>/ # e.g., chf, eur, usd, btc
|
|
224
|
+
│ │ └── classified.csv
|
|
225
|
+
│ ├── ubs/
|
|
226
|
+
│ │ └── chf/
|
|
227
|
+
│ │ └── transactions-ubs-0235-90250546.csv
|
|
228
|
+
│ └── revolut/
|
|
229
|
+
│ └── eur/
|
|
230
|
+
│ └── account-statement_2026-02.csv
|
|
231
|
+
│
|
|
232
|
+
└── {paths.unrecognized}/ # Unclassified files (default: import/unrecognized)
|
|
233
|
+
└── mystery-bank.csv
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Typical Workflow
|
|
237
|
+
|
|
238
|
+
### Scenario 1: Successful Classification
|
|
239
|
+
|
|
240
|
+
1. Drop CSV files into `{paths.import}/` (e.g., `import/incoming/`)
|
|
241
|
+
2. Run `classify-statements` tool (no arguments)
|
|
242
|
+
3. Check output - all files classified successfully
|
|
243
|
+
4. Files organized in `{paths.pending}/<provider>/<currency>/`
|
|
244
|
+
5. Proceed to `import-statements` tool
|
|
245
|
+
|
|
246
|
+
### Scenario 2: Handling Unrecognized Files
|
|
247
|
+
|
|
248
|
+
1. Run `classify-statements` tool
|
|
249
|
+
2. Review `unrecognized` array in output
|
|
250
|
+
3. Check files in `{paths.unrecognized}/` directory
|
|
251
|
+
4. Options to resolve:
|
|
252
|
+
- **Add provider config**: Update `config/import/providers.yaml` with detection rules
|
|
253
|
+
- **Manual classification**: Move file to correct `{paths.pending}/<provider>/<currency>/` directory
|
|
254
|
+
- **Investigate format**: Check if CSV format matches expected patterns
|
|
255
|
+
5. Re-run tool after adding configuration
|
|
256
|
+
|
|
257
|
+
### Scenario 3: Resolving Collisions
|
|
258
|
+
|
|
259
|
+
1. Run `classify-statements` tool
|
|
260
|
+
2. Tool reports collision - file would overwrite existing file
|
|
261
|
+
3. Check `collisions` array for affected files
|
|
262
|
+
4. Options to resolve:
|
|
263
|
+
- **Archive existing**: Move existing pending file to `{paths.done}/` if already processed
|
|
264
|
+
- **Rename**: Rename one of the conflicting files
|
|
265
|
+
- **Remove**: Delete duplicate file if confirmed redundant
|
|
266
|
+
5. Re-run tool after resolving collision
|
|
267
|
+
|
|
268
|
+
**Important:** No files are moved until ALL collisions are resolved. This prevents partial/inconsistent state.
|
|
269
|
+
|
|
270
|
+
## Handling Unrecognized Files
|
|
271
|
+
|
|
272
|
+
### What "Unrecognized" Means
|
|
273
|
+
|
|
274
|
+
A file is unrecognized when:
|
|
275
|
+
|
|
276
|
+
- Filename doesn't match any `filenamePattern` (if patterns are specified)
|
|
277
|
+
- CSV header doesn't match any configured provider's `header`
|
|
278
|
+
- CSV is malformed or has unexpected structure
|
|
279
|
+
- Currency value doesn't map to configured currencies
|
|
280
|
+
|
|
281
|
+
### Common Causes
|
|
282
|
+
|
|
283
|
+
| Cause | Solution |
|
|
284
|
+
| ------------------------- | ------------------------------------------------------------------------------- |
|
|
285
|
+
| New bank/provider | Add provider config to `config/import/providers.yaml` |
|
|
286
|
+
| Non-standard CSV format | Check CSV structure; add detection rules with correct header/skipRows/delimiter |
|
|
287
|
+
| Filename pattern mismatch | Update `filenamePattern` or remove it (header-only detection) |
|
|
288
|
+
| Unknown currency | Add currency mapping to provider's `currencies` section |
|
|
289
|
+
| Metadata in header rows | Use `skipRows` to skip non-CSV rows before header |
|
|
290
|
+
| Wrong delimiter | Specify `delimiter` (e.g., `";"` for semicolon-delimited) |
|
|
291
|
+
|
|
292
|
+
### Adding Provider Detection
|
|
293
|
+
|
|
294
|
+
Example: Adding a new bank called "SwissBank":
|
|
295
|
+
|
|
296
|
+
```yaml
|
|
297
|
+
providers:
|
|
298
|
+
swissbank:
|
|
299
|
+
detect:
|
|
300
|
+
- filenamePattern: '^swissbank-'
|
|
301
|
+
header: 'Date,Description,Amount,Balance,Currency'
|
|
302
|
+
currencyField: Currency
|
|
303
|
+
currencies:
|
|
304
|
+
CHF: chf
|
|
305
|
+
EUR: eur
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
After updating config, re-run `classify-statements` to classify previously unrecognized files.
|
|
309
|
+
|
|
310
|
+
## Collision Safety
|
|
311
|
+
|
|
312
|
+
The tool uses a **two-pass approach** to ensure atomic operations:
|
|
313
|
+
|
|
314
|
+
### Two-Pass Process
|
|
315
|
+
|
|
316
|
+
**Pass 1: Detection & Planning**
|
|
317
|
+
|
|
318
|
+
- Scan all CSV files in `{paths.import}/`
|
|
319
|
+
- Detect provider/currency for each file
|
|
320
|
+
- Determine target path for each file
|
|
321
|
+
- Build complete move plan
|
|
322
|
+
|
|
323
|
+
**Pass 2: Collision Check**
|
|
324
|
+
|
|
325
|
+
- Check if ANY target file already exists
|
|
326
|
+
- If collisions found: abort with error (no files moved)
|
|
327
|
+
- If no collisions: proceed to Pass 3
|
|
328
|
+
|
|
329
|
+
**Pass 3: Move Files**
|
|
330
|
+
|
|
331
|
+
- Execute all planned moves atomically
|
|
332
|
+
- All files moved successfully or none at all
|
|
333
|
+
|
|
334
|
+
### Why This Matters
|
|
335
|
+
|
|
336
|
+
Without collision checking, partial classification could occur:
|
|
337
|
+
|
|
338
|
+
- Some files moved, others fail mid-process
|
|
339
|
+
- Inconsistent state requiring manual cleanup
|
|
340
|
+
- Risk of lost data or confusion about what was processed
|
|
341
|
+
|
|
342
|
+
With two-pass approach:
|
|
343
|
+
|
|
344
|
+
- All-or-nothing operation
|
|
345
|
+
- Easy to retry after fixing collisions
|
|
346
|
+
- No partial/inconsistent states
|
|
347
|
+
|
|
348
|
+
### Resolving Collisions
|
|
349
|
+
|
|
350
|
+
Check the `collisions` array in the error output:
|
|
351
|
+
|
|
352
|
+
```json
|
|
353
|
+
"collisions": [
|
|
354
|
+
{
|
|
355
|
+
"filename": "transactions.csv",
|
|
356
|
+
"existingPath": "{paths.pending}/ubs/chf/transactions.csv"
|
|
357
|
+
}
|
|
358
|
+
]
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Then:
|
|
362
|
+
|
|
363
|
+
1. Inspect existing file: `cat {paths.pending}/ubs/chf/transactions.csv`
|
|
364
|
+
2. Determine if it's already processed (check if transactions were imported)
|
|
365
|
+
3. If processed: Move to `{paths.done}/` or delete
|
|
366
|
+
4. If not processed: Rename one of the files or merge manually
|
|
367
|
+
5. Re-run `classify-statements`
|
|
368
|
+
|
|
369
|
+
## Error Handling
|
|
370
|
+
|
|
371
|
+
### Common Errors
|
|
372
|
+
|
|
373
|
+
| Error | Cause | Solution |
|
|
374
|
+
| ------------------- | ------------------------------------------------- | --------------------------------------------------------------------- |
|
|
375
|
+
| File collision | Target file already exists in pending directory | Move existing file to done, rename, or delete; then re-run |
|
|
376
|
+
| Configuration error | Missing or invalid `config/import/providers.yaml` | Ensure config file exists with proper YAML syntax and required fields |
|
|
377
|
+
| Agent restriction | Called by wrong agent | Use `Task(subagent_type='accountant', prompt='classify statements')` |
|
|
378
|
+
| Permission error | Cannot read/write directories | Check file permissions on import/pending/unrecognized directories |
|
|
379
|
+
| No CSV files found | Import directory is empty | Add CSV files to `{paths.import}` directory first |
|
|
380
|
+
| CSV parsing error | Malformed CSV file | Check CSV structure; ensure proper delimiter and header row |
|
|
381
|
+
|
|
382
|
+
### Configuration File Required Fields
|
|
383
|
+
|
|
384
|
+
Ensure `config/import/providers.yaml` contains:
|
|
385
|
+
|
|
386
|
+
```yaml
|
|
387
|
+
paths:
|
|
388
|
+
import: <path> # Required
|
|
389
|
+
pending: <path> # Required
|
|
390
|
+
done: <path> # Required (used by import-statements tool)
|
|
391
|
+
unrecognized: <path> # Required
|
|
392
|
+
rules: <path> # Required (used by import-statements tool)
|
|
393
|
+
|
|
394
|
+
providers:
|
|
395
|
+
<provider-name>:
|
|
396
|
+
detect:
|
|
397
|
+
- header: <exact-csv-header> # Required
|
|
398
|
+
currencyField: <column-name> # Required
|
|
399
|
+
# Optional: filenamePattern, skipRows, delimiter, renamePattern, metadata
|
|
400
|
+
currencies:
|
|
401
|
+
<RAW-VALUE>: <normalized-folder> # Required (at least one)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Missing any required field will cause a configuration error.
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# import-statements Tool
|
|
2
|
+
|
|
3
|
+
The `import-statements` tool imports classified CSV bank statements into hledger using rules files. It operates in two modes:
|
|
4
|
+
|
|
5
|
+
- **Check mode** (`checkOnly: true`, default): Validates transactions and reports any that cannot be categorized
|
|
6
|
+
- **Import mode** (`checkOnly: false`): Imports validated transactions and moves processed files to the done directory
|
|
7
|
+
|
|
8
|
+
## Year-Based Journal Routing
|
|
9
|
+
|
|
10
|
+
Transactions are automatically routed to year-specific journal files based on transaction dates:
|
|
11
|
+
|
|
12
|
+
- Transactions from 2025 → `ledger/2025.journal`
|
|
13
|
+
- Transactions from 2026 → `ledger/2026.journal`
|
|
14
|
+
|
|
15
|
+
**Automatic setup:**
|
|
16
|
+
|
|
17
|
+
- If the year journal doesn't exist, it is created automatically
|
|
18
|
+
- The include directive (`include ledger/YYYY.journal`) is added to `.hledger.journal` if not already present
|
|
19
|
+
|
|
20
|
+
**Constraint:** Each CSV file must contain transactions from a single year. CSVs with transactions spanning multiple years are rejected during check mode with an error message listing the years found.
|
|
21
|
+
|
|
22
|
+
## Arguments
|
|
23
|
+
|
|
24
|
+
| Argument | Type | Default | Description |
|
|
25
|
+
| ----------- | ------- | ------- | ------------------------------------------- |
|
|
26
|
+
| `provider` | string | - | Filter by provider (e.g., `revolut`, `ubs`) |
|
|
27
|
+
| `currency` | string | - | Filter by currency (e.g., `chf`, `eur`) |
|
|
28
|
+
| `checkOnly` | boolean | `true` | If true, only validate without importing |
|
|
29
|
+
|
|
30
|
+
## Output Format
|
|
31
|
+
|
|
32
|
+
**Note on paths:** All file paths in the examples below use `{paths.*}` variables. These are configured in `config/import/providers.yaml`. Default values are:
|
|
33
|
+
|
|
34
|
+
- `{paths.pending}` = `import/pending`
|
|
35
|
+
- `{paths.done}` = `import/done`
|
|
36
|
+
- `{paths.rules}` = `ledger/rules`
|
|
37
|
+
|
|
38
|
+
### Check Mode - All Transactions Matched
|
|
39
|
+
|
|
40
|
+
When all transactions have matching rules:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"success": true,
|
|
45
|
+
"files": [
|
|
46
|
+
{
|
|
47
|
+
"csv": "{paths.pending}/ubs/chf/transactions-ubs-0235-90250546.0.csv",
|
|
48
|
+
"rulesFile": "{paths.rules}/ubs-0235-90250546.0.rules",
|
|
49
|
+
"transactions": 25,
|
|
50
|
+
"unknownPostings": [],
|
|
51
|
+
"transactionYear": 2026
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"summary": {
|
|
55
|
+
"filesProcessed": 1,
|
|
56
|
+
"totalTransactions": 25,
|
|
57
|
+
"matched": 25,
|
|
58
|
+
"unknown": 0
|
|
59
|
+
},
|
|
60
|
+
"message": "All transactions matched. Ready to import with checkOnly: false"
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Check Mode - Unknown Postings Found
|
|
65
|
+
|
|
66
|
+
When transactions don't match any `if` pattern in the rules file, the tool returns the full CSV row data for each unknown posting to provide context for classification:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"success": false,
|
|
71
|
+
"files": [
|
|
72
|
+
{
|
|
73
|
+
"csv": "{paths.pending}/ubs/chf/transactions-ubs-0235-90250546.0.csv",
|
|
74
|
+
"rulesFile": "{paths.rules}/ubs-0235-90250546.0.rules",
|
|
75
|
+
"transactions": 25,
|
|
76
|
+
"unknownPostings": [
|
|
77
|
+
{
|
|
78
|
+
"date": "2026-01-16",
|
|
79
|
+
"description": "Connor, John",
|
|
80
|
+
"amount": "CHF95.25",
|
|
81
|
+
"account": "income:unknown",
|
|
82
|
+
"csvRow": {
|
|
83
|
+
"trade_date": "2026-01-16",
|
|
84
|
+
"trade_time": "",
|
|
85
|
+
"booking_date": "2026-01-16",
|
|
86
|
+
"value_date": "2026-01-16",
|
|
87
|
+
"currency": "CHF",
|
|
88
|
+
"debit": "",
|
|
89
|
+
"credit": "95.25",
|
|
90
|
+
"individual_amount": "",
|
|
91
|
+
"balance": "4746.23",
|
|
92
|
+
"transaction_no": "ABC123",
|
|
93
|
+
"description1": "Connor, John",
|
|
94
|
+
"description2": "Twint deposit",
|
|
95
|
+
"description3": "Ref: TW-12345",
|
|
96
|
+
"footnotes": ""
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"date": "2026-01-30",
|
|
101
|
+
"description": "Balance closing of service prices",
|
|
102
|
+
"amount": "CHF-10.00",
|
|
103
|
+
"account": "expenses:unknown",
|
|
104
|
+
"csvRow": {
|
|
105
|
+
"trade_date": "2026-01-30",
|
|
106
|
+
"trade_time": "",
|
|
107
|
+
"booking_date": "2026-01-30",
|
|
108
|
+
"value_date": "2026-01-30",
|
|
109
|
+
"currency": "CHF",
|
|
110
|
+
"debit": "10.00",
|
|
111
|
+
"credit": "",
|
|
112
|
+
"individual_amount": "",
|
|
113
|
+
"balance": "2364.69",
|
|
114
|
+
"transaction_no": "DEF456",
|
|
115
|
+
"description1": "Balance closing of service prices",
|
|
116
|
+
"description2": "",
|
|
117
|
+
"description3": "",
|
|
118
|
+
"footnotes": ""
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
],
|
|
124
|
+
"summary": {
|
|
125
|
+
"filesProcessed": 1,
|
|
126
|
+
"totalTransactions": 25,
|
|
127
|
+
"matched": 23,
|
|
128
|
+
"unknown": 2
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Check Mode - Missing Rules File
|
|
134
|
+
|
|
135
|
+
When a CSV file has no matching rules file:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"success": false,
|
|
140
|
+
"files": [
|
|
141
|
+
{
|
|
142
|
+
"csv": "{paths.pending}/ubs/chf/transactions.csv",
|
|
143
|
+
"error": "No matching rules file found. Create a rules file with 'source' directive pointing to this CSV."
|
|
144
|
+
}
|
|
145
|
+
],
|
|
146
|
+
"summary": {
|
|
147
|
+
"filesProcessed": 1,
|
|
148
|
+
"filesWithoutRules": 1
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Import Mode - Success
|
|
154
|
+
|
|
155
|
+
When importing with all transactions matched:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"success": true,
|
|
160
|
+
"files": [
|
|
161
|
+
{
|
|
162
|
+
"csv": "{paths.pending}/ubs/chf/transactions.csv",
|
|
163
|
+
"rulesFile": "{paths.rules}/ubs.rules",
|
|
164
|
+
"imported": true,
|
|
165
|
+
"movedTo": "{paths.done}/ubs/chf/transactions.csv"
|
|
166
|
+
}
|
|
167
|
+
],
|
|
168
|
+
"summary": {
|
|
169
|
+
"filesProcessed": 1,
|
|
170
|
+
"filesImported": 1,
|
|
171
|
+
"totalTransactions": 25
|
|
172
|
+
},
|
|
173
|
+
"message": "Successfully imported 1 file(s)"
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Import Mode - Blocked by Unknown Postings
|
|
178
|
+
|
|
179
|
+
Import mode runs a check first and aborts if any unknowns exist:
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"success": false,
|
|
184
|
+
"error": "Cannot import: 2 transactions have unknown accounts. Run with checkOnly: true to see details and add rules.",
|
|
185
|
+
"hint": "Run with checkOnly: true first to identify and fix unknown postings"
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Unknown Posting Types
|
|
190
|
+
|
|
191
|
+
hledger assigns transactions to `income:unknown` or `expenses:unknown` based on the direction:
|
|
192
|
+
|
|
193
|
+
| Transaction Type | Account Assigned |
|
|
194
|
+
| -------------------------- | ------------------ |
|
|
195
|
+
| Money coming in (positive) | `income:unknown` |
|
|
196
|
+
| Money going out (negative) | `expenses:unknown` |
|
|
197
|
+
|
|
198
|
+
## Fixing Unknown Postings
|
|
199
|
+
|
|
200
|
+
When the tool reports unknown postings, the `csvRow` field contains all available data from the original CSV to help determine the correct account. This includes additional description fields, transaction references, and other metadata that may help with classification.
|
|
201
|
+
|
|
202
|
+
Add `if` rules to the appropriate rules file based on the posting details:
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
# Example: Categorize a friend's reimbursement
|
|
206
|
+
# (csvRow showed description2: "Twint deposit" confirming it's a payment app transfer)
|
|
207
|
+
if Connor, John
|
|
208
|
+
account1 income:reimbursements
|
|
209
|
+
|
|
210
|
+
# Example: Categorize bank service charges
|
|
211
|
+
if Balance closing of service prices
|
|
212
|
+
account1 expenses:fees:bank
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Then run the tool again with `checkOnly: true` to verify the rules work.
|
|
216
|
+
|
|
217
|
+
### CSV Row Field Names
|
|
218
|
+
|
|
219
|
+
The `csvRow` object uses field names from the `fields` directive in the rules file. Common fields include:
|
|
220
|
+
|
|
221
|
+
| Field | Description |
|
|
222
|
+
| ---------------- | ------------------------------------------------------- |
|
|
223
|
+
| `trade_date` | When the transaction occurred |
|
|
224
|
+
| `booking_date` | When it was booked |
|
|
225
|
+
| `description1` | Primary description |
|
|
226
|
+
| `description2` | Secondary description (often useful for classification) |
|
|
227
|
+
| `description3` | Additional reference information |
|
|
228
|
+
| `transaction_no` | Unique transaction identifier |
|
|
229
|
+
| `debit` | Debit amount (money out) |
|
|
230
|
+
| `credit` | Credit amount (money in) |
|
|
231
|
+
|
|
232
|
+
The exact field names depend on your rules file configuration.
|
|
233
|
+
|
|
234
|
+
## Error Handling
|
|
235
|
+
|
|
236
|
+
### hledger Errors
|
|
237
|
+
|
|
238
|
+
If hledger fails to parse a CSV or rules file:
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"success": false,
|
|
243
|
+
"files": [
|
|
244
|
+
{
|
|
245
|
+
"csv": "{paths.pending}/ubs/chf/transactions.csv",
|
|
246
|
+
"rulesFile": "{paths.rules}/ubs.rules",
|
|
247
|
+
"error": "hledger error: Parse error at line 5: invalid date format"
|
|
248
|
+
}
|
|
249
|
+
],
|
|
250
|
+
"summary": {
|
|
251
|
+
"filesProcessed": 1,
|
|
252
|
+
"filesWithErrors": 1
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Configuration Errors
|
|
258
|
+
|
|
259
|
+
If the config file is missing or invalid:
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"success": false,
|
|
264
|
+
"error": "Failed to load configuration: Configuration file not found: config/import/providers.yaml",
|
|
265
|
+
"hint": "Ensure config/import/providers.yaml exists with a 'rules' path configured"
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Multi-Year CSV Error
|
|
270
|
+
|
|
271
|
+
If a CSV contains transactions from multiple years:
|
|
272
|
+
|
|
273
|
+
```json
|
|
274
|
+
{
|
|
275
|
+
"success": false,
|
|
276
|
+
"files": [
|
|
277
|
+
{
|
|
278
|
+
"csv": "{paths.pending}/ubs/chf/transactions.csv",
|
|
279
|
+
"rulesFile": "{paths.rules}/ubs.rules",
|
|
280
|
+
"totalTransactions": 10,
|
|
281
|
+
"matchedTransactions": 10,
|
|
282
|
+
"unknownPostings": [],
|
|
283
|
+
"error": "CSV contains transactions from multiple years (2025, 2026). Split the CSV by year before importing."
|
|
284
|
+
}
|
|
285
|
+
],
|
|
286
|
+
"summary": {
|
|
287
|
+
"filesProcessed": 1,
|
|
288
|
+
"filesWithErrors": 1,
|
|
289
|
+
"totalTransactions": 0,
|
|
290
|
+
"matched": 0,
|
|
291
|
+
"unknown": 0
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Missing Main Journal
|
|
297
|
+
|
|
298
|
+
If `.hledger.journal` doesn't exist when attempting import:
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
{
|
|
302
|
+
"success": false,
|
|
303
|
+
"error": ".hledger.journal not found at /path/to/.hledger.journal. Create it first with appropriate includes."
|
|
304
|
+
}
|
|
305
|
+
```
|