moex-vibe/docs/research/2026-06-25-backend-audit.md
Sergey Krylov ccb1082535 refactor(backend): unify envelope DTOs and fix shares/bonds inconsistency
- Replace 4 duplicate meta DTOs (AuthResponseMetaDto, PortfolioResponseMetaDto,
  BrokerResponseMetaDto, ScreenerResponseMetaDto) with shared ApiResponseMeta
- Wrap shares getShare() in ApiEnvelopePayload (was raw object, unlike bonds)
- Remove unnecessary CacheModule import from securities module
- Update portfolio controller nullDataEnvelopeSchema to use shared ApiResponseMeta
- All 116 tests pass
2026-06-25 20:20:59 +03:00

50 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Backend Architecture Audit 2026-06-25
## Что хорошо
- **Чистая модульная структура** — каждый домен в своём каталоге, NestJS-модули, DI
- **Global-модули** (`PrismaModule`, `CacheModule`, `MoexClientModule`) — не дублируются импорты
- **`CacheService.getOrFetch()`** — универсальный примитив кеширования, config-driven TTL
- **MOEX rate limiter + circuit breaker** — p-queue + ручной CB, защищают от внешнего API
- **TBank mappers layer** — proto wire type → domain type → DTO, правильная изоляция
- **Batch-оптимизация в PortfolioService** — `fetchShareBatch/fetchBondBatch/fetchDividendsBatch` собирает всё одним MOEX-запросом
- **Спецификации и тесты** — 25 spec-файлов, есть TDD-подход
## Ключевые архитектурные проблемы
| # | Проблема | Где | Описание |
|---|----------|-----|----------|
| 1 | **Дублирование Envelope DTO** | Все модули | Каждый модуль переопределяет свой `*ResponseMetaDto / *EnvelopeDto`. 6+ копий одной структуры |
| 2 | **Inconsistent caching** | `shares/shares.service.ts:getShare()` (raw) vs `bonds/bonds.service.ts:getBond()` (envelope) | Разное поведение одинаковых по смыслу методов |
| 3 | **Screener — in-memory filtering** | `securities/screener.service.ts` | При пустом массиве `getShareMarketDataBatch([])` тянет **все** бумаги с MOEX и фильтрует в памяти. Не масштабируется |
| 4 | **MoexClientService — God Service** | 11 публичных методов | Один сервис делает всё: search, shares, bonds, candles, history, dividends. Нарушает SRP |
| 5 | **Отсутствие доменных исключений** | Весь код | Нет иерархии исключений (`SecurityNotFoundException`, `PortfolioAccessDeniedException`, `MoexApiException`) — только generic `NotFoundException` / `ForbiddenException` |
| 6 | **Health check — заглушка** | `health/` | Не проверяет БД, MOEX, T-Bank. Только `{ status: 'ok', timestamp, uptime }` |
| 7 | **Prisma JSON как String** | `targets`, `tags`, `payment`, `price` | JSON хранится как `String` без валидации на уровне БД. Нет типизированных JSON-полей |
| 8 | **tbank/ — перегруженный модуль** | 22 файла, 8 сервисов | Один модуль содержит gRPC клиент, мапперы, CRUD, аналитику, синхронизацию. Можно разбить |
| 9 | **Auth глобальные гарды** | `JwtAuthGuard` + `RolesGuard` как `APP_GUARD` | Неявная защита всех эндпоинтов. Приходится использовать `@Public()` для открытых |
| 10 | **RequestLoggingMiddleware** | Подключён через `.use()`, а не через `configure()` | Работает, но не идёт через DI и не является частью модуля |
## Рекомендации
### 🔴 Critical
1. **Shared envelope DTO** — вынести `ApiResponseMeta` и один generic `EnvelopeDto<T>` в `common/dto/`, убрать дублирование. Унифицировать формат ответа screener'а под общий envelope.
2. **Разделить MoexClientService** — выделить `MoexSecuritiesClient`, `MoexMarketDataClient`, `MoexCandlesClient` — каждый со своим набором методов.
3. **Убрать inconsistency shares vs bonds**`getShare()` должен возвращать `ApiEnvelopePayload` как и `getBond()`.
### 🟡 Medium
4. **Domain exception hierarchy** — создать `BaseDomainException``MoexApiException`, `SecurityNotFoundException`, `PortfolioAccessDeniedException`, `TBankApiException`. Добавить соответствующие фильтры в `HttpExceptionFilter`.
5. **Screener — server-side пагинация** — кешировать полный результат screener'а отдельным TTL.
6. **Health check прокачка** — добавить проверки Prisma (`db.ping()`), MOEX (`/health`), T-Bank gRPC connectivity.
7. **Prisma JSON → typed JSON** — использовать строки с `JSON.parse` в геттерах или перейти на отдельные таблицы.
8. **Отвязать `securities/` от прямого импорта `CacheModule`** — раз он `@Global()`, убрать лишний импорт.
### 🟢 Low / Nice to have
9. **RequestLoggingMiddleware** — перевести на `configure()` в `AppModule` для единообразия.
10. **Refactor `tbank/`** — выделить `broker-analytics` и `broker-sync` в отдельные модули, если будут расти.
11. **Swagger schema object для обёртки** — глобально настроить OpenAPI для автоматической обёртки `{ data, meta }`.
12. **Circuit breaker — вынести в декоратор** — обобщить в `@CircuitBreaker()` декоратор.