refactor: clarify frontend model ownership
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
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[];
|
||||
};
|
||||
|
||||
type AccountPagePositionSection = {
|
||||
groups: AccountPagePositionGroup[];
|
||||
kind: PositionSectionKind;
|
||||
};
|
||||
|
||||
type AccountPageReturnTone = "negative" | "positive";
|
||||
|
||||
type PositionSectionKind = "bonds"
|
||||
| "other"
|
||||
| "stocks";
|
||||
|
||||
function groupPositions(
|
||||
positions: AccountPagePosition[],
|
||||
): AccountPagePositionGroup[] {
|
||||
const groups = new Map<string, AccountPagePositionGroup>();
|
||||
|
||||
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,
|
||||
instrumentDetails: Map<string, undefined
|
||||
| { slug: string }> = new Map(),
|
||||
): AccountPagePositionSection[] {
|
||||
const denominators = new Map<string, number>();
|
||||
|
||||
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,
|
||||
instrumentDetails,
|
||||
);
|
||||
|
||||
const bonds = toSection("bonds", portfolio, denominators, instrumentDetails);
|
||||
const other = toSection("other", portfolio, denominators, instrumentDetails);
|
||||
|
||||
return [stocks, bonds, other].filter(section => section.groups.length > 0);
|
||||
}
|
||||
|
||||
function toPosition(
|
||||
position: Portfolio["positions"][number],
|
||||
denominators: Map<string, number>,
|
||||
kind: PositionSectionKind,
|
||||
instrumentSlug?: string,
|
||||
): 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(instrumentSlug ?? 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<string, number>,
|
||||
instrumentDetails: Map<string, undefined
|
||||
| { slug: string }>,
|
||||
): 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,
|
||||
instrumentDetails.get(position.instrumentUid ?? "")?.slug,
|
||||
),
|
||||
),
|
||||
),
|
||||
kind,
|
||||
};
|
||||
}
|
||||
|
||||
export {
|
||||
type AccountPagePosition,
|
||||
type AccountPagePositionGroup,
|
||||
type AccountPagePositionSection,
|
||||
toAccountPagePositionSections,
|
||||
};
|
||||
Reference in New Issue
Block a user