@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/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/errors.ts","../src/rates/index.ts","../src/parser/warnings.ts","../src/parser/index.ts","../src/calculator/index.ts","../src/classifier/index.ts"],"sourcesContent":["export class ParseError extends Error {\n readonly code: \"INVALID_CSV\" | \"MISSING_COLUMN\";\n readonly columnName?: string;\n\n constructor(code: \"INVALID_CSV\");\n constructor(code: \"MISSING_COLUMN\", columnName: string);\n constructor(code: ParseError[\"code\"], columnName?: string) {\n const msg =\n code === \"INVALID_CSV\"\n ? \"Invalid CSV: unable to parse\"\n : `Missing required column: ${columnName}`;\n super(msg);\n this.name = \"ParseError\";\n this.code = code;\n this.columnName = columnName;\n }\n}\n\nexport class CalculationError extends Error {\n readonly isin: string;\n readonly date: string;\n\n constructor(isin: string, date: string) {\n super(`No open lots for ISIN ${isin} on ${date}`);\n this.name = \"CalculationError\";\n this.isin = isin;\n this.date = date;\n }\n}\n\nexport class ClassificationError extends Error {\n readonly code:\n | \"NETWORK_ERROR\"\n | \"SIDECAR_NOT_FOUND\"\n | \"SIDECAR_VERSION\"\n | \"SIDECAR_MALFORMED\"\n | \"WRITE_ERROR\";\n\n constructor(code: ClassificationError[\"code\"], message?: string) {\n super(\n message ??\n (code === \"SIDECAR_NOT_FOUND\"\n ? \"Sidecar file not found\"\n : code === \"SIDECAR_VERSION\"\n ? \"Sidecar version mismatch\"\n : code === \"SIDECAR_MALFORMED\"\n ? \"Sidecar file is malformed JSON\"\n : code === \"NETWORK_ERROR\"\n ? \"Network error contacting OpenFIGI\"\n : \"Failed to write sidecar file\"),\n );\n this.name = \"ClassificationError\";\n this.code = code;\n }\n}\n","import { fileURLToPath } from \"node:url\";\nimport * as path from \"node:path\";\nimport * as fs from \"node:fs\";\n\nexport type RatesSnapshot = Record<string, Record<string, number>>;\n\n// Load the bundled snapshot lazily; returns null if the file is absent (e.g. broken install)\nlet _bundled: RatesSnapshot | null | undefined = undefined;\n\nfunction getBundledSnapshot(): RatesSnapshot | null {\n if (_bundled !== undefined) return _bundled;\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n const bundledPath = path.join(__dirname, \"../data/ecb-rates.json\");\n try {\n _bundled = JSON.parse(\n fs.readFileSync(bundledPath, \"utf8\"),\n ) as RatesSnapshot;\n } catch {\n _bundled = null;\n }\n return _bundled;\n}\n\nfunction getUserSnapshot(): RatesSnapshot | null {\n const platform = process.platform;\n let configDir: string;\n if (platform === \"win32\") {\n configDir =\n process.env.APPDATA ??\n path.join(process.env.USERPROFILE ?? \"~\", \"AppData\", \"Roaming\");\n } else {\n configDir =\n process.env.XDG_CONFIG_HOME ??\n path.join(process.env.HOME ?? \"~\", \".config\");\n }\n const userPath = path.join(configDir, \"minus-tracker\", \"ecb-rates.json\");\n try {\n return JSON.parse(fs.readFileSync(userPath, \"utf8\")) as RatesSnapshot;\n } catch {\n return null;\n }\n}\n\nexport function getActiveSnapshot(): RatesSnapshot {\n const bundled = getBundledSnapshot();\n const user = getUserSnapshot();\n if (!bundled && !user) {\n throw new Error(\n \"No ECB rates snapshot available. Run `minus-tracker rates --update` to fetch rates.\",\n );\n }\n if (!bundled) return user!;\n if (!user) return bundled;\n // Merge: user entries override bundled for same date keys\n const merged: RatesSnapshot = {};\n for (const ccy of new Set([...Object.keys(bundled), ...Object.keys(user)])) {\n merged[ccy] = { ...(bundled[ccy] ?? {}), ...(user[ccy] ?? {}) };\n }\n return merged;\n}\n\nexport function isSnapshotStale(\n snapshot: RatesSnapshot,\n today?: string,\n): boolean {\n const todayStr = today ?? new Date().toISOString().slice(0, 10);\n let maxDate = \"0000-01-01\";\n for (const dates of Object.values(snapshot)) {\n for (const d of Object.keys(dates)) {\n if (d > maxDate) maxDate = d;\n }\n }\n if (maxDate === \"0000-01-01\") return true;\n const diffDays =\n (new Date(todayStr).getTime() - new Date(maxDate).getTime()) /\n (1000 * 60 * 60 * 24);\n return diffDays >= 7;\n}\n\nfunction subtractDays(isoDate: string, days: number): string {\n const d = new Date(isoDate + \"T00:00:00Z\");\n d.setUTCDate(d.getUTCDate() - days);\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * Look up ECB rate for a currency on a given date.\n * Returns 1.0 for EUR. Walks back up to 3 calendar days for weekend/holiday gaps.\n * Returns null if not found within the window.\n *\n * @param currency ISO 4217 currency code\n * @param date YYYY-MM-DD trade date\n * @param snapshot Optional override (used in tests to inject stub data)\n */\nexport function lookupRate(\n currency: string,\n date: string,\n snapshot?: RatesSnapshot,\n): number | null {\n if (currency === \"EUR\") return 1.0;\n\n const s = snapshot ?? getActiveSnapshot();\n const currencyRates = s[currency];\n if (!currencyRates) return null; // unsupported currency\n\n for (let i = 0; i <= 3; i++) {\n const d = i === 0 ? date : subtractDays(date, i);\n if (currencyRates[d] !== undefined) {\n return currencyRates[d];\n }\n }\n return null;\n}\n","export type WarningEntry =\n | { code: \"MISSING_ISIN\"; row: number }\n | { code: \"UNSUPPORTED_CURRENCY\"; row: number; currency: string }\n | { code: \"NO_ECB_RATE\"; row: number; currency: string; date: string }\n | { code: \"QUANTITY_ZERO\"; row: number };\n\nexport function warningToEnglish(w: WarningEntry): string {\n switch (w.code) {\n case \"MISSING_ISIN\":\n return `Row ${w.row}: missing ISIN — skipped`;\n case \"UNSUPPORTED_CURRENCY\":\n return `Row ${w.row}: unsupported currency ${w.currency} — skipped`;\n case \"NO_ECB_RATE\":\n return `Row ${w.row}: no ECB rate for ${w.currency} on ${w.date} — skipped`;\n case \"QUANTITY_ZERO\":\n return `Row ${w.row}: quantity is 0 — skipped`;\n }\n}\n","import { Transaction } from \"../types.js\";\nimport { ParseError } from \"../errors.js\";\nimport {\n lookupRate,\n getActiveSnapshot,\n RatesSnapshot,\n} from \"../rates/index.js\";\nimport { WarningEntry, warningToEnglish } from \"./warnings.js\";\n\nconst REQUIRED_COLUMNS = [\n \"ISIN\",\n \"Quantity\",\n \"Price\",\n \"Date\",\n \"Local value\",\n \"Local value currency\",\n \"Transaction costs\",\n \"Transaction costs currency\",\n \"Product\",\n] as const;\n\n/**\n * Parse a single CSV line, respecting RFC-4180-style double-quote escaping.\n * Does not support embedded newlines in fields (not needed for DEGIRO exports).\n */\nfunction parseCSVRow(line: string): string[] {\n const fields: string[] = [];\n let i = 0;\n\n while (i <= line.length) {\n // End of line — stop (handles trailing comma by not emitting extra empty field)\n if (i === line.length) break;\n\n if (line[i] === '\"') {\n // Quoted field\n let field = \"\";\n i++; // skip opening quote\n while (i < line.length) {\n if (line[i] === '\"' && i + 1 < line.length && line[i + 1] === '\"') {\n // Escaped double-quote\n field += '\"';\n i += 2;\n } else if (line[i] === '\"') {\n i++; // skip closing quote\n break;\n } else {\n field += line[i++];\n }\n }\n fields.push(field);\n if (i < line.length && line[i] === \",\") i++; // skip delimiter\n } else {\n // Unquoted field\n const start = i;\n while (i < line.length && line[i] !== \",\") i++;\n fields.push(line.slice(start, i));\n if (i < line.length) i++; // skip delimiter\n }\n }\n\n return fields;\n}\n\n/**\n * Parse a full CSV string (CRLF or LF line endings) into a 2-D array of strings.\n */\nfunction parseCSV(input: string): string[][] {\n const normalized = input.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n return normalized.split(\"\\n\").map(parseCSVRow);\n}\n\n/**\n * Convert a DEGIRO date \"DD-MM-YYYY\" to ISO format \"YYYY-MM-DD\".\n */\nfunction parseDate(ddmmyyyy: string): string {\n const [dd, mm, yyyy] = ddmmyyyy.split(\"-\");\n return `${yyyy}-${mm}-${dd}`;\n}\n\n/**\n * Parses a DEGIRO Transactions CSV export into normalised Transaction objects.\n *\n * Export from Activity → Transactions in the DEGIRO UI (not the Account Statement).\n * Supported currencies: EUR, USD, GBP, CHF. ECB historical rates are used for conversion.\n */\nexport class DEGIROParser {\n private _warningEntries: WarningEntry[] = [];\n private _snapshot?: RatesSnapshot;\n\n constructor(snapshot?: RatesSnapshot) {\n this._snapshot = snapshot;\n }\n\n /**\n * Parse a DEGIRO Transactions CSV export string.\n *\n * @param csv - Raw UTF-8 string from the DEGIRO Transactions export.\n * @returns Array of normalised Transaction objects. Empty if no data rows are found.\n * @throws {ParseError} code `\"INVALID_CSV\"` — malformed CSV or binary content.\n * @throws {ParseError} code `\"MISSING_COLUMN\"` (+ `columnName`) — required column absent.\n *\n * Rows with missing ISIN, unsupported currency, zero quantity, or no ECB rate within\n * 3 trading days are skipped silently. Inspect `parser.warnings` for details.\n */\n parse(csv: string): Transaction[] {\n this._warningEntries = [];\n\n // Binary content (null bytes) is not valid CSV\n if (typeof csv !== \"string\" || csv.includes(\"\\x00\")) {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n let rows: string[][];\n try {\n rows = parseCSV(csv);\n } catch {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n if (rows.length === 0) {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n // Build column-name → index map from the header row\n const header = rows[0];\n const colIndex: Record<string, number> = {};\n for (let i = 0; i < header.length; i++) {\n colIndex[header[i].trim()] = i;\n }\n\n // Validate that every required column is present\n for (const col of REQUIRED_COLUMNS) {\n if (colIndex[col] === undefined) {\n throw new ParseError(\"MISSING_COLUMN\", col);\n }\n }\n\n // Resolve the rates snapshot once for the whole parse run\n const snapshot: RatesSnapshot = this._snapshot ?? getActiveSnapshot();\n\n const transactions: Transaction[] = [];\n\n for (let r = 1; r < rows.length; r++) {\n const row = rows[r];\n // 1-indexed row number: header = 1, first data row = 2\n const rowIndex = r + 1;\n\n // Skip completely empty rows (including the trailing empty line after a\n // final newline, and rows that are all whitespace)\n if (row.every((cell) => cell.trim() === \"\")) continue;\n\n const get = (col: string): string => (row[colIndex[col]] ?? \"\").trim();\n\n // --- ISIN ---\n const isin = get(\"ISIN\");\n if (!isin) {\n this._warningEntries.push({ code: \"MISSING_ISIN\", row: rowIndex });\n continue;\n }\n\n // --- Quantity ---\n const rawQty = parseFloat(get(\"Quantity\"));\n if (isNaN(rawQty) || rawQty === 0) {\n this._warningEntries.push({ code: \"QUANTITY_ZERO\", row: rowIndex });\n continue;\n }\n\n const type: \"BUY\" | \"SELL\" = rawQty > 0 ? \"BUY\" : \"SELL\";\n const quantity = Math.abs(rawQty);\n\n // --- Date ---\n const isoDate = parseDate(get(\"Date\"));\n\n // --- Currency & FX ---\n const currency = get(\"Local value currency\");\n const rawTotalLocal = parseFloat(get(\"Local value\"));\n const totalLocal = isNaN(rawTotalLocal) ? 0 : rawTotalLocal;\n\n let totalEUR: number;\n let fxRate: number | undefined;\n\n if (currency === \"EUR\") {\n totalEUR = Math.abs(totalLocal);\n fxRate = undefined;\n } else {\n const rate = lookupRate(currency, isoDate, snapshot);\n if (rate === null) {\n if (snapshot[currency] === undefined) {\n this._warningEntries.push({\n code: \"UNSUPPORTED_CURRENCY\",\n row: rowIndex,\n currency,\n });\n } else {\n this._warningEntries.push({\n code: \"NO_ECB_RATE\",\n row: rowIndex,\n currency,\n date: isoDate,\n });\n }\n continue;\n }\n totalEUR = Math.abs(totalLocal) / rate;\n fxRate = rate;\n }\n\n // --- Fees ---\n const rawFees = parseFloat(get(\"Transaction costs\"));\n const feesEUR = Math.abs(isNaN(rawFees) ? 0 : rawFees);\n\n // --- Remaining fields ---\n const pricePerUnit = parseFloat(get(\"Price\"));\n const product = get(\"Product\");\n\n transactions.push({\n isin,\n product,\n date: isoDate,\n type,\n quantity,\n pricePerUnit,\n currency,\n totalLocal,\n totalEUR,\n feesEUR,\n fxRate,\n });\n }\n\n return transactions;\n }\n\n get warnings(): string[] {\n return this._warningEntries.map(warningToEnglish);\n }\n\n get warningEntries(): WarningEntry[] {\n return this._warningEntries;\n }\n}\n","import type {\n Transaction,\n MatchedLot,\n GainsReport,\n LotMethod,\n CalculatorOptions,\n AssetClass,\n BucketAReport,\n BucketBReport,\n} from \"../types.js\";\nimport { CalculationError } from \"../errors.js\";\n\ninterface Lot {\n date: string;\n quantity: number;\n pricePerUnitEUR: number;\n feesEUR: number;\n originalQty: number;\n fxRate?: number;\n}\n\nfunction roundHalfUp(x: number): number {\n return (Math.sign(x) * Math.round(Math.abs(x) * 100)) / 100;\n}\n\nfunction inferTaxYear(transactions: Transaction[]): {\n year: number;\n multipleYears: boolean;\n} {\n const counts: Record<string, number> = {};\n for (const t of transactions) {\n const y = t.date.slice(0, 4);\n counts[y] = (counts[y] ?? 0) + 1;\n }\n const years = Object.keys(counts);\n if (years.length === 0)\n return { year: new Date().getFullYear(), multipleYears: false };\n const year = parseInt(\n years.reduce((a, b) => (counts[a] >= counts[b] ? a : b)),\n );\n return { year, multipleYears: years.length > 1 };\n}\n\n/**\n * Computes Italian capital gains and losses from a list of parsed transactions.\n *\n * Uses LIFO or FIFO lot matching and converts all amounts to EUR via ECB rates.\n */\nexport class Calculator {\n private readonly _transactions: Transaction[];\n private readonly _parseWarnings: string[];\n private readonly _options: CalculatorOptions;\n\n constructor(\n transactions: Transaction[],\n parseWarnings?: string[],\n options?: CalculatorOptions,\n ) {\n this._transactions = transactions;\n this._parseWarnings = parseWarnings ?? [];\n this._options = options ?? {};\n }\n\n /**\n * Run LIFO or FIFO lot-matching over the transaction list.\n *\n * @param method - `\"LIFO\"` or `\"FIFO\"`.\n * @returns GainsReport with `plusvalenze`, `minusvalenze`, `netResult`, per-lot breakdown,\n * ECB rates used, and any accumulated warnings.\n * @throws {CalculationError} when a SELL has no matching open buy lots.\n * `error.isin` and `error.date` identify the problematic transaction.\n */\n calculateGains(method: LotMethod): GainsReport {\n const warnings: string[] = [...this._parseWarnings];\n\n // 1. Sort ascending by date; BUY before SELL on same date\n const sorted = [...this._transactions].sort((a, b) => {\n if (a.date < b.date) return -1;\n if (a.date > b.date) return 1;\n // Same date: BUY before SELL\n if (a.type === \"BUY\" && b.type === \"SELL\") return -1;\n if (a.type === \"SELL\" && b.type === \"BUY\") return 1;\n return 0;\n });\n\n // 2. Tax year inference\n const { year: taxYear, multipleYears } = inferTaxYear(sorted);\n if (multipleYears) {\n warnings.push(\n \"CSV contains transactions from multiple years — filter to a single year for accurate reporting.\",\n );\n }\n\n // 3. Lot matching\n const openLots = new Map<string, Lot[]>();\n const matchedLots: MatchedLot[] = [];\n const ratesUsed: Record<string, number> = {};\n\n for (const tx of sorted) {\n // Collect rates used\n if (tx.fxRate !== undefined) {\n ratesUsed[`${tx.currency}:${tx.date}`] = tx.fxRate;\n }\n\n if (tx.type === \"BUY\") {\n const lot: Lot = {\n date: tx.date,\n quantity: tx.quantity,\n pricePerUnitEUR: tx.totalEUR / tx.quantity,\n feesEUR: tx.feesEUR,\n originalQty: tx.quantity,\n fxRate: tx.fxRate,\n };\n if (!openLots.has(tx.isin)) openLots.set(tx.isin, []);\n openLots.get(tx.isin)!.push(lot);\n } else {\n // SELL\n const lots = openLots.get(tx.isin);\n if (!lots || lots.length === 0) {\n throw new CalculationError(tx.isin, tx.date);\n }\n\n const sellPricePerUnitEUR = tx.totalEUR / tx.quantity;\n let remainingSellQty = tx.quantity;\n\n while (remainingSellQty > 0) {\n if (!lots || lots.length === 0) {\n throw new CalculationError(tx.isin, tx.date);\n }\n\n const lot = method === \"LIFO\" ? lots[lots.length - 1] : lots[0];\n const matchedQty = Math.min(lot.quantity, remainingSellQty);\n\n // Fee allocation\n const allocatedBuyFeesEUR =\n lot.feesEUR * (matchedQty / lot.originalQty);\n const allocatedSellFeesEUR = tx.feesEUR * (matchedQty / tx.quantity);\n\n const buyCostEUR =\n lot.pricePerUnitEUR * matchedQty + allocatedBuyFeesEUR;\n const sellProceedsEUR =\n sellPricePerUnitEUR * matchedQty - allocatedSellFeesEUR;\n const gainLossEUR = sellProceedsEUR - buyCostEUR;\n\n matchedLots.push({\n isin: tx.isin,\n product: tx.product,\n quantity: matchedQty,\n buyDate: lot.date,\n sellDate: tx.date,\n buyPriceEUR: lot.pricePerUnitEUR,\n sellPriceEUR: sellPricePerUnitEUR,\n buyCostEUR: roundHalfUp(buyCostEUR),\n sellProceedsEUR: roundHalfUp(sellProceedsEUR),\n gainLossEUR: roundHalfUp(gainLossEUR),\n buyFxRate: lot.fxRate,\n sellFxRate: tx.fxRate,\n });\n\n lot.quantity -= matchedQty;\n remainingSellQty -= matchedQty;\n\n if (lot.quantity <= 0) {\n if (method === \"LIFO\") {\n lots.pop();\n } else {\n lots.shift();\n }\n }\n }\n }\n }\n\n // 4. Aggregate\n let plusvalenze = 0;\n let minusvalenze = 0;\n for (const lot of matchedLots) {\n if (lot.gainLossEUR > 0) plusvalenze += lot.gainLossEUR;\n else minusvalenze += Math.abs(lot.gainLossEUR);\n }\n\n // Two-bucket routing (only when classification map provided)\n if (this._options.classification) {\n const classification = this._options.classification;\n const unclassifiedIsins = new Set<string>();\n\n for (const lot of matchedLots) {\n const entry = classification[lot.isin];\n if (!entry) {\n lot.bucket = \"B\";\n unclassifiedIsins.add(lot.isin);\n } else if (entry.bucketGain === \"A\" && lot.gainLossEUR >= 0) {\n lot.bucket = \"A\";\n } else {\n lot.bucket = \"B\";\n }\n }\n\n for (const isin of unclassifiedIsins) {\n warnings.push(\n `ISIN ${isin} not found in classification map — assigned to Bucket B.`,\n );\n }\n\n // Bucket A computation\n const bucketALots = matchedLots.filter((l) => l.bucket === \"A\");\n const groupsByRate = new Map<\n number,\n { assetClasses: Set<string>; plusvalenze: number }\n >();\n for (const lot of bucketALots) {\n const entry = classification[lot.isin]!;\n const rate = entry.taxRate;\n if (!groupsByRate.has(rate))\n groupsByRate.set(rate, { assetClasses: new Set(), plusvalenze: 0 });\n const g = groupsByRate.get(rate)!;\n g.assetClasses.add(entry.assetClass);\n g.plusvalenze += lot.gainLossEUR;\n }\n const bucketAGroups = [...groupsByRate.entries()].map(([taxRate, g]) => ({\n taxRate,\n assetClasses: [...g.assetClasses] as AssetClass[],\n plusvalenze: roundHalfUp(g.plusvalenze),\n imposta: roundHalfUp(g.plusvalenze * taxRate),\n }));\n const bucketAReport: BucketAReport = {\n groups: bucketAGroups,\n totalImposta: roundHalfUp(\n bucketAGroups.reduce((s, g) => s + g.imposta, 0),\n ),\n };\n\n // Bucket B computation\n const bucketBLots = matchedLots.filter((l) => l.bucket === \"B\");\n let bPlusvalenze = 0;\n let bMinusvalenze = 0;\n for (const lot of bucketBLots) {\n if (lot.gainLossEUR >= 0) bPlusvalenze += lot.gainLossEUR;\n else bMinusvalenze += Math.abs(lot.gainLossEUR);\n }\n bPlusvalenze = roundHalfUp(bPlusvalenze);\n bMinusvalenze = roundHalfUp(bMinusvalenze);\n\n const carryForwards = [...(this._options.carryForward ?? [])].sort(\n (a, b) => a.year - b.year,\n );\n let remaining = bPlusvalenze - bMinusvalenze;\n let carryForwardApplied = 0;\n for (const entry of carryForwards) {\n if (taxYear - entry.year > 4) continue;\n if (remaining <= 0) break;\n const consumed = Math.min(entry.amount, remaining);\n carryForwardApplied += consumed;\n remaining -= consumed;\n }\n carryForwardApplied = roundHalfUp(carryForwardApplied);\n const bNetResult = roundHalfUp(\n bPlusvalenze - bMinusvalenze - carryForwardApplied,\n );\n const carryForwardRemaining = roundHalfUp(Math.max(0, -bNetResult));\n\n const bucketBReport: BucketBReport = {\n plusvalenze: bPlusvalenze,\n minusvalenze: bMinusvalenze,\n carryForwardApplied,\n carryForwardRemaining,\n netResult: bNetResult,\n };\n\n return {\n method,\n taxYear,\n plusvalenze: roundHalfUp(plusvalenze),\n minusvalenze: roundHalfUp(minusvalenze),\n netResult: roundHalfUp(plusvalenze - minusvalenze),\n lots: matchedLots,\n ratesUsed,\n warnings,\n generatedAt: new Date().toISOString(),\n bucketA: bucketAGroups.length > 0 ? bucketAReport : undefined,\n bucketB: bucketBReport,\n };\n } else {\n // Mixed-asset heuristic (no classification map)\n const allIsins = [...new Set(this._transactions.map((t) => t.isin))];\n const hasIePrefix = allIsins.some((isin) => isin.startsWith(\"IE\"));\n const hasOther = allIsins.some((isin) => !isin.startsWith(\"IE\"));\n if (hasIePrefix && hasOther) {\n warnings.push(\n \"AVVISO: CSV contiene tipi di strumenti misti (es. ETF + Azioni).\\n\" +\n \" Il calcolo a bucket unico può non essere fiscalmente corretto.\\n\" +\n \" Esegui: minus-tracker classify trades.csv\",\n );\n }\n }\n\n return {\n method,\n taxYear,\n plusvalenze: roundHalfUp(plusvalenze),\n minusvalenze: roundHalfUp(minusvalenze),\n netResult: roundHalfUp(plusvalenze - minusvalenze),\n lots: matchedLots,\n ratesUsed,\n warnings,\n generatedAt: new Date().toISOString(),\n };\n }\n}\n","import { ClassificationError } from \"../errors.js\";\nimport type {\n AssetClass,\n ClassificationEntry,\n ClassificationMap,\n Transaction,\n} from \"../types.js\";\nimport * as fs from \"node:fs\";\nimport * as https from \"node:https\";\n\ntype HttpPost = (\n url: string,\n body: string,\n timeoutMs: number,\n) => Promise<{ status: number; data: string }>;\n\nfunction httpsPost(\n url: string,\n body: string,\n timeoutMs: number,\n): Promise<{ status: number; data: string }> {\n return new Promise((resolve, reject) => {\n const u = new URL(url);\n const req = https.request(\n {\n hostname: u.hostname,\n path: u.pathname + u.search,\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(body),\n },\n },\n (res) => {\n let data = \"\";\n res.on(\"data\", (chunk) => {\n data += chunk;\n });\n res.on(\"end\", () => resolve({ status: res.statusCode ?? 0, data }));\n },\n );\n req.setTimeout(timeoutMs, () => {\n req.destroy();\n reject(new Error(\"timeout\"));\n });\n req.on(\"error\", reject);\n req.write(body);\n req.end();\n });\n}\n\nconst GOVT_BOND_WHITELIST = new Set([\n \"IT\",\n \"DE\",\n \"FR\",\n \"AT\",\n \"BE\",\n \"NL\",\n \"ES\",\n \"PT\",\n \"FI\",\n \"IE\",\n \"LU\",\n \"GR\",\n \"SK\",\n \"SI\",\n \"LT\",\n \"LV\",\n \"EE\",\n \"MT\",\n \"CY\",\n \"US\",\n \"GB\",\n \"CH\",\n \"NO\",\n \"SE\",\n \"DK\",\n \"JP\",\n \"CA\",\n \"AU\",\n \"NZ\",\n \"SG\",\n \"HK\",\n \"KR\",\n]);\n\ntype TypeMapping = {\n assetClass: AssetClass;\n bucketGain: \"A\" | \"B\";\n bucketLoss: \"A\" | \"B\";\n taxRate: number;\n whiteListed: boolean | null;\n};\n\nconst SECURITY_TYPE_MAP: Record<string, TypeMapping> = {\n ETP: {\n assetClass: \"ETF\",\n bucketGain: \"A\",\n bucketLoss: \"B\",\n taxRate: 0.26,\n whiteListed: null,\n },\n ETF: {\n assetClass: \"ETF\",\n bucketGain: \"A\",\n bucketLoss: \"B\",\n taxRate: 0.26,\n whiteListed: null,\n },\n \"Mutual Fund\": {\n assetClass: \"ETF\",\n bucketGain: \"A\",\n bucketLoss: \"B\",\n taxRate: 0.26,\n whiteListed: null,\n },\n \"Open-End Fund\": {\n assetClass: \"ETF\",\n bucketGain: \"A\",\n bucketLoss: \"B\",\n taxRate: 0.26,\n whiteListed: null,\n },\n \"Common Stock\": {\n assetClass: \"Stock\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n Equity: {\n assetClass: \"Stock\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n \"Exchange Traded Commodity\": {\n assetClass: \"ETC\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n \"Corporate Bond\": {\n assetClass: \"CorpBond\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n Option: {\n assetClass: \"Derivative\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n Future: {\n assetClass: \"Derivative\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n Warrant: {\n assetClass: \"Derivative\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n CFD: {\n assetClass: \"Derivative\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n Swap: {\n assetClass: \"Derivative\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n \"Leverage Certificate\": {\n assetClass: \"LeverageCert\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n \"Turbo Certificate\": {\n assetClass: \"LeverageCert\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n \"Capital Protected Certificate\": {\n assetClass: \"CapProtectedCert\",\n bucketGain: \"A\",\n bucketLoss: \"B\",\n taxRate: 0.26,\n whiteListed: null,\n },\n};\n\nfunction isKnownType(t: string): boolean {\n return t === \"Government Bond\" || t in SECURITY_TYPE_MAP;\n}\n\nfunction classifyByType(\n isin: string,\n securityType: string,\n product: string,\n warnings: string[],\n): ClassificationEntry {\n if (securityType === \"Government Bond\") {\n const prefix = isin.slice(0, 2).toUpperCase();\n const whiteListed = GOVT_BOND_WHITELIST.has(prefix);\n return {\n product,\n assetClass: whiteListed ? \"GovtBondWL\" : \"GovtBondOther\",\n bucketGain: \"A\",\n bucketLoss: \"B\",\n taxRate: whiteListed ? 0.125 : 0.26,\n whiteListed,\n confirmedByUser: false,\n source: \"openfigi\",\n };\n }\n\n const mapped = SECURITY_TYPE_MAP[securityType];\n if (mapped) {\n return {\n product,\n assetClass: mapped.assetClass,\n bucketGain: mapped.bucketGain,\n bucketLoss: mapped.bucketLoss,\n taxRate: mapped.taxRate,\n whiteListed: mapped.whiteListed,\n confirmedByUser: false,\n source: \"openfigi\",\n };\n }\n\n // Unknown type — flag for manual review\n warnings.push(\n `Unrecognized type: ${securityType}. Please classify manually.`,\n );\n return {\n product,\n assetClass: \"Stock\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n confirmedByUser: false,\n source: \"user\",\n };\n}\n\ntype OpenFIGIItem = { securityType?: string; securityType2?: string };\ntype OpenFIGIResult = { data?: OpenFIGIItem[]; error?: string };\n\nexport class Classifier {\n private readonly interactive: boolean;\n\n constructor(options?: { interactive?: boolean }) {\n this.interactive = options?.interactive ?? true;\n }\n\n async load(sidecarPath: string): Promise<ClassificationMap> {\n if (!fs.existsSync(sidecarPath)) {\n throw new ClassificationError(\"SIDECAR_NOT_FOUND\");\n }\n\n let parsed: unknown;\n try {\n const raw = fs.readFileSync(sidecarPath, \"utf-8\");\n parsed = JSON.parse(raw);\n } catch {\n throw new ClassificationError(\"SIDECAR_MALFORMED\");\n }\n\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n (parsed as Record<string, unknown>)[\"version\"] !== 1\n ) {\n throw new ClassificationError(\"SIDECAR_VERSION\");\n }\n\n return (parsed as { classifications: ClassificationMap }).classifications;\n }\n\n async classify(\n transactions: Transaction[],\n sidecarPath: string,\n _httpPost: HttpPost = httpsPost,\n ): Promise<ClassificationMap> {\n // 1. Deduplicate ISINs; track first product name seen per ISIN\n const isinToProduct = new Map<string, string>();\n for (const tx of transactions) {\n if (tx.isin && !isinToProduct.has(tx.isin)) {\n isinToProduct.set(tx.isin, tx.product);\n }\n }\n\n // 2. Load existing sidecar; separate confirmed from to-process\n const confirmed: ClassificationMap = {};\n\n if (fs.existsSync(sidecarPath)) {\n const existingMap = await this.load(sidecarPath);\n for (const [isin, entry] of Object.entries(existingMap)) {\n if (entry.confirmedByUser) {\n confirmed[isin] = entry;\n }\n }\n }\n\n // 3. ISINs that need (re-)processing: not user-confirmed\n const toProcess: string[] = [];\n for (const isin of isinToProduct.keys()) {\n if (!(isin in confirmed)) {\n toProcess.push(isin);\n }\n }\n\n // 4. Batch-query OpenFIGI\n const warnings: string[] = [];\n const newEntries: ClassificationMap = {};\n const BATCH_SIZE = 10;\n\n for (let i = 0; i < toProcess.length; i += BATCH_SIZE) {\n // Wait 6 s between consecutive batch requests\n if (i > 0) {\n await new Promise<void>((resolve) => setTimeout(resolve, 6000));\n }\n\n const batch = toProcess.slice(i, i + BATCH_SIZE);\n const requestBody = JSON.stringify(\n batch.map((isin) => ({ idType: \"ID_ISIN\", idValue: isin })),\n );\n\n let response: { status: number; data: string } | null = null;\n\n // Try once; retry once on 5xx\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n const r = await _httpPost(\n \"https://api.openfigi.com/v3/mapping\",\n requestBody,\n 10_000,\n );\n if (r.status < 500) {\n response = r;\n break;\n }\n // 5xx — loop will retry once\n } catch {\n // Network-level error — no retry\n throw new ClassificationError(\"NETWORK_ERROR\");\n }\n }\n\n if (response === null) {\n throw new ClassificationError(\"NETWORK_ERROR\");\n }\n\n const results = JSON.parse(response.data) as OpenFIGIResult[];\n\n for (let j = 0; j < batch.length; j++) {\n const isin = batch[j]!;\n const result = results[j];\n const product = isinToProduct.get(isin) ?? isin;\n\n if (\n !result ||\n result.error ||\n !result.data ||\n result.data.length === 0\n ) {\n warnings.push(\n `Unrecognized type: unknown. Please classify manually.`,\n );\n newEntries[isin] = {\n product,\n assetClass: \"Stock\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n confirmedByUser: false,\n source: \"user\",\n };\n continue;\n }\n\n const st = result.data[0]?.securityType;\n const st2 = result.data[0]?.securityType2;\n\n // Prefer the type that resolves to a known mapping\n const typeToUse =\n st && isKnownType(st)\n ? st\n : st2 && isKnownType(st2)\n ? st2\n : (st ?? st2 ?? \"Unknown\");\n\n newEntries[isin] = classifyByType(isin, typeToUse, product, warnings);\n }\n }\n\n // 5. Merge: confirmed entries take precedence and remain untouched\n const mergedMap: ClassificationMap = { ...newEntries, ...confirmed };\n\n // 6. Write sidecar\n const sidecarContent = JSON.stringify(\n {\n version: 1,\n generatedAt: new Date().toISOString(),\n classifications: mergedMap,\n warnings,\n },\n null,\n 2,\n );\n\n try {\n fs.writeFileSync(sidecarPath, sidecarContent, \"utf-8\");\n } catch {\n throw new ClassificationError(\"WRITE_ERROR\");\n }\n\n return mergedMap;\n }\n}\n"],"mappings":"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","names":["fs"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/rates/index.ts","../src/parser/warnings.ts","../src/parser/index.ts","../src/dichiarazione/engine.ts","../src/calculator/index.ts","../src/classifier/index.ts"],"sourcesContent":["export class ParseError extends Error {\n readonly code: \"INVALID_CSV\" | \"MISSING_COLUMN\";\n readonly columnName?: string;\n\n constructor(code: \"INVALID_CSV\");\n constructor(code: \"MISSING_COLUMN\", columnName: string);\n constructor(code: ParseError[\"code\"], columnName?: string) {\n const msg =\n code === \"INVALID_CSV\"\n ? \"Invalid CSV: unable to parse\"\n : `Missing required column: ${columnName}`;\n super(msg);\n this.name = \"ParseError\";\n this.code = code;\n this.columnName = columnName;\n }\n}\n\nexport class CalculationError extends Error {\n readonly isin: string;\n readonly date: string;\n\n constructor(isin: string, date: string) {\n super(`No open lots for ISIN ${isin} on ${date}`);\n this.name = \"CalculationError\";\n this.isin = isin;\n this.date = date;\n }\n}\n\nexport class ClassificationError extends Error {\n readonly code:\n | \"NETWORK_ERROR\"\n | \"SIDECAR_NOT_FOUND\"\n | \"SIDECAR_VERSION\"\n | \"SIDECAR_MALFORMED\"\n | \"WRITE_ERROR\";\n\n constructor(code: ClassificationError[\"code\"], message?: string) {\n super(\n message ??\n (code === \"SIDECAR_NOT_FOUND\"\n ? \"Sidecar file not found\"\n : code === \"SIDECAR_VERSION\"\n ? \"Sidecar version mismatch\"\n : code === \"SIDECAR_MALFORMED\"\n ? \"Sidecar file is malformed JSON\"\n : code === \"NETWORK_ERROR\"\n ? \"Network error contacting OpenFIGI\"\n : \"Failed to write sidecar file\"),\n );\n this.name = \"ClassificationError\";\n this.code = code;\n }\n}\n","import { fileURLToPath } from \"node:url\";\nimport * as path from \"node:path\";\nimport * as fs from \"node:fs\";\n\nexport type RatesSnapshot = Record<string, Record<string, number>>;\n\n// Load the bundled snapshot lazily; returns null if the file is absent (e.g. broken install)\nlet _bundled: RatesSnapshot | null | undefined = undefined;\n\nfunction getBundledSnapshot(): RatesSnapshot | null {\n if (_bundled !== undefined) return _bundled;\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n const bundledPath = path.join(__dirname, \"../data/ecb-rates.json\");\n try {\n _bundled = JSON.parse(\n fs.readFileSync(bundledPath, \"utf8\"),\n ) as RatesSnapshot;\n } catch {\n _bundled = null;\n }\n return _bundled;\n}\n\nfunction getUserSnapshot(): RatesSnapshot | null {\n const platform = process.platform;\n let configDir: string;\n if (platform === \"win32\") {\n configDir =\n process.env.APPDATA ??\n path.join(process.env.USERPROFILE ?? \"~\", \"AppData\", \"Roaming\");\n } else {\n configDir =\n process.env.XDG_CONFIG_HOME ??\n path.join(process.env.HOME ?? \"~\", \".config\");\n }\n const userPath = path.join(configDir, \"minus-tracker\", \"ecb-rates.json\");\n try {\n return JSON.parse(fs.readFileSync(userPath, \"utf8\")) as RatesSnapshot;\n } catch {\n return null;\n }\n}\n\nexport function getActiveSnapshot(): RatesSnapshot {\n const bundled = getBundledSnapshot();\n const user = getUserSnapshot();\n if (!bundled && !user) {\n throw new Error(\n \"No ECB rates snapshot available. Run `minus-tracker rates --update` to fetch rates.\",\n );\n }\n if (!bundled) return user!;\n if (!user) return bundled;\n // Merge: user entries override bundled for same date keys\n const merged: RatesSnapshot = {};\n for (const ccy of new Set([...Object.keys(bundled), ...Object.keys(user)])) {\n merged[ccy] = { ...(bundled[ccy] ?? {}), ...(user[ccy] ?? {}) };\n }\n return merged;\n}\n\nexport function isSnapshotStale(\n snapshot: RatesSnapshot,\n today?: string,\n): boolean {\n const todayStr = today ?? new Date().toISOString().slice(0, 10);\n let maxDate = \"0000-01-01\";\n for (const dates of Object.values(snapshot)) {\n for (const d of Object.keys(dates)) {\n if (d > maxDate) maxDate = d;\n }\n }\n if (maxDate === \"0000-01-01\") return true;\n const diffDays =\n (new Date(todayStr).getTime() - new Date(maxDate).getTime()) /\n (1000 * 60 * 60 * 24);\n return diffDays >= 7;\n}\n\nfunction subtractDays(isoDate: string, days: number): string {\n const d = new Date(isoDate + \"T00:00:00Z\");\n d.setUTCDate(d.getUTCDate() - days);\n return d.toISOString().slice(0, 10);\n}\n\n/**\n * Look up ECB rate for a currency on a given date.\n * Returns 1.0 for EUR. Walks back up to 3 calendar days for weekend/holiday gaps.\n * Returns null if not found within the window.\n *\n * @param currency ISO 4217 currency code\n * @param date YYYY-MM-DD trade date\n * @param snapshot Optional override (used in tests to inject stub data)\n */\nexport function lookupRate(\n currency: string,\n date: string,\n snapshot?: RatesSnapshot,\n): number | null {\n if (currency === \"EUR\") return 1.0;\n\n const s = snapshot ?? getActiveSnapshot();\n const currencyRates = s[currency];\n if (!currencyRates) return null; // unsupported currency\n\n for (let i = 0; i <= 3; i++) {\n const d = i === 0 ? date : subtractDays(date, i);\n if (currencyRates[d] !== undefined) {\n return currencyRates[d];\n }\n }\n return null;\n}\n","export type WarningEntry =\n | { code: \"MISSING_ISIN\"; row: number }\n | { code: \"UNSUPPORTED_CURRENCY\"; row: number; currency: string }\n | { code: \"NO_ECB_RATE\"; row: number; currency: string; date: string }\n | { code: \"QUANTITY_ZERO\"; row: number }\n | { code: \"MISSING_ISIN_INCOME\"; row: number }\n | { code: \"ORPHAN_WITHHOLDING\"; isin: string; date: string };\n\nexport function warningToEnglish(w: WarningEntry): string {\n switch (w.code) {\n case \"MISSING_ISIN\":\n return `Row ${w.row}: missing ISIN — skipped`;\n case \"UNSUPPORTED_CURRENCY\":\n return `Row ${w.row}: unsupported currency ${w.currency} — skipped`;\n case \"NO_ECB_RATE\":\n return `Row ${w.row}: no ECB rate for ${w.currency} on ${w.date} — skipped`;\n case \"QUANTITY_ZERO\":\n return `Row ${w.row}: quantity is 0 — skipped`;\n case \"MISSING_ISIN_INCOME\":\n return `Row ${w.row}: blank ISIN on income row — skipped`;\n case \"ORPHAN_WITHHOLDING\":\n return `Withholding row for ${w.isin} on ${w.date}: no matching income row — skipped`;\n }\n}\n","import { Transaction, IncomeRow } from \"../types.js\";\nimport { ParseError } from \"../errors.js\";\nimport {\n lookupRate,\n getActiveSnapshot,\n RatesSnapshot,\n} from \"../rates/index.js\";\nimport { WarningEntry, warningToEnglish } from \"./warnings.js\";\n\nconst REQUIRED_COLUMNS = [\n \"ISIN\",\n \"Quantity\",\n \"Price\",\n \"Date\",\n \"Local value\",\n \"Local value currency\",\n \"Transaction costs\",\n \"Transaction costs currency\",\n \"Product\",\n] as const;\n\n// Income-row product keyword matching (case-insensitive substring match).\n// Tax keywords are checked first because \"DIVIDEND TAX\" also contains \"DIVIDEND\".\nconst INCOME_KEYWORDS = [\"DIVIDEND\", \"COUPON\", \"INTEREST\", \"CEDOLA\"] as const;\nconst TAX_KEYWORDS = [\"DIVIDEND TAX\", \"WITHHOLDING\", \"RITENUTA\"] as const;\n\ninterface IncomeCandidate {\n isin: string;\n product: string;\n date: string;\n incomeType: \"dividend\" | \"coupon\";\n localValue: number;\n currency: string;\n row: number;\n}\n\ninterface WithholdingCandidate {\n isin: string;\n date: string;\n amount: number; // absolute value, in source currency\n row: number;\n}\n\n/**\n * Parse a single CSV line, respecting RFC-4180-style double-quote escaping.\n * Does not support embedded newlines in fields (not needed for DEGIRO exports).\n */\nfunction parseCSVRow(line: string): string[] {\n const fields: string[] = [];\n let i = 0;\n\n while (i <= line.length) {\n // End of line — stop (handles trailing comma by not emitting extra empty field)\n if (i === line.length) break;\n\n if (line[i] === '\"') {\n // Quoted field\n let field = \"\";\n i++; // skip opening quote\n while (i < line.length) {\n if (line[i] === '\"' && i + 1 < line.length && line[i + 1] === '\"') {\n // Escaped double-quote\n field += '\"';\n i += 2;\n } else if (line[i] === '\"') {\n i++; // skip closing quote\n break;\n } else {\n field += line[i++];\n }\n }\n fields.push(field);\n if (i < line.length && line[i] === \",\") i++; // skip delimiter\n } else {\n // Unquoted field\n const start = i;\n while (i < line.length && line[i] !== \",\") i++;\n fields.push(line.slice(start, i));\n if (i < line.length) i++; // skip delimiter\n }\n }\n\n return fields;\n}\n\n/**\n * Parse a full CSV string (CRLF or LF line endings) into a 2-D array of strings.\n */\nfunction parseCSV(input: string): string[][] {\n const normalized = input.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n return normalized.split(\"\\n\").map(parseCSVRow);\n}\n\n/**\n * Convert a DEGIRO date \"DD-MM-YYYY\" to ISO format \"YYYY-MM-DD\".\n */\nfunction parseDate(ddmmyyyy: string): string {\n const [dd, mm, yyyy] = ddmmyyyy.split(\"-\");\n return `${yyyy}-${mm}-${dd}`;\n}\n\n/**\n * Parses a DEGIRO Transactions CSV export into normalised Transaction objects.\n *\n * Export from Activity → Transactions in the DEGIRO UI (not the Account Statement).\n * Supported currencies: EUR, USD, GBP, CHF. ECB historical rates are used for conversion.\n */\nexport class DEGIROParser {\n private _warningEntries: WarningEntry[] = [];\n private _snapshot?: RatesSnapshot;\n private _incomeRows: IncomeRow[] = [];\n\n constructor(snapshot?: RatesSnapshot) {\n this._snapshot = snapshot;\n }\n\n /**\n * Parse a DEGIRO Transactions CSV export string.\n *\n * @param csv - Raw UTF-8 string from the DEGIRO Transactions export.\n * @returns Array of normalised Transaction objects. Empty if no data rows are found.\n * @throws {ParseError} code `\"INVALID_CSV\"` — malformed CSV or binary content.\n * @throws {ParseError} code `\"MISSING_COLUMN\"` (+ `columnName`) — required column absent.\n *\n * Rows with missing ISIN, unsupported currency, zero quantity, or no ECB rate within\n * 3 trading days are skipped silently. Inspect `parser.warnings` for details.\n */\n parse(csv: string): Transaction[] {\n this._warningEntries = [];\n this._incomeRows = [];\n\n // Binary content (null bytes) is not valid CSV\n if (typeof csv !== \"string\" || csv.includes(\"\\x00\")) {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n let rows: string[][];\n try {\n rows = parseCSV(csv);\n } catch {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n if (rows.length === 0) {\n throw new ParseError(\"INVALID_CSV\");\n }\n\n // Build column-name → index map from the header row\n const header = rows[0];\n const colIndex: Record<string, number> = {};\n for (let i = 0; i < header.length; i++) {\n colIndex[header[i].trim()] = i;\n }\n\n // Validate that every required column is present\n for (const col of REQUIRED_COLUMNS) {\n if (colIndex[col] === undefined) {\n throw new ParseError(\"MISSING_COLUMN\", col);\n }\n }\n\n // Resolve the rates snapshot once for the whole parse run\n const snapshot: RatesSnapshot = this._snapshot ?? getActiveSnapshot();\n\n const transactions: Transaction[] = [];\n const incomeCandidates: IncomeCandidate[] = [];\n const withholdingCandidates: WithholdingCandidate[] = [];\n\n for (let r = 1; r < rows.length; r++) {\n const row = rows[r];\n // 1-indexed row number: header = 1, first data row = 2\n const rowIndex = r + 1;\n\n // Skip completely empty rows (including the trailing empty line after a\n // final newline, and rows that are all whitespace)\n if (row.every((cell) => cell.trim() === \"\")) continue;\n\n const get = (col: string): string => (row[colIndex[col]] ?? \"\").trim();\n\n const isin = get(\"ISIN\");\n const product = get(\"Product\");\n const qtyStr = get(\"Quantity\");\n const rawQty = parseFloat(qtyStr);\n const qtyIsZeroOrBlank = qtyStr === \"\" || isNaN(rawQty) || rawQty === 0;\n\n // --- Income-row detection (Quantity == 0 or blank) ---\n if (qtyIsZeroOrBlank) {\n const upperProduct = product.toUpperCase();\n const isTaxKeyword = TAX_KEYWORDS.some((k) => upperProduct.includes(k));\n const isIncomeKeyword =\n !isTaxKeyword &&\n INCOME_KEYWORDS.some((k) => upperProduct.includes(k));\n\n if (isTaxKeyword || isIncomeKeyword) {\n const rawLocalValue = parseFloat(get(\"Local value\"));\n const localValue = isNaN(rawLocalValue) ? 0 : rawLocalValue;\n const isoDate = parseDate(get(\"Date\"));\n\n if (isTaxKeyword && localValue < 0) {\n if (!isin) {\n this._warningEntries.push({\n code: \"MISSING_ISIN_INCOME\",\n row: rowIndex,\n });\n continue;\n }\n withholdingCandidates.push({\n isin,\n date: isoDate,\n amount: Math.abs(localValue),\n row: rowIndex,\n });\n continue;\n }\n\n if (isIncomeKeyword && localValue > 0) {\n if (!isin) {\n this._warningEntries.push({\n code: \"MISSING_ISIN_INCOME\",\n row: rowIndex,\n });\n continue;\n }\n const incomeType: \"dividend\" | \"coupon\" = upperProduct.includes(\n \"DIVIDEND\",\n )\n ? \"dividend\"\n : \"coupon\";\n incomeCandidates.push({\n isin,\n product,\n date: isoDate,\n incomeType,\n localValue,\n currency: get(\"Local value currency\"),\n row: rowIndex,\n });\n continue;\n }\n // Keyword matched but sign doesn't match the expected direction —\n // fall through to standard transaction handling below.\n }\n }\n\n // --- ISIN ---\n if (!isin) {\n this._warningEntries.push({ code: \"MISSING_ISIN\", row: rowIndex });\n continue;\n }\n\n // --- Quantity ---\n if (isNaN(rawQty) || rawQty === 0) {\n this._warningEntries.push({ code: \"QUANTITY_ZERO\", row: rowIndex });\n continue;\n }\n\n const type: \"BUY\" | \"SELL\" = rawQty > 0 ? \"BUY\" : \"SELL\";\n const quantity = Math.abs(rawQty);\n\n // --- Date ---\n const isoDate = parseDate(get(\"Date\"));\n\n // --- Currency & FX ---\n const currency = get(\"Local value currency\");\n const rawTotalLocal = parseFloat(get(\"Local value\"));\n const totalLocal = isNaN(rawTotalLocal) ? 0 : rawTotalLocal;\n\n let totalEUR: number;\n let fxRate: number | undefined;\n\n if (currency === \"EUR\") {\n totalEUR = Math.abs(totalLocal);\n fxRate = undefined;\n } else {\n const rate = lookupRate(currency, isoDate, snapshot);\n if (rate === null) {\n if (snapshot[currency] === undefined) {\n this._warningEntries.push({\n code: \"UNSUPPORTED_CURRENCY\",\n row: rowIndex,\n currency,\n });\n } else {\n this._warningEntries.push({\n code: \"NO_ECB_RATE\",\n row: rowIndex,\n currency,\n date: isoDate,\n });\n }\n continue;\n }\n totalEUR = Math.abs(totalLocal) / rate;\n fxRate = rate;\n }\n\n // --- Fees ---\n const rawFees = parseFloat(get(\"Transaction costs\"));\n const feesEUR = Math.abs(isNaN(rawFees) ? 0 : rawFees);\n\n // --- Remaining fields ---\n const pricePerUnit = parseFloat(get(\"Price\"));\n\n transactions.push({\n isin,\n product,\n date: isoDate,\n type,\n quantity,\n pricePerUnit,\n currency,\n totalLocal,\n totalEUR,\n feesEUR,\n fxRate,\n });\n }\n\n // --- Pair withholding candidates with income candidates by (ISIN, date) ---\n // Order-independent: candidates were collected regardless of CSV row order.\n const withholdingByKey = new Map<\n string,\n { isin: string; date: string; amount: number }\n >();\n for (const w of withholdingCandidates) {\n const key = `${w.isin}|${w.date}`;\n const existing = withholdingByKey.get(key);\n withholdingByKey.set(key, {\n isin: w.isin,\n date: w.date,\n amount: (existing?.amount ?? 0) + w.amount,\n });\n }\n const consumedKeys = new Set<string>();\n\n const incomeRows: IncomeRow[] = [];\n for (const c of incomeCandidates) {\n const key = `${c.isin}|${c.date}`;\n const matchedWithholding = withholdingByKey.get(key);\n\n let grossAmount: number;\n let withholdingTax: number;\n let fxRate: number | undefined;\n\n if (c.currency === \"EUR\") {\n grossAmount = Math.abs(c.localValue);\n withholdingTax = matchedWithholding?.amount ?? 0;\n fxRate = undefined;\n } else {\n const rate = lookupRate(c.currency, c.date, snapshot);\n if (rate === null) {\n if (snapshot[c.currency] === undefined) {\n this._warningEntries.push({\n code: \"UNSUPPORTED_CURRENCY\",\n row: c.row,\n currency: c.currency,\n });\n } else {\n this._warningEntries.push({\n code: \"NO_ECB_RATE\",\n row: c.row,\n currency: c.currency,\n date: c.date,\n });\n }\n continue;\n }\n grossAmount = Math.abs(c.localValue) / rate;\n withholdingTax = matchedWithholding\n ? matchedWithholding.amount / rate\n : 0;\n fxRate = rate;\n }\n\n if (matchedWithholding) consumedKeys.add(key);\n\n incomeRows.push({\n isin: c.isin,\n product: c.product,\n date: c.date,\n incomeType: c.incomeType,\n grossAmount,\n withholdingTax,\n currency: c.currency,\n fxRate,\n });\n }\n\n // Orphan withholding candidates — no matching income row for (ISIN, date)\n for (const [key, w] of withholdingByKey) {\n if (!consumedKeys.has(key)) {\n this._warningEntries.push({\n code: \"ORPHAN_WITHHOLDING\",\n isin: w.isin,\n date: w.date,\n });\n }\n }\n\n this._incomeRows = incomeRows;\n\n return transactions;\n }\n\n get warnings(): string[] {\n return this._warningEntries.map(warningToEnglish);\n }\n\n get warningEntries(): WarningEntry[] {\n return this._warningEntries;\n }\n\n get incomeRows(): IncomeRow[] {\n return this._incomeRows;\n }\n}\n","import { writeFile } from \"node:fs/promises\";\nimport type {\n BucketAReport,\n BucketBReport,\n CarryForward,\n CarryForwardEntry,\n DichiarazioneReport,\n IncomeRow,\n QuadroRMReport,\n QuadroRTReport,\n} from \"../types.js\";\n\nfunction roundHalfUp(x: number): number {\n return (Math.sign(x) * Math.round(Math.abs(x) * 100)) / 100;\n}\n\nexport function buildQuadroRT(\n bucketB: BucketBReport,\n carryForward: CarryForward[],\n taxYear: number,\n): QuadroRTReport {\n const plusvalenze = bucketB.plusvalenze;\n const minusvalenze = bucketB.minusvalenze;\n const differenza = roundHalfUp(plusvalenze - minusvalenze);\n\n if (differenza > 0) {\n const sorted = [...carryForward].sort((a, b) => a.year - b.year);\n let remaining = differenza;\n const carryForwardApplied: CarryForwardEntry[] = [];\n\n for (const entry of sorted) {\n if (taxYear - entry.year > 4) continue;\n const consumed = Math.min(entry.amount, remaining);\n if (consumed > 0) {\n carryForwardApplied.push({\n annoOrigine: entry.year,\n importo: roundHalfUp(consumed),\n });\n remaining -= consumed;\n }\n }\n\n const imponibileNetto = roundHalfUp(remaining);\n const imposta = roundHalfUp(imponibileNetto * 0.26);\n\n return {\n plusvalenze,\n minusvalenze,\n differenza,\n carryForwardApplied,\n imponibileNetto,\n imposta,\n carryForwardRiportato: [],\n };\n } else if (differenza < 0) {\n return {\n plusvalenze,\n minusvalenze,\n differenza,\n carryForwardApplied: [],\n imponibileNetto: 0,\n imposta: 0,\n carryForwardRiportato: [\n { annoOrigine: taxYear, importo: roundHalfUp(Math.abs(differenza)) },\n ],\n };\n } else {\n return {\n plusvalenze,\n minusvalenze,\n differenza: 0,\n carryForwardApplied: [],\n imponibileNetto: 0,\n imposta: 0,\n carryForwardRiportato: [],\n };\n }\n}\n\nexport function buildQuadroRM(\n bucketA: BucketAReport | undefined,\n incomeRows: IncomeRow[],\n taxYear: number,\n): QuadroRMReport {\n const groups = bucketA?.groups ?? [];\n\n const group26 = groups.find((g) => g.taxRate === 0.26);\n const group125 = groups.find((g) => g.taxRate === 0.125);\n\n const capitaleAliquota26 = {\n plusvalenze: group26?.plusvalenze ?? 0,\n imposta: group26?.imposta ?? 0,\n };\n const capitaleAliquota125 = {\n plusvalenze: group125?.plusvalenze ?? 0,\n imposta: group125?.imposta ?? 0,\n };\n\n const dividendiEsteri = incomeRows\n .filter((r) => r.incomeType === \"dividend\")\n .map((r) => ({\n isin: r.isin,\n prodotto: r.product,\n lordo: r.grossAmount,\n rittenutaEstera: r.withholdingTax,\n }));\n\n const cedole = incomeRows\n .filter((r) => r.incomeType === \"coupon\")\n .map((r) => ({\n isin: r.isin,\n prodotto: r.product,\n importo: r.grossAmount,\n rittenutaEstera: r.withholdingTax,\n }));\n\n return {\n capitaleAliquota26,\n capitaleAliquota125,\n dividendiEsteri,\n cedole,\n };\n}\n\nexport function buildDichiarazioneReport(\n quadroRT: QuadroRTReport,\n quadroRM: QuadroRMReport,\n taxYear: number,\n): DichiarazioneReport {\n const version = 1;\n const modello = \"Redditi PF\" as const;\n const generatedAt = new Date().toISOString();\n\n return {\n version,\n annoImposta: taxYear,\n modello,\n generatedAt,\n quadroRT,\n quadroRM,\n exportTo: async (path: string) => {\n await writeFile(\n path,\n JSON.stringify(\n {\n version,\n annoImposta: taxYear,\n modello,\n generatedAt,\n quadroRT,\n quadroRM,\n },\n null,\n 2,\n ),\n );\n },\n };\n}\n","import type {\n Transaction,\n MatchedLot,\n GainsReport,\n LotMethod,\n CalculatorOptions,\n AssetClass,\n BucketAReport,\n BucketBReport,\n} from \"../types.js\";\nimport { CalculationError } from \"../errors.js\";\nimport {\n buildQuadroRT,\n buildQuadroRM,\n buildDichiarazioneReport,\n} from \"../dichiarazione/engine.js\";\n\ninterface Lot {\n date: string;\n quantity: number;\n pricePerUnitEUR: number;\n feesEUR: number;\n originalQty: number;\n fxRate?: number;\n}\n\nfunction roundHalfUp(x: number): number {\n return (Math.sign(x) * Math.round(Math.abs(x) * 100)) / 100;\n}\n\nfunction inferTaxYear(transactions: Transaction[]): {\n year: number;\n multipleYears: boolean;\n} {\n const counts: Record<string, number> = {};\n for (const t of transactions) {\n const y = t.date.slice(0, 4);\n counts[y] = (counts[y] ?? 0) + 1;\n }\n const years = Object.keys(counts);\n if (years.length === 0)\n return { year: new Date().getFullYear(), multipleYears: false };\n const year = parseInt(\n years.reduce((a, b) => (counts[a] >= counts[b] ? a : b)),\n );\n return { year, multipleYears: years.length > 1 };\n}\n\n/**\n * Computes Italian capital gains and losses from a list of parsed transactions.\n *\n * Uses LIFO or FIFO lot matching and converts all amounts to EUR via ECB rates.\n */\nexport class Calculator {\n private readonly _transactions: Transaction[];\n private readonly _parseWarnings: string[];\n private readonly _options: CalculatorOptions;\n\n constructor(\n transactions: Transaction[],\n parseWarnings?: string[],\n options?: CalculatorOptions,\n ) {\n this._transactions = transactions;\n this._parseWarnings = parseWarnings ?? [];\n this._options = options ?? {};\n }\n\n /**\n * Run LIFO or FIFO lot-matching over the transaction list.\n *\n * @param method - `\"LIFO\"` or `\"FIFO\"`.\n * @returns GainsReport with `plusvalenze`, `minusvalenze`, `netResult`, per-lot breakdown,\n * ECB rates used, and any accumulated warnings.\n * @throws {CalculationError} when a SELL has no matching open buy lots.\n * `error.isin` and `error.date` identify the problematic transaction.\n */\n calculateGains(method: LotMethod): GainsReport {\n const warnings: string[] = [...this._parseWarnings];\n\n // 1. Sort ascending by date; BUY before SELL on same date\n const sorted = [...this._transactions].sort((a, b) => {\n if (a.date < b.date) return -1;\n if (a.date > b.date) return 1;\n // Same date: BUY before SELL\n if (a.type === \"BUY\" && b.type === \"SELL\") return -1;\n if (a.type === \"SELL\" && b.type === \"BUY\") return 1;\n return 0;\n });\n\n // 2. Tax year inference\n const { year: taxYear, multipleYears } = inferTaxYear(sorted);\n if (multipleYears) {\n warnings.push(\n \"CSV contains transactions from multiple years — filter to a single year for accurate reporting.\",\n );\n }\n\n // 3. Lot matching\n const openLots = new Map<string, Lot[]>();\n const matchedLots: MatchedLot[] = [];\n const ratesUsed: Record<string, number> = {};\n\n for (const tx of sorted) {\n // Collect rates used\n if (tx.fxRate !== undefined) {\n ratesUsed[`${tx.currency}:${tx.date}`] = tx.fxRate;\n }\n\n if (tx.type === \"BUY\") {\n const lot: Lot = {\n date: tx.date,\n quantity: tx.quantity,\n pricePerUnitEUR: tx.totalEUR / tx.quantity,\n feesEUR: tx.feesEUR,\n originalQty: tx.quantity,\n fxRate: tx.fxRate,\n };\n if (!openLots.has(tx.isin)) openLots.set(tx.isin, []);\n openLots.get(tx.isin)!.push(lot);\n } else {\n // SELL\n const lots = openLots.get(tx.isin);\n if (!lots || lots.length === 0) {\n throw new CalculationError(tx.isin, tx.date);\n }\n\n const sellPricePerUnitEUR = tx.totalEUR / tx.quantity;\n let remainingSellQty = tx.quantity;\n\n while (remainingSellQty > 0) {\n if (!lots || lots.length === 0) {\n throw new CalculationError(tx.isin, tx.date);\n }\n\n const lot = method === \"LIFO\" ? lots[lots.length - 1] : lots[0];\n const matchedQty = Math.min(lot.quantity, remainingSellQty);\n\n // Fee allocation\n const allocatedBuyFeesEUR =\n lot.feesEUR * (matchedQty / lot.originalQty);\n const allocatedSellFeesEUR = tx.feesEUR * (matchedQty / tx.quantity);\n\n const buyCostEUR =\n lot.pricePerUnitEUR * matchedQty + allocatedBuyFeesEUR;\n const sellProceedsEUR =\n sellPricePerUnitEUR * matchedQty - allocatedSellFeesEUR;\n const gainLossEUR = sellProceedsEUR - buyCostEUR;\n\n matchedLots.push({\n isin: tx.isin,\n product: tx.product,\n quantity: matchedQty,\n buyDate: lot.date,\n sellDate: tx.date,\n buyPriceEUR: lot.pricePerUnitEUR,\n sellPriceEUR: sellPricePerUnitEUR,\n buyCostEUR: roundHalfUp(buyCostEUR),\n sellProceedsEUR: roundHalfUp(sellProceedsEUR),\n gainLossEUR: roundHalfUp(gainLossEUR),\n buyFxRate: lot.fxRate,\n sellFxRate: tx.fxRate,\n });\n\n lot.quantity -= matchedQty;\n remainingSellQty -= matchedQty;\n\n if (lot.quantity <= 0) {\n if (method === \"LIFO\") {\n lots.pop();\n } else {\n lots.shift();\n }\n }\n }\n }\n }\n\n // 4. Aggregate\n let plusvalenze = 0;\n let minusvalenze = 0;\n for (const lot of matchedLots) {\n if (lot.gainLossEUR > 0) plusvalenze += lot.gainLossEUR;\n else minusvalenze += Math.abs(lot.gainLossEUR);\n }\n\n // Two-bucket routing (only when classification map provided)\n if (this._options.classification) {\n const classification = this._options.classification;\n const unclassifiedIsins = new Set<string>();\n\n for (const lot of matchedLots) {\n const entry = classification[lot.isin];\n if (!entry) {\n lot.bucket = \"B\";\n unclassifiedIsins.add(lot.isin);\n } else if (entry.bucketGain === \"A\" && lot.gainLossEUR >= 0) {\n lot.bucket = \"A\";\n } else {\n lot.bucket = \"B\";\n }\n }\n\n for (const isin of unclassifiedIsins) {\n warnings.push(\n `ISIN ${isin} not found in classification map — assigned to Bucket B.`,\n );\n }\n\n // Bucket A computation\n const bucketALots = matchedLots.filter((l) => l.bucket === \"A\");\n const groupsByRate = new Map<\n number,\n { assetClasses: Set<string>; plusvalenze: number }\n >();\n for (const lot of bucketALots) {\n const entry = classification[lot.isin]!;\n const rate = entry.taxRate;\n if (!groupsByRate.has(rate))\n groupsByRate.set(rate, { assetClasses: new Set(), plusvalenze: 0 });\n const g = groupsByRate.get(rate)!;\n g.assetClasses.add(entry.assetClass);\n g.plusvalenze += lot.gainLossEUR;\n }\n const bucketAGroups = [...groupsByRate.entries()].map(([taxRate, g]) => ({\n taxRate,\n assetClasses: [...g.assetClasses] as AssetClass[],\n plusvalenze: roundHalfUp(g.plusvalenze),\n imposta: roundHalfUp(g.plusvalenze * taxRate),\n }));\n const bucketAReport: BucketAReport = {\n groups: bucketAGroups,\n totalImposta: roundHalfUp(\n bucketAGroups.reduce((s, g) => s + g.imposta, 0),\n ),\n };\n\n // Bucket B computation\n const bucketBLots = matchedLots.filter((l) => l.bucket === \"B\");\n let bPlusvalenze = 0;\n let bMinusvalenze = 0;\n for (const lot of bucketBLots) {\n if (lot.gainLossEUR >= 0) bPlusvalenze += lot.gainLossEUR;\n else bMinusvalenze += Math.abs(lot.gainLossEUR);\n }\n bPlusvalenze = roundHalfUp(bPlusvalenze);\n bMinusvalenze = roundHalfUp(bMinusvalenze);\n\n const carryForwards = [...(this._options.carryForward ?? [])].sort(\n (a, b) => a.year - b.year,\n );\n let remaining = bPlusvalenze - bMinusvalenze;\n let carryForwardApplied = 0;\n for (const entry of carryForwards) {\n if (taxYear - entry.year > 4) continue;\n if (remaining <= 0) break;\n const consumed = Math.min(entry.amount, remaining);\n carryForwardApplied += consumed;\n remaining -= consumed;\n }\n carryForwardApplied = roundHalfUp(carryForwardApplied);\n const bNetResult = roundHalfUp(\n bPlusvalenze - bMinusvalenze - carryForwardApplied,\n );\n const carryForwardRemaining = roundHalfUp(Math.max(0, -bNetResult));\n\n const bucketBReport: BucketBReport = {\n plusvalenze: bPlusvalenze,\n minusvalenze: bMinusvalenze,\n carryForwardApplied,\n carryForwardRemaining,\n netResult: bNetResult,\n };\n\n // Filter income rows to the tax year and build the dichiarazione report\n const allIncomeRows = this._options.incomeRows ?? [];\n const filteredIncomeRows = allIncomeRows.filter(\n (row) => new Date(row.date).getFullYear() === taxYear,\n );\n if (filteredIncomeRows.length < allIncomeRows.length) {\n warnings.push(`Income rows outside tax year ${taxYear} were skipped.`);\n }\n\n const quadroRT = buildQuadroRT(\n bucketBReport,\n this._options.carryForward ?? [],\n taxYear,\n );\n const quadroRM = buildQuadroRM(\n bucketAGroups.length > 0 ? bucketAReport : undefined,\n filteredIncomeRows,\n taxYear,\n );\n const dichiarazioneReport = buildDichiarazioneReport(\n quadroRT,\n quadroRM,\n taxYear,\n );\n\n return {\n method,\n taxYear,\n plusvalenze: roundHalfUp(plusvalenze),\n minusvalenze: roundHalfUp(minusvalenze),\n netResult: roundHalfUp(plusvalenze - minusvalenze),\n lots: matchedLots,\n ratesUsed,\n warnings,\n generatedAt: new Date().toISOString(),\n bucketA: bucketAGroups.length > 0 ? bucketAReport : undefined,\n bucketB: bucketBReport,\n dichiarazione: dichiarazioneReport,\n };\n } else {\n // Mixed-asset heuristic (no classification map)\n const allIsins = [...new Set(this._transactions.map((t) => t.isin))];\n const hasIePrefix = allIsins.some((isin) => isin.startsWith(\"IE\"));\n const hasOther = allIsins.some((isin) => !isin.startsWith(\"IE\"));\n if (hasIePrefix && hasOther) {\n warnings.push(\n \"AVVISO: CSV contiene tipi di strumenti misti (es. ETF + Azioni).\\n\" +\n \" Il calcolo a bucket unico può non essere fiscalmente corretto.\\n\" +\n \" Esegui: minus-tracker classify trades.csv\",\n );\n }\n }\n\n return {\n method,\n taxYear,\n plusvalenze: roundHalfUp(plusvalenze),\n minusvalenze: roundHalfUp(minusvalenze),\n netResult: roundHalfUp(plusvalenze - minusvalenze),\n lots: matchedLots,\n ratesUsed,\n warnings,\n generatedAt: new Date().toISOString(),\n };\n }\n}\n","import { ClassificationError } from \"../errors.js\";\nimport type {\n AssetClass,\n ClassificationEntry,\n ClassificationMap,\n ClassifyOptions,\n Transaction,\n} from \"../types.js\";\nimport * as fs from \"node:fs\";\nimport * as https from \"node:https\";\n\ntype HttpPost = (\n url: string,\n body: string,\n timeoutMs: number,\n) => Promise<{ status: number; data: string }>;\n\nfunction httpsPost(\n url: string,\n body: string,\n timeoutMs: number,\n): Promise<{ status: number; data: string }> {\n return new Promise((resolve, reject) => {\n const u = new URL(url);\n const req = https.request(\n {\n hostname: u.hostname,\n path: u.pathname + u.search,\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(body),\n },\n },\n (res) => {\n let data = \"\";\n res.on(\"data\", (chunk) => {\n data += chunk;\n });\n res.on(\"end\", () => resolve({ status: res.statusCode ?? 0, data }));\n },\n );\n req.setTimeout(timeoutMs, () => {\n req.destroy();\n reject(new Error(\"timeout\"));\n });\n req.on(\"error\", reject);\n req.write(body);\n req.end();\n });\n}\n\nconst GOVT_BOND_WHITELIST = new Set([\n \"IT\",\n \"DE\",\n \"FR\",\n \"AT\",\n \"BE\",\n \"NL\",\n \"ES\",\n \"PT\",\n \"FI\",\n \"IE\",\n \"LU\",\n \"GR\",\n \"SK\",\n \"SI\",\n \"LT\",\n \"LV\",\n \"EE\",\n \"MT\",\n \"CY\",\n \"US\",\n \"GB\",\n \"CH\",\n \"NO\",\n \"SE\",\n \"DK\",\n \"JP\",\n \"CA\",\n \"AU\",\n \"NZ\",\n \"SG\",\n \"HK\",\n \"KR\",\n]);\n\ntype TypeMapping = {\n assetClass: AssetClass;\n bucketGain: \"A\" | \"B\";\n bucketLoss: \"A\" | \"B\";\n taxRate: number;\n whiteListed: boolean | null;\n};\n\nconst SECURITY_TYPE_MAP: Record<string, TypeMapping> = {\n ETP: {\n assetClass: \"ETF\",\n bucketGain: \"A\",\n bucketLoss: \"B\",\n taxRate: 0.26,\n whiteListed: null,\n },\n ETF: {\n assetClass: \"ETF\",\n bucketGain: \"A\",\n bucketLoss: \"B\",\n taxRate: 0.26,\n whiteListed: null,\n },\n \"Mutual Fund\": {\n assetClass: \"ETF\",\n bucketGain: \"A\",\n bucketLoss: \"B\",\n taxRate: 0.26,\n whiteListed: null,\n },\n \"Open-End Fund\": {\n assetClass: \"ETF\",\n bucketGain: \"A\",\n bucketLoss: \"B\",\n taxRate: 0.26,\n whiteListed: null,\n },\n \"Common Stock\": {\n assetClass: \"Stock\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n Equity: {\n assetClass: \"Stock\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n \"Exchange Traded Commodity\": {\n assetClass: \"ETC\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n \"Corporate Bond\": {\n assetClass: \"CorpBond\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n Option: {\n assetClass: \"Derivative\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n Future: {\n assetClass: \"Derivative\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n Warrant: {\n assetClass: \"Derivative\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n CFD: {\n assetClass: \"Derivative\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n Swap: {\n assetClass: \"Derivative\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n \"Leverage Certificate\": {\n assetClass: \"LeverageCert\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n \"Turbo Certificate\": {\n assetClass: \"LeverageCert\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n \"Capital Protected Certificate\": {\n assetClass: \"CapProtectedCert\",\n bucketGain: \"A\",\n bucketLoss: \"B\",\n taxRate: 0.26,\n whiteListed: null,\n },\n};\n\nfunction isKnownType(t: string): boolean {\n return t === \"Government Bond\" || t in SECURITY_TYPE_MAP;\n}\n\nfunction classifyByType(\n isin: string,\n securityType: string,\n product: string,\n warnings: string[],\n): ClassificationEntry {\n if (securityType === \"Government Bond\") {\n const prefix = isin.slice(0, 2).toUpperCase();\n const whiteListed = GOVT_BOND_WHITELIST.has(prefix);\n return {\n product,\n assetClass: whiteListed ? \"GovtBondWL\" : \"GovtBondOther\",\n bucketGain: \"A\",\n bucketLoss: \"B\",\n taxRate: whiteListed ? 0.125 : 0.26,\n whiteListed,\n confirmedByUser: false,\n source: \"openfigi\",\n };\n }\n\n const mapped = SECURITY_TYPE_MAP[securityType];\n if (mapped) {\n return {\n product,\n assetClass: mapped.assetClass,\n bucketGain: mapped.bucketGain,\n bucketLoss: mapped.bucketLoss,\n taxRate: mapped.taxRate,\n whiteListed: mapped.whiteListed,\n confirmedByUser: false,\n source: \"openfigi\",\n };\n }\n\n // Unknown type — flag for manual review\n warnings.push(\n `Unrecognized type for ${isin}: ${securityType}. Please classify manually.`,\n );\n return {\n product,\n assetClass: \"Stock\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n confirmedByUser: false,\n source: \"user\",\n };\n}\n\nconst ASSET_CLASS_DEFAULTS: Record<AssetClass, TypeMapping> = {\n ETF: {\n assetClass: \"ETF\",\n bucketGain: \"A\",\n bucketLoss: \"B\",\n taxRate: 0.26,\n whiteListed: null,\n },\n Stock: {\n assetClass: \"Stock\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n ETC: {\n assetClass: \"ETC\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n GovtBondWL: {\n assetClass: \"GovtBondWL\",\n bucketGain: \"A\",\n bucketLoss: \"B\",\n taxRate: 0.125,\n whiteListed: true,\n },\n GovtBondOther: {\n assetClass: \"GovtBondOther\",\n bucketGain: \"A\",\n bucketLoss: \"B\",\n taxRate: 0.26,\n whiteListed: false,\n },\n CorpBond: {\n assetClass: \"CorpBond\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n Derivative: {\n assetClass: \"Derivative\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n LeverageCert: {\n assetClass: \"LeverageCert\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n },\n CapProtectedCert: {\n assetClass: \"CapProtectedCert\",\n bucketGain: \"A\",\n bucketLoss: \"B\",\n taxRate: 0.26,\n whiteListed: null,\n },\n};\n\n/**\n * Builds a user-confirmed `ClassificationEntry` from a caller-supplied\n * `AssetClass` override (v0.8.0 stateless mode). The caller picked the asset\n * class directly, so no ISIN-prefix whitelist lookup is performed here — the\n * caller owns gov-bond-whitelisted-vs-other correctness.\n */\nfunction buildEntryFromAssetClass(\n assetClass: AssetClass,\n product: string,\n): ClassificationEntry {\n const mapped = ASSET_CLASS_DEFAULTS[assetClass];\n return {\n product,\n assetClass: mapped.assetClass,\n bucketGain: mapped.bucketGain,\n bucketLoss: mapped.bucketLoss,\n taxRate: mapped.taxRate,\n whiteListed: mapped.whiteListed,\n confirmedByUser: true,\n source: \"user\",\n };\n}\n\ntype OpenFIGIItem = { securityType?: string; securityType2?: string };\ntype OpenFIGIResult = { data?: OpenFIGIItem[]; error?: string };\n\nexport class Classifier {\n private readonly interactive: boolean;\n\n constructor(options?: { interactive?: boolean }) {\n this.interactive = options?.interactive ?? true;\n }\n\n async load(sidecarPath: string): Promise<ClassificationMap> {\n if (!fs.existsSync(sidecarPath)) {\n throw new ClassificationError(\"SIDECAR_NOT_FOUND\");\n }\n\n let parsed: unknown;\n try {\n const raw = fs.readFileSync(sidecarPath, \"utf-8\");\n parsed = JSON.parse(raw);\n } catch {\n throw new ClassificationError(\"SIDECAR_MALFORMED\");\n }\n\n if (\n typeof parsed !== \"object\" ||\n parsed === null ||\n (parsed as Record<string, unknown>)[\"version\"] !== 1\n ) {\n throw new ClassificationError(\"SIDECAR_VERSION\");\n }\n\n return (parsed as { classifications: ClassificationMap }).classifications;\n }\n\n async classify(\n transactions: Transaction[],\n sidecarPath?: string,\n options?: ClassifyOptions,\n _httpPost: HttpPost = httpsPost,\n ): Promise<ClassificationMap> {\n // 1. Deduplicate ISINs; track first product name seen per ISIN\n const isinToProduct = new Map<string, string>();\n for (const tx of transactions) {\n if (tx.isin && !isinToProduct.has(tx.isin)) {\n isinToProduct.set(tx.isin, tx.product);\n }\n }\n\n // 2. Build the \"already resolved\" set.\n // Sidecar mode (unchanged v0.6.0 behavior): only confirmedByUser:true\n // entries from disk are trusted; everything else is re-processed.\n // Stateless mode (sidecarPath undefined, v0.8.0): no disk access at\n // all — ALL entries in options.existingClassification are trusted\n // as-is, regardless of confirmedByUser.\n const confirmed: ClassificationMap = {};\n\n if (sidecarPath !== undefined) {\n if (fs.existsSync(sidecarPath)) {\n const existingMap = await this.load(sidecarPath);\n for (const [isin, entry] of Object.entries(existingMap)) {\n if (entry.confirmedByUser) {\n confirmed[isin] = entry;\n }\n }\n }\n } else if (options?.existingClassification) {\n Object.assign(confirmed, options.existingClassification);\n }\n\n // overrides always win, in both modes, and are never sent to OpenFIGI\n for (const [isin, assetClass] of Object.entries(options?.overrides ?? {})) {\n const product = isinToProduct.get(isin) ?? isin;\n confirmed[isin] = buildEntryFromAssetClass(assetClass, product);\n }\n\n // 3. ISINs that need (re-)processing: not already resolved\n const toProcess: string[] = [];\n for (const isin of isinToProduct.keys()) {\n if (!(isin in confirmed)) {\n toProcess.push(isin);\n }\n }\n\n // 4. Batch-query OpenFIGI (skipped entirely in offline mode)\n const warnings: string[] = [];\n const newEntries: ClassificationMap = {};\n\n if (options?.offline) {\n for (const isin of toProcess) {\n const product = isinToProduct.get(isin) ?? isin;\n warnings.push(\n `Unrecognized type for ${isin}: unknown. Please classify manually.`,\n );\n newEntries[isin] = {\n product,\n assetClass: \"Stock\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n confirmedByUser: false,\n source: \"user\",\n };\n }\n } else {\n const BATCH_SIZE = 10;\n const totalBatches = Math.ceil(toProcess.length / BATCH_SIZE);\n\n for (let i = 0; i < toProcess.length; i += BATCH_SIZE) {\n // Wait 6 s between consecutive batch requests\n if (i > 0) {\n await new Promise<void>((resolve) => setTimeout(resolve, 6000));\n }\n\n const batch = toProcess.slice(i, i + BATCH_SIZE);\n const requestBody = JSON.stringify(\n batch.map((isin) => ({ idType: \"ID_ISIN\", idValue: isin })),\n );\n\n let response: { status: number; data: string } | null = null;\n\n // Try once; retry once on 5xx\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n const r = await _httpPost(\n \"https://api.openfigi.com/v3/mapping\",\n requestBody,\n 10_000,\n );\n if (r.status < 500) {\n response = r;\n break;\n }\n // 5xx — loop will retry once\n } catch {\n // Network-level error — no retry\n throw new ClassificationError(\"NETWORK_ERROR\");\n }\n }\n\n if (response === null) {\n throw new ClassificationError(\"NETWORK_ERROR\");\n }\n\n const results = JSON.parse(response.data) as OpenFIGIResult[];\n\n for (let j = 0; j < batch.length; j++) {\n const isin = batch[j]!;\n const result = results[j];\n const product = isinToProduct.get(isin) ?? isin;\n\n if (\n !result ||\n result.error ||\n !result.data ||\n result.data.length === 0\n ) {\n warnings.push(\n `Unrecognized type for ${isin}: unknown. Please classify manually.`,\n );\n newEntries[isin] = {\n product,\n assetClass: \"Stock\",\n bucketGain: \"B\",\n bucketLoss: \"B\",\n taxRate: 0,\n whiteListed: null,\n confirmedByUser: false,\n source: \"user\",\n };\n continue;\n }\n\n const st = result.data[0]?.securityType;\n const st2 = result.data[0]?.securityType2;\n\n // Prefer the type that resolves to a known mapping\n const typeToUse =\n st && isKnownType(st)\n ? st\n : st2 && isKnownType(st2)\n ? st2\n : (st ?? st2 ?? \"Unknown\");\n\n newEntries[isin] = classifyByType(isin, typeToUse, product, warnings);\n }\n\n const batchIndex = i / BATCH_SIZE;\n options?.onBatchProgress?.(batchIndex + 1, totalBatches);\n }\n }\n\n // 5. Merge: confirmed/overrides entries take precedence and remain untouched\n const mergedMap: ClassificationMap = { ...newEntries, ...confirmed };\n\n // 6. Write sidecar — only in sidecar mode; stateless mode never touches disk\n if (sidecarPath !== undefined) {\n const sidecarContent = JSON.stringify(\n {\n version: 1,\n generatedAt: new Date().toISOString(),\n classifications: mergedMap,\n warnings,\n },\n null,\n 2,\n );\n\n try {\n fs.writeFileSync(sidecarPath, sidecarContent, \"utf-8\");\n } catch {\n throw new ClassificationError(\"WRITE_ERROR\");\n }\n }\n\n return mergedMap;\n }\n}\n"],"mappings":"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","names":["isoDate","path","roundHalfUp","fs"]}