refactor: simplify auth and instrument pages
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import type { Portfolio } from "@/entities/portfolio";
|
||||
|
||||
const portfolio: Portfolio
|
||||
= {
|
||||
cashBalances: [{ currency: "USD", value: "1" }],
|
||||
isStale: true,
|
||||
lastSuccessfulAt: "2026-08-13T10:00:00Z",
|
||||
positions: [
|
||||
{
|
||||
absoluteReturn: { currency: "RUB", value: "10" },
|
||||
averagePurchasePrice: null,
|
||||
currentValue: { currency: "RUB", value: "100" },
|
||||
displayName: "Акция",
|
||||
figi: "FIGI-STOCK",
|
||||
isin: null,
|
||||
logoName: "akciya.png",
|
||||
quantity: "2",
|
||||
ticker: null,
|
||||
type: "STOCK",
|
||||
},
|
||||
{
|
||||
absoluteReturn: { currency: "RUB", value: "-5" },
|
||||
averagePurchasePrice: { currency: "RUB", value: "45" },
|
||||
currentValue: { currency: "RUB", value: "50" },
|
||||
displayName: "Облигация",
|
||||
figi: "FIGI-BOND",
|
||||
isin: "RU000A0JX0J2",
|
||||
logoName: null,
|
||||
quantity: "1",
|
||||
ticker: null,
|
||||
type: "BOND",
|
||||
},
|
||||
{
|
||||
absoluteReturn: null,
|
||||
averagePurchasePrice: null,
|
||||
currentValue: null,
|
||||
displayName: "Фонд",
|
||||
figi: "FIGI-FUND",
|
||||
isin: null,
|
||||
logoName: "fond.png",
|
||||
quantity: "3",
|
||||
ticker: null,
|
||||
type: "FUND",
|
||||
},
|
||||
],
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
totalsByCurrency: [{ currency: "RUB", value: "150" }],
|
||||
};
|
||||
|
||||
const position: Portfolio["positions"][number] = {
|
||||
...portfolio.positions[0],
|
||||
absoluteReturn: { currency: "RUB", value: "25" },
|
||||
account: { id: "a", name: "Основной" },
|
||||
averagePurchasePrice: { currency: "RUB", value: "280" },
|
||||
currentValue: { currency: "RUB", value: "375" },
|
||||
displayName: "Сбербанк",
|
||||
figi: "FIGI-SBER",
|
||||
isin: "RU0009029540",
|
||||
logoName: null,
|
||||
quantity: "1.25",
|
||||
ticker: "SBER",
|
||||
};
|
||||
|
||||
const second = {
|
||||
...position,
|
||||
absoluteReturn: null,
|
||||
account: { id: "b", name: "ИИС" },
|
||||
averagePurchasePrice: null,
|
||||
currentValue: null,
|
||||
quantity: "-2",
|
||||
};
|
||||
|
||||
export { portfolio, position, second };
|
||||
@@ -0,0 +1,189 @@
|
||||
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;
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -69,8 +69,6 @@ function positionIdentity(position: AccountPagePosition) {
|
||||
|
||||
function toAccountPagePositionSections(
|
||||
portfolio: Portfolio,
|
||||
instrumentDetails: Map<string, undefined
|
||||
| { slug: string }> = new Map(),
|
||||
): AccountPagePositionSection[] {
|
||||
const denominators = new Map<string, number>();
|
||||
|
||||
@@ -87,11 +85,10 @@ function toAccountPagePositionSections(
|
||||
"stocks",
|
||||
portfolio,
|
||||
denominators,
|
||||
instrumentDetails,
|
||||
);
|
||||
|
||||
const bonds = toSection("bonds", portfolio, denominators, instrumentDetails);
|
||||
const other = toSection("other", portfolio, denominators, instrumentDetails);
|
||||
const bonds = toSection("bonds", portfolio, denominators);
|
||||
const other = toSection("other", portfolio, denominators);
|
||||
|
||||
return [stocks, bonds, other].filter(section => section.groups.length > 0);
|
||||
}
|
||||
@@ -100,7 +97,6 @@ function toPosition(
|
||||
position: Portfolio["positions"][number],
|
||||
denominators: Map<string, number>,
|
||||
kind: PositionSectionKind,
|
||||
instrumentSlug?: string,
|
||||
): AccountPagePosition {
|
||||
const isExpanded = kind !== "other";
|
||||
|
||||
@@ -119,7 +115,7 @@ function toPosition(
|
||||
...position,
|
||||
instrumentTo:
|
||||
(kind === "stocks" || kind === "bonds") && position.instrumentUid
|
||||
? `/${kind}/${encodeURIComponent(instrumentSlug ?? position.instrumentUid)}`
|
||||
? `/${kind}/${encodeURIComponent(position.instrumentUid)}`
|
||||
: undefined,
|
||||
logoSrc: isExpanded
|
||||
? instrumentLogoUrl(position.logoName)
|
||||
@@ -141,8 +137,6 @@ function toSection(
|
||||
kind: PositionSectionKind,
|
||||
portfolio: Portfolio,
|
||||
denominators: Map<string, number>,
|
||||
instrumentDetails: Map<string, undefined
|
||||
| { slug: string }>,
|
||||
): AccountPagePositionSection {
|
||||
return {
|
||||
groups: groupPositions(
|
||||
@@ -161,7 +155,6 @@ function toSection(
|
||||
position,
|
||||
denominators,
|
||||
kind,
|
||||
instrumentDetails.get(position.instrumentUid ?? "")?.slug,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user