diff --git a/src/app/routes/AppRoutes.tsx b/src/app/routes/AppRoutes.tsx
index e6ad30f..8c7bf2c 100644
--- a/src/app/routes/AppRoutes.tsx
+++ b/src/app/routes/AppRoutes.tsx
@@ -1,6 +1,7 @@
import { Navigate, Route, Routes, useLocation } from "react-router";
import { useAuth } from "@/features/auth";
import { AccountPage } from "@/pages/account";
+import { AccountEventsPage } from "@/pages/account-events";
import { AccountOperationsPage } from "@/pages/account-operations";
import { AccountsPage } from "@/pages/accounts";
import { LoginPage } from "@/pages/login";
@@ -60,6 +61,10 @@ export function AppRoutes() {
>
} />
} />
+ }
+ />
}
diff --git a/src/app/ui/AppBreadcrumbs.tsx b/src/app/ui/AppBreadcrumbs.tsx
index ce314f4..d78cdbe 100644
--- a/src/app/ui/AppBreadcrumbs.tsx
+++ b/src/app/ui/AppBreadcrumbs.tsx
@@ -38,9 +38,11 @@ function BreadcrumbTrail({ items }: { items: Crumb[] }) {
function AccountBreadcrumbs({
accountId,
+ events,
operations,
}: {
accountId: string;
+ events: boolean;
operations: boolean;
}) {
const { accessToken } = useAuth();
@@ -53,16 +55,22 @@ function AccountBreadcrumbs({
return (
);
@@ -74,6 +82,10 @@ export function AppBreadcrumbs() {
path: "/accounts/:accountId/operations",
end: true,
});
+ const accountEvents = useMatch({
+ path: "/accounts/:accountId/events",
+ end: true,
+ });
const account = useMatch({ path: "/accounts/:accountId", end: true });
const accounts = useMatch({ path: "/accounts", end: true });
const settings = useMatch({ path: "/settings", end: true });
@@ -82,13 +94,23 @@ export function AppBreadcrumbs() {
return (
);
+ if (accountEvents)
+ return (
+
+ );
if (account)
return (
);
diff --git a/src/entities/brokerage-event/api/useBrokerageEvents.ts b/src/entities/brokerage-event/api/useBrokerageEvents.ts
new file mode 100644
index 0000000..2a4f8eb
--- /dev/null
+++ b/src/entities/brokerage-event/api/useBrokerageEvents.ts
@@ -0,0 +1,25 @@
+import { useQuery } from "@tanstack/react-query";
+import { apiQueryClient } from "@/shared/api";
+import type { paths } from "@/shared/api/generated/schema";
+
+type EventsEndpoint =
+ paths["/api/v1/brokerage-accounts/{accountId}/events"]["get"];
+export type BrokerageEvent =
+ EventsEndpoint["responses"][200]["content"]["application/json"]["events"][number];
+
+export function useBrokerageEvents(
+ accountId: string | undefined,
+ accessToken: string | null,
+) {
+ return useQuery({
+ queryKey: ["brokerage-events", accountId ?? ""] as const,
+ enabled: Boolean(accountId && accessToken),
+ staleTime: 30_000,
+ retry: false,
+ ...apiQueryClient.queryOptions(
+ "get",
+ "/api/v1/brokerage-accounts/{accountId}/events",
+ { params: { path: { accountId: accountId ?? "" } } },
+ ),
+ });
+}
diff --git a/src/entities/brokerage-event/index.ts b/src/entities/brokerage-event/index.ts
new file mode 100644
index 0000000..79b8b4e
--- /dev/null
+++ b/src/entities/brokerage-event/index.ts
@@ -0,0 +1,4 @@
+export {
+ type BrokerageEvent,
+ useBrokerageEvents,
+} from "./api/useBrokerageEvents";
diff --git a/src/pages/account-events/index.ts b/src/pages/account-events/index.ts
new file mode 100644
index 0000000..73763c3
--- /dev/null
+++ b/src/pages/account-events/index.ts
@@ -0,0 +1 @@
+export { AccountEventsPage } from "./ui/AccountEventsPage";
diff --git a/src/pages/account-events/ui/AccountEventsPage.test.tsx b/src/pages/account-events/ui/AccountEventsPage.test.tsx
new file mode 100644
index 0000000..efe7532
--- /dev/null
+++ b/src/pages/account-events/ui/AccountEventsPage.test.tsx
@@ -0,0 +1,102 @@
+import { MantineProvider } from "@mantine/core";
+import { render, screen } from "@testing-library/react";
+import { MemoryRouter } from "react-router";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { useBrokerageEvents } from "@/entities/brokerage-event";
+import { AccountEventsPage } from "./AccountEventsPage";
+
+vi.mock("@/entities/brokerage-event", () => ({
+ useBrokerageEvents: vi.fn(),
+}));
+vi.mock("@/features/auth", () => ({
+ useAuth: () => ({ accessToken: "token" }),
+}));
+
+const query = vi.mocked(useBrokerageEvents);
+
+function renderPage() {
+ return render(
+
+
+
+
+ ,
+ );
+}
+
+describe("AccountEventsPage", () => {
+ beforeEach(() => {
+ query.mockReturnValue({
+ data: {
+ accountId: "account",
+ calculatedAt: "2026-08-14T00:00:00Z",
+ events: [],
+ },
+ isError: false,
+ isLoading: false,
+ refetch: vi.fn(),
+ } as never);
+ });
+
+ it("renders events and marks an offer without an amount", () => {
+ query.mockReturnValue({
+ data: {
+ accountId: "account",
+ calculatedAt: "2026-08-14T00:00:00Z",
+ events: [
+ {
+ amount: "1250",
+ currency: "RUB",
+ date: "2026-08-20",
+ dateKind: "event",
+ displayName: "ОФЗ 26238",
+ figi: "FIGI",
+ instrumentUid: "UID",
+ ticker: "OFZ",
+ type: "coupon",
+ },
+ {
+ amount: null,
+ currency: null,
+ date: "2026-08-25",
+ dateKind: "event",
+ displayName: "ОФЗ 26238",
+ figi: "FIGI",
+ instrumentUid: "UID",
+ ticker: "OFZ",
+ type: "offer",
+ },
+ ],
+ },
+ isError: false,
+ isLoading: false,
+ refetch: vi.fn(),
+ } as never);
+ renderPage();
+ expect(screen.getByText("1 250 ₽")).toBeInTheDocument();
+ expect(screen.getByText("Сумма неизвестна")).toBeInTheDocument();
+ });
+
+ it("renders empty and retryable error states", () => {
+ const refetch = vi.fn();
+ const { rerender } = renderPage();
+ expect(
+ screen.getByText("На ближайшие 30 дней событий не найдено"),
+ ).toBeInTheDocument();
+ query.mockReturnValue({
+ data: undefined,
+ isError: true,
+ isLoading: false,
+ refetch,
+ } as never);
+ rerender(
+
+
+
+
+ ,
+ );
+ screen.getByRole("button", { name: "Повторить" }).click();
+ expect(refetch).toHaveBeenCalledOnce();
+ });
+});
diff --git a/src/pages/account-events/ui/AccountEventsPage.tsx b/src/pages/account-events/ui/AccountEventsPage.tsx
new file mode 100644
index 0000000..3e9d861
--- /dev/null
+++ b/src/pages/account-events/ui/AccountEventsPage.tsx
@@ -0,0 +1,105 @@
+import { Button, Card, Group, Stack, Text, Title } from "@mantine/core";
+import { useTranslation } from "react-i18next";
+import { useParams } from "react-router";
+import {
+ type BrokerageEvent,
+ useBrokerageEvents,
+} from "@/entities/brokerage-event";
+import { formatMoney } from "@/entities/portfolio";
+import { useAuth } from "@/features/auth";
+import { StateView } from "@/shared/ui";
+
+export function AccountEventsPage() {
+ const { accountId } = useParams();
+ const { accessToken } = useAuth();
+ const events = useBrokerageEvents(accountId, accessToken);
+ if (events.isLoading) return ;
+ if (events.isError)
+ return events.refetch()} />;
+ if (!events.data.events.length) return ;
+ return ;
+}
+
+function AccountEventsPageLoading() {
+ const { t } = useTranslation();
+ return (
+
+ );
+}
+
+function AccountEventsPageError({ retry }: { retry: () => void }) {
+ const { t } = useTranslation();
+ return (
+ {t("events.retry")}}
+ />
+ );
+}
+
+function AccountEventsPageEmpty() {
+ const { t } = useTranslation();
+ return (
+
+ {t("events.title")}
+ {t("events.hint")}
+
+
+ );
+}
+
+function AccountEventsPageLoaded({ events }: { events: BrokerageEvent[] }) {
+ const { t } = useTranslation();
+ return (
+
+ {t("events.title")}
+ {t("events.hint")}
+ {events.map((event) => (
+
+ ))}
+
+ );
+}
+
+function AccountEventRow({ event }: { event: BrokerageEvent }) {
+ const { t } = useTranslation();
+ return (
+
+
+
+ {event.displayName}
+
+ {formatCalendarDate(event.date)} ·{" "}
+ {t(`events.dateKind.${event.dateKind}`)}
+
+
+
+
+ {event.amount && event.currency
+ ? formatMoney({ currency: event.currency, value: event.amount })
+ : t("events.noAmount")}
+
+
+ {t(`events.type.${event.type}`)}
+
+
+
+
+ );
+}
+
+function formatCalendarDate(date: string) {
+ return new Intl.DateTimeFormat("ru-RU", {
+ dateStyle: "medium",
+ timeZone: "UTC",
+ }).format(new Date(`${date}T00:00:00Z`));
+}
diff --git a/src/pages/account/ui/AccountPage.test.tsx b/src/pages/account/ui/AccountPage.test.tsx
index 0377d4d..7beca37 100644
--- a/src/pages/account/ui/AccountPage.test.tsx
+++ b/src/pages/account/ui/AccountPage.test.tsx
@@ -141,4 +141,26 @@ describe("AccountPage states", () => {
);
expect(screen.getByText("Вторая")).toBeInTheDocument();
});
+
+ it("keeps the portfolio visible when upcoming events fail", async () => {
+ server.use(
+ http.get("*/api/v1/brokerage-accounts/:accountId/events", () =>
+ HttpResponse.json({ code: "MOEX_UNAVAILABLE" }, { status: 503 }),
+ ),
+ );
+ renderPage(
+ ,
+ );
+
+ await waitFor(() =>
+ expect(
+ screen.getByText("Не удалось загрузить будущие события."),
+ ).toBeInTheDocument(),
+ );
+ expect(screen.getByText("Вторая")).toBeInTheDocument();
+ });
});
diff --git a/src/pages/account/ui/AccountPage.view.tsx b/src/pages/account/ui/AccountPage.view.tsx
index 3b031df..922d0d3 100644
--- a/src/pages/account/ui/AccountPage.view.tsx
+++ b/src/pages/account/ui/AccountPage.view.tsx
@@ -14,6 +14,10 @@ import { Link } from "react-router";
import { useBrokerageOperations } from "@/entities/broker-operation";
import type { BrokerageAccount } from "@/entities/brokerage-account";
import { accountTypeKey } from "@/entities/brokerage-account";
+import {
+ type BrokerageEvent,
+ useBrokerageEvents,
+} from "@/entities/brokerage-event";
import type { Portfolio } from "@/entities/portfolio";
import { formatMoney, formatSnapshotTime } from "@/entities/portfolio";
import { StateView } from "@/shared/ui";
@@ -99,6 +103,7 @@ export function AccountPageEmpty({
text={t("portfolio.emptyText")}
/>
+
);
}
@@ -166,6 +171,7 @@ export function AccountPageLoaded({
accountId={accountId}
accessToken={accessToken ?? null}
/>
+
);
}
@@ -211,6 +217,55 @@ function RecentOperations({
);
}
+function RecentEvents({
+ accountId,
+ accessToken,
+}: {
+ accountId: string | undefined;
+ accessToken: string | null;
+}) {
+ const { t } = useTranslation();
+ const events = useBrokerageEvents(accountId, accessToken);
+ return (
+
+ {events.isLoading ? (
+ {t("events.recentLoading")}
+ ) : null}
+ {events.isError ? (
+ {t("events.recentError")}
+ ) : null}
+ {events.data?.events.length === 0 ? (
+ {t("events.recentEmpty")}
+ ) : null}
+ {events.data?.events.slice(0, 5).map((event) => (
+
+ ))}
+
+
+ );
+}
+function RecentEvent({ event }: { event: BrokerageEvent }) {
+ const { t } = useTranslation();
+ return (
+
+ {`${t(`events.type.${event.type}`)} · ${event.displayName}`}
+
+ {event.amount && event.currency
+ ? formatMoney({ currency: event.currency, value: event.amount })
+ : t("events.noAmount")}
+
+
+ );
+}
function PositionSection({
title,
positions,
diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts
index 6ca14ee..b990e65 100644
--- a/src/shared/api/generated/schema.d.ts
+++ b/src/shared/api/generated/schema.d.ts
@@ -148,6 +148,22 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/api/v1/brokerage-accounts/{accountId}/events": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get: operations["UpcomingEventsController_get"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
}
export type webhooks = Record;
export interface components {
@@ -245,6 +261,26 @@ export interface components {
positions: components["schemas"]["PortfolioPositionDto"][];
cashBalances: components["schemas"]["MoneyDto"][];
};
+ UpcomingEventDto: {
+ /** @enum {string} */
+ type: "coupon" | "dividend" | "maturity" | "offer";
+ /** Format: date */
+ date: string;
+ /** @enum {string} */
+ dateKind: "event" | "registry";
+ figi: string | null;
+ instrumentUid: string | null;
+ ticker: string;
+ displayName: string;
+ currency: string | null;
+ amount: string | null;
+ };
+ UpcomingEventsDto: {
+ accountId: string;
+ /** Format: date-time */
+ calculatedAt: string;
+ events: components["schemas"]["UpcomingEventDto"][];
+ };
};
responses: never;
parameters: never;
@@ -311,6 +347,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
+ /** @description Upstream service unavailable */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["ProblemDetailsDto"];
+ };
+ };
};
};
AuthController_login: {
@@ -370,6 +415,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
+ /** @description Upstream service unavailable */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["ProblemDetailsDto"];
+ };
+ };
};
};
AuthController_refresh: {
@@ -425,6 +479,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
+ /** @description Upstream service unavailable */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["ProblemDetailsDto"];
+ };
+ };
};
};
AuthController_logout: {
@@ -478,6 +541,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
+ /** @description Upstream service unavailable */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["ProblemDetailsDto"];
+ };
+ };
};
};
AuthController_me: {
@@ -533,6 +605,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
+ /** @description Upstream service unavailable */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["ProblemDetailsDto"];
+ };
+ };
};
};
TbankConnectionController_status: {
@@ -588,6 +669,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
+ /** @description Upstream service unavailable */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["ProblemDetailsDto"];
+ };
+ };
};
};
TbankConnectionController_put: {
@@ -647,6 +737,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
+ /** @description Upstream service unavailable */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["ProblemDetailsDto"];
+ };
+ };
};
};
TbankConnectionController_delete: {
@@ -700,6 +799,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
+ /** @description Upstream service unavailable */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["ProblemDetailsDto"];
+ };
+ };
};
};
BrokerageAccountsController_list: {
@@ -755,6 +863,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
+ /** @description Upstream service unavailable */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["ProblemDetailsDto"];
+ };
+ };
};
};
BrokerageOperationsController_get: {
@@ -818,6 +935,15 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
+ /** @description Upstream service unavailable */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["ProblemDetailsDto"];
+ };
+ };
};
};
PortfolioController_get: {
@@ -875,6 +1001,81 @@ export interface operations {
"application/problem+json": components["schemas"]["ProblemDetailsDto"];
};
};
+ /** @description Upstream service unavailable */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["ProblemDetailsDto"];
+ };
+ };
+ };
+ };
+ UpcomingEventsController_get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ accountId: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["UpcomingEventsDto"];
+ };
+ };
+ /** @description Invalid request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["ProblemDetailsDto"];
+ };
+ };
+ /** @description Authentication required */
+ 401: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["ProblemDetailsDto"];
+ };
+ };
+ /** @description Resource not found */
+ 404: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["ProblemDetailsDto"];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["ProblemDetailsDto"];
+ };
+ };
+ /** @description Upstream service unavailable */
+ 503: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/problem+json": components["schemas"]["ProblemDetailsDto"];
+ };
+ };
};
};
}
diff --git a/src/shared/api/spec/openapi.json b/src/shared/api/spec/openapi.json
index 8052431..4a62ace 100644
--- a/src/shared/api/spec/openapi.json
+++ b/src/shared/api/spec/openapi.json
@@ -65,6 +65,16 @@
}
}
}
+ },
+ "503": {
+ "description": "Upstream service unavailable",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetailsDto"
+ }
+ }
+ }
}
},
"tags": [
@@ -136,6 +146,16 @@
}
}
}
+ },
+ "503": {
+ "description": "Upstream service unavailable",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetailsDto"
+ }
+ }
+ }
}
},
"tags": [
@@ -197,6 +217,16 @@
}
}
}
+ },
+ "503": {
+ "description": "Upstream service unavailable",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetailsDto"
+ }
+ }
+ }
}
},
"tags": [
@@ -251,6 +281,16 @@
}
}
}
+ },
+ "503": {
+ "description": "Upstream service unavailable",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetailsDto"
+ }
+ }
+ }
}
},
"tags": [
@@ -312,6 +352,16 @@
}
}
}
+ },
+ "503": {
+ "description": "Upstream service unavailable",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetailsDto"
+ }
+ }
+ }
}
},
"security": [
@@ -378,6 +428,16 @@
}
}
}
+ },
+ "503": {
+ "description": "Upstream service unavailable",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetailsDto"
+ }
+ }
+ }
}
},
"security": [
@@ -452,6 +512,16 @@
}
}
}
+ },
+ "503": {
+ "description": "Upstream service unavailable",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetailsDto"
+ }
+ }
+ }
}
},
"security": [
@@ -509,6 +579,16 @@
}
}
}
+ },
+ "503": {
+ "description": "Upstream service unavailable",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetailsDto"
+ }
+ }
+ }
}
},
"security": [
@@ -578,6 +658,16 @@
}
}
}
+ },
+ "503": {
+ "description": "Upstream service unavailable",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetailsDto"
+ }
+ }
+ }
}
},
"security": [
@@ -709,6 +799,16 @@
}
}
}
+ },
+ "503": {
+ "description": "Upstream service unavailable",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetailsDto"
+ }
+ }
+ }
}
},
"security": [
@@ -784,6 +884,101 @@
}
}
}
+ },
+ "503": {
+ "description": "Upstream service unavailable",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetailsDto"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ }
+ ],
+ "tags": [
+ "brokerage-accounts"
+ ]
+ }
+ },
+ "/api/v1/brokerage-accounts/{accountId}/events": {
+ "get": {
+ "operationId": "UpcomingEventsController_get",
+ "parameters": [
+ {
+ "name": "accountId",
+ "required": true,
+ "in": "path",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/UpcomingEventsDto"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid request",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetailsDto"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Authentication required",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetailsDto"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Resource not found",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetailsDto"
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetailsDto"
+ }
+ }
+ }
+ },
+ "503": {
+ "description": "Upstream service unavailable",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/ProblemDetailsDto"
+ }
+ }
+ }
}
},
"security": [
@@ -1194,6 +1389,87 @@
"positions",
"cashBalances"
]
+ },
+ "UpcomingEventDto": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "enum": [
+ "coupon",
+ "dividend",
+ "maturity",
+ "offer"
+ ]
+ },
+ "date": {
+ "type": "string",
+ "format": "date"
+ },
+ "dateKind": {
+ "type": "string",
+ "enum": [
+ "event",
+ "registry"
+ ]
+ },
+ "figi": {
+ "type": "string",
+ "nullable": true
+ },
+ "instrumentUid": {
+ "type": "string",
+ "nullable": true
+ },
+ "ticker": {
+ "type": "string"
+ },
+ "displayName": {
+ "type": "string"
+ },
+ "currency": {
+ "type": "string",
+ "nullable": true
+ },
+ "amount": {
+ "type": "string",
+ "nullable": true
+ }
+ },
+ "required": [
+ "type",
+ "date",
+ "dateKind",
+ "figi",
+ "instrumentUid",
+ "ticker",
+ "displayName",
+ "currency",
+ "amount"
+ ]
+ },
+ "UpcomingEventsDto": {
+ "type": "object",
+ "properties": {
+ "accountId": {
+ "type": "string"
+ },
+ "calculatedAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "events": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/UpcomingEventDto"
+ }
+ }
+ },
+ "required": [
+ "accountId",
+ "calculatedAt",
+ "events"
+ ]
}
}
}
diff --git a/src/shared/i18n/locales/ru.ts b/src/shared/i18n/locales/ru.ts
index 573ddd9..5897db7 100644
--- a/src/shared/i18n/locales/ru.ts
+++ b/src/shared/i18n/locales/ru.ts
@@ -7,6 +7,7 @@ export const ru = {
accounts: "Брокерские счета",
accountFallback: "Счет",
operations: "Операции",
+ events: "Будущие события",
settings: "Настройки",
},
auth: {
@@ -98,6 +99,29 @@ export const ru = {
other: "Прочее",
},
},
+ events: {
+ title: "Будущие события",
+ open: "Открыть будущие события",
+ recentTitle: "Будущие события",
+ recentLoading: "Загрузка будущих событий",
+ recentError: "Не удалось загрузить будущие события.",
+ recentEmpty: "На ближайшие 30 дней событий не найдено.",
+ hint: "Прогноз рассчитан по текущей позиции до налогов. Покрытие зависит от доступных данных MOEX.",
+ emptyTitle: "На ближайшие 30 дней событий не найдено",
+ emptyText:
+ "Доступные источники не сообщили о купонах, дивидендах, погашениях или офертах в этом периоде.",
+ errorTitle: "Не удалось загрузить будущие события",
+ errorText: "Попробуйте запросить прогноз еще раз.",
+ retry: "Повторить",
+ noAmount: "Сумма неизвестна",
+ dateKind: { event: "Дата события", registry: "Дата реестра" },
+ type: {
+ coupon: "Купон",
+ dividend: "Дивиденд",
+ maturity: "Погашение",
+ offer: "Оферта",
+ },
+ },
settings: { title: "Настройки" },
tbank: {
title: "T-Bank Invest",
diff --git a/src/test/handlers.ts b/src/test/handlers.ts
index 6f5e5bc..38ddb49 100644
--- a/src/test/handlers.ts
+++ b/src/test/handlers.ts
@@ -45,4 +45,11 @@ export const handlers = [
observedAt: "2026-08-14T00:00:00Z",
}),
),
+ http.get("*/api/v1/brokerage-accounts/:accountId/events", () =>
+ HttpResponse.json({
+ accountId: "account-1",
+ calculatedAt: "2026-08-14T00:00:00Z",
+ events: [],
+ }),
+ ),
];