@fuzzle/opencode-accountant 0.13.18 → 0.13.19-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/dist/index.js CHANGED
@@ -4280,7 +4280,7 @@ __export(exports_accountSuggester, {
4280
4280
  extractRulePatternsFromFile: () => extractRulePatternsFromFile,
4281
4281
  clearSuggestionCache: () => clearSuggestionCache
4282
4282
  });
4283
- import * as fs21 from "fs";
4283
+ import * as fs22 from "fs";
4284
4284
  import * as crypto from "crypto";
4285
4285
  function clearSuggestionCache() {
4286
4286
  Object.keys(suggestionCache).forEach((key) => delete suggestionCache[key]);
@@ -4290,10 +4290,10 @@ function hashTransaction(posting) {
4290
4290
  return crypto.createHash("md5").update(data).digest("hex");
4291
4291
  }
4292
4292
  function loadExistingAccounts(yearJournalPath) {
4293
- if (!fs21.existsSync(yearJournalPath)) {
4293
+ if (!fs22.existsSync(yearJournalPath)) {
4294
4294
  return [];
4295
4295
  }
4296
- const content = fs21.readFileSync(yearJournalPath, "utf-8");
4296
+ const content = fs22.readFileSync(yearJournalPath, "utf-8");
4297
4297
  const lines = content.split(`
4298
4298
  `);
4299
4299
  const accounts = [];
@@ -4309,10 +4309,10 @@ function loadExistingAccounts(yearJournalPath) {
4309
4309
  return accounts.sort();
4310
4310
  }
4311
4311
  function extractRulePatternsFromFile(rulesPath) {
4312
- if (!fs21.existsSync(rulesPath)) {
4312
+ if (!fs22.existsSync(rulesPath)) {
4313
4313
  return [];
4314
4314
  }
4315
- const content = fs21.readFileSync(rulesPath, "utf-8");
4315
+ const content = fs22.readFileSync(rulesPath, "utf-8");
4316
4316
  const lines = content.split(`
4317
4317
  `);
4318
4318
  const patterns = [];
@@ -4547,7 +4547,7 @@ var init_accountSuggester = __esm(() => {
4547
4547
 
4548
4548
  // src/index.ts
4549
4549
  init_agentLoader();
4550
- import { dirname as dirname9, join as join16 } from "path";
4550
+ import { dirname as dirname9, join as join17 } from "path";
4551
4551
  import { fileURLToPath as fileURLToPath3 } from "url";
4552
4552
 
4553
4553
  // node_modules/zod/v4/classic/external.js
@@ -24208,8 +24208,8 @@ Note: This tool requires a contextId from a prior classify/import step.`,
24208
24208
  }
24209
24209
  });
24210
24210
  // src/tools/import-pipeline.ts
24211
- import * as fs22 from "fs";
24212
- import * as path16 from "path";
24211
+ import * as fs23 from "fs";
24212
+ import * as path17 from "path";
24213
24213
 
24214
24214
  // src/utils/accountDeclarations.ts
24215
24215
  init_journalMatchers();
@@ -24954,8 +24954,8 @@ Got:
24954
24954
  }
24955
24955
 
24956
24956
  // src/utils/swissquoteCsvPreprocessor.ts
24957
- import * as fs18 from "fs";
24958
- import * as path13 from "path";
24957
+ import * as fs19 from "fs";
24958
+ import * as path14 from "path";
24959
24959
 
24960
24960
  // src/utils/symbolNormalizer.ts
24961
24961
  function normalizeSymbol(symbol2) {
@@ -25272,7 +25272,8 @@ function generateSellEntry(trade, consumed, logger) {
25272
25272
  }
25273
25273
  function generateDividendEntry(dividend, logger) {
25274
25274
  const date5 = formatDate(dividend.date);
25275
- const description = escapeDescription(`Dividend ${dividend.symbol} - ${dividend.name}`);
25275
+ const label = dividend.transactionType ?? "Dividend";
25276
+ const description = escapeDescription(`${label} ${dividend.symbol} - ${dividend.name}`);
25276
25277
  logger?.debug(`Generating Dividend entry: ${dividend.symbol}, net: ${dividend.netAmount} ${dividend.currency}`);
25277
25278
  const postings = [
25278
25279
  {
@@ -25544,6 +25545,52 @@ function formatJournalFile(entries, year, currency) {
25544
25545
  `);
25545
25546
  }
25546
25547
 
25548
+ // src/utils/mergerFmvConfig.ts
25549
+ init_js_yaml();
25550
+ import * as fs18 from "fs";
25551
+ import * as path13 from "path";
25552
+ var CONFIG_FILE3 = "config/import/merger-fmv.yaml";
25553
+ function loadMergerFmvConfig(projectDir) {
25554
+ const configPath = path13.join(projectDir, CONFIG_FILE3);
25555
+ if (!fs18.existsSync(configPath)) {
25556
+ return null;
25557
+ }
25558
+ const content = fs18.readFileSync(configPath, "utf-8");
25559
+ const parsed = jsYaml.load(content);
25560
+ if (typeof parsed !== "object" || parsed === null) {
25561
+ return null;
25562
+ }
25563
+ const config2 = {};
25564
+ for (const [rawKey, symbols] of Object.entries(parsed)) {
25565
+ let dateKey;
25566
+ const d = new Date(rawKey);
25567
+ if (!isNaN(d.getTime()) && !/^\d{4}-\d{2}-\d{2}$/.test(rawKey)) {
25568
+ dateKey = d.toISOString().split("T")[0];
25569
+ } else {
25570
+ dateKey = rawKey;
25571
+ }
25572
+ if (typeof symbols !== "object" || symbols === null) {
25573
+ continue;
25574
+ }
25575
+ const symbolMap = {};
25576
+ for (const [symbol2, price] of Object.entries(symbols)) {
25577
+ if (typeof price === "number") {
25578
+ symbolMap[symbol2] = price;
25579
+ }
25580
+ }
25581
+ if (Object.keys(symbolMap).length > 0) {
25582
+ config2[dateKey] = symbolMap;
25583
+ }
25584
+ }
25585
+ return Object.keys(config2).length > 0 ? config2 : null;
25586
+ }
25587
+ function lookupFmvForDate(config2, date5) {
25588
+ if (!config2) {
25589
+ return null;
25590
+ }
25591
+ return config2[date5] ?? null;
25592
+ }
25593
+
25547
25594
  // src/utils/swissquoteCsvPreprocessor.ts
25548
25595
  var SWISSQUOTE_HEADER = "Date;Order #;Transaction;Symbol;Name;ISIN;Quantity;Unit price;Costs;Accrued Interest;Net Amount;Balance;Currency";
25549
25596
  var SIMPLE_TRANSACTION_TYPES = new Set([
@@ -25556,13 +25603,11 @@ var SIMPLE_TRANSACTION_TYPES = new Set([
25556
25603
  "Payment",
25557
25604
  "Debit",
25558
25605
  "Fees Tax Statement",
25559
- "Dividend (Reversed)",
25560
- "Reversal (Dividend)",
25561
25606
  "Redemption"
25562
25607
  ]);
25563
25608
  var FOREX_TYPES = new Set(["Forex credit", "Forex debit"]);
25564
25609
  var TRADE_TYPES = new Set(["Buy", "Sell"]);
25565
- var DIVIDEND_TYPES = new Set(["Dividend"]);
25610
+ var DIVIDEND_TYPES = new Set(["Dividend", "Dividend (Reversed)", "Reversal (Dividend)"]);
25566
25611
  var CORPORATE_ACTION_TYPES = new Set([
25567
25612
  "Merger",
25568
25613
  "Reverse Split",
@@ -25671,7 +25716,8 @@ function toDividendEntry(txn) {
25671
25716
  grossAmount,
25672
25717
  withholdingTax,
25673
25718
  netAmount,
25674
- currency: txn.currency
25719
+ currency: txn.currency,
25720
+ transactionType: txn.transaction
25675
25721
  };
25676
25722
  }
25677
25723
  function toCorporateActionEntry(txn) {
@@ -25754,7 +25800,26 @@ function groupMergerActions(actions) {
25754
25800
  }
25755
25801
  return Array.from(groupMap.values());
25756
25802
  }
25757
- function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, logger) {
25803
+ function computeAllocationProportions(incoming, fmvData, date5, logger) {
25804
+ if (incoming.length <= 1) {
25805
+ return incoming.map(() => 1);
25806
+ }
25807
+ const quantities = incoming.map((a) => Math.abs(a.quantity));
25808
+ const symbols = incoming.map((a) => a.symbol);
25809
+ if (fmvData) {
25810
+ const allCovered = symbols.every((s) => (s in fmvData));
25811
+ if (allCovered) {
25812
+ const fmvValues = symbols.map((s, i2) => fmvData[s] * quantities[i2]);
25813
+ const totalFmv = fmvValues.reduce((sum, v) => sum + v, 0);
25814
+ if (totalFmv > 0) {
25815
+ logger?.info(`Using FMV-based cost allocation for merger (total FMV: ${totalFmv.toFixed(2)})`);
25816
+ return fmvValues.map((v) => v / totalFmv);
25817
+ }
25818
+ }
25819
+ }
25820
+ throw new Error(`Merger on ${date5} has ${incoming.length} incoming symbols (${symbols.join(", ")}) but no FMV data. ` + `Add prices to config/import/merger-fmv.yaml for this date.`);
25821
+ }
25822
+ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, mergerFmv, logger) {
25758
25823
  const entries = [];
25759
25824
  if (group.outgoing.length === 0 && group.incoming.length === 0) {
25760
25825
  return entries;
@@ -25773,12 +25838,14 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25773
25838
  quantity: -(pendingState.outgoingQuantities[i2] || 0)
25774
25839
  });
25775
25840
  }
25776
- const totalIncomingQty2 = group.incoming.reduce((sum, a) => sum + Math.abs(a.quantity), 0);
25841
+ const mergerDate = formatDate(group.date);
25842
+ const fmvData2 = lookupFmvForDate(mergerFmv, mergerDate);
25843
+ const proportions2 = computeAllocationProportions(group.incoming, fmvData2, mergerDate, logger);
25777
25844
  const incomingTotalCosts2 = [];
25778
- for (const inc of group.incoming) {
25845
+ for (let idx = 0;idx < group.incoming.length; idx++) {
25846
+ const inc = group.incoming[idx];
25779
25847
  const absQty = Math.abs(inc.quantity);
25780
- const proportion = absQty / totalIncomingQty2;
25781
- const allocatedCost = pendingState.totalCostBasis * proportion;
25848
+ const allocatedCost = pendingState.totalCostBasis * proportions2[idx];
25782
25849
  const costBasisPerUnit = absQty > 0 ? allocatedCost / absQty : 0;
25783
25850
  incomingTotalCosts2.push(allocatedCost);
25784
25851
  const lot = {
@@ -25850,12 +25917,14 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25850
25917
  removeLots(inventory, out.symbol, logger);
25851
25918
  logger?.debug(`Merger outgoing: removed ${absQty} ${out.symbol}`);
25852
25919
  }
25853
- const totalIncomingQty = group.incoming.reduce((sum, a) => sum + Math.abs(a.quantity), 0);
25920
+ const mergerDateFull = formatDate(group.date);
25921
+ const fmvData = lookupFmvForDate(mergerFmv, mergerDateFull);
25922
+ const proportions = computeAllocationProportions(group.incoming, fmvData, mergerDateFull, logger);
25854
25923
  const incomingTotalCosts = [];
25855
- for (const inc of group.incoming) {
25924
+ for (let idx = 0;idx < group.incoming.length; idx++) {
25925
+ const inc = group.incoming[idx];
25856
25926
  const absQty = Math.abs(inc.quantity);
25857
- const proportion = totalIncomingQty > 0 ? absQty / totalIncomingQty : 0;
25858
- const allocatedCost = totalCostBasis * proportion;
25927
+ const allocatedCost = totalCostBasis * proportions[idx];
25859
25928
  const costBasisPerUnit = absQty > 0 ? allocatedCost / absQty : 0;
25860
25929
  incomingTotalCosts.push(allocatedCost);
25861
25930
  const lot = {
@@ -25883,25 +25952,25 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25883
25952
  }
25884
25953
  function getPendingMergerDir(projectDir, lotInventoryPath) {
25885
25954
  const lotDir = lotInventoryPath.replace(/{symbol}[^/]*$/, "");
25886
- return path13.join(projectDir, lotDir, "pending-mergers");
25955
+ return path14.join(projectDir, lotDir, "pending-mergers");
25887
25956
  }
25888
25957
  function savePendingMerger(projectDir, lotInventoryPath, key, state, logger) {
25889
25958
  const dir = getPendingMergerDir(projectDir, lotInventoryPath);
25890
- if (!fs18.existsSync(dir)) {
25891
- fs18.mkdirSync(dir, { recursive: true });
25959
+ if (!fs19.existsSync(dir)) {
25960
+ fs19.mkdirSync(dir, { recursive: true });
25892
25961
  }
25893
- const filePath = path13.join(dir, `${key.replace(/[^a-zA-Z0-9-]/g, "_")}.json`);
25894
- fs18.writeFileSync(filePath, JSON.stringify(state, null, 2));
25962
+ const filePath = path14.join(dir, `${key.replace(/[^a-zA-Z0-9-]/g, "_")}.json`);
25963
+ fs19.writeFileSync(filePath, JSON.stringify(state, null, 2));
25895
25964
  logger?.debug(`Saved pending merger state: ${key}`);
25896
25965
  }
25897
25966
  function loadPendingMerger(projectDir, lotInventoryPath, key, logger) {
25898
25967
  const dir = getPendingMergerDir(projectDir, lotInventoryPath);
25899
- const filePath = path13.join(dir, `${key.replace(/[^a-zA-Z0-9-]/g, "_")}.json`);
25900
- if (!fs18.existsSync(filePath)) {
25968
+ const filePath = path14.join(dir, `${key.replace(/[^a-zA-Z0-9-]/g, "_")}.json`);
25969
+ if (!fs19.existsSync(filePath)) {
25901
25970
  return null;
25902
25971
  }
25903
25972
  try {
25904
- const content = fs18.readFileSync(filePath, "utf-8");
25973
+ const content = fs19.readFileSync(filePath, "utf-8");
25905
25974
  return JSON.parse(content);
25906
25975
  } catch (error45) {
25907
25976
  const message = error45 instanceof Error ? error45.message : String(error45);
@@ -25911,27 +25980,27 @@ function loadPendingMerger(projectDir, lotInventoryPath, key, logger) {
25911
25980
  }
25912
25981
  function removePendingMerger(projectDir, lotInventoryPath, key, logger) {
25913
25982
  const dir = getPendingMergerDir(projectDir, lotInventoryPath);
25914
- const filePath = path13.join(dir, `${key.replace(/[^a-zA-Z0-9-]/g, "_")}.json`);
25915
- if (fs18.existsSync(filePath)) {
25916
- fs18.unlinkSync(filePath);
25983
+ const filePath = path14.join(dir, `${key.replace(/[^a-zA-Z0-9-]/g, "_")}.json`);
25984
+ if (fs19.existsSync(filePath)) {
25985
+ fs19.unlinkSync(filePath);
25917
25986
  logger?.debug(`Removed pending merger state: ${key}`);
25918
25987
  }
25919
25988
  }
25920
25989
  function resolveRemainingPendingMergers(projectDir, lotInventoryPath, logger) {
25921
25990
  const pendingDir = getPendingMergerDir(projectDir, lotInventoryPath);
25922
- if (!fs18.existsSync(pendingDir)) {
25991
+ if (!fs19.existsSync(pendingDir)) {
25923
25992
  return { resolved: 0, journalFilesUpdated: [] };
25924
25993
  }
25925
- const files = fs18.readdirSync(pendingDir).filter((f) => f.endsWith(".json"));
25994
+ const files = fs19.readdirSync(pendingDir).filter((f) => f.endsWith(".json"));
25926
25995
  if (files.length === 0) {
25927
25996
  return { resolved: 0, journalFilesUpdated: [] };
25928
25997
  }
25929
25998
  const entriesByJournal = new Map;
25930
25999
  const resolvedFiles = [];
25931
26000
  for (const file2 of files) {
25932
- const filePath = path13.join(pendingDir, file2);
26001
+ const filePath = path14.join(pendingDir, file2);
25933
26002
  try {
25934
- const content = fs18.readFileSync(filePath, "utf-8");
26003
+ const content = fs19.readFileSync(filePath, "utf-8");
25935
26004
  const state = JSON.parse(content);
25936
26005
  const dateMatch = state.date.match(/\d{2}-\d{2}-(\d{4})/);
25937
26006
  if (!dateMatch) {
@@ -25940,12 +26009,12 @@ function resolveRemainingPendingMergers(projectDir, lotInventoryPath, logger) {
25940
26009
  }
25941
26010
  const year = parseInt(dateMatch[1], 10);
25942
26011
  const entry = generateDirectWorthlessEntry(state.date, state.orderNum, state.outgoingSymbols, state.outgoingIsins, state.outgoingQuantities || [], state.outgoingTotalCosts || [], state.totalCostBasis, state.currency, logger);
25943
- const journalFile = path13.join(projectDir, "ledger", "investments", `${year}-${state.currency.toLowerCase()}.journal`);
26012
+ const journalFile = path14.join(projectDir, "ledger", "investments", `${year}-${state.currency.toLowerCase()}.journal`);
25944
26013
  if (!entriesByJournal.has(journalFile)) {
25945
26014
  entriesByJournal.set(journalFile, []);
25946
26015
  }
25947
26016
  entriesByJournal.get(journalFile).push(entry);
25948
- fs18.unlinkSync(filePath);
26017
+ fs19.unlinkSync(filePath);
25949
26018
  resolvedFiles.push(file2);
25950
26019
  logger?.info(`Resolved pending merger ${file2} as worthless: ${state.outgoingSymbols.join(", ")} (${state.totalCostBasis.toFixed(2)} ${state.currency})`);
25951
26020
  } catch (error45) {
@@ -25955,32 +26024,33 @@ function resolveRemainingPendingMergers(projectDir, lotInventoryPath, logger) {
25955
26024
  }
25956
26025
  const journalFilesUpdated = [];
25957
26026
  for (const [journalFile, journalEntries] of entriesByJournal) {
25958
- if (fs18.existsSync(journalFile)) {
25959
- fs18.appendFileSync(journalFile, `
26027
+ if (fs19.existsSync(journalFile)) {
26028
+ fs19.appendFileSync(journalFile, `
25960
26029
  ` + journalEntries.join(`
25961
26030
  `));
25962
26031
  } else {
25963
- const basename5 = path13.basename(journalFile, ".journal");
26032
+ const basename5 = path14.basename(journalFile, ".journal");
25964
26033
  const parts = basename5.split("-");
25965
26034
  const yearStr = parts[0];
25966
26035
  const currency = parts.slice(1).join("-");
25967
26036
  const header = formatJournalFile(journalEntries, parseInt(yearStr, 10), currency);
25968
- fs18.writeFileSync(journalFile, header);
26037
+ fs19.writeFileSync(journalFile, header);
25969
26038
  }
25970
26039
  journalFilesUpdated.push(journalFile);
25971
- logger?.info(`Updated ${path13.basename(journalFile)} with worthless resolution entries`);
26040
+ logger?.info(`Updated ${path14.basename(journalFile)} with worthless resolution entries`);
25972
26041
  }
25973
- const remaining = fs18.readdirSync(pendingDir).filter((f) => f.endsWith(".json"));
26042
+ const remaining = fs19.readdirSync(pendingDir).filter((f) => f.endsWith(".json"));
25974
26043
  if (remaining.length === 0) {
25975
26044
  try {
25976
- fs18.rmdirSync(pendingDir);
26045
+ fs19.rmdirSync(pendingDir);
25977
26046
  logger?.debug("Removed empty pending-mergers directory");
25978
26047
  } catch {}
25979
26048
  }
25980
26049
  return { resolved: resolvedFiles.length, journalFilesUpdated };
25981
26050
  }
25982
26051
  async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInventoryPath = DEFAULT_LOT_INVENTORY_PATH, symbolMap = {}, logger) {
25983
- logger?.startSection(`Swissquote Preprocessing: ${path13.basename(csvPath)}`);
26052
+ logger?.startSection(`Swissquote Preprocessing: ${path14.basename(csvPath)}`);
26053
+ const mergerFmv = loadMergerFmvConfig(projectDir);
25984
26054
  const stats = {
25985
26055
  totalRows: 0,
25986
26056
  simpleTransactions: 0,
@@ -25990,7 +26060,7 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
25990
26060
  forexTransactions: 0,
25991
26061
  skipped: 0
25992
26062
  };
25993
- if (!fs18.existsSync(csvPath)) {
26063
+ if (!fs19.existsSync(csvPath)) {
25994
26064
  logger?.error(`CSV file not found: ${csvPath}`);
25995
26065
  logger?.endSection();
25996
26066
  return {
@@ -26002,7 +26072,7 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26002
26072
  };
26003
26073
  }
26004
26074
  logger?.logStep("parse-csv", "start", `Reading ${csvPath}`);
26005
- const content = fs18.readFileSync(csvPath, "utf-8");
26075
+ const content = fs19.readFileSync(csvPath, "utf-8");
26006
26076
  const lines = content.split(`
26007
26077
  `).filter((line) => line.trim());
26008
26078
  const header = lines[0];
@@ -26190,7 +26260,7 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26190
26260
  break;
26191
26261
  }
26192
26262
  case "mergerGroup": {
26193
- const groupEntries = processMultiWayMerger(event.group, inventory, lotInventoryPath, projectDir, logger);
26263
+ const groupEntries = processMultiWayMerger(event.group, inventory, lotInventoryPath, projectDir, mergerFmv, logger);
26194
26264
  journalEntries.push(...groupEntries);
26195
26265
  break;
26196
26266
  }
@@ -26207,9 +26277,9 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26207
26277
  logger?.logStep("save-inventory", "success", "Lot inventory saved");
26208
26278
  let simpleTransactionsCsv = null;
26209
26279
  let journalFile = null;
26210
- const csvDir = path13.dirname(csvPath);
26211
- const csvBasename = path13.basename(csvPath, ".csv");
26212
- simpleTransactionsCsv = path13.join(csvDir, `${csvBasename}-simple.csv`);
26280
+ const csvDir = path14.dirname(csvPath);
26281
+ const csvBasename = path14.basename(csvPath, ".csv");
26282
+ simpleTransactionsCsv = path14.join(csvDir, `${csvBasename}-simple.csv`);
26213
26283
  const csvContent = [
26214
26284
  SWISSQUOTE_HEADER,
26215
26285
  ...simpleTransactions.map((txn) => [
@@ -26229,23 +26299,23 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26229
26299
  ].join(";"))
26230
26300
  ].join(`
26231
26301
  `);
26232
- fs18.writeFileSync(simpleTransactionsCsv, csvContent);
26233
- logger?.logStep("write-csv", "success", `Wrote ${simpleTransactions.length} simple transactions to ${path13.basename(simpleTransactionsCsv)}`);
26302
+ fs19.writeFileSync(simpleTransactionsCsv, csvContent);
26303
+ logger?.logStep("write-csv", "success", `Wrote ${simpleTransactions.length} simple transactions to ${path14.basename(simpleTransactionsCsv)}`);
26234
26304
  if (journalEntries.length > 0) {
26235
- const investmentsDir = path13.join(projectDir, "ledger", "investments");
26236
- if (!fs18.existsSync(investmentsDir)) {
26237
- fs18.mkdirSync(investmentsDir, { recursive: true });
26305
+ const investmentsDir = path14.join(projectDir, "ledger", "investments");
26306
+ if (!fs19.existsSync(investmentsDir)) {
26307
+ fs19.mkdirSync(investmentsDir, { recursive: true });
26238
26308
  }
26239
- journalFile = path13.join(investmentsDir, `${year}-${currency}.journal`);
26309
+ journalFile = path14.join(investmentsDir, `${year}-${currency}.journal`);
26240
26310
  const journalContent = formatJournalFile(journalEntries, year, currency);
26241
- if (fs18.existsSync(journalFile)) {
26242
- fs18.appendFileSync(journalFile, `
26311
+ if (fs19.existsSync(journalFile)) {
26312
+ fs19.appendFileSync(journalFile, `
26243
26313
  ` + journalEntries.join(`
26244
26314
  `));
26245
- logger?.logStep("write-journal", "success", `Appended ${journalEntries.length} entries to ${path13.basename(journalFile)}`);
26315
+ logger?.logStep("write-journal", "success", `Appended ${journalEntries.length} entries to ${path14.basename(journalFile)}`);
26246
26316
  } else {
26247
- fs18.writeFileSync(journalFile, journalContent);
26248
- logger?.logStep("write-journal", "success", `Created ${path13.basename(journalFile)} with ${journalEntries.length} entries`);
26317
+ fs19.writeFileSync(journalFile, journalContent);
26318
+ logger?.logStep("write-journal", "success", `Created ${path14.basename(journalFile)} with ${journalEntries.length} entries`);
26249
26319
  }
26250
26320
  }
26251
26321
  const stockSymbols = new Set;
@@ -26262,9 +26332,9 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26262
26332
  }
26263
26333
  }
26264
26334
  if (stockSymbols.size > 0) {
26265
- const commodityJournalPath = path13.join(projectDir, "ledger", "investments", "commodities.journal");
26335
+ const commodityJournalPath = path14.join(projectDir, "ledger", "investments", "commodities.journal");
26266
26336
  ensureCommodityDeclarations(commodityJournalPath, stockSymbols, logger);
26267
- const accountJournalPath = path13.join(projectDir, "ledger", "investments", "accounts.journal");
26337
+ const accountJournalPath = path14.join(projectDir, "ledger", "investments", "accounts.journal");
26268
26338
  const investmentAccounts = new Set;
26269
26339
  for (const symbol2 of stockSymbols) {
26270
26340
  investmentAccounts.add(`assets:investments:stocks:${symbol2}`);
@@ -26301,10 +26371,10 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26301
26371
  }
26302
26372
 
26303
26373
  // src/utils/currencyExchangeGenerator.ts
26304
- import * as fs19 from "fs";
26305
- import * as path14 from "path";
26374
+ import * as fs20 from "fs";
26375
+ import * as path15 from "path";
26306
26376
  function parseExchangeRows(csvPath) {
26307
- const content = fs19.readFileSync(csvPath, "utf-8");
26377
+ const content = fs20.readFileSync(csvPath, "utf-8");
26308
26378
  const lines = content.trim().split(`
26309
26379
  `);
26310
26380
  if (lines.length < 2)
@@ -26428,7 +26498,7 @@ function formatDateIso2(date5) {
26428
26498
  return `${y}-${m}-${d}`;
26429
26499
  }
26430
26500
  function filterExchangeRows(csvPath, rowIndicesToRemove, logger) {
26431
- const content = fs19.readFileSync(csvPath, "utf-8");
26501
+ const content = fs20.readFileSync(csvPath, "utf-8");
26432
26502
  const lines = content.trim().split(`
26433
26503
  `);
26434
26504
  const filteredLines = [lines[0]];
@@ -26437,13 +26507,13 @@ function filterExchangeRows(csvPath, rowIndicesToRemove, logger) {
26437
26507
  continue;
26438
26508
  filteredLines.push(lines[i2]);
26439
26509
  }
26440
- const dir = path14.dirname(csvPath);
26441
- const basename6 = path14.basename(csvPath, ".csv");
26442
- const filteredPath = path14.join(dir, `${basename6}-filtered.csv`);
26443
- fs19.writeFileSync(filteredPath, filteredLines.join(`
26510
+ const dir = path15.dirname(csvPath);
26511
+ const basename6 = path15.basename(csvPath, ".csv");
26512
+ const filteredPath = path15.join(dir, `${basename6}-filtered.csv`);
26513
+ fs20.writeFileSync(filteredPath, filteredLines.join(`
26444
26514
  `) + `
26445
26515
  `);
26446
- logger?.info(`Filtered ${rowIndicesToRemove.size} exchange rows from ${path14.basename(csvPath)} \u2192 ${path14.basename(filteredPath)}`);
26516
+ logger?.info(`Filtered ${rowIndicesToRemove.size} exchange rows from ${path15.basename(csvPath)} \u2192 ${path15.basename(filteredPath)}`);
26447
26517
  return filteredPath;
26448
26518
  }
26449
26519
  function generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger) {
@@ -26453,7 +26523,7 @@ function generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger)
26453
26523
  const rows = parseExchangeRows(csvPath);
26454
26524
  if (rows.length > 0) {
26455
26525
  rowsByCsv.set(csvPath, rows);
26456
- logger?.info(`Found ${rows.length} EXCHANGE rows in ${path14.basename(csvPath)}`);
26526
+ logger?.info(`Found ${rows.length} EXCHANGE rows in ${path15.basename(csvPath)}`);
26457
26527
  }
26458
26528
  }
26459
26529
  if (rowsByCsv.size < 2) {
@@ -26470,8 +26540,8 @@ function generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger)
26470
26540
  const matches = matchExchangePairs(rowsByCsv);
26471
26541
  logger?.info(`Matched ${matches.length} exchange pairs`);
26472
26542
  let journalContent = "";
26473
- if (fs19.existsSync(yearJournalPath)) {
26474
- journalContent = fs19.readFileSync(yearJournalPath, "utf-8");
26543
+ if (fs20.existsSync(yearJournalPath)) {
26544
+ journalContent = fs20.readFileSync(yearJournalPath, "utf-8");
26475
26545
  }
26476
26546
  const newEntries = [];
26477
26547
  let skippedDuplicates = 0;
@@ -26498,8 +26568,8 @@ function generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger)
26498
26568
 
26499
26569
  `) + `
26500
26570
  `;
26501
- fs19.appendFileSync(yearJournalPath, appendContent);
26502
- logger?.info(`Appended ${newEntries.length} currency exchange entries to ${path14.basename(yearJournalPath)}`);
26571
+ fs20.appendFileSync(yearJournalPath, appendContent);
26572
+ logger?.info(`Appended ${newEntries.length} currency exchange entries to ${path15.basename(yearJournalPath)}`);
26503
26573
  } else {
26504
26574
  logger?.info("No new currency exchange entries to add");
26505
26575
  }
@@ -26522,8 +26592,8 @@ function generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger)
26522
26592
  }
26523
26593
 
26524
26594
  // src/utils/swissquoteForexGenerator.ts
26525
- import * as fs20 from "fs";
26526
- import * as path15 from "path";
26595
+ import * as fs21 from "fs";
26596
+ import * as path16 from "path";
26527
26597
  function formatDate2(dateStr) {
26528
26598
  const match2 = dateStr.match(/^(\d{2})-(\d{2})-(\d{4})/);
26529
26599
  if (!match2) {
@@ -26630,8 +26700,8 @@ function generateSwissquoteForexJournal(allForexRows, yearJournalPath, logger) {
26630
26700
  logger?.warn(`${unmatchedCount} forex rows could not be matched`);
26631
26701
  }
26632
26702
  let journalContent = "";
26633
- if (fs20.existsSync(yearJournalPath)) {
26634
- journalContent = fs20.readFileSync(yearJournalPath, "utf-8");
26703
+ if (fs21.existsSync(yearJournalPath)) {
26704
+ journalContent = fs21.readFileSync(yearJournalPath, "utf-8");
26635
26705
  }
26636
26706
  const newEntries = [];
26637
26707
  let skippedDuplicates = 0;
@@ -26653,17 +26723,17 @@ function generateSwissquoteForexJournal(allForexRows, yearJournalPath, logger) {
26653
26723
  newEntries.push(formatForexEntry(match2));
26654
26724
  }
26655
26725
  if (newEntries.length > 0) {
26656
- const dir = path15.dirname(yearJournalPath);
26657
- if (!fs20.existsSync(dir)) {
26658
- fs20.mkdirSync(dir, { recursive: true });
26726
+ const dir = path16.dirname(yearJournalPath);
26727
+ if (!fs21.existsSync(dir)) {
26728
+ fs21.mkdirSync(dir, { recursive: true });
26659
26729
  }
26660
26730
  const appendContent = `
26661
26731
  ` + newEntries.join(`
26662
26732
 
26663
26733
  `) + `
26664
26734
  `;
26665
- fs20.appendFileSync(yearJournalPath, appendContent);
26666
- logger?.info(`Appended ${newEntries.length} forex entries to ${path15.basename(yearJournalPath)}`);
26735
+ fs21.appendFileSync(yearJournalPath, appendContent);
26736
+ logger?.info(`Appended ${newEntries.length} forex entries to ${path16.basename(yearJournalPath)}`);
26667
26737
  } else {
26668
26738
  logger?.info("No new forex entries to add");
26669
26739
  }
@@ -26748,7 +26818,7 @@ function executePreprocessFiatStep(context, contextIds, logger) {
26748
26818
  for (const contextId of contextIds) {
26749
26819
  const importCtx = loadContext(context.directory, contextId);
26750
26820
  if (importCtx.provider === "revolut" && importCtx.currency !== "btc") {
26751
- fiatCsvPaths.push(path16.join(context.directory, importCtx.filePath));
26821
+ fiatCsvPaths.push(path17.join(context.directory, importCtx.filePath));
26752
26822
  }
26753
26823
  }
26754
26824
  if (fiatCsvPaths.length === 0) {
@@ -26792,7 +26862,7 @@ function executePreprocessBtcStep(context, contextIds, logger) {
26792
26862
  for (const contextId of contextIds) {
26793
26863
  const importCtx = loadContext(context.directory, contextId);
26794
26864
  if (importCtx.provider === "revolut" && importCtx.currency === "btc") {
26795
- btcCsvPath = path16.join(context.directory, importCtx.filePath);
26865
+ btcCsvPath = path17.join(context.directory, importCtx.filePath);
26796
26866
  break;
26797
26867
  }
26798
26868
  }
@@ -26826,7 +26896,7 @@ async function executeBtcPurchaseStep(context, contextIds, logger) {
26826
26896
  const importCtx = loadContext(context.directory, contextId);
26827
26897
  if (importCtx.provider !== "revolut")
26828
26898
  continue;
26829
- const csvPath = path16.join(context.directory, importCtx.filePath);
26899
+ const csvPath = path17.join(context.directory, importCtx.filePath);
26830
26900
  if (importCtx.currency === "btc") {
26831
26901
  btcCsvPath = csvPath;
26832
26902
  } else {
@@ -26839,7 +26909,7 @@ async function executeBtcPurchaseStep(context, contextIds, logger) {
26839
26909
  }
26840
26910
  logger?.startSection("Step 1b: Generate BTC Purchase Entries");
26841
26911
  logger?.logStep("BTC Purchases", "start");
26842
- const btcFilename = path16.basename(btcCsvPath);
26912
+ const btcFilename = path17.basename(btcCsvPath);
26843
26913
  const yearMatch = btcFilename.match(/(\d{4})-\d{2}-\d{2}/);
26844
26914
  const year = yearMatch ? parseInt(yearMatch[1], 10) : new Date().getFullYear();
26845
26915
  const yearJournalPath = ensureYearJournalExists(context.directory, year);
@@ -26866,7 +26936,7 @@ async function executeCurrencyExchangeStep(context, contextIds, logger) {
26866
26936
  continue;
26867
26937
  if (importCtx.currency === "btc")
26868
26938
  continue;
26869
- const csvPath = path16.join(context.directory, importCtx.filePath);
26939
+ const csvPath = path17.join(context.directory, importCtx.filePath);
26870
26940
  fiatContexts.push({ contextId, csvPath });
26871
26941
  }
26872
26942
  if (fiatContexts.length < 2) {
@@ -26875,7 +26945,7 @@ async function executeCurrencyExchangeStep(context, contextIds, logger) {
26875
26945
  }
26876
26946
  logger?.startSection("Step 1e: Generate Currency Exchange Entries");
26877
26947
  logger?.logStep("Currency Exchanges", "start");
26878
- const firstFilename = path16.basename(fiatContexts[0].csvPath);
26948
+ const firstFilename = path17.basename(fiatContexts[0].csvPath);
26879
26949
  const yearMatch = firstFilename.match(/(\d{4})-\d{2}-\d{2}/);
26880
26950
  const year = yearMatch ? parseInt(yearMatch[1], 10) : new Date().getFullYear();
26881
26951
  const yearJournalPath = ensureYearJournalExists(context.directory, year);
@@ -26891,9 +26961,9 @@ async function executeCurrencyExchangeStep(context, contextIds, logger) {
26891
26961
  continue;
26892
26962
  const filteredCsvPath = filterExchangeRows(csvPath, indices, logger);
26893
26963
  updateContext(context.directory, contextId, {
26894
- filePath: path16.relative(context.directory, filteredCsvPath)
26964
+ filePath: path17.relative(context.directory, filteredCsvPath)
26895
26965
  });
26896
- logger?.info(`Updated context ${contextId} to use filtered CSV: ${path16.basename(filteredCsvPath)}`);
26966
+ logger?.info(`Updated context ${contextId} to use filtered CSV: ${path17.basename(filteredCsvPath)}`);
26897
26967
  }
26898
26968
  }
26899
26969
  const message = result.entriesAdded > 0 ? `Generated ${result.entriesAdded} currency exchange entries (${result.matchCount} matched, ${result.skippedDuplicates} skipped)` : `No new currency exchange entries (${result.matchCount} matched, ${result.skippedDuplicates} duplicates)`;
@@ -26910,7 +26980,7 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
26910
26980
  for (const contextId of contextIds) {
26911
26981
  const importCtx = loadContext(context.directory, contextId);
26912
26982
  if (importCtx.provider === "swissquote") {
26913
- const csvPath = path16.join(context.directory, importCtx.filePath);
26983
+ const csvPath = path17.join(context.directory, importCtx.filePath);
26914
26984
  const toDateMatch = importCtx.filename.match(/to-\d{4}(\d{4})\./);
26915
26985
  let year = new Date().getFullYear();
26916
26986
  if (toDateMatch) {
@@ -26934,11 +27004,11 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
26934
27004
  const lotInventoryPath = swissquoteProvider?.lotInventoryPath ?? DEFAULT_LOT_INVENTORY_PATH;
26935
27005
  const symbolMapPath = swissquoteProvider?.symbolMapPath ?? "config/import/symbolMap.yaml";
26936
27006
  let symbolMap = {};
26937
- const symbolMapFullPath = path16.join(context.directory, symbolMapPath);
26938
- if (fs22.existsSync(symbolMapFullPath)) {
27007
+ const symbolMapFullPath = path17.join(context.directory, symbolMapPath);
27008
+ if (fs23.existsSync(symbolMapFullPath)) {
26939
27009
  try {
26940
27010
  const yaml = await Promise.resolve().then(() => (init_js_yaml(), exports_js_yaml));
26941
- const content = fs22.readFileSync(symbolMapFullPath, "utf-8");
27011
+ const content = fs23.readFileSync(symbolMapFullPath, "utf-8");
26942
27012
  const parsed = yaml.load(content);
26943
27013
  if (parsed && typeof parsed === "object") {
26944
27014
  symbolMap = parsed;
@@ -26962,7 +27032,7 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
26962
27032
  let lastJournalFile = null;
26963
27033
  const allForexRows = [];
26964
27034
  for (const sqCtx of swissquoteContexts) {
26965
- logger?.logStep("Swissquote Preprocess", "start", `Processing ${path16.basename(sqCtx.csvPath)}`);
27035
+ logger?.logStep("Swissquote Preprocess", "start", `Processing ${path17.basename(sqCtx.csvPath)}`);
26966
27036
  const result = await preprocessSwissquote(sqCtx.csvPath, context.directory, sqCtx.currency, sqCtx.year, lotInventoryPath, symbolMap, logger);
26967
27037
  totalStats.totalRows += result.stats.totalRows;
26968
27038
  totalStats.simpleTransactions += result.stats.simpleTransactions;
@@ -26977,9 +27047,9 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
26977
27047
  }
26978
27048
  if (result.simpleTransactionsCsv) {
26979
27049
  updateContext(context.directory, sqCtx.contextId, {
26980
- filePath: path16.relative(context.directory, result.simpleTransactionsCsv)
27050
+ filePath: path17.relative(context.directory, result.simpleTransactionsCsv)
26981
27051
  });
26982
- logger?.info(`Updated context ${sqCtx.contextId} to use filtered CSV: ${path16.basename(result.simpleTransactionsCsv)}`);
27052
+ logger?.info(`Updated context ${sqCtx.contextId} to use filtered CSV: ${path17.basename(result.simpleTransactionsCsv)}`);
26983
27053
  }
26984
27054
  logger?.logStep("Swissquote Preprocess", "success", `Processed: ${result.stats.trades} trades, ${result.stats.dividends} dividends, ${result.stats.corporateActions} corporate actions, ${result.stats.forexTransactions} forex`);
26985
27055
  }
@@ -26989,11 +27059,11 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
26989
27059
  }
26990
27060
  if (allForexRows.length > 0) {
26991
27061
  const firstCtx = swissquoteContexts[0];
26992
- const yearJournalPath = path16.join(context.directory, "ledger", `${firstCtx.year}.journal`);
27062
+ const yearJournalPath = path17.join(context.directory, "ledger", `${firstCtx.year}.journal`);
26993
27063
  logger?.info(`Generating forex journal entries for ${allForexRows.length} forex rows...`);
26994
27064
  const forexResult = generateSwissquoteForexJournal(allForexRows, yearJournalPath, logger);
26995
27065
  if (forexResult.accountsUsed.size > 0) {
26996
- const accountJournalPath = path16.join(context.directory, "ledger", "investments", "accounts.journal");
27066
+ const accountJournalPath = path17.join(context.directory, "ledger", "investments", "accounts.journal");
26997
27067
  ensureInvestmentAccountDeclarations(accountJournalPath, forexResult.accountsUsed, logger);
26998
27068
  }
26999
27069
  logger?.logStep("Swissquote Forex", "success", `Matched ${forexResult.matchCount} forex pairs, added ${forexResult.entriesAdded} entries` + (forexResult.skippedDuplicates > 0 ? `, skipped ${forexResult.skippedDuplicates} duplicates` : "") + (forexResult.unmatchedRows > 0 ? `, ${forexResult.unmatchedRows} unmatched rows` : ""));
@@ -27019,9 +27089,9 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
27019
27089
  logger?.startSection("Step 2: Check Account Declarations");
27020
27090
  logger?.logStep("Check Accounts", "start");
27021
27091
  const config2 = context.configLoader(context.directory);
27022
- const rulesDir = path16.join(context.directory, config2.paths.rules);
27092
+ const rulesDir = path17.join(context.directory, config2.paths.rules);
27023
27093
  const importCtx = loadContext(context.directory, contextId);
27024
- const csvPath = path16.join(context.directory, importCtx.filePath);
27094
+ const csvPath = path17.join(context.directory, importCtx.filePath);
27025
27095
  const csvFiles = [csvPath];
27026
27096
  const rulesMapping = loadRulesMapping(rulesDir);
27027
27097
  const matchedRulesFiles = new Set;
@@ -27044,7 +27114,7 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
27044
27114
  context.result.steps.accountDeclarations = buildStepResult(true, "No accounts found in rules files", {
27045
27115
  accountsAdded: [],
27046
27116
  journalUpdated: "",
27047
- rulesScanned: Array.from(matchedRulesFiles).map((f) => path16.relative(context.directory, f))
27117
+ rulesScanned: Array.from(matchedRulesFiles).map((f) => path17.relative(context.directory, f))
27048
27118
  });
27049
27119
  return;
27050
27120
  }
@@ -27068,7 +27138,7 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
27068
27138
  context.result.steps.accountDeclarations = buildStepResult(true, "No transactions found, skipping account declarations", {
27069
27139
  accountsAdded: [],
27070
27140
  journalUpdated: "",
27071
- rulesScanned: Array.from(matchedRulesFiles).map((f) => path16.relative(context.directory, f))
27141
+ rulesScanned: Array.from(matchedRulesFiles).map((f) => path17.relative(context.directory, f))
27072
27142
  });
27073
27143
  return;
27074
27144
  }
@@ -27079,12 +27149,12 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
27079
27149
  context.result.steps.accountDeclarations = buildStepResult(false, `Failed to create year journal: ${error45 instanceof Error ? error45.message : String(error45)}`, {
27080
27150
  accountsAdded: [],
27081
27151
  journalUpdated: "",
27082
- rulesScanned: Array.from(matchedRulesFiles).map((f) => path16.relative(context.directory, f))
27152
+ rulesScanned: Array.from(matchedRulesFiles).map((f) => path17.relative(context.directory, f))
27083
27153
  });
27084
27154
  return;
27085
27155
  }
27086
27156
  const result = ensureAccountDeclarations(yearJournalPath, allAccounts);
27087
- const message = result.added.length > 0 ? `Added ${result.added.length} account declaration(s) to ${path16.relative(context.directory, yearJournalPath)}` : "All required accounts already declared";
27157
+ const message = result.added.length > 0 ? `Added ${result.added.length} account declaration(s) to ${path17.relative(context.directory, yearJournalPath)}` : "All required accounts already declared";
27088
27158
  logger?.logStep("Check Accounts", "success", message);
27089
27159
  if (result.added.length > 0) {
27090
27160
  for (const account of result.added) {
@@ -27093,17 +27163,17 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
27093
27163
  }
27094
27164
  context.result.steps.accountDeclarations = buildStepResult(true, message, {
27095
27165
  accountsAdded: result.added,
27096
- journalUpdated: path16.relative(context.directory, yearJournalPath),
27097
- rulesScanned: Array.from(matchedRulesFiles).map((f) => path16.relative(context.directory, f))
27166
+ journalUpdated: path17.relative(context.directory, yearJournalPath),
27167
+ rulesScanned: Array.from(matchedRulesFiles).map((f) => path17.relative(context.directory, f))
27098
27168
  });
27099
27169
  logger?.endSection();
27100
27170
  }
27101
27171
  async function buildSuggestionContext(context, contextId, logger) {
27102
27172
  const { loadExistingAccounts: loadExistingAccounts2, extractRulePatternsFromFile: extractRulePatternsFromFile2 } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
27103
27173
  const config2 = context.configLoader(context.directory);
27104
- const rulesDir = path16.join(context.directory, config2.paths.rules);
27174
+ const rulesDir = path17.join(context.directory, config2.paths.rules);
27105
27175
  const importCtx = loadContext(context.directory, contextId);
27106
- const csvPath = path16.join(context.directory, importCtx.filePath);
27176
+ const csvPath = path17.join(context.directory, importCtx.filePath);
27107
27177
  const rulesMapping = loadRulesMapping(rulesDir);
27108
27178
  const rulesFile = findRulesForCsv(csvPath, rulesMapping);
27109
27179
  if (!rulesFile) {
@@ -27332,8 +27402,8 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
27332
27402
  }
27333
27403
  }
27334
27404
  const importConfig = loadImportConfig(context.directory);
27335
- const pendingDir = path16.join(context.directory, importConfig.paths.pending);
27336
- const doneDir = path16.join(context.directory, importConfig.paths.done);
27405
+ const pendingDir = path17.join(context.directory, importConfig.paths.pending);
27406
+ const doneDir = path17.join(context.directory, importConfig.paths.done);
27337
27407
  const orderedContextIds = [...contextIds].sort((a, b) => {
27338
27408
  const ctxA = loadContext(context.directory, a);
27339
27409
  const ctxB = loadContext(context.directory, b);
@@ -27356,8 +27426,8 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
27356
27426
  totalTransactions += context.result.steps.import?.details?.summary?.totalTransactions || 0;
27357
27427
  } catch (error45) {
27358
27428
  if (error45 instanceof NoTransactionsError) {
27359
- const csvPath = path16.join(context.directory, importContext.filePath);
27360
- if (fs22.existsSync(csvPath)) {
27429
+ const csvPath = path17.join(context.directory, importContext.filePath);
27430
+ if (fs23.existsSync(csvPath)) {
27361
27431
  moveCsvToDone(csvPath, pendingDir, doneDir);
27362
27432
  logger.info(`No transactions to import for ${importContext.filename}, moved to done`);
27363
27433
  } else {
@@ -27438,16 +27508,16 @@ This tool orchestrates the full import workflow:
27438
27508
  }
27439
27509
  });
27440
27510
  // src/tools/init-directories.ts
27441
- import * as fs23 from "fs";
27442
- import * as path17 from "path";
27511
+ import * as fs24 from "fs";
27512
+ import * as path18 from "path";
27443
27513
  async function initDirectories(directory) {
27444
27514
  try {
27445
27515
  const config2 = loadImportConfig(directory);
27446
27516
  const directoriesCreated = [];
27447
27517
  const gitkeepFiles = [];
27448
- const importBase = path17.join(directory, "import");
27449
- if (!fs23.existsSync(importBase)) {
27450
- fs23.mkdirSync(importBase, { recursive: true });
27518
+ const importBase = path18.join(directory, "import");
27519
+ if (!fs24.existsSync(importBase)) {
27520
+ fs24.mkdirSync(importBase, { recursive: true });
27451
27521
  directoriesCreated.push("import");
27452
27522
  }
27453
27523
  const pathsToCreate = [
@@ -27457,20 +27527,20 @@ async function initDirectories(directory) {
27457
27527
  { key: "unrecognized", path: config2.paths.unrecognized }
27458
27528
  ];
27459
27529
  for (const { path: dirPath } of pathsToCreate) {
27460
- const fullPath = path17.join(directory, dirPath);
27461
- if (!fs23.existsSync(fullPath)) {
27462
- fs23.mkdirSync(fullPath, { recursive: true });
27530
+ const fullPath = path18.join(directory, dirPath);
27531
+ if (!fs24.existsSync(fullPath)) {
27532
+ fs24.mkdirSync(fullPath, { recursive: true });
27463
27533
  directoriesCreated.push(dirPath);
27464
27534
  }
27465
- const gitkeepPath = path17.join(fullPath, ".gitkeep");
27466
- if (!fs23.existsSync(gitkeepPath)) {
27467
- fs23.writeFileSync(gitkeepPath, "");
27468
- gitkeepFiles.push(path17.join(dirPath, ".gitkeep"));
27535
+ const gitkeepPath = path18.join(fullPath, ".gitkeep");
27536
+ if (!fs24.existsSync(gitkeepPath)) {
27537
+ fs24.writeFileSync(gitkeepPath, "");
27538
+ gitkeepFiles.push(path18.join(dirPath, ".gitkeep"));
27469
27539
  }
27470
27540
  }
27471
- const gitignorePath = path17.join(importBase, ".gitignore");
27541
+ const gitignorePath = path18.join(importBase, ".gitignore");
27472
27542
  let gitignoreCreated = false;
27473
- if (!fs23.existsSync(gitignorePath)) {
27543
+ if (!fs24.existsSync(gitignorePath)) {
27474
27544
  const gitignoreContent = `# Ignore CSV/PDF files in temporary directories
27475
27545
  /incoming/*.csv
27476
27546
  /incoming/*.pdf
@@ -27488,7 +27558,7 @@ async function initDirectories(directory) {
27488
27558
  .DS_Store
27489
27559
  Thumbs.db
27490
27560
  `;
27491
- fs23.writeFileSync(gitignorePath, gitignoreContent);
27561
+ fs24.writeFileSync(gitignorePath, gitignoreContent);
27492
27562
  gitignoreCreated = true;
27493
27563
  }
27494
27564
  const parts = [];
@@ -27564,32 +27634,32 @@ You can now drop CSV files into import/incoming/ and run import-pipeline.`);
27564
27634
  }
27565
27635
  });
27566
27636
  // src/tools/generate-btc-purchases.ts
27567
- import * as path18 from "path";
27568
- import * as fs24 from "fs";
27637
+ import * as path19 from "path";
27638
+ import * as fs25 from "fs";
27569
27639
  function findFiatCsvPaths(directory, pendingDir, provider) {
27570
- const providerDir = path18.join(directory, pendingDir, provider);
27571
- if (!fs24.existsSync(providerDir))
27640
+ const providerDir = path19.join(directory, pendingDir, provider);
27641
+ if (!fs25.existsSync(providerDir))
27572
27642
  return [];
27573
27643
  const csvPaths = [];
27574
- const entries = fs24.readdirSync(providerDir, { withFileTypes: true });
27644
+ const entries = fs25.readdirSync(providerDir, { withFileTypes: true });
27575
27645
  for (const entry of entries) {
27576
27646
  if (!entry.isDirectory())
27577
27647
  continue;
27578
27648
  if (entry.name === "btc")
27579
27649
  continue;
27580
- const csvFiles = findCsvFiles(path18.join(providerDir, entry.name), { fullPaths: true });
27650
+ const csvFiles = findCsvFiles(path19.join(providerDir, entry.name), { fullPaths: true });
27581
27651
  csvPaths.push(...csvFiles);
27582
27652
  }
27583
27653
  return csvPaths;
27584
27654
  }
27585
27655
  function findBtcCsvPath(directory, pendingDir, provider) {
27586
- const btcDir = path18.join(directory, pendingDir, provider, "btc");
27656
+ const btcDir = path19.join(directory, pendingDir, provider, "btc");
27587
27657
  const csvFiles = findCsvFiles(btcDir, { fullPaths: true });
27588
27658
  return csvFiles[0];
27589
27659
  }
27590
27660
  function determineYear(csvPaths) {
27591
27661
  for (const csvPath of csvPaths) {
27592
- const match2 = path18.basename(csvPath).match(/(\d{4})-\d{2}-\d{2}/);
27662
+ const match2 = path19.basename(csvPath).match(/(\d{4})-\d{2}-\d{2}/);
27593
27663
  if (match2)
27594
27664
  return parseInt(match2[1], 10);
27595
27665
  }
@@ -27642,7 +27712,7 @@ async function generateBtcPurchases(directory, agent, options = {}, configLoader
27642
27712
  skippedDuplicates: result.skippedDuplicates,
27643
27713
  unmatchedFiat: result.unmatchedFiat.length,
27644
27714
  unmatchedBtc: result.unmatchedBtc.length,
27645
- yearJournal: path18.relative(directory, yearJournalPath)
27715
+ yearJournal: path19.relative(directory, yearJournalPath)
27646
27716
  });
27647
27717
  } catch (err) {
27648
27718
  logger.error("Failed to generate BTC purchases", err);
@@ -27685,7 +27755,7 @@ to produce equity conversion entries for Bitcoin purchases.
27685
27755
  });
27686
27756
  // src/index.ts
27687
27757
  var __dirname2 = dirname9(fileURLToPath3(import.meta.url));
27688
- var AGENT_FILE = join16(__dirname2, "..", "agent", "accountant.md");
27758
+ var AGENT_FILE = join17(__dirname2, "..", "agent", "accountant.md");
27689
27759
  var AccountantPlugin = async () => {
27690
27760
  const agent = loadAgent(AGENT_FILE);
27691
27761
  return {
@@ -0,0 +1,67 @@
1
+ # Merger FMV Configuration
2
+
3
+ When a merger or spinoff produces multiple incoming securities, cost basis must be allocated across them. Quantity-proportional allocation is incorrect when the incoming securities have different market values.
4
+
5
+ The `merger-fmv.yaml` file provides fair market value (FMV) prices so cost basis is allocated by market value instead.
6
+
7
+ ## File Location
8
+
9
+ `config/import/merger-fmv.yaml` (relative to project root)
10
+
11
+ This file is **required** for any multi-way merger (more than one incoming symbol). The pipeline will fail with an error if a multi-way merger lacks FMV data. Single-symbol mergers (1→1) do not need FMV since 100% of the cost basis transfers to the single incoming symbol.
12
+
13
+ ## Format
14
+
15
+ ```yaml
16
+ # date (YYYY-MM-DD) -> symbol -> FMV per share
17
+ 2015-10-14:
18
+ fr.to: 5.23
19
+ sil.to: 0.17
20
+ ```
21
+
22
+ - **Date keys** use `YYYY-MM-DD` format, matching the merger transaction date
23
+ - **Symbol keys** use normalized names (post-symbolMap, lowercase)
24
+ - **Values** are the closing price per share on the distribution date
25
+
26
+ Multiple dates can be specified for different merger events.
27
+
28
+ ## How It Works
29
+
30
+ For each merger group, the preprocessor looks up FMV data for the transaction date. If FMV prices are found for **all** incoming symbols, cost basis is allocated by FMV-weighted proportion:
31
+
32
+ ```
33
+ proportion[i] = (fmv[symbol] * quantity) / sum(fmv[symbol] * quantity)
34
+ allocated_cost[i] = total_cost_basis * proportion[i]
35
+ ```
36
+
37
+ If FMV data is missing or doesn't cover all incoming symbols for a multi-way merger, the pipeline fails with an error directing you to add the missing prices.
38
+
39
+ ## Example
40
+
41
+ Given a merger: 3,300 SVL.TO (cost basis 5,660 CAD) → 914 FR.TO + 550 SIL.TO
42
+
43
+ **Without FMV** (quantity-proportional):
44
+ - FR.TO: 5,660 × (914 / 1,464) = 3,533.63 CAD (3.87/share)
45
+ - SIL.TO: 5,660 × (550 / 1,464) = 2,126.37 CAD (3.87/share)
46
+
47
+ Both get the same per-share cost, which is wrong.
48
+
49
+ **With FMV** (FR.TO @ 5.23, SIL.TO @ 0.17):
50
+ - FR.TO FMV total: 914 × 5.23 = 4,780.22
51
+ - SIL.TO FMV total: 550 × 0.17 = 93.50
52
+ - FR.TO: 5,660 × (4,780.22 / 4,873.72) = 5,551.42 CAD (6.07/share)
53
+ - SIL.TO: 5,660 × (93.50 / 4,873.72) = 108.58 CAD (0.20/share)
54
+
55
+ ## Finding FMV Prices
56
+
57
+ Use the regular closing price on the distribution date, not the adjusted close (which accounts for later dividends/splits):
58
+
59
+ ```bash
60
+ pricehist fetch yahoo AG.TO -t close -s 2015-10-14 -e 2015-10-14
61
+ ```
62
+
63
+ For delisted or illiquid securities, check portfolio market-data exports or broker statements.
64
+
65
+ ## When Needed
66
+
67
+ FMV config is **required** whenever a merger produces multiple incoming symbols (N>1). The pipeline will refuse to process the merger without it. Single-symbol mergers (1→1) always allocate 100% and don't need FMV.
@@ -257,7 +257,7 @@ See [classify-statements](classify-statements.md) for details.
257
257
 
258
258
  **Per-Year Journals**: Each CSV writes to a journal file named `{year}-{currency}.journal` where the year is extracted from the `to-DDMMYYYY` portion of the filename (e.g., `swissquote-cad-transactions-01012013-to-31122013.csv` → `2013-cad.journal`).
259
259
 
260
- **Multi-Way Mergers**: Mergers can involve multiple incoming/outgoing securities (1→N, N→1, N→M). The preprocessor groups all entries sharing the same date+orderNum, collects total cost basis from outgoing lots, and distributes it proportionally across incoming symbols.
260
+ **Multi-Way Mergers**: Mergers can involve multiple incoming/outgoing securities (1→N, N→1, N→M). The preprocessor groups all entries sharing the same date+orderNum, collects total cost basis from outgoing lots, and distributes it across incoming symbols using FMV-weighted proportions from `config/import/merger-fmv.yaml`. FMV data is **required** for multi-way mergers (>1 incoming symbol) — the pipeline fails with an error if it is missing. Single-symbol mergers (1→1) always allocate 100% without needing FMV. See [Merger FMV Configuration](../configuration/merger-fmv.md).
261
261
 
262
262
  **Cross-Currency Mergers**: When the outgoing and incoming sides of a merger appear in different currency CSV files, the outgoing side saves its state to `pending-mergers/` under the lot inventory directory. When the incoming side is processed later, it loads the pending state to create lots with the correct cost basis.
263
263
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.13.18",
3
+ "version": "0.13.19-next.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",