@fuzzle/opencode-accountant 0.0.13 → 0.0.14

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/dist/index.js CHANGED
@@ -17689,6 +17689,18 @@ function countTransactions(hledgerOutput) {
17689
17689
  }
17690
17690
  return count;
17691
17691
  }
17692
+ function extractTransactionYears(hledgerOutput) {
17693
+ const years = new Set;
17694
+ const lines = hledgerOutput.split(`
17695
+ `);
17696
+ for (const line of lines) {
17697
+ const match = line.match(/^(\d{4})-\d{2}-\d{2}\s+/);
17698
+ if (match) {
17699
+ years.add(parseInt(match[1], 10));
17700
+ }
17701
+ }
17702
+ return years;
17703
+ }
17692
17704
 
17693
17705
  // src/utils/rulesParser.ts
17694
17706
  function parseSkipRows(rulesContent) {
@@ -17903,6 +17915,30 @@ function findMatchingCsvRow(posting, csvRows, config2) {
17903
17915
  }
17904
17916
 
17905
17917
  // src/tools/import-statements.ts
17918
+ function ensureYearJournalExists(directory, year) {
17919
+ const ledgerDir = path6.join(directory, "ledger");
17920
+ const yearJournalPath = path6.join(ledgerDir, `${year}.journal`);
17921
+ const mainJournalPath = path6.join(directory, ".hledger.journal");
17922
+ if (!fs7.existsSync(ledgerDir)) {
17923
+ fs7.mkdirSync(ledgerDir, { recursive: true });
17924
+ }
17925
+ if (!fs7.existsSync(yearJournalPath)) {
17926
+ fs7.writeFileSync(yearJournalPath, `; ${year} transactions
17927
+ `);
17928
+ }
17929
+ if (!fs7.existsSync(mainJournalPath)) {
17930
+ throw new Error(`.hledger.journal not found at ${mainJournalPath}. Create it first with appropriate includes.`);
17931
+ }
17932
+ const mainJournalContent = fs7.readFileSync(mainJournalPath, "utf-8");
17933
+ const includeDirective = `include ledger/${year}.journal`;
17934
+ if (!mainJournalContent.includes(includeDirective)) {
17935
+ const newContent = mainJournalContent.trimEnd() + `
17936
+ ` + includeDirective + `
17937
+ `;
17938
+ fs7.writeFileSync(mainJournalPath, newContent);
17939
+ }
17940
+ return yearJournalPath;
17941
+ }
17906
17942
  function findPendingCsvFiles(pendingDir, provider, currency) {
17907
17943
  const csvFiles = [];
17908
17944
  if (!fs7.existsSync(pendingDir)) {
@@ -18006,18 +18042,32 @@ async function importStatementsCore(directory, agent, options, configLoader = lo
18006
18042
  const unknownPostings = parseUnknownPostings(result.stdout);
18007
18043
  const transactionCount = countTransactions(result.stdout);
18008
18044
  const matchedCount = transactionCount - unknownPostings.length;
18045
+ const years = extractTransactionYears(result.stdout);
18046
+ if (years.size > 1) {
18047
+ const yearList = Array.from(years).sort().join(", ");
18048
+ filesWithErrors++;
18049
+ fileResults.push({
18050
+ csv: path6.relative(directory, csvFile),
18051
+ rulesFile: path6.relative(directory, rulesFile),
18052
+ totalTransactions: transactionCount,
18053
+ matchedTransactions: matchedCount,
18054
+ unknownPostings: [],
18055
+ error: `CSV contains transactions from multiple years (${yearList}). Split the CSV by year before importing.`
18056
+ });
18057
+ continue;
18058
+ }
18059
+ const transactionYear = years.size === 1 ? Array.from(years)[0] : undefined;
18009
18060
  if (unknownPostings.length > 0) {
18010
18061
  try {
18011
18062
  const rulesContent = fs7.readFileSync(rulesFile, "utf-8");
18012
18063
  const rulesConfig = parseRulesFile(rulesContent);
18013
18064
  const csvRows = parseCsvFile(csvFile, rulesConfig);
18014
18065
  for (const posting of unknownPostings) {
18015
- const csvRow = findMatchingCsvRow({
18066
+ posting.csvRow = findMatchingCsvRow({
18016
18067
  date: posting.date,
18017
18068
  description: posting.description,
18018
18069
  amount: posting.amount
18019
18070
  }, csvRows, rulesConfig);
18020
- posting.csvRow = csvRow;
18021
18071
  }
18022
18072
  } catch {
18023
18073
  for (const posting of unknownPostings) {
@@ -18033,7 +18083,8 @@ async function importStatementsCore(directory, agent, options, configLoader = lo
18033
18083
  rulesFile: path6.relative(directory, rulesFile),
18034
18084
  totalTransactions: transactionCount,
18035
18085
  matchedTransactions: matchedCount,
18036
- unknownPostings
18086
+ unknownPostings,
18087
+ transactionYear
18037
18088
  });
18038
18089
  }
18039
18090
  const hasUnknowns = totalUnknown > 0;
@@ -18077,11 +18128,53 @@ async function importStatementsCore(directory, agent, options, configLoader = lo
18077
18128
  });
18078
18129
  }
18079
18130
  const importedFiles = [];
18080
- for (const csvFile of csvFiles) {
18081
- const rulesFile = findRulesForCsv(csvFile, rulesMapping);
18131
+ for (const fileResult of fileResults) {
18132
+ const csvFile = path6.join(directory, fileResult.csv);
18133
+ const rulesFile = fileResult.rulesFile ? path6.join(directory, fileResult.rulesFile) : null;
18082
18134
  if (!rulesFile)
18083
18135
  continue;
18084
- const result = await hledgerExecutor(["import", csvFile, "--rules-file", rulesFile]);
18136
+ const year = fileResult.transactionYear;
18137
+ if (!year) {
18138
+ return JSON.stringify({
18139
+ success: false,
18140
+ files: fileResults,
18141
+ summary: {
18142
+ filesProcessed: csvFiles.length,
18143
+ filesWithErrors: 1,
18144
+ filesWithoutRules,
18145
+ totalTransactions,
18146
+ matched: totalMatched,
18147
+ unknown: totalUnknown
18148
+ },
18149
+ error: `No transactions found in ${fileResult.csv}`
18150
+ });
18151
+ }
18152
+ let yearJournalPath;
18153
+ try {
18154
+ yearJournalPath = ensureYearJournalExists(directory, year);
18155
+ } catch (error45) {
18156
+ return JSON.stringify({
18157
+ success: false,
18158
+ files: fileResults,
18159
+ summary: {
18160
+ filesProcessed: csvFiles.length,
18161
+ filesWithErrors: 1,
18162
+ filesWithoutRules,
18163
+ totalTransactions,
18164
+ matched: totalMatched,
18165
+ unknown: totalUnknown
18166
+ },
18167
+ error: error45 instanceof Error ? error45.message : String(error45)
18168
+ });
18169
+ }
18170
+ const result = await hledgerExecutor([
18171
+ "import",
18172
+ "-f",
18173
+ yearJournalPath,
18174
+ csvFile,
18175
+ "--rules-file",
18176
+ rulesFile
18177
+ ]);
18085
18178
  if (result.exitCode !== 0) {
18086
18179
  return JSON.stringify({
18087
18180
  success: false,
@@ -18094,7 +18187,7 @@ async function importStatementsCore(directory, agent, options, configLoader = lo
18094
18187
  matched: totalMatched,
18095
18188
  unknown: totalUnknown
18096
18189
  },
18097
- error: `Import failed for ${path6.relative(directory, csvFile)}: ${result.stderr.trim()}`
18190
+ error: `Import failed for ${fileResult.csv}: ${result.stderr.trim()}`
18098
18191
  });
18099
18192
  }
18100
18193
  importedFiles.push(csvFile);
@@ -5,6 +5,20 @@ The `import-statements` tool imports classified CSV bank statements into hledger
5
5
  - **Check mode** (`checkOnly: true`, default): Validates transactions and reports any that cannot be categorized
6
6
  - **Import mode** (`checkOnly: false`): Imports validated transactions and moves processed files to the done directory
7
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
+
8
22
  ## Arguments
9
23
 
10
24
  | Argument | Type | Default | Description |
@@ -33,7 +47,8 @@ When all transactions have matching rules:
33
47
  "csv": "{paths.pending}/ubs/chf/transactions-ubs-0235-90250546.0.csv",
34
48
  "rulesFile": "{paths.rules}/ubs-0235-90250546.0.rules",
35
49
  "transactions": 25,
36
- "unknownPostings": []
50
+ "unknownPostings": [],
51
+ "transactionYear": 2026
37
52
  }
38
53
  ],
39
54
  "summary": {
@@ -250,3 +265,41 @@ If the config file is missing or invalid:
250
265
  "hint": "Ensure config/import/providers.yaml exists with a 'rules' path configured"
251
266
  }
252
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
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",