@fuzzle/opencode-accountant 0.13.0 → 0.13.1-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 +212 -87
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -24456,6 +24456,24 @@ function createImportLogger(directory, worktreeId, provider) {
24456
24456
 
24457
24457
  // src/utils/btcPurchaseGenerator.ts
24458
24458
  import * as fs14 from "fs";
24459
+
24460
+ // src/utils/journalFormatting.ts
24461
+ var MIN_GAP = 5;
24462
+ var INDENT = " ";
24463
+ function formatPostings(postings) {
24464
+ const maxAccountLen = postings.reduce((max, p) => Math.max(max, p.account.length), 0);
24465
+ return postings.map((p) => {
24466
+ const gap = Math.max(MIN_GAP, maxAccountLen - p.account.length + MIN_GAP);
24467
+ const line = `${INDENT}${p.account}${" ".repeat(gap)}${p.amount}`;
24468
+ if (p.comment) {
24469
+ return `${line} ${p.comment}`;
24470
+ }
24471
+ return line;
24472
+ }).join(`
24473
+ `);
24474
+ }
24475
+
24476
+ // src/utils/btcPurchaseGenerator.ts
24459
24477
  function parseRevolutFiatDatetime(dateStr) {
24460
24478
  const [datePart, timePart] = dateStr.split(" ");
24461
24479
  if (!datePart || !timePart) {
@@ -24669,30 +24687,33 @@ function formatJournalEntry(match2) {
24669
24687
  const { fiatRow, btcRow } = match2;
24670
24688
  const date5 = fiatRow.dateStr;
24671
24689
  const fiatCurrency = fiatRow.currency;
24672
- const fiatAmount = formatAmount(fiatRow.amount);
24673
24690
  const btcQuantity = formatBtcQuantity(btcRow.quantity);
24674
- const btcPrice = formatAmount(btcRow.price.amount);
24675
24691
  const priceCurrency = btcRow.price.currency;
24676
24692
  const hasFees = btcRow.fees.amount > 0;
24677
24693
  const isBaseCurrency = fiatCurrency === "CHF";
24694
+ const netFiatAmount = hasFees ? fiatRow.amount - btcRow.fees.amount : fiatRow.amount;
24695
+ const effectivePrice = hasFees ? netFiatAmount / btcRow.quantity : btcRow.price.amount;
24696
+ const fiatAmount = formatAmount(fiatRow.amount);
24697
+ const netFiat = formatAmount(netFiatAmount);
24698
+ const btcPrice = formatAmount(effectivePrice);
24678
24699
  const pair = `${fiatCurrency.toLowerCase()}-btc`;
24679
24700
  const equityFiat = `equity:conversion:${pair}:${fiatCurrency.toLowerCase()}`;
24680
24701
  const equityBtc = `equity:conversion:${pair}:btc`;
24681
- const assetCostAnnotation = isBaseCurrency ? ` @ ${btcPrice} ${priceCurrency}` : "";
24682
- const lines = [
24683
- `${date5} Bitcoin purchase`,
24684
- ` assets:bank:revolut:${fiatCurrency.toLowerCase()} -${fiatAmount} ${fiatCurrency}`,
24685
- ` ${equityFiat} ${fiatAmount} ${fiatCurrency}`,
24686
- ` ${equityBtc} -${btcQuantity} BTC`,
24687
- ` assets:bank:revolut:btc ${btcQuantity} BTC${assetCostAnnotation}`
24702
+ const costAnnotation = isBaseCurrency ? ` @ ${btcPrice} ${priceCurrency}` : "";
24703
+ const postings = [
24704
+ {
24705
+ account: `assets:bank:revolut:${fiatCurrency.toLowerCase()}`,
24706
+ amount: `-${fiatAmount} ${fiatCurrency}`
24707
+ }
24688
24708
  ];
24689
24709
  if (hasFees) {
24690
24710
  const feeAmount = formatAmount(btcRow.fees.amount);
24691
24711
  const feeCurrency = btcRow.fees.currency;
24692
- lines.push(` expenses:fees:btc ${feeAmount} ${feeCurrency}`, ` ${equityFiat} -${feeAmount} ${feeCurrency}`);
24712
+ postings.push({ account: "expenses:fees:btc", amount: `${feeAmount} ${feeCurrency}` });
24693
24713
  }
24694
- return lines.join(`
24695
- `);
24714
+ postings.push({ account: equityFiat, amount: `${netFiat} ${fiatCurrency}` }, { account: equityBtc, amount: `-${btcQuantity} BTC` }, { account: "assets:bank:revolut:btc", amount: `${btcQuantity} BTC${costAnnotation}` });
24715
+ return `${date5} Bitcoin purchase
24716
+ ${formatPostings(postings)}`;
24696
24717
  }
24697
24718
  function formatAmount(amount) {
24698
24719
  return amount.toFixed(2);
@@ -24745,8 +24766,18 @@ function generateBtcPurchaseJournal(fiatCsvPaths, btcCsvPath, yearJournalPath, l
24745
24766
  }
24746
24767
  const newEntries = [];
24747
24768
  let skippedDuplicates = 0;
24769
+ const accountsUsed = new Set;
24748
24770
  const sortedMatches = [...matches].sort((a, b) => a.fiatRow.date.getTime() - b.fiatRow.date.getTime());
24749
24771
  for (const match2 of sortedMatches) {
24772
+ const fiat = match2.fiatRow.currency.toLowerCase();
24773
+ const pair = `${fiat}-btc`;
24774
+ accountsUsed.add(`assets:bank:revolut:${fiat}`);
24775
+ accountsUsed.add(`equity:conversion:${pair}:${fiat}`);
24776
+ accountsUsed.add(`equity:conversion:${pair}:btc`);
24777
+ accountsUsed.add("assets:bank:revolut:btc");
24778
+ if (match2.btcRow.fees.amount > 0) {
24779
+ accountsUsed.add("expenses:fees:btc");
24780
+ }
24750
24781
  if (isDuplicate(match2, journalContent)) {
24751
24782
  skippedDuplicates++;
24752
24783
  logger?.debug(`Skipping duplicate: ${match2.fiatRow.dateStr} ${formatAmount(match2.fiatRow.amount)} ${match2.fiatRow.currency}`);
@@ -24770,7 +24801,8 @@ function generateBtcPurchaseJournal(fiatCsvPaths, btcCsvPath, yearJournalPath, l
24770
24801
  entriesAdded: newEntries.length,
24771
24802
  skippedDuplicates,
24772
24803
  unmatchedFiat,
24773
- unmatchedBtc
24804
+ unmatchedBtc,
24805
+ accountsUsed
24774
24806
  };
24775
24807
  }
24776
24808
 
@@ -25119,17 +25151,27 @@ function generateBuyEntry(trade, logger) {
25119
25151
  const cashOut = totalCost + fees;
25120
25152
  logger?.debug(`Generating Buy entry: ${qty} ${trade.symbol} @ ${price} ${trade.currency}`);
25121
25153
  const commodity = formatCommodity(trade.symbol);
25154
+ const postings = [
25155
+ {
25156
+ account: `assets:investments:stocks:${trade.symbol}`,
25157
+ amount: `${qty} ${commodity} @ ${price} ${trade.currency}`
25158
+ }
25159
+ ];
25160
+ if (fees > 0) {
25161
+ postings.push({
25162
+ account: "expenses:fees:trading:swissquote",
25163
+ amount: formatAmount2(fees, trade.currency)
25164
+ });
25165
+ }
25166
+ postings.push({
25167
+ account: `assets:broker:swissquote:${trade.currency.toLowerCase()}`,
25168
+ amount: formatAmount2(-cashOut, trade.currency)
25169
+ });
25122
25170
  let entry = `${date5} ${description}
25123
25171
  `;
25124
25172
  entry += ` ; swissquote:order:${trade.orderNum} isin:${trade.isin}
25125
25173
  `;
25126
- entry += ` assets:investments:stocks:${trade.symbol} ${qty} ${commodity} @ ${price} ${trade.currency}
25127
- `;
25128
- if (fees > 0) {
25129
- entry += ` expenses:fees:trading:swissquote ${formatAmount2(fees, trade.currency)}
25130
- `;
25131
- }
25132
- entry += ` assets:broker:swissquote:${trade.currency.toLowerCase()} ${formatAmount2(-cashOut, trade.currency)}
25174
+ entry += formatPostings(postings) + `
25133
25175
  `;
25134
25176
  return entry;
25135
25177
  }
@@ -25144,49 +25186,72 @@ function generateSellEntry(trade, consumed, logger) {
25144
25186
  const gain = calculateCapitalGain(consumed, salePrice, trade.quantity);
25145
25187
  logger?.debug(`Generating Sell entry: ${qty} ${trade.symbol} @ ${salePrice} ${trade.currency}, gain: ${gain.toFixed(2)}`);
25146
25188
  const commodity = formatCommodity(trade.symbol);
25147
- let entry = `${date5} ${description}
25148
- `;
25149
- entry += ` ; swissquote:order:${trade.orderNum} isin:${trade.isin}
25150
- `;
25151
25189
  const lotDetails = consumed.map((c) => `${c.lot.date}: ${formatQuantity(c.quantity)}@${formatPrice(c.lot.costBasis)}`).join(", ");
25152
- entry += ` ; FIFO lots: ${lotDetails}
25153
- `;
25190
+ const postings = [];
25154
25191
  for (const c of consumed) {
25155
25192
  const lotQty = formatQuantity(c.quantity);
25156
25193
  const lotPrice = formatPrice(c.lot.costBasis);
25157
- entry += ` assets:investments:stocks:${trade.symbol} -${lotQty} ${commodity} @ ${lotPrice} ${trade.currency}
25158
- `;
25194
+ postings.push({
25195
+ account: `assets:investments:stocks:${trade.symbol}`,
25196
+ amount: `-${lotQty} ${commodity} @ ${lotPrice} ${trade.currency}`
25197
+ });
25159
25198
  }
25160
- entry += ` assets:broker:swissquote:${trade.currency.toLowerCase()} ${formatAmount2(cashIn, trade.currency)}
25161
- `;
25199
+ postings.push({
25200
+ account: `assets:broker:swissquote:${trade.currency.toLowerCase()}`,
25201
+ amount: formatAmount2(cashIn, trade.currency)
25202
+ });
25162
25203
  if (fees > 0) {
25163
- entry += ` expenses:fees:trading:swissquote ${formatAmount2(fees, trade.currency)}
25164
- `;
25204
+ postings.push({
25205
+ account: "expenses:fees:trading:swissquote",
25206
+ amount: formatAmount2(fees, trade.currency)
25207
+ });
25165
25208
  }
25166
25209
  if (gain >= 0) {
25167
- entry += ` income:capital-gains:realized ${formatAmount2(-gain, trade.currency)}
25168
- `;
25210
+ postings.push({
25211
+ account: "income:capital-gains:realized",
25212
+ amount: formatAmount2(-gain, trade.currency)
25213
+ });
25169
25214
  } else {
25170
- entry += ` expenses:losses:capital ${formatAmount2(-gain, trade.currency)}
25171
- `;
25215
+ postings.push({
25216
+ account: "expenses:losses:capital",
25217
+ amount: formatAmount2(-gain, trade.currency)
25218
+ });
25172
25219
  }
25220
+ let entry = `${date5} ${description}
25221
+ `;
25222
+ entry += ` ; swissquote:order:${trade.orderNum} isin:${trade.isin}
25223
+ `;
25224
+ entry += ` ; FIFO lots: ${lotDetails}
25225
+ `;
25226
+ entry += formatPostings(postings) + `
25227
+ `;
25173
25228
  return entry;
25174
25229
  }
25175
25230
  function generateDividendEntry(dividend, logger) {
25176
25231
  const date5 = formatDate(dividend.date);
25177
25232
  const description = escapeDescription(`Dividend ${dividend.symbol} - ${dividend.name}`);
25178
25233
  logger?.debug(`Generating Dividend entry: ${dividend.symbol}, net: ${dividend.netAmount} ${dividend.currency}`);
25234
+ const postings = [
25235
+ {
25236
+ account: `assets:broker:swissquote:${dividend.currency.toLowerCase()}`,
25237
+ amount: formatAmount2(dividend.netAmount, dividend.currency)
25238
+ }
25239
+ ];
25240
+ if (dividend.withholdingTax > 0) {
25241
+ postings.push({
25242
+ account: "expenses:taxes:withholding",
25243
+ amount: formatAmount2(dividend.withholdingTax, dividend.currency)
25244
+ });
25245
+ }
25246
+ postings.push({
25247
+ account: `income:dividends:${dividend.symbol}`,
25248
+ amount: formatAmount2(-dividend.grossAmount, dividend.currency)
25249
+ });
25179
25250
  let entry = `${date5} ${description}
25180
25251
  `;
25181
25252
  entry += ` ; swissquote:order:${dividend.orderNum} isin:${dividend.isin}
25182
25253
  `;
25183
- entry += ` assets:broker:swissquote:${dividend.currency.toLowerCase()} ${formatAmount2(dividend.netAmount, dividend.currency)}
25184
- `;
25185
- if (dividend.withholdingTax > 0) {
25186
- entry += ` expenses:taxes:withholding ${formatAmount2(dividend.withholdingTax, dividend.currency)}
25187
- `;
25188
- }
25189
- entry += ` income:dividends:${dividend.symbol} ${formatAmount2(-dividend.grossAmount, dividend.currency)}
25254
+ entry += formatPostings(postings) + `
25190
25255
  `;
25191
25256
  return entry;
25192
25257
  }
@@ -25196,20 +25261,26 @@ function generateSplitEntry(action, oldQuantity, newQuantity, logger) {
25196
25261
  const splitType = ratio > 1 ? "Split" : "Reverse Split";
25197
25262
  const description = escapeDescription(`${splitType}: ${action.symbol} (${action.name})`);
25198
25263
  logger?.debug(`Generating ${splitType} entry: ${oldQuantity} -> ${newQuantity} ${action.symbol}`);
25264
+ const commodity = formatCommodity(action.symbol);
25265
+ const postings = [
25266
+ {
25267
+ account: `assets:investments:stocks:${action.symbol}`,
25268
+ amount: `-${formatQuantity(oldQuantity)} ${commodity}`
25269
+ },
25270
+ { account: "equity:conversion", amount: `${formatQuantity(oldQuantity)} ${commodity}` },
25271
+ { account: "equity:conversion", amount: `-${formatQuantity(newQuantity)} ${commodity}` },
25272
+ {
25273
+ account: `assets:investments:stocks:${action.symbol}`,
25274
+ amount: `${formatQuantity(newQuantity)} ${commodity}`
25275
+ }
25276
+ ];
25199
25277
  let entry = `${date5} ${description}
25200
25278
  `;
25201
25279
  entry += ` ; swissquote:order:${action.orderNum} isin:${action.isin}
25202
25280
  `;
25203
25281
  entry += ` ; Ratio: ${ratio.toFixed(4)} (${oldQuantity} -> ${newQuantity})
25204
25282
  `;
25205
- const commodity = formatCommodity(action.symbol);
25206
- entry += ` assets:investments:stocks:${action.symbol} -${formatQuantity(oldQuantity)} ${commodity}
25207
- `;
25208
- entry += ` equity:conversion ${formatQuantity(oldQuantity)} ${commodity}
25209
- `;
25210
- entry += ` equity:conversion -${formatQuantity(newQuantity)} ${commodity}
25211
- `;
25212
- entry += ` assets:investments:stocks:${action.symbol} ${formatQuantity(newQuantity)} ${commodity}
25283
+ entry += formatPostings(postings) + `
25213
25284
  `;
25214
25285
  return entry;
25215
25286
  }
@@ -25220,20 +25291,24 @@ function generateWorthlessEntry(action, removedLots, logger) {
25220
25291
  const totalCost = removedLots.reduce((sum, lot) => sum + lot.quantity * lot.costBasis, 0);
25221
25292
  const currency = removedLots[0]?.currency || "USD";
25222
25293
  logger?.debug(`Generating Worthless entry: ${totalQuantity} ${action.symbol}, loss: ${totalCost} ${currency}`);
25294
+ const commodity = formatCommodity(action.symbol);
25295
+ const postings = [];
25296
+ for (const lot of removedLots) {
25297
+ const qty = formatQuantity(lot.quantity);
25298
+ const price = formatPrice(lot.costBasis);
25299
+ postings.push({
25300
+ account: `assets:investments:stocks:${action.symbol}`,
25301
+ amount: `-${qty} ${commodity} @ ${price} ${currency}`
25302
+ });
25303
+ }
25304
+ postings.push({ account: "expenses:losses:capital", amount: formatAmount2(totalCost, currency) });
25223
25305
  let entry = `${date5} ${description}
25224
25306
  `;
25225
25307
  entry += ` ; swissquote:order:${action.orderNum} isin:${action.isin}
25226
25308
  `;
25227
25309
  entry += ` ; Total loss: ${totalCost.toFixed(2)} ${currency}
25228
25310
  `;
25229
- const commodity = formatCommodity(action.symbol);
25230
- for (const lot of removedLots) {
25231
- const qty = formatQuantity(lot.quantity);
25232
- const price = formatPrice(lot.costBasis);
25233
- entry += ` assets:investments:stocks:${action.symbol} -${qty} ${commodity} @ ${price} ${currency}
25234
- `;
25235
- }
25236
- entry += ` expenses:losses:capital ${formatAmount2(totalCost, currency)}
25311
+ entry += formatPostings(postings) + `
25237
25312
  `;
25238
25313
  return entry;
25239
25314
  }
@@ -25244,32 +25319,37 @@ function generateMultiWayMergerEntry(group, crossCurrencyOutgoingSymbols, crossC
25244
25319
  const descriptionParts = outSymbols.join(" + ");
25245
25320
  const description = escapeDescription(inSymbols.length > 0 ? `Merger: ${descriptionParts} -> ${inSymbols.join(" + ")}` : `Merger: ${descriptionParts}`);
25246
25321
  logger?.debug(`Generating multi-way merger entry: ${outSymbols.join(", ")} -> ${inSymbols.join(", ")}`);
25247
- let entry = `${date5} ${description}
25248
- `;
25249
- entry += ` ; swissquote:order:${group.orderNum}
25250
- `;
25251
25322
  const oldIsins = (crossCurrencyOutgoingIsins ?? group.outgoing.map((a) => a.isin)).filter(Boolean);
25252
25323
  const newIsins = group.incoming.map((a) => a.isin).filter(Boolean);
25253
- if (oldIsins.length > 0 || newIsins.length > 0) {
25254
- entry += ` ; Old ISIN: ${oldIsins.join(", ") || "n/a"}, New ISINs: ${newIsins.join(", ") || "n/a"}
25255
- `;
25256
- }
25324
+ const postings = [];
25257
25325
  for (const out of group.outgoing) {
25258
25326
  const qty = formatQuantity(Math.abs(out.quantity));
25259
25327
  const commodity = formatCommodity(out.symbol);
25260
- entry += ` assets:investments:stocks:${out.symbol} -${qty} ${commodity}
25261
- `;
25262
- entry += ` equity:conversion ${qty} ${commodity}
25263
- `;
25328
+ postings.push({
25329
+ account: `assets:investments:stocks:${out.symbol}`,
25330
+ amount: `-${qty} ${commodity}`
25331
+ });
25332
+ postings.push({ account: "equity:conversion", amount: `${qty} ${commodity}` });
25264
25333
  }
25265
25334
  for (const inc of group.incoming) {
25266
25335
  const qty = formatQuantity(Math.abs(inc.quantity));
25267
25336
  const commodity = formatCommodity(inc.symbol);
25268
- entry += ` equity:conversion -${qty} ${commodity}
25337
+ postings.push({ account: "equity:conversion", amount: `-${qty} ${commodity}` });
25338
+ postings.push({
25339
+ account: `assets:investments:stocks:${inc.symbol}`,
25340
+ amount: `${qty} ${commodity}`
25341
+ });
25342
+ }
25343
+ let entry = `${date5} ${description}
25344
+ `;
25345
+ entry += ` ; swissquote:order:${group.orderNum}
25269
25346
  `;
25270
- entry += ` assets:investments:stocks:${inc.symbol} ${qty} ${commodity}
25347
+ if (oldIsins.length > 0 || newIsins.length > 0) {
25348
+ entry += ` ; Old ISIN: ${oldIsins.join(", ") || "n/a"}, New ISINs: ${newIsins.join(", ") || "n/a"}
25271
25349
  `;
25272
25350
  }
25351
+ entry += formatPostings(postings) + `
25352
+ `;
25273
25353
  return entry;
25274
25354
  }
25275
25355
  function generateRightsDistributionEntry(action, logger) {
@@ -25278,13 +25358,18 @@ function generateRightsDistributionEntry(action, logger) {
25278
25358
  const description = escapeDescription(`Rights Distribution: ${action.symbol} - ${action.name}`);
25279
25359
  logger?.debug(`Generating Rights Distribution entry: ${qty} ${action.symbol}`);
25280
25360
  const commodity = formatCommodity(action.symbol);
25361
+ const postings = [
25362
+ {
25363
+ account: `assets:investments:stocks:${action.symbol}`,
25364
+ amount: `${qty} ${commodity} @ 0.00 CAD`
25365
+ },
25366
+ { account: "income:capital-gains:rights-distribution", amount: "0.00 CAD" }
25367
+ ];
25281
25368
  let entry = `${date5} ${description}
25282
25369
  `;
25283
25370
  entry += ` ; swissquote:order:${action.orderNum} isin:${action.isin}
25284
25371
  `;
25285
- entry += ` assets:investments:stocks:${action.symbol} ${qty} ${commodity} @ 0.00 CAD
25286
- `;
25287
- entry += ` income:capital-gains:rights-distribution 0.00 CAD
25372
+ entry += formatPostings(postings) + `
25288
25373
  `;
25289
25374
  return entry;
25290
25375
  }
@@ -25985,14 +26070,20 @@ function formatExchangeEntry(match2) {
25985
26070
  const pair = `${sourceCurrency.toLowerCase()}-${targetCurrency.toLowerCase()}`;
25986
26071
  const equitySource = `equity:conversion:${pair}:${sourceCurrency.toLowerCase()}`;
25987
26072
  const equityTarget = `equity:conversion:${pair}:${targetCurrency.toLowerCase()}`;
25988
- return [
25989
- `${date5} ${description}`,
25990
- ` assets:bank:revolut:${sourceCurrency.toLowerCase()} -${sourceAmount} ${sourceCurrency}`,
25991
- ` ${equitySource} ${sourceAmount} ${sourceCurrency}`,
25992
- ` ${equityTarget} -${targetAmount} ${targetCurrency}`,
25993
- ` assets:bank:revolut:${targetCurrency.toLowerCase()} ${targetAmount} ${targetCurrency}`
25994
- ].join(`
25995
- `);
26073
+ const postings = [
26074
+ {
26075
+ account: `assets:bank:revolut:${sourceCurrency.toLowerCase()}`,
26076
+ amount: `-${sourceAmount} ${sourceCurrency}`
26077
+ },
26078
+ { account: equitySource, amount: `${sourceAmount} ${sourceCurrency}` },
26079
+ { account: equityTarget, amount: `-${targetAmount} ${targetCurrency}` },
26080
+ {
26081
+ account: `assets:bank:revolut:${targetCurrency.toLowerCase()}`,
26082
+ amount: `${targetAmount} ${targetCurrency}`
26083
+ }
26084
+ ];
26085
+ return `${date5} ${description}
26086
+ ${formatPostings(postings)}`;
25996
26087
  }
25997
26088
  function formatAmount3(amount) {
25998
26089
  return amount.toFixed(2);
@@ -26054,7 +26145,13 @@ function generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger)
26054
26145
  }
26055
26146
  if (rowsByCsv.size < 2) {
26056
26147
  logger?.info("Need at least 2 CSVs with EXCHANGE rows to match pairs, skipping");
26057
- return { matchCount: 0, entriesAdded: 0, skippedDuplicates: 0, matchedRowIndices: new Map };
26148
+ return {
26149
+ matchCount: 0,
26150
+ entriesAdded: 0,
26151
+ skippedDuplicates: 0,
26152
+ matchedRowIndices: new Map,
26153
+ accountsUsed: new Set
26154
+ };
26058
26155
  }
26059
26156
  logger?.info("Matching exchange pairs across CSVs...");
26060
26157
  const matches = matchExchangePairs(rowsByCsv);
@@ -26065,8 +26162,16 @@ function generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger)
26065
26162
  }
26066
26163
  const newEntries = [];
26067
26164
  let skippedDuplicates = 0;
26165
+ const accountsUsed = new Set;
26068
26166
  const sortedMatches = [...matches].sort((a, b) => a.source.date.getTime() - b.source.date.getTime());
26069
26167
  for (const match2 of sortedMatches) {
26168
+ const source = match2.source.currency.toLowerCase();
26169
+ const target = match2.target.currency.toLowerCase();
26170
+ const pair = `${source}-${target}`;
26171
+ accountsUsed.add(`assets:bank:revolut:${source}`);
26172
+ accountsUsed.add(`equity:conversion:${pair}:${source}`);
26173
+ accountsUsed.add(`equity:conversion:${pair}:${target}`);
26174
+ accountsUsed.add(`assets:bank:revolut:${target}`);
26070
26175
  if (isDuplicate2(match2, journalContent)) {
26071
26176
  skippedDuplicates++;
26072
26177
  logger?.debug(`Skipping duplicate: ${match2.source.dateStr} ${formatAmount3(match2.source.amount)} ${match2.source.currency} \u2192 ${formatAmount3(match2.target.amount)} ${match2.target.currency}`);
@@ -26098,7 +26203,8 @@ function generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger)
26098
26203
  matchCount: matches.length,
26099
26204
  entriesAdded: newEntries.length,
26100
26205
  skippedDuplicates,
26101
- matchedRowIndices
26206
+ matchedRowIndices,
26207
+ accountsUsed
26102
26208
  };
26103
26209
  }
26104
26210
 
@@ -26270,6 +26376,9 @@ async function executeBtcPurchaseStep(context, contextIds, logger) {
26270
26376
  const year = yearMatch ? parseInt(yearMatch[1], 10) : new Date().getFullYear();
26271
26377
  const yearJournalPath = ensureYearJournalExists(context.directory, year);
26272
26378
  const result = generateBtcPurchaseJournal(fiatCsvPaths, btcCsvPath, yearJournalPath, logger);
26379
+ for (const account of result.accountsUsed) {
26380
+ context.generatedAccounts.add(account);
26381
+ }
26273
26382
  const message = result.entriesAdded > 0 ? `Generated ${result.entriesAdded} BTC purchase entries (${result.matchCount} matched, ${result.skippedDuplicates} skipped)` : `No new BTC purchase entries (${result.matchCount} matched, ${result.skippedDuplicates} duplicates)`;
26274
26383
  logger?.logStep("BTC Purchases", "success", message);
26275
26384
  logger?.endSection();
@@ -26304,6 +26413,9 @@ async function executeCurrencyExchangeStep(context, contextIds, logger) {
26304
26413
  const yearJournalPath = ensureYearJournalExists(context.directory, year);
26305
26414
  const fiatCsvPaths = fiatContexts.map((c) => c.csvPath);
26306
26415
  const result = generateCurrencyExchangeJournal(fiatCsvPaths, yearJournalPath, logger);
26416
+ for (const account of result.accountsUsed) {
26417
+ context.generatedAccounts.add(account);
26418
+ }
26307
26419
  if (result.matchedRowIndices.size > 0) {
26308
26420
  for (const { contextId, csvPath } of fiatContexts) {
26309
26421
  const indices = result.matchedRowIndices.get(csvPath);
@@ -26706,7 +26818,8 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
26706
26818
  options,
26707
26819
  configLoader,
26708
26820
  hledgerExecutor,
26709
- result
26821
+ result,
26822
+ generatedAccounts: new Set
26710
26823
  };
26711
26824
  try {
26712
26825
  const contextIds = await executeClassifyStep(context, logger);
@@ -26719,6 +26832,18 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
26719
26832
  await executeBtcPurchaseStep(context, contextIds, logger);
26720
26833
  await executeCurrencyExchangeStep(context, contextIds, logger);
26721
26834
  await executeSwissquotePreprocessStep(context, contextIds, logger);
26835
+ if (context.generatedAccounts.size > 0) {
26836
+ const firstCtx = contextIds.map((id) => loadContext(context.directory, id)).find((c) => c.provider === "revolut");
26837
+ if (firstCtx) {
26838
+ const yearMatch = firstCtx.filePath.match(/(\d{4})-\d{2}-\d{2}/);
26839
+ const year = yearMatch ? parseInt(yearMatch[1], 10) : new Date().getFullYear();
26840
+ const yearJournalPath = ensureYearJournalExists(context.directory, year);
26841
+ const declResult = ensureAccountDeclarations(yearJournalPath, context.generatedAccounts);
26842
+ if (declResult.added.length > 0) {
26843
+ logger.info(`Declared ${declResult.added.length} account(s) from generated entries: ${declResult.added.join(", ")}`);
26844
+ }
26845
+ }
26846
+ }
26722
26847
  const importConfig = loadImportConfig(context.directory);
26723
26848
  const orderedContextIds = [...contextIds].sort((a, b) => {
26724
26849
  const ctxA = loadContext(context.directory, a);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.13.0",
3
+ "version": "0.13.1-next.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",