From a209d715892862bb6d48084df0c84a10b8d56bf1 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Mon, 21 Sep 2026 21:09:23 +0300 Subject: [PATCH 1/8] fix(auth): isolate session races and financial caches TD-001 and TD-002: serialize cookie writes, ignore stale responses and reset financial queries on identity changes. Backend cookie lifecycle regression: ec5780d. --- src/features/auth/model/auth.test.tsx | 162 +++++++++++++++++++++++++- src/features/auth/model/auth.tsx | 89 +++++++++----- src/shared/api/client.test.ts | 29 +++++ src/shared/api/client.ts | 19 ++- 4 files changed, 261 insertions(+), 38 deletions(-) diff --git a/src/features/auth/model/auth.test.tsx b/src/features/auth/model/auth.test.tsx index ecc997d..e349958 100644 --- a/src/features/auth/model/auth.test.tsx +++ b/src/features/auth/model/auth.test.tsx @@ -3,7 +3,7 @@ import { act, renderHook, waitFor } from "@testing-library/react"; import { HttpResponse, http } from "msw"; import type { FC, PropsWithChildren } from "react"; import { describe, expect, it } from "vitest"; -import { apiClient, apiQueryClient } from "@/shared/api"; +import { apiClient, apiQueryClient, useAuthenticatedQuery } from "@/shared/api"; import { createTestQueryClient } from "@/test/query"; import { server } from "@/test/server"; import { AuthProvider, useAuth } from "./auth"; @@ -167,4 +167,164 @@ describe("AuthProvider", () => { expect(queryClient.getQueryData(portfolioKey)).toBeUndefined(); expect(queryClient.getQueryData(bankCardsKey)).toBeUndefined(); }); + it.each([ + [false, false], + [true, false], + [false, true], + [true, true], + ])( + "serializes late refresh (failure=%s, login=%s)", + async (failure, loginAfterLogout) => { + let release!: () => void; + const pending = new Promise((resolve) => { + release = resolve; + }); + let calls = 0; + const order: string[] = []; + server.use( + http.post("*/api/v1/auth/refresh", async () => { + if (++calls > 1) { + order.push("refresh-start"); + await pending; + order.push("refresh-cookie"); + } + return failure && calls > 1 + ? HttpResponse.error() + : HttpResponse.json({ + accessToken: "a", + user: { id: "a", email: "a@example.com" }, + }); + }), + http.post("*/api/v1/auth/logout", () => { + order.push("logout-cookie"); + return new HttpResponse(null, { status: 204 }); + }), + http.post("*/api/v1/auth/login", () => { + order.push("login-cookie"); + return HttpResponse.json({ + accessToken: "b", + user: { id: "b", email: "b@example.com" }, + }); + }), + http.get("*/api/v1/brokerage-accounts", () => + HttpResponse.json({ code: "UNAUTHORIZED" }, { status: 401 }), + ), + ); + const queryClient = createTestQueryClient(); + const wrapper: FC = ({ children }) => ( + + {children} + + ); + const { result } = renderHook(() => useAuth(), { wrapper }); + await waitFor(() => expect(result.current.ready).toBe(true)); + const oldRequest = apiClient + .GET("/api/v1/brokerage-accounts") + .catch(() => null); + await waitFor(() => expect(order).toContain("refresh-start")); + let logout!: Promise; + act(() => { + logout = result.current.logout(); + }); + await waitFor(() => expect(result.current.user).toBeNull()); + let login!: Promise; + act(() => { + login = loginAfterLogout + ? result.current.login("b@example.com", "password") + : Promise.resolve(); + }); + await act(async () => { + release(); + await Promise.all([oldRequest, logout, login]); + }); + expect(result.current.user?.id ?? null).toBe( + loginAfterLogout ? "b" : null, + ); + expect(order).toEqual([ + "refresh-start", + "refresh-cookie", + "logout-cookie", + ...(loginAfterLogout ? ["login-cookie"] : []), + ]); + }, + ); + + it("preserves same-user cache and removes inactive and pending data on refresh identity change", async () => { + let identity = "a"; + server.use( + http.post("*/api/v1/auth/refresh", () => + HttpResponse.json({ + accessToken: identity, + user: { id: identity, email: `${identity}@example.com` }, + }), + ), + ); + const queryClient = createTestQueryClient(); + const wrapper: FC = ({ children }) => ( + + {children} + + ); + const { result } = renderHook(() => useAuth(), { wrapper }); + await waitFor(() => expect(result.current.ready).toBe(true)); + queryClient.setQueryData(bankCardsKey, [{ id: "a-card" }]); + await act(() => result.current.refresh()); + expect(queryClient.getQueryData(bankCardsKey)).toEqual([{ id: "a-card" }]); + let release!: (value: string) => void; + const pending = queryClient + .fetchQuery({ + queryKey: ["pending-finance"], + queryFn: () => + new Promise((resolve) => { + release = resolve; + }), + }) + .catch(() => null); + identity = "b"; + await act(() => result.current.refresh()); + release("a-data"); + await pending; + expect(result.current.user?.id).toBe("b"); + expect(queryClient.getQueryData(bankCardsKey)).toBeUndefined(); + expect(queryClient.getQueryData(["pending-finance"])).toBeUndefined(); + }); + it("replaces an observed financial query when refresh changes identity", async () => { + let identity = "a"; + server.use( + http.post("*/api/v1/auth/refresh", () => + HttpResponse.json({ + accessToken: identity, + user: { id: identity, email: `${identity}@example.com` }, + }), + ), + http.get("*/api/v1/bank-cards", () => + HttpResponse.json([{ id: `${identity}-card` }]), + ), + ); + const queryClient = createTestQueryClient(); + const wrapper: FC = (props) => ( + + {props.children} + + ); + const { result } = renderHook( + () => ({ + auth: useAuth(), + cards: useAuthenticatedQuery( + apiQueryClient.queryOptions("get", "/api/v1/bank-cards", { + params: { query: { includeArchived: "false" } }, + }), + ), + }), + { wrapper }, + ); + await waitFor(() => + expect(result.current.cards.data?.[0].id).toBe("a-card"), + ); + identity = "b"; + await act(() => result.current.auth.refresh()); + await waitFor(() => + expect(result.current.cards.data?.[0].id).toBe("b-card"), + ); + }); }); diff --git a/src/features/auth/model/auth.tsx b/src/features/auth/model/auth.tsx index 6bd700c..16d13ca 100644 --- a/src/features/auth/model/auth.tsx +++ b/src/features/auth/model/auth.tsx @@ -47,49 +47,70 @@ const AuthProvider: FC = (props) => { const accessTokenRef = useRef(null); const [ready, setReady] = useState(false); + const generation = useRef(0); + const userRef = useRef(null); + const authQueue = useRef>(Promise.resolve()); const refreshPromise = useRef | null>(null); - const applyAuthentication = useCallback((authentication: Authentication) => { - setUser(authentication.user); - setAccessToken(authentication.accessToken); - accessTokenRef.current = authentication.accessToken; + // Cookie-writing requests share ordering, including across tabs with Web Locks. + const serialize = useCallback((run: () => Promise): Promise => { + const next = authQueue.current.then(async () => + "locks" in navigator + ? navigator.locks.request("wealth-ledger-refresh", run) + : run(), + ); + authQueue.current = next.catch(() => undefined); + return next; }, []); + const applyAuthentication = useCallback( + (authentication: Authentication) => { + const changedIdentity = + userRef.current && userRef.current.id !== authentication.user.id; + accessTokenRef.current = authentication.accessToken; + if (changedIdentity) { + generation.current += 1; + queryClient.removeQueries({ type: "inactive" }); + queryClient.getMutationCache().clear(); + void queryClient.resetQueries(); + } + userRef.current = authentication.user; + setUser(authentication.user); + setAccessToken(authentication.accessToken); + }, + [queryClient], + ); const clearSession = useCallback(() => { + generation.current += 1; + userRef.current = null; setUser(null); setAccessToken(null); accessTokenRef.current = null; queryClient.clear(); }, [queryClient]); - const refresh = useCallback(async (): Promise => { + const refresh = useCallback((): Promise => { if (refreshPromise.current) return refreshPromise.current; - refreshPromise.current = (async () => { - const run = async () => { - try { - const { data } = await apiClient.POST("/api/v1/auth/refresh"); - if (!data) { - clearSession(); - return false; - } - applyAuthentication(data as Authentication); - return true; - } catch { + const started = generation.current; + const pending = serialize(async () => { + if (started !== generation.current) return false; + try { + const { data } = await apiClient.POST("/api/v1/auth/refresh"); + if (started !== generation.current) return false; + if (!data) { clearSession(); return false; } - }; - try { - return "locks" in navigator - ? navigator.locks.request("wealth-ledger-refresh", run) - : run(); + applyAuthentication(data); + return true; } catch { - clearSession(); + if (started === generation.current) clearSession(); return false; } - })().finally(() => { - refreshPromise.current = null; + }).finally(() => { + if (refreshPromise.current === pending) refreshPromise.current = null; }); - return refreshPromise.current; - }, [applyAuthentication, clearSession]); + refreshPromise.current = pending; + return pending; + }, [applyAuthentication, clearSession, serialize]); useEffect(() => { void refresh().finally(() => setReady(true)); @@ -100,21 +121,25 @@ const AuthProvider: FC = (props) => { email: string, password: string, ) { - const authentication = await request(path, email, password); - applyAuthentication(authentication); + clearSession(); + const started = generation.current; + await serialize(async () => { + const authentication = await request(path, email, password); + if (started === generation.current) applyAuthentication(authentication); + }); } async function logout() { - try { + clearSession(); + await serialize(async () => { await apiClient.POST("/api/v1/auth/logout"); - } finally { - clearSession(); - } + }); } useEffect(() => { configureAuthClient({ accessToken: () => accessTokenRef.current, + generation: () => generation.current, refresh, clear: clearSession, }); diff --git a/src/shared/api/client.test.ts b/src/shared/api/client.test.ts index fa83613..e679c1a 100644 --- a/src/shared/api/client.test.ts +++ b/src/shared/api/client.test.ts @@ -64,4 +64,33 @@ describe("apiClient authentication", () => { expect(authorization).toBeNull(); }); + it("discards a retried request response after another session takes over", async () => { + let generation = 0; + let calls = 0; + let release!: () => void; + const pending = new Promise((resolve) => { + release = resolve; + }); + server.use( + http.get("*/api/v1/brokerage-accounts", async () => { + if (++calls === 1) + return HttpResponse.json({ code: "UNAUTHORIZED" }, { status: 401 }); + await pending; + return HttpResponse.json([{ id: "previous-user-account" }]); + }), + ); + configureAuthClient({ + accessToken: () => "synthetic", + generation: () => generation, + refresh: async () => true, + clear: vi.fn(), + }); + const response = expect( + apiClient.GET("/api/v1/brokerage-accounts"), + ).rejects.toMatchObject({ name: "AbortError" }); + await vi.waitFor(() => expect(calls).toBe(2)); + generation += 1; + release(); + await response; + }); }); diff --git a/src/shared/api/client.ts b/src/shared/api/client.ts index cb87d40..d73ea56 100644 --- a/src/shared/api/client.ts +++ b/src/shared/api/client.ts @@ -8,6 +8,7 @@ const HTTP_UNAUTHORIZED = 401; const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:3000"; type AuthHandlers = { + generation?: () => number; accessToken: () => string | null; refresh: () => Promise; clear: () => void; @@ -41,7 +42,10 @@ async function authenticatedFetch(request: Request): Promise { const isAuthRequest = new URL(request.url).pathname.startsWith( "/api/v1/auth/", ); - const token = auth?.accessToken(); + const session = auth; + const generation = session?.generation?.(); + const isCurrent = () => generation === auth?.generation?.(); + const token = session?.accessToken(); const withToken = (source: Request, value: string) => { const headers = new Headers(source.headers); headers.set("Authorization", `Bearer ${value}`); @@ -51,6 +55,8 @@ async function authenticatedFetch(request: Request): Promise { token && !isAuthRequest ? withToken(request, token) : request; const retryRequest = authorized.clone(); const response = await fetch(authorized); + if (!isAuthRequest && !isCurrent()) + throw new DOMException("Session changed", "AbortError"); if ( response.status !== HTTP_UNAUTHORIZED || isAuthRequest || @@ -63,14 +69,17 @@ async function authenticatedFetch(request: Request): Promise { )?.code !== "UNAUTHORIZED" ) return response; + if (!isCurrent()) throw new DOMException("Session changed", "AbortError"); if (!(await auth.refresh())) { - auth.clear(); + if (isCurrent()) auth.clear(); return response; } + if (!isCurrent()) throw new DOMException("Session changed", "AbortError"); const refreshedToken = auth.accessToken(); - return refreshedToken - ? fetch(withToken(retryRequest, refreshedToken)) - : response; + if (!refreshedToken) return response; + const retried = await fetch(withToken(retryRequest, refreshedToken)); + if (!isCurrent()) throw new DOMException("Session changed", "AbortError"); + return retried; } const apiClient = createClient({ -- 2.54.0 From db184c8caec01609e8a3df3b16097ce634e5eff0 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Mon, 21 Sep 2026 21:09:41 +0300 Subject: [PATCH 2/8] fix(api): update validation and conflict response contract TD-003 and TD-005: explicitly deliver OpenAPI and generated types from backend commit 1667991. --- src/shared/api/generated/schema.d.ts | 2502 ++++++++------- src/shared/api/spec/openapi.json | 4309 ++++++++++++++------------ 2 files changed, 3681 insertions(+), 3130 deletions(-) diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts index 7f76b34..3a17015 100644 --- a/src/shared/api/generated/schema.d.ts +++ b/src/shared/api/generated/schema.d.ts @@ -228,6 +228,182 @@ export interface paths { patch: operations["BankCardsController_update"]; trace?: never; }; + "/api/v1/brokerage-accounts": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["BrokerageAccountsController_list"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/brokerage-accounts/all/portfolio": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["PortfolioController_all"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/brokerage-accounts/{accountId}/portfolio": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["PortfolioController_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/brokerage-accounts/all/operations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["BrokerageOperationsController_all"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/brokerage-accounts/{accountId}/operations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["BrokerageOperationsController_get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/brokerage-accounts/all/income-summary": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["IncomeAnalyticsController_allSummary"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/brokerage-accounts/all/income-analytics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["IncomeAnalyticsController_allAnalytics"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/brokerage-accounts/{accountId}/income-summary": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["IncomeAnalyticsController_summary"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/brokerage-accounts/{accountId}/income-analytics": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["IncomeAnalyticsController_analytics"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/brokerage-accounts/all/events": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["UpcomingEventsController_all"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + 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; + }; "/api/v1/stocks": { parameters: { query?: never; @@ -500,186 +676,14 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/brokerage-accounts": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["BrokerageAccountsController_list"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/brokerage-accounts/all/operations": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["BrokerageOperationsController_all"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/brokerage-accounts/{accountId}/operations": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["BrokerageOperationsController_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/brokerage-accounts/all/portfolio": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["PortfolioController_all"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/brokerage-accounts/{accountId}/portfolio": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["PortfolioController_get"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/brokerage-accounts/all/events": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["UpcomingEventsController_all"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - 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; - }; - "/api/v1/brokerage-accounts/all/income-summary": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["IncomeAnalyticsController_allSummary"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/brokerage-accounts/all/income-analytics": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["IncomeAnalyticsController_allAnalytics"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/brokerage-accounts/{accountId}/income-summary": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["IncomeAnalyticsController_summary"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - "/api/v1/brokerage-accounts/{accountId}/income-analytics": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get: operations["IncomeAnalyticsController_analytics"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; } export type webhooks = Record; export interface components { schemas: { + ValidationProblemDto: { + pointer: string; + detail: string; + }; ProblemDetailsDto: { /** @example 401 */ status: number; @@ -693,7 +697,7 @@ export interface components { * } * ] */ - errors?: string[]; + errors?: components["schemas"]["ValidationProblemDto"][]; }; CredentialsDto: { /** @example user@example.com */ @@ -846,6 +850,206 @@ export interface components { version: number; archived?: boolean; }; + PortfolioSummaryDto: { + totalsByCurrency: components["schemas"]["MoneyDto"][]; + /** @description Number of non-zero stock positions, not the number of units. */ + stockPositionCount: number; + /** @description Number of non-zero bond positions, not the number of units. */ + bondPositionCount: number; + }; + BrokerageAccountDto: { + id: string; + name: string; + /** @enum {string} */ + type: "BROKERAGE" | "INDIVIDUAL_INVESTMENT" | "INVEST_BOX" | "MONEY_MARKET_FUND"; + /** Format: date */ + openedOn: string | null; + portfolioSummary: components["schemas"]["PortfolioSummaryDto"] | null; + }; + AccountReferenceDto: { + id: string; + name: string; + }; + PortfolioPositionDto: { + instrumentUid?: string | null; + account?: components["schemas"]["AccountReferenceDto"]; + figi: string | null; + displayName: string; + /** @enum {string} */ + type: "STOCK" | "BOND" | "FUND" | "CURRENCY" | "FUTURE" | "OPTION" | "STRUCTURED_NOTE" | "DIGITAL_FINANCIAL_ASSET" | "OTHER"; + quantity: string; + currentValue: components["schemas"]["MoneyDto"] | null; + averagePurchasePrice: components["schemas"]["MoneyDto"] | null; + absoluteReturn: components["schemas"]["MoneyDto"] | null; + isin: string | null; + ticker: string | null; + logoName: string | null; + }; + PortfolioStructureCategoryDto: { + /** @enum {string} */ + type: "STOCK" | "BOND" | "OTHER" | "CASH"; + /** @description Exact known value as a decimal string. */ + value: string; + /** @description Percentage including cash, rounded to six decimal places. Null for incomplete, negative or zero-total compositions. */ + sharePercent: string | null; + }; + PortfolioStructureCurrencyDto: { + currency: string; + totalValue: string; + /** @description Any negative constituent, even if netted positive. */ + hasNegativeValues: boolean; + categories: components["schemas"]["PortfolioStructureCategoryDto"][]; + }; + PortfolioUnvaluedCategoryDto: { + /** @enum {string} */ + type: "STOCK" | "BOND" | "OTHER"; + count: number; + }; + PortfolioStructureDto: { + currencies: components["schemas"]["PortfolioStructureCurrencyDto"][]; + /** @description Positions without current value and known valuation currency. Shares are unavailable across all currencies when nonempty. */ + unvaluedCategories: components["schemas"]["PortfolioUnvaluedCategoryDto"][]; + }; + PortfolioDto: { + /** Format: date-time */ + lastSuccessfulAt: string; + isStale: boolean; + totalsByCurrency: components["schemas"]["MoneyDto"][]; + positions: components["schemas"]["PortfolioPositionDto"][]; + cashBalances: components["schemas"]["MoneyDto"][]; + structure: components["schemas"]["PortfolioStructureDto"]; + }; + BrokerageOperationDto: { + account?: components["schemas"]["AccountReferenceDto"]; + id: string; + /** Format: date-time */ + occurredAt: string; + type: string; + /** @enum {string} */ + category: "trade" | "income" | "tax" | "fee" | "transfer" | "other"; + description: string; + figi: string | null; + instrumentUid: string | null; + ticker: string | null; + instrumentType: string | null; + logoName: string | null; + payment: components["schemas"]["MoneyDto"] | null; + quantity: string | null; + price: components["schemas"]["MoneyDto"] | null; + accruedInterest: components["schemas"]["MoneyDto"] | null; + commission: components["schemas"]["MoneyDto"] | null; + }; + BrokerageOperationsPageDto: { + accountId: string; + items: components["schemas"]["BrokerageOperationDto"][]; + nextCursor: string | null; + hasNext: boolean; + /** Format: date-time */ + observedAt: string; + }; + IncomePeriodDto: { + /** Format: date */ + from: string; + /** Format: date */ + to: string; + }; + IncomeCurrencySummaryDto: { + currency: string; + coupon: components["schemas"]["MoneyDto"]; + dividend: components["schemas"]["MoneyDto"]; + total: components["schemas"]["MoneyDto"]; + }; + IncomeSummaryDto: { + accountId: string; + period: components["schemas"]["IncomePeriodDto"]; + totalsByCurrency: components["schemas"]["IncomeCurrencySummaryDto"][]; + /** Format: date-time */ + calculatedAt: string; + }; + IncomeComparisonCurrencyDto: { + currency: string; + selected: components["schemas"]["IncomeCurrencySummaryDto"]; + compared: components["schemas"]["IncomeCurrencySummaryDto"]; + difference: components["schemas"]["IncomeCurrencySummaryDto"]; + }; + IncomeComparisonDto: { + period: components["schemas"]["IncomePeriodDto"]; + totalsByCurrency: components["schemas"]["IncomeComparisonCurrencyDto"][]; + }; + IncomeBucketDto: { + period: components["schemas"]["IncomePeriodDto"]; + totalsByCurrency: components["schemas"]["IncomeCurrencySummaryDto"][]; + }; + IncomeInstrumentDto: { + key: string; + figi: string | null; + instrumentUid: string | null; + ticker: string | null; + instrumentType: string | null; + displayName: string; + coupon: components["schemas"]["MoneyDto"]; + dividend: components["schemas"]["MoneyDto"]; + total: components["schemas"]["MoneyDto"]; + receiptCount: number; + }; + IncomeAnalyticsEntryDto: { + account?: components["schemas"]["AccountReferenceDto"]; + id: string; + /** Format: date-time */ + occurredAt: string; + type: string; + /** @enum {string} */ + category: "trade" | "income" | "tax" | "fee" | "transfer" | "other"; + description: string; + figi: string | null; + instrumentUid: string | null; + ticker: string | null; + instrumentType: string | null; + logoName: string | null; + payment: components["schemas"]["MoneyDto"] | null; + quantity: string | null; + price: components["schemas"]["MoneyDto"] | null; + accruedInterest: components["schemas"]["MoneyDto"] | null; + commission: components["schemas"]["MoneyDto"] | null; + instrumentKey: string; + }; + IncomeAnalyticsDto: { + accountId: string; + period: components["schemas"]["IncomePeriodDto"]; + totalsByCurrency: components["schemas"]["IncomeCurrencySummaryDto"][]; + /** Format: date-time */ + calculatedAt: string; + previous: components["schemas"]["IncomeComparisonDto"]; + yearComparison: components["schemas"]["IncomeComparisonDto"] | null; + /** @enum {string} */ + bucketGranularity: "day" | "week" | "month"; + buckets: components["schemas"]["IncomeBucketDto"][]; + instruments: components["schemas"]["IncomeInstrumentDto"][]; + entries: components["schemas"]["IncomeAnalyticsEntryDto"][]; + }; + UpcomingEventDto: { + account?: components["schemas"]["AccountReferenceDto"]; + /** @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; + isin: string | null; + logoName: string | null; + projectedAmount: components["schemas"]["MoneyDto"] | null; + }; + UpcomingEventsDto: { + accountId: string; + /** Format: date-time */ + calculatedAt: string; + events: components["schemas"]["UpcomingEventDto"][]; + incomeTotalsByCurrency: components["schemas"]["MoneyDto"][]; + }; StockSearchResultDto: { /** Format: uuid */ instrumentUid: string; @@ -915,10 +1119,6 @@ export interface components { /** Format: date-time */ observedAt: string; }; - AccountReferenceDto: { - id: string; - name: string; - }; StockPositionRowDto: { instrumentUid?: string | null; account?: components["schemas"]["AccountReferenceDto"]; @@ -1154,202 +1354,6 @@ export interface components { PutTbankConnectionDto: { token: string; }; - PortfolioSummaryDto: { - totalsByCurrency: components["schemas"]["MoneyDto"][]; - /** @description Number of non-zero stock positions, not the number of units. */ - stockPositionCount: number; - /** @description Number of non-zero bond positions, not the number of units. */ - bondPositionCount: number; - }; - BrokerageAccountDto: { - id: string; - name: string; - /** @enum {string} */ - type: "BROKERAGE" | "INDIVIDUAL_INVESTMENT" | "INVEST_BOX" | "MONEY_MARKET_FUND"; - /** Format: date */ - openedOn: string | null; - portfolioSummary: components["schemas"]["PortfolioSummaryDto"] | null; - }; - BrokerageOperationDto: { - account?: components["schemas"]["AccountReferenceDto"]; - id: string; - /** Format: date-time */ - occurredAt: string; - type: string; - /** @enum {string} */ - category: "trade" | "income" | "tax" | "fee" | "transfer" | "other"; - description: string; - figi: string | null; - instrumentUid: string | null; - ticker: string | null; - instrumentType: string | null; - logoName: string | null; - payment: components["schemas"]["MoneyDto"] | null; - quantity: string | null; - price: components["schemas"]["MoneyDto"] | null; - accruedInterest: components["schemas"]["MoneyDto"] | null; - commission: components["schemas"]["MoneyDto"] | null; - }; - BrokerageOperationsPageDto: { - accountId: string; - items: components["schemas"]["BrokerageOperationDto"][]; - nextCursor: string | null; - hasNext: boolean; - /** Format: date-time */ - observedAt: string; - }; - PortfolioPositionDto: { - instrumentUid?: string | null; - account?: components["schemas"]["AccountReferenceDto"]; - figi: string | null; - displayName: string; - /** @enum {string} */ - type: "STOCK" | "BOND" | "FUND" | "CURRENCY" | "FUTURE" | "OPTION" | "STRUCTURED_NOTE" | "DIGITAL_FINANCIAL_ASSET" | "OTHER"; - quantity: string; - currentValue: components["schemas"]["MoneyDto"] | null; - averagePurchasePrice: components["schemas"]["MoneyDto"] | null; - absoluteReturn: components["schemas"]["MoneyDto"] | null; - isin: string | null; - ticker: string | null; - logoName: string | null; - }; - PortfolioStructureCategoryDto: { - /** @enum {string} */ - type: "STOCK" | "BOND" | "OTHER" | "CASH"; - /** @description Exact known value as a decimal string. */ - value: string; - /** @description Percentage including cash, rounded to six decimal places. Null for incomplete, negative or zero-total compositions. */ - sharePercent: string | null; - }; - PortfolioStructureCurrencyDto: { - currency: string; - totalValue: string; - /** @description Any negative constituent, even if netted positive. */ - hasNegativeValues: boolean; - categories: components["schemas"]["PortfolioStructureCategoryDto"][]; - }; - PortfolioUnvaluedCategoryDto: { - /** @enum {string} */ - type: "STOCK" | "BOND" | "OTHER"; - count: number; - }; - PortfolioStructureDto: { - currencies: components["schemas"]["PortfolioStructureCurrencyDto"][]; - /** @description Positions without current value and known valuation currency. Shares are unavailable across all currencies when nonempty. */ - unvaluedCategories: components["schemas"]["PortfolioUnvaluedCategoryDto"][]; - }; - PortfolioDto: { - /** Format: date-time */ - lastSuccessfulAt: string; - isStale: boolean; - totalsByCurrency: components["schemas"]["MoneyDto"][]; - positions: components["schemas"]["PortfolioPositionDto"][]; - cashBalances: components["schemas"]["MoneyDto"][]; - structure: components["schemas"]["PortfolioStructureDto"]; - }; - UpcomingEventDto: { - account?: components["schemas"]["AccountReferenceDto"]; - /** @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; - isin: string | null; - logoName: string | null; - projectedAmount: components["schemas"]["MoneyDto"] | null; - }; - UpcomingEventsDto: { - accountId: string; - /** Format: date-time */ - calculatedAt: string; - events: components["schemas"]["UpcomingEventDto"][]; - incomeTotalsByCurrency: components["schemas"]["MoneyDto"][]; - }; - IncomePeriodDto: { - /** Format: date */ - from: string; - /** Format: date */ - to: string; - }; - IncomeCurrencySummaryDto: { - currency: string; - coupon: components["schemas"]["MoneyDto"]; - dividend: components["schemas"]["MoneyDto"]; - total: components["schemas"]["MoneyDto"]; - }; - IncomeSummaryDto: { - accountId: string; - period: components["schemas"]["IncomePeriodDto"]; - totalsByCurrency: components["schemas"]["IncomeCurrencySummaryDto"][]; - /** Format: date-time */ - calculatedAt: string; - }; - IncomeComparisonCurrencyDto: { - currency: string; - selected: components["schemas"]["IncomeCurrencySummaryDto"]; - compared: components["schemas"]["IncomeCurrencySummaryDto"]; - difference: components["schemas"]["IncomeCurrencySummaryDto"]; - }; - IncomeComparisonDto: { - period: components["schemas"]["IncomePeriodDto"]; - totalsByCurrency: components["schemas"]["IncomeComparisonCurrencyDto"][]; - }; - IncomeBucketDto: { - period: components["schemas"]["IncomePeriodDto"]; - totalsByCurrency: components["schemas"]["IncomeCurrencySummaryDto"][]; - }; - IncomeInstrumentDto: { - key: string; - figi: string | null; - instrumentUid: string | null; - ticker: string | null; - instrumentType: string | null; - displayName: string; - coupon: components["schemas"]["MoneyDto"]; - dividend: components["schemas"]["MoneyDto"]; - total: components["schemas"]["MoneyDto"]; - receiptCount: number; - }; - IncomeAnalyticsEntryDto: { - account?: components["schemas"]["AccountReferenceDto"]; - id: string; - /** Format: date-time */ - occurredAt: string; - type: string; - /** @enum {string} */ - category: "trade" | "income" | "tax" | "fee" | "transfer" | "other"; - description: string; - figi: string | null; - instrumentUid: string | null; - ticker: string | null; - instrumentType: string | null; - logoName: string | null; - payment: components["schemas"]["MoneyDto"] | null; - quantity: string | null; - price: components["schemas"]["MoneyDto"] | null; - accruedInterest: components["schemas"]["MoneyDto"] | null; - commission: components["schemas"]["MoneyDto"] | null; - instrumentKey: string; - }; - IncomeAnalyticsDto: { - accountId: string; - period: components["schemas"]["IncomePeriodDto"]; - totalsByCurrency: components["schemas"]["IncomeCurrencySummaryDto"][]; - /** Format: date-time */ - calculatedAt: string; - previous: components["schemas"]["IncomeComparisonDto"]; - yearComparison: components["schemas"]["IncomeComparisonDto"] | null; - /** @enum {string} */ - bucketGranularity: "day" | "week" | "month"; - buckets: components["schemas"]["IncomeBucketDto"][]; - instruments: components["schemas"]["IncomeInstrumentDto"][]; - entries: components["schemas"]["IncomeAnalyticsEntryDto"][]; - }; }; responses: never; parameters: never; @@ -1407,6 +1411,15 @@ export interface operations { "application/problem+json": components["schemas"]["ProblemDetailsDto"]; }; }; + /** @description Email already registered */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetailsDto"]; + }; + }; /** @description Internal server error */ 500: { headers: { @@ -2895,6 +2908,851 @@ export interface operations { }; }; }; + BrokerageAccountsController_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrokerageAccountDto"][]; + }; + }; + /** @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 T-Bank connection required */ + 409: { + 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"]; + }; + }; + }; + }; + PortfolioController_all: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PortfolioDto"]; + }; + }; + /** @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 T-Bank connection required */ + 409: { + 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"]; + }; + }; + }; + }; + PortfolioController_get: { + parameters: { + query?: never; + header?: never; + path: { + accountId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PortfolioDto"]; + }; + }; + /** @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 T-Bank connection required */ + 409: { + 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"]; + }; + }; + }; + }; + BrokerageOperationsController_all: { + parameters: { + query?: { + /** @description Instrument UID. Without from, includes history since the earliest available account opening. */ + instrumentUid?: string; + from?: string; + to?: string; + categories?: ("trade" | "income" | "tax" | "fee" | "transfer" | "other")[]; + cursor?: string; + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrokerageOperationsPageDto"]; + }; + }; + /** @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 T-Bank connection required */ + 409: { + 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"]; + }; + }; + }; + }; + BrokerageOperationsController_get: { + parameters: { + query?: { + /** @description Instrument UID. Without from, includes history since the earliest available account opening. */ + instrumentUid?: string; + from?: string; + to?: string; + categories?: ("trade" | "income" | "tax" | "fee" | "transfer" | "other")[]; + cursor?: string; + limit?: number; + }; + header?: never; + path: { + accountId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BrokerageOperationsPageDto"]; + }; + }; + /** @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 T-Bank connection required */ + 409: { + 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"]; + }; + }; + }; + }; + IncomeAnalyticsController_allSummary: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["IncomeSummaryDto"]; + }; + }; + /** @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 T-Bank connection required */ + 409: { + 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"]; + }; + }; + }; + }; + IncomeAnalyticsController_allAnalytics: { + parameters: { + query?: { + from?: string; + to?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["IncomeAnalyticsDto"]; + }; + }; + /** @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 T-Bank connection required */ + 409: { + 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"]; + }; + }; + }; + }; + IncomeAnalyticsController_summary: { + parameters: { + query?: never; + header?: never; + path: { + accountId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["IncomeSummaryDto"]; + }; + }; + /** @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 T-Bank connection required */ + 409: { + 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"]; + }; + }; + }; + }; + IncomeAnalyticsController_analytics: { + parameters: { + query?: { + from?: string; + to?: string; + }; + header?: never; + path: { + accountId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["IncomeAnalyticsDto"]; + }; + }; + /** @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 T-Bank connection required */ + 409: { + 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"]; + }; + }; + }; + }; + UpcomingEventsController_all: { + parameters: { + query?: { + /** @description Inclusive start date (YYYY-MM-DD). Defaults to today in UTC. */ + from?: string; + /** @description Inclusive end date (YYYY-MM-DD). Defaults to 30 days after from. */ + to?: string; + }; + header?: never; + path?: never; + 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 T-Bank connection required */ + 409: { + 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"]; + }; + }; + }; + }; + UpcomingEventsController_get: { + parameters: { + query?: { + /** @description Inclusive start date (YYYY-MM-DD). Defaults to today in UTC. */ + from?: string; + /** @description Inclusive end date (YYYY-MM-DD). Defaults to 30 days after from. */ + to?: string; + }; + 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 T-Bank connection required */ + 409: { + 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"]; + }; + }; + }; + }; StockController_search: { parameters: { query: { @@ -2941,6 +3799,15 @@ export interface operations { "application/problem+json": components["schemas"]["ProblemDetailsDto"]; }; }; + /** @description T-Bank connection required */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetailsDto"]; + }; + }; /** @description Internal server error */ 500: { headers: { @@ -3007,6 +3874,15 @@ export interface operations { "application/problem+json": components["schemas"]["ProblemDetailsDto"]; }; }; + /** @description T-Bank connection required */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetailsDto"]; + }; + }; /** @description Internal server error */ 500: { headers: { @@ -3073,6 +3949,15 @@ export interface operations { "application/problem+json": components["schemas"]["ProblemDetailsDto"]; }; }; + /** @description T-Bank connection required */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetailsDto"]; + }; + }; /** @description Internal server error */ 500: { headers: { @@ -3139,6 +4024,15 @@ export interface operations { "application/problem+json": components["schemas"]["ProblemDetailsDto"]; }; }; + /** @description T-Bank connection required */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetailsDto"]; + }; + }; /** @description Internal server error */ 500: { headers: { @@ -3207,6 +4101,15 @@ export interface operations { "application/problem+json": components["schemas"]["ProblemDetailsDto"]; }; }; + /** @description T-Bank connection required */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetailsDto"]; + }; + }; /** @description Internal server error */ 500: { headers: { @@ -3273,6 +4176,15 @@ export interface operations { "application/problem+json": components["schemas"]["ProblemDetailsDto"]; }; }; + /** @description T-Bank connection required */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetailsDto"]; + }; + }; /** @description Internal server error */ 500: { headers: { @@ -3339,6 +4251,15 @@ export interface operations { "application/problem+json": components["schemas"]["ProblemDetailsDto"]; }; }; + /** @description T-Bank connection required */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetailsDto"]; + }; + }; /** @description Internal server error */ 500: { headers: { @@ -3405,6 +4326,15 @@ export interface operations { "application/problem+json": components["schemas"]["ProblemDetailsDto"]; }; }; + /** @description T-Bank connection required */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetailsDto"]; + }; + }; /** @description Internal server error */ 500: { headers: { @@ -3471,6 +4401,15 @@ export interface operations { "application/problem+json": components["schemas"]["ProblemDetailsDto"]; }; }; + /** @description T-Bank connection required */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetailsDto"]; + }; + }; /** @description Internal server error */ 500: { headers: { @@ -3537,6 +4476,15 @@ export interface operations { "application/problem+json": components["schemas"]["ProblemDetailsDto"]; }; }; + /** @description T-Bank connection required */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetailsDto"]; + }; + }; /** @description Internal server error */ 500: { headers: { @@ -3603,6 +4551,15 @@ export interface operations { "application/problem+json": components["schemas"]["ProblemDetailsDto"]; }; }; + /** @description T-Bank connection required */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetailsDto"]; + }; + }; /** @description Internal server error */ 500: { headers: { @@ -3669,6 +4626,15 @@ export interface operations { "application/problem+json": components["schemas"]["ProblemDetailsDto"]; }; }; + /** @description T-Bank connection required */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetailsDto"]; + }; + }; /** @description Internal server error */ 500: { headers: { @@ -3735,6 +4701,15 @@ export interface operations { "application/problem+json": components["schemas"]["ProblemDetailsDto"]; }; }; + /** @description T-Bank connection required */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetailsDto"]; + }; + }; /** @description Internal server error */ 500: { headers: { @@ -3803,6 +4778,15 @@ export interface operations { "application/problem+json": components["schemas"]["ProblemDetailsDto"]; }; }; + /** @description T-Bank connection required */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetailsDto"]; + }; + }; /** @description Internal server error */ 500: { headers: { @@ -3869,6 +4853,15 @@ export interface operations { "application/problem+json": components["schemas"]["ProblemDetailsDto"]; }; }; + /** @description T-Bank connection required */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetailsDto"]; + }; + }; /** @description Internal server error */ 500: { headers: { @@ -3935,6 +4928,15 @@ export interface operations { "application/problem+json": components["schemas"]["ProblemDetailsDto"]; }; }; + /** @description T-Bank connection required */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["ProblemDetailsDto"]; + }; + }; /** @description Internal server error */ 500: { headers: { @@ -4149,750 +5151,4 @@ export interface operations { }; }; }; - BrokerageAccountsController_list: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["BrokerageAccountDto"][]; - }; - }; - /** @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"]; - }; - }; - }; - }; - BrokerageOperationsController_all: { - parameters: { - query?: { - /** @description Instrument UID. Without from, includes history since the earliest available account opening. */ - instrumentUid?: string; - from?: string; - to?: string; - categories?: ("trade" | "income" | "tax" | "fee" | "transfer" | "other")[]; - cursor?: string; - limit?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["BrokerageOperationsPageDto"]; - }; - }; - /** @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"]; - }; - }; - }; - }; - BrokerageOperationsController_get: { - parameters: { - query?: { - /** @description Instrument UID. Without from, includes history since the earliest available account opening. */ - instrumentUid?: string; - from?: string; - to?: string; - categories?: ("trade" | "income" | "tax" | "fee" | "transfer" | "other")[]; - cursor?: string; - limit?: number; - }; - header?: never; - path: { - accountId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["BrokerageOperationsPageDto"]; - }; - }; - /** @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"]; - }; - }; - }; - }; - PortfolioController_all: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["PortfolioDto"]; - }; - }; - /** @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"]; - }; - }; - }; - }; - PortfolioController_get: { - parameters: { - query?: never; - header?: never; - path: { - accountId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["PortfolioDto"]; - }; - }; - /** @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"]; - }; - }; - }; - }; - UpcomingEventsController_all: { - parameters: { - query?: { - /** @description Inclusive start date (YYYY-MM-DD). Defaults to today in UTC. */ - from?: string; - /** @description Inclusive end date (YYYY-MM-DD). Defaults to 30 days after from. */ - to?: string; - }; - header?: never; - path?: never; - 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"]; - }; - }; - }; - }; - UpcomingEventsController_get: { - parameters: { - query?: { - /** @description Inclusive start date (YYYY-MM-DD). Defaults to today in UTC. */ - from?: string; - /** @description Inclusive end date (YYYY-MM-DD). Defaults to 30 days after from. */ - to?: string; - }; - 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"]; - }; - }; - }; - }; - IncomeAnalyticsController_allSummary: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["IncomeSummaryDto"]; - }; - }; - /** @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"]; - }; - }; - }; - }; - IncomeAnalyticsController_allAnalytics: { - parameters: { - query?: { - from?: string; - to?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["IncomeAnalyticsDto"]; - }; - }; - /** @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"]; - }; - }; - }; - }; - IncomeAnalyticsController_summary: { - parameters: { - query?: never; - header?: never; - path: { - accountId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["IncomeSummaryDto"]; - }; - }; - /** @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"]; - }; - }; - }; - }; - IncomeAnalyticsController_analytics: { - parameters: { - query?: { - from?: string; - to?: string; - }; - header?: never; - path: { - accountId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["IncomeAnalyticsDto"]; - }; - }; - /** @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 c3a3242..410d0b6 100644 --- a/src/shared/api/spec/openapi.json +++ b/src/shared/api/spec/openapi.json @@ -56,6 +56,16 @@ } } }, + "409": { + "description": "Email already registered", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -1973,6 +1983,1211 @@ ] } }, + "/api/v1/brokerage-accounts": { + "get": { + "operationId": "BrokerageAccountsController_list", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BrokerageAccountDto" + } + } + } + } + }, + "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" + } + } + } + }, + "409": { + "description": "T-Bank connection required", + "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": [ + { + "bearer": [] + } + ], + "tags": [ + "brokerage-accounts" + ] + } + }, + "/api/v1/brokerage-accounts/all/portfolio": { + "get": { + "operationId": "PortfolioController_all", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PortfolioDto" + } + } + } + }, + "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" + } + } + } + }, + "409": { + "description": "T-Bank connection required", + "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": [ + { + "bearer": [] + } + ], + "tags": [ + "brokerage-accounts" + ] + } + }, + "/api/v1/brokerage-accounts/{accountId}/portfolio": { + "get": { + "operationId": "PortfolioController_get", + "parameters": [ + { + "name": "accountId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PortfolioDto" + } + } + } + }, + "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" + } + } + } + }, + "409": { + "description": "T-Bank connection required", + "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": [ + { + "bearer": [] + } + ], + "tags": [ + "brokerage-accounts" + ] + } + }, + "/api/v1/brokerage-accounts/all/operations": { + "get": { + "operationId": "BrokerageOperationsController_all", + "parameters": [ + { + "name": "instrumentUid", + "required": false, + "in": "query", + "description": "Instrument UID. Without from, includes history since the earliest available account opening.", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "from", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "to", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "categories", + "required": false, + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "trade", + "income", + "tax", + "fee", + "transfer", + "other" + ] + } + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "schema": { + "minimum": 1, + "maximum": 1000, + "default": 100, + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrokerageOperationsPageDto" + } + } + } + }, + "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" + } + } + } + }, + "409": { + "description": "T-Bank connection required", + "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": [ + { + "bearer": [] + } + ], + "tags": [ + "brokerage-accounts" + ] + } + }, + "/api/v1/brokerage-accounts/{accountId}/operations": { + "get": { + "operationId": "BrokerageOperationsController_get", + "parameters": [ + { + "name": "accountId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "instrumentUid", + "required": false, + "in": "query", + "description": "Instrument UID. Without from, includes history since the earliest available account opening.", + "schema": { + "format": "uuid", + "type": "string" + } + }, + { + "name": "from", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "to", + "required": false, + "in": "query", + "schema": { + "format": "date-time", + "type": "string" + } + }, + { + "name": "categories", + "required": false, + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "trade", + "income", + "tax", + "fee", + "transfer", + "other" + ] + } + } + }, + { + "name": "cursor", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "schema": { + "minimum": 1, + "maximum": 1000, + "default": 100, + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BrokerageOperationsPageDto" + } + } + } + }, + "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" + } + } + } + }, + "409": { + "description": "T-Bank connection required", + "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": [ + { + "bearer": [] + } + ], + "tags": [ + "brokerage-accounts" + ] + } + }, + "/api/v1/brokerage-accounts/all/income-summary": { + "get": { + "operationId": "IncomeAnalyticsController_allSummary", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IncomeSummaryDto" + } + } + } + }, + "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" + } + } + } + }, + "409": { + "description": "T-Bank connection required", + "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": [ + { + "bearer": [] + } + ], + "tags": [ + "brokerage-accounts" + ] + } + }, + "/api/v1/brokerage-accounts/all/income-analytics": { + "get": { + "operationId": "IncomeAnalyticsController_allAnalytics", + "parameters": [ + { + "name": "from", + "required": false, + "in": "query", + "schema": { + "format": "date", + "type": "string" + } + }, + { + "name": "to", + "required": false, + "in": "query", + "schema": { + "format": "date", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IncomeAnalyticsDto" + } + } + } + }, + "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" + } + } + } + }, + "409": { + "description": "T-Bank connection required", + "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": [ + { + "bearer": [] + } + ], + "tags": [ + "brokerage-accounts" + ] + } + }, + "/api/v1/brokerage-accounts/{accountId}/income-summary": { + "get": { + "operationId": "IncomeAnalyticsController_summary", + "parameters": [ + { + "name": "accountId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IncomeSummaryDto" + } + } + } + }, + "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" + } + } + } + }, + "409": { + "description": "T-Bank connection required", + "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": [ + { + "bearer": [] + } + ], + "tags": [ + "brokerage-accounts" + ] + } + }, + "/api/v1/brokerage-accounts/{accountId}/income-analytics": { + "get": { + "operationId": "IncomeAnalyticsController_analytics", + "parameters": [ + { + "name": "accountId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "from", + "required": false, + "in": "query", + "schema": { + "format": "date", + "type": "string" + } + }, + { + "name": "to", + "required": false, + "in": "query", + "schema": { + "format": "date", + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IncomeAnalyticsDto" + } + } + } + }, + "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" + } + } + } + }, + "409": { + "description": "T-Bank connection required", + "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": [ + { + "bearer": [] + } + ], + "tags": [ + "brokerage-accounts" + ] + } + }, + "/api/v1/brokerage-accounts/all/events": { + "get": { + "operationId": "UpcomingEventsController_all", + "parameters": [ + { + "name": "from", + "required": false, + "in": "query", + "description": "Inclusive start date (YYYY-MM-DD). Defaults to today in UTC.", + "schema": { + "format": "date", + "type": "string" + } + }, + { + "name": "to", + "required": false, + "in": "query", + "description": "Inclusive end date (YYYY-MM-DD). Defaults to 30 days after from.", + "schema": { + "format": "date", + "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" + } + } + } + }, + "409": { + "description": "T-Bank connection required", + "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": [ + { + "bearer": [] + } + ], + "tags": [ + "brokerage-accounts" + ] + } + }, + "/api/v1/brokerage-accounts/{accountId}/events": { + "get": { + "operationId": "UpcomingEventsController_get", + "parameters": [ + { + "name": "accountId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + }, + { + "name": "from", + "required": false, + "in": "query", + "description": "Inclusive start date (YYYY-MM-DD). Defaults to today in UTC.", + "schema": { + "format": "date", + "type": "string" + } + }, + { + "name": "to", + "required": false, + "in": "query", + "description": "Inclusive end date (YYYY-MM-DD). Defaults to 30 days after from.", + "schema": { + "format": "date", + "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" + } + } + } + }, + "409": { + "description": "T-Bank connection required", + "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": [ + { + "bearer": [] + } + ], + "tags": [ + "brokerage-accounts" + ] + } + }, "/api/v1/stocks": { "get": { "operationId": "StockController_search", @@ -2032,6 +3247,16 @@ } } }, + "409": { + "description": "T-Bank connection required", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2120,6 +3345,16 @@ } } }, + "409": { + "description": "T-Bank connection required", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2205,6 +3440,16 @@ } } }, + "409": { + "description": "T-Bank connection required", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2290,6 +3535,16 @@ } } }, + "409": { + "description": "T-Bank connection required", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2390,6 +3645,16 @@ } } }, + "409": { + "description": "T-Bank connection required", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2475,6 +3740,16 @@ } } }, + "409": { + "description": "T-Bank connection required", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2560,6 +3835,16 @@ } } }, + "409": { + "description": "T-Bank connection required", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2645,6 +3930,16 @@ } } }, + "409": { + "description": "T-Bank connection required", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2730,6 +4025,16 @@ } } }, + "409": { + "description": "T-Bank connection required", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2820,6 +4125,16 @@ } } }, + "409": { + "description": "T-Bank connection required", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2908,6 +4223,16 @@ } } }, + "409": { + "description": "T-Bank connection required", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -2993,6 +4318,16 @@ } } }, + "409": { + "description": "T-Bank connection required", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -3078,6 +4413,16 @@ } } }, + "409": { + "description": "T-Bank connection required", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -3178,6 +4523,16 @@ } } }, + "409": { + "description": "T-Bank connection required", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -3263,6 +4618,16 @@ } } }, + "409": { + "description": "T-Bank connection required", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -3348,6 +4713,16 @@ } } }, + "409": { + "description": "T-Bank connection required", + "content": { + "application/problem+json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetailsDto" + } + } + } + }, "500": { "description": "Internal server error", "content": { @@ -3605,1101 +4980,6 @@ "tbank-connection" ] } - }, - "/api/v1/brokerage-accounts": { - "get": { - "operationId": "BrokerageAccountsController_list", - "parameters": [], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BrokerageAccountDto" - } - } - } - } - }, - "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": [ - { - "bearer": [] - } - ], - "tags": [ - "brokerage-accounts" - ] - } - }, - "/api/v1/brokerage-accounts/all/operations": { - "get": { - "operationId": "BrokerageOperationsController_all", - "parameters": [ - { - "name": "instrumentUid", - "required": false, - "in": "query", - "description": "Instrument UID. Without from, includes history since the earliest available account opening.", - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "name": "from", - "required": false, - "in": "query", - "schema": { - "format": "date-time", - "type": "string" - } - }, - { - "name": "to", - "required": false, - "in": "query", - "schema": { - "format": "date-time", - "type": "string" - } - }, - { - "name": "categories", - "required": false, - "in": "query", - "schema": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "trade", - "income", - "tax", - "fee", - "transfer", - "other" - ] - } - } - }, - { - "name": "cursor", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "required": false, - "in": "query", - "schema": { - "minimum": 1, - "maximum": 1000, - "default": 100, - "type": "number" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BrokerageOperationsPageDto" - } - } - } - }, - "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": [ - { - "bearer": [] - } - ], - "tags": [ - "brokerage-accounts" - ] - } - }, - "/api/v1/brokerage-accounts/{accountId}/operations": { - "get": { - "operationId": "BrokerageOperationsController_get", - "parameters": [ - { - "name": "accountId", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "instrumentUid", - "required": false, - "in": "query", - "description": "Instrument UID. Without from, includes history since the earliest available account opening.", - "schema": { - "format": "uuid", - "type": "string" - } - }, - { - "name": "from", - "required": false, - "in": "query", - "schema": { - "format": "date-time", - "type": "string" - } - }, - { - "name": "to", - "required": false, - "in": "query", - "schema": { - "format": "date-time", - "type": "string" - } - }, - { - "name": "categories", - "required": false, - "in": "query", - "schema": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "trade", - "income", - "tax", - "fee", - "transfer", - "other" - ] - } - } - }, - { - "name": "cursor", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "limit", - "required": false, - "in": "query", - "schema": { - "minimum": 1, - "maximum": 1000, - "default": 100, - "type": "number" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BrokerageOperationsPageDto" - } - } - } - }, - "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": [ - { - "bearer": [] - } - ], - "tags": [ - "brokerage-accounts" - ] - } - }, - "/api/v1/brokerage-accounts/all/portfolio": { - "get": { - "operationId": "PortfolioController_all", - "parameters": [], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PortfolioDto" - } - } - } - }, - "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": [ - { - "bearer": [] - } - ], - "tags": [ - "brokerage-accounts" - ] - } - }, - "/api/v1/brokerage-accounts/{accountId}/portfolio": { - "get": { - "operationId": "PortfolioController_get", - "parameters": [ - { - "name": "accountId", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PortfolioDto" - } - } - } - }, - "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": [ - { - "bearer": [] - } - ], - "tags": [ - "brokerage-accounts" - ] - } - }, - "/api/v1/brokerage-accounts/all/events": { - "get": { - "operationId": "UpcomingEventsController_all", - "parameters": [ - { - "name": "from", - "required": false, - "in": "query", - "description": "Inclusive start date (YYYY-MM-DD). Defaults to today in UTC.", - "schema": { - "format": "date", - "type": "string" - } - }, - { - "name": "to", - "required": false, - "in": "query", - "description": "Inclusive end date (YYYY-MM-DD). Defaults to 30 days after from.", - "schema": { - "format": "date", - "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": [ - { - "bearer": [] - } - ], - "tags": [ - "brokerage-accounts" - ] - } - }, - "/api/v1/brokerage-accounts/{accountId}/events": { - "get": { - "operationId": "UpcomingEventsController_get", - "parameters": [ - { - "name": "accountId", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "from", - "required": false, - "in": "query", - "description": "Inclusive start date (YYYY-MM-DD). Defaults to today in UTC.", - "schema": { - "format": "date", - "type": "string" - } - }, - { - "name": "to", - "required": false, - "in": "query", - "description": "Inclusive end date (YYYY-MM-DD). Defaults to 30 days after from.", - "schema": { - "format": "date", - "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": [ - { - "bearer": [] - } - ], - "tags": [ - "brokerage-accounts" - ] - } - }, - "/api/v1/brokerage-accounts/all/income-summary": { - "get": { - "operationId": "IncomeAnalyticsController_allSummary", - "parameters": [], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IncomeSummaryDto" - } - } - } - }, - "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": [ - { - "bearer": [] - } - ], - "tags": [ - "brokerage-accounts" - ] - } - }, - "/api/v1/brokerage-accounts/all/income-analytics": { - "get": { - "operationId": "IncomeAnalyticsController_allAnalytics", - "parameters": [ - { - "name": "from", - "required": false, - "in": "query", - "schema": { - "format": "date", - "type": "string" - } - }, - { - "name": "to", - "required": false, - "in": "query", - "schema": { - "format": "date", - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IncomeAnalyticsDto" - } - } - } - }, - "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": [ - { - "bearer": [] - } - ], - "tags": [ - "brokerage-accounts" - ] - } - }, - "/api/v1/brokerage-accounts/{accountId}/income-summary": { - "get": { - "operationId": "IncomeAnalyticsController_summary", - "parameters": [ - { - "name": "accountId", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IncomeSummaryDto" - } - } - } - }, - "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": [ - { - "bearer": [] - } - ], - "tags": [ - "brokerage-accounts" - ] - } - }, - "/api/v1/brokerage-accounts/{accountId}/income-analytics": { - "get": { - "operationId": "IncomeAnalyticsController_analytics", - "parameters": [ - { - "name": "accountId", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - }, - { - "name": "from", - "required": false, - "in": "query", - "schema": { - "format": "date", - "type": "string" - } - }, - { - "name": "to", - "required": false, - "in": "query", - "schema": { - "format": "date", - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IncomeAnalyticsDto" - } - } - } - }, - "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": [ - { - "bearer": [] - } - ], - "tags": [ - "brokerage-accounts" - ] - } } }, "info": { @@ -4712,6 +4992,21 @@ "servers": [], "components": { "schemas": { + "ValidationProblemDto": { + "type": "object", + "properties": { + "pointer": { + "type": "string" + }, + "detail": { + "type": "string" + } + }, + "required": [ + "pointer", + "detail" + ] + }, "ProblemDetailsDto": { "type": "object", "properties": { @@ -4732,7 +5027,7 @@ ], "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/ValidationProblemDto" } } }, @@ -5306,6 +5601,917 @@ "version" ] }, + "PortfolioSummaryDto": { + "type": "object", + "properties": { + "totalsByCurrency": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MoneyDto" + } + }, + "stockPositionCount": { + "type": "integer", + "minimum": 0, + "description": "Number of non-zero stock positions, not the number of units." + }, + "bondPositionCount": { + "type": "integer", + "minimum": 0, + "description": "Number of non-zero bond positions, not the number of units." + } + }, + "required": [ + "totalsByCurrency", + "stockPositionCount", + "bondPositionCount" + ] + }, + "BrokerageAccountDto": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "BROKERAGE", + "INDIVIDUAL_INVESTMENT", + "INVEST_BOX", + "MONEY_MARKET_FUND" + ] + }, + "openedOn": { + "type": "string", + "nullable": true, + "format": "date" + }, + "portfolioSummary": { + "nullable": true, + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/PortfolioSummaryDto" + } + ] + } + }, + "required": [ + "id", + "name", + "type", + "openedOn", + "portfolioSummary" + ] + }, + "AccountReferenceDto": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "PortfolioPositionDto": { + "type": "object", + "properties": { + "instrumentUid": { + "type": "string", + "nullable": true + }, + "account": { + "$ref": "#/components/schemas/AccountReferenceDto" + }, + "figi": { + "type": "string", + "nullable": true + }, + "displayName": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "STOCK", + "BOND", + "FUND", + "CURRENCY", + "FUTURE", + "OPTION", + "STRUCTURED_NOTE", + "DIGITAL_FINANCIAL_ASSET", + "OTHER" + ] + }, + "quantity": { + "type": "string" + }, + "currentValue": { + "nullable": true, + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MoneyDto" + } + ] + }, + "averagePurchasePrice": { + "nullable": true, + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MoneyDto" + } + ] + }, + "absoluteReturn": { + "nullable": true, + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MoneyDto" + } + ] + }, + "isin": { + "type": "string", + "nullable": true + }, + "ticker": { + "type": "string", + "nullable": true + }, + "logoName": { + "type": "string", + "nullable": true + } + }, + "required": [ + "figi", + "displayName", + "type", + "quantity", + "currentValue", + "averagePurchasePrice", + "absoluteReturn", + "isin", + "ticker", + "logoName" + ] + }, + "PortfolioStructureCategoryDto": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "STOCK", + "BOND", + "OTHER", + "CASH" + ] + }, + "value": { + "type": "string", + "description": "Exact known value as a decimal string." + }, + "sharePercent": { + "type": "string", + "nullable": true, + "description": "Percentage including cash, rounded to six decimal places. Null for incomplete, negative or zero-total compositions." + } + }, + "required": [ + "type", + "value", + "sharePercent" + ] + }, + "PortfolioStructureCurrencyDto": { + "type": "object", + "properties": { + "currency": { + "type": "string" + }, + "totalValue": { + "type": "string" + }, + "hasNegativeValues": { + "type": "boolean", + "description": "Any negative constituent, even if netted positive." + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PortfolioStructureCategoryDto" + } + } + }, + "required": [ + "currency", + "totalValue", + "hasNegativeValues", + "categories" + ] + }, + "PortfolioUnvaluedCategoryDto": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "STOCK", + "BOND", + "OTHER" + ] + }, + "count": { + "type": "number", + "minimum": 1 + } + }, + "required": [ + "type", + "count" + ] + }, + "PortfolioStructureDto": { + "type": "object", + "properties": { + "currencies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PortfolioStructureCurrencyDto" + } + }, + "unvaluedCategories": { + "description": "Positions without current value and known valuation currency. Shares are unavailable across all currencies when nonempty.", + "type": "array", + "items": { + "$ref": "#/components/schemas/PortfolioUnvaluedCategoryDto" + } + } + }, + "required": [ + "currencies", + "unvaluedCategories" + ] + }, + "PortfolioDto": { + "type": "object", + "properties": { + "lastSuccessfulAt": { + "type": "string", + "format": "date-time" + }, + "isStale": { + "type": "boolean" + }, + "totalsByCurrency": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MoneyDto" + } + }, + "positions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PortfolioPositionDto" + } + }, + "cashBalances": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MoneyDto" + } + }, + "structure": { + "$ref": "#/components/schemas/PortfolioStructureDto" + } + }, + "required": [ + "lastSuccessfulAt", + "isStale", + "totalsByCurrency", + "positions", + "cashBalances", + "structure" + ] + }, + "BrokerageOperationDto": { + "type": "object", + "properties": { + "account": { + "$ref": "#/components/schemas/AccountReferenceDto" + }, + "id": { + "type": "string" + }, + "occurredAt": { + "type": "string", + "format": "date-time" + }, + "type": { + "type": "string" + }, + "category": { + "type": "string", + "enum": [ + "trade", + "income", + "tax", + "fee", + "transfer", + "other" + ] + }, + "description": { + "type": "string" + }, + "figi": { + "type": "string", + "nullable": true + }, + "instrumentUid": { + "type": "string", + "nullable": true + }, + "ticker": { + "type": "string", + "nullable": true + }, + "instrumentType": { + "type": "string", + "nullable": true + }, + "logoName": { + "type": "string", + "nullable": true + }, + "payment": { + "nullable": true, + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MoneyDto" + } + ] + }, + "quantity": { + "type": "string", + "nullable": true + }, + "price": { + "nullable": true, + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MoneyDto" + } + ] + }, + "accruedInterest": { + "nullable": true, + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MoneyDto" + } + ] + }, + "commission": { + "nullable": true, + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MoneyDto" + } + ] + } + }, + "required": [ + "id", + "occurredAt", + "type", + "category", + "description", + "figi", + "instrumentUid", + "ticker", + "instrumentType", + "logoName", + "payment", + "quantity", + "price", + "accruedInterest", + "commission" + ] + }, + "BrokerageOperationsPageDto": { + "type": "object", + "properties": { + "accountId": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BrokerageOperationDto" + } + }, + "nextCursor": { + "type": "string", + "nullable": true + }, + "hasNext": { + "type": "boolean" + }, + "observedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "accountId", + "items", + "nextCursor", + "hasNext", + "observedAt" + ] + }, + "IncomePeriodDto": { + "type": "object", + "properties": { + "from": { + "type": "string", + "format": "date" + }, + "to": { + "type": "string", + "format": "date" + } + }, + "required": [ + "from", + "to" + ] + }, + "IncomeCurrencySummaryDto": { + "type": "object", + "properties": { + "currency": { + "type": "string" + }, + "coupon": { + "$ref": "#/components/schemas/MoneyDto" + }, + "dividend": { + "$ref": "#/components/schemas/MoneyDto" + }, + "total": { + "$ref": "#/components/schemas/MoneyDto" + } + }, + "required": [ + "currency", + "coupon", + "dividend", + "total" + ] + }, + "IncomeSummaryDto": { + "type": "object", + "properties": { + "accountId": { + "type": "string" + }, + "period": { + "$ref": "#/components/schemas/IncomePeriodDto" + }, + "totalsByCurrency": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IncomeCurrencySummaryDto" + } + }, + "calculatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "accountId", + "period", + "totalsByCurrency", + "calculatedAt" + ] + }, + "IncomeComparisonCurrencyDto": { + "type": "object", + "properties": { + "currency": { + "type": "string" + }, + "selected": { + "$ref": "#/components/schemas/IncomeCurrencySummaryDto" + }, + "compared": { + "$ref": "#/components/schemas/IncomeCurrencySummaryDto" + }, + "difference": { + "$ref": "#/components/schemas/IncomeCurrencySummaryDto" + } + }, + "required": [ + "currency", + "selected", + "compared", + "difference" + ] + }, + "IncomeComparisonDto": { + "type": "object", + "properties": { + "period": { + "$ref": "#/components/schemas/IncomePeriodDto" + }, + "totalsByCurrency": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IncomeComparisonCurrencyDto" + } + } + }, + "required": [ + "period", + "totalsByCurrency" + ] + }, + "IncomeBucketDto": { + "type": "object", + "properties": { + "period": { + "$ref": "#/components/schemas/IncomePeriodDto" + }, + "totalsByCurrency": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IncomeCurrencySummaryDto" + } + } + }, + "required": [ + "period", + "totalsByCurrency" + ] + }, + "IncomeInstrumentDto": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "figi": { + "type": "string", + "nullable": true + }, + "instrumentUid": { + "type": "string", + "nullable": true + }, + "ticker": { + "type": "string", + "nullable": true + }, + "instrumentType": { + "type": "string", + "nullable": true + }, + "displayName": { + "type": "string" + }, + "coupon": { + "$ref": "#/components/schemas/MoneyDto" + }, + "dividend": { + "$ref": "#/components/schemas/MoneyDto" + }, + "total": { + "$ref": "#/components/schemas/MoneyDto" + }, + "receiptCount": { + "type": "number" + } + }, + "required": [ + "key", + "figi", + "instrumentUid", + "ticker", + "instrumentType", + "displayName", + "coupon", + "dividend", + "total", + "receiptCount" + ] + }, + "IncomeAnalyticsEntryDto": { + "type": "object", + "properties": { + "account": { + "$ref": "#/components/schemas/AccountReferenceDto" + }, + "id": { + "type": "string" + }, + "occurredAt": { + "type": "string", + "format": "date-time" + }, + "type": { + "type": "string" + }, + "category": { + "type": "string", + "enum": [ + "trade", + "income", + "tax", + "fee", + "transfer", + "other" + ] + }, + "description": { + "type": "string" + }, + "figi": { + "type": "string", + "nullable": true + }, + "instrumentUid": { + "type": "string", + "nullable": true + }, + "ticker": { + "type": "string", + "nullable": true + }, + "instrumentType": { + "type": "string", + "nullable": true + }, + "logoName": { + "type": "string", + "nullable": true + }, + "payment": { + "nullable": true, + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MoneyDto" + } + ] + }, + "quantity": { + "type": "string", + "nullable": true + }, + "price": { + "nullable": true, + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MoneyDto" + } + ] + }, + "accruedInterest": { + "nullable": true, + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MoneyDto" + } + ] + }, + "commission": { + "nullable": true, + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MoneyDto" + } + ] + }, + "instrumentKey": { + "type": "string" + } + }, + "required": [ + "id", + "occurredAt", + "type", + "category", + "description", + "figi", + "instrumentUid", + "ticker", + "instrumentType", + "logoName", + "payment", + "quantity", + "price", + "accruedInterest", + "commission", + "instrumentKey" + ] + }, + "IncomeAnalyticsDto": { + "type": "object", + "properties": { + "accountId": { + "type": "string" + }, + "period": { + "$ref": "#/components/schemas/IncomePeriodDto" + }, + "totalsByCurrency": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IncomeCurrencySummaryDto" + } + }, + "calculatedAt": { + "type": "string", + "format": "date-time" + }, + "previous": { + "$ref": "#/components/schemas/IncomeComparisonDto" + }, + "yearComparison": { + "nullable": true, + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/IncomeComparisonDto" + } + ] + }, + "bucketGranularity": { + "type": "string", + "enum": [ + "day", + "week", + "month" + ] + }, + "buckets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IncomeBucketDto" + } + }, + "instruments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IncomeInstrumentDto" + } + }, + "entries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IncomeAnalyticsEntryDto" + } + } + }, + "required": [ + "accountId", + "period", + "totalsByCurrency", + "calculatedAt", + "previous", + "yearComparison", + "bucketGranularity", + "buckets", + "instruments", + "entries" + ] + }, + "UpcomingEventDto": { + "type": "object", + "properties": { + "account": { + "$ref": "#/components/schemas/AccountReferenceDto" + }, + "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" + }, + "isin": { + "type": "string", + "nullable": true + }, + "logoName": { + "type": "string", + "nullable": true + }, + "projectedAmount": { + "nullable": true, + "type": "object", + "allOf": [ + { + "$ref": "#/components/schemas/MoneyDto" + } + ] + } + }, + "required": [ + "type", + "date", + "dateKind", + "figi", + "instrumentUid", + "ticker", + "displayName", + "isin", + "logoName", + "projectedAmount" + ] + }, + "UpcomingEventsDto": { + "type": "object", + "properties": { + "accountId": { + "type": "string" + }, + "calculatedAt": { + "type": "string", + "format": "date-time" + }, + "events": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UpcomingEventDto" + } + }, + "incomeTotalsByCurrency": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MoneyDto" + } + } + }, + "required": [ + "accountId", + "calculatedAt", + "events", + "incomeTotalsByCurrency" + ] + }, "StockSearchResultDto": { "type": "object", "properties": { @@ -5597,21 +6803,6 @@ "observedAt" ] }, - "AccountReferenceDto": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - } - }, - "required": [ - "id", - "name" - ] - }, "StockPositionRowDto": { "type": "object", "properties": { @@ -6686,902 +7877,6 @@ "required": [ "token" ] - }, - "PortfolioSummaryDto": { - "type": "object", - "properties": { - "totalsByCurrency": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MoneyDto" - } - }, - "stockPositionCount": { - "type": "integer", - "minimum": 0, - "description": "Number of non-zero stock positions, not the number of units." - }, - "bondPositionCount": { - "type": "integer", - "minimum": 0, - "description": "Number of non-zero bond positions, not the number of units." - } - }, - "required": [ - "totalsByCurrency", - "stockPositionCount", - "bondPositionCount" - ] - }, - "BrokerageAccountDto": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "BROKERAGE", - "INDIVIDUAL_INVESTMENT", - "INVEST_BOX", - "MONEY_MARKET_FUND" - ] - }, - "openedOn": { - "type": "string", - "nullable": true, - "format": "date" - }, - "portfolioSummary": { - "nullable": true, - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/PortfolioSummaryDto" - } - ] - } - }, - "required": [ - "id", - "name", - "type", - "openedOn", - "portfolioSummary" - ] - }, - "BrokerageOperationDto": { - "type": "object", - "properties": { - "account": { - "$ref": "#/components/schemas/AccountReferenceDto" - }, - "id": { - "type": "string" - }, - "occurredAt": { - "type": "string", - "format": "date-time" - }, - "type": { - "type": "string" - }, - "category": { - "type": "string", - "enum": [ - "trade", - "income", - "tax", - "fee", - "transfer", - "other" - ] - }, - "description": { - "type": "string" - }, - "figi": { - "type": "string", - "nullable": true - }, - "instrumentUid": { - "type": "string", - "nullable": true - }, - "ticker": { - "type": "string", - "nullable": true - }, - "instrumentType": { - "type": "string", - "nullable": true - }, - "logoName": { - "type": "string", - "nullable": true - }, - "payment": { - "nullable": true, - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MoneyDto" - } - ] - }, - "quantity": { - "type": "string", - "nullable": true - }, - "price": { - "nullable": true, - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MoneyDto" - } - ] - }, - "accruedInterest": { - "nullable": true, - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MoneyDto" - } - ] - }, - "commission": { - "nullable": true, - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MoneyDto" - } - ] - } - }, - "required": [ - "id", - "occurredAt", - "type", - "category", - "description", - "figi", - "instrumentUid", - "ticker", - "instrumentType", - "logoName", - "payment", - "quantity", - "price", - "accruedInterest", - "commission" - ] - }, - "BrokerageOperationsPageDto": { - "type": "object", - "properties": { - "accountId": { - "type": "string" - }, - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/BrokerageOperationDto" - } - }, - "nextCursor": { - "type": "string", - "nullable": true - }, - "hasNext": { - "type": "boolean" - }, - "observedAt": { - "type": "string", - "format": "date-time" - } - }, - "required": [ - "accountId", - "items", - "nextCursor", - "hasNext", - "observedAt" - ] - }, - "PortfolioPositionDto": { - "type": "object", - "properties": { - "instrumentUid": { - "type": "string", - "nullable": true - }, - "account": { - "$ref": "#/components/schemas/AccountReferenceDto" - }, - "figi": { - "type": "string", - "nullable": true - }, - "displayName": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "STOCK", - "BOND", - "FUND", - "CURRENCY", - "FUTURE", - "OPTION", - "STRUCTURED_NOTE", - "DIGITAL_FINANCIAL_ASSET", - "OTHER" - ] - }, - "quantity": { - "type": "string" - }, - "currentValue": { - "nullable": true, - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MoneyDto" - } - ] - }, - "averagePurchasePrice": { - "nullable": true, - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MoneyDto" - } - ] - }, - "absoluteReturn": { - "nullable": true, - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MoneyDto" - } - ] - }, - "isin": { - "type": "string", - "nullable": true - }, - "ticker": { - "type": "string", - "nullable": true - }, - "logoName": { - "type": "string", - "nullable": true - } - }, - "required": [ - "figi", - "displayName", - "type", - "quantity", - "currentValue", - "averagePurchasePrice", - "absoluteReturn", - "isin", - "ticker", - "logoName" - ] - }, - "PortfolioStructureCategoryDto": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "STOCK", - "BOND", - "OTHER", - "CASH" - ] - }, - "value": { - "type": "string", - "description": "Exact known value as a decimal string." - }, - "sharePercent": { - "type": "string", - "nullable": true, - "description": "Percentage including cash, rounded to six decimal places. Null for incomplete, negative or zero-total compositions." - } - }, - "required": [ - "type", - "value", - "sharePercent" - ] - }, - "PortfolioStructureCurrencyDto": { - "type": "object", - "properties": { - "currency": { - "type": "string" - }, - "totalValue": { - "type": "string" - }, - "hasNegativeValues": { - "type": "boolean", - "description": "Any negative constituent, even if netted positive." - }, - "categories": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PortfolioStructureCategoryDto" - } - } - }, - "required": [ - "currency", - "totalValue", - "hasNegativeValues", - "categories" - ] - }, - "PortfolioUnvaluedCategoryDto": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "STOCK", - "BOND", - "OTHER" - ] - }, - "count": { - "type": "number", - "minimum": 1 - } - }, - "required": [ - "type", - "count" - ] - }, - "PortfolioStructureDto": { - "type": "object", - "properties": { - "currencies": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PortfolioStructureCurrencyDto" - } - }, - "unvaluedCategories": { - "description": "Positions without current value and known valuation currency. Shares are unavailable across all currencies when nonempty.", - "type": "array", - "items": { - "$ref": "#/components/schemas/PortfolioUnvaluedCategoryDto" - } - } - }, - "required": [ - "currencies", - "unvaluedCategories" - ] - }, - "PortfolioDto": { - "type": "object", - "properties": { - "lastSuccessfulAt": { - "type": "string", - "format": "date-time" - }, - "isStale": { - "type": "boolean" - }, - "totalsByCurrency": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MoneyDto" - } - }, - "positions": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PortfolioPositionDto" - } - }, - "cashBalances": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MoneyDto" - } - }, - "structure": { - "$ref": "#/components/schemas/PortfolioStructureDto" - } - }, - "required": [ - "lastSuccessfulAt", - "isStale", - "totalsByCurrency", - "positions", - "cashBalances", - "structure" - ] - }, - "UpcomingEventDto": { - "type": "object", - "properties": { - "account": { - "$ref": "#/components/schemas/AccountReferenceDto" - }, - "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" - }, - "isin": { - "type": "string", - "nullable": true - }, - "logoName": { - "type": "string", - "nullable": true - }, - "projectedAmount": { - "nullable": true, - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MoneyDto" - } - ] - } - }, - "required": [ - "type", - "date", - "dateKind", - "figi", - "instrumentUid", - "ticker", - "displayName", - "isin", - "logoName", - "projectedAmount" - ] - }, - "UpcomingEventsDto": { - "type": "object", - "properties": { - "accountId": { - "type": "string" - }, - "calculatedAt": { - "type": "string", - "format": "date-time" - }, - "events": { - "type": "array", - "items": { - "$ref": "#/components/schemas/UpcomingEventDto" - } - }, - "incomeTotalsByCurrency": { - "type": "array", - "items": { - "$ref": "#/components/schemas/MoneyDto" - } - } - }, - "required": [ - "accountId", - "calculatedAt", - "events", - "incomeTotalsByCurrency" - ] - }, - "IncomePeriodDto": { - "type": "object", - "properties": { - "from": { - "type": "string", - "format": "date" - }, - "to": { - "type": "string", - "format": "date" - } - }, - "required": [ - "from", - "to" - ] - }, - "IncomeCurrencySummaryDto": { - "type": "object", - "properties": { - "currency": { - "type": "string" - }, - "coupon": { - "$ref": "#/components/schemas/MoneyDto" - }, - "dividend": { - "$ref": "#/components/schemas/MoneyDto" - }, - "total": { - "$ref": "#/components/schemas/MoneyDto" - } - }, - "required": [ - "currency", - "coupon", - "dividend", - "total" - ] - }, - "IncomeSummaryDto": { - "type": "object", - "properties": { - "accountId": { - "type": "string" - }, - "period": { - "$ref": "#/components/schemas/IncomePeriodDto" - }, - "totalsByCurrency": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IncomeCurrencySummaryDto" - } - }, - "calculatedAt": { - "type": "string", - "format": "date-time" - } - }, - "required": [ - "accountId", - "period", - "totalsByCurrency", - "calculatedAt" - ] - }, - "IncomeComparisonCurrencyDto": { - "type": "object", - "properties": { - "currency": { - "type": "string" - }, - "selected": { - "$ref": "#/components/schemas/IncomeCurrencySummaryDto" - }, - "compared": { - "$ref": "#/components/schemas/IncomeCurrencySummaryDto" - }, - "difference": { - "$ref": "#/components/schemas/IncomeCurrencySummaryDto" - } - }, - "required": [ - "currency", - "selected", - "compared", - "difference" - ] - }, - "IncomeComparisonDto": { - "type": "object", - "properties": { - "period": { - "$ref": "#/components/schemas/IncomePeriodDto" - }, - "totalsByCurrency": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IncomeComparisonCurrencyDto" - } - } - }, - "required": [ - "period", - "totalsByCurrency" - ] - }, - "IncomeBucketDto": { - "type": "object", - "properties": { - "period": { - "$ref": "#/components/schemas/IncomePeriodDto" - }, - "totalsByCurrency": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IncomeCurrencySummaryDto" - } - } - }, - "required": [ - "period", - "totalsByCurrency" - ] - }, - "IncomeInstrumentDto": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "figi": { - "type": "string", - "nullable": true - }, - "instrumentUid": { - "type": "string", - "nullable": true - }, - "ticker": { - "type": "string", - "nullable": true - }, - "instrumentType": { - "type": "string", - "nullable": true - }, - "displayName": { - "type": "string" - }, - "coupon": { - "$ref": "#/components/schemas/MoneyDto" - }, - "dividend": { - "$ref": "#/components/schemas/MoneyDto" - }, - "total": { - "$ref": "#/components/schemas/MoneyDto" - }, - "receiptCount": { - "type": "number" - } - }, - "required": [ - "key", - "figi", - "instrumentUid", - "ticker", - "instrumentType", - "displayName", - "coupon", - "dividend", - "total", - "receiptCount" - ] - }, - "IncomeAnalyticsEntryDto": { - "type": "object", - "properties": { - "account": { - "$ref": "#/components/schemas/AccountReferenceDto" - }, - "id": { - "type": "string" - }, - "occurredAt": { - "type": "string", - "format": "date-time" - }, - "type": { - "type": "string" - }, - "category": { - "type": "string", - "enum": [ - "trade", - "income", - "tax", - "fee", - "transfer", - "other" - ] - }, - "description": { - "type": "string" - }, - "figi": { - "type": "string", - "nullable": true - }, - "instrumentUid": { - "type": "string", - "nullable": true - }, - "ticker": { - "type": "string", - "nullable": true - }, - "instrumentType": { - "type": "string", - "nullable": true - }, - "logoName": { - "type": "string", - "nullable": true - }, - "payment": { - "nullable": true, - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MoneyDto" - } - ] - }, - "quantity": { - "type": "string", - "nullable": true - }, - "price": { - "nullable": true, - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MoneyDto" - } - ] - }, - "accruedInterest": { - "nullable": true, - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MoneyDto" - } - ] - }, - "commission": { - "nullable": true, - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/MoneyDto" - } - ] - }, - "instrumentKey": { - "type": "string" - } - }, - "required": [ - "id", - "occurredAt", - "type", - "category", - "description", - "figi", - "instrumentUid", - "ticker", - "instrumentType", - "logoName", - "payment", - "quantity", - "price", - "accruedInterest", - "commission", - "instrumentKey" - ] - }, - "IncomeAnalyticsDto": { - "type": "object", - "properties": { - "accountId": { - "type": "string" - }, - "period": { - "$ref": "#/components/schemas/IncomePeriodDto" - }, - "totalsByCurrency": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IncomeCurrencySummaryDto" - } - }, - "calculatedAt": { - "type": "string", - "format": "date-time" - }, - "previous": { - "$ref": "#/components/schemas/IncomeComparisonDto" - }, - "yearComparison": { - "nullable": true, - "type": "object", - "allOf": [ - { - "$ref": "#/components/schemas/IncomeComparisonDto" - } - ] - }, - "bucketGranularity": { - "type": "string", - "enum": [ - "day", - "week", - "month" - ] - }, - "buckets": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IncomeBucketDto" - } - }, - "instruments": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IncomeInstrumentDto" - } - }, - "entries": { - "type": "array", - "items": { - "$ref": "#/components/schemas/IncomeAnalyticsEntryDto" - } - } - }, - "required": [ - "accountId", - "period", - "totalsByCurrency", - "calculatedAt", - "previous", - "yearComparison", - "bucketGranularity", - "buckets", - "instruments", - "entries" - ] } } } -- 2.54.0 From 6a87379813ba1d4002779b23b2a65446b4d10258 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Mon, 21 Sep 2026 21:09:53 +0300 Subject: [PATCH 3/8] fix(ci): resume verification of immutable release images TD-009: retry verification of an existing digest without republishing. Paired backend workflow fix: 82eccc3. --- .gitea/workflows/image.yml | 14 +++--- scripts/check-image-publication.cjs | 75 +++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 scripts/check-image-publication.cjs diff --git a/.gitea/workflows/image.yml b/.gitea/workflows/image.yml index 67ce36c..d802cea 100644 --- a/.gitea/workflows/image.yml +++ b/.gitea/workflows/image.yml @@ -45,15 +45,17 @@ jobs: run: | VERSION="${RELEASE_TAG#v}" if docker manifest inspect "$IMAGE:$VERSION" >/dev/null 2>manifest-error.log; then - echo 'Refusing to overwrite an existing release tag'; exit 1 + DIGEST="$(docker buildx imagetools inspect "$IMAGE:$VERSION" --format '{{.Manifest.Digest}}')" + else + if ! grep -Eq 'manifest unknown|no such manifest' manifest-error.log; then + echo 'Cannot prove tag absence; refusing publication'; exit 1 + fi + docker buildx build --target production --build-arg VERSION="$VERSION" --build-arg REVISION="$REVISION" --tag "$IMAGE:$VERSION" --metadata-file image-metadata.json --push . + DIGEST="$(node --input-type=commonjs -p 'require("./image-metadata.json")["containerimage.digest"]')" fi - if ! grep -Eq 'manifest unknown|no such manifest' manifest-error.log; then - echo 'Cannot prove tag absence; refusing publication'; exit 1 - fi - docker buildx build --target production --build-arg VERSION="$VERSION" --build-arg REVISION="$REVISION" --tag "$IMAGE:$VERSION" --metadata-file image-metadata.json --push . - DIGEST="$(node --input-type=commonjs -p 'require("./image-metadata.json")["containerimage.digest"]')" docker pull "$IMAGE@$DIGEST" test "$(docker image inspect "$IMAGE@$DIGEST" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" = "$REVISION" + test "$(docker image inspect "$IMAGE@$DIGEST" --format '{{index .Config.Labels "org.opencontainers.image.version"}}')" = "$VERSION" docker run --rm --entrypoint sh "$IMAGE@$DIGEST" -c 'test -s /usr/share/nginx/html/index.html && nginx -t && nginx && wget -q -O /dev/null http://127.0.0.1:8080/' printf 'Frontend image: %s@%s\n' "$IMAGE" "$DIGEST" - name: Remove registry session diff --git a/scripts/check-image-publication.cjs b/scripts/check-image-publication.cjs new file mode 100644 index 0000000..d0fda81 --- /dev/null +++ b/scripts/check-image-publication.cjs @@ -0,0 +1,75 @@ +const { test } = require("node:test"); +const assert = require("node:assert/strict"); +const { + mkdtempSync, + readFileSync, + writeFileSync, + chmodSync, + rmSync, +} = require("node:fs"); +const { tmpdir } = require("node:os"); +const { join, resolve } = require("node:path"); +const EXECUTABLE_MODE = 0o755; +const { spawnSync } = require("node:child_process"); + +test("immutable publication resumes exact digest checks after post-push failure", () => { + const directory = mkdtempSync(join(tmpdir(), "wl-image-retry-")); + const workflow = readFileSync( + resolve(__dirname, "../.gitea/workflows/image.yml"), + "utf8", + ); + const block = workflow + .split(" - name: Publish immutable release tag\n run: |\n")[1] + .split(" - name:")[0] + .replace(/^ {10}/gm, ""); + const double = `#!${process.execPath} +const fs = require('node:fs'); +const args = process.argv.slice(2).join(' '); +fs.appendFileSync('calls', args + '\\n'); +if (args.startsWith('manifest inspect')) { + if (!fs.existsSync('published')) { console.error('manifest unknown'); process.exit(1); } +} else if (args.startsWith('buildx imagetools inspect')) { + console.log('sha256:exact'); +} else if (args.startsWith('buildx build')) { + fs.writeFileSync('published', 'yes'); + fs.writeFileSync('image-metadata.json', JSON.stringify({'containerimage.digest': 'sha256:exact'})); +} else if (args.startsWith('pull')) { + if (!fs.existsSync('pull-failed')) { fs.writeFileSync('pull-failed', 'yes'); process.exit(1); } +} else if (args.startsWith('image inspect')) { + console.log(args.includes('image.version') ? '1.2.3' : process.env.BAD_REVISION ? 'other' : 'commit'); +} else if (args.startsWith('run') && process.env.BAD_SMOKE) process.exit(1); +`; + try { + for (const name of ["docker", "npm"]) { + writeFileSync(join(directory, name), double); + chmodSync(join(directory, name), EXECUTABLE_MODE); + } + const run = (extra = {}) => + spawnSync("/bin/sh", ["-ec", block], { + cwd: directory, + encoding: "utf8", + env: { + ...process.env, + PATH: `${directory}:${process.env.PATH}`, + IMAGE: "synthetic/image", + RELEASE_TAG: "v1.2.3", + REVISION: "commit", + ...extra, + }, + }); + assert.notEqual(run().status, 0); + const retry = run(); + assert.equal(retry.status, 0, retry.stdout + retry.stderr); + assert.notEqual(run({ BAD_REVISION: "1" }).status, 0); + assert.notEqual(run({ BAD_SMOKE: "1" }).status, 0); + const calls = readFileSync(join(directory, "calls"), "utf8"); + assert.equal( + calls.split("\n").filter((line) => line.startsWith("buildx build")) + .length, + 1, + ); + assert.ok(calls.includes("synthetic/image@sha256:exact")); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); -- 2.54.0 From c86be7bf73d0141e0606d259ada6453c0d8e3dec Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Mon, 21 Sep 2026 21:10:10 +0300 Subject: [PATCH 4/8] fix(ui): improve financial text contrast TD-010: use readable semantic text colors on financial surfaces while preserving signs and state meanings. --- src/pages/account/ui/AccountPage.module.css | 4 +- src/shared/theme/contrast.test.ts | 45 +++++++++++++++++++++ src/shared/theme/tokens.json | 6 +-- src/shared/ui/EntryCard.module.css | 6 +-- 4 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 src/shared/theme/contrast.test.ts diff --git a/src/pages/account/ui/AccountPage.module.css b/src/pages/account/ui/AccountPage.module.css index f5aa21c..c097c31 100644 --- a/src/pages/account/ui/AccountPage.module.css +++ b/src/pages/account/ui/AccountPage.module.css @@ -63,11 +63,11 @@ } .positive { - color: var(--mantine-color-green-6); + color: var(--wl-color-positive); } .negative { - color: var(--mantine-color-red-6); + color: var(--wl-color-negative); } .expandedCard { diff --git a/src/shared/theme/contrast.test.ts b/src/shared/theme/contrast.test.ts new file mode 100644 index 0000000..129b98b --- /dev/null +++ b/src/shared/theme/contrast.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from "vitest"; +import tokens from "./tokens.json"; + +function luminance(color: string) { + const channels = [1, 3, 5] + .map((index) => Number.parseInt(color.slice(index, index + 2), 16) / 255) + .map((value) => + value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4, + ); + return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722; +} +describe("financial text contrast", () => { + it.each(["positive", "negative"] as const)( + "keeps %s readable on journal, position and tinted card surfaces", + (tone) => { + const role = tokens.color.roles[tone]; + const palettes = tokens.color.palettes; + const foreground = + palettes[role.color as keyof typeof palettes][role.shade]; + const surfaces = [ + palettes.gray[0], + ...Object.entries(palettes) + .filter(([name]) => name !== "dark") + .map( + ([, palette]) => + `#${[1, 3, 5] + .map((index) => + Math.round( + Number.parseInt(palette[0].slice(index, index + 2), 16) * + 0.45 + + 255 * 0.55, + ) + .toString(16) + .padStart(2, "0"), + ) + .join("")}`, + ), + ]; + for (const background of surfaces) + expect( + (luminance(background) + 0.05) / (luminance(foreground) + 0.05), + ).toBeGreaterThanOrEqual(4.5); + }, + ); +}); diff --git a/src/shared/theme/tokens.json b/src/shared/theme/tokens.json index 32cab6d..4f5d284 100644 --- a/src/shared/theme/tokens.json +++ b/src/shared/theme/tokens.json @@ -53,7 +53,7 @@ "#40c057", "#37b24d", "#2f9e44", - "#2b8a3e" + "#237032" ], "red": [ "#fff5f5", @@ -151,12 +151,12 @@ }, "positive": { "color": "green", - "shade": 6, + "shade": 9, "description": "Положительный результат; дополните знаком и текстом" }, "negative": { "color": "red", - "shade": 6, + "shade": 9, "description": "Ошибка / отрицательный результат; дополните текстом" }, "warning": { diff --git a/src/shared/ui/EntryCard.module.css b/src/shared/ui/EntryCard.module.css index efdcb3e..cfb27d0 100644 --- a/src/shared/ui/EntryCard.module.css +++ b/src/shared/ui/EntryCard.module.css @@ -19,15 +19,15 @@ } .positive { - color: var(--mantine-color-green-6); + color: var(--wl-color-positive); } .negative { - color: var(--mantine-color-red-6); + color: var(--wl-color-negative); } .neutral { - color: var(--mantine-color-dimmed); + color: var(--mantine-color-gray-7); } @media (max-width: 36em) { -- 2.54.0 From 85fcfe3d073f80e759dafb14281c9462c1d7410d Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Tue, 22 Sep 2026 22:11:02 +0300 Subject: [PATCH 5/8] refactor: establish local frontend workspace boundaries OpenSpec change: evolve-frontend-stack --- .ai/architecture.md | 31 +- .ai/conventions/storybook.md | 5 +- .ai/conventions/ui.md | 30 +- .dockerignore | 27 ++ .storybook/main.ts | 2 +- .storybook/preview.tsx | 3 +- Dockerfile | 4 + README.md | 33 +- biome.json | 4 +- dependency-cruiser.cjs | 13 + knip.json | 17 +- package-lock.json | 59 +++ package.json | 35 +- packages/icons/package.json | 30 ++ packages/icons/src/Icons.stories.tsx | 45 ++ packages/icons/src/Icons.test.tsx | 34 ++ packages/icons/src/index.ts | 4 + packages/icons/src/svg/AddIcon.tsx | 24 + packages/icons/src/svg/DeleteIcon.tsx | 25 + packages/icons/src/svg/EditIcon.tsx | 25 + packages/icons/src/types.ts | 10 + packages/icons/tsconfig.json | 17 + packages/icons/vite.config.ts | 8 + packages/tokens/package.json | 12 + .../theme => packages/tokens}/tokens.json | 0 packages/ui/package.json | 44 ++ .../ui/src}/EntryCard.module.css | 4 +- packages/ui/src/EntryCard.stories.tsx | 37 ++ packages/ui/src/EntryCard.test.tsx | 35 ++ .../ui => packages/ui/src}/EntryCard.tsx | 2 +- .../ui/src}/LoadingSkeleton.stories.tsx | 0 .../ui/src}/LoadingSkeleton.tsx | 0 .../ui/src}/StateView.stories.tsx | 0 .../ui => packages/ui/src}/StateView.test.tsx | 0 .../ui => packages/ui/src}/StateView.tsx | 0 {src/shared/ui => packages/ui/src}/index.ts | 1 - .../theme => packages/ui/src}/styles.css | 0 .../theme => packages/ui/src}/theme.test.ts | 2 +- .../shared/theme => packages/ui/src}/theme.ts | 3 +- packages/ui/tsconfig.json | 19 + packages/ui/vite.config.ts | 14 + src/app/routes/AppRoutes.test.tsx | 3 +- src/app/routes/ProtectedLayout.tsx | 3 +- src/app/ui/App.tsx | 2 +- src/{shared => app}/ui/AppShell.module.css | 0 src/{shared => app}/ui/AppShell.tsx | 2 +- src/app/ui/index.ts | 1 + src/entities/bank-card/index.ts | 1 + .../bank-card/model/operation-kinds.ts | 10 + .../ui/BankOperationIcon.stories.tsx | 2 +- .../bank-card/ui/BankSummary.view.tsx | 2 +- .../broker-operation/ui/LedgerEntryRow.tsx | 2 +- .../brokerage-event/ui/BrokerageEventCard.tsx | 2 +- .../ui/BrokerageEventsPreview.tsx | 2 +- .../ui/InstrumentSection.view.tsx | 2 +- .../ui/TbankConnectionSection.view.tsx | 2 +- src/main.tsx | 1 + .../AccountEventsPageEmpty.view.tsx | 2 +- .../AccountEventsPageError.view.tsx | 2 +- .../AccountEventsPageLoading.view.tsx | 2 +- src/pages/account-income/model/period.ts | 46 ++ .../ui/AccountIncomePage.test.tsx | 3 +- .../account-income/ui/AccountIncomePage.vm.ts | 50 +- .../AccountIncomePageError.view.tsx | 2 +- .../AccountIncomePageLoaded.view.tsx | 2 +- .../AccountIncomePageLoading.view.tsx | 2 +- .../components/IncomePeriodForm.stories.tsx | 58 +++ .../ui/components/IncomePeriodForm.view.tsx | 4 +- .../AccountOperationsPageError.view.tsx | 2 +- .../AccountOperationsPageLoaded.view.tsx | 2 +- .../AccountOperationsPageLoading.view.tsx | 2 +- .../ui/components/AccountPageEmpty.view.tsx | 2 +- .../ui/components/AccountPageError.view.tsx | 2 +- .../ui/components/AccountPageLoading.view.tsx | 2 +- .../ui/components/IncomeSummary.view.tsx | 2 +- .../ui/components/RecentOperations.view.tsx | 2 +- .../ui/components/AccountsPageEmpty.view.tsx | 2 +- .../ui/components/AccountsPageError.view.tsx | 2 +- .../components/AccountsPageLoading.view.tsx | 2 +- src/pages/bond/ui/BondPage.tsx | 2 +- src/pages/bond/ui/BondPage.view.tsx | 2 +- src/pages/bonds/ui/BondsPage.view.tsx | 2 +- src/pages/card/ui/CardPage.view.tsx | 94 +--- src/pages/card/ui/OperationForm.stories.tsx | 113 +++++ src/pages/card/ui/OperationForm.test.tsx | 38 ++ src/pages/card/ui/OperationForm.tsx | 453 ++---------------- src/pages/card/ui/OperationForm.types.ts | 11 +- src/pages/card/ui/OperationForm.view.tsx | 170 +++++-- src/pages/card/ui/OperationForm.vm.ts | 318 ++++++++++++ src/pages/cards/ui/CardsPage.view.tsx | 16 +- src/pages/not-found/ui/NotFoundPage.tsx | 2 +- src/pages/stock/ui/StockPage.tsx | 2 +- src/pages/stock/ui/StockPage.view.tsx | 2 +- src/pages/stocks/ui/StocksPage.view.tsx | 2 +- src/shared/theme/Foundations.stories.tsx | 6 +- src/shared/theme/contrast.test.ts | 2 +- src/shared/theme/index.ts | 3 - src/test/storybook.test.tsx | 9 +- tsconfig.app.json | 2 +- tsconfig.node.json | 6 +- vitest.config.ts | 8 +- 101 files changed, 1487 insertions(+), 666 deletions(-) create mode 100644 packages/icons/package.json create mode 100644 packages/icons/src/Icons.stories.tsx create mode 100644 packages/icons/src/Icons.test.tsx create mode 100644 packages/icons/src/index.ts create mode 100644 packages/icons/src/svg/AddIcon.tsx create mode 100644 packages/icons/src/svg/DeleteIcon.tsx create mode 100644 packages/icons/src/svg/EditIcon.tsx create mode 100644 packages/icons/src/types.ts create mode 100644 packages/icons/tsconfig.json create mode 100644 packages/icons/vite.config.ts create mode 100644 packages/tokens/package.json rename {src/shared/theme => packages/tokens}/tokens.json (100%) create mode 100644 packages/ui/package.json rename {src/shared/ui => packages/ui/src}/EntryCard.module.css (93%) create mode 100644 packages/ui/src/EntryCard.stories.tsx create mode 100644 packages/ui/src/EntryCard.test.tsx rename {src/shared/ui => packages/ui/src}/EntryCard.tsx (97%) rename {src/shared/ui => packages/ui/src}/LoadingSkeleton.stories.tsx (100%) rename {src/shared/ui => packages/ui/src}/LoadingSkeleton.tsx (100%) rename {src/shared/ui => packages/ui/src}/StateView.stories.tsx (100%) rename {src/shared/ui => packages/ui/src}/StateView.test.tsx (100%) rename {src/shared/ui => packages/ui/src}/StateView.tsx (100%) rename {src/shared/ui => packages/ui/src}/index.ts (77%) rename {src/shared/theme => packages/ui/src}/styles.css (100%) rename {src/shared/theme => packages/ui/src}/theme.test.ts (95%) rename {src/shared/theme => packages/ui/src}/theme.ts (96%) create mode 100644 packages/ui/tsconfig.json create mode 100644 packages/ui/vite.config.ts rename src/{shared => app}/ui/AppShell.module.css (100%) rename src/{shared => app}/ui/AppShell.tsx (98%) create mode 100644 src/entities/bank-card/model/operation-kinds.ts create mode 100644 src/pages/account-income/model/period.ts create mode 100644 src/pages/account-income/ui/components/IncomePeriodForm.stories.tsx create mode 100644 src/pages/card/ui/OperationForm.stories.tsx create mode 100644 src/pages/card/ui/OperationForm.vm.ts delete mode 100644 src/shared/theme/index.ts diff --git a/.ai/architecture.md b/.ai/architecture.md index 877b55d..be5a6cd 100644 --- a/.ai/architecture.md +++ b/.ai/architecture.md @@ -51,23 +51,32 @@ Cross-slice consumer использует `@/` и `index.ts` владельца, композиция принадлежит вышележащему owner. Existing меж-slice исключения не являются разрешением добавлять новые. -Направление зависимостей и public imports частично проверяет -`npm run deps`; Knip проверяет использование exports, а не смысл placement. +Направление зависимостей, public imports и запрет соседних slices в +`pages`, `features`, `entities` проверяет `npm run deps`. Внутренние imports +одного slice и связи shared segments этот запрет не ограничивает. +Knip проверяет использование exports, а не смысл placement. ## UI foundation -`shared/theme/tokens.json` — переносимый source design tokens; `theme.ts` +Private workspace `packages/tokens` экспортирует переносимый source design tokens +как `@wealthledger/tokens/tokens.json`. Private workspace `packages/ui` адаптирует доверенный JSON к Mantine без runtime schema. App и Storybook читают -public API `shared/theme`. Storybook показывает synthetic presentation states, +`@wealthledger/ui/theme` и явно подключают `@wealthledger/ui/styles.css` после +Mantine CSS. Потребители используют public exports пакетов напрямую. Storybook показывает synthetic presentation states, не использует application API. RTL/MSW проверяют поведение приложения отдельно. -Existing route-aware AppShell расположен в shared/ui; это legacy placement, -а не образец нового shared primitive. При изменении foundation прочитай +Route-aware AppShell и его navigation styles принадлежат `app/ui`; +`ProtectedLayout` использует public API этого владельца. При изменении foundation прочитай [UI](conventions/ui.md#визуальная-основа) и [Storybook](conventions/storybook.md). +Статичные Add/Edit/Delete и публичный `IconProps` принадлежат private +`packages/icons`; package не зависит от UI, domain или motion. +`BankOperationIcon` и mappings остаются у entity. Card/Cards используют package +Add/Edit/Delete; restore/category SVG остаются локальными. + ## Примеры -- Общая busy-область: существующий [LoadingSkeleton](../src/shared/ui/LoadingSkeleton.tsx), а не domain widget в shared. +- Общая busy-область: существующий [LoadingSkeleton](../packages/ui/src/LoadingSkeleton.tsx), а не domain widget в shared. - Account-income state views: [страница](../src/pages/account-income/ui/AccountIncomePage.tsx) и её flat `components/*.view.tsx`; новый page-local filter остаётся здесь. - Portfolio query: [entity public API](../src/entities/portfolio/index.ts); новая UI-сводка с бизнес-смыслом не становится shared только из-за нескольких экранов. @@ -75,7 +84,7 @@ Existing route-aware AppShell расположен в shared/ui; это legacy p После placement/import change: `npm run deps`, `npm run typecheck`, `npm run knip`; после собственного behavior — применимые tests. Финальный gate указан в AGENTS. -Existing dependency config не запрещает все same-layer imports: их и смысл -owner проверяет review. Не объявляй Steiger установленным; graph проверяет -текущий dependency-cruiser. Review отдельно проверяет смысл owner: разрешённый dependency graph не доказывает -правильный выбор слоя. +Graph `src` и `packages` проверяет текущий dependency-cruiser; зависимости +из packages в код приложения `src` запрещены. Generated `packages/*/dist` +не анализируется как исходник; все package sources остаются в graph. Review отдельно проверяет смысл +owner: разрешённый dependency graph не доказывает правильный выбор слоя. diff --git a/.ai/conventions/storybook.md b/.ai/conventions/storybook.md index 62fb6b9..1871394 100644 --- a/.ai/conventions/storybook.md +++ b/.ai/conventions/storybook.md @@ -1,5 +1,8 @@ # Storybook +Единый каталог включает stories приложения и `packages/*/src`; общие +StateView/LoadingSkeleton и их stories принадлежат `packages/ui`. + ## Когда нужна story - Shared UI/theme: показывай принятые visual roles и переиспользуемое поведение. @@ -23,7 +26,7 @@ Stories не достигают application API, auth, VM и query providers д контракт: keyboard activation, доступность действия, local callback. Не повторяй RTL suite и не тестируй внутренности Mantine. Существующий [Button](../../src/shared/ui/Button.stories.tsx) показывает themed control -без production wrapper; [LoadingSkeleton](../../src/shared/ui/LoadingSkeleton.stories.tsx) +без production wrapper; [LoadingSkeleton](../../packages/ui/src/LoadingSkeleton.stories.tsx) — значимые busy states. `npm run typecheck`, `npm run deps:storybook`, `npm run build:catalog` проверяют diff --git a/.ai/conventions/ui.md b/.ai/conventions/ui.md index badd161..a741cce 100644 --- a/.ai/conventions/ui.md +++ b/.ai/conventions/ui.md @@ -13,7 +13,10 @@ Mantine component и общую тему. Для выпадающего выбо Icons ищи у существующего owner (например, [BankOperationIcon](../../src/entities/bank-card/ui/BankOperationIcon.tsx)); -общая icon library сейчас не принята. Не добавляй пакет ради одного символа. Business/domain +статичные Add/Edit/Delete доступны в `@wealthledger/icons`. Их SVG декоративны +по умолчанию; имя action принадлежит кнопке. Standalone meaningful icon получает +`aria-hidden={false}`, `role="img"` и доступное имя. Category/kind mappings +и анимации остаются в entity. Не добавляй пакет ради одного символа. Business/domain components живут у feature/entity/page, даже при повторном использовании. Wrapper нужен для проектной ответственности, а не ради нового имени @@ -107,10 +110,29 @@ loading/error/empty/loaded и recovery по применимости. [Storybook ## Визуальная основа -Общая тема живёт в `src/shared/theme`: `tokens.json` задаёт переносимые значения, -`theme.ts` адаптирует их к Mantine. Используй public API темы и её semantic roles, -без отдельной палитры в page/component. Поддерживается светлая схема. +Private workspace `packages/tokens` владеет переносимыми значениями +`tokens.json`; `packages/ui/src/theme.ts` читает export пакета +`@wealthledger/tokens/tokens.json` и адаптирует его к Mantine. Используй +`@wealthledger/ui/theme` и его semantic roles; runtime подключает +`@wealthledger/ui/styles.css` после Mantine CSS. Providers остаются у runtime. +Палитра page/component следует этим semantic roles. Поддерживается светлая схема. Цвет дополняет подпись или знак; проверяй контраст конкретной пары text/background. Warning не используй для мелкого текста на белом. Деньги сохраняют валюту, знак и различие между нулём и неизвестной суммой. Общие состояния демонстрирует Storybook; presentation stories используют synthetic data без application API. + +EntryCard, StateView и LoadingSkeleton принадлежат `packages/ui`; публичный import — +`@wealthledger/ui`. Переводы, retry и формы skeleton остаются у callers. +Compatibility re-exports удалены; callers используют public export пакета. + +## Modal animation + +BankCardForm, OperationForm, DeleteOperation и CategoryManager монтируют Modal +с `opened=true`: Mantine Transition начинает в состоянии `entered`. +CSS `modal-appear`/`modal-backdrop-appear` в `packages/ui/src/styles.css` +обеспечивает появление при mount (220ms, только no-preference). Mantine theme +владеет exit transition (180ms), overlay blur и completion callback; +`onExitTransitionEnd` позволяет дождаться закрытия перед unmount и вернуть фокус. +Удаление CSS без изменения mount lifecycle убирает вступительную анимацию. +TbankConnection Modal использует toggle `opened`; вывод о разделении фаз +банковских форм на этот сценарий автоматически не распространяется. diff --git a/.dockerignore b/.dockerignore index ec510c1..0c527df 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,3 +13,30 @@ !public/** !docker/ !docker/** +!packages/ +!packages/tokens/ +!packages/tokens/package.json +!packages/tokens/tokens.json +!packages/ui/ +!packages/ui/package.json +!packages/ui/vite.config.ts +!packages/ui/tsconfig.json +!packages/ui/src/ +!packages/ui/src/theme.ts +!packages/ui/src/styles.css +!packages/ui/src/index.ts +!packages/ui/src/StateView.tsx +!packages/ui/src/LoadingSkeleton.tsx +!packages/ui/src/EntryCard.tsx +!packages/ui/src/EntryCard.module.css +!packages/icons/ +!packages/icons/package.json +!packages/icons/tsconfig.json +!packages/icons/vite.config.ts +!packages/icons/src/ +!packages/icons/src/index.ts +!packages/icons/src/types.ts +!packages/icons/src/svg/ +!packages/icons/src/svg/AddIcon.tsx +!packages/icons/src/svg/EditIcon.tsx +!packages/icons/src/svg/DeleteIcon.tsx diff --git a/.storybook/main.ts b/.storybook/main.ts index ca5d4b1..8557f9c 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,7 +1,7 @@ import type { StorybookConfig } from "@storybook/react-vite"; const config: StorybookConfig = { - stories: ["../src/**/*.stories.tsx"], + stories: ["../src/**/*.stories.tsx", "../packages/*/src/**/*.stories.tsx"], addons: ["@storybook/addon-docs"], framework: "@storybook/react-vite", core: { disableTelemetry: true }, diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index b5bfa08..acc23dc 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -1,12 +1,13 @@ import "@mantine/core/styles.css"; import "@mantine/charts/styles.css"; import "@mantine/dates/styles.css"; +import "@wealthledger/ui/styles.css"; import "dayjs/locale/ru"; import "../src/shared/i18n"; import { MantineProvider } from "@mantine/core"; import { DatesProvider } from "@mantine/dates"; import type { Preview } from "@storybook/react-vite"; -import { cssVariablesResolver, theme } from "@/shared/theme"; +import { cssVariablesResolver, theme } from "@wealthledger/ui/theme"; const colorSchemeManager = { get: () => "light" as const, diff --git a/Dockerfile b/Dockerfile index 88feb2d..2f3c85c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,10 +2,14 @@ FROM node:24.21.0-bookworm-slim AS dependencies WORKDIR /app ENV HUSKY=0 COPY package.json package-lock.json ./ +COPY packages/tokens/package.json ./packages/tokens/ +COPY packages/ui/package.json ./packages/ui/ +COPY packages/icons/package.json ./packages/icons/ RUN npm ci FROM dependencies AS development COPY . . +RUN npm run build:packages ENV VITE_API_BASE_URL="" API_PROXY_TARGET=http://backend:3000 EXPOSE 8080 CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "8080", "--strictPort"] diff --git a/README.md b/README.md index e5ee4d9..94f3292 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,8 @@ staged diff с полной редакцией секретов, затем за `Dockerfile` имеет targets `development` (Vite HMR и proxy `/api/` во внутренний backend) и `production` (nginx на 8080, SPA fallback и same-origin API proxy). Node соответствует `.nvmrc`. Build context ограничен `.dockerignore`; `.env`, local DB и host node_modules -не попадают в image. +не попадают в image. Изменение `packages/tokens` или `packages/ui` требует rebuild development +image, если локальный запуск использует bind-mount только `src`/`public`. ```bash docker build --target development -t wealthledger-frontend:development . @@ -194,3 +195,33 @@ Commits и PR titles — Conventional Commits, включая `spec:`. Верс администратором, обязательны `verify`, `security`, `commits`; теги `v*` защищены. Workflow сам не включает эти настройки. Отдельное review approval для одного владельца не требуется. Npm publishing, deploy и Gitea Releases не включены. + +## Локальные пакеты + +`packages/tokens` — private npm workspace с единственным export +`@wealthledger/tokens/tokens.json`. `packages/ui` экспортирует собранные +`@wealthledger/ui/theme` (JS + declarations) и `@wealthledger/ui/styles.css`. +Корневой export `@wealthledger/ui` содержит EntryCard, StateView и LoadingSkeleton; +Потребители импортируют компоненты напрямую из `@wealthledger/ui`. +CSS явно подключается после Mantine; providers остаются в приложении/Storybook. +Тема доступна через `@wealthledger/ui/theme`; compatibility re-exports удалены. + +JSON не требует сборки. `npm ci` связывает workspaces, а существующие команды +разработки, проверок и сборки сначала выполняют `npm run build:packages`. +Library build использует отдельный Vite config без app plugins и TypeScript +для declarations. После изменения package sources перезапустите `npm run dev` +или `npm run dev:catalog`; отдельный watch-процесс пока не нужен. +Начальная версия manifest взята из приложения; отдельный release-процесс +и публикация не настроены. + +Private `@wealthledger/icons` экспортирует `AddIcon`, `EditIcon`, `DeleteIcon` +и type `IconProps`. Единственный source рисунка — TSX в `packages/icons/src/svg`; +поставка содержит ESM/declarations, без raw SVG, CSS, Mantine и motion. +React — peer/external. Размер `size` по умолчанию 24, `strokeWidth` — 1.5; +цвет наследуется через `currentColor`, SVG props можно передать явно. +В action используйте декоративный ``, +доступное имя задавайте кнопке. Standalone пример: +``. +Поддерживается также стандартный `aria-labelledby`; подпись выбирает caller. +Card/Cards используют пакетные Add/Edit/Delete с прежними размерами/strokeWidth. +Restore/category SVG и предметный BankOperationIcon остаются в приложении. diff --git a/biome.json b/biome.json index 6eff3f8..babf97e 100644 --- a/biome.json +++ b/biome.json @@ -7,6 +7,7 @@ "includes": [ "**", "!dist", + "!packages/*/dist", "!storybook-static", "!src/shared/api/generated", "!src/shared/api/spec", @@ -38,7 +39,8 @@ "vite.config.ts", "vitest.config.ts", ".storybook/main.ts", - ".storybook/preview.tsx" + ".storybook/preview.tsx", + "packages/*/vite.config.ts" ], "linter": { "rules": { diff --git a/dependency-cruiser.cjs b/dependency-cruiser.cjs index a87fcea..a2d1217 100644 --- a/dependency-cruiser.cjs +++ b/dependency-cruiser.cjs @@ -1,5 +1,11 @@ module.exports = { forbidden: [ + { + name: "packages-must-not-depend-on-app", + severity: "error", + from: { path: "^packages/" }, + to: { path: "^src/" }, + }, { name: "no-circular", severity: "error", @@ -17,6 +23,12 @@ module.exports = { pathNot: "^src/$1/$2/", }, }, + { + name: "no-same-layer-cross-slice-imports", + severity: "error", + from: { path: "^src/(pages|features|entities)/([^/]+)/" }, + to: { path: "^src/$1/", pathNot: "^src/$1/$2/" }, + }, { name: "shared-must-not-depend-on-layers", severity: "error", @@ -43,6 +55,7 @@ module.exports = { }, ], options: { + exclude: "^packages/[^/]+/dist/", doNotFollow: { path: "node_modules" }, tsConfig: { fileName: "tsconfig.app.json" }, tsPreCompilationDeps: true, diff --git a/knip.json b/knip.json index f2b3ac7..5f5b315 100644 --- a/knip.json +++ b/knip.json @@ -1,4 +1,17 @@ { - "project": ["src/**/*.{ts,tsx}", "!src/shared/api/generated/**"], - "ignoreDependencies": ["conventional-changelog-conventionalcommits"] + "ignoreDependencies": ["conventional-changelog-conventionalcommits"], + "workspaces": { + ".": { + "project": ["src/**/*.{ts,tsx}", "!src/shared/api/generated/**"] + }, + "packages/tokens": { + "entry": ["tokens.json"] + }, + "packages/ui": { + "project": ["src/**/*.{ts,tsx}"] + }, + "packages/icons": { + "project": ["src/**/*.{ts,tsx}"] + } + } } diff --git a/package-lock.json b/package-lock.json index 0caad7d..3205fa6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "wealth-ledger-frontend", "version": "0.3.0", + "workspaces": [ + "packages/*" + ], "dependencies": { "@hookform/resolvers": "5.9.1", "@mantine/charts": "9.6.1", @@ -14,6 +17,9 @@ "@mantine/dates": "9.6.1", "@mantine/hooks": "9.6.1", "@tanstack/react-query": "5.102.8", + "@wealthledger/icons": "file:packages/icons", + "@wealthledger/tokens": "file:packages/tokens", + "@wealthledger/ui": "file:packages/ui", "dayjs": "1.11.23", "i18next": "26.4.2", "motion": "13.2.0", @@ -5460,6 +5466,18 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@wealthledger/icons": { + "resolved": "packages/icons", + "link": true + }, + "node_modules/@wealthledger/tokens": { + "resolved": "packages/tokens", + "link": true + }, + "node_modules/@wealthledger/ui": { + "resolved": "packages/ui", + "link": true + }, "node_modules/@webcontainer/env": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@webcontainer/env/-/env-1.1.1.tgz", @@ -16399,6 +16417,47 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "packages/icons": { + "name": "@wealthledger/icons", + "version": "0.3.0", + "devDependencies": { + "@storybook/react-vite": "10.6.0", + "@testing-library/react": "16.3.3", + "@types/react": "19.3.0", + "typescript": "5.9.3", + "vite": "8.3.0", + "vitest": "5.0.0" + }, + "peerDependencies": { + "react": "19.3.0" + } + }, + "packages/tokens": { + "name": "@wealthledger/tokens", + "version": "0.3.0" + }, + "packages/ui": { + "name": "@wealthledger/ui", + "version": "0.3.0", + "dependencies": { + "@wealthledger/tokens": "0.3.0" + }, + "devDependencies": { + "@storybook/react-vite": "10.6.0", + "@testing-library/react": "16.3.3", + "@types/react": "19.3.0", + "storybook": "10.6.0", + "typescript": "5.9.3", + "vite": "8.3.0", + "vitest": "5.0.0" + }, + "peerDependencies": { + "@mantine/core": "9.6.1", + "@mantine/hooks": "9.6.1", + "react": "19.3.0", + "react-dom": "19.3.0" + } } } } diff --git a/package.json b/package.json index 9f5ecc7..d0a529b 100644 --- a/package.json +++ b/package.json @@ -7,27 +7,28 @@ "node": "24.21.0" }, "scripts": { - "dev": "vite", - "dev:catalog": "storybook dev -p 6006 --no-open --disable-telemetry", - "build:catalog": "storybook build --output-dir storybook-static --disable-telemetry", + "dev": "npm run build:packages && vite", + "dev:catalog": "npm run build:packages && storybook dev -p 6006 --no-open --disable-telemetry", + "build:catalog": "npm run build:packages && storybook build --output-dir storybook-static --disable-telemetry", "preview:catalog": "vite preview --outDir storybook-static", - "typecheck": "tsc -b --noEmit", - "build": "tsc -b && vite build", + "typecheck": "npm run build:packages && tsc -b --noEmit", + "build": "npm run build:packages && tsc -b && vite build", "lint": "biome check .", - "test": "vitest", - "test:run": "vitest run", - "test:coverage": "vitest run --coverage", + "test": "npm run build:packages && vitest", + "test:run": "npm run build:packages && vitest run", + "test:coverage": "npm run build:packages && vitest run --coverage", "openapi:generate": "openapi-typescript src/shared/api/spec/openapi.json -o src/shared/api/generated/schema.d.ts", "openapi:check": "node scripts/check-openapi.mjs", - "knip": "knip", - "deps": "depcruise src --config dependency-cruiser.cjs --output-type err-long", - "verify": "npm run openapi:check && npm run typecheck && npm run lint && npm run test:coverage && npm run knip && npm run deps && npm run deps:storybook && npm run build && npm run build:catalog", + "knip": "npm run build:packages && knip", + "deps": "npm run build:packages && depcruise src packages --config dependency-cruiser.cjs --output-type err-long", + "verify": "npm run build:packages && npm run openapi:check && npm run typecheck && npm run lint && npm run test:coverage && npm run knip && npm run deps && npm run deps:storybook && npm run build && npm run build:catalog", "prepare": "husky", "preview": "vite preview", - "deps:storybook": "depcruise src .storybook/preview.tsx --config .storybook/dependency-cruiser.cjs --output-type err-long", + "deps:storybook": "npm run build:packages && depcruise src packages .storybook/preview.tsx --config .storybook/dependency-cruiser.cjs --output-type err-long", "release:preview": "release-it --dry-run --ci", "release:prepare": "release-it --ci", - "commitlint": "commitlint" + "commitlint": "commitlint", + "build:packages": "npm run build --workspaces --if-present" }, "lint-staged": { "*.{css,html,json,md,ts,tsx}": "biome format --write --no-errors-on-unmatched" @@ -39,6 +40,9 @@ "@mantine/dates": "9.6.1", "@mantine/hooks": "9.6.1", "@tanstack/react-query": "5.102.8", + "@wealthledger/icons": "file:packages/icons", + "@wealthledger/tokens": "file:packages/tokens", + "@wealthledger/ui": "file:packages/ui", "dayjs": "1.11.23", "i18next": "26.4.2", "motion": "13.2.0", @@ -88,5 +92,8 @@ "allowScripts": { "esbuild@0.28.2": true, "fsevents@2.3.3": true - } + }, + "workspaces": [ + "packages/*" + ] } diff --git a/packages/icons/package.json b/packages/icons/package.json new file mode 100644 index 0000000..e3f8c13 --- /dev/null +++ b/packages/icons/package.json @@ -0,0 +1,30 @@ +{ + "name": "@wealthledger/icons", + "version": "0.3.0", + "private": true, + "type": "module", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "sideEffects": false, + "scripts": { + "build": "vite build && tsc -p tsconfig.json" + }, + "peerDependencies": { + "react": "19.3.0" + }, + "devDependencies": { + "@storybook/react-vite": "10.6.0", + "@testing-library/react": "16.3.3", + "@types/react": "19.3.0", + "typescript": "5.9.3", + "vite": "8.3.0", + "vitest": "5.0.0" + } +} diff --git a/packages/icons/src/Icons.stories.tsx b/packages/icons/src/Icons.stories.tsx new file mode 100644 index 0000000..f9b21e2 --- /dev/null +++ b/packages/icons/src/Icons.stories.tsx @@ -0,0 +1,45 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { AddIcon, DeleteIcon, EditIcon } from "./index"; + +const meta = { + title: "Design System/Icons", + component: AddIcon, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: + "Статичные SVG: size по умолчанию 24, strokeWidth 1.5, цвет currentColor. В кнопке иконка декоративная, имя принадлежит действию. Для самостоятельного изображения задайте aria-hidden={false}, role=img и aria-label либо aria-labelledby. Геометрия взята из Card/Cards; подписи и действия остаются у приложения.", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Decorative: Story = { + render: () => ( +
+ + + +
+ ), +}; + +export const Meaningful: Story = { + render: () => ( +
+ + + +
+ ), +}; diff --git a/packages/icons/src/Icons.test.tsx b/packages/icons/src/Icons.test.tsx new file mode 100644 index 0000000..31c48ef --- /dev/null +++ b/packages/icons/src/Icons.test.tsx @@ -0,0 +1,34 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { AddIcon, DeleteIcon, EditIcon } from "./index"; + +describe("icon accessibility", () => { + it.each([AddIcon, EditIcon, DeleteIcon])( + "keeps decoration hidden and exposes explicitly named images: %s", + (Icon) => { + const { rerender } = render( + , + ); + expect(screen.queryByRole("img")).not.toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "Действие" }).querySelector("svg"), + ).toHaveAttribute("focusable", "false"); + rerender( + , + ); + const icon = screen.getByRole("img", { name: "Пример" }); + expect(icon).toHaveAttribute("width", "18"); + expect(icon).toHaveAttribute("height", "18"); + expect(icon).toHaveAttribute("stroke-width", "2"); + expect(icon).toHaveAttribute("stroke", "currentColor"); + }, + ); +}); diff --git a/packages/icons/src/index.ts b/packages/icons/src/index.ts new file mode 100644 index 0000000..296d458 --- /dev/null +++ b/packages/icons/src/index.ts @@ -0,0 +1,4 @@ +export { AddIcon } from "./svg/AddIcon"; +export { DeleteIcon } from "./svg/DeleteIcon"; +export { EditIcon } from "./svg/EditIcon"; +export type { IconProps } from "./types"; diff --git a/packages/icons/src/svg/AddIcon.tsx b/packages/icons/src/svg/AddIcon.tsx new file mode 100644 index 0000000..cbd7ff5 --- /dev/null +++ b/packages/icons/src/svg/AddIcon.tsx @@ -0,0 +1,24 @@ +import type { FC } from "react"; +import type { IconProps } from "../types"; + +const AddIcon: FC = (props) => { + const { size = "24", ...svgProps } = props; + return ( + + ); +}; + +export { AddIcon }; diff --git a/packages/icons/src/svg/DeleteIcon.tsx b/packages/icons/src/svg/DeleteIcon.tsx new file mode 100644 index 0000000..6e6321b --- /dev/null +++ b/packages/icons/src/svg/DeleteIcon.tsx @@ -0,0 +1,25 @@ +import type { FC } from "react"; +import type { IconProps } from "../types"; + +const DeleteIcon: FC = (props) => { + const { size = "24", ...svgProps } = props; + return ( + + ); +}; + +export { DeleteIcon }; diff --git a/packages/icons/src/svg/EditIcon.tsx b/packages/icons/src/svg/EditIcon.tsx new file mode 100644 index 0000000..d2c4940 --- /dev/null +++ b/packages/icons/src/svg/EditIcon.tsx @@ -0,0 +1,25 @@ +import type { FC } from "react"; +import type { IconProps } from "../types"; + +const EditIcon: FC = (props) => { + const { size = "24", ...svgProps } = props; + return ( + + ); +}; + +export { EditIcon }; diff --git a/packages/icons/src/types.ts b/packages/icons/src/types.ts new file mode 100644 index 0000000..7f106f5 --- /dev/null +++ b/packages/icons/src/types.ts @@ -0,0 +1,10 @@ +import type { ComponentPropsWithoutRef } from "react"; + +type IconProps = Omit< + ComponentPropsWithoutRef<"svg">, + "children" | "dangerouslySetInnerHTML" +> & { + size?: number | string; +}; + +export type { IconProps }; diff --git a/packages/icons/tsconfig.json b/packages/icons/tsconfig.json new file mode 100644 index 0000000..96b5e87 --- /dev/null +++ b/packages/icons/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "declaration": true, + "emitDeclarationOnly": true, + "rootDir": "src", + "outDir": "dist", + "jsx": "react-jsx" + }, + "include": ["src/index.ts"] +} diff --git a/packages/icons/vite.config.ts b/packages/icons/vite.config.ts new file mode 100644 index 0000000..9dd7242 --- /dev/null +++ b/packages/icons/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + build: { + lib: { entry: "src/index.ts", formats: ["es"], fileName: "index" }, + rolldownOptions: { external: /^react(\/|$)/ }, + }, +}); diff --git a/packages/tokens/package.json b/packages/tokens/package.json new file mode 100644 index 0000000..67b3554 --- /dev/null +++ b/packages/tokens/package.json @@ -0,0 +1,12 @@ +{ + "name": "@wealthledger/tokens", + "version": "0.3.0", + "private": true, + "type": "module", + "files": [ + "tokens.json" + ], + "exports": { + "./tokens.json": "./tokens.json" + } +} diff --git a/src/shared/theme/tokens.json b/packages/tokens/tokens.json similarity index 100% rename from src/shared/theme/tokens.json rename to packages/tokens/tokens.json diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 0000000..7f998bc --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,44 @@ +{ + "name": "@wealthledger/ui", + "version": "0.3.0", + "private": true, + "type": "module", + "files": [ + "dist" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./theme": { + "types": "./dist/theme.d.ts", + "import": "./dist/theme.js" + }, + "./styles.css": "./dist/styles.css" + }, + "sideEffects": [ + "**/*.css" + ], + "scripts": { + "build": "vite build && tsc -p tsconfig.json" + }, + "dependencies": { + "@wealthledger/tokens": "0.3.0" + }, + "peerDependencies": { + "@mantine/core": "9.6.1", + "@mantine/hooks": "9.6.1", + "react": "19.3.0", + "react-dom": "19.3.0" + }, + "devDependencies": { + "@storybook/react-vite": "10.6.0", + "@testing-library/react": "16.3.3", + "@types/react": "19.3.0", + "storybook": "10.6.0", + "typescript": "5.9.3", + "vite": "8.3.0", + "vitest": "5.0.0" + } +} diff --git a/src/shared/ui/EntryCard.module.css b/packages/ui/src/EntryCard.module.css similarity index 93% rename from src/shared/ui/EntryCard.module.css rename to packages/ui/src/EntryCard.module.css index cfb27d0..2aca6de 100644 --- a/src/shared/ui/EntryCard.module.css +++ b/packages/ui/src/EntryCard.module.css @@ -4,8 +4,8 @@ gap: var(--mantine-spacing-sm); align-items: center; padding: var(--mantine-spacing-md); - border-color: var(--mantine-color-gray-3); - background: var(--mantine-color-gray-0); + border-color: var(--wl-color-border); + background: var(--wl-color-subtle); } .identity, diff --git a/packages/ui/src/EntryCard.stories.tsx b/packages/ui/src/EntryCard.stories.tsx new file mode 100644 index 0000000..d4a2fd3 --- /dev/null +++ b/packages/ui/src/EntryCard.stories.tsx @@ -0,0 +1,37 @@ +import { Button } from "@mantine/core"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { fn } from "storybook/test"; +import { EntryCard } from "./EntryCard"; + +const meta = { + title: "Design System/Components/EntryCard", + component: EntryCard, + tags: ["autodocs"], + args: { + title: + "Учебная запись с очень длинным названием для проверки узкого экрана", + metadata: "22.09.2026 · Синтетические данные", + initial: "У", + imageSrc: "data:image/png;base64,invalid", + value: "−1 250,00 ₽", + valueLabel: "Тестовое значение", + tone: "negative", + actions: ( + + ), + }, + parameters: { + docs: { + description: { + component: + "Готовые строки и slots без финансовых вычислений. Невалидное локальное изображение показывает fallback; действие доступно с клавиатуры и на узком экране. Данные вымышлены.", + }, + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; +export const LongContentAndFallback: Story = {}; diff --git a/packages/ui/src/EntryCard.test.tsx b/packages/ui/src/EntryCard.test.tsx new file mode 100644 index 0000000..213e3d1 --- /dev/null +++ b/packages/ui/src/EntryCard.test.tsx @@ -0,0 +1,35 @@ +import { MantineProvider } from "@mantine/core"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { assert, describe, expect, it } from "vitest"; +import { EntryCard } from "./EntryCard"; + +describe("EntryCard", () => { + it("keeps the fallback after an image fails", () => { + const props = { + title: "Пример", + metadata: "Описание", + initial: "У", + value: "—", + valueLabel: "Неизвестно", + tone: "neutral" as const, + imageSrc: "data:image/png;base64,invalid", + }; + const { container, rerender } = render( + + + , + ); + // Decorative image has no accessible name; trigger its actual failure event. + const image = container.querySelector("img"); + assert(image); + fireEvent.error(image); + rerender( + + + , + ); + expect(container.querySelector("img")).toBeNull(); + expect(screen.getByText("У")).toBeInTheDocument(); + expect(screen.getByText("Обновлённый пример")).toBeInTheDocument(); + }); +}); diff --git a/src/shared/ui/EntryCard.tsx b/packages/ui/src/EntryCard.tsx similarity index 97% rename from src/shared/ui/EntryCard.tsx rename to packages/ui/src/EntryCard.tsx index 7c2ad0c..455e2c4 100644 --- a/src/shared/ui/EntryCard.tsx +++ b/packages/ui/src/EntryCard.tsx @@ -25,7 +25,7 @@ const EntryCard: FC = (props) => { style={ props.color ? { - background: `color-mix(in srgb, var(--mantine-color-${props.color}-0) 45%, white)`, + background: `color-mix(in srgb, var(--mantine-color-${props.color}-0) 45%, var(--wl-color-surface))`, } : undefined } diff --git a/src/shared/ui/LoadingSkeleton.stories.tsx b/packages/ui/src/LoadingSkeleton.stories.tsx similarity index 100% rename from src/shared/ui/LoadingSkeleton.stories.tsx rename to packages/ui/src/LoadingSkeleton.stories.tsx diff --git a/src/shared/ui/LoadingSkeleton.tsx b/packages/ui/src/LoadingSkeleton.tsx similarity index 100% rename from src/shared/ui/LoadingSkeleton.tsx rename to packages/ui/src/LoadingSkeleton.tsx diff --git a/src/shared/ui/StateView.stories.tsx b/packages/ui/src/StateView.stories.tsx similarity index 100% rename from src/shared/ui/StateView.stories.tsx rename to packages/ui/src/StateView.stories.tsx diff --git a/src/shared/ui/StateView.test.tsx b/packages/ui/src/StateView.test.tsx similarity index 100% rename from src/shared/ui/StateView.test.tsx rename to packages/ui/src/StateView.test.tsx diff --git a/src/shared/ui/StateView.tsx b/packages/ui/src/StateView.tsx similarity index 100% rename from src/shared/ui/StateView.tsx rename to packages/ui/src/StateView.tsx diff --git a/src/shared/ui/index.ts b/packages/ui/src/index.ts similarity index 77% rename from src/shared/ui/index.ts rename to packages/ui/src/index.ts index db527c2..cb0030f 100644 --- a/src/shared/ui/index.ts +++ b/packages/ui/src/index.ts @@ -1,4 +1,3 @@ -export { AppShell } from "./AppShell"; export { EntryCard } from "./EntryCard"; export { LoadingSkeleton } from "./LoadingSkeleton"; export { StateView } from "./StateView"; diff --git a/src/shared/theme/styles.css b/packages/ui/src/styles.css similarity index 100% rename from src/shared/theme/styles.css rename to packages/ui/src/styles.css diff --git a/src/shared/theme/theme.test.ts b/packages/ui/src/theme.test.ts similarity index 95% rename from src/shared/theme/theme.test.ts rename to packages/ui/src/theme.test.ts index cd1dc79..e689f76 100644 --- a/src/shared/theme/theme.test.ts +++ b/packages/ui/src/theme.test.ts @@ -1,7 +1,7 @@ import { DEFAULT_THEME, mergeMantineTheme } from "@mantine/core"; +import source from "@wealthledger/tokens/tokens.json"; import { describe, expect, it } from "vitest"; import { colorRoles, cssVariablesResolver, theme } from "./theme"; -import source from "./tokens.json"; describe("design theme adapter", () => { it("preserves scale-aware sizes, unscaled breakpoints and shadow layers", () => { diff --git a/src/shared/theme/theme.ts b/packages/ui/src/theme.ts similarity index 96% rename from src/shared/theme/theme.ts rename to packages/ui/src/theme.ts index 07737b3..ba8504b 100644 --- a/src/shared/theme/theme.ts +++ b/packages/ui/src/theme.ts @@ -1,3 +1,4 @@ +import "./styles.css"; import { type CSSVariablesResolver, colorsTuple, @@ -8,7 +9,7 @@ import { rem, rgba, } from "@mantine/core"; -import designTokens from "./tokens.json"; +import designTokens from "@wealthledger/tokens/tokens.json"; const { color, typography, spacing, radius, shadows, breakpoints } = designTokens; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json new file mode 100644 index 0000000..b287378 --- /dev/null +++ b/packages/ui/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "strict": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "declaration": true, + "emitDeclarationOnly": true, + "rootDir": "src", + "outDir": "dist", + "jsx": "react-jsx", + "types": ["vite/client"] + }, + "include": ["src/index.ts", "src/theme.ts"] +} diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts new file mode 100644 index 0000000..5057d03 --- /dev/null +++ b/packages/ui/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + build: { + lib: { + entry: { index: "src/index.ts", theme: "src/theme.ts" }, + formats: ["es"], + cssFileName: "styles", + }, + rolldownOptions: { + external: /^(react(?:-dom)?|@mantine\/[^/]+|@wealthledger\/tokens)(\/|$)/, + }, + }, +}); diff --git a/src/app/routes/AppRoutes.test.tsx b/src/app/routes/AppRoutes.test.tsx index fd796f1..6900830 100644 --- a/src/app/routes/AppRoutes.test.tsx +++ b/src/app/routes/AppRoutes.test.tsx @@ -1,12 +1,11 @@ import { MantineProvider } from "@mantine/core"; import { QueryClientProvider } from "@tanstack/react-query"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { cssVariablesResolver, theme } from "@wealthledger/ui/theme"; import { HttpResponse, http } from "msw"; import { MemoryRouter } from "react-router"; import { describe, expect, it } from "vitest"; - import { AuthProvider } from "@/features/auth"; -import { cssVariablesResolver, theme } from "@/shared/theme"; import { createTestQueryClient } from "@/test/query"; import { server } from "@/test/server"; import { AppRoutes } from "./AppRoutes"; diff --git a/src/app/routes/ProtectedLayout.tsx b/src/app/routes/ProtectedLayout.tsx index 895b5b7..1aa9176 100644 --- a/src/app/routes/ProtectedLayout.tsx +++ b/src/app/routes/ProtectedLayout.tsx @@ -1,6 +1,5 @@ import type { FC } from "react"; -import { AppBreadcrumbs } from "@/app/ui"; -import { AppShell } from "@/shared/ui"; +import { AppBreadcrumbs, AppShell } from "@/app/ui"; import { ProtectedDocumentTitle } from "./DocumentTitle"; const ProtectedLayout: FC = () => { diff --git a/src/app/ui/App.tsx b/src/app/ui/App.tsx index 5621e02..74953f3 100644 --- a/src/app/ui/App.tsx +++ b/src/app/ui/App.tsx @@ -1,11 +1,11 @@ import { MantineProvider } from "@mantine/core"; import { DatesProvider } from "@mantine/dates"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { cssVariablesResolver, theme } from "@wealthledger/ui/theme"; import type { FC } from "react"; import { BrowserRouter } from "react-router"; import { AppRoutes, RouteErrorBoundary } from "@/app/routes"; import { AuthProvider } from "@/features/auth"; -import { cssVariablesResolver, theme } from "@/shared/theme"; const queryClient = new QueryClient({ defaultOptions: { diff --git a/src/shared/ui/AppShell.module.css b/src/app/ui/AppShell.module.css similarity index 100% rename from src/shared/ui/AppShell.module.css rename to src/app/ui/AppShell.module.css diff --git a/src/shared/ui/AppShell.tsx b/src/app/ui/AppShell.tsx similarity index 98% rename from src/shared/ui/AppShell.tsx rename to src/app/ui/AppShell.tsx index cc2e63f..855cd29 100644 --- a/src/shared/ui/AppShell.tsx +++ b/src/app/ui/AppShell.tsx @@ -2,7 +2,7 @@ import { AppShell as MantineAppShell, NavLink, Text } from "@mantine/core"; import type { FC, ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { Outlet, NavLink as RouterNavLink, useLocation } from "react-router"; -import { bindIconAnimation } from "../lib"; +import { bindIconAnimation } from "@/shared/lib"; import classes from "./AppShell.module.css"; type AppShellProps = { diff --git a/src/app/ui/index.ts b/src/app/ui/index.ts index 0f97dc8..256c689 100644 --- a/src/app/ui/index.ts +++ b/src/app/ui/index.ts @@ -1 +1,2 @@ export { AppBreadcrumbs } from "./AppBreadcrumbs"; +export { AppShell } from "./AppShell"; diff --git a/src/entities/bank-card/index.ts b/src/entities/bank-card/index.ts index ba1c14f..d186bd9 100644 --- a/src/entities/bank-card/index.ts +++ b/src/entities/bank-card/index.ts @@ -18,6 +18,7 @@ export { moneyCents, positiveMoneySchema, } from "./model/form-validation"; +export { bankOperationKinds } from "./model/operation-kinds"; export { bankPeriod } from "./model/period"; export { BankCardFace } from "./ui/BankCardFace"; export { BankCardForm } from "./ui/BankCardForm"; diff --git a/src/entities/bank-card/model/operation-kinds.ts b/src/entities/bank-card/model/operation-kinds.ts new file mode 100644 index 0000000..e849652 --- /dev/null +++ b/src/entities/bank-card/model/operation-kinds.ts @@ -0,0 +1,10 @@ +import type { BankOperation } from "../api/queries"; + +const bankOperationKinds = [ + "expense", + "topup", + "transfer", + "refund", +] as const satisfies readonly BankOperation["kind"][]; + +export { bankOperationKinds }; diff --git a/src/entities/bank-card/ui/BankOperationIcon.stories.tsx b/src/entities/bank-card/ui/BankOperationIcon.stories.tsx index 4929acc..377c320 100644 --- a/src/entities/bank-card/ui/BankOperationIcon.stories.tsx +++ b/src/entities/bank-card/ui/BankOperationIcon.stories.tsx @@ -1,6 +1,6 @@ import { Stack } from "@mantine/core"; import type { Meta, StoryObj } from "@storybook/react-vite"; -import { EntryCard } from "@/shared/ui"; +import { EntryCard } from "@wealthledger/ui"; import { BankOperationIcon } from "./BankOperationIcon"; const meta = { diff --git a/src/entities/bank-card/ui/BankSummary.view.tsx b/src/entities/bank-card/ui/BankSummary.view.tsx index 6fc9598..bd4ac37 100644 --- a/src/entities/bank-card/ui/BankSummary.view.tsx +++ b/src/entities/bank-card/ui/BankSummary.view.tsx @@ -1,8 +1,8 @@ import { Button, Group, Skeleton, Stack, Text, Title } from "@mantine/core"; +import { LoadingSkeleton } from "@wealthledger/ui"; import type { CSSProperties, FC } from "react"; import { useTranslation } from "react-i18next"; import { formatMoney } from "@/shared/lib"; -import { LoadingSkeleton } from "@/shared/ui"; import type { BankSummary } from "../api/queries"; import { BankOperationIcon } from "./BankOperationIcon"; import classes from "./BankSummary.module.css"; diff --git a/src/entities/broker-operation/ui/LedgerEntryRow.tsx b/src/entities/broker-operation/ui/LedgerEntryRow.tsx index 8944e50..8aaab15 100644 --- a/src/entities/broker-operation/ui/LedgerEntryRow.tsx +++ b/src/entities/broker-operation/ui/LedgerEntryRow.tsx @@ -1,4 +1,5 @@ import { Anchor, Group, Text } from "@mantine/core"; +import { EntryCard } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router"; @@ -8,7 +9,6 @@ import { formatSnapshotTime, instrumentLogoUrl, } from "@/shared/lib"; -import { EntryCard } from "@/shared/ui"; import type { BrokerageOperation } from "../api/useBrokerageOperations"; import { operationLabelKey, paymentPresentation } from "../model/display"; diff --git a/src/entities/brokerage-event/ui/BrokerageEventCard.tsx b/src/entities/brokerage-event/ui/BrokerageEventCard.tsx index feb11bb..18f61f7 100644 --- a/src/entities/brokerage-event/ui/BrokerageEventCard.tsx +++ b/src/entities/brokerage-event/ui/BrokerageEventCard.tsx @@ -1,7 +1,7 @@ +import { EntryCard } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; import { formatDate, formatMoney, instrumentLogoUrl } from "@/shared/lib"; -import { EntryCard } from "@/shared/ui"; import type { BrokerageEvent } from "../api/useBrokerageEvents"; type BrokerageEventCardProps = { event: BrokerageEvent }; diff --git a/src/entities/brokerage-event/ui/BrokerageEventsPreview.tsx b/src/entities/brokerage-event/ui/BrokerageEventsPreview.tsx index 36fff05..a365439 100644 --- a/src/entities/brokerage-event/ui/BrokerageEventsPreview.tsx +++ b/src/entities/brokerage-event/ui/BrokerageEventsPreview.tsx @@ -1,8 +1,8 @@ import { Alert, Button, Skeleton, Stack, Text } from "@mantine/core"; +import { LoadingSkeleton } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router"; -import { LoadingSkeleton } from "@/shared/ui"; import type { BrokerageEvent } from "../api/useBrokerageEvents"; import { BrokerageEventCard } from "./BrokerageEventCard"; diff --git a/src/features/instrument-details/ui/InstrumentSection.view.tsx b/src/features/instrument-details/ui/InstrumentSection.view.tsx index f106845..5534f1e 100644 --- a/src/features/instrument-details/ui/InstrumentSection.view.tsx +++ b/src/features/instrument-details/ui/InstrumentSection.view.tsx @@ -8,9 +8,9 @@ import { Text, Title, } from "@mantine/core"; +import { LoadingSkeleton } from "@wealthledger/ui"; import type { FC, PropsWithChildren, ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import { LoadingSkeleton } from "@/shared/ui"; import type { SectionState } from "../model/section"; import classes from "./InstrumentDetails.module.css"; diff --git a/src/features/tbank-connection/ui/TbankConnectionSection.view.tsx b/src/features/tbank-connection/ui/TbankConnectionSection.view.tsx index dd5c9cc..d090d3c 100644 --- a/src/features/tbank-connection/ui/TbankConnectionSection.view.tsx +++ b/src/features/tbank-connection/ui/TbankConnectionSection.view.tsx @@ -1,7 +1,7 @@ import { Alert, Button, Skeleton } from "@mantine/core"; +import { LoadingSkeleton } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; -import { LoadingSkeleton } from "@/shared/ui"; type TbankConnectionErrorProps = { retry: () => void; diff --git a/src/main.tsx b/src/main.tsx index 1c7e32f..3a2c079 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client"; import "@mantine/core/styles.css"; import "@mantine/charts/styles.css"; import "@mantine/dates/styles.css"; +import "@wealthledger/ui/styles.css"; import "dayjs/locale/ru"; import "./shared/i18n"; import { App } from "./app"; diff --git a/src/pages/account-events/ui/components/AccountEventsPageEmpty.view.tsx b/src/pages/account-events/ui/components/AccountEventsPageEmpty.view.tsx index 5e09eb9..f3cec28 100644 --- a/src/pages/account-events/ui/components/AccountEventsPageEmpty.view.tsx +++ b/src/pages/account-events/ui/components/AccountEventsPageEmpty.view.tsx @@ -1,6 +1,6 @@ +import { StateView } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; -import { StateView } from "@/shared/ui"; const AccountEventsPageEmpty: FC = () => { const { t } = useTranslation(); diff --git a/src/pages/account-events/ui/components/AccountEventsPageError.view.tsx b/src/pages/account-events/ui/components/AccountEventsPageError.view.tsx index ae03f49..95d463e 100644 --- a/src/pages/account-events/ui/components/AccountEventsPageError.view.tsx +++ b/src/pages/account-events/ui/components/AccountEventsPageError.view.tsx @@ -1,7 +1,7 @@ import { Button } from "@mantine/core"; +import { StateView } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; -import { StateView } from "@/shared/ui"; type AccountEventsPageErrorProps = { retry: () => void }; diff --git a/src/pages/account-events/ui/components/AccountEventsPageLoading.view.tsx b/src/pages/account-events/ui/components/AccountEventsPageLoading.view.tsx index 0370db8..0f04cca 100644 --- a/src/pages/account-events/ui/components/AccountEventsPageLoading.view.tsx +++ b/src/pages/account-events/ui/components/AccountEventsPageLoading.view.tsx @@ -1,7 +1,7 @@ import { Group, Skeleton } from "@mantine/core"; +import { LoadingSkeleton } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; -import { LoadingSkeleton } from "@/shared/ui"; const skeletonRowIds = ["first", "second", "third", "fourth"]; diff --git a/src/pages/account-income/model/period.ts b/src/pages/account-income/model/period.ts new file mode 100644 index 0000000..1513a50 --- /dev/null +++ b/src/pages/account-income/model/period.ts @@ -0,0 +1,46 @@ +type IncomePeriod = { from: string; to: string }; +type IncomeForm = IncomePeriod; + +const datePattern = /^\d{4}-\d{2}-\d{2}$/; + +function formatDraftDate(value: string): string { + const [year, month, day] = value.split("-"); + return `${day}.${month}.${year}`; +} + +function parseDraftDate(value: string): string | null { + if (!/^\d{2}\.\d{2}\.\d{4}$/.test(value)) return null; + const [day, month, year] = value.split("."); + const date = `${year}-${month}-${day}`; + return isDate(date) ? date : null; +} + +function isDate(value: string): boolean { + if (!datePattern.test(value)) return false; + const parsed = new Date(`${value}T00:00:00Z`); + return ( + Number.isFinite(parsed.valueOf()) && + parsed.toISOString().slice(0, 10) === value + ); +} + +function moscowDate(value: Date): string { + const parts = new Intl.DateTimeFormat("en-CA", { + timeZone: "Europe/Moscow", + year: "numeric", + month: "2-digit", + day: "2-digit", + }).formatToParts(value); + const part = (type: string) => + parts.find((item) => item.type === type)?.value; + return `${part("year")}-${part("month")}-${part("day")}`; +} + +export { + formatDraftDate, + type IncomeForm, + type IncomePeriod, + isDate, + moscowDate, + parseDraftDate, +}; diff --git a/src/pages/account-income/ui/AccountIncomePage.test.tsx b/src/pages/account-income/ui/AccountIncomePage.test.tsx index 3a587f3..2416043 100644 --- a/src/pages/account-income/ui/AccountIncomePage.test.tsx +++ b/src/pages/account-income/ui/AccountIncomePage.test.tsx @@ -23,8 +23,9 @@ import type { IncomeAnalytics } from "@/entities/income"; import { configureAuthClient } from "@/shared/api"; import { createQueryWrapper } from "@/test/query"; import { server } from "@/test/server"; +import { isDate } from "../model/period"; import { AccountIncomePage } from "./AccountIncomePage"; -import { isDate, toIncomeDisplayModel } from "./AccountIncomePage.vm"; +import { toIncomeDisplayModel } from "./AccountIncomePage.vm"; import { IncomePayments } from "./components/IncomePayments.view"; const analytics = { diff --git a/src/pages/account-income/ui/AccountIncomePage.vm.ts b/src/pages/account-income/ui/AccountIncomePage.vm.ts index 9237330..f3eaf64 100644 --- a/src/pages/account-income/ui/AccountIncomePage.vm.ts +++ b/src/pages/account-income/ui/AccountIncomePage.vm.ts @@ -6,12 +6,17 @@ import { z } from "zod"; import { type IncomeAnalytics, useIncomeAnalytics } from "@/entities/income"; import { validationMessage } from "@/shared/i18n"; import { formatDate } from "@/shared/lib"; +import { + formatDraftDate, + type IncomeForm, + type IncomePeriod, + isDate, + moscowDate, + parseDraftDate, +} from "../model/period"; const ISO_MONTH_LENGTH = 7; -type IncomePeriod = { from: string; to: string }; -type IncomeForm = IncomePeriod; - type IncomeDisplayModel = { entries: { key: string; operation: IncomeAnalytics["entries"][number] }[]; comparison: { @@ -41,7 +46,6 @@ type AccountIncomePageViewModel = onSubmit: (values: IncomeForm) => void; }; -const datePattern = /^\d{4}-\d{2}-\d{2}$/; const draftDate = z .string() .refine((value) => parseDraftDate(value) !== null, { @@ -129,18 +133,6 @@ const useAccountIncomePageViewModel = ( }; }; -function formatDraftDate(value: string): string { - const [year, month, day] = value.split("-"); - return `${day}.${month}.${year}`; -} - -function parseDraftDate(value: string): string | null { - if (!/^\d{2}\.\d{2}\.\d{4}$/.test(value)) return null; - const [day, month, year] = value.split("."); - const date = `${year}-${month}-${day}`; - return isDate(date) ? date : null; -} - function draftPeriod(period: IncomePeriod): IncomeForm { return { from: formatDraftDate(period.from), to: formatDraftDate(period.to) }; } @@ -163,15 +155,6 @@ function validPeriod(period: IncomePeriod): period is IncomePeriod { ); } -function isDate(value: string): boolean { - if (!datePattern.test(value)) return false; - const parsed = new Date(`${value}T00:00:00Z`); - return ( - Number.isFinite(parsed.valueOf()) && - parsed.toISOString().slice(0, 10) === value - ); -} - function toIncomeDisplayModel( analytics: IncomeAnalytics, ): IncomeDisplayModel | null { @@ -276,26 +259,9 @@ function formatIncomeChange( }).format(ratio); } -function moscowDate(value: Date): string { - const parts = new Intl.DateTimeFormat("en-CA", { - timeZone: "Europe/Moscow", - year: "numeric", - month: "2-digit", - day: "2-digit", - }).formatToParts(value); - const part = (type: string) => - parts.find((item) => item.type === type)?.value; - return `${part("year")}-${part("month")}-${part("day")}`; -} - export { type AccountIncomePageViewModel, - formatDraftDate, type IncomeDisplayModel, - type IncomeForm, - isDate, - moscowDate, - parseDraftDate, toIncomeDisplayModel, useAccountIncomePageViewModel, }; diff --git a/src/pages/account-income/ui/components/AccountIncomePageError.view.tsx b/src/pages/account-income/ui/components/AccountIncomePageError.view.tsx index 3bf8094..e125d81 100644 --- a/src/pages/account-income/ui/components/AccountIncomePageError.view.tsx +++ b/src/pages/account-income/ui/components/AccountIncomePageError.view.tsx @@ -1,7 +1,7 @@ import { Button } from "@mantine/core"; +import { StateView } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; -import { StateView } from "@/shared/ui"; import type { AccountIncomePageViewModel } from "../AccountIncomePage.vm"; type ErrorProps = Extract; diff --git a/src/pages/account-income/ui/components/AccountIncomePageLoaded.view.tsx b/src/pages/account-income/ui/components/AccountIncomePageLoaded.view.tsx index 4e36b1e..87f003e 100644 --- a/src/pages/account-income/ui/components/AccountIncomePageLoaded.view.tsx +++ b/src/pages/account-income/ui/components/AccountIncomePageLoaded.view.tsx @@ -7,10 +7,10 @@ import { Text, Title, } from "@mantine/core"; +import { StateView } from "@wealthledger/ui"; import type { FC } from "react"; import { FormProvider } from "react-hook-form"; import { useTranslation } from "react-i18next"; -import { StateView } from "@/shared/ui"; import classes from "../AccountIncomePage.module.css"; import type { AccountIncomePageViewModel } from "../AccountIncomePage.vm"; import { IncomeComparisons } from "./IncomeComparisons.view"; diff --git a/src/pages/account-income/ui/components/AccountIncomePageLoading.view.tsx b/src/pages/account-income/ui/components/AccountIncomePageLoading.view.tsx index bf93469..f465dbb 100644 --- a/src/pages/account-income/ui/components/AccountIncomePageLoading.view.tsx +++ b/src/pages/account-income/ui/components/AccountIncomePageLoading.view.tsx @@ -1,7 +1,7 @@ import { Paper, Skeleton, Stack } from "@mantine/core"; +import { LoadingSkeleton } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; -import { LoadingSkeleton } from "@/shared/ui"; import classes from "../AccountIncomePage.module.css"; const AccountIncomePageLoading: FC = () => { diff --git a/src/pages/account-income/ui/components/IncomePeriodForm.stories.tsx b/src/pages/account-income/ui/components/IncomePeriodForm.stories.tsx new file mode 100644 index 0000000..29f735c --- /dev/null +++ b/src/pages/account-income/ui/components/IncomePeriodForm.stories.tsx @@ -0,0 +1,58 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { FC } from "react"; +import { FormProvider, useForm, useFormState } from "react-hook-form"; +import type { IncomeForm } from "../../model/period"; +import { IncomePeriodForm } from "./IncomePeriodForm.view"; + +const period = { from: "01.08.2026", to: "27.08.2026" }; +const errors = { + to: { type: "manual", message: "Синтетическая ошибка периода" }, +}; + +type PeriodExampleProps = { invalid?: boolean }; + +const PeriodExample: FC = (props) => { + const methods = useForm({ + defaultValues: period, + errors: props.invalid ? errors : undefined, + }); + const { isSubmitSuccessful } = useFormState({ control: methods.control }); + const submitted = isSubmitSuccessful ? methods.getValues() : null; + return ( + + undefined)} + onReset={() => { + methods.reset(period); + }} + /> + + {submitted + ? `Применено: ${submitted.from} – ${submitted.to}` + : "Период не применён"} + + + ); +}; + +const meta = { + title: "Product/IncomePeriodForm", + component: PeriodExample, + tags: ["autodocs"], + parameters: { + docs: { + description: { + component: + "Изолированная форма с синтетическим периодом. Submit показывает значения локально; reset возвращает fixture. Ошибка задана явно: query, URL и production validation остаются у page VM, API и router providers не используются.", + }, + }, + }, + render: () => , +} satisfies Meta; + +export default meta; +type Story = StoryObj; +export const Default: Story = {}; +export const ValidationError: Story = { + render: () => , +}; diff --git a/src/pages/account-income/ui/components/IncomePeriodForm.view.tsx b/src/pages/account-income/ui/components/IncomePeriodForm.view.tsx index ae78c66..7b1be80 100644 --- a/src/pages/account-income/ui/components/IncomePeriodForm.view.tsx +++ b/src/pages/account-income/ui/components/IncomePeriodForm.view.tsx @@ -3,13 +3,13 @@ import { DatePickerInput } from "@mantine/dates"; import type { FC, FormEventHandler } from "react"; import { useController, useFormContext, useWatch } from "react-hook-form"; import { useTranslation } from "react-i18next"; -import classes from "../AccountIncomePage.module.css"; import { formatDraftDate, type IncomeForm, moscowDate, parseDraftDate, -} from "../AccountIncomePage.vm"; +} from "../../model/period"; +import classes from "../AccountIncomePage.module.css"; type IncomePeriodFormProps = { onSubmit: FormEventHandler; diff --git a/src/pages/account-operations/ui/components/AccountOperationsPageError.view.tsx b/src/pages/account-operations/ui/components/AccountOperationsPageError.view.tsx index c5987d6..ace026c 100644 --- a/src/pages/account-operations/ui/components/AccountOperationsPageError.view.tsx +++ b/src/pages/account-operations/ui/components/AccountOperationsPageError.view.tsx @@ -1,7 +1,7 @@ import { Button } from "@mantine/core"; +import { StateView } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; -import { StateView } from "@/shared/ui"; import type { AccountOperationsPageViewModel } from "../AccountOperationsPage.vm"; type AccountOperationsPageErrorProps = Extract< diff --git a/src/pages/account-operations/ui/components/AccountOperationsPageLoaded.view.tsx b/src/pages/account-operations/ui/components/AccountOperationsPageLoaded.view.tsx index 2f02397..019d6ba 100644 --- a/src/pages/account-operations/ui/components/AccountOperationsPageLoaded.view.tsx +++ b/src/pages/account-operations/ui/components/AccountOperationsPageLoaded.view.tsx @@ -8,6 +8,7 @@ import { Title, } from "@mantine/core"; import { DatePickerInput } from "@mantine/dates"; +import { StateView } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; import { @@ -15,7 +16,6 @@ import { type OperationCategory, operationCategories, } from "@/entities/broker-operation"; -import { StateView } from "@/shared/ui"; import type { AccountOperationsPageViewModel } from "../AccountOperationsPage.vm"; type AccountOperationsPageLoadedProps = Extract< diff --git a/src/pages/account-operations/ui/components/AccountOperationsPageLoading.view.tsx b/src/pages/account-operations/ui/components/AccountOperationsPageLoading.view.tsx index df54e8e..63b8310 100644 --- a/src/pages/account-operations/ui/components/AccountOperationsPageLoading.view.tsx +++ b/src/pages/account-operations/ui/components/AccountOperationsPageLoading.view.tsx @@ -1,7 +1,7 @@ import { Group, Skeleton, Stack } from "@mantine/core"; +import { LoadingSkeleton } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; -import { LoadingSkeleton } from "@/shared/ui"; const skeletonRowIds = ["first", "second", "third", "fourth"]; diff --git a/src/pages/account/ui/components/AccountPageEmpty.view.tsx b/src/pages/account/ui/components/AccountPageEmpty.view.tsx index 8e8ca97..0292eb7 100644 --- a/src/pages/account/ui/components/AccountPageEmpty.view.tsx +++ b/src/pages/account/ui/components/AccountPageEmpty.view.tsx @@ -1,9 +1,9 @@ import { Stack } from "@mantine/core"; +import { StateView } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; import type { BrokerageAccount } from "@/entities/brokerage-account"; import { BrokerageEventsPreview } from "@/entities/brokerage-event"; -import { StateView } from "@/shared/ui"; import type { AccountPageIncomeSummary, AccountPageRecentEvents, diff --git a/src/pages/account/ui/components/AccountPageError.view.tsx b/src/pages/account/ui/components/AccountPageError.view.tsx index f10b739..4d85c56 100644 --- a/src/pages/account/ui/components/AccountPageError.view.tsx +++ b/src/pages/account/ui/components/AccountPageError.view.tsx @@ -1,8 +1,8 @@ import { Button } from "@mantine/core"; +import { StateView } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router"; -import { StateView } from "@/shared/ui"; type AccountPageErrorProps = { code?: string; retry: () => void }; diff --git a/src/pages/account/ui/components/AccountPageLoading.view.tsx b/src/pages/account/ui/components/AccountPageLoading.view.tsx index ac07237..47a6a97 100644 --- a/src/pages/account/ui/components/AccountPageLoading.view.tsx +++ b/src/pages/account/ui/components/AccountPageLoading.view.tsx @@ -1,7 +1,7 @@ import { Paper, SimpleGrid, Skeleton } from "@mantine/core"; +import { LoadingSkeleton } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; -import { LoadingSkeleton } from "@/shared/ui"; import classes from "../AccountPage.module.css"; const AccountPageLoading: FC = () => { diff --git a/src/pages/account/ui/components/IncomeSummary.view.tsx b/src/pages/account/ui/components/IncomeSummary.view.tsx index 6b86d66..c789b98 100644 --- a/src/pages/account/ui/components/IncomeSummary.view.tsx +++ b/src/pages/account/ui/components/IncomeSummary.view.tsx @@ -1,10 +1,10 @@ import { Button, Paper, Stack, Text } from "@mantine/core"; +import { StateView } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router"; import type { IncomeSummary as IncomeSummaryData } from "@/entities/income"; import { formatMoney } from "@/shared/lib"; -import { StateView } from "@/shared/ui"; type IncomeSummaryProps = | { state: "loading"; to: string } diff --git a/src/pages/account/ui/components/RecentOperations.view.tsx b/src/pages/account/ui/components/RecentOperations.view.tsx index 5829887..4b4c69a 100644 --- a/src/pages/account/ui/components/RecentOperations.view.tsx +++ b/src/pages/account/ui/components/RecentOperations.view.tsx @@ -1,9 +1,9 @@ import { Alert, Button, Skeleton, Stack, Text } from "@mantine/core"; +import { LoadingSkeleton } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router"; import { LedgerEntryRow } from "@/entities/broker-operation"; -import { LoadingSkeleton } from "@/shared/ui"; import type { AccountPageRecentOperations } from "../AccountPage.vm"; type RecentOperationsProps = { diff --git a/src/pages/accounts/ui/components/AccountsPageEmpty.view.tsx b/src/pages/accounts/ui/components/AccountsPageEmpty.view.tsx index c7e63b8..5e16dc4 100644 --- a/src/pages/accounts/ui/components/AccountsPageEmpty.view.tsx +++ b/src/pages/accounts/ui/components/AccountsPageEmpty.view.tsx @@ -1,7 +1,7 @@ import { Stack } from "@mantine/core"; +import { StateView } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; -import { StateView } from "@/shared/ui"; import { AccountsRefreshState, type AccountsRefreshStateProps, diff --git a/src/pages/accounts/ui/components/AccountsPageError.view.tsx b/src/pages/accounts/ui/components/AccountsPageError.view.tsx index 46faf78..a808eec 100644 --- a/src/pages/accounts/ui/components/AccountsPageError.view.tsx +++ b/src/pages/accounts/ui/components/AccountsPageError.view.tsx @@ -1,8 +1,8 @@ import { Button, Stack } from "@mantine/core"; +import { StateView } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router"; -import { StateView } from "@/shared/ui"; type AccountsPageErrorProps = { code?: string; retry?: () => void }; diff --git a/src/pages/accounts/ui/components/AccountsPageLoading.view.tsx b/src/pages/accounts/ui/components/AccountsPageLoading.view.tsx index 5248e56..da72c3b 100644 --- a/src/pages/accounts/ui/components/AccountsPageLoading.view.tsx +++ b/src/pages/accounts/ui/components/AccountsPageLoading.view.tsx @@ -1,7 +1,7 @@ import { Card, Group, SimpleGrid, Skeleton, Stack } from "@mantine/core"; +import { LoadingSkeleton } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; -import { LoadingSkeleton } from "@/shared/ui"; const AccountsPageLoading: FC = () => { const { t } = useTranslation(); diff --git a/src/pages/bond/ui/BondPage.tsx b/src/pages/bond/ui/BondPage.tsx index 63e0047..906dab0 100644 --- a/src/pages/bond/ui/BondPage.tsx +++ b/src/pages/bond/ui/BondPage.tsx @@ -1,8 +1,8 @@ import { Anchor, Button, Skeleton, Stack, Text, Title } from "@mantine/core"; +import { LoadingSkeleton } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; import { Link, Navigate } from "react-router"; -import { LoadingSkeleton } from "@/shared/ui"; import { BondPageView } from "./BondPage.view"; import { useBondPageVm } from "./BondPage.vm"; diff --git a/src/pages/bond/ui/BondPage.view.tsx b/src/pages/bond/ui/BondPage.view.tsx index d194c8f..ee92b95 100644 --- a/src/pages/bond/ui/BondPage.view.tsx +++ b/src/pages/bond/ui/BondPage.view.tsx @@ -12,6 +12,7 @@ import { Text, Title, } from "@mantine/core"; +import { LoadingSkeleton } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; import { @@ -26,7 +27,6 @@ import { formatMoney, formatSnapshotTime, } from "@/shared/lib"; -import { LoadingSkeleton } from "@/shared/ui"; import classes from "./BondPage.module.css"; import type { BondPageVm } from "./BondPage.vm"; diff --git a/src/pages/bonds/ui/BondsPage.view.tsx b/src/pages/bonds/ui/BondsPage.view.tsx index 66c42bf..e8e9ab3 100644 --- a/src/pages/bonds/ui/BondsPage.view.tsx +++ b/src/pages/bonds/ui/BondsPage.view.tsx @@ -12,6 +12,7 @@ import { TextInput, Title, } from "@mantine/core"; +import { LoadingSkeleton } from "@wealthledger/ui"; import type { FC } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router"; @@ -21,7 +22,6 @@ import { formatMoney, formatSnapshotTime, } from "@/shared/lib"; -import { LoadingSkeleton } from "@/shared/ui"; import classes from "./BondsPage.module.css"; import type { BondsPageVm } from "./BondsPage.vm"; diff --git a/src/pages/card/ui/CardPage.view.tsx b/src/pages/card/ui/CardPage.view.tsx index b04d6c1..50698e9 100644 --- a/src/pages/card/ui/CardPage.view.tsx +++ b/src/pages/card/ui/CardPage.view.tsx @@ -10,6 +10,8 @@ import { Text, Title, } from "@mantine/core"; +import { AddIcon, DeleteIcon, EditIcon } from "@wealthledger/icons"; +import { EntryCard, LoadingSkeleton, StateView } from "@wealthledger/ui"; import { type FC, useLayoutEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router"; @@ -20,7 +22,6 @@ import { BankPeriodPicker, BankSummaryView, } from "@/entities/bank-card"; -import { EntryCard, LoadingSkeleton, StateView } from "@/shared/ui"; import classes from "./CardPage.module.css"; import type { CardPageVm } from "./CardPage.vm"; @@ -106,19 +107,7 @@ const CardPageView: FC = (props) => { title={t("bank.editCard")} onClick={() => vm.setEditingCard(true)} > - + = (props) => { loading={vm.archivePending} onClick={() => void vm.toggleArchive()} > - + {card.archivedAt ? ( + + ) : ( + + )} @@ -196,18 +183,7 @@ const CardPageView: FC = (props) => { disabled={Boolean(card.archivedAt) || !vm.cards.data} onClick={() => vm.setOperation("new")} > - + ({ value, label: t(`bank.kinds.${value}`) }), - )} + data={props.kinds} {...field} disabled={isSubmitting || props.editing} onChange={(value) => { @@ -99,24 +106,22 @@ const OperationFields: FC = (props) => { label={t("bank.date")} required valueFormat="DD.MM.YYYY" - value={ - bankDateSchema.safeParse(field.value).success ? field.value : null - } + value={props.validDate(field.value) ? field.value : null} dateParser={(value) => - bankDateSchema.safeParse(bankDateInputValue(value)).success - ? bankDateInputValue(value) + props.validDate(props.dateInputValue(value)) + ? props.dateInputValue(value) : null } fixOnBlur={false} onInput={(event) => - field.onChange(bankDateInputValue(event.currentTarget.value)) + field.onChange(props.dateInputValue(event.currentTarget.value)) } onChange={(value) => field.onChange(value ?? "")} onBlur={(event) => { - field.onChange(bankDateInputValue(event.currentTarget.value)); + field.onChange(props.dateInputValue(event.currentTarget.value)); field.onBlur(); }} - maxDate={bankToday()} + maxDate={props.maxDate()} popoverProps={{ withinPortal: false, middlewares: { shift: { crossAxis: true } }, @@ -153,7 +158,6 @@ const OperationFields: FC = (props) => { type RefundFieldsProps = { editing: boolean; expenses: Choice[]; - selected?: Choice; onSelect: (id: string) => void; limit: BankOperation["availableRefund"] | undefined; }; @@ -172,14 +176,7 @@ const RefundFields: FC = (props) => {