@gabrielerandelli/minus-tracker 0.6.0 → 0.8.0
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 +150 -30
- package/dist/cli/index.js +751 -240
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +474 -90
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +79 -2
- package/dist/index.d.ts +79 -2
- package/dist/index.js +474 -90
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +1885 -0
- package/dist/mcp/index.js.map +1 -0
- package/package.json +18 -5
package/dist/cli/index.js
CHANGED
|
@@ -13,6 +13,8 @@ var it = {
|
|
|
13
13
|
warnUnsupportedCurrency: (row, currency) => `Riga ${row}: valuta non supportata ${currency} \u2014 riga ignorata`,
|
|
14
14
|
warnNoEcbRate: (row, currency, date) => `Riga ${row}: nessun tasso BCE per ${currency} in data ${date} \u2014 riga ignorata`,
|
|
15
15
|
warnQuantityZero: (row) => `Riga ${row}: quantit\xE0 pari a 0 \u2014 riga ignorata`,
|
|
16
|
+
warnMissingIsinIncome: (row) => `Riga ${row}: ISIN mancante su riga di reddito \u2014 riga ignorata`,
|
|
17
|
+
warnOrphanWithholding: (isin, date) => `Riga di ritenuta su ${isin}/${date} senza riga di reddito corrispondente \u2014 riga ignorata`,
|
|
16
18
|
warnMultipleYears: "Il CSV contiene transazioni di pi\xF9 anni \u2014 filtra per un singolo anno per un calcolo accurato.",
|
|
17
19
|
headerMethod: "METODO",
|
|
18
20
|
headerTaxYear: "ANNO FISCALE",
|
|
@@ -64,7 +66,30 @@ var it = {
|
|
|
64
66
|
headerBucket: "BUCKET",
|
|
65
67
|
warnUnclassifiedIsin: (isin) => `ISIN ${isin} non trovato nella mappa di classificazione \u2014 assegnato a Bucket B.`,
|
|
66
68
|
carryForwardInvalidFormat: "Formato --carry-forward non valido. Usa: AAAA:importo (es. 2023:2500)",
|
|
67
|
-
disclaimer: "minus-tracker \xE8 un ausilio al calcolo, non consulenza fiscale."
|
|
69
|
+
disclaimer: "minus-tracker \xE8 un ausilio al calcolo, non consulenza fiscale.",
|
|
70
|
+
// v0.7.0 dichiarazione section
|
|
71
|
+
dichiarazioneHeader: (anno) => `\u2500\u2500 MODELLO REDDITI PF [ANNO D'IMPOSTA: ${anno}]`,
|
|
72
|
+
dichiarazioneNota: "Nota: i valori riportano i codici riga indicativi \u2014 verificare la modulistica ufficiale.",
|
|
73
|
+
dichiarazioneWarningRow: "\u26A0 I codici riga tra parentesi quadre sono indicativi. Verificare la versione corrente del modello.",
|
|
74
|
+
quadroRTHeader: "QUADRO RT \u2014 SEZIONE II (redditi diversi)",
|
|
75
|
+
quadroRTPlusvalenze: "[RT-P] Plusvalenze totali",
|
|
76
|
+
quadroRTMinusvalenze: "[RT-M] Minusvalenze totali",
|
|
77
|
+
quadroRTDifferenza: "[RT-D] Differenza",
|
|
78
|
+
quadroRTRiporto: (anno) => `[RT-C*] Riporto ${anno} (consumato)`,
|
|
79
|
+
quadroRTImponibile: "[RT-N] Imponibile netto",
|
|
80
|
+
quadroRTImposta: "[RT-I] Imposta (26%)",
|
|
81
|
+
quadroRTRiportabile: "[RT-R] Minusvalenze da riportare",
|
|
82
|
+
quadroRTPerdita: "perdita netta:",
|
|
83
|
+
quadroRMHeader: "QUADRO RM (redditi da capitale)",
|
|
84
|
+
quadroRMEtf26: "[RM-A1] ETF/OICR plusvalenze (26%)",
|
|
85
|
+
quadroRMImposta: "Imposta sostitutiva",
|
|
86
|
+
quadroRMBtp: "[RM-A2] BTP/WL plusvalenze (12,5%)",
|
|
87
|
+
quadroRMDividendi: "[RM-D] Dividendi esteri lordi",
|
|
88
|
+
quadroRMRitenuta: "Ritenuta estera (credito)",
|
|
89
|
+
quadroRMCedole: "[RM-C] Cedole obbligazionarie",
|
|
90
|
+
dichiarazioneDisclaimer: "minus-tracker \xE8 un ausilio al calcolo, non consulenza fiscale.",
|
|
91
|
+
warnNoDichiarazioneSidecar: "Eseguire prima: minus-tracker classify <file>",
|
|
92
|
+
warnNoCarryForwardProvided: "(eventuali perdite pregresse non applicate)"
|
|
68
93
|
};
|
|
69
94
|
|
|
70
95
|
// src/i18n/en.ts
|
|
@@ -77,6 +102,8 @@ var en = {
|
|
|
77
102
|
warnUnsupportedCurrency: (row, currency) => `Row ${row}: unsupported currency ${currency} \u2014 skipped`,
|
|
78
103
|
warnNoEcbRate: (row, currency, date) => `Row ${row}: no ECB rate for ${currency} on ${date} \u2014 skipped`,
|
|
79
104
|
warnQuantityZero: (row) => `Row ${row}: quantity is 0 \u2014 skipped`,
|
|
105
|
+
warnMissingIsinIncome: (row) => `Row ${row}: income row has blank ISIN \u2014 skipped`,
|
|
106
|
+
warnOrphanWithholding: (isin, date) => `Withholding row on ${isin}/${date} has no matching income row \u2014 skipped`,
|
|
80
107
|
warnMultipleYears: "CSV contains transactions from multiple years \u2014 filter to a single year for accurate reporting.",
|
|
81
108
|
headerMethod: "METHOD",
|
|
82
109
|
headerTaxYear: "TAX YEAR",
|
|
@@ -128,7 +155,30 @@ var en = {
|
|
|
128
155
|
headerBucket: "BUCKET",
|
|
129
156
|
warnUnclassifiedIsin: (isin) => `ISIN ${isin} not found in classification map \u2014 assigned to Bucket B.`,
|
|
130
157
|
carryForwardInvalidFormat: "Invalid --carry-forward format. Use: YYYY:amount (e.g. 2023:2500)",
|
|
131
|
-
disclaimer: "minus-tracker \xE8 un ausilio al calcolo, non consulenza fiscale."
|
|
158
|
+
disclaimer: "minus-tracker \xE8 un ausilio al calcolo, non consulenza fiscale.",
|
|
159
|
+
// v0.7.0 dichiarazione section
|
|
160
|
+
dichiarazioneHeader: (anno) => `\u2500\u2500 MODELLO REDDITI PF [TAX YEAR: ${anno}]`,
|
|
161
|
+
dichiarazioneNota: "Note: row codes are indicative \u2014 verify against the official tax form.",
|
|
162
|
+
dichiarazioneWarningRow: "\u26A0 Row codes in square brackets are indicative. Check the current form version.",
|
|
163
|
+
quadroRTHeader: "QUADRO RT \u2014 SECTION II (capital gains)",
|
|
164
|
+
quadroRTPlusvalenze: "[RT-P] Total gains",
|
|
165
|
+
quadroRTMinusvalenze: "[RT-M] Total losses",
|
|
166
|
+
quadroRTDifferenza: "[RT-D] Difference",
|
|
167
|
+
quadroRTRiporto: (anno) => `[RT-C*] Prior loss ${anno} (applied)`,
|
|
168
|
+
quadroRTImponibile: "[RT-N] Net taxable",
|
|
169
|
+
quadroRTImposta: "[RT-I] Tax (26%)",
|
|
170
|
+
quadroRTRiportabile: "[RT-R] Losses to carry forward",
|
|
171
|
+
quadroRTPerdita: "net loss:",
|
|
172
|
+
quadroRMHeader: "QUADRO RM (capital income)",
|
|
173
|
+
quadroRMEtf26: "[RM-A1] ETF/OICR gains (26%)",
|
|
174
|
+
quadroRMImposta: "Substitute tax",
|
|
175
|
+
quadroRMBtp: "[RM-A2] BTP/WL gains (12.5%)",
|
|
176
|
+
quadroRMDividendi: "[RM-D] Foreign dividends (gross)",
|
|
177
|
+
quadroRMRitenuta: "Foreign withholding (credit)",
|
|
178
|
+
quadroRMCedole: "[RM-C] Bond coupons",
|
|
179
|
+
dichiarazioneDisclaimer: "minus-tracker \xE8 un ausilio al calcolo, non consulenza fiscale.",
|
|
180
|
+
warnNoDichiarazioneSidecar: "Run first: minus-tracker classify <file>",
|
|
181
|
+
warnNoCarryForwardProvided: "(prior-year losses not applied)"
|
|
132
182
|
};
|
|
133
183
|
|
|
134
184
|
// src/i18n/settings.ts
|
|
@@ -223,9 +273,9 @@ var ClassificationError = class extends Error {
|
|
|
223
273
|
};
|
|
224
274
|
|
|
225
275
|
// src/cli/commands/calc.ts
|
|
226
|
-
import * as
|
|
227
|
-
import * as
|
|
228
|
-
import * as
|
|
276
|
+
import * as fs4 from "fs";
|
|
277
|
+
import * as path3 from "path";
|
|
278
|
+
import * as os2 from "os";
|
|
229
279
|
|
|
230
280
|
// src/rates/index.ts
|
|
231
281
|
import { fileURLToPath } from "url";
|
|
@@ -276,18 +326,6 @@ function getActiveSnapshot() {
|
|
|
276
326
|
}
|
|
277
327
|
return merged;
|
|
278
328
|
}
|
|
279
|
-
function isSnapshotStale(snapshot, today) {
|
|
280
|
-
const todayStr = today ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
281
|
-
let maxDate = "0000-01-01";
|
|
282
|
-
for (const dates of Object.values(snapshot)) {
|
|
283
|
-
for (const d of Object.keys(dates)) {
|
|
284
|
-
if (d > maxDate) maxDate = d;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
if (maxDate === "0000-01-01") return true;
|
|
288
|
-
const diffDays = (new Date(todayStr).getTime() - new Date(maxDate).getTime()) / (1e3 * 60 * 60 * 24);
|
|
289
|
-
return diffDays >= 7;
|
|
290
|
-
}
|
|
291
329
|
function subtractDays(isoDate, days) {
|
|
292
330
|
const d = /* @__PURE__ */ new Date(isoDate + "T00:00:00Z");
|
|
293
331
|
d.setUTCDate(d.getUTCDate() - days);
|
|
@@ -318,6 +356,10 @@ function warningToEnglish(w) {
|
|
|
318
356
|
return `Row ${w.row}: no ECB rate for ${w.currency} on ${w.date} \u2014 skipped`;
|
|
319
357
|
case "QUANTITY_ZERO":
|
|
320
358
|
return `Row ${w.row}: quantity is 0 \u2014 skipped`;
|
|
359
|
+
case "MISSING_ISIN_INCOME":
|
|
360
|
+
return `Row ${w.row}: blank ISIN on income row \u2014 skipped`;
|
|
361
|
+
case "ORPHAN_WITHHOLDING":
|
|
362
|
+
return `Withholding row for ${w.isin} on ${w.date}: no matching income row \u2014 skipped`;
|
|
321
363
|
}
|
|
322
364
|
}
|
|
323
365
|
|
|
@@ -333,6 +375,8 @@ var REQUIRED_COLUMNS = [
|
|
|
333
375
|
"Transaction costs currency",
|
|
334
376
|
"Product"
|
|
335
377
|
];
|
|
378
|
+
var INCOME_KEYWORDS = ["DIVIDEND", "COUPON", "INTEREST", "CEDOLA"];
|
|
379
|
+
var TAX_KEYWORDS = ["DIVIDEND TAX", "WITHHOLDING", "RITENUTA"];
|
|
336
380
|
function parseCSVRow(line) {
|
|
337
381
|
const fields = [];
|
|
338
382
|
let i = 0;
|
|
@@ -374,6 +418,7 @@ function parseDate(ddmmyyyy) {
|
|
|
374
418
|
var DEGIROParser = class {
|
|
375
419
|
_warningEntries = [];
|
|
376
420
|
_snapshot;
|
|
421
|
+
_incomeRows = [];
|
|
377
422
|
constructor(snapshot) {
|
|
378
423
|
this._snapshot = snapshot;
|
|
379
424
|
}
|
|
@@ -390,6 +435,7 @@ var DEGIROParser = class {
|
|
|
390
435
|
*/
|
|
391
436
|
parse(csv) {
|
|
392
437
|
this._warningEntries = [];
|
|
438
|
+
this._incomeRows = [];
|
|
393
439
|
if (typeof csv !== "string" || csv.includes("\0")) {
|
|
394
440
|
throw new ParseError("INVALID_CSV");
|
|
395
441
|
}
|
|
@@ -414,17 +460,70 @@ var DEGIROParser = class {
|
|
|
414
460
|
}
|
|
415
461
|
const snapshot = this._snapshot ?? getActiveSnapshot();
|
|
416
462
|
const transactions = [];
|
|
463
|
+
const incomeCandidates = [];
|
|
464
|
+
const withholdingCandidates = [];
|
|
417
465
|
for (let r = 1; r < rows.length; r++) {
|
|
418
466
|
const row = rows[r];
|
|
419
467
|
const rowIndex = r + 1;
|
|
420
468
|
if (row.every((cell) => cell.trim() === "")) continue;
|
|
421
469
|
const get2 = (col) => (row[colIndex[col]] ?? "").trim();
|
|
422
470
|
const isin = get2("ISIN");
|
|
471
|
+
const product = get2("Product");
|
|
472
|
+
const qtyStr = get2("Quantity");
|
|
473
|
+
const rawQty = parseFloat(qtyStr);
|
|
474
|
+
const qtyIsZeroOrBlank = qtyStr === "" || isNaN(rawQty) || rawQty === 0;
|
|
475
|
+
if (qtyIsZeroOrBlank) {
|
|
476
|
+
const upperProduct = product.toUpperCase();
|
|
477
|
+
const isTaxKeyword = TAX_KEYWORDS.some((k) => upperProduct.includes(k));
|
|
478
|
+
const isIncomeKeyword = !isTaxKeyword && INCOME_KEYWORDS.some((k) => upperProduct.includes(k));
|
|
479
|
+
if (isTaxKeyword || isIncomeKeyword) {
|
|
480
|
+
const rawLocalValue = parseFloat(get2("Local value"));
|
|
481
|
+
const localValue = isNaN(rawLocalValue) ? 0 : rawLocalValue;
|
|
482
|
+
const isoDate2 = parseDate(get2("Date"));
|
|
483
|
+
if (isTaxKeyword && localValue < 0) {
|
|
484
|
+
if (!isin) {
|
|
485
|
+
this._warningEntries.push({
|
|
486
|
+
code: "MISSING_ISIN_INCOME",
|
|
487
|
+
row: rowIndex
|
|
488
|
+
});
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
withholdingCandidates.push({
|
|
492
|
+
isin,
|
|
493
|
+
date: isoDate2,
|
|
494
|
+
amount: Math.abs(localValue),
|
|
495
|
+
row: rowIndex
|
|
496
|
+
});
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (isIncomeKeyword && localValue > 0) {
|
|
500
|
+
if (!isin) {
|
|
501
|
+
this._warningEntries.push({
|
|
502
|
+
code: "MISSING_ISIN_INCOME",
|
|
503
|
+
row: rowIndex
|
|
504
|
+
});
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const incomeType = upperProduct.includes(
|
|
508
|
+
"DIVIDEND"
|
|
509
|
+
) ? "dividend" : "coupon";
|
|
510
|
+
incomeCandidates.push({
|
|
511
|
+
isin,
|
|
512
|
+
product,
|
|
513
|
+
date: isoDate2,
|
|
514
|
+
incomeType,
|
|
515
|
+
localValue,
|
|
516
|
+
currency: get2("Local value currency"),
|
|
517
|
+
row: rowIndex
|
|
518
|
+
});
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
423
523
|
if (!isin) {
|
|
424
524
|
this._warningEntries.push({ code: "MISSING_ISIN", row: rowIndex });
|
|
425
525
|
continue;
|
|
426
526
|
}
|
|
427
|
-
const rawQty = parseFloat(get2("Quantity"));
|
|
428
527
|
if (isNaN(rawQty) || rawQty === 0) {
|
|
429
528
|
this._warningEntries.push({ code: "QUANTITY_ZERO", row: rowIndex });
|
|
430
529
|
continue;
|
|
@@ -465,7 +564,6 @@ var DEGIROParser = class {
|
|
|
465
564
|
const rawFees = parseFloat(get2("Transaction costs"));
|
|
466
565
|
const feesEUR = Math.abs(isNaN(rawFees) ? 0 : rawFees);
|
|
467
566
|
const pricePerUnit = parseFloat(get2("Price"));
|
|
468
|
-
const product = get2("Product");
|
|
469
567
|
transactions.push({
|
|
470
568
|
isin,
|
|
471
569
|
product,
|
|
@@ -480,6 +578,73 @@ var DEGIROParser = class {
|
|
|
480
578
|
fxRate
|
|
481
579
|
});
|
|
482
580
|
}
|
|
581
|
+
const withholdingByKey = /* @__PURE__ */ new Map();
|
|
582
|
+
for (const w of withholdingCandidates) {
|
|
583
|
+
const key = `${w.isin}|${w.date}`;
|
|
584
|
+
const existing = withholdingByKey.get(key);
|
|
585
|
+
withholdingByKey.set(key, {
|
|
586
|
+
isin: w.isin,
|
|
587
|
+
date: w.date,
|
|
588
|
+
amount: (existing?.amount ?? 0) + w.amount
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
const consumedKeys = /* @__PURE__ */ new Set();
|
|
592
|
+
const incomeRows = [];
|
|
593
|
+
for (const c of incomeCandidates) {
|
|
594
|
+
const key = `${c.isin}|${c.date}`;
|
|
595
|
+
const matchedWithholding = withholdingByKey.get(key);
|
|
596
|
+
let grossAmount;
|
|
597
|
+
let withholdingTax;
|
|
598
|
+
let fxRate;
|
|
599
|
+
if (c.currency === "EUR") {
|
|
600
|
+
grossAmount = Math.abs(c.localValue);
|
|
601
|
+
withholdingTax = matchedWithholding?.amount ?? 0;
|
|
602
|
+
fxRate = void 0;
|
|
603
|
+
} else {
|
|
604
|
+
const rate = lookupRate(c.currency, c.date, snapshot);
|
|
605
|
+
if (rate === null) {
|
|
606
|
+
if (snapshot[c.currency] === void 0) {
|
|
607
|
+
this._warningEntries.push({
|
|
608
|
+
code: "UNSUPPORTED_CURRENCY",
|
|
609
|
+
row: c.row,
|
|
610
|
+
currency: c.currency
|
|
611
|
+
});
|
|
612
|
+
} else {
|
|
613
|
+
this._warningEntries.push({
|
|
614
|
+
code: "NO_ECB_RATE",
|
|
615
|
+
row: c.row,
|
|
616
|
+
currency: c.currency,
|
|
617
|
+
date: c.date
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
grossAmount = Math.abs(c.localValue) / rate;
|
|
623
|
+
withholdingTax = matchedWithholding ? matchedWithholding.amount / rate : 0;
|
|
624
|
+
fxRate = rate;
|
|
625
|
+
}
|
|
626
|
+
if (matchedWithholding) consumedKeys.add(key);
|
|
627
|
+
incomeRows.push({
|
|
628
|
+
isin: c.isin,
|
|
629
|
+
product: c.product,
|
|
630
|
+
date: c.date,
|
|
631
|
+
incomeType: c.incomeType,
|
|
632
|
+
grossAmount,
|
|
633
|
+
withholdingTax,
|
|
634
|
+
currency: c.currency,
|
|
635
|
+
fxRate
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
for (const [key, w] of withholdingByKey) {
|
|
639
|
+
if (!consumedKeys.has(key)) {
|
|
640
|
+
this._warningEntries.push({
|
|
641
|
+
code: "ORPHAN_WITHHOLDING",
|
|
642
|
+
isin: w.isin,
|
|
643
|
+
date: w.date
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
this._incomeRows = incomeRows;
|
|
483
648
|
return transactions;
|
|
484
649
|
}
|
|
485
650
|
get warnings() {
|
|
@@ -488,12 +653,136 @@ var DEGIROParser = class {
|
|
|
488
653
|
get warningEntries() {
|
|
489
654
|
return this._warningEntries;
|
|
490
655
|
}
|
|
656
|
+
get incomeRows() {
|
|
657
|
+
return this._incomeRows;
|
|
658
|
+
}
|
|
491
659
|
};
|
|
492
660
|
|
|
493
|
-
// src/
|
|
661
|
+
// src/dichiarazione/engine.ts
|
|
662
|
+
import { writeFile } from "fs/promises";
|
|
494
663
|
function roundHalfUp(x) {
|
|
495
664
|
return Math.sign(x) * Math.round(Math.abs(x) * 100) / 100;
|
|
496
665
|
}
|
|
666
|
+
function buildQuadroRT(bucketB, carryForward, taxYear) {
|
|
667
|
+
const plusvalenze = bucketB.plusvalenze;
|
|
668
|
+
const minusvalenze = bucketB.minusvalenze;
|
|
669
|
+
const differenza = roundHalfUp(plusvalenze - minusvalenze);
|
|
670
|
+
if (differenza > 0) {
|
|
671
|
+
const sorted = [...carryForward].sort((a, b) => a.year - b.year);
|
|
672
|
+
let remaining = differenza;
|
|
673
|
+
const carryForwardApplied = [];
|
|
674
|
+
for (const entry of sorted) {
|
|
675
|
+
if (taxYear - entry.year > 4) continue;
|
|
676
|
+
const consumed = Math.min(entry.amount, remaining);
|
|
677
|
+
if (consumed > 0) {
|
|
678
|
+
carryForwardApplied.push({
|
|
679
|
+
annoOrigine: entry.year,
|
|
680
|
+
importo: roundHalfUp(consumed)
|
|
681
|
+
});
|
|
682
|
+
remaining -= consumed;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const imponibileNetto = roundHalfUp(remaining);
|
|
686
|
+
const imposta = roundHalfUp(imponibileNetto * 0.26);
|
|
687
|
+
return {
|
|
688
|
+
plusvalenze,
|
|
689
|
+
minusvalenze,
|
|
690
|
+
differenza,
|
|
691
|
+
carryForwardApplied,
|
|
692
|
+
imponibileNetto,
|
|
693
|
+
imposta,
|
|
694
|
+
carryForwardRiportato: []
|
|
695
|
+
};
|
|
696
|
+
} else if (differenza < 0) {
|
|
697
|
+
return {
|
|
698
|
+
plusvalenze,
|
|
699
|
+
minusvalenze,
|
|
700
|
+
differenza,
|
|
701
|
+
carryForwardApplied: [],
|
|
702
|
+
imponibileNetto: 0,
|
|
703
|
+
imposta: 0,
|
|
704
|
+
carryForwardRiportato: [
|
|
705
|
+
{ annoOrigine: taxYear, importo: roundHalfUp(Math.abs(differenza)) }
|
|
706
|
+
]
|
|
707
|
+
};
|
|
708
|
+
} else {
|
|
709
|
+
return {
|
|
710
|
+
plusvalenze,
|
|
711
|
+
minusvalenze,
|
|
712
|
+
differenza: 0,
|
|
713
|
+
carryForwardApplied: [],
|
|
714
|
+
imponibileNetto: 0,
|
|
715
|
+
imposta: 0,
|
|
716
|
+
carryForwardRiportato: []
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function buildQuadroRM(bucketA, incomeRows, taxYear) {
|
|
721
|
+
const groups = bucketA?.groups ?? [];
|
|
722
|
+
const group26 = groups.find((g) => g.taxRate === 0.26);
|
|
723
|
+
const group125 = groups.find((g) => g.taxRate === 0.125);
|
|
724
|
+
const capitaleAliquota26 = {
|
|
725
|
+
plusvalenze: group26?.plusvalenze ?? 0,
|
|
726
|
+
imposta: group26?.imposta ?? 0
|
|
727
|
+
};
|
|
728
|
+
const capitaleAliquota125 = {
|
|
729
|
+
plusvalenze: group125?.plusvalenze ?? 0,
|
|
730
|
+
imposta: group125?.imposta ?? 0
|
|
731
|
+
};
|
|
732
|
+
const dividendiEsteri = incomeRows.filter((r) => r.incomeType === "dividend").map((r) => ({
|
|
733
|
+
isin: r.isin,
|
|
734
|
+
prodotto: r.product,
|
|
735
|
+
lordo: r.grossAmount,
|
|
736
|
+
rittenutaEstera: r.withholdingTax
|
|
737
|
+
}));
|
|
738
|
+
const cedole = incomeRows.filter((r) => r.incomeType === "coupon").map((r) => ({
|
|
739
|
+
isin: r.isin,
|
|
740
|
+
prodotto: r.product,
|
|
741
|
+
importo: r.grossAmount,
|
|
742
|
+
rittenutaEstera: r.withholdingTax
|
|
743
|
+
}));
|
|
744
|
+
return {
|
|
745
|
+
capitaleAliquota26,
|
|
746
|
+
capitaleAliquota125,
|
|
747
|
+
dividendiEsteri,
|
|
748
|
+
cedole
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
function buildDichiarazioneReport(quadroRT, quadroRM, taxYear) {
|
|
752
|
+
const version = 1;
|
|
753
|
+
const modello = "Redditi PF";
|
|
754
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
755
|
+
return {
|
|
756
|
+
version,
|
|
757
|
+
annoImposta: taxYear,
|
|
758
|
+
modello,
|
|
759
|
+
generatedAt,
|
|
760
|
+
quadroRT,
|
|
761
|
+
quadroRM,
|
|
762
|
+
exportTo: async (path7) => {
|
|
763
|
+
await writeFile(
|
|
764
|
+
path7,
|
|
765
|
+
JSON.stringify(
|
|
766
|
+
{
|
|
767
|
+
version,
|
|
768
|
+
annoImposta: taxYear,
|
|
769
|
+
modello,
|
|
770
|
+
generatedAt,
|
|
771
|
+
quadroRT,
|
|
772
|
+
quadroRM
|
|
773
|
+
},
|
|
774
|
+
null,
|
|
775
|
+
2
|
|
776
|
+
)
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// src/calculator/index.ts
|
|
783
|
+
function roundHalfUp2(x) {
|
|
784
|
+
return Math.sign(x) * Math.round(Math.abs(x) * 100) / 100;
|
|
785
|
+
}
|
|
497
786
|
function inferTaxYear(transactions) {
|
|
498
787
|
const counts = {};
|
|
499
788
|
for (const t of transactions) {
|
|
@@ -585,9 +874,9 @@ var Calculator = class {
|
|
|
585
874
|
sellDate: tx.date,
|
|
586
875
|
buyPriceEUR: lot.pricePerUnitEUR,
|
|
587
876
|
sellPriceEUR: sellPricePerUnitEUR,
|
|
588
|
-
buyCostEUR:
|
|
589
|
-
sellProceedsEUR:
|
|
590
|
-
gainLossEUR:
|
|
877
|
+
buyCostEUR: roundHalfUp2(buyCostEUR),
|
|
878
|
+
sellProceedsEUR: roundHalfUp2(sellProceedsEUR),
|
|
879
|
+
gainLossEUR: roundHalfUp2(gainLossEUR),
|
|
591
880
|
buyFxRate: lot.fxRate,
|
|
592
881
|
sellFxRate: tx.fxRate
|
|
593
882
|
});
|
|
@@ -642,12 +931,12 @@ var Calculator = class {
|
|
|
642
931
|
const bucketAGroups = [...groupsByRate.entries()].map(([taxRate, g]) => ({
|
|
643
932
|
taxRate,
|
|
644
933
|
assetClasses: [...g.assetClasses],
|
|
645
|
-
plusvalenze:
|
|
646
|
-
imposta:
|
|
934
|
+
plusvalenze: roundHalfUp2(g.plusvalenze),
|
|
935
|
+
imposta: roundHalfUp2(g.plusvalenze * taxRate)
|
|
647
936
|
}));
|
|
648
937
|
const bucketAReport = {
|
|
649
938
|
groups: bucketAGroups,
|
|
650
|
-
totalImposta:
|
|
939
|
+
totalImposta: roundHalfUp2(
|
|
651
940
|
bucketAGroups.reduce((s, g) => s + g.imposta, 0)
|
|
652
941
|
)
|
|
653
942
|
};
|
|
@@ -658,8 +947,8 @@ var Calculator = class {
|
|
|
658
947
|
if (lot.gainLossEUR >= 0) bPlusvalenze += lot.gainLossEUR;
|
|
659
948
|
else bMinusvalenze += Math.abs(lot.gainLossEUR);
|
|
660
949
|
}
|
|
661
|
-
bPlusvalenze =
|
|
662
|
-
bMinusvalenze =
|
|
950
|
+
bPlusvalenze = roundHalfUp2(bPlusvalenze);
|
|
951
|
+
bMinusvalenze = roundHalfUp2(bMinusvalenze);
|
|
663
952
|
const carryForwards = [...this._options.carryForward ?? []].sort(
|
|
664
953
|
(a, b) => a.year - b.year
|
|
665
954
|
);
|
|
@@ -672,11 +961,11 @@ var Calculator = class {
|
|
|
672
961
|
carryForwardApplied += consumed;
|
|
673
962
|
remaining -= consumed;
|
|
674
963
|
}
|
|
675
|
-
carryForwardApplied =
|
|
676
|
-
const bNetResult =
|
|
964
|
+
carryForwardApplied = roundHalfUp2(carryForwardApplied);
|
|
965
|
+
const bNetResult = roundHalfUp2(
|
|
677
966
|
bPlusvalenze - bMinusvalenze - carryForwardApplied
|
|
678
967
|
);
|
|
679
|
-
const carryForwardRemaining =
|
|
968
|
+
const carryForwardRemaining = roundHalfUp2(Math.max(0, -bNetResult));
|
|
680
969
|
const bucketBReport = {
|
|
681
970
|
plusvalenze: bPlusvalenze,
|
|
682
971
|
minusvalenze: bMinusvalenze,
|
|
@@ -684,18 +973,41 @@ var Calculator = class {
|
|
|
684
973
|
carryForwardRemaining,
|
|
685
974
|
netResult: bNetResult
|
|
686
975
|
};
|
|
976
|
+
const allIncomeRows = this._options.incomeRows ?? [];
|
|
977
|
+
const filteredIncomeRows = allIncomeRows.filter(
|
|
978
|
+
(row) => new Date(row.date).getFullYear() === taxYear
|
|
979
|
+
);
|
|
980
|
+
if (filteredIncomeRows.length < allIncomeRows.length) {
|
|
981
|
+
warnings.push(`Income rows outside tax year ${taxYear} were skipped.`);
|
|
982
|
+
}
|
|
983
|
+
const quadroRT = buildQuadroRT(
|
|
984
|
+
bucketBReport,
|
|
985
|
+
this._options.carryForward ?? [],
|
|
986
|
+
taxYear
|
|
987
|
+
);
|
|
988
|
+
const quadroRM = buildQuadroRM(
|
|
989
|
+
bucketAGroups.length > 0 ? bucketAReport : void 0,
|
|
990
|
+
filteredIncomeRows,
|
|
991
|
+
taxYear
|
|
992
|
+
);
|
|
993
|
+
const dichiarazioneReport = buildDichiarazioneReport(
|
|
994
|
+
quadroRT,
|
|
995
|
+
quadroRM,
|
|
996
|
+
taxYear
|
|
997
|
+
);
|
|
687
998
|
return {
|
|
688
999
|
method,
|
|
689
1000
|
taxYear,
|
|
690
|
-
plusvalenze:
|
|
691
|
-
minusvalenze:
|
|
692
|
-
netResult:
|
|
1001
|
+
plusvalenze: roundHalfUp2(plusvalenze),
|
|
1002
|
+
minusvalenze: roundHalfUp2(minusvalenze),
|
|
1003
|
+
netResult: roundHalfUp2(plusvalenze - minusvalenze),
|
|
693
1004
|
lots: matchedLots,
|
|
694
1005
|
ratesUsed,
|
|
695
1006
|
warnings,
|
|
696
1007
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
697
1008
|
bucketA: bucketAGroups.length > 0 ? bucketAReport : void 0,
|
|
698
|
-
bucketB: bucketBReport
|
|
1009
|
+
bucketB: bucketBReport,
|
|
1010
|
+
dichiarazione: dichiarazioneReport
|
|
699
1011
|
};
|
|
700
1012
|
} else {
|
|
701
1013
|
const allIsins = [...new Set(this._transactions.map((t) => t.isin))];
|
|
@@ -710,9 +1022,9 @@ var Calculator = class {
|
|
|
710
1022
|
return {
|
|
711
1023
|
method,
|
|
712
1024
|
taxYear,
|
|
713
|
-
plusvalenze:
|
|
714
|
-
minusvalenze:
|
|
715
|
-
netResult:
|
|
1025
|
+
plusvalenze: roundHalfUp2(plusvalenze),
|
|
1026
|
+
minusvalenze: roundHalfUp2(minusvalenze),
|
|
1027
|
+
netResult: roundHalfUp2(plusvalenze - minusvalenze),
|
|
716
1028
|
lots: matchedLots,
|
|
717
1029
|
ratesUsed,
|
|
718
1030
|
warnings,
|
|
@@ -934,7 +1246,7 @@ function classifyByType(isin, securityType, product, warnings) {
|
|
|
934
1246
|
};
|
|
935
1247
|
}
|
|
936
1248
|
warnings.push(
|
|
937
|
-
`Unrecognized type: ${securityType}. Please classify manually.`
|
|
1249
|
+
`Unrecognized type for ${isin}: ${securityType}. Please classify manually.`
|
|
938
1250
|
);
|
|
939
1251
|
return {
|
|
940
1252
|
product,
|
|
@@ -947,6 +1259,84 @@ function classifyByType(isin, securityType, product, warnings) {
|
|
|
947
1259
|
source: "user"
|
|
948
1260
|
};
|
|
949
1261
|
}
|
|
1262
|
+
var ASSET_CLASS_DEFAULTS = {
|
|
1263
|
+
ETF: {
|
|
1264
|
+
assetClass: "ETF",
|
|
1265
|
+
bucketGain: "A",
|
|
1266
|
+
bucketLoss: "B",
|
|
1267
|
+
taxRate: 0.26,
|
|
1268
|
+
whiteListed: null
|
|
1269
|
+
},
|
|
1270
|
+
Stock: {
|
|
1271
|
+
assetClass: "Stock",
|
|
1272
|
+
bucketGain: "B",
|
|
1273
|
+
bucketLoss: "B",
|
|
1274
|
+
taxRate: 0,
|
|
1275
|
+
whiteListed: null
|
|
1276
|
+
},
|
|
1277
|
+
ETC: {
|
|
1278
|
+
assetClass: "ETC",
|
|
1279
|
+
bucketGain: "B",
|
|
1280
|
+
bucketLoss: "B",
|
|
1281
|
+
taxRate: 0,
|
|
1282
|
+
whiteListed: null
|
|
1283
|
+
},
|
|
1284
|
+
GovtBondWL: {
|
|
1285
|
+
assetClass: "GovtBondWL",
|
|
1286
|
+
bucketGain: "A",
|
|
1287
|
+
bucketLoss: "B",
|
|
1288
|
+
taxRate: 0.125,
|
|
1289
|
+
whiteListed: true
|
|
1290
|
+
},
|
|
1291
|
+
GovtBondOther: {
|
|
1292
|
+
assetClass: "GovtBondOther",
|
|
1293
|
+
bucketGain: "A",
|
|
1294
|
+
bucketLoss: "B",
|
|
1295
|
+
taxRate: 0.26,
|
|
1296
|
+
whiteListed: false
|
|
1297
|
+
},
|
|
1298
|
+
CorpBond: {
|
|
1299
|
+
assetClass: "CorpBond",
|
|
1300
|
+
bucketGain: "B",
|
|
1301
|
+
bucketLoss: "B",
|
|
1302
|
+
taxRate: 0,
|
|
1303
|
+
whiteListed: null
|
|
1304
|
+
},
|
|
1305
|
+
Derivative: {
|
|
1306
|
+
assetClass: "Derivative",
|
|
1307
|
+
bucketGain: "B",
|
|
1308
|
+
bucketLoss: "B",
|
|
1309
|
+
taxRate: 0,
|
|
1310
|
+
whiteListed: null
|
|
1311
|
+
},
|
|
1312
|
+
LeverageCert: {
|
|
1313
|
+
assetClass: "LeverageCert",
|
|
1314
|
+
bucketGain: "B",
|
|
1315
|
+
bucketLoss: "B",
|
|
1316
|
+
taxRate: 0,
|
|
1317
|
+
whiteListed: null
|
|
1318
|
+
},
|
|
1319
|
+
CapProtectedCert: {
|
|
1320
|
+
assetClass: "CapProtectedCert",
|
|
1321
|
+
bucketGain: "A",
|
|
1322
|
+
bucketLoss: "B",
|
|
1323
|
+
taxRate: 0.26,
|
|
1324
|
+
whiteListed: null
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
function buildEntryFromAssetClass(assetClass, product) {
|
|
1328
|
+
const mapped = ASSET_CLASS_DEFAULTS[assetClass];
|
|
1329
|
+
return {
|
|
1330
|
+
product,
|
|
1331
|
+
assetClass: mapped.assetClass,
|
|
1332
|
+
bucketGain: mapped.bucketGain,
|
|
1333
|
+
bucketLoss: mapped.bucketLoss,
|
|
1334
|
+
taxRate: mapped.taxRate,
|
|
1335
|
+
whiteListed: mapped.whiteListed,
|
|
1336
|
+
confirmedByUser: true,
|
|
1337
|
+
source: "user"
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
950
1340
|
var Classifier = class {
|
|
951
1341
|
interactive;
|
|
952
1342
|
constructor(options) {
|
|
@@ -968,7 +1358,7 @@ var Classifier = class {
|
|
|
968
1358
|
}
|
|
969
1359
|
return parsed.classifications;
|
|
970
1360
|
}
|
|
971
|
-
async classify(transactions, sidecarPath, _httpPost = httpsPost) {
|
|
1361
|
+
async classify(transactions, sidecarPath, options, _httpPost = httpsPost) {
|
|
972
1362
|
const isinToProduct = /* @__PURE__ */ new Map();
|
|
973
1363
|
for (const tx of transactions) {
|
|
974
1364
|
if (tx.isin && !isinToProduct.has(tx.isin)) {
|
|
@@ -976,13 +1366,21 @@ var Classifier = class {
|
|
|
976
1366
|
}
|
|
977
1367
|
}
|
|
978
1368
|
const confirmed = {};
|
|
979
|
-
if (
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1369
|
+
if (sidecarPath !== void 0) {
|
|
1370
|
+
if (fs3.existsSync(sidecarPath)) {
|
|
1371
|
+
const existingMap = await this.load(sidecarPath);
|
|
1372
|
+
for (const [isin, entry] of Object.entries(existingMap)) {
|
|
1373
|
+
if (entry.confirmedByUser) {
|
|
1374
|
+
confirmed[isin] = entry;
|
|
1375
|
+
}
|
|
984
1376
|
}
|
|
985
1377
|
}
|
|
1378
|
+
} else if (options?.existingClassification) {
|
|
1379
|
+
Object.assign(confirmed, options.existingClassification);
|
|
1380
|
+
}
|
|
1381
|
+
for (const [isin, assetClass] of Object.entries(options?.overrides ?? {})) {
|
|
1382
|
+
const product = isinToProduct.get(isin) ?? isin;
|
|
1383
|
+
confirmed[isin] = buildEntryFromAssetClass(assetClass, product);
|
|
986
1384
|
}
|
|
987
1385
|
const toProcess = [];
|
|
988
1386
|
for (const isin of isinToProduct.keys()) {
|
|
@@ -992,76 +1390,100 @@ var Classifier = class {
|
|
|
992
1390
|
}
|
|
993
1391
|
const warnings = [];
|
|
994
1392
|
const newEntries = {};
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1393
|
+
if (options?.offline) {
|
|
1394
|
+
for (const isin of toProcess) {
|
|
1395
|
+
const product = isinToProduct.get(isin) ?? isin;
|
|
1396
|
+
warnings.push(
|
|
1397
|
+
`Unrecognized type for ${isin}: unknown. Please classify manually.`
|
|
1398
|
+
);
|
|
1399
|
+
newEntries[isin] = {
|
|
1400
|
+
product,
|
|
1401
|
+
assetClass: "Stock",
|
|
1402
|
+
bucketGain: "B",
|
|
1403
|
+
bucketLoss: "B",
|
|
1404
|
+
taxRate: 0,
|
|
1405
|
+
whiteListed: null,
|
|
1406
|
+
confirmedByUser: false,
|
|
1407
|
+
source: "user"
|
|
1408
|
+
};
|
|
999
1409
|
}
|
|
1000
|
-
|
|
1001
|
-
const
|
|
1002
|
-
|
|
1003
|
-
)
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1410
|
+
} else {
|
|
1411
|
+
const BATCH_SIZE = 10;
|
|
1412
|
+
const totalBatches = Math.ceil(toProcess.length / BATCH_SIZE);
|
|
1413
|
+
for (let i = 0; i < toProcess.length; i += BATCH_SIZE) {
|
|
1414
|
+
if (i > 0) {
|
|
1415
|
+
await new Promise((resolve2) => setTimeout(resolve2, 6e3));
|
|
1416
|
+
}
|
|
1417
|
+
const batch = toProcess.slice(i, i + BATCH_SIZE);
|
|
1418
|
+
const requestBody = JSON.stringify(
|
|
1419
|
+
batch.map((isin) => ({ idType: "ID_ISIN", idValue: isin }))
|
|
1420
|
+
);
|
|
1421
|
+
let response = null;
|
|
1422
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1423
|
+
try {
|
|
1424
|
+
const r = await _httpPost(
|
|
1425
|
+
"https://api.openfigi.com/v3/mapping",
|
|
1426
|
+
requestBody,
|
|
1427
|
+
1e4
|
|
1428
|
+
);
|
|
1429
|
+
if (r.status < 500) {
|
|
1430
|
+
response = r;
|
|
1431
|
+
break;
|
|
1432
|
+
}
|
|
1433
|
+
} catch {
|
|
1434
|
+
throw new ClassificationError("NETWORK_ERROR");
|
|
1015
1435
|
}
|
|
1016
|
-
}
|
|
1436
|
+
}
|
|
1437
|
+
if (response === null) {
|
|
1017
1438
|
throw new ClassificationError("NETWORK_ERROR");
|
|
1018
1439
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1440
|
+
const results = JSON.parse(response.data);
|
|
1441
|
+
for (let j = 0; j < batch.length; j++) {
|
|
1442
|
+
const isin = batch[j];
|
|
1443
|
+
const result = results[j];
|
|
1444
|
+
const product = isinToProduct.get(isin) ?? isin;
|
|
1445
|
+
if (!result || result.error || !result.data || result.data.length === 0) {
|
|
1446
|
+
warnings.push(
|
|
1447
|
+
`Unrecognized type for ${isin}: unknown. Please classify manually.`
|
|
1448
|
+
);
|
|
1449
|
+
newEntries[isin] = {
|
|
1450
|
+
product,
|
|
1451
|
+
assetClass: "Stock",
|
|
1452
|
+
bucketGain: "B",
|
|
1453
|
+
bucketLoss: "B",
|
|
1454
|
+
taxRate: 0,
|
|
1455
|
+
whiteListed: null,
|
|
1456
|
+
confirmedByUser: false,
|
|
1457
|
+
source: "user"
|
|
1458
|
+
};
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
const st = result.data[0]?.securityType;
|
|
1462
|
+
const st2 = result.data[0]?.securityType2;
|
|
1463
|
+
const typeToUse = st && isKnownType(st) ? st : st2 && isKnownType(st2) ? st2 : st ?? st2 ?? "Unknown";
|
|
1464
|
+
newEntries[isin] = classifyByType(isin, typeToUse, product, warnings);
|
|
1043
1465
|
}
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
const typeToUse = st && isKnownType(st) ? st : st2 && isKnownType(st2) ? st2 : st ?? st2 ?? "Unknown";
|
|
1047
|
-
newEntries[isin] = classifyByType(isin, typeToUse, product, warnings);
|
|
1466
|
+
const batchIndex = i / BATCH_SIZE;
|
|
1467
|
+
options?.onBatchProgress?.(batchIndex + 1, totalBatches);
|
|
1048
1468
|
}
|
|
1049
1469
|
}
|
|
1050
1470
|
const mergedMap = { ...newEntries, ...confirmed };
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1471
|
+
if (sidecarPath !== void 0) {
|
|
1472
|
+
const sidecarContent = JSON.stringify(
|
|
1473
|
+
{
|
|
1474
|
+
version: 1,
|
|
1475
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1476
|
+
classifications: mergedMap,
|
|
1477
|
+
warnings
|
|
1478
|
+
},
|
|
1479
|
+
null,
|
|
1480
|
+
2
|
|
1481
|
+
);
|
|
1482
|
+
try {
|
|
1483
|
+
fs3.writeFileSync(sidecarPath, sidecarContent, "utf-8");
|
|
1484
|
+
} catch {
|
|
1485
|
+
throw new ClassificationError("WRITE_ERROR");
|
|
1486
|
+
}
|
|
1065
1487
|
}
|
|
1066
1488
|
return mergedMap;
|
|
1067
1489
|
}
|
|
@@ -1084,7 +1506,80 @@ function assetClassLabel(ac, s) {
|
|
|
1084
1506
|
if (ac === "GovtBondWL" || ac === "GovtBondOther") return s.bucketABtpWl;
|
|
1085
1507
|
return ac;
|
|
1086
1508
|
}
|
|
1087
|
-
function
|
|
1509
|
+
function renderDichiarazione(report, s, carryForwardWasProvided) {
|
|
1510
|
+
const d = report.dichiarazione;
|
|
1511
|
+
const rt = d.quadroRT;
|
|
1512
|
+
const rm = d.quadroRM;
|
|
1513
|
+
const fmt = (n) => formatEUR(n, s.numberLocale);
|
|
1514
|
+
const lines = [];
|
|
1515
|
+
lines.push(SEPARATOR);
|
|
1516
|
+
lines.push(s.dichiarazioneHeader(d.annoImposta));
|
|
1517
|
+
lines.push(s.dichiarazioneNota);
|
|
1518
|
+
lines.push("");
|
|
1519
|
+
lines.push(s.quadroRTHeader);
|
|
1520
|
+
lines.push(` ${s.quadroRTPlusvalenze} ${fmt(rt.plusvalenze)}`);
|
|
1521
|
+
lines.push(` ${s.quadroRTMinusvalenze} ${fmt(rt.minusvalenze)}`);
|
|
1522
|
+
lines.push(` ${s.quadroRTDifferenza} ${fmt(rt.differenza)}`);
|
|
1523
|
+
for (const entry of rt.carryForwardApplied) {
|
|
1524
|
+
lines.push(
|
|
1525
|
+
` ${s.quadroRTRiporto(entry.annoOrigine)} ${fmt(entry.importo)}`
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
let rtNLine = ` ${s.quadroRTImponibile} ${fmt(rt.imponibileNetto)}`;
|
|
1529
|
+
if (rt.differenza < 0) {
|
|
1530
|
+
rtNLine += ` (${s.quadroRTPerdita} ${fmt(Math.abs(rt.differenza))})`;
|
|
1531
|
+
}
|
|
1532
|
+
if (rt.differenza > 0 && !carryForwardWasProvided) {
|
|
1533
|
+
rtNLine += ` ${s.warnNoCarryForwardProvided}`;
|
|
1534
|
+
}
|
|
1535
|
+
lines.push(rtNLine);
|
|
1536
|
+
lines.push(` ${s.quadroRTImposta} ${fmt(rt.imposta)}`);
|
|
1537
|
+
if (rt.differenza < 0) {
|
|
1538
|
+
const riportato = rt.carryForwardRiportato.reduce(
|
|
1539
|
+
(sum, e) => sum + e.importo,
|
|
1540
|
+
0
|
|
1541
|
+
);
|
|
1542
|
+
lines.push(
|
|
1543
|
+
` ${s.quadroRTRiportabile} ${fmt(riportato)} ${s.bucketBCarryNote}`
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
lines.push("");
|
|
1547
|
+
lines.push(s.quadroRMHeader);
|
|
1548
|
+
if (rm.capitaleAliquota26.plusvalenze > 0) {
|
|
1549
|
+
lines.push(
|
|
1550
|
+
` ${s.quadroRMEtf26} ${fmt(rm.capitaleAliquota26.plusvalenze)}`
|
|
1551
|
+
);
|
|
1552
|
+
lines.push(
|
|
1553
|
+
` [RM-A1] ${s.quadroRMImposta} ${fmt(rm.capitaleAliquota26.imposta)}`
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1556
|
+
if (rm.capitaleAliquota125.plusvalenze > 0) {
|
|
1557
|
+
lines.push(
|
|
1558
|
+
` ${s.quadroRMBtp} ${fmt(rm.capitaleAliquota125.plusvalenze)}`
|
|
1559
|
+
);
|
|
1560
|
+
lines.push(
|
|
1561
|
+
` [RM-A2] ${s.quadroRMImposta} ${fmt(rm.capitaleAliquota125.imposta)}`
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
for (const div of rm.dividendiEsteri) {
|
|
1565
|
+
lines.push(
|
|
1566
|
+
` ${s.quadroRMDividendi} \u2014 ${div.prodotto} (${div.isin}) ${fmt(div.lordo)}`
|
|
1567
|
+
);
|
|
1568
|
+
lines.push(
|
|
1569
|
+
` [RM-D] ${s.quadroRMRitenuta} \u2014 ${div.prodotto} (${div.isin}) ${fmt(div.rittenutaEstera)}`
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
for (const c of rm.cedole) {
|
|
1573
|
+
lines.push(
|
|
1574
|
+
` ${s.quadroRMCedole} \u2014 ${c.prodotto} (${c.isin}) ${fmt(c.importo)}`
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
lines.push("");
|
|
1578
|
+
lines.push(s.dichiarazioneWarningRow);
|
|
1579
|
+
lines.push(s.dichiarazioneDisclaimer);
|
|
1580
|
+
return lines.join("\n");
|
|
1581
|
+
}
|
|
1582
|
+
function renderReport(report, s, carryForwardWasProvided) {
|
|
1088
1583
|
const fmt = (n) => formatEUR(n, s.numberLocale);
|
|
1089
1584
|
const lines = [];
|
|
1090
1585
|
const hasBuckets = !!(report.bucketA || report.bucketB);
|
|
@@ -1173,111 +1668,12 @@ function renderReport(report, s) {
|
|
|
1173
1668
|
lines.push(s.warnMixedBuckets);
|
|
1174
1669
|
lines.push("");
|
|
1175
1670
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
// src/cli/commands/rates.ts
|
|
1181
|
-
import * as fs4 from "fs";
|
|
1182
|
-
import * as path3 from "path";
|
|
1183
|
-
import * as os2 from "os";
|
|
1184
|
-
import * as https2 from "https";
|
|
1185
|
-
function getSnapshotPath() {
|
|
1186
|
-
const platform = process.platform;
|
|
1187
|
-
let configDir;
|
|
1188
|
-
if (platform === "win32") {
|
|
1189
|
-
configDir = process.env.APPDATA ?? path3.join(os2.homedir(), "AppData", "Roaming");
|
|
1671
|
+
if (report.dichiarazione) {
|
|
1672
|
+
lines.push(renderDichiarazione(report, s, carryForwardWasProvided));
|
|
1190
1673
|
} else {
|
|
1191
|
-
|
|
1674
|
+
lines.push(s.disclaimer);
|
|
1192
1675
|
}
|
|
1193
|
-
return
|
|
1194
|
-
}
|
|
1195
|
-
function getCoverage(snapshot) {
|
|
1196
|
-
let start = "9999-12-31";
|
|
1197
|
-
let end = "0000-01-01";
|
|
1198
|
-
const currencies = [];
|
|
1199
|
-
for (const [ccy, dates] of Object.entries(snapshot)) {
|
|
1200
|
-
currencies.push(ccy);
|
|
1201
|
-
for (const d of Object.keys(dates)) {
|
|
1202
|
-
if (d < start) start = d;
|
|
1203
|
-
if (d > end) end = d;
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
return { start, end, currencies: currencies.sort().join(", ") };
|
|
1207
|
-
}
|
|
1208
|
-
async function fetchEcbData(currency) {
|
|
1209
|
-
return new Promise((resolve2, reject) => {
|
|
1210
|
-
const url = `https://data-api.ecb.europa.eu/service/data/EXR/D.${currency}.EUR.SP00.A?format=csvdata&startPeriod=2019-01-01`;
|
|
1211
|
-
https2.get(url, (res) => {
|
|
1212
|
-
let data = "";
|
|
1213
|
-
res.on("data", (chunk) => data += chunk);
|
|
1214
|
-
res.on("end", () => {
|
|
1215
|
-
const rates = {};
|
|
1216
|
-
const lines = data.split("\n");
|
|
1217
|
-
for (const line of lines) {
|
|
1218
|
-
const parts = line.split(",");
|
|
1219
|
-
if (parts.length < 8) continue;
|
|
1220
|
-
const date = parts[6].trim();
|
|
1221
|
-
const rate = parseFloat(parts[7].trim());
|
|
1222
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(date) && !isNaN(rate) && rate > 0) {
|
|
1223
|
-
rates[date] = rate;
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
resolve2(rates);
|
|
1227
|
-
});
|
|
1228
|
-
res.on("error", reject);
|
|
1229
|
-
}).on("error", reject);
|
|
1230
|
-
});
|
|
1231
|
-
}
|
|
1232
|
-
async function updateRates(snapshotPath, stdout, stderr, s) {
|
|
1233
|
-
stdout.write(s.ratesUpdateFetching + "\n");
|
|
1234
|
-
let existing = {};
|
|
1235
|
-
try {
|
|
1236
|
-
existing = JSON.parse(
|
|
1237
|
-
fs4.readFileSync(snapshotPath, "utf8")
|
|
1238
|
-
);
|
|
1239
|
-
} catch {
|
|
1240
|
-
}
|
|
1241
|
-
let addedCount = 0;
|
|
1242
|
-
for (const currency of ["USD", "GBP", "CHF"]) {
|
|
1243
|
-
try {
|
|
1244
|
-
const newRates = await fetchEcbData(currency);
|
|
1245
|
-
const existing_ccy = existing[currency] ?? {};
|
|
1246
|
-
let added = 0;
|
|
1247
|
-
for (const [date, rate] of Object.entries(newRates)) {
|
|
1248
|
-
if (!existing_ccy[date]) added++;
|
|
1249
|
-
existing_ccy[date] = rate;
|
|
1250
|
-
}
|
|
1251
|
-
existing[currency] = existing_ccy;
|
|
1252
|
-
addedCount += added;
|
|
1253
|
-
} catch {
|
|
1254
|
-
stderr.write(`Failed to fetch ${currency} rates
|
|
1255
|
-
`);
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
fs4.mkdirSync(path3.dirname(snapshotPath), { recursive: true });
|
|
1259
|
-
fs4.writeFileSync(
|
|
1260
|
-
snapshotPath,
|
|
1261
|
-
JSON.stringify(existing, null, 2) + "\n",
|
|
1262
|
-
"utf8"
|
|
1263
|
-
);
|
|
1264
|
-
stdout.write(s.ratesUpdateDone(addedCount) + "\n");
|
|
1265
|
-
stdout.write(s.ratesSnapshotWritten(snapshotPath) + "\n");
|
|
1266
|
-
}
|
|
1267
|
-
async function runRates(positional, flags, s, stdout, stderr) {
|
|
1268
|
-
if (flags["check"]) {
|
|
1269
|
-
const snapshot = getActiveSnapshot();
|
|
1270
|
-
const { start, end, currencies } = getCoverage(snapshot);
|
|
1271
|
-
stdout.write(s.ratesCoverage(start, end, currencies) + "\n");
|
|
1272
|
-
stdout.write(s.ratesGapsNone + "\n");
|
|
1273
|
-
return 0;
|
|
1274
|
-
}
|
|
1275
|
-
if (flags["update"]) {
|
|
1276
|
-
await updateRates(getSnapshotPath(), stdout, stderr, s);
|
|
1277
|
-
return 0;
|
|
1278
|
-
}
|
|
1279
|
-
stderr.write("Usage: minus-tracker rates --check | --update\n");
|
|
1280
|
-
return 2;
|
|
1676
|
+
return lines.join("\n");
|
|
1281
1677
|
}
|
|
1282
1678
|
|
|
1283
1679
|
// src/cli/commands/calc.ts
|
|
@@ -1291,30 +1687,12 @@ async function runCalc(positional, flags, s, stdout, stderr) {
|
|
|
1291
1687
|
}
|
|
1292
1688
|
let csv;
|
|
1293
1689
|
try {
|
|
1294
|
-
csv =
|
|
1690
|
+
csv = fs4.readFileSync(filePath, "utf8");
|
|
1295
1691
|
} catch {
|
|
1296
1692
|
stderr.write(`Cannot read file: ${filePath}
|
|
1297
1693
|
`);
|
|
1298
1694
|
return 1;
|
|
1299
1695
|
}
|
|
1300
|
-
try {
|
|
1301
|
-
const snapshot = getActiveSnapshot();
|
|
1302
|
-
if (isSnapshotStale(snapshot)) {
|
|
1303
|
-
stderr.write(s.ratesAutoUpdateStart + "\n");
|
|
1304
|
-
try {
|
|
1305
|
-
await updateRates(getSnapshotPath(), stdout, stderr, s);
|
|
1306
|
-
} catch {
|
|
1307
|
-
stderr.write(s.ratesAutoUpdateFailed + "\n");
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
} catch {
|
|
1311
|
-
stderr.write(s.ratesAutoUpdateStart + "\n");
|
|
1312
|
-
try {
|
|
1313
|
-
await updateRates(getSnapshotPath(), stdout, stderr, s);
|
|
1314
|
-
} catch {
|
|
1315
|
-
stderr.write(s.ratesAutoUpdateFailed + "\n");
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
1696
|
const method = flags["method"] ?? "LIFO";
|
|
1319
1697
|
if (method !== "LIFO" && method !== "FIFO") {
|
|
1320
1698
|
stderr.write("--method must be LIFO or FIFO\n");
|
|
@@ -1322,7 +1700,7 @@ async function runCalc(positional, flags, s, stdout, stderr) {
|
|
|
1322
1700
|
}
|
|
1323
1701
|
const sidecarPath = filePath.replace(/\.csv$/i, "") + ".classify.json";
|
|
1324
1702
|
let classification;
|
|
1325
|
-
if (
|
|
1703
|
+
if (fs4.existsSync(sidecarPath)) {
|
|
1326
1704
|
try {
|
|
1327
1705
|
const classifier = new Classifier({ interactive: false });
|
|
1328
1706
|
classification = await classifier.load(sidecarPath);
|
|
@@ -1332,6 +1710,13 @@ async function runCalc(positional, flags, s, stdout, stderr) {
|
|
|
1332
1710
|
return 1;
|
|
1333
1711
|
}
|
|
1334
1712
|
}
|
|
1713
|
+
const exportFlagRaw = flags["export-dichiarazione"];
|
|
1714
|
+
const exportRequested = exportFlagRaw !== void 0 && exportFlagRaw !== false;
|
|
1715
|
+
const exportPath = typeof exportFlagRaw === "string" ? exportFlagRaw : filePath.replace(/\.csv$/i, "") + ".dichiarazione.json";
|
|
1716
|
+
if (exportRequested && classification === void 0) {
|
|
1717
|
+
stderr.write(s.warnNoDichiarazioneSidecar + "\n");
|
|
1718
|
+
return 1;
|
|
1719
|
+
}
|
|
1335
1720
|
const rawCf = flags["carry-forward"];
|
|
1336
1721
|
const rawCarryForwards = Array.isArray(rawCf) ? rawCf : rawCf ? [rawCf] : [];
|
|
1337
1722
|
const cfRegex = /^\d{4}:\d+(\.\d+)?$/;
|
|
@@ -1341,16 +1726,16 @@ async function runCalc(positional, flags, s, stdout, stderr) {
|
|
|
1341
1726
|
return 2;
|
|
1342
1727
|
}
|
|
1343
1728
|
}
|
|
1344
|
-
const xdgConfig = process.env["XDG_CONFIG_HOME"] ??
|
|
1345
|
-
const cfConfigPath =
|
|
1729
|
+
const xdgConfig = process.env["XDG_CONFIG_HOME"] ?? path3.join(os2.homedir(), ".config");
|
|
1730
|
+
const cfConfigPath = path3.join(
|
|
1346
1731
|
xdgConfig,
|
|
1347
1732
|
"minus-tracker",
|
|
1348
1733
|
"carryforward.json"
|
|
1349
1734
|
);
|
|
1350
1735
|
const cfFromFile = {};
|
|
1351
|
-
if (
|
|
1736
|
+
if (fs4.existsSync(cfConfigPath)) {
|
|
1352
1737
|
try {
|
|
1353
|
-
const raw = JSON.parse(
|
|
1738
|
+
const raw = JSON.parse(fs4.readFileSync(cfConfigPath, "utf-8"));
|
|
1354
1739
|
for (const entry of raw.losses ?? []) {
|
|
1355
1740
|
cfFromFile[entry.year] = entry.amount;
|
|
1356
1741
|
}
|
|
@@ -1382,19 +1767,36 @@ async function runCalc(positional, flags, s, stdout, stderr) {
|
|
|
1382
1767
|
}
|
|
1383
1768
|
const calculator = new Calculator(transactions, parser.warnings, {
|
|
1384
1769
|
classification,
|
|
1385
|
-
carryForward: carryForward.length > 0 ? carryForward : void 0
|
|
1770
|
+
carryForward: carryForward.length > 0 ? carryForward : void 0,
|
|
1771
|
+
incomeRows: parser.incomeRows
|
|
1386
1772
|
});
|
|
1387
1773
|
const report = calculator.calculateGains(method);
|
|
1774
|
+
const carryForwardWasProvided = carryForward.length > 0;
|
|
1775
|
+
if (exportRequested) {
|
|
1776
|
+
try {
|
|
1777
|
+
await report.dichiarazione.exportTo(exportPath);
|
|
1778
|
+
} catch {
|
|
1779
|
+
stderr.write(`Cannot write dichiarazione export: ${exportPath}
|
|
1780
|
+
`);
|
|
1781
|
+
return 1;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1388
1784
|
if (flags["json"]) {
|
|
1389
|
-
|
|
1785
|
+
const { dichiarazione: _dichiarazione, ...jsonReport } = report;
|
|
1786
|
+
stdout.write(JSON.stringify(jsonReport, null, 2) + "\n");
|
|
1390
1787
|
} else {
|
|
1391
|
-
stdout.write(renderReport(report, s) + "\n");
|
|
1788
|
+
stdout.write(renderReport(report, s, carryForwardWasProvided) + "\n");
|
|
1789
|
+
}
|
|
1790
|
+
if (exportRequested) {
|
|
1791
|
+
stdout.write(s.classifyWritten(exportPath) + "\n");
|
|
1792
|
+
} else if (!report.dichiarazione) {
|
|
1793
|
+
stderr.write(s.warnNoDichiarazioneSidecar + "\n");
|
|
1392
1794
|
}
|
|
1393
1795
|
return 0;
|
|
1394
1796
|
}
|
|
1395
1797
|
|
|
1396
1798
|
// src/cli/commands/validate.ts
|
|
1397
|
-
import * as
|
|
1799
|
+
import * as fs5 from "fs";
|
|
1398
1800
|
async function runValidate(positional, flags, s, stdout, stderr) {
|
|
1399
1801
|
const filePath = positional[0];
|
|
1400
1802
|
if (!filePath) {
|
|
@@ -1403,7 +1805,7 @@ async function runValidate(positional, flags, s, stdout, stderr) {
|
|
|
1403
1805
|
}
|
|
1404
1806
|
let csv;
|
|
1405
1807
|
try {
|
|
1406
|
-
csv =
|
|
1808
|
+
csv = fs5.readFileSync(filePath, "utf8");
|
|
1407
1809
|
} catch {
|
|
1408
1810
|
stderr.write(`Cannot read file: ${filePath}
|
|
1409
1811
|
`);
|
|
@@ -1440,12 +1842,121 @@ async function runValidate(positional, flags, s, stdout, stderr) {
|
|
|
1440
1842
|
case "QUANTITY_ZERO":
|
|
1441
1843
|
reason = s.warnQuantityZero(entry.row);
|
|
1442
1844
|
break;
|
|
1845
|
+
case "MISSING_ISIN_INCOME":
|
|
1846
|
+
reason = s.warnMissingIsinIncome(entry.row);
|
|
1847
|
+
break;
|
|
1848
|
+
case "ORPHAN_WITHHOLDING":
|
|
1849
|
+
reason = s.warnOrphanWithholding(entry.isin, entry.date);
|
|
1850
|
+
break;
|
|
1443
1851
|
}
|
|
1444
1852
|
stdout.write(reason + "\n");
|
|
1445
1853
|
}
|
|
1446
1854
|
return 0;
|
|
1447
1855
|
}
|
|
1448
1856
|
|
|
1857
|
+
// src/cli/commands/rates.ts
|
|
1858
|
+
import * as fs6 from "fs";
|
|
1859
|
+
import * as path4 from "path";
|
|
1860
|
+
import * as os3 from "os";
|
|
1861
|
+
import * as https2 from "https";
|
|
1862
|
+
function getSnapshotPath() {
|
|
1863
|
+
const platform = process.platform;
|
|
1864
|
+
let configDir;
|
|
1865
|
+
if (platform === "win32") {
|
|
1866
|
+
configDir = process.env.APPDATA ?? path4.join(os3.homedir(), "AppData", "Roaming");
|
|
1867
|
+
} else {
|
|
1868
|
+
configDir = process.env.XDG_CONFIG_HOME ?? path4.join(os3.homedir(), ".config");
|
|
1869
|
+
}
|
|
1870
|
+
return path4.join(configDir, "minus-tracker", "ecb-rates.json");
|
|
1871
|
+
}
|
|
1872
|
+
function getCoverage(snapshot) {
|
|
1873
|
+
let start = "9999-12-31";
|
|
1874
|
+
let end = "0000-01-01";
|
|
1875
|
+
const currencies = [];
|
|
1876
|
+
for (const [ccy, dates] of Object.entries(snapshot)) {
|
|
1877
|
+
currencies.push(ccy);
|
|
1878
|
+
for (const d of Object.keys(dates)) {
|
|
1879
|
+
if (d < start) start = d;
|
|
1880
|
+
if (d > end) end = d;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
return { start, end, currencies: currencies.sort().join(", ") };
|
|
1884
|
+
}
|
|
1885
|
+
async function fetchEcbData(currency) {
|
|
1886
|
+
return new Promise((resolve2, reject) => {
|
|
1887
|
+
const url = `https://data-api.ecb.europa.eu/service/data/EXR/D.${currency}.EUR.SP00.A?format=csvdata&startPeriod=2019-01-01`;
|
|
1888
|
+
https2.get(url, (res) => {
|
|
1889
|
+
let data = "";
|
|
1890
|
+
res.on("data", (chunk) => data += chunk);
|
|
1891
|
+
res.on("end", () => {
|
|
1892
|
+
const rates = {};
|
|
1893
|
+
const lines = data.split("\n");
|
|
1894
|
+
for (const line of lines) {
|
|
1895
|
+
const parts = line.split(",");
|
|
1896
|
+
if (parts.length < 8) continue;
|
|
1897
|
+
const date = parts[6].trim();
|
|
1898
|
+
const rate = parseFloat(parts[7].trim());
|
|
1899
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(date) && !isNaN(rate) && rate > 0) {
|
|
1900
|
+
rates[date] = rate;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
resolve2(rates);
|
|
1904
|
+
});
|
|
1905
|
+
res.on("error", reject);
|
|
1906
|
+
}).on("error", reject);
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
async function updateRates(snapshotPath, stdout, stderr, s) {
|
|
1910
|
+
stdout.write(s.ratesUpdateFetching + "\n");
|
|
1911
|
+
let existing = {};
|
|
1912
|
+
try {
|
|
1913
|
+
existing = JSON.parse(
|
|
1914
|
+
fs6.readFileSync(snapshotPath, "utf8")
|
|
1915
|
+
);
|
|
1916
|
+
} catch {
|
|
1917
|
+
}
|
|
1918
|
+
let addedCount = 0;
|
|
1919
|
+
for (const currency of ["USD", "GBP", "CHF"]) {
|
|
1920
|
+
try {
|
|
1921
|
+
const newRates = await fetchEcbData(currency);
|
|
1922
|
+
const existing_ccy = existing[currency] ?? {};
|
|
1923
|
+
let added = 0;
|
|
1924
|
+
for (const [date, rate] of Object.entries(newRates)) {
|
|
1925
|
+
if (!existing_ccy[date]) added++;
|
|
1926
|
+
existing_ccy[date] = rate;
|
|
1927
|
+
}
|
|
1928
|
+
existing[currency] = existing_ccy;
|
|
1929
|
+
addedCount += added;
|
|
1930
|
+
} catch {
|
|
1931
|
+
stderr.write(`Failed to fetch ${currency} rates
|
|
1932
|
+
`);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
fs6.mkdirSync(path4.dirname(snapshotPath), { recursive: true });
|
|
1936
|
+
fs6.writeFileSync(
|
|
1937
|
+
snapshotPath,
|
|
1938
|
+
JSON.stringify(existing, null, 2) + "\n",
|
|
1939
|
+
"utf8"
|
|
1940
|
+
);
|
|
1941
|
+
stdout.write(s.ratesUpdateDone(addedCount) + "\n");
|
|
1942
|
+
stdout.write(s.ratesSnapshotWritten(snapshotPath) + "\n");
|
|
1943
|
+
}
|
|
1944
|
+
async function runRates(positional, flags, s, stdout, stderr) {
|
|
1945
|
+
if (flags["check"]) {
|
|
1946
|
+
const snapshot = getActiveSnapshot();
|
|
1947
|
+
const { start, end, currencies } = getCoverage(snapshot);
|
|
1948
|
+
stdout.write(s.ratesCoverage(start, end, currencies) + "\n");
|
|
1949
|
+
stdout.write(s.ratesGapsNone + "\n");
|
|
1950
|
+
return 0;
|
|
1951
|
+
}
|
|
1952
|
+
if (flags["update"]) {
|
|
1953
|
+
await updateRates(getSnapshotPath(), stdout, stderr, s);
|
|
1954
|
+
return 0;
|
|
1955
|
+
}
|
|
1956
|
+
stderr.write("Usage: minus-tracker rates --check | --update\n");
|
|
1957
|
+
return 2;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1449
1960
|
// src/cli/commands/config.ts
|
|
1450
1961
|
var SUPPORTED_LOCALES2 = ["it", "en"];
|
|
1451
1962
|
async function runConfig(positional, flags, s, stdout, stderr) {
|