@gabrielerandelli/minus-tracker 0.8.1 → 0.8.2

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 CHANGED
@@ -113,16 +113,16 @@ npx @gabrielerandelli/minus-tracker calc trades.csv
113
113
 
114
114
  ### Utilizzo CLI
115
115
 
116
- | Comando | Flag principali | Note |
117
- | ---------------------- | -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
118
- | `calc <file.csv>` | `--method LIFO\|FIFO` (default: LIFO), `--lang it\|en`, `--json`, `--export-dichiarazione [path]`, `--carry-forward` | Non aggiorna mai i tassi BCE da solo — esegui `rates --update` periodicamente; usa il sidecar di `classify` se presente |
119
- | `classify <file.csv>` | `--offline` | Classifica gli strumenti (Bucket A/B) e crea/aggiorna il sidecar `*.classify.json` |
120
- | `validate <file.csv>` | `--lang it\|en` | Exit 0 con avvisi; exit 1 in caso di errori bloccanti |
121
- | `rates --check` | — | Mostra la copertura della snapshot BCE in locale |
122
- | `rates --update` | — | Scarica i tassi aggiornati dall'API BCE |
123
- | `config --lang it\|en` | — | Salva la lingua preferita |
124
- | `config --show` | — | Mostra la lingua correntemente impostata |
125
- | `stress-test` | `--range N-M`, `--keep`, `--json`, `--output-dir` | Documentato in fondo |
116
+ | Comando | Flag principali | Note |
117
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
118
+ | `calc <file.csv>` | `--method LIFO\|FIFO` (default: LIFO), `--lang it\|en`, `--json`, `--export-dichiarazione [path]`, `--carry-forward` | Non aggiorna mai i tassi BCE da solo — esegui `rates --update` periodicamente; usa il sidecar di `classify` se presente |
119
+ | `classify <file.csv>` | `--offline` | Classifica gli strumenti (Bucket A/B) e crea/aggiorna il sidecar `*.classify.json`. Richiede un terminale interattivo (TTY), oppure il flag `--offline` in contesti scriptati/CI |
120
+ | `validate <file.csv>` | `--lang it\|en` | Exit 0 con avvisi; exit 1 in caso di errori bloccanti |
121
+ | `rates --check` | — | Mostra la copertura della snapshot BCE in locale |
122
+ | `rates --update` | — | Scarica i tassi aggiornati dall'API BCE |
123
+ | `config --lang it\|en` | — | Salva la lingua preferita |
124
+ | `config --show` | — | Mostra la lingua correntemente impostata |
125
+ | `stress-test` | `--range N-M`, `--keep`, `--json`, `--output-dir` | Documentato in fondo |
126
126
 
127
127
  Precedenza lingua: `--lang` > lingua salvata > italiano (default).
128
128
 
@@ -409,16 +409,16 @@ npx @gabrielerandelli/minus-tracker calc trades.csv
409
409
 
410
410
  ### CLI Usage
411
411
 
412
- | Command | Key flags | Notes |
413
- | ---------------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
414
- | `calc <file.csv>` | `--method LIFO\|FIFO` (default: LIFO), `--lang it\|en`, `--json`, `--export-dichiarazione [path]`, `--carry-forward` | Never fetches ECB rates on its own — run `rates --update` periodically; uses `classify`'s sidecar when present |
415
- | `classify <file.csv>` | `--offline` | Classifies instruments (Bucket A/B) and creates/updates the `*.classify.json` sidecar |
416
- | `validate <file.csv>` | `--lang it\|en` | Exit 0 with warnings; exit 1 on hard errors |
417
- | `rates --check` | — | Shows bundled ECB snapshot coverage |
418
- | `rates --update` | — | Fetches fresh rates from the ECB API |
419
- | `config --lang it\|en` | — | Saves language preference |
420
- | `config --show` | — | Shows current language setting |
421
- | `stress-test` | `--range N-M`, `--keep`, `--json`, `--output-dir` | Documented below |
412
+ | Command | Key flags | Notes |
413
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
414
+ | `calc <file.csv>` | `--method LIFO\|FIFO` (default: LIFO), `--lang it\|en`, `--json`, `--export-dichiarazione [path]`, `--carry-forward` | Never fetches ECB rates on its own — run `rates --update` periodically; uses `classify`'s sidecar when present |
415
+ | `classify <file.csv>` | `--offline` | Classifies instruments (Bucket A/B) and creates/updates the `*.classify.json` sidecar. Requires an interactive terminal (TTY), or the `--offline` flag in scripted/CI contexts |
416
+ | `validate <file.csv>` | `--lang it\|en` | Exit 0 with warnings; exit 1 on hard errors |
417
+ | `rates --check` | — | Shows bundled ECB snapshot coverage |
418
+ | `rates --update` | — | Fetches fresh rates from the ECB API |
419
+ | `config --lang it\|en` | — | Saves language preference |
420
+ | `config --show` | — | Shows current language setting |
421
+ | `stress-test` | `--range N-M`, `--keep`, `--json`, `--output-dir` | Documented below |
422
422
 
423
423
  Language precedence: `--lang` flag > saved config > Italian (default).
424
424
 
package/dist/cli/index.js CHANGED
@@ -383,6 +383,16 @@ var REQUIRED_COLUMNS = [
383
383
  ];
384
384
  var INCOME_KEYWORDS = ["DIVIDEND", "COUPON", "INTEREST", "CEDOLA"];
385
385
  var TAX_KEYWORDS = ["DIVIDEND TAX", "WITHHOLDING", "RITENUTA"];
386
+ var BINARY_GARBAGE_CONTROL_CHAR_RATIO = 0.1;
387
+ function looksLikeBinaryGarbage(firstLine) {
388
+ if (firstLine.length === 0) return false;
389
+ let controlChars = 0;
390
+ for (let i = 0; i < firstLine.length; i++) {
391
+ const code = firstLine.charCodeAt(i);
392
+ if (code < 32 && code !== 9) controlChars++;
393
+ }
394
+ return controlChars / firstLine.length > BINARY_GARBAGE_CONTROL_CHAR_RATIO;
395
+ }
386
396
  function parseCSVRow(line) {
387
397
  const fields = [];
388
398
  let i = 0;
@@ -445,6 +455,10 @@ var DEGIROParser = class {
445
455
  if (typeof csv !== "string" || csv.includes("\0")) {
446
456
  throw new ParseError("INVALID_CSV");
447
457
  }
458
+ const firstLine = csv.split("\n", 1)[0] ?? "";
459
+ if (looksLikeBinaryGarbage(firstLine)) {
460
+ throw new ParseError("INVALID_CSV");
461
+ }
448
462
  let rows;
449
463
  try {
450
464
  rows = parseCSV(csv);
@@ -960,12 +974,19 @@ var Calculator = class {
960
974
  );
961
975
  let remaining = bPlusvalenze - bMinusvalenze;
962
976
  let carryForwardApplied = 0;
977
+ const carryForwardEntriesRemaining = [];
963
978
  for (const entry of carryForwards) {
964
979
  if (taxYear - entry.year > 4) continue;
965
- if (remaining <= 0) break;
966
- const consumed = Math.min(entry.amount, remaining);
980
+ const consumed = remaining > 0 ? Math.min(entry.amount, remaining) : 0;
967
981
  carryForwardApplied += consumed;
968
982
  remaining -= consumed;
983
+ const residual = roundHalfUp2(entry.amount - consumed);
984
+ if (residual > 0) {
985
+ carryForwardEntriesRemaining.push({
986
+ annoOrigine: entry.year,
987
+ importo: residual
988
+ });
989
+ }
969
990
  }
970
991
  carryForwardApplied = roundHalfUp2(carryForwardApplied);
971
992
  const bNetResult = roundHalfUp2(
@@ -977,6 +998,7 @@ var Calculator = class {
977
998
  minusvalenze: bMinusvalenze,
978
999
  carryForwardApplied,
979
1000
  carryForwardRemaining,
1001
+ carryForwardEntriesRemaining,
980
1002
  netResult: bNetResult
981
1003
  };
982
1004
  const allIncomeRows = this._options.incomeRows ?? [];
@@ -1021,7 +1043,7 @@ var Calculator = class {
1021
1043
  const hasOther = allIsins.some((isin) => !isin.startsWith("IE"));
1022
1044
  if (hasIePrefix && hasOther) {
1023
1045
  warnings.push(
1024
- "AVVISO: CSV contiene tipi di strumenti misti (es. ETF + Azioni).\n Il calcolo a bucket unico pu\xF2 non essere fiscalmente corretto.\n Esegui: minus-tracker classify trades.csv"
1046
+ "WARNING: CSV contains mixed instrument types (e.g. ETFs + Stocks).\n Single-bucket calculation may not be fiscally correct.\n Run: minus-tracker classify trades.csv"
1025
1047
  );
1026
1048
  }
1027
1049
  }
@@ -2370,6 +2392,7 @@ async function runStressTest(_positionals, flags, stdout, stderr) {
2370
2392
  const cliBin = process.argv[1];
2371
2393
  let outputDir;
2372
2394
  const outputDirFlag = flags["output-dir"];
2395
+ const isAutoGenerated = !outputDirFlag;
2373
2396
  if (outputDirFlag) {
2374
2397
  outputDir = outputDirFlag;
2375
2398
  fs7.mkdirSync(outputDir, { recursive: true });
@@ -2410,8 +2433,15 @@ async function runStressTest(_positionals, flags, stdout, stderr) {
2410
2433
  } else {
2411
2434
  stdout.write(formatTable(report) + "\n");
2412
2435
  }
2413
- if (!flags["keep"]) {
2414
- fs7.rmSync(outputDir, { recursive: true, force: true });
2436
+ if (isAutoGenerated) {
2437
+ if (!flags["keep"]) {
2438
+ fs7.rmSync(outputDir, { recursive: true, force: true });
2439
+ }
2440
+ } else if (!flags["keep"]) {
2441
+ stdout.write(
2442
+ `Note: --output-dir was user-specified \u2014 leaving ${outputDir} in place.
2443
+ `
2444
+ );
2415
2445
  }
2416
2446
  return report.failed === 0 ? 0 : 1;
2417
2447
  }
@@ -2457,43 +2487,9 @@ async function runClassify(positional, flags, s, stdout, stderr) {
2457
2487
  const sidecarPath = base + ".classify.json";
2458
2488
  if (offline) {
2459
2489
  stdout.write(s.classifyOfflineWarning + "\n");
2460
- const isinToProduct = /* @__PURE__ */ new Map();
2461
- for (const tx of transactions) {
2462
- if (tx.isin && !isinToProduct.has(tx.isin)) {
2463
- isinToProduct.set(tx.isin, tx.product);
2464
- }
2465
- }
2466
- const classifications = {};
2467
- for (const [isin, product] of isinToProduct.entries()) {
2468
- classifications[isin] = {
2469
- product,
2470
- assetClass: "ETF",
2471
- bucketGain: "A",
2472
- bucketLoss: "B",
2473
- taxRate: 0.26,
2474
- whiteListed: null,
2475
- confirmedByUser: true,
2476
- source: "user"
2477
- };
2478
- }
2479
- const sidecarContent = JSON.stringify(
2480
- {
2481
- version: 1,
2482
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2483
- classifications,
2484
- warnings: []
2485
- },
2486
- null,
2487
- 2
2488
- );
2489
- try {
2490
- fs8.writeFileSync(sidecarPath, sidecarContent, "utf-8");
2491
- } catch {
2492
- stderr.write(`Cannot write sidecar: ${sidecarPath}
2493
- `);
2494
- return 1;
2495
- }
2496
- stdout.write(s.classifyWritten(sidecarPath) + "\n");
2490
+ const classifier2 = new Classifier({ interactive: false });
2491
+ await classifier2.classify(transactions, sidecarPath, { offline: true });
2492
+ stdout.write(s.classifyWritten(path6.resolve(sidecarPath)) + "\n");
2497
2493
  return 0;
2498
2494
  }
2499
2495
  const classifier = new Classifier({ interactive: true });