import { describe, expect, it } from "vitest"; import type { Portfolio } from "@/entities/portfolio"; import { toConcentration } from "./concentration"; import { portfolio, position } from "./positions.fixtures"; function snapshot(positions: Portfolio["positions"]): Portfolio { return { ...portfolio, positions }; } function stock( name: string, value: null | string, currency = "RUB", overrides: Partial = {}, ): Portfolio["positions"][number] { return { ...position, currentValue: value === null ? null : { currency, value }, displayName: name, ...overrides, }; } const labels = { missingIssuer: "Эмитент не указан", missingSector: "Сектор не указан", other: "Прочее", sector: (code: string) => code, }; describe("account concentration", () => { it("uses exact positive values and excludes cash, negatives and nulls", () => { const model = toConcentration(snapshot([ stock("A", "1"), stock("B", "15"), stock("C", "0"), stock("D", "-50"), stock("E", null), stock("F", null, "USD"), stock("G", "-3", "USD"), ]), "stocks", "RUB", labels); expect(model.currencies).toEqual(["RUB", "USD"]); expect(model.total).toBe("16"); expect(model.items.map(item => item.share)).toEqual(["93,8 %", "6,3 %"]); expect(model.unaccounted).toBe(3); const negative = snapshot([stock("A", "-3", "USD")]); expect(toConcentration(negative, "stocks", "USD", labels)) .toMatchObject({ empty: true, unaccounted: 1 }); }); it("ranks seven positions with exact other and top-five totals", () => { const model = toConcentration(snapshot([ stock("A", "400"), stock("B", "250"), stock("C", "150"), stock("D", "100"), stock("E", "50"), stock("F", "30"), stock("G", "20"), ]), "stocks", "RUB", labels); expect(model.items.map(item => item.amount)).toEqual([ "400", "250", "150", "100", "50", ]); expect(model.other).toMatchObject({ amount: "50", count: 2, share: "5 %", }); expect(model.top5Share).toBe("95 %"); expect(model.other?.entries.map(item => item.share)).toEqual([ "3 %", "2 %", ]); }); it("keeps sixth row, tiny share and stable alphabetical ties", () => { const model = toConcentration(snapshot([ stock("Сбер", "100"), stock("X5", "100"), stock("VK", "100"), stock("D", "100"), stock("E", "100"), stock("F", "100"), ]), "stocks", "RUB", labels); expect(model.items).toHaveLength(6); expect(model.other).toBeNull(); expect(model.items.slice(0, 2).map(item => item.name)).toEqual([ "D", "E", ]); const tiny = snapshot([ stock("Large", "100000000"), stock("Small", "1"), ]); expect(toConcentration(tiny, "stocks", "RUB", labels).items[1]?.share) .toBe("< 0,1 %"); }); it("groups sectors and issuers by codes, not labels", () => { const positions = [ stock("A", "300", "RUB", { issuer: { id: "one", name: "Сбербанк" }, sector: "energy", }), stock("B", "200", "RUB", { issuer: { id: "one", name: "Сбер" }, sector: "energy", type: "BOND", }), stock("C", "100", "RUB", { issuer: { id: "two", name: "Сбер" }, sector: "financial", type: "FUND", }), stock("D", "50", "RUB", { issuer: null, sector: null }), ]; expect(toConcentration(snapshot(positions), "sectors", "RUB", labels) .items.map(item => item.amount)).toEqual(["500", "100", "50"]); expect(toConcentration(snapshot(positions), "issuers", "RUB", labels) .items.map(item => [item.name, item.amount])).toEqual([ ["Сбербанк", "500"], ["", "50"], ]); }); it("orders equal sums by displayed name", () => { const issuers = snapshot([ stock("A", "300", "RUB", { issuer: { id: "g", name: "Газпром" }, sector: "energy", }), stock("B", "300", "RUB", { issuer: { id: "s", name: "Сбер" }, sector: "energy", }), stock("C", "200", "RUB", { issuer: null, sector: "energy" }), stock("D", "100", "RUB", { issuer: null, sector: "energy" }), ]); expect(toConcentration(issuers, "issuers", "RUB", labels) .items.map(item => [item.label, item.amount])).toEqual([ ["Газпром", "300"], ["Сбер", "300"], ["Эмитент не указан", "300"], ]); const sectors = snapshot([ stock("A", "100", "RUB", { sector: "it" }), stock("B", "100", "RUB", { sector: "energy" }), ]); const sectorLabels = { ...labels, sector: (code: string) => code === "it" ? "Информационные технологии" : "Энергетика", }; expect(toConcentration(sectors, "sectors", "RUB", sectorLabels) .items.map(item => item.label)) .toEqual(["Информационные технологии", "Энергетика"]); }); it("chooses issuer name from the largest position, then alphabetically", () => { const data = snapshot([ stock("A", "100", "RUB", { issuer: { id: "brand", name: "Ястреб" }, }), stock("B", "300", "RUB", { issuer: { id: "brand", name: "Альфа" }, }), stock("C", "400", "RUB", { issuer: { id: "other", name: "Бета" }, }), ]); expect(toConcentration(data, "issuers", "RUB", labels).items .map(item => [item.name, item.label, item.amount])).toEqual([ ["Альфа", "Альфа", "400"], ["Бета", "Бета", "400"], ]); }); it("uses ISIN before ticker for bonds", () => { const data = snapshot([stock("Облигация", "100", "RUB", { isin: "RU000A0JX0J2", ticker: "BOND", type: "BOND", })]); expect(toConcentration(data, "bonds", "RUB", labels) .items[0]?.logo?.subtitle).toBe("RU000A0JX0J2"); }); });