@fuzzle/opencode-accountant 0.12.1 → 0.12.2-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
@@ -23936,17 +23936,7 @@ function determineClosingBalance(csvFile, config2, importContext, manualClosingB
23936
23936
  if (analysisBalance) {
23937
23937
  return { closingBalance: analysisBalance, metadata, fromCSVAnalysis: true };
23938
23938
  }
23939
- const currency = importContext.currency?.toUpperCase() || "CHF";
23940
- const exampleBalance = `${currency} <amount>`;
23941
- const retryCmd = buildRetryCommand(importContext.id, exampleBalance);
23942
- return {
23943
- error: buildErrorResult2({
23944
- csvFile: relativeCsvPath,
23945
- error: "No closing balance found in CSV metadata or data",
23946
- hint: `Provide closingBalance parameter manually. Example retry: ${retryCmd}`,
23947
- metadata
23948
- })
23949
- };
23939
+ return { notAvailable: true, metadata };
23950
23940
  }
23951
23941
  function buildRetryCommand(contextId, closingBalance, account) {
23952
23942
  const parts = ["reconcile-statement", `--contextId ${contextId}`];
@@ -24071,6 +24061,14 @@ async function reconcileStatement(directory, agent, options, configLoader = load
24071
24061
  }
24072
24062
  const { csvFile, relativePath: relativeCsvPath } = csvResult;
24073
24063
  const balanceResult = determineClosingBalance(csvFile, config2, importContext, options.closingBalance, relativeCsvPath, rulesDir);
24064
+ if ("notAvailable" in balanceResult) {
24065
+ return JSON.stringify({
24066
+ success: true,
24067
+ skipped: true,
24068
+ csvFile: relativeCsvPath,
24069
+ note: "No closing balance available \u2014 reconciliation skipped"
24070
+ });
24071
+ }
24074
24072
  if ("error" in balanceResult) {
24075
24073
  return balanceResult.error;
24076
24074
  }
@@ -24676,17 +24674,22 @@ function formatJournalEntry(match2) {
24676
24674
  const btcPrice = formatAmount(btcRow.price.amount);
24677
24675
  const priceCurrency = btcRow.price.currency;
24678
24676
  const hasFees = btcRow.fees.amount > 0;
24677
+ const isBaseCurrency = fiatCurrency === "CHF";
24678
+ const pair = `${fiatCurrency.toLowerCase()}-btc`;
24679
+ const equityFiat = `equity:conversion:${pair}:${fiatCurrency.toLowerCase()}`;
24680
+ const equityBtc = `equity:conversion:${pair}:btc`;
24681
+ const assetCostAnnotation = isBaseCurrency ? ` @ ${btcPrice} ${priceCurrency}` : "";
24679
24682
  const lines = [
24680
24683
  `${date5} Bitcoin purchase`,
24681
24684
  ` assets:bank:revolut:${fiatCurrency.toLowerCase()} -${fiatAmount} ${fiatCurrency}`,
24682
- ` equity:bitcoin:conversion ${fiatAmount} ${fiatCurrency}`,
24683
- ` equity:bitcoin:conversion -${btcQuantity} BTC @ ${btcPrice} ${priceCurrency}`,
24684
- ` assets:bank:revolut:btc ${btcQuantity} BTC @ ${btcPrice} ${priceCurrency}`
24685
+ ` ${equityFiat} ${fiatAmount} ${fiatCurrency}`,
24686
+ ` ${equityBtc} -${btcQuantity} BTC`,
24687
+ ` assets:bank:revolut:btc ${btcQuantity} BTC${assetCostAnnotation}`
24685
24688
  ];
24686
24689
  if (hasFees) {
24687
24690
  const feeAmount = formatAmount(btcRow.fees.amount);
24688
24691
  const feeCurrency = btcRow.fees.currency;
24689
- lines.push(` expenses:fees:btc ${feeAmount} ${feeCurrency}`, ` equity:bitcoin:conversion -${feeAmount} ${feeCurrency}`);
24692
+ lines.push(` expenses:fees:btc ${feeAmount} ${feeCurrency}`, ` ${equityFiat} -${feeAmount} ${feeCurrency}`);
24690
24693
  }
24691
24694
  return lines.join(`
24692
24695
  `);
@@ -25939,8 +25942,8 @@ function matchExchangePairs(rowsByCsv) {
25939
25942
  for (const rows of rowsByCsv.values()) {
25940
25943
  allRows.push(...rows);
25941
25944
  }
25942
- const sources = allRows.filter((r) => r.amount > 0);
25943
- const targets = allRows.filter((r) => r.amount < 0);
25945
+ const sources = allRows.filter((r) => r.amount < 0);
25946
+ const targets = allRows.filter((r) => r.amount > 0);
25944
25947
  const matches = [];
25945
25948
  const matchedSourceIndices = new Set;
25946
25949
  const matchedTargetIndices = new Set;
@@ -25958,8 +25961,8 @@ function matchExchangePairs(rowsByCsv) {
25958
25961
  if (!source.description.includes(targetCurrency))
25959
25962
  continue;
25960
25963
  matches.push({
25961
- source,
25962
- target: { ...target, amount: Math.abs(target.amount) }
25964
+ source: { ...source, amount: Math.abs(source.amount) },
25965
+ target
25963
25966
  });
25964
25967
  matchedSourceIndices.add(si);
25965
25968
  matchedTargetIndices.add(ti);
@@ -25979,11 +25982,14 @@ function formatExchangeEntry(match2) {
25979
25982
  const targetCurrency = target.currency;
25980
25983
  const sourceAmount = formatAmount3(source.amount);
25981
25984
  const targetAmount = formatAmount3(target.amount);
25985
+ const pair = `${sourceCurrency.toLowerCase()}-${targetCurrency.toLowerCase()}`;
25986
+ const equitySource = `equity:conversion:${pair}:${sourceCurrency.toLowerCase()}`;
25987
+ const equityTarget = `equity:conversion:${pair}:${targetCurrency.toLowerCase()}`;
25982
25988
  return [
25983
25989
  `${date5} ${description}`,
25984
25990
  ` assets:bank:revolut:${sourceCurrency.toLowerCase()} -${sourceAmount} ${sourceCurrency}`,
25985
- ` equity:currency:conversion ${sourceAmount} ${sourceCurrency}`,
25986
- ` equity:currency:conversion -${targetAmount} ${targetCurrency}`,
25991
+ ` ${equitySource} ${sourceAmount} ${sourceCurrency}`,
25992
+ ` ${equityTarget} -${targetAmount} ${targetCurrency}`,
25987
25993
  ` assets:bank:revolut:${targetCurrency.toLowerCase()} ${targetAmount} ${targetCurrency}`
25988
25994
  ].join(`
25989
25995
  `);
@@ -26634,6 +26640,12 @@ async function executeReconcileStep(context, contextId, logger) {
26634
26640
  account: context.options.account
26635
26641
  }, context.configLoader, context.hledgerExecutor);
26636
26642
  const reconcileParsed = JSON.parse(reconcileResult);
26643
+ if (reconcileParsed.skipped) {
26644
+ logger?.logStep("Reconcile", "success", "Skipped \u2014 no closing balance available");
26645
+ context.result.steps.reconcile = buildStepResult(true, "Reconciliation skipped \u2014 no closing balance available", { success: true, skipped: true, note: reconcileParsed.note });
26646
+ logger?.endSection();
26647
+ return;
26648
+ }
26637
26649
  const message = reconcileParsed.success ? `Balance reconciled: ${reconcileParsed.actualBalance}` : `Balance mismatch: expected ${reconcileParsed.expectedBalance}, got ${reconcileParsed.actualBalance}`;
26638
26650
  logger?.logStep("Reconcile", reconcileParsed.success ? "success" : "error", message);
26639
26651
  if (reconcileParsed.success) {
@@ -211,7 +211,7 @@ See [classify-statements](classify-statements.md) for details.
211
211
  1. Finds all Revolut fiat contexts (provider=revolut, currency≠btc) from classification
212
212
  2. Parses each CSV for `EXCHANGE` rows with "Exchanged to" descriptions
213
213
  3. Matches source (debited) and target (credited) rows across CSVs by timestamp (within 1 second)
214
- 4. Generates unified 4-posting journal entries using `equity:currency:conversion` with `@ cost` notation
214
+ 4. Generates unified 4-posting journal entries using per-pair `equity:conversion:{pair}:{currency}` trading sub-accounts
215
215
  5. Appends entries to year journal, skipping duplicates
216
216
  6. Filters matched EXCHANGE rows from CSVs (writes `-filtered.csv`) so hledger import doesn't double-count them
217
217
  7. Updates import contexts to point to filtered CSVs
@@ -220,13 +220,13 @@ See [classify-statements](classify-statements.md) for details.
220
220
 
221
221
  ```journal
222
222
  2026-01-15 Exchanged to EUR
223
- assets:bank:revolut:chf -100.00 CHF @ 1.0713 EUR
224
- equity:currency:conversion 100.00 CHF
225
- equity:currency:conversion -107.13 EUR
226
- assets:bank:revolut:eur 107.13 EUR
223
+ assets:bank:revolut:chf -100.00 CHF
224
+ equity:conversion:chf-eur:chf 100.00 CHF
225
+ equity:conversion:chf-eur:eur -107.13 EUR
226
+ assets:bank:revolut:eur 107.13 EUR
227
227
  ```
228
228
 
229
- **Why**: Revolut exports currency exchanges as two separate CSV rows — one per currency. Processing each through its own rules file produces two independent 2-posting transactions with a non-zero `equity:conversion` balance. This step intercepts EXCHANGE rows before hledger import, matches them across currencies, and generates a single unified transaction with explicit cost notation that keeps `equity:currency:conversion` balanced.
229
+ **Why**: Revolut exports currency exchanges as two separate CSV rows — one per currency. Processing each through its own rules file produces two independent 2-posting transactions with a non-zero `equity:conversion` balance. This step intercepts EXCHANGE rows before hledger import, matches them across currencies, and generates a single unified transaction with per-pair trading sub-accounts that keep each `equity:conversion:{pair}` balanced.
230
230
 
231
231
  **Skipped when**: Fewer than 2 Revolut fiat CSVs with EXCHANGE rows are present among the classified files.
232
232
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzzle/opencode-accountant",
3
- "version": "0.12.1",
3
+ "version": "0.12.2-next.1",
4
4
  "description": "An OpenCode accounting agent, specialized in double-entry-bookkepping with hledger",
5
5
  "author": {
6
6
  "name": "ali bengali",