# QA Analysis Issue: WealthLedger/main#54 ## Verification Scope **In:** - все Requirements и Scenarios `portfolio/investment-performance` и `instruments/market-index-history`; - контракт `investment-performance.openapi.yaml`; - семантика ошибок System Analysis. **Out:** - TWR, периоды, валютная конвертация; - корректность самой синхронизации T-Bank (покрыта её спецификациями); - точность значений MCFTRR у источника. ## Coverage Matrix | Requirement / Scenario | Проверка на уровне системы | | --- | --- | | Результат счёта: рублёвый счёт с известным началом | API: ответ 200 со всеми полями; значения совпадают с эталоном (ниже) | | Результат счёта: счёт недоступен через подключение | API: чужой `accountId` → 404, в теле нет финансовых полей | | Внешние потоки: пополнения, вывод, дивиденды | Эталонный счёт A: `netInvested = 130000`, `profit = 10000` | | Внешние потоки: кандидат на перевод между счетами в `all` | Счета B и C с равными суммами в пределах 5 минут исключаются с `UNCONFIRMED_TRANSFER`, а не объявляются внутренней парой | | Внешние потоки: перевод на счёт вне результата | Счёт B исключён (USD), его пополнение от C → вывод у C в `all` | | Доходность: период больше года | Счёт D: 100 000 → 121 000 за 730 дн. → `return.value = 0.1`, `basis = annual` | | Доходность: меньше года | Счёт E, 200 дн.: `basis = period`; в UI нет слова «годовых» ни в одном значении | | Доходность: нет решения | Счёт F (текущая стоимость 0 после пополнений, без выводов): `NO_SOLUTION`, `profit` доступен, `outperformance = null` | | Доходность: несколько решений | Набор потоков с двумя корнями XIRR: `AMBIGUOUS_RETURN`, прибыль доступна, `outperformance = null` | | Индекс: пополнение и рост индекса | Подменённая история (1 000 → 1 100) или ручной расчёт по реальным закрытиям: `benchmark.value = 110000`, `outperformance.money = 5000` | | Индекс: поток в выходной | Пополнение в субботу → используется закрытие пятницы (проверяется по `series` в день потока) | | Индекс: отставание | Счёт со стоимостью ниже теневой: отрицательные `percentagePoints` и `money`, в UI «Отставание от рынка» | | Справочный индекс недоступен | IMOEX нет, MCFTRR есть: `reference.unavailableReason` задан, `benchmark` и `outperformance` есть, в UI нет пунктирной линии | | Недоступно: неизвестная дата открытия | Счёт с `historyBoundary = unknown`: `data: null`, `HISTORY_START_UNKNOWN`; в UI «Доходность недоступна», нет нулей | | USD-поток игнорируется | Счёт с пополнением в USD: результат считается по рублёвым потокам, `excludedAccounts` пуст | | Недоступно: даты оценок в `all` различаются | Последние оценки A и B в разные московские дни → `data: null`, `ACCOUNT_VALUATION_DATES_DIFFER`; стоимость не складывается | | Недоступно: индекс недоступен | MOEX заблокирован и истории нет: `profit` есть, `benchmark.unavailableReason = INDEX_HISTORY_UNAVAILABLE`, `series = []` | | Недоступно: остальные причины | Перевод бумаг без пары → `UNVALUED_SECURITIES_TRANSFER`; позиция без цены → `PORTFOLIO_VALUATION_INCOMPLETE`; незавершённая синхронизация → `OPERATIONS_HISTORY_INCOMPLETE` | | Вкладка: обгоняю, меньше года, неполный, недоступный, методика | UI по соответствующим данным; формулировки и тоны как в UI/UX Package; «Как считали» содержит все пункты спецификации | | Вкладка: загрузка, ошибка не ломает другие вкладки | Задержка ответа → skeleton без чисел; 500 на `/performance` → ошибка только во вкладке, остальные вкладки работают | | График: наведение, отсутствие прошлой стоимости | Наведение и фокус показывают дату и 3 значения; есть пояснение под графиком; `aria-label` с итогами | | История индексов: первое получение, выходной | После первого чтения есть закрытия торговых дней без дубликатов; повторное чтение не меняет число записей; субботних дат нет | | История индексов: источник недоступен, история есть / нет | Блокировка `iss.moex.com` после первичной загрузки → результат полный; на чистой БД → `INDEX_HISTORY_UNAVAILABLE`, остальные вкладки работают | **Эталон.** Для счетов A–F и реальных данных пользователя XIRR пересчитывается независимо (LibreOffice `XIRR` по тем же потокам и `currentValue` в дату `to`). Допуск — 0,1 п.п., по Success Criteria. ## Cross-system Verification - **Контракт.** Ответы обоих маршрутов валидируются по `investment-performance.openapi.yaml`: required, nullable, enum, шаблон `Decimal`. Неизвестное значение `UnavailableReason`, подставленное в ответ через прокси или мок, UI показывает общей недоступностью без падения. - **Ошибки доступа.** 401 без сессии; 503 `TBANK_NOT_CONNECTED` без подключения; 404 для счёта не из подключения. `all` без подключения тоже 503. - **Таймаут MOEX.** Ответ ISS задерживается больше 10 с: результат приходит с сохранённой историей или с `INDEX_HISTORY_UNAVAILABLE`. Время ответа — не больше ~10 с плюс время расчёта, без бесконечного ожидания. - **Параллельные чтения.** Два одновременных GET на чистой БД дают одну серию запросов к ISS и одинаковые ответы. - **Частота догрузки.** Повторное чтение в течение часа после сбоя не обращается к ISS (проверяется по логам). - **Порядок данных.** Пополнение, исполненное после `fetchedAt` Portfolio, в потоки не входит. ## Risk-based Priorities 1. **Критично — деньги.** Значения прибыли, XIRR и опережения совпадают с эталоном. Ни один недоступный случай не показывает число. 2. **Критично — доступ.** Нет утечки чужих счетов в `all` и `meta.unavailable`. 3. **Высокий риск — переводы в `all`** (эвристика парности) и счета с реальной историей пользователя. 4. **Высокий риск — меньше года.** Ни одного «годовых». 5. **Средний риск — устойчивость к MOEX**, график и a11y. ## Test Data and Environment Needs - Пользователь с подключением T-Bank и наборами счетов: - A — пополнения, вывод, дивиденды; - B — USD; - C — перевод с B и C; - D — 730 дн.; - E — 200 дн.; - F — нет решения; - G — `historyBoundary = unknown`; - H — перевод бумаг без пары; - I — позиция без цены. Если sandbox T-Bank такие данные не даёт, их записывают напрямую в одноразовую SQLite-копию. - Возможность блокировать `iss.moex.com` (сеть или прокси) и задерживать его ответы. - Чистая БД и БД с предзагруженной историей индексов. - Реальные данные Change Owner для финальной приёмки. ## Manual / Exploratory Scope - Реальные счета пользователя: - правдоподобие результата; - парность переводов между своими счетами; - какие счета исключаются и почему. - Вёрстка 390px и desktop; клавиатурная навигация (вкладка, hint, «Как считали», график); экранный диктор читает итоги графика. - Переключение счёт ↔ `all` и обратно: нет устаревших данных другого счёта. ## Automation Candidates - Black-box API-проверки контракта и сценариев A–I: deferred until QA repo exists. - E2E вкладки «Результат» (состояния ready, unavailable, incomplete, error): deferred until QA repo exists. ## Observability Verification - `integration.moex.index_history.succeeded` / `.failed` появляются при догрузке и сбое; в них есть secid, диапазон, pages, durationMs и нет данных пользователя. - В логах расчёта нет сумм и идентификаторов операций. ## Regression Impact - Вкладки счёта: позиции, концентрация, деньги, операции, события, доход — переключение и загрузка не меняются. - Запись истории оценки Portfolio после выноса признака полной оценки (`portfolio-value-history`): полные оценки по-прежнему создают точки, неполные — нет. - Существующие запросы к MOEX по облигациям. - `openapi:check` обоих репозиториев. ## Gaps / Blockers - **Подтверждение переводов** отсутствует в текущем контракте T-Bank. Routing: System Analysis при появлении нового признака связи; до этого кандидаты исключаются с явной причиной, не выдавая сомнительный процент. - **Sandbox T-Bank** может не воспроизвести все наборы. Routing: Test Data — наполнение одноразовой SQLite-копии; не блокирует.