diff --git a/AGENTS.md b/AGENTS.md index f659aa5..f63193d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Репозиторий -npm workspaces монорепозиторий: `apps/backend` (NestJS), `apps/frontend` (React + Vite). +npm workspaces монорепозиторий: `apps/backend` (NestJS), `apps/frontend` (React + Vite), `apps/docs` (Docusaurus). ## Обязательный подход к разработке @@ -16,13 +16,18 @@ npm workspaces монорепозиторий: `apps/backend` (NestJS), `apps/fr |---|---| | `npm run dev:backend` | Запуск NestJS в режиме watch на :3000 | | `npm run dev:frontend` | Vite dev-сервер на :5173, проксирует `/api` → :3000 | +| `npm run dev:docs` | Docusaurus dev-сервер | | `npm run build:backend` | `nest build` | | `npm run build:frontend` | `tsc -b && vite build` (в две фазы) | +| `npm run build:docs` | `docusaurus build` | | `npm run test:backend` | `vitest run` (SWC, не ts-jest) | -| `npm run lint` | ESLint только для бэкенда | +| `npm run test:frontend` | Frontend Vitest suite | +| `npm run lint` | ESLint для backend и frontend | | `npm run format` | Prettier для всех `*.{ts,tsx}` | | `npm run codegen -w apps/frontend` | `openapi-typescript` из локального Swagger → `src/api/types.ts` | +Live MOEX integration tests opt-in: `npm run test:integration -w apps/backend`. + Один тест: `npx vitest run path/to/test.spec.ts -w apps/backend` ## Переменные окружения @@ -46,7 +51,7 @@ npm workspaces монорепозиторий: `apps/backend` (NestJS), `apps/fr ## Архитектура - **Бэкенд** — единственный клиент MOEX. Фронтенд никогда не обращается к MOEX напрямую. -- Feature-модули: `PrismaModule` (глобальный), `MoexClientModule` (глобальный), `CacheModule` (глобальный), `AuthModule`, `SharesModule`, `BondsModule`, `SecuritiesModule`, `CandlesModule`, `HealthModule`. +- Feature-модули: `PrismaModule` (глобальный), `MoexClientModule` (глобальный), `CacheModule` (глобальный), `AuthModule`, `SharesModule`, `BondsModule`, `SecuritiesModule`, `CandlesModule`, `PortfolioModule`, `HealthModule`. - `MoexClientService` использует p-queue (rate limiter) + circuit breaker (5 ошибок → 30s открыт). - In-memory кеш через `@nestjs/cache-manager`. Путь миграции на Redis описан (см. ADR-002). - Аутентификация: JWT access token (15m, в памяти) + refresh token (7d, httpOnly cookie, bcrypt hash в БД). Глобальный `JwtAuthGuard` (`@Public()` для открытых эндпоинтов). @@ -70,5 +75,6 @@ npm workspaces монорепозиторий: `apps/backend` (NestJS), `apps/fr - Prettier: одинарные кавычки, trailing commas, printWidth 100, точки с запятой. - Бэкенд: `const`, PascalCase для модулей/контроллеров/сервисов, DTO в `dto/` внутри каждого модуля. - Бэкенд использует SWC через `unplugin-swc` (vitest config). -- Тесты фронтенда отсутствуют. -- CI/CD в репозитории нет. +- Тесты фронтенда есть: Vitest + Testing Library + MSW. +- CI находится в `.gitea/workflows/ci.yml`. +- Pre-commit checks настроены через Husky и lint-staged. diff --git a/README.md b/README.md index 9663633..c4618bf 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ - **Backend:** NestJS, TypeScript, OpenAPI (Swagger) - **Frontend:** React, TypeScript, Vite, TanStack Query, lightweight-charts +- **Docs:** Docusaurus - **Infrastructure:** Docker, docker-compose ## Quick Start @@ -36,6 +37,13 @@ docker compose up --build ```bash npm run test:backend +npm run test:frontend +``` + +Live MOEX integration checks are opt-in: + +```bash +npm run test:integration -w apps/backend ``` ## Project Structure @@ -44,6 +52,7 @@ npm run test:backend apps/ backend/ — NestJS API (single point of access to MOEX ISS) frontend/ — React SPA with Vite + docs/ — Docusaurus documentation site docs/ architecture/ — ADR documents and diagrams openapi/ — OpenAPI specification diff --git a/apps/docs/docs/backend/api.md b/apps/docs/docs/backend/api.md index 8276499..11af40d 100644 --- a/apps/docs/docs/backend/api.md +++ b/apps/docs/docs/backend/api.md @@ -130,6 +130,25 @@ } ``` +### `GET /securities/screener` + +Скринер ценных бумаг по параметрам цены, объёма, доходности, дюрации, купона и срока погашения. + +**Parameters:** + +| Param | Type | Required | Description | +|---|---|---|---| +| `type` | enum | yes | `share` или `bond` | +| `priceMin`, `priceMax` | number | no | Диапазон цены | +| `volumeMin` | number | no | Минимальный объём | +| `yieldMin`, `yieldMax` | number | no | Диапазон доходности облигаций | +| `durationMin`, `durationMax` | number | no | Диапазон дюрации | +| `sortBy` | string | no | Поле сортировки | +| `sortOrder` | enum | no | `asc` или `desc` | +| `page`, `pageSize` | integer | no | Пагинация | + +**Response:** `{ data: { items, total, page, pageSize, totalPages }, meta }`. + ## Shares ### `GET /securities/shares/:secid` @@ -354,3 +373,22 @@ "meta": { "fromCache": false, "cachedAt": null } } ``` + +## Portfolios + +Все portfolio endpoints защищены JWT и возвращают envelope `{ data, meta }`. + +| Endpoint | Method | Description | +|---|---|---| +| `/api/v1/portfolios` | GET | Список портфелей пользователя | +| `/api/v1/portfolios` | POST | Создать портфель | +| `/api/v1/portfolios/:id` | GET | Детали портфеля с позициями и текущими ценами | +| `/api/v1/portfolios/:id` | PATCH | Обновить портфель | +| `/api/v1/portfolios/:id` | DELETE | Удалить портфель | +| `/api/v1/portfolios/:id/positions` | POST | Добавить позицию | +| `/api/v1/portfolios/:id/positions/:positionId` | PATCH | Обновить позицию | +| `/api/v1/portfolios/:id/positions/:positionId` | DELETE | Удалить позицию | +| `/api/v1/portfolios/:id/analytics` | GET | Аналитика портфеля и PnL | + +`DELETE` endpoints возвращают `{ data: null, meta }`, что отражено в OpenAPI schema и frontend +codegen types. diff --git a/apps/docs/docs/backend/portfolio.md b/apps/docs/docs/backend/portfolio.md index a7fcbde..92914f4 100644 --- a/apps/docs/docs/backend/portfolio.md +++ b/apps/docs/docs/backend/portfolio.md @@ -1,6 +1,7 @@ # Portfolio Module -The Portfolio module allows users to create and manage virtual investment portfolios for tracking purposes. +Portfolio module позволяет пользователям создавать и вести виртуальные инвестиционные портфели для +аналитики и отслеживания позиций. ## Overview @@ -10,19 +11,19 @@ The Portfolio module allows users to create and manage virtual investment portfo ## API Endpoints -All endpoints require JWT authentication (`JwtAuthGuard`). +Все endpoints требуют JWT authentication (`JwtAuthGuard`). | Endpoint | Method | Description | |---|---|---| -| `/api/v1/portfolios` | GET | List user's portfolios | -| `/api/v1/portfolios` | POST | Create portfolio | -| `/api/v1/portfolios/:id` | GET | Portfolio detail with enriched positions and analytics summary | -| `/api/v1/portfolios/:id/analytics` | GET | Detailed portfolio analytics with PnL | -| `/api/v1/portfolios/:id/patch` | PATCH | Update portfolio (name, description, currency) | -| `/api/v1/portfolios/:id` | DELETE | Delete portfolio (cascade deletes positions) | -| `/api/v1/portfolios/:id/positions` | POST | Add position (accepts buyPrice, buyDate) | -| `/api/v1/portfolios/:id/positions/:posId` | PATCH | Update position (quantity, buyPrice, buyDate, notes) | -| `/api/v1/portfolios/:id/positions/:posId` | DELETE | Remove position | +| `/api/v1/portfolios` | GET | Список портфелей пользователя | +| `/api/v1/portfolios` | POST | Создать портфель | +| `/api/v1/portfolios/:id` | GET | Детали портфеля с обогащёнными позициями и analytics summary | +| `/api/v1/portfolios/:id/analytics` | GET | Детальная аналитика портфеля и PnL | +| `/api/v1/portfolios/:id` | PATCH | Обновить портфель (name, description, currency) | +| `/api/v1/portfolios/:id` | DELETE | Удалить портфель вместе с позициями | +| `/api/v1/portfolios/:id/positions` | POST | Добавить позицию (buyPrice, buyDate, notes, tags) | +| `/api/v1/portfolios/:id/positions/:positionId` | PATCH | Обновить позицию | +| `/api/v1/portfolios/:id/positions/:positionId` | DELETE | Удалить позицию | ## Domain Model diff --git a/apps/docs/docs/development/codegen.md b/apps/docs/docs/development/codegen.md index a15dffc..c04c015 100644 --- a/apps/docs/docs/development/codegen.md +++ b/apps/docs/docs/development/codegen.md @@ -24,6 +24,19 @@ openapi-typescript http://localhost:3000/api/docs-json -o src/api/types.ts ### Output - `apps/frontend/src/api/types.ts` — сгенерированные типы `paths` и `operations` +- `docs/openapi/openapi.yaml` — статический snapshot Swagger JSON для ревью и документации + +### Verify Artifacts + +После регенерации OpenAPI artifacts запустите: + +```bash +npm run test -w apps/backend -- src/openapi-artifacts.spec.ts +``` + +Тест проверяет, что checked-in frontend types и YAML содержат актуальные auth, screener и portfolio +paths, не содержат локальный alternate port и сохраняют важные schema metadata для nullable полей, +array enum tags и typed response envelopes. ### Manual Types diff --git a/apps/docs/docs/development/commands.md b/apps/docs/docs/development/commands.md index 9a1dcdb..aee5e8f 100644 --- a/apps/docs/docs/development/commands.md +++ b/apps/docs/docs/development/commands.md @@ -5,13 +5,16 @@ | Command | Description | |---|---| | `npm run dev:backend` | Запуск NestJS в режиме watch на :3000 | -| `npm run dev:frontend` | Vite dev-сервер на :5173, проксирует `/api` → :3000 | +| `npm run dev:frontend` | Vite dev-сервер на `:5173`, проксирует `/api` на backend | +| `npm run dev:docs` | Docusaurus dev-сервер документации | | `npm run build:backend` | `nest build` | -| `npm run build:frontend` | `tsc -b && vite build` (в две фазы) | -| `npm run test:backend` | `vitest run` (SWC, не ts-jest) | -| `npm run lint` | ESLint только для бэкенда | +| `npm run build:frontend` | `tsc -b && vite build` | +| `npm run build:docs` | `docusaurus build` | +| `npm run test:backend` | Offline backend unit tests через Vitest | +| `npm run test:frontend` | Frontend tests через Vitest + Testing Library | +| `npm run lint` | ESLint для backend и frontend | | `npm run format` | Prettier для всех `*.{ts,tsx}` | -| `npm run format:check` | Prettier check для всех `*.{ts,tsx}` | +| `npm run format:check` | Проверка Prettier для всех `*.{ts,tsx}` | ## Backend Workspace @@ -23,6 +26,7 @@ | `npm run lint -w apps/backend` | ESLint для `{src,test}/**/*.ts` | | `npm run test -w apps/backend` | `vitest run` | | `npm run test:watch -w apps/backend` | `vitest` | +| `npm run test:integration -w apps/backend` | Opt-in live MOEX integration tests, требуется network access | ## Frontend Workspace @@ -32,6 +36,16 @@ | `npm run build -w apps/frontend` | `tsc -b && vite build` | | `npm run preview -w apps/frontend` | `vite preview` | | `npm run codegen -w apps/frontend` | `openapi-typescript` из Swagger → `src/api/types.ts` | +| `npm run lint -w apps/frontend` | ESLint для `src/**/*.{ts,tsx}` | +| `npm run test -w apps/frontend` | Frontend Vitest suite | + +## Docs Workspace + +| Command | Description | +|---|---| +| `npm run dev -w apps/docs` | Docusaurus dev-server | +| `npm run build -w apps/docs` | Production build документации | +| `npm run serve -w apps/docs` | Локальная проверка production build | ## Single Test diff --git a/apps/docs/docs/development/conventions.md b/apps/docs/docs/development/conventions.md index 385bd3b..63b5426 100644 --- a/apps/docs/docs/development/conventions.md +++ b/apps/docs/docs/development/conventions.md @@ -18,7 +18,7 @@ Prettier (`.prettierrc`): ## Linting -ESLint только для бэкенда (`apps/backend`). +ESLint запускается для backend (`apps/backend`) и frontend (`apps/frontend`). Плагины: - `@typescript-eslint/eslint-plugin` diff --git a/apps/docs/docs/development/testing.md b/apps/docs/docs/development/testing.md index c755178..85bcf1f 100644 --- a/apps/docs/docs/development/testing.md +++ b/apps/docs/docs/development/testing.md @@ -39,4 +39,25 @@ npm run test:watch -w apps/backend ## Frontend Tests -Фронтенд-тесты отсутствуют. +Фреймворк: **Vitest 4** + **React Testing Library** + **MSW**. + +Запуск: + +```bash +npm run test:frontend +# или +npm run test -w apps/frontend +``` + +Тесты покрывают API-клиент, auth context, hooks, базовые pages и shared components. + +## Live MOEX Integration Tests + +Live MOEX checks вынесены из default backend suite. + +```bash +npm run test:integration -w apps/backend +``` + +Эта команда opt-in: она требует network access и может падать при недоступности MOEX или сетевых +ограничениях окружения. diff --git a/apps/docs/docs/frontend/api-client.md b/apps/docs/docs/frontend/api-client.md index e39a33d..f974de8 100644 --- a/apps/docs/docs/frontend/api-client.md +++ b/apps/docs/docs/frontend/api-client.md @@ -8,6 +8,7 @@ async function request( path: string, params?: Record, + options?: { method?: string; body?: unknown; skipAuth?: boolean }, ): Promise<{ data: T; meta: ApiResponseMeta }> ``` @@ -20,7 +21,14 @@ async function request( | Function | Method | Path | |---|---|---| | `getHealth()` | GET | `/api/v1/health` | +| `register(data)` | POST | `/api/v1/auth/register` | +| `login(data)` | POST | `/api/v1/auth/login` | +| `refresh()` | POST | `/api/v1/auth/refresh` | +| `logout()` | POST | `/api/v1/auth/logout` | +| `getProfile()` / `getMe()` | GET | `/api/v1/auth/me` | +| `updateProfile(data)` | PATCH | `/api/v1/auth/me` | | `searchSecurities(q, type?, limit?)` | GET | `/api/v1/securities/search` | +| `screenSecurities(query)` / `getScreenerResults(query)` | GET | `/api/v1/securities/screener` | | `getShare(secid)` | GET | `/api/v1/securities/shares/:secid` | | `getShareMarketData(secid)` | GET | `/api/v1/securities/shares/:secid/marketdata` | | `getShareDividends(secid)` | GET | `/api/v1/securities/shares/:secid/dividends` | @@ -30,6 +38,15 @@ async function request( | `getBondHistory(secid, from, till)` | GET | `/api/v1/securities/bonds/:secid/history` | | `getShareCandles(secid, interval, from, till)` | GET | `/api/v1/securities/shares/:secid/candles` | | `getBondCandles(secid, interval, from, till)` | GET | `/api/v1/securities/bonds/:secid/candles` | +| `getPortfolios()` | GET | `/api/v1/portfolios` | +| `createPortfolio(data)` | POST | `/api/v1/portfolios` | +| `getPortfolio(id)` | GET | `/api/v1/portfolios/:id` | +| `updatePortfolio(id, data)` | PATCH | `/api/v1/portfolios/:id` | +| `deletePortfolio(id)` | DELETE | `/api/v1/portfolios/:id` | +| `addPosition(portfolioId, data)` | POST | `/api/v1/portfolios/:id/positions` | +| `updatePosition(portfolioId, positionId, data)` | PATCH | `/api/v1/portfolios/:id/positions/:positionId` | +| `removePosition(portfolioId, positionId)` | DELETE | `/api/v1/portfolios/:id/positions/:positionId` | +| `getPortfolioAnalytics(portfolioId)` | GET | `/api/v1/portfolios/:id/analytics` | ## Types @@ -41,5 +58,8 @@ async function request( - `BondResponse` — спецификация облигации + `BondMarketData` - `CandleItem`, `DividendItem`, `ShareHistoryItem`, `BondHistoryItem` - `SearchResultItem`, `HealthResponse` +- `UserResponse`, `AuthResponse` +- `Portfolio`, `PortfolioDetail`, `Position`, `PortfolioSummary`, `AnalyticsResponse` +- `ScreenerItem`, `ScreenerResult` Codegen-типы из OpenAPI в `api/types.ts` (генерируются через `npm run codegen`). diff --git a/apps/docs/docs/frontend/overview.md b/apps/docs/docs/frontend/overview.md index b589ad0..3cb6e60 100644 --- a/apps/docs/docs/frontend/overview.md +++ b/apps/docs/docs/frontend/overview.md @@ -9,6 +9,7 @@ React SPA, собранная с Vite. - TanStack Query v5 - lightweight-charts v4 - openapi-fetch (с рукописными типами `responses.ts`) +- Vitest + Testing Library + MSW - Vite 5 ## Source Layout @@ -19,10 +20,22 @@ apps/frontend/src/ ├── App.tsx # BrowserRouter ├── routes.tsx # Маршруты ├── api/ +│ ├── auth.ts # Auth API helpers │ ├── client.ts # HTTP-клиент (fetch) +│ ├── portfolio.ts # Portfolio API helpers │ ├── responses.ts # Типы ответов (ручные) +│ ├── screener.ts # Screener API helpers │ └── types.ts # Типы из openapi-typescript +├── context/ +│ └── AuthContext.tsx ├── hooks/ +│ ├── useAuth.ts +│ ├── usePortfolio.ts +│ ├── usePortfolioAnalytics.ts +│ ├── usePortfolioMutations.ts +│ ├── usePortfolios.ts +│ ├── usePositionMutations.ts +│ ├── useScreener.ts │ ├── useSearch.ts │ ├── useStock.ts │ ├── useStockCandles.ts @@ -31,14 +44,24 @@ apps/frontend/src/ │ └── useBondCandles.ts ├── components/ │ ├── Layout.tsx +│ ├── ProtectedRoute.tsx │ ├── SearchBar.tsx │ ├── PriceChart.tsx │ ├── StockDetails.tsx -│ └── BondDetails.tsx +│ ├── BondDetails.tsx +│ └── portfolios/ ├── pages/ │ ├── HomePage.tsx │ ├── StockPage.tsx -│ └── BondPage.tsx +│ ├── BondPage.tsx +│ ├── LoginPage.tsx +│ ├── RegisterPage.tsx +│ ├── ProfilePage.tsx +│ ├── portfolios/ +│ └── screener/ +├── test/ +│ ├── handlers.ts # MSW handlers +│ └── test-utils.tsx └── styles.css ``` diff --git a/apps/docs/docs/frontend/routes.md b/apps/docs/docs/frontend/routes.md index d4ba323..8fa479d 100644 --- a/apps/docs/docs/frontend/routes.md +++ b/apps/docs/docs/frontend/routes.md @@ -2,11 +2,17 @@ Определены в `apps/frontend/src/routes.tsx`. -| Path | Component | Description | -|---|---|---| -| `/` | `HomePage` | Главная страница с приветствием | -| `/stocks/:secid` | `StockPage` | Страница акции | -| `/bonds/:secid` | `BondPage` | Страница облигации | +| Path | Component | Access | Description | +|---|---|---|---| +| `/` | `HomePage` | Public | Главная страница | +| `/stocks/:secid` | `StockPage` | Public | Страница акции | +| `/bonds/:secid` | `BondPage` | Public | Страница облигации | +| `/screener` | `ScreenerPage` | Public | Скринер ценных бумаг | +| `/login` | `LoginPage` | Public | Вход | +| `/register` | `RegisterPage` | Public | Регистрация | +| `/profile` | `ProfilePage` | Protected | Профиль текущего пользователя | +| `/portfolios` | `PortfoliosListPage` | Protected | Список портфелей | +| `/portfolios/:id` | `PortfolioDetailPage` | Protected | Детальная страница портфеля | Все страницы обёрнуты в `Layout`, который содержит: @@ -22,6 +28,33 @@ } /> } /> } /> + } /> + } /> + } /> + + + + } + /> + + + + } + /> + + + + } + /> ``` diff --git a/apps/docs/docs/intro.md b/apps/docs/docs/intro.md index 91e2f5f..895392d 100644 --- a/apps/docs/docs/intro.md +++ b/apps/docs/docs/intro.md @@ -1,3 +1,7 @@ +--- +slug: / +--- + # MoexVibe Веб-приложение для анализа ценных бумаг Московской биржи (MOEX). @@ -15,7 +19,8 @@ moex-vibe/ ├── apps/ │ ├── backend/ # NestJS API (единственная точка доступа к MOEX) -│ └── frontend/ # React SPA +│ ├── frontend/ # React SPA +│ └── docs/ # Docusaurus documentation site ├── docs/ │ ├── architecture/ # ADR и диаграммы │ ├── openapi/ # OpenAPI-спецификация @@ -31,6 +36,6 @@ moex-vibe/ ## Key Principles - Backend — единственный клиент MOEX. Frontend никогда не обращается к MOEX напрямую. -- npm workspaces монорепозиторий: `apps/backend` и `apps/frontend`. +- npm workspaces монорепозиторий: `apps/backend`, `apps/frontend` и `apps/docs`. - Глобальный префикс API: `/api/v1`. Swagger: `/api/docs`. - Ответы API обёрнуты в `{ data: T, meta: { fromCache, cachedAt } }`.