# API Envelope Runtime Contract ## Goal Устранить расхождение между следующим API-контрактом и реальными runtime-ответами backend'а, чтобы каждый endpoint возвращал ровно один `{ data, meta }`: ```ts // Единственный публичный контракт ApiResponse = { data: T, meta: { fromCache: boolean, cachedAt: string | null } } ``` Убрать поддержку broken double-wrapping на frontend и сделать `TransformInterceptor` единственной точкой формирования публичного envelope. ## Requirements ### Backend 1. **Единый владелец envelope** — `TransformInterceptor` является единственной точкой, формирующей `{ data, meta }` в HTTP-ответе. 2. **Internal carrier** — Services и controllers, которым нужно передать cache metadata, используют внутренний carrier-тип (`ApiEnvelopePayload`), а не создают `{ data, meta }` вручную. 3. **Controllers без meta возвращают чистое DTO** — Если endpoint не использует cache, controller возвращает только domain data, interceptor оборачивает её сам. 4. **Ни один endpoint не приводит к `data.data`** — Runtime-ответ каждого endpoint проверяется интеграционным тестом на одинарный envelope. ### Frontend 1. **Единый формат envelope** — `normalizeEnvelope()` больше не поддерживает double-wrapped ответы. 2. **request()** остаётся `Promise<{ data: T; meta: ApiResponseMeta }>` — контракт не меняется, ясность не уменьшается. ### Testing 1. **HTTP contract tests** — минимальный набор проверяет, что ключевые endpoint'ы возвращают `{ data, meta }` без вложенности. 2. **Существующие тесты обновлены** — сервисные и контроллерные тесты проверяют новый carrier-механизм вместо ручного `{ data, meta }`. 3. **Regression-тесты** — endpoint'ы autentification, shares, bonds, securities, portfolios, T-Bank покрыты минимум одним contract-тестом на shape ответа. ## Acceptance Criteria 1. `GET /api/v1/health` возвращает `{ data: { ... }, meta: { fromCache, cachedAt } }` — без `data.data`. 2. `GET /api/v1/securities/shares/{secid}` — одинарный envelope. 3. `GET /api/v1/securities/shares/{secid}/marketdata` — одинарный envelope с корректным `meta` от cache. 4. `GET /api/v1/securities/bonds/{secid}` — одинарный envelope. 5. `GET /api/v1/securities/search?q=` — одинарный envelope. 6. `GET /api/v1/securities/screener?` — одинарный envelope. 7. `GET /api/v1/portfolios` … — одинарный envelope. 8. `GET /api/v1/broker/accounts` … — одинарный envelope. 9. `POST /api/v1/auth/register`, `login`, `refresh` — одинарный envelope. 10. Backend тесты проходят: `npm test -w apps/backend`. 11. Frontend тесты проходят: `npm test -w apps/frontend`. 12. `npm run build -w apps/frontend` проходит. 13. `normalizeEnvelope()` больше не проверяет `data.data`. 14. Swagger/OpenAPI артефакты не ломаются (`npm exec -w apps/backend -- vitest run openapi`). ## Non-goals - Не менять форму DTO и Swagger-типы (они уже корректны). - Не менять формат error-ответов. - Не добавлять версионирование API. - Не рефакторить бизнес-логику endpoint'ов.