@fuzzle/opencode-accountant 0.7.4 → 0.8.0-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/README.md +1 -1
- package/agent/accountant.md +1 -1
- package/dist/index.js +121 -25
- package/docs/tools/import-pipeline.md +69 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -261,7 +261,7 @@ See the hledger documentation for details on rules file format and syntax.
|
|
|
261
261
|
The pipeline automatically manages account declarations:
|
|
262
262
|
|
|
263
263
|
- Scans rules files matched to the CSVs being imported
|
|
264
|
-
- Extracts all accounts (account1
|
|
264
|
+
- Extracts all accounts (account1, account2, account3, etc. directives)
|
|
265
265
|
- Creates or updates year journal files with sorted account declarations
|
|
266
266
|
- Ensures `hledger check --strict` validation passes
|
|
267
267
|
|
package/agent/accountant.md
CHANGED
|
@@ -148,7 +148,7 @@ The `import-pipeline` tool operates directly on the working directory:
|
|
|
148
148
|
|
|
149
149
|
The import pipeline automatically:
|
|
150
150
|
|
|
151
|
-
- Scans matched rules files for all account references (account1, account2 directives)
|
|
151
|
+
- Scans matched rules files for all account references (account1, account2, account3, etc. directives)
|
|
152
152
|
- Creates/updates year journal files (e.g., ledger/2026.journal) with sorted account declarations
|
|
153
153
|
- Prevents `hledger check --strict` failures due to missing account declarations
|
|
154
154
|
- No manual account setup required
|
package/dist/index.js
CHANGED
|
@@ -4249,7 +4249,7 @@ __export(exports_accountSuggester, {
|
|
|
4249
4249
|
extractRulePatternsFromFile: () => extractRulePatternsFromFile,
|
|
4250
4250
|
clearSuggestionCache: () => clearSuggestionCache
|
|
4251
4251
|
});
|
|
4252
|
-
import * as
|
|
4252
|
+
import * as fs17 from "fs";
|
|
4253
4253
|
import * as crypto from "crypto";
|
|
4254
4254
|
function clearSuggestionCache() {
|
|
4255
4255
|
Object.keys(suggestionCache).forEach((key) => delete suggestionCache[key]);
|
|
@@ -4259,10 +4259,10 @@ function hashTransaction(posting) {
|
|
|
4259
4259
|
return crypto.createHash("md5").update(data).digest("hex");
|
|
4260
4260
|
}
|
|
4261
4261
|
function loadExistingAccounts(yearJournalPath) {
|
|
4262
|
-
if (!
|
|
4262
|
+
if (!fs17.existsSync(yearJournalPath)) {
|
|
4263
4263
|
return [];
|
|
4264
4264
|
}
|
|
4265
|
-
const content =
|
|
4265
|
+
const content = fs17.readFileSync(yearJournalPath, "utf-8");
|
|
4266
4266
|
const lines = content.split(`
|
|
4267
4267
|
`);
|
|
4268
4268
|
const accounts = [];
|
|
@@ -4278,10 +4278,10 @@ function loadExistingAccounts(yearJournalPath) {
|
|
|
4278
4278
|
return accounts.sort();
|
|
4279
4279
|
}
|
|
4280
4280
|
function extractRulePatternsFromFile(rulesPath) {
|
|
4281
|
-
if (!
|
|
4281
|
+
if (!fs17.existsSync(rulesPath)) {
|
|
4282
4282
|
return [];
|
|
4283
4283
|
}
|
|
4284
|
-
const content =
|
|
4284
|
+
const content = fs17.readFileSync(rulesPath, "utf-8");
|
|
4285
4285
|
const lines = content.split(`
|
|
4286
4286
|
`);
|
|
4287
4287
|
const patterns = [];
|
|
@@ -24098,15 +24098,11 @@ function extractAccountsFromRulesFile(rulesPath) {
|
|
|
24098
24098
|
if (trimmed.startsWith("#") || trimmed.startsWith(";") || trimmed === "") {
|
|
24099
24099
|
continue;
|
|
24100
24100
|
}
|
|
24101
|
-
const
|
|
24102
|
-
if (
|
|
24103
|
-
accounts.add(
|
|
24101
|
+
const accountMatch = trimmed.match(/^account\d+\s+(.+?)(?:\s+|$)/);
|
|
24102
|
+
if (accountMatch) {
|
|
24103
|
+
accounts.add(accountMatch[1].trim());
|
|
24104
24104
|
continue;
|
|
24105
24105
|
}
|
|
24106
|
-
const account2Match = trimmed.match(/account2\s+(.+?)(?:\s+|$)/);
|
|
24107
|
-
if (account2Match) {
|
|
24108
|
-
accounts.add(account2Match[1].trim());
|
|
24109
|
-
}
|
|
24110
24106
|
}
|
|
24111
24107
|
return accounts;
|
|
24112
24108
|
}
|
|
@@ -24736,6 +24732,54 @@ Got:
|
|
|
24736
24732
|
return { rowsProcessed, sendRowsEnriched, alreadyPreprocessed: false };
|
|
24737
24733
|
}
|
|
24738
24734
|
|
|
24735
|
+
// src/utils/revolutFiatCsvPreprocessor.ts
|
|
24736
|
+
import * as fs16 from "fs";
|
|
24737
|
+
var ORIGINAL_HEADER2 = "Type,Product,Started Date,Completed Date,Description,Amount,Fee,Currency,State,Balance";
|
|
24738
|
+
var PREPROCESSED_HEADER2 = "Type,Product,Started Date,Completed Date,Description,Amount,Fee,Currency,State,Balance,Net_Amount";
|
|
24739
|
+
function preprocessRevolutFiatCsv(csvPath, logger) {
|
|
24740
|
+
const content = fs16.readFileSync(csvPath, "utf-8");
|
|
24741
|
+
const lines = content.trim().split(`
|
|
24742
|
+
`);
|
|
24743
|
+
if (lines.length < 2) {
|
|
24744
|
+
return { rowsProcessed: 0, sendRowsEnriched: 0, alreadyPreprocessed: false };
|
|
24745
|
+
}
|
|
24746
|
+
const header = lines[0].trim();
|
|
24747
|
+
if (header === PREPROCESSED_HEADER2) {
|
|
24748
|
+
logger?.info("CSV already preprocessed, skipping");
|
|
24749
|
+
return { rowsProcessed: lines.length - 1, sendRowsEnriched: 0, alreadyPreprocessed: true };
|
|
24750
|
+
}
|
|
24751
|
+
if (header !== ORIGINAL_HEADER2) {
|
|
24752
|
+
throw new Error(`Unexpected CSV header. Expected:
|
|
24753
|
+
${ORIGINAL_HEADER2}
|
|
24754
|
+
Got:
|
|
24755
|
+
${header}`);
|
|
24756
|
+
}
|
|
24757
|
+
const outputLines = [PREPROCESSED_HEADER2];
|
|
24758
|
+
let enrichedRows = 0;
|
|
24759
|
+
for (let i2 = 1;i2 < lines.length; i2++) {
|
|
24760
|
+
const line = lines[i2];
|
|
24761
|
+
if (line.trim() === "")
|
|
24762
|
+
continue;
|
|
24763
|
+
const fields = line.split(",");
|
|
24764
|
+
if (fields.length < 7) {
|
|
24765
|
+
outputLines.push(line + ",");
|
|
24766
|
+
continue;
|
|
24767
|
+
}
|
|
24768
|
+
const amount = parseFloat(fields[5]);
|
|
24769
|
+
const fee = parseFloat(fields[6]);
|
|
24770
|
+
const netAmount = amount - fee;
|
|
24771
|
+
outputLines.push(`${line},${netAmount.toFixed(2)}`);
|
|
24772
|
+
enrichedRows++;
|
|
24773
|
+
}
|
|
24774
|
+
const outputContent = outputLines.join(`
|
|
24775
|
+
`) + `
|
|
24776
|
+
`;
|
|
24777
|
+
fs16.writeFileSync(csvPath, outputContent);
|
|
24778
|
+
const rowsProcessed = outputLines.length - 1;
|
|
24779
|
+
logger?.info(`Preprocessed ${rowsProcessed} rows (${enrichedRows} rows enriched with Net_Amount)`);
|
|
24780
|
+
return { rowsProcessed, sendRowsEnriched: enrichedRows, alreadyPreprocessed: false };
|
|
24781
|
+
}
|
|
24782
|
+
|
|
24739
24783
|
// src/tools/import-pipeline.ts
|
|
24740
24784
|
class NoTransactionsError extends Error {
|
|
24741
24785
|
constructor() {
|
|
@@ -24803,6 +24847,50 @@ async function executeClassifyStep(context, logger) {
|
|
|
24803
24847
|
logger?.endSection();
|
|
24804
24848
|
return contextIds;
|
|
24805
24849
|
}
|
|
24850
|
+
function executePreprocessFiatStep(context, contextIds, logger) {
|
|
24851
|
+
const fiatCsvPaths = [];
|
|
24852
|
+
for (const contextId of contextIds) {
|
|
24853
|
+
const importCtx = loadContext(context.directory, contextId);
|
|
24854
|
+
if (importCtx.provider === "revolut" && importCtx.currency !== "btc") {
|
|
24855
|
+
fiatCsvPaths.push(path12.join(context.directory, importCtx.filePath));
|
|
24856
|
+
}
|
|
24857
|
+
}
|
|
24858
|
+
if (fiatCsvPaths.length === 0) {
|
|
24859
|
+
logger?.info("No revolut fiat CSV found, skipping fiat preprocessing");
|
|
24860
|
+
return;
|
|
24861
|
+
}
|
|
24862
|
+
logger?.startSection("Step 1a: Preprocess Fiat CSV");
|
|
24863
|
+
logger?.logStep("Fiat Preprocess", "start");
|
|
24864
|
+
try {
|
|
24865
|
+
let totalRows = 0;
|
|
24866
|
+
let totalEnriched = 0;
|
|
24867
|
+
let anyPreprocessed = true;
|
|
24868
|
+
for (const csvPath of fiatCsvPaths) {
|
|
24869
|
+
const result = preprocessRevolutFiatCsv(csvPath, logger);
|
|
24870
|
+
totalRows += result.rowsProcessed;
|
|
24871
|
+
totalEnriched += result.sendRowsEnriched;
|
|
24872
|
+
if (!result.alreadyPreprocessed)
|
|
24873
|
+
anyPreprocessed = false;
|
|
24874
|
+
}
|
|
24875
|
+
const message = anyPreprocessed && fiatCsvPaths.length > 0 && totalEnriched === 0 ? "Fiat CSV(s) already preprocessed" : `Preprocessed ${totalRows} rows (${totalEnriched} enriched with Net_Amount)`;
|
|
24876
|
+
logger?.logStep("Fiat Preprocess", "success", message);
|
|
24877
|
+
context.result.steps.fiatPreprocess = buildStepResult(true, message, {
|
|
24878
|
+
rowsProcessed: totalRows,
|
|
24879
|
+
enrichedRows: totalEnriched,
|
|
24880
|
+
alreadyPreprocessed: anyPreprocessed && fiatCsvPaths.length > 0
|
|
24881
|
+
});
|
|
24882
|
+
} catch (error45) {
|
|
24883
|
+
const errorMessage = `Fiat CSV preprocessing failed: ${error45 instanceof Error ? error45.message : String(error45)}`;
|
|
24884
|
+
logger?.error(errorMessage);
|
|
24885
|
+
logger?.logStep("Fiat Preprocess", "error", errorMessage);
|
|
24886
|
+
context.result.steps.fiatPreprocess = buildStepResult(false, errorMessage);
|
|
24887
|
+
context.result.error = errorMessage;
|
|
24888
|
+
context.result.hint = "Check the fiat CSV format \u2014 expected Revolut account statement";
|
|
24889
|
+
throw new Error(errorMessage);
|
|
24890
|
+
} finally {
|
|
24891
|
+
logger?.endSection();
|
|
24892
|
+
}
|
|
24893
|
+
}
|
|
24806
24894
|
function executePreprocessBtcStep(context, contextIds, logger) {
|
|
24807
24895
|
let btcCsvPath;
|
|
24808
24896
|
for (const contextId of contextIds) {
|
|
@@ -25163,10 +25251,18 @@ async function importPipeline(directory, agent, options, configLoader = loadImpo
|
|
|
25163
25251
|
logger.info("No files classified, nothing to import");
|
|
25164
25252
|
return buildPipelineSuccessResult(result, "No files to import");
|
|
25165
25253
|
}
|
|
25254
|
+
executePreprocessFiatStep(context, contextIds, logger);
|
|
25166
25255
|
executePreprocessBtcStep(context, contextIds, logger);
|
|
25167
25256
|
await executeBtcPurchaseStep(context, contextIds, logger);
|
|
25257
|
+
const orderedContextIds = [...contextIds].sort((a, b) => {
|
|
25258
|
+
const ctxA = loadContext(context.directory, a);
|
|
25259
|
+
const ctxB = loadContext(context.directory, b);
|
|
25260
|
+
const isBtcA = ctxA.provider === "revolut" && ctxA.currency === "btc" ? 1 : 0;
|
|
25261
|
+
const isBtcB = ctxB.provider === "revolut" && ctxB.currency === "btc" ? 1 : 0;
|
|
25262
|
+
return isBtcA - isBtcB;
|
|
25263
|
+
});
|
|
25168
25264
|
let totalTransactions = 0;
|
|
25169
|
-
for (const contextId of
|
|
25265
|
+
for (const contextId of orderedContextIds) {
|
|
25170
25266
|
const importContext = loadContext(context.directory, contextId);
|
|
25171
25267
|
logger.info(`Processing: ${importContext.filename} (${importContext.accountNumber || "unknown account"})`);
|
|
25172
25268
|
try {
|
|
@@ -25251,7 +25347,7 @@ This tool orchestrates the full import workflow:
|
|
|
25251
25347
|
}
|
|
25252
25348
|
});
|
|
25253
25349
|
// src/tools/init-directories.ts
|
|
25254
|
-
import * as
|
|
25350
|
+
import * as fs18 from "fs";
|
|
25255
25351
|
import * as path13 from "path";
|
|
25256
25352
|
async function initDirectories(directory) {
|
|
25257
25353
|
try {
|
|
@@ -25259,8 +25355,8 @@ async function initDirectories(directory) {
|
|
|
25259
25355
|
const directoriesCreated = [];
|
|
25260
25356
|
const gitkeepFiles = [];
|
|
25261
25357
|
const importBase = path13.join(directory, "import");
|
|
25262
|
-
if (!
|
|
25263
|
-
|
|
25358
|
+
if (!fs18.existsSync(importBase)) {
|
|
25359
|
+
fs18.mkdirSync(importBase, { recursive: true });
|
|
25264
25360
|
directoriesCreated.push("import");
|
|
25265
25361
|
}
|
|
25266
25362
|
const pathsToCreate = [
|
|
@@ -25271,19 +25367,19 @@ async function initDirectories(directory) {
|
|
|
25271
25367
|
];
|
|
25272
25368
|
for (const { path: dirPath } of pathsToCreate) {
|
|
25273
25369
|
const fullPath = path13.join(directory, dirPath);
|
|
25274
|
-
if (!
|
|
25275
|
-
|
|
25370
|
+
if (!fs18.existsSync(fullPath)) {
|
|
25371
|
+
fs18.mkdirSync(fullPath, { recursive: true });
|
|
25276
25372
|
directoriesCreated.push(dirPath);
|
|
25277
25373
|
}
|
|
25278
25374
|
const gitkeepPath = path13.join(fullPath, ".gitkeep");
|
|
25279
|
-
if (!
|
|
25280
|
-
|
|
25375
|
+
if (!fs18.existsSync(gitkeepPath)) {
|
|
25376
|
+
fs18.writeFileSync(gitkeepPath, "");
|
|
25281
25377
|
gitkeepFiles.push(path13.join(dirPath, ".gitkeep"));
|
|
25282
25378
|
}
|
|
25283
25379
|
}
|
|
25284
25380
|
const gitignorePath = path13.join(importBase, ".gitignore");
|
|
25285
25381
|
let gitignoreCreated = false;
|
|
25286
|
-
if (!
|
|
25382
|
+
if (!fs18.existsSync(gitignorePath)) {
|
|
25287
25383
|
const gitignoreContent = `# Ignore CSV/PDF files in temporary directories
|
|
25288
25384
|
/incoming/*.csv
|
|
25289
25385
|
/incoming/*.pdf
|
|
@@ -25301,7 +25397,7 @@ async function initDirectories(directory) {
|
|
|
25301
25397
|
.DS_Store
|
|
25302
25398
|
Thumbs.db
|
|
25303
25399
|
`;
|
|
25304
|
-
|
|
25400
|
+
fs18.writeFileSync(gitignorePath, gitignoreContent);
|
|
25305
25401
|
gitignoreCreated = true;
|
|
25306
25402
|
}
|
|
25307
25403
|
const parts = [];
|
|
@@ -25378,13 +25474,13 @@ You can now drop CSV files into import/incoming/ and run import-pipeline.`);
|
|
|
25378
25474
|
});
|
|
25379
25475
|
// src/tools/generate-btc-purchases.ts
|
|
25380
25476
|
import * as path14 from "path";
|
|
25381
|
-
import * as
|
|
25477
|
+
import * as fs19 from "fs";
|
|
25382
25478
|
function findFiatCsvPaths(directory, pendingDir, provider) {
|
|
25383
25479
|
const providerDir = path14.join(directory, pendingDir, provider);
|
|
25384
|
-
if (!
|
|
25480
|
+
if (!fs19.existsSync(providerDir))
|
|
25385
25481
|
return [];
|
|
25386
25482
|
const csvPaths = [];
|
|
25387
|
-
const entries =
|
|
25483
|
+
const entries = fs19.readdirSync(providerDir, { withFileTypes: true });
|
|
25388
25484
|
for (const entry of entries) {
|
|
25389
25485
|
if (!entry.isDirectory())
|
|
25390
25486
|
continue;
|
|
@@ -9,13 +9,14 @@ This tool is **restricted to the accountant agent only**.
|
|
|
9
9
|
The pipeline automates these sequential steps:
|
|
10
10
|
|
|
11
11
|
1. **Classify** - Detect provider/currency, create contexts, organize files
|
|
12
|
-
1a. **Preprocess
|
|
13
|
-
1b. **
|
|
12
|
+
1a. **Preprocess Fiat CSV** _(Revolut only)_ - Add computed `Net_Amount` column (`Amount - Fee`) for hledger rules
|
|
13
|
+
1b. **Preprocess BTC CSV** _(Revolut only)_ - Add computed columns (fees in BTC, total, clean price) for hledger rules
|
|
14
|
+
1c. **Generate BTC Purchases** _(Revolut only)_ - Cross-reference fiat + BTC CSVs for equity conversion entries
|
|
14
15
|
2. **Account Declarations** - Ensure all accounts exist in year journals
|
|
15
16
|
3. **Import** - Validate transactions, add to journals, move files to done
|
|
16
17
|
4. **Reconcile** - Verify balances match expectations
|
|
17
18
|
|
|
18
|
-
Steps 1a
|
|
19
|
+
Steps 1a–1c are Revolut-specific and are automatically skipped when no matching Revolut CSVs are present.
|
|
19
20
|
|
|
20
21
|
**Key behavior**: The pipeline processes files via **import contexts**. Each classified CSV gets a unique context ID, and subsequent steps operate on these contexts **sequentially** with **fail-fast** error handling.
|
|
21
22
|
|
|
@@ -154,7 +155,24 @@ When reconciliation fails:
|
|
|
154
155
|
|
|
155
156
|
See [classify-statements](classify-statements.md) for details.
|
|
156
157
|
|
|
157
|
-
### Step 1a: Preprocess
|
|
158
|
+
### Step 1a: Preprocess Fiat CSV
|
|
159
|
+
|
|
160
|
+
**Purpose**: Add a computed `Net_Amount` column to Revolut fiat CSVs for use in hledger rules
|
|
161
|
+
|
|
162
|
+
**What happens**:
|
|
163
|
+
|
|
164
|
+
1. Finds Revolut fiat contexts (provider=revolut, currency≠btc) from classification
|
|
165
|
+
2. For each row, computes: `Net_Amount = Amount - Fee` (formatted to 2 decimal places)
|
|
166
|
+
3. Appends `Net_Amount` column to the CSV (in-place)
|
|
167
|
+
4. Idempotent — skips if CSV is already preprocessed
|
|
168
|
+
|
|
169
|
+
**Why**: Revolut fiat CSVs have a separate `Fee` column (e.g., weekend FX surcharge). The total bank deduction is `Amount - Fee`, but this value isn't in the CSV. hledger rules can't perform arithmetic, so the preprocessor computes it as a new column that rules can reference via `%net_amount`.
|
|
170
|
+
|
|
171
|
+
**Example**: Amount=`-45.32`, Fee=`0.45` → Net_Amount=`-45.77`
|
|
172
|
+
|
|
173
|
+
**Skipped when**: No Revolut fiat CSV is present among the classified files.
|
|
174
|
+
|
|
175
|
+
### Step 1b: Preprocess BTC CSV
|
|
158
176
|
|
|
159
177
|
**Purpose**: Add computed columns to Revolut BTC CSVs for use in hledger rules
|
|
160
178
|
|
|
@@ -169,7 +187,7 @@ See [classify-statements](classify-statements.md) for details.
|
|
|
169
187
|
|
|
170
188
|
**Skipped when**: No Revolut BTC CSV is present among the classified files.
|
|
171
189
|
|
|
172
|
-
### Step
|
|
190
|
+
### Step 1c: Generate BTC Purchase Entries
|
|
173
191
|
|
|
174
192
|
**Purpose**: Cross-reference Revolut fiat and BTC CSVs to generate equity conversion journal entries for BTC purchases
|
|
175
193
|
|
|
@@ -255,37 +273,43 @@ See [reconcile-statement](reconcile-statement.md) for details.
|
|
|
255
273
|
### Visual Flow
|
|
256
274
|
|
|
257
275
|
```
|
|
258
|
-
|
|
259
|
-
│ USER: Drop CSV files into import/incoming/
|
|
260
|
-
|
|
276
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
277
|
+
│ USER: Drop CSV files into import/incoming/ │
|
|
278
|
+
└──────────────────────────────────────────────────────────────┘
|
|
279
|
+
│
|
|
280
|
+
▼
|
|
281
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
282
|
+
│ STEP 1: classify-statements │
|
|
283
|
+
│ ┌────────────────────────────────────────────────────────┐ │
|
|
284
|
+
│ │ For each CSV: │ │
|
|
285
|
+
│ │ • Detect provider/currency │ │
|
|
286
|
+
│ │ • Extract metadata (account#, dates, balances) │ │
|
|
287
|
+
│ │ • Generate UUID │ │
|
|
288
|
+
│ │ • CREATE: .memory/{uuid}.json │ │
|
|
289
|
+
│ │ • Move: incoming/ → pending/{provider}/{currency}/ │ │
|
|
290
|
+
│ └────────────────────────────────────────────────────────┘ │
|
|
291
|
+
│ │
|
|
292
|
+
│ OUTPUT: ["uuid-1", "uuid-2"] │
|
|
293
|
+
└──────────────────────────────────────────────────────────────┘
|
|
261
294
|
│
|
|
262
295
|
▼
|
|
263
|
-
|
|
264
|
-
│ STEP
|
|
265
|
-
│
|
|
266
|
-
|
|
267
|
-
│ │ • Detect provider/currency │ │
|
|
268
|
-
│ │ • Extract metadata (account#, dates, balances) │ │
|
|
269
|
-
│ │ • Generate UUID │ │
|
|
270
|
-
│ │ • CREATE: .memory/{uuid}.json │ │
|
|
271
|
-
│ │ • Move: incoming/ → pending/{provider}/{currency}/ │ │
|
|
272
|
-
│ └──────────────────────────────────────────────────────────┘ │
|
|
273
|
-
│ │
|
|
274
|
-
│ OUTPUT: ["uuid-1", "uuid-2"] │
|
|
275
|
-
└────────────────────────────────────────────────────────────────┘
|
|
296
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
297
|
+
│ STEP 1a: Preprocess Fiat CSV (if revolut fiat context) │
|
|
298
|
+
│ • Add Net_Amount column (Amount - Fee) │
|
|
299
|
+
└──────────────────────────────────────────────────────────────┘
|
|
276
300
|
│
|
|
277
301
|
▼
|
|
278
|
-
|
|
279
|
-
│ STEP
|
|
280
|
-
│ • Add Fees_BTC, Total_BTC, Price_Amount columns
|
|
281
|
-
|
|
302
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
303
|
+
│ STEP 1b: Preprocess BTC CSV (if revolut/btc context) │
|
|
304
|
+
│ • Add Fees_BTC, Total_BTC, Price_Amount columns │
|
|
305
|
+
└──────────────────────────────────────────────────────────────┘
|
|
282
306
|
│
|
|
283
307
|
▼
|
|
284
|
-
|
|
285
|
-
│ STEP
|
|
286
|
-
│ • Cross-reference fiat + BTC CSVs
|
|
287
|
-
│ • Generate equity conversion journal entries
|
|
288
|
-
|
|
308
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
309
|
+
│ STEP 1c: Generate BTC Purchase Entries │
|
|
310
|
+
│ • Cross-reference fiat + BTC CSVs │
|
|
311
|
+
│ • Generate equity conversion journal entries │
|
|
312
|
+
└──────────────────────────────────────────────────────────────┘
|
|
289
313
|
│
|
|
290
314
|
▼
|
|
291
315
|
┌───────────────────┴───────────────────┐
|
|
@@ -297,24 +321,23 @@ See [reconcile-statement](reconcile-statement.md) for details.
|
|
|
297
321
|
└──────────┘ └──────────┘
|
|
298
322
|
│ │
|
|
299
323
|
▼ │
|
|
300
|
-
|
|
301
|
-
│ STEPS 2-4 (for uuid-1):
|
|
302
|
-
│ • Account Declarations
|
|
303
|
-
│ •
|
|
304
|
-
│ •
|
|
305
|
-
│ •
|
|
306
|
-
│
|
|
307
|
-
|
|
308
|
-
│ ✓ Success │
|
|
324
|
+
┌──────────────────────────────────────┐ │
|
|
325
|
+
│ STEPS 2-4 (for uuid-1): │ │
|
|
326
|
+
│ • Account Declarations │ │
|
|
327
|
+
│ • Import │ │
|
|
328
|
+
│ • Reconcile │ │
|
|
329
|
+
│ • UPDATE: .memory/uuid-1.json │ │
|
|
330
|
+
└──────────────────────────────────────┘ │
|
|
331
|
+
│ ✓ Success │
|
|
309
332
|
│ │
|
|
310
333
|
│ ▼
|
|
311
|
-
│
|
|
312
|
-
│
|
|
313
|
-
│
|
|
314
|
-
│
|
|
315
|
-
│
|
|
316
|
-
│
|
|
317
|
-
│
|
|
334
|
+
│ ┌──────────────────────────────────────┐
|
|
335
|
+
│ │ STEPS 2-4 (for uuid-2): │
|
|
336
|
+
│ │ • Account Declarations │
|
|
337
|
+
│ │ • Import │
|
|
338
|
+
│ │ • Reconcile │
|
|
339
|
+
│ │ • UPDATE: .memory/uuid-2.json │
|
|
340
|
+
│ └──────────────────────────────────────┘
|
|
318
341
|
│ │ ✓ Success
|
|
319
342
|
│ │
|
|
320
343
|
▼ ▼
|