refactor: address frontend review feedback
Frontend verify / commits (pull_request) Successful in 29s
Frontend verify / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 8m54s
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 12s
Frontend verify / verify (push) Successful in 9m59s
Frontend verify / commits (pull_request) Successful in 29s
Frontend verify / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 8m54s
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 12s
Frontend verify / verify (push) Successful in 9m59s
This commit was merged in pull request #43.
This commit is contained in:
@@ -29,8 +29,6 @@ const accounts = [
|
||||
{ id: "b", name: "ИИС" },
|
||||
];
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const position: Portfolio["positions"][number]
|
||||
= {
|
||||
absoluteReturn: null,
|
||||
@@ -45,7 +43,6 @@ const position: Portfolio["positions"][number]
|
||||
type: "STOCK",
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
const portfolio: Portfolio = {
|
||||
cashBalances: [],
|
||||
isStale: false,
|
||||
@@ -55,8 +52,6 @@ const portfolio: Portfolio = {
|
||||
totalsByCurrency: [{ currency: "RUB", value: "1200" }],
|
||||
};
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const receipt: BrokerageOperation
|
||||
= {
|
||||
accruedInterest: null,
|
||||
@@ -76,11 +71,8 @@ const receipt: BrokerageOperation
|
||||
type: "OPERATION_TYPE_COUPON",
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
const items = accounts.map(account => ({ ...receipt, account }));
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const analytics: IncomeAnalytics
|
||||
= {
|
||||
accountId: "all",
|
||||
@@ -118,7 +110,6 @@ const analytics: IncomeAnalytics
|
||||
yearComparison: null,
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
const CurrentLocation: FC = () => {
|
||||
const location = useLocation();
|
||||
|
||||
@@ -162,7 +153,7 @@ beforeEach(() => {
|
||||
HttpResponse.json(
|
||||
resource(
|
||||
accounts.map(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
account =>
|
||||
({
|
||||
...account,
|
||||
@@ -170,7 +161,6 @@ beforeEach(() => {
|
||||
portfolioSummary: null,
|
||||
type: "BROKERAGE",
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -182,7 +172,7 @@ beforeEach(() => {
|
||||
"*/api/v1/brokerage-accounts/:accountId/operations",
|
||||
({ params }) =>
|
||||
HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{
|
||||
accountId: params.accountId,
|
||||
@@ -191,7 +181,6 @@ beforeEach(() => {
|
||||
nextCursor: null,
|
||||
observedAt: receipt.occurredAt,
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -204,7 +193,7 @@ beforeEach(() => {
|
||||
accountId: "all",
|
||||
calculatedAt: receipt.occurredAt,
|
||||
events: accounts.map(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
account =>
|
||||
({
|
||||
account,
|
||||
@@ -219,7 +208,6 @@ beforeEach(() => {
|
||||
ticker: "BOND",
|
||||
type: "coupon",
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
incomeTotalsByCurrency: [],
|
||||
}),
|
||||
@@ -278,7 +266,7 @@ describe("All accounts routes", () => {
|
||||
requests.push(url);
|
||||
|
||||
return HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{
|
||||
accountId: params.accountId,
|
||||
@@ -294,7 +282,6 @@ describe("All accounts routes", () => {
|
||||
nextCursor: url.searchParams.has("cursor") ? null : "next",
|
||||
observedAt: receipt.occurredAt,
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -104,7 +104,7 @@ describe("AppRoutes", () => {
|
||||
),
|
||||
http.get("*/api/v1/bank-cards", () => HttpResponse.json([])),
|
||||
http.get("*/api/v1/bank-cards/summary", () =>
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
HttpResponse.json(
|
||||
{
|
||||
cardId: null,
|
||||
@@ -114,7 +114,6 @@ describe("AppRoutes", () => {
|
||||
net: { currency: "RUB", value: "0" },
|
||||
refunds: { currency: "RUB", value: "0" },
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -106,12 +106,8 @@ const ProtectedDocumentTitle: FC = () => {
|
||||
if (stocks) return <DocumentTitle title={t("navigation.stocks")} />;
|
||||
|
||||
return settings
|
||||
? (
|
||||
<DocumentTitle title={t("navigation.settings")} />
|
||||
)
|
||||
: (
|
||||
<DocumentTitle title={t("navigation.accounts")} />
|
||||
);
|
||||
? <DocumentTitle title={t("navigation.settings")} />
|
||||
: <DocumentTitle title={t("navigation.accounts")} />;
|
||||
};
|
||||
|
||||
export { ProtectedDocumentTitle };
|
||||
|
||||
@@ -8,10 +8,10 @@ import { RouteErrorBoundary } from "./RouteErrorBoundary";
|
||||
|
||||
const session = { isAuthenticated: false };
|
||||
vi.mock("@/features/auth", () => ({
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
useAuth: () =>
|
||||
({ user: session.isAuthenticated ? {} : null }),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
}));
|
||||
|
||||
const ThrowingRoute: FC = () => {
|
||||
@@ -25,9 +25,7 @@ const HealthyRoute: FC = () => {
|
||||
? (
|
||||
"healthy route"
|
||||
)
|
||||
: (
|
||||
<ThrowingRoute />
|
||||
)}
|
||||
: <ThrowingRoute />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -34,9 +34,7 @@ class Boundary extends Component<BoundaryProps, State> {
|
||||
|
||||
render() {
|
||||
return this.state.hasError
|
||||
? (
|
||||
<RouteErrorFallback authenticated={this.props.authenticated} />
|
||||
)
|
||||
? <RouteErrorFallback authenticated={this.props.authenticated} />
|
||||
: (
|
||||
this.props.children
|
||||
);
|
||||
|
||||
@@ -79,9 +79,7 @@ const LazyPage: FC<LazyPageProps> = (props) => {
|
||||
? (
|
||||
children
|
||||
)
|
||||
: (
|
||||
<RouteTransition>{children}</RouteTransition>
|
||||
)}
|
||||
: <RouteTransition>{children}</RouteTransition>}
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,9 +13,7 @@ const Protected: FC<PropsWithChildren> = (props) => {
|
||||
if (!ready) return <RouteSkeleton variant="shell" />;
|
||||
|
||||
return user
|
||||
? (
|
||||
<>{children}</>
|
||||
)
|
||||
? <>{children}</>
|
||||
: (
|
||||
<Navigate
|
||||
replace
|
||||
|
||||
@@ -88,9 +88,7 @@ const AppBreadcrumbs: FC = () => {
|
||||
return <BreadcrumbTrail items={[{ label: t("breadcrumbs.accounts") }]} />;
|
||||
|
||||
return settings
|
||||
? (
|
||||
<BreadcrumbTrail items={[{ label: t("breadcrumbs.settings") }]} />
|
||||
)
|
||||
? <BreadcrumbTrail items={[{ label: t("breadcrumbs.settings") }]} />
|
||||
: undefined;
|
||||
};
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ import {
|
||||
useBankSummary,
|
||||
} from "./queries";
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const card: BankCard
|
||||
= {
|
||||
archivedAt: null,
|
||||
@@ -35,7 +33,6 @@ const card: BankCard
|
||||
version: 1,
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
const transfer: CreateBankOperation = {
|
||||
amount: { currency: "RUB", value: "90071992547409.93" },
|
||||
cardId: "a",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
enum BankName {
|
||||
AlfaBank = "Альфа-Банк",
|
||||
Sberbank = "Сбербанк",
|
||||
TBank = "Т-Банк",
|
||||
Vtb = "ВТБ",
|
||||
}
|
||||
|
||||
export { BankName };
|
||||
@@ -32,7 +32,6 @@ function bankToday() {
|
||||
function moneyCents(value: string): bigint | null {
|
||||
const normalized = value.trim().replace(",", ".");
|
||||
if (!/^-?\d+$|^-?\d+\.\d{1,2}$/.test(normalized))
|
||||
// eslint-disable-next-line unicorn/no-null -- The money parser’s callers distinguish invalid input with null.
|
||||
return null;
|
||||
|
||||
const [whole, fraction = ""] = normalized
|
||||
|
||||
@@ -2,129 +2,68 @@ import type { OperationIconProps } from "@wealthledger/icons";
|
||||
|
||||
import type { BankOperation } from "../api/queries";
|
||||
|
||||
type OperationIcon = {
|
||||
animation?: string;
|
||||
color: string;
|
||||
variant: OperationIconProps["variant"];
|
||||
};
|
||||
const categoryIcons: {
|
||||
appearance: OperationIconProps;
|
||||
name: string;
|
||||
}[] = [
|
||||
{ appearance: { color: "grape", variant: "wine" }, name: "Алкоголь" },
|
||||
{ appearance: { color: "green", variant: "deposit" }, name: "Вклады" },
|
||||
{ appearance: { color: "orange", variant: "home" }, name: "Жильё" },
|
||||
{ appearance: { color: "red", variant: "medical" }, name: "Здоровье" },
|
||||
{
|
||||
appearance: { color: "green", variant: "growth" },
|
||||
name: "Инвестиции и накопления",
|
||||
},
|
||||
{ appearance: { color: "orange", variant: "coffee" }, name: "Кафе" },
|
||||
{ appearance: { color: "blue", variant: "education" }, name: "Образование" },
|
||||
{ appearance: { color: "blue", variant: "transfer" }, name: "Переводы" },
|
||||
{
|
||||
appearance: { color: "orange", variant: "repayment" },
|
||||
name: "Погашение кредита",
|
||||
},
|
||||
{
|
||||
appearance: { color: "grape", variant: "subscriptions" },
|
||||
name: "Подписки",
|
||||
},
|
||||
{ appearance: { color: "grape", variant: "shopping" }, name: "Покупки" },
|
||||
{ appearance: { color: "green", variant: "basket" }, name: "Продукты" },
|
||||
{ appearance: { color: "blue", variant: "travel" }, name: "Путешествия" },
|
||||
{ appearance: { color: "grape", variant: "music" }, name: "Развлечения" },
|
||||
{ appearance: { color: "blue", variant: "wifi" }, name: "Связь и интернет" },
|
||||
{
|
||||
appearance: { color: "green", variant: "cashWithdrawal" },
|
||||
name: "Снятие наличных",
|
||||
},
|
||||
{ appearance: { color: "grape", variant: "dice" }, name: "Ставки" },
|
||||
{ appearance: { color: "green", variant: "shield" }, name: "Страхование" },
|
||||
{ appearance: { color: "orange", variant: "smoking" }, name: "Табак" },
|
||||
{ appearance: { color: "yellow", variant: "taxi" }, name: "Такси" },
|
||||
{ appearance: { color: "blue", variant: "car" }, name: "Транспорт" },
|
||||
{
|
||||
appearance: { color: "grape", variant: "server" },
|
||||
name: "Хостинг и ИИ-сервисы",
|
||||
},
|
||||
{
|
||||
appearance: { color: "blue", variant: "download" },
|
||||
name: "Цифровые покупки",
|
||||
},
|
||||
];
|
||||
|
||||
const categoryIcons: Record<string, OperationIcon> = {
|
||||
"алкоголь": {
|
||||
animation: "wine",
|
||||
color: "grape",
|
||||
variant: "wine",
|
||||
},
|
||||
"вклады": {
|
||||
animation: "deposit",
|
||||
color: "green",
|
||||
variant: "deposit",
|
||||
},
|
||||
"жилье": { color: "orange", variant: "home" },
|
||||
"здоровье": { color: "red", variant: "medical" },
|
||||
"инвестиции и накопления": {
|
||||
animation: "growth",
|
||||
color: "green",
|
||||
variant: "growth",
|
||||
},
|
||||
"кафе": {
|
||||
animation: "coffee",
|
||||
color: "orange",
|
||||
variant: "coffee",
|
||||
},
|
||||
"образование": {
|
||||
color: "blue",
|
||||
variant: "education",
|
||||
},
|
||||
"переводы": {
|
||||
animation: "transfer",
|
||||
color: "blue",
|
||||
variant: "transfer",
|
||||
},
|
||||
"погашение кредита": {
|
||||
animation: "repayment",
|
||||
color: "orange",
|
||||
variant: "repayment",
|
||||
},
|
||||
"подписки": {
|
||||
color: "grape",
|
||||
variant: "subscriptions",
|
||||
},
|
||||
"покупки": {
|
||||
color: "grape",
|
||||
variant: "shopping",
|
||||
},
|
||||
"продукты": {
|
||||
animation: "basket",
|
||||
color: "green",
|
||||
variant: "basket",
|
||||
},
|
||||
"путешествия": {
|
||||
color: "blue",
|
||||
variant: "travel",
|
||||
},
|
||||
"развлечения": {
|
||||
color: "grape",
|
||||
variant: "music",
|
||||
},
|
||||
"связь и интернет": {
|
||||
animation: "wifi",
|
||||
color: "blue",
|
||||
variant: "wifi",
|
||||
},
|
||||
"снятие наличных": {
|
||||
color: "green",
|
||||
variant: "cashWithdrawal",
|
||||
},
|
||||
"ставки": {
|
||||
color: "grape",
|
||||
variant: "dice",
|
||||
},
|
||||
"страхование": {
|
||||
color: "green",
|
||||
variant: "shield",
|
||||
},
|
||||
"табак": {
|
||||
color: "orange",
|
||||
variant: "smoking",
|
||||
},
|
||||
"такси": {
|
||||
color: "yellow",
|
||||
variant: "taxi",
|
||||
},
|
||||
"транспорт": {
|
||||
animation: "car",
|
||||
color: "blue",
|
||||
variant: "car",
|
||||
},
|
||||
"хостинг и ии-сервисы": {
|
||||
animation: "server",
|
||||
color: "grape",
|
||||
variant: "server",
|
||||
},
|
||||
"цифровые покупки": {
|
||||
color: "blue",
|
||||
variant: "download",
|
||||
},
|
||||
};
|
||||
|
||||
const otherIcon: OperationIcon = {
|
||||
animation: "grid",
|
||||
const otherIcon: OperationIconProps = {
|
||||
color: "gray",
|
||||
variant: "grid",
|
||||
};
|
||||
|
||||
const operationIcons: Record<
|
||||
Exclude<BankOperation["kind"], "expense">,
|
||||
OperationIcon
|
||||
OperationIconProps
|
||||
> = {
|
||||
refund: { color: "teal", variant: "refund" },
|
||||
topup: {
|
||||
animation: "topup",
|
||||
color: "green",
|
||||
variant: "topup",
|
||||
},
|
||||
transfer: {
|
||||
animation: "transfer",
|
||||
color: "blue",
|
||||
variant: "transfer",
|
||||
},
|
||||
@@ -135,17 +74,20 @@ function bankOperationAppearance(
|
||||
categoryName?: null | string,
|
||||
) {
|
||||
if (kind !== "expense")
|
||||
// eslint-disable-next-line security/detect-object-injection -- Operation kinds are finite; category names are guarded by Object.hasOwn.
|
||||
// eslint-disable-next-line security/detect-object-injection -- Operation kinds are a finite API union.
|
||||
return operationIcons[kind];
|
||||
|
||||
const name = categoryName
|
||||
?.trim()
|
||||
.toLocaleLowerCase("ru-RU")
|
||||
.replaceAll("ё", "е");
|
||||
const name = normalizeCategoryName(categoryName ?? "");
|
||||
|
||||
return name && Object.hasOwn(categoryIcons, name)
|
||||
? categoryIcons[name] // eslint-disable-line security/detect-object-injection -- Operation kinds are finite; category names are guarded by Object.hasOwn.
|
||||
: otherIcon;
|
||||
const category = categoryIcons.find(
|
||||
item => normalizeCategoryName(item.name) === name,
|
||||
);
|
||||
|
||||
return category?.appearance ?? otherIcon;
|
||||
}
|
||||
|
||||
function normalizeCategoryName(name: string) {
|
||||
return name.trim().toLocaleLowerCase("ru-RU").replaceAll("ё", "е");
|
||||
}
|
||||
|
||||
export { bankOperationAppearance };
|
||||
|
||||
@@ -2,17 +2,16 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
|
||||
import { SimpleGrid } from "@mantine/core";
|
||||
|
||||
import { BankName } from "../../model/bank";
|
||||
import { BankCardFace } from "./BankCardFace";
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const meta
|
||||
= {
|
||||
args: {
|
||||
card: {
|
||||
archivedAt: null,
|
||||
balance: { currency: "RUB", value: "128450.75" },
|
||||
bankName: "Сбербанк",
|
||||
bankName: BankName.Sberbank,
|
||||
id: "fictional",
|
||||
lastFour: "4829",
|
||||
name: "Для повседневных покупок",
|
||||
@@ -25,7 +24,7 @@ const meta
|
||||
component: BankCardFace,
|
||||
tags: ["autodocs"],
|
||||
title: "Product/BankCards/CardAppearance",
|
||||
} satisfies /* eslint-enable unicorn/no-null */ Meta<typeof BankCardFace>;
|
||||
} satisfies Meta<typeof BankCardFace>;
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -35,33 +34,28 @@ export const Banks: Story = {
|
||||
render: arguments_ => (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} maw={920}>
|
||||
{[
|
||||
"Сбербанк",
|
||||
"Т-Банк",
|
||||
"Альфа-Банк",
|
||||
"ВТБ",
|
||||
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
BankName.Sberbank,
|
||||
BankName.TBank,
|
||||
BankName.AlfaBank,
|
||||
BankName.Vtb,
|
||||
null,
|
||||
].map((bankName, index) => (
|
||||
<BankCardFace
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
card={
|
||||
{
|
||||
...arguments_.card,
|
||||
bankName,
|
||||
paymentSystem:
|
||||
card={{
|
||||
...arguments_.card,
|
||||
bankName,
|
||||
paymentSystem:
|
||||
// eslint-disable-next-line security/detect-object-injection -- This is a positional index into the local array, not a user-supplied object key.
|
||||
(["mir", "mastercard", "visa", "mir", null] as const)[index]
|
||||
?? null,
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */
|
||||
}
|
||||
}}
|
||||
key={bankName ?? "unknown"}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
),
|
||||
};
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const MissingDetails: Story
|
||||
= {
|
||||
args: {
|
||||
@@ -73,7 +67,7 @@ export const MissingDetails: Story
|
||||
},
|
||||
},
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
export const LongArchived: Story = {
|
||||
args: {
|
||||
card: {
|
||||
|
||||
@@ -22,9 +22,7 @@ const BankCardFace: FC<BankCardFaceProps> = (props) => {
|
||||
<div className={classes.top}>
|
||||
<span className={classes.bank}>{card.bankName || t("bank.card")}</span>
|
||||
|
||||
{card.archivedAt && (
|
||||
<span className={classes.archive}>{t("bank.archived")}</span>
|
||||
)}
|
||||
{card.archivedAt && <span className={classes.archive}>{t("bank.archived")}</span>}
|
||||
</div>
|
||||
|
||||
<div aria-hidden="true" className={classes.hardware}>
|
||||
@@ -46,9 +44,7 @@ const BankCardFace: FC<BankCardFaceProps> = (props) => {
|
||||
|
||||
{card.paymentSystem && (
|
||||
<span className={classes.network} data-network={card.paymentSystem}>
|
||||
{card.paymentSystem === "mastercard" && (
|
||||
<span aria-hidden="true" className={classes.circles} />
|
||||
)}
|
||||
{card.paymentSystem === "mastercard" && <span aria-hidden="true" className={classes.circles} />}
|
||||
|
||||
{t(`bank.paymentSystems.${card.paymentSystem}`)}
|
||||
</span>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { DateInput } from "@mantine/dates";
|
||||
import { useController, useFormContext } from "react-hook-form";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { bankDateInputValue } from "../../model/form-validation";
|
||||
import { type BankCardValues, bankDateSchema } from "../../model/form.schema";
|
||||
import type { BankCardValues } from "../../model/form.schema";
|
||||
|
||||
import { useOpeningDateField } from "./useOpeningDateField";
|
||||
import { usePaymentSystemField } from "./usePaymentSystemField";
|
||||
|
||||
function useBankCardFieldsVm() {
|
||||
const {
|
||||
@@ -15,62 +14,10 @@ function useBankCardFieldsVm() {
|
||||
} = useFormContext<BankCardValues>();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const openingDate = useOpeningDateField(control);
|
||||
const paymentSystem = usePaymentSystemField(control);
|
||||
|
||||
const { field: paymentSystemField } = useController({
|
||||
control,
|
||||
name: "paymentSystem",
|
||||
});
|
||||
|
||||
const { field: openingDateField } = useController({
|
||||
control,
|
||||
name: "openingDate",
|
||||
});
|
||||
|
||||
const paymentSystems = ["mir", "visa", "mastercard"].map(value => ({
|
||||
label: t(`bank.paymentSystems.${value}`),
|
||||
value,
|
||||
}));
|
||||
|
||||
const onPaymentSystemChange: (value: null | string) => void = value =>
|
||||
paymentSystemField.onChange(value ?? "");
|
||||
|
||||
const dateParser: ComponentProps<typeof DateInput>["dateParser"] = value =>
|
||||
bankDateSchema.safeParse(bankDateInputValue(value)).success
|
||||
? bankDateInputValue(value)
|
||||
: null; // eslint-disable-line unicorn/no-null -- Mantine represents an empty controlled openingDateField as null.
|
||||
|
||||
const onOpeningDateBlur: ComponentProps<typeof DateInput>["onBlur"] = (
|
||||
event,
|
||||
) => {
|
||||
openingDateField.onChange(bankDateInputValue(event.currentTarget.value));
|
||||
openingDateField.onBlur();
|
||||
};
|
||||
|
||||
const onOpeningDateChange: (value: null | string) => void = value =>
|
||||
openingDateField.onChange(value ?? "");
|
||||
|
||||
const onOpeningDateInput: ComponentProps<typeof DateInput>["onInput"] = event =>
|
||||
openingDateField.onChange(bankDateInputValue(event.currentTarget.value));
|
||||
|
||||
const openingDateValue = bankDateSchema.safeParse(openingDateField.value)
|
||||
.success
|
||||
? openingDateField.value
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
dateParser,
|
||||
errors,
|
||||
onOpeningDateBlur,
|
||||
onOpeningDateChange,
|
||||
onOpeningDateInput,
|
||||
onPaymentSystemChange,
|
||||
openingDateField,
|
||||
openingDateValue,
|
||||
paymentSystemField,
|
||||
paymentSystems,
|
||||
register,
|
||||
t,
|
||||
};
|
||||
return { ...openingDate, ...paymentSystem, errors, register, t };
|
||||
}
|
||||
|
||||
export { useBankCardFieldsVm };
|
||||
|
||||
@@ -12,8 +12,6 @@ import type { BankCard } from "../../api/queries";
|
||||
|
||||
import { BankCardForm } from "./BankCardForm";
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const card: BankCard
|
||||
= {
|
||||
archivedAt: null,
|
||||
@@ -28,7 +26,6 @@ const card: BankCard
|
||||
version: 1,
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
function setup(value: BankCard) {
|
||||
configureAuthClient({
|
||||
accessToken: () => "test",
|
||||
@@ -75,7 +72,7 @@ describe("BankCardForm", () => {
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Сохранить" }));
|
||||
await waitFor(() => expect(close).toHaveBeenCalled());
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
expect(body).toEqual(
|
||||
{
|
||||
bankName: null,
|
||||
@@ -84,7 +81,6 @@ describe("BankCardForm", () => {
|
||||
paymentSystem: null,
|
||||
version: 1,
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
);
|
||||
});
|
||||
it("loads and saves the selected payment system", async () => {
|
||||
|
||||
@@ -57,7 +57,6 @@ const BankCardFields: FC<BankCardFieldsProps> = (props) => {
|
||||
placeholder={t("bank.paymentSystemUnknown")}
|
||||
value={
|
||||
paymentSystemField.value
|
||||
// eslint-disable-next-line unicorn/no-null -- Mantine represents an empty controlled paymentSystemField as null.
|
||||
|| null
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -49,16 +49,13 @@ function useBankCardFormVm(props: BankCardFormProps) {
|
||||
const metadata = {
|
||||
bankName:
|
||||
values.bankName
|
||||
// eslint-disable-next-line unicorn/no-null -- PATCH uses null to clear these optional card fields.
|
||||
|| null,
|
||||
lastFour:
|
||||
values.lastFour
|
||||
// eslint-disable-next-line unicorn/no-null -- PATCH uses null to clear these optional card fields.
|
||||
|| null,
|
||||
name: values.name,
|
||||
paymentSystem:
|
||||
values.paymentSystem
|
||||
// eslint-disable-next-line unicorn/no-null -- PATCH uses null to clear these optional card fields.
|
||||
|| null,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { ComponentProps } from "react";
|
||||
import type { Control } from "react-hook-form";
|
||||
|
||||
import { DateInput } from "@mantine/dates";
|
||||
import { useController } from "react-hook-form";
|
||||
|
||||
import { bankDateInputValue } from "../../model/form-validation";
|
||||
import { type BankCardValues, bankDateSchema } from "../../model/form.schema";
|
||||
|
||||
function useOpeningDateField(control: Control<BankCardValues>) {
|
||||
const { field: openingDateField } = useController({
|
||||
control,
|
||||
name: "openingDate",
|
||||
});
|
||||
|
||||
const dateParser: ComponentProps<typeof DateInput>["dateParser"] = value =>
|
||||
bankDateSchema.safeParse(bankDateInputValue(value)).success
|
||||
? bankDateInputValue(value)
|
||||
: null;
|
||||
|
||||
const onOpeningDateBlur: ComponentProps<typeof DateInput>["onBlur"] = (
|
||||
event,
|
||||
) => {
|
||||
openingDateField.onChange(bankDateInputValue(event.currentTarget.value));
|
||||
openingDateField.onBlur();
|
||||
};
|
||||
|
||||
const onOpeningDateChange = (value: null | string) =>
|
||||
openingDateField.onChange(value ?? "");
|
||||
|
||||
const onOpeningDateInput: ComponentProps<typeof DateInput>["onInput"] = event =>
|
||||
openingDateField.onChange(bankDateInputValue(event.currentTarget.value));
|
||||
|
||||
const openingDateValue = bankDateSchema.safeParse(openingDateField.value)
|
||||
.success
|
||||
? openingDateField.value
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
dateParser,
|
||||
onOpeningDateBlur,
|
||||
onOpeningDateChange,
|
||||
onOpeningDateInput,
|
||||
openingDateField,
|
||||
openingDateValue,
|
||||
};
|
||||
}
|
||||
|
||||
export { useOpeningDateField };
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Control } from "react-hook-form";
|
||||
|
||||
import { useController } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { BankCardValues } from "../../model/form.schema";
|
||||
|
||||
function usePaymentSystemField(control: Control<BankCardValues>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { field: paymentSystemField } = useController({
|
||||
control,
|
||||
name: "paymentSystem",
|
||||
});
|
||||
|
||||
const paymentSystems = ["mir", "visa", "mastercard"].map(value => ({
|
||||
label: t(`bank.paymentSystems.${value}`),
|
||||
value,
|
||||
}));
|
||||
|
||||
const onPaymentSystemChange = (value: null | string) =>
|
||||
paymentSystemField.onChange(value ?? "");
|
||||
|
||||
return { onPaymentSystemChange, paymentSystemField, paymentSystems };
|
||||
}
|
||||
|
||||
export { usePaymentSystemField };
|
||||
@@ -10,7 +10,9 @@ const BankOperationIcon = (props: {
|
||||
categoryName?: null | string;
|
||||
kind: "expense" | "refund" | "topup" | "transfer";
|
||||
}) => (
|
||||
<OperationIcon {...bankOperationAppearance(props.kind, props.categoryName)} />
|
||||
<OperationIcon
|
||||
{...bankOperationAppearance(props.kind, props.categoryName)}
|
||||
/>
|
||||
);
|
||||
|
||||
const meta = {
|
||||
|
||||
@@ -8,7 +8,9 @@ const BankOperationIcon = (props: {
|
||||
categoryName?: null | string;
|
||||
kind: "expense" | "refund" | "topup" | "transfer";
|
||||
}) => (
|
||||
<OperationIcon {...bankOperationAppearance(props.kind, props.categoryName)} />
|
||||
<OperationIcon
|
||||
{...bankOperationAppearance(props.kind, props.categoryName)}
|
||||
/>
|
||||
);
|
||||
|
||||
it("matches normalized category names, falls back after renaming and distinguishes operation kinds", () => {
|
||||
@@ -28,7 +30,6 @@ it("matches normalized category names, falls back after renaming and distinguish
|
||||
for (const categoryName of [
|
||||
"Моя категория",
|
||||
"constructor",
|
||||
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
null,
|
||||
]) {
|
||||
rerender(<BankOperationIcon categoryName={categoryName} kind="expense" />);
|
||||
|
||||
@@ -4,8 +4,6 @@ import { BankSummaryView } from "./BankSummary.view";
|
||||
|
||||
const money = (value: string) => ({ currency: "RUB", value });
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const meta
|
||||
= {
|
||||
args: {
|
||||
@@ -62,14 +60,14 @@ const meta
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
title: "Product/BankCards/Summary",
|
||||
} satisfies /* eslint-enable unicorn/no-null */ Meta<typeof BankSummaryView>;
|
||||
} satisfies Meta<typeof BankSummaryView>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Loaded: Story = { name: "Расходы по категориям" };
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const RefundMonth: Story
|
||||
= {
|
||||
args: {
|
||||
@@ -93,8 +91,7 @@ export const RefundMonth: Story
|
||||
},
|
||||
name: "Возврат покупки прошлого месяца",
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const Empty: Story
|
||||
= {
|
||||
args: {
|
||||
@@ -109,7 +106,7 @@ export const Empty: Story
|
||||
},
|
||||
name: "Известные нули",
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
export const Loading: Story = {
|
||||
args: { data: undefined, loading: true },
|
||||
name: "Начальная загрузка",
|
||||
@@ -122,7 +119,7 @@ export const CachedError: Story = {
|
||||
args: { error: true },
|
||||
name: "Сохранённые суммы при ошибке обновления",
|
||||
};
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const LongAmounts: Story
|
||||
= {
|
||||
args: {
|
||||
@@ -145,4 +142,3 @@ export const LongAmounts: Story
|
||||
},
|
||||
name: "Точный итог длиннее предела отдельного ввода",
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
@@ -109,9 +109,7 @@ const BankSummaryView: FC<BankSummaryViewProps> = (props) => {
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{props.data && props.fetching && (
|
||||
<Text role="status">{t("bank.refreshing")}</Text>
|
||||
)}
|
||||
{props.data && props.fetching && <Text role="status">{t("bank.refreshing")}</Text>}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -88,9 +88,7 @@ const CategoryManager: FC<CategoryManagerProps> = (props) => {
|
||||
|
||||
<Text>{category.name}</Text>
|
||||
|
||||
{category.isDefault && (
|
||||
<Badge>{t("bank.defaultCategory")}</Badge>
|
||||
)}
|
||||
{category.isDefault && <Badge>{t("bank.defaultCategory")}</Badge>}
|
||||
</Group>
|
||||
|
||||
<Button onClick={category.onRename} variant="subtle">
|
||||
@@ -100,9 +98,7 @@ const CategoryManager: FC<CategoryManagerProps> = (props) => {
|
||||
);
|
||||
})}
|
||||
|
||||
{categories.isLoading && (
|
||||
<Text role="status">{t("bank.loading")}</Text>
|
||||
)}
|
||||
{categories.isLoading && <Text role="status">{t("bank.loading")}</Text>}
|
||||
|
||||
{categories.isError && (
|
||||
<BankFeedback
|
||||
|
||||
@@ -34,7 +34,7 @@ const LedgerEntryRow: FC<LedgerEntryRowProps> = (props) => {
|
||||
t("ledger.occurredAt", { date: formatSnapshotTime(operation.occurredAt) }),
|
||||
instrument,
|
||||
props.withDetails
|
||||
? null // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
? null
|
||||
: operation.account?.name,
|
||||
]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -45,7 +45,6 @@ function brokerageAccountCards(
|
||||
return {
|
||||
account,
|
||||
positionCounts:
|
||||
// eslint-disable-next-line unicorn/no-null -- The existing formatter contract uses null for unavailable data.
|
||||
null,
|
||||
source,
|
||||
summary: { state: "unavailable" },
|
||||
|
||||
-6
@@ -64,8 +64,6 @@ export const Available: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const Unavailable: Story
|
||||
= {
|
||||
args: {
|
||||
@@ -83,7 +81,6 @@ export const Unavailable: Story
|
||||
},
|
||||
},
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
@@ -193,8 +190,6 @@ export const LargeValues: Story = {
|
||||
name: "Длинные суммы",
|
||||
};
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const IndependentFreshness: Story
|
||||
= {
|
||||
args: {
|
||||
@@ -236,4 +231,3 @@ export const IndependentFreshness: Story
|
||||
},
|
||||
name: "Независимая свежесть и ошибка сводок",
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
+4
-5
@@ -29,7 +29,7 @@ describe("BrokerageAccountList", () => {
|
||||
],
|
||||
},
|
||||
type: "BROKERAGE",
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
},
|
||||
{
|
||||
id: "account-2",
|
||||
@@ -38,10 +38,10 @@ describe("BrokerageAccountList", () => {
|
||||
portfolioSummary: null,
|
||||
type: "BROKERAGE",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */ {
|
||||
{
|
||||
id: "account-3",
|
||||
name: "Пустой",
|
||||
openedOn: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
openedOn: null,
|
||||
portfolioSummary: {
|
||||
bondPositionCount: 0,
|
||||
stockPositionCount: 2,
|
||||
@@ -84,7 +84,7 @@ describe("BrokerageAccountList", () => {
|
||||
it("shows portfolio summary freshness independently for each account", () => {
|
||||
const accounts = brokerageAccountCards(
|
||||
["fresh", "stale"].map(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
id =>
|
||||
({
|
||||
id,
|
||||
@@ -97,7 +97,6 @@ it("shows portfolio summary freshness independently for each account", () => {
|
||||
},
|
||||
type: "BROKERAGE" as const,
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
[
|
||||
resource(null, { freshness: "fresh", resourceKey: "portfolio/fresh" })
|
||||
|
||||
@@ -105,13 +105,9 @@ const BrokerageAccountList: FC<BrokerageAccountListProps> = (props) => {
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{source?.sync.state === "failed" && (
|
||||
<Text size="xs">{t("accounts.refreshError")}</Text>
|
||||
)}
|
||||
{source?.sync.state === "failed" && <Text size="xs">{t("accounts.refreshError")}</Text>}
|
||||
|
||||
{source?.freshness === "stale" && (
|
||||
<Text size="xs">{t("investment.stale")}</Text>
|
||||
)}
|
||||
{source?.freshness === "stale" && <Text size="xs">{t("investment.stale")}</Text>}
|
||||
</Stack>
|
||||
|
||||
{!!positionCounts && (
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("useBrokerageEvents", () => {
|
||||
server.use(
|
||||
http.get("*/api/v1/brokerage-accounts/account-1/events", () =>
|
||||
HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{
|
||||
accountId: "account-1",
|
||||
@@ -56,7 +56,6 @@ describe("useBrokerageEvents", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -67,7 +66,7 @@ describe("useBrokerageEvents", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
expect(result.current.data?.events).toEqual([
|
||||
{
|
||||
date: "2026-09-01",
|
||||
@@ -80,7 +79,7 @@ describe("useBrokerageEvents", () => {
|
||||
ticker: "SBER",
|
||||
type: "dividend",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ const BrokerageEventCard: FC<BrokerageEventCardProps> = (props) => {
|
||||
date,
|
||||
event.dateKind === "registry"
|
||||
? t("events.dateKind.registry")
|
||||
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null,
|
||||
event.isin,
|
||||
event.account?.name,
|
||||
]
|
||||
|
||||
+1
-9
@@ -6,8 +6,6 @@ import type { BrokerageEvent } from "../../api/useBrokerageEvents";
|
||||
|
||||
import { BrokerageEventsPreview } from "./BrokerageEventsPreview";
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const syntheticEvent
|
||||
= {
|
||||
date: "2026-10-15",
|
||||
@@ -20,7 +18,7 @@ const syntheticEvent
|
||||
projectedAmount: { currency: "RUB", value: "850.50" },
|
||||
ticker: "DEMO",
|
||||
type: "coupon",
|
||||
} satisfies /* eslint-enable unicorn/no-null */ BrokerageEvent;
|
||||
} satisfies BrokerageEvent;
|
||||
|
||||
const eventsPath = "/accounts/storybook-account/events";
|
||||
|
||||
@@ -121,8 +119,6 @@ export const Empty: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const MissingAmount: Story
|
||||
= {
|
||||
args: {
|
||||
@@ -142,9 +138,6 @@ export const MissingAmount: Story
|
||||
},
|
||||
},
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const LongEvent: Story
|
||||
= {
|
||||
@@ -165,4 +158,3 @@ export const LongEvent: Story
|
||||
},
|
||||
name: "Длинное название на узком экране",
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
@@ -46,31 +46,21 @@ const BrokerageEventsPreview: FC<BrokerageEventsPreviewProps> = (props) => {
|
||||
|
||||
{!!events.stale && <Text size="xs">{t("investment.stale")}</Text>}
|
||||
|
||||
{!!events.refreshError && (
|
||||
<Text role="status">{t("accounts.refreshError")}</Text>
|
||||
)}
|
||||
{!!events.refreshError && <Text role="status">{t("accounts.refreshError")}</Text>}
|
||||
</>
|
||||
)}
|
||||
|
||||
{events.state === "loading" && (
|
||||
<LoadingSkeleton label={t("events.recentLoading")}>
|
||||
{["first", "second", "third"].map(placeholder => (
|
||||
<Skeleton height={48} key={placeholder} radius="sm" />
|
||||
))}
|
||||
{["first", "second", "third"].map(placeholder => <Skeleton height={48} key={placeholder} radius="sm" />)}
|
||||
</LoadingSkeleton>
|
||||
)}
|
||||
|
||||
{events.state === "error" && (
|
||||
<Alert color="red">{t("events.recentError")}</Alert>
|
||||
)}
|
||||
{events.state === "error" && <Alert color="red">{t("events.recentError")}</Alert>}
|
||||
|
||||
{events.state === "empty" && (
|
||||
<Text c="dimmed">{t("events.recentEmpty")}</Text>
|
||||
)}
|
||||
{events.state === "empty" && <Text c="dimmed">{t("events.recentEmpty")}</Text>}
|
||||
|
||||
{events.state === "partial" && (
|
||||
<Text role="status">{t("investment.partial")}</Text>
|
||||
)}
|
||||
{events.state === "partial" && <Text role="status">{t("investment.partial")}</Text>}
|
||||
|
||||
{(events.state === "loaded" || events.state === "partial")
|
||||
&& events.items.map(event => (
|
||||
|
||||
@@ -56,7 +56,7 @@ describe("persistent resource query lifecycle", () => {
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
await act(() => result.current.observe("new"));
|
||||
await waitFor(() => expect(result.current.meta?.stateVersion).toBe("new"));
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
|
||||
|
||||
expect(modes).toEqual([null, "observe", "observe"]);
|
||||
expect(queryClient.getQueryCache().getAll()).toHaveLength(1);
|
||||
expect(
|
||||
@@ -89,14 +89,13 @@ describe("persistent resource query lifecycle", () => {
|
||||
expect(result.current.status).toBe("pending");
|
||||
expect(result.current.isPending).toBe(true);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
expect(result.current.transport).toEqual(
|
||||
{
|
||||
error: null,
|
||||
fetchStatus: "idle",
|
||||
status: "success",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
);
|
||||
expect(result.current.meta?.availability).toBe("missing");
|
||||
});
|
||||
|
||||
@@ -9,15 +9,12 @@ function formatShare(
|
||||
denominator: Money | null,
|
||||
): null | string {
|
||||
if (!numerator || !denominator)
|
||||
// eslint-disable-next-line unicorn/no-null -- The existing formatter contract uses null for unavailable data.
|
||||
return null;
|
||||
const total = Number(denominator.value);
|
||||
if (!Number.isFinite(total) || total <= 0)
|
||||
// eslint-disable-next-line unicorn/no-null -- The existing formatter contract uses null for unavailable data.
|
||||
return null;
|
||||
const percent = (Number(numerator.value) / total) * PERCENT_SCALE;
|
||||
if (!Number.isFinite(percent))
|
||||
// eslint-disable-next-line unicorn/no-null -- The existing formatter contract uses null for unavailable data.
|
||||
return null;
|
||||
|
||||
const rounded = new Intl.NumberFormat("ru-RU", {
|
||||
|
||||
@@ -30,7 +30,7 @@ function createAuthWrapper(
|
||||
describe("AuthProvider", () => {
|
||||
it("authorizes requests after restoring the initial session", async () => {
|
||||
let authorization: null | string
|
||||
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
|
||||
= null;
|
||||
|
||||
server.use(
|
||||
@@ -234,12 +234,12 @@ describe("AuthProvider", () => {
|
||||
});
|
||||
expect(
|
||||
result.current.user?.id
|
||||
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
|
||||
?? null,
|
||||
).toBe(
|
||||
loginAfterLogout
|
||||
? "b"
|
||||
: null, // eslint-disable-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
: null,
|
||||
);
|
||||
expect(order).toEqual([
|
||||
"refresh-start",
|
||||
|
||||
@@ -30,9 +30,7 @@ const InstrumentPosition: FC<InstrumentPositionProps> = (props) => {
|
||||
<InstrumentSection state={vm.positionState} title={t("stock.position")}>
|
||||
{!!vm.position && (
|
||||
<Stack gap="md">
|
||||
{!!vm.position.isStale && (
|
||||
<Alert color="orange">{t("portfolio.staleWarning")}</Alert>
|
||||
)}
|
||||
{!!vm.position.isStale && <Alert color="orange">{t("portfolio.staleWarning")}</Alert>}
|
||||
|
||||
<div className={`${classes.metrics} ${classes.holding}`}>
|
||||
<div>
|
||||
@@ -103,9 +101,7 @@ const InstrumentPosition: FC<InstrumentPositionProps> = (props) => {
|
||||
{account.account.name}
|
||||
</Anchor>
|
||||
)
|
||||
: (
|
||||
<Text>{t("stock.portfolio")}</Text>
|
||||
)}
|
||||
: <Text>{t("stock.portfolio")}</Text>}
|
||||
|
||||
<div>
|
||||
<Text c="dimmed" size="xs">
|
||||
|
||||
@@ -82,9 +82,7 @@ const InstrumentSection: FC<InstrumentSectionProps> = (props) => {
|
||||
|
||||
{!!props.state.stale && <Text size="xs">{t("investment.stale")}</Text>}
|
||||
|
||||
{!!props.state.partial && (
|
||||
<Text role="status">{t("investment.partial")}</Text>
|
||||
)}
|
||||
{!!props.state.partial && <Text role="status">{t("investment.partial")}</Text>}
|
||||
|
||||
{!!props.state.lastSuccessfulAt && (
|
||||
<Text c="dimmed" size="xs">
|
||||
|
||||
@@ -135,12 +135,11 @@ describe("page-owned screen synchronization", () => {
|
||||
expect(modes).toEqual(
|
||||
cached
|
||||
? [
|
||||
null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
null,
|
||||
null,
|
||||
"observe",
|
||||
]
|
||||
: [null, "observe"], // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
: [null, "observe"],
|
||||
);
|
||||
expect(posts).toBe(0);
|
||||
expect(result.current.disabled.data).toBeUndefined();
|
||||
@@ -316,7 +315,7 @@ describe("page-owned screen synchronization", () => {
|
||||
|
||||
await waitFor(() => expect(result.current.accounts.isSuccess).toBe(true));
|
||||
expect(result.current.accounts.data).toEqual([]);
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
|
||||
|
||||
expect(modes).toEqual([null, "observe"]);
|
||||
expect(
|
||||
queryClient
|
||||
@@ -357,8 +356,6 @@ describe("page-owned screen synchronization", () => {
|
||||
http.get("*/api/v1/tbank-sync/:id", () => {
|
||||
polls++;
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
return HttpResponse.json(
|
||||
{
|
||||
errorCode: null,
|
||||
@@ -369,7 +366,6 @@ describe("page-owned screen synchronization", () => {
|
||||
scope: { screen: "accounts" },
|
||||
state: "succeeded",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -397,14 +393,14 @@ describe("page-owned screen synchronization", () => {
|
||||
expect(result.current.accounts.meta?.stateVersion).toBe("2"),
|
||||
);
|
||||
expect(bodies).toEqual([{ screen: "accounts" }]);
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
|
||||
|
||||
expect(modes).toEqual([null, "observe"]);
|
||||
vi.useFakeTimers();
|
||||
|
||||
try {
|
||||
await act(() => vi.advanceTimersByTimeAsync(120_000));
|
||||
expect(polls).toBe(1);
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
|
||||
|
||||
expect(modes).toEqual([null, "observe"]);
|
||||
unmount();
|
||||
await vi.advanceTimersByTimeAsync(120_000);
|
||||
@@ -429,10 +425,10 @@ describe("page-owned screen synchronization", () => {
|
||||
resource(
|
||||
isCompleted
|
||||
? []
|
||||
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable API fixture.
|
||||
: null,
|
||||
{
|
||||
stateVersion: isCompleted ? "covered" : "initial",
|
||||
sync: { errorCode: null, id: "history", state: "running" }, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
sync: { errorCode: null, id: "history", state: "running" },
|
||||
},
|
||||
);
|
||||
|
||||
@@ -445,8 +441,6 @@ describe("page-owned screen synchronization", () => {
|
||||
http.get("*/api/v1/tbank-sync/:id", () => {
|
||||
polls++;
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
return HttpResponse.json(
|
||||
{
|
||||
errorCode: null,
|
||||
@@ -461,7 +455,6 @@ describe("page-owned screen synchronization", () => {
|
||||
scope: null,
|
||||
state: "running",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -493,7 +486,7 @@ describe("page-owned screen synchronization", () => {
|
||||
await act(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 1100));
|
||||
});
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
|
||||
|
||||
expect(modes).toEqual([null, "observe"]);
|
||||
if (boundary === "leave") unmount();
|
||||
else act(() => configureAuthClient());
|
||||
@@ -515,12 +508,11 @@ describe("page-owned screen synchronization", () => {
|
||||
http.get("*/api/v1/brokerage-accounts", () =>
|
||||
HttpResponse.json(
|
||||
resource(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
[],
|
||||
{
|
||||
sync: { errorCode: null, id: "revoked", state: "running" },
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -550,7 +542,7 @@ describe("page-owned screen synchronization", () => {
|
||||
const modes: (null | string)[] = [];
|
||||
|
||||
const dependency = resource(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
[],
|
||||
{
|
||||
resourceKey: "operations",
|
||||
@@ -561,7 +553,6 @@ describe("page-owned screen synchronization", () => {
|
||||
state: isObserved ? "idle" : "running",
|
||||
},
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
).meta;
|
||||
|
||||
server.use(
|
||||
@@ -574,7 +565,7 @@ describe("page-owned screen synchronization", () => {
|
||||
resource(
|
||||
isObserved
|
||||
? []
|
||||
: null, // eslint-disable-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
: null,
|
||||
{
|
||||
dependencies: [
|
||||
{
|
||||
@@ -588,7 +579,7 @@ describe("page-owned screen synchronization", () => {
|
||||
);
|
||||
}),
|
||||
http.get("*/api/v1/tbank-sync/:id", () =>
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
HttpResponse.json(
|
||||
{
|
||||
errorCode: null,
|
||||
@@ -599,7 +590,6 @@ describe("page-owned screen synchronization", () => {
|
||||
scope: null,
|
||||
state: "succeeded",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -615,7 +605,7 @@ describe("page-owned screen synchronization", () => {
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.data).toEqual([]));
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
|
||||
|
||||
expect(modes).toEqual([null, "observe"]);
|
||||
});
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ function useScreenSync(scope: InvestmentScreen, resources: ScreenResource[]) {
|
||||
finally {
|
||||
if (requestInFlightRef.current === scopeKey)
|
||||
requestInFlightRef.current
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
|
||||
= null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -48,8 +48,6 @@ describe("sync connection and session boundaries", () => {
|
||||
started += 1;
|
||||
await blocked;
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
return HttpResponse.json(
|
||||
{
|
||||
...accepted,
|
||||
@@ -59,7 +57,6 @@ describe("sync connection and session boundaries", () => {
|
||||
resources: [],
|
||||
state: "succeeded",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -103,13 +100,12 @@ describe("sync connection and session boundaries", () => {
|
||||
|
||||
if (boundary === "connection")
|
||||
commitTbankConnectionBoundary(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
queryClient,
|
||||
{
|
||||
connected: false,
|
||||
connectedAt: null,
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
);
|
||||
else {
|
||||
generation += 1;
|
||||
|
||||
+3
-4
@@ -77,7 +77,7 @@ function renderSection(client: QueryClient) {
|
||||
|
||||
function seededClient() {
|
||||
const client = createTestQueryClient();
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
client.setQueryData(brokerageAccountsQueryKey, [
|
||||
{
|
||||
id: "account-1",
|
||||
@@ -85,7 +85,7 @@ function seededClient() {
|
||||
openedOn: null,
|
||||
type: "BROKERAGE",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
]);
|
||||
client.setQueryData(portfolioQueryKey, { source: "previous-connection" });
|
||||
client.setQueryData(stockQueryKey, { source: "previous-connection" });
|
||||
@@ -157,10 +157,9 @@ describe("TbankConnectionSection", () => {
|
||||
|
||||
return reads === 1
|
||||
? HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 })
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
: HttpResponse.json(
|
||||
{ connected: false, connectedAt: null },
|
||||
/* eslint-enable unicorn/no-null */
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -12,12 +12,8 @@ const TbankConnectionSection: FC = () => {
|
||||
if (viewModel.state === "loading") return <TbankConnectionLoading />;
|
||||
|
||||
return viewModel.state === "error"
|
||||
? (
|
||||
<TbankConnectionError retry={viewModel.retry} />
|
||||
)
|
||||
: (
|
||||
<TbankConnectionSectionLoaded {...viewModel} />
|
||||
);
|
||||
? <TbankConnectionError retry={viewModel.retry} />
|
||||
: <TbankConnectionSectionLoaded {...viewModel} />;
|
||||
};
|
||||
|
||||
export { TbankConnectionSection };
|
||||
|
||||
+1
-3
@@ -129,9 +129,7 @@ const TbankConnectionSectionLoaded: FC<TbankConnectionSectionLoadedProps> = (
|
||||
<Stack>
|
||||
<Text>{t("tbank.deleteText")}</Text>
|
||||
|
||||
{remove.isError && (
|
||||
<Alert color="red">{t("tbank.requestError")}</Alert>
|
||||
)}
|
||||
{remove.isError && <Alert color="red">{t("tbank.requestError")}</Alert>}
|
||||
|
||||
<Button
|
||||
color="red"
|
||||
|
||||
-1
@@ -59,7 +59,6 @@ function useTbankConnectionSectionLoadedVm(
|
||||
commitTbankConnectionBoundary(queryClient, {
|
||||
connected: false,
|
||||
connectedAt:
|
||||
// eslint-disable-next-line unicorn/no-null -- The disconnected API snapshot requires a null connection timestamp.
|
||||
null,
|
||||
});
|
||||
setIsDeleteOpened(false);
|
||||
|
||||
@@ -100,7 +100,6 @@ describe("Upcoming event date filters", () => {
|
||||
|
||||
return HttpResponse.json(
|
||||
resource(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
null,
|
||||
{
|
||||
sync: {
|
||||
@@ -109,7 +108,6 @@ describe("Upcoming event date filters", () => {
|
||||
state: "failed",
|
||||
},
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -37,7 +37,7 @@ function renderPage() {
|
||||
describe("AccountEventsPage", () => {
|
||||
beforeEach(() => {
|
||||
refresh.mockClear();
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
query.mockReturnValue(
|
||||
{
|
||||
data: {
|
||||
@@ -52,7 +52,7 @@ describe("AccountEventsPage", () => {
|
||||
sync: { errorCode: null, id: null, state: "idle" },
|
||||
},
|
||||
refetch: vi.fn(),
|
||||
} as /* eslint-enable unicorn/no-null */ never,
|
||||
} as never,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -73,7 +73,6 @@ describe("AccountEventsPage", () => {
|
||||
});
|
||||
|
||||
it("keeps the loading fallback when no result or account is available", () => {
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
query.mockReturnValue(
|
||||
{
|
||||
data: undefined,
|
||||
@@ -84,7 +83,7 @@ describe("AccountEventsPage", () => {
|
||||
sync: { errorCode: null, id: null, state: "idle" },
|
||||
},
|
||||
refetch: vi.fn(),
|
||||
} as /* eslint-enable unicorn/no-null */ never,
|
||||
} as never,
|
||||
);
|
||||
renderPage();
|
||||
expect(query).toHaveBeenCalledWith("", {});
|
||||
@@ -95,7 +94,6 @@ describe("AccountEventsPage", () => {
|
||||
});
|
||||
|
||||
it("renders events and marks an offer without an amount", () => {
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
query.mockReturnValue(
|
||||
{
|
||||
data: {
|
||||
@@ -135,7 +133,7 @@ describe("AccountEventsPage", () => {
|
||||
sync: { errorCode: null, id: null, state: "idle" },
|
||||
},
|
||||
refetch: vi.fn(),
|
||||
} as /* eslint-enable unicorn/no-null */ never,
|
||||
} as never,
|
||||
);
|
||||
renderPage();
|
||||
expect(screen.getByText("1 250 ₽")).toBeInTheDocument();
|
||||
@@ -183,7 +181,6 @@ describe("AccountEventsPage", () => {
|
||||
});
|
||||
|
||||
it("shows a structural busy state before events are available", () => {
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
query.mockReturnValue(
|
||||
{
|
||||
data: undefined,
|
||||
@@ -194,7 +191,7 @@ describe("AccountEventsPage", () => {
|
||||
sync: { errorCode: null, id: null, state: "idle" },
|
||||
},
|
||||
refetch: vi.fn(),
|
||||
} as /* eslint-enable unicorn/no-null */ never,
|
||||
} as never,
|
||||
);
|
||||
|
||||
renderPage();
|
||||
|
||||
@@ -12,12 +12,8 @@ const AccountEventsPage: FC<AccountEventsPageProps> = (props) => {
|
||||
if (viewModel.state === "loading") return <AccountEventsPageLoading />;
|
||||
|
||||
return viewModel.state === "error"
|
||||
? (
|
||||
<AccountEventsPageError {...viewModel} />
|
||||
)
|
||||
: (
|
||||
<AccountEventsPageLoaded {...viewModel} />
|
||||
);
|
||||
? <AccountEventsPageError {...viewModel} />
|
||||
: <AccountEventsPageLoaded {...viewModel} />;
|
||||
};
|
||||
|
||||
export { AccountEventsPage };
|
||||
|
||||
@@ -41,11 +41,11 @@ function useAccountEventsPageViewModel(
|
||||
|
||||
const appliedFrom = isValidUrlPeriod
|
||||
? urlFrom
|
||||
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null;
|
||||
|
||||
const appliedTo = isValidUrlPeriod
|
||||
? urlTo
|
||||
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null;
|
||||
|
||||
const [draftFrom, setDraftFrom] = useState<null | string>(appliedFrom);
|
||||
const [draftTo, setDraftTo] = useState<null | string>(appliedTo);
|
||||
|
||||
+1
-3
@@ -6,9 +6,7 @@ import { useTranslation } from "react-i18next";
|
||||
const AccountEventsPageEmpty: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<StateView text={t("events.emptyText")} title={t("events.emptyTitle")} />
|
||||
);
|
||||
return <StateView text={t("events.emptyText")} title={t("events.emptyTitle")} />;
|
||||
};
|
||||
|
||||
export { AccountEventsPageEmpty };
|
||||
|
||||
+2
-6
@@ -74,13 +74,9 @@ const AccountEventsPageLoaded: FC<AccountEventsPageLoadedProps> = (props) => {
|
||||
{t("investment.refresh")}
|
||||
</Button>
|
||||
|
||||
{!!props.partial && (
|
||||
<Alert role="status">{t("investment.partial")}</Alert>
|
||||
)}
|
||||
{!!props.partial && <Alert role="status">{t("investment.partial")}</Alert>}
|
||||
|
||||
{!!props.refreshing && (
|
||||
<Progress animated aria-label={t("accounts.refreshing")} value={100} />
|
||||
)}
|
||||
{!!props.refreshing && <Progress animated aria-label={t("accounts.refreshing")} value={100} />}
|
||||
|
||||
{!!props.refreshError && (
|
||||
<Alert aria-live="polite" color="yellow" role="status">
|
||||
|
||||
+1
-3
@@ -27,9 +27,7 @@ const AccountEventsPageLoading: FC = () => {
|
||||
<Skeleton height={36} width={108} />
|
||||
</Group>
|
||||
|
||||
{skeletonRowIds.map(id => (
|
||||
<Skeleton height={82} key={id} radius="md" />
|
||||
))}
|
||||
{skeletonRowIds.map(id => <Skeleton height={82} key={id} radius="md" />)}
|
||||
</LoadingSkeleton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -37,14 +37,13 @@ function moscowDate(value: Date): string {
|
||||
|
||||
function parseDraftDate(value: string): null | string {
|
||||
if (!/^\d{2}\.\d{2}\.\d{4}$/.test(value))
|
||||
// eslint-disable-next-line unicorn/no-null -- Mantine date fields use null for an incomplete or invalid date.
|
||||
return null;
|
||||
const [day, month, year] = value.split(".", DATE_PART_COUNT);
|
||||
const date = `${year}-${month}-${day}`;
|
||||
|
||||
return isDate(date)
|
||||
? date
|
||||
: null; // eslint-disable-line unicorn/no-null -- Mantine date fields use null for an incomplete or invalid date.
|
||||
: null;
|
||||
}
|
||||
|
||||
export {
|
||||
|
||||
@@ -38,8 +38,6 @@ import {
|
||||
} from "./AccountIncomePage.vm";
|
||||
import { IncomePayments } from "./components/IncomePayments.view";
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const analytics
|
||||
= {
|
||||
accountId: "account-1",
|
||||
@@ -81,7 +79,7 @@ const analytics
|
||||
},
|
||||
totalsByCurrency: [summary("RUB", "10", "2")],
|
||||
yearComparison: null,
|
||||
} satisfies /* eslint-enable unicorn/no-null */ IncomeAnalytics;
|
||||
} satisfies IncomeAnalytics;
|
||||
|
||||
function comparison(currency: string) {
|
||||
return {
|
||||
@@ -93,28 +91,24 @@ function comparison(currency: string) {
|
||||
}
|
||||
|
||||
function entry(): IncomeAnalytics["entries"][number] {
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
return (
|
||||
{
|
||||
accruedInterest: null,
|
||||
category: "income",
|
||||
commission: null,
|
||||
description: "Купон",
|
||||
figi: "FIGI",
|
||||
id: "coupon-1",
|
||||
instrumentKey: "income-group-opaque-v2",
|
||||
instrumentType: "bond",
|
||||
instrumentUid: null,
|
||||
logoName: null,
|
||||
occurredAt: "2026-08-01T12:00:00Z",
|
||||
payment: money("RUB", "10"),
|
||||
price: null,
|
||||
quantity: null,
|
||||
ticker: "BOND",
|
||||
type: "OPERATION_TYPE_COUPON",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */
|
||||
);
|
||||
return {
|
||||
accruedInterest: null,
|
||||
category: "income",
|
||||
commission: null,
|
||||
description: "Купон",
|
||||
figi: "FIGI",
|
||||
id: "coupon-1",
|
||||
instrumentKey: "income-group-opaque-v2",
|
||||
instrumentType: "bond",
|
||||
instrumentUid: null,
|
||||
logoName: null,
|
||||
occurredAt: "2026-08-01T12:00:00Z",
|
||||
payment: money("RUB", "10"),
|
||||
price: null,
|
||||
quantity: null,
|
||||
ticker: "BOND",
|
||||
type: "OPERATION_TYPE_COUPON",
|
||||
};
|
||||
}
|
||||
|
||||
function money(currency: string, value: string) {
|
||||
@@ -211,7 +205,6 @@ describe("AccountIncomePage", () => {
|
||||
|
||||
return HttpResponse.json(
|
||||
resource(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
null,
|
||||
{
|
||||
sync: {
|
||||
@@ -220,7 +213,6 @@ describe("AccountIncomePage", () => {
|
||||
state: "failed",
|
||||
},
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -434,7 +426,6 @@ describe("AccountIncomePage", () => {
|
||||
});
|
||||
|
||||
it("renders the signed drill-down using opaque membership rather than instrument identity", async () => {
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
const response
|
||||
= {
|
||||
...analytics,
|
||||
@@ -464,7 +455,6 @@ describe("AccountIncomePage", () => {
|
||||
],
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
server.use(
|
||||
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
|
||||
HttpResponse.json(resource(response)),
|
||||
@@ -607,37 +597,31 @@ describe("income comparison display", () => {
|
||||
[
|
||||
"10",
|
||||
"0",
|
||||
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
null,
|
||||
],
|
||||
[
|
||||
"0",
|
||||
"0",
|
||||
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
null,
|
||||
],
|
||||
[
|
||||
"10",
|
||||
"-0.00",
|
||||
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
null,
|
||||
],
|
||||
[
|
||||
"10",
|
||||
"-100",
|
||||
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
null,
|
||||
],
|
||||
[
|
||||
"9".repeat(400),
|
||||
"1",
|
||||
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
null,
|
||||
],
|
||||
[
|
||||
"1",
|
||||
"9".repeat(400),
|
||||
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
null,
|
||||
],
|
||||
])("formats change %s against base %s", (difference, base, expected) => {
|
||||
@@ -656,7 +640,7 @@ describe("income comparison display", () => {
|
||||
const result = display?.comparison?.values[0];
|
||||
expect(
|
||||
result?.percentages.coupon?.replaceAll("\u{A0}", " ")
|
||||
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
|
||||
?? null,
|
||||
).toBe(expected);
|
||||
expect(result?.difference.coupon.value).toBe(difference);
|
||||
@@ -863,7 +847,7 @@ describe("income period and snapshot boundary", () => {
|
||||
),
|
||||
),
|
||||
http.get("*/api/v1/tbank-sync/:id", () =>
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
HttpResponse.json(
|
||||
{
|
||||
errorCode: null,
|
||||
@@ -874,7 +858,6 @@ describe("income period and snapshot boundary", () => {
|
||||
scope: null,
|
||||
state: "succeeded",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
),
|
||||
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
|
||||
@@ -998,8 +981,6 @@ describe("income period and snapshot boundary", () => {
|
||||
it("maps comparison notices by role regardless of dependency order or resource key", () => {
|
||||
const selected = { ...resource(analytics).meta, role: "selected" as const };
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const previous
|
||||
= {
|
||||
...resource(null).meta,
|
||||
@@ -1012,7 +993,6 @@ it("maps comparison notices by role regardless of dependency order or resource k
|
||||
},
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
const year = {
|
||||
...resource(null).meta,
|
||||
resourceKey: "opaque/previous-looking-key",
|
||||
@@ -1041,10 +1021,10 @@ it.each([false, true])(
|
||||
server.use(
|
||||
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
|
||||
HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{ ...analytics, previous: null, yearComparison: null },
|
||||
/* eslint-enable unicorn/no-null */ {
|
||||
{
|
||||
dependencies: legacy
|
||||
? [
|
||||
resource(null).meta,
|
||||
@@ -1073,22 +1053,21 @@ it.each([false, true])(
|
||||
|
||||
it("shows selected income before comparison coverage without inventing a zero baseline", async () => {
|
||||
const dependencies = (["year", "selected", "previous"] as const).map(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
role =>
|
||||
({
|
||||
...resource(role === "selected" ? analytics : null).meta,
|
||||
role,
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
);
|
||||
|
||||
server.use(
|
||||
http.get("*/api/v1/brokerage-accounts/account-1/income-analytics", () =>
|
||||
HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{ ...analytics, previous: null, yearComparison: null },
|
||||
/* eslint-enable unicorn/no-null */ { dependencies },
|
||||
{ dependencies },
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1116,7 +1095,7 @@ it("keeps complete previous comparison while year source failed without inventin
|
||||
{
|
||||
...resource(analytics).meta,
|
||||
role: "previous" as const,
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
},
|
||||
{
|
||||
...resource(null).meta,
|
||||
@@ -1127,7 +1106,7 @@ it("keeps complete previous comparison while year source failed without inventin
|
||||
state: "failed" as const,
|
||||
},
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */ {
|
||||
{
|
||||
...resource(analytics).meta,
|
||||
role: "selected" as const,
|
||||
},
|
||||
|
||||
@@ -12,12 +12,8 @@ const AccountIncomePage: FC<AccountIncomePageProps> = (props) => {
|
||||
if (viewModel.state === "loading") return <AccountIncomePageLoading />;
|
||||
|
||||
return viewModel.state === "error"
|
||||
? (
|
||||
<AccountIncomePageError {...viewModel} />
|
||||
)
|
||||
: (
|
||||
<AccountIncomePageLoaded {...viewModel} />
|
||||
);
|
||||
? <AccountIncomePageError {...viewModel} />
|
||||
: <AccountIncomePageLoaded {...viewModel} />;
|
||||
};
|
||||
|
||||
export { AccountIncomePage };
|
||||
|
||||
@@ -161,7 +161,6 @@ function comparisonDisplay(
|
||||
comparison: IncomeAnalytics["yearComparison"],
|
||||
): IncomeDisplayModel["comparison"] {
|
||||
if (!comparison)
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
return null;
|
||||
|
||||
return {
|
||||
@@ -220,11 +219,9 @@ function formatIncomeChange(
|
||||
// Display-only ratio; server-provided Money values remain exact decimal strings.
|
||||
const base = Number(compared);
|
||||
if (!Number.isFinite(base) || base <= 0)
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
return null;
|
||||
const ratio = Number(difference) / base;
|
||||
if (!Number.isFinite(ratio))
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
return null;
|
||||
|
||||
return new Intl.NumberFormat("ru-RU", {
|
||||
@@ -272,7 +269,7 @@ function toIncomeDisplayModel(
|
||||
|| instruments.has(instrument.key)
|
||||
|| !currencies.has(instrument.total.currency)
|
||||
)
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the existing nullable validation/ref contract.
|
||||
|
||||
return null;
|
||||
instruments.set(instrument.key, {
|
||||
currency: instrument.total.currency,
|
||||
@@ -286,11 +283,9 @@ function toIncomeDisplayModel(
|
||||
|
||||
for (const entry of analytics.entries) {
|
||||
if (typeof entry.instrumentKey !== "string" || !entry.instrumentKey.trim())
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the existing nullable validation/ref contract.
|
||||
return null;
|
||||
const instrument = instruments.get(entry.instrumentKey);
|
||||
if (!instrument || entry.payment?.currency !== instrument.currency)
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the existing nullable validation/ref contract.
|
||||
return null;
|
||||
const occurrenceKey = JSON.stringify([entry.instrumentKey, entry.id]);
|
||||
const occurrence = occurrences.get(occurrenceKey) ?? 0;
|
||||
@@ -309,7 +304,6 @@ function toIncomeDisplayModel(
|
||||
|
||||
for (const instrument of instruments.values()) {
|
||||
if (instrument.includedCount !== instrument.receiptCount)
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+2
-6
@@ -35,12 +35,8 @@ const AccountIncomePageLoadedSummary: FC<LoadedProps> = (props) => {
|
||||
}
|
||||
|
||||
return analytics.totalsByCurrency.length > 0
|
||||
? (
|
||||
<IncomeSummary totals={analytics.totalsByCurrency} />
|
||||
)
|
||||
: (
|
||||
<StateView text={t("income.emptyText")} title={t("income.emptyTitle")} />
|
||||
);
|
||||
? <IncomeSummary totals={analytics.totalsByCurrency} />
|
||||
: <StateView text={t("income.emptyText")} title={t("income.emptyTitle")} />;
|
||||
};
|
||||
|
||||
const AccountIncomePageLoaded: FC<LoadedProps> = (props) => {
|
||||
|
||||
@@ -34,9 +34,7 @@ const IncomeComparisons: FC<IncomeComparisonsProps> = (props) => {
|
||||
<Title order={2}>{props.title}</Title>
|
||||
|
||||
{props.comparison
|
||||
? (
|
||||
<Comparison {...props.comparison} title={props.title} />
|
||||
)
|
||||
? <Comparison {...props.comparison} title={props.title} />
|
||||
: (
|
||||
<Paper className={classes.comparisonNotice} p="sm" radius="md">
|
||||
<Text aria-live="polite" c="gray.7" role="status" size="sm">
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ const PeriodExample: FC<PeriodExampleProps> = (props) => {
|
||||
|
||||
const submitted = isSubmitSuccessful
|
||||
? methods.getValues()
|
||||
: null; // eslint-disable-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
: null;
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
|
||||
+6
-15
@@ -24,8 +24,6 @@ import { server } from "@/test/server";
|
||||
|
||||
import { AccountOperationsPage } from "./AccountOperationsPage";
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const operation: BrokerageOperation
|
||||
= {
|
||||
accruedInterest: null,
|
||||
@@ -44,7 +42,6 @@ const operation: BrokerageOperation
|
||||
ticker: "SBER",
|
||||
type: "OPERATION_TYPE_BUY",
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
const Navigation: FC = () => {
|
||||
const location = useLocation();
|
||||
@@ -322,10 +319,9 @@ describe("AccountOperationsPage", () => {
|
||||
new URL(request.url).searchParams.has("from")
|
||||
? HttpResponse.json({ code: "VALIDATION_ERROR" }, { status: 400 })
|
||||
: HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{ hasNext: false, items: [], nextCursor: null },
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -368,7 +364,7 @@ describe("AccountOperationsPage", () => {
|
||||
searches.push(url.search);
|
||||
|
||||
return HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{
|
||||
accountId: "account-1",
|
||||
@@ -377,7 +373,6 @@ describe("AccountOperationsPage", () => {
|
||||
nextCursor: url.searchParams.get("cursor") ? null : "next-1",
|
||||
observedAt: "2026-08-14T00:00:00Z",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -411,7 +406,7 @@ describe("AccountOperationsPage", () => {
|
||||
searches.push(url.search);
|
||||
|
||||
return HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{
|
||||
accountId: "account-1",
|
||||
@@ -420,7 +415,6 @@ describe("AccountOperationsPage", () => {
|
||||
nextCursor: null,
|
||||
observedAt: "2026-08-14T00:00:00Z",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -451,7 +445,7 @@ describe("AccountOperationsPage", () => {
|
||||
searches.push(url.search);
|
||||
|
||||
return HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{
|
||||
accountId: "account-1",
|
||||
@@ -460,7 +454,6 @@ describe("AccountOperationsPage", () => {
|
||||
nextCursor: null,
|
||||
observedAt: "2026-08-14T00:00:00Z",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -508,7 +501,6 @@ it("keeps UTC operation boundaries in GET and source sync requests", async () =>
|
||||
|
||||
return HttpResponse.json(
|
||||
resource(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
null,
|
||||
{
|
||||
sync: {
|
||||
@@ -517,7 +509,6 @@ it("keeps UTC operation boundaries in GET and source sync requests", async () =>
|
||||
state: "failed",
|
||||
},
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -559,7 +550,7 @@ it("does not label incomplete empty operations as a confirmed empty period", asy
|
||||
server.use(
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
|
||||
HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{
|
||||
accountId: "account-1",
|
||||
@@ -567,7 +558,7 @@ it("does not label incomplete empty operations as a confirmed empty period", asy
|
||||
items: [],
|
||||
nextCursor: null,
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */ { availability: "partial" },
|
||||
{ availability: "partial" },
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -15,12 +15,8 @@ const AccountOperationsPage: FC<AccountOperationsPageProps> = (props) => {
|
||||
}
|
||||
|
||||
return viewModel.state === "error"
|
||||
? (
|
||||
<AccountOperationsPageError {...viewModel} />
|
||||
)
|
||||
: (
|
||||
<AccountOperationsPageLoaded {...viewModel} />
|
||||
);
|
||||
? <AccountOperationsPageError {...viewModel} />
|
||||
: <AccountOperationsPageLoaded {...viewModel} />;
|
||||
};
|
||||
|
||||
export { AccountOperationsPage };
|
||||
|
||||
@@ -54,11 +54,11 @@ const useAccountOperationsPageVm = (
|
||||
|
||||
const appliedFrom = isValidUrlPeriod
|
||||
? urlFrom
|
||||
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null;
|
||||
|
||||
const appliedTo = isValidUrlPeriod
|
||||
? urlTo
|
||||
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null;
|
||||
|
||||
const appliedCategories = operationCategories.filter(category =>
|
||||
(searchParameters.get("categories") ?? "").split(",").includes(category),
|
||||
|
||||
+3
-9
@@ -50,9 +50,7 @@ const AccountOperationsPageLoadedOperations: FC<
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<StateView text={t("ledger.emptyText")} title={t("ledger.emptyTitle")} />
|
||||
);
|
||||
return <StateView text={t("ledger.emptyText")} title={t("ledger.emptyTitle")} />;
|
||||
};
|
||||
|
||||
const AccountOperationsPageLoaded: FC<AccountOperationsPageLoadedProps> = (
|
||||
@@ -91,15 +89,11 @@ const AccountOperationsPageLoaded: FC<AccountOperationsPageLoadedProps> = (
|
||||
{t("investment.refresh")}
|
||||
</Button>
|
||||
|
||||
{!!props.partial && (
|
||||
<Alert role="status">{t("investment.partial")}</Alert>
|
||||
)}
|
||||
{!!props.partial && <Alert role="status">{t("investment.partial")}</Alert>}
|
||||
|
||||
{!!props.historyLoading && <Alert>{t("investment.history")}</Alert>}
|
||||
|
||||
{!!props.refreshing && (
|
||||
<Progress animated aria-label={t("accounts.refreshing")} value={100} />
|
||||
)}
|
||||
{!!props.refreshing && <Progress animated aria-label={t("accounts.refreshing")} value={100} />}
|
||||
|
||||
{!!props.refreshError && (
|
||||
<Alert aria-live="polite" color="yellow" role="status">
|
||||
|
||||
+1
-3
@@ -28,9 +28,7 @@ const AccountOperationsPageLoading: FC = () => {
|
||||
</Group>
|
||||
|
||||
<Stack gap="xs">
|
||||
{skeletonRowIds.map(id => (
|
||||
<Skeleton height={64} key={id} radius="md" />
|
||||
))}
|
||||
{skeletonRowIds.map(id => <Skeleton height={64} key={id} radius="md" />)}
|
||||
</Stack>
|
||||
</LoadingSkeleton>
|
||||
);
|
||||
|
||||
@@ -67,8 +67,6 @@ function renderPage(ui: React.ReactNode) {
|
||||
);
|
||||
}
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const portfolio: Portfolio
|
||||
= {
|
||||
cashBalances: [{ currency: "USD", value: "0.5" }],
|
||||
@@ -103,7 +101,6 @@ const portfolio: Portfolio
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
totalsByCurrency: [{ currency: "RUB", value: "1234.5" }],
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
const emptyRecentOperations: AccountPageRecentOperations = {
|
||||
state: "empty",
|
||||
@@ -146,8 +143,6 @@ describe("AccountPage states", () => {
|
||||
const uid = "c190ff1f-1447-4227-b543-316332699ca5";
|
||||
const requests = vi.fn();
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const position
|
||||
= {
|
||||
...portfolio.positions[0],
|
||||
@@ -158,7 +153,6 @@ describe("AccountPage states", () => {
|
||||
type,
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
server.use(
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
|
||||
HttpResponse.json(
|
||||
@@ -483,7 +477,6 @@ describe("AccountPage states", () => {
|
||||
it("renders stock cards with logo fallback and signed unlabeled return", () => {
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{...loadedProps(
|
||||
{
|
||||
...portfolio,
|
||||
@@ -514,7 +507,6 @@ describe("AccountPage states", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
@@ -528,7 +520,6 @@ describe("AccountPage states", () => {
|
||||
it("groups stock supporting metrics into quiet single lines", () => {
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{...loadedProps(
|
||||
{
|
||||
...portfolio,
|
||||
@@ -547,7 +538,6 @@ describe("AccountPage states", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
@@ -563,7 +553,6 @@ describe("AccountPage states", () => {
|
||||
it("uses the ticker only for stock cards and otherwise keeps the ISIN", () => {
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{...loadedProps(
|
||||
{
|
||||
...portfolio,
|
||||
@@ -606,7 +595,6 @@ describe("AccountPage states", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
@@ -623,7 +611,6 @@ describe("AccountPage states", () => {
|
||||
it("renders bond cards with logo, fallback, and signed unlabeled return like stocks", () => {
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{...loadedProps(
|
||||
{
|
||||
...portfolio,
|
||||
@@ -654,7 +641,6 @@ describe("AccountPage states", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
@@ -674,7 +660,6 @@ describe("AccountPage states", () => {
|
||||
it("renders bond cards in expanded layout with bond-specific classes", () => {
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{...loadedProps(
|
||||
{
|
||||
...portfolio,
|
||||
@@ -693,7 +678,6 @@ describe("AccountPage states", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
@@ -707,7 +691,6 @@ describe("AccountPage states", () => {
|
||||
it("keeps other-position cards compact with labeled return and no logo", () => {
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
{...loadedProps(
|
||||
{
|
||||
...portfolio,
|
||||
@@ -726,7 +709,6 @@ describe("AccountPage states", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
)}
|
||||
/>,
|
||||
);
|
||||
@@ -738,17 +720,14 @@ describe("AccountPage states", () => {
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
{...loadedProps()}
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
account={
|
||||
{
|
||||
id: "account-1",
|
||||
name: "Основной",
|
||||
openedOn: null,
|
||||
portfolioSummary: null,
|
||||
type: "BROKERAGE",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */
|
||||
}
|
||||
|
||||
account={{
|
||||
id: "account-1",
|
||||
name: "Основной",
|
||||
openedOn: null,
|
||||
portfolioSummary: null,
|
||||
type: "BROKERAGE",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
@@ -777,33 +756,30 @@ describe("AccountPage states", () => {
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
{...loadedProps()}
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
recentOperations={
|
||||
{
|
||||
items: [
|
||||
{
|
||||
accruedInterest: null,
|
||||
category: "trade",
|
||||
commission: null,
|
||||
description: "Покупка акции",
|
||||
figi: "FIGI-1",
|
||||
id: "operation-1",
|
||||
instrumentType: "share",
|
||||
instrumentUid: "UID-1",
|
||||
logoName: null,
|
||||
occurredAt: "2026-08-14T10:00:00Z",
|
||||
payment: { currency: "RUB", value: "-12" },
|
||||
price: null,
|
||||
quantity: null,
|
||||
ticker: "SBER",
|
||||
type: "OPERATION_TYPE_BUY",
|
||||
},
|
||||
],
|
||||
state: "loaded",
|
||||
to: "/accounts/account-1/operations",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */
|
||||
}
|
||||
|
||||
recentOperations={{
|
||||
items: [
|
||||
{
|
||||
accruedInterest: null,
|
||||
category: "trade",
|
||||
commission: null,
|
||||
description: "Покупка акции",
|
||||
figi: "FIGI-1",
|
||||
id: "operation-1",
|
||||
instrumentType: "share",
|
||||
instrumentUid: "UID-1",
|
||||
logoName: null,
|
||||
occurredAt: "2026-08-14T10:00:00Z",
|
||||
payment: { currency: "RUB", value: "-12" },
|
||||
price: null,
|
||||
quantity: null,
|
||||
ticker: "SBER",
|
||||
type: "OPERATION_TYPE_BUY",
|
||||
},
|
||||
],
|
||||
state: "loaded",
|
||||
to: "/accounts/account-1/operations",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -812,7 +788,6 @@ describe("AccountPage states", () => {
|
||||
});
|
||||
|
||||
it("keeps future events ordered with calendar dates and projected rather than posted amounts", () => {
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
const event: BrokerageEvent
|
||||
= {
|
||||
date: "2026-09-10",
|
||||
@@ -827,41 +802,37 @@ describe("AccountPage states", () => {
|
||||
type: "coupon",
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
renderPage(
|
||||
<AccountPageLoaded
|
||||
{...loadedProps()}
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
recentEvents={
|
||||
{
|
||||
items: [
|
||||
event,
|
||||
{
|
||||
...event,
|
||||
date: "2026-09-11",
|
||||
dateKind: "registry",
|
||||
displayName: "Акция",
|
||||
projectedAmount: { currency: "USD", value: "0" },
|
||||
type: "dividend",
|
||||
},
|
||||
{
|
||||
...event,
|
||||
date: "2026-09-12",
|
||||
projectedAmount: { currency: "RUB", value: "1000" },
|
||||
type: "maturity",
|
||||
},
|
||||
{
|
||||
...event,
|
||||
date: "2026-09-13",
|
||||
projectedAmount: null,
|
||||
type: "offer",
|
||||
},
|
||||
],
|
||||
state: "loaded",
|
||||
to: "/accounts/account-1/events",
|
||||
}
|
||||
/* eslint-enable unicorn/no-null */
|
||||
}
|
||||
|
||||
recentEvents={{
|
||||
items: [
|
||||
event,
|
||||
{
|
||||
...event,
|
||||
date: "2026-09-11",
|
||||
dateKind: "registry",
|
||||
displayName: "Акция",
|
||||
projectedAmount: { currency: "USD", value: "0" },
|
||||
type: "dividend",
|
||||
},
|
||||
{
|
||||
...event,
|
||||
date: "2026-09-12",
|
||||
projectedAmount: { currency: "RUB", value: "1000" },
|
||||
type: "maturity",
|
||||
},
|
||||
{
|
||||
...event,
|
||||
date: "2026-09-13",
|
||||
projectedAmount: null,
|
||||
type: "offer",
|
||||
},
|
||||
],
|
||||
state: "loaded",
|
||||
to: "/accounts/account-1/events",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -953,7 +924,6 @@ describe("AccountPage states", () => {
|
||||
it.each([503])(
|
||||
"preserves totals and UTC snapshot through refresh error %i, retry and success",
|
||||
async (status) => {
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
const initialPortfolio: Portfolio
|
||||
= {
|
||||
...portfolio,
|
||||
@@ -970,7 +940,6 @@ describe("AccountPage states", () => {
|
||||
},
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
let requestCount = 0;
|
||||
let resolveRetry: ((response: Response) => void) | undefined;
|
||||
server.use(
|
||||
@@ -985,7 +954,7 @@ describe("AccountPage states", () => {
|
||||
),
|
||||
),
|
||||
http.get("*/api/v1/tbank-sync/:id", () =>
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
HttpResponse.json(
|
||||
{
|
||||
errorCode: null,
|
||||
@@ -999,7 +968,6 @@ describe("AccountPage states", () => {
|
||||
scope: { accountId: "account-1", screen: "account" },
|
||||
state: "succeeded",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
),
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () => {
|
||||
|
||||
@@ -18,8 +18,6 @@ import {
|
||||
useAccountPageViewModel,
|
||||
} from "./AccountPage.vm";
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const portfolio: Portfolio
|
||||
= {
|
||||
cashBalances: [{ currency: "USD", value: "1" }],
|
||||
@@ -66,7 +64,6 @@ const portfolio: Portfolio
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
totalsByCurrency: [{ currency: "RUB", value: "150" }],
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
function configureTestAuth() {
|
||||
configureAuthClient({
|
||||
@@ -101,7 +98,7 @@ function mockLoadedAccountPage() {
|
||||
),
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
|
||||
HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{
|
||||
accountId: "account-1",
|
||||
@@ -127,7 +124,6 @@ function mockLoadedAccountPage() {
|
||||
nextCursor: null,
|
||||
observedAt: "2026-08-13T10:00:00Z",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -138,7 +134,7 @@ function mockLoadedAccountPage() {
|
||||
calculatedAt: "2026-08-13T10:00:00Z",
|
||||
events: Array.from(
|
||||
{ length: 6 },
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
(_, index) =>
|
||||
({
|
||||
date: `2026-09-${String(index + 1).padStart(2, "0")}`,
|
||||
@@ -150,7 +146,6 @@ function mockLoadedAccountPage() {
|
||||
ticker: `BND${index}`,
|
||||
type: "coupon",
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
}),
|
||||
),
|
||||
@@ -181,7 +176,7 @@ describe("useAccountPageViewModel", () => {
|
||||
type: "CURRENCY",
|
||||
},
|
||||
...otherTypes.map(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
type =>
|
||||
({
|
||||
...portfolio.positions[2],
|
||||
@@ -190,7 +185,6 @@ describe("useAccountPageViewModel", () => {
|
||||
figi: `FIGI-${type}-NEXT`,
|
||||
type,
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
],
|
||||
structure: { currencies: [], unvaluedCategories: [] },
|
||||
@@ -248,7 +242,7 @@ describe("useAccountPageViewModel", () => {
|
||||
shareText: "66,7 %",
|
||||
},
|
||||
],
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
},
|
||||
{
|
||||
kind: "bonds",
|
||||
@@ -262,15 +256,15 @@ describe("useAccountPageViewModel", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */ {
|
||||
{
|
||||
kind: "other",
|
||||
positions: [
|
||||
{
|
||||
displayName: "Фонд",
|
||||
logoSrc: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
returnText: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
logoSrc: null,
|
||||
returnText: null,
|
||||
returnTone: undefined,
|
||||
shareText: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
shareText: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -351,10 +345,10 @@ it("keeps overview sections independent when their source data is incomplete", a
|
||||
),
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
|
||||
HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{ hasNext: false, items: [], nextCursor: null },
|
||||
/* eslint-enable unicorn/no-null */ { availability: "partial" },
|
||||
{ availability: "partial" },
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -385,8 +379,6 @@ it("keeps overview sections independent when their source data is incomplete", a
|
||||
it("refreshes every overview section through one account descriptor and observational reads", async () => {
|
||||
configureTestAuth();
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const sections: Record<string, unknown>
|
||||
= {
|
||||
"events": { events: [], incomeTotalsByCurrency: [] },
|
||||
@@ -395,7 +387,6 @@ it("refreshes every overview section through one account descriptor and observat
|
||||
portfolio,
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
const observed: string[] = [];
|
||||
const scopes: unknown[] = [];
|
||||
let version = "1";
|
||||
@@ -427,7 +418,7 @@ it("refreshes every overview section through one account descriptor and observat
|
||||
);
|
||||
}),
|
||||
http.get("*/api/v1/tbank-sync/:id", () =>
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
HttpResponse.json(
|
||||
{
|
||||
errorCode: null,
|
||||
@@ -441,7 +432,6 @@ it("refreshes every overview section through one account descriptor and observat
|
||||
scope: null,
|
||||
state: "succeeded",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -209,7 +209,7 @@ function toPosition(
|
||||
|
||||
const returnValue = position.absoluteReturn
|
||||
? Number(position.absoluteReturn.value)
|
||||
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null;
|
||||
|
||||
let returnTone: AccountPageReturnTone | undefined;
|
||||
if (returnValue !== null && returnValue !== 0)
|
||||
@@ -226,17 +226,17 @@ function toPosition(
|
||||
: undefined,
|
||||
logoSrc: isExpanded
|
||||
? instrumentLogoUrl(position.logoName)
|
||||
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null,
|
||||
returnText: position.absoluteReturn
|
||||
? `${returnPrefix}${formatMoney(position.absoluteReturn)}`
|
||||
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null,
|
||||
returnTone,
|
||||
shareText: position.currentValue?.currency
|
||||
? formatShare(position.currentValue, {
|
||||
currency: position.currentValue.currency,
|
||||
value: String(denominators.get(position.currentValue.currency) ?? 0),
|
||||
})
|
||||
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -62,9 +62,7 @@ const AccountPageLoaded: FC<AccountPageLoadedProps> = (props) => {
|
||||
sourceError={props.sourceError}
|
||||
/>
|
||||
|
||||
{!!props.portfolio.isStale && (
|
||||
<Alert color="blue">{t("portfolio.staleWarning")}</Alert>
|
||||
)}
|
||||
{!!props.portfolio.isStale && <Alert color="blue">{t("portfolio.staleWarning")}</Alert>}
|
||||
|
||||
<Text c="dimmed" size="sm">
|
||||
{t(
|
||||
@@ -78,12 +76,8 @@ const AccountPageLoaded: FC<AccountPageLoadedProps> = (props) => {
|
||||
</Text>
|
||||
|
||||
{props.partial
|
||||
? (
|
||||
<Alert role="status">{t("investment.partial")}</Alert>
|
||||
)
|
||||
: (
|
||||
<CurrencyCards amounts={props.portfolio.totalsByCurrency} />
|
||||
)}
|
||||
? <Alert role="status">{t("investment.partial")}</Alert>
|
||||
: <CurrencyCards amounts={props.portfolio.totalsByCurrency} />}
|
||||
|
||||
{!props.partial && (
|
||||
<PortfolioStructure
|
||||
|
||||
+1
-3
@@ -34,9 +34,7 @@ const AccountPageLoading: FC = () => {
|
||||
|
||||
<Skeleton height={260} radius="sm" />
|
||||
|
||||
{["first", "second", "third"].map(placeholder => (
|
||||
<Skeleton height={52} key={placeholder} radius="sm" />
|
||||
))}
|
||||
{["first", "second", "third"].map(placeholder => <Skeleton height={52} key={placeholder} radius="sm" />)}
|
||||
</LoadingSkeleton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -69,9 +69,7 @@ const IncomeSummary: FC<IncomeSummaryProps> = (props) => {
|
||||
|
||||
{!!props.stale && <Text size="xs">{t("investment.stale")}</Text>}
|
||||
|
||||
{!!props.refreshError && (
|
||||
<Text role="status">{t("income.refreshError")}</Text>
|
||||
)}
|
||||
{!!props.refreshError && <Text role="status">{t("income.refreshError")}</Text>}
|
||||
|
||||
{props.summary.totalsByCurrency.map(total => (
|
||||
<Paper key={total.currency} p="sm" withBorder>
|
||||
|
||||
+2
-4
@@ -81,13 +81,12 @@ export const Incomplete: Story = {
|
||||
{
|
||||
...rub,
|
||||
categories: rub.categories.map(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
row =>
|
||||
({
|
||||
...row,
|
||||
sharePercent: null,
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -110,14 +109,13 @@ export const Negative: Story = {
|
||||
{
|
||||
...rub,
|
||||
categories: rub.categories.map(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
row =>
|
||||
({
|
||||
...row,
|
||||
sharePercent: null,
|
||||
value: row.type === "OTHER" ? "25000" : row.value,
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
hasNegativeValues: true,
|
||||
totalValue: "1150000",
|
||||
|
||||
+2
-5
@@ -27,7 +27,6 @@ const ui = (structure: null | Portfolio["structure"] | undefined) => (
|
||||
describe("portfolio structure", () => {
|
||||
it.each([
|
||||
undefined,
|
||||
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
null,
|
||||
])(
|
||||
"keeps an older snapshot without structure available (%s) and recovers on refresh",
|
||||
@@ -74,13 +73,12 @@ describe("portfolio structure", () => {
|
||||
{
|
||||
...rub,
|
||||
categories: rub.categories.map(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
row =>
|
||||
({
|
||||
...row,
|
||||
sharePercent: null,
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -106,14 +104,13 @@ describe("portfolio structure", () => {
|
||||
{
|
||||
...rub,
|
||||
categories: rub.categories.map(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
row =>
|
||||
({
|
||||
...row,
|
||||
sharePercent: null,
|
||||
value: row.type === "STOCK" ? "-60" : row.value,
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
hasNegativeValues: true,
|
||||
},
|
||||
|
||||
+1
-3
@@ -97,9 +97,7 @@ const PortfolioStructureView: FC<PortfolioStructureViewProps> = (props) => {
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{!!props.negative && (
|
||||
<Text size="sm">{t("portfolio.structure.negative")}</Text>
|
||||
)}
|
||||
{!!props.negative && <Text size="sm">{t("portfolio.structure.negative")}</Text>}
|
||||
|
||||
{!!(
|
||||
!props.unavailable
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ const usePortfolioStructureVm = (props: PortfolioStructureProps) => {
|
||||
return {
|
||||
chartData,
|
||||
currencies: structure?.currencies.map(item => item.currency) ?? [],
|
||||
// eslint-disable-next-line unicorn/no-null -- Mantine clears the controlled currency with null.
|
||||
|
||||
currency: selected?.currency ?? null,
|
||||
incomplete: isIncomplete,
|
||||
negative: selected?.hasNegativeValues ?? false,
|
||||
|
||||
@@ -4,8 +4,6 @@ import type { AccountPagePosition } from "../../AccountPage.vm";
|
||||
|
||||
import { PositionCard } from "./PositionCard.view";
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const syntheticPosition
|
||||
= {
|
||||
absoluteReturn: { currency: "RUB", value: "5000" },
|
||||
@@ -22,7 +20,7 @@ const syntheticPosition
|
||||
shareText: "20%",
|
||||
ticker: "DEMO",
|
||||
type: "STOCK",
|
||||
} satisfies /* eslint-enable unicorn/no-null */ AccountPagePosition;
|
||||
} satisfies AccountPagePosition;
|
||||
|
||||
const meta = {
|
||||
args: { kind: "stocks", position: syntheticPosition },
|
||||
@@ -62,8 +60,6 @@ export const StockGain: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const BondLoss: Story
|
||||
= {
|
||||
args: {
|
||||
@@ -94,9 +90,6 @@ export const BondLoss: Story
|
||||
},
|
||||
},
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const Other: Story
|
||||
= {
|
||||
@@ -120,9 +113,6 @@ export const Other: Story
|
||||
},
|
||||
},
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
export const MissingValuation: Story
|
||||
= {
|
||||
@@ -149,4 +139,3 @@ export const MissingValuation: Story
|
||||
},
|
||||
},
|
||||
};
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
@@ -60,10 +60,10 @@ const PositionCard: FC<PositionCardProps> = (props) => {
|
||||
? t("portfolio.avgPrice", {
|
||||
value: formatMoney(position.averagePurchasePrice),
|
||||
})
|
||||
: null, // eslint-disable-line unicorn/no-null -- Mantine represents an empty controlled field as null.
|
||||
: null,
|
||||
position.shareText
|
||||
? t("portfolio.share", { value: position.shareText })
|
||||
: null, // eslint-disable-line unicorn/no-null -- Mantine represents an empty controlled field as null.
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
|
||||
@@ -44,9 +44,7 @@ const PositionGroupCard: FC<PositionGroupCardProps> = (props) => {
|
||||
{identity.displayName}
|
||||
</Anchor>
|
||||
)
|
||||
: (
|
||||
<Text fw={600}>{identity.displayName}</Text>
|
||||
)}
|
||||
: <Text fw={600}>{identity.displayName}</Text>}
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{identity.ticker ?? identity.isin ?? identity.figi}
|
||||
@@ -90,9 +88,7 @@ const PositionGroupCard: FC<PositionGroupCardProps> = (props) => {
|
||||
</Group>
|
||||
|
||||
<Group gap="xs" justify="space-between">
|
||||
{!!position.currentValue && (
|
||||
<Text size="sm">{formatMoney(position.currentValue)}</Text>
|
||||
)}
|
||||
{!!position.currentValue && <Text size="sm">{formatMoney(position.currentValue)}</Text>}
|
||||
|
||||
{!!position.returnText && (
|
||||
<Text className={appearanceClass} size="sm">
|
||||
|
||||
@@ -17,9 +17,7 @@ const PositionLogo: FC<PositionLogoProps> = (props) => {
|
||||
|
||||
const logo
|
||||
= src && !failed
|
||||
? (
|
||||
<Avatar alt={label} onError={onError} radius="md" size={40} src={src} />
|
||||
)
|
||||
? <Avatar alt={label} onError={onError} radius="md" size={40} src={src} />
|
||||
: (
|
||||
<Avatar radius="md" size={40}>
|
||||
{(label.trim()[0] ?? "?").toUpperCase()}
|
||||
|
||||
@@ -32,31 +32,21 @@ const RecentOperations: FC<RecentOperationsProps> = (props) => {
|
||||
|
||||
{!!operations.stale && <Text size="xs">{t("investment.stale")}</Text>}
|
||||
|
||||
{!!operations.refreshError && (
|
||||
<Text role="status">{t("accounts.refreshError")}</Text>
|
||||
)}
|
||||
{!!operations.refreshError && <Text role="status">{t("accounts.refreshError")}</Text>}
|
||||
</>
|
||||
)}
|
||||
|
||||
{operations.state === "loading" && (
|
||||
<LoadingSkeleton label={t("ledger.recentLoading")}>
|
||||
{["first", "second", "third"].map(placeholder => (
|
||||
<Skeleton height={48} key={placeholder} radius="sm" />
|
||||
))}
|
||||
{["first", "second", "third"].map(placeholder => <Skeleton height={48} key={placeholder} radius="sm" />)}
|
||||
</LoadingSkeleton>
|
||||
)}
|
||||
|
||||
{operations.state === "error" && (
|
||||
<Alert color="red">{t("ledger.recentError")}</Alert>
|
||||
)}
|
||||
{operations.state === "error" && <Alert color="red">{t("ledger.recentError")}</Alert>}
|
||||
|
||||
{operations.state === "empty" && (
|
||||
<Text c="dimmed">{t("ledger.recentEmpty")}</Text>
|
||||
)}
|
||||
{operations.state === "empty" && <Text c="dimmed">{t("ledger.recentEmpty")}</Text>}
|
||||
|
||||
{operations.state === "partial" && (
|
||||
<Text role="status">{t("investment.partial")}</Text>
|
||||
)}
|
||||
{operations.state === "partial" && <Text role="status">{t("investment.partial")}</Text>}
|
||||
|
||||
{(operations.state === "loaded" || operations.state === "partial")
|
||||
&& operations.items.map(operation => (
|
||||
|
||||
@@ -8,8 +8,6 @@ import type { Portfolio } from "@/entities/portfolio";
|
||||
import { toAccountPagePositionSections } from "./AccountPage.vm";
|
||||
import { PositionSection } from "./components/PositionSection.view";
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const position: Portfolio["positions"][number]
|
||||
= {
|
||||
absoluteReturn: { currency: "RUB", value: "25" },
|
||||
@@ -25,8 +23,6 @@ const position: Portfolio["positions"][number]
|
||||
type: "STOCK",
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
const second
|
||||
= {
|
||||
...position,
|
||||
@@ -37,7 +33,6 @@ const second
|
||||
quantity: "-2",
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
function sections(positions: Portfolio["positions"]) {
|
||||
return toAccountPagePositionSections({
|
||||
cashBalances: [],
|
||||
@@ -84,30 +79,30 @@ describe("Positions grouped by security", () => {
|
||||
|
||||
it("uses ISIN only when FIGI is absent, never merging by name or ticker", () => {
|
||||
expect(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
sections([
|
||||
{
|
||||
...position,
|
||||
figi: null,
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */ { ...second, figi: null }, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
{ ...second, figi: null },
|
||||
])[0].groups,
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
sections([position, { ...second, figi: "OTHER-FIGI" }])[0].groups,
|
||||
).toHaveLength(2);
|
||||
expect(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
sections([
|
||||
{
|
||||
...position,
|
||||
figi: null,
|
||||
isin: null,
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */ {
|
||||
{
|
||||
...second,
|
||||
figi: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
isin: null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
figi: null,
|
||||
isin: null,
|
||||
},
|
||||
])[0].groups,
|
||||
).toHaveLength(2);
|
||||
|
||||
@@ -31,7 +31,7 @@ it("keeps expanded positions vertical while other positions adapt to the viewpor
|
||||
const otherType = kind === "bonds" ? "BOND" : "FUND";
|
||||
|
||||
const positions: AccountPagePosition[] = [1, 2, 3].map(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
index =>
|
||||
({
|
||||
absoluteReturn: null,
|
||||
@@ -48,7 +48,6 @@ it("keeps expanded positions vertical while other positions adapt to the viewpor
|
||||
ticker: null,
|
||||
type: kind === "stocks" ? "STOCK" : otherType,
|
||||
}),
|
||||
/* eslint-enable unicorn/no-null */
|
||||
);
|
||||
|
||||
const { unmount } = render(
|
||||
|
||||
@@ -60,7 +60,7 @@ describe("AccountsPage", () => {
|
||||
renderPage(createTestQueryClient());
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Повторить" }));
|
||||
expect(await screen.findByText(account.name)).toBeVisible();
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the API/DOM null sentinel in the expected response.
|
||||
|
||||
expect(modes).toEqual([null, "observe"]);
|
||||
});
|
||||
it.each([
|
||||
@@ -212,12 +212,11 @@ it("keeps unknown membership loading instead of declaring no accounts", async ()
|
||||
const client = createTestQueryClient();
|
||||
renderPage(client);
|
||||
await waitFor(() =>
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
expect(client.getQueryData(brokerageAccountsQueryKey)).toMatchObject(
|
||||
{
|
||||
data: null,
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
expect(
|
||||
|
||||
@@ -27,8 +27,6 @@ const LocationProbe = () => {
|
||||
const uid = "e6123145-9665-43e0-8413-cd61b8aa9b13";
|
||||
const money = (value: string) => ({ currency: "RUB", value });
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const bond
|
||||
= {
|
||||
aci: money("12"),
|
||||
@@ -52,7 +50,6 @@ const bond
|
||||
ticker: "TEST",
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
const position = {
|
||||
absoluteReturn: money("1000"),
|
||||
accounts: [
|
||||
@@ -74,8 +71,6 @@ const position = {
|
||||
returnPercent: "50",
|
||||
};
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const quote
|
||||
= {
|
||||
aci: money("12"),
|
||||
@@ -90,7 +85,6 @@ const quote
|
||||
unavailable: [],
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
function setup(
|
||||
options: {
|
||||
ambiguousTicker?: boolean;
|
||||
@@ -122,7 +116,7 @@ function setup(
|
||||
requests.push(new URL(request.url));
|
||||
|
||||
return HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{
|
||||
coupon: money("32.88"),
|
||||
@@ -158,7 +152,6 @@ function setup(
|
||||
unavailable:
|
||||
options.ratingState === "unavailable" ? ["rating"] : [],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
}),
|
||||
@@ -179,7 +172,7 @@ function setup(
|
||||
);
|
||||
if (params.section === "candles")
|
||||
return HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{
|
||||
candles: [
|
||||
@@ -193,7 +186,6 @@ function setup(
|
||||
historyFrom: null,
|
||||
observedAt: quote.observedAt,
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
if (options.failCoupons)
|
||||
@@ -203,7 +195,7 @@ function setup(
|
||||
);
|
||||
|
||||
return HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{
|
||||
coupons: [
|
||||
@@ -235,7 +227,6 @@ function setup(
|
||||
positionIsStale: false,
|
||||
to: "2027-09-10",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
}),
|
||||
@@ -245,7 +236,7 @@ function setup(
|
||||
const isNext = url.searchParams.has("cursor");
|
||||
|
||||
return HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{
|
||||
accountId: "all",
|
||||
@@ -268,7 +259,6 @@ function setup(
|
||||
nextCursor: isNext ? null : "next",
|
||||
observedAt: quote.observedAt,
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -172,17 +172,11 @@ const BondPageView: FC<BondPageViewProps> = (props) => {
|
||||
</Group>
|
||||
|
||||
<Group gap="xs" mt="md">
|
||||
{!!bond.floatingCoupon && (
|
||||
<Badge variant="light">{t("bond.floatingCoupon")}</Badge>
|
||||
)}
|
||||
{!!bond.floatingCoupon && <Badge variant="light">{t("bond.floatingCoupon")}</Badge>}
|
||||
|
||||
{!!bond.amortization && (
|
||||
<Badge variant="light">{t("bond.amortization")}</Badge>
|
||||
)}
|
||||
{!!bond.amortization && <Badge variant="light">{t("bond.amortization")}</Badge>}
|
||||
|
||||
{!!bond.perpetual && (
|
||||
<Badge variant="light">{t("bond.perpetual")}</Badge>
|
||||
)}
|
||||
{!!bond.perpetual && <Badge variant="light">{t("bond.perpetual")}</Badge>}
|
||||
|
||||
{!!bond.subordinated && (
|
||||
<Badge color="orange" variant="light">
|
||||
@@ -414,9 +408,7 @@ const BondCoupons: FC<BondPageViewProps> = (props) => {
|
||||
/>
|
||||
))
|
||||
)
|
||||
: (
|
||||
<Text c="dimmed">{t("bond.noCoupons")}</Text>
|
||||
)}
|
||||
: <Text c="dimmed">{t("bond.noCoupons")}</Text>}
|
||||
|
||||
<Text c="dimmed" size="xs">
|
||||
{t("bond.couponWindow", {
|
||||
@@ -452,9 +444,7 @@ const BondCoupons: FC<BondPageViewProps> = (props) => {
|
||||
/>
|
||||
))
|
||||
)
|
||||
: (
|
||||
<Text c="dimmed">{t("bond.noCoupons")}</Text>
|
||||
)}
|
||||
: <Text c="dimmed">{t("bond.noCoupons")}</Text>}
|
||||
</Stack>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
|
||||
@@ -91,7 +91,6 @@ function useBondPageVm() {
|
||||
|
||||
return {
|
||||
averageLine:
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
null,
|
||||
bond: bond.data,
|
||||
bondState: section(bond, uid ? sync.retry : () => bond.refetch()),
|
||||
@@ -100,7 +99,7 @@ function useBondPageVm() {
|
||||
canonicalTo:
|
||||
bond.data?.slug && bond.data.slug !== bondId
|
||||
? `/bonds/${encodeURIComponent(bond.data.slug)}${search}`
|
||||
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null,
|
||||
chartData,
|
||||
code: apiErrorCode(bond.error),
|
||||
coupons: coupons.data,
|
||||
@@ -111,12 +110,12 @@ function useBondPageVm() {
|
||||
logoName:
|
||||
item.logoName
|
||||
?? bond.data?.logoName
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
|
||||
?? null,
|
||||
ticker:
|
||||
item.ticker
|
||||
?? bond.data?.ticker
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
|
||||
?? null,
|
||||
})) ?? [],
|
||||
filter,
|
||||
|
||||
@@ -19,8 +19,6 @@ import { CardPage } from "./CardPage";
|
||||
|
||||
const money = (value: string) => ({ currency: "RUB", value });
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const card
|
||||
= {
|
||||
archivedAt: null,
|
||||
@@ -34,8 +32,6 @@ const card
|
||||
version: 1,
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
const operation
|
||||
= {
|
||||
amount: money("50.01"),
|
||||
@@ -55,7 +51,6 @@ const operation
|
||||
version: 1,
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
function setup(
|
||||
options: {
|
||||
cardsError?: boolean;
|
||||
@@ -342,13 +337,13 @@ describe("CardPage", () => {
|
||||
};
|
||||
|
||||
bodies.push(body);
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
current
|
||||
= {
|
||||
...current,
|
||||
archivedAt: body.archived ? "2026-09-01" : null,
|
||||
version: current.version + 1,
|
||||
} as /* eslint-enable unicorn/no-null */ typeof card;
|
||||
} as typeof card;
|
||||
|
||||
return HttpResponse.json(current);
|
||||
}),
|
||||
|
||||
@@ -227,12 +227,8 @@ const CardPageView: FC<CardPageViewProps> = (props) => {
|
||||
variant="subtle"
|
||||
>
|
||||
{card.archivedAt
|
||||
? (
|
||||
<RestoreIcon size={18} />
|
||||
)
|
||||
: (
|
||||
<DeleteIcon size={18} strokeWidth={1.5} />
|
||||
)}
|
||||
? <RestoreIcon size={18} />
|
||||
: <DeleteIcon size={18} strokeWidth={1.5} />}
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</div>
|
||||
|
||||
@@ -84,7 +84,7 @@ function useCardPageVm() {
|
||||
otherCard:
|
||||
item.kind === "transfer"
|
||||
? otherCardId
|
||||
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null,
|
||||
title,
|
||||
tone: isOutgoing ? ("negative" as const) : ("positive" as const),
|
||||
value: `${isOutgoing ? "−" : "+"}${formatMoney(item.amount)}`,
|
||||
|
||||
@@ -25,7 +25,7 @@ function useCardPagePresentationVm(vm: CardPageVm) {
|
||||
|
||||
if (pending.scope !== scope || vm.history.isError) {
|
||||
pendingPageRef.current
|
||||
// eslint-disable-next-line unicorn/no-null -- Reset the pending-pagination ref to its initial null sentinel.
|
||||
|
||||
= null;
|
||||
|
||||
return;
|
||||
@@ -40,7 +40,6 @@ function useCardPagePresentationVm(vm: CardPageVm) {
|
||||
if (delta) window.scrollBy({ behavior: "instant", top: delta });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the existing nullable validation/ref contract.
|
||||
pendingPageRef.current = null;
|
||||
}, [
|
||||
scope,
|
||||
|
||||
@@ -12,8 +12,6 @@ import { server } from "@/test/server";
|
||||
|
||||
import { DeleteOperation } from "./DeleteOperation";
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const transfer: BankOperation
|
||||
= {
|
||||
amount: { currency: "RUB", value: "100" },
|
||||
@@ -33,7 +31,6 @@ const transfer: BankOperation
|
||||
version: 1,
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
describe("DeleteOperation", () => {
|
||||
it("names both sides and requires a deliberate fresh version after conflict", async () => {
|
||||
configureAuthClient({
|
||||
|
||||
@@ -59,7 +59,7 @@ function useOperationFieldsVm(props: OperationFieldsProps) {
|
||||
const dateParser: ComponentProps<typeof DateInput>["dateParser"] = value =>
|
||||
props.validDate(props.dateInputValue(value))
|
||||
? props.dateInputValue(value)
|
||||
: null; // eslint-disable-line unicorn/no-null -- Mantine represents an empty controlled dateField as null.
|
||||
: null;
|
||||
|
||||
const onDateBlur: ComponentProps<typeof DateInput>["onBlur"] = (event) => {
|
||||
dateField.onChange(props.dateInputValue(event.currentTarget.value));
|
||||
|
||||
@@ -20,8 +20,6 @@ import { OperationForm } from "./OperationForm";
|
||||
|
||||
const money = (value: string) => ({ currency: "RUB", value });
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const card: BankCard
|
||||
= {
|
||||
archivedAt: null,
|
||||
@@ -36,7 +34,6 @@ const card: BankCard
|
||||
version: 1,
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
async function select(label: RegExp, value: RegExp | string) {
|
||||
fireEvent.click(screen.getByRole("combobox", { name: label }));
|
||||
fireEvent.click(await screen.findByRole("option", { name: value }));
|
||||
@@ -229,7 +226,6 @@ describe("OperationForm", () => {
|
||||
expect(body).not.toHaveProperty("categoryId");
|
||||
});
|
||||
it("finds a previous-month purchase across pages, checks remaining refund and retains the complete-refund intent", async () => {
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
const purchase: BankOperation
|
||||
= {
|
||||
amount: money("500"),
|
||||
@@ -249,7 +245,6 @@ describe("OperationForm", () => {
|
||||
version: 1,
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
server.use(
|
||||
http.get("*/api/v1/bank-cards/a/expenses", ({ request }) => {
|
||||
return HttpResponse.json({
|
||||
@@ -313,7 +308,6 @@ describe("OperationForm", () => {
|
||||
});
|
||||
});
|
||||
it("edits a full refund with its own amount excluded from the limit and immutable purchase", async () => {
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
const refund: BankOperation
|
||||
= {
|
||||
amount: money("500"),
|
||||
@@ -333,7 +327,6 @@ describe("OperationForm", () => {
|
||||
version: 2,
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
const purchase = {
|
||||
...refund,
|
||||
availableRefund: money("0"),
|
||||
@@ -394,7 +387,6 @@ describe("OperationForm", () => {
|
||||
});
|
||||
|
||||
it("keeps edit draft and stale version until explicit adoption after conflict", async () => {
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
const operation: BankOperation
|
||||
= {
|
||||
amount: money("100"),
|
||||
@@ -414,7 +406,6 @@ describe("OperationForm", () => {
|
||||
version: 1,
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
let readVersion = 1;
|
||||
const bodies: Record<string, unknown>[] = [];
|
||||
server.use(
|
||||
|
||||
@@ -228,9 +228,7 @@ const OperationFormView: FC<OperationFormViewProps> = (props) => {
|
||||
<>
|
||||
<RefundFields {...props.refund} />
|
||||
|
||||
{props.loadingExpenses && (
|
||||
<Text role="status">{t("bank.loadingHistory")}</Text>
|
||||
)}
|
||||
{props.loadingExpenses && <Text role="status">{t("bank.loadingHistory")}</Text>}
|
||||
|
||||
{props.emptyExpenses && <Text>{t("bank.noPurchases")}</Text>}
|
||||
|
||||
|
||||
@@ -165,13 +165,13 @@ const useOperationFormVm = (props: OperationFormProps) => {
|
||||
return {
|
||||
categoriesError: categories.isError
|
||||
? categories.error
|
||||
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null,
|
||||
categoriesOpen,
|
||||
closeCategories: () => setCategoriesOpen(false),
|
||||
error,
|
||||
expensesError: expenses.isError
|
||||
? expenses.error
|
||||
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null,
|
||||
methods,
|
||||
onCategoryCreated: (id: string) => {
|
||||
methods.setValue("categoryId", id);
|
||||
@@ -182,7 +182,7 @@ const useOperationFormVm = (props: OperationFormProps) => {
|
||||
retrySource: () => void sourceQuery.refetch(),
|
||||
sourceError: sourceQuery.isError
|
||||
? sourceQuery.error
|
||||
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null,
|
||||
view: {
|
||||
categoriesOpen,
|
||||
editing: Boolean(props.operation),
|
||||
|
||||
@@ -33,7 +33,7 @@ function renderCards(isFail = false, hasCategoryFailure = false) {
|
||||
isFail ? HttpResponse.error() : HttpResponse.json([]),
|
||||
),
|
||||
http.get("*/api/v1/bank-cards/summary", () =>
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
HttpResponse.json(
|
||||
{
|
||||
cardId: null,
|
||||
@@ -43,7 +43,6 @@ function renderCards(isFail = false, hasCategoryFailure = false) {
|
||||
net: money("0"),
|
||||
refunds: money("0"),
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -119,7 +118,7 @@ describe("CardsPage", () => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument(),
|
||||
);
|
||||
expect(submitted[0]).toEqual(submitted[1]);
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
expect(submitted[0]).toMatchObject(
|
||||
{
|
||||
bankName: null,
|
||||
@@ -127,7 +126,6 @@ describe("CardsPage", () => {
|
||||
name: "Повседневная",
|
||||
openingBalance: money("-92233720368547758.08"),
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
);
|
||||
});
|
||||
it("uses archive only for card list and keeps all-card summary including negative net", async () => {
|
||||
@@ -137,8 +135,6 @@ describe("CardsPage", () => {
|
||||
http.get("*/api/v1/bank-cards", ({ request }) => {
|
||||
requests.push(new URL(request.url).search);
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
return HttpResponse.json([
|
||||
{
|
||||
archivedAt: "2026-09-01",
|
||||
@@ -151,7 +147,7 @@ describe("CardsPage", () => {
|
||||
openingDate: "2026-01-01",
|
||||
version: 1,
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
|
||||
]);
|
||||
}),
|
||||
);
|
||||
@@ -175,8 +171,6 @@ describe("CardsPage", () => {
|
||||
http.get("*/api/v1/bank-cards/summary", ({ request }) => {
|
||||
requests.push(new URL(request.url).search);
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
return HttpResponse.json(
|
||||
{
|
||||
cardId: null,
|
||||
@@ -185,7 +179,6 @@ describe("CardsPage", () => {
|
||||
net: money("0"),
|
||||
refunds: money("0"),
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -125,9 +125,7 @@ const CardsPageView: FC<CardsPageViewProps> = (props) => {
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{vm.cards.data && vm.cards.isFetching && (
|
||||
<Text role="status">{t("bank.refreshing")}</Text>
|
||||
)}
|
||||
{vm.cards.data && vm.cards.isFetching && <Text role="status">{t("bank.refreshing")}</Text>}
|
||||
|
||||
<Group align="flex-end" justify="space-between">
|
||||
<BankPeriodPicker onChange={vm.setPeriod} period={vm.period} />
|
||||
|
||||
@@ -29,8 +29,6 @@ const LocationProbe = () => {
|
||||
const uid = "e6123145-9665-43e0-8413-cd61b8aa9b13";
|
||||
const money = (value: string) => ({ currency: "RUB", value });
|
||||
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
const stock
|
||||
= {
|
||||
currency: "RUB",
|
||||
@@ -44,7 +42,6 @@ const stock
|
||||
ticker: "TEST",
|
||||
};
|
||||
|
||||
/* eslint-enable unicorn/no-null */
|
||||
const position = {
|
||||
absoluteReturn: money("1000"),
|
||||
accounts: [
|
||||
@@ -124,7 +121,7 @@ function setup(
|
||||
);
|
||||
if (params.section === "candles")
|
||||
return HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{
|
||||
candles: [],
|
||||
@@ -132,14 +129,13 @@ function setup(
|
||||
historyFrom: null,
|
||||
observedAt: quote.observedAt,
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
if (params.section === "company")
|
||||
return options.failCompany
|
||||
? HttpResponse.json({ code: "TBANK_UNAVAILABLE" }, { status: 503 })
|
||||
: HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{
|
||||
description: "Описание компании",
|
||||
@@ -155,12 +151,11 @@ function setup(
|
||||
observedAt: quote.observedAt,
|
||||
unavailable: [],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
|
||||
return HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{
|
||||
dividends: [
|
||||
@@ -188,7 +183,6 @@ function setup(
|
||||
positionIsStale: false,
|
||||
to: "2027-09-10",
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
}),
|
||||
@@ -200,7 +194,7 @@ function setup(
|
||||
return HttpResponse.json({ code: cursorError }, { status: 400 });
|
||||
|
||||
return HttpResponse.json(
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
|
||||
resource(
|
||||
{
|
||||
accountId: "all",
|
||||
@@ -225,7 +219,6 @@ function setup(
|
||||
nextCursor: isNext ? null : "next",
|
||||
observedAt: quote.observedAt,
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
),
|
||||
);
|
||||
}),
|
||||
@@ -264,7 +257,6 @@ function setup(
|
||||
|
||||
describe("stock page", () => {
|
||||
it("preserves unknown and unambiguous currencies with compact, ratio and percent formatting", async () => {
|
||||
/* eslint-disable unicorn/no-null -- Fixture preserves the production contract’s explicit null values. */
|
||||
setup(
|
||||
{
|
||||
metrics: [
|
||||
@@ -312,7 +304,6 @@ describe("stock page", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
/* eslint-enable unicorn/no-null */
|
||||
);
|
||||
expect(await screen.findByText("1,23 млн валюта неизвестна")).toBeVisible();
|
||||
for (const value of [
|
||||
@@ -468,9 +459,8 @@ describe("stock page", () => {
|
||||
it.each([
|
||||
[
|
||||
"INVALID_OPERATIONS_CURSOR",
|
||||
// eslint-disable-next-line unicorn/no-null -- Exercise the explicit null returned by the production API/DOM contract.
|
||||
null,
|
||||
null, // eslint-disable-line unicorn/no-null -- Preserve explicit null in the API fixture.
|
||||
null,
|
||||
],
|
||||
["TBANK_UNAVAILABLE", "next", "observe"],
|
||||
])(
|
||||
|
||||
@@ -146,9 +146,7 @@ const StockPageView: FC<StockPageViewProps> = (props) => {
|
||||
);
|
||||
}
|
||||
else {
|
||||
companyMetricsContent = (
|
||||
<Text c="dimmed">{t("stock.noFundamentals")}</Text>
|
||||
);
|
||||
companyMetricsContent = <Text c="dimmed">{t("stock.noFundamentals")}</Text>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,9 +217,7 @@ const StockPageView: FC<StockPageViewProps> = (props) => {
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{stock.lot !== null && (
|
||||
<Text size="sm">{t("stock.lot", { value: stock.lot })}</Text>
|
||||
)}
|
||||
{stock.lot !== null && <Text size="sm">{t("stock.lot", { value: stock.lot })}</Text>}
|
||||
</Group>
|
||||
|
||||
{!!vm.company && (
|
||||
|
||||
@@ -92,7 +92,7 @@ function useStockPageVm() {
|
||||
const averageLine
|
||||
= average && average.currency === candles.data?.currency
|
||||
? Number(average.value)
|
||||
: null; // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null;
|
||||
|
||||
const companyData = company.data;
|
||||
const search = parameters.size > 0 ? `?${parameters.toString()}` : "";
|
||||
@@ -101,13 +101,13 @@ function useStockPageVm() {
|
||||
averageLine:
|
||||
averageLine !== null && Number.isFinite(averageLine)
|
||||
? averageLine
|
||||
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null,
|
||||
candles: candles.data,
|
||||
candlesState: section(candles, sync.retry),
|
||||
canonicalTo:
|
||||
stock.data?.slug && stock.data.slug !== stockId
|
||||
? `/stocks/${encodeURIComponent(stock.data.slug)}${search}`
|
||||
: null, // eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
: null,
|
||||
chartData,
|
||||
code: apiErrorCode(stock.error),
|
||||
company: companyData,
|
||||
@@ -120,12 +120,12 @@ function useStockPageVm() {
|
||||
logoName:
|
||||
item.logoName
|
||||
?? stock.data?.logoName
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
|
||||
?? null,
|
||||
ticker:
|
||||
item.ticker
|
||||
?? stock.data?.ticker
|
||||
// eslint-disable-next-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model.
|
||||
|
||||
?? null,
|
||||
})) ?? [],
|
||||
filter,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user