spec: record account-top-positions planning and implementation (#48) #69

Merged
codex-bot merged 4 commits from feature/48-account-top-positions into develop 2026-11-01 19:29:12 +03:00
Showing only changes of commit c54df86a7c - Show all commits
+23 -23
View File
@@ -4,49 +4,49 @@ Issue-координатор: [WealthLedger/main#48](https://git.personal.ksv741
## 1. Backend: хранение, классификация, DTO, OpenAPI
- [ ] 1.1 [backend] Verification: ветка `feature/48-account-top-positions` от актуального `develop`
- [x] 1.1 [backend] Verification: ветка `feature/48-account-top-positions` от актуального `develop`
- **Covers:** N/A — организационный шаг cross-repo delivery
- **Source:** `.ai/conventions/engineering-workflow.md` «Analysis → Branch», «Branch → Implementation»
- **Verification:** `git -C backend branch --show-current` выводит `feature/48-account-top-positions`; `git log origin/develop..HEAD` пуст до первой задачи
- **Depends on:** none
- [ ] 1.2 [backend] Migration: аддитивная модель `TbankInstrumentClassification` и миграция `CREATE TABLE`
- [x] 1.2 [backend] Migration: аддитивная модель `TbankInstrumentClassification` и миграция `CREATE TABLE`
- **Covers:** `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`
- **Source:** design.md «Data / Storage Changes», B1; «Migration / Rollout / Rollback»
- **Verification:** `npx prisma validate`; `npx prisma generate`; `npm run typecheck:app`; миграция применена к disposable копии БД с существующими Snapshot, прежние таблицы и строки не изменились; `DROP TABLE "TbankInstrumentClassification"` на копии возвращает её к исходной схеме
- **Depends on:** 1.1
- [ ] 1.3 [backend] Implementation: `TbankClient.getEtf`, `getAssetBrand` и opt-in NOT_FOUND для новых вызовов классификации
- [x] 1.3 [backend] Implementation: `TbankClient.getEtf`, `getAssetBrand` и opt-in NOT_FOUND для новых вызовов классификации
- **Covers:** `portfolio-view` — `Результат получения сектора и эмитента`
- **Source:** design.md B4 «Новые методы», «NOT_FOUND»; analysis/data-mapping.md «Источники-кандидаты в T-Bank»
- **Verification:** `npm test -- src/integrations/tbank/tbank.client.spec.ts`: NOT_FOUND даёт `NotFoundException` для `bondBy`/`etfBy`/`getAssetBy` только при opt-in; прежние `getBond`/`getAssetDescription` по-прежнему дают Unavailable; `PERMISSION_DENIED` даёт `InvalidToken`
- **Depends on:** 1.1
- [ ] 1.4 [backend] Implementation: `PositionClassificationService` — правило 24 ч, четыре исхода по каждому полю, UID→FIGI после NOT_FOUND, дедупликация по инструменту и `asset_uid`
- [x] 1.4 [backend] Implementation: `PositionClassificationService` — правило 24 ч, четыре исхода по каждому полю, UID→FIGI после NOT_FOUND, дедупликация по инструменту и `asset_uid`
- **Covers:** `portfolio-view` — `Обновление сектора и эмитента`; `portfolio-view` — `Результат получения сектора и эмитента`; `portfolio-view` — `Нормализованный состав Portfolio`
- **Source:** design.md B3, B4, B5; analysis/failure-model.md «Классификация отказов по исходам PVΔ «Результат получения сектора и эмитента»», F02–F12
- **Verification:** `npm test -- src/modules/investments/portfolio`: границы 23:59 и 24:00 от `walkStartedAt`; при отсутствии значения получение идёт в каждом обходе; 4 исхода × {sector, brand}; mismatch не меняет поле; повтор по FIGI после NOT_FOUND по UID; один `GetAssetBy` на общий `asset_uid`; используется сохранённый `assetUid`; `InvalidToken` пробрасывается, остальные ошибки — нет; типы кроме STOCK/BOND/FUND не запрашиваются
- **Depends on:** 1.2, 1.3
- [ ] 1.5 [backend] Implementation: шаг классификации в `PortfolioService.queueSnapshot` до publish; `walkStartedAt` из `TbankHourlyScheduler`; mapper пишет `sector`/`issuer`
- [x] 1.5 [backend] Implementation: шаг классификации в `PortfolioService.queueSnapshot` до publish; `walkStartedAt` из `TbankHourlyScheduler`; mapper пишет `sector`/`issuer`
- **Covers:** `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`; `portfolio-view` — `Нормализованный состав Portfolio`
- **Source:** design.md B2, B3; analysis/failure-model.md F01, F08
- **Verification:** `npm test -- src/modules/investments/portfolio/application/portfolio.service.spec.ts`: отказ token в классификации → Snapshot не опубликован, вызван `rejectMembershipToken`; прочая ошибка → Snapshot опубликован, `lastSuccessfulAt` и `isStale` не изменены, поля — прежние известные; ошибка GetPortfolio → публикации нет независимо от классификации
- **Depends on:** 1.4
- [ ] 1.6 [backend] Implementation: DTO `sector` и `PortfolioIssuerDto` (nullable), нормализация старых Snapshot в `saved()`
- [x] 1.6 [backend] Implementation: DTO `sector` и `PortfolioIssuerDto` (nullable), нормализация старых Snapshot в `saved()`
- **Covers:** `portfolio-view` — `Нормализованный состав Portfolio`
- **Source:** design.md B6, B7; analysis/system-analysis.md «Семантическая delta `PortfolioPositionDto`», «Совместимость и миграция»
- **Verification:** `npm run test:e2e`: GET `/api/v1/brokerage-accounts/{accountId}/portfolio` и `/all/portfolio` возвращают у каждой позиции `sector` и `issuer`; для payload без этих полей — `null`; после отказа token — `400 INVALID_TBANK_TOKEN`
- **Depends on:** 1.5
- [ ] 1.7 [backend] Contract Parity: OpenAPI-artifact с новыми полями совпадает с target-delta
- [x] 1.7 [backend] Contract Parity: OpenAPI-artifact с новыми полями совпадает с target-delta
- **Covers:** `portfolio-view` — `Нормализованный состав Portfolio`
- **Source:** design.md «Contract Implementation»; analysis/system-analysis.md «Семантическая delta `PortfolioPositionDto`»
- **Verification:** `npm run openapi:generate` и `npm run test:openapi-contract` проходят; в корневом `openapi.json` у `PortfolioPositionDto` поля `sector` (string, nullable, required) и `issuer` (nullable, required, `id`/`name` обязательные строки); версия OpenAPI 3.0.x; запросы, маршруты и статусы не изменились (diff схемы с `frontend/src/shared/api/spec/openapi.json` содержит только эти добавления)
- **Depends on:** 1.6
- [ ] 1.8 [backend] Verification: полный gate и PR в `develop` со ссылкой на WealthLedger/main#48
- [x] 1.8 [backend] Verification: полный gate и PR в `develop` со ссылкой на WealthLedger/main#48
- **Covers:** N/A — финальный gate repo
- **Source:** backend `.ai/conventions/engineering.md` «Проверки и завершение»; `.ai/conventions/engineering-workflow.md` «Validation → PR»
- **Verification:** `npm run verify` зелёный на финальной ревизии; PR открыт, в теле `WealthLedger/main#48` и OpenAPI-artifact приложен как вход для frontend
@@ -54,55 +54,55 @@ Issue-координатор: [WealthLedger/main#48](https://git.personal.ksv741
## 2. Frontend: контракт, модель, UI
- [ ] 2.1 [frontend] Verification: ветка `feature/48-account-top-positions` от актуального `develop`
- [x] 2.1 [frontend] Verification: ветка `feature/48-account-top-positions` от актуального `develop`
- **Covers:** N/A — организационный шаг cross-repo delivery
- **Source:** `.ai/conventions/engineering-workflow.md` «Analysis → Branch»
- **Verification:** `git -C frontend branch --show-current` выводит `feature/48-account-top-positions`
- **Depends on:** none
- [ ] 2.2 [frontend] Implementation: предоставленный backend snapshot в `src/shared/api/spec/openapi.json` и generated `schema.d.ts`
- [x] 2.2 [frontend] Implementation: предоставленный backend snapshot в `src/shared/api/spec/openapi.json` и generated `schema.d.ts`
- **Covers:** `portfolio-view` — `Нормализованный состав Portfolio`
- **Source:** design.md B6; frontend `.ai/conventions/data.md` «Типы и contract»
- **Verification:** `npm run openapi:generate`; `npm run openapi:check` без diff; `npm run typecheck`; generated `paths` содержит `sector` и `issuer` у позиции
- **Depends on:** 1.7, 2.1
- [ ] 2.3 [frontend] Implementation: `pages/account/model/concentration.ts` — состав разрезов, учитываемые позиции, валюты, точные суммы и доли, топ-5, «Прочее», порядок равных, имя эмитента, группы «не указан»
- [x] 2.3 [frontend] Implementation: `pages/account/model/concentration.ts` — состав разрезов, учитываемые позиции, валюты, точные суммы и доли, топ-5, «Прочее», порядок равных, имя эмитента, группы «не указан»
- **Covers:** `portfolio-concentration` — `Состав разрезов`; `portfolio-concentration` — `Названия элементов`; `portfolio-concentration` — `Название эмитента при разных названиях бренда`; `portfolio-concentration` — `Позиции, учитываемые в разрезе`; `portfolio-concentration` — `Сноска о не учтённых позициях`; `portfolio-concentration` — `Одна валюта за раз`; `portfolio-concentration` — `Доступные валюты вкладки`; `portfolio-concentration` — `Топ-5 и строка «Прочее»`; `portfolio-concentration` — `Порядок элементов с равной суммой`; `portfolio-concentration` — `Расчёт долей`; `portfolio-concentration` — `Показ долей`; `portfolio-concentration` — `Суммарная доля топ-5`; `portfolio-concentration` — `Группы «Сектор не указан» и «Эмитент не указан»`
- **Source:** design.md F1; analysis/data-mapping.md M3, I1–I8; analysis/test-cases/conc.md
- **Verification:** `npm run test:run -- src/pages/account/model/concentration.test.ts` по oracle TC-CONC-007, TC-CONC-008, TC-CONC-016, TC-CONC-020; сумма точных долей = 100 %; 6,25 → «6,3 %»; «< 0,1 %»; FUND исключён из «Эмитентов»
- **Depends on:** 2.2
- [ ] 2.4 [frontend] Implementation: state `{accountId, tab, slice, currency}` в `AccountPage.vm.ts`, управляемые `Tabs`, вкладка только для одного счёта
- [x] 2.4 [frontend] Implementation: state `{accountId, tab, slice, currency}` в `AccountPage.vm.ts`, управляемые `Tabs`, вкладка только для одного счёта
- **Covers:** `portfolio-concentration` — `Вкладка «Концентрация» страницы счёта`; `portfolio-concentration` — `Вкладка в состояниях страницы`; `portfolio-concentration` — `Переключение разрезов`; `portfolio-concentration` — `Начальное состояние вкладки`; `portfolio-concentration` — `Независимый выбор валюты вкладки`; `portfolio-concentration` — `Состояния Portfolio на вкладке «Концентрация»`
- **Source:** design.md F2, F3; analysis/test-cases/conc.md TC-CONC-002, TC-CONC-005, TC-CONC-006, TC-CONC-015
- **Verification:** `npm run test:run -- src/pages/account/ui/AccountPage`: вкладки нет для `all`, при загрузке, ошибке и пустом Portfolio; по умолчанию открыта «Позиции»; разрез и валюта сохраняются при переключении вкладок и сбрасываются при смене счёта; при исчезновении валюты выбирается первая
- **Depends on:** 2.3
- [ ] 2.5 [frontend] Implementation: `Concentration.view` — список, раскрытие «Прочее», пустое состояние, сноска, выбор валюты, Alert «Данные неполные», подавление страничного Alert на вкладке
- [x] 2.5 [frontend] Implementation: `Concentration.view` — список, раскрытие «Прочее», пустое состояние, сноска, выбор валюты, Alert «Данные неполные», подавление страничного Alert на вкладке
- **Covers:** `portfolio-concentration` — `Раскрытие строки «Прочее»`; `portfolio-concentration` — `Строки без переходов`; `portfolio-concentration` — `Пустое состояние разреза`; `portfolio-concentration` — `Предупреждение о неполных данных`
- **Source:** design.md F2, F4, «UI Implementation»; analysis/ux-design.md «UI States», «Responsive Behavior»; visuals v2 (SHA-256 по ux-design «Materials» сверены до начала)
- **Verification:** `npm run test:run -- src/pages/account/ui/AccountPage`: при `partial` на «Концентрации» одно сообщение «Данные неполные», на других вкладках — прежний Alert; строки не являются ссылками; browser-тест 320 px по образцу `position-layout.browser.test.tsx` без горизонтального скролла страницы
- **Depends on:** 2.4
- [ ] 2.6 [frontend] Implementation: DonutChart — центр-оверлей, обводка и штриховка через `cellProps`, цифры ранга, текстовый эквивалент
- [x] 2.6 [frontend] Implementation: DonutChart — центр-оверлей, обводка и штриховка через `cellProps`, цифры ранга, текстовый эквивалент
- **Covers:** `portfolio-concentration` — `Диаграмма распределения разреза`; `portfolio-concentration` — `Центр диаграммы`; `portfolio-concentration` — `Доступность диаграммы без цвета`
- **Source:** design.md F5; analysis/ux-design.md «Accessibility», D7; analysis/test-cases/conc.md TC-CONC-027
- **Verification:** `npm run test:run -- src/pages/account/ui/AccountPage`: 7 элементов → 6 сегментов в порядке списка; центр «Топ-5 · 95 %» и «1 000 ₽»; при ≤ 5 элементах только сумма; `role="img"` и `aria-describedby` с названиями, суммами и долями
- **Depends on:** 2.5
- [ ] 2.7 [frontend] Implementation: подписи секторов через общий словарь `stock.sectors` + ключ `government` («Государственный сектор»), видимый и в каталогах
- [x] 2.7 [frontend] Implementation: подписи секторов через общий словарь `stock.sectors` + ключ `government` («Государственный сектор»), видимый и в каталогах
- **Covers:** `portfolio-concentration` — `Подписи секторов`; `portfolio-concentration` — `Русские подписи известных секторов`
- **Source:** design.md F6; решение владельца [3084](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3084); analysis/test-cases/conc.md TC-CONC-024
- **Verification:** `npm run test:run`: 11 кодов из таблицы PC дают русские подписи; `other` выводится как есть; тест `StockCompany` (страница акции): `sector: "government"` отображается как «Сектор: Государственный сектор» — owner-approved изменение вне Specs (design.md F6, [3084](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3084))
- **Depends on:** 2.3
- [ ] 2.8 [frontend] Automated Testing: регрессия вкладки «Позиции» и «Структуры портфеля»
- [x] 2.8 [frontend] Automated Testing: регрессия вкладки «Позиции» и «Структуры портфеля»
- **Covers:** `portfolio-concentration` — `Вкладка «Концентрация» страницы счёта`
- **Source:** analysis/qa-analysis.md «Regression Impact»; analysis/test-cases/reg.md TC-REG-001
- **Verification:** существующие `AccountPage.test.tsx`, `position-groups.test.tsx`, тесты `widgets/portfolio-structure` проходят без изменения ожиданий
- **Depends on:** 2.6, 2.7
- [ ] 2.9 [frontend] Verification: полный gate и PR в `develop` со ссылкой на WealthLedger/main#48 и backend PR
- [x] 2.9 [frontend] Verification: полный gate и PR в `develop` со ссылкой на WealthLedger/main#48 и backend PR
- **Covers:** N/A — финальный gate repo
- **Source:** frontend `.ai/conventions/engineering.md`; `.ai/conventions/engineering-workflow.md` «Validation → PR»
- **Verification:** `npm run verify` зелёный на финальной ревизии; PR открыт со ссылками на `WealthLedger/main#48` и backend PR
@@ -136,37 +136,37 @@ Issue-координатор: [WealthLedger/main#48](https://git.personal.ksv741
## 4. Docs (по analysis/docs-impact.md, после merge кода в `develop`)
- [ ] 4.1 [docs] Verification: ветка `feature/48-account-top-positions` от актуального `develop`
- [x] 4.1 [docs] Verification: ветка `feature/48-account-top-positions` от актуального `develop`
- **Covers:** N/A — организационный шаг cross-repo delivery
- **Source:** `.ai/conventions/engineering-workflow.md` «Analysis → Branch»; analysis/docs-impact.md «Summary» (правки AS-IS только после merge кода)
- **Verification:** backend и frontend PR слиты в `develop`; `git -C docs branch --show-current` выводит `feature/48-account-top-positions`
- **Depends on:** 1.8, 2.9, 3.3
- [ ] 4.2 [docs] Docs: домен Portfolio — `overview.md`, при необходимости `concentration.md`, `data-reference.md`, `flows/portfolio-loading.md`, условная строка в `all-accounts.md`
- [x] 4.2 [docs] Docs: домен Portfolio — `overview.md`, при необходимости `concentration.md`, `data-reference.md`, `flows/portfolio-loading.md`, условная строка в `all-accounts.md`
- **Covers:** `portfolio-concentration` — `Вкладка «Концентрация» страницы счёта`; `portfolio-view` — `Нормализованный состав Portfolio`
- **Source:** analysis/docs-impact.md «Documentation Changes» (строки домена и flow)
- **Verification:** `npm run verify` в docs; текст соответствует реализованному AS-IS
- **Depends on:** 4.1
- [ ] 4.3 [docs] Docs: интеграция и архитектура — `integrations/tbank.md`, `architecture/cross-cutting/data-freshness.md`, `architecture/containers.md`
- [x] 4.3 [docs] Docs: интеграция и архитектура — `integrations/tbank.md`, `architecture/cross-cutting/data-freshness.md`, `architecture/containers.md`
- **Covers:** `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`; `portfolio-view` — `Обновление сектора и эмитента`
- **Source:** analysis/docs-impact.md «Documentation Changes» (Integration, Architecture); design.md B1, B4
- **Verification:** `npm run verify` в docs
- **Depends on:** 4.1
- [ ] 4.4 [docs] Docs: ADR 022 `stored-instrument-classification` (Accepted, AS-IS), строка в `adr/README.md`, ссылка в Status ADR 019
- [x] 4.4 [docs] Docs: ADR 022 `stored-instrument-classification` (Accepted, AS-IS), строка в `adr/README.md`, ссылка в Status ADR 019
- **Covers:** `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`
- **Source:** analysis/docs-impact.md «ADR Decision»; design.md «ADR Candidates», «Solution Assessment»; решение владельца [3083](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3083)
- **Verification:** `npm run verify` в docs; текст ADR 019 кроме строки Status не изменён
- **Depends on:** 4.1
- [ ] 4.5 [docs] Docs: глоссарий и навигация — `glossary.md`, `overview.md`, `README.md`
- [x] 4.5 [docs] Docs: глоссарий и навигация — `glossary.md`, `overview.md`, `README.md`
- **Covers:** `portfolio-concentration` — `Переключение разрезов`
- **Source:** analysis/docs-impact.md «Glossary / Navigation Impact»
- **Verification:** `npm run verify` в docs; ссылки из навигации ведут на домен Portfolio
- **Depends on:** 4.2
- [ ] 4.6 [docs] Verification: PR в `develop` со ссылкой на WealthLedger/main#48 и code PR
- [x] 4.6 [docs] Verification: PR в `develop` со ссылкой на WealthLedger/main#48 и code PR
- **Covers:** N/A — финальный gate repo
- **Source:** `.ai/conventions/engineering-workflow.md` «Validation → PR»
- **Verification:** `npm run verify` зелёный; PR открыт со ссылками на `WealthLedger/main#48`, backend и frontend PR