@gabrielerandelli/minus-tracker 0.5.3 → 0.5.4
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 +69 -38
- package/dist/cli/index.js +15 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/data/ecb-rates.json +34 -0
- package/dist/index.cjs +15 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,44 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
[](./LICENSE)
|
|
4
4
|
|
|
5
|
-
> **Status:** v0.5.
|
|
5
|
+
> **Status:** v0.5.4 — published on npm as `@gabrielerandelli/minus-tracker`.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## Italiano
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
minus-tracker è un progetto open-source sviluppato principalmente in TypeScript per il calcolo automatico di
|
|
12
12
|
**plusvalenze e minusvalenze** nel _Regime Dichiarativo_ — abbinamento lotti LIFO/FIFO,
|
|
13
|
-
normalizzazione multi-valuta con tassi BCE storici
|
|
13
|
+
normalizzazione multi-valuta con tassi BCE storici.
|
|
14
|
+
|
|
15
|
+
Carica dati seguendo il formato CSV adottato da DEGIRO.
|
|
14
16
|
|
|
15
17
|
### Cosa fa
|
|
16
18
|
|
|
17
19
|
- Calcolo plusvalenze/minusvalenze con abbinamento lotti **LIFO e FIFO** (configurabile)
|
|
18
|
-
- **Parser CSV DEGIRO**
|
|
20
|
+
- **Parser CSV DEGIRO**
|
|
19
21
|
- Gestione **multi-valuta** con tassi BCE storici (EUR, USD, GBP, CHF)
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
- Libreria npm + CLI
|
|
22
|
+
- Suite di test basata su **FAQ Agenzia Entrate**
|
|
23
|
+
- L'ouput può essere restituito in **italiano** (predefinito) o **inglese** (`--lang en`)
|
|
24
|
+
- minus-tracker è un package NPM con supporto CLI
|
|
24
25
|
|
|
25
|
-
###
|
|
26
|
+
### Installazione CLI
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
import { DEGIROParser, Calculator } from "@gabrielerandelli/minus-tracker";
|
|
28
|
+
**Installazione globale** (comando disponibile da qualsiasi directory):
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g @gabrielerandelli/minus-tracker
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
**Senza installazione globale** (nessuna configurazione necessaria):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx @gabrielerandelli/minus-tracker calc trades.csv
|
|
38
|
+
```
|
|
35
39
|
|
|
36
|
-
### CLI
|
|
40
|
+
### Utilizzo CLI
|
|
37
41
|
|
|
38
42
|
```bash
|
|
39
|
-
# Calcola plusvalenze/minusvalenze (
|
|
43
|
+
# Calcola plusvalenze/minusvalenze (output in italiano)
|
|
40
44
|
minus-tracker calc trades.csv
|
|
41
45
|
|
|
42
|
-
#
|
|
46
|
+
# Output in inglese
|
|
43
47
|
minus-tracker calc --lang en trades.csv
|
|
44
48
|
|
|
45
49
|
# Imposta la lingua in modo permanente
|
|
@@ -53,14 +57,25 @@ minus-tracker rates --check
|
|
|
53
57
|
minus-tracker rates --update
|
|
54
58
|
```
|
|
55
59
|
|
|
56
|
-
### Installazione
|
|
57
|
-
|
|
58
|
-
**Prerequisiti:** Node.js ≥ 24 ([nodejs.org](https://nodejs.org))
|
|
60
|
+
### Installazione libreria
|
|
59
61
|
|
|
60
62
|
```bash
|
|
61
63
|
npm install @gabrielerandelli/minus-tracker
|
|
62
64
|
```
|
|
63
65
|
|
|
66
|
+
### Utilizzo libreria
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { DEGIROParser, Calculator } from "@gabrielerandelli/minus-tracker";
|
|
70
|
+
|
|
71
|
+
const transactions = new DEGIROParser().parse(csv);
|
|
72
|
+
const report = new Calculator(transactions).calculateGains("LIFO"); // o "FIFO"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
minus-tracker è **solo calcolo fiscale** — nessuna UI, autenticazione, database o PDF.
|
|
76
|
+
|
|
77
|
+
**Prerequisiti:** Node.js ≥ 24 ([nodejs.org](https://nodejs.org))
|
|
78
|
+
|
|
64
79
|
### Avvertenza
|
|
65
80
|
|
|
66
81
|
minus-tracker è un **ausilio al calcolo, non consulenza fiscale**. I risultati sono
|
|
@@ -70,31 +85,36 @@ destinati alla revisione e alla consegna a un commercialista qualificato.
|
|
|
70
85
|
|
|
71
86
|
## English
|
|
72
87
|
|
|
73
|
-
|
|
74
|
-
calculation**
|
|
75
|
-
normalisation
|
|
88
|
+
minus-tracker is an open-source project developed primarily in TypeScript for the automatic
|
|
89
|
+
calculation of **capital gains and losses** in the _Regime Dichiarativo_ — LIFO/FIFO lot
|
|
90
|
+
matching, multi-currency normalisation with historical ECB rates.
|
|
91
|
+
|
|
92
|
+
It loads data following the CSV format used by DEGIRO.
|
|
76
93
|
|
|
77
94
|
### What it does
|
|
78
95
|
|
|
79
96
|
- Capital-gains/loss calculation with configurable **LIFO and FIFO** lot matching
|
|
80
|
-
- **DEGIRO CSV parser
|
|
97
|
+
- **DEGIRO CSV parser**
|
|
81
98
|
- **Multi-currency** handling with historical ECB rates (EUR, USD, GBP, CHF)
|
|
82
|
-
- Test suite based on **Agenzia Entrate FAQ**
|
|
99
|
+
- Test suite based on **Agenzia Entrate FAQ**
|
|
83
100
|
- Output in **Italian** (default) or **English** (`--lang en`)
|
|
84
|
-
-
|
|
101
|
+
- minus-tracker is an NPM package with CLI support
|
|
85
102
|
|
|
86
|
-
###
|
|
103
|
+
### CLI Installation
|
|
87
104
|
|
|
88
|
-
|
|
89
|
-
import { DEGIROParser, Calculator } from "@gabrielerandelli/minus-tracker";
|
|
105
|
+
**Global install** (command available system-wide):
|
|
90
106
|
|
|
91
|
-
|
|
92
|
-
|
|
107
|
+
```bash
|
|
108
|
+
npm install -g @gabrielerandelli/minus-tracker
|
|
93
109
|
```
|
|
94
110
|
|
|
95
|
-
|
|
111
|
+
**Without global install** (no setup required):
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npx @gabrielerandelli/minus-tracker calc trades.csv
|
|
115
|
+
```
|
|
96
116
|
|
|
97
|
-
### CLI
|
|
117
|
+
### CLI Usage
|
|
98
118
|
|
|
99
119
|
```bash
|
|
100
120
|
# Calculate gains/losses (Italian output, default)
|
|
@@ -114,18 +134,29 @@ minus-tracker rates --check
|
|
|
114
134
|
minus-tracker rates --update
|
|
115
135
|
```
|
|
116
136
|
|
|
117
|
-
### Installation
|
|
118
|
-
|
|
119
|
-
**Prerequisites:** Node.js ≥ 24 ([nodejs.org](https://nodejs.org))
|
|
137
|
+
### Library Installation
|
|
120
138
|
|
|
121
139
|
```bash
|
|
122
140
|
npm install @gabrielerandelli/minus-tracker
|
|
123
141
|
```
|
|
124
142
|
|
|
143
|
+
### Library Usage
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { DEGIROParser, Calculator } from "@gabrielerandelli/minus-tracker";
|
|
147
|
+
|
|
148
|
+
const transactions = new DEGIROParser().parse(csv);
|
|
149
|
+
const report = new Calculator(transactions).calculateGains("LIFO"); // or "FIFO"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
minus-tracker is **pure tax math** — no UI, auth, database, or PDF.
|
|
153
|
+
|
|
154
|
+
**Prerequisites:** Node.js ≥ 24 ([nodejs.org](https://nodejs.org))
|
|
155
|
+
|
|
125
156
|
### Disclaimer
|
|
126
157
|
|
|
127
|
-
|
|
128
|
-
|
|
158
|
+
minus-tracker is a **calculation aid, not tax advice**. Outputs are intended for review
|
|
159
|
+
and handoff to a qualified commercialista.
|
|
129
160
|
|
|
130
161
|
---
|
|
131
162
|
|
package/dist/cli/index.js
CHANGED
|
@@ -169,12 +169,18 @@ import * as fs3 from "fs";
|
|
|
169
169
|
import { fileURLToPath } from "url";
|
|
170
170
|
import * as path2 from "path";
|
|
171
171
|
import * as fs2 from "fs";
|
|
172
|
-
var _bundled =
|
|
172
|
+
var _bundled = void 0;
|
|
173
173
|
function getBundledSnapshot() {
|
|
174
|
-
if (_bundled) return _bundled;
|
|
174
|
+
if (_bundled !== void 0) return _bundled;
|
|
175
175
|
const __dirname = path2.dirname(fileURLToPath(import.meta.url));
|
|
176
176
|
const bundledPath = path2.join(__dirname, "../data/ecb-rates.json");
|
|
177
|
-
|
|
177
|
+
try {
|
|
178
|
+
_bundled = JSON.parse(
|
|
179
|
+
fs2.readFileSync(bundledPath, "utf8")
|
|
180
|
+
);
|
|
181
|
+
} catch {
|
|
182
|
+
_bundled = null;
|
|
183
|
+
}
|
|
178
184
|
return _bundled;
|
|
179
185
|
}
|
|
180
186
|
function getUserSnapshot() {
|
|
@@ -195,6 +201,12 @@ function getUserSnapshot() {
|
|
|
195
201
|
function getActiveSnapshot() {
|
|
196
202
|
const bundled = getBundledSnapshot();
|
|
197
203
|
const user = getUserSnapshot();
|
|
204
|
+
if (!bundled && !user) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
"No ECB rates snapshot available. Run `minus-tracker rates --update` to fetch rates."
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (!bundled) return user;
|
|
198
210
|
if (!user) return bundled;
|
|
199
211
|
const merged = {};
|
|
200
212
|
for (const ccy of /* @__PURE__ */ new Set([...Object.keys(bundled), ...Object.keys(user)])) {
|
package/dist/cli/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cli/index.ts","../../src/i18n/it.ts","../../src/i18n/en.ts","../../src/i18n/settings.ts","../../src/i18n/index.ts","../../src/errors.ts","../../src/cli/commands/calc.ts","../../src/rates/index.ts","../../src/parser/warnings.ts","../../src/parser/index.ts","../../src/calculator/index.ts","../../src/cli/renderer.ts","../../src/cli/commands/validate.ts","../../src/cli/commands/rates.ts","../../src/cli/commands/config.ts"],"sourcesContent":["import { parseArgs } from \"node:util\";\nimport { resolveLocale, getStrings } from \"../i18n/index.js\";\nimport { ParseError, CalculationError } from \"../errors.js\";\nimport { runCalc } from \"./commands/calc.js\";\nimport { runValidate } from \"./commands/validate.js\";\nimport { runRates } from \"./commands/rates.js\";\nimport { runConfig } from \"./commands/config.js\";\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n args: process.argv.slice(2),\n options: {\n lang: { type: \"string\" },\n method: { type: \"string\" },\n year: { type: \"string\" },\n json: { type: \"boolean\", default: false },\n check: { type: \"boolean\", default: false },\n update: { type: \"boolean\", default: false },\n show: { type: \"boolean\", default: false },\n },\n allowPositionals: true,\n strict: false,\n });\n\n // Resolve locale once — exits 2 on invalid value\n const locale = resolveLocale(values.lang as string | undefined);\n const s = getStrings(locale);\n\n const command = positionals[0];\n const restPositionals = positionals.slice(1);\n const flags = values as Record<string, string | boolean>;\n\n const stdout = process.stdout;\n const stderr = process.stderr;\n\n let exitCode = 0;\n\n try {\n switch (command) {\n case \"calc\":\n exitCode = await runCalc(restPositionals, flags, s, stdout, stderr);\n break;\n case \"validate\":\n exitCode = await runValidate(restPositionals, flags, s, stdout, stderr);\n break;\n case \"rates\":\n exitCode = await runRates(restPositionals, flags, s, stdout, stderr);\n break;\n case \"config\":\n exitCode = await runConfig(restPositionals, flags, s, stdout, stderr);\n break;\n default:\n stderr.write(\n \"Usage: minus-tracker <calc|validate|rates|config> [options] [file]\\n\",\n );\n exitCode = 2;\n }\n } catch (err) {\n if (err instanceof ParseError) {\n if (err.code === \"INVALID_CSV\") {\n stderr.write(s.errorInvalidCsv + \"\\n\");\n } else {\n stderr.write(s.errorMissingColumn(err.columnName!) + \"\\n\");\n }\n exitCode = 1;\n } else if (err instanceof CalculationError) {\n stderr.write(s.errorNoOpenLots(err.isin, err.date) + \"\\n\");\n exitCode = 1;\n } else {\n throw err;\n }\n }\n\n process.exit(exitCode);\n}\n\nmain().catch((err) => {\n process.stderr.write(`Unhandled error: ${String(err)}\\n`);\n process.exit(1);\n});\n","import type { LocaleStrings } from \"./types.js\";\n\nexport const it: LocaleStrings = {\n numberLocale: \"it-IT\",\n\n errorInvalidCsv: \"CSV non valido: impossibile analizzare il file\",\n errorMissingColumn: (col) => `Colonna obbligatoria mancante: ${col}`,\n errorNoOpenLots: (isin, date) =>\n `Nessun lotto aperto per ISIN ${isin} in data ${date}`,\n\n warnMissingIsin: (row) => `Riga ${row}: ISIN mancante — riga ignorata`,\n warnUnsupportedCurrency: (row, currency) =>\n `Riga ${row}: valuta non supportata ${currency} — riga ignorata`,\n warnNoEcbRate: (row, currency, date) =>\n `Riga ${row}: nessun tasso BCE per ${currency} in data ${date} — riga ignorata`,\n warnQuantityZero: (row) => `Riga ${row}: quantità pari a 0 — riga ignorata`,\n\n warnMultipleYears:\n \"Il CSV contiene transazioni di più anni — filtra per un singolo anno per un calcolo accurato.\",\n\n headerMethod: \"METODO\",\n headerTaxYear: \"ANNO FISCALE\",\n headerIsin: \"ISIN\",\n headerProduct: \"TITOLO\",\n headerQty: \"QTÀ\",\n headerBuyDate: \"DATA ACQUISTO\",\n headerSellDate: \"DATA VENDITA\",\n headerBuyEur: \"ACQUISTO EUR\",\n headerSellEur: \"VENDITA EUR\",\n headerGainLoss: \"GUADAGNO/PERDITA\",\n\n summaryPlusvalenze: \"PLUSVALENZE\",\n summaryMinusvalenze: \"MINUSVALENZE\",\n summaryNetResult: \"RISULTATO NETTO\",\n summaryWarnings: \"AVVERTENZE\",\n summaryGenerated: \"Generato\",\n\n validateOk: (count, errors) =>\n `OK: ${count} transazioni analizzate, ${errors} errori gravi`,\n validateWarn: (count, reason) =>\n `AVVISO: ${count} righe ignorate (${reason})`,\n\n ratesCoverage: (start, end, currencies) =>\n `Copertura: ${start} → ${end} | Valute: ${currencies}`,\n ratesGapsNone: \"Lacune: nessuna\",\n ratesGaps: (list) => `Lacune: ${list}`,\n ratesUpdateFetching: \"Recupero dati BCE SDMX in corso...\",\n ratesUpdateDone: (n) => `Completato. Aggiunte ${n} nuove date.`,\n ratesSnapshotWritten: (path) => `Snapshot scritto in ${path}`,\n\n configLangSet: (lang) => `Lingua impostata su: ${lang}`,\n configCurrentLang: (lang) => `Lingua corrente: ${lang}`,\n\n disclaimer: \"minus-tracker è un ausilio al calcolo, non consulenza fiscale.\",\n};\n","import type { LocaleStrings } from \"./types.js\";\n\nexport const en: LocaleStrings = {\n numberLocale: \"en-US\",\n\n errorInvalidCsv: \"Invalid CSV: unable to parse\",\n errorMissingColumn: (col) => `Missing required column: ${col}`,\n errorNoOpenLots: (isin, date) => `No open lots for ISIN ${isin} on ${date}`,\n\n warnMissingIsin: (row) => `Row ${row}: missing ISIN — skipped`,\n warnUnsupportedCurrency: (row, currency) =>\n `Row ${row}: unsupported currency ${currency} — skipped`,\n warnNoEcbRate: (row, currency, date) =>\n `Row ${row}: no ECB rate for ${currency} on ${date} — skipped`,\n warnQuantityZero: (row) => `Row ${row}: quantity is 0 — skipped`,\n\n warnMultipleYears:\n \"CSV contains transactions from multiple years — filter to a single year for accurate reporting.\",\n\n headerMethod: \"METHOD\",\n headerTaxYear: \"TAX YEAR\",\n headerIsin: \"ISIN\",\n headerProduct: \"PRODUCT\",\n headerQty: \"QTY\",\n headerBuyDate: \"BUY DATE\",\n headerSellDate: \"SELL DATE\",\n headerBuyEur: \"BUY EUR\",\n headerSellEur: \"SELL EUR\",\n headerGainLoss: \"GAIN/LOSS\",\n\n summaryPlusvalenze: \"PLUSVALENZE\",\n summaryMinusvalenze: \"MINUSVALENZE\",\n summaryNetResult: \"NET RESULT\",\n summaryWarnings: \"WARNINGS\",\n summaryGenerated: \"Generated\",\n\n validateOk: (count, errors) =>\n `OK: ${count} transactions parsed, ${errors} hard errors`,\n validateWarn: (count, reason) => `WARN: ${count} rows skipped (${reason})`,\n\n ratesCoverage: (start, end, currencies) =>\n `Coverage: ${start} → ${end} | Currencies: ${currencies}`,\n ratesGapsNone: \"Gaps: none\",\n ratesGaps: (list) => `Gaps: ${list}`,\n ratesUpdateFetching: \"Fetching ECB SDMX...\",\n ratesUpdateDone: (n) => `Done. Added ${n} new dates.`,\n ratesSnapshotWritten: (path) => `Snapshot written to ${path}`,\n\n configLangSet: (lang) => `Language set to: ${lang}`,\n configCurrentLang: (lang) => `Current language: ${lang}`,\n\n disclaimer: \"minus-tracker è un ausilio al calcolo, non consulenza fiscale.\",\n};\n","import * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { MinusTrackerConfig, SupportedLocale } from \"./types.js\";\n\nconst SUPPORTED_LOCALES: SupportedLocale[] = [\"it\", \"en\"];\n\nexport function getConfigPath(): string {\n if (process.platform === \"win32\") {\n const appData =\n process.env.APPDATA ?? path.join(os.homedir(), \"AppData\", \"Roaming\");\n return path.join(appData, \"minus-tracker\", \"config.json\");\n }\n const xdg = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), \".config\");\n return path.join(xdg, \"minus-tracker\", \"config.json\");\n}\n\nexport function readConfig(): MinusTrackerConfig {\n try {\n const raw = fs.readFileSync(getConfigPath(), \"utf8\");\n return JSON.parse(raw) as MinusTrackerConfig;\n } catch {\n return {};\n }\n}\n\nexport function saveLocale(lang: SupportedLocale): void {\n const configPath = getConfigPath();\n fs.mkdirSync(path.dirname(configPath), { recursive: true });\n const existing = readConfig();\n const updated: MinusTrackerConfig = { ...existing, locale: lang };\n fs.writeFileSync(configPath, JSON.stringify(updated, null, 2) + \"\\n\", \"utf8\");\n}\n\nexport function resolveLocale(cliLang?: string): SupportedLocale {\n // 1. --lang flag\n if (cliLang !== undefined) {\n if (!SUPPORTED_LOCALES.includes(cliLang as SupportedLocale)) {\n process.stderr.write(`Unsupported locale \"${cliLang}\". Use: it, en\\n`);\n process.exit(2);\n }\n return cliLang as SupportedLocale;\n }\n // 2. env var\n const envLang = process.env.MINUS_TRACKER_LANG;\n if (envLang !== undefined) {\n if (!SUPPORTED_LOCALES.includes(envLang as SupportedLocale)) {\n process.stderr.write(`Unsupported locale \"${envLang}\". Use: it, en\\n`);\n process.exit(2);\n }\n return envLang as SupportedLocale;\n }\n // 3. config file\n const config = readConfig();\n if (config.locale && SUPPORTED_LOCALES.includes(config.locale)) {\n return config.locale;\n }\n // 4. default\n return \"it\";\n}\n","import { it } from \"./it.js\";\nimport { en } from \"./en.js\";\nimport type { LocaleStrings, SupportedLocale } from \"./types.js\";\n\nexport { it, en };\nexport type { LocaleStrings, SupportedLocale };\nexport type { MinusTrackerConfig } from \"./types.js\";\nexport {\n resolveLocale,\n saveLocale,\n readConfig,\n getConfigPath,\n} from \"./settings.js\";\n\nexport function getStrings(locale: SupportedLocale): LocaleStrings {\n return locale === \"it\" ? it : en;\n}\n","export class ParseError extends Error {\n readonly code: \"INVALID_CSV\" | \"MISSING_COLUMN\";\n readonly columnName?: string;\n\n constructor(code: \"INVALID_CSV\");\n constructor(code: \"MISSING_COLUMN\", columnName: string);\n constructor(code: ParseError[\"code\"], columnName?: string) {\n const msg =\n code === \"INVALID_CSV\"\n ? \"Invalid CSV: unable to parse\"\n : `Missing required column: ${columnName}`;\n super(msg);\n this.name = \"ParseError\";\n this.code = code;\n this.columnName = columnName;\n }\n}\n\nexport class CalculationError extends Error {\n readonly isin: string;\n readonly date: string;\n\n constructor(isin: string, date: string) {\n super(`No open lots for ISIN ${isin} on ${date}`);\n this.name = \"CalculationError\";\n this.isin = isin;\n this.date = date;\n }\n}\n","import * as fs from \"node:fs\";\nimport { DEGIROParser } from \"../../parser/index.js\";\nimport { Calculator } from \"../../calculator/index.js\";\nimport { ParseError } from \"../../errors.js\";\nimport { renderReport } from \"../renderer.js\";\nimport type { LocaleStrings } from \"../../i18n/types.js\";\nimport type { LotMethod } from \"../../types.js\";\n\nexport async function runCalc(\n positional: string[],\n flags: Record<string, string | boolean>,\n s: LocaleStrings,\n stdout: NodeJS.WritableStream,\n stderr: NodeJS.WritableStream,\n): Promise<number> {\n const filePath = positional[0];\n if (!filePath) {\n stderr.write(\n \"Usage: minus-tracker calc [--method LIFO|FIFO] [--json] <file.csv>\\n\",\n );\n return 2;\n }\n\n let csv: string;\n try {\n csv = fs.readFileSync(filePath, \"utf8\");\n } catch {\n stderr.write(`Cannot read file: ${filePath}\\n`);\n return 1;\n }\n\n const method = (flags[\"method\"] as LotMethod) ?? \"LIFO\";\n if (method !== \"LIFO\" && method !== \"FIFO\") {\n stderr.write(\"--method must be LIFO or FIFO\\n\");\n return 2;\n }\n\n const parser = new DEGIROParser();\n let transactions;\n try {\n transactions = parser.parse(csv);\n } catch (err) {\n if (err instanceof ParseError) {\n if (err.code === \"INVALID_CSV\") {\n stderr.write(s.errorInvalidCsv + \"\\n\");\n } else {\n stderr.write(s.errorMissingColumn(err.columnName!) + \"\\n\");\n }\n return 1;\n }\n throw err;\n }\n\n const calculator = new Calculator(transactions, parser.warnings);\n const report = calculator.calculateGains(method);\n\n if (flags[\"json\"]) {\n stdout.write(JSON.stringify(report, null, 2) + \"\\n\");\n } else {\n stdout.write(renderReport(report, s) + \"\\n\");\n }\n return 0;\n}\n","import { fileURLToPath } from \"node:url\";\nimport * as path from \"node:path\";\nimport * as fs from \"node:fs\";\n\nexport type RatesSnapshot = Record<string, Record<string, number>>;\n\n// Load the bundled snapshot lazily\nlet _bundled: RatesSnapshot | null = null;\n\nfunction getBundledSnapshot(): RatesSnapshot {\n if (_bundled) return _bundled;\n // Use import.meta.url-relative path to locate the bundled JSON\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n const bundledPath = path.join(__dirname, \"../data/ecb-rates.json\");\n _bundled = JSON.parse(fs.readFileSync(bundledPath, \"utf8\")) as RatesSnapshot;\n return _bundled;\n}\n\nfunction getUserSnapshot(): RatesSnapshot | null {\n const platform = process.platform;\n let configDir: string;\n if (platform === \"win32\") {\n configDir =\n process.env.APPDATA ??\n path.join(process.env.USERPROFILE ?? \"~\", \"AppData\", \"Roaming\");\n } else {\n configDir =\n process.env.XDG_CONFIG_HOME ??\n path.join(process.env.HOME ?? \"~\", \".config\");\n }\n const userPath = path.join(configDir, \"minus-tracker\", \"ecb-rates.json\");\n try {\n return JSON.parse(fs.readFileSync(userPath, \"utf8\")) as RatesSnapshot;\n } catch {\n return null;\n }\n}\n\nexport function getActiveSnapshot(): RatesSnapshot {\n const bundled = getBundledSnapshot();\n const user = getUserSnapshot();\n if (!user) return bundled;\n // Merge: user entries override bundled for same date keys\n const merged: RatesSnapshot = {};\n for (const ccy of new Set([...Object.keys(bundled), ...Object.keys(user)])) {\n merged[ccy] = { ...(bundled[ccy] ?? {}), ...(user[ccy] ?? {}) };\n }\n return merged;\n}\n\nfunction subtractDays(isoDate: string, days: number): string {\n const d = new Date(isoDate + \"T00:00:00Z\");\n d.setUTCDate(d.getUTCDate() - days);\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * Look up ECB rate for a currency on a given date.\n * Returns 1.0 for EUR. Walks back up to 3 calendar days for weekend/holiday gaps.\n * Returns null if not found within the window.\n *\n * @param currency ISO 4217 currency code\n * @param date YYYY-MM-DD trade date\n * @param snapshot Optional override (used in tests to inject stub data)\n */\nexport function lookupRate(\n currency: string,\n date: string,\n snapshot?: RatesSnapshot,\n): number | null {\n if (currency === \"EUR\") return 1.0;\n\n const s = snapshot ?? getActiveSnapshot();\n const currencyRates = s[currency];\n if (!currencyRates) return null; // unsupported currency\n\n for (let i = 0; i <= 3; i++) {\n const d = i === 0 ? date : subtractDays(date, i);\n if (currencyRates[d] !== undefined) {\n return currencyRates[d];\n }\n }\n return null;\n}\n","export type WarningEntry =\n | { code: \"MISSING_ISIN\"; row: number }\n | { code: \"UNSUPPORTED_CURRENCY\"; row: number; currency: string }\n | { code: \"NO_ECB_RATE\"; row: number; currency: string; date: string }\n | { code: \"QUANTITY_ZERO\"; row: number };\n\nexport function warningToEnglish(w: WarningEntry): string {\n switch (w.code) {\n case \"MISSING_ISIN\":\n return `Row ${w.row}: missing ISIN — skipped`;\n case \"UNSUPPORTED_CURRENCY\":\n return `Row ${w.row}: unsupported currency ${w.currency} — skipped`;\n case \"NO_ECB_RATE\":\n return `Row ${w.row}: no ECB rate for ${w.currency} on ${w.date} — skipped`;\n case \"QUANTITY_ZERO\":\n return `Row ${w.row}: quantity is 0 — skipped`;\n }\n}\n","import { Transaction } from \"../types.js\";\nimport { ParseError } from \"../errors.js\";\nimport {\n lookupRate,\n getActiveSnapshot,\n RatesSnapshot,\n} from \"../rates/index.js\";\nimport { WarningEntry, warningToEnglish } from \"./warnings.js\";\n\nconst REQUIRED_COLUMNS = [\n \"ISIN\",\n \"Quantity\",\n \"Price\",\n \"Date\",\n \"Local value\",\n \"Local value currency\",\n \"Transaction costs\",\n \"Transaction costs currency\",\n \"Product\",\n] as const;\n\n/**\n * Parse a single CSV line, respecting RFC-4180-style double-quote escaping.\n * Does not support embedded newlines in fields (not needed for DEGIRO exports).\n */\nfunction parseCSVRow(line: string): string[] {\n const fields: string[] = [];\n let i = 0;\n\n while (i <= line.length) {\n // End of line — stop (handles trailing comma by not emitting extra empty field)\n if (i === line.length) break;\n\n if (line[i] === '\"') {\n // Quoted field\n let field = \"\";\n i++; // skip opening quote\n while (i < line.length) {\n if (line[i] === '\"' && i + 1 < line.length && line[i + 1] === '\"') {\n // Escaped double-quote\n field += '\"';\n i += 2;\n } else if (line[i] === '\"') {\n i++; // skip closing quote\n break;\n } else {\n field += line[i++];\n }\n }\n fields.push(field);\n if (i < line.length && line[i] === \",\") i++; // skip delimiter\n } else {\n // Unquoted field\n const start = i;\n while (i < line.length && line[i] !== \",\") i++;\n fields.push(line.slice(start, i));\n if (i < line.length) i++; // skip delimiter\n }\n }\n\n return fields;\n}\n\n/**\n * Parse a full CSV string (CRLF or LF line endings) into a 2-D array of strings.\n */\nfunction parseCSV(input: string): string[][] {\n const normalized = input.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n return normalized.split(\"\\n\").map(parseCSVRow);\n}\n\n/**\n * Convert a DEGIRO date \"DD-MM-YYYY\" to ISO format \"YYYY-MM-DD\".\n */\nfunction parseDate(ddmmyyyy: string): string {\n const [dd, mm, yyyy] = ddmmyyyy.split(\"-\");\n return `${yyyy}-${mm}-${dd}`;\n}\n\nexport class DEGIROParser {\n private _warningEntries: WarningEntry[] = [];\n private _snapshot?: RatesSnapshot;\n\n constructor(snapshot?: RatesSnapshot) {\n this._snapshot = snapshot;\n }\n\n parse(csv: string): Transaction[] {\n this._warningEntries = [];\n\n // Binary content (null bytes) is not valid CSV\n if (typeof csv !== \"string\" || csv.includes(\"\\x00\")) {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n let rows: string[][];\n try {\n rows = parseCSV(csv);\n } catch {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n if (rows.length === 0) {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n // Build column-name → index map from the header row\n const header = rows[0];\n const colIndex: Record<string, number> = {};\n for (let i = 0; i < header.length; i++) {\n colIndex[header[i].trim()] = i;\n }\n\n // Validate that every required column is present\n for (const col of REQUIRED_COLUMNS) {\n if (colIndex[col] === undefined) {\n throw new ParseError(\"MISSING_COLUMN\", col);\n }\n }\n\n // Resolve the rates snapshot once for the whole parse run\n const snapshot: RatesSnapshot = this._snapshot ?? getActiveSnapshot();\n\n const transactions: Transaction[] = [];\n\n for (let r = 1; r < rows.length; r++) {\n const row = rows[r];\n // 1-indexed row number: header = 1, first data row = 2\n const rowIndex = r + 1;\n\n // Skip completely empty rows (including the trailing empty line after a\n // final newline, and rows that are all whitespace)\n if (row.every((cell) => cell.trim() === \"\")) continue;\n\n const get = (col: string): string => (row[colIndex[col]] ?? \"\").trim();\n\n // --- ISIN ---\n const isin = get(\"ISIN\");\n if (!isin) {\n this._warningEntries.push({ code: \"MISSING_ISIN\", row: rowIndex });\n continue;\n }\n\n // --- Quantity ---\n const rawQty = parseFloat(get(\"Quantity\"));\n if (isNaN(rawQty) || rawQty === 0) {\n this._warningEntries.push({ code: \"QUANTITY_ZERO\", row: rowIndex });\n continue;\n }\n\n const type: \"BUY\" | \"SELL\" = rawQty > 0 ? \"BUY\" : \"SELL\";\n const quantity = Math.abs(rawQty);\n\n // --- Date ---\n const isoDate = parseDate(get(\"Date\"));\n\n // --- Currency & FX ---\n const currency = get(\"Local value currency\");\n const rawTotalLocal = parseFloat(get(\"Local value\"));\n const totalLocal = isNaN(rawTotalLocal) ? 0 : rawTotalLocal;\n\n let totalEUR: number;\n let fxRate: number | undefined;\n\n if (currency === \"EUR\") {\n totalEUR = Math.abs(totalLocal);\n fxRate = undefined;\n } else {\n const rate = lookupRate(currency, isoDate, snapshot);\n if (rate === null) {\n if (snapshot[currency] === undefined) {\n this._warningEntries.push({\n code: \"UNSUPPORTED_CURRENCY\",\n row: rowIndex,\n currency,\n });\n } else {\n this._warningEntries.push({\n code: \"NO_ECB_RATE\",\n row: rowIndex,\n currency,\n date: isoDate,\n });\n }\n continue;\n }\n totalEUR = Math.abs(totalLocal) / rate;\n fxRate = rate;\n }\n\n // --- Fees ---\n const rawFees = parseFloat(get(\"Transaction costs\"));\n const feesEUR = Math.abs(isNaN(rawFees) ? 0 : rawFees);\n\n // --- Remaining fields ---\n const pricePerUnit = parseFloat(get(\"Price\"));\n const product = get(\"Product\");\n\n transactions.push({\n isin,\n product,\n date: isoDate,\n type,\n quantity,\n pricePerUnit,\n currency,\n totalLocal,\n totalEUR,\n feesEUR,\n fxRate,\n });\n }\n\n return transactions;\n }\n\n get warnings(): string[] {\n return this._warningEntries.map(warningToEnglish);\n }\n\n get warningEntries(): WarningEntry[] {\n return this._warningEntries;\n }\n}\n","import type {\n Transaction,\n MatchedLot,\n GainsReport,\n LotMethod,\n} from \"../types.js\";\nimport { CalculationError } from \"../errors.js\";\n\ninterface Lot {\n date: string;\n quantity: number;\n pricePerUnitEUR: number;\n feesEUR: number;\n originalQty: number;\n fxRate?: number;\n}\n\nfunction roundHalfUp(x: number): number {\n return (Math.sign(x) * Math.round(Math.abs(x) * 100)) / 100;\n}\n\nfunction inferTaxYear(transactions: Transaction[]): {\n year: number;\n multipleYears: boolean;\n} {\n const counts: Record<string, number> = {};\n for (const t of transactions) {\n const y = t.date.slice(0, 4);\n counts[y] = (counts[y] ?? 0) + 1;\n }\n const years = Object.keys(counts);\n if (years.length === 0)\n return { year: new Date().getFullYear(), multipleYears: false };\n const year = parseInt(\n years.reduce((a, b) => (counts[a] >= counts[b] ? a : b)),\n );\n return { year, multipleYears: years.length > 1 };\n}\n\nexport class Calculator {\n private readonly _transactions: Transaction[];\n private readonly _parseWarnings: string[];\n\n constructor(transactions: Transaction[], parseWarnings?: string[]) {\n this._transactions = transactions;\n this._parseWarnings = parseWarnings ?? [];\n }\n\n calculateGains(method: LotMethod): GainsReport {\n const warnings: string[] = [...this._parseWarnings];\n\n // 1. Sort ascending by date; BUY before SELL on same date\n const sorted = [...this._transactions].sort((a, b) => {\n if (a.date < b.date) return -1;\n if (a.date > b.date) return 1;\n // Same date: BUY before SELL\n if (a.type === \"BUY\" && b.type === \"SELL\") return -1;\n if (a.type === \"SELL\" && b.type === \"BUY\") return 1;\n return 0;\n });\n\n // 2. Tax year inference\n const { year: taxYear, multipleYears } = inferTaxYear(sorted);\n if (multipleYears) {\n warnings.push(\n \"CSV contains transactions from multiple years — filter to a single year for accurate reporting.\",\n );\n }\n\n // 3. Lot matching\n const openLots = new Map<string, Lot[]>();\n const matchedLots: MatchedLot[] = [];\n const ratesUsed: Record<string, number> = {};\n\n for (const tx of sorted) {\n // Collect rates used\n if (tx.fxRate !== undefined) {\n ratesUsed[`${tx.currency}:${tx.date}`] = tx.fxRate;\n }\n\n if (tx.type === \"BUY\") {\n const lot: Lot = {\n date: tx.date,\n quantity: tx.quantity,\n pricePerUnitEUR: tx.totalEUR / tx.quantity,\n feesEUR: tx.feesEUR,\n originalQty: tx.quantity,\n fxRate: tx.fxRate,\n };\n if (!openLots.has(tx.isin)) openLots.set(tx.isin, []);\n openLots.get(tx.isin)!.push(lot);\n } else {\n // SELL\n const lots = openLots.get(tx.isin);\n if (!lots || lots.length === 0) {\n throw new CalculationError(tx.isin, tx.date);\n }\n\n const sellPricePerUnitEUR = tx.totalEUR / tx.quantity;\n let remainingSellQty = tx.quantity;\n\n while (remainingSellQty > 0) {\n if (!lots || lots.length === 0) {\n throw new CalculationError(tx.isin, tx.date);\n }\n\n const lot = method === \"LIFO\" ? lots[lots.length - 1] : lots[0];\n const matchedQty = Math.min(lot.quantity, remainingSellQty);\n\n // Fee allocation\n const allocatedBuyFeesEUR =\n lot.feesEUR * (matchedQty / lot.originalQty);\n const allocatedSellFeesEUR = tx.feesEUR * (matchedQty / tx.quantity);\n\n const buyCostEUR =\n lot.pricePerUnitEUR * matchedQty + allocatedBuyFeesEUR;\n const sellProceedsEUR =\n sellPricePerUnitEUR * matchedQty - allocatedSellFeesEUR;\n const gainLossEUR = sellProceedsEUR - buyCostEUR;\n\n matchedLots.push({\n isin: tx.isin,\n product: tx.product,\n quantity: matchedQty,\n buyDate: lot.date,\n sellDate: tx.date,\n buyPriceEUR: lot.pricePerUnitEUR,\n sellPriceEUR: sellPricePerUnitEUR,\n buyCostEUR: roundHalfUp(buyCostEUR),\n sellProceedsEUR: roundHalfUp(sellProceedsEUR),\n gainLossEUR: roundHalfUp(gainLossEUR),\n buyFxRate: lot.fxRate,\n sellFxRate: tx.fxRate,\n });\n\n lot.quantity -= matchedQty;\n remainingSellQty -= matchedQty;\n\n if (lot.quantity <= 0) {\n if (method === \"LIFO\") {\n lots.pop();\n } else {\n lots.shift();\n }\n }\n }\n }\n }\n\n // 4. Aggregate\n let plusvalenze = 0;\n let minusvalenze = 0;\n for (const lot of matchedLots) {\n if (lot.gainLossEUR > 0) plusvalenze += lot.gainLossEUR;\n else minusvalenze += Math.abs(lot.gainLossEUR);\n }\n\n return {\n method,\n taxYear,\n plusvalenze: roundHalfUp(plusvalenze),\n minusvalenze: roundHalfUp(minusvalenze),\n netResult: roundHalfUp(plusvalenze - minusvalenze),\n lots: matchedLots,\n ratesUsed,\n warnings,\n generatedAt: new Date().toISOString(),\n };\n }\n}\n","import type { GainsReport, MatchedLot } from \"../types.js\";\nimport type { LocaleStrings } from \"../i18n/types.js\";\n\nconst SEPARATOR = \"─\".repeat(72);\n\nfunction formatEUR(amount: number, locale: string): string {\n return new Intl.NumberFormat(locale, {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n }).format(amount);\n}\n\nfunction formatGainLoss(amount: number, locale: string): string {\n const formatted = formatEUR(Math.abs(amount), locale);\n return amount >= 0 ? `+${formatted}` : `-${formatted}`;\n}\n\nexport function renderReport(report: GainsReport, s: LocaleStrings): string {\n const fmt = (n: number) => formatEUR(n, s.numberLocale);\n const lines: string[] = [];\n\n // Header\n lines.push(\n `${s.headerMethod}: ${report.method} | ${s.headerTaxYear}: ${report.taxYear}`,\n );\n lines.push(\"\");\n\n // Column headers\n const headers = [\n s.headerIsin.padEnd(14),\n s.headerProduct.padEnd(20),\n s.headerQty.padStart(5),\n s.headerBuyDate.padEnd(12),\n s.headerSellDate.padEnd(12),\n s.headerBuyEur.padStart(14),\n s.headerSellEur.padStart(13),\n s.headerGainLoss.padStart(17),\n ].join(\" \");\n lines.push(headers);\n\n // Lot rows\n for (const lot of report.lots) {\n const row = [\n lot.isin.padEnd(14),\n lot.product.substring(0, 20).padEnd(20),\n String(lot.quantity).padStart(5),\n lot.buyDate.padEnd(12),\n lot.sellDate.padEnd(12),\n fmt(lot.buyCostEUR).padStart(14),\n fmt(lot.sellProceedsEUR).padStart(13),\n formatGainLoss(lot.gainLossEUR, s.numberLocale).padStart(17),\n ].join(\" \");\n lines.push(row);\n }\n\n lines.push(\"\");\n lines.push(SEPARATOR);\n\n // Summary\n lines.push(`${s.summaryPlusvalenze}: ${fmt(report.plusvalenze)} EUR`);\n lines.push(`${s.summaryMinusvalenze}: ${fmt(report.minusvalenze)} EUR`);\n lines.push(`${s.summaryNetResult}: ${fmt(report.netResult)} EUR`);\n lines.push(\"\");\n lines.push(`${s.summaryWarnings}: ${report.warnings.length}`);\n lines.push(`${s.summaryGenerated}: ${report.generatedAt}`);\n lines.push(\"\");\n lines.push(s.disclaimer);\n\n return lines.join(\"\\n\");\n}\n","import * as fs from \"node:fs\";\nimport { DEGIROParser } from \"../../parser/index.js\";\nimport { ParseError } from \"../../errors.js\";\nimport type { LocaleStrings } from \"../../i18n/types.js\";\n\nexport async function runValidate(\n positional: string[],\n flags: Record<string, string | boolean>,\n s: LocaleStrings,\n stdout: NodeJS.WritableStream,\n stderr: NodeJS.WritableStream,\n): Promise<number> {\n const filePath = positional[0];\n if (!filePath) {\n stderr.write(\"Usage: minus-tracker validate <file.csv>\\n\");\n return 2;\n }\n\n let csv: string;\n try {\n csv = fs.readFileSync(filePath, \"utf8\");\n } catch {\n stderr.write(`Cannot read file: ${filePath}\\n`);\n return 1;\n }\n\n const parser = new DEGIROParser();\n let transactions;\n try {\n transactions = parser.parse(csv);\n } catch (err) {\n if (err instanceof ParseError) {\n if (err.code === \"INVALID_CSV\") {\n stderr.write(s.errorInvalidCsv + \"\\n\");\n } else {\n stderr.write(s.errorMissingColumn(err.columnName!) + \"\\n\");\n }\n return 1;\n }\n throw err;\n }\n\n stdout.write(s.validateOk(transactions.length, 0) + \"\\n\");\n\n for (const entry of parser.warningEntries) {\n let reason: string;\n switch (entry.code) {\n case \"MISSING_ISIN\":\n reason = s.warnMissingIsin(entry.row);\n break;\n case \"UNSUPPORTED_CURRENCY\":\n reason = s.warnUnsupportedCurrency(entry.row, entry.currency);\n break;\n case \"NO_ECB_RATE\":\n reason = s.warnNoEcbRate(entry.row, entry.currency, entry.date);\n break;\n case \"QUANTITY_ZERO\":\n reason = s.warnQuantityZero(entry.row);\n break;\n }\n stdout.write(reason + \"\\n\");\n }\n\n return 0;\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport * as https from \"node:https\";\nimport { getActiveSnapshot, type RatesSnapshot } from \"../../rates/index.js\";\nimport type { LocaleStrings } from \"../../i18n/types.js\";\n\nfunction getSnapshotPath(): string {\n const platform = process.platform;\n let configDir: string;\n if (platform === \"win32\") {\n configDir =\n process.env.APPDATA ?? path.join(os.homedir(), \"AppData\", \"Roaming\");\n } else {\n configDir =\n process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), \".config\");\n }\n return path.join(configDir, \"minus-tracker\", \"ecb-rates.json\");\n}\n\nfunction getCoverage(snapshot: RatesSnapshot): {\n start: string;\n end: string;\n currencies: string;\n} {\n let start = \"9999-12-31\";\n let end = \"0000-01-01\";\n const currencies: string[] = [];\n\n for (const [ccy, dates] of Object.entries(snapshot)) {\n currencies.push(ccy);\n for (const d of Object.keys(dates)) {\n if (d < start) start = d;\n if (d > end) end = d;\n }\n }\n return { start, end, currencies: currencies.sort().join(\", \") };\n}\n\nasync function fetchEcbData(currency: string): Promise<Record<string, number>> {\n return new Promise((resolve, reject) => {\n const url = `https://data-api.ecb.europa.eu/service/data/EXR/D.${currency}.EUR.SP00.A?format=csvdata&startPeriod=2019-01-01`;\n https\n .get(url, (res) => {\n let data = \"\";\n res.on(\"data\", (chunk) => (data += chunk));\n res.on(\"end\", () => {\n const rates: Record<string, number> = {};\n const lines = data.split(\"\\n\");\n for (const line of lines) {\n // Simple parsing: look for lines matching YYYY-MM-DD in the expected ECB CSV layout\n const dateMatch = line.match(\n /^(\\d{4}-\\d{2}-\\d{2}),.*?,.*?,.*?,.*?,([\\d.]+)/,\n );\n if (dateMatch) {\n rates[dateMatch[1]] = parseFloat(dateMatch[2]);\n }\n }\n resolve(rates);\n });\n res.on(\"error\", reject);\n })\n .on(\"error\", reject);\n });\n}\n\nexport async function runRates(\n positional: string[],\n flags: Record<string, string | boolean>,\n s: LocaleStrings,\n stdout: NodeJS.WritableStream,\n stderr: NodeJS.WritableStream,\n): Promise<number> {\n if (flags[\"check\"]) {\n const snapshot = getActiveSnapshot();\n const { start, end, currencies } = getCoverage(snapshot);\n stdout.write(s.ratesCoverage(start, end, currencies) + \"\\n\");\n stdout.write(s.ratesGapsNone + \"\\n\");\n return 0;\n }\n\n if (flags[\"update\"]) {\n stdout.write(s.ratesUpdateFetching + \"\\n\");\n const snapshotPath = getSnapshotPath();\n\n let existing: RatesSnapshot = {};\n try {\n existing = JSON.parse(\n fs.readFileSync(snapshotPath, \"utf8\"),\n ) as RatesSnapshot;\n } catch {\n /* file doesn't exist yet */\n }\n\n let addedCount = 0;\n for (const currency of [\"USD\", \"GBP\", \"CHF\"]) {\n try {\n const newRates = await fetchEcbData(currency);\n const existing_ccy = existing[currency] ?? {};\n let added = 0;\n for (const [date, rate] of Object.entries(newRates)) {\n if (!existing_ccy[date]) added++;\n existing_ccy[date] = rate;\n }\n existing[currency] = existing_ccy;\n addedCount += added;\n } catch {\n stderr.write(`Failed to fetch ${currency} rates\\n`);\n }\n }\n\n fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });\n fs.writeFileSync(\n snapshotPath,\n JSON.stringify(existing, null, 2) + \"\\n\",\n \"utf8\",\n );\n stdout.write(s.ratesUpdateDone(addedCount) + \"\\n\");\n stdout.write(s.ratesSnapshotWritten(snapshotPath) + \"\\n\");\n return 0;\n }\n\n stderr.write(\"Usage: minus-tracker rates --check | --update\\n\");\n return 2;\n}\n","import { resolveLocale, saveLocale } from \"../../i18n/settings.js\";\nimport type { LocaleStrings, SupportedLocale } from \"../../i18n/types.js\";\n\nconst SUPPORTED_LOCALES: SupportedLocale[] = [\"it\", \"en\"];\n\nexport async function runConfig(\n positional: string[],\n flags: Record<string, string | boolean>,\n s: LocaleStrings,\n stdout: NodeJS.WritableStream,\n stderr: NodeJS.WritableStream,\n): Promise<number> {\n if (flags[\"lang\"] !== undefined) {\n const lang = flags[\"lang\"] as string;\n if (!SUPPORTED_LOCALES.includes(lang as SupportedLocale)) {\n stderr.write(`Unsupported locale \"${lang}\". Use: it, en\\n`);\n return 2;\n }\n saveLocale(lang as SupportedLocale);\n stdout.write(s.configLangSet(lang as SupportedLocale) + \"\\n\");\n return 0;\n }\n\n if (flags[\"show\"]) {\n const locale = resolveLocale();\n stdout.write(s.configCurrentLang(locale) + \"\\n\");\n return 0;\n }\n\n stderr.write(\"Usage: minus-tracker config --lang <it|en> | --show\\n\");\n return 2;\n}\n"],"mappings":"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","names":["path","path","fs","path","fs","get","fs","fs","path","os","SUPPORTED_LOCALES"]}
|
|
1
|
+
{"version":3,"sources":["../../src/cli/index.ts","../../src/i18n/it.ts","../../src/i18n/en.ts","../../src/i18n/settings.ts","../../src/i18n/index.ts","../../src/errors.ts","../../src/cli/commands/calc.ts","../../src/rates/index.ts","../../src/parser/warnings.ts","../../src/parser/index.ts","../../src/calculator/index.ts","../../src/cli/renderer.ts","../../src/cli/commands/validate.ts","../../src/cli/commands/rates.ts","../../src/cli/commands/config.ts"],"sourcesContent":["import { parseArgs } from \"node:util\";\nimport { resolveLocale, getStrings } from \"../i18n/index.js\";\nimport { ParseError, CalculationError } from \"../errors.js\";\nimport { runCalc } from \"./commands/calc.js\";\nimport { runValidate } from \"./commands/validate.js\";\nimport { runRates } from \"./commands/rates.js\";\nimport { runConfig } from \"./commands/config.js\";\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n args: process.argv.slice(2),\n options: {\n lang: { type: \"string\" },\n method: { type: \"string\" },\n year: { type: \"string\" },\n json: { type: \"boolean\", default: false },\n check: { type: \"boolean\", default: false },\n update: { type: \"boolean\", default: false },\n show: { type: \"boolean\", default: false },\n },\n allowPositionals: true,\n strict: false,\n });\n\n // Resolve locale once — exits 2 on invalid value\n const locale = resolveLocale(values.lang as string | undefined);\n const s = getStrings(locale);\n\n const command = positionals[0];\n const restPositionals = positionals.slice(1);\n const flags = values as Record<string, string | boolean>;\n\n const stdout = process.stdout;\n const stderr = process.stderr;\n\n let exitCode = 0;\n\n try {\n switch (command) {\n case \"calc\":\n exitCode = await runCalc(restPositionals, flags, s, stdout, stderr);\n break;\n case \"validate\":\n exitCode = await runValidate(restPositionals, flags, s, stdout, stderr);\n break;\n case \"rates\":\n exitCode = await runRates(restPositionals, flags, s, stdout, stderr);\n break;\n case \"config\":\n exitCode = await runConfig(restPositionals, flags, s, stdout, stderr);\n break;\n default:\n stderr.write(\n \"Usage: minus-tracker <calc|validate|rates|config> [options] [file]\\n\",\n );\n exitCode = 2;\n }\n } catch (err) {\n if (err instanceof ParseError) {\n if (err.code === \"INVALID_CSV\") {\n stderr.write(s.errorInvalidCsv + \"\\n\");\n } else {\n stderr.write(s.errorMissingColumn(err.columnName!) + \"\\n\");\n }\n exitCode = 1;\n } else if (err instanceof CalculationError) {\n stderr.write(s.errorNoOpenLots(err.isin, err.date) + \"\\n\");\n exitCode = 1;\n } else {\n throw err;\n }\n }\n\n process.exit(exitCode);\n}\n\nmain().catch((err) => {\n process.stderr.write(`Unhandled error: ${String(err)}\\n`);\n process.exit(1);\n});\n","import type { LocaleStrings } from \"./types.js\";\n\nexport const it: LocaleStrings = {\n numberLocale: \"it-IT\",\n\n errorInvalidCsv: \"CSV non valido: impossibile analizzare il file\",\n errorMissingColumn: (col) => `Colonna obbligatoria mancante: ${col}`,\n errorNoOpenLots: (isin, date) =>\n `Nessun lotto aperto per ISIN ${isin} in data ${date}`,\n\n warnMissingIsin: (row) => `Riga ${row}: ISIN mancante — riga ignorata`,\n warnUnsupportedCurrency: (row, currency) =>\n `Riga ${row}: valuta non supportata ${currency} — riga ignorata`,\n warnNoEcbRate: (row, currency, date) =>\n `Riga ${row}: nessun tasso BCE per ${currency} in data ${date} — riga ignorata`,\n warnQuantityZero: (row) => `Riga ${row}: quantità pari a 0 — riga ignorata`,\n\n warnMultipleYears:\n \"Il CSV contiene transazioni di più anni — filtra per un singolo anno per un calcolo accurato.\",\n\n headerMethod: \"METODO\",\n headerTaxYear: \"ANNO FISCALE\",\n headerIsin: \"ISIN\",\n headerProduct: \"TITOLO\",\n headerQty: \"QTÀ\",\n headerBuyDate: \"DATA ACQUISTO\",\n headerSellDate: \"DATA VENDITA\",\n headerBuyEur: \"ACQUISTO EUR\",\n headerSellEur: \"VENDITA EUR\",\n headerGainLoss: \"GUADAGNO/PERDITA\",\n\n summaryPlusvalenze: \"PLUSVALENZE\",\n summaryMinusvalenze: \"MINUSVALENZE\",\n summaryNetResult: \"RISULTATO NETTO\",\n summaryWarnings: \"AVVERTENZE\",\n summaryGenerated: \"Generato\",\n\n validateOk: (count, errors) =>\n `OK: ${count} transazioni analizzate, ${errors} errori gravi`,\n validateWarn: (count, reason) =>\n `AVVISO: ${count} righe ignorate (${reason})`,\n\n ratesCoverage: (start, end, currencies) =>\n `Copertura: ${start} → ${end} | Valute: ${currencies}`,\n ratesGapsNone: \"Lacune: nessuna\",\n ratesGaps: (list) => `Lacune: ${list}`,\n ratesUpdateFetching: \"Recupero dati BCE SDMX in corso...\",\n ratesUpdateDone: (n) => `Completato. Aggiunte ${n} nuove date.`,\n ratesSnapshotWritten: (path) => `Snapshot scritto in ${path}`,\n\n configLangSet: (lang) => `Lingua impostata su: ${lang}`,\n configCurrentLang: (lang) => `Lingua corrente: ${lang}`,\n\n disclaimer: \"minus-tracker è un ausilio al calcolo, non consulenza fiscale.\",\n};\n","import type { LocaleStrings } from \"./types.js\";\n\nexport const en: LocaleStrings = {\n numberLocale: \"en-US\",\n\n errorInvalidCsv: \"Invalid CSV: unable to parse\",\n errorMissingColumn: (col) => `Missing required column: ${col}`,\n errorNoOpenLots: (isin, date) => `No open lots for ISIN ${isin} on ${date}`,\n\n warnMissingIsin: (row) => `Row ${row}: missing ISIN — skipped`,\n warnUnsupportedCurrency: (row, currency) =>\n `Row ${row}: unsupported currency ${currency} — skipped`,\n warnNoEcbRate: (row, currency, date) =>\n `Row ${row}: no ECB rate for ${currency} on ${date} — skipped`,\n warnQuantityZero: (row) => `Row ${row}: quantity is 0 — skipped`,\n\n warnMultipleYears:\n \"CSV contains transactions from multiple years — filter to a single year for accurate reporting.\",\n\n headerMethod: \"METHOD\",\n headerTaxYear: \"TAX YEAR\",\n headerIsin: \"ISIN\",\n headerProduct: \"PRODUCT\",\n headerQty: \"QTY\",\n headerBuyDate: \"BUY DATE\",\n headerSellDate: \"SELL DATE\",\n headerBuyEur: \"BUY EUR\",\n headerSellEur: \"SELL EUR\",\n headerGainLoss: \"GAIN/LOSS\",\n\n summaryPlusvalenze: \"PLUSVALENZE\",\n summaryMinusvalenze: \"MINUSVALENZE\",\n summaryNetResult: \"NET RESULT\",\n summaryWarnings: \"WARNINGS\",\n summaryGenerated: \"Generated\",\n\n validateOk: (count, errors) =>\n `OK: ${count} transactions parsed, ${errors} hard errors`,\n validateWarn: (count, reason) => `WARN: ${count} rows skipped (${reason})`,\n\n ratesCoverage: (start, end, currencies) =>\n `Coverage: ${start} → ${end} | Currencies: ${currencies}`,\n ratesGapsNone: \"Gaps: none\",\n ratesGaps: (list) => `Gaps: ${list}`,\n ratesUpdateFetching: \"Fetching ECB SDMX...\",\n ratesUpdateDone: (n) => `Done. Added ${n} new dates.`,\n ratesSnapshotWritten: (path) => `Snapshot written to ${path}`,\n\n configLangSet: (lang) => `Language set to: ${lang}`,\n configCurrentLang: (lang) => `Current language: ${lang}`,\n\n disclaimer: \"minus-tracker è un ausilio al calcolo, non consulenza fiscale.\",\n};\n","import * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { MinusTrackerConfig, SupportedLocale } from \"./types.js\";\n\nconst SUPPORTED_LOCALES: SupportedLocale[] = [\"it\", \"en\"];\n\nexport function getConfigPath(): string {\n if (process.platform === \"win32\") {\n const appData =\n process.env.APPDATA ?? path.join(os.homedir(), \"AppData\", \"Roaming\");\n return path.join(appData, \"minus-tracker\", \"config.json\");\n }\n const xdg = process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), \".config\");\n return path.join(xdg, \"minus-tracker\", \"config.json\");\n}\n\nexport function readConfig(): MinusTrackerConfig {\n try {\n const raw = fs.readFileSync(getConfigPath(), \"utf8\");\n return JSON.parse(raw) as MinusTrackerConfig;\n } catch {\n return {};\n }\n}\n\nexport function saveLocale(lang: SupportedLocale): void {\n const configPath = getConfigPath();\n fs.mkdirSync(path.dirname(configPath), { recursive: true });\n const existing = readConfig();\n const updated: MinusTrackerConfig = { ...existing, locale: lang };\n fs.writeFileSync(configPath, JSON.stringify(updated, null, 2) + \"\\n\", \"utf8\");\n}\n\nexport function resolveLocale(cliLang?: string): SupportedLocale {\n // 1. --lang flag\n if (cliLang !== undefined) {\n if (!SUPPORTED_LOCALES.includes(cliLang as SupportedLocale)) {\n process.stderr.write(`Unsupported locale \"${cliLang}\". Use: it, en\\n`);\n process.exit(2);\n }\n return cliLang as SupportedLocale;\n }\n // 2. env var\n const envLang = process.env.MINUS_TRACKER_LANG;\n if (envLang !== undefined) {\n if (!SUPPORTED_LOCALES.includes(envLang as SupportedLocale)) {\n process.stderr.write(`Unsupported locale \"${envLang}\". Use: it, en\\n`);\n process.exit(2);\n }\n return envLang as SupportedLocale;\n }\n // 3. config file\n const config = readConfig();\n if (config.locale && SUPPORTED_LOCALES.includes(config.locale)) {\n return config.locale;\n }\n // 4. default\n return \"it\";\n}\n","import { it } from \"./it.js\";\nimport { en } from \"./en.js\";\nimport type { LocaleStrings, SupportedLocale } from \"./types.js\";\n\nexport { it, en };\nexport type { LocaleStrings, SupportedLocale };\nexport type { MinusTrackerConfig } from \"./types.js\";\nexport {\n resolveLocale,\n saveLocale,\n readConfig,\n getConfigPath,\n} from \"./settings.js\";\n\nexport function getStrings(locale: SupportedLocale): LocaleStrings {\n return locale === \"it\" ? it : en;\n}\n","export class ParseError extends Error {\n readonly code: \"INVALID_CSV\" | \"MISSING_COLUMN\";\n readonly columnName?: string;\n\n constructor(code: \"INVALID_CSV\");\n constructor(code: \"MISSING_COLUMN\", columnName: string);\n constructor(code: ParseError[\"code\"], columnName?: string) {\n const msg =\n code === \"INVALID_CSV\"\n ? \"Invalid CSV: unable to parse\"\n : `Missing required column: ${columnName}`;\n super(msg);\n this.name = \"ParseError\";\n this.code = code;\n this.columnName = columnName;\n }\n}\n\nexport class CalculationError extends Error {\n readonly isin: string;\n readonly date: string;\n\n constructor(isin: string, date: string) {\n super(`No open lots for ISIN ${isin} on ${date}`);\n this.name = \"CalculationError\";\n this.isin = isin;\n this.date = date;\n }\n}\n","import * as fs from \"node:fs\";\nimport { DEGIROParser } from \"../../parser/index.js\";\nimport { Calculator } from \"../../calculator/index.js\";\nimport { ParseError } from \"../../errors.js\";\nimport { renderReport } from \"../renderer.js\";\nimport type { LocaleStrings } from \"../../i18n/types.js\";\nimport type { LotMethod } from \"../../types.js\";\n\nexport async function runCalc(\n positional: string[],\n flags: Record<string, string | boolean>,\n s: LocaleStrings,\n stdout: NodeJS.WritableStream,\n stderr: NodeJS.WritableStream,\n): Promise<number> {\n const filePath = positional[0];\n if (!filePath) {\n stderr.write(\n \"Usage: minus-tracker calc [--method LIFO|FIFO] [--json] <file.csv>\\n\",\n );\n return 2;\n }\n\n let csv: string;\n try {\n csv = fs.readFileSync(filePath, \"utf8\");\n } catch {\n stderr.write(`Cannot read file: ${filePath}\\n`);\n return 1;\n }\n\n const method = (flags[\"method\"] as LotMethod) ?? \"LIFO\";\n if (method !== \"LIFO\" && method !== \"FIFO\") {\n stderr.write(\"--method must be LIFO or FIFO\\n\");\n return 2;\n }\n\n const parser = new DEGIROParser();\n let transactions;\n try {\n transactions = parser.parse(csv);\n } catch (err) {\n if (err instanceof ParseError) {\n if (err.code === \"INVALID_CSV\") {\n stderr.write(s.errorInvalidCsv + \"\\n\");\n } else {\n stderr.write(s.errorMissingColumn(err.columnName!) + \"\\n\");\n }\n return 1;\n }\n throw err;\n }\n\n const calculator = new Calculator(transactions, parser.warnings);\n const report = calculator.calculateGains(method);\n\n if (flags[\"json\"]) {\n stdout.write(JSON.stringify(report, null, 2) + \"\\n\");\n } else {\n stdout.write(renderReport(report, s) + \"\\n\");\n }\n return 0;\n}\n","import { fileURLToPath } from \"node:url\";\nimport * as path from \"node:path\";\nimport * as fs from \"node:fs\";\n\nexport type RatesSnapshot = Record<string, Record<string, number>>;\n\n// Load the bundled snapshot lazily; returns null if the file is absent (e.g. broken install)\nlet _bundled: RatesSnapshot | null | undefined = undefined;\n\nfunction getBundledSnapshot(): RatesSnapshot | null {\n if (_bundled !== undefined) return _bundled;\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n const bundledPath = path.join(__dirname, \"../data/ecb-rates.json\");\n try {\n _bundled = JSON.parse(\n fs.readFileSync(bundledPath, \"utf8\"),\n ) as RatesSnapshot;\n } catch {\n _bundled = null;\n }\n return _bundled;\n}\n\nfunction getUserSnapshot(): RatesSnapshot | null {\n const platform = process.platform;\n let configDir: string;\n if (platform === \"win32\") {\n configDir =\n process.env.APPDATA ??\n path.join(process.env.USERPROFILE ?? \"~\", \"AppData\", \"Roaming\");\n } else {\n configDir =\n process.env.XDG_CONFIG_HOME ??\n path.join(process.env.HOME ?? \"~\", \".config\");\n }\n const userPath = path.join(configDir, \"minus-tracker\", \"ecb-rates.json\");\n try {\n return JSON.parse(fs.readFileSync(userPath, \"utf8\")) as RatesSnapshot;\n } catch {\n return null;\n }\n}\n\nexport function getActiveSnapshot(): RatesSnapshot {\n const bundled = getBundledSnapshot();\n const user = getUserSnapshot();\n if (!bundled && !user) {\n throw new Error(\n \"No ECB rates snapshot available. Run `minus-tracker rates --update` to fetch rates.\",\n );\n }\n if (!bundled) return user!;\n if (!user) return bundled;\n // Merge: user entries override bundled for same date keys\n const merged: RatesSnapshot = {};\n for (const ccy of new Set([...Object.keys(bundled), ...Object.keys(user)])) {\n merged[ccy] = { ...(bundled[ccy] ?? {}), ...(user[ccy] ?? {}) };\n }\n return merged;\n}\n\nfunction subtractDays(isoDate: string, days: number): string {\n const d = new Date(isoDate + \"T00:00:00Z\");\n d.setUTCDate(d.getUTCDate() - days);\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * Look up ECB rate for a currency on a given date.\n * Returns 1.0 for EUR. Walks back up to 3 calendar days for weekend/holiday gaps.\n * Returns null if not found within the window.\n *\n * @param currency ISO 4217 currency code\n * @param date YYYY-MM-DD trade date\n * @param snapshot Optional override (used in tests to inject stub data)\n */\nexport function lookupRate(\n currency: string,\n date: string,\n snapshot?: RatesSnapshot,\n): number | null {\n if (currency === \"EUR\") return 1.0;\n\n const s = snapshot ?? getActiveSnapshot();\n const currencyRates = s[currency];\n if (!currencyRates) return null; // unsupported currency\n\n for (let i = 0; i <= 3; i++) {\n const d = i === 0 ? date : subtractDays(date, i);\n if (currencyRates[d] !== undefined) {\n return currencyRates[d];\n }\n }\n return null;\n}\n","export type WarningEntry =\n | { code: \"MISSING_ISIN\"; row: number }\n | { code: \"UNSUPPORTED_CURRENCY\"; row: number; currency: string }\n | { code: \"NO_ECB_RATE\"; row: number; currency: string; date: string }\n | { code: \"QUANTITY_ZERO\"; row: number };\n\nexport function warningToEnglish(w: WarningEntry): string {\n switch (w.code) {\n case \"MISSING_ISIN\":\n return `Row ${w.row}: missing ISIN — skipped`;\n case \"UNSUPPORTED_CURRENCY\":\n return `Row ${w.row}: unsupported currency ${w.currency} — skipped`;\n case \"NO_ECB_RATE\":\n return `Row ${w.row}: no ECB rate for ${w.currency} on ${w.date} — skipped`;\n case \"QUANTITY_ZERO\":\n return `Row ${w.row}: quantity is 0 — skipped`;\n }\n}\n","import { Transaction } from \"../types.js\";\nimport { ParseError } from \"../errors.js\";\nimport {\n lookupRate,\n getActiveSnapshot,\n RatesSnapshot,\n} from \"../rates/index.js\";\nimport { WarningEntry, warningToEnglish } from \"./warnings.js\";\n\nconst REQUIRED_COLUMNS = [\n \"ISIN\",\n \"Quantity\",\n \"Price\",\n \"Date\",\n \"Local value\",\n \"Local value currency\",\n \"Transaction costs\",\n \"Transaction costs currency\",\n \"Product\",\n] as const;\n\n/**\n * Parse a single CSV line, respecting RFC-4180-style double-quote escaping.\n * Does not support embedded newlines in fields (not needed for DEGIRO exports).\n */\nfunction parseCSVRow(line: string): string[] {\n const fields: string[] = [];\n let i = 0;\n\n while (i <= line.length) {\n // End of line — stop (handles trailing comma by not emitting extra empty field)\n if (i === line.length) break;\n\n if (line[i] === '\"') {\n // Quoted field\n let field = \"\";\n i++; // skip opening quote\n while (i < line.length) {\n if (line[i] === '\"' && i + 1 < line.length && line[i + 1] === '\"') {\n // Escaped double-quote\n field += '\"';\n i += 2;\n } else if (line[i] === '\"') {\n i++; // skip closing quote\n break;\n } else {\n field += line[i++];\n }\n }\n fields.push(field);\n if (i < line.length && line[i] === \",\") i++; // skip delimiter\n } else {\n // Unquoted field\n const start = i;\n while (i < line.length && line[i] !== \",\") i++;\n fields.push(line.slice(start, i));\n if (i < line.length) i++; // skip delimiter\n }\n }\n\n return fields;\n}\n\n/**\n * Parse a full CSV string (CRLF or LF line endings) into a 2-D array of strings.\n */\nfunction parseCSV(input: string): string[][] {\n const normalized = input.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n return normalized.split(\"\\n\").map(parseCSVRow);\n}\n\n/**\n * Convert a DEGIRO date \"DD-MM-YYYY\" to ISO format \"YYYY-MM-DD\".\n */\nfunction parseDate(ddmmyyyy: string): string {\n const [dd, mm, yyyy] = ddmmyyyy.split(\"-\");\n return `${yyyy}-${mm}-${dd}`;\n}\n\nexport class DEGIROParser {\n private _warningEntries: WarningEntry[] = [];\n private _snapshot?: RatesSnapshot;\n\n constructor(snapshot?: RatesSnapshot) {\n this._snapshot = snapshot;\n }\n\n parse(csv: string): Transaction[] {\n this._warningEntries = [];\n\n // Binary content (null bytes) is not valid CSV\n if (typeof csv !== \"string\" || csv.includes(\"\\x00\")) {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n let rows: string[][];\n try {\n rows = parseCSV(csv);\n } catch {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n if (rows.length === 0) {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n // Build column-name → index map from the header row\n const header = rows[0];\n const colIndex: Record<string, number> = {};\n for (let i = 0; i < header.length; i++) {\n colIndex[header[i].trim()] = i;\n }\n\n // Validate that every required column is present\n for (const col of REQUIRED_COLUMNS) {\n if (colIndex[col] === undefined) {\n throw new ParseError(\"MISSING_COLUMN\", col);\n }\n }\n\n // Resolve the rates snapshot once for the whole parse run\n const snapshot: RatesSnapshot = this._snapshot ?? getActiveSnapshot();\n\n const transactions: Transaction[] = [];\n\n for (let r = 1; r < rows.length; r++) {\n const row = rows[r];\n // 1-indexed row number: header = 1, first data row = 2\n const rowIndex = r + 1;\n\n // Skip completely empty rows (including the trailing empty line after a\n // final newline, and rows that are all whitespace)\n if (row.every((cell) => cell.trim() === \"\")) continue;\n\n const get = (col: string): string => (row[colIndex[col]] ?? \"\").trim();\n\n // --- ISIN ---\n const isin = get(\"ISIN\");\n if (!isin) {\n this._warningEntries.push({ code: \"MISSING_ISIN\", row: rowIndex });\n continue;\n }\n\n // --- Quantity ---\n const rawQty = parseFloat(get(\"Quantity\"));\n if (isNaN(rawQty) || rawQty === 0) {\n this._warningEntries.push({ code: \"QUANTITY_ZERO\", row: rowIndex });\n continue;\n }\n\n const type: \"BUY\" | \"SELL\" = rawQty > 0 ? \"BUY\" : \"SELL\";\n const quantity = Math.abs(rawQty);\n\n // --- Date ---\n const isoDate = parseDate(get(\"Date\"));\n\n // --- Currency & FX ---\n const currency = get(\"Local value currency\");\n const rawTotalLocal = parseFloat(get(\"Local value\"));\n const totalLocal = isNaN(rawTotalLocal) ? 0 : rawTotalLocal;\n\n let totalEUR: number;\n let fxRate: number | undefined;\n\n if (currency === \"EUR\") {\n totalEUR = Math.abs(totalLocal);\n fxRate = undefined;\n } else {\n const rate = lookupRate(currency, isoDate, snapshot);\n if (rate === null) {\n if (snapshot[currency] === undefined) {\n this._warningEntries.push({\n code: \"UNSUPPORTED_CURRENCY\",\n row: rowIndex,\n currency,\n });\n } else {\n this._warningEntries.push({\n code: \"NO_ECB_RATE\",\n row: rowIndex,\n currency,\n date: isoDate,\n });\n }\n continue;\n }\n totalEUR = Math.abs(totalLocal) / rate;\n fxRate = rate;\n }\n\n // --- Fees ---\n const rawFees = parseFloat(get(\"Transaction costs\"));\n const feesEUR = Math.abs(isNaN(rawFees) ? 0 : rawFees);\n\n // --- Remaining fields ---\n const pricePerUnit = parseFloat(get(\"Price\"));\n const product = get(\"Product\");\n\n transactions.push({\n isin,\n product,\n date: isoDate,\n type,\n quantity,\n pricePerUnit,\n currency,\n totalLocal,\n totalEUR,\n feesEUR,\n fxRate,\n });\n }\n\n return transactions;\n }\n\n get warnings(): string[] {\n return this._warningEntries.map(warningToEnglish);\n }\n\n get warningEntries(): WarningEntry[] {\n return this._warningEntries;\n }\n}\n","import type {\n Transaction,\n MatchedLot,\n GainsReport,\n LotMethod,\n} from \"../types.js\";\nimport { CalculationError } from \"../errors.js\";\n\ninterface Lot {\n date: string;\n quantity: number;\n pricePerUnitEUR: number;\n feesEUR: number;\n originalQty: number;\n fxRate?: number;\n}\n\nfunction roundHalfUp(x: number): number {\n return (Math.sign(x) * Math.round(Math.abs(x) * 100)) / 100;\n}\n\nfunction inferTaxYear(transactions: Transaction[]): {\n year: number;\n multipleYears: boolean;\n} {\n const counts: Record<string, number> = {};\n for (const t of transactions) {\n const y = t.date.slice(0, 4);\n counts[y] = (counts[y] ?? 0) + 1;\n }\n const years = Object.keys(counts);\n if (years.length === 0)\n return { year: new Date().getFullYear(), multipleYears: false };\n const year = parseInt(\n years.reduce((a, b) => (counts[a] >= counts[b] ? a : b)),\n );\n return { year, multipleYears: years.length > 1 };\n}\n\nexport class Calculator {\n private readonly _transactions: Transaction[];\n private readonly _parseWarnings: string[];\n\n constructor(transactions: Transaction[], parseWarnings?: string[]) {\n this._transactions = transactions;\n this._parseWarnings = parseWarnings ?? [];\n }\n\n calculateGains(method: LotMethod): GainsReport {\n const warnings: string[] = [...this._parseWarnings];\n\n // 1. Sort ascending by date; BUY before SELL on same date\n const sorted = [...this._transactions].sort((a, b) => {\n if (a.date < b.date) return -1;\n if (a.date > b.date) return 1;\n // Same date: BUY before SELL\n if (a.type === \"BUY\" && b.type === \"SELL\") return -1;\n if (a.type === \"SELL\" && b.type === \"BUY\") return 1;\n return 0;\n });\n\n // 2. Tax year inference\n const { year: taxYear, multipleYears } = inferTaxYear(sorted);\n if (multipleYears) {\n warnings.push(\n \"CSV contains transactions from multiple years — filter to a single year for accurate reporting.\",\n );\n }\n\n // 3. Lot matching\n const openLots = new Map<string, Lot[]>();\n const matchedLots: MatchedLot[] = [];\n const ratesUsed: Record<string, number> = {};\n\n for (const tx of sorted) {\n // Collect rates used\n if (tx.fxRate !== undefined) {\n ratesUsed[`${tx.currency}:${tx.date}`] = tx.fxRate;\n }\n\n if (tx.type === \"BUY\") {\n const lot: Lot = {\n date: tx.date,\n quantity: tx.quantity,\n pricePerUnitEUR: tx.totalEUR / tx.quantity,\n feesEUR: tx.feesEUR,\n originalQty: tx.quantity,\n fxRate: tx.fxRate,\n };\n if (!openLots.has(tx.isin)) openLots.set(tx.isin, []);\n openLots.get(tx.isin)!.push(lot);\n } else {\n // SELL\n const lots = openLots.get(tx.isin);\n if (!lots || lots.length === 0) {\n throw new CalculationError(tx.isin, tx.date);\n }\n\n const sellPricePerUnitEUR = tx.totalEUR / tx.quantity;\n let remainingSellQty = tx.quantity;\n\n while (remainingSellQty > 0) {\n if (!lots || lots.length === 0) {\n throw new CalculationError(tx.isin, tx.date);\n }\n\n const lot = method === \"LIFO\" ? lots[lots.length - 1] : lots[0];\n const matchedQty = Math.min(lot.quantity, remainingSellQty);\n\n // Fee allocation\n const allocatedBuyFeesEUR =\n lot.feesEUR * (matchedQty / lot.originalQty);\n const allocatedSellFeesEUR = tx.feesEUR * (matchedQty / tx.quantity);\n\n const buyCostEUR =\n lot.pricePerUnitEUR * matchedQty + allocatedBuyFeesEUR;\n const sellProceedsEUR =\n sellPricePerUnitEUR * matchedQty - allocatedSellFeesEUR;\n const gainLossEUR = sellProceedsEUR - buyCostEUR;\n\n matchedLots.push({\n isin: tx.isin,\n product: tx.product,\n quantity: matchedQty,\n buyDate: lot.date,\n sellDate: tx.date,\n buyPriceEUR: lot.pricePerUnitEUR,\n sellPriceEUR: sellPricePerUnitEUR,\n buyCostEUR: roundHalfUp(buyCostEUR),\n sellProceedsEUR: roundHalfUp(sellProceedsEUR),\n gainLossEUR: roundHalfUp(gainLossEUR),\n buyFxRate: lot.fxRate,\n sellFxRate: tx.fxRate,\n });\n\n lot.quantity -= matchedQty;\n remainingSellQty -= matchedQty;\n\n if (lot.quantity <= 0) {\n if (method === \"LIFO\") {\n lots.pop();\n } else {\n lots.shift();\n }\n }\n }\n }\n }\n\n // 4. Aggregate\n let plusvalenze = 0;\n let minusvalenze = 0;\n for (const lot of matchedLots) {\n if (lot.gainLossEUR > 0) plusvalenze += lot.gainLossEUR;\n else minusvalenze += Math.abs(lot.gainLossEUR);\n }\n\n return {\n method,\n taxYear,\n plusvalenze: roundHalfUp(plusvalenze),\n minusvalenze: roundHalfUp(minusvalenze),\n netResult: roundHalfUp(plusvalenze - minusvalenze),\n lots: matchedLots,\n ratesUsed,\n warnings,\n generatedAt: new Date().toISOString(),\n };\n }\n}\n","import type { GainsReport, MatchedLot } from \"../types.js\";\nimport type { LocaleStrings } from \"../i18n/types.js\";\n\nconst SEPARATOR = \"─\".repeat(72);\n\nfunction formatEUR(amount: number, locale: string): string {\n return new Intl.NumberFormat(locale, {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n }).format(amount);\n}\n\nfunction formatGainLoss(amount: number, locale: string): string {\n const formatted = formatEUR(Math.abs(amount), locale);\n return amount >= 0 ? `+${formatted}` : `-${formatted}`;\n}\n\nexport function renderReport(report: GainsReport, s: LocaleStrings): string {\n const fmt = (n: number) => formatEUR(n, s.numberLocale);\n const lines: string[] = [];\n\n // Header\n lines.push(\n `${s.headerMethod}: ${report.method} | ${s.headerTaxYear}: ${report.taxYear}`,\n );\n lines.push(\"\");\n\n // Column headers\n const headers = [\n s.headerIsin.padEnd(14),\n s.headerProduct.padEnd(20),\n s.headerQty.padStart(5),\n s.headerBuyDate.padEnd(12),\n s.headerSellDate.padEnd(12),\n s.headerBuyEur.padStart(14),\n s.headerSellEur.padStart(13),\n s.headerGainLoss.padStart(17),\n ].join(\" \");\n lines.push(headers);\n\n // Lot rows\n for (const lot of report.lots) {\n const row = [\n lot.isin.padEnd(14),\n lot.product.substring(0, 20).padEnd(20),\n String(lot.quantity).padStart(5),\n lot.buyDate.padEnd(12),\n lot.sellDate.padEnd(12),\n fmt(lot.buyCostEUR).padStart(14),\n fmt(lot.sellProceedsEUR).padStart(13),\n formatGainLoss(lot.gainLossEUR, s.numberLocale).padStart(17),\n ].join(\" \");\n lines.push(row);\n }\n\n lines.push(\"\");\n lines.push(SEPARATOR);\n\n // Summary\n lines.push(`${s.summaryPlusvalenze}: ${fmt(report.plusvalenze)} EUR`);\n lines.push(`${s.summaryMinusvalenze}: ${fmt(report.minusvalenze)} EUR`);\n lines.push(`${s.summaryNetResult}: ${fmt(report.netResult)} EUR`);\n lines.push(\"\");\n lines.push(`${s.summaryWarnings}: ${report.warnings.length}`);\n lines.push(`${s.summaryGenerated}: ${report.generatedAt}`);\n lines.push(\"\");\n lines.push(s.disclaimer);\n\n return lines.join(\"\\n\");\n}\n","import * as fs from \"node:fs\";\nimport { DEGIROParser } from \"../../parser/index.js\";\nimport { ParseError } from \"../../errors.js\";\nimport type { LocaleStrings } from \"../../i18n/types.js\";\n\nexport async function runValidate(\n positional: string[],\n flags: Record<string, string | boolean>,\n s: LocaleStrings,\n stdout: NodeJS.WritableStream,\n stderr: NodeJS.WritableStream,\n): Promise<number> {\n const filePath = positional[0];\n if (!filePath) {\n stderr.write(\"Usage: minus-tracker validate <file.csv>\\n\");\n return 2;\n }\n\n let csv: string;\n try {\n csv = fs.readFileSync(filePath, \"utf8\");\n } catch {\n stderr.write(`Cannot read file: ${filePath}\\n`);\n return 1;\n }\n\n const parser = new DEGIROParser();\n let transactions;\n try {\n transactions = parser.parse(csv);\n } catch (err) {\n if (err instanceof ParseError) {\n if (err.code === \"INVALID_CSV\") {\n stderr.write(s.errorInvalidCsv + \"\\n\");\n } else {\n stderr.write(s.errorMissingColumn(err.columnName!) + \"\\n\");\n }\n return 1;\n }\n throw err;\n }\n\n stdout.write(s.validateOk(transactions.length, 0) + \"\\n\");\n\n for (const entry of parser.warningEntries) {\n let reason: string;\n switch (entry.code) {\n case \"MISSING_ISIN\":\n reason = s.warnMissingIsin(entry.row);\n break;\n case \"UNSUPPORTED_CURRENCY\":\n reason = s.warnUnsupportedCurrency(entry.row, entry.currency);\n break;\n case \"NO_ECB_RATE\":\n reason = s.warnNoEcbRate(entry.row, entry.currency, entry.date);\n break;\n case \"QUANTITY_ZERO\":\n reason = s.warnQuantityZero(entry.row);\n break;\n }\n stdout.write(reason + \"\\n\");\n }\n\n return 0;\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport * as https from \"node:https\";\nimport { getActiveSnapshot, type RatesSnapshot } from \"../../rates/index.js\";\nimport type { LocaleStrings } from \"../../i18n/types.js\";\n\nfunction getSnapshotPath(): string {\n const platform = process.platform;\n let configDir: string;\n if (platform === \"win32\") {\n configDir =\n process.env.APPDATA ?? path.join(os.homedir(), \"AppData\", \"Roaming\");\n } else {\n configDir =\n process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), \".config\");\n }\n return path.join(configDir, \"minus-tracker\", \"ecb-rates.json\");\n}\n\nfunction getCoverage(snapshot: RatesSnapshot): {\n start: string;\n end: string;\n currencies: string;\n} {\n let start = \"9999-12-31\";\n let end = \"0000-01-01\";\n const currencies: string[] = [];\n\n for (const [ccy, dates] of Object.entries(snapshot)) {\n currencies.push(ccy);\n for (const d of Object.keys(dates)) {\n if (d < start) start = d;\n if (d > end) end = d;\n }\n }\n return { start, end, currencies: currencies.sort().join(\", \") };\n}\n\nasync function fetchEcbData(currency: string): Promise<Record<string, number>> {\n return new Promise((resolve, reject) => {\n const url = `https://data-api.ecb.europa.eu/service/data/EXR/D.${currency}.EUR.SP00.A?format=csvdata&startPeriod=2019-01-01`;\n https\n .get(url, (res) => {\n let data = \"\";\n res.on(\"data\", (chunk) => (data += chunk));\n res.on(\"end\", () => {\n const rates: Record<string, number> = {};\n const lines = data.split(\"\\n\");\n for (const line of lines) {\n // Simple parsing: look for lines matching YYYY-MM-DD in the expected ECB CSV layout\n const dateMatch = line.match(\n /^(\\d{4}-\\d{2}-\\d{2}),.*?,.*?,.*?,.*?,([\\d.]+)/,\n );\n if (dateMatch) {\n rates[dateMatch[1]] = parseFloat(dateMatch[2]);\n }\n }\n resolve(rates);\n });\n res.on(\"error\", reject);\n })\n .on(\"error\", reject);\n });\n}\n\nexport async function runRates(\n positional: string[],\n flags: Record<string, string | boolean>,\n s: LocaleStrings,\n stdout: NodeJS.WritableStream,\n stderr: NodeJS.WritableStream,\n): Promise<number> {\n if (flags[\"check\"]) {\n const snapshot = getActiveSnapshot();\n const { start, end, currencies } = getCoverage(snapshot);\n stdout.write(s.ratesCoverage(start, end, currencies) + \"\\n\");\n stdout.write(s.ratesGapsNone + \"\\n\");\n return 0;\n }\n\n if (flags[\"update\"]) {\n stdout.write(s.ratesUpdateFetching + \"\\n\");\n const snapshotPath = getSnapshotPath();\n\n let existing: RatesSnapshot = {};\n try {\n existing = JSON.parse(\n fs.readFileSync(snapshotPath, \"utf8\"),\n ) as RatesSnapshot;\n } catch {\n /* file doesn't exist yet */\n }\n\n let addedCount = 0;\n for (const currency of [\"USD\", \"GBP\", \"CHF\"]) {\n try {\n const newRates = await fetchEcbData(currency);\n const existing_ccy = existing[currency] ?? {};\n let added = 0;\n for (const [date, rate] of Object.entries(newRates)) {\n if (!existing_ccy[date]) added++;\n existing_ccy[date] = rate;\n }\n existing[currency] = existing_ccy;\n addedCount += added;\n } catch {\n stderr.write(`Failed to fetch ${currency} rates\\n`);\n }\n }\n\n fs.mkdirSync(path.dirname(snapshotPath), { recursive: true });\n fs.writeFileSync(\n snapshotPath,\n JSON.stringify(existing, null, 2) + \"\\n\",\n \"utf8\",\n );\n stdout.write(s.ratesUpdateDone(addedCount) + \"\\n\");\n stdout.write(s.ratesSnapshotWritten(snapshotPath) + \"\\n\");\n return 0;\n }\n\n stderr.write(\"Usage: minus-tracker rates --check | --update\\n\");\n return 2;\n}\n","import { resolveLocale, saveLocale } from \"../../i18n/settings.js\";\nimport type { LocaleStrings, SupportedLocale } from \"../../i18n/types.js\";\n\nconst SUPPORTED_LOCALES: SupportedLocale[] = [\"it\", \"en\"];\n\nexport async function runConfig(\n positional: string[],\n flags: Record<string, string | boolean>,\n s: LocaleStrings,\n stdout: NodeJS.WritableStream,\n stderr: NodeJS.WritableStream,\n): Promise<number> {\n if (flags[\"lang\"] !== undefined) {\n const lang = flags[\"lang\"] as string;\n if (!SUPPORTED_LOCALES.includes(lang as SupportedLocale)) {\n stderr.write(`Unsupported locale \"${lang}\". Use: it, en\\n`);\n return 2;\n }\n saveLocale(lang as SupportedLocale);\n stdout.write(s.configLangSet(lang as SupportedLocale) + \"\\n\");\n return 0;\n }\n\n if (flags[\"show\"]) {\n const locale = resolveLocale();\n stdout.write(s.configCurrentLang(locale) + \"\\n\");\n return 0;\n }\n\n stderr.write(\"Usage: minus-tracker config --lang <it|en> | --show\\n\");\n return 2;\n}\n"],"mappings":"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","names":["path","path","fs","path","fs","get","fs","fs","path","os","SUPPORTED_LOCALES"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"USD": {
|
|
3
|
+
"2024-01-02": 1.094,
|
|
4
|
+
"2024-01-03": 1.091,
|
|
5
|
+
"2024-01-04": 1.096,
|
|
6
|
+
"2024-01-05": 1.0941,
|
|
7
|
+
"2024-01-08": 1.098,
|
|
8
|
+
"2024-01-09": 1.095,
|
|
9
|
+
"2024-01-10": 1.098,
|
|
10
|
+
"2024-06-03": 1.085,
|
|
11
|
+
"2024-06-04": 1.087,
|
|
12
|
+
"2024-06-05": 1.089
|
|
13
|
+
},
|
|
14
|
+
"GBP": {
|
|
15
|
+
"2024-01-02": 0.866,
|
|
16
|
+
"2024-01-03": 0.865,
|
|
17
|
+
"2024-01-04": 0.864,
|
|
18
|
+
"2024-01-05": 0.863,
|
|
19
|
+
"2024-01-08": 0.862,
|
|
20
|
+
"2024-06-03": 0.852,
|
|
21
|
+
"2024-06-04": 0.851,
|
|
22
|
+
"2024-06-05": 0.85
|
|
23
|
+
},
|
|
24
|
+
"CHF": {
|
|
25
|
+
"2024-01-02": 0.931,
|
|
26
|
+
"2024-01-03": 0.93,
|
|
27
|
+
"2024-01-04": 0.929,
|
|
28
|
+
"2024-01-05": 0.928,
|
|
29
|
+
"2024-01-08": 0.927,
|
|
30
|
+
"2024-06-03": 0.96,
|
|
31
|
+
"2024-06-04": 0.961,
|
|
32
|
+
"2024-06-05": 0.962
|
|
33
|
+
}
|
|
34
|
+
}
|
package/dist/index.cjs
CHANGED
|
@@ -65,12 +65,18 @@ var import_node_url = require("url");
|
|
|
65
65
|
var path = __toESM(require("path"), 1);
|
|
66
66
|
var fs = __toESM(require("fs"), 1);
|
|
67
67
|
var import_meta = {};
|
|
68
|
-
var _bundled =
|
|
68
|
+
var _bundled = void 0;
|
|
69
69
|
function getBundledSnapshot() {
|
|
70
|
-
if (_bundled) return _bundled;
|
|
70
|
+
if (_bundled !== void 0) return _bundled;
|
|
71
71
|
const __dirname = path.dirname((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
72
72
|
const bundledPath = path.join(__dirname, "../data/ecb-rates.json");
|
|
73
|
-
|
|
73
|
+
try {
|
|
74
|
+
_bundled = JSON.parse(
|
|
75
|
+
fs.readFileSync(bundledPath, "utf8")
|
|
76
|
+
);
|
|
77
|
+
} catch {
|
|
78
|
+
_bundled = null;
|
|
79
|
+
}
|
|
74
80
|
return _bundled;
|
|
75
81
|
}
|
|
76
82
|
function getUserSnapshot() {
|
|
@@ -91,6 +97,12 @@ function getUserSnapshot() {
|
|
|
91
97
|
function getActiveSnapshot() {
|
|
92
98
|
const bundled = getBundledSnapshot();
|
|
93
99
|
const user = getUserSnapshot();
|
|
100
|
+
if (!bundled && !user) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
"No ECB rates snapshot available. Run `minus-tracker rates --update` to fetch rates."
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (!bundled) return user;
|
|
94
106
|
if (!user) return bundled;
|
|
95
107
|
const merged = {};
|
|
96
108
|
for (const ccy of /* @__PURE__ */ new Set([...Object.keys(bundled), ...Object.keys(user)])) {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/rates/index.ts","../src/parser/warnings.ts","../src/parser/index.ts","../src/calculator/index.ts"],"sourcesContent":["export { DEGIROParser } from \"./parser/index.js\";\nexport { Calculator } from \"./calculator/index.js\";\nexport { ParseError, CalculationError } from \"./errors.js\";\nexport type {\n Transaction,\n MatchedLot,\n GainsReport,\n LotMethod,\n} from \"./types.js\";\n","export class ParseError extends Error {\n readonly code: \"INVALID_CSV\" | \"MISSING_COLUMN\";\n readonly columnName?: string;\n\n constructor(code: \"INVALID_CSV\");\n constructor(code: \"MISSING_COLUMN\", columnName: string);\n constructor(code: ParseError[\"code\"], columnName?: string) {\n const msg =\n code === \"INVALID_CSV\"\n ? \"Invalid CSV: unable to parse\"\n : `Missing required column: ${columnName}`;\n super(msg);\n this.name = \"ParseError\";\n this.code = code;\n this.columnName = columnName;\n }\n}\n\nexport class CalculationError extends Error {\n readonly isin: string;\n readonly date: string;\n\n constructor(isin: string, date: string) {\n super(`No open lots for ISIN ${isin} on ${date}`);\n this.name = \"CalculationError\";\n this.isin = isin;\n this.date = date;\n }\n}\n","import { fileURLToPath } from \"node:url\";\nimport * as path from \"node:path\";\nimport * as fs from \"node:fs\";\n\nexport type RatesSnapshot = Record<string, Record<string, number>>;\n\n// Load the bundled snapshot lazily\nlet _bundled: RatesSnapshot | null = null;\n\nfunction getBundledSnapshot(): RatesSnapshot {\n if (_bundled) return _bundled;\n // Use import.meta.url-relative path to locate the bundled JSON\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n const bundledPath = path.join(__dirname, \"../data/ecb-rates.json\");\n _bundled = JSON.parse(fs.readFileSync(bundledPath, \"utf8\")) as RatesSnapshot;\n return _bundled;\n}\n\nfunction getUserSnapshot(): RatesSnapshot | null {\n const platform = process.platform;\n let configDir: string;\n if (platform === \"win32\") {\n configDir =\n process.env.APPDATA ??\n path.join(process.env.USERPROFILE ?? \"~\", \"AppData\", \"Roaming\");\n } else {\n configDir =\n process.env.XDG_CONFIG_HOME ??\n path.join(process.env.HOME ?? \"~\", \".config\");\n }\n const userPath = path.join(configDir, \"minus-tracker\", \"ecb-rates.json\");\n try {\n return JSON.parse(fs.readFileSync(userPath, \"utf8\")) as RatesSnapshot;\n } catch {\n return null;\n }\n}\n\nexport function getActiveSnapshot(): RatesSnapshot {\n const bundled = getBundledSnapshot();\n const user = getUserSnapshot();\n if (!user) return bundled;\n // Merge: user entries override bundled for same date keys\n const merged: RatesSnapshot = {};\n for (const ccy of new Set([...Object.keys(bundled), ...Object.keys(user)])) {\n merged[ccy] = { ...(bundled[ccy] ?? {}), ...(user[ccy] ?? {}) };\n }\n return merged;\n}\n\nfunction subtractDays(isoDate: string, days: number): string {\n const d = new Date(isoDate + \"T00:00:00Z\");\n d.setUTCDate(d.getUTCDate() - days);\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * Look up ECB rate for a currency on a given date.\n * Returns 1.0 for EUR. Walks back up to 3 calendar days for weekend/holiday gaps.\n * Returns null if not found within the window.\n *\n * @param currency ISO 4217 currency code\n * @param date YYYY-MM-DD trade date\n * @param snapshot Optional override (used in tests to inject stub data)\n */\nexport function lookupRate(\n currency: string,\n date: string,\n snapshot?: RatesSnapshot,\n): number | null {\n if (currency === \"EUR\") return 1.0;\n\n const s = snapshot ?? getActiveSnapshot();\n const currencyRates = s[currency];\n if (!currencyRates) return null; // unsupported currency\n\n for (let i = 0; i <= 3; i++) {\n const d = i === 0 ? date : subtractDays(date, i);\n if (currencyRates[d] !== undefined) {\n return currencyRates[d];\n }\n }\n return null;\n}\n","export type WarningEntry =\n | { code: \"MISSING_ISIN\"; row: number }\n | { code: \"UNSUPPORTED_CURRENCY\"; row: number; currency: string }\n | { code: \"NO_ECB_RATE\"; row: number; currency: string; date: string }\n | { code: \"QUANTITY_ZERO\"; row: number };\n\nexport function warningToEnglish(w: WarningEntry): string {\n switch (w.code) {\n case \"MISSING_ISIN\":\n return `Row ${w.row}: missing ISIN — skipped`;\n case \"UNSUPPORTED_CURRENCY\":\n return `Row ${w.row}: unsupported currency ${w.currency} — skipped`;\n case \"NO_ECB_RATE\":\n return `Row ${w.row}: no ECB rate for ${w.currency} on ${w.date} — skipped`;\n case \"QUANTITY_ZERO\":\n return `Row ${w.row}: quantity is 0 — skipped`;\n }\n}\n","import { Transaction } from \"../types.js\";\nimport { ParseError } from \"../errors.js\";\nimport {\n lookupRate,\n getActiveSnapshot,\n RatesSnapshot,\n} from \"../rates/index.js\";\nimport { WarningEntry, warningToEnglish } from \"./warnings.js\";\n\nconst REQUIRED_COLUMNS = [\n \"ISIN\",\n \"Quantity\",\n \"Price\",\n \"Date\",\n \"Local value\",\n \"Local value currency\",\n \"Transaction costs\",\n \"Transaction costs currency\",\n \"Product\",\n] as const;\n\n/**\n * Parse a single CSV line, respecting RFC-4180-style double-quote escaping.\n * Does not support embedded newlines in fields (not needed for DEGIRO exports).\n */\nfunction parseCSVRow(line: string): string[] {\n const fields: string[] = [];\n let i = 0;\n\n while (i <= line.length) {\n // End of line — stop (handles trailing comma by not emitting extra empty field)\n if (i === line.length) break;\n\n if (line[i] === '\"') {\n // Quoted field\n let field = \"\";\n i++; // skip opening quote\n while (i < line.length) {\n if (line[i] === '\"' && i + 1 < line.length && line[i + 1] === '\"') {\n // Escaped double-quote\n field += '\"';\n i += 2;\n } else if (line[i] === '\"') {\n i++; // skip closing quote\n break;\n } else {\n field += line[i++];\n }\n }\n fields.push(field);\n if (i < line.length && line[i] === \",\") i++; // skip delimiter\n } else {\n // Unquoted field\n const start = i;\n while (i < line.length && line[i] !== \",\") i++;\n fields.push(line.slice(start, i));\n if (i < line.length) i++; // skip delimiter\n }\n }\n\n return fields;\n}\n\n/**\n * Parse a full CSV string (CRLF or LF line endings) into a 2-D array of strings.\n */\nfunction parseCSV(input: string): string[][] {\n const normalized = input.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n return normalized.split(\"\\n\").map(parseCSVRow);\n}\n\n/**\n * Convert a DEGIRO date \"DD-MM-YYYY\" to ISO format \"YYYY-MM-DD\".\n */\nfunction parseDate(ddmmyyyy: string): string {\n const [dd, mm, yyyy] = ddmmyyyy.split(\"-\");\n return `${yyyy}-${mm}-${dd}`;\n}\n\nexport class DEGIROParser {\n private _warningEntries: WarningEntry[] = [];\n private _snapshot?: RatesSnapshot;\n\n constructor(snapshot?: RatesSnapshot) {\n this._snapshot = snapshot;\n }\n\n parse(csv: string): Transaction[] {\n this._warningEntries = [];\n\n // Binary content (null bytes) is not valid CSV\n if (typeof csv !== \"string\" || csv.includes(\"\\x00\")) {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n let rows: string[][];\n try {\n rows = parseCSV(csv);\n } catch {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n if (rows.length === 0) {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n // Build column-name → index map from the header row\n const header = rows[0];\n const colIndex: Record<string, number> = {};\n for (let i = 0; i < header.length; i++) {\n colIndex[header[i].trim()] = i;\n }\n\n // Validate that every required column is present\n for (const col of REQUIRED_COLUMNS) {\n if (colIndex[col] === undefined) {\n throw new ParseError(\"MISSING_COLUMN\", col);\n }\n }\n\n // Resolve the rates snapshot once for the whole parse run\n const snapshot: RatesSnapshot = this._snapshot ?? getActiveSnapshot();\n\n const transactions: Transaction[] = [];\n\n for (let r = 1; r < rows.length; r++) {\n const row = rows[r];\n // 1-indexed row number: header = 1, first data row = 2\n const rowIndex = r + 1;\n\n // Skip completely empty rows (including the trailing empty line after a\n // final newline, and rows that are all whitespace)\n if (row.every((cell) => cell.trim() === \"\")) continue;\n\n const get = (col: string): string => (row[colIndex[col]] ?? \"\").trim();\n\n // --- ISIN ---\n const isin = get(\"ISIN\");\n if (!isin) {\n this._warningEntries.push({ code: \"MISSING_ISIN\", row: rowIndex });\n continue;\n }\n\n // --- Quantity ---\n const rawQty = parseFloat(get(\"Quantity\"));\n if (isNaN(rawQty) || rawQty === 0) {\n this._warningEntries.push({ code: \"QUANTITY_ZERO\", row: rowIndex });\n continue;\n }\n\n const type: \"BUY\" | \"SELL\" = rawQty > 0 ? \"BUY\" : \"SELL\";\n const quantity = Math.abs(rawQty);\n\n // --- Date ---\n const isoDate = parseDate(get(\"Date\"));\n\n // --- Currency & FX ---\n const currency = get(\"Local value currency\");\n const rawTotalLocal = parseFloat(get(\"Local value\"));\n const totalLocal = isNaN(rawTotalLocal) ? 0 : rawTotalLocal;\n\n let totalEUR: number;\n let fxRate: number | undefined;\n\n if (currency === \"EUR\") {\n totalEUR = Math.abs(totalLocal);\n fxRate = undefined;\n } else {\n const rate = lookupRate(currency, isoDate, snapshot);\n if (rate === null) {\n if (snapshot[currency] === undefined) {\n this._warningEntries.push({\n code: \"UNSUPPORTED_CURRENCY\",\n row: rowIndex,\n currency,\n });\n } else {\n this._warningEntries.push({\n code: \"NO_ECB_RATE\",\n row: rowIndex,\n currency,\n date: isoDate,\n });\n }\n continue;\n }\n totalEUR = Math.abs(totalLocal) / rate;\n fxRate = rate;\n }\n\n // --- Fees ---\n const rawFees = parseFloat(get(\"Transaction costs\"));\n const feesEUR = Math.abs(isNaN(rawFees) ? 0 : rawFees);\n\n // --- Remaining fields ---\n const pricePerUnit = parseFloat(get(\"Price\"));\n const product = get(\"Product\");\n\n transactions.push({\n isin,\n product,\n date: isoDate,\n type,\n quantity,\n pricePerUnit,\n currency,\n totalLocal,\n totalEUR,\n feesEUR,\n fxRate,\n });\n }\n\n return transactions;\n }\n\n get warnings(): string[] {\n return this._warningEntries.map(warningToEnglish);\n }\n\n get warningEntries(): WarningEntry[] {\n return this._warningEntries;\n }\n}\n","import type {\n Transaction,\n MatchedLot,\n GainsReport,\n LotMethod,\n} from \"../types.js\";\nimport { CalculationError } from \"../errors.js\";\n\ninterface Lot {\n date: string;\n quantity: number;\n pricePerUnitEUR: number;\n feesEUR: number;\n originalQty: number;\n fxRate?: number;\n}\n\nfunction roundHalfUp(x: number): number {\n return (Math.sign(x) * Math.round(Math.abs(x) * 100)) / 100;\n}\n\nfunction inferTaxYear(transactions: Transaction[]): {\n year: number;\n multipleYears: boolean;\n} {\n const counts: Record<string, number> = {};\n for (const t of transactions) {\n const y = t.date.slice(0, 4);\n counts[y] = (counts[y] ?? 0) + 1;\n }\n const years = Object.keys(counts);\n if (years.length === 0)\n return { year: new Date().getFullYear(), multipleYears: false };\n const year = parseInt(\n years.reduce((a, b) => (counts[a] >= counts[b] ? a : b)),\n );\n return { year, multipleYears: years.length > 1 };\n}\n\nexport class Calculator {\n private readonly _transactions: Transaction[];\n private readonly _parseWarnings: string[];\n\n constructor(transactions: Transaction[], parseWarnings?: string[]) {\n this._transactions = transactions;\n this._parseWarnings = parseWarnings ?? [];\n }\n\n calculateGains(method: LotMethod): GainsReport {\n const warnings: string[] = [...this._parseWarnings];\n\n // 1. Sort ascending by date; BUY before SELL on same date\n const sorted = [...this._transactions].sort((a, b) => {\n if (a.date < b.date) return -1;\n if (a.date > b.date) return 1;\n // Same date: BUY before SELL\n if (a.type === \"BUY\" && b.type === \"SELL\") return -1;\n if (a.type === \"SELL\" && b.type === \"BUY\") return 1;\n return 0;\n });\n\n // 2. Tax year inference\n const { year: taxYear, multipleYears } = inferTaxYear(sorted);\n if (multipleYears) {\n warnings.push(\n \"CSV contains transactions from multiple years — filter to a single year for accurate reporting.\",\n );\n }\n\n // 3. Lot matching\n const openLots = new Map<string, Lot[]>();\n const matchedLots: MatchedLot[] = [];\n const ratesUsed: Record<string, number> = {};\n\n for (const tx of sorted) {\n // Collect rates used\n if (tx.fxRate !== undefined) {\n ratesUsed[`${tx.currency}:${tx.date}`] = tx.fxRate;\n }\n\n if (tx.type === \"BUY\") {\n const lot: Lot = {\n date: tx.date,\n quantity: tx.quantity,\n pricePerUnitEUR: tx.totalEUR / tx.quantity,\n feesEUR: tx.feesEUR,\n originalQty: tx.quantity,\n fxRate: tx.fxRate,\n };\n if (!openLots.has(tx.isin)) openLots.set(tx.isin, []);\n openLots.get(tx.isin)!.push(lot);\n } else {\n // SELL\n const lots = openLots.get(tx.isin);\n if (!lots || lots.length === 0) {\n throw new CalculationError(tx.isin, tx.date);\n }\n\n const sellPricePerUnitEUR = tx.totalEUR / tx.quantity;\n let remainingSellQty = tx.quantity;\n\n while (remainingSellQty > 0) {\n if (!lots || lots.length === 0) {\n throw new CalculationError(tx.isin, tx.date);\n }\n\n const lot = method === \"LIFO\" ? lots[lots.length - 1] : lots[0];\n const matchedQty = Math.min(lot.quantity, remainingSellQty);\n\n // Fee allocation\n const allocatedBuyFeesEUR =\n lot.feesEUR * (matchedQty / lot.originalQty);\n const allocatedSellFeesEUR = tx.feesEUR * (matchedQty / tx.quantity);\n\n const buyCostEUR =\n lot.pricePerUnitEUR * matchedQty + allocatedBuyFeesEUR;\n const sellProceedsEUR =\n sellPricePerUnitEUR * matchedQty - allocatedSellFeesEUR;\n const gainLossEUR = sellProceedsEUR - buyCostEUR;\n\n matchedLots.push({\n isin: tx.isin,\n product: tx.product,\n quantity: matchedQty,\n buyDate: lot.date,\n sellDate: tx.date,\n buyPriceEUR: lot.pricePerUnitEUR,\n sellPriceEUR: sellPricePerUnitEUR,\n buyCostEUR: roundHalfUp(buyCostEUR),\n sellProceedsEUR: roundHalfUp(sellProceedsEUR),\n gainLossEUR: roundHalfUp(gainLossEUR),\n buyFxRate: lot.fxRate,\n sellFxRate: tx.fxRate,\n });\n\n lot.quantity -= matchedQty;\n remainingSellQty -= matchedQty;\n\n if (lot.quantity <= 0) {\n if (method === \"LIFO\") {\n lots.pop();\n } else {\n lots.shift();\n }\n }\n }\n }\n }\n\n // 4. Aggregate\n let plusvalenze = 0;\n let minusvalenze = 0;\n for (const lot of matchedLots) {\n if (lot.gainLossEUR > 0) plusvalenze += lot.gainLossEUR;\n else minusvalenze += Math.abs(lot.gainLossEUR);\n }\n\n return {\n method,\n taxYear,\n plusvalenze: roundHalfUp(plusvalenze),\n minusvalenze: roundHalfUp(minusvalenze),\n netResult: roundHalfUp(plusvalenze - minusvalenze),\n lots: matchedLots,\n ratesUsed,\n warnings,\n generatedAt: new Date().toISOString(),\n };\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/rates/index.ts","../src/parser/warnings.ts","../src/parser/index.ts","../src/calculator/index.ts"],"sourcesContent":["export { DEGIROParser } from \"./parser/index.js\";\nexport { Calculator } from \"./calculator/index.js\";\nexport { ParseError, CalculationError } from \"./errors.js\";\nexport type {\n Transaction,\n MatchedLot,\n GainsReport,\n LotMethod,\n} from \"./types.js\";\n","export class ParseError extends Error {\n readonly code: \"INVALID_CSV\" | \"MISSING_COLUMN\";\n readonly columnName?: string;\n\n constructor(code: \"INVALID_CSV\");\n constructor(code: \"MISSING_COLUMN\", columnName: string);\n constructor(code: ParseError[\"code\"], columnName?: string) {\n const msg =\n code === \"INVALID_CSV\"\n ? \"Invalid CSV: unable to parse\"\n : `Missing required column: ${columnName}`;\n super(msg);\n this.name = \"ParseError\";\n this.code = code;\n this.columnName = columnName;\n }\n}\n\nexport class CalculationError extends Error {\n readonly isin: string;\n readonly date: string;\n\n constructor(isin: string, date: string) {\n super(`No open lots for ISIN ${isin} on ${date}`);\n this.name = \"CalculationError\";\n this.isin = isin;\n this.date = date;\n }\n}\n","import { fileURLToPath } from \"node:url\";\nimport * as path from \"node:path\";\nimport * as fs from \"node:fs\";\n\nexport type RatesSnapshot = Record<string, Record<string, number>>;\n\n// Load the bundled snapshot lazily; returns null if the file is absent (e.g. broken install)\nlet _bundled: RatesSnapshot | null | undefined = undefined;\n\nfunction getBundledSnapshot(): RatesSnapshot | null {\n if (_bundled !== undefined) return _bundled;\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n const bundledPath = path.join(__dirname, \"../data/ecb-rates.json\");\n try {\n _bundled = JSON.parse(\n fs.readFileSync(bundledPath, \"utf8\"),\n ) as RatesSnapshot;\n } catch {\n _bundled = null;\n }\n return _bundled;\n}\n\nfunction getUserSnapshot(): RatesSnapshot | null {\n const platform = process.platform;\n let configDir: string;\n if (platform === \"win32\") {\n configDir =\n process.env.APPDATA ??\n path.join(process.env.USERPROFILE ?? \"~\", \"AppData\", \"Roaming\");\n } else {\n configDir =\n process.env.XDG_CONFIG_HOME ??\n path.join(process.env.HOME ?? \"~\", \".config\");\n }\n const userPath = path.join(configDir, \"minus-tracker\", \"ecb-rates.json\");\n try {\n return JSON.parse(fs.readFileSync(userPath, \"utf8\")) as RatesSnapshot;\n } catch {\n return null;\n }\n}\n\nexport function getActiveSnapshot(): RatesSnapshot {\n const bundled = getBundledSnapshot();\n const user = getUserSnapshot();\n if (!bundled && !user) {\n throw new Error(\n \"No ECB rates snapshot available. Run `minus-tracker rates --update` to fetch rates.\",\n );\n }\n if (!bundled) return user!;\n if (!user) return bundled;\n // Merge: user entries override bundled for same date keys\n const merged: RatesSnapshot = {};\n for (const ccy of new Set([...Object.keys(bundled), ...Object.keys(user)])) {\n merged[ccy] = { ...(bundled[ccy] ?? {}), ...(user[ccy] ?? {}) };\n }\n return merged;\n}\n\nfunction subtractDays(isoDate: string, days: number): string {\n const d = new Date(isoDate + \"T00:00:00Z\");\n d.setUTCDate(d.getUTCDate() - days);\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * Look up ECB rate for a currency on a given date.\n * Returns 1.0 for EUR. Walks back up to 3 calendar days for weekend/holiday gaps.\n * Returns null if not found within the window.\n *\n * @param currency ISO 4217 currency code\n * @param date YYYY-MM-DD trade date\n * @param snapshot Optional override (used in tests to inject stub data)\n */\nexport function lookupRate(\n currency: string,\n date: string,\n snapshot?: RatesSnapshot,\n): number | null {\n if (currency === \"EUR\") return 1.0;\n\n const s = snapshot ?? getActiveSnapshot();\n const currencyRates = s[currency];\n if (!currencyRates) return null; // unsupported currency\n\n for (let i = 0; i <= 3; i++) {\n const d = i === 0 ? date : subtractDays(date, i);\n if (currencyRates[d] !== undefined) {\n return currencyRates[d];\n }\n }\n return null;\n}\n","export type WarningEntry =\n | { code: \"MISSING_ISIN\"; row: number }\n | { code: \"UNSUPPORTED_CURRENCY\"; row: number; currency: string }\n | { code: \"NO_ECB_RATE\"; row: number; currency: string; date: string }\n | { code: \"QUANTITY_ZERO\"; row: number };\n\nexport function warningToEnglish(w: WarningEntry): string {\n switch (w.code) {\n case \"MISSING_ISIN\":\n return `Row ${w.row}: missing ISIN — skipped`;\n case \"UNSUPPORTED_CURRENCY\":\n return `Row ${w.row}: unsupported currency ${w.currency} — skipped`;\n case \"NO_ECB_RATE\":\n return `Row ${w.row}: no ECB rate for ${w.currency} on ${w.date} — skipped`;\n case \"QUANTITY_ZERO\":\n return `Row ${w.row}: quantity is 0 — skipped`;\n }\n}\n","import { Transaction } from \"../types.js\";\nimport { ParseError } from \"../errors.js\";\nimport {\n lookupRate,\n getActiveSnapshot,\n RatesSnapshot,\n} from \"../rates/index.js\";\nimport { WarningEntry, warningToEnglish } from \"./warnings.js\";\n\nconst REQUIRED_COLUMNS = [\n \"ISIN\",\n \"Quantity\",\n \"Price\",\n \"Date\",\n \"Local value\",\n \"Local value currency\",\n \"Transaction costs\",\n \"Transaction costs currency\",\n \"Product\",\n] as const;\n\n/**\n * Parse a single CSV line, respecting RFC-4180-style double-quote escaping.\n * Does not support embedded newlines in fields (not needed for DEGIRO exports).\n */\nfunction parseCSVRow(line: string): string[] {\n const fields: string[] = [];\n let i = 0;\n\n while (i <= line.length) {\n // End of line — stop (handles trailing comma by not emitting extra empty field)\n if (i === line.length) break;\n\n if (line[i] === '\"') {\n // Quoted field\n let field = \"\";\n i++; // skip opening quote\n while (i < line.length) {\n if (line[i] === '\"' && i + 1 < line.length && line[i + 1] === '\"') {\n // Escaped double-quote\n field += '\"';\n i += 2;\n } else if (line[i] === '\"') {\n i++; // skip closing quote\n break;\n } else {\n field += line[i++];\n }\n }\n fields.push(field);\n if (i < line.length && line[i] === \",\") i++; // skip delimiter\n } else {\n // Unquoted field\n const start = i;\n while (i < line.length && line[i] !== \",\") i++;\n fields.push(line.slice(start, i));\n if (i < line.length) i++; // skip delimiter\n }\n }\n\n return fields;\n}\n\n/**\n * Parse a full CSV string (CRLF or LF line endings) into a 2-D array of strings.\n */\nfunction parseCSV(input: string): string[][] {\n const normalized = input.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n return normalized.split(\"\\n\").map(parseCSVRow);\n}\n\n/**\n * Convert a DEGIRO date \"DD-MM-YYYY\" to ISO format \"YYYY-MM-DD\".\n */\nfunction parseDate(ddmmyyyy: string): string {\n const [dd, mm, yyyy] = ddmmyyyy.split(\"-\");\n return `${yyyy}-${mm}-${dd}`;\n}\n\nexport class DEGIROParser {\n private _warningEntries: WarningEntry[] = [];\n private _snapshot?: RatesSnapshot;\n\n constructor(snapshot?: RatesSnapshot) {\n this._snapshot = snapshot;\n }\n\n parse(csv: string): Transaction[] {\n this._warningEntries = [];\n\n // Binary content (null bytes) is not valid CSV\n if (typeof csv !== \"string\" || csv.includes(\"\\x00\")) {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n let rows: string[][];\n try {\n rows = parseCSV(csv);\n } catch {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n if (rows.length === 0) {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n // Build column-name → index map from the header row\n const header = rows[0];\n const colIndex: Record<string, number> = {};\n for (let i = 0; i < header.length; i++) {\n colIndex[header[i].trim()] = i;\n }\n\n // Validate that every required column is present\n for (const col of REQUIRED_COLUMNS) {\n if (colIndex[col] === undefined) {\n throw new ParseError(\"MISSING_COLUMN\", col);\n }\n }\n\n // Resolve the rates snapshot once for the whole parse run\n const snapshot: RatesSnapshot = this._snapshot ?? getActiveSnapshot();\n\n const transactions: Transaction[] = [];\n\n for (let r = 1; r < rows.length; r++) {\n const row = rows[r];\n // 1-indexed row number: header = 1, first data row = 2\n const rowIndex = r + 1;\n\n // Skip completely empty rows (including the trailing empty line after a\n // final newline, and rows that are all whitespace)\n if (row.every((cell) => cell.trim() === \"\")) continue;\n\n const get = (col: string): string => (row[colIndex[col]] ?? \"\").trim();\n\n // --- ISIN ---\n const isin = get(\"ISIN\");\n if (!isin) {\n this._warningEntries.push({ code: \"MISSING_ISIN\", row: rowIndex });\n continue;\n }\n\n // --- Quantity ---\n const rawQty = parseFloat(get(\"Quantity\"));\n if (isNaN(rawQty) || rawQty === 0) {\n this._warningEntries.push({ code: \"QUANTITY_ZERO\", row: rowIndex });\n continue;\n }\n\n const type: \"BUY\" | \"SELL\" = rawQty > 0 ? \"BUY\" : \"SELL\";\n const quantity = Math.abs(rawQty);\n\n // --- Date ---\n const isoDate = parseDate(get(\"Date\"));\n\n // --- Currency & FX ---\n const currency = get(\"Local value currency\");\n const rawTotalLocal = parseFloat(get(\"Local value\"));\n const totalLocal = isNaN(rawTotalLocal) ? 0 : rawTotalLocal;\n\n let totalEUR: number;\n let fxRate: number | undefined;\n\n if (currency === \"EUR\") {\n totalEUR = Math.abs(totalLocal);\n fxRate = undefined;\n } else {\n const rate = lookupRate(currency, isoDate, snapshot);\n if (rate === null) {\n if (snapshot[currency] === undefined) {\n this._warningEntries.push({\n code: \"UNSUPPORTED_CURRENCY\",\n row: rowIndex,\n currency,\n });\n } else {\n this._warningEntries.push({\n code: \"NO_ECB_RATE\",\n row: rowIndex,\n currency,\n date: isoDate,\n });\n }\n continue;\n }\n totalEUR = Math.abs(totalLocal) / rate;\n fxRate = rate;\n }\n\n // --- Fees ---\n const rawFees = parseFloat(get(\"Transaction costs\"));\n const feesEUR = Math.abs(isNaN(rawFees) ? 0 : rawFees);\n\n // --- Remaining fields ---\n const pricePerUnit = parseFloat(get(\"Price\"));\n const product = get(\"Product\");\n\n transactions.push({\n isin,\n product,\n date: isoDate,\n type,\n quantity,\n pricePerUnit,\n currency,\n totalLocal,\n totalEUR,\n feesEUR,\n fxRate,\n });\n }\n\n return transactions;\n }\n\n get warnings(): string[] {\n return this._warningEntries.map(warningToEnglish);\n }\n\n get warningEntries(): WarningEntry[] {\n return this._warningEntries;\n }\n}\n","import type {\n Transaction,\n MatchedLot,\n GainsReport,\n LotMethod,\n} from \"../types.js\";\nimport { CalculationError } from \"../errors.js\";\n\ninterface Lot {\n date: string;\n quantity: number;\n pricePerUnitEUR: number;\n feesEUR: number;\n originalQty: number;\n fxRate?: number;\n}\n\nfunction roundHalfUp(x: number): number {\n return (Math.sign(x) * Math.round(Math.abs(x) * 100)) / 100;\n}\n\nfunction inferTaxYear(transactions: Transaction[]): {\n year: number;\n multipleYears: boolean;\n} {\n const counts: Record<string, number> = {};\n for (const t of transactions) {\n const y = t.date.slice(0, 4);\n counts[y] = (counts[y] ?? 0) + 1;\n }\n const years = Object.keys(counts);\n if (years.length === 0)\n return { year: new Date().getFullYear(), multipleYears: false };\n const year = parseInt(\n years.reduce((a, b) => (counts[a] >= counts[b] ? a : b)),\n );\n return { year, multipleYears: years.length > 1 };\n}\n\nexport class Calculator {\n private readonly _transactions: Transaction[];\n private readonly _parseWarnings: string[];\n\n constructor(transactions: Transaction[], parseWarnings?: string[]) {\n this._transactions = transactions;\n this._parseWarnings = parseWarnings ?? [];\n }\n\n calculateGains(method: LotMethod): GainsReport {\n const warnings: string[] = [...this._parseWarnings];\n\n // 1. Sort ascending by date; BUY before SELL on same date\n const sorted = [...this._transactions].sort((a, b) => {\n if (a.date < b.date) return -1;\n if (a.date > b.date) return 1;\n // Same date: BUY before SELL\n if (a.type === \"BUY\" && b.type === \"SELL\") return -1;\n if (a.type === \"SELL\" && b.type === \"BUY\") return 1;\n return 0;\n });\n\n // 2. Tax year inference\n const { year: taxYear, multipleYears } = inferTaxYear(sorted);\n if (multipleYears) {\n warnings.push(\n \"CSV contains transactions from multiple years — filter to a single year for accurate reporting.\",\n );\n }\n\n // 3. Lot matching\n const openLots = new Map<string, Lot[]>();\n const matchedLots: MatchedLot[] = [];\n const ratesUsed: Record<string, number> = {};\n\n for (const tx of sorted) {\n // Collect rates used\n if (tx.fxRate !== undefined) {\n ratesUsed[`${tx.currency}:${tx.date}`] = tx.fxRate;\n }\n\n if (tx.type === \"BUY\") {\n const lot: Lot = {\n date: tx.date,\n quantity: tx.quantity,\n pricePerUnitEUR: tx.totalEUR / tx.quantity,\n feesEUR: tx.feesEUR,\n originalQty: tx.quantity,\n fxRate: tx.fxRate,\n };\n if (!openLots.has(tx.isin)) openLots.set(tx.isin, []);\n openLots.get(tx.isin)!.push(lot);\n } else {\n // SELL\n const lots = openLots.get(tx.isin);\n if (!lots || lots.length === 0) {\n throw new CalculationError(tx.isin, tx.date);\n }\n\n const sellPricePerUnitEUR = tx.totalEUR / tx.quantity;\n let remainingSellQty = tx.quantity;\n\n while (remainingSellQty > 0) {\n if (!lots || lots.length === 0) {\n throw new CalculationError(tx.isin, tx.date);\n }\n\n const lot = method === \"LIFO\" ? lots[lots.length - 1] : lots[0];\n const matchedQty = Math.min(lot.quantity, remainingSellQty);\n\n // Fee allocation\n const allocatedBuyFeesEUR =\n lot.feesEUR * (matchedQty / lot.originalQty);\n const allocatedSellFeesEUR = tx.feesEUR * (matchedQty / tx.quantity);\n\n const buyCostEUR =\n lot.pricePerUnitEUR * matchedQty + allocatedBuyFeesEUR;\n const sellProceedsEUR =\n sellPricePerUnitEUR * matchedQty - allocatedSellFeesEUR;\n const gainLossEUR = sellProceedsEUR - buyCostEUR;\n\n matchedLots.push({\n isin: tx.isin,\n product: tx.product,\n quantity: matchedQty,\n buyDate: lot.date,\n sellDate: tx.date,\n buyPriceEUR: lot.pricePerUnitEUR,\n sellPriceEUR: sellPricePerUnitEUR,\n buyCostEUR: roundHalfUp(buyCostEUR),\n sellProceedsEUR: roundHalfUp(sellProceedsEUR),\n gainLossEUR: roundHalfUp(gainLossEUR),\n buyFxRate: lot.fxRate,\n sellFxRate: tx.fxRate,\n });\n\n lot.quantity -= matchedQty;\n remainingSellQty -= matchedQty;\n\n if (lot.quantity <= 0) {\n if (method === \"LIFO\") {\n lots.pop();\n } else {\n lots.shift();\n }\n }\n }\n }\n }\n\n // 4. Aggregate\n let plusvalenze = 0;\n let minusvalenze = 0;\n for (const lot of matchedLots) {\n if (lot.gainLossEUR > 0) plusvalenze += lot.gainLossEUR;\n else minusvalenze += Math.abs(lot.gainLossEUR);\n }\n\n return {\n method,\n taxYear,\n plusvalenze: roundHalfUp(plusvalenze),\n minusvalenze: roundHalfUp(minusvalenze),\n netResult: roundHalfUp(plusvalenze - minusvalenze),\n lots: matchedLots,\n ratesUsed,\n warnings,\n generatedAt: new Date().toISOString(),\n };\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -25,12 +25,18 @@ var CalculationError = class extends Error {
|
|
|
25
25
|
import { fileURLToPath } from "url";
|
|
26
26
|
import * as path from "path";
|
|
27
27
|
import * as fs from "fs";
|
|
28
|
-
var _bundled =
|
|
28
|
+
var _bundled = void 0;
|
|
29
29
|
function getBundledSnapshot() {
|
|
30
|
-
if (_bundled) return _bundled;
|
|
30
|
+
if (_bundled !== void 0) return _bundled;
|
|
31
31
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
32
32
|
const bundledPath = path.join(__dirname, "../data/ecb-rates.json");
|
|
33
|
-
|
|
33
|
+
try {
|
|
34
|
+
_bundled = JSON.parse(
|
|
35
|
+
fs.readFileSync(bundledPath, "utf8")
|
|
36
|
+
);
|
|
37
|
+
} catch {
|
|
38
|
+
_bundled = null;
|
|
39
|
+
}
|
|
34
40
|
return _bundled;
|
|
35
41
|
}
|
|
36
42
|
function getUserSnapshot() {
|
|
@@ -51,6 +57,12 @@ function getUserSnapshot() {
|
|
|
51
57
|
function getActiveSnapshot() {
|
|
52
58
|
const bundled = getBundledSnapshot();
|
|
53
59
|
const user = getUserSnapshot();
|
|
60
|
+
if (!bundled && !user) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
"No ECB rates snapshot available. Run `minus-tracker rates --update` to fetch rates."
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (!bundled) return user;
|
|
54
66
|
if (!user) return bundled;
|
|
55
67
|
const merged = {};
|
|
56
68
|
for (const ccy of /* @__PURE__ */ new Set([...Object.keys(bundled), ...Object.keys(user)])) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/rates/index.ts","../src/parser/warnings.ts","../src/parser/index.ts","../src/calculator/index.ts"],"sourcesContent":["export class ParseError extends Error {\n readonly code: \"INVALID_CSV\" | \"MISSING_COLUMN\";\n readonly columnName?: string;\n\n constructor(code: \"INVALID_CSV\");\n constructor(code: \"MISSING_COLUMN\", columnName: string);\n constructor(code: ParseError[\"code\"], columnName?: string) {\n const msg =\n code === \"INVALID_CSV\"\n ? \"Invalid CSV: unable to parse\"\n : `Missing required column: ${columnName}`;\n super(msg);\n this.name = \"ParseError\";\n this.code = code;\n this.columnName = columnName;\n }\n}\n\nexport class CalculationError extends Error {\n readonly isin: string;\n readonly date: string;\n\n constructor(isin: string, date: string) {\n super(`No open lots for ISIN ${isin} on ${date}`);\n this.name = \"CalculationError\";\n this.isin = isin;\n this.date = date;\n }\n}\n","import { fileURLToPath } from \"node:url\";\nimport * as path from \"node:path\";\nimport * as fs from \"node:fs\";\n\nexport type RatesSnapshot = Record<string, Record<string, number>>;\n\n// Load the bundled snapshot lazily\nlet _bundled: RatesSnapshot | null = null;\n\nfunction getBundledSnapshot(): RatesSnapshot {\n if (_bundled) return _bundled;\n // Use import.meta.url-relative path to locate the bundled JSON\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n const bundledPath = path.join(__dirname, \"../data/ecb-rates.json\");\n _bundled = JSON.parse(fs.readFileSync(bundledPath, \"utf8\")) as RatesSnapshot;\n return _bundled;\n}\n\nfunction getUserSnapshot(): RatesSnapshot | null {\n const platform = process.platform;\n let configDir: string;\n if (platform === \"win32\") {\n configDir =\n process.env.APPDATA ??\n path.join(process.env.USERPROFILE ?? \"~\", \"AppData\", \"Roaming\");\n } else {\n configDir =\n process.env.XDG_CONFIG_HOME ??\n path.join(process.env.HOME ?? \"~\", \".config\");\n }\n const userPath = path.join(configDir, \"minus-tracker\", \"ecb-rates.json\");\n try {\n return JSON.parse(fs.readFileSync(userPath, \"utf8\")) as RatesSnapshot;\n } catch {\n return null;\n }\n}\n\nexport function getActiveSnapshot(): RatesSnapshot {\n const bundled = getBundledSnapshot();\n const user = getUserSnapshot();\n if (!user) return bundled;\n // Merge: user entries override bundled for same date keys\n const merged: RatesSnapshot = {};\n for (const ccy of new Set([...Object.keys(bundled), ...Object.keys(user)])) {\n merged[ccy] = { ...(bundled[ccy] ?? {}), ...(user[ccy] ?? {}) };\n }\n return merged;\n}\n\nfunction subtractDays(isoDate: string, days: number): string {\n const d = new Date(isoDate + \"T00:00:00Z\");\n d.setUTCDate(d.getUTCDate() - days);\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * Look up ECB rate for a currency on a given date.\n * Returns 1.0 for EUR. Walks back up to 3 calendar days for weekend/holiday gaps.\n * Returns null if not found within the window.\n *\n * @param currency ISO 4217 currency code\n * @param date YYYY-MM-DD trade date\n * @param snapshot Optional override (used in tests to inject stub data)\n */\nexport function lookupRate(\n currency: string,\n date: string,\n snapshot?: RatesSnapshot,\n): number | null {\n if (currency === \"EUR\") return 1.0;\n\n const s = snapshot ?? getActiveSnapshot();\n const currencyRates = s[currency];\n if (!currencyRates) return null; // unsupported currency\n\n for (let i = 0; i <= 3; i++) {\n const d = i === 0 ? date : subtractDays(date, i);\n if (currencyRates[d] !== undefined) {\n return currencyRates[d];\n }\n }\n return null;\n}\n","export type WarningEntry =\n | { code: \"MISSING_ISIN\"; row: number }\n | { code: \"UNSUPPORTED_CURRENCY\"; row: number; currency: string }\n | { code: \"NO_ECB_RATE\"; row: number; currency: string; date: string }\n | { code: \"QUANTITY_ZERO\"; row: number };\n\nexport function warningToEnglish(w: WarningEntry): string {\n switch (w.code) {\n case \"MISSING_ISIN\":\n return `Row ${w.row}: missing ISIN — skipped`;\n case \"UNSUPPORTED_CURRENCY\":\n return `Row ${w.row}: unsupported currency ${w.currency} — skipped`;\n case \"NO_ECB_RATE\":\n return `Row ${w.row}: no ECB rate for ${w.currency} on ${w.date} — skipped`;\n case \"QUANTITY_ZERO\":\n return `Row ${w.row}: quantity is 0 — skipped`;\n }\n}\n","import { Transaction } from \"../types.js\";\nimport { ParseError } from \"../errors.js\";\nimport {\n lookupRate,\n getActiveSnapshot,\n RatesSnapshot,\n} from \"../rates/index.js\";\nimport { WarningEntry, warningToEnglish } from \"./warnings.js\";\n\nconst REQUIRED_COLUMNS = [\n \"ISIN\",\n \"Quantity\",\n \"Price\",\n \"Date\",\n \"Local value\",\n \"Local value currency\",\n \"Transaction costs\",\n \"Transaction costs currency\",\n \"Product\",\n] as const;\n\n/**\n * Parse a single CSV line, respecting RFC-4180-style double-quote escaping.\n * Does not support embedded newlines in fields (not needed for DEGIRO exports).\n */\nfunction parseCSVRow(line: string): string[] {\n const fields: string[] = [];\n let i = 0;\n\n while (i <= line.length) {\n // End of line — stop (handles trailing comma by not emitting extra empty field)\n if (i === line.length) break;\n\n if (line[i] === '\"') {\n // Quoted field\n let field = \"\";\n i++; // skip opening quote\n while (i < line.length) {\n if (line[i] === '\"' && i + 1 < line.length && line[i + 1] === '\"') {\n // Escaped double-quote\n field += '\"';\n i += 2;\n } else if (line[i] === '\"') {\n i++; // skip closing quote\n break;\n } else {\n field += line[i++];\n }\n }\n fields.push(field);\n if (i < line.length && line[i] === \",\") i++; // skip delimiter\n } else {\n // Unquoted field\n const start = i;\n while (i < line.length && line[i] !== \",\") i++;\n fields.push(line.slice(start, i));\n if (i < line.length) i++; // skip delimiter\n }\n }\n\n return fields;\n}\n\n/**\n * Parse a full CSV string (CRLF or LF line endings) into a 2-D array of strings.\n */\nfunction parseCSV(input: string): string[][] {\n const normalized = input.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n return normalized.split(\"\\n\").map(parseCSVRow);\n}\n\n/**\n * Convert a DEGIRO date \"DD-MM-YYYY\" to ISO format \"YYYY-MM-DD\".\n */\nfunction parseDate(ddmmyyyy: string): string {\n const [dd, mm, yyyy] = ddmmyyyy.split(\"-\");\n return `${yyyy}-${mm}-${dd}`;\n}\n\nexport class DEGIROParser {\n private _warningEntries: WarningEntry[] = [];\n private _snapshot?: RatesSnapshot;\n\n constructor(snapshot?: RatesSnapshot) {\n this._snapshot = snapshot;\n }\n\n parse(csv: string): Transaction[] {\n this._warningEntries = [];\n\n // Binary content (null bytes) is not valid CSV\n if (typeof csv !== \"string\" || csv.includes(\"\\x00\")) {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n let rows: string[][];\n try {\n rows = parseCSV(csv);\n } catch {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n if (rows.length === 0) {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n // Build column-name → index map from the header row\n const header = rows[0];\n const colIndex: Record<string, number> = {};\n for (let i = 0; i < header.length; i++) {\n colIndex[header[i].trim()] = i;\n }\n\n // Validate that every required column is present\n for (const col of REQUIRED_COLUMNS) {\n if (colIndex[col] === undefined) {\n throw new ParseError(\"MISSING_COLUMN\", col);\n }\n }\n\n // Resolve the rates snapshot once for the whole parse run\n const snapshot: RatesSnapshot = this._snapshot ?? getActiveSnapshot();\n\n const transactions: Transaction[] = [];\n\n for (let r = 1; r < rows.length; r++) {\n const row = rows[r];\n // 1-indexed row number: header = 1, first data row = 2\n const rowIndex = r + 1;\n\n // Skip completely empty rows (including the trailing empty line after a\n // final newline, and rows that are all whitespace)\n if (row.every((cell) => cell.trim() === \"\")) continue;\n\n const get = (col: string): string => (row[colIndex[col]] ?? \"\").trim();\n\n // --- ISIN ---\n const isin = get(\"ISIN\");\n if (!isin) {\n this._warningEntries.push({ code: \"MISSING_ISIN\", row: rowIndex });\n continue;\n }\n\n // --- Quantity ---\n const rawQty = parseFloat(get(\"Quantity\"));\n if (isNaN(rawQty) || rawQty === 0) {\n this._warningEntries.push({ code: \"QUANTITY_ZERO\", row: rowIndex });\n continue;\n }\n\n const type: \"BUY\" | \"SELL\" = rawQty > 0 ? \"BUY\" : \"SELL\";\n const quantity = Math.abs(rawQty);\n\n // --- Date ---\n const isoDate = parseDate(get(\"Date\"));\n\n // --- Currency & FX ---\n const currency = get(\"Local value currency\");\n const rawTotalLocal = parseFloat(get(\"Local value\"));\n const totalLocal = isNaN(rawTotalLocal) ? 0 : rawTotalLocal;\n\n let totalEUR: number;\n let fxRate: number | undefined;\n\n if (currency === \"EUR\") {\n totalEUR = Math.abs(totalLocal);\n fxRate = undefined;\n } else {\n const rate = lookupRate(currency, isoDate, snapshot);\n if (rate === null) {\n if (snapshot[currency] === undefined) {\n this._warningEntries.push({\n code: \"UNSUPPORTED_CURRENCY\",\n row: rowIndex,\n currency,\n });\n } else {\n this._warningEntries.push({\n code: \"NO_ECB_RATE\",\n row: rowIndex,\n currency,\n date: isoDate,\n });\n }\n continue;\n }\n totalEUR = Math.abs(totalLocal) / rate;\n fxRate = rate;\n }\n\n // --- Fees ---\n const rawFees = parseFloat(get(\"Transaction costs\"));\n const feesEUR = Math.abs(isNaN(rawFees) ? 0 : rawFees);\n\n // --- Remaining fields ---\n const pricePerUnit = parseFloat(get(\"Price\"));\n const product = get(\"Product\");\n\n transactions.push({\n isin,\n product,\n date: isoDate,\n type,\n quantity,\n pricePerUnit,\n currency,\n totalLocal,\n totalEUR,\n feesEUR,\n fxRate,\n });\n }\n\n return transactions;\n }\n\n get warnings(): string[] {\n return this._warningEntries.map(warningToEnglish);\n }\n\n get warningEntries(): WarningEntry[] {\n return this._warningEntries;\n }\n}\n","import type {\n Transaction,\n MatchedLot,\n GainsReport,\n LotMethod,\n} from \"../types.js\";\nimport { CalculationError } from \"../errors.js\";\n\ninterface Lot {\n date: string;\n quantity: number;\n pricePerUnitEUR: number;\n feesEUR: number;\n originalQty: number;\n fxRate?: number;\n}\n\nfunction roundHalfUp(x: number): number {\n return (Math.sign(x) * Math.round(Math.abs(x) * 100)) / 100;\n}\n\nfunction inferTaxYear(transactions: Transaction[]): {\n year: number;\n multipleYears: boolean;\n} {\n const counts: Record<string, number> = {};\n for (const t of transactions) {\n const y = t.date.slice(0, 4);\n counts[y] = (counts[y] ?? 0) + 1;\n }\n const years = Object.keys(counts);\n if (years.length === 0)\n return { year: new Date().getFullYear(), multipleYears: false };\n const year = parseInt(\n years.reduce((a, b) => (counts[a] >= counts[b] ? a : b)),\n );\n return { year, multipleYears: years.length > 1 };\n}\n\nexport class Calculator {\n private readonly _transactions: Transaction[];\n private readonly _parseWarnings: string[];\n\n constructor(transactions: Transaction[], parseWarnings?: string[]) {\n this._transactions = transactions;\n this._parseWarnings = parseWarnings ?? [];\n }\n\n calculateGains(method: LotMethod): GainsReport {\n const warnings: string[] = [...this._parseWarnings];\n\n // 1. Sort ascending by date; BUY before SELL on same date\n const sorted = [...this._transactions].sort((a, b) => {\n if (a.date < b.date) return -1;\n if (a.date > b.date) return 1;\n // Same date: BUY before SELL\n if (a.type === \"BUY\" && b.type === \"SELL\") return -1;\n if (a.type === \"SELL\" && b.type === \"BUY\") return 1;\n return 0;\n });\n\n // 2. Tax year inference\n const { year: taxYear, multipleYears } = inferTaxYear(sorted);\n if (multipleYears) {\n warnings.push(\n \"CSV contains transactions from multiple years — filter to a single year for accurate reporting.\",\n );\n }\n\n // 3. Lot matching\n const openLots = new Map<string, Lot[]>();\n const matchedLots: MatchedLot[] = [];\n const ratesUsed: Record<string, number> = {};\n\n for (const tx of sorted) {\n // Collect rates used\n if (tx.fxRate !== undefined) {\n ratesUsed[`${tx.currency}:${tx.date}`] = tx.fxRate;\n }\n\n if (tx.type === \"BUY\") {\n const lot: Lot = {\n date: tx.date,\n quantity: tx.quantity,\n pricePerUnitEUR: tx.totalEUR / tx.quantity,\n feesEUR: tx.feesEUR,\n originalQty: tx.quantity,\n fxRate: tx.fxRate,\n };\n if (!openLots.has(tx.isin)) openLots.set(tx.isin, []);\n openLots.get(tx.isin)!.push(lot);\n } else {\n // SELL\n const lots = openLots.get(tx.isin);\n if (!lots || lots.length === 0) {\n throw new CalculationError(tx.isin, tx.date);\n }\n\n const sellPricePerUnitEUR = tx.totalEUR / tx.quantity;\n let remainingSellQty = tx.quantity;\n\n while (remainingSellQty > 0) {\n if (!lots || lots.length === 0) {\n throw new CalculationError(tx.isin, tx.date);\n }\n\n const lot = method === \"LIFO\" ? lots[lots.length - 1] : lots[0];\n const matchedQty = Math.min(lot.quantity, remainingSellQty);\n\n // Fee allocation\n const allocatedBuyFeesEUR =\n lot.feesEUR * (matchedQty / lot.originalQty);\n const allocatedSellFeesEUR = tx.feesEUR * (matchedQty / tx.quantity);\n\n const buyCostEUR =\n lot.pricePerUnitEUR * matchedQty + allocatedBuyFeesEUR;\n const sellProceedsEUR =\n sellPricePerUnitEUR * matchedQty - allocatedSellFeesEUR;\n const gainLossEUR = sellProceedsEUR - buyCostEUR;\n\n matchedLots.push({\n isin: tx.isin,\n product: tx.product,\n quantity: matchedQty,\n buyDate: lot.date,\n sellDate: tx.date,\n buyPriceEUR: lot.pricePerUnitEUR,\n sellPriceEUR: sellPricePerUnitEUR,\n buyCostEUR: roundHalfUp(buyCostEUR),\n sellProceedsEUR: roundHalfUp(sellProceedsEUR),\n gainLossEUR: roundHalfUp(gainLossEUR),\n buyFxRate: lot.fxRate,\n sellFxRate: tx.fxRate,\n });\n\n lot.quantity -= matchedQty;\n remainingSellQty -= matchedQty;\n\n if (lot.quantity <= 0) {\n if (method === \"LIFO\") {\n lots.pop();\n } else {\n lots.shift();\n }\n }\n }\n }\n }\n\n // 4. Aggregate\n let plusvalenze = 0;\n let minusvalenze = 0;\n for (const lot of matchedLots) {\n if (lot.gainLossEUR > 0) plusvalenze += lot.gainLossEUR;\n else minusvalenze += Math.abs(lot.gainLossEUR);\n }\n\n return {\n method,\n taxYear,\n plusvalenze: roundHalfUp(plusvalenze),\n minusvalenze: roundHalfUp(minusvalenze),\n netResult: roundHalfUp(plusvalenze - minusvalenze),\n lots: matchedLots,\n ratesUsed,\n warnings,\n generatedAt: new Date().toISOString(),\n };\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/rates/index.ts","../src/parser/warnings.ts","../src/parser/index.ts","../src/calculator/index.ts"],"sourcesContent":["export class ParseError extends Error {\n readonly code: \"INVALID_CSV\" | \"MISSING_COLUMN\";\n readonly columnName?: string;\n\n constructor(code: \"INVALID_CSV\");\n constructor(code: \"MISSING_COLUMN\", columnName: string);\n constructor(code: ParseError[\"code\"], columnName?: string) {\n const msg =\n code === \"INVALID_CSV\"\n ? \"Invalid CSV: unable to parse\"\n : `Missing required column: ${columnName}`;\n super(msg);\n this.name = \"ParseError\";\n this.code = code;\n this.columnName = columnName;\n }\n}\n\nexport class CalculationError extends Error {\n readonly isin: string;\n readonly date: string;\n\n constructor(isin: string, date: string) {\n super(`No open lots for ISIN ${isin} on ${date}`);\n this.name = \"CalculationError\";\n this.isin = isin;\n this.date = date;\n }\n}\n","import { fileURLToPath } from \"node:url\";\nimport * as path from \"node:path\";\nimport * as fs from \"node:fs\";\n\nexport type RatesSnapshot = Record<string, Record<string, number>>;\n\n// Load the bundled snapshot lazily; returns null if the file is absent (e.g. broken install)\nlet _bundled: RatesSnapshot | null | undefined = undefined;\n\nfunction getBundledSnapshot(): RatesSnapshot | null {\n if (_bundled !== undefined) return _bundled;\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n const bundledPath = path.join(__dirname, \"../data/ecb-rates.json\");\n try {\n _bundled = JSON.parse(\n fs.readFileSync(bundledPath, \"utf8\"),\n ) as RatesSnapshot;\n } catch {\n _bundled = null;\n }\n return _bundled;\n}\n\nfunction getUserSnapshot(): RatesSnapshot | null {\n const platform = process.platform;\n let configDir: string;\n if (platform === \"win32\") {\n configDir =\n process.env.APPDATA ??\n path.join(process.env.USERPROFILE ?? \"~\", \"AppData\", \"Roaming\");\n } else {\n configDir =\n process.env.XDG_CONFIG_HOME ??\n path.join(process.env.HOME ?? \"~\", \".config\");\n }\n const userPath = path.join(configDir, \"minus-tracker\", \"ecb-rates.json\");\n try {\n return JSON.parse(fs.readFileSync(userPath, \"utf8\")) as RatesSnapshot;\n } catch {\n return null;\n }\n}\n\nexport function getActiveSnapshot(): RatesSnapshot {\n const bundled = getBundledSnapshot();\n const user = getUserSnapshot();\n if (!bundled && !user) {\n throw new Error(\n \"No ECB rates snapshot available. Run `minus-tracker rates --update` to fetch rates.\",\n );\n }\n if (!bundled) return user!;\n if (!user) return bundled;\n // Merge: user entries override bundled for same date keys\n const merged: RatesSnapshot = {};\n for (const ccy of new Set([...Object.keys(bundled), ...Object.keys(user)])) {\n merged[ccy] = { ...(bundled[ccy] ?? {}), ...(user[ccy] ?? {}) };\n }\n return merged;\n}\n\nfunction subtractDays(isoDate: string, days: number): string {\n const d = new Date(isoDate + \"T00:00:00Z\");\n d.setUTCDate(d.getUTCDate() - days);\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * Look up ECB rate for a currency on a given date.\n * Returns 1.0 for EUR. Walks back up to 3 calendar days for weekend/holiday gaps.\n * Returns null if not found within the window.\n *\n * @param currency ISO 4217 currency code\n * @param date YYYY-MM-DD trade date\n * @param snapshot Optional override (used in tests to inject stub data)\n */\nexport function lookupRate(\n currency: string,\n date: string,\n snapshot?: RatesSnapshot,\n): number | null {\n if (currency === \"EUR\") return 1.0;\n\n const s = snapshot ?? getActiveSnapshot();\n const currencyRates = s[currency];\n if (!currencyRates) return null; // unsupported currency\n\n for (let i = 0; i <= 3; i++) {\n const d = i === 0 ? date : subtractDays(date, i);\n if (currencyRates[d] !== undefined) {\n return currencyRates[d];\n }\n }\n return null;\n}\n","export type WarningEntry =\n | { code: \"MISSING_ISIN\"; row: number }\n | { code: \"UNSUPPORTED_CURRENCY\"; row: number; currency: string }\n | { code: \"NO_ECB_RATE\"; row: number; currency: string; date: string }\n | { code: \"QUANTITY_ZERO\"; row: number };\n\nexport function warningToEnglish(w: WarningEntry): string {\n switch (w.code) {\n case \"MISSING_ISIN\":\n return `Row ${w.row}: missing ISIN — skipped`;\n case \"UNSUPPORTED_CURRENCY\":\n return `Row ${w.row}: unsupported currency ${w.currency} — skipped`;\n case \"NO_ECB_RATE\":\n return `Row ${w.row}: no ECB rate for ${w.currency} on ${w.date} — skipped`;\n case \"QUANTITY_ZERO\":\n return `Row ${w.row}: quantity is 0 — skipped`;\n }\n}\n","import { Transaction } from \"../types.js\";\nimport { ParseError } from \"../errors.js\";\nimport {\n lookupRate,\n getActiveSnapshot,\n RatesSnapshot,\n} from \"../rates/index.js\";\nimport { WarningEntry, warningToEnglish } from \"./warnings.js\";\n\nconst REQUIRED_COLUMNS = [\n \"ISIN\",\n \"Quantity\",\n \"Price\",\n \"Date\",\n \"Local value\",\n \"Local value currency\",\n \"Transaction costs\",\n \"Transaction costs currency\",\n \"Product\",\n] as const;\n\n/**\n * Parse a single CSV line, respecting RFC-4180-style double-quote escaping.\n * Does not support embedded newlines in fields (not needed for DEGIRO exports).\n */\nfunction parseCSVRow(line: string): string[] {\n const fields: string[] = [];\n let i = 0;\n\n while (i <= line.length) {\n // End of line — stop (handles trailing comma by not emitting extra empty field)\n if (i === line.length) break;\n\n if (line[i] === '\"') {\n // Quoted field\n let field = \"\";\n i++; // skip opening quote\n while (i < line.length) {\n if (line[i] === '\"' && i + 1 < line.length && line[i + 1] === '\"') {\n // Escaped double-quote\n field += '\"';\n i += 2;\n } else if (line[i] === '\"') {\n i++; // skip closing quote\n break;\n } else {\n field += line[i++];\n }\n }\n fields.push(field);\n if (i < line.length && line[i] === \",\") i++; // skip delimiter\n } else {\n // Unquoted field\n const start = i;\n while (i < line.length && line[i] !== \",\") i++;\n fields.push(line.slice(start, i));\n if (i < line.length) i++; // skip delimiter\n }\n }\n\n return fields;\n}\n\n/**\n * Parse a full CSV string (CRLF or LF line endings) into a 2-D array of strings.\n */\nfunction parseCSV(input: string): string[][] {\n const normalized = input.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n return normalized.split(\"\\n\").map(parseCSVRow);\n}\n\n/**\n * Convert a DEGIRO date \"DD-MM-YYYY\" to ISO format \"YYYY-MM-DD\".\n */\nfunction parseDate(ddmmyyyy: string): string {\n const [dd, mm, yyyy] = ddmmyyyy.split(\"-\");\n return `${yyyy}-${mm}-${dd}`;\n}\n\nexport class DEGIROParser {\n private _warningEntries: WarningEntry[] = [];\n private _snapshot?: RatesSnapshot;\n\n constructor(snapshot?: RatesSnapshot) {\n this._snapshot = snapshot;\n }\n\n parse(csv: string): Transaction[] {\n this._warningEntries = [];\n\n // Binary content (null bytes) is not valid CSV\n if (typeof csv !== \"string\" || csv.includes(\"\\x00\")) {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n let rows: string[][];\n try {\n rows = parseCSV(csv);\n } catch {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n if (rows.length === 0) {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n // Build column-name → index map from the header row\n const header = rows[0];\n const colIndex: Record<string, number> = {};\n for (let i = 0; i < header.length; i++) {\n colIndex[header[i].trim()] = i;\n }\n\n // Validate that every required column is present\n for (const col of REQUIRED_COLUMNS) {\n if (colIndex[col] === undefined) {\n throw new ParseError(\"MISSING_COLUMN\", col);\n }\n }\n\n // Resolve the rates snapshot once for the whole parse run\n const snapshot: RatesSnapshot = this._snapshot ?? getActiveSnapshot();\n\n const transactions: Transaction[] = [];\n\n for (let r = 1; r < rows.length; r++) {\n const row = rows[r];\n // 1-indexed row number: header = 1, first data row = 2\n const rowIndex = r + 1;\n\n // Skip completely empty rows (including the trailing empty line after a\n // final newline, and rows that are all whitespace)\n if (row.every((cell) => cell.trim() === \"\")) continue;\n\n const get = (col: string): string => (row[colIndex[col]] ?? \"\").trim();\n\n // --- ISIN ---\n const isin = get(\"ISIN\");\n if (!isin) {\n this._warningEntries.push({ code: \"MISSING_ISIN\", row: rowIndex });\n continue;\n }\n\n // --- Quantity ---\n const rawQty = parseFloat(get(\"Quantity\"));\n if (isNaN(rawQty) || rawQty === 0) {\n this._warningEntries.push({ code: \"QUANTITY_ZERO\", row: rowIndex });\n continue;\n }\n\n const type: \"BUY\" | \"SELL\" = rawQty > 0 ? \"BUY\" : \"SELL\";\n const quantity = Math.abs(rawQty);\n\n // --- Date ---\n const isoDate = parseDate(get(\"Date\"));\n\n // --- Currency & FX ---\n const currency = get(\"Local value currency\");\n const rawTotalLocal = parseFloat(get(\"Local value\"));\n const totalLocal = isNaN(rawTotalLocal) ? 0 : rawTotalLocal;\n\n let totalEUR: number;\n let fxRate: number | undefined;\n\n if (currency === \"EUR\") {\n totalEUR = Math.abs(totalLocal);\n fxRate = undefined;\n } else {\n const rate = lookupRate(currency, isoDate, snapshot);\n if (rate === null) {\n if (snapshot[currency] === undefined) {\n this._warningEntries.push({\n code: \"UNSUPPORTED_CURRENCY\",\n row: rowIndex,\n currency,\n });\n } else {\n this._warningEntries.push({\n code: \"NO_ECB_RATE\",\n row: rowIndex,\n currency,\n date: isoDate,\n });\n }\n continue;\n }\n totalEUR = Math.abs(totalLocal) / rate;\n fxRate = rate;\n }\n\n // --- Fees ---\n const rawFees = parseFloat(get(\"Transaction costs\"));\n const feesEUR = Math.abs(isNaN(rawFees) ? 0 : rawFees);\n\n // --- Remaining fields ---\n const pricePerUnit = parseFloat(get(\"Price\"));\n const product = get(\"Product\");\n\n transactions.push({\n isin,\n product,\n date: isoDate,\n type,\n quantity,\n pricePerUnit,\n currency,\n totalLocal,\n totalEUR,\n feesEUR,\n fxRate,\n });\n }\n\n return transactions;\n }\n\n get warnings(): string[] {\n return this._warningEntries.map(warningToEnglish);\n }\n\n get warningEntries(): WarningEntry[] {\n return this._warningEntries;\n }\n}\n","import type {\n Transaction,\n MatchedLot,\n GainsReport,\n LotMethod,\n} from \"../types.js\";\nimport { CalculationError } from \"../errors.js\";\n\ninterface Lot {\n date: string;\n quantity: number;\n pricePerUnitEUR: number;\n feesEUR: number;\n originalQty: number;\n fxRate?: number;\n}\n\nfunction roundHalfUp(x: number): number {\n return (Math.sign(x) * Math.round(Math.abs(x) * 100)) / 100;\n}\n\nfunction inferTaxYear(transactions: Transaction[]): {\n year: number;\n multipleYears: boolean;\n} {\n const counts: Record<string, number> = {};\n for (const t of transactions) {\n const y = t.date.slice(0, 4);\n counts[y] = (counts[y] ?? 0) + 1;\n }\n const years = Object.keys(counts);\n if (years.length === 0)\n return { year: new Date().getFullYear(), multipleYears: false };\n const year = parseInt(\n years.reduce((a, b) => (counts[a] >= counts[b] ? a : b)),\n );\n return { year, multipleYears: years.length > 1 };\n}\n\nexport class Calculator {\n private readonly _transactions: Transaction[];\n private readonly _parseWarnings: string[];\n\n constructor(transactions: Transaction[], parseWarnings?: string[]) {\n this._transactions = transactions;\n this._parseWarnings = parseWarnings ?? [];\n }\n\n calculateGains(method: LotMethod): GainsReport {\n const warnings: string[] = [...this._parseWarnings];\n\n // 1. Sort ascending by date; BUY before SELL on same date\n const sorted = [...this._transactions].sort((a, b) => {\n if (a.date < b.date) return -1;\n if (a.date > b.date) return 1;\n // Same date: BUY before SELL\n if (a.type === \"BUY\" && b.type === \"SELL\") return -1;\n if (a.type === \"SELL\" && b.type === \"BUY\") return 1;\n return 0;\n });\n\n // 2. Tax year inference\n const { year: taxYear, multipleYears } = inferTaxYear(sorted);\n if (multipleYears) {\n warnings.push(\n \"CSV contains transactions from multiple years — filter to a single year for accurate reporting.\",\n );\n }\n\n // 3. Lot matching\n const openLots = new Map<string, Lot[]>();\n const matchedLots: MatchedLot[] = [];\n const ratesUsed: Record<string, number> = {};\n\n for (const tx of sorted) {\n // Collect rates used\n if (tx.fxRate !== undefined) {\n ratesUsed[`${tx.currency}:${tx.date}`] = tx.fxRate;\n }\n\n if (tx.type === \"BUY\") {\n const lot: Lot = {\n date: tx.date,\n quantity: tx.quantity,\n pricePerUnitEUR: tx.totalEUR / tx.quantity,\n feesEUR: tx.feesEUR,\n originalQty: tx.quantity,\n fxRate: tx.fxRate,\n };\n if (!openLots.has(tx.isin)) openLots.set(tx.isin, []);\n openLots.get(tx.isin)!.push(lot);\n } else {\n // SELL\n const lots = openLots.get(tx.isin);\n if (!lots || lots.length === 0) {\n throw new CalculationError(tx.isin, tx.date);\n }\n\n const sellPricePerUnitEUR = tx.totalEUR / tx.quantity;\n let remainingSellQty = tx.quantity;\n\n while (remainingSellQty > 0) {\n if (!lots || lots.length === 0) {\n throw new CalculationError(tx.isin, tx.date);\n }\n\n const lot = method === \"LIFO\" ? lots[lots.length - 1] : lots[0];\n const matchedQty = Math.min(lot.quantity, remainingSellQty);\n\n // Fee allocation\n const allocatedBuyFeesEUR =\n lot.feesEUR * (matchedQty / lot.originalQty);\n const allocatedSellFeesEUR = tx.feesEUR * (matchedQty / tx.quantity);\n\n const buyCostEUR =\n lot.pricePerUnitEUR * matchedQty + allocatedBuyFeesEUR;\n const sellProceedsEUR =\n sellPricePerUnitEUR * matchedQty - allocatedSellFeesEUR;\n const gainLossEUR = sellProceedsEUR - buyCostEUR;\n\n matchedLots.push({\n isin: tx.isin,\n product: tx.product,\n quantity: matchedQty,\n buyDate: lot.date,\n sellDate: tx.date,\n buyPriceEUR: lot.pricePerUnitEUR,\n sellPriceEUR: sellPricePerUnitEUR,\n buyCostEUR: roundHalfUp(buyCostEUR),\n sellProceedsEUR: roundHalfUp(sellProceedsEUR),\n gainLossEUR: roundHalfUp(gainLossEUR),\n buyFxRate: lot.fxRate,\n sellFxRate: tx.fxRate,\n });\n\n lot.quantity -= matchedQty;\n remainingSellQty -= matchedQty;\n\n if (lot.quantity <= 0) {\n if (method === \"LIFO\") {\n lots.pop();\n } else {\n lots.shift();\n }\n }\n }\n }\n }\n\n // 4. Aggregate\n let plusvalenze = 0;\n let minusvalenze = 0;\n for (const lot of matchedLots) {\n if (lot.gainLossEUR > 0) plusvalenze += lot.gainLossEUR;\n else minusvalenze += Math.abs(lot.gainLossEUR);\n }\n\n return {\n method,\n taxYear,\n plusvalenze: roundHalfUp(plusvalenze),\n minusvalenze: roundHalfUp(minusvalenze),\n netResult: roundHalfUp(plusvalenze - minusvalenze),\n lots: matchedLots,\n ratesUsed,\n warnings,\n generatedAt: new Date().toISOString(),\n };\n }\n}\n"],"mappings":";AAAO,IAAM,aAAN,cAAyB,MAAM;AAAA,EAC3B;AAAA,EACA;AAAA,EAIT,YAAY,MAA0B,YAAqB;AACzD,UAAM,MACJ,SAAS,gBACL,iCACA,4BAA4B,UAAU;AAC5C,UAAM,GAAG;AACT,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,aAAa;AAAA,EACpB;AACF;AAEO,IAAM,mBAAN,cAA+B,MAAM;AAAA,EACjC;AAAA,EACA;AAAA,EAET,YAAY,MAAc,MAAc;AACtC,UAAM,yBAAyB,IAAI,OAAO,IAAI,EAAE;AAChD,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;;;AC5BA,SAAS,qBAAqB;AAC9B,YAAY,UAAU;AACtB,YAAY,QAAQ;AAKpB,IAAI,WAA6C;AAEjD,SAAS,qBAA2C;AAClD,MAAI,aAAa,OAAW,QAAO;AACnC,QAAM,YAAiB,aAAQ,cAAc,YAAY,GAAG,CAAC;AAC7D,QAAM,cAAmB,UAAK,WAAW,wBAAwB;AACjE,MAAI;AACF,eAAW,KAAK;AAAA,MACX,gBAAa,aAAa,MAAM;AAAA,IACrC;AAAA,EACF,QAAQ;AACN,eAAW;AAAA,EACb;AACA,SAAO;AACT;AAEA,SAAS,kBAAwC;AAC/C,QAAM,WAAW,QAAQ;AACzB,MAAI;AACJ,MAAI,aAAa,SAAS;AACxB,gBACE,QAAQ,IAAI,WACP,UAAK,QAAQ,IAAI,eAAe,KAAK,WAAW,SAAS;AAAA,EAClE,OAAO;AACL,gBACE,QAAQ,IAAI,mBACP,UAAK,QAAQ,IAAI,QAAQ,KAAK,SAAS;AAAA,EAChD;AACA,QAAM,WAAgB,UAAK,WAAW,iBAAiB,gBAAgB;AACvE,MAAI;AACF,WAAO,KAAK,MAAS,gBAAa,UAAU,MAAM,CAAC;AAAA,EACrD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,oBAAmC;AACjD,QAAM,UAAU,mBAAmB;AACnC,QAAM,OAAO,gBAAgB;AAC7B,MAAI,CAAC,WAAW,CAAC,MAAM;AACrB,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,CAAC,KAAM,QAAO;AAElB,QAAM,SAAwB,CAAC;AAC/B,aAAW,OAAO,oBAAI,IAAI,CAAC,GAAG,OAAO,KAAK,OAAO,GAAG,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC,GAAG;AAC1E,WAAO,GAAG,IAAI,EAAE,GAAI,QAAQ,GAAG,KAAK,CAAC,GAAI,GAAI,KAAK,GAAG,KAAK,CAAC,EAAG;AAAA,EAChE;AACA,SAAO;AACT;AAEA,SAAS,aAAa,SAAiB,MAAsB;AAC3D,QAAM,IAAI,oBAAI,KAAK,UAAU,YAAY;AACzC,IAAE,WAAW,EAAE,WAAW,IAAI,IAAI;AAClC,SAAO,EAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACpC;AAWO,SAAS,WACd,UACA,MACA,UACe;AACf,MAAI,aAAa,MAAO,QAAO;AAE/B,QAAM,IAAI,YAAY,kBAAkB;AACxC,QAAM,gBAAgB,EAAE,QAAQ;AAChC,MAAI,CAAC,cAAe,QAAO;AAE3B,WAAS,IAAI,GAAG,KAAK,GAAG,KAAK;AAC3B,UAAM,IAAI,MAAM,IAAI,OAAO,aAAa,MAAM,CAAC;AAC/C,QAAI,cAAc,CAAC,MAAM,QAAW;AAClC,aAAO,cAAc,CAAC;AAAA,IACxB;AAAA,EACF;AACA,SAAO;AACT;;;ACxFO,SAAS,iBAAiB,GAAyB;AACxD,UAAQ,EAAE,MAAM;AAAA,IACd,KAAK;AACH,aAAO,OAAO,EAAE,GAAG;AAAA,IACrB,KAAK;AACH,aAAO,OAAO,EAAE,GAAG,0BAA0B,EAAE,QAAQ;AAAA,IACzD,KAAK;AACH,aAAO,OAAO,EAAE,GAAG,qBAAqB,EAAE,QAAQ,OAAO,EAAE,IAAI;AAAA,IACjE,KAAK;AACH,aAAO,OAAO,EAAE,GAAG;AAAA,EACvB;AACF;;;ACRA,IAAM,mBAAmB;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAMA,SAAS,YAAY,MAAwB;AAC3C,QAAM,SAAmB,CAAC;AAC1B,MAAI,IAAI;AAER,SAAO,KAAK,KAAK,QAAQ;AAEvB,QAAI,MAAM,KAAK,OAAQ;AAEvB,QAAI,KAAK,CAAC,MAAM,KAAK;AAEnB,UAAI,QAAQ;AACZ;AACA,aAAO,IAAI,KAAK,QAAQ;AACtB,YAAI,KAAK,CAAC,MAAM,OAAO,IAAI,IAAI,KAAK,UAAU,KAAK,IAAI,CAAC,MAAM,KAAK;AAEjE,mBAAS;AACT,eAAK;AAAA,QACP,WAAW,KAAK,CAAC,MAAM,KAAK;AAC1B;AACA;AAAA,QACF,OAAO;AACL,mBAAS,KAAK,GAAG;AAAA,QACnB;AAAA,MACF;AACA,aAAO,KAAK,KAAK;AACjB,UAAI,IAAI,KAAK,UAAU,KAAK,CAAC,MAAM,IAAK;AAAA,IAC1C,OAAO;AAEL,YAAM,QAAQ;AACd,aAAO,IAAI,KAAK,UAAU,KAAK,CAAC,MAAM,IAAK;AAC3C,aAAO,KAAK,KAAK,MAAM,OAAO,CAAC,CAAC;AAChC,UAAI,IAAI,KAAK,OAAQ;AAAA,IACvB;AAAA,EACF;AAEA,SAAO;AACT;AAKA,SAAS,SAAS,OAA2B;AAC3C,QAAM,aAAa,MAAM,QAAQ,SAAS,IAAI,EAAE,QAAQ,OAAO,IAAI;AACnE,SAAO,WAAW,MAAM,IAAI,EAAE,IAAI,WAAW;AAC/C;AAKA,SAAS,UAAU,UAA0B;AAC3C,QAAM,CAAC,IAAI,IAAI,IAAI,IAAI,SAAS,MAAM,GAAG;AACzC,SAAO,GAAG,IAAI,IAAI,EAAE,IAAI,EAAE;AAC5B;AAEO,IAAM,eAAN,MAAmB;AAAA,EAChB,kBAAkC,CAAC;AAAA,EACnC;AAAA,EAER,YAAY,UAA0B;AACpC,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,MAAM,KAA4B;AAChC,SAAK,kBAAkB,CAAC;AAGxB,QAAI,OAAO,QAAQ,YAAY,IAAI,SAAS,IAAM,GAAG;AACnD,YAAM,IAAI,WAAW,aAAa;AAAA,IACpC;AAEA,QAAI;AACJ,QAAI;AACF,aAAO,SAAS,GAAG;AAAA,IACrB,QAAQ;AACN,YAAM,IAAI,WAAW,aAAa;AAAA,IACpC;AAEA,QAAI,KAAK,WAAW,GAAG;AACrB,YAAM,IAAI,WAAW,aAAa;AAAA,IACpC;AAGA,UAAM,SAAS,KAAK,CAAC;AACrB,UAAM,WAAmC,CAAC;AAC1C,aAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,eAAS,OAAO,CAAC,EAAE,KAAK,CAAC,IAAI;AAAA,IAC/B;AAGA,eAAW,OAAO,kBAAkB;AAClC,UAAI,SAAS,GAAG,MAAM,QAAW;AAC/B,cAAM,IAAI,WAAW,kBAAkB,GAAG;AAAA,MAC5C;AAAA,IACF;AAGA,UAAM,WAA0B,KAAK,aAAa,kBAAkB;AAEpE,UAAM,eAA8B,CAAC;AAErC,aAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,YAAM,MAAM,KAAK,CAAC;AAElB,YAAM,WAAW,IAAI;AAIrB,UAAI,IAAI,MAAM,CAAC,SAAS,KAAK,KAAK,MAAM,EAAE,EAAG;AAE7C,YAAM,MAAM,CAAC,SAAyB,IAAI,SAAS,GAAG,CAAC,KAAK,IAAI,KAAK;AAGrE,YAAM,OAAO,IAAI,MAAM;AACvB,UAAI,CAAC,MAAM;AACT,aAAK,gBAAgB,KAAK,EAAE,MAAM,gBAAgB,KAAK,SAAS,CAAC;AACjE;AAAA,MACF;AAGA,YAAM,SAAS,WAAW,IAAI,UAAU,CAAC;AACzC,UAAI,MAAM,MAAM,KAAK,WAAW,GAAG;AACjC,aAAK,gBAAgB,KAAK,EAAE,MAAM,iBAAiB,KAAK,SAAS,CAAC;AAClE;AAAA,MACF;AAEA,YAAM,OAAuB,SAAS,IAAI,QAAQ;AAClD,YAAM,WAAW,KAAK,IAAI,MAAM;AAGhC,YAAM,UAAU,UAAU,IAAI,MAAM,CAAC;AAGrC,YAAM,WAAW,IAAI,sBAAsB;AAC3C,YAAM,gBAAgB,WAAW,IAAI,aAAa,CAAC;AACnD,YAAM,aAAa,MAAM,aAAa,IAAI,IAAI;AAE9C,UAAI;AACJ,UAAI;AAEJ,UAAI,aAAa,OAAO;AACtB,mBAAW,KAAK,IAAI,UAAU;AAC9B,iBAAS;AAAA,MACX,OAAO;AACL,cAAM,OAAO,WAAW,UAAU,SAAS,QAAQ;AACnD,YAAI,SAAS,MAAM;AACjB,cAAI,SAAS,QAAQ,MAAM,QAAW;AACpC,iBAAK,gBAAgB,KAAK;AAAA,cACxB,MAAM;AAAA,cACN,KAAK;AAAA,cACL;AAAA,YACF,CAAC;AAAA,UACH,OAAO;AACL,iBAAK,gBAAgB,KAAK;AAAA,cACxB,MAAM;AAAA,cACN,KAAK;AAAA,cACL;AAAA,cACA,MAAM;AAAA,YACR,CAAC;AAAA,UACH;AACA;AAAA,QACF;AACA,mBAAW,KAAK,IAAI,UAAU,IAAI;AAClC,iBAAS;AAAA,MACX;AAGA,YAAM,UAAU,WAAW,IAAI,mBAAmB,CAAC;AACnD,YAAM,UAAU,KAAK,IAAI,MAAM,OAAO,IAAI,IAAI,OAAO;AAGrD,YAAM,eAAe,WAAW,IAAI,OAAO,CAAC;AAC5C,YAAM,UAAU,IAAI,SAAS;AAE7B,mBAAa,KAAK;AAAA,QAChB;AAAA,QACA;AAAA,QACA,MAAM;AAAA,QACN;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,IAAI,WAAqB;AACvB,WAAO,KAAK,gBAAgB,IAAI,gBAAgB;AAAA,EAClD;AAAA,EAEA,IAAI,iBAAiC;AACnC,WAAO,KAAK;AAAA,EACd;AACF;;;AC9MA,SAAS,YAAY,GAAmB;AACtC,SAAQ,KAAK,KAAK,CAAC,IAAI,KAAK,MAAM,KAAK,IAAI,CAAC,IAAI,GAAG,IAAK;AAC1D;AAEA,SAAS,aAAa,cAGpB;AACA,QAAM,SAAiC,CAAC;AACxC,aAAW,KAAK,cAAc;AAC5B,UAAM,IAAI,EAAE,KAAK,MAAM,GAAG,CAAC;AAC3B,WAAO,CAAC,KAAK,OAAO,CAAC,KAAK,KAAK;AAAA,EACjC;AACA,QAAM,QAAQ,OAAO,KAAK,MAAM;AAChC,MAAI,MAAM,WAAW;AACnB,WAAO,EAAE,OAAM,oBAAI,KAAK,GAAE,YAAY,GAAG,eAAe,MAAM;AAChE,QAAM,OAAO;AAAA,IACX,MAAM,OAAO,CAAC,GAAG,MAAO,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,IAAI,CAAE;AAAA,EACzD;AACA,SAAO,EAAE,MAAM,eAAe,MAAM,SAAS,EAAE;AACjD;AAEO,IAAM,aAAN,MAAiB;AAAA,EACL;AAAA,EACA;AAAA,EAEjB,YAAY,cAA6B,eAA0B;AACjE,SAAK,gBAAgB;AACrB,SAAK,iBAAiB,iBAAiB,CAAC;AAAA,EAC1C;AAAA,EAEA,eAAe,QAAgC;AAC7C,UAAM,WAAqB,CAAC,GAAG,KAAK,cAAc;AAGlD,UAAM,SAAS,CAAC,GAAG,KAAK,aAAa,EAAE,KAAK,CAAC,GAAG,MAAM;AACpD,UAAI,EAAE,OAAO,EAAE,KAAM,QAAO;AAC5B,UAAI,EAAE,OAAO,EAAE,KAAM,QAAO;AAE5B,UAAI,EAAE,SAAS,SAAS,EAAE,SAAS,OAAQ,QAAO;AAClD,UAAI,EAAE,SAAS,UAAU,EAAE,SAAS,MAAO,QAAO;AAClD,aAAO;AAAA,IACT,CAAC;AAGD,UAAM,EAAE,MAAM,SAAS,cAAc,IAAI,aAAa,MAAM;AAC5D,QAAI,eAAe;AACjB,eAAS;AAAA,QACP;AAAA,MACF;AAAA,IACF;AAGA,UAAM,WAAW,oBAAI,IAAmB;AACxC,UAAM,cAA4B,CAAC;AACnC,UAAM,YAAoC,CAAC;AAE3C,eAAW,MAAM,QAAQ;AAEvB,UAAI,GAAG,WAAW,QAAW;AAC3B,kBAAU,GAAG,GAAG,QAAQ,IAAI,GAAG,IAAI,EAAE,IAAI,GAAG;AAAA,MAC9C;AAEA,UAAI,GAAG,SAAS,OAAO;AACrB,cAAM,MAAW;AAAA,UACf,MAAM,GAAG;AAAA,UACT,UAAU,GAAG;AAAA,UACb,iBAAiB,GAAG,WAAW,GAAG;AAAA,UAClC,SAAS,GAAG;AAAA,UACZ,aAAa,GAAG;AAAA,UAChB,QAAQ,GAAG;AAAA,QACb;AACA,YAAI,CAAC,SAAS,IAAI,GAAG,IAAI,EAAG,UAAS,IAAI,GAAG,MAAM,CAAC,CAAC;AACpD,iBAAS,IAAI,GAAG,IAAI,EAAG,KAAK,GAAG;AAAA,MACjC,OAAO;AAEL,cAAM,OAAO,SAAS,IAAI,GAAG,IAAI;AACjC,YAAI,CAAC,QAAQ,KAAK,WAAW,GAAG;AAC9B,gBAAM,IAAI,iBAAiB,GAAG,MAAM,GAAG,IAAI;AAAA,QAC7C;AAEA,cAAM,sBAAsB,GAAG,WAAW,GAAG;AAC7C,YAAI,mBAAmB,GAAG;AAE1B,eAAO,mBAAmB,GAAG;AAC3B,cAAI,CAAC,QAAQ,KAAK,WAAW,GAAG;AAC9B,kBAAM,IAAI,iBAAiB,GAAG,MAAM,GAAG,IAAI;AAAA,UAC7C;AAEA,gBAAM,MAAM,WAAW,SAAS,KAAK,KAAK,SAAS,CAAC,IAAI,KAAK,CAAC;AAC9D,gBAAM,aAAa,KAAK,IAAI,IAAI,UAAU,gBAAgB;AAG1D,gBAAM,sBACJ,IAAI,WAAW,aAAa,IAAI;AAClC,gBAAM,uBAAuB,GAAG,WAAW,aAAa,GAAG;AAE3D,gBAAM,aACJ,IAAI,kBAAkB,aAAa;AACrC,gBAAM,kBACJ,sBAAsB,aAAa;AACrC,gBAAM,cAAc,kBAAkB;AAEtC,sBAAY,KAAK;AAAA,YACf,MAAM,GAAG;AAAA,YACT,SAAS,GAAG;AAAA,YACZ,UAAU;AAAA,YACV,SAAS,IAAI;AAAA,YACb,UAAU,GAAG;AAAA,YACb,aAAa,IAAI;AAAA,YACjB,cAAc;AAAA,YACd,YAAY,YAAY,UAAU;AAAA,YAClC,iBAAiB,YAAY,eAAe;AAAA,YAC5C,aAAa,YAAY,WAAW;AAAA,YACpC,WAAW,IAAI;AAAA,YACf,YAAY,GAAG;AAAA,UACjB,CAAC;AAED,cAAI,YAAY;AAChB,8BAAoB;AAEpB,cAAI,IAAI,YAAY,GAAG;AACrB,gBAAI,WAAW,QAAQ;AACrB,mBAAK,IAAI;AAAA,YACX,OAAO;AACL,mBAAK,MAAM;AAAA,YACb;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,QAAI,cAAc;AAClB,QAAI,eAAe;AACnB,eAAW,OAAO,aAAa;AAC7B,UAAI,IAAI,cAAc,EAAG,gBAAe,IAAI;AAAA,UACvC,iBAAgB,KAAK,IAAI,IAAI,WAAW;AAAA,IAC/C;AAEA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,aAAa,YAAY,WAAW;AAAA,MACpC,cAAc,YAAY,YAAY;AAAA,MACtC,WAAW,YAAY,cAAc,YAAY;AAAA,MACjD,MAAM;AAAA,MACN;AAAA,MACA;AAAA,MACA,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACtC;AAAA,EACF;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gabrielerandelli/minus-tracker",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Italian capital-gains/loss calculator for the Regime Dichiarativo",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"dist"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"build": "tsup",
|
|
19
|
+
"build": "tsup && node scripts/copy-assets.js",
|
|
20
20
|
"test": "vitest run",
|
|
21
21
|
"test:watch": "vitest"
|
|
22
22
|
},
|