@fuzzle/opencode-accountant 0.13.12-next.1 → 0.13.13-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +159 -40
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -25416,6 +25416,47 @@ function generateRightsDistributionEntry(action, logger) {
25416
25416
  `;
25417
25417
  return entry;
25418
25418
  }
25419
+ function generatePendingMergerWorthlessEntry(date5, orderNum, outgoingSymbols, outgoingIsins, outgoingQuantities, totalCostBasis, currency, logger) {
25420
+ const formattedDate = formatDate(date5);
25421
+ const symbols = outgoingSymbols.join(" + ");
25422
+ const description = escapeDescription(`Worthless: ${symbols} (unresolved merger)`);
25423
+ logger?.debug(`Generating pending merger worthless entry: ${symbols}, loss: ${totalCostBasis.toFixed(2)} ${currency}`);
25424
+ const postings = [];
25425
+ if (outgoingQuantities.length > 0) {
25426
+ const totalQty = outgoingQuantities.reduce((a, b) => a + b, 0);
25427
+ for (let i2 = 0;i2 < outgoingSymbols.length; i2++) {
25428
+ const symbol2 = outgoingSymbols[i2];
25429
+ const quantity = outgoingQuantities[i2] || 0;
25430
+ if (quantity <= 0)
25431
+ continue;
25432
+ const proportion = totalQty > 0 ? quantity / totalQty : 0;
25433
+ const costForSymbol = totalCostBasis * proportion;
25434
+ const costBasisPerUnit = costForSymbol / quantity;
25435
+ const commodity = formatCommodity(symbol2);
25436
+ postings.push({
25437
+ account: "equity:conversion",
25438
+ amount: `-${formatQuantity(quantity)} ${commodity} @ ${formatPrice(costBasisPerUnit)} ${currency}`
25439
+ });
25440
+ }
25441
+ }
25442
+ postings.push({
25443
+ account: "expenses:losses:capital",
25444
+ amount: formatAmount2(totalCostBasis, currency)
25445
+ });
25446
+ let entry = `${formattedDate} ${description}
25447
+ `;
25448
+ entry += ` ; swissquote:order:${orderNum}
25449
+ `;
25450
+ entry += ` ; Pending merger resolved as worthless - total loss: ${totalCostBasis.toFixed(2)} ${currency}
25451
+ `;
25452
+ if (outgoingIsins.length > 0) {
25453
+ entry += ` ; Original ISINs: ${outgoingIsins.join(", ")}
25454
+ `;
25455
+ }
25456
+ entry += formatPostings(postings) + `
25457
+ `;
25458
+ return entry;
25459
+ }
25419
25460
  function formatJournalFile(entries, year, currency) {
25420
25461
  const header = `; Swissquote ${currency.toUpperCase()} investment transactions for ${year}
25421
25462
  ; Generated by opencode-accountant
@@ -25573,6 +25614,44 @@ var MERGER_LIKE_TYPES = new Set([
25573
25614
  "Exchange of securities",
25574
25615
  "Corporate Action"
25575
25616
  ]);
25617
+ function processNonMergerAction(action, inventory, entries, logger) {
25618
+ switch (action.type) {
25619
+ case "Reverse Split": {
25620
+ const qty = action.quantity;
25621
+ if (qty < 0) {
25622
+ return;
25623
+ }
25624
+ const oldQty = getHeldQuantity(inventory, action.symbol);
25625
+ const newQty = Math.abs(qty);
25626
+ if (oldQty > 0) {
25627
+ const ratio = newQty / oldQty;
25628
+ adjustLotsForSplit(inventory, action.symbol, ratio, logger);
25629
+ const entry = generateSplitEntry(action, oldQty, newQty, logger);
25630
+ entries.push(entry);
25631
+ } else {
25632
+ logger?.warn(`Reverse split for ${action.symbol} but no lots found`);
25633
+ }
25634
+ break;
25635
+ }
25636
+ case "Worthless Liquidation": {
25637
+ const removedLots = removeLots(inventory, action.symbol, logger);
25638
+ if (removedLots.length > 0) {
25639
+ const entry = generateWorthlessEntry(action, removedLots, logger);
25640
+ entries.push(entry);
25641
+ } else {
25642
+ logger?.warn(`Worthless liquidation for ${action.symbol} but no lots found`);
25643
+ }
25644
+ break;
25645
+ }
25646
+ case "Rights Distribution": {
25647
+ if (action.quantity > 0) {
25648
+ const entry = generateRightsDistributionEntry(action, logger);
25649
+ entries.push(entry);
25650
+ }
25651
+ break;
25652
+ }
25653
+ }
25654
+ }
25576
25655
  function processCorporateActions(actions, inventory, lotInventoryPath, projectDir, logger) {
25577
25656
  const entries = [];
25578
25657
  actions.sort((a, b) => formatDate(a.date).localeCompare(formatDate(b.date)));
@@ -25586,46 +25665,19 @@ function processCorporateActions(actions, inventory, lotInventoryPath, projectDi
25586
25665
  }
25587
25666
  }
25588
25667
  const mergerGroups = groupMergerActions(mergerActions);
25589
- for (const group of mergerGroups) {
25590
- const groupEntries = processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, logger);
25591
- entries.push(...groupEntries);
25592
- }
25593
- for (const action of otherActions) {
25594
- switch (action.type) {
25595
- case "Reverse Split": {
25596
- const qty = action.quantity;
25597
- if (qty < 0) {
25598
- continue;
25599
- }
25600
- const oldQty = getHeldQuantity(inventory, action.symbol);
25601
- const newQty = Math.abs(qty);
25602
- if (oldQty > 0) {
25603
- const ratio = newQty / oldQty;
25604
- adjustLotsForSplit(inventory, action.symbol, ratio, logger);
25605
- const entry = generateSplitEntry(action, oldQty, newQty, logger);
25606
- entries.push(entry);
25607
- } else {
25608
- logger?.warn(`Reverse split for ${action.symbol} but no lots found`);
25609
- }
25610
- break;
25611
- }
25612
- case "Worthless Liquidation": {
25613
- const removedLots = removeLots(inventory, action.symbol, logger);
25614
- if (removedLots.length > 0) {
25615
- const entry = generateWorthlessEntry(action, removedLots, logger);
25616
- entries.push(entry);
25617
- } else {
25618
- logger?.warn(`Worthless liquidation for ${action.symbol} but no lots found`);
25619
- }
25620
- break;
25621
- }
25622
- case "Rights Distribution": {
25623
- if (action.quantity > 0) {
25624
- const entry = generateRightsDistributionEntry(action, logger);
25625
- entries.push(entry);
25626
- }
25627
- break;
25628
- }
25668
+ mergerGroups.sort((a, b) => formatDate(a.date).localeCompare(formatDate(b.date)));
25669
+ let mi = 0;
25670
+ let oi = 0;
25671
+ while (mi < mergerGroups.length || oi < otherActions.length) {
25672
+ const mergerDate = mi < mergerGroups.length ? formatDate(mergerGroups[mi].date) : "\uFFFF";
25673
+ const otherDate = oi < otherActions.length ? formatDate(otherActions[oi].date) : "\uFFFF";
25674
+ if (mergerDate <= otherDate) {
25675
+ const groupEntries = processMultiWayMerger(mergerGroups[mi], inventory, lotInventoryPath, projectDir, logger);
25676
+ entries.push(...groupEntries);
25677
+ mi++;
25678
+ } else {
25679
+ processNonMergerAction(otherActions[oi], inventory, entries, logger);
25680
+ oi++;
25629
25681
  }
25630
25682
  }
25631
25683
  return entries;
@@ -25712,6 +25764,7 @@ function processMultiWayMerger(group, inventory, lotInventoryPath, projectDir, l
25712
25764
  orderNum: group.orderNum,
25713
25765
  outgoingSymbols,
25714
25766
  outgoingIsins: group.outgoing.map((a) => a.isin),
25767
+ outgoingQuantities: group.outgoing.map((a) => Math.abs(a.quantity)),
25715
25768
  totalCostBasis,
25716
25769
  currency: outgoingCurrency || "CAD"
25717
25770
  }, logger);
@@ -25785,6 +25838,68 @@ function removePendingMerger(projectDir, lotInventoryPath, key, logger) {
25785
25838
  logger?.debug(`Removed pending merger state: ${key}`);
25786
25839
  }
25787
25840
  }
25841
+ function resolveRemainingPendingMergers(projectDir, lotInventoryPath, logger) {
25842
+ const pendingDir = getPendingMergerDir(projectDir, lotInventoryPath);
25843
+ if (!fs18.existsSync(pendingDir)) {
25844
+ return { resolved: 0, journalFilesUpdated: [] };
25845
+ }
25846
+ const files = fs18.readdirSync(pendingDir).filter((f) => f.endsWith(".json"));
25847
+ if (files.length === 0) {
25848
+ return { resolved: 0, journalFilesUpdated: [] };
25849
+ }
25850
+ const entriesByJournal = new Map;
25851
+ const resolvedFiles = [];
25852
+ for (const file2 of files) {
25853
+ const filePath = path13.join(pendingDir, file2);
25854
+ try {
25855
+ const content = fs18.readFileSync(filePath, "utf-8");
25856
+ const state = JSON.parse(content);
25857
+ const dateMatch = state.date.match(/\d{2}-\d{2}-(\d{4})/);
25858
+ if (!dateMatch) {
25859
+ logger?.warn(`Invalid date in pending merger ${file2}: ${state.date}`);
25860
+ continue;
25861
+ }
25862
+ const year = parseInt(dateMatch[1], 10);
25863
+ const entry = generatePendingMergerWorthlessEntry(state.date, state.orderNum, state.outgoingSymbols, state.outgoingIsins, state.outgoingQuantities || [], state.totalCostBasis, state.currency, logger);
25864
+ const journalFile = path13.join(projectDir, "ledger", "investments", `${year}-${state.currency.toLowerCase()}.journal`);
25865
+ if (!entriesByJournal.has(journalFile)) {
25866
+ entriesByJournal.set(journalFile, []);
25867
+ }
25868
+ entriesByJournal.get(journalFile).push(entry);
25869
+ fs18.unlinkSync(filePath);
25870
+ resolvedFiles.push(file2);
25871
+ logger?.info(`Resolved pending merger ${file2} as worthless: ${state.outgoingSymbols.join(", ")} (${state.totalCostBasis.toFixed(2)} ${state.currency})`);
25872
+ } catch (error45) {
25873
+ const message = error45 instanceof Error ? error45.message : String(error45);
25874
+ logger?.warn(`Failed to resolve pending merger ${file2}: ${message}`);
25875
+ }
25876
+ }
25877
+ const journalFilesUpdated = [];
25878
+ for (const [journalFile, journalEntries] of entriesByJournal) {
25879
+ if (fs18.existsSync(journalFile)) {
25880
+ fs18.appendFileSync(journalFile, `
25881
+ ` + journalEntries.join(`
25882
+ `));
25883
+ } else {
25884
+ const basename5 = path13.basename(journalFile, ".journal");
25885
+ const parts = basename5.split("-");
25886
+ const yearStr = parts[0];
25887
+ const currency = parts.slice(1).join("-");
25888
+ const header = formatJournalFile(journalEntries, parseInt(yearStr, 10), currency);
25889
+ fs18.writeFileSync(journalFile, header);
25890
+ }
25891
+ journalFilesUpdated.push(journalFile);
25892
+ logger?.info(`Updated ${path13.basename(journalFile)} with worthless resolution entries`);
25893
+ }
25894
+ const remaining = fs18.readdirSync(pendingDir).filter((f) => f.endsWith(".json"));
25895
+ if (remaining.length === 0) {
25896
+ try {
25897
+ fs18.rmdirSync(pendingDir);
25898
+ logger?.debug("Removed empty pending-mergers directory");
25899
+ } catch {}
25900
+ }
25901
+ return { resolved: resolvedFiles.length, journalFilesUpdated };
25902
+ }
25788
25903
  async function preprocessSwissquote(csvPath, projectDir, currency, year, lotInventoryPath = DEFAULT_LOT_INVENTORY_PATH, symbolMap = {}, logger) {
25789
25904
  logger?.startSection(`Swissquote Preprocessing: ${path13.basename(csvPath)}`);
25790
25905
  const stats = {
@@ -26730,6 +26845,10 @@ async function executeSwissquotePreprocessStep(context, contextIds, logger) {
26730
26845
  }
26731
26846
  logger?.logStep("Swissquote Preprocess", "success", `Processed: ${result.stats.trades} trades, ${result.stats.dividends} dividends, ${result.stats.corporateActions} corporate actions, ${result.stats.forexTransactions} forex`);
26732
26847
  }
26848
+ const pendingResolution = resolveRemainingPendingMergers(context.directory, lotInventoryPath, logger);
26849
+ if (pendingResolution.resolved > 0) {
26850
+ logger?.logStep("Swissquote Pending Mergers", "success", `Resolved ${pendingResolution.resolved} pending merger(s) as worthless`);
26851
+ }
26733
26852
  if (allForexRows.length > 0) {
26734
26853
  const firstCtx = swissquoteContexts[0];
26735
26854
  const yearJournalPath = path16.join(context.directory, "ledger", `${firstCtx.year}.journal`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.13.12-next.1",
3
+ "version": "0.13.13-next.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",