From 0e8c8c803f8ce8b49cdb7cbf2d5d1b0018736ee9 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 9 Sep 2026 20:36:33 +0300 Subject: [PATCH] docs: describe all-accounts behavior --- AI_INDEX.md | 1 + features/all-accounts.md | 109 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 features/all-accounts.md diff --git a/AI_INDEX.md b/AI_INDEX.md index ec55277..4b64027 100644 --- a/AI_INDEX.md +++ b/AI_INDEX.md @@ -50,6 +50,7 @@ Cross-area topology — [system overview](architecture/system-overview.md). | Предмет задачи | Current behavior | | --- | --- | | Счета и их сводки | [Brokerage accounts](features/brokerage-accounts.md) | +| Общий портфель и разделы по всем счетам | [All accounts](features/all-accounts.md) | | Позиции/суммы портфеля, backend cache | [Portfolio](features/portfolio-view.md) | | Операции/фильтры/cursor | [Account ledger](features/brokerage-account-ledger.md) | | Будущие выплаты | [Upcoming events](features/brokerage-account-upcoming-events.md) | diff --git a/features/all-accounts.md b/features/all-accounts.md new file mode 100644 index 0000000..d03eb8c --- /dev/null +++ b/features/all-accounts.md @@ -0,0 +1,109 @@ +# Портфель по всем счетам + +Режим «Все счета» объединяет открытые поддерживаемые инвестиционные счета, +доступные через текущее T-Bank connection. Состав совпадает с политикой +[списка счетов](brokerage-accounts.md); закрытые и недоступные счета не включаются. +`all` — зарезервированное обозначение общего режима, которое не передаётся +брокеру как идентификатор счёта. + +## Маршруты и навигация + +| Маршрут | Содержание | +| --- | --- | +| `/accounts` | Список отдельных счетов и действие открытия общего портфеля | +| `/accounts/all` | Общий портфель и превью операций, дохода и будущих событий | +| `/accounts/all/operations` | Общий журнал исполненных операций | +| `/accounts/all/income` | Купоны и дивиденды за календарный период | +| `/accounts/all/events` | Будущие события всех доступных счетов | + +Статические маршруты используют существующие страницы с явно переданным +контекстом `all`. На страницах счёта и общего режима переключатель «Счёт» +сохраняет текущий раздел и применённые URL-фильтры. При переключении пагинация +начинается заново. Журнал хранит применённые `from`, `to` и `categories` в URL; +черновые значения применяются кнопкой. Раздел дохода сохраняет свой прежний +календарный контракт. Хлебные крошки называют общий режим «Все счета». + +## Данные и вычисления + +Backend публикует статические endpoints под `/api/v1/brokerage-accounts/all`: +`/portfolio`, `/operations`, `/income-summary`, `/income-analytics`, `/events`. +У них те же формы данных и фильтры, что у соответствующих разделов отдельного +счёта. Строки общего ответа дополнительно содержат `account: { id, name }`; +в ответах одного счёта это поле отсутствует. Источник wire contract — runtime +OpenAPI, snapshot и frontend types генерируются из него. + +- Портфель и доступные денежные остатки суммируются сервером точно, по исходной + валюте, через существующую decimal-арифметику. FX-конвертации нет; правила + неизвестных значений и состава сумм наследуются от [портфеля](portfolio-view.md). +- Одинаковые бумаги разных счетов показаны одной карточкой с общим названием + и идентификатором. Внутри всегда видны строки счетов: ссылка на портфель, + количество, доступная стоимость, результат, средняя цена и доля позиции. + Денежные показатели и количества остаются исходными для каждого счёта; + общая средняя цена и суммарное количество карточки не рассчитываются. + Группировка выполняется по типу инструмента и FIGI, при отсутствии FIGI — + по ISIN. Разные FIGI, отсутствующие идентификаторы и одно лишь совпадение + имени или тикера не объединяются. Порядок карточек задаёт первое вхождение + бумаги; порядок счетов внутри карточки сохраняется. Одиночные позиции + используют прежнюю карточку. Доля относится к позициям той же валюты + во всём показанном портфеле, без денежных остатков. +- Журнал объединяет операции по убыванию времени, при равном времени использует + порядок идентификаторов счетов, сохраняя порядок внутри одного счёта. ID + операций разных счетов не считаются дублями. Переводы не схлопываются. +- Доходы собираются из всех страниц операций каждого счёта за общие периоды. + Итоги, сравнения, buckets и membership рассчитывает существующий сервис дохода. + Для сравнения с годом ранее используется текущий состав счетов; новый счёт + добавляет только реально полученные выплаты. При известных датах открытия + доступность сравнения определяется самым ранним открытием в этом составе. + Закрытые счета в историческое сравнение не добавляются. +- Будущие события сортируются по дате и сохраняют отдельную строку каждого счёта. + Прогноз не включается в реализованные доходы. + +## Полнота, свежесть и доступ + +Каждый общий запрос получает состав счетов и единую generation через +`TbankAccessService`. Получение данных использует эту же connection boundary; +проверки сессии, доступа и существующий лимитер внешних запросов сохраняются. +Финансовые данные не сохраняются в БД; новые зависимости не добавляются. + +Ошибка источника не превращается в частичный успешный ответ раздела. +Портфель может использовать разрешённый существующей политикой stale snapshot +отдельного счёта. Тогда общий `isStale` равен `true`, а `lastSuccessfulAt` +показывает самое раннее время среди включённых снимков. Для пустого состава +возвращается корректный пустой результат. Общий запрос не является атомарным +наблюдением всех счетов в один момент времени. + +Ошибка блока операций, доходов или будущих событий не скрывает основной +портфель. При фоновом обновлении страницы уже полученный результат сохраняется +с индикатором или предупреждением и повторной попыткой. Смена сессии или +connection сбрасывает общий режим вместе с существующим financial query subtree. + +## Общая пагинация операций + +Сервис объединяет потоки страниц каждого счёта и сохраняет непрочитанные строки +между запросами. Фильтрованные пустые страницы не обрывают поток. Повреждённая +cursor chain и нарушенный порядок дат возвращают ошибку без частичной страницы. + +Курсор общего журнала — случайный непрозрачный идентификатор продолжения, +привязанного к пользователю, generation подключения, составу счетов, фильтрам +и размеру страницы. Верхняя граница периода фиксируется первым запросом. +Состояние ограничено 100 продолжениями на процесс с TTL пять минут; +повторный запрос того же продолжения возвращает тот же успешный результат. + +Неизвестный, вытесненный, истёкший либо несовместимый курсор возвращает +`400 INVALID_OPERATIONS_CURSOR`. Действие «Повторить» сбрасывает пагинацию, +обновляет первую страницу и сохраняет фильтры. Перезапуск backend также +инвалидирует продолжения. Раздел рассчитан на один backend instance, как +существующий финансовый кэш. + +## Проверка и совместимость + +Агрегация проверяется service tests: точные суммы, исходные счета, порядок и +буферизация страниц, совпадающие ID, фильтрация, полнота, connection boundary, +сравнение дохода, stale и пустые результаты. Configured HTTP tests проверяют +Bearer guard, выбор статических маршрутов, валидацию и публичные ошибки. +Frontend tests проверяют прямые ссылки, переключение с фильтрами, сброс курсора +и сохранение снимка при ошибке обновления. + +Обновление аддитивное: сначала backend, затем frontend с generated contract. +Прежний frontend продолжает работать с отдельными счетами. При откате сначала +возвращается frontend; изменений схемы БД и миграции пользовательских данных нет.