refactor: simplify auth and instrument pages
Frontend verify / commits (pull_request) Successful in 24s
Frontend verify / security (pull_request) Failing after 9s
Frontend verify / verify (pull_request) Skipped

This commit is contained in:
2026-09-29 05:45:08 +03:00
parent 32a77860c8
commit b404c1802a
38 changed files with 644 additions and 790 deletions
@@ -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 };
+189
View File
@@ -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);
});
});
+3 -10
View File
@@ -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,
),
),
),