@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 +33 -21
- package/docs/tools/import-pipeline.md +6 -6
- package/package.json +1 -1
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
|
-
|
|
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
|
-
`
|
|
24683
|
-
`
|
|
24684
|
-
` assets:bank:revolut:btc ${btcQuantity} BTC
|
|
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}`, `
|
|
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
|
|
25943
|
-
const targets = allRows.filter((r) => r.amount
|
|
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
|
|
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
|
-
`
|
|
25986
|
-
`
|
|
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
|
|
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
|
|
224
|
-
equity:
|
|
225
|
-
equity:
|
|
226
|
-
assets:bank:revolut: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
|
|
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
|
|