refactor: enforce frontend view model boundaries
Frontend verify / commits (pull_request) Successful in 28s
Frontend verify / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 8m19s

This commit is contained in:
2026-09-28 18:48:23 +03:00
parent 5b69003516
commit ca0ad4f824
287 changed files with 8558 additions and 4538 deletions
@@ -22,6 +22,7 @@ describe("useBrokerageOperations", () => {
cursor: "next",
}),
).toEqual({ categories: ["income", "tax"], cursor: "next" });
const { result } = renderHook(
() => useBrokerageOperations("account-1", { limit: 5 }),
{ wrapper: createQueryWrapper().wrapper },
@@ -8,7 +8,9 @@ type BrokerageOperationsQuery = Omit<
NonNullable<OperationsEndpoint["parameters"]["query"]>,
"sync"
>;
type OperationCategory = BrokerageOperation["category"];
type OperationsEndpoint
= paths["/api/v1/brokerage-accounts/{accountId}/operations"]["get"];
@@ -42,7 +44,11 @@ function brokerageOperationsQueryParameters(query: BrokerageOperationsQuery) {
...(query.instrumentUid && { instrumentUid: query.instrumentUid }),
...(query.from && { from: query.from }),
...(query.to && { to: query.to }),
...(query.categories?.length && { categories: query.categories.toSorted((left, right) => left.localeCompare(right)) }),
...(query.categories?.length && {
categories: query.categories.toSorted((left, right) =>
left.localeCompare(right),
),
}),
...(query.cursor && { cursor: query.cursor }),
...(query.limit && { limit: query.limit }),
};
+15 -12
View File
@@ -22,7 +22,9 @@ const operationTransferPrefix = "OPERATION_TYPE_TRANS_";
type PaymentPresentation = {
amount: string;
label: string;
tone: "negative" | "neutral" | "positive";
tone: "negative"
| "neutral"
| "positive";
};
function entryLabelKey(operation: BrokerageOperation): string {
@@ -30,6 +32,7 @@ function entryLabelKey(operation: BrokerageOperation): string {
return isOperationType(operation.type, OperationType.Coupon)
? "ledger.entryLabel.income.coupon"
: "ledger.entryLabel.income.dividend";
return `ledger.category.${operation.category}`;
}
@@ -43,19 +46,15 @@ function isZeroDecimal(value: string): boolean {
function operationLabelKey(type: string): string {
if (
[
OperationType.Buy,
OperationType.BuyCard,
OperationType.BuyMargin,
].some(candidate => isOperationType(type, candidate))
[OperationType.Buy, OperationType.BuyCard, OperationType.BuyMargin].some(
candidate => isOperationType(type, candidate),
)
)
return "ledger.operationType.buy";
if (
[
OperationType.Sell,
OperationType.SellCard,
OperationType.SellMargin,
].some(candidate => isOperationType(type, candidate))
[OperationType.Sell, OperationType.SellCard, OperationType.SellMargin].some(
candidate => isOperationType(type, candidate),
)
)
return "ledger.operationType.sell";
if (
@@ -72,7 +71,10 @@ function operationLabelKey(type: string): string {
return "ledger.operationType.input";
if (isOperationType(type, OperationType.OutputSecurities))
return "ledger.operationType.output";
return type.startsWith(operationTransferPrefix) ? "ledger.operationType.transfer" : "ledger.operationType.other";
return type.startsWith(operationTransferPrefix)
? "ledger.operationType.transfer"
: "ledger.operationType.other";
}
function paymentPresentation(
@@ -99,6 +101,7 @@ function paymentPresentation(
label: entryLabelKey(operation),
tone: "negative",
};
return {
amount: `+${formatMoney(operation.payment)}`,
label: entryLabelKey(operation),
@@ -24,50 +24,59 @@ type LedgerEntryRowProps = {
const LedgerEntryRow: FC<LedgerEntryRowProps> = (props) => {
const { operation } = props;
const { t } = useTranslation();
const instrument
= operation.ticker ?? operation.instrumentType ?? t("ledger.noInstrument");
const payment = paymentPresentation(operation);
const metadata = [
t("ledger.occurredAt", { date: formatSnapshotTime(operation.occurredAt) }),
instrument,
props.withDetails ? /* eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model. */ null : operation.account?.name,
props.withDetails
? null // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
: operation.account?.name,
]
.filter(Boolean)
.join(" · ");
let details;
if (props.withDetails) {
details = (
<>
<Group gap="sm">
{operation.quantity != undefined && (
<Text c="dimmed" size="xs">
{t("ledger.quantity", {
value: formatDecimal(operation.quantity),
})}
</Text>
)}
{!!operation.price && (
<Text c="dimmed" size="xs">
{t("ledger.price", { value: formatMoney(operation.price) })}
</Text>
)}
</Group>
{!!operation.account && (
<Anchor
component={Link}
size="sm"
to={`/accounts/${encodeURIComponent(operation.account.id)}`}
>
{operation.account.name}
</Anchor>
)}
</>
);
}
else details = undefined;
return (
<EntryCard
details={
(props.withDetails)
? ((
<>
<Group gap="sm">
{operation.quantity != undefined && ((
<Text c="dimmed" size="xs">
{t("ledger.quantity", {
value: formatDecimal(operation.quantity),
})}
</Text>
))}
{!!(operation.price) && ((
<Text c="dimmed" size="xs">
{t("ledger.price", { value: formatMoney(operation.price) })}
</Text>
))}
</Group>
{!!(operation.account) && ((
<Anchor
component={Link}
size="sm"
to={`/accounts/${encodeURIComponent(operation.account.id)}`}
>
{operation.account.name}
</Anchor>
))}
</>
))
: undefined
}
details={details}
imageSrc={instrumentLogoUrl(operation.logoName)}
initial={(instrument.trim()[0] ?? "?").toUpperCase()}
metadata={metadata}