Files
codex-bot b6a9ff2d10
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
spec: require RUB-only behavior (WealthLedger/main#55) (#86)
2026-11-11 09:20:25 +03:00

140 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-копии; не блокирует.