Files
specs/openspec/changes/2026-11-07-investment-performance-vs-index/qa-analysis.md
T
codex-bot 892800301b
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
spec: plan investment performance vs Moscow Exchange index (WealthLedger/main#54) (#85)
2026-11-08 16:30:32 +03:00

12 KiB
Raw Blame History

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 в all 3 счёта, один с USD: completeness = incomplete, excludedAccounts содержит счёт и NON_RUB_ACTIVITY; в UI предупреждение
Недоступно: даты оценок в 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-копии; не блокирует.