import type { Money } from '../../../../common/money.types'; import type { PortfolioDto, PortfolioPositionDto } from '../presentation/portfolio.dto'; import { add, decimalFromString, formatDecimal, isZero, multiply, subtract, } from '../../../../common/decimal'; import { decimal } from '../../../../integrations/tbank/decimal'; import { metadataKey, type TbankInstrument, type TbankMoney, TbankPortfolio, TbankPositions, } from '../../../../integrations/tbank/tbank.api'; import { portfolioStructure } from '../domain/portfolio-structure'; const types: Record = { share: 'STOCK', bond: 'BOND', etf: 'FUND', currency: 'CURRENCY', futures: 'FUTURE', option: 'OPTION', sp: 'STRUCTURED_NOTE', dfa: 'DIGITAL_FINANCIAL_ASSET', }; function mapPortfolio( upstream: NonNullable, cash: TbankMoney[], metadata: ReadonlyMap, lastSuccessfulAt: string, ): PortfolioDto { const positions = upstream .map((position) => mapPosition(position, metadata.get(metadataKey(position))?.data ?? null)) .filter((position): position is PortfolioPositionDto => position !== null); const balances = new Map>(); for (const value of cash) { const currency = value.currency?.trim().toUpperCase(); const amount = decimal(value); if (!currency || isZero(amount)) continue; balances.set(currency, add(balances.get(currency) ?? decimal({}), amount)); } const cashBalances = [...balances].map(([currency, value]) => ({ currency, value: formatDecimal(value), })); return { lastSuccessfulAt, isStale: false, totalsByCurrency: totalsByCurrency(upstream, cash), positions, cashBalances, structure: portfolioStructure(positions, cashBalances), }; } function mapPosition( position: NonNullable[number], metadata: null | TbankInstrument, ): null | PortfolioPositionDto { const quantity = decimal(position.quantity ?? {}); if (isZero(quantity)) return null; const figi = position.figi?.trim() || null; const instrumentUid = position.instrumentUid?.trim() || null; const currentPrice = money(position.currentPrice); const averagePurchasePrice = money(position.averagePositionPrice); const currentValue = currentPrice && { currency: currentPrice.currency, value: formatDecimal(multiply(quantity, decimalFromString(currentPrice.value))), }; const absoluteReturn = currentPrice && averagePurchasePrice && currentPrice.currency === averagePurchasePrice.currency ? { currency: currentPrice.currency, value: formatDecimal(multiply( subtract(decimalFromString(currentPrice.value), decimalFromString(averagePurchasePrice.value)), quantity, )), } : null; return { figi, instrumentUid, displayName: metadata?.name?.trim() || metadata?.ticker?.trim() || figi || 'Unknown instrument', type: types[position.instrumentType?.toLowerCase() ?? ''] ?? 'OTHER', quantity: formatDecimal(quantity), currentValue, averagePurchasePrice, absoluteReturn, isin: metadata?.isin?.trim() || null, ticker: metadata?.ticker?.trim() || null, logoName: metadata?.brand?.logoName?.trim() || null, }; } function money(value: TbankMoney | undefined): Money | null { const currency = value?.currency?.trim().toUpperCase(); return currency && value ? { currency, value: formatDecimal(decimal(value)) } : null; } function totalsByCurrency( positions: NonNullable, money: NonNullable, ): Money[] { const totals = new Map>(); const addTotal = ( currency: string | undefined, amount: ReturnType, ) => { const normalizedCurrency = currency?.trim().toUpperCase(); if (!normalizedCurrency || isZero(amount)) return; totals.set( normalizedCurrency, add(totals.get(normalizedCurrency) ?? decimal({}), amount), ); }; for (const value of money) addTotal(value.currency, decimal(value)); for (const position of positions) { const price = position.currentPrice; // CURRENCY positions mirror the cash balances already added above. if (!price || types[position.instrumentType?.toLowerCase() ?? ''] === 'CURRENCY') continue; addTotal( price.currency, multiply(decimal(position.quantity ?? {}), decimal(price)), ); } return [...totals] .filter(([, value]) => !isZero(value)) .map(([currency, value]) => ({ currency, value: formatDecimal(value) })); } export { mapPortfolio, totalsByCurrency };