@fuzzle/opencode-accountant 0.13.17 → 0.13.18-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) {
@@ -25462,6 +25462,18 @@ function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, crossC
25462
25462
  entries.push(sub);
25463
25463
  }
25464
25464
  }
25465
+ if (group.cashSettlement) {
25466
+ const cashCurrency = group.cashSettlement.currency.toLowerCase();
25467
+ const cashAmount = formatAmount2(group.cashSettlement.amount, group.cashSettlement.currency);
25468
+ let sub = `${date5} ${description}
25469
+ `;
25470
+ sub += metadata;
25471
+ sub += ` assets:broker:swissquote:${cashCurrency} ${cashAmount}
25472
+ `;
25473
+ sub += ` income:capital-gains:realized
25474
+ `;
25475
+ entries.push(sub);
25476
+ }
25465
25477
  return entries.join(`
25466
25478
  `);
25467
25479
  }
@@ -25532,6 +25544,52 @@ function formatJournalFile(entries, year, currency) {
25532
25544
  `);
25533
25545
  }
25534
25546
 
25547
+ // src/utils/mergerFmvConfig.ts
25548
+ init_js_yaml();
25549
+ import * as fs18 from "fs";
25550
+ import * as path13 from "path";
25551
+ var CONFIG_FILE3 = "config/import/merger-fmv.yaml";
25552
+ function loadMergerFmvConfig(projectDir) {
25553
+ const configPath = path13.join(projectDir, CONFIG_FILE3);
25554
+ if (!fs18.existsSync(configPath)) {
25555
+ return null;
25556
+ }
25557
+ const content = fs18.readFileSync(configPath, "utf-8");
25558
+ const parsed = jsYaml.load(content);
25559
+ if (typeof parsed !== "object" || parsed === null) {
25560
+ return null;
25561
+ }
25562
+ const config2 = {};
25563
+ for (const [rawKey, symbols] of Object.entries(parsed)) {
25564
+ let dateKey;
25565
+ const d = new Date(rawKey);
25566
+ if (!isNaN(d.getTime()) && !/^\d{4}-\d{2}-\d{2}$/.test(rawKey)) {
25567
+ dateKey = d.toISOString().split("T")[0];
25568
+ } else {
25569
+ dateKey = rawKey;
25570
+ }
25571
+ if (typeof symbols !== "object" || symbols === null) {
25572
+ continue;
25573
+ }
25574
+ const symbolMap = {};
25575
+ for (const [symbol2, price] of Object.entries(symbols)) {
25576
+ if (typeof price === "number") {
25577
+ symbolMap[symbol2] = price;
25578
+ }
25579
+ }
25580
+ if (Object.keys(symbolMap).length > 0) {
25581
+ config2[dateKey] = symbolMap;
25582
+ }
25583
+ }
25584
+ return Object.keys(config2).length > 0 ? config2 : null;
25585
+ }
25586
+ function lookupFmvForDate(config2, date5) {
25587
+ if (!config2) {
25588
+ return null;
25589
+ }
25590
+ return config2[date5] ?? null;
25591
+ }
25592
+
25535
25593
  // src/utils/swissquoteCsvPreprocessor.ts
25536
25594
  var SWISSQUOTE_HEADER = "Date;Order #;Transaction;Symbol;Name;ISIN;Quantity;Unit price;Costs;Accrued Interest;Net Amount;Balance;Currency";
25537
25595
  var SIMPLE_TRANSACTION_TYPES = new Set([
@@ -25742,7 +25800,26 @@ function groupMergerActions(actions) {
25742
25800
  }
25743
25801
  return Array.from(groupMap.values());
25744
25802
  }
25745
- 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) {
25746
25823
  const entries = [];
25747
25824
  if (group.outgoing.length === 0 && group.incoming.length === 0) {
25748
25825
  return entries;
@@ -25761,12 +25838,14 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25761
25838
  quantity: -(pendingState.outgoingQuantities[i2] || 0)
25762
25839
  });
25763
25840
  }
25764
- 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);
25765
25844
  const incomingTotalCosts2 = [];
25766
- for (const inc of group.incoming) {
25845
+ for (let idx = 0;idx < group.incoming.length; idx++) {
25846
+ const inc = group.incoming[idx];
25767
25847
  const absQty = Math.abs(inc.quantity);
25768
- const proportion = absQty / totalIncomingQty2;
25769
- const allocatedCost = pendingState.totalCostBasis * proportion;
25848
+ const allocatedCost = pendingState.totalCostBasis * proportions2[idx];
25770
25849
  const costBasisPerUnit = absQty > 0 ? allocatedCost / absQty : 0;
25771
25850
  incomingTotalCosts2.push(allocatedCost);
25772
25851
  const lot = {
@@ -25838,12 +25917,14 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25838
25917
  removeLots(inventory, out.symbol, logger);
25839
25918
  logger?.debug(`Merger outgoing: removed ${absQty} ${out.symbol}`);
25840
25919
  }
25841
- 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);
25842
25923
  const incomingTotalCosts = [];
25843
- for (const inc of group.incoming) {
25924
+ for (let idx = 0;idx < group.incoming.length; idx++) {
25925
+ const inc = group.incoming[idx];
25844
25926
  const absQty = Math.abs(inc.quantity);
25845
- const proportion = totalIncomingQty > 0 ? absQty / totalIncomingQty : 0;
25846
- const allocatedCost = totalCostBasis * proportion;
25927
+ const allocatedCost = totalCostBasis * proportions[idx];
25847
25928
  const costBasisPerUnit = absQty > 0 ? allocatedCost / absQty : 0;
25848
25929
  incomingTotalCosts.push(allocatedCost);
25849
25930
  const lot = {
@@ -25871,25 +25952,25 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25871
25952
  }
25872
25953
  function getPendingMergerDir(projectDir, lotInventoryPath) {
25873
25954
  const lotDir = lotInventoryPath.replace(/{symbol}[^/]*$/, "");
25874
- return path13.join(projectDir, lotDir, "pending-mergers");
25955
+ return path14.join(projectDir, lotDir, "pending-mergers");
25875
25956
  }
25876
25957
  function savePendingMerger(projectDir, lotInventoryPath, key, state, logger) {
25877
25958
  const dir = getPendingMergerDir(projectDir, lotInventoryPath);
25878
- if (!fs18.existsSync(dir)) {
25879
- fs18.mkdirSync(dir, { recursive: true });
25959
+ if (!fs19.existsSync(dir)) {
25960
+ fs19.mkdirSync(dir, { recursive: true });
25880
25961
  }
25881
- const filePath = path13.join(dir, `${key.replace(/[^a-zA-Z0-9-]/g, "_")}.json`);
25882
- 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));
25883
25964
  logger?.debug(`Saved pending merger state: ${key}`);
25884
25965
  }
25885
25966
  function loadPendingMerger(projectDir, lotInventoryPath, key, logger) {
25886
25967
  const dir = getPendingMergerDir(projectDir, lotInventoryPath);
25887
- const filePath = path13.join(dir, `${key.replace(/[^a-zA-Z0-9-]/g, "_")}.json`);
25888
- if (!fs18.existsSync(filePath)) {
25968
+ const filePath = path14.join(dir, `${key.replace(/[^a-zA-Z0-9-]/g, "_")}.json`);
25969
+ if (!fs19.existsSync(filePath)) {
25889
25970
  return null;
25890
25971
  }
25891
25972
  try {
25892
- const content = fs18.readFileSync(filePath, "utf-8");
25973
+ const content = fs19.readFileSync(filePath, "utf-8");
25893
25974
  return JSON.parse(content);
25894
25975
  } catch (error45) {
25895
25976
  const message = error45 instanceof Error ? error45.message : String(error45);
@@ -25899,27 +25980,27 @@ function loadPendingMerger(projectDir, lotInventoryPath, key, logger) {
25899
25980
  }
25900
25981
  function removePendingMerger(projectDir, lotInventoryPath, key, logger) {
25901
25982
  const dir = getPendingMergerDir(projectDir, lotInventoryPath);
25902
- const filePath = path13.join(dir, `${key.replace(/[^a-zA-Z0-9-]/g, "_")}.json`);
25903
- if (fs18.existsSync(filePath)) {
25904
- 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);
25905
25986
  logger?.debug(`Removed pending merger state: ${key}`);
25906
25987
  }
25907
25988
  }
25908
25989
  function resolveRemainingPendingMergers(projectDir, lotInventoryPath, logger) {
25909
25990
  const pendingDir = getPendingMergerDir(projectDir, lotInventoryPath);
25910
- if (!fs18.existsSync(pendingDir)) {
25991
+ if (!fs19.existsSync(pendingDir)) {
25911
25992
  return { resolved: 0, journalFilesUpdated: [] };
25912
25993
  }
25913
- const files = fs18.readdirSync(pendingDir).filter((f) => f.endsWith(".json"));
25994
+ const files = fs19.readdirSync(pendingDir).filter((f) => f.endsWith(".json"));
25914
25995
  if (files.length === 0) {
25915
25996
  return { resolved: 0, journalFilesUpdated: [] };
25916
25997
  }
25917
25998
  const entriesByJournal = new Map;
25918
25999
  const resolvedFiles = [];
25919
26000
  for (const file2 of files) {
25920
- const filePath = path13.join(pendingDir, file2);
26001
+ const filePath = path14.join(pendingDir, file2);
25921
26002
  try {
25922
- const content = fs18.readFileSync(filePath, "utf-8");
26003
+ const content = fs19.readFileSync(filePath, "utf-8");
25923
26004
  const state = JSON.parse(content);
25924
26005
  const dateMatch = state.date.match(/\d{2}-\d{2}-(\d{4})/);
25925
26006
  if (!dateMatch) {
@@ -25928,12 +26009,12 @@ function resolveRemainingPendingMergers(projectDir, lotInventoryPath, logger) {
25928
26009
  }
25929
26010
  const year = parseInt(dateMatch[1], 10);
25930
26011
  const entry = generateDirectWorthlessEntry(state.date, state.orderNum, state.outgoingSymbols, state.outgoingIsins, state.outgoingQuantities || [], state.outgoingTotalCosts || [], state.totalCostBasis, state.currency, logger);
25931
- 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`);
25932
26013
  if (!entriesByJournal.has(journalFile)) {
25933
26014
  entriesByJournal.set(journalFile, []);
25934
26015
  }
25935
26016
  entriesByJournal.get(journalFile).push(entry);
25936
- fs18.unlinkSync(filePath);
26017
+ fs19.unlinkSync(filePath);
25937
26018
  resolvedFiles.push(file2);
25938
26019
  logger?.info(`Resolved pending merger ${file2} as worthless: ${state.outgoingSymbols.join(", ")} (${state.totalCostBasis.toFixed(2)} ${state.currency})`);
25939
26020
  } catch (error45) {
@@ -25943,32 +26024,33 @@ function resolveRemainingPendingMergers(projectDir, lotInventoryPath, logger) {
25943
26024
  }
25944
26025
  const journalFilesUpdated = [];
25945
26026
  for (const [journalFile, journalEntries] of entriesByJournal) {
25946
- if (fs18.existsSync(journalFile)) {
25947
- fs18.appendFileSync(journalFile, `
26027
+ if (fs19.existsSync(journalFile)) {
26028
+ fs19.appendFileSync(journalFile, `
25948
26029
  ` + journalEntries.join(`
25949
26030
  `));
25950
26031
  } else {
25951
- const basename5 = path13.basename(journalFile, ".journal");
26032
+ const basename5 = path14.basename(journalFile, ".journal");
25952
26033
  const parts = basename5.split("-");
25953
26034
  const yearStr = parts[0];
25954
26035
  const currency = parts.slice(1).join("-");
25955
26036
  const header = formatJournalFile(journalEntries, parseInt(yearStr, 10), currency);
25956
- fs18.writeFileSync(journalFile, header);
26037
+ fs19.writeFileSync(journalFile, header);
25957
26038
  }
25958
26039
  journalFilesUpdated.push(journalFile);
25959
- logger?.info(`Updated ${path13.basename(journalFile)} with worthless resolution entries`);
26040
+ logger?.info(`Updated ${path14.basename(journalFile)} with worthless resolution entries`);
25960
26041
  }
25961
- const remaining = fs18.readdirSync(pendingDir).filter((f) => f.endsWith(".json"));
26042
+ const remaining = fs19.readdirSync(pendingDir).filter((f) => f.endsWith(".json"));
25962
26043
  if (remaining.length === 0) {
25963
26044
  try {
25964
- fs18.rmdirSync(pendingDir);
26045
+ fs19.rmdirSync(pendingDir);
25965
26046
  logger?.debug("Removed empty pending-mergers directory");
25966
26047
  } catch {}
25967
26048
  }
25968
26049
  return { resolved: resolvedFiles.length, journalFilesUpdated };
25969
26050
  }
25970
26051
  async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInventoryPath = DEFAULT_LOT_INVENTORY_PATH, symbolMap = {}, logger) {
25971
- logger?.startSection(`Swissquote Preprocessing: ${path13.basename(csvPath)}`);
26052
+ logger?.startSection(`Swissquote Preprocessing: ${path14.basename(csvPath)}`);
26053
+ const mergerFmv = loadMergerFmvConfig(projectDir);
25972
26054
  const stats = {
25973
26055
  totalRows: 0,
25974
26056
  simpleTransactions: 0,
@@ -25978,7 +26060,7 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
25978
26060
  forexTransactions: 0,
25979
26061
  skipped: 0
25980
26062
  };
25981
- if (!fs18.existsSync(csvPath)) {
26063
+ if (!fs19.existsSync(csvPath)) {
25982
26064
  logger?.error(`CSV file not found: ${csvPath}`);
25983
26065
  logger?.endSection();
25984
26066
  return {
@@ -25990,7 +26072,7 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
25990
26072
  };
25991
26073
  }
25992
26074
  logger?.logStep("parse-csv", "start", `Reading ${csvPath}`);
25993
- const content = fs18.readFileSync(csvPath, "utf-8");
26075
+ const content = fs19.readFileSync(csvPath, "utf-8");
25994
26076
  const lines = content.split(`
25995
26077
  `).filter((line) => line.trim());
25996
26078
  const header = lines[0];
@@ -26034,6 +26116,7 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26034
26116
  const dividends = [];
26035
26117
  const corporateActions = [];
26036
26118
  const forexTransactions = [];
26119
+ const mergerCashSettlements = [];
26037
26120
  for (const txn of transactions) {
26038
26121
  const category = categorizeTransaction(txn);
26039
26122
  switch (category) {
@@ -26052,8 +26135,12 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26052
26135
  case "corporate":
26053
26136
  if (!txn.symbol || txn.symbol.trim() === "") {
26054
26137
  if (txn.netAmount && txn.netAmount !== "-") {
26055
- simpleTransactions.push(txn);
26056
- stats.simpleTransactions++;
26138
+ mergerCashSettlements.push({
26139
+ dateOrderKey: `${txn.date}-${txn.orderNum}`,
26140
+ amount: parseNumber(txn.netAmount),
26141
+ currency: txn.currency
26142
+ });
26143
+ stats.corporateActions++;
26057
26144
  } else {
26058
26145
  stats.skipped++;
26059
26146
  }
@@ -26097,6 +26184,31 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26097
26184
  }
26098
26185
  }
26099
26186
  const mergerGroups = groupMergerActions(mergerActions);
26187
+ for (const cash of mergerCashSettlements) {
26188
+ const matchingGroup = mergerGroups.find((g) => g.key === cash.dateOrderKey);
26189
+ if (matchingGroup) {
26190
+ matchingGroup.cashSettlement = { amount: cash.amount, currency: cash.currency };
26191
+ } else {
26192
+ simpleTransactions.push({
26193
+ date: cash.dateOrderKey.split("-").slice(0, 3).join("-"),
26194
+ orderNum: cash.dateOrderKey.split("-").slice(3).join("-"),
26195
+ transaction: "Merger",
26196
+ symbol: "",
26197
+ name: "",
26198
+ isin: "",
26199
+ quantity: "",
26200
+ unitPrice: "",
26201
+ costs: "",
26202
+ accruedInterest: "",
26203
+ netAmount: String(cash.amount),
26204
+ balance: "",
26205
+ currency: cash.currency
26206
+ });
26207
+ stats.simpleTransactions++;
26208
+ stats.corporateActions--;
26209
+ logger?.warn(`Cash settlement ${cash.dateOrderKey} has no matching merger group \u2014 routed to simple CSV`);
26210
+ }
26211
+ }
26100
26212
  for (const group of mergerGroups) {
26101
26213
  timeline.push({ kind: "mergerGroup", sortDate: formatDate(group.date), group });
26102
26214
  }
@@ -26148,7 +26260,7 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26148
26260
  break;
26149
26261
  }
26150
26262
  case "mergerGroup": {
26151
- const groupEntries = processMultiWayMerger(event.group, inventory, lotInventoryPath, projectDir, logger);
26263
+ const groupEntries = processMultiWayMerger(event.group, inventory, lotInventoryPath, projectDir, mergerFmv, logger);
26152
26264
  journalEntries.push(...groupEntries);
26153
26265
  break;
26154
26266
  }
@@ -26165,9 +26277,9 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26165
26277
  logger?.logStep("save-inventory", "success", "Lot inventory saved");
26166
26278
  let simpleTransactionsCsv = null;
26167
26279
  let journalFile = null;
26168
- const csvDir = path13.dirname(csvPath);
26169
- const csvBasename = path13.basename(csvPath, ".csv");
26170
- 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`);
26171
26283
  const csvContent = [
26172
26284
  SWISSQUOTE_HEADER,
26173
26285
  ...simpleTransactions.map((txn) => [
@@ -26187,23 +26299,23 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26187
26299
  ].join(";"))
26188
26300
  ].join(`
26189
26301
  `);
26190
- fs18.writeFileSync(simpleTransactionsCsv, csvContent);
26191
- 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)}`);
26192
26304
  if (journalEntries.length > 0) {
26193
- const investmentsDir = path13.join(projectDir, "ledger", "investments");
26194
- if (!fs18.existsSync(investmentsDir)) {
26195
- fs18.mkdirSync(investmentsDir, { recursive: true });
26305
+ const investmentsDir = path14.join(projectDir, "ledger", "investments");
26306
+ if (!fs19.existsSync(investmentsDir)) {
26307
+ fs19.mkdirSync(investmentsDir, { recursive: true });
26196
26308
  }
26197
- journalFile = path13.join(investmentsDir, `${year}-${currency}.journal`);
26309
+ journalFile = path14.join(investmentsDir, `${year}-${currency}.journal`);
26198
26310
  const journalContent = formatJournalFile(journalEntries, year, currency);
26199
- if (fs18.existsSync(journalFile)) {
26200
- fs18.appendFileSync(journalFile, `
26311
+ if (fs19.existsSync(journalFile)) {
26312
+ fs19.appendFileSync(journalFile, `
26201
26313
  ` + journalEntries.join(`
26202
26314
  `));
26203
- 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)}`);
26204
26316
  } else {
26205
- fs18.writeFileSync(journalFile, journalContent);
26206
- 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`);
26207
26319
  }
26208
26320
  }
26209
26321
  const stockSymbols = new Set;
@@ -26220,9 +26332,9 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26220
26332
  }
26221
26333
  }
26222
26334
  if (stockSymbols.size > 0) {
26223
- const commodityJournalPath = path13.join(projectDir, "ledger", "investments", "commodities.journal");
26335
+ const commodityJournalPath = path14.join(projectDir, "ledger", "investments", "commodities.journal");
26224
26336
  ensureCommodityDeclarations(commodityJournalPath, stockSymbols, logger);
26225
- const accountJournalPath = path13.join(projectDir, "ledger", "investments", "accounts.journal");
26337
+ const accountJournalPath = path14.join(projectDir, "ledger", "investments", "accounts.journal");
26226
26338
  const investmentAccounts = new Set;
26227
26339
  for (const symbol2 of stockSymbols) {
26228
26340
  investmentAccounts.add(`assets:investments:stocks:${symbol2}`);
@@ -26259,10 +26371,10 @@ async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInve
26259
26371
  }
26260
26372
 
26261
26373
  // src/utils/currencyExchangeGenerator.ts
26262
- import * as fs19 from "fs";
26263
- import * as path14 from "path";
26374
+ import * as fs20 from "fs";
26375
+ import * as path15 from "path";
26264
26376
  function parseExchangeRows(csvPath) {
26265
- const content = fs19.readFileSync(csvPath, "utf-8");
26377
+ const content = fs20.readFileSync(csvPath, "utf-8");
26266
26378
  const lines = content.trim().split(`
26267
26379
  `);
26268
26380
  if (lines.length < 2)
@@ -26386,7 +26498,7 @@ function formatDateIso2(date5) {
26386
26498
  return `${y}-${m}-${d}`;
26387
26499
  }
26388
26500
  function filterExchangeRows(csvPath, rowIndicesToRemove, logger) {
26389
- const content = fs19.readFileSync(csvPath, "utf-8");
26501
+ const content = fs20.readFileSync(csvPath, "utf-8");
26390
26502
  const lines = content.trim().split(`
26391
26503
  `);
26392
26504
  const filteredLines = [lines[0]];
@@ -26395,13 +26507,13 @@ function filterExchangeRows(csvPath, rowIndicesToRemove, logger) {
26395
26507
  continue;
26396
26508
  filteredLines.push(lines[i2]);
26397
26509
  }
26398
- const dir = path14.dirname(csvPath);
26399
- const basename6 = path14.basename(csvPath, ".csv");
26400
- const filteredPath = path14.join(dir, `${basename6}-filtered.csv`);
26401
- 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(`
26402
26514
  `) + `
26403
26515
  `);
26404
- 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)}`);
26405
26517
  return filteredPath;
26406
26518
  }
26407
26519
  function generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger) {
@@ -26411,7 +26523,7 @@ function generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger)
26411
26523
  const rows = parseExchangeRows(csvPath);
26412
26524
  if (rows.length > 0) {
26413
26525
  rowsByCsv.set(csvPath, rows);
26414
- logger?.info(`Found ${rows.length} EXCHANGE rows in ${path14.basename(csvPath)}`);
26526
+ logger?.info(`Found ${rows.length} EXCHANGE rows in ${path15.basename(csvPath)}`);
26415
26527
  }
26416
26528
  }
26417
26529
  if (rowsByCsv.size < 2) {
@@ -26428,8 +26540,8 @@ function generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger)
26428
26540
  const matches = matchExchangePairs(rowsByCsv);
26429
26541
  logger?.info(`Matched ${matches.length} exchange pairs`);
26430
26542
  let journalContent = "";
26431
- if (fs19.existsSync(yearJournalPath)) {
26432
- journalContent = fs19.readFileSync(yearJournalPath, "utf-8");
26543
+ if (fs20.existsSync(yearJournalPath)) {
26544
+ journalContent = fs20.readFileSync(yearJournalPath, "utf-8");
26433
26545
  }
26434
26546
  const newEntries = [];
26435
26547
  let skippedDuplicates = 0;
@@ -26456,8 +26568,8 @@ function generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger)
26456
26568
 
26457
26569
  `) + `
26458
26570
  `;
26459
- fs19.appendFileSync(yearJournalPath, appendContent);
26460
- 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)}`);
26461
26573
  } else {
26462
26574
  logger?.info("No new currency exchange entries to add");
26463
26575
  }
@@ -26480,8 +26592,8 @@ function generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger)
26480
26592
  }
26481
26593
 
26482
26594
  // src/utils/swissquoteForexGenerator.ts
26483
- import * as fs20 from "fs";
26484
- import * as path15 from "path";
26595
+ import * as fs21 from "fs";
26596
+ import * as path16 from "path";
26485
26597
  function formatDate2(dateStr) {
26486
26598
  const match2 = dateStr.match(/^(\d{2})-(\d{2})-(\d{4})/);
26487
26599
  if (!match2) {
@@ -26588,8 +26700,8 @@ function generateSwissquoteForexJournal(allForexRows, yearJournalPath, logger) {
26588
26700
  logger?.warn(`${unmatchedCount} forex rows could not be matched`);
26589
26701
  }
26590
26702
  let journalContent = "";
26591
- if (fs20.existsSync(yearJournalPath)) {
26592
- journalContent = fs20.readFileSync(yearJournalPath, "utf-8");
26703
+ if (fs21.existsSync(yearJournalPath)) {
26704
+ journalContent = fs21.readFileSync(yearJournalPath, "utf-8");
26593
26705
  }
26594
26706
  const newEntries = [];
26595
26707
  let skippedDuplicates = 0;
@@ -26611,17 +26723,17 @@ function generateSwissquoteForexJournal(allForexRows, yearJournalPath, logger) {
26611
26723
  newEntries.push(formatForexEntry(match2));
26612
26724
  }
26613
26725
  if (newEntries.length > 0) {
26614
- const dir = path15.dirname(yearJournalPath);
26615
- if (!fs20.existsSync(dir)) {
26616
- fs20.mkdirSync(dir, { recursive: true });
26726
+ const dir = path16.dirname(yearJournalPath);
26727
+ if (!fs21.existsSync(dir)) {
26728
+ fs21.mkdirSync(dir, { recursive: true });
26617
26729
  }
26618
26730
  const appendContent = `
26619
26731
  ` + newEntries.join(`
26620
26732
 
26621
26733
  `) + `
26622
26734
  `;
26623
- fs20.appendFileSync(yearJournalPath, appendContent);
26624
- 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)}`);
26625
26737
  } else {
26626
26738
  logger?.info("No new forex entries to add");
26627
26739
  }
@@ -26706,7 +26818,7 @@ function executePreprocessFiatStep(context, contextIds, logger) {
26706
26818
  for (const contextId of contextIds) {
26707
26819
  const importCtx = loadContext(context.directory, contextId);
26708
26820
  if (importCtx.provider === "revolut" && importCtx.currency !== "btc") {
26709
- fiatCsvPaths.push(path16.join(context.directory, importCtx.filePath));
26821
+ fiatCsvPaths.push(path17.join(context.directory, importCtx.filePath));
26710
26822
  }
26711
26823
  }
26712
26824
  if (fiatCsvPaths.length === 0) {
@@ -26750,7 +26862,7 @@ function executePreprocessBtcStep(context, contextIds, logger) {
26750
26862
  for (const contextId of contextIds) {
26751
26863
  const importCtx = loadContext(context.directory, contextId);
26752
26864
  if (importCtx.provider === "revolut" && importCtx.currency === "btc") {
26753
- btcCsvPath = path16.join(context.directory, importCtx.filePath);
26865
+ btcCsvPath = path17.join(context.directory, importCtx.filePath);
26754
26866
  break;
26755
26867
  }
26756
26868
  }
@@ -26784,7 +26896,7 @@ async function executeBtcPurchaseStep(context, contextIds, logger) {
26784
26896
  const importCtx = loadContext(context.directory, contextId);
26785
26897
  if (importCtx.provider !== "revolut")
26786
26898
  continue;
26787
- const csvPath = path16.join(context.directory, importCtx.filePath);
26899
+ const csvPath = path17.join(context.directory, importCtx.filePath);
26788
26900
  if (importCtx.currency === "btc") {
26789
26901
  btcCsvPath = csvPath;
26790
26902
  } else {
@@ -26797,7 +26909,7 @@ async function executeBtcPurchaseStep(context, contextIds, logger) {
26797
26909
  }
26798
26910
  logger?.startSection("Step 1b: Generate BTC Purchase Entries");
26799
26911
  logger?.logStep("BTC Purchases", "start");
26800
- const btcFilename = path16.basename(btcCsvPath);
26912
+ const btcFilename = path17.basename(btcCsvPath);
26801
26913
  const yearMatch = btcFilename.match(/(\d{4})-\d{2}-\d{2}/);
26802
26914
  const year = yearMatch ? parseInt(yearMatch[1], 10) : new Date().getFullYear();
26803
26915
  const yearJournalPath = ensureYearJournalExists(context.directory, year);
@@ -26824,7 +26936,7 @@ async function executeCurrencyExchangeStep(context, contextIds, logger) {
26824
26936
  continue;
26825
26937
  if (importCtx.currency === "btc")
26826
26938
  continue;
26827
- const csvPath = path16.join(context.directory, importCtx.filePath);
26939
+ const csvPath = path17.join(context.directory, importCtx.filePath);
26828
26940
  fiatContexts.push({ contextId, csvPath });
26829
26941
  }
26830
26942
  if (fiatContexts.length < 2) {
@@ -26833,7 +26945,7 @@ async function executeCurrencyExchangeStep(context, contextIds, logger) {
26833
26945
  }
26834
26946
  logger?.startSection("Step 1e: Generate Currency Exchange Entries");
26835
26947
  logger?.logStep("Currency Exchanges", "start");
26836
- const firstFilename = path16.basename(fiatContexts[0].csvPath);
26948
+ const firstFilename = path17.basename(fiatContexts[0].csvPath);
26837
26949
  const yearMatch = firstFilename.match(/(\d{4})-\d{2}-\d{2}/);
26838
26950
  const year = yearMatch ? parseInt(yearMatch[1], 10) : new Date().getFullYear();
26839
26951
  const yearJournalPath = ensureYearJournalExists(context.directory, year);
@@ -26849,9 +26961,9 @@ async function executeCurrencyExchangeStep(context, contextIds, logger) {
26849
26961
  continue;
26850
26962
  const filteredCsvPath = filterExchangeRows(csvPath, indices, logger);
26851
26963
  updateContext(context.directory, contextId, {
26852
- filePath: path16.relative(context.directory, filteredCsvPath)
26964
+ filePath: path17.relative(context.directory, filteredCsvPath)
26853
26965
  });
26854
- 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)}`);
26855
26967
  }
26856
26968
  }
26857
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)`;
@@ -26868,7 +26980,7 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
26868
26980
  for (const contextId of contextIds) {
26869
26981
  const importCtx = loadContext(context.directory, contextId);
26870
26982
  if (importCtx.provider === "swissquote") {
26871
- const csvPath = path16.join(context.directory, importCtx.filePath);
26983
+ const csvPath = path17.join(context.directory, importCtx.filePath);
26872
26984
  const toDateMatch = importCtx.filename.match(/to-\d{4}(\d{4})\./);
26873
26985
  let year = new Date().getFullYear();
26874
26986
  if (toDateMatch) {
@@ -26892,11 +27004,11 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
26892
27004
  const lotInventoryPath = swissquoteProvider?.lotInventoryPath ?? DEFAULT_LOT_INVENTORY_PATH;
26893
27005
  const symbolMapPath = swissquoteProvider?.symbolMapPath ?? "config/import/symbolMap.yaml";
26894
27006
  let symbolMap = {};
26895
- const symbolMapFullPath = path16.join(context.directory, symbolMapPath);
26896
- if (fs22.existsSync(symbolMapFullPath)) {
27007
+ const symbolMapFullPath = path17.join(context.directory, symbolMapPath);
27008
+ if (fs23.existsSync(symbolMapFullPath)) {
26897
27009
  try {
26898
27010
  const yaml = await Promise.resolve().then(() => (init_js_yaml(), exports_js_yaml));
26899
- const content = fs22.readFileSync(symbolMapFullPath, "utf-8");
27011
+ const content = fs23.readFileSync(symbolMapFullPath, "utf-8");
26900
27012
  const parsed = yaml.load(content);
26901
27013
  if (parsed && typeof parsed === "object") {
26902
27014
  symbolMap = parsed;
@@ -26920,7 +27032,7 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
26920
27032
  let lastJournalFile = null;
26921
27033
  const allForexRows = [];
26922
27034
  for (const sqCtx of swissquoteContexts) {
26923
- logger?.logStep("Swissquote Preprocess", "start", `Processing ${path16.basename(sqCtx.csvPath)}`);
27035
+ logger?.logStep("Swissquote Preprocess", "start", `Processing ${path17.basename(sqCtx.csvPath)}`);
26924
27036
  const result = await preprocessSwissquote(sqCtx.csvPath, context.directory, sqCtx.currency, sqCtx.year, lotInventoryPath, symbolMap, logger);
26925
27037
  totalStats.totalRows += result.stats.totalRows;
26926
27038
  totalStats.simpleTransactions += result.stats.simpleTransactions;
@@ -26935,9 +27047,9 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
26935
27047
  }
26936
27048
  if (result.simpleTransactionsCsv) {
26937
27049
  updateContext(context.directory, sqCtx.contextId, {
26938
- filePath: path16.relative(context.directory, result.simpleTransactionsCsv)
27050
+ filePath: path17.relative(context.directory, result.simpleTransactionsCsv)
26939
27051
  });
26940
- 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)}`);
26941
27053
  }
26942
27054
  logger?.logStep("Swissquote Preprocess", "success", `Processed: ${result.stats.trades} trades, ${result.stats.dividends} dividends, ${result.stats.corporateActions} corporate actions, ${result.stats.forexTransactions} forex`);
26943
27055
  }
@@ -26947,11 +27059,11 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
26947
27059
  }
26948
27060
  if (allForexRows.length > 0) {
26949
27061
  const firstCtx = swissquoteContexts[0];
26950
- const yearJournalPath = path16.join(context.directory, "ledger", `${firstCtx.year}.journal`);
27062
+ const yearJournalPath = path17.join(context.directory, "ledger", `${firstCtx.year}.journal`);
26951
27063
  logger?.info(`Generating forex journal entries for ${allForexRows.length} forex rows...`);
26952
27064
  const forexResult = generateSwissquoteForexJournal(allForexRows, yearJournalPath, logger);
26953
27065
  if (forexResult.accountsUsed.size > 0) {
26954
- const accountJournalPath = path16.join(context.directory, "ledger", "investments", "accounts.journal");
27066
+ const accountJournalPath = path17.join(context.directory, "ledger", "investments", "accounts.journal");
26955
27067
  ensureInvestmentAccountDeclarations(accountJournalPath, forexResult.accountsUsed, logger);
26956
27068
  }
26957
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` : ""));
@@ -26977,9 +27089,9 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
26977
27089
  logger?.startSection("Step 2: Check Account Declarations");
26978
27090
  logger?.logStep("Check Accounts", "start");
26979
27091
  const config2 = context.configLoader(context.directory);
26980
- const rulesDir = path16.join(context.directory, config2.paths.rules);
27092
+ const rulesDir = path17.join(context.directory, config2.paths.rules);
26981
27093
  const importCtx = loadContext(context.directory, contextId);
26982
- const csvPath = path16.join(context.directory, importCtx.filePath);
27094
+ const csvPath = path17.join(context.directory, importCtx.filePath);
26983
27095
  const csvFiles = [csvPath];
26984
27096
  const rulesMapping = loadRulesMapping(rulesDir);
26985
27097
  const matchedRulesFiles = new Set;
@@ -27002,7 +27114,7 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
27002
27114
  context.result.steps.accountDeclarations = buildStepResult(true, "No accounts found in rules files", {
27003
27115
  accountsAdded: [],
27004
27116
  journalUpdated: "",
27005
- rulesScanned: Array.from(matchedRulesFiles).map((f) => path16.relative(context.directory, f))
27117
+ rulesScanned: Array.from(matchedRulesFiles).map((f) => path17.relative(context.directory, f))
27006
27118
  });
27007
27119
  return;
27008
27120
  }
@@ -27026,7 +27138,7 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
27026
27138
  context.result.steps.accountDeclarations = buildStepResult(true, "No transactions found, skipping account declarations", {
27027
27139
  accountsAdded: [],
27028
27140
  journalUpdated: "",
27029
- rulesScanned: Array.from(matchedRulesFiles).map((f) => path16.relative(context.directory, f))
27141
+ rulesScanned: Array.from(matchedRulesFiles).map((f) => path17.relative(context.directory, f))
27030
27142
  });
27031
27143
  return;
27032
27144
  }
@@ -27037,12 +27149,12 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
27037
27149
  context.result.steps.accountDeclarations = buildStepResult(false, `Failed to create year journal: ${error45 instanceof Error ? error45.message : String(error45)}`, {
27038
27150
  accountsAdded: [],
27039
27151
  journalUpdated: "",
27040
- rulesScanned: Array.from(matchedRulesFiles).map((f) => path16.relative(context.directory, f))
27152
+ rulesScanned: Array.from(matchedRulesFiles).map((f) => path17.relative(context.directory, f))
27041
27153
  });
27042
27154
  return;
27043
27155
  }
27044
27156
  const result = ensureAccountDeclarations(yearJournalPath, allAccounts);
27045
- 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";
27046
27158
  logger?.logStep("Check Accounts", "success", message);
27047
27159
  if (result.added.length > 0) {
27048
27160
  for (const account of result.added) {
@@ -27051,17 +27163,17 @@ async function executeAccountDeclarationsStep(context, contextId, logger) {
27051
27163
  }
27052
27164
  context.result.steps.accountDeclarations = buildStepResult(true, message, {
27053
27165
  accountsAdded: result.added,
27054
- journalUpdated: path16.relative(context.directory, yearJournalPath),
27055
- 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))
27056
27168
  });
27057
27169
  logger?.endSection();
27058
27170
  }
27059
27171
  async function buildSuggestionContext(context, contextId, logger) {
27060
27172
  const { loadExistingAccounts: loadExistingAccounts2, extractRulePatternsFromFile: extractRulePatternsFromFile2 } = await Promise.resolve().then(() => (init_accountSuggester(), exports_accountSuggester));
27061
27173
  const config2 = context.configLoader(context.directory);
27062
- const rulesDir = path16.join(context.directory, config2.paths.rules);
27174
+ const rulesDir = path17.join(context.directory, config2.paths.rules);
27063
27175
  const importCtx = loadContext(context.directory, contextId);
27064
- const csvPath = path16.join(context.directory, importCtx.filePath);
27176
+ const csvPath = path17.join(context.directory, importCtx.filePath);
27065
27177
  const rulesMapping = loadRulesMapping(rulesDir);
27066
27178
  const rulesFile = findRulesForCsv(csvPath, rulesMapping);
27067
27179
  if (!rulesFile) {
@@ -27290,8 +27402,8 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
27290
27402
  }
27291
27403
  }
27292
27404
  const importConfig = loadImportConfig(context.directory);
27293
- const pendingDir = path16.join(context.directory, importConfig.paths.pending);
27294
- 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);
27295
27407
  const orderedContextIds = [...contextIds].sort((a, b) => {
27296
27408
  const ctxA = loadContext(context.directory, a);
27297
27409
  const ctxB = loadContext(context.directory, b);
@@ -27314,8 +27426,8 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
27314
27426
  totalTransactions += context.result.steps.import?.details?.summary?.totalTransactions || 0;
27315
27427
  } catch (error45) {
27316
27428
  if (error45 instanceof NoTransactionsError) {
27317
- const csvPath = path16.join(context.directory, importContext.filePath);
27318
- if (fs22.existsSync(csvPath)) {
27429
+ const csvPath = path17.join(context.directory, importContext.filePath);
27430
+ if (fs23.existsSync(csvPath)) {
27319
27431
  moveCsvToDone(csvPath, pendingDir, doneDir);
27320
27432
  logger.info(`No transactions to import for ${importContext.filename}, moved to done`);
27321
27433
  } else {
@@ -27396,16 +27508,16 @@ This tool orchestrates the full import workflow:
27396
27508
  }
27397
27509
  });
27398
27510
  // src/tools/init-directories.ts
27399
- import * as fs23 from "fs";
27400
- import * as path17 from "path";
27511
+ import * as fs24 from "fs";
27512
+ import * as path18 from "path";
27401
27513
  async function initDirectories(directory) {
27402
27514
  try {
27403
27515
  const config2 = loadImportConfig(directory);
27404
27516
  const directoriesCreated = [];
27405
27517
  const gitkeepFiles = [];
27406
- const importBase = path17.join(directory, "import");
27407
- if (!fs23.existsSync(importBase)) {
27408
- fs23.mkdirSync(importBase, { recursive: true });
27518
+ const importBase = path18.join(directory, "import");
27519
+ if (!fs24.existsSync(importBase)) {
27520
+ fs24.mkdirSync(importBase, { recursive: true });
27409
27521
  directoriesCreated.push("import");
27410
27522
  }
27411
27523
  const pathsToCreate = [
@@ -27415,20 +27527,20 @@ async function initDirectories(directory) {
27415
27527
  { key: "unrecognized", path: config2.paths.unrecognized }
27416
27528
  ];
27417
27529
  for (const { path: dirPath } of pathsToCreate) {
27418
- const fullPath = path17.join(directory, dirPath);
27419
- if (!fs23.existsSync(fullPath)) {
27420
- fs23.mkdirSync(fullPath, { recursive: true });
27530
+ const fullPath = path18.join(directory, dirPath);
27531
+ if (!fs24.existsSync(fullPath)) {
27532
+ fs24.mkdirSync(fullPath, { recursive: true });
27421
27533
  directoriesCreated.push(dirPath);
27422
27534
  }
27423
- const gitkeepPath = path17.join(fullPath, ".gitkeep");
27424
- if (!fs23.existsSync(gitkeepPath)) {
27425
- fs23.writeFileSync(gitkeepPath, "");
27426
- 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"));
27427
27539
  }
27428
27540
  }
27429
- const gitignorePath = path17.join(importBase, ".gitignore");
27541
+ const gitignorePath = path18.join(importBase, ".gitignore");
27430
27542
  let gitignoreCreated = false;
27431
- if (!fs23.existsSync(gitignorePath)) {
27543
+ if (!fs24.existsSync(gitignorePath)) {
27432
27544
  const gitignoreContent = `# Ignore CSV/PDF files in temporary directories
27433
27545
  /incoming/*.csv
27434
27546
  /incoming/*.pdf
@@ -27446,7 +27558,7 @@ async function initDirectories(directory) {
27446
27558
  .DS_Store
27447
27559
  Thumbs.db
27448
27560
  `;
27449
- fs23.writeFileSync(gitignorePath, gitignoreContent);
27561
+ fs24.writeFileSync(gitignorePath, gitignoreContent);
27450
27562
  gitignoreCreated = true;
27451
27563
  }
27452
27564
  const parts = [];
@@ -27522,32 +27634,32 @@ You can now drop CSV files into import/incoming/ and run import-pipeline.`);
27522
27634
  }
27523
27635
  });
27524
27636
  // src/tools/generate-btc-purchases.ts
27525
- import * as path18 from "path";
27526
- import * as fs24 from "fs";
27637
+ import * as path19 from "path";
27638
+ import * as fs25 from "fs";
27527
27639
  function findFiatCsvPaths(directory, pendingDir, provider) {
27528
- const providerDir = path18.join(directory, pendingDir, provider);
27529
- if (!fs24.existsSync(providerDir))
27640
+ const providerDir = path19.join(directory, pendingDir, provider);
27641
+ if (!fs25.existsSync(providerDir))
27530
27642
  return [];
27531
27643
  const csvPaths = [];
27532
- const entries = fs24.readdirSync(providerDir, { withFileTypes: true });
27644
+ const entries = fs25.readdirSync(providerDir, { withFileTypes: true });
27533
27645
  for (const entry of entries) {
27534
27646
  if (!entry.isDirectory())
27535
27647
  continue;
27536
27648
  if (entry.name === "btc")
27537
27649
  continue;
27538
- const csvFiles = findCsvFiles(path18.join(providerDir, entry.name), { fullPaths: true });
27650
+ const csvFiles = findCsvFiles(path19.join(providerDir, entry.name), { fullPaths: true });
27539
27651
  csvPaths.push(...csvFiles);
27540
27652
  }
27541
27653
  return csvPaths;
27542
27654
  }
27543
27655
  function findBtcCsvPath(directory, pendingDir, provider) {
27544
- const btcDir = path18.join(directory, pendingDir, provider, "btc");
27656
+ const btcDir = path19.join(directory, pendingDir, provider, "btc");
27545
27657
  const csvFiles = findCsvFiles(btcDir, { fullPaths: true });
27546
27658
  return csvFiles[0];
27547
27659
  }
27548
27660
  function determineYear(csvPaths) {
27549
27661
  for (const csvPath of csvPaths) {
27550
- 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}/);
27551
27663
  if (match2)
27552
27664
  return parseInt(match2[1], 10);
27553
27665
  }
@@ -27600,7 +27712,7 @@ async function generateBtcPurchases(directory, agent, options = {}, configLoader
27600
27712
  skippedDuplicates: result.skippedDuplicates,
27601
27713
  unmatchedFiat: result.unmatchedFiat.length,
27602
27714
  unmatchedBtc: result.unmatchedBtc.length,
27603
- yearJournal: path18.relative(directory, yearJournalPath)
27715
+ yearJournal: path19.relative(directory, yearJournalPath)
27604
27716
  });
27605
27717
  } catch (err) {
27606
27718
  logger.error("Failed to generate BTC purchases", err);
@@ -27643,7 +27755,7 @@ to produce equity conversion entries for Bitcoin purchases.
27643
27755
  });
27644
27756
  // src/index.ts
27645
27757
  var __dirname2 = dirname9(fileURLToPath3(import.meta.url));
27646
- var AGENT_FILE = join16(__dirname2, "..", "agent", "accountant.md");
27758
+ var AGENT_FILE = join17(__dirname2, "..", "agent", "accountant.md");
27647
27759
  var AccountantPlugin = async () => {
27648
27760
  const agent = loadAgent(AGENT_FILE);
27649
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.17",
3
+ "version": "0.13.18-next.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",