@gabrielerandelli/minus-tracker 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -9,6 +9,9 @@ var it = {
9
9
  errorInvalidCsv: "CSV non valido: impossibile analizzare il file",
10
10
  errorMissingColumn: (col) => `Colonna obbligatoria mancante: ${col}`,
11
11
  errorNoOpenLots: (isin, date) => `Nessun lotto aperto per ISIN ${isin} in data ${date}`,
12
+ errorCannotReadFile: (path7) => `Impossibile leggere il file: ${path7}`,
13
+ errorCannotLoadSidecar: (path7) => `Impossibile caricare il sidecar: ${path7}`,
14
+ errorCannotWriteExport: (path7) => `Impossibile scrivere l'export della dichiarazione: ${path7}`,
12
15
  warnMissingIsin: (row) => `Riga ${row}: ISIN mancante \u2014 riga ignorata`,
13
16
  warnUnsupportedCurrency: (row, currency) => `Riga ${row}: valuta non supportata ${currency} \u2014 riga ignorata`,
14
17
  warnNoEcbRate: (row, currency, date) => `Riga ${row}: nessun tasso BCE per ${currency} in data ${date} \u2014 riga ignorata`,
@@ -88,7 +91,7 @@ var it = {
88
91
  quadroRMRitenuta: "Ritenuta estera (credito)",
89
92
  quadroRMCedole: "[RM-C] Cedole obbligazionarie",
90
93
  dichiarazioneDisclaimer: "minus-tracker \xE8 un ausilio al calcolo, non consulenza fiscale.",
91
- warnNoDichiarazioneSidecar: "Eseguire prima: minus-tracker classify <file>",
94
+ warnNoDichiarazioneSidecar: "Suggerimento: esegui prima 'minus-tracker classify <file>' per abilitare il Quadro RT/RM.",
92
95
  warnNoCarryForwardProvided: "(eventuali perdite pregresse non applicate)"
93
96
  };
94
97
 
@@ -98,6 +101,9 @@ var en = {
98
101
  errorInvalidCsv: "Invalid CSV: unable to parse",
99
102
  errorMissingColumn: (col) => `Missing required column: ${col}`,
100
103
  errorNoOpenLots: (isin, date) => `No open lots for ISIN ${isin} on ${date}`,
104
+ errorCannotReadFile: (path7) => `Cannot read file: ${path7}`,
105
+ errorCannotLoadSidecar: (path7) => `Cannot load sidecar: ${path7}`,
106
+ errorCannotWriteExport: (path7) => `Cannot write dichiarazione export: ${path7}`,
101
107
  warnMissingIsin: (row) => `Row ${row}: missing ISIN \u2014 skipped`,
102
108
  warnUnsupportedCurrency: (row, currency) => `Row ${row}: unsupported currency ${currency} \u2014 skipped`,
103
109
  warnNoEcbRate: (row, currency, date) => `Row ${row}: no ECB rate for ${currency} on ${date} \u2014 skipped`,
@@ -177,7 +183,7 @@ var en = {
177
183
  quadroRMRitenuta: "Foreign withholding (credit)",
178
184
  quadroRMCedole: "[RM-C] Bond coupons",
179
185
  dichiarazioneDisclaimer: "minus-tracker \xE8 un ausilio al calcolo, non consulenza fiscale.",
180
- warnNoDichiarazioneSidecar: "Run first: minus-tracker classify <file>",
186
+ warnNoDichiarazioneSidecar: "Tip: run 'minus-tracker classify <file>' first to enable Quadro RT/RM.",
181
187
  warnNoCarryForwardProvided: "(prior-year losses not applied)"
182
188
  };
183
189
 
@@ -1246,7 +1252,7 @@ function classifyByType(isin, securityType, product, warnings) {
1246
1252
  };
1247
1253
  }
1248
1254
  warnings.push(
1249
- `Unrecognized type: ${securityType}. Please classify manually.`
1255
+ `Unrecognized type for ${isin}: ${securityType}. Please classify manually.`
1250
1256
  );
1251
1257
  return {
1252
1258
  product,
@@ -1259,6 +1265,84 @@ function classifyByType(isin, securityType, product, warnings) {
1259
1265
  source: "user"
1260
1266
  };
1261
1267
  }
1268
+ var ASSET_CLASS_DEFAULTS = {
1269
+ ETF: {
1270
+ assetClass: "ETF",
1271
+ bucketGain: "A",
1272
+ bucketLoss: "B",
1273
+ taxRate: 0.26,
1274
+ whiteListed: null
1275
+ },
1276
+ Stock: {
1277
+ assetClass: "Stock",
1278
+ bucketGain: "B",
1279
+ bucketLoss: "B",
1280
+ taxRate: 0,
1281
+ whiteListed: null
1282
+ },
1283
+ ETC: {
1284
+ assetClass: "ETC",
1285
+ bucketGain: "B",
1286
+ bucketLoss: "B",
1287
+ taxRate: 0,
1288
+ whiteListed: null
1289
+ },
1290
+ GovtBondWL: {
1291
+ assetClass: "GovtBondWL",
1292
+ bucketGain: "A",
1293
+ bucketLoss: "B",
1294
+ taxRate: 0.125,
1295
+ whiteListed: true
1296
+ },
1297
+ GovtBondOther: {
1298
+ assetClass: "GovtBondOther",
1299
+ bucketGain: "A",
1300
+ bucketLoss: "B",
1301
+ taxRate: 0.26,
1302
+ whiteListed: false
1303
+ },
1304
+ CorpBond: {
1305
+ assetClass: "CorpBond",
1306
+ bucketGain: "B",
1307
+ bucketLoss: "B",
1308
+ taxRate: 0,
1309
+ whiteListed: null
1310
+ },
1311
+ Derivative: {
1312
+ assetClass: "Derivative",
1313
+ bucketGain: "B",
1314
+ bucketLoss: "B",
1315
+ taxRate: 0,
1316
+ whiteListed: null
1317
+ },
1318
+ LeverageCert: {
1319
+ assetClass: "LeverageCert",
1320
+ bucketGain: "B",
1321
+ bucketLoss: "B",
1322
+ taxRate: 0,
1323
+ whiteListed: null
1324
+ },
1325
+ CapProtectedCert: {
1326
+ assetClass: "CapProtectedCert",
1327
+ bucketGain: "A",
1328
+ bucketLoss: "B",
1329
+ taxRate: 0.26,
1330
+ whiteListed: null
1331
+ }
1332
+ };
1333
+ function buildEntryFromAssetClass(assetClass, product) {
1334
+ const mapped = ASSET_CLASS_DEFAULTS[assetClass];
1335
+ return {
1336
+ product,
1337
+ assetClass: mapped.assetClass,
1338
+ bucketGain: mapped.bucketGain,
1339
+ bucketLoss: mapped.bucketLoss,
1340
+ taxRate: mapped.taxRate,
1341
+ whiteListed: mapped.whiteListed,
1342
+ confirmedByUser: true,
1343
+ source: "user"
1344
+ };
1345
+ }
1262
1346
  var Classifier = class {
1263
1347
  interactive;
1264
1348
  constructor(options) {
@@ -1280,7 +1364,7 @@ var Classifier = class {
1280
1364
  }
1281
1365
  return parsed.classifications;
1282
1366
  }
1283
- async classify(transactions, sidecarPath, _httpPost = httpsPost) {
1367
+ async classify(transactions, sidecarPath, options, _httpPost = httpsPost) {
1284
1368
  const isinToProduct = /* @__PURE__ */ new Map();
1285
1369
  for (const tx of transactions) {
1286
1370
  if (tx.isin && !isinToProduct.has(tx.isin)) {
@@ -1288,13 +1372,21 @@ var Classifier = class {
1288
1372
  }
1289
1373
  }
1290
1374
  const confirmed = {};
1291
- if (fs3.existsSync(sidecarPath)) {
1292
- const existingMap = await this.load(sidecarPath);
1293
- for (const [isin, entry] of Object.entries(existingMap)) {
1294
- if (entry.confirmedByUser) {
1295
- confirmed[isin] = entry;
1375
+ if (sidecarPath !== void 0) {
1376
+ if (fs3.existsSync(sidecarPath)) {
1377
+ const existingMap = await this.load(sidecarPath);
1378
+ for (const [isin, entry] of Object.entries(existingMap)) {
1379
+ if (entry.confirmedByUser) {
1380
+ confirmed[isin] = entry;
1381
+ }
1296
1382
  }
1297
1383
  }
1384
+ } else if (options?.existingClassification) {
1385
+ Object.assign(confirmed, options.existingClassification);
1386
+ }
1387
+ for (const [isin, assetClass] of Object.entries(options?.overrides ?? {})) {
1388
+ const product = isinToProduct.get(isin) ?? isin;
1389
+ confirmed[isin] = buildEntryFromAssetClass(assetClass, product);
1298
1390
  }
1299
1391
  const toProcess = [];
1300
1392
  for (const isin of isinToProduct.keys()) {
@@ -1304,76 +1396,100 @@ var Classifier = class {
1304
1396
  }
1305
1397
  const warnings = [];
1306
1398
  const newEntries = {};
1307
- const BATCH_SIZE = 10;
1308
- for (let i = 0; i < toProcess.length; i += BATCH_SIZE) {
1309
- if (i > 0) {
1310
- await new Promise((resolve2) => setTimeout(resolve2, 6e3));
1399
+ if (options?.offline) {
1400
+ for (const isin of toProcess) {
1401
+ const product = isinToProduct.get(isin) ?? isin;
1402
+ warnings.push(
1403
+ `Unrecognized type for ${isin}: unknown. Please classify manually.`
1404
+ );
1405
+ newEntries[isin] = {
1406
+ product,
1407
+ assetClass: "Stock",
1408
+ bucketGain: "B",
1409
+ bucketLoss: "B",
1410
+ taxRate: 0,
1411
+ whiteListed: null,
1412
+ confirmedByUser: false,
1413
+ source: "user"
1414
+ };
1311
1415
  }
1312
- const batch = toProcess.slice(i, i + BATCH_SIZE);
1313
- const requestBody = JSON.stringify(
1314
- batch.map((isin) => ({ idType: "ID_ISIN", idValue: isin }))
1315
- );
1316
- let response = null;
1317
- for (let attempt = 0; attempt < 2; attempt++) {
1318
- try {
1319
- const r = await _httpPost(
1320
- "https://api.openfigi.com/v3/mapping",
1321
- requestBody,
1322
- 1e4
1323
- );
1324
- if (r.status < 500) {
1325
- response = r;
1326
- break;
1416
+ } else {
1417
+ const BATCH_SIZE = 10;
1418
+ const totalBatches = Math.ceil(toProcess.length / BATCH_SIZE);
1419
+ for (let i = 0; i < toProcess.length; i += BATCH_SIZE) {
1420
+ if (i > 0) {
1421
+ await new Promise((resolve2) => setTimeout(resolve2, 6e3));
1422
+ }
1423
+ const batch = toProcess.slice(i, i + BATCH_SIZE);
1424
+ const requestBody = JSON.stringify(
1425
+ batch.map((isin) => ({ idType: "ID_ISIN", idValue: isin }))
1426
+ );
1427
+ let response = null;
1428
+ for (let attempt = 0; attempt < 2; attempt++) {
1429
+ try {
1430
+ const r = await _httpPost(
1431
+ "https://api.openfigi.com/v3/mapping",
1432
+ requestBody,
1433
+ 1e4
1434
+ );
1435
+ if (r.status < 500) {
1436
+ response = r;
1437
+ break;
1438
+ }
1439
+ } catch {
1440
+ throw new ClassificationError("NETWORK_ERROR");
1327
1441
  }
1328
- } catch {
1442
+ }
1443
+ if (response === null) {
1329
1444
  throw new ClassificationError("NETWORK_ERROR");
1330
1445
  }
1331
- }
1332
- if (response === null) {
1333
- throw new ClassificationError("NETWORK_ERROR");
1334
- }
1335
- const results = JSON.parse(response.data);
1336
- for (let j = 0; j < batch.length; j++) {
1337
- const isin = batch[j];
1338
- const result = results[j];
1339
- const product = isinToProduct.get(isin) ?? isin;
1340
- if (!result || result.error || !result.data || result.data.length === 0) {
1341
- warnings.push(
1342
- `Unrecognized type: unknown. Please classify manually.`
1343
- );
1344
- newEntries[isin] = {
1345
- product,
1346
- assetClass: "Stock",
1347
- bucketGain: "B",
1348
- bucketLoss: "B",
1349
- taxRate: 0,
1350
- whiteListed: null,
1351
- confirmedByUser: false,
1352
- source: "user"
1353
- };
1354
- continue;
1446
+ const results = JSON.parse(response.data);
1447
+ for (let j = 0; j < batch.length; j++) {
1448
+ const isin = batch[j];
1449
+ const result = results[j];
1450
+ const product = isinToProduct.get(isin) ?? isin;
1451
+ if (!result || result.error || !result.data || result.data.length === 0) {
1452
+ warnings.push(
1453
+ `Unrecognized type for ${isin}: unknown. Please classify manually.`
1454
+ );
1455
+ newEntries[isin] = {
1456
+ product,
1457
+ assetClass: "Stock",
1458
+ bucketGain: "B",
1459
+ bucketLoss: "B",
1460
+ taxRate: 0,
1461
+ whiteListed: null,
1462
+ confirmedByUser: false,
1463
+ source: "user"
1464
+ };
1465
+ continue;
1466
+ }
1467
+ const st = result.data[0]?.securityType;
1468
+ const st2 = result.data[0]?.securityType2;
1469
+ const typeToUse = st && isKnownType(st) ? st : st2 && isKnownType(st2) ? st2 : st ?? st2 ?? "Unknown";
1470
+ newEntries[isin] = classifyByType(isin, typeToUse, product, warnings);
1355
1471
  }
1356
- const st = result.data[0]?.securityType;
1357
- const st2 = result.data[0]?.securityType2;
1358
- const typeToUse = st && isKnownType(st) ? st : st2 && isKnownType(st2) ? st2 : st ?? st2 ?? "Unknown";
1359
- newEntries[isin] = classifyByType(isin, typeToUse, product, warnings);
1472
+ const batchIndex = i / BATCH_SIZE;
1473
+ options?.onBatchProgress?.(batchIndex + 1, totalBatches);
1360
1474
  }
1361
1475
  }
1362
1476
  const mergedMap = { ...newEntries, ...confirmed };
1363
- const sidecarContent = JSON.stringify(
1364
- {
1365
- version: 1,
1366
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1367
- classifications: mergedMap,
1368
- warnings
1369
- },
1370
- null,
1371
- 2
1372
- );
1373
- try {
1374
- fs3.writeFileSync(sidecarPath, sidecarContent, "utf-8");
1375
- } catch {
1376
- throw new ClassificationError("WRITE_ERROR");
1477
+ if (sidecarPath !== void 0) {
1478
+ const sidecarContent = JSON.stringify(
1479
+ {
1480
+ version: 1,
1481
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1482
+ classifications: mergedMap,
1483
+ warnings
1484
+ },
1485
+ null,
1486
+ 2
1487
+ );
1488
+ try {
1489
+ fs3.writeFileSync(sidecarPath, sidecarContent, "utf-8");
1490
+ } catch {
1491
+ throw new ClassificationError("WRITE_ERROR");
1492
+ }
1377
1493
  }
1378
1494
  return mergedMap;
1379
1495
  }
@@ -1579,8 +1695,7 @@ async function runCalc(positional, flags, s, stdout, stderr) {
1579
1695
  try {
1580
1696
  csv = fs4.readFileSync(filePath, "utf8");
1581
1697
  } catch {
1582
- stderr.write(`Cannot read file: ${filePath}
1583
- `);
1698
+ stderr.write(s.errorCannotReadFile(filePath) + "\n");
1584
1699
  return 1;
1585
1700
  }
1586
1701
  const method = flags["method"] ?? "LIFO";
@@ -1595,8 +1710,7 @@ async function runCalc(positional, flags, s, stdout, stderr) {
1595
1710
  const classifier = new Classifier({ interactive: false });
1596
1711
  classification = await classifier.load(sidecarPath);
1597
1712
  } catch {
1598
- stderr.write(`Cannot load sidecar: ${sidecarPath}
1599
- `);
1713
+ stderr.write(s.errorCannotLoadSidecar(sidecarPath) + "\n");
1600
1714
  return 1;
1601
1715
  }
1602
1716
  }
@@ -1666,8 +1780,7 @@ async function runCalc(positional, flags, s, stdout, stderr) {
1666
1780
  try {
1667
1781
  await report.dichiarazione.exportTo(exportPath);
1668
1782
  } catch {
1669
- stderr.write(`Cannot write dichiarazione export: ${exportPath}
1670
- `);
1783
+ stderr.write(s.errorCannotWriteExport(exportPath) + "\n");
1671
1784
  return 1;
1672
1785
  }
1673
1786
  }
@@ -1759,18 +1872,45 @@ function getSnapshotPath() {
1759
1872
  }
1760
1873
  return path4.join(configDir, "minus-tracker", "ecb-rates.json");
1761
1874
  }
1875
+ function countMissingBusinessDays(startDate, endDate, dates) {
1876
+ let missing = 0;
1877
+ const cursor = /* @__PURE__ */ new Date(startDate + "T00:00:00Z");
1878
+ const end = /* @__PURE__ */ new Date(endDate + "T00:00:00Z");
1879
+ while (cursor <= end) {
1880
+ const day = cursor.getUTCDay();
1881
+ const iso = cursor.toISOString().slice(0, 10);
1882
+ if (day !== 0 && day !== 6 && dates[iso] === void 0) missing++;
1883
+ cursor.setUTCDate(cursor.getUTCDate() + 1);
1884
+ }
1885
+ return missing;
1886
+ }
1762
1887
  function getCoverage(snapshot) {
1763
1888
  let start = "9999-12-31";
1764
1889
  let end = "0000-01-01";
1765
1890
  const currencies = [];
1891
+ const gapEntries = [];
1766
1892
  for (const [ccy, dates] of Object.entries(snapshot)) {
1767
1893
  currencies.push(ccy);
1768
- for (const d of Object.keys(dates)) {
1894
+ const keys = Object.keys(dates).sort();
1895
+ for (const d of keys) {
1769
1896
  if (d < start) start = d;
1770
1897
  if (d > end) end = d;
1771
1898
  }
1899
+ if (keys.length > 0) {
1900
+ const missing = countMissingBusinessDays(
1901
+ keys[0],
1902
+ keys[keys.length - 1],
1903
+ dates
1904
+ );
1905
+ if (missing > 0) gapEntries.push(`${ccy}: ${missing}`);
1906
+ }
1772
1907
  }
1773
- return { start, end, currencies: currencies.sort().join(", ") };
1908
+ return {
1909
+ start,
1910
+ end,
1911
+ currencies: currencies.sort().join(", "),
1912
+ gaps: gapEntries.sort().join(", ")
1913
+ };
1774
1914
  }
1775
1915
  async function fetchEcbData(currency) {
1776
1916
  return new Promise((resolve2, reject) => {
@@ -1834,9 +1974,9 @@ async function updateRates(snapshotPath, stdout, stderr, s) {
1834
1974
  async function runRates(positional, flags, s, stdout, stderr) {
1835
1975
  if (flags["check"]) {
1836
1976
  const snapshot = getActiveSnapshot();
1837
- const { start, end, currencies } = getCoverage(snapshot);
1977
+ const { start, end, currencies, gaps } = getCoverage(snapshot);
1838
1978
  stdout.write(s.ratesCoverage(start, end, currencies) + "\n");
1839
- stdout.write(s.ratesGapsNone + "\n");
1979
+ stdout.write((gaps ? s.ratesGaps(gaps) : s.ratesGapsNone) + "\n");
1840
1980
  return 0;
1841
1981
  }
1842
1982
  if (flags["update"]) {
@@ -2402,7 +2542,9 @@ async function main() {
2402
2542
  range: { type: "string" },
2403
2543
  keep: { type: "boolean", default: false },
2404
2544
  "output-dir": { type: "string" },
2405
- offline: { type: "boolean", default: false }
2545
+ offline: { type: "boolean", default: false },
2546
+ "carry-forward": { type: "string", multiple: true },
2547
+ "export-dichiarazione": { type: "string" }
2406
2548
  },
2407
2549
  allowPositionals: true,
2408
2550
  strict: false