import { formatShare, type Portfolio } from "@/entities/portfolio"; import { formatMoney, instrumentLogoUrl } from "@/shared/lib"; type AccountPagePosition = Portfolio["positions"][number] & { instrumentTo?: string; logoSrc: null | string; returnText: null | string; returnTone?: AccountPageReturnTone; shareText: null | string; }; type AccountPagePositionGroup = { key: string; positions: [AccountPagePosition, ...AccountPagePosition[]]; }; type AccountPagePositionSection = { groups: AccountPagePositionGroup[]; kind: PositionSectionKind; }; type AccountPageReturnTone = "negative" | "positive"; type PositionSectionKind = "bonds" | "other" | "stocks"; function groupPositions( positions: AccountPagePosition[], ): AccountPagePositionGroup[] { const groups = new Map(); for (const [index, position] of positions.entries()) { const identity = positionIdentity(position); const rowKey = JSON.stringify(["row", position.account?.id, index]); const key = identity && position.account ? JSON.stringify([position.type, ...identity]) : rowKey; const group = groups.get(key); if ( group && group.positions.every( item => item.account?.id !== position.account?.id, ) ) { group.positions.push(position); } else { const uniqueKey = group ? rowKey : key; groups.set(uniqueKey, { key: uniqueKey, positions: [position] }); } } // eslint-disable-next-line unicorn/prefer-iterator-to-array -- Production targets ES2023; Iterator helpers require a newer browser baseline. return [...groups.values()]; } function positionIdentity(position: AccountPagePosition) { if (position.instrumentUid?.trim()) return ["uid", position.instrumentUid.trim()]; if (position.figi?.trim()) return ["figi", position.figi.trim()]; return position.isin?.trim() ? ["isin", position.isin.trim()] : undefined; } function toAccountPagePositionSections( portfolio: Portfolio, ): AccountPagePositionSection[] { const denominators = new Map(); for (const position of portfolio.positions) { if (!position.currentValue) continue; const currency = position.currentValue.currency; denominators.set( currency, (denominators.get(currency) ?? 0) + Number(position.currentValue.value), ); } const stocks = toSection( "stocks", portfolio, denominators, ); const bonds = toSection("bonds", portfolio, denominators); const other = toSection("other", portfolio, denominators); return [stocks, bonds, other].filter(section => section.groups.length > 0); } function toPosition( position: Portfolio["positions"][number], denominators: Map, kind: PositionSectionKind, ): AccountPagePosition { const isExpanded = kind !== "other"; const returnValue = position.absoluteReturn ? Number(position.absoluteReturn.value) : null; let returnTone: AccountPageReturnTone | undefined; if (returnValue !== null && returnValue !== 0) returnTone = returnValue > 0 ? "positive" : "negative"; const returnPrefix = isExpanded && returnValue !== null && returnValue > 0 ? "+" : ""; return { ...position, instrumentTo: (kind === "stocks" || kind === "bonds") && position.instrumentUid ? `/${kind}/${encodeURIComponent(position.instrumentUid)}` : undefined, logoSrc: isExpanded ? instrumentLogoUrl(position.logoName) : null, returnText: position.absoluteReturn ? `${returnPrefix}${formatMoney(position.absoluteReturn)}` : null, returnTone, shareText: position.currentValue?.currency ? formatShare(position.currentValue, { currency: position.currentValue.currency, value: String(denominators.get(position.currentValue.currency) ?? 0), }) : null, }; } function toSection( kind: PositionSectionKind, portfolio: Portfolio, denominators: Map, ): AccountPagePositionSection { return { groups: groupPositions( portfolio.positions .filter((position) => { if (kind === "stocks") return position.type === "STOCK"; return kind === "bonds" ? position.type === "BOND" : position.type !== "STOCK" && position.type !== "BOND" && position.type !== "CURRENCY"; }) .map(position => toPosition( position, denominators, kind, ), ), ), kind, }; } export { type AccountPagePosition, type AccountPagePositionGroup, type AccountPagePositionSection, toAccountPagePositionSections, };