@gabrielerandelli/minus-tracker 0.5.8 → 0.6.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 +29 -4
- package/dist/cli/index.js +762 -42
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +461 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -6
- package/dist/index.d.ts +60 -6
- package/dist/index.js +459 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -32,6 +32,8 @@ var src_exports = {};
|
|
|
32
32
|
__export(src_exports, {
|
|
33
33
|
CalculationError: () => CalculationError,
|
|
34
34
|
Calculator: () => Calculator,
|
|
35
|
+
ClassificationError: () => ClassificationError,
|
|
36
|
+
Classifier: () => Classifier,
|
|
35
37
|
DEGIROParser: () => DEGIROParser,
|
|
36
38
|
ParseError: () => ParseError
|
|
37
39
|
});
|
|
@@ -59,6 +61,16 @@ var CalculationError = class extends Error {
|
|
|
59
61
|
this.date = date;
|
|
60
62
|
}
|
|
61
63
|
};
|
|
64
|
+
var ClassificationError = class extends Error {
|
|
65
|
+
code;
|
|
66
|
+
constructor(code, message) {
|
|
67
|
+
super(
|
|
68
|
+
message ?? (code === "SIDECAR_NOT_FOUND" ? "Sidecar file not found" : code === "SIDECAR_VERSION" ? "Sidecar version mismatch" : code === "SIDECAR_MALFORMED" ? "Sidecar file is malformed JSON" : code === "NETWORK_ERROR" ? "Network error contacting OpenFIGI" : "Failed to write sidecar file")
|
|
69
|
+
);
|
|
70
|
+
this.name = "ClassificationError";
|
|
71
|
+
this.code = code;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
62
74
|
|
|
63
75
|
// src/rates/index.ts
|
|
64
76
|
var import_node_url = require("url");
|
|
@@ -333,13 +345,11 @@ function inferTaxYear(transactions) {
|
|
|
333
345
|
var Calculator = class {
|
|
334
346
|
_transactions;
|
|
335
347
|
_parseWarnings;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
* @param parseWarnings - Warnings collected during the import step; forwarded into the report.
|
|
339
|
-
*/
|
|
340
|
-
constructor(transactions, parseWarnings) {
|
|
348
|
+
_options;
|
|
349
|
+
constructor(transactions, parseWarnings, options) {
|
|
341
350
|
this._transactions = transactions;
|
|
342
351
|
this._parseWarnings = parseWarnings ?? [];
|
|
352
|
+
this._options = options ?? {};
|
|
343
353
|
}
|
|
344
354
|
/**
|
|
345
355
|
* Run LIFO or FIFO lot-matching over the transaction list.
|
|
@@ -433,6 +443,104 @@ var Calculator = class {
|
|
|
433
443
|
if (lot.gainLossEUR > 0) plusvalenze += lot.gainLossEUR;
|
|
434
444
|
else minusvalenze += Math.abs(lot.gainLossEUR);
|
|
435
445
|
}
|
|
446
|
+
if (this._options.classification) {
|
|
447
|
+
const classification = this._options.classification;
|
|
448
|
+
const unclassifiedIsins = /* @__PURE__ */ new Set();
|
|
449
|
+
for (const lot of matchedLots) {
|
|
450
|
+
const entry = classification[lot.isin];
|
|
451
|
+
if (!entry) {
|
|
452
|
+
lot.bucket = "B";
|
|
453
|
+
unclassifiedIsins.add(lot.isin);
|
|
454
|
+
} else if (entry.bucketGain === "A" && lot.gainLossEUR >= 0) {
|
|
455
|
+
lot.bucket = "A";
|
|
456
|
+
} else {
|
|
457
|
+
lot.bucket = "B";
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
for (const isin of unclassifiedIsins) {
|
|
461
|
+
warnings.push(
|
|
462
|
+
`ISIN ${isin} not found in classification map \u2014 assigned to Bucket B.`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const bucketALots = matchedLots.filter((l) => l.bucket === "A");
|
|
466
|
+
const groupsByRate = /* @__PURE__ */ new Map();
|
|
467
|
+
for (const lot of bucketALots) {
|
|
468
|
+
const entry = classification[lot.isin];
|
|
469
|
+
const rate = entry.taxRate;
|
|
470
|
+
if (!groupsByRate.has(rate))
|
|
471
|
+
groupsByRate.set(rate, { assetClasses: /* @__PURE__ */ new Set(), plusvalenze: 0 });
|
|
472
|
+
const g = groupsByRate.get(rate);
|
|
473
|
+
g.assetClasses.add(entry.assetClass);
|
|
474
|
+
g.plusvalenze += lot.gainLossEUR;
|
|
475
|
+
}
|
|
476
|
+
const bucketAGroups = [...groupsByRate.entries()].map(([taxRate, g]) => ({
|
|
477
|
+
taxRate,
|
|
478
|
+
assetClasses: [...g.assetClasses],
|
|
479
|
+
plusvalenze: roundHalfUp(g.plusvalenze),
|
|
480
|
+
imposta: roundHalfUp(g.plusvalenze * taxRate)
|
|
481
|
+
}));
|
|
482
|
+
const bucketAReport = {
|
|
483
|
+
groups: bucketAGroups,
|
|
484
|
+
totalImposta: roundHalfUp(
|
|
485
|
+
bucketAGroups.reduce((s, g) => s + g.imposta, 0)
|
|
486
|
+
)
|
|
487
|
+
};
|
|
488
|
+
const bucketBLots = matchedLots.filter((l) => l.bucket === "B");
|
|
489
|
+
let bPlusvalenze = 0;
|
|
490
|
+
let bMinusvalenze = 0;
|
|
491
|
+
for (const lot of bucketBLots) {
|
|
492
|
+
if (lot.gainLossEUR >= 0) bPlusvalenze += lot.gainLossEUR;
|
|
493
|
+
else bMinusvalenze += Math.abs(lot.gainLossEUR);
|
|
494
|
+
}
|
|
495
|
+
bPlusvalenze = roundHalfUp(bPlusvalenze);
|
|
496
|
+
bMinusvalenze = roundHalfUp(bMinusvalenze);
|
|
497
|
+
const carryForwards = [...this._options.carryForward ?? []].sort(
|
|
498
|
+
(a, b) => a.year - b.year
|
|
499
|
+
);
|
|
500
|
+
let remaining = bPlusvalenze - bMinusvalenze;
|
|
501
|
+
let carryForwardApplied = 0;
|
|
502
|
+
for (const entry of carryForwards) {
|
|
503
|
+
if (taxYear - entry.year > 4) continue;
|
|
504
|
+
if (remaining <= 0) break;
|
|
505
|
+
const consumed = Math.min(entry.amount, remaining);
|
|
506
|
+
carryForwardApplied += consumed;
|
|
507
|
+
remaining -= consumed;
|
|
508
|
+
}
|
|
509
|
+
carryForwardApplied = roundHalfUp(carryForwardApplied);
|
|
510
|
+
const bNetResult = roundHalfUp(
|
|
511
|
+
bPlusvalenze - bMinusvalenze - carryForwardApplied
|
|
512
|
+
);
|
|
513
|
+
const carryForwardRemaining = roundHalfUp(Math.max(0, -bNetResult));
|
|
514
|
+
const bucketBReport = {
|
|
515
|
+
plusvalenze: bPlusvalenze,
|
|
516
|
+
minusvalenze: bMinusvalenze,
|
|
517
|
+
carryForwardApplied,
|
|
518
|
+
carryForwardRemaining,
|
|
519
|
+
netResult: bNetResult
|
|
520
|
+
};
|
|
521
|
+
return {
|
|
522
|
+
method,
|
|
523
|
+
taxYear,
|
|
524
|
+
plusvalenze: roundHalfUp(plusvalenze),
|
|
525
|
+
minusvalenze: roundHalfUp(minusvalenze),
|
|
526
|
+
netResult: roundHalfUp(plusvalenze - minusvalenze),
|
|
527
|
+
lots: matchedLots,
|
|
528
|
+
ratesUsed,
|
|
529
|
+
warnings,
|
|
530
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
531
|
+
bucketA: bucketAGroups.length > 0 ? bucketAReport : void 0,
|
|
532
|
+
bucketB: bucketBReport
|
|
533
|
+
};
|
|
534
|
+
} else {
|
|
535
|
+
const allIsins = [...new Set(this._transactions.map((t) => t.isin))];
|
|
536
|
+
const hasIePrefix = allIsins.some((isin) => isin.startsWith("IE"));
|
|
537
|
+
const hasOther = allIsins.some((isin) => !isin.startsWith("IE"));
|
|
538
|
+
if (hasIePrefix && hasOther) {
|
|
539
|
+
warnings.push(
|
|
540
|
+
"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"
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
436
544
|
return {
|
|
437
545
|
method,
|
|
438
546
|
taxYear,
|
|
@@ -446,10 +554,358 @@ var Calculator = class {
|
|
|
446
554
|
};
|
|
447
555
|
}
|
|
448
556
|
};
|
|
557
|
+
|
|
558
|
+
// src/classifier/index.ts
|
|
559
|
+
var fs2 = __toESM(require("fs"), 1);
|
|
560
|
+
var https = __toESM(require("https"), 1);
|
|
561
|
+
function httpsPost(url, body, timeoutMs) {
|
|
562
|
+
return new Promise((resolve, reject) => {
|
|
563
|
+
const u = new URL(url);
|
|
564
|
+
const req = https.request(
|
|
565
|
+
{
|
|
566
|
+
hostname: u.hostname,
|
|
567
|
+
path: u.pathname + u.search,
|
|
568
|
+
method: "POST",
|
|
569
|
+
headers: {
|
|
570
|
+
"Content-Type": "application/json",
|
|
571
|
+
"Content-Length": Buffer.byteLength(body)
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
(res) => {
|
|
575
|
+
let data = "";
|
|
576
|
+
res.on("data", (chunk) => {
|
|
577
|
+
data += chunk;
|
|
578
|
+
});
|
|
579
|
+
res.on("end", () => resolve({ status: res.statusCode ?? 0, data }));
|
|
580
|
+
}
|
|
581
|
+
);
|
|
582
|
+
req.setTimeout(timeoutMs, () => {
|
|
583
|
+
req.destroy();
|
|
584
|
+
reject(new Error("timeout"));
|
|
585
|
+
});
|
|
586
|
+
req.on("error", reject);
|
|
587
|
+
req.write(body);
|
|
588
|
+
req.end();
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
var GOVT_BOND_WHITELIST = /* @__PURE__ */ new Set([
|
|
592
|
+
"IT",
|
|
593
|
+
"DE",
|
|
594
|
+
"FR",
|
|
595
|
+
"AT",
|
|
596
|
+
"BE",
|
|
597
|
+
"NL",
|
|
598
|
+
"ES",
|
|
599
|
+
"PT",
|
|
600
|
+
"FI",
|
|
601
|
+
"IE",
|
|
602
|
+
"LU",
|
|
603
|
+
"GR",
|
|
604
|
+
"SK",
|
|
605
|
+
"SI",
|
|
606
|
+
"LT",
|
|
607
|
+
"LV",
|
|
608
|
+
"EE",
|
|
609
|
+
"MT",
|
|
610
|
+
"CY",
|
|
611
|
+
"US",
|
|
612
|
+
"GB",
|
|
613
|
+
"CH",
|
|
614
|
+
"NO",
|
|
615
|
+
"SE",
|
|
616
|
+
"DK",
|
|
617
|
+
"JP",
|
|
618
|
+
"CA",
|
|
619
|
+
"AU",
|
|
620
|
+
"NZ",
|
|
621
|
+
"SG",
|
|
622
|
+
"HK",
|
|
623
|
+
"KR"
|
|
624
|
+
]);
|
|
625
|
+
var SECURITY_TYPE_MAP = {
|
|
626
|
+
ETP: {
|
|
627
|
+
assetClass: "ETF",
|
|
628
|
+
bucketGain: "A",
|
|
629
|
+
bucketLoss: "B",
|
|
630
|
+
taxRate: 0.26,
|
|
631
|
+
whiteListed: null
|
|
632
|
+
},
|
|
633
|
+
ETF: {
|
|
634
|
+
assetClass: "ETF",
|
|
635
|
+
bucketGain: "A",
|
|
636
|
+
bucketLoss: "B",
|
|
637
|
+
taxRate: 0.26,
|
|
638
|
+
whiteListed: null
|
|
639
|
+
},
|
|
640
|
+
"Mutual Fund": {
|
|
641
|
+
assetClass: "ETF",
|
|
642
|
+
bucketGain: "A",
|
|
643
|
+
bucketLoss: "B",
|
|
644
|
+
taxRate: 0.26,
|
|
645
|
+
whiteListed: null
|
|
646
|
+
},
|
|
647
|
+
"Open-End Fund": {
|
|
648
|
+
assetClass: "ETF",
|
|
649
|
+
bucketGain: "A",
|
|
650
|
+
bucketLoss: "B",
|
|
651
|
+
taxRate: 0.26,
|
|
652
|
+
whiteListed: null
|
|
653
|
+
},
|
|
654
|
+
"Common Stock": {
|
|
655
|
+
assetClass: "Stock",
|
|
656
|
+
bucketGain: "B",
|
|
657
|
+
bucketLoss: "B",
|
|
658
|
+
taxRate: 0,
|
|
659
|
+
whiteListed: null
|
|
660
|
+
},
|
|
661
|
+
Equity: {
|
|
662
|
+
assetClass: "Stock",
|
|
663
|
+
bucketGain: "B",
|
|
664
|
+
bucketLoss: "B",
|
|
665
|
+
taxRate: 0,
|
|
666
|
+
whiteListed: null
|
|
667
|
+
},
|
|
668
|
+
"Exchange Traded Commodity": {
|
|
669
|
+
assetClass: "ETC",
|
|
670
|
+
bucketGain: "B",
|
|
671
|
+
bucketLoss: "B",
|
|
672
|
+
taxRate: 0,
|
|
673
|
+
whiteListed: null
|
|
674
|
+
},
|
|
675
|
+
"Corporate Bond": {
|
|
676
|
+
assetClass: "CorpBond",
|
|
677
|
+
bucketGain: "B",
|
|
678
|
+
bucketLoss: "B",
|
|
679
|
+
taxRate: 0,
|
|
680
|
+
whiteListed: null
|
|
681
|
+
},
|
|
682
|
+
Option: {
|
|
683
|
+
assetClass: "Derivative",
|
|
684
|
+
bucketGain: "B",
|
|
685
|
+
bucketLoss: "B",
|
|
686
|
+
taxRate: 0,
|
|
687
|
+
whiteListed: null
|
|
688
|
+
},
|
|
689
|
+
Future: {
|
|
690
|
+
assetClass: "Derivative",
|
|
691
|
+
bucketGain: "B",
|
|
692
|
+
bucketLoss: "B",
|
|
693
|
+
taxRate: 0,
|
|
694
|
+
whiteListed: null
|
|
695
|
+
},
|
|
696
|
+
Warrant: {
|
|
697
|
+
assetClass: "Derivative",
|
|
698
|
+
bucketGain: "B",
|
|
699
|
+
bucketLoss: "B",
|
|
700
|
+
taxRate: 0,
|
|
701
|
+
whiteListed: null
|
|
702
|
+
},
|
|
703
|
+
CFD: {
|
|
704
|
+
assetClass: "Derivative",
|
|
705
|
+
bucketGain: "B",
|
|
706
|
+
bucketLoss: "B",
|
|
707
|
+
taxRate: 0,
|
|
708
|
+
whiteListed: null
|
|
709
|
+
},
|
|
710
|
+
Swap: {
|
|
711
|
+
assetClass: "Derivative",
|
|
712
|
+
bucketGain: "B",
|
|
713
|
+
bucketLoss: "B",
|
|
714
|
+
taxRate: 0,
|
|
715
|
+
whiteListed: null
|
|
716
|
+
},
|
|
717
|
+
"Leverage Certificate": {
|
|
718
|
+
assetClass: "LeverageCert",
|
|
719
|
+
bucketGain: "B",
|
|
720
|
+
bucketLoss: "B",
|
|
721
|
+
taxRate: 0,
|
|
722
|
+
whiteListed: null
|
|
723
|
+
},
|
|
724
|
+
"Turbo Certificate": {
|
|
725
|
+
assetClass: "LeverageCert",
|
|
726
|
+
bucketGain: "B",
|
|
727
|
+
bucketLoss: "B",
|
|
728
|
+
taxRate: 0,
|
|
729
|
+
whiteListed: null
|
|
730
|
+
},
|
|
731
|
+
"Capital Protected Certificate": {
|
|
732
|
+
assetClass: "CapProtectedCert",
|
|
733
|
+
bucketGain: "A",
|
|
734
|
+
bucketLoss: "B",
|
|
735
|
+
taxRate: 0.26,
|
|
736
|
+
whiteListed: null
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
function isKnownType(t) {
|
|
740
|
+
return t === "Government Bond" || t in SECURITY_TYPE_MAP;
|
|
741
|
+
}
|
|
742
|
+
function classifyByType(isin, securityType, product, warnings) {
|
|
743
|
+
if (securityType === "Government Bond") {
|
|
744
|
+
const prefix = isin.slice(0, 2).toUpperCase();
|
|
745
|
+
const whiteListed = GOVT_BOND_WHITELIST.has(prefix);
|
|
746
|
+
return {
|
|
747
|
+
product,
|
|
748
|
+
assetClass: whiteListed ? "GovtBondWL" : "GovtBondOther",
|
|
749
|
+
bucketGain: "A",
|
|
750
|
+
bucketLoss: "B",
|
|
751
|
+
taxRate: whiteListed ? 0.125 : 0.26,
|
|
752
|
+
whiteListed,
|
|
753
|
+
confirmedByUser: false,
|
|
754
|
+
source: "openfigi"
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
const mapped = SECURITY_TYPE_MAP[securityType];
|
|
758
|
+
if (mapped) {
|
|
759
|
+
return {
|
|
760
|
+
product,
|
|
761
|
+
assetClass: mapped.assetClass,
|
|
762
|
+
bucketGain: mapped.bucketGain,
|
|
763
|
+
bucketLoss: mapped.bucketLoss,
|
|
764
|
+
taxRate: mapped.taxRate,
|
|
765
|
+
whiteListed: mapped.whiteListed,
|
|
766
|
+
confirmedByUser: false,
|
|
767
|
+
source: "openfigi"
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
warnings.push(
|
|
771
|
+
`Unrecognized type: ${securityType}. Please classify manually.`
|
|
772
|
+
);
|
|
773
|
+
return {
|
|
774
|
+
product,
|
|
775
|
+
assetClass: "Stock",
|
|
776
|
+
bucketGain: "B",
|
|
777
|
+
bucketLoss: "B",
|
|
778
|
+
taxRate: 0,
|
|
779
|
+
whiteListed: null,
|
|
780
|
+
confirmedByUser: false,
|
|
781
|
+
source: "user"
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
var Classifier = class {
|
|
785
|
+
interactive;
|
|
786
|
+
constructor(options) {
|
|
787
|
+
this.interactive = options?.interactive ?? true;
|
|
788
|
+
}
|
|
789
|
+
async load(sidecarPath) {
|
|
790
|
+
if (!fs2.existsSync(sidecarPath)) {
|
|
791
|
+
throw new ClassificationError("SIDECAR_NOT_FOUND");
|
|
792
|
+
}
|
|
793
|
+
let parsed;
|
|
794
|
+
try {
|
|
795
|
+
const raw = fs2.readFileSync(sidecarPath, "utf-8");
|
|
796
|
+
parsed = JSON.parse(raw);
|
|
797
|
+
} catch {
|
|
798
|
+
throw new ClassificationError("SIDECAR_MALFORMED");
|
|
799
|
+
}
|
|
800
|
+
if (typeof parsed !== "object" || parsed === null || parsed["version"] !== 1) {
|
|
801
|
+
throw new ClassificationError("SIDECAR_VERSION");
|
|
802
|
+
}
|
|
803
|
+
return parsed.classifications;
|
|
804
|
+
}
|
|
805
|
+
async classify(transactions, sidecarPath, _httpPost = httpsPost) {
|
|
806
|
+
const isinToProduct = /* @__PURE__ */ new Map();
|
|
807
|
+
for (const tx of transactions) {
|
|
808
|
+
if (tx.isin && !isinToProduct.has(tx.isin)) {
|
|
809
|
+
isinToProduct.set(tx.isin, tx.product);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const confirmed = {};
|
|
813
|
+
if (fs2.existsSync(sidecarPath)) {
|
|
814
|
+
const existingMap = await this.load(sidecarPath);
|
|
815
|
+
for (const [isin, entry] of Object.entries(existingMap)) {
|
|
816
|
+
if (entry.confirmedByUser) {
|
|
817
|
+
confirmed[isin] = entry;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
const toProcess = [];
|
|
822
|
+
for (const isin of isinToProduct.keys()) {
|
|
823
|
+
if (!(isin in confirmed)) {
|
|
824
|
+
toProcess.push(isin);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
const warnings = [];
|
|
828
|
+
const newEntries = {};
|
|
829
|
+
const BATCH_SIZE = 10;
|
|
830
|
+
for (let i = 0; i < toProcess.length; i += BATCH_SIZE) {
|
|
831
|
+
if (i > 0) {
|
|
832
|
+
await new Promise((resolve) => setTimeout(resolve, 6e3));
|
|
833
|
+
}
|
|
834
|
+
const batch = toProcess.slice(i, i + BATCH_SIZE);
|
|
835
|
+
const requestBody = JSON.stringify(
|
|
836
|
+
batch.map((isin) => ({ idType: "ID_ISIN", idValue: isin }))
|
|
837
|
+
);
|
|
838
|
+
let response = null;
|
|
839
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
840
|
+
try {
|
|
841
|
+
const r = await _httpPost(
|
|
842
|
+
"https://api.openfigi.com/v3/mapping",
|
|
843
|
+
requestBody,
|
|
844
|
+
1e4
|
|
845
|
+
);
|
|
846
|
+
if (r.status < 500) {
|
|
847
|
+
response = r;
|
|
848
|
+
break;
|
|
849
|
+
}
|
|
850
|
+
} catch {
|
|
851
|
+
throw new ClassificationError("NETWORK_ERROR");
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
if (response === null) {
|
|
855
|
+
throw new ClassificationError("NETWORK_ERROR");
|
|
856
|
+
}
|
|
857
|
+
const results = JSON.parse(response.data);
|
|
858
|
+
for (let j = 0; j < batch.length; j++) {
|
|
859
|
+
const isin = batch[j];
|
|
860
|
+
const result = results[j];
|
|
861
|
+
const product = isinToProduct.get(isin) ?? isin;
|
|
862
|
+
if (!result || result.error || !result.data || result.data.length === 0) {
|
|
863
|
+
warnings.push(
|
|
864
|
+
`Unrecognized type: unknown. Please classify manually.`
|
|
865
|
+
);
|
|
866
|
+
newEntries[isin] = {
|
|
867
|
+
product,
|
|
868
|
+
assetClass: "Stock",
|
|
869
|
+
bucketGain: "B",
|
|
870
|
+
bucketLoss: "B",
|
|
871
|
+
taxRate: 0,
|
|
872
|
+
whiteListed: null,
|
|
873
|
+
confirmedByUser: false,
|
|
874
|
+
source: "user"
|
|
875
|
+
};
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
const st = result.data[0]?.securityType;
|
|
879
|
+
const st2 = result.data[0]?.securityType2;
|
|
880
|
+
const typeToUse = st && isKnownType(st) ? st : st2 && isKnownType(st2) ? st2 : st ?? st2 ?? "Unknown";
|
|
881
|
+
newEntries[isin] = classifyByType(isin, typeToUse, product, warnings);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
const mergedMap = { ...newEntries, ...confirmed };
|
|
885
|
+
const sidecarContent = JSON.stringify(
|
|
886
|
+
{
|
|
887
|
+
version: 1,
|
|
888
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
889
|
+
classifications: mergedMap,
|
|
890
|
+
warnings
|
|
891
|
+
},
|
|
892
|
+
null,
|
|
893
|
+
2
|
|
894
|
+
);
|
|
895
|
+
try {
|
|
896
|
+
fs2.writeFileSync(sidecarPath, sidecarContent, "utf-8");
|
|
897
|
+
} catch {
|
|
898
|
+
throw new ClassificationError("WRITE_ERROR");
|
|
899
|
+
}
|
|
900
|
+
return mergedMap;
|
|
901
|
+
}
|
|
902
|
+
};
|
|
449
903
|
// Annotate the CommonJS export names for ESM import in node:
|
|
450
904
|
0 && (module.exports = {
|
|
451
905
|
CalculationError,
|
|
452
906
|
Calculator,
|
|
907
|
+
ClassificationError,
|
|
908
|
+
Classifier,
|
|
453
909
|
DEGIROParser,
|
|
454
910
|
ParseError
|
|
455
911
|
});
|