spec: complete persist-tbank-data implementation
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 11s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 11s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
Record the approved proposal, delta specifications, design and 27 verified implementation tasks. Keep the change active; archiving and main-spec synchronization are deferred.
This commit was merged in pull request #16.
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
schema: wealth-ledger-sdd
|
||||||
|
created: 2026-09-24
|
||||||
@@ -0,0 +1,566 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
Основание — принятый [ADR 017](../../../../docs/adr/017-local-tbank-reads-and-background-sync.md)
|
||||||
|
и [дельты поведения](specs/tbank-data-sync/spec.md). Это детализация согласованной
|
||||||
|
модели для реализации. Компоненты приложения сейчас не изменены.
|
||||||
|
|
||||||
|
Исследованы локальные владельцы после чтения AGENTS каждого репозитория.
|
||||||
|
Свидетельства относятся к checkout на 2026-09-24, а не к обещаниям API:
|
||||||
|
|
||||||
|
| Граница | Текущий owner и переиспользуемое поведение |
|
||||||
|
| --- | --- |
|
||||||
|
| Хранение | `backend/prisma/schema.prisma` и `src/prisma/prisma.service.ts`: SQLite/Prisma, пользователи, сессии, подключение и ручные карты; T-Bank datasets отсутствуют. Условия владения и транзакции уже применяются в `modules/bank-cards/infrastructure/bank-cards.repository.ts`. |
|
||||||
|
| Доступ | `backend/src/integrations/tbank/tbank-access.service.ts`: `withSupportedAccounts` и `withAccountForConnection` вызывают `getOpenAccounts`; generation вычисляется из encryptedToken. `tbank-account.policy.ts` владеет допустимыми типами счетов. |
|
||||||
|
| Интеграция | `backend/src/integrations/tbank/tbank.client.ts`: общий FIFO, лимит 5 RPC/с по умолчанию, максимум 50 ожидающих, deadline и ограниченные повторы; повторный limiter не нужен. |
|
||||||
|
| Личный источник | `portfolio/application/portfolio.service.ts` хранит RAM-снимок 30 с / fallback 5 мин; `operations/application/brokerage-operations.service.ts` — страницы 30 с и обход курсоров. `income/income-analytics.service.ts` строит selected/previous/yearAgo через общий сборщик операций. Пути внутри `backend/src/modules/investments/`. |
|
||||||
|
| Общий источник | `backend/src/integrations/tbank/instrument-metadata.service.ts` уже разделяет общие metadata и pending lookup. Каталоги stocks/bonds смешивают общую информацию и личную позицию в user+generation RAM-кэше. Market services режут свечи по 365 суток. |
|
||||||
|
| Выплаты и смежные данные | `upcoming-events/upcoming-events.service.ts` отдельно получает portfolio; stock dividends приходят из T-Bank, bond events — из MOEX. Stock company use case получает asset description/fundamentals; bond metrics включают MOEX. Источники не подменяются. |
|
||||||
|
| Frontend | `frontend/src/entities/*/api` владеют generated queries; page VM собирает зависимости. `features/instrument-details/model/section.ts` знает только HTTP/query lifecycle. `features/tbank-connection/model/connection-boundary.ts` уже сбрасывает финансовую границу после успешной mutation. |
|
||||||
|
| Контракт | Backend DTO/controllers/Swagger — editable source. `frontend/src/shared/api/spec/openapi.json` — явно передаваемый snapshot. Accounts и search сейчас массивы; общего sync/coverage API нет. |
|
||||||
|
|
||||||
|
Исходный proto допускает изменение operation ID
|
||||||
|
(`backend/src/integrations/tbank/proto/contracts/operations.proto:496`),
|
||||||
|
но проект явно принимает стабильность. `is_complete` свечи задаёт источник
|
||||||
|
(`marketdata.proto:438`). В `operations.proto:472–489` не определены
|
||||||
|
inclusive/exclusive границы, срок жизни cursor и значение пропущенного `from`.
|
||||||
|
Эти пробелы не заменяются предположениями о гарантиях брокера.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:** один постоянный источник чтения в существующей БД; независимые
|
||||||
|
полнота, свежесть и состояние работы; атомарная публикация согласованного
|
||||||
|
набора; безопасный перезапуск; отсутствие RPC на пути ответа заполненной БД;
|
||||||
|
контракт всех экранных зависимостей и минимальный наблюдаемый worker.
|
||||||
|
|
||||||
|
**Non-Goals:** Redis, новый сервер БД, отдельный сервис очередей, cron,
|
||||||
|
распределённое выполнение, версии каждого портфельного снимка, live
|
||||||
|
подтверждение доступа при каждом чтении, архив отключённых счетов,
|
||||||
|
новый frontend store, изменение денежных правил или источников MOEX.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 1. Существующие domain owners читают БД; RPC принадлежит синхронизации
|
||||||
|
|
||||||
|
Сохраняются controllers, application services, нормализация Money и domain
|
||||||
|
политики. Их read path получает сохранённые источники и согласованное
|
||||||
|
metadata, а запрос работы передаёт небольшому T-Bank sync coordinator.
|
||||||
|
PortfolioService становится общим владельцем последнего полного portfolio
|
||||||
|
для accounts summary, личных позиций инструментов и будущих событий.
|
||||||
|
Сборщик операций становится одним локальным источником ledger/income;
|
||||||
|
filtered HTTP page больше не является единицей источниковой полноты.
|
||||||
|
Рассчитанные доходы не требуют отдельной постоянно дублируемой таблицы.
|
||||||
|
|
||||||
|
Синхронизация использует существующие TbankClient, metadata mapping,
|
||||||
|
market-window splitting, точную денежную арифметику и MOEX adapters.
|
||||||
|
Нормализованные числа сохраняются десятичными строками, не float.
|
||||||
|
Presentation metadata не блокирует выдачу финансового снимка; fallback
|
||||||
|
UID/FIGI/name остаётся доступен до её фоновой загрузки.
|
||||||
|
|
||||||
|
Синхронная проверка token при PUT подключения сохраняется. Успешно полученный
|
||||||
|
в этой проверке список можно сразу опубликовать как membership нового
|
||||||
|
подключения в той же транзакции, без второго GetAccounts.
|
||||||
|
|
||||||
|
### 2. Личные и общие модели в SQLite
|
||||||
|
|
||||||
|
Это логические группы таблиц, не универсальный JSON-кэш всех HTTP-ответов:
|
||||||
|
|
||||||
|
| Группа | Ключ, содержимое и инвариант |
|
||||||
|
| --- | --- |
|
||||||
|
| Подключение и membership | Существующий `TbankConnection` получает opaque `revision` и состояние известного отказа token. Список открытых поддерживаемых счетов принадлежит `(userId, revision)`; хранит accountId, type, name, openedOn, upstream order, fetchedAt, membershipRevision. Список заменяется целиком атомарно, включая успешный пустой. |
|
||||||
|
| Portfolio | `(userId, revision, accountId)` → последний полный нормализованный snapshot, fetchedAt и dataRevision. Допустимо хранить состав snapshot как versioned JSON: он читается целиком, не нужен отдельный исторический агрегат. |
|
||||||
|
| Operations | Уникальность `(userId, revision, accountId, brokerOperationId)`; индекс `(userId, revision, accountId, occurredAt, brokerOperationId)`, дополнительные instrument/category поля для фильтров. Полный исходный нормализуемый payload сохраняет точные поля, executed state и version маппинга. Cursor/пустая строка не заменяют отсутствующий ID. |
|
||||||
|
| Инструменты | Общий ключ provider + instrument UID; FIGI/ticker — aliases с существующими правилами неоднозначности. Metadata, company/fundamentals и catalog discovery имеют собственные успешные timestamps и coverage; отрицательный успешный lookup отличается от ошибки. |
|
||||||
|
| Рыночные данные | Общие quote/trading-status и необходимые price dependencies; дневная свеча уникальна по `(provider, instrumentUid, interval, time)`, содержит `isComplete`. Календари имеют source/type/instrument/range и исходные события. MOEX enrichment сохраняет собственный source и существующую семантику недоступности. |
|
||||||
|
| Последний полный прогноз | Private snapshot account events и instrument personal payout estimates по `(userId, revision, account scope, instrument при наличии, applied range)`, с dependency revisions, source timestamps и calculatedAt. Заменяется только после готовности всех обязательных источников; это fallback результата, не история portfolio. |
|
||||||
|
| Покрытие и работа | Завершённые диапазоны, fetchedThrough/fetchedAt, dataRevision, известная нижняя граница и её происхождение; pending intent, attemptId, processRunId, диапазон, стадия и прогресс страниц. Staging страниц текущей попытки изолирован от опубликованного набора. |
|
||||||
|
|
||||||
|
Общие таблицы не содержат user/account/quantity/estimatedAmount или token.
|
||||||
|
Личные payout estimates, holdings и фильтрованная личная история вычисляются
|
||||||
|
из разрешённых accounts и portfolio/operations. Статус пользовательской
|
||||||
|
работы хранится отдельно от общих instrument records; ошибка credentials
|
||||||
|
не становится общим отрицательным lookup.
|
||||||
|
|
||||||
|
FK и unique constraints обеспечивают идентичность; транзакции проверяют
|
||||||
|
владение и revision. Не создаются generic repository framework, event bus
|
||||||
|
или универсальный job engine. Точные SQL-имена выбираются по локальным
|
||||||
|
Prisma conventions без изменения этой модели.
|
||||||
|
|
||||||
|
### 3. Доступ без синхронного GetAccounts
|
||||||
|
|
||||||
|
Read path: локальная сессия → существующее подключение и его revision →
|
||||||
|
известная ошибка token → membership текущей revision → данные разрешённых
|
||||||
|
счетов. Token для этого не расшифровывается.
|
||||||
|
|
||||||
|
- Membership отсутствует: `200` с `data: null`, `availability: missing` и
|
||||||
|
состоянием discovery, без данных неизвестного accountId. После discovery
|
||||||
|
следующий read возвращает данные либо прежний `404`.
|
||||||
|
- Membership свежий: разрешённые данные читаются локально.
|
||||||
|
- Membership старше пяти минут: используется последнее успешное наблюдение,
|
||||||
|
обычный запрос ставит его фоновое обновление. Это ограниченная задержка
|
||||||
|
обнаружения отзыва доступа, принятая локальной моделью, не live authority.
|
||||||
|
- Успешный новый список немедленно ограничивает все последующие одиночные
|
||||||
|
и aggregate reads; добавленный ещё не загруженный счёт делает aggregate
|
||||||
|
неполным. Ответ «Все счета» использует одну согласованную DB read transaction
|
||||||
|
с фиксированной membershipRevision.
|
||||||
|
- Известный `INVALID_TBANK_TOKEN` блокирует личные reads и возвращает прежний
|
||||||
|
`400`; `409 TBANK_NOT_CONNECTED`, `404`, session/validation errors сохраняются.
|
||||||
|
Старые записи не удаляются вследствие transient failure, но не выдаются
|
||||||
|
вопреки известному отказу доступа. Исправление token выполняется существующим PUT.
|
||||||
|
|
||||||
|
При замене успешная транзакция назначает новую случайную revision, сохраняет
|
||||||
|
новый token и очищает/делает недоступными прежние private datasets/jobs
|
||||||
|
по существующей семантике invalidation. Delete остаётся идемпотентным и
|
||||||
|
каскадно убирает личные данные подключения; специальный архив не вводится.
|
||||||
|
Общие неперсональные данные могут оставаться.
|
||||||
|
|
||||||
|
Каждая публикация данных, coverage, ошибки, timestamp и результата работы
|
||||||
|
проверяет в той же короткой транзакции текущие connection revision и
|
||||||
|
attemptId. Проверка после RPC только в памяти недостаточна. Проверка нужна
|
||||||
|
и при общей публикации, начатой credentials старого подключения.
|
||||||
|
Поздняя попытка не вправе изменить даже freshness; результаты старой revision
|
||||||
|
отбрасываются. Транзакция никогда не удерживается во время сетевого вызова.
|
||||||
|
|
||||||
|
### 4. Интервалы и атомарная сверка операций
|
||||||
|
|
||||||
|
Внутри БД диапазоны — `[from, to)` в UTC. Public semantics не меняются:
|
||||||
|
журнал принимает действующие UTC timestamps, его default начинается 1 января
|
||||||
|
UTC; даты доходов включительны в `Europe/Moscow`, и существующий
|
||||||
|
`planIncomePeriod` вычисляет selected/previous/yearAgo. Преобразование границ
|
||||||
|
выполняется один раз до планирования. Полнота доходного месяца проверяется
|
||||||
|
по объединению UTC coverage, а не по совпадению названия месяца.
|
||||||
|
|
||||||
|
Источник по операциям загружается ограниченными календарными UTC-месяцами
|
||||||
|
и фрагментами нужных диапазонов. Верхняя граница текущей попытки фиксируется
|
||||||
|
в момент её создания. Для неизвестной inclusivity upstream применяется
|
||||||
|
перекрытие соседних окон с защитной секундой; записи локально отбираются
|
||||||
|
по точному `[from, to)` и дедуплицируются по исходному ID. Это защищает
|
||||||
|
стык, но не доказывает отсутствие будущих исправлений брокера.
|
||||||
|
|
||||||
|
Обычная полнота счёта строится только по всем EXECUTED операциям интервала
|
||||||
|
без category/instrument фильтров. UI filters и pagination применяются после
|
||||||
|
чтения. Ручной refresh личной истории инструмента может перечитать только
|
||||||
|
`instrumentUid + shown range`; его reconciliation удаляет отсутствующие
|
||||||
|
записи только этого predicate и не присваивает полноту всему счёту.
|
||||||
|
Уже полное общее покрытие может обслуживать такой subset; обратное неверно.
|
||||||
|
|
||||||
|
Каждая успешная страница записывается в staging attempt с page count и
|
||||||
|
прогрессом. При наличии полного прежнего набора staging никогда не виден
|
||||||
|
как его замена. Новые большие периоды становятся доступными по завершении
|
||||||
|
малых интервалов при первичном заполнении, поэтому экран может иметь `partial` coverage без показа
|
||||||
|
непроверенных страниц. Сбой, repeated/missing next cursor, malformed ID или
|
||||||
|
противоречащие версии одного ID внутри попытки оставляют интервал незавершённым.
|
||||||
|
Идентичные дубликаты страницы безопасно схлопываются.
|
||||||
|
|
||||||
|
Только успешное исчерпание всех страниц фиксированного интервала разрешает
|
||||||
|
транзакцию публикации: upsert новых/изменённых полей, удаление отсутствующих записей
|
||||||
|
в перечитанном predicate, публикация coverage и времени успешной сверки.
|
||||||
|
Account dataRevision увеличивается только при изменении rowset;
|
||||||
|
coverage/timestamp/status без изменения строк меняют только stateVersion.
|
||||||
|
Читатель видит старый либо новый набор,
|
||||||
|
не их смесь. Между интервалами частичная готовность допустима и явно видима;
|
||||||
|
сумма всего периода окончательна только при полном покрытии его зависимостей.
|
||||||
|
|
||||||
|
Для повторной сверки уже полного показанного периода единица публикации —
|
||||||
|
весь запрошенный scope, а не отдельный календарный chunk. Staging объединяет
|
||||||
|
его интервалы, сравнения и счета (`all`) в одну publication group; успешные
|
||||||
|
страницы/окна сохраняют прогресс, но commit/upsert/delete/coverage всех
|
||||||
|
участников выполняются только после успеха группы. Любая ошибка оставляет
|
||||||
|
весь прежний результат. Такое же правило действует для ручного refresh
|
||||||
|
показанного диапазона свечей. Первичная загрузка ранее отсутствующих данных
|
||||||
|
по малым интервалам остаётся инкрементальной. Пересекающиеся работы
|
||||||
|
присоединяются к группе и не публикуют её часть независимо. DB transaction
|
||||||
|
короткая и локальная; RPC выполняются до неё, а staging не держится в RAM.
|
||||||
|
|
||||||
|
Для изменяемого конца `to=now` полнота означает «все операции по состоянию
|
||||||
|
на `asOf` последней успешной сверки». Новый ещё не наблюдённый хвост времени
|
||||||
|
в пределах того же текущего месяца и TTL не считается пропуском, запускающим
|
||||||
|
новую загрузку; ответ сохраняет исходный `asOf`, не утверждая покрытие будущих
|
||||||
|
операций. Настоящие пропуски до `asOf`, новый явно запрошенный исторический
|
||||||
|
диапазон и переход месяца этому исключению не подчиняются.
|
||||||
|
Текущий месяц обновляется целиком до зафиксированного now при истечении
|
||||||
|
пяти минут, чтобы учесть изменения уже виденных операций. После смены месяца
|
||||||
|
его покрытие до прежнего now не распространяется до конца месяца:
|
||||||
|
следующий подходящий запрос завершает хвост. Полные прошлые интервалы
|
||||||
|
не сверяются автоматически. Успешная наблюдённая смена полей того же ID
|
||||||
|
обновляет запись и производные результаты; поиск сменившихся ID отсутствует.
|
||||||
|
|
||||||
|
### 5. Планирование, приоритет и восстановление
|
||||||
|
|
||||||
|
Поддерживаемая поставка — один backend process с существующей SQLite.
|
||||||
|
Запрос сохраняет intent, возвращает локальные данные и передаёт выполнение
|
||||||
|
in-process coordinator. На startup нет обхода очереди или запуска импорта.
|
||||||
|
Process получает случайный runId; следующий подходящий запрос перехватывает
|
||||||
|
незавершённую работу другого runId новой attemptId. Persisted provider cursor
|
||||||
|
не переиспользуется после перезапуска: незавершённый интервал начинается
|
||||||
|
с первой страницы, staging прежней попытки очищается. Завершённые интервалы
|
||||||
|
и опубликованные данные сохраняются.
|
||||||
|
|
||||||
|
Приоритеты: discovery доступа → нужные экрану зависимости и интервалы,
|
||||||
|
включая comparisons → остальная история счёта и рыночная история его бумаг.
|
||||||
|
Между окнами/страницами coordinator отдаёт приоритет новой экранной работе;
|
||||||
|
уже отправленный RPC не прерывается ради перестановки. Один producer
|
||||||
|
догрузки не ставит тысячи RPC заранее: страницы идут последовательно,
|
||||||
|
новые окна планируются постепенно, через прежний bounded limiter.
|
||||||
|
Очередь RPC и bounded retry не дублируются на уровне worker.
|
||||||
|
|
||||||
|
Ключ personal work: user + connectionRevision + account + source kind +
|
||||||
|
normalized interval/predicate. Координатор сериализует публикации одного
|
||||||
|
account source и для обычной загрузки вычитает покрытые/выполняемые
|
||||||
|
пересечения; одновременно запрошенные остатки объединяет. Force refresh
|
||||||
|
не вычитает полное покрытие: он обязан перечитать запрошенный target,
|
||||||
|
но присоединяется к уже выполняемой подходящей загрузке. Повторный force refresh присоединяется к
|
||||||
|
подходящей активной попытке, уже получающей полный запрошенный диапазон;
|
||||||
|
TTL при этом не отменяет запрос. Новая область расширяет pending intent.
|
||||||
|
|
||||||
|
Shared work использует provider/instrument/source/range identity.
|
||||||
|
Общий успешный результат переиспользуется независимо от пользователя.
|
||||||
|
У каждого наблюдателя свой авторизованный handle, без выдачи чужих job IDs
|
||||||
|
и credentials errors. Ошибка token инициатора завершает его попытку, не
|
||||||
|
портит общий источник; новый допустимый запрос другого пользователя может
|
||||||
|
запустить попытку со своими credentials. Старый job не перехватывает token
|
||||||
|
другого пользователя сам.
|
||||||
|
|
||||||
|
Остальная история начинается с известной даты открытия; если её нет,
|
||||||
|
используется существующая техническая граница `1970-01-01T00:00:00Z`,
|
||||||
|
явно помеченная как requested floor, а не доказанная дата открытия/минимум
|
||||||
|
источника. Сохраняются confirmed intervals и `historyBoundary: unknown`;
|
||||||
|
«вся история загружена» не показывается до подтверждения нижней границы.
|
||||||
|
Отказ источника за пределами доступного диапазона сохраняется как ограничение,
|
||||||
|
не как пустой полный интервал. Отсутствие `from` не используется как
|
||||||
|
недокументированное обещание all-time. Это не мешает полной готовности
|
||||||
|
подтверждённых периодов экрана.
|
||||||
|
|
||||||
|
После transient failure попытка заканчивается с error; новый обычный запрос
|
||||||
|
может возобновить недостающее/устаревшее, ручной — повторить независимо от TTL.
|
||||||
|
Наблюдение результата этого не делает. Logout/уход останавливает наблюдение
|
||||||
|
frontend, но не отменяет уже запрошенную допустимую серверную работу.
|
||||||
|
Замена/delete подключения останавливает публикацию по revision fence.
|
||||||
|
|
||||||
|
### 6. Общие market data и производные выплаты
|
||||||
|
|
||||||
|
Quotes и portfolio обновляются через минуту; metadata/catalog/company и
|
||||||
|
календари — через сутки. Freshness принадлежит конкретному source/range,
|
||||||
|
поэтому успешный lookup одного инструмента не делает свежим весь каталог.
|
||||||
|
Catalog discovery сохраняет успешный набор UID/порядок отдельно от
|
||||||
|
enrichment по нормализованному поисковому query и типу инструмента, сохраняя
|
||||||
|
существующий предел результатов. `findShares/findBonds` не загружают весь
|
||||||
|
каталог: успешный поиск SBER не подтверждает результат GAZP. Новый query
|
||||||
|
без подтверждённого покрытия запускает discovery, а не возвращает ложный
|
||||||
|
пустой результат. Metadata уже известных UID переиспользуется; повтор того
|
||||||
|
же свежего полного query не вызывает discovery RPC. Отсутствие enrichment
|
||||||
|
не превращает найденный набор в пустой поиск.
|
||||||
|
|
||||||
|
Свечи сохраняют исходную `isComplete`, диапазоны и `historyFrom`.
|
||||||
|
Переиспользуются окна до 365 суток. Завершённый диапазон без свечей хранится
|
||||||
|
как проверенное пустое покрытие. Текущий UTC-день всегда изменяемый,
|
||||||
|
включая пустое окно до торгов; его TTL — минута. Смена UTC-дня сама не
|
||||||
|
закрывает ни пустое открытое окно, ни незавершённую свечу: следующий запрос
|
||||||
|
повторяет прошедшее окно; успешный закрытый пустой диапазон подтверждает
|
||||||
|
отсутствие, для имеющейся свечи нужно `isComplete=true`.
|
||||||
|
|
||||||
|
Известный `first_1dayCandleDate` определяет нижнюю границу all-range.
|
||||||
|
Если его нет, сохраняется существующее `*_HISTORY_START_UNAVAILABLE`,
|
||||||
|
не создаётся ложная полная история. Предзагрузка касается бумаг сохранённого
|
||||||
|
текущего портфеля и использует ту же общую coverage, что открытый график.
|
||||||
|
|
||||||
|
События инструментов сохраняются без пользовательской ожидаемой суммы.
|
||||||
|
Календари купонов/дивидендов и company data охватываются общим read path;
|
||||||
|
future events используют сохранённый portfolio и исходный календарь,
|
||||||
|
сохраняя неопределённость прогноза, cancelled-dividend и offer semantics.
|
||||||
|
MOEX bond events/metrics остаются MOEX: необходимые для экрана результаты
|
||||||
|
также сохраняются и обновляются в этой работе, без нового MOEX scheduler.
|
||||||
|
Недоступность optional enrichment не делает финансовые данные ошибочными;
|
||||||
|
неполнота обязательного календаря не выдаётся за полный пустой прогноз.
|
||||||
|
Последний полный прогноз/личная оценка сохраняется отдельным private
|
||||||
|
snapshot по показанному scope/range. Если новый portfolio добавил бумагу,
|
||||||
|
а её календарь не загрузился, следующий read, в том числе после restart,
|
||||||
|
возвращает прежний полный forecast с его прежними dependency timestamps и
|
||||||
|
ошибкой обновления. При отсутствии такого snapshot возвращается missing/partial
|
||||||
|
без окончательного списка. Готовый новый результат атомарно заменяет snapshot;
|
||||||
|
снимок с иным периодом или membership не выдаётся под новым scope, а доступ
|
||||||
|
всегда повторно ограничивается текущим membership.
|
||||||
|
|
||||||
|
### 7. Публичный контракт и наблюдение работы
|
||||||
|
|
||||||
|
**Contract Gate Decision: required.** Меняется wire shape всех T-Bank data
|
||||||
|
GET, включая bare-array accounts и stock/bond search. Backend сохраняет
|
||||||
|
маршруты и тип внутреннего payload, добавляя единый envelope:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{ data: T | null, meta: {
|
||||||
|
resourceKey, dataRevision, stateVersion,
|
||||||
|
availability: missing | partial | complete,
|
||||||
|
freshness: unknown | fresh | stale, lastSuccessfulAt: timestamp | null,
|
||||||
|
sync: { id: opaque | null, state: idle | queued | running | failed,
|
||||||
|
errorCode: safeCode | null },
|
||||||
|
coverage?: { requested, completeRanges, missingRanges,
|
||||||
|
asOf, historyBoundary, historyState },
|
||||||
|
dependencies?: [{ resourceKey, availability, freshness,
|
||||||
|
lastSuccessfulAt, sync, coverage?, unavailableReason? }]
|
||||||
|
} }
|
||||||
|
```
|
||||||
|
|
||||||
|
Connection generation/revision остаётся внутренней: существующая security
|
||||||
|
policy запрещает её в API/logs/errors, даже если она случайная. Frontend
|
||||||
|
использует свою существующую session/connection boundary.
|
||||||
|
`resourceKey` — нормализованная идентичность запроса без user secrets и
|
||||||
|
connection generation. `dataRevision` меняется при публикации данных;
|
||||||
|
изменение статуса работы не меняет страницу операций. Availability означает
|
||||||
|
готовность обязательных источников данного результата, а не обещание полноты
|
||||||
|
рынка или будущих выплат. `data:null` используется для missing и для
|
||||||
|
неполных финансовых агрегатов; `partial` ledger/candles может содержать
|
||||||
|
подтверждённые части. Полнота selected/comparisons задаётся отдельно:
|
||||||
|
analytics `data` доступен после полного selected, а `previous` и
|
||||||
|
`yearComparison` nullable до готовности соответствующего сравнения.
|
||||||
|
`meta.dependencies` различает missing/loading/failed comparison и
|
||||||
|
заведомо недоступное по календарю/границе истории сравнение; последнее
|
||||||
|
имеет отдельный `unavailableReason` и не планируется как пропуск.
|
||||||
|
Это явное изменение вложенного analytics DTO, а не только envelope.
|
||||||
|
|
||||||
|
Для зависимостей с разными TTL metadata содержит их состояния по
|
||||||
|
resourceKey; freshness производного результата — худшее обязательное
|
||||||
|
состояние, lastSuccessfulAt — самый ранний из использованных успешных
|
||||||
|
источников. Чтение/`calculatedAt`/`observedAt` не продлевает source freshness.
|
||||||
|
Portfolio `data.lastSuccessfulAt` и `data.isStale` сохраняются для текущих
|
||||||
|
consumers и отображают тот же portfolio source, без прежнего пятиминутного
|
||||||
|
предела хранения. Error фонового обновления не меняет успешный timestamp
|
||||||
|
и сама по себе не отменяет ещё действующий TTL.
|
||||||
|
|
||||||
|
| Операция | Контракт |
|
||||||
|
| --- | --- |
|
||||||
|
| Обычный GET существующего resource | `200` envelope, в том числе cold/partial; планирует missing/stale зависимости. До постановки проверяет auth, connection и известную принадлежность. |
|
||||||
|
| Тот же GET с `sync=observe` | Только локальное чтение; никогда не создаёт и не возобновляет work, даже при missing/stale/error. Нужен для получения изменившихся данных после status observation. |
|
||||||
|
| `POST /api/v1/tbank-sync` | Валидированный screen descriptor, force по TTL; `202` с авторизованным work handle и normalized scope. При совпадении присоединяет к work. Приём не означает успех. |
|
||||||
|
| `GET /api/v1/tbank-sync/:id` | Только статус разрешённого handle, progress, changed resource revisions и terminal outcome; никаких новых запусков/повторов. Чужой/прежней connection handle — `404`. |
|
||||||
|
|
||||||
|
Descriptor — discriminated union `screen: accounts | account | operations |
|
||||||
|
income | events | stocks | bonds | stock | bond`. Поля разрешены только
|
||||||
|
соответствующему screen: account scope (конкретный ID/`all`), applied period,
|
||||||
|
filters, instrumentUid, candle period/range. Сервер использует прежние
|
||||||
|
валидаторы дат/лимитов/UID и сам раскрывает dependency graph; произвольного
|
||||||
|
списка RPC/таблиц/чужого userId во входе нет. Зафиксированный now и нормализованный
|
||||||
|
scope возвращаются клиенту, чтобы ручная операция не переезжала на другой период.
|
||||||
|
|
||||||
|
| Экран | Зависимости force refresh |
|
||||||
|
| --- | --- |
|
||||||
|
| Accounts | Membership + portfolio summary каждого текущего доступного счёта; metadata показанных позиций по необходимости. Не вся история операций. |
|
||||||
|
| Account / all overview | Membership, portfolio, показанный recent-ledger range, current-month income, upcoming events и необходимые instrument metadata/calendars. |
|
||||||
|
| Operations | Применённые account scope и период; категории — локальная проекция. Пагинация не расширяет источниковой период. |
|
||||||
|
| Income | Применённый период + previous/yearAgo из существующего planner, membership, требуемая metadata выплат. |
|
||||||
|
| Events | Membership, portfolio и прогноз применённого включительного from/to из сохранённых календарей; default from=today UTC, to=from+30 дней. |
|
||||||
|
| Stocks / bonds search | Общий каталог, результаты/enrichment текущего поиска и показываемые prices; без истории всего каталога. |
|
||||||
|
| Stock / bond | Detail/company либо metrics, quote и price dependencies, видимый candle range, payout calendar, личная position и применённый диапазон/инструмент personal operations. |
|
||||||
|
|
||||||
|
HTTP ошибки auth/input/access сохраняют `application/problem+json`.
|
||||||
|
`TBANK_UNAVAILABLE` из принятой фоновой работы передаётся как безопасный
|
||||||
|
`meta.sync.errorCode`, включая cold failure; `503` остаётся для запроса,
|
||||||
|
который не удалось принять/обслужить. Известный отказ credentials остаётся
|
||||||
|
`400 INVALID_TBANK_TOKEN`, не успешным envelope с недоступными private data.
|
||||||
|
Frontend различает server work error и transport failure.
|
||||||
|
|
||||||
|
Работа имеет terminal `succeeded | failed | cancelled` в status endpoint;
|
||||||
|
envelope отображает successful отсутствие активной работы как `idle`.
|
||||||
|
Статус имеет `stateVersion` нужных экрану ресурсов и отдельный прогресс background
|
||||||
|
history, поэтому готовые данные можно прочитать до окончания всей догрузки.
|
||||||
|
Frontend наблюдает только известный active handle на открытом экране
|
||||||
|
с интервалом 1–5 секунд и без автоматического retry failed work. После
|
||||||
|
изменения stateVersion или terminal status выполняется `sync=observe` GET.
|
||||||
|
После ухода/terminal status наблюдение прекращается. Это наблюдение уже
|
||||||
|
запрошенной работы, не периодический запуск импорта.
|
||||||
|
|
||||||
|
`stateVersion` — opaque идентичность состояния ответа, меняется при изменении
|
||||||
|
данных, coverage (включая пустое завершённое окно), successfulAt, ошибок,
|
||||||
|
работы и доступа; это не pagination revision. Он вычисляется из версий
|
||||||
|
зависимостей и состояния без выдачи внутренних connection identifiers.
|
||||||
|
Прогресс далёкой истории, не меняющий этот resource, не меняет его
|
||||||
|
stateVersion и не заставляет повторно читать уже готовый экран.
|
||||||
|
Истечение времени не создаёт работу: freshness вычисляется из timestamp.
|
||||||
|
|
||||||
|
Entity query owner записывает observe-ответ в ту же canonical cache identity
|
||||||
|
исходного GET; не создаётся второй cache по параметру observe. Query keys
|
||||||
|
берутся из generated owner, не собираются вручную. Все новые status/mutation
|
||||||
|
ответы участвуют в существующих session/connection boundary fences.
|
||||||
|
Кроме них owner использует latest-request sequence для каждого resource:
|
||||||
|
ответ более раннего запроса не заменяет уже принятый более поздний ответ.
|
||||||
|
При status notification запоминается ожидаемый stateVersion; observe-ответ
|
||||||
|
с прежней версией не публикуется, выполняется только новое observational read.
|
||||||
|
Подтверждённый access loss отменяет/инвалидирует соответствующие personal
|
||||||
|
queries и повышает локальную access epoch; даже поздний успешный ответ той же
|
||||||
|
connection, начатый до отзыва, не восстанавливает скрытые данные.
|
||||||
|
|
||||||
|
### 8. Локальная пагинация и финансовые результаты
|
||||||
|
|
||||||
|
Порядок операций сохраняет действующую сортировку с детерминированным
|
||||||
|
tie-break по accountId/operationId. Оpaque cursor связан с user, connection,
|
||||||
|
membershipRevision для all, нормализованными filters/period/limit и
|
||||||
|
account operations dataRevision. Любое изменение relevant rowset делает
|
||||||
|
cursor недействительным. Binding хранится серверно либо проверяется keyed
|
||||||
|
digest: connection generation не находится даже в декодируемом cursor payload.
|
||||||
|
Изменение rowset возвращает существующий `INVALID_OPERATIONS_CURSOR`;
|
||||||
|
исторические копии всех rowsets не хранятся. Первая реализация использует
|
||||||
|
revision на счёт: изменение другого периода тоже может сбросить его cursor;
|
||||||
|
более узкая revision нужна только при доказанно мешающих сбросах.
|
||||||
|
|
||||||
|
Frontend сбрасывает cursor stack на первую страницу, сохраняя applied filters
|
||||||
|
и объясняя обновление списка; ни страницы, ни суммы разных revisions не
|
||||||
|
склеиваются. `hasNext=false` означает конец текущей локальной выдачи,
|
||||||
|
а не полноту истории. Coverage/status без изменения строк cursor не ломают.
|
||||||
|
|
||||||
|
Income сохраняет точные signed coupon/dividend payments и существующие
|
||||||
|
comparison rules. Stable operation ID устраняет повторный финансовый учёт;
|
||||||
|
прежняя норма о разных entries с одинаковым ID явно изменена delta spec.
|
||||||
|
Analytics читает согласованный набор операций и coverage в read transaction.
|
||||||
|
Неполнота selected скрывает его окончательные totals; неполнота comparison
|
||||||
|
скрывает только его суммы/разницы и не создаёт нулевую базу. Уже опубликованный полный результат того же
|
||||||
|
period/context сохраняется при неудачной повторной сверке; данные другого
|
||||||
|
периода никогда не подписываются новым диапазоном.
|
||||||
|
|
||||||
|
### 9. Измеримый бюджет и стоимость интеграции
|
||||||
|
|
||||||
|
500 мс — цель, не текущий результат. Начальный профиль приёмки: один backend
|
||||||
|
process, локальная SQLite на SSD, 4 CPU / 8 GiB, 3 счёта, по 100 000 операций
|
||||||
|
за 10 лет, 200 инструментов и по 5 000 дневных свечей, включая пустые
|
||||||
|
интервалы; до 1 000 операций/200 доходных выплат на счёт в активном месяце.
|
||||||
|
Измеряются account/all reads, ledger page 50, monthly income с comparisons,
|
||||||
|
instrument detail/quote/company/metrics, year/all candles и payouts/search.
|
||||||
|
|
||||||
|
Бюджет — p95 ≤ 500 мс от входа HTTP на backend до окончания ответа при
|
||||||
|
10 одновременных клиентах и не менее 100 чтений каждой группы после прогрева.
|
||||||
|
Отдельно сохраняются p50/p99/max, размер ответа, состояние БД, hardware и
|
||||||
|
время первого DB-only ответа после перезапуска. Cold import исключён из
|
||||||
|
бюджета заполненной БД, но его HTTP ответ также не ожидает RPC. Больший
|
||||||
|
реальный профиль не объявляется измеренным по этим данным.
|
||||||
|
|
||||||
|
Изоляция от брокера проверяется задержанным/отказывающим T-Bank; источник
|
||||||
|
замеряемых данных — HTTP timing и счётчики RPC существующего logger. Для
|
||||||
|
fresh полной выборки ожидается 0 RPC; для 20 simultaneous stale requests —
|
||||||
|
один логический проход каждой нужной зависимости, с явным учётом pages и
|
||||||
|
разрешённых retries. После завершения, без screen/manual requests — 0 RPC.
|
||||||
|
Повторные старые полные intervals/candles, в том числе после restart, —
|
||||||
|
0 повторных RPC этих диапазонов. Сравнение с исходной реализацией выполняется
|
||||||
|
на одинаковом navigation trace; сохраняются абсолютные числа и доля снижения
|
||||||
|
`1 - new/old`, если baseline ненулевой. Измерения не требуют real tokens
|
||||||
|
в артефактах или логах.
|
||||||
|
|
||||||
|
## UI/UX Applicability and Package
|
||||||
|
|
||||||
|
**Applicability and scope:** `new/changed` — общие состояния и действие
|
||||||
|
обновления всех T-Bank экранов, без перестройки их содержимого.
|
||||||
|
|
||||||
|
**Package revision:** `1`. Пакет полностью inline; локальные PNG/SVG не
|
||||||
|
нужны, поскольку меняются информационные состояния и существующие controls,
|
||||||
|
а не композиция экранов. Self-hash и внешние визуальные зависимости отсутствуют.
|
||||||
|
|
||||||
|
**Screens, states and interactions:**
|
||||||
|
|
||||||
|
| Состояние | Представление и действие |
|
||||||
|
| --- | --- |
|
||||||
|
| Missing без результата | Существующий структурный skeleton только отсутствующих секций, «Загружаем данные», доступное busy-состояние; без нулей/«операций нет». |
|
||||||
|
| Partial period | Подтверждённые строки видны с «Период загружен не полностью». Финансовые totals/comparisons ждут полноты. «История догружается» отдельно от состояния показанного периода. |
|
||||||
|
| Complete fresh | Контент, время источника и «Обновить» в заголовке экрана. Истечение TTL само не запускает UI timer refresh. |
|
||||||
|
| Complete stale / active | Прежний контент и timestamp, «Обновляем» либо «Данные на …»; без skeleton поверх сумм. Повторная кнопка disabled для уже запрошенного текущего scope с доступным пояснением. |
|
||||||
|
| Failed with data | Прежний контент, исходное время, безопасное предупреждение и повторное «Обновить». Transport failure и server work failure различимы; статус не сообщает об успешном обновлении. |
|
||||||
|
| Failed without data | Локальное error-состояние секции, безопасный retry либо действие настроек согласно коду. |
|
||||||
|
| Complete empty | Только подтверждённое пустое состояние; empty portfolio не создаёт нулевые карточки, пустой доход объясняет отсутствие купонов/дивидендов. |
|
||||||
|
| Access lost / connection changed | Очистка недоступного personal view, настройки для token/connection, возврат к счетам для 404. Нельзя удерживать чужую/прежнюю revision под предупреждением. |
|
||||||
|
|
||||||
|
Accounts показывают freshness списка отдельно от локального состояния
|
||||||
|
portfolio summary каждой карточки. Account/all overview сохраняет независимые
|
||||||
|
секции. Operations сохраняет применённые фильтры; income не переименовывает
|
||||||
|
старый snapshot новым period. Stock/bond показывают состояния по sections,
|
||||||
|
не смешивая общий график с личными holdings/payout estimates. Детали candle
|
||||||
|
по-прежнему обозначают незавершённость.
|
||||||
|
|
||||||
|
**Responsive behavior:** на 320–375 px timestamp, предупреждение и кнопка
|
||||||
|
переносятся без горизонтального overflow и обрезания сумм; на широком экране
|
||||||
|
используют существующий header/section layout. Focus сохраняется после
|
||||||
|
обновления; button доступен с клавиатуры, текст статуса не зависит от цвета,
|
||||||
|
`aria-live=polite` объявляет смену стадии/ошибку, но не каждую страницу.
|
||||||
|
Skeleton не фокусируется; reduced motion сохраняется.
|
||||||
|
|
||||||
|
**Reused source and differences:** runtime Mantine controls, существующие
|
||||||
|
Section/VM patterns и generated query owners переиспользуются как код.
|
||||||
|
Это новый inline пакет, не reuse изменяемого или архивного design package;
|
||||||
|
его состояние полностью определено выше.
|
||||||
|
|
||||||
|
## Delivery Map
|
||||||
|
|
||||||
|
| Area | Status | Architectural outcome or N/A reason |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| frontend | `affected` | Новый snapshot/types, metadata-aware entity queries/VMs всех экранов, scoped refresh и observe-only status, connection/cursor boundaries, inline UI package. |
|
||||||
|
| backend | `affected` | Prisma storage/migration, local access/read owners, durable interval sync/fencing, shared market sources, DTO и OpenAPI; evidence корректности, latency и RPC. |
|
||||||
|
| specs | `affected` | Этот change и семь delta capabilities; traceability реализации без преждевременного изменения main specs или чужих changes. |
|
||||||
|
| docs | `affected` | As-built freshness/storage/access/flows/API/glossary и актуальный статус ADR после поставки; не второй источник требований. |
|
||||||
|
|
||||||
|
## Solution Assessment
|
||||||
|
|
||||||
|
Trigger: storage, external integration, caching, concurrency и public API.
|
||||||
|
|
||||||
|
| Вариант | Fit, maturity, security, operating cost и migration cost |
|
||||||
|
| --- | --- |
|
||||||
|
| Существующие SQLite/Prisma + транзакции, unique keys и небольшой request-driven coordinator | Выбран: зрелая используемая БД, один процесс, отсутствие нового сервиса; ownership/fencing доступны в SQL. Миграция добавляет таблицы и revision. Специфичные broker intervals/reconciliation остаются доменной логикой. |
|
||||||
|
| Продлить RAM/TanStack кэш | Зрелый дешёвый механизм показа, но несовместим с durability и полнотой после restart. Остаётся presentation cache, не решение хранения. |
|
||||||
|
| Redis + BullMQ | Зрелые retries/locks/queues, но требует нового сервиса, persistence/security/backup и двойной согласованности с SQLite; его jobs всё равно не решают domain coverage/атомарную сверку. Нет нужды в распределённых workers, поэтому цена миграции/эксплуатации не оправдана. |
|
||||||
|
| Temporal либо аналогичный workflow server | Зрелое recovery, но отдельная инфраструктура и execution model для одной последовательности RPC; domain data transactions всё равно необходимы. Не соответствует минимальному одному backend. |
|
||||||
|
| Cron/планировщик | Зрелый механизм, но несовместим с явным запретом периодических запусков. |
|
||||||
|
|
||||||
|
Собственный код ограничен доменными интервалами, dependency planning,
|
||||||
|
staging/publish и coalescing; transport queue/retries и DB lifecycle уже
|
||||||
|
существуют. Существующий runtime не предоставляет durable job framework;
|
||||||
|
добавлять его ради этих конечных стадий не требуется. При переходе к нескольким
|
||||||
|
backend processes понадобится отдельное решение о durable claims/leases;
|
||||||
|
эта поставка не выдаёт processRunId за распределённую блокировку.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- Локальный membership не замечает отзыв доступа мгновенно. Он обновляется
|
||||||
|
по пятиминутной политике; известный отказ применяется немедленно.
|
||||||
|
- Stable operation ID противоречит возможностям upstream proto, но принят
|
||||||
|
как допущение; сменившийся ID может потребовать отдельной будущей сверки.
|
||||||
|
- Broker cursor не обещает snapshot isolation: полнота означает успешный
|
||||||
|
обход доступного интервала на момент сверки, не отсутствие последующих
|
||||||
|
исправлений. Staging защищает от наших частичных публикаций.
|
||||||
|
- Долгая первичная история расходует rate-limit и диск. Ограниченные окна,
|
||||||
|
приоритет экрана и отсутствие предварительной массовой очереди сдерживают
|
||||||
|
ресурсы; объём и время первой загрузки пока не измерены.
|
||||||
|
- Запись SQLite сериализуется. Короткие транзакции и индексы должны уложиться
|
||||||
|
в бюджет; это проверяется, а не считается доказанным архитектурой.
|
||||||
|
- Envelope — несовместимое wire изменение, поэтому backend/frontend
|
||||||
|
поставляются согласованной парой. API version path сам по себе не делает
|
||||||
|
старый frontend совместимым.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. Зафиксировать текущие DB/API invariants и measurement baseline на
|
||||||
|
изолированной базе. Добавить additive schema с revision и пустыми новыми
|
||||||
|
datasets; текущие users/sessions/connection/manual-card записи сохранить.
|
||||||
|
У существующих подключений появляется новая revision без изменения token.
|
||||||
|
2. Реализовать storage/coordinator и local owners; старые RAM snapshots
|
||||||
|
не мигрировать как доказанно полную историю. Проверить crash/rollback и
|
||||||
|
fences до включения нового read path.
|
||||||
|
3. Backend публикует OpenAPI artifact; frontend получает его явно и
|
||||||
|
регенерирует типы локально. Совместно поставляется новый UI и server
|
||||||
|
read path, включая observer semantics и terminal errors.
|
||||||
|
4. Первые пользовательские запросы заполняют данные. Startup/deploy не
|
||||||
|
запускает импорт. После подтверждённых checks обновляются as-built docs;
|
||||||
|
specs остаётся единственным SDD authority.
|
||||||
|
|
||||||
|
### Rollback Contract
|
||||||
|
|
||||||
|
| Field | Decision |
|
||||||
|
| --- | --- |
|
||||||
|
| Status | `required` |
|
||||||
|
| Trigger | Cross-user/connection leak, частичная публикация финансовых данных, повреждение миграции, несовместимый API pair либо существенная регрессия latency. |
|
||||||
|
| Actions | Остановить новый worker/backend, сохранить согласованную копию SQLite, вернуть совместимую пару backend/frontend. Не запускать старую и новую write-модель одновременно. Отдельно проверить Prisma migration state; не удалять additive tables как способ отката. |
|
||||||
|
| Data safety | Сохранить новые datasets, progress и все прежние User/Session/TbankConnection/manual-card записи. Если старая версия не поддерживает новую Prisma schema, использовать совместимый rollback build либо восстановить backup в отдельной БД; нельзя перезаписать рабочую БД старой копией, потеряв новые ручные операции. |
|
||||||
|
| Verification | Проверить integrity/FK и равенство прежних пользовательских данных, невозможность публикации остановленного worker, auth/connection boundaries, согласованность API pair и повторное безопасное чтение/возобновление после возврата исправленной версии. |
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
Блокирующих продуктовых вопросов нет. Неизвестны измеренные latency/объём
|
||||||
|
истории и provider semantics при отсутствующем `from`/openedDate. Они не
|
||||||
|
меняют выбранный путь: явные интервалы, replay с начала, unknown lower bound
|
||||||
|
и отсутствие ложного признака полной истории. Подтверждение минимальной
|
||||||
|
даты источника может уточнить только boundary metadata; до него используется
|
||||||
|
описанный безопасный fallback. Числовые результаты будут получены задачами
|
||||||
|
проверки, а не вписаны заранее.
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
Повторные открытия инвестиционных экранов зависят от скорости и доступности
|
||||||
|
T-Bank, а перезапуск приложения теряет уже загруженную историю. Пользователь
|
||||||
|
согласовал [ADR 017](../../../../docs/adr/017-local-tbank-reads-and-background-sync.md):
|
||||||
|
показывать сохранённые данные сразу и обновлять их в фоне только по запросу.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Все экраны T-Bank — счета, портфель, операции, доходы, инструменты,
|
||||||
|
текущие цены, свечи и выплаты — повторно используют постоянно сохранённые
|
||||||
|
данные, в том числе после перезапуска. Обычный ответ не ждёт брокера;
|
||||||
|
целевой ориентир до 500 мс ещё должен быть подтверждён измерениями.
|
||||||
|
- Запрос экрана запускает обновление отсутствующих или устаревших данных:
|
||||||
|
портфель, текущие цены и незавершённая дневная свеча — 1 минута;
|
||||||
|
текущий месяц операций и список счетов — 5 минут;
|
||||||
|
инструменты и календари выплат — сутки. Само течение времени работу не запускает.
|
||||||
|
- Полные прошлые месяцы операций и завершённые свечи сохраняются.
|
||||||
|
Пропуски догружаются при запросах; ручное обновление перечитывает показанный
|
||||||
|
период и зависимости текущего экрана, а не весь накопленный архив.
|
||||||
|
- При первой загрузке сначала доступны нужный экрану период и сравнения
|
||||||
|
доходов, затем догружается вся доступная история счёта. История бумаг
|
||||||
|
текущего портфеля загружается заранее в рамках этой работы; остальных —
|
||||||
|
при открытии инструмента. Начатая работа может завершиться после ухода.
|
||||||
|
- Пользователь различает свежесть, полноту, загрузку и ошибку обновления.
|
||||||
|
Неполный период не считается окончательным финансовым результатом или
|
||||||
|
подтверждением отсутствия операций. Ошибка сохраняет прежние данные.
|
||||||
|
- Личные данные доступны только своему пользователю в границах текущего
|
||||||
|
подключения. Старое подключение не может опубликовать поздний результат.
|
||||||
|
Существующие проверки доступа и ошибки интеграции сохраняются.
|
||||||
|
|
||||||
|
«Полнота истории счёта» используется в смысле
|
||||||
|
[глоссария](../../../../docs/glossary.md#полнота-истории-счёта): все доступные
|
||||||
|
исполненные операции периода на момент успешной сверки, включая полный пустой
|
||||||
|
период. Полнота не означает свежесть и не исключает поздние исправления.
|
||||||
|
Стабильность ID операции — принятое допущение проекта, не гарантия T-Bank;
|
||||||
|
повторно полученные поля операции могут изменяться.
|
||||||
|
|
||||||
|
**Non-goals:** периодическая синхронизация, автоматический поиск исправлений
|
||||||
|
полной старой истории, обнаружение сменившихся ID, история стоимости портфеля,
|
||||||
|
предзагрузка всего каталога, архив без подключения, новые сценарии отключения
|
||||||
|
и закрытых счетов. Эта сессия создаёт planning artifacts; реализация не выполняется.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- [specs/tbank-data-sync/spec.md](specs/tbank-data-sync/spec.md): постоянное локальное чтение, запросы
|
||||||
|
синхронизации, интервальная полнота, восстановление, общие данные инструментов,
|
||||||
|
границы подключения и проверяемые критерии быстродействия.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- [specs/brokerage-accounts/spec.md](specs/brokerage-accounts/spec.md): локальный список, сохранённые сводки,
|
||||||
|
состояния загрузки и ошибок без синхронного ожидания брокера.
|
||||||
|
- [specs/portfolio-view/spec.md](specs/portfolio-view/spec.md): постоянный последний полный снимок,
|
||||||
|
минутная свежесть и ручное обновление.
|
||||||
|
- [specs/brokerage-account-ledger/spec.md](specs/brokerage-account-ledger/spec.md): локальная пагинация и фильтрация,
|
||||||
|
полнота периода вместо обязательного перечитывания RAM-кэша.
|
||||||
|
- [specs/brokerage-account-income-analytics/spec.md](specs/brokerage-account-income-analytics/spec.md): полнота выбранного периода
|
||||||
|
и сравнений, сохранение результата, идентичность повторно загруженных операций.
|
||||||
|
- [specs/brokerage-account-upcoming-events/spec.md](specs/brokerage-account-upcoming-events/spec.md): прогноз из сохранённых
|
||||||
|
источников с видимой полнотой и ошибкой обновления.
|
||||||
|
- [specs/frontend-data-resilience/spec.md](specs/frontend-data-resilience/spec.md): различимые состояния серверного
|
||||||
|
сохранённого результата и клиентского кэша на всех экранах T-Bank.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Изменение затрагивает пользователей всех инвестиционных экранов, серверное
|
||||||
|
хранение и получение данных, публичный контракт состояний, интерфейс обновления
|
||||||
|
и объясняющую документацию. Финансовые правила, валютная точность, значение
|
||||||
|
прогнозов и изоляция пользователей сохраняются. Основные specs и чужие active
|
||||||
|
changes не изменяются до отдельного завершения lifecycle этого change.
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Realized income analytics is account-scoped and period-bound
|
||||||
|
|
||||||
|
Система MUST предоставлять аутентифицированному User аналитику фактически полученного дохода выбранного открытого поддерживаемого Brokerage Account либо существующего режима «Все счета» (`all`) из счетов, доступных через текущий T-Bank connection User. В режиме `all` система MUST проверять доступ каждого входящего счёта и подтверждать полноту каждого исходного периода по всем входящим счетам; отсутствующий вклад счёта MUST NOT заменяться нулём. Применённый период MUST задаваться включительными календарными датами `from` и `to` в Europe/Moscow, MUST NOT начинаться после своего окончания, MUST NOT содержать будущую дату. Система MUST NOT ограничивать максимальную длительность периода. При отсутствии применённого периода система MUST использовать интервал от первого дня текущего месяца до сегодняшнего дня включительно.
|
||||||
|
|
||||||
|
Чтение MUST использовать сохранённые операции текущего подключения и локальное подтверждение доступа без синхронного запроса T-Bank. Ответ MUST иметь envelope `{ data, meta }` по `tbank-data-sync`. Полнота MUST определяться для выбранного периода и каждого необходимого периода сравнения отдельно. Запрос MUST приоритетно загружать их пропуски до фоновой догрузки остальной истории; известные правила календарных дат MUST сохраняться.
|
||||||
|
|
||||||
|
#### Scenario: Аналитика за период по умолчанию
|
||||||
|
|
||||||
|
- **WHEN** аутентифицированный User запрашивает аналитику доступного Brokerage Account без периода
|
||||||
|
- **THEN** система рассчитывает результат от первого дня текущего месяца по Europe/Moscow до сегодняшнего дня включительно
|
||||||
|
|
||||||
|
#### Scenario: Аналитика за допустимый пользовательский период
|
||||||
|
|
||||||
|
- **WHEN** User применяет включительный период любой длительности, в том числе более 366 дней, без будущих дат
|
||||||
|
- **THEN** система рассчитывает аналитику доступного Brokerage Account за все календарные даты этого периода по Europe/Moscow
|
||||||
|
|
||||||
|
#### Scenario: Отклонение недопустимого периода
|
||||||
|
|
||||||
|
- **WHEN** User запрашивает период с некорректной или будущей датой либо с `from` после `to`
|
||||||
|
- **THEN** система отклоняет запрос и не возвращает результат для другого или усечённого периода
|
||||||
|
|
||||||
|
#### Scenario: Отклонение недоступного счёта
|
||||||
|
|
||||||
|
- **WHEN** User запрашивает аналитику Brokerage Account, который не является открытым поддерживаемым счётом, доступным через его текущий T-Bank connection
|
||||||
|
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает его доход или операции
|
||||||
|
|
||||||
|
#### Scenario: Общий доход требует полноты всех счетов
|
||||||
|
- **WHEN** User запрашивает режим `all` и выбранный период одного доступного счёта ещё не загружен полностью
|
||||||
|
- **THEN** система не возвращает окончательные общие суммы, сохраняет раздельное состояние покрытия счетов и запрашивает недостающую разрешённую историю
|
||||||
|
|
||||||
|
#### Scenario: Открыт ещё не загруженный период
|
||||||
|
- **WHEN** запрошенный допустимый период ещё не имеет сохранённых операций или подтверждённого полного пустого покрытия
|
||||||
|
- **THEN** система отвечает `200` со статусом первичной загрузки или частичного покрытия, запрашивает нужные интервалы и не выдаёт нулевой окончательный доход
|
||||||
|
|
||||||
|
### Requirement: Period comparisons preserve calendar and currency meaning
|
||||||
|
|
||||||
|
Система MUST сопоставлять выбранный период с непосредственно предшествующим периодом. Для полного календарного месяца предыдущим периодом MUST быть предыдущий полный календарный месяц; для любого другого периода предыдущий период MUST иметь ту же включительную продолжительность и непосредственно предшествовать выбранному. Когда соответствующие календарные даты годом ранее доступны, система MUST также показывать year-over-year comparison; когда они недоступны, система MUST обозначать сравнение как недоступное, а не подменять его нулём. Только сравнение с подтверждённым полным покрытием обоих периодов MUST показывать абсолютные coupon, dividend и total значения и их денежные разницы отдельно по исходным валютам и MUST NOT характеризовать их как инвестиционную эффективность.
|
||||||
|
|
||||||
|
Отсутствие ещё не загруженной истории сравнения MUST отличаться от невозможности сравнения по календарю или доступной истории счёта. Система MUST сообщать загрузку, неполноту либо ошибку каждого периода сравнения и MUST NOT подменять их нулевыми суммами или денежными разницами. Полный основной период MAY отображаться при отдельно обозначенной незавершённой загрузке сравнения; его значения MUST NOT смешиваться с частичным сравнением.
|
||||||
|
|
||||||
|
#### Scenario: Сравнение полного календарного месяца
|
||||||
|
|
||||||
|
- **WHEN** User применяет период от первого до последнего дня календарного месяца
|
||||||
|
- **THEN** система сравнивает его с полным непосредственно предшествующим календарным месяцем
|
||||||
|
|
||||||
|
#### Scenario: Сравнение произвольного периода
|
||||||
|
|
||||||
|
- **WHEN** User применяет период, не являющийся полным календарным месяцем
|
||||||
|
- **THEN** система сравнивает его с непосредственно предшествующим включительным периодом той же продолжительности
|
||||||
|
|
||||||
|
#### Scenario: Доступно сравнение год к году
|
||||||
|
|
||||||
|
- **WHEN** для соответствующих календарных дат годом ранее доступна полная история подходящих операций
|
||||||
|
- **THEN** система показывает отдельные source-currency значения и денежные разницы относительно этого периода
|
||||||
|
|
||||||
|
#### Scenario: Годовая история недоступна
|
||||||
|
|
||||||
|
- **WHEN** полная история для соответствующих календарных дат годом ранее недоступна
|
||||||
|
- **THEN** система обозначает year-over-year comparison как недоступное и не показывает отсутствующие данные как нулевой доход
|
||||||
|
|
||||||
|
#### Scenario: Основной период полон, сравнение ещё загружается
|
||||||
|
- **WHEN** выбранный период имеет подтверждённое полное покрытие, а необходимый предыдущий период ещё загружается
|
||||||
|
- **THEN** система возвращает полный итог выбранного периода, отмечает загрузку сравнения и не возвращает для него окончательные суммы или разницы
|
||||||
|
|
||||||
|
#### Scenario: Сравнение догружено после основного периода
|
||||||
|
- **WHEN** все необходимые интервалы периода сравнения успешно завершены
|
||||||
|
- **THEN** следующее локальное чтение возвращает его точные валютные суммы и разницы без ожидания T-Bank
|
||||||
|
|
||||||
|
### Requirement: Realized analytics never presents an incomplete result as complete
|
||||||
|
|
||||||
|
Система MUST формировать окончательные totals, trend buckets, instrument breakdown и drill-down выбранного периода только по подтверждённому полному покрытию его исполненных операций. Каждое comparison MUST отдельно требовать полного покрытия своих исходных периодов. Повторное локальное чтение полного периода MUST NOT ожидать повторного получения cursor pages T-Bank. Ошибка любой необходимой страницы фоновой загрузки MUST сохранять подтверждённый ранее результат и покрытие, фиксировать восстанавливаемую ошибку синхронизации и MUST NOT возвращать частичные totals, comparisons, buckets или breakdown как полный результат. Если полный выбранный результат ещё не получен, envelope MUST возвращать `data: null` с отсутствующим либо частичным покрытием в `meta`, а интерфейс MUST показывать незавершённую загрузку либо её ошибку без окончательных финансовых итогов. Успешный период без подходящих выплат MUST возвращать нулевой результат и отображаться с явным объяснением пустого состояния.
|
||||||
|
|
||||||
|
#### Scenario: Все страницы операций успешно получены
|
||||||
|
|
||||||
|
- **WHEN** для покрытия расчёта требуется несколько cursor pages и каждая страница успешно сохранена с подтверждением окончания диапазона
|
||||||
|
- **THEN** система формирует результат по всем подходящим Ledger Entries со всех страниц
|
||||||
|
|
||||||
|
#### Scenario: Одна из страниц операций недоступна
|
||||||
|
|
||||||
|
- **WHEN** получение любой необходимой cursor page завершается ошибкой
|
||||||
|
- **THEN** ответ сообщает ошибку синхронизации, а страница сохраняет ранее полный результат с прежним временем либо показывает ошибку незавершённой загрузки без окончательных частичных итогов
|
||||||
|
|
||||||
|
#### Scenario: В периоде нет купонов и дивидендов
|
||||||
|
|
||||||
|
- **WHEN** все необходимые операции успешно получены, но среди них нет подходящих coupon или dividend Ledger Entries
|
||||||
|
- **THEN** страница показывает нулевой результат и объясняет, что за выбранный период купоны и дивиденды не найдены
|
||||||
|
|
||||||
|
#### Scenario: Частичная загрузка пока не нашла дохода
|
||||||
|
- **WHEN** только часть операций выбранного периода получена и в этой части нет купонов или дивидендов
|
||||||
|
- **THEN** система не возвращает окончательный нулевой результат и сообщает неполноту периода
|
||||||
|
|
||||||
|
#### Scenario: Ошибка ручной сверки полного периода
|
||||||
|
- **WHEN** ранее полный период перечитывается вручную и промежуточная страница не получена
|
||||||
|
- **THEN** прежний полный результат и время его успешной сверки остаются доступными с ошибкой обновления, а частичная новая выборка не заменяет их
|
||||||
|
|
||||||
|
### Requirement: Account overview has an independent current-month income summary
|
||||||
|
|
||||||
|
Обзор выбранного Brokerage Account MUST показывать компактный source-currency summary «Получено купонами и дивидендами» за период от первого дня текущего месяца по Europe/Moscow до сегодняшнего дня включительно и действие перехода на страницу «Пассивный доход». Summary MUST всегда использовать этот current-month период и MUST NOT наследовать последний пользовательский период страницы аналитики. Пустой или неуспешный расчёт summary MUST оставаться локальным состоянием блока и MUST NOT заменять успешно отображённый Portfolio или другие блоки обзора.
|
||||||
|
|
||||||
|
Summary MUST использовать тот же локальный источник операций и правило полноты, что и полная аналитика. До завершения требуемого текущего периода он MUST показывать состояние загрузки или неполноты без окончательного нулевого дохода. Ошибка фонового обновления MUST сохранять ранее полный summary с прежним временем и безопасным предупреждением.
|
||||||
|
|
||||||
|
#### Scenario: Обзор показывает доход текущего месяца
|
||||||
|
|
||||||
|
- **WHEN** обзор Brokerage Account загружен и current-month analytics успешно рассчитана
|
||||||
|
- **THEN** он показывает отдельные source-currency итоги с начала текущего месяца по сегодняшний день и переход к полной аналитике
|
||||||
|
|
||||||
|
#### Scenario: Пользовательский период не влияет на обзор
|
||||||
|
|
||||||
|
- **WHEN** User ранее применил на странице аналитики период, отличный от текущего месяца
|
||||||
|
- **THEN** summary обзора по-прежнему рассчитывается от первого дня текущего месяца по сегодняшний день
|
||||||
|
|
||||||
|
#### Scenario: Current-month summary недоступен
|
||||||
|
|
||||||
|
- **WHEN** Portfolio обзора успешно загружен, но расчёт current-month summary завершается ошибкой
|
||||||
|
- **THEN** Portfolio и остальные успешно загруженные блоки остаются доступными, а summary показывает собственное состояние восстановления
|
||||||
|
|
||||||
|
#### Scenario: Текущий месяц ещё не загружен полностью
|
||||||
|
- **WHEN** обзор доступен, но текущий месяц операций имеет пропуски
|
||||||
|
- **THEN** блок дохода обозначает неполноту или загрузку, не выдаёт частичный доход за окончательный и не скрывает Portfolio
|
||||||
|
|
||||||
|
### Requirement: Income payments remain traceable to exact Ledger Entries
|
||||||
|
|
||||||
|
В разделе «Выплаты» страница MUST показывать единый список карточек точных исполненных Ledger Entries применённого периода в исходном серверном порядке entries. Каждая выплата MUST иметь собственную сразу видимую карточку с описанием, датой, доступной identity Instrument и signed Money в исходной валюте. Страница MUST NOT добавлять внешние группировочные карточки или заголовки по Instrument либо валюте, subtotals инструмента или количество выплат инструмента. Список MUST сохранять отрицательные корректировки как самостоятельные Ledger Entries и MUST отражать одну запись на идентичность User, Brokerage Account, текущее подключение и operation ID; повторная загрузка того же ID MUST обновлять поля и MUST NOT удваивать выплату. Одинаковые operation IDs разных счетов MUST оставаться отдельными записями. Список MUST NOT включать записи вне аналитического итога. Отказ от группировочного UI MUST NOT отменять проверки принадлежности entries к instrumentKey, валюты и receipt count ответа или приводить к пересчёту денежных итогов на клиенте.
|
||||||
|
|
||||||
|
#### Scenario: Каждая выплата показана отдельной карточкой
|
||||||
|
|
||||||
|
- **WHEN** выбранный период содержит несколько включённых Ledger Entries одного или разных Instrument
|
||||||
|
- **THEN** раздел «Выплаты» сразу показывает отдельную карточку каждой записи в исходном порядке без внешних группировочных блоков, раскрытия, итогов инструмента и receipt count на UI
|
||||||
|
|
||||||
|
#### Scenario: Список воспроизводит точный состав дохода
|
||||||
|
|
||||||
|
- **WHEN** User просматривает загруженный раздел «Выплаты»
|
||||||
|
- **THEN** он видит все и только включённые coupon и dividend Ledger Entries применённого периода с датой, описанием, доступной identity и исходной суммой
|
||||||
|
|
||||||
|
#### Scenario: Отрицательная корректировка и повторяющиеся IDs сохраняются
|
||||||
|
|
||||||
|
- **WHEN** доход содержит отрицательную корректировку, повторно загруженный ID того же счёта либо одинаковые operation IDs разных счетов
|
||||||
|
- **THEN** корректировка отображается с исходным знаком; одна операция того же счёта имеет одну карточку с актуальными полями, а операции разных счетов остаются отдельными карточками
|
||||||
|
|
||||||
|
#### Scenario: Валюта видна в сумме каждой выплаты
|
||||||
|
|
||||||
|
- **WHEN** подходящие выплаты одного Instrument представлены в нескольких исходных валютах
|
||||||
|
- **THEN** каждая карточка показывает свою Money с исходной валютой, без отдельного заголовка валютной группы или объединённого мультивалютного итога
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: User can retrieve an account ledger
|
||||||
|
Система MUST предоставлять аутентифицированный журнал только для чтения выбранного Brokerage Account либо существующего режима «Все счета» (`all`) в границах текущего User и его T-Bank connection. В режиме `all` доступ MUST проверяться для каждого входящего счёта, операции MUST сохранять принадлежность исходному счёту, а полнота периода MUST учитывать покрытие каждого входящего счёта. Журнал MUST содержать только исполненные операции T-Bank и при отсутствии периода по умолчанию охватывать интервал от начала текущего календарного года в UTC до момента запроса.
|
||||||
|
|
||||||
|
Система MUST читать сохранённые операции и локальное подтверждение доступа текущего подключения без синхронного обращения к T-Bank, включая запрос списка счетов. Ответ MUST содержать envelope `{ data, meta }` по `tbank-data-sync`, отражающий полноту запрошенного периода независимо от текущей страницы. Отсутствующие данные MUST возвращаться с `data: null` и состоянием загрузки, а частично сохранённый период MUST обозначаться как `partial`. Только подтверждённо полный пустой период MUST обозначать отсутствие операций.
|
||||||
|
|
||||||
|
#### Scenario: Получение журнала с периодом по умолчанию
|
||||||
|
- **WHEN** аутентифицированный User запрашивает журнал принадлежащего ему Brokerage Account без периода
|
||||||
|
- **THEN** система возвращает сохранённые исполненные операции в `data` и полноту периода в `meta` за период от начала текущего календарного года в UTC до момента запроса, запрашивая отсутствующие интервалы в фоне
|
||||||
|
|
||||||
|
#### Scenario: Отклонение недоступного счёта
|
||||||
|
- **WHEN** аутентифицированный User запрашивает журнал счёта, для которого локальные данные текущего подключения подтверждают отсутствие доступа либо неподдерживаемое или закрытое состояние
|
||||||
|
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает операции
|
||||||
|
|
||||||
|
#### Scenario: Общий журнал содержит неполный счёт
|
||||||
|
- **WHEN** User запрашивает режим `all` и у одного из разрешённых счетов есть незагруженный интервал периода
|
||||||
|
- **THEN** система возвращает только разрешённые сохранённые записи с принадлежностью счёту и обозначает общий период неполным, не подменяя отсутствующий вклад пустым счётом
|
||||||
|
|
||||||
|
#### Scenario: Пустая загруженная часть не доказывает отсутствие операций
|
||||||
|
- **WHEN** в сохранённой части периода нет записей, но покрытие периода не завершено
|
||||||
|
- **THEN** ответ обозначает неполноту и запрошенную загрузку, а интерфейс не сообщает, что операций за период нет
|
||||||
|
|
||||||
|
### Requirement: User can browse a filtered ledger
|
||||||
|
Система MUST позволять User ограничивать журнал включительным периодом дат и любым подмножеством категорий trade, income, tax, fee, transfer, other. Система MUST фильтровать сохранённые операции локально, не перечитывая T-Bank из-за категории, instrument UID, размера страницы или перехода между страницами полного периода. Система MUST разбивать упорядоченный результат на страницы с непрозрачным cursor и возвращать признак доступности следующей страницы.
|
||||||
|
|
||||||
|
Cursor MUST быть привязан к User, текущему подключению, составу счетов, применённым фильтрам, зафиксированной верхней границе периода и версии читаемых данных. Курсор другой границы или изменившейся версии MUST отклоняться существующим `INVALID_OPERATIONS_CURSOR`, позволяя повторить первую страницу с прежними фильтрами. Доступность следующей локальной страницы MUST NOT заменять признак полноты периода. Равнозначные порядок и дубли categories MUST NOT создавать другую идентичность запроса.
|
||||||
|
|
||||||
|
#### Scenario: Фильтрация доходных операций за период
|
||||||
|
- **WHEN** User запрашивает период дат и категорию income
|
||||||
|
- **THEN** результат содержит только исполненные доходные операции с датой в пределах этого периода
|
||||||
|
|
||||||
|
#### Scenario: Продолжение истории
|
||||||
|
- **WHEN** User запрашивает cursor следующей страницы, возвращённый предыдущим ответом журнала, и версия данных не изменилась
|
||||||
|
- **THEN** система возвращает следующую страницу без повторения записей предыдущей страницы
|
||||||
|
|
||||||
|
#### Scenario: Обновление изменило данные между страницами
|
||||||
|
- **WHEN** после выдачи первой страницы успешная синхронизация изменила её набор операций и User передаёт прежний cursor
|
||||||
|
- **THEN** система возвращает `INVALID_OPERATIONS_CURSOR`, а повторная загрузка начинает первую страницу с сохранёнными применёнными фильтрами без пропуска или дублирования записей
|
||||||
|
|
||||||
|
#### Scenario: Полный период фильтруется локально
|
||||||
|
- **WHEN** User меняет категории или страницы в полностью сохранённом прошлом периоде
|
||||||
|
- **THEN** система формирует результат из сохранённых операций без нового запроса операций T-Bank
|
||||||
|
|
||||||
|
### Requirement: Ledger entries preserve broker operation meaning
|
||||||
|
Система MUST представлять одну upstream-операцию одной записью журнала и MUST передавать её timestamp, тип операции, категорию, описание, доступные идентификаторы инструмента, доступную presentation metadata инструмента, денежную выплату, количество, цену, НКД и комиссию. Денежные поля MUST сохранять исходную валюту и точное десятичное значение.
|
||||||
|
|
||||||
|
В пределах текущего подключения идентичность Ledger Entry MUST включать User, Brokerage Account и upstream operation ID. Повторное получение того же ID MUST обновлять доступные поля этой операции без создания второй записи; одинаковые ID разных счетов MUST оставаться разными записями. Стабильность ID MUST считаться допущением проекта, а не гарантией T-Bank.
|
||||||
|
|
||||||
|
#### Scenario: Отображение купонной выплаты
|
||||||
|
- **WHEN** T-Bank возвращает исполненную купонную операцию с выплатой и данными инструмента
|
||||||
|
- **THEN** запись журнала определяет её как income и передаёт исходную сумму выплаты, валюту и данные инструмента
|
||||||
|
|
||||||
|
#### Scenario: Отображение комиссии сделки без синтетической записи
|
||||||
|
- **WHEN** T-Bank возвращает исполненную сделку, содержащую выплату и комиссию
|
||||||
|
- **THEN** журнал содержит одну запись сделки с комиссией в качестве её детали
|
||||||
|
|
||||||
|
#### Scenario: Отображение движения бумаг без денег
|
||||||
|
- **WHEN** T-Bank возвращает исполненную операцию, изменяющую состав инструментов, но без денежной выплаты
|
||||||
|
- **THEN** запись журнала передаёт доступное количество и не содержит искусственной денежной выплаты с нулевым значением
|
||||||
|
|
||||||
|
#### Scenario: Доступна визуальная идентичность инструмента
|
||||||
|
- **WHEN** для инструмента исполненной операции доступна presentation metadata бренда
|
||||||
|
- **THEN** запись журнала передаёт её вместе с доступными идентификаторами инструмента, не меняя остальные сведения операции
|
||||||
|
|
||||||
|
#### Scenario: Повторная операция содержит исправленные поля
|
||||||
|
- **WHEN** успешная синхронизация получает операцию с уже сохранённым ID того же счёта и изменёнными полями
|
||||||
|
- **THEN** последующее чтение возвращает одну запись с обновлёнными полями без удвоения денежного движения
|
||||||
|
|
||||||
|
### Requirement: Account overview includes recent ledger events
|
||||||
|
Обзор счёта MUST показывать не более пяти последних сохранённых исполненных записей журнала этого счёта с состоянием полноты и загрузки требуемого периода и действие перехода к его полному журналу. Каждая показанная запись MUST иметь ту же визуальную идентичность инструмента и семантику денежного движения, что и запись полного журнала. Неуспешный или пустой запрос последних записей MUST оставаться локальным состоянием блока последних событий и MUST NOT заменять успешно отображённый обзор Portfolio.
|
||||||
|
|
||||||
|
#### Scenario: Отображение последних событий
|
||||||
|
- **WHEN** загружается обзор счёта и его журнал содержит исполненные операции
|
||||||
|
- **THEN** обзор показывает не более пяти последних записей и ссылку на полный журнал
|
||||||
|
|
||||||
|
#### Scenario: Последние записи недоступны
|
||||||
|
- **WHEN** обзор счёта успешно загружается, но запрос последних записей завершается ошибкой
|
||||||
|
- **THEN** обзор Portfolio остаётся доступным, а блок последних событий отображает собственное состояние восстановления
|
||||||
|
|
||||||
|
#### Scenario: Последняя запись не содержит visual metadata инструмента
|
||||||
|
- **WHEN** одна из последних записей не имеет доступной presentation metadata инструмента
|
||||||
|
- **THEN** обзор показывает для неё понятный текстовый fallback и сохраняет дату, тип, инструмент и денежное изменение записи доступными для чтения
|
||||||
|
|
||||||
|
#### Scenario: Первичная история обзора неполна
|
||||||
|
- **WHEN** обзор получил только часть требуемых операций или `data: null`
|
||||||
|
- **THEN** блок обозначает загрузку или неполноту и не утверждает, что показанные записи окончательны либо операции отсутствуют
|
||||||
|
|
||||||
|
## REMOVED Requirements
|
||||||
|
|
||||||
|
### Requirement: Кэш страниц журнала ограничивает локальные ресурсы
|
||||||
|
|
||||||
|
**Reason:** Постоянное хранение операций и запросная синхронизация заменяют обязательный RAM-кэш на 100 страниц с TTL 30 секунд; его запрет на выдачу старой страницы противоречит локальному чтению сохранённой истории.
|
||||||
|
|
||||||
|
**Migration:** Чтение, свежесть, объединение загрузок и сохранение прогресса переходят к `tbank-data-sync`; локальная фильтрация и привязанный к версии данных cursor регулируются `User can browse a filtered ledger`. Существующие transient cursors не являются доказательством полноты и при несовместимости возвращают `INVALID_OPERATIONS_CURSOR`.
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: User can retrieve upcoming events for an account
|
||||||
|
Система MUST предоставлять аутентифицированному User read-only список известных будущих событий выбранного открытого поддерживаемого Brokerage Account либо существующего режима «Все счета» (`all`) в границах текущего T-Bank connection User. Параметры `from` и `to` MUST сохранять существующий контракт: необязательные строгие календарные даты `YYYY-MM-DD` с включительными границами в UTC. Без `from` начало MUST соответствовать текущей календарной дате UTC; без `to` конец MUST соответствовать дате `from` плюс 30 календарных дней. При `from` позже `to` система MUST сохранять существующий отказ `INVALID_EVENTS_PERIOD`, не подменяя период. В режиме `all` доступ и полнота обязательных источников MUST проверяться для каждого входящего счёта, события MUST сохранять принадлежность счёту. Ответ MUST иметь envelope `{ data, meta }` по `tbank-data-sync`; доступный прогноз в `data` MUST содержать только события coupon, dividend, maturity и offer, отсортированные по дате по возрастанию, и момент расчёта прогноза.
|
||||||
|
|
||||||
|
Список будущих событий является прогнозом по последнему успешно сохранённому наблюдаемому состоянию счёта и MUST NOT означать, что WealthLedger хранит долговременное право владения Brokerage Account независимо от T-Bank connection.
|
||||||
|
|
||||||
|
Система MUST формировать прогноз из сохранённых позиций и календарей, не ожидая T-Bank при локальном чтении и подтверждении доступа. Свежесть и загрузка зависимостей MUST быть видимы. Отсутствующий подтверждённый результат MUST возвращаться как `data: null` с состоянием загрузки, а не пустой список. Полнота локальной загрузки поддерживаемых данных MUST NOT означать полноту информации внешнего источника обо всех возможных событиях.
|
||||||
|
|
||||||
|
#### Scenario: Получение событий доступного счёта
|
||||||
|
- **WHEN** аутентифицированный User запрашивает будущие события открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection, без явного периода
|
||||||
|
- **THEN** система возвращает известные события от сегодняшней даты UTC до даты через 30 календарных дней включительно и момент, на который был выполнен расчёт
|
||||||
|
|
||||||
|
#### Scenario: Отсутствие событий в периоде
|
||||||
|
- **WHEN** все необходимые данные применённого периода успешно сохранены и для счёта в нём нет известных событий
|
||||||
|
- **THEN** система возвращает успешный пустой список и момент расчёта
|
||||||
|
|
||||||
|
#### Scenario: Отклонение недоступного счёта
|
||||||
|
- **WHEN** аутентифицированный User запрашивает события счёта, который не является открытым поддерживаемым счётом, доступным через его T-Bank connection
|
||||||
|
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает события
|
||||||
|
|
||||||
|
#### Scenario: Недопустимый период сохраняет ошибку
|
||||||
|
- **WHEN** User передаёт `from` позже `to` для выбранного счёта либо режима `all`
|
||||||
|
- **THEN** система возвращает существующую ошибку `INVALID_EVENTS_PERIOD`, не запускает загрузку другого периода и не возвращает его прогноз
|
||||||
|
|
||||||
|
#### Scenario: Прогноз ещё не может быть рассчитан
|
||||||
|
- **WHEN** позиции либо необходимый поддерживаемый календарь ещё не сохранены полностью
|
||||||
|
- **THEN** система возвращает `200` с `data: null` и состоянием запрошенной загрузки или её ошибки вместо утверждения об отсутствии событий
|
||||||
|
|
||||||
|
### Requirement: Forecasts preserve event meaning and uncertainty
|
||||||
|
Система MUST передавать для каждого события его календарную дату, тип, доступные идентификаторы и название Instrument, а также прогнозную сумму до налогов как Money, если она известна. Прогнозная сумма MUST рассчитываться по количеству бумаг полного успешно сохранённого Portfolio Snapshot, использованного при последнем успешном расчёте показываемого прогноза, с указанием времени этого наблюдения. При неудачном обновлении сохранённый прогноз MUST сохранять эту исходную зависимость и MUST NOT приписываться более новому Portfolio Snapshot. Если денежные условия события неизвестны или неприменимы, прогнозная сумма MUST быть `null`, а не нулём и не парой разрозненных полей. Дивиденд MUST обозначать переданную дату как дату реестра, а offer MUST передаваться без вымышленной суммы.
|
||||||
|
|
||||||
|
Instrument в Future Cash Event MUST иметь тот же доменный смысл, что и в Portfolio: это финансовый инструмент, к которому относится событие, даже если событие не является Portfolio Position.
|
||||||
|
|
||||||
|
#### Scenario: Прогноз купона по текущей позиции
|
||||||
|
- **WHEN** поддерживаемая облигация в текущей Portfolio Position имеет известный купон в пределах периода
|
||||||
|
- **THEN** система возвращает событие coupon с прогнозной суммой до налогов как Money в исходной валюте, рассчитанной по текущему количеству облигаций
|
||||||
|
|
||||||
|
#### Scenario: Прогноз дивиденда
|
||||||
|
- **WHEN** поддерживаемая акция в текущей Portfolio Position имеет объявленный дивиденд с датой фиксации реестра в пределах периода
|
||||||
|
- **THEN** система возвращает событие dividend с датой, обозначенной как дата реестра (`record_date`), и прогнозной суммой до налогов как Money, равной `dividend_net` на одну бумагу, умноженной на текущее количество акций
|
||||||
|
|
||||||
|
#### Scenario: Отменённый дивиденд не попадает в прогноз
|
||||||
|
- **WHEN** запись дивиденда T-Bank имеет тип выплаты `Cancelled`
|
||||||
|
- **THEN** система не добавляет такое событие в результат
|
||||||
|
|
||||||
|
#### Scenario: Отображение оферты без суммы
|
||||||
|
- **WHEN** для поддерживаемой облигации известна оферта в пределах периода, но её денежные условия недоступны
|
||||||
|
- **THEN** система возвращает событие offer с прогнозной суммой `null`
|
||||||
|
|
||||||
|
#### Scenario: Неподдерживаемое частичное погашение
|
||||||
|
- **WHEN** источник не предоставляет расписание будущих частичных погашений или амортизации облигации
|
||||||
|
- **THEN** система НЕ создаёт синтетическое событие частичного погашения
|
||||||
|
|
||||||
|
### Requirement: Upcoming events use complete source results
|
||||||
|
Система MUST использовать последний прогноз по полным успешно полученным необходимым источникам и MUST NOT возвращать частичный список как полный результат. Если обновление необходимых данных брокера или MOEX неуспешно, система MUST сохранять прежний подтверждённый прогноз и время его расчёта, возвращая `200` с ошибкой синхронизации в `meta`. Если полного прогноза ещё нет, ответ MUST содержать `data: null` и отсутствующее либо частичное покрытие с ошибкой или состоянием загрузки. Подтверждённые ошибки сессии, текущего подключения и доступа MUST сохранять существующие Problem Details и MUST NOT маскироваться прогнозом. Система MUST пропускать инструменты, для которых источник не предоставляет поддерживаемые события, не создавая строки «нет данных».
|
||||||
|
|
||||||
|
#### Scenario: Недоступность источника
|
||||||
|
- **WHEN** необходимый источник данных брокера или MOEX временно недоступен
|
||||||
|
- **THEN** система сообщает ошибку обновления, сохраняя ранее полный прогноз и время его расчёта либо `data: null`, без выдачи частичного списка за полный
|
||||||
|
|
||||||
|
#### Scenario: Инструмент без поддерживаемых данных
|
||||||
|
- **WHEN** Portfolio содержит инструмент, для которого источник не предоставляет поддерживаемые будущие события
|
||||||
|
- **THEN** система не добавляет для него синтетическую строку в результат
|
||||||
|
|
||||||
|
#### Scenario: MOEX отвечает без запрошенного блока
|
||||||
|
- **WHEN** MOEX отвечает успешно, но не содержит запрошенного блока данных (например, дивидендов)
|
||||||
|
- **THEN** система трактует ответ как отсутствие данных по инструменту и не завершает запрос ошибкой
|
||||||
|
|
||||||
|
### Requirement: Account page presents an upcoming events list
|
||||||
|
Страница Brokerage Account MUST предоставлять действие перехода на отдельную страницу «Будущие события». Эта страница MUST сохранять существующий выбор и явное применение периода через `from`/`to` и показывать все полученные события применённого периода компактным списком без пагинации и суммарного итога, с датой, типом, инструментом и прогнозной суммой в исходной валюте, если она известна. Страница MUST объяснять, что прогноз основан на текущей позиции, указан до налогов и может быть неполным из-за доступности данных источника.
|
||||||
|
|
||||||
|
Страница MUST показывать время сохранённого прогноза, состояние свежести и загрузки, а также действие «Обновить» для текущего применённого периода и его зависимостей. Неполученный результат MUST отличаться от подтверждённого пустого списка. Временная ошибка обновления MUST NOT скрывать сохранённый полный прогноз.
|
||||||
|
|
||||||
|
#### Scenario: Просмотр ближайших событий
|
||||||
|
- **WHEN** User открывает «Будущие события» из обзора Brokerage Account без явного периода
|
||||||
|
- **THEN** он видит отсортированный список событий периода по умолчанию от сегодняшней даты UTC до даты через 30 календарных дней включительно, где offer обозначен без суммы
|
||||||
|
|
||||||
|
#### Scenario: Пустое состояние страницы
|
||||||
|
- **WHEN** запрос будущих событий возвращает подтверждённый полный пустой список в `data`
|
||||||
|
- **THEN** страница сообщает, что за применённый период событий не найдено, и объясняет ограничения покрытия источника
|
||||||
|
|
||||||
|
#### Scenario: Обновление применённого периода
|
||||||
|
- **WHEN** User применил допустимый период будущих событий и нажал «Обновить»
|
||||||
|
- **THEN** обновляются необходимые позиции и календари для применённого периода, его фильтры сохраняются и обновление не подменяется периодом по умолчанию
|
||||||
|
|
||||||
|
#### Scenario: Ошибка страницы
|
||||||
|
- **WHEN** фоновое обновление будущих событий завершается временной ошибкой
|
||||||
|
- **THEN** страница сохраняет прежний полный прогноз с его временем и предупреждением либо показывает ошибку первичной загрузки при его отсутствии, не выдаёт частичный список за полный и предоставляет действие повторить обновление
|
||||||
|
|
||||||
|
### Requirement: Account overview includes upcoming events
|
||||||
|
Обзор Brokerage Account MUST показывать не более пяти ближайших Future Cash Event и действие перехода на полную страницу «Будущие события». Неуспешный или пустой запрос этого блока MUST оставаться локальным состоянием блока и не заменять успешно отображённый Portfolio.
|
||||||
|
|
||||||
|
#### Scenario: Отображение ближайших событий
|
||||||
|
- **WHEN** обзор счёта успешно загружен и календарь содержит будущие события
|
||||||
|
- **THEN** обзор показывает первые пять событий в порядке даты и действие перехода к полной странице
|
||||||
|
|
||||||
|
#### Scenario: Ближайшие события недоступны
|
||||||
|
- **WHEN** обзор счёта успешно загружен, но запрос будущих событий завершается ошибкой
|
||||||
|
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственную ошибку
|
||||||
|
|
||||||
|
#### Scenario: Ошибка обновления сохранённого прогноза обзора
|
||||||
|
- **WHEN** блок обзора уже показывает полный прогноз, а его обновление временно неуспешно
|
||||||
|
- **THEN** блок сохраняет показанные события и время расчёта с локальным предупреждением, не скрывая Portfolio
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Список активных брокерских счетов
|
||||||
|
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts`, возвращающий envelope `{ data, meta }`, где `data` содержит сохранённый массив открытых поддерживаемых инвестиционных счетов T-Bank, доступных через текущий T-Bank connection User, в порядке, полученном от T-Bank. Каждый элемент MUST содержать стабильный `id`, непустое `name`, нормализованный `type` (`BROKERAGE`, `INDIVIDUAL_INVESTMENT`, `INVEST_BOX` или `MONEY_MARKET_FUND`) и nullable `openedOn` в формате `YYYY-MM-DD`. Система MUST включать только счета T-Bank типов «брокерский счёт», «ИИС», «Инвесткопилка» и «фонд денежного рынка»; банковские и неизвестные типы MUST NOT возвращаться. Система MUST удалить внешние пробелы из имени и при пустом результате использовать непустое название соответствующего типа.
|
||||||
|
|
||||||
|
Каждый элемент MUST также содержать nullable `portfolioSummary`. Доступная сводка MUST содержать `totalsByCurrency`: точные Money-итоги Portfolio по исходным ISO-валютам, включающие стоимость позиций и Cash Balance в той же валюте. Она MUST NOT содержать FX-конвертированный или межвалютный итог и MUST NOT выдавать Cash Balance за самостоятельный второй итог. Если успешная сводка Portfolio конкретного доступного счёта ещё не сохранена, `portfolioSummary` MUST быть `null`, не исключая этот счёт из списка и не делая список неуспешным. Пустой доступный Portfolio MUST передаваться доступной сводкой с пустым `totalsByCurrency`, а не `null`.
|
||||||
|
|
||||||
|
Список Brokerage Account и его сводки постоянно сохраняются и являются результатом последнего успешного наблюдения текущего брокерского подключения и MUST NOT создавать ожидание, что WealthLedger хранит долговременное право владения счётом или Portfolio независимо от T-Bank connection.
|
||||||
|
|
||||||
|
Обычный запрос MUST читать сохранённый список и доступные сводки без ожидания T-Bank. Срок свежести списка MUST составлять пять минут; сводки MUST следовать минутной свежести Portfolio. Отсутствующий список MUST возвращаться как `200` с `data: null`, `meta.availability: missing` и состоянием запрошенной загрузки. Только успешно полученный полный пустой список MUST возвращаться как `data: []` с `meta.availability: complete`. Время, свежесть и состояние загрузки MUST передаваться по контракту `tbank-data-sync`. Отсутствие либо ошибка загрузки сводки MUST отличаться от отсутствия самого счёта; сохранённая успешная сводка MUST оставаться доступной после временной ошибки её обновления.
|
||||||
|
|
||||||
|
#### Scenario: Поддерживаемые счета возвращаются в upstream-порядке
|
||||||
|
- **WHEN** User с действующим T-Bank connection запрашивает список, а последняя успешная синхронизация сохранила открытые брокерский счёт, ИИС и неподдержанный дебетовый счёт
|
||||||
|
- **THEN** система отвечает `200` с массивом брокерского счёта и ИИС в `data` в исходном порядке T-Bank без дебетового счёта
|
||||||
|
|
||||||
|
#### Scenario: Пустой список поддерживаемых счетов
|
||||||
|
- **WHEN** User с действующим T-Bank connection имеет сохранённый успешно подтверждённый пустой список открытых поддерживаемых инвестиционных счетов
|
||||||
|
- **THEN** система отвечает `200` с `data: []` и `meta.availability: complete`
|
||||||
|
|
||||||
|
#### Scenario: Пустое название получает fallback
|
||||||
|
- **WHEN** T-Bank возвращает поддерживаемый открытый счёт с пустым либо состоящим из пробелов именем
|
||||||
|
- **THEN** система отвечает этим счётом с непустым названием его нормализованного типа
|
||||||
|
|
||||||
|
#### Scenario: Дата открытия отсутствует
|
||||||
|
- **WHEN** T-Bank не возвращает дату открытия поддерживаемого открытого счёта
|
||||||
|
- **THEN** система отвечает этим счётом с `openedOn: null`
|
||||||
|
|
||||||
|
#### Scenario: Мультивалютная сводка счёта
|
||||||
|
- **WHEN** доступный Portfolio счёта содержит итоги в RUB и USD, включая Cash Balance в USD
|
||||||
|
- **THEN** ответ содержит одну доступную `portfolioSummary` с отдельными Money-итогами RUB и USD без FX-конвертации, общего итога или отдельного итога Cash Balance
|
||||||
|
|
||||||
|
#### Scenario: Сводка одного счёта временно недоступна
|
||||||
|
- **WHEN** список доступных счетов получен, но Portfolio одного из них ещё не сохранён и его загрузка временно недоступна
|
||||||
|
- **THEN** система отвечает `200` всеми счетами в исходном порядке, передаёт для этого счёта `portfolioSummary: null` и не заменяет успешные сводки остальных счетов ошибкой списка
|
||||||
|
|
||||||
|
#### Scenario: Доступный Portfolio пуст
|
||||||
|
- **WHEN** доступный счёт не имеет ненулевых позиций и доступных денежных остатков
|
||||||
|
- **THEN** система передаёт доступную `portfolioSummary` с пустым `totalsByCurrency`
|
||||||
|
|
||||||
|
#### Scenario: Список ещё не загружен
|
||||||
|
- **WHEN** User впервые запрашивает список текущего подключения и успешного сохранённого результата ещё нет
|
||||||
|
- **THEN** система возвращает `200` с `data: null`, состоянием загрузки в `meta`, запрашивает фоновую синхронизацию и не выдаёт отсутствие данных за отсутствие счетов
|
||||||
|
|
||||||
|
#### Scenario: Ошибка обновления сохранённой сводки
|
||||||
|
- **WHEN** обновление Portfolio одного счёта временно неуспешно и его успешная сводка уже сохранена
|
||||||
|
- **THEN** список сохраняет эту сводку с прежним временем и видимым состоянием ошибки обновления вместо `null`
|
||||||
|
|
||||||
|
### Requirement: Состояния недоступности списка счетов
|
||||||
|
Система MUST возвращать последний полностью сохранённый список текущего подключения без синхронной проверки T-Bank. Неполный ответ обновления MUST NOT заменять полный список. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если для текущего подключения уже подтверждено отклонение token как неаутентифицированного или без прав, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection. При временной либо иной upstream-ошибке фонового обновления система MUST сохранять прежний список и время его успешной загрузки, при наблюдении завершённой попытки возвращая `200` с `meta.sync.state: failed` и безопасным `meta.sync.errorCode`. Обычный запрос экрана либо ручное обновление MAY запросить новую попытку согласно `tbank-data-sync`; наблюдение статуса MUST NOT запускать её. При отсутствии сохранённого списка `data` MUST оставаться `null`. Ошибка обновления MUST NOT менять время успешной загрузки и сама по себе MUST NOT делать ещё свежие данные устаревшими; свежесть MUST продолжать определяться сроком соответствующего источника. Подтверждённая недействительность текущего token MUST запрещать выдачу личных данных, включая сохранённый список; локальная сессия и текущая граница подключения MUST проверяться до чтения.
|
||||||
|
|
||||||
|
#### Scenario: Подключение отсутствует
|
||||||
|
- **WHEN** User без сохранённого T-Bank connection запрашивает список
|
||||||
|
- **THEN** система не обращается к T-Bank и отвечает `409` Problem Details с code `TBANK_NOT_CONNECTED`
|
||||||
|
|
||||||
|
#### Scenario: Сохранённый token больше не действителен
|
||||||
|
- **WHEN** фоновая проверка подтвердила, что T-Bank отклоняет token текущего подключения как неаутентифицированный или без прав, и User снова запрашивает список
|
||||||
|
- **THEN** система отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и не удаляет connection
|
||||||
|
|
||||||
|
#### Scenario: T-Bank временно недоступен
|
||||||
|
- **WHEN** фоновое обновление списка завершается временной либо иной upstream-ошибкой
|
||||||
|
- **THEN** последующее наблюдение результата без нового запуска отвечает `200` с последним сохранённым списком либо `data: null`, `meta.sync.state: failed` и `meta.sync.errorCode: TBANK_UNAVAILABLE`, не подтверждая новую успешную загрузку
|
||||||
|
|
||||||
|
### Requirement: Экран списка брокерских счетов
|
||||||
|
Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` и предоставлять действие «Обновить», без автоматического refetch при возврате фокуса. Клиент MUST использовать успешный frontend-кэш списка в течение 60 секунд. После истечения этого срока клиент MUST запрашивать локальный результат и состояние синхронизации; при уже имеющемся успешном списке он MUST сохранять последний успешный Client Cached Snapshot видимым во время фонового обновления и после его ошибки, ясно отделяя это состояние от свежего результата. Клиент MUST читать список из `data`, показывать свежесть и состояние синхронизации из `meta` и MUST NOT считать успешный HTTP-ответ с `data: null` пустым списком. Ручное обновление MUST обходить проверку давности списка и показанных сводок, не обновляя всю историю счетов. При успешном непустом ответе клиент MUST отобразить адаптивный одноколоночный список широких интерактивных карточек счетов с локализованными типом и, если доступна, датой открытия без времени; каждая карточка MUST ясно показывать тип, имя, финансовую сводку, вторичные метаданные и заметное действие открытия счёта, вести на `/accounts/:accountId` по всей области и сохранять доступное клавиатурное состояние. После успешной замены или удаления T-Bank connection клиент MUST инвалидировать список счетов.
|
||||||
|
|
||||||
|
Для доступной `portfolioSummary` карточка MUST показывать не более двух Money-итогов в исходных валютах и обозначать наличие остальных валют, если они есть; она MUST NOT показывать или подразумевать FX-конвертацию, межвалютный агрегированный итог либо Cash Balance как отдельный второй итог. При `portfolioSummary: null` карточка MUST показывать локальное безопасное состояние временной недоступности сводки, сохраняя метаданные счёта и возможность открыть его. При доступной пустой сводке карточка MUST не выдавать отсутствие итогов за временную ошибку.
|
||||||
|
|
||||||
|
#### Scenario: Пользователь открывает счёт из списка
|
||||||
|
- **WHEN** клиент успешно загрузил непустой список и User выбирает карточку счёта
|
||||||
|
- **THEN** клиент переходит на `/accounts/:accountId` с `id` выбранного счёта
|
||||||
|
|
||||||
|
#### Scenario: Список пуст
|
||||||
|
- **WHEN** клиент получает `data: []` с `meta.availability: complete`
|
||||||
|
- **THEN** клиент показывает штатное сообщение об отсутствии инвестиционных счетов T-Bank
|
||||||
|
|
||||||
|
#### Scenario: Список берётся из свежего frontend-кэша
|
||||||
|
- **WHEN** User повторно открывает список в течение 60 секунд после успешной загрузки
|
||||||
|
- **THEN** клиент показывает сохранённые счета без нового API-запроса
|
||||||
|
|
||||||
|
#### Scenario: Устаревший список загружается повторно
|
||||||
|
- **WHEN** User открывает список после истечения 60 секунд frontend-кэша и уже существует последний успешный список
|
||||||
|
- **THEN** клиент оставляет этот список видимым во время нового запроса и ясно обозначает фоновое обновление
|
||||||
|
|
||||||
|
#### Scenario: Ошибка фонового обновления не скрывает snapshot
|
||||||
|
- **WHEN** обновление устаревшего списка завершается временной ошибкой при наличии последнего успешного Client Cached Snapshot
|
||||||
|
- **THEN** клиент сохраняет snapshot видимым, безопасно сообщает о неуспешном обновлении и предоставляет применимое действие повторной загрузки
|
||||||
|
|
||||||
|
#### Scenario: Карточка ограничивает валютные итоги
|
||||||
|
- **WHEN** доступная сводка счёта содержит итоги в трёх или более валютах
|
||||||
|
- **THEN** карточка показывает не более двух исходных валютных итогов и обозначает наличие остальных без общего сконвертированного значения
|
||||||
|
|
||||||
|
#### Scenario: Недоступность сводки локальна карточке
|
||||||
|
- **WHEN** клиент получает список, в котором `portfolioSummary` одного счёта равна `null`
|
||||||
|
- **THEN** он показывает временную недоступность только на этой карточке, не скрывает остальные карточки и сохраняет переход на детали недоступной сводки счёта
|
||||||
|
|
||||||
|
#### Scenario: Пустая сводка не является ошибкой
|
||||||
|
- **WHEN** клиент получает доступную `portfolioSummary` с пустым `totalsByCurrency`
|
||||||
|
- **THEN** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной недоступности
|
||||||
|
|
||||||
|
#### Scenario: Начальная загрузка не становится пустым списком
|
||||||
|
- **WHEN** клиент получает `data: null` со статусом запрошенной синхронизации
|
||||||
|
- **THEN** он показывает состояние первичной загрузки или её ошибки, а не сообщение об отсутствии инвестиционных счетов
|
||||||
|
|
||||||
|
#### Scenario: Ручное обновление списка
|
||||||
|
- **WHEN** User нажимает «Обновить» на экране списка счетов
|
||||||
|
- **THEN** клиент запрашивает обновление списка и показанных сводок независимо от давности, сохраняя доступные данные и отображая прогресс
|
||||||
|
|
||||||
|
### Requirement: Клиентские состояния ошибки списка счетов
|
||||||
|
Клиент MUST NOT автоматически повторять неуспешный запрос списка. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторной загрузки и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторной загрузки. При `TBANK_NOT_CONNECTED` или `INVALID_TBANK_TOKEN` клиент MUST скрыть личные данные даже при наличии Client Cached Snapshot. Для временной и неизвестной ошибки без snapshot клиент MUST показать ошибку без списка; при наличии snapshot он MUST сохранить его видимым с безопасным предупреждением. `meta.sync.state: failed` MUST отображаться как ошибка фоновой синхронизации, отдельно от ошибки HTTP-запроса; успешный HTTP-ответ сам по себе MUST NOT скрывать такую ошибку.
|
||||||
|
|
||||||
|
#### Scenario: Подключение требуется
|
||||||
|
- **WHEN** клиент получает `TBANK_NOT_CONNECTED`
|
||||||
|
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
|
||||||
|
|
||||||
|
#### Scenario: Известная ошибка не повторяется автоматически
|
||||||
|
- **WHEN** запрос списка завершается известной ошибкой либо сервер сообщает завершённую неуспешную синхронизацию
|
||||||
|
- **THEN** клиент не выполняет дополнительные запросы, пока User не выберет действие повторной загрузки
|
||||||
|
|
||||||
|
#### Scenario: Token требует внимания
|
||||||
|
- **WHEN** клиент получает `INVALID_TBANK_TOKEN`
|
||||||
|
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
|
||||||
|
|
||||||
|
#### Scenario: Временная ошибка T-Bank
|
||||||
|
- **WHEN** клиент получает `TBANK_UNAVAILABLE` и сохранённого списка нет
|
||||||
|
- **THEN** клиент не показывает список и предлагает повторить загрузку либо открыть настройки T-Bank
|
||||||
|
|
||||||
|
#### Scenario: Неизвестная ошибка загрузки
|
||||||
|
- **WHEN** клиент получает ошибку без известного кода списка счетов и сохранённого списка нет
|
||||||
|
- **THEN** клиент не показывает список и предлагает только повторить загрузку
|
||||||
|
|
||||||
|
#### Scenario: Известная ошибка при сохранённом snapshot
|
||||||
|
- **WHEN** фоновое обновление списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE` через HTTP-ошибку либо `meta.sync.errorCode`
|
||||||
|
- **THEN** клиент сохраняет snapshot видимым, сообщает о временной недоступности и предлагает повторить загрузку либо открыть настройки T-Bank
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio
|
||||||
|
Frontend MUST сохранять Client Cached Snapshot на всех экранах с данными T-Bank: список Brokerage Accounts, Portfolio, операции, доходы, инструменты, котировки, свечи и выплаты. Снимок MUST сохраняться для того же пользователя, текущего подключения и набора данных, если ранее получен успешный результат с непустым `data`. Подтверждённые `data: []` или пустой полный Portfolio также MUST считаться успешным результатом. Начальная загрузка без такого ответа может показывать блокирующее
|
||||||
|
состояние загрузки. Client Cached Snapshot MUST оставаться видимым во время
|
||||||
|
фонового обновления и после его временной ошибки; он отличается от постоянно сохранённых серверных данных и MUST NOT изменять поле `Portfolio.isStale`. Клиент MUST независимо показывать свежесть серверного результата, полноту требуемых периодов, ход синхронизации и ошибку HTTP-refetch. Успешный HTTP-ответ с `meta.sync.state: failed` MUST NOT представляться успешным обновлением источника. Поля `meta.lastSuccessfulAt`, `meta.availability`, `meta.freshness`, `meta.sync` и при наличии `meta.coverage` MUST определять эти состояния по `tbank-data-sync`; клиент MUST NOT выводить их из возраста своего query-кэша. Ошибка ручного обновления ещё свежего источника MUST показываться отдельно от его свежести: клиент MUST NOT объявлять данные устаревшими до истечения серверного TTL либо изменять исходное успешное время.
|
||||||
|
|
||||||
|
Состояние `data: null` MUST показывать отсутствие загруженного результата и ход первичной загрузки либо её ошибку. Неполный период MUST явно отличаться от полного и MUST NOT представляться окончательным финансовым итогом или подтверждённым отсутствием операций. Неполное сравнение доходов MUST иметь собственное состояние и MUST NOT подменяться нулём. Статусы MUST быть доступны текстом, не только цветом; действия обновления и повтора MUST быть доступны с клавиатуры и сохранять читаемость на узком экране.
|
||||||
|
|
||||||
|
Действие «Обновить» MUST обходить серверную проверку давности для текущего экрана, его применённого периода и зависимостей, сохраняя доступные данные и фильтры. Обычное повторное получение HTTP-ответа MUST NOT считаться ручным принудительным обновлением. Наблюдение за уже запрошенной работой MUST прекращаться после её завершения, ошибки или ухода с экрана, MUST NOT запускать новую синхронизацию и MUST NOT создавать периодическое обновление источника. Уже начатая серверная работа MAY завершиться после ухода.
|
||||||
|
|
||||||
|
При подтверждённом `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, недоступности счёта либо изменении пользователя/подключения frontend MUST скрыть личный Snapshot прежней границы. Поздние ответы данных и статусов прежнего подключения MUST NOT попадать в новое состояние; обычная временная ошибка MUST NOT менять эту границу.
|
||||||
|
|
||||||
|
#### Scenario: Список Brokerage Accounts обновляется в фоне
|
||||||
|
- **WHEN** экран Accounts уже показывает Client Cached Snapshot и начинается
|
||||||
|
фоновое обновление того же списка
|
||||||
|
- **THEN** frontend оставляет список видимым и показывает только
|
||||||
|
неблокирующий индикатор обновления
|
||||||
|
|
||||||
|
#### Scenario: Обновление Portfolio завершается ошибкой после успешного ответа
|
||||||
|
- **WHEN** экран Brokerage Account уже показывает Client Cached Snapshot
|
||||||
|
Portfolio, а последующее обновление этого же Portfolio завершается временной ошибкой
|
||||||
|
- **THEN** frontend сохраняет Portfolio видимым, показывает локализованное
|
||||||
|
неблокирующее предупреждение о показе последних полученных данных и
|
||||||
|
предоставляет повторную попытку обновления
|
||||||
|
|
||||||
|
#### Scenario: Server-side fallback и client cached snapshot различаются
|
||||||
|
- **WHEN** backend возвращает Portfolio с `isStale` либо фоновое обновление
|
||||||
|
Client Cached Snapshot завершается временной ошибкой
|
||||||
|
- **THEN** frontend отображает соответствующее server-side либо client-side
|
||||||
|
предупреждение независимо, не представляя один вид состояния другим
|
||||||
|
|
||||||
|
#### Scenario: Серверная синхронизация не завершена после успешного HTTP-ответа
|
||||||
|
- **WHEN** экран получает `200` с сохранёнными данными и `meta.sync.state: running`
|
||||||
|
- **THEN** он показывает данные и прежнее успешное время вместе с состоянием обновления, не считая HTTP-успех завершением синхронизации
|
||||||
|
|
||||||
|
#### Scenario: Первичная загрузка без данных
|
||||||
|
- **WHEN** любой экран T-Bank получает `data: null` с отсутствующим либо частичным покрытием
|
||||||
|
- **THEN** он показывает загрузку либо её ошибку без вымышленных нулей, окончательных итогов и утверждения об отсутствии операций
|
||||||
|
|
||||||
|
#### Scenario: Ошибка синхронизации при успешном HTTP-ответе
|
||||||
|
- **WHEN** экран получает `200` с сохранёнными данными, `meta.sync.state: failed` и безопасным кодом ошибки
|
||||||
|
- **THEN** данные и время их успешного получения остаются видимыми с отдельным предупреждением об ошибке обновления и действием повтора
|
||||||
|
|
||||||
|
#### Scenario: Ошибка обновления не подменяет свежесть
|
||||||
|
- **WHEN** экран получает ещё свежие данные с `meta.freshness: fresh` и `meta.sync.state: failed` после ручного обновления
|
||||||
|
- **THEN** он показывает ошибку обновления и сохранённое успешное время без предупреждения об устаревших данных только из-за этой ошибки
|
||||||
|
|
||||||
|
#### Scenario: Ручное обновление сохраняет текущий контекст
|
||||||
|
- **WHEN** User нажимает «Обновить» на экране с выбранным счётом, периодом и фильтрами
|
||||||
|
- **THEN** клиент запрашивает принудительное обновление этого экрана и зависимостей, сохраняет контекст и доступные данные и не запрашивает обновление всей накопленной истории
|
||||||
|
|
||||||
|
#### Scenario: Недействительный token скрывает личный снимок
|
||||||
|
- **WHEN** источник подтвердил недействительность текущего token и клиент получает `INVALID_TBANK_TOKEN` при наличии сохранённого личного Snapshot
|
||||||
|
- **THEN** клиент скрывает личные данные и показывает существующее действие настройки подключения вместо маскировки отказа устаревшими данными
|
||||||
|
|
||||||
|
#### Scenario: Наблюдение не превращается в расписание
|
||||||
|
- **WHEN** запрошенная синхронизация завершилась, завершилась ошибкой либо User ушёл со страницы
|
||||||
|
- **THEN** клиент прекращает наблюдение и не начинает новые периодические синхронизации; уже начатая серверная работа может завершиться
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Получение Portfolio выбранного Brokerage Account
|
||||||
|
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts/:accountId/portfolio`. Она MUST возвращать нормализованный полный Portfolio Snapshot только для открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User. Ответ MUST иметь envelope `{ data, meta }` по контракту `tbank-data-sync`; сохранённый полный Snapshot в `data` MUST содержать `lastSuccessfulAt` в RFC 3339 UTC без миллисекунд, boolean `isStale`, `totalsByCurrency`, `positions` и `cashBalances`.
|
||||||
|
|
||||||
|
Portfolio Snapshot является результатом наблюдения Portfolio на конкретный момент. Система MUST сохранять последний полный Snapshot постоянно, без накопления исторических снимков стоимости. Это MUST NOT подтверждать доступность Brokerage Account вне текущего T-Bank connection. Проверка локальной сессии, текущего подключения и сохранённого состава доступных счетов MUST выполняться без синхронного запроса списка счетов T-Bank. При отсутствии сохранённого Snapshot ответ MUST иметь `200`, `data: null`, `meta.availability: missing` и состояние запрошенной загрузки; это MUST NOT обозначать пустой Portfolio.
|
||||||
|
|
||||||
|
#### Scenario: Получение текущего Portfolio
|
||||||
|
- **WHEN** User запрашивает Portfolio доступного Brokerage Account, а полный Snapshot текущего подключения успешно сохранён и ещё свеж
|
||||||
|
- **THEN** система отвечает `200` с полным нормализованным Portfolio Snapshot в `data` без ожидания T-Bank с `isStale: false` и временем успешного получения финансового снимка
|
||||||
|
|
||||||
|
#### Scenario: Недоступный Brokerage Account
|
||||||
|
- **WHEN** для текущего подключения подтверждено, что `accountId` не принадлежит User, закрыт, неподдерживаем или больше не доступен
|
||||||
|
- **THEN** система отвечает `404` без Portfolio Snapshot
|
||||||
|
|
||||||
|
#### Scenario: Сохранённый Portfolio читается после перезапуска
|
||||||
|
- **WHEN** backend перезапущен, текущий connection не изменился и полный Snapshot уже сохранён
|
||||||
|
- **THEN** запрос возвращает его из постоянного хранилища без ожидания T-Bank и отдельно сообщает свежесть и состояние фонового обновления
|
||||||
|
|
||||||
|
### Requirement: Свежесть и fallback полного Portfolio Snapshot
|
||||||
|
Система MUST постоянно сохранять последний полный успешный Portfolio Snapshot отдельно для User, Brokerage Account и текущего T-Bank connection. Обычный запрос MUST возвращать сохранённый Snapshot без ожидания T-Bank; при возрасте не менее одной минуты он MUST запросить фоновое обновление. Временная ошибка MUST NOT удалять Snapshot или ограничивать его выдачу прежним пределом пяти минут. `lastSuccessfulAt` MUST сохранять время успешного получения финансового снимка и совпадать с соответствующим временем в `meta`; `isStale` MUST отражать `meta.freshness: stale`, а не состояние клиентского кэша. Ручное действие «Обновить» MUST обходить проверку давности текущего Portfolio и его показанных зависимостей. Неуспешное ручное обновление MUST сохранять исходное `lastSuccessfulAt` и MUST NOT само по себе устанавливать `isStale: true`, пока минутный срок свежести сохранённого финансового снимка не истёк; ошибка MUST отображаться отдельно через `meta.sync`.
|
||||||
|
|
||||||
|
Metadata инструмента MUST NOT влиять на успешность или свежесть Portfolio Snapshot. Сохранённые metadata MUST использоваться без ожидания их обновления, а отсутствующие сведения MUST допускать существующий fallback идентификатора. Новые доступные metadata MUST дополнять представление, не меняя времени финансового снимка. Общие данные инструментов MUST следовать суточной свежести `tbank-data-sync`, без сохранения персональных позиций в общих данных.
|
||||||
|
|
||||||
|
После успешной замены или удаления T-Bank connection прежние Snapshot и поздние результаты его запросов MUST NOT обслуживать новое подключение. Известный отказ token текущего подключения MUST блокировать личное чтение с существующим `INVALID_TBANK_TOKEN`; он MUST NOT маскироваться сохранённым Snapshot.
|
||||||
|
|
||||||
|
#### Scenario: Свежий Snapshot
|
||||||
|
- **WHEN** User повторно запрашивает тот же Portfolio ранее чем через одну минуту после успешного получения
|
||||||
|
- **THEN** система возвращает свежий Snapshot без нового обращения к T-Bank
|
||||||
|
|
||||||
|
#### Scenario: Медленный lookup инструмента
|
||||||
|
- **WHEN** metadata инструмента ещё отсутствует и его lookup не завершился
|
||||||
|
- **THEN** система возвращает сохранённый финансовый Snapshot без ожидания lookup с доступным fallback, а после успешного lookup последующее чтение использует полученное name
|
||||||
|
|
||||||
|
#### Scenario: Fallback после временной upstream-ошибки
|
||||||
|
- **WHEN** обновление устаревшего Snapshot неуспешно из-за временной недоступности T-Bank, а последний полный успешный Snapshot получен более пяти минут назад
|
||||||
|
- **THEN** система отвечает `200` этим Snapshot с `isStale: true`, исходным `lastSuccessfulAt` и ошибкой обновления в `meta.sync`, без повторной синхронной попытки
|
||||||
|
|
||||||
|
#### Scenario: Ручное обновление ещё свежего Snapshot неуспешно
|
||||||
|
- **WHEN** User принудительно обновляет Snapshot, полученный менее минуты назад, и попытка завершается временной ошибкой до истечения его TTL
|
||||||
|
- **THEN** наблюдение возвращает прежний Snapshot с `isStale: false`, исходным `lastSuccessfulAt` и отдельной ошибкой синхронизации; ошибка не продлевает TTL и не делает данные устаревшими раньше срока
|
||||||
|
|
||||||
|
#### Scenario: Недействительный token не маскируется fallback-данными
|
||||||
|
- **WHEN** фоновое обновление подтвердило отклонение token текущего подключения как неаутентифицированного или без прав, после чего запрошен Portfolio
|
||||||
|
- **THEN** система не возвращает fallback Snapshot и отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN`
|
||||||
|
|
||||||
|
### Requirement: Состояния и ошибки Portfolio на клиенте
|
||||||
|
|
||||||
|
Клиент SHALL загружать Portfolio при открытии защищённого маршрута
|
||||||
|
`/accounts/:accountId`. Он MUST использовать успешный ответ в течение
|
||||||
|
30 секунд, без automatic retry неуспешного запроса, периодической синхронизации или refetch при возврате фокуса. Наблюдение за состоянием уже запрошенной работы MUST следовать `tbank-data-sync` и MUST NOT запускать новую работу.
|
||||||
|
При загрузке без ранее полученного успешного ответа либо при `data: null`
|
||||||
|
с состоянием загрузки для этого же Brokerage Account клиент MUST показывать структурное skeleton-состояние ожидаемого
|
||||||
|
содержимого, включая сводку, без вымышленных сумм. Область загрузки MUST
|
||||||
|
иметь доступное обозначение занятости; skeleton MUST NOT быть интерактивным
|
||||||
|
или получать клавиатурный фокус.
|
||||||
|
|
||||||
|
Истечение срока свежести MUST NOT само по себе скрывать уже доступный
|
||||||
|
Client Cached Snapshot. Во время фонового обновления и после его временной ошибки
|
||||||
|
клиент MUST сохранять ранее полученные данные того же Portfolio, включая
|
||||||
|
валютные итоги и исходное `lastSuccessfulAt`. При обновлении клиент MUST
|
||||||
|
показывать компактную неблокирующую индикацию без замены данных skeleton.
|
||||||
|
При ошибке обновления он MUST показывать локализованное неблокирующее
|
||||||
|
предупреждение о последних полученных данных и предоставлять действие
|
||||||
|
повторной попытки. Сохранённые данные MUST NOT считаться новым успешным
|
||||||
|
снимком или подтверждением текущей доступности Brokerage Account.
|
||||||
|
|
||||||
|
Клиент MUST показать штатное empty-состояние только при `meta.availability: complete`, непустом `data` и пустых `positions`
|
||||||
|
и `cashBalances`, без нулевых валютных карточек. Для показываемого состава
|
||||||
|
Portfolio клиент MUST сохранять время успешного получения снимка
|
||||||
|
`lastSuccessfulAt`, форматируемое в UTC. При `isStale: true` клиент MUST
|
||||||
|
показывать Snapshot, его исходное время и нейтральное предупреждение
|
||||||
|
об устаревших данных. Ошибка синхронизации MUST иметь отдельное безопасное объяснение. На экране MUST быть доступно действие «Обновить», обновляющее текущий экран и его зависимости независимо от давности. Server-side stale и ошибка фонового
|
||||||
|
обновления Client Cached Snapshot MUST оставаться раздельными состояниями:
|
||||||
|
ошибка клиента MUST NOT менять или подменять `isStale`, а оба предупреждения
|
||||||
|
и действие повтора клиентского обновления MUST оставаться доступными при
|
||||||
|
одновременном наличии этих состояний.
|
||||||
|
|
||||||
|
При ответе `404` для этого же Portfolio клиент MUST показать состояние недоступного счёта
|
||||||
|
с действием возврата к списку счетов без действия повтора. При `TBANK_NOT_CONNECTED` или `INVALID_TBANK_TOKEN` клиент MUST скрыть личные данные даже при наличии Snapshot. При отсутствии Snapshot и `TBANK_UNAVAILABLE`
|
||||||
|
или иной ошибке клиент MUST показать безопасное error-состояние с доступными
|
||||||
|
для ошибки действиями; действие повтора разрешено только для временной
|
||||||
|
и неизвестной ошибки.
|
||||||
|
|
||||||
|
#### Scenario: Показ устаревшего Portfolio
|
||||||
|
|
||||||
|
- **WHEN** клиент получает успешный ответ с `isStale: true`
|
||||||
|
- **THEN** клиент показывает позиции, остатки, валютные итоги, время
|
||||||
|
последнего успешного снимка, нейтральное предупреждение об устаревании
|
||||||
|
и доступное действие «Обновить»
|
||||||
|
|
||||||
|
#### Scenario: Недоступный счёт на клиенте
|
||||||
|
|
||||||
|
- **WHEN** клиент получает `404` для текущего Portfolio
|
||||||
|
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку
|
||||||
|
Brokerage Account без действия повтора
|
||||||
|
|
||||||
|
#### Scenario: Первичная загрузка Portfolio
|
||||||
|
|
||||||
|
- **WHEN** клиент ожидает Portfolio этого Brokerage Account без ранее
|
||||||
|
полученного успешного ответа либо получает `data: null` с активной загрузкой
|
||||||
|
- **THEN** он показывает структурный skeleton, включая место сводки,
|
||||||
|
с доступным обозначением загрузки, без вымышленных или нулевых сумм
|
||||||
|
и без фокусируемых placeholders
|
||||||
|
|
||||||
|
#### Scenario: Обновление после истечения срока свежести
|
||||||
|
|
||||||
|
- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое
|
||||||
|
обновление данных того же Brokerage Account
|
||||||
|
- **THEN** клиент сохраняет прежние валютные итоги и время их успешного
|
||||||
|
получения, показывает неблокирующую индикацию обновления и не заменяет
|
||||||
|
доступный состав и сводку skeleton-состоянием
|
||||||
|
|
||||||
|
#### Scenario: Ошибка обновления после успешного ответа
|
||||||
|
|
||||||
|
- **WHEN** клиент уже показывает Client Cached Snapshot Portfolio,
|
||||||
|
а обновление этих же данных завершается временной ошибкой
|
||||||
|
- **THEN** клиент сохраняет данные, суммы и время прежнего снимка,
|
||||||
|
показывает неблокирующее предупреждение и действие повтора;
|
||||||
|
ошибка не делает сохранённый снимок свежим и не изменяет `isStale`
|
||||||
|
|
||||||
|
#### Scenario: Ошибка клиентского обновления при server-side stale
|
||||||
|
|
||||||
|
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое
|
||||||
|
обновление этого же Portfolio завершается временной ошибкой
|
||||||
|
- **THEN** прежние суммы и время остаются видимыми одновременно
|
||||||
|
с нейтральным server-side предупреждением, отдельным предупреждением
|
||||||
|
об ошибке клиентского обновления и его действием повтора
|
||||||
|
|
||||||
|
#### Scenario: Повторное обновление успешно
|
||||||
|
|
||||||
|
- **WHEN** после ошибки фонового обновления User запускает повторную
|
||||||
|
попытку и получает успешный Portfolio Snapshot
|
||||||
|
- **THEN** до ответа прежние суммы и время остаются видимыми, затем
|
||||||
|
клиент показывает значения и `lastSuccessfulAt` успешного ответа,
|
||||||
|
убирает предупреждение об ошибке клиентского обновления и показывает
|
||||||
|
server-side предупреждение только в соответствии с новым `isStale`
|
||||||
|
|
||||||
|
#### Scenario: Штатный пустой Portfolio на клиенте
|
||||||
|
|
||||||
|
- **WHEN** успешно полученный ответ имеет `meta.availability: complete` и Portfolio в `data` содержит пустые `positions`
|
||||||
|
и `cashBalances`
|
||||||
|
- **THEN** клиент показывает существующее empty-состояние без валютных
|
||||||
|
карточек с вымышленными нулевыми итогами
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
Определяет постоянное хранение данных T-Bank, локальное чтение и синхронизацию
|
||||||
|
по запросу с проверяемыми полнотой, свежестью и изоляцией подключения.
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Все экраны читают сохранённые данные без ожидания T-Bank
|
||||||
|
|
||||||
|
Система SHALL постоянно сохранять уже загруженные счета, последний полный
|
||||||
|
портфель, исполненные операции, данные инструментов, текущие котировки,
|
||||||
|
дневные свечи и календари выплат. Доходы, личные позиции и прогнозные выплаты
|
||||||
|
SHALL рассчитываться по сохранённым источникам. Все соответствующие экраны,
|
||||||
|
включая режим «Все счета», поиск, сведения о компании и зависимости котировок,
|
||||||
|
MUST читать доступные данные без ожидания ответа T-Bank. Перезапуск backend
|
||||||
|
MUST NOT требовать повторной загрузки сохранённых полных интервалов.
|
||||||
|
|
||||||
|
#### Scenario: Брокер медленно отвечает при заполненном хранилище
|
||||||
|
- **WHEN** User открывает любой экран с сохранёнными данными текущего подключения, а T-Bank задерживает ответ
|
||||||
|
- **THEN** обычный серверный ответ содержит сохранённые данные и их состояние, не ожидая T-Bank, включая проверку доступных счетов
|
||||||
|
|
||||||
|
#### Scenario: Перезапуск сохраняет историю
|
||||||
|
- **WHEN** backend перезапущен и User запрашивает ранее полностью загруженный прошлый период
|
||||||
|
- **THEN** система возвращает его из постоянного хранилища без повторного получения операций или завершённых свечей этого периода
|
||||||
|
|
||||||
|
#### Scenario: Данные ещё не загружены
|
||||||
|
- **WHEN** User впервые запрашивает отсутствующие данные
|
||||||
|
- **THEN** ответ различает отсутствие загруженного результата и подтверждённый пустой результат, сообщает о запрошенной загрузке и не ожидает её завершения
|
||||||
|
|
||||||
|
### Requirement: Обновление запускается только запросом с учётом свежести
|
||||||
|
|
||||||
|
Система MUST запускать синхронизацию только запросом экрана либо ручным
|
||||||
|
обновлением. Обычный запрос SHALL планировать только отсутствующие зависимости,
|
||||||
|
пропуски и данные с истекшим сроком свежести: портфель и текущие цены —
|
||||||
|
60 секунд; операции текущего месяца и доступные счета — 300 секунд;
|
||||||
|
справочник инструментов, включая сведения о компании, и календари выплат —
|
||||||
|
24 часа; незавершённая дневная свеча — 60 секунд. Свежесть MUST отсчитываться
|
||||||
|
от последнего успешного получения соответствующего источника, а не чтения
|
||||||
|
ответа или расчёта производного представления. Полнота и свежесть MUST
|
||||||
|
оставаться независимыми признаками. Периодические запуски MUST NOT выполняться.
|
||||||
|
Для текущего изменяемого периода полнота SHALL относиться к состоянию на
|
||||||
|
последний успешный `asOf`: рост `to=now` внутри TTL не является новым
|
||||||
|
пропуском, требующим RPC. Настоящие пропуски до `asOf` и незавершённый хвост
|
||||||
|
при переходе месяца MUST NOT маскироваться этим правилом.
|
||||||
|
|
||||||
|
#### Scenario: Данные ещё свежие
|
||||||
|
- **WHEN** экран повторно запрошен до истечения сроков всех его полных зависимостей
|
||||||
|
- **THEN** запрос не создаёт новых обращений к T-Bank
|
||||||
|
|
||||||
|
#### Scenario: Текущий период запрошен через секунду
|
||||||
|
- **WHEN** текущий месяц полностью загружен на `asOf` и повторный запрос через секунду имеет более поздний `to=now`
|
||||||
|
- **THEN** система возвращает сохранённый результат с исходным `asOf` без нового RPC до истечения пяти минут, не утверждая наблюдение новых операций после `asOf`
|
||||||
|
|
||||||
|
#### Scenario: Истекла только свежесть портфеля
|
||||||
|
- **WHEN** портфель получен 60 секунд назад, а полные счета и metadata ещё свежие
|
||||||
|
- **THEN** обычный запрос инициирует обновление портфеля и только иных необходимых устаревших зависимостей, сохраняя прежний ответ доступным
|
||||||
|
|
||||||
|
#### Scenario: Пользователь не обращается к системе
|
||||||
|
- **WHEN** запрошенная работа завершилась и сроки свежести истекают без новых запросов
|
||||||
|
- **THEN** никакая синхронизация не запускается, в том числе при запуске backend
|
||||||
|
|
||||||
|
### Requirement: Ручное обновление ограничено текущим экраном
|
||||||
|
|
||||||
|
Действие «Обновить» SHALL обходить давность данных и запрашивать обновление
|
||||||
|
экрана с его зависимостями: текущего счёта либо доступных счетов в режиме
|
||||||
|
«Все счета», применённого периода и сравнений доходов, либо инструмента
|
||||||
|
с показанными свечами, выплатами и личной историей. Сервер MUST определять
|
||||||
|
разрешённые зависимости сам, сохраняя проверку входа и доступа. Обновление
|
||||||
|
MUST NOT перечитывать всю накопленную историю других периодов и инструментов.
|
||||||
|
Подтверждение приёма работы MUST NOT означать успешное обновление данных.
|
||||||
|
|
||||||
|
#### Scenario: Обновление старого месяца
|
||||||
|
- **WHEN** User нажимает «Обновить» на журнале за полностью загруженный прошлый месяц
|
||||||
|
- **THEN** перечитывается показанный период счёта и необходимые зависимости без принудительного перечитывания остальных полных месяцев
|
||||||
|
|
||||||
|
#### Scenario: Обновление доходов
|
||||||
|
- **WHEN** User обновляет аналитику за выбранный период
|
||||||
|
- **THEN** обновление охватывает выбранный и требуемые периоды сравнения, а результаты разных периодов не подменяют друг друга
|
||||||
|
|
||||||
|
#### Scenario: Обновление инструмента
|
||||||
|
- **WHEN** User обновляет инструмент с выбранным диапазоном графика и показанной личной историей
|
||||||
|
- **THEN** обновляются зависимости этого инструмента, диапазон свечей и показанный период личной истории разрешённых счетов без обновления всего каталога
|
||||||
|
|
||||||
|
### Requirement: Первая загрузка приоритизирует экран и продолжает историю
|
||||||
|
|
||||||
|
Система SHALL сначала загружать данные нужного экрану периода, включая
|
||||||
|
периоды сравнения доходов, затем всю доступную историю запрошенного счёта.
|
||||||
|
Для режима «Все счета» это применяется к каждому доступному счёту.
|
||||||
|
В рамках уже запрошенной работы SHALL заранее загружаться доступная рыночная
|
||||||
|
история бумаг текущего портфеля; для остальных инструментов — при первом
|
||||||
|
открытии. Новый запрос нужного экрану пропуска MUST получать приоритет перед
|
||||||
|
ещё не начатой догрузкой старой истории. Начатая работа MAY завершиться
|
||||||
|
после ухода со страницы. Недоказанная нижняя граница доступной истории
|
||||||
|
MUST NOT выдаваться за подтверждение её полной загрузки.
|
||||||
|
|
||||||
|
#### Scenario: Сначала сравнения доходов
|
||||||
|
- **WHEN** история счёта отсутствует и User открывает доходы за месяц
|
||||||
|
- **THEN** выбранный месяц и доступные обязательные периоды сравнения загружаются перед остальной историей; состояние старой истории не задерживает уже полный результат экрана
|
||||||
|
|
||||||
|
#### Scenario: Уход со страницы
|
||||||
|
- **WHEN** User уходит после начала догрузки истории
|
||||||
|
- **THEN** запрошенная работа может закончиться и сохранить результат, но не создаёт расписание последующих обновлений
|
||||||
|
|
||||||
|
#### Scenario: Инструмент отсутствует в портфеле
|
||||||
|
- **WHEN** User впервые открывает такой инструмент
|
||||||
|
- **THEN** система запрашивает и сохраняет его историю, не предзагружая весь каталог
|
||||||
|
|
||||||
|
### Requirement: Полнота подтверждается успешным покрытием интервалов
|
||||||
|
|
||||||
|
Система MUST считать историю счёта полной за период только после успешного
|
||||||
|
получения всех доступных исполненных операций всего этого интервала на момент
|
||||||
|
сверки. Полный период MAY быть пустым. Фильтр категорий, инструмента, размер
|
||||||
|
страницы, отсутствие записей, максимальная дата операции и наличие курсора
|
||||||
|
MUST NOT служить доказательством полноты. Производное представление «Все
|
||||||
|
счета» MUST учитывать покрытие каждого входящего счёта. Полные прошлые месяцы
|
||||||
|
SHALL сохраняться без автоматической повторной сверки; пропуски SHALL
|
||||||
|
догружаться при запросе. Смена месяца MUST NOT автоматически закрывать
|
||||||
|
незагруженный хвост прошлого месяца. Существующая семантика входных дат
|
||||||
|
журнала и доходов MUST сохраняться без пропусков и двойного учёта на границах.
|
||||||
|
|
||||||
|
#### Scenario: Пустой полный интервал
|
||||||
|
- **WHEN** все страницы интервала успешно получены и операций нет
|
||||||
|
- **THEN** период обозначается полным и пустым; последующее обычное чтение прошлого месяца не обращается к брокеру за этим интервалом
|
||||||
|
|
||||||
|
#### Scenario: Сбой промежуточной страницы
|
||||||
|
- **WHEN** одна из нескольких страниц интервала недоступна
|
||||||
|
- **THEN** интервал остаётся неполным, сохранённый результат не удаляется, частичные записи не выдаются за полный период
|
||||||
|
|
||||||
|
#### Scenario: Граница месяца
|
||||||
|
- **WHEN** последний успешный запрос прошлого месяца закончился до его конца
|
||||||
|
- **THEN** первый подходящий запрос нового месяца догружает недостающий хвост; смена даты сама не подтверждает полноту прошлого месяца
|
||||||
|
|
||||||
|
#### Scenario: Неполон один из счетов
|
||||||
|
- **WHEN** все счета, кроме одного, полностью покрывают период общего дохода
|
||||||
|
- **THEN** общий период не считается полным, а отсутствующий вклад счёта не заменяется нулём
|
||||||
|
|
||||||
|
### Requirement: Сверка операций сохраняет идентичность и прежний результат
|
||||||
|
|
||||||
|
Система SHALL использовать стабильный ID брокерской операции в границах
|
||||||
|
пользователя, подключения и счёта как принятое допущение проекта, а не гарантию
|
||||||
|
API. Повторное получение ID MUST обновлять поля той же операции без дубля
|
||||||
|
и двойного финансового учёта. Ручная сверка MUST согласовывать состав и поля
|
||||||
|
исполненных операций показанного интервала: отсутствующие в полном новом
|
||||||
|
ответе записи могут быть удалены только вместе с успешным завершением всех
|
||||||
|
страниц этого интервала. Частичный либо ошибочный новый ответ MUST NOT
|
||||||
|
изменять ранее опубликованный полный финансовый результат и время его
|
||||||
|
успешной сверки. Автоматическое обнаружение изменений полной старой истории
|
||||||
|
и смены ID не требуется.
|
||||||
|
|
||||||
|
#### Scenario: Поля операции исправлены
|
||||||
|
- **WHEN** успешная повторная сверка получает прежний ID с другой суммой или статусом, меняющим вхождение в исполненную историю
|
||||||
|
- **THEN** итоговый набор отражает новый полный ответ без дублирования ID, а доходы пересчитываются из согласованного набора
|
||||||
|
|
||||||
|
#### Scenario: Операция исчезла при ручной сверке
|
||||||
|
- **WHEN** все страницы перечитанного интервала успешно завершились и прежняя операция отсутствует
|
||||||
|
- **THEN** операция исключается из опубликованного набора этого интервала и производных финансовых результатов
|
||||||
|
|
||||||
|
#### Scenario: Перезагрузка полного периода оборвалась
|
||||||
|
- **WHEN** после нескольких страниц ручной сверки возникает ошибка
|
||||||
|
- **THEN** прежний полный набор, его суммы, полнота и время успешной сверки остаются доступными вместе с ошибкой обновления
|
||||||
|
|
||||||
|
#### Scenario: Сбой во втором месяце перечитываемого квартала
|
||||||
|
- **WHEN** ручная сверка полного квартала получила первый месяц, но оборвалась на странице второго
|
||||||
|
- **THEN** весь прежний результат показанного квартала, включая режим «Все счета», остаётся опубликованным; успешный новый первый месяц не смешивается с прежними остальными месяцами
|
||||||
|
|
||||||
|
### Requirement: Прогресс переживает перезапуск и одновременные запросы
|
||||||
|
|
||||||
|
Система MUST сохранять завершённые интервалы, незавершённую работу и её
|
||||||
|
прогресс. После перезапуска следующий подходящий запрос экрана или ручное
|
||||||
|
обновление SHALL возобновлять запрошенную работу, повторяя незавершённый
|
||||||
|
интервал безопасно и без повторной загрузки полных старых интервалов.
|
||||||
|
Одновременные запросы одной границы данных SHALL объединять одинаковую
|
||||||
|
работу, а пересекающиеся интервалы SHALL использовать общую загрузку
|
||||||
|
пересечения. Ошибка MUST оставлять возможность следующего явного запроса,
|
||||||
|
не создавая бесконечный цикл повторов. Поздняя публикация устаревшей попытки
|
||||||
|
MUST NOT перезаписывать результат новой попытки.
|
||||||
|
|
||||||
|
#### Scenario: Двадцать одновременных запросов
|
||||||
|
- **WHEN** двадцать запросов требуют один отсутствующий интервал одного счёта
|
||||||
|
- **THEN** выполняется один проход страниц этого интервала с разрешёнными транспортными повторами, а все запросы видят общее состояние работы
|
||||||
|
|
||||||
|
#### Scenario: Перезапуск между страницами
|
||||||
|
- **WHEN** процесс остановлен после сохранения прогресса, но до полного завершения интервала
|
||||||
|
- **THEN** до следующего запроса работа не возобновляется; после запроса интервал перечитывается без дублей и без ложного признака полноты
|
||||||
|
|
||||||
|
#### Scenario: Ошибка завершила попытку
|
||||||
|
- **WHEN** запрошенная синхронизация исчерпала допустимые транспортные повторы
|
||||||
|
- **THEN** она показывает безопасную ошибку и прекращает попытки до следующего запроса; наблюдение её состояния не запускает её заново
|
||||||
|
|
||||||
|
### Requirement: Общие данные инструментов отделены от личных
|
||||||
|
|
||||||
|
Система SHALL совместно использовать неперсональные metadata, котировки,
|
||||||
|
свечи и календари одного инструмента между пользователями. Общие записи
|
||||||
|
MUST NOT содержать аккаунты, личные позиции, остатки, операции, оценки выплат
|
||||||
|
пользователя, token либо персональные ошибки. Личные данные и вычисления
|
||||||
|
MUST оставаться ограничены пользователем, доступным счётом и текущим
|
||||||
|
подключением. Успешный общий результат MAY переиспользоваться между счетами
|
||||||
|
без повторного получения того же источника.
|
||||||
|
|
||||||
|
#### Scenario: Два пользователя открывают инструмент
|
||||||
|
- **WHEN** оба имеют право пользоваться интеграцией и общие данные инструмента уже загружены
|
||||||
|
- **THEN** они используют одни общие данные, но видят только собственные позиции, операции и оценки выплат
|
||||||
|
|
||||||
|
#### Scenario: Ошибка token одного пользователя
|
||||||
|
- **WHEN** источник отклоняет token одного пользователя при загрузке общего инструмента
|
||||||
|
- **THEN** личная ошибка не становится состоянием инструмента для другого пользователя и не удаляет ранее сохранённые общие данные
|
||||||
|
|
||||||
|
### Requirement: Локальная проверка доступа сохраняет границу подключения
|
||||||
|
|
||||||
|
Каждое чтение и действие синхронизации MUST проверять сессию, текущее
|
||||||
|
подключение и локально подтверждённую принадлежность счёта этому подключению.
|
||||||
|
Отсутствие ранее подтверждённого списка SHALL означать загрузку доступа,
|
||||||
|
а не доступ к произвольному accountId или доказанный пустой список.
|
||||||
|
Устаревший подтверждённый список MAY использоваться до фоновой сверки;
|
||||||
|
успешная сверка MUST отзывать доступ к исчезнувшим, закрытым или неподдержанным
|
||||||
|
счетам. Известный отказ token MUST сохранять существующую ошибку
|
||||||
|
`INVALID_TBANK_TOKEN`, отсутствие подключения — `TBANK_NOT_CONNECTED`,
|
||||||
|
недоступный счёт — `404`. Хранение не вводит архив без подключения.
|
||||||
|
Успешная замена или удаление подключения MUST блокировать выдачу прежних
|
||||||
|
личных данных и публикацию любых поздних результатов прежней работы.
|
||||||
|
Неуспешная замена MUST сохранять действующую границу.
|
||||||
|
|
||||||
|
#### Scenario: Подтверждённый счёт проверяется локально
|
||||||
|
- **WHEN** User читает заполненные данные доступного счёта текущего подключения
|
||||||
|
- **THEN** проверка принадлежности не обращается синхронно к T-Bank и не использует список другого пользователя или подключения
|
||||||
|
|
||||||
|
#### Scenario: Прямой переход до загрузки счетов
|
||||||
|
- **WHEN** список текущего подключения ещё неизвестен и User запрашивает конкретный accountId
|
||||||
|
- **THEN** система сообщает о загрузке проверки доступа, не выдаёт личных данных этого ID и загружает доступные счета
|
||||||
|
|
||||||
|
#### Scenario: Доступ к счёту отозван
|
||||||
|
- **WHEN** успешная фоновая сверка больше не подтверждает доступ к ранее видимому счёту
|
||||||
|
- **THEN** последующие запросы этого счёта возвращают `404`, а клиент прекращает показывать его как доступный
|
||||||
|
|
||||||
|
#### Scenario: Поздний результат прежнего подключения
|
||||||
|
- **WHEN** старый запрос заканчивается после успешной замены или удаления подключения
|
||||||
|
- **THEN** его данные, прогресс, ошибки и признаки свежести не публикуются и не становятся результатом нового подключения
|
||||||
|
|
||||||
|
### Requirement: Завершённость свечей подтверждается источником
|
||||||
|
|
||||||
|
Система SHALL сохранять дневные свечи по инструменту и времени источника.
|
||||||
|
Завершённые свечи MUST NOT перечитываться обычным запросом при полном
|
||||||
|
покрытии диапазона. Незавершённая свеча SHALL обновляться при запросе после
|
||||||
|
60 секунд до явного подтверждения завершённости источником, даже если её
|
||||||
|
календарный день прошёл. Успешное пустое окно MAY подтверждать отсутствие
|
||||||
|
свечей только в завершённом диапазоне и MUST NOT создавать синтетические
|
||||||
|
свечи. Открытое окно текущего дня, в том числе пока пустое, SHALL оставаться
|
||||||
|
изменяемым с минутным сроком свежести. Ручное обновление SHALL
|
||||||
|
перечитывать показанный диапазон, сохраняя прежние данные при ошибке.
|
||||||
|
|
||||||
|
#### Scenario: Наступил новый день
|
||||||
|
- **WHEN** сохранённая свеча предыдущего дня не подтверждена как завершённая источником
|
||||||
|
- **THEN** новый день не делает её окончательной, и подходящий запрос продолжает обновлять её с минутным сроком свежести
|
||||||
|
|
||||||
|
#### Scenario: Выходной без свечей
|
||||||
|
- **WHEN** источник успешно подтвердил пустое окно без торговых свечей
|
||||||
|
- **THEN** окно сохраняется как проверенное без выдуманных цен и повторной обычной загрузки завершённого диапазона
|
||||||
|
|
||||||
|
#### Scenario: До начала торгов свеча отсутствует
|
||||||
|
- **WHEN** запрос текущего дня вернул пустое окно, а после начала торгов User снова открывает график по истечении минуты
|
||||||
|
- **THEN** система запрашивает изменяемое окно и сохраняет появившуюся свечу; утренний ответ не закрепляет окончательное отсутствие свечи
|
||||||
|
|
||||||
|
### Requirement: Интерфейс различает свежесть полноту и состояние работы
|
||||||
|
|
||||||
|
На всех экранах T-Bank клиент MUST показывать время последнего успешного
|
||||||
|
получения, свежесть, загрузку и безопасную ошибку обновления применимых
|
||||||
|
данных. Полнота MUST относиться к показанному периоду и его зависимостям;
|
||||||
|
общий прогресс истории счёта MUST NOT подменять полноту экрана. Отсутствующие
|
||||||
|
данные SHALL иметь состояние первичной загрузки; доступные данные SHALL
|
||||||
|
оставаться видимыми во время обновления. Неполные totals/comparisons MUST
|
||||||
|
NOT показываться как окончательные или как ноль; неполный пустой журнал
|
||||||
|
MUST NOT сообщать, что операций нет. Наблюдение запрошенной работы MUST
|
||||||
|
прекращаться после завершения либо ухода и MUST NOT запускать синхронизацию.
|
||||||
|
Известные ошибки доступа MUST NOT маскироваться сохранённым результатом.
|
||||||
|
|
||||||
|
#### Scenario: Недавний период готов раньше всей истории
|
||||||
|
- **WHEN** показанный период и его зависимости полны, но старая история ещё загружается
|
||||||
|
- **THEN** пользователь видит готовый результат периода и отдельное состояние догрузки истории
|
||||||
|
|
||||||
|
#### Scenario: Полнота изменилась без новых записей
|
||||||
|
- **WHEN** источник подтвердил пустой период экрана, а остальная история ещё загружается
|
||||||
|
- **THEN** наблюдающий клиент получает изменение полноты и показывает подтверждённый пустой результат, не ожидая конца всей истории
|
||||||
|
|
||||||
|
#### Scenario: Ответы наблюдения завершились в обратном порядке
|
||||||
|
- **WHEN** поздний ответ чтения содержит прежние данные или доступ, а клиент уже принял более новое состояние этого ресурса либо отзыв доступа
|
||||||
|
- **THEN** прежний ответ не откатывает показанные данные и не восстанавливает недоступный личный результат
|
||||||
|
|
||||||
|
#### Scenario: Источник временно недоступен
|
||||||
|
- **WHEN** обновление сохранённого полного результата завершается временной ошибкой
|
||||||
|
- **THEN** прежние данные и время сохраняются, ошибка видима и доступно ручное повторное обновление; ошибка не обновляет время успешного получения и не считается успешной сверкой
|
||||||
|
|
||||||
|
#### Scenario: Неполное сравнение дохода
|
||||||
|
- **WHEN** выбранный период готов, а необходимый период сравнения ещё не загружен
|
||||||
|
- **THEN** сравнение обозначается как незавершённое без нулевой базы или окончательной разницы
|
||||||
|
|
||||||
|
### Requirement: Быстродействие и сокращение запросов проверяются измерениями
|
||||||
|
|
||||||
|
Для обычных чтений заполненной БД система SHALL иметь целевой бюджет
|
||||||
|
серверного ответа до 500 мс в зафиксированном профиле нагрузки. Выполнение
|
||||||
|
цели MUST подтверждаться измерениями, а не предполагаться из наличия БД.
|
||||||
|
Свежие полные зависимости MUST давать ноль новых RPC T-Bank; двадцать
|
||||||
|
одновременных запросов одного устаревшего набора MUST создавать не более
|
||||||
|
одной логической загрузки каждой зависимости. Полные старые операции и
|
||||||
|
завершённые свечи MUST давать ноль повторных RPC для своих интервалов при
|
||||||
|
обычном чтении, включая чтение после перезапуска.
|
||||||
|
|
||||||
|
#### Scenario: Измеряется заполненное хранилище
|
||||||
|
- **WHEN** проверяется согласованный набор обычных запросов при задержанном или недоступном T-Bank
|
||||||
|
- **THEN** фиксируются распределение серверных задержек, число RPC и параметры нагрузки; цель 500 мс не объявляется достигнутой без этих результатов
|
||||||
|
|
||||||
|
#### Scenario: Повторные открытия не перечитывают историю
|
||||||
|
- **WHEN** один и тот же полный прошлый период читается повторно без ручного обновления
|
||||||
|
- **THEN** сохранённые данные выдаются без RPC этого интервала, независимо от числа страниц клиентской выдачи
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user