import { describe, expect, it } from "vitest"; import type { Portfolio } from "@/entities/portfolio"; import { toAccountPagePositionSections } from "./positions"; import { portfolio, position, second } from "./positions.fixtures"; function sections(positions: Portfolio["positions"]) { return toAccountPagePositionSections({ ...portfolio, positions, }); } describe("account position sections", () => { it("excludes currency instruments only from display, preserving order and share denominators", () => { const otherTypes = [ "OPTION", "OTHER", "FUTURE", "STRUCTURED_NOTE", "DIGITAL_FINANCIAL_ASSET", "FUND", ] as const; const snapshot: Portfolio = { ...portfolio, cashBalances: [{ currency: "RUB", value: "1000" }], positions: [ ...portfolio.positions, { ...portfolio.positions[0], currentValue: { currency: "RUB", value: "50" }, figi: "FIGI-CURRENCY", type: "CURRENCY", }, ...otherTypes.map( type => ({ ...portfolio.positions[2], currentValue: type === "OPTION" ? { currency: "RUB", value: "50" } : null, figi: `FIGI-${type}-NEXT`, type, }), ), ], structure: { currencies: [], unvaluedCategories: [] }, totalsByCurrency: [{ currency: "RUB", value: "1250" }], }; const original = structuredClone(snapshot); const sections = toAccountPagePositionSections(snapshot); expect(sections.map(section => section.kind)).toEqual([ "stocks", "bonds", "other", ]); expect(sections[2]?.groups.map(group => group.positions[0].figi)).toEqual([ "FIGI-FUND", ...otherTypes.map(type => `FIGI-${type}-NEXT`), ]); expect(sections[0]?.groups[0]?.positions[0].shareText).toBe("40 %"); expect(sections[1]?.groups[0]?.positions[0].shareText).toBe("20 %"); expect(sections[2]?.groups[1]?.positions[0].shareText).toBe("20 %"); expect(snapshot).toEqual(original); }); it("prepares grouped position sections with derived display data", () => { expect( toAccountPagePositionSections(portfolio).map(({ groups, kind }) => ({ kind, positions: groups.flatMap(group => group.positions), })), ).toMatchObject([ { kind: "stocks", positions: [ { displayName: "Акция", logoSrc: "https://invest-brands.cdn-tinkoff.ru/akciyax160.png", returnText: "+10 ₽", returnTone: "positive", shareText: "66,7 %", }, ], }, { kind: "bonds", positions: [ { displayName: "Облигация", logoSrc: null, returnText: "-5 ₽", returnTone: "negative", shareText: "33,3 %", }, ], }, { kind: "other", positions: [ { displayName: "Фонд", logoSrc: null, returnText: null, returnTone: undefined, shareText: null, }, ], }, ]); }); it("uses ISIN only when FIGI is absent, never merging by name or ticker", () => { expect( sections([ { ...position, figi: null, }, { ...second, figi: null }, ])[0]?.groups, ).toHaveLength(1); expect( sections([position, { ...second, figi: "OTHER-FIGI" }])[0]?.groups, ).toHaveLength(2); expect( sections([ { ...position, figi: null, isin: null, }, { ...second, figi: null, isin: null, }, ])[0]?.groups, ).toHaveLength(2); }); it("preserves one-account cards and does not merge repeated rows on the same account", () => { expect( sections([position, { ...second, account: position.account }])[0]?.groups, ).toHaveLength(2); expect( sections([ { ...position, account: undefined }, { ...second, account: undefined }, ])[0]?.groups, ).toHaveLength(2); }); it("keeps currencies and purchase prices attached to their source positions", () => { const source = [ position, { ...second, averagePurchasePrice: { currency: "USD", value: "0.99" }, currentValue: { currency: "USD", value: "1.01" }, }, ]; const before = structuredClone(source); const group = sections(source)[0]?.groups[0]; expect( group?.positions.map(item => [ item.quantity, item.currentValue, item.averagePurchasePrice, ]), ).toEqual( source.map(item => [ item.quantity, item.currentValue, item.averagePurchasePrice, ]), ); expect(source).toEqual(before); }); });